Files
SRCmail/components/ui/toast.tsx
T
Matthieu MALVACHEandMatthieu MALVACHE fea21d5bcd feat(calendar): add event notifications with alert evaluation
Add client-side calendar event notification system that evaluates JMAP
CalendarEventAlert triggers and displays toast notifications when alert
times are reached. Includes configurable notification sound, acknowledged
alert persistence, and proactive event fetching for background alerts.
Also mounts ToastContainer globally to fix silent toast failures.
2026-02-16 23:49:35 +01:00

92 lines
2.6 KiB
TypeScript

"use client";
import { useEffect } from "react";
import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from "lucide-react";
import { cn } from "@/lib/utils";
export type ToastType = "success" | "error" | "info" | "warning";
export interface Toast {
id: string;
type: ToastType;
title: string;
message?: string;
duration?: number;
onClick?: () => void;
icon?: React.ReactNode;
}
interface ToastProps {
toast: Toast;
onClose: (id: string) => void;
}
const icons = {
success: CheckCircle,
error: AlertCircle,
info: Info,
warning: AlertTriangle,
};
const styles = {
success: "bg-green-50 dark:bg-green-950/30 border-green-200 dark:border-green-800 text-green-800 dark:text-green-200",
error: "bg-red-50 dark:bg-red-950/30 border-red-200 dark:border-red-800 text-red-800 dark:text-red-200",
info: "bg-blue-50 dark:bg-blue-950/30 border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-200",
warning: "bg-amber-50 dark:bg-amber-950/30 border-amber-200 dark:border-amber-800 text-amber-800 dark:text-amber-200",
};
export function ToastItem({ toast, onClose }: ToastProps) {
const Icon = icons[toast.type];
useEffect(() => {
if (toast.duration && toast.duration > 0) {
const timer = setTimeout(() => {
onClose(toast.id);
}, toast.duration);
return () => clearTimeout(timer);
}
}, [toast, onClose]);
return (
<div
className={cn(
"flex items-start gap-3 p-4 rounded-lg border shadow-lg bg-background animate-slide-in",
styles[toast.type],
toast.onClick && "cursor-pointer hover:opacity-90 transition-opacity"
)}
onClick={() => {
if (toast.onClick) {
toast.onClick();
onClose(toast.id);
}
}}
>
{toast.icon !== undefined ? toast.icon : <Icon className="w-5 h-5 flex-shrink-0 mt-0.5" />}
<div className="flex-1">
<h4 className="font-medium">{toast.title}</h4>
{toast.message && (
<p className="text-sm mt-1 opacity-90">{toast.message}</p>
)}
</div>
<button
onClick={(e) => {
e.stopPropagation();
onClose(toast.id);
}}
className="text-muted-foreground hover:text-foreground transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
);
}
export function ToastContainer({ toasts, onClose }: { toasts: Toast[]; onClose: (id: string) => void }) {
return (
<div className="fixed bottom-4 right-4 z-50 space-y-2 max-w-sm">
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} onClose={onClose} />
))}
</div>
);
}