1
0

Create calendar

This commit is contained in:
2025-07-18 21:02:21 +02:00
parent a15956a960
commit 8bb0c56d01
6 changed files with 1079 additions and 15 deletions

View File

@@ -7,7 +7,7 @@ import Search from './pages/Search'
import InputOverlay from './components/InputOverlay' import InputOverlay from './components/InputOverlay'
// Configuration // 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 = { const APP_BACKGROUND_STYLE = {
backgroundImage: `url(${BACKGROUND_IMAGE_URL})`, backgroundImage: `url(${BACKGROUND_IMAGE_URL})`,

View File

@@ -23,10 +23,6 @@ html, body
body body
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif 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 color: #1e293b
font-weight: 400 font-weight: 400
letter-spacing: -0.01em letter-spacing: -0.01em

View File

@@ -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 = () => { const Calendar = () => {
return ( const [appointments, setAppointments] = useState({});
<div> const [users, setUsers] = useState([]);
<InputDemo /> const [loading, setLoading] = useState(true);
</div> 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 (
<div className="calendar-container loading">
<div className="loading-spinner">
<FaCalendarAlt />
<p>Loading calendar...</p>
</div>
</div>
);
}
return (
<div className="calendar-container">
{/* Header */}
<header className="calendar-header">
<div className="header-content">
<div className="month-navigation">
<button
className="nav-btn prev"
onClick={() => navigateMonth(-1)}
>
</button>
<h1 className="month-title">{getCurrentMonthYear()}</h1>
<button
className="nav-btn next"
onClick={() => navigateMonth(1)}
>
</button>
</div>
<button className="today-btn" onClick={goToToday}>
Today
</button>
</div>
</header>
{/* Error Message */}
{error && (
<div className="error-message">
{error}
<button onClick={() => setError(null)}>×</button>
</div>
)}
{/* Calendar Grid */}
<main className="calendar-grid">
{Array.from({ length: getDaysInMonth() }, (_, i) => {
const day = i + 1;
const dayAppointments = appointments[day] || [];
return (
<div
key={day}
className={`day-row ${isWeekend(day) ? 'weekend' : ''} ${isToday(day) ? 'today' : ''}`}
>
{/* Day Info Box */}
<div className="day-info">
<span className="day-number">{day}</span>
<span className="day-name">{getDayName(day)}</span>
</div>
{/* Add Button */}
<button
className="add-button"
onClick={() => openAddModal(day)}
title={`Add appointment for ${day}`}
>
<FaPlus />
</button>
{/* Appointments Container */}
<div className="appointments-container">
{dayAppointments.map((appointment, index) => (
<div
key={appointment.id}
className={`appointment-card ${appointment.type === 'birthday' ? 'birthday' : ''}`}
style={{ borderLeftColor: getUserColor(appointment.user) }}
>
<div className="appointment-content">
<div className="appointment-title">{appointment.title}</div>
<div className="appointment-user" style={{ color: getUserColor(appointment.user) }}>
{appointment.user}
</div>
</div>
{appointment.type !== 'birthday' && (
<button
className="delete-btn"
onClick={() => handleDeleteAppointment(day, appointment.id, appointment.user)}
title="Delete appointment"
>
<FaTrash />
</button>
)}
</div>
))}
</div>
</div>
);
})}
</main>
{/* Add Appointment Modal */}
{showModal && (
<div className="modal-overlay" onClick={closeModal}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h3>Add Appointment - Day {selectedDay}</h3>
<button className="close-btn" onClick={closeModal}>
<FaTimes />
</button>
</div>
<form onSubmit={handleAddAppointment} className="appointment-form">
<div className="form-group">
<label htmlFor="user">User</label>
<select
id="user"
value={newAppointment.user}
onChange={(e) => setNewAppointment({ ...newAppointment, user: e.target.value })}
required
>
{users.map(user => (
<option key={user.id} value={user.name}>
{user.name}
</option>
))}
</select>
</div>
<div className="form-group">
<label htmlFor="title">Title</label>
<input
type="text"
id="title"
placeholder="Enter appointment title..."
value={newAppointment.title}
onChange={(e) => setNewAppointment({ ...newAppointment, title: e.target.value })}
autoFocus
required
/>
</div>
<div className="form-actions">
<button type="button" className="btn btn-secondary" onClick={closeModal}>
<FaTimes />
Cancel
</button>
<button type="submit" className="btn btn-primary">
<FaSave />
Save
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
export default Calendar;

View File

@@ -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

View File

@@ -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;

View File

@@ -73,9 +73,10 @@ class RequestUtil {
} }
// DELETE request // DELETE request
async delete(endpoint) { async delete(endpoint, data = {}) {
return this.request(endpoint, { return this.request(endpoint, {
method: 'DELETE' method: 'DELETE',
body: JSON.stringify(data)
}); });
} }
} }