flowstate / src /components /ui /Toast.tsx
muthuk1's picture
feat: full Torque integration — live events, toast, bulk rescue, auto-scan
c6b6c96
'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>
)
}