feat(calendar): add reminder functionality for events with notifications
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled
This commit is contained in:
@@ -77,6 +77,15 @@ export function NotificationProvider({ children }) {
|
||||
seenIds.current.add(n.id);
|
||||
const icon = notificationIcon(n.type);
|
||||
toast(`${icon} ${n.title}`, { duration: 5000 });
|
||||
// Browser notification for calendar reminders
|
||||
if (n.type === 'calendar_reminder' && 'Notification' in window) {
|
||||
const fire = () => new Notification(n.title, { body: n.body || '', icon: '/favicon.ico' });
|
||||
if (Notification.permission === 'granted') {
|
||||
fire();
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
Notification.requestPermission().then(p => { if (p === 'granted') fire(); });
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
/* silent – server may not be reachable */
|
||||
@@ -166,6 +175,7 @@ function notificationIcon(type) {
|
||||
case 'room_share_added': return '🔗';
|
||||
case 'room_share_removed': return '🚫';
|
||||
case 'federation_invite_received': return '📩';
|
||||
case 'calendar_reminder': return '🔔';
|
||||
default: return '🔔';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,6 +510,14 @@
|
||||
"linkedRoom": "Verknüpfter Raum",
|
||||
"noRoom": "Kein Raum (kein Videomeeting)",
|
||||
"linkedRoomHint": "Verknüpfe einen Raum, um die Beitritts-URL automatisch ins Event einzufügen.",
|
||||
"reminderLabel": "Erinnerung",
|
||||
"reminderNone": "Keine Erinnerung",
|
||||
"reminder5": "5 Minuten vorher",
|
||||
"reminder15": "15 Minuten vorher",
|
||||
"reminder30": "30 Minuten vorher",
|
||||
"reminder60": "1 Stunde vorher",
|
||||
"reminder120": "2 Stunden vorher",
|
||||
"reminder1440": "1 Tag vorher",
|
||||
"timezone": "Zeitzone",
|
||||
"color": "Farbe",
|
||||
"eventCreated": "Event erstellt!",
|
||||
|
||||
@@ -510,6 +510,14 @@
|
||||
"linkedRoom": "Linked Room",
|
||||
"noRoom": "No room (no video meeting)",
|
||||
"linkedRoomHint": "Link a room to automatically include the join-URL in the event.",
|
||||
"reminderLabel": "Reminder",
|
||||
"reminderNone": "No reminder",
|
||||
"reminder5": "5 minutes before",
|
||||
"reminder15": "15 minutes before",
|
||||
"reminder30": "30 minutes before",
|
||||
"reminder60": "1 hour before",
|
||||
"reminder120": "2 hours before",
|
||||
"reminder1440": "1 day before",
|
||||
"timezone": "Timezone",
|
||||
"color": "Color",
|
||||
"eventCreated": "Event created!",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
ChevronLeft, ChevronRight, Plus, Clock, Video,
|
||||
ChevronLeft, ChevronRight, Plus, Clock, Video, Bell,
|
||||
Loader2, Download, Share2, Globe, Trash2, Edit, X, UserPlus, Send, ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import api from '../services/api';
|
||||
@@ -32,7 +32,7 @@ export default function Calendar() {
|
||||
// Create/Edit form
|
||||
const [form, setForm] = useState({
|
||||
title: '', description: '', start_time: '', end_time: '',
|
||||
room_uid: '', color: '#6366f1',
|
||||
room_uid: '', color: '#6366f1', reminder_minutes: null,
|
||||
});
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
@@ -157,7 +157,7 @@ export default function Calendar() {
|
||||
title: '', description: '',
|
||||
start_time: toLocalDateTimeStr(start),
|
||||
end_time: toLocalDateTimeStr(end),
|
||||
room_uid: '', color: '#6366f1',
|
||||
room_uid: '', color: '#6366f1', reminder_minutes: null,
|
||||
});
|
||||
setEditingEvent(null);
|
||||
setShowCreate(true);
|
||||
@@ -171,6 +171,7 @@ export default function Calendar() {
|
||||
end_time: toLocalDateTimeStr(new Date(ev.end_time)),
|
||||
room_uid: ev.room_uid || '',
|
||||
color: ev.color || '#6366f1',
|
||||
reminder_minutes: ev.reminder_minutes ?? null,
|
||||
});
|
||||
setEditingEvent(ev);
|
||||
setShowDetail(null);
|
||||
@@ -494,7 +495,10 @@ export default function Calendar() {
|
||||
className="text-xs px-2 py-1.5 rounded text-white font-medium cursor-pointer hover:opacity-80 transition-opacity"
|
||||
style={{ backgroundColor: ev.color || '#6366f1' }}
|
||||
>
|
||||
<div className="truncate">{ev.title}</div>
|
||||
<div className="flex items-center gap-1 truncate">
|
||||
{ev.reminder_minutes && <Bell size={9} className="flex-shrink-0 opacity-70" />}
|
||||
<span className="truncate">{ev.title}</span>
|
||||
</div>
|
||||
<div className="opacity-80 text-[10px]">{formatTime(ev.start_time)} - {formatTime(ev.end_time)}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -570,6 +574,26 @@ export default function Calendar() {
|
||||
<p className="text-xs text-th-text-s mt-1">{t('calendar.linkedRoomHint')}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('calendar.reminderLabel')}</label>
|
||||
<div className="relative">
|
||||
<Bell size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-th-text-s pointer-events-none" />
|
||||
<select
|
||||
value={form.reminder_minutes ?? ''}
|
||||
onChange={e => setForm({ ...form, reminder_minutes: e.target.value === '' ? null : Number(e.target.value) })}
|
||||
className="input-field pl-9"
|
||||
>
|
||||
<option value="">{t('calendar.reminderNone')}</option>
|
||||
<option value="5">{t('calendar.reminder5')}</option>
|
||||
<option value="15">{t('calendar.reminder15')}</option>
|
||||
<option value="30">{t('calendar.reminder30')}</option>
|
||||
<option value="60">{t('calendar.reminder60')}</option>
|
||||
<option value="120">{t('calendar.reminder120')}</option>
|
||||
<option value="1440">{t('calendar.reminder1440')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-th-text mb-1.5">{t('calendar.color')}</label>
|
||||
<div className="flex gap-2">
|
||||
|
||||
Reference in New Issue
Block a user