1
0

Add birthday support for mobile calendar

This commit is contained in:
2025-07-18 14:12:57 +02:00
parent ddb3c682f5
commit b7e8d1c921
5 changed files with 575 additions and 65 deletions

View File

@@ -9,7 +9,9 @@ import {
FaChevronLeft,
FaChevronRight,
FaSave,
FaTimes
FaTimes,
FaBirthdayCake,
FaGift
} from 'react-icons/fa';
import calendarService from './services/CalendarService';
import './App.sass';
@@ -99,18 +101,44 @@ const App = () => {
));
setEditingEvent(null);
} catch (err) {
setError('Fehler beim Aktualisieren des Termins');
console.error('Error updating event:', err);
// Show more specific error messages based on the error
if (err.message && err.message.includes('belong to you')) {
setError('Dieser Termin gehört einem anderen Benutzer und kann nicht bearbeitet werden');
} else if (err.message && err.message.includes('not found')) {
setError('Termin nicht gefunden');
} else if (err.message && err.message.includes('uid already exists')) {
setError('Fehler beim Aktualisieren: Termin-ID bereits vorhanden');
} else {
setError('Fehler beim Aktualisieren des Termins');
}
}
};
const handleDeleteEvent = async (eventId, user) => {
try {
// Additional protection: don't allow deleting birthday events
if (eventId && eventId.includes('birthday-')) {
setError('Geburtstage können nicht über die Kalender-App gelöscht werden');
return;
}
await calendarService.deleteEvent(eventId, user);
setEvents(events.filter(event => event.id !== eventId));
} catch (err) {
setError('Fehler beim Löschen des Termins');
console.error('Error deleting event:', err);
// Show more specific error messages based on the error
if (err.message && err.message.includes('belong to you')) {
setError('Dieser Termin gehört einem anderen Benutzer und kann nicht gelöscht werden');
} else if (err.message && err.message.includes('not found')) {
setError('Termin nicht gefunden oder bereits gelöscht');
} else if (err.message && err.message.includes('Birthday events')) {
setError('Geburtstage können nicht gelöscht werden');
} else {
setError('Fehler beim Löschen des Termins');
}
}
};
@@ -127,11 +155,14 @@ const App = () => {
const today = new Date();
today.setHours(0, 0, 0, 0); // Reset time for comparison
// Create a date object for the event date for comparison
const eventDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
return {
dayNumber: date.getDate(),
dayName: date.toLocaleDateString('de-DE', { weekday: 'long' }),
monthYear: date.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }),
isToday: date.getTime() === today.getTime(),
isToday: eventDate.getTime() === today.getTime(),
isThisMonth: date.getMonth() === currentDate.getMonth() && date.getFullYear() === currentDate.getFullYear()
};
};
@@ -144,8 +175,23 @@ const App = () => {
};
const groupEventsByDate = (events) => {
// First, ensure events are properly sorted by date
const sortedEvents = [...events].sort((a, b) => {
// Compare dates as strings to avoid timezone issues
if (a.date !== b.date) {
return a.date.localeCompare(b.date);
}
// If dates are the same, sort by type (birthdays first)
if (a.type === 'birthday' && b.type !== 'birthday') return -1;
if (a.type !== 'birthday' && b.type === 'birthday') return 1;
// Then sort by event text for consistent ordering
return (a.text || '').localeCompare(b.text || '');
});
const grouped = {};
events.forEach(event => {
sortedEvents.forEach(event => {
const date = event.date;
if (!grouped[date]) {
grouped[date] = [];
@@ -153,10 +199,12 @@ const App = () => {
grouped[date].push(event);
});
// Sort dates
const sortedDates = Object.keys(grouped).sort();
// Sort dates chronologically using string comparison for YYYY-MM-DD format
const sortedDates = Object.keys(grouped).sort((a, b) => a.localeCompare(b));
const result = {};
sortedDates.forEach(date => {
// Events within each date are already sorted from the initial sort
result[date] = grouped[date];
});
@@ -165,7 +213,12 @@ const App = () => {
const groupedEvents = groupEventsByDate(events);
const getUserColor = (userName) => {
const getUserColor = (userName, eventType) => {
// Different color scheme for birthdays
if (eventType === 'birthday') {
return '#ff6b9d'; // Pink for birthdays
}
const colors = [
'#3b82f6', // blue
'#10b981', // green
@@ -355,9 +408,11 @@ const App = () => {
onDelete={handleDeleteEvent}
editingEvent={editingEvent}
setEditingEvent={setEditingEvent}
userColor={getUserColor(event.user)}
userColor={getUserColor(event.user, event.type)}
showUser={viewType === 'family'}
users={users}
viewType={viewType}
selectedUser={selectedUser}
/>
))}
</div>
@@ -390,7 +445,9 @@ const CalendarEvent = ({
setEditingEvent,
userColor,
showUser,
users
users,
viewType,
selectedUser
}) => {
const [editData, setEditData] = useState({
user: event.user,
@@ -399,6 +456,16 @@ const CalendarEvent = ({
});
const handleEdit = () => {
// Don't allow editing of birthday events (they come from external calendar)
if (event.type === 'birthday') {
return;
}
// In individual view, only allow editing events that belong to the selected user
if (viewType === 'individual' && event.user !== selectedUser) {
return;
}
setEditingEvent(event.id);
setEditData({
user: event.user,
@@ -420,10 +487,30 @@ const CalendarEvent = ({
});
};
const handleDelete = () => {
// Don't allow deleting of birthday events (they come from external calendar)
if (event.type === 'birthday') {
return;
}
// In individual view, only allow deleting events that belong to the selected user
if (viewType === 'individual' && event.user !== selectedUser) {
return;
}
// Always pass the event's original user for deletion (important for family view)
onDelete(event.id, event.user);
};
const isEditing = editingEvent === event.id;
const isBirthday = event.type === 'birthday';
// Determine if this event can be edited/deleted
const canEdit = !isBirthday && (viewType === 'family' || event.user === selectedUser);
const canDelete = !isBirthday && (viewType === 'family' || event.user === selectedUser);
return (
<div className="calendar-event" style={{ borderLeftColor: userColor }}>
<div className={`calendar-event ${isBirthday ? 'birthday-event' : ''}`} style={{ borderLeftColor: userColor }}>
{isEditing ? (
<div className="edit-form">
<div className="form-group">
@@ -469,24 +556,54 @@ const CalendarEvent = ({
) : (
<>
<div className="event-content">
<div className="event-text">{event.text}</div>
<div className="event-text">
{isBirthday && (
<span className="birthday-icon">
<FaBirthdayCake />
</span>
)}
{isBirthday && event.contactName ? event.contactName : event.text}
{isBirthday && (
<span className="birthday-label">Geburtstag</span>
)}
</div>
{showUser && (
<div className="event-user" style={{ color: userColor }}>
{event.user}
{isBirthday && <span className="event-source"> (Kontakte)</span>}
{!canEdit && !isBirthday && <span className="read-only-indicator"> (nur lesen)</span>}
</div>
)}
</div>
<div className="event-actions">
<button className="btn btn-secondary btn-small" onClick={handleEdit}>
<FaEdit />
</button>
<button
className="btn btn-danger btn-small"
onClick={() => onDelete(event.id, event.user)}
>
<FaTrash />
</button>
</div>
{canEdit && (
<div className="event-actions">
<button className="btn btn-secondary btn-small" onClick={handleEdit}>
<FaEdit />
</button>
{canDelete && (
<button
className="btn btn-danger btn-small"
onClick={handleDelete}
>
<FaTrash />
</button>
)}
</div>
)}
{isBirthday && (
<div className="event-actions">
<div className="birthday-indicator">
<FaGift className="gift-icon" />
</div>
</div>
)}
{!canEdit && !isBirthday && (
<div className="event-actions">
<div className="read-only-indicator">
<span className="read-only-text">👁</span>
</div>
</div>
)}
</>
)}
</div>

View File

@@ -477,6 +477,17 @@ body
&::before
width: 8px
&.birthday-event
background: linear-gradient(135deg, rgba(255, 107, 157, 0.1) 0%, rgba(255, 182, 193, 0.15) 100%)
border-left: 4px solid #ff6b9d
&::before
background: linear-gradient(to bottom, #ff6b9d, rgba(255, 107, 157, 0.3))
&:hover
background: linear-gradient(135deg, rgba(255, 107, 157, 0.15) 0%, rgba(255, 182, 193, 0.2) 100%)
box-shadow: 0 4px 20px rgba(255, 107, 157, 0.3)
.event-content
flex: 1
min-width: 0
@@ -490,6 +501,28 @@ body
margin-bottom: var(--spacing-xs)
word-break: break-word
line-height: 1.4
display: flex
align-items: center
gap: var(--spacing-xs)
flex-wrap: wrap
.birthday-icon
color: #ff6b9d
font-size: 1.1rem
animation: birthday-bounce 2s infinite
filter: drop-shadow(0 2px 4px rgba(255, 107, 157, 0.3))
.birthday-label
background: linear-gradient(135deg, #ff6b9d 0%, #e91e63 100%)
color: white
font-size: 0.625rem
font-weight: 600
padding: 2px 6px
border-radius: 8px
text-transform: uppercase
letter-spacing: 0.5px
margin-left: auto
box-shadow: 0 2px 6px rgba(255, 107, 157, 0.3)
.event-user
font-size: 0.875rem
@@ -507,6 +540,17 @@ body
background: currentColor
opacity: 0.6
.event-source
font-size: 0.75rem
opacity: 0.7
font-style: italic
.read-only-indicator
font-size: 0.75rem
opacity: 0.7
font-style: italic
color: var(--text-muted)
.event-actions
display: flex
gap: var(--spacing-xs)
@@ -514,6 +558,36 @@ body
position: relative
z-index: 1
.birthday-indicator
display: flex
align-items: center
justify-content: center
width: 36px
height: 36px
border-radius: 50%
background: linear-gradient(135deg, #ff6b9d 0%, #e91e63 100%)
color: white
box-shadow: 0 3px 10px rgba(255, 107, 157, 0.4)
animation: birthday-glow 3s infinite
.gift-icon
font-size: 0.875rem
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2))
.read-only-indicator
display: flex
align-items: center
justify-content: center
width: 36px
height: 36px
border-radius: 50%
background: linear-gradient(135deg, var(--text-muted) 0%, var(--text-secondary) 100%)
color: white
opacity: 0.7
.read-only-text
font-size: 0.875rem
.edit-form
width: 100%
display: flex
@@ -552,6 +626,18 @@ body
justify-content: flex-end
margin-top: var(--spacing-sm)
@keyframes birthday-bounce
0%, 100%
transform: translateY(0) scale(1)
50%
transform: translateY(-2px) scale(1.1)
@keyframes birthday-glow
0%, 100%
box-shadow: 0 3px 10px rgba(255, 107, 157, 0.4)
50%
box-shadow: 0 6px 20px rgba(255, 107, 157, 0.6)
// Buttons
.btn
border: none
@@ -851,13 +937,30 @@ body
.event-text
font-size: 0.9rem
.birthday-icon
font-size: 1rem
.birthday-label
font-size: 0.5rem
padding: 1px 4px
.event-user
font-size: 0.8rem
.event-source
font-size: 0.7rem
.event-actions
justify-content: flex-end
margin-top: var(--spacing-xs)
.birthday-indicator
width: 32px
height: 32px
.gift-icon
font-size: 0.75rem
.btn-small
width: 32px
height: 32px

View File

@@ -34,6 +34,15 @@ class CalendarService {
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');