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