87 lines
2.6 KiB
TypeScript
87 lines
2.6 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useEffect, useRef } from "react";
|
||
|
|
import { useSession } from "@/lib/auth-client";
|
||
|
|
import { ReminderKind, loadHellthData } from "@/lib/hellth-data";
|
||
|
|
|
||
|
|
const labels: Record<ReminderKind, string> = {
|
||
|
|
water: "Wasser",
|
||
|
|
snack: "Snack",
|
||
|
|
movement: "Bewegung",
|
||
|
|
};
|
||
|
|
|
||
|
|
function minutesOfDay(value: string) {
|
||
|
|
const [hours = "0", minutes = "0"] = value.split(":");
|
||
|
|
return Number(hours) * 60 + Number(minutes);
|
||
|
|
}
|
||
|
|
|
||
|
|
function canNotifyNow(startTime: string, endTime: string) {
|
||
|
|
const now = new Date();
|
||
|
|
const current = now.getHours() * 60 + now.getMinutes();
|
||
|
|
return current >= minutesOfDay(startTime) && current <= minutesOfDay(endTime);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function showReminder(kind: ReminderKind, message: string) {
|
||
|
|
if (typeof window === "undefined" || !("Notification" in window)) return;
|
||
|
|
if (Notification.permission !== "granted") return;
|
||
|
|
|
||
|
|
const title = `Hellth Hub: ${labels[kind]}`;
|
||
|
|
if ("serviceWorker" in navigator) {
|
||
|
|
try {
|
||
|
|
const registration = await navigator.serviceWorker.ready;
|
||
|
|
await registration.showNotification(title, {
|
||
|
|
body: message,
|
||
|
|
tag: `hellth-${kind}`,
|
||
|
|
badge: "/icon-192.png",
|
||
|
|
});
|
||
|
|
return;
|
||
|
|
} catch {
|
||
|
|
// Fall through to regular notification.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
new Notification(title, { body: message, tag: `hellth-${kind}` });
|
||
|
|
}
|
||
|
|
|
||
|
|
export function ReminderRunner() {
|
||
|
|
const { data: session } = useSession();
|
||
|
|
const userKey = session?.user?.id ?? session?.user?.email ?? null;
|
||
|
|
const lastSentRef = useRef<Record<string, number>>({});
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!("serviceWorker" in navigator)) return;
|
||
|
|
void navigator.serviceWorker.register("/hellth-sw.js").catch(() => undefined);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!userKey) return;
|
||
|
|
|
||
|
|
function tick() {
|
||
|
|
const data = loadHellthData(userKey);
|
||
|
|
const reminders = data.reminders;
|
||
|
|
if (!reminders.enabled) return;
|
||
|
|
if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
|
||
|
|
|
||
|
|
const now = Date.now();
|
||
|
|
(Object.keys(reminders.rules) as ReminderKind[]).forEach((kind) => {
|
||
|
|
const rule = reminders.rules[kind];
|
||
|
|
if (!rule.enabled) return;
|
||
|
|
if (!canNotifyNow(rule.startTime, rule.endTime)) return;
|
||
|
|
|
||
|
|
const lastSentKey = `${userKey}:${kind}`;
|
||
|
|
const lastSent = lastSentRef.current[lastSentKey] ?? 0;
|
||
|
|
if (now - lastSent < rule.intervalMinutes * 60_000) return;
|
||
|
|
|
||
|
|
lastSentRef.current[lastSentKey] = now;
|
||
|
|
void showReminder(kind, rule.message);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
tick();
|
||
|
|
const interval = window.setInterval(tick, 60_000);
|
||
|
|
return () => window.clearInterval(interval);
|
||
|
|
}, [userKey]);
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|