135 lines
4.0 KiB
TypeScript
135 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import React, { createContext, useContext, useState, useCallback } from "react";
|
|
import { Toast, ToastTitle, ToastDescription, ToastClose } from "./toast";
|
|
import { CheckCircle, AlertCircle, AlertTriangle, Info, X } from "lucide-react";
|
|
|
|
interface ToastMessage {
|
|
id: string;
|
|
type: "success" | "error" | "warning" | "info";
|
|
title: string;
|
|
description?: string;
|
|
duration?: number;
|
|
}
|
|
|
|
interface ToastContextType {
|
|
showToast: (message: Omit<ToastMessage, "id">) => void;
|
|
showSuccess: (title: string, description?: string) => void;
|
|
showError: (title: string, description?: string) => void;
|
|
showWarning: (title: string, description?: string) => void;
|
|
showInfo: (title: string, description?: string) => void;
|
|
}
|
|
|
|
const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
|
|
|
export const useToast = () => {
|
|
const context = useContext(ToastContext);
|
|
if (!context) {
|
|
throw new Error("useToast must be used within a ToastProvider");
|
|
}
|
|
return context;
|
|
};
|
|
|
|
interface ToastProviderProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
|
|
const [toasts, setToasts] = useState<ToastMessage[]>([]);
|
|
|
|
const removeToast = useCallback((id: string) => {
|
|
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
|
}, []);
|
|
|
|
const showToast = useCallback((message: Omit<ToastMessage, "id">) => {
|
|
const id = Math.random().toString(36).substr(2, 9);
|
|
const newToast: ToastMessage = {
|
|
...message,
|
|
id,
|
|
duration: message.duration || 5000,
|
|
};
|
|
|
|
setToasts((prev) => [...prev, newToast]);
|
|
|
|
// Auto remove toast after duration
|
|
setTimeout(() => {
|
|
removeToast(id);
|
|
}, newToast.duration);
|
|
}, [removeToast]);
|
|
|
|
const showSuccess = useCallback((title: string, description?: string) => {
|
|
showToast({ type: "success", title, description });
|
|
}, [showToast]);
|
|
|
|
const showError = useCallback((title: string, description?: string) => {
|
|
showToast({ type: "error", title, description });
|
|
}, [showToast]);
|
|
|
|
const showWarning = useCallback((title: string, description?: string) => {
|
|
showToast({ type: "warning", title, description });
|
|
}, [showToast]);
|
|
|
|
const showInfo = useCallback((title: string, description?: string) => {
|
|
showToast({ type: "info", title, description });
|
|
}, [showToast]);
|
|
|
|
const getToastVariant = (type: ToastMessage["type"]) => {
|
|
switch (type) {
|
|
case "success":
|
|
return "success";
|
|
case "error":
|
|
return "destructive";
|
|
case "warning":
|
|
return "warning";
|
|
case "info":
|
|
return "info";
|
|
default:
|
|
return "default";
|
|
}
|
|
};
|
|
|
|
const getToastIcon = (type: ToastMessage["type"]) => {
|
|
switch (type) {
|
|
case "success":
|
|
return <CheckCircle className="h-5 w-5 text-green-600" />;
|
|
case "error":
|
|
return <AlertCircle className="h-5 w-5 text-red-600" />;
|
|
case "warning":
|
|
return <AlertTriangle className="h-5 w-5 text-yellow-600" />;
|
|
case "info":
|
|
return <Info className="h-5 w-5 text-blue-600" />;
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ToastContext.Provider
|
|
value={{ showToast, showSuccess, showError, showWarning, showInfo }}
|
|
>
|
|
{children}
|
|
|
|
{/* Toast Container */}
|
|
<div className="fixed top-4 right-4 z-50 space-y-2">
|
|
{toasts.map((toast) => (
|
|
<Toast
|
|
key={toast.id}
|
|
variant={getToastVariant(toast.type)}
|
|
className="min-w-[300px]"
|
|
>
|
|
<div className="flex items-start space-x-3">
|
|
{getToastIcon(toast.type)}
|
|
<div className="flex-1">
|
|
<ToastTitle>{toast.title}</ToastTitle>
|
|
{toast.description && (
|
|
<ToastDescription>{toast.description}</ToastDescription>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<ToastClose onClick={() => removeToast(toast.id)} />
|
|
</Toast>
|
|
))}
|
|
</div>
|
|
</ToastContext.Provider>
|
|
);
|
|
};
|