This commit is contained in:
35
frontend/src/components/toast/ToastItem.tsx
Normal file
35
frontend/src/components/toast/ToastItem.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { ToastMessage } from '../../hooks/useToast';
|
||||
|
||||
const STYLES: Record<ToastMessage['type'], string> = {
|
||||
success: 'border-emerald-300 bg-emerald-500/15 text-emerald-100',
|
||||
error: 'border-red-300 bg-red-500/15 text-red-100',
|
||||
warning: 'border-amber-300 bg-amber-500/15 text-amber-100',
|
||||
info: 'border-sky-300 bg-sky-500/15 text-sky-100',
|
||||
};
|
||||
|
||||
interface ToastItemProps {
|
||||
toast: ToastMessage;
|
||||
onClose: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function ToastItem({ toast, onClose }: ToastItemProps) {
|
||||
return (
|
||||
<article
|
||||
className={`pointer-events-auto rounded-lg border px-4 py-3 shadow-lg backdrop-blur ${STYLES[toast.type]}`}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<p className="text-sm">{toast.message}</p>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs opacity-80 hover:opacity-100"
|
||||
onClick={() => onClose(toast.id)}
|
||||
aria-label="Close toast"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
28
frontend/src/components/toast/ToastProvider.tsx
Normal file
28
frontend/src/components/toast/ToastProvider.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import ToastItem from './ToastItem';
|
||||
import { removeToast, toastMessages } from '../../hooks/useToast';
|
||||
|
||||
export default function ToastProvider() {
|
||||
const toasts = Array.isArray(toastMessages.value) ? toastMessages.value : [];
|
||||
|
||||
useEffect(() => {
|
||||
const timers = toasts.map((toast) =>
|
||||
window.setTimeout(() => removeToast(toast.id), toast.durationMs)
|
||||
);
|
||||
return () => {
|
||||
timers.forEach((timer) => window.clearTimeout(timer));
|
||||
};
|
||||
}, [toasts]);
|
||||
|
||||
if (!toasts.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="pointer-events-none fixed right-4 top-4 z-[80] flex w-full max-w-sm flex-col gap-2">
|
||||
{toasts.map((toast) => (
|
||||
<ToastItem key={toast.id} toast={toast} onClose={removeToast} />
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user