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 */}
+
+
+ {/* 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}
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+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)
});
}
}