hellth-hub/apps/admin/app/(authed)/erinnerungen/page.tsx

246 lines
9.2 KiB
TypeScript
Raw Normal View History

"use client";
import {
BeakerIcon,
BellAlertIcon,
CheckCircleIcon,
ClockIcon,
FireIcon,
} from "@heroicons/react/24/outline";
import { useEffect, useState } from "react";
import { useSession } from "@/lib/auth-client";
import {
ReminderKind,
ReminderRule,
ReminderSettings,
defaultReminderSettings,
loadHellthData,
saveHellthData,
} from "@/lib/hellth-data";
const reminderMeta: Record<
ReminderKind,
{ title: string; description: string; icon: typeof BeakerIcon; color: string }
> = {
water: {
title: "Wasser trinken",
description: "Regelmäßig kleine Trinkimpulse statt abends alles nachholen.",
icon: BeakerIcon,
color: "text-blue-500",
},
snack: {
title: "Snack prüfen",
description: "Hilft, geplante Snacks bewusst zu essen und ungeplantes Snacken zu vermeiden.",
icon: FireIcon,
color: "text-orange-500",
},
movement: {
title: "Kurz bewegen",
description: "Steh vom Schreibtisch auf, streck dich oder geh ein paar Schritte.",
icon: ClockIcon,
color: "text-emerald-500",
},
};
function permissionLabel(permission: NotificationPermission | "unsupported") {
if (permission === "granted") return "Benachrichtigungen sind erlaubt.";
if (permission === "denied") return "Benachrichtigungen sind im Browser blockiert.";
if (permission === "unsupported") return "Dieser Browser unterstützt keine Web-Benachrichtigungen.";
return "Benachrichtigungen sind noch nicht aktiviert.";
}
export default function ErinnerungenPage() {
const { data: session } = useSession();
const userKey = session?.user?.id ?? session?.user?.email ?? null;
const [settings, setSettings] = useState<ReminderSettings>(defaultReminderSettings);
const [permission, setPermission] = useState<NotificationPermission | "unsupported">("default");
const [saved, setSaved] = useState(false);
useEffect(() => {
if (!userKey) return;
setSettings(loadHellthData(userKey).reminders);
setPermission(typeof Notification === "undefined" ? "unsupported" : Notification.permission);
}, [userKey]);
function persist(nextSettings: ReminderSettings) {
if (!userKey) return;
setSettings(nextSettings);
const data = loadHellthData(userKey);
saveHellthData({ ...data, reminders: nextSettings }, userKey);
setSaved(true);
window.setTimeout(() => setSaved(false), 1500);
}
function updateRule(kind: ReminderKind, patch: Partial<ReminderRule>) {
persist({
...settings,
rules: {
...settings.rules,
[kind]: { ...settings.rules[kind], ...patch },
},
});
}
async function requestPermission() {
if (typeof Notification === "undefined") {
setPermission("unsupported");
return;
}
const result = await Notification.requestPermission();
setPermission(result);
persist({
...settings,
enabled: result === "granted" ? settings.enabled : false,
permissionRequestedAt: new Date().toISOString(),
});
}
async function sendTest(kind: ReminderKind) {
if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
const rule = settings.rules[kind];
if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.ready;
await registration.showNotification(`Hellth Hub: ${reminderMeta[kind].title}`, {
body: rule.message,
tag: `hellth-test-${kind}`,
});
return;
} catch {
// Fall through to regular notification.
}
}
new Notification(`Hellth Hub: ${reminderMeta[kind].title}`, { body: rule.message });
}
return (
<div className="mx-auto max-w-5xl space-y-5 pb-24">
<header className="rounded-[2rem] bg-white p-6 shadow-sm ring-1 ring-gray-100">
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-sm font-black uppercase text-[#0F766E]">Coach Impulse</p>
<h1 className="mt-2 text-4xl font-black tracking-normal text-gray-950">Erinnerungen</h1>
<p className="mt-3 max-w-2xl text-sm font-semibold text-gray-400">
Kleine Push-Impulse für Wasser, geplante Snacks und Bewegung im Arbeitsalltag.
</p>
</div>
<BellAlertIcon className="h-10 w-10 text-[#0F766E]" />
</div>
</header>
<section className="rounded-[2rem] bg-white p-6 shadow-sm ring-1 ring-gray-100">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-xl font-black text-gray-950">Benachrichtigungen</h2>
<p className="mt-1 text-sm font-semibold text-gray-400">{permissionLabel(permission)}</p>
</div>
<div className="flex flex-wrap gap-3">
<button
type="button"
onClick={() => void requestPermission()}
className="rounded-full bg-gray-950 px-5 py-3 text-sm font-black text-white"
>
Berechtigung anfragen
</button>
<button
type="button"
disabled={permission !== "granted"}
onClick={() => persist({ ...settings, enabled: !settings.enabled })}
className={`rounded-full px-5 py-3 text-sm font-black disabled:opacity-40 ${
settings.enabled ? "bg-emerald-600 text-white" : "bg-gray-100 text-gray-950"
}`}
>
{settings.enabled ? "Aktiv" : "Pausiert"}
</button>
</div>
</div>
</section>
<div className="grid gap-4 lg:grid-cols-3">
{(Object.keys(settings.rules) as ReminderKind[]).map((kind) => {
const meta = reminderMeta[kind];
const rule = settings.rules[kind];
return (
<section key={kind} className="rounded-[2rem] bg-white p-5 shadow-sm ring-1 ring-gray-100">
<div className="flex items-start gap-3">
<meta.icon className={`h-8 w-8 ${meta.color}`} />
<div>
<h2 className="text-xl font-black text-gray-950">{meta.title}</h2>
<p className="mt-1 text-sm font-semibold text-gray-400">{meta.description}</p>
</div>
</div>
<label className="mt-5 flex items-center justify-between rounded-2xl bg-gray-50 px-4 py-3 text-sm font-black text-gray-950">
Aktivieren
<input
type="checkbox"
checked={rule.enabled}
onChange={(event) => updateRule(kind, { enabled: event.target.checked })}
/>
</label>
<div className="mt-4 grid grid-cols-2 gap-2">
<label className="text-xs font-black uppercase text-gray-400">
Von
<input
type="time"
value={rule.startTime}
onChange={(event) => updateRule(kind, { startTime: event.target.value })}
className="mt-1 w-full rounded-2xl border border-gray-200 px-3 py-2 text-sm text-gray-950"
/>
</label>
<label className="text-xs font-black uppercase text-gray-400">
Bis
<input
type="time"
value={rule.endTime}
onChange={(event) => updateRule(kind, { endTime: event.target.value })}
className="mt-1 w-full rounded-2xl border border-gray-200 px-3 py-2 text-sm text-gray-950"
/>
</label>
</div>
<label className="mt-4 block text-xs font-black uppercase text-gray-400">
Intervall Minuten
<input
type="number"
min={15}
step={15}
value={rule.intervalMinutes}
onChange={(event) => updateRule(kind, { intervalMinutes: Number(event.target.value) || 60 })}
className="mt-1 w-full rounded-2xl border border-gray-200 px-3 py-2 text-sm text-gray-950"
/>
</label>
<label className="mt-4 block text-xs font-black uppercase text-gray-400">
Nachricht
<textarea
value={rule.message}
onChange={(event) => updateRule(kind, { message: event.target.value })}
className="mt-1 min-h-24 w-full rounded-2xl border border-gray-200 px-3 py-2 text-sm font-semibold normal-case text-gray-950"
/>
</label>
<button
type="button"
disabled={permission !== "granted"}
onClick={() => void sendTest(kind)}
className="mt-4 w-full rounded-full bg-gray-100 px-4 py-3 text-sm font-black text-gray-950 disabled:opacity-40"
>
Test senden
</button>
</section>
);
})}
</div>
{saved ? (
<div className="fixed bottom-24 left-1/2 z-50 flex -translate-x-1/2 items-center gap-2 rounded-full bg-emerald-600 px-5 py-3 text-sm font-black text-white shadow-lg">
<CheckCircleIcon className="h-5 w-5" />
Gespeichert
</div>
) : null}
</div>
);
}