"use client"; import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; import { clsx } from "clsx"; import type { ActionResult } from "@/app/actions/library"; /** * Kurze Rückmeldungen am unteren Rand — mit optionalem „Rückgängig". * * Warum überhaupt: Beim Abhaken einer Episode passiert die eigentliche Wirkung * weit weg vom Klick (Fortschrittsbalken, Status, „nächste Episode"). Eine * kurze Bestätigung sagt, was gerade geschehen ist — und lässt es zurücknehmen, * ohne die Stelle wiederfinden zu müssen. */ type Toast = { id: number; message: string; tone: "info" | "error" | "erfolg"; undo?: () => Promise | void; }; type ToastContext = { show(message: string, options?: { tone?: Toast["tone"]; undo?: Toast["undo"] }): void; /** Bequemer Weg für Aktionsergebnisse samt Rückgängig-Möglichkeit. */ report(result: ActionResult, undo?: (watchIds: string[]) => Promise): void; }; const Context = createContext(null); const VISIBLE_MS = 6000; export function ToastProvider({ children }: { children: React.ReactNode }) { const [toasts, setToasts] = useState([]); const nextId = useRef(1); const dismiss = useCallback((id: number) => { setToasts((current) => current.filter((toast) => toast.id !== id)); }, []); const show = useCallback((message, options) => { const id = nextId.current++; setToasts((current) => [ ...current.slice(-2), { id, message, tone: options?.tone ?? "info", undo: options?.undo }, ]); }, []); const report = useCallback( (result, undo) => { if (!result.ok) { show(result.message ?? "Das hat nicht geklappt.", { tone: "error" }); return; } if (!result.message) return; const ids = result.watchIds ?? []; show(result.message, { undo: undo && ids.length > 0 ? () => void undo(ids) : undefined, }); // Errungenschaften bekommen eine eigene Einblendung — sie sollen nicht // in der Meldung über das Abhaken untergehen. for (const name of result.achievements ?? []) { show(`Errungenschaft: ${name}`, { tone: "erfolg" }); } }, [show], ); return ( {children}
{toasts.map((toast) => ( dismiss(toast.id)} /> ))}
); } function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }) { useEffect(() => { const timer = setTimeout(onDismiss, VISIBLE_MS); return () => clearTimeout(timer); }, [onDismiss]); return (
{toast.message} {toast.undo && ( )}
); } export function useToasts(): ToastContext { const context = useContext(Context); if (!context) { throw new Error("useToasts braucht einen ToastProvider im Elternbaum."); } return context; }