From 8bb0c56d01b1086d6cde0a87265649d90839c22e Mon Sep 17 00:00:00 2001 From: Mathias Wagner Date: Fri, 18 Jul 2025 21:02:21 +0200 Subject: [PATCH] Create calendar --- dashboard/src/renderer/src/App.jsx | 2 +- dashboard/src/renderer/src/index.sass | 4 - dashboard/src/renderer/src/pages/Calendar.jsx | 366 +++++++++- .../src/renderer/src/pages/Calendar.sass | 663 ++++++++++++++++++ .../renderer/src/services/CalendarService.js | 54 ++ .../src/renderer/src/utils/RequestUtil.js | 5 +- 6 files changed, 1079 insertions(+), 15 deletions(-) create mode 100644 dashboard/src/renderer/src/pages/Calendar.sass create mode 100644 dashboard/src/renderer/src/services/CalendarService.js diff --git a/dashboard/src/renderer/src/App.jsx b/dashboard/src/renderer/src/App.jsx index cd00a11..52fe146 100644 --- a/dashboard/src/renderer/src/App.jsx +++ b/dashboard/src/renderer/src/App.jsx @@ -7,7 +7,7 @@ import Search from './pages/Search' import InputOverlay from './components/InputOverlay' // Configuration -const BACKGROUND_IMAGE_URL = 'https://cdn.pixabay.com/photo/2018/11/19/03/26/iceland-3824494_1280.jpg' +const BACKGROUND_IMAGE_URL = 'https://i.imgur.com/SjjtyaO.jpeg' const APP_BACKGROUND_STYLE = { backgroundImage: `url(${BACKGROUND_IMAGE_URL})`, diff --git a/dashboard/src/renderer/src/index.sass b/dashboard/src/renderer/src/index.sass index 9a93ed7..82dd5db 100644 --- a/dashboard/src/renderer/src/index.sass +++ b/dashboard/src/renderer/src/index.sass @@ -23,10 +23,6 @@ html, body body font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif - background: linear-gradient(rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.3)), url('https://cdn.pixabay.com/photo/2024/07/16/23/33/waterfall-8900207_1280.png') - background-size: cover - background-position: center - background-attachment: fixed color: #1e293b font-weight: 400 letter-spacing: -0.01em diff --git a/dashboard/src/renderer/src/pages/Calendar.jsx b/dashboard/src/renderer/src/pages/Calendar.jsx index 103adfb..fb9b2d4 100644 --- a/dashboard/src/renderer/src/pages/Calendar.jsx +++ b/dashboard/src/renderer/src/pages/Calendar.jsx @@ -1,11 +1,361 @@ -import InputDemo from "../components/InputDemo" +import React, { useState, useEffect } from 'react'; +import { FaPlus, FaTrash, FaCalendarAlt, FaTimes, FaSave } from 'react-icons/fa'; +import calendarService from '../services/CalendarService'; +import './Calendar.sass'; const Calendar = () => { - return ( -
- -
- ) -} + const [appointments, setAppointments] = useState({}); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [currentDate, setCurrentDate] = useState(new Date()); + const [showModal, setShowModal] = useState(false); + const [selectedDay, setSelectedDay] = useState(null); + const [newAppointment, setNewAppointment] = useState({ + title: '', + user: '' + }); -export default Calendar + useEffect(() => { + loadUsers(); + }, []); + + useEffect(() => { + loadAppointments(); + }, [currentDate]); + + const loadUsers = async () => { + try { + const userData = await calendarService.getUsers(); + setUsers(userData); + if (userData.length > 0 && !newAppointment.user) { + setNewAppointment(prev => ({ ...prev, user: userData[0].name })); + } + } catch (err) { + setError('Failed to load users'); + console.error('Error loading users:', err); + } + }; + + const loadAppointments = async () => { + try { + setLoading(true); + const year = currentDate.getFullYear(); + const month = String(currentDate.getMonth() + 1).padStart(2, '0'); + + const eventData = await calendarService.getEventsForMonth(year, month, null, 'family'); + + // Group events by day + const appointmentsByDay = {}; + eventData.forEach(event => { + const day = parseInt(event.date.split('-')[2]); + if (!appointmentsByDay[day]) { + appointmentsByDay[day] = []; + } + appointmentsByDay[day].push({ + id: event.id, + title: event.text, + user: event.user, + type: event.type + }); + }); + + setAppointments(appointmentsByDay); + setError(null); + } catch (err) { + setError('Failed to load appointments'); + console.error('Error loading appointments:', err); + } finally { + setLoading(false); + } + }; + + const handleAddAppointment = async (e) => { + e.preventDefault(); + if (!newAppointment.title.trim() || !newAppointment.user) return; + + try { + const year = currentDate.getFullYear(); + const month = String(currentDate.getMonth() + 1).padStart(2, '0'); + const day = String(selectedDay).padStart(2, '0'); + const date = `${year}-${month}-${day}`; + + const eventData = { + user: newAppointment.user, + date: date, + text: newAppointment.title + }; + + const createdEvent = await calendarService.createEvent(eventData); + + // Update local state + const newAppt = { + id: createdEvent.id, + title: newAppointment.title, + user: newAppointment.user + }; + + setAppointments(prev => ({ + ...prev, + [selectedDay]: [...(prev[selectedDay] || []), newAppt] + })); + + // Reset form + setNewAppointment({ title: '', user: users[0]?.name || '' }); + setShowModal(false); + setSelectedDay(null); + } catch (err) { + setError('Failed to create appointment'); + console.error('Error creating appointment:', err); + } + }; + + const handleDeleteAppointment = async (day, appointmentId, user) => { + try { + if (appointmentId.includes('birthday-')) { + setError('Birthday events cannot be deleted'); + return; + } + + await calendarService.deleteEvent(appointmentId, user); + + setAppointments(prev => ({ + ...prev, + [day]: prev[day].filter(appt => appt.id !== appointmentId) + })); + } catch (err) { + setError('Failed to delete appointment'); + console.error('Error deleting appointment:', err); + } + }; + + const openAddModal = (day) => { + setSelectedDay(day); + setShowModal(true); + }; + + const closeModal = () => { + setShowModal(false); + setSelectedDay(null); + setNewAppointment({ title: '', user: users[0]?.name || '' }); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const navigateMonth = (direction) => { + const newDate = new Date(currentDate); + newDate.setMonth(newDate.getMonth() + direction); + setCurrentDate(newDate); + }; + + const getDayName = (day) => { + const date = new Date(currentDate.getFullYear(), currentDate.getMonth(), day); + return date.toLocaleDateString('de-DE', { weekday: 'short' }).toUpperCase(); + }; + + const isWeekend = (day) => { + const date = new Date(currentDate.getFullYear(), currentDate.getMonth(), day); + const dayOfWeek = date.getDay(); + return dayOfWeek === 0 || dayOfWeek === 6; // Sunday or Saturday + }; + + const isToday = (day) => { + const today = new Date(); + return ( + day === today.getDate() && + currentDate.getMonth() === today.getMonth() && + currentDate.getFullYear() === today.getFullYear() + ); + }; + + const getCurrentMonthYear = () => { + return currentDate.toLocaleDateString('en-US', { + month: 'long', + year: 'numeric' + }); + }; + + const getDaysInMonth = () => { + return new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0).getDate(); + }; + + const getUserColor = (userName) => { + const colors = [ + '#3b82f6', // blue + '#10b981', // green + '#f59e0b', // amber + '#ef4444', // red + '#8b5cf6', // purple + '#06b6d4', // cyan + '#ec4899', // pink + '#84cc16' // lime + ]; + + const userIndex = users.findIndex(user => user.name === userName); + return colors[userIndex % colors.length]; + }; + + if (loading) { + return ( +
+
+ +

Loading calendar...

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+ +

{getCurrentMonthYear()}

+ +
+ +
+
+ + {/* Error Message */} + {error && ( +
+ {error} + +
+ )} + + {/* Calendar Grid */} +
+ {Array.from({ length: getDaysInMonth() }, (_, i) => { + const day = i + 1; + const dayAppointments = appointments[day] || []; + + return ( +
+ {/* Day Info Box */} +
+ {day} + {getDayName(day)} +
+ + {/* Add Button */} + + + {/* Appointments Container */} +
+ {dayAppointments.map((appointment, index) => ( +
+
+
{appointment.title}
+
+ {appointment.user} +
+
+ {appointment.type !== 'birthday' && ( + + )} +
+ ))} +
+
+ ); + })} +
+ + {/* Add Appointment Modal */} + {showModal && ( +
+
e.stopPropagation()}> +
+

Add Appointment - Day {selectedDay}

+ +
+ +
+
+ + +
+ +
+ + setNewAppointment({ ...newAppointment, title: e.target.value })} + autoFocus + required + /> +
+ +
+ + +
+
+
+
+ )} +
+ ); +}; + +export default Calendar; diff --git a/dashboard/src/renderer/src/pages/Calendar.sass b/dashboard/src/renderer/src/pages/Calendar.sass new file mode 100644 index 0000000..aebb537 --- /dev/null +++ b/dashboard/src/renderer/src/pages/Calendar.sass @@ -0,0 +1,663 @@ +// Calendar.sass - Wall-mounted display calendar with glassmorphism design +// Optimized for 9:16 aspect ratio, touch interaction, no scrolling + +.calendar-container + height: calc(100vh - 5rem - 60px) // Full viewport minus app header minus margins + width: calc(100vw - 40px) // Full width minus left/right margins + background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05)) + backdrop-filter: blur(20px) saturate(180%) + border: 1px solid rgba(255, 255, 255, 0.2) + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif + color: #1a1a1a + position: fixed + top: calc(5rem + 40px) // Account for the app header plus top margin + left: 20px // Left margin + right: 20px // Right margin + bottom: 20px // Bottom margin + overflow: hidden + display: flex + flex-direction: column + margin: 0 + padding: 0 + border-radius: 20px + box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37), inset 0 1px 0 rgba(255, 255, 255, 0.4) + + // Loading state + &.loading + display: flex + justify-content: center + align-items: center + + .loading-spinner + background: rgba(255, 255, 255, 0.25) + backdrop-filter: blur(40px) saturate(180%) + border: 1px solid rgba(255, 255, 255, 0.3) + border-radius: 20px + padding: 40px + text-align: center + box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37), inset 0 1px 0 rgba(255, 255, 255, 0.4) + + svg + font-size: 3rem + color: #ffffff + margin-bottom: 20px + animation: pulse 2s infinite + + p + color: #ffffff + font-size: 1.2rem + margin: 0 + +// Header with glassmorphism +.calendar-header + background: rgba(255, 255, 255, 0.15) + backdrop-filter: blur(30px) saturate(180%) + border-bottom: 1px solid rgba(255, 255, 255, 0.2) + padding: 10px 15px + position: sticky + top: 0 + z-index: 100 + flex-shrink: 0 + height: 60px + box-sizing: border-box + width: 100% + margin: 0 + border-radius: 20px 20px 0 0 + + .header-content + display: flex + justify-content: space-between + align-items: center + width: 100% + height: 100% + margin: 0 + padding: 0 + + .month-navigation + display: flex + align-items: center + gap: 20px + + .nav-btn + background: rgba(255, 255, 255, 0.2) + backdrop-filter: blur(20px) saturate(180%) + border: 1px solid rgba(255, 255, 255, 0.3) + color: #ffffff + font-size: 1.5rem + width: 45px + height: 45px + border-radius: 50% + cursor: pointer + transition: all 0.3s ease + display: flex + align-items: center + justify-content: center + box-shadow: 0 4px 16px rgba(31, 38, 135, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.4) + + &:hover + background: rgba(255, 255, 255, 0.3) + transform: scale(1.05) + box-shadow: 0 6px 20px rgba(31, 38, 135, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.5) + + &:active + transform: scale(0.95) + + .month-title + color: #ffffff + font-size: 1.8rem + font-weight: 600 + margin: 0 + text-shadow: none + min-width: 250px + text-align: center + + .today-btn + background: rgba(255, 255, 255, 0.2) + backdrop-filter: blur(20px) saturate(180%) + border: 1px solid rgba(255, 255, 255, 0.3) + color: #ffffff + padding: 10px 20px + border-radius: 25px + cursor: pointer + font-size: 0.9rem + font-weight: 500 + transition: all 0.3s ease + box-shadow: 0 4px 16px rgba(31, 38, 135, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.4) + + &:hover + background: rgba(255, 255, 255, 0.3) + transform: translateY(-2px) + box-shadow: 0 6px 20px rgba(31, 38, 135, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.5) + +// Error message +.error-message + background: rgba(220, 38, 38, 0.9) + color: white + padding: 10px 15px + margin: 0 + border-radius: 0 + display: flex + justify-content: space-between + align-items: center + backdrop-filter: blur(10px) + animation: slideDown 0.3s ease + width: 100% + box-sizing: border-box + + button + background: none + border: none + color: white + font-size: 1.2rem + cursor: pointer + padding: 0 + width: 30px + height: 30px + border-radius: 50% + display: flex + align-items: center + justify-content: center + + &:hover + background: rgba(255, 255, 255, 0.2) + +// Main calendar grid - optimized for no scrolling +.calendar-grid + flex: 1 + padding: 12px + display: grid + grid-template-columns: 1fr + gap: 3px + width: 100% + box-sizing: border-box + margin: 0 + + // Calculate exact height using the remaining space after header + height: calc(100% - 60px) // Full container height minus calendar header + grid-template-rows: repeat(31, 1fr) + overflow: hidden + +// Individual day row with three distinct sections +.day-row + display: grid + grid-template-columns: 120px 50px 1fr + gap: 8px + align-items: stretch + height: 100% + padding: 0 + margin: 0 + transition: all 0.3s ease + width: 100% + box-sizing: border-box + + // Weekend styling + &.weekend + .day-info + background: rgba(200, 200, 200, 0.2) + backdrop-filter: blur(20px) saturate(180%) + border: 1px solid rgba(200, 200, 200, 0.3) + + .day-number + color: #ffffff + + .day-name + color: #e5e7eb + + .appointments-container + background: rgba(200, 200, 200, 0.15) + backdrop-filter: blur(20px) saturate(180%) + border: 1px solid rgba(200, 200, 200, 0.25) + + // Today highlighting + &.today + .day-info + background: rgba(59, 130, 246, 0.25) + backdrop-filter: blur(20px) saturate(180%) + border: 2px solid rgba(59, 130, 246, 0.4) + box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.5) + + .day-number + color: #1e40af + font-weight: 800 + + .day-name + color: #3b82f6 + font-weight: 700 + + .appointments-container + background: rgba(59, 130, 246, 0.15) + border: 1px solid rgba(59, 130, 246, 0.3) + box-shadow: 0 4px 16px rgba(59, 130, 246, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.4) + +// Day info box (left section) +.day-info + background: rgba(255, 255, 255, 0.2) + backdrop-filter: blur(20px) saturate(180%) + border: 1px solid rgba(255, 255, 255, 0.3) + border-radius: 12px + padding: 8px + display: flex + flex-direction: row + align-items: center + justify-content: center + gap: 6px + box-shadow: 0 4px 16px rgba(31, 38, 135, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.4) + transition: all 0.3s ease + height: 100% + width: 100% + box-sizing: border-box + + .day-number + font-size: 1.3rem + font-weight: 700 + color: #ffffff + line-height: 1 + text-shadow: none + + .day-name + font-size: 0.7rem + color: #e5e7eb + font-weight: 600 + text-transform: uppercase + letter-spacing: 0.3px + text-shadow: none + +// Add button (center section) +.add-button + background: linear-gradient(135deg, rgba(34, 197, 94, 0.8), rgba(16, 185, 129, 0.8)) + backdrop-filter: blur(20px) saturate(180%) + border: 1px solid rgba(255, 255, 255, 0.3) + color: white + width: 45px + height: 45px + border-radius: 50% + cursor: pointer + display: flex + align-items: center + justify-content: center + font-size: 1.1rem + transition: all 0.3s ease + box-shadow: 0 4px 16px rgba(34, 197, 94, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.4) + align-self: center + + &:hover + background: linear-gradient(135deg, rgba(34, 197, 94, 1), rgba(16, 185, 129, 1)) + transform: scale(1.1) + box-shadow: 0 6px 20px rgba(34, 197, 94, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.5) + + &:active + transform: scale(0.95) + + svg + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2)) + +// Appointments container (right section) +.appointments-container + background: rgba(255, 255, 255, 0.2) + backdrop-filter: blur(20px) saturate(180%) + border: 1px solid rgba(255, 255, 255, 0.3) + border-radius: 12px + padding: 6px + display: flex + flex-wrap: wrap + gap: 4px + height: 100% + width: 100% + align-items: flex-start + align-content: flex-start + box-shadow: 0 4px 16px rgba(31, 38, 135, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.4) + transition: all 0.3s ease + box-sizing: border-box + + // When empty, show subtle hint + &:empty::after + content: "" + width: 100% + height: 100% + display: block + +// Individual appointment cards +.appointment-card + background: rgba(255, 255, 255, 0.25) + backdrop-filter: blur(15px) saturate(180%) + border: 1px solid rgba(255, 255, 255, 0.3) + border-radius: 8px + padding: 6px 8px + border-left: 3px solid #3b82f6 + box-shadow: 0 2px 8px rgba(31, 38, 135, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.4) + display: flex + align-items: center + gap: 6px + transition: all 0.3s ease + min-width: 80px + max-width: 150px + font-size: 0.7rem + + &:hover + transform: translateY(-2px) + background: rgba(255, 255, 255, 0.3) + box-shadow: 0 4px 16px rgba(31, 38, 135, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.5) + + &.birthday + border-left-color: #f59e0b + background: rgba(251, 191, 36, 0.2) + + .appointment-content + flex: 1 + min-width: 0 + + .appointment-title + font-size: 0.65rem + font-weight: 600 + color: #ffffff + line-height: 1.2 + overflow: hidden + text-overflow: ellipsis + white-space: nowrap + text-shadow: none + + .appointment-user + font-size: 0.55rem + font-weight: 600 + margin-top: 2px + text-transform: uppercase + letter-spacing: 0.3px + text-shadow: none + color: #d1d5db + + .delete-btn + background: rgba(239, 68, 68, 0.2) + backdrop-filter: blur(10px) + border: 1px solid rgba(255, 255, 255, 0.3) + color: #dc2626 + width: 22px + height: 22px + border-radius: 50% + cursor: pointer + display: flex + align-items: center + justify-content: center + font-size: 0.6rem + transition: all 0.3s ease + opacity: 0.8 + flex-shrink: 0 + box-shadow: 0 2px 8px rgba(239, 68, 68, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.4) + + &:hover + background: rgba(239, 68, 68, 0.3) + opacity: 1 + transform: scale(1.1) + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.5) + + &:active + transform: scale(0.9) + +// Modal styling with glassmorphism +.modal-overlay + position: fixed + top: 0 + left: 0 + right: 0 + bottom: 0 + background: rgba(0, 0, 0, 0.5) + display: flex + align-items: center + justify-content: center + z-index: 1000 + backdrop-filter: blur(10px) + animation: fadeIn 0.3s ease + +.modal-content + background: rgba(255, 255, 255, 0.15) + backdrop-filter: blur(40px) saturate(180%) + border: 1px solid rgba(255, 255, 255, 0.3) + border-radius: 24px + padding: 0 + width: 90% + max-width: 500px + box-shadow: 0 20px 60px rgba(31, 38, 135, 0.37), inset 0 1px 0 rgba(255, 255, 255, 0.4) + animation: slideUp 0.3s ease + overflow: hidden + +.modal-header + background: rgba(59, 130, 246, 0.15) + backdrop-filter: blur(20px) saturate(180%) + border-bottom: 1px solid rgba(255, 255, 255, 0.2) + padding: 20px 25px + display: flex + justify-content: space-between + align-items: center + + h3 + margin: 0 + color: #ffffff + font-size: 1.3rem + font-weight: 600 + text-shadow: none + + .close-btn + background: rgba(239, 68, 68, 0.15) + backdrop-filter: blur(15px) + border: 1px solid rgba(255, 255, 255, 0.3) + color: #dc2626 + font-size: 1.2rem + cursor: pointer + width: 35px + height: 35px + border-radius: 50% + display: flex + align-items: center + justify-content: center + transition: all 0.3s ease + box-shadow: 0 2px 8px rgba(239, 68, 68, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.4) + + &:hover + background: rgba(239, 68, 68, 0.25) + transform: scale(1.1) + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.5) + +// Form styling +.appointment-form + padding: 25px + + .form-group + margin-bottom: 20px + + label + display: block + margin-bottom: 8px + color: #ffffff + font-weight: 500 + font-size: 0.9rem + + input, select + width: 100% + padding: 12px 15px + border: 1px solid rgba(255, 255, 255, 0.3) + border-radius: 12px + font-size: 1rem + background: rgba(255, 255, 255, 0.2) + backdrop-filter: blur(20px) saturate(180%) + box-shadow: 0 2px 8px rgba(31, 38, 135, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.4) + transition: all 0.3s ease + box-sizing: border-box + color: #000000 + + &:focus + outline: none + border-color: rgba(59, 130, 246, 0.5) + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1), 0 2px 8px rgba(31, 38, 135, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.5) + background: rgba(255, 255, 255, 0.3) + + &::placeholder + color: #9ca3af + + .form-actions + display: flex + gap: 15px + justify-content: flex-end + margin-top: 30px + + .btn + padding: 12px 20px + border: 1px solid rgba(255, 255, 255, 0.3) + border-radius: 12px + font-size: 1rem + font-weight: 500 + cursor: pointer + display: flex + align-items: center + gap: 8px + transition: all 0.3s ease + backdrop-filter: blur(20px) saturate(180%) + box-shadow: 0 2px 8px rgba(31, 38, 135, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.4) + + &.btn-secondary + background: rgba(107, 114, 128, 0.15) + color: #ffffff + + &:hover + background: rgba(107, 114, 128, 0.25) + transform: translateY(-2px) + box-shadow: 0 4px 12px rgba(107, 114, 128, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.5) + + &.btn-primary + background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(29, 78, 216, 0.8)) + color: white + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) + + &:hover + background: linear-gradient(135deg, rgba(59, 130, 246, 1), rgba(29, 78, 216, 1)) + transform: translateY(-2px) + box-shadow: 0 6px 16px rgba(59, 130, 246, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.5) + + &:active + transform: translateY(0) + +// Animations +@keyframes fadeIn + from + opacity: 0 + to + opacity: 1 + +@keyframes slideUp + from + opacity: 0 + transform: translateY(50px) + to + opacity: 1 + transform: translateY(0) + +@keyframes slideDown + from + opacity: 0 + transform: translateY(-20px) + to + opacity: 1 + transform: translateY(0) + +@keyframes pulse + 0%, 100% + opacity: 1 + 50% + opacity: 0.5 + +// Responsive adjustments for different screen sizes +@media (max-height: 900px) + .calendar-container + top: calc(5rem + 15px) + left: 15px + right: 15px + bottom: 15px + width: calc(100vw - 30px) + height: calc(100vh - 5rem - 30px) + + .calendar-grid + gap: 2px + padding: 8px + + .day-row + gap: 6px + grid-template-columns: 110px 45px 1fr + + .day-info + padding: 6px + + .day-number + font-size: 1rem + + .day-name + font-size: 0.6rem + + .appointments-container + padding: 4px + +@media (max-height: 800px) + .calendar-container + top: calc(5rem + 10px) + left: 10px + right: 10px + bottom: 10px + width: calc(100vw - 20px) + height: calc(100vh - 5rem - 20px) + + .calendar-grid + gap: 1px + padding: 6px + + .day-row + gap: 4px + grid-template-columns: 100px 40px 1fr + + .day-info + padding: 4px + + .day-number + font-size: 0.9rem + + .day-name + font-size: 0.55rem + + .appointments-container + padding: 3px + + .add-button + width: 35px + height: 35px + font-size: 0.9rem + +@media (min-width: 1400px) + .calendar-container + top: calc(5rem + 30px) + left: 30px + right: 30px + bottom: 30px + width: calc(100vw - 60px) + height: calc(100vh - 5rem - 60px) + + .calendar-grid + gap: 4px + padding: 16px + + .day-row + gap: 10px + grid-template-columns: 140px 55px 1fr + +// Touch optimization for wall displays +@media (pointer: coarse) + .add-button + width: 50px + height: 50px + + .delete-btn + width: 28px + height: 28px + + .nav-btn + width: 50px + height: 50px + + .today-btn + padding: 15px 30px + font-size: 1.1rem diff --git a/dashboard/src/renderer/src/services/CalendarService.js b/dashboard/src/renderer/src/services/CalendarService.js new file mode 100644 index 0000000..e47cfb2 --- /dev/null +++ b/dashboard/src/renderer/src/services/CalendarService.js @@ -0,0 +1,54 @@ +import requestUtil from '../utils/RequestUtil'; + +class CalendarService { + constructor() { + this.endpoint = '/api/calendar'; + } + + // Get all calendar users + async getUsers() { + return requestUtil.get(`${this.endpoint}/users`); + } + + // Get calendar events for a specific month + async getEventsForMonth(year, month, user = null, type = 'family') { + let url = `${this.endpoint}/events/${year}/${month}?type=${type}`; + if (user && type === 'individual') { + url += `&user=${encodeURIComponent(user)}`; + } + return requestUtil.get(url); + } + + // Create a new calendar event + async createEvent(event) { + return requestUtil.post(`${this.endpoint}/events`, event); + } + + // Update a calendar event + async updateEvent(eventId, event) { + return requestUtil.put(`${this.endpoint}/events/${eventId}`, event); + } + + // Delete a calendar event + async deleteEvent(eventId, user) { + return requestUtil.delete(`${this.endpoint}/events/${eventId}`, { user }); + } + + // Get contact birthdays for a specific month + async getContactBirthdaysForMonth(year, month, user) { + let url = `${this.endpoint}/birthdays/${year}/${month}`; + if (user) { + url += `?user=${encodeURIComponent(user)}`; + } + return requestUtil.get(url); + } + + // Health check + async healthCheck() { + return requestUtil.get('/api/health'); + } +} + +// Create and export a singleton instance +const calendarService = new CalendarService(); +export default calendarService; diff --git a/dashboard/src/renderer/src/utils/RequestUtil.js b/dashboard/src/renderer/src/utils/RequestUtil.js index aff454a..7cbb58b 100644 --- a/dashboard/src/renderer/src/utils/RequestUtil.js +++ b/dashboard/src/renderer/src/utils/RequestUtil.js @@ -73,9 +73,10 @@ class RequestUtil { } // DELETE request - async delete(endpoint) { + async delete(endpoint, data = {}) { return this.request(endpoint, { - method: 'DELETE' + method: 'DELETE', + body: JSON.stringify(data) }); } }