Create calendar
This commit is contained in:
@@ -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})`,
|
||||||
|
@@ -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
|
||||||
|
@@ -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;
|
||||||
|
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
|
// 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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user