1"use client";2import { createContext, useCallback, useContext, useMemo, useRef, useState, type ReactNode } from "react";3import { isApiError } from "./api";45export type ToastKind = "info" | "success" | "warning" | "error";6export interface ToastItem {7 id: number;8 kind: ToastKind;9 title: string;10 detail?: string;11 requestId?: string | null;12}1314interface ToastCtx {15 toasts: ToastItem[];16 push: (t: Omit<ToastItem, "id">) => void;17 dismiss: (id: number) => void;18 error: (e: unknown, fallback?: string) => void;19 success: (title: string, detail?: string) => void;20 info: (title: string, detail?: string) => void;21}2223const Ctx = createContext<ToastCtx | null>(null);2425export function ToastProvider({ children }: { children: ReactNode }) {26 const [toasts, setToasts] = useState<ToastItem[]>([]);27 const seq = useRef(0);2829 const dismiss = useCallback((id: number) => setToasts((t) => t.filter((x) => x.id !== id)), []);30 const push = useCallback(31 (t: Omit<ToastItem, "id">) => {32 const id = ++seq.current;33 setToasts((prev) => [...prev.slice(-4), { ...t, id }]);34 const ttl = t.kind === "error" ? 9000 : 4500;35 window.setTimeout(() => dismiss(id), ttl);36 },37 [dismiss],38 );39 const error = useCallback(40 (e: unknown, fallback = "Request failed") => {41 if (isApiError(e)) {42 push({ kind: "error", title: `${e.message}`, detail: `${e.code} · HTTP ${e.status}`, requestId: e.request_id });43 } else if (e instanceof Error) {44 push({ kind: "error", title: fallback, detail: e.message });45 } else {46 push({ kind: "error", title: fallback });47 }48 },49 [push],50 );51 const success = useCallback((title: string, detail?: string) => push({ kind: "success", title, detail }), [push]);52 const info = useCallback((title: string, detail?: string) => push({ kind: "info", title, detail }), [push]);5354 const value = useMemo(() => ({ toasts, push, dismiss, error, success, info }), [toasts, push, dismiss, error, success, info]);55 return <Ctx.Provider value={value}>{children}</Ctx.Provider>;56}5758export function useToast(): ToastCtx {59 const c = useContext(Ctx);60 if (!c) throw new Error("useToast outside ToastProvider");61 return c;62}63