| 'use client' |
| import { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react' |
| import { CheckCircle2, AlertCircle, Zap, X } from 'lucide-react' |
| import { cn } from '@/lib/utils' |
|
|
| type ToastType = 'success' | 'error' | 'event' |
| interface Toast { id: string; type: ToastType; title: string; body?: string } |
| interface ToastCtx { fire: (t: Omit<Toast, 'id'>) => void } |
|
|
| const Ctx = createContext<ToastCtx>({ fire: () => {} }) |
| export const useToast = () => useContext(Ctx) |
|
|
| const ICONS = { |
| success: CheckCircle2, |
| error: AlertCircle, |
| event: Zap, |
| } |
| const COLORS = { |
| success: 'border-trading-up/40 bg-trading-up/10', |
| error: 'border-trading-down/40 bg-trading-down/10', |
| event: 'border-brand-yellow/40 bg-brand-yellow/10', |
| } |
| const ICON_COLORS = { |
| success: 'text-trading-up', |
| error: 'text-trading-down', |
| event: 'text-brand-yellow', |
| } |
|
|
| function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }) { |
| const [visible, setVisible] = useState(false) |
| const Icon = ICONS[toast.type] |
|
|
| useEffect(() => { |
| requestAnimationFrame(() => setVisible(true)) |
| const t = setTimeout(() => { setVisible(false); setTimeout(onDismiss, 300) }, 4000) |
| return () => clearTimeout(t) |
| }, [onDismiss]) |
|
|
| return ( |
| <div className={cn( |
| 'flex items-start gap-3 w-80 rounded-xl border p-3.5 shadow-2xl backdrop-blur-sm transition-all duration-300', |
| COLORS[toast.type], |
| visible ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-full' |
| )}> |
| <Icon className={cn('w-4 h-4 mt-0.5 flex-shrink-0', ICON_COLORS[toast.type])} /> |
| <div className="flex-1 min-w-0"> |
| <p className="text-body-sm font-semibold text-[#eaecef] leading-tight">{toast.title}</p> |
| {toast.body && <p className="font-mono text-[10px] text-muted mt-0.5 truncate">{toast.body}</p>} |
| </div> |
| <button onClick={() => { setVisible(false); setTimeout(onDismiss, 300) }} className="text-muted hover:text-[#eaecef] transition flex-shrink-0"> |
| <X className="w-3.5 h-3.5" /> |
| </button> |
| </div> |
| ) |
| } |
|
|
| export function ToastProvider({ children }: { children: React.ReactNode }) { |
| const [toasts, setToasts] = useState<Toast[]>([]) |
| const counter = useRef(0) |
|
|
| const fire = useCallback((t: Omit<Toast, 'id'>) => { |
| const id = `t-${counter.current++}` |
| setToasts(p => [...p.slice(-4), { ...t, id }]) |
| }, []) |
|
|
| const dismiss = useCallback((id: string) => { |
| setToasts(p => p.filter(t => t.id !== id)) |
| }, []) |
|
|
| return ( |
| <Ctx.Provider value={{ fire }}> |
| {children} |
| <div className="fixed bottom-6 right-6 z-[100] flex flex-col gap-2 items-end pointer-events-none"> |
| {toasts.map(t => ( |
| <div key={t.id} className="pointer-events-auto"> |
| <ToastItem toast={t} onDismiss={() => dismiss(t.id)} /> |
| </div> |
| ))} |
| </div> |
| </Ctx.Provider> |
| ) |
| } |
|
|