100 lines
3.5 KiB
TypeScript
100 lines
3.5 KiB
TypeScript
import { Fragment, useEffect } from 'react';
|
|
import { Transition } from '@headlessui/react';
|
|
import {
|
|
CheckCircleIcon,
|
|
XCircleIcon,
|
|
InformationCircleIcon,
|
|
XMarkIcon
|
|
} from '@heroicons/react/24/outline';
|
|
|
|
export interface Toast {
|
|
id: string;
|
|
type: 'success' | 'error' | 'info';
|
|
title: string;
|
|
message?: string;
|
|
}
|
|
|
|
interface ToastNotificationProps {
|
|
toast: Toast;
|
|
onClose: (id: string) => void;
|
|
}
|
|
|
|
export default function ToastNotification({ toast, onClose }: ToastNotificationProps) {
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
onClose(toast.id);
|
|
}, 5000);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [toast.id, onClose]);
|
|
|
|
const styles = {
|
|
success: {
|
|
bg: 'bg-emerald-50 dark:bg-emerald-900/20',
|
|
border: 'border-emerald-200 dark:border-emerald-900/30',
|
|
icon: 'text-emerald-600 dark:text-emerald-400',
|
|
title: 'text-emerald-900 dark:text-emerald-300',
|
|
IconComponent: CheckCircleIcon
|
|
},
|
|
error: {
|
|
bg: 'bg-red-50 dark:bg-red-900/20',
|
|
border: 'border-red-200 dark:border-red-900/30',
|
|
icon: 'text-red-600 dark:text-red-400',
|
|
title: 'text-red-900 dark:text-red-300',
|
|
IconComponent: XCircleIcon
|
|
},
|
|
info: {
|
|
bg: 'bg-blue-50 dark:bg-blue-900/20',
|
|
border: 'border-blue-200 dark:border-blue-900/30',
|
|
icon: 'text-blue-600 dark:text-blue-400',
|
|
title: 'text-blue-900 dark:text-blue-300',
|
|
IconComponent: InformationCircleIcon
|
|
}
|
|
};
|
|
|
|
const style = styles[toast.type];
|
|
const Icon = style.IconComponent;
|
|
|
|
return (
|
|
<Transition
|
|
show={true}
|
|
as={Fragment}
|
|
enter="transform ease-out duration-300 transition"
|
|
enterFrom="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
|
|
enterTo="translate-y-0 opacity-100 sm:translate-x-0"
|
|
leave="transition ease-in duration-100"
|
|
leaveFrom="opacity-100"
|
|
leaveTo="opacity-0"
|
|
>
|
|
<div className={`pointer-events-auto w-full rounded-lg border shadow-lg ${style.bg} ${style.border}`}>
|
|
<div className="p-4">
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex-shrink-0">
|
|
<Icon className={`h-6 w-6 ${style.icon}`} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className={`text-sm font-semibold ${style.title}`}>
|
|
{toast.title}
|
|
</p>
|
|
{toast.message && (
|
|
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
|
|
{toast.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex-shrink-0">
|
|
<button
|
|
type="button"
|
|
onClick={() => onClose(toast.id)}
|
|
className="inline-flex rounded-md text-zinc-400 hover:text-zinc-500 dark:hover:text-zinc-300 focus:outline-none"
|
|
>
|
|
<XMarkIcon className="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
);
|
|
}
|