'use client'; /** * @file ToastProvider.tsx * @description Lightweight global Toast/Notification system. * Uses React Context + Zustand-like imperative API via a singleton store. * * Usage: * 1. Wrap your layout with * 2. Call toast.success('Mensaje') anywhere in the app */ import React, { createContext, useCallback, useContext, useReducer, useRef } from 'react'; import { createPortal } from 'react-dom'; import { CheckCircle, AlertCircle, Info, X, AlertTriangle } from 'lucide-react'; // ───────────────────────────────────────────── // TYPES // ───────────────────────────────────────────── export type ToastVariant = 'success' | 'error' | 'warning' | 'info'; export interface Toast { id: string; message: string; variant: ToastVariant; duration?: number; } type ToastAction = | { type: 'ADD'; toast: Toast } | { type: 'REMOVE'; id: string }; interface ToastContextValue { toasts: Toast[]; addToast: (message: string, variant?: ToastVariant, duration?: number) => void; removeToast: (id: string) => void; } // ───────────────────────────────────────────── // CONTEXT // ───────────────────────────────────────────── const ToastContext = createContext(null); function toastReducer(state: Toast[], action: ToastAction): Toast[] { switch (action.type) { case 'ADD': return [action.toast, ...state].slice(0, 5); // Max 5 toasts case 'REMOVE': return state.filter((t) => t.id !== action.id); default: return state; } } // ───────────────────────────────────────────── // PROVIDER // ───────────────────────────────────────────── export function ToastProvider({ children }: { children: React.ReactNode }) { const [toasts, dispatch] = useReducer(toastReducer, []); const counterRef = useRef(0); const removeToast = useCallback((id: string) => { dispatch({ type: 'REMOVE', id }); }, []); const addToast = useCallback(( message: string, variant: ToastVariant = 'info', duration = 4000 ) => { const id = `toast-${Date.now()}-${++counterRef.current}`; dispatch({ type: 'ADD', toast: { id, message, variant, duration } }); if (duration > 0) { setTimeout(() => dispatch({ type: 'REMOVE', id }), duration); } }, []); return ( {children} ); } // ───────────────────────────────────────────── // HOOK // ───────────────────────────────────────────── export function useToast() { const ctx = useContext(ToastContext); if (!ctx) throw new Error('useToast must be used inside '); return { success: (msg: string, duration?: number) => ctx.addToast(msg, 'success', duration), error: (msg: string, duration?: number) => ctx.addToast(msg, 'error', duration), warning: (msg: string, duration?: number) => ctx.addToast(msg, 'warning', duration), info: (msg: string, duration?: number) => ctx.addToast(msg, 'info', duration), remove: ctx.removeToast, }; } // ───────────────────────────────────────────── // TOAST ITEM // ───────────────────────────────────────────── const variantStyles: Record = { success: { bg: 'bg-emerald-950/90', border: 'border-emerald-500/40', icon: , }, error: { bg: 'bg-red-950/90', border: 'border-red-500/40', icon: , }, warning: { bg: 'bg-amber-950/90', border: 'border-amber-500/40', icon: , }, info: { bg: 'bg-slate-900/90', border: 'border-cyan-500/30', icon: , }, }; const ToastItem = React.memo(function ToastItem({ toast, onRemove, }: { toast: Toast; onRemove: (id: string) => void; }) { const styles = variantStyles[toast.variant]; return (
{styles.icon}

{toast.message}

); }); // ───────────────────────────────────────────── // TOAST CONTAINER (Portal) // ───────────────────────────────────────────── function ToastContainer({ toasts, onRemove, }: { toasts: Toast[]; onRemove: (id: string) => void; }) { if (typeof document === 'undefined' || toasts.length === 0) return null; return createPortal(
{toasts.map((t) => ( ))}
, document.body ); }