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 }> = {
success: { bg: "#052e16", border: "#166534", text: "#4ade80" },
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> = {
@@ -48,30 +48,28 @@ function ToastItem({
onDismiss: (id: number) => void;
}) {
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>>();
useEffect(() => {
if (dismissMs !== null) {
timerRef.current = setTimeout(() => onDismiss(toast.id), dismissMs);
if (timeout) {
timerRef.current = setTimeout(() => onDismiss(toast.id), timeout);
return () => clearTimeout(timerRef.current);
}
}, [toast.id, dismissMs, onDismiss]);
}, [toast.id, timeout, onDismiss]);
return (
<div
className="flex items-center gap-3 rounded-lg px-4 py-3 shadow-lg"
style={{ backgroundColor: bg, border: `1px solid ${border}` }}
className="flex items-start gap-2 rounded-lg px-4 py-3 text-sm shadow-lg"
style={{ backgroundColor: bg, border: `1px solid ${border}`, color: text }}
>
<span className="flex-1 text-sm" style={{ color: text }}>
{toast.message}
</span>
<span className="flex-1">{toast.message}</span>
<button
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 }}
>
{"\u2715"}
&times;
</button>
</div>
);
@@ -83,20 +81,20 @@ export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const showToast = useCallback((message: string, type: ToastType = "info") => {
const id = ++nextId;
setToasts((t) => [...t, { id, message, type }]);
const id = nextId++;
setToasts((prev) => [...prev, { id, message, type }]);
}, []);
const dismiss = useCallback((id: number) => {
setToasts((t) => t.filter((toast) => toast.id !== id));
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
return (
<ToastContext.Provider value={{ showToast }}>
{children}
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2" style={{ maxWidth: "400px" }}>
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} onDismiss={dismiss} />
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2" style={{ maxWidth: "360px" }}>
{toasts.map((t) => (
<ToastItem key={t.id} toast={t} onDismiss={dismiss} />
))}
</div>
</ToastContext.Provider>