Create calendar
This commit is contained in:
@@ -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})`,
|
||||
|
@@ -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
|
||||
|
@@ -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 (
|
||||
<div>
|
||||
<InputDemo />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
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 (
|
||||
<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;
|
||||
|
663
dashboard/src/renderer/src/pages/Calendar.sass
Normal file
663
dashboard/src/renderer/src/pages/Calendar.sass
Normal 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
|
54
dashboard/src/renderer/src/services/CalendarService.js
Normal file
54
dashboard/src/renderer/src/services/CalendarService.js
Normal 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;
|
@@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user