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 }> = {
|
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"}
|
×
|
||||||
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user