2.4 KB103 lines
Blame
1"use client";
2
3import {
4 createContext,
5 useContext,
6 useState,
7 useCallback,
8 useEffect,
9} from "react";
10import { createPortal } from "react-dom";
11
12interface ToastItem {
13 id: string;
14 message: string;
15 variant: "success" | "error" | "info";
16}
17
18interface ToastContextValue {
19 toast: (message: string, variant?: ToastItem["variant"]) => void;
20}
21
22const ToastContext = createContext<ToastContextValue>({
23 toast: () => {},
24});
25
26export function useToast() {
27 return useContext(ToastContext);
28}
29
30const variantStyles: Record<
31 ToastItem["variant"],
32 { bg: string; border: string; text: string }
33> = {
34 success: {
35 bg: "var(--status-open-bg)",
36 border: "var(--status-open-border)",
37 text: "var(--status-open-text)",
38 },
39 error: {
40 bg: "var(--error-bg)",
41 border: "var(--error-border)",
42 text: "var(--error-text)",
43 },
44 info: {
45 bg: "var(--accent-subtle)",
46 border: "var(--accent)",
47 text: "var(--accent)",
48 },
49};
50
51export function ToastProvider({ children }: { children: React.ReactNode }) {
52 const [toasts, setToasts] = useState<ToastItem[]>([]);
53 const [mounted, setMounted] = useState(false);
54
55 useEffect(() => {
56 setMounted(true);
57 }, []);
58
59 const toast = useCallback(
60 (message: string, variant: ToastItem["variant"] = "info") => {
61 const id = Math.random().toString(36).slice(2);
62 setToasts((prev) => [...prev, { id, message, variant }]);
63 setTimeout(() => {
64 setToasts((prev) => prev.filter((t) => t.id !== id));
65 }, 3000);
66 },
67 []
68 );
69
70 return (
71 <ToastContext.Provider value={{ toast }}>
72 {children}
73 {mounted &&
74 createPortal(
75 <div
76 className="fixed z-50"
77 style={{ bottom: 16, right: 16, display: "flex", flexDirection: "column", gap: 8 }}
78 >
79 {toasts.map((t) => {
80 const s = variantStyles[t.variant];
81 return (
82 <div
83 key={t.id}
84 className="toast-enter text-sm px-4 py-3"
85 style={{
86 backgroundColor: s.bg,
87 border: `1px solid ${s.border}`,
88 color: s.text,
89 minWidth: 200,
90 maxWidth: 360,
91 }}
92 >
93 {t.message}
94 </div>
95 );
96 })}
97 </div>,
98 document.body
99 )}
100 </ToastContext.Provider>
101 );
102}
103