feat: add error display, error boundaries, and toast notifications

This commit is contained in:
plenarius
2026-04-20 22:12:50 -04:00
parent 3df79d3b17
commit 3a1df485ed
+16 -18
View File
@@ -31,7 +31,7 @@ export function useToast() {
const COLORS: Record<ToastType, { bg: string; border: string; text: string }> = { const COLORS: Record<ToastType, { bg: string; border: string; text: string }> = {
success: { bg: "#052e16", border: "#166534", text: "#4ade80" }, success: { bg: "#052e16", border: "#166534", text: "#4ade80" },
error: { bg: "#3b1111", border: "#7f1d1d", text: "#fca5a5" }, error: { bg: "#3b1111", border: "#7f1d1d", text: "#fca5a5" },
info: { bg: "#1c1a00", border: "#713f12", text: "#fbbf24" }, info: { bg: "#1c1403", border: "#946200", text: "#fbbf24" },
}; };
const AUTO_DISMISS: Record<ToastType, number | null> = { const AUTO_DISMISS: Record<ToastType, number | null> = {
@@ -48,30 +48,28 @@ function ToastItem({
onDismiss: (id: number) => void; onDismiss: (id: number) => void;
}) { }) {
const { bg, border, text } = COLORS[toast.type]; const { bg, border, text } = COLORS[toast.type];
const dismissMs = AUTO_DISMISS[toast.type]; const timeout = AUTO_DISMISS[toast.type];
const timerRef = useRef<ReturnType<typeof setTimeout>>(); const timerRef = useRef<ReturnType<typeof setTimeout>>();
useEffect(() => { useEffect(() => {
if (dismissMs !== null) { if (timeout) {
timerRef.current = setTimeout(() => onDismiss(toast.id), dismissMs); timerRef.current = setTimeout(() => onDismiss(toast.id), timeout);
return () => clearTimeout(timerRef.current); return () => clearTimeout(timerRef.current);
} }
}, [toast.id, dismissMs, onDismiss]); }, [toast.id, timeout, onDismiss]);
return ( return (
<div <div
className="flex items-center gap-3 rounded-lg px-4 py-3 shadow-lg" className="flex items-start gap-2 rounded-lg px-4 py-3 text-sm shadow-lg"
style={{ backgroundColor: bg, border: `1px solid ${border}` }} style={{ backgroundColor: bg, border: `1px solid ${border}`, color: text }}
> >
<span className="flex-1 text-sm" style={{ color: text }}> <span className="flex-1">{toast.message}</span>
{toast.message}
</span>
<button <button
onClick={() => onDismiss(toast.id)} onClick={() => onDismiss(toast.id)}
className="text-sm opacity-60 hover:opacity-100" className="ml-2 shrink-0 opacity-60 hover:opacity-100"
style={{ color: text }} style={{ color: text }}
> >
{"\u2715"} &times;
</button> </button>
</div> </div>
); );
@@ -83,20 +81,20 @@ export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]); const [toasts, setToasts] = useState<Toast[]>([]);
const showToast = useCallback((message: string, type: ToastType = "info") => { const showToast = useCallback((message: string, type: ToastType = "info") => {
const id = ++nextId; const id = nextId++;
setToasts((t) => [...t, { id, message, type }]); setToasts((prev) => [...prev, { id, message, type }]);
}, []); }, []);
const dismiss = useCallback((id: number) => { const dismiss = useCallback((id: number) => {
setToasts((t) => t.filter((toast) => toast.id !== id)); setToasts((prev) => prev.filter((t) => t.id !== id));
}, []); }, []);
return ( return (
<ToastContext.Provider value={{ showToast }}> <ToastContext.Provider value={{ showToast }}>
{children} {children}
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2" style={{ maxWidth: "400px" }}> <div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2" style={{ maxWidth: "360px" }}>
{toasts.map((toast) => ( {toasts.map((t) => (
<ToastItem key={toast.id} toast={toast} onDismiss={dismiss} /> <ToastItem key={t.id} toast={t} onDismiss={dismiss} />
))} ))}
</div> </div>
</ToastContext.Provider> </ToastContext.Provider>