'use client'; import { useLocale, useTranslations } from 'next-intl'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { browserPermissionState } from '@/lib/reminder-notify'; import { isSameLocalDay, type SnoozePreset, snoozeTarget } from '@/lib/reminder-time'; import { deleteReminder, getReminderEmailStatus, listReminders, REMINDERS_CHANGED_EVENT, type Reminder, type ReminderEmailStatus, ReminderRequestError, snoozeReminder, } from '@/lib/reminders-api'; import type { WidgetProps } from '../widget-registry'; import { ReminderFormModal } from './reminder-form-modal'; /** Abstand der Auffrischung beim Server. */ const REFRESH_MS = 60_000; /** Abstand, in dem die Kachel neu entscheidet, welche Erinnerung faellig ist. */ const NOW_TICK_MS = 10_000; const SNOOZE_PRESETS: SnoozePreset[] = ['10m', '1h', 'tomorrow']; /** * Kachel „Erinnerungen“ (quick-260929-if2): die persoenlichen Erinnerungen des * Benutzers, die naechste Faelligkeit zuerst. Die Benachrichtigung selbst macht * der globale `ReminderNotifier` im Portal-Rahmen — die Kachel muss dafuer * nicht sichtbar sein. Nach jeder Aenderung laedt sie neu und meldet * `REMINDERS_CHANGED_EVENT`, damit der Melder eine neue Faelligkeit sofort kennt. * * Faellige Zeilen (D-03) bleiben hervorgehoben stehen und bieten „Erledigt“ * (loescht die Zeile, E-02) und „Spaeter erinnern“ (+10 Minuten, +1 Stunde, * morgen zur gleichen Uhrzeit). Noch nicht faellige lassen sich bearbeiten und * loeschen. Ob eine Zeile faellig ist, entscheidet ein 10-s-Takt lokal, ohne * Neuladen. */ export function ReminderWidget(_props: WidgetProps) { const t = useTranslations('widgets'); const locale = useLocale(); const [reminders, setReminders] = useState(null); const [loadError, setLoadError] = useState(false); const [now, setNow] = useState(() => Date.now()); const [modal, setModal] = useState<{ reminder: Reminder | null } | null>(null); const [snoozeOpenId, setSnoozeOpenId] = useState(null); const [confirmDeleteId, setConfirmDeleteId] = useState(null); const [notice, setNotice] = useState(null); // Einmal je Einhaengen der Kachel geladen; unbekannt/fehlgeschlagen = E-Mail nicht moeglich. const [emailStatus, setEmailStatus] = useState(null); const refetch = useCallback(async () => { try { setReminders(await listReminders()); setLoadError(false); } catch { setLoadError(true); } }, []); useEffect(() => { let cancelled = false; getReminderEmailStatus() .then((status) => { if (!cancelled) setEmailStatus(status); }) .catch(() => undefined); return () => { cancelled = true; }; }, []); useEffect(() => { void refetch(); const refresh = setInterval(() => void refetch(), REFRESH_MS); const tick = setInterval(() => setNow(Date.now()), NOW_TICK_MS); const onChanged = () => void refetch(); window.addEventListener(REMINDERS_CHANGED_EVENT, onChanged); return () => { clearInterval(refresh); clearInterval(tick); window.removeEventListener(REMINDERS_CHANGED_EVENT, onChanged); }; }, [refetch]); const sorted = useMemo( () => [...(reminders ?? [])].sort( (a, b) => new Date(a.dueAt).getTime() - new Date(b.dueAt).getTime(), ), [reminders], ); const dateTime = useMemo( () => new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' }), [locale], ); const timeOnly = useMemo(() => new Intl.DateTimeFormat(locale, { timeStyle: 'short' }), [locale]); /** Nach jeder erfolgreichen Aenderung: neu laden und den Melder informieren. */ const afterMutation = () => { void refetch(); window.dispatchEvent(new Event(REMINDERS_CHANGED_EVENT)); }; /** Fehler einer Zeilenaktion: 409 heisst, der Stand war veraltet — Text zeigen und neu laden. */ const handleFailure = (err: unknown, conflictKey: string) => { setNotice( err instanceof ReminderRequestError && err.status === 409 ? t(conflictKey) : t('reminder.saveError'), ); void refetch(); }; const markDone = async (id: string) => { setNotice(null); try { await deleteReminder(id); afterMutation(); } catch (err) { handleFailure(err, 'reminder.saveError'); } }; const snooze = async (r: Reminder, preset: SnoozePreset) => { setNotice(null); setSnoozeOpenId(null); try { const target = snoozeTarget(preset, new Date(r.dueAt), new Date()); await snoozeReminder(r.id, target.toISOString()); afterMutation(); } catch (err) { handleFailure(err, 'reminder.notDue'); } }; // „Heute“ oder „Morgen“ aus dem tatsaechlich berechneten Zeitpunkt: kurz nach // Mitternacht liegt das naechste Vorkommen der Uhrzeit noch heute. const tomorrowLabel = (r: Reminder) => { const target = snoozeTarget('tomorrow', new Date(r.dueAt), new Date(now)); const time = timeOnly.format(target); return isSameLocalDay(target, new Date(now)) ? t('reminder.snoozeToday', { time }) : t('reminder.snoozeTomorrow', { time }); }; const remove = async (id: string) => { setNotice(null); setConfirmDeleteId(null); try { await deleteReminder(id); afterMutation(); } catch (err) { handleFailure(err, 'reminder.saveError'); } }; const blocked = browserPermissionState() === 'denied'; return (
{loadError && (

{t('reminder.loadError')}

)} {notice && (

{notice}

)} {!loadError && reminders !== null && sorted.length === 0 && (

{t('reminder.empty')}

)} {sorted.map((r) => { const due = new Date(r.dueAt).getTime() <= now; return (

{r.title}

{r.emailEnabled && ( )} {due && ( {t('reminder.due')} )}

{dateTime.format(new Date(r.dueAt))}

{r.description !== '' && (

{r.description}

)} {due ? (
{snoozeOpenId === r.id && (
{SNOOZE_PRESETS.map((preset) => ( ))}
)}
) : (
{confirmDeleteId === r.id ? ( <> {t('reminder.deleteConfirm')} ) : ( <> )}
)}
); })}
{blocked && (

{t('reminder.permissionDenied')}

)} {modal && ( setModal(null)} onSaved={afterMutation} onStale={() => void refetch()} /> )}
); }