feat: add error display, error boundaries, and toast notifications
This commit is contained in:
@@ -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"}
|
||||
×
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user