import React from 'react'; import { YStack, XStack } from '@tamagui/stacks'; import { SizableText as Text } from '@tamagui/text'; import { Button } from '@tamagui/button'; import { useGuestThemeVariant } from '../lib/guestTheme'; type ToastAction = { label: string; onClick: () => void }; type ToastType = 'success' | 'error' | 'info'; export type GuestToast = { id: number; text: string; type?: ToastType; action?: ToastAction; durationMs?: number; }; type ToastPayload = Omit; const DEFAULT_DURATION = 3000; export default function ToastHost() { const [list, setList] = React.useState([]); const timeouts = React.useRef>(new Map()); const { isDark } = useGuestThemeVariant(); const dismiss = React.useCallback((id: number) => { setList((arr) => arr.filter((t) => t.id !== id)); const timeout = timeouts.current.get(id); if (timeout) { window.clearTimeout(timeout); timeouts.current.delete(id); } }, []); const push = React.useCallback( (toast: ToastPayload) => { const id = Date.now() + Math.floor(Math.random() * 1000); const durationMs = toast.durationMs ?? DEFAULT_DURATION; setList((arr) => [...arr, { id, ...toast, durationMs }]); if (durationMs > 0 && typeof window !== 'undefined') { const timeout = window.setTimeout(() => dismiss(id), durationMs); timeouts.current.set(id, timeout); } }, [dismiss] ); React.useEffect(() => { if (typeof window === 'undefined') return; const onEvt = (e: Event) => { const detail = (e as CustomEvent).detail; if (detail?.text) { push(detail); } }; window.addEventListener('guest-toast', onEvt as EventListener); return () => window.removeEventListener('guest-toast', onEvt as EventListener); }, [push]); React.useEffect(() => { return () => { for (const timeout of timeouts.current.values()) { window.clearTimeout(timeout); } timeouts.current.clear(); }; }, []); const resolveTone = (type?: ToastType) => { if (type === 'error') { return { border: isDark ? 'rgba(248, 113, 113, 0.5)' : 'rgba(248, 113, 113, 0.4)', background: isDark ? 'rgba(127, 29, 29, 0.7)' : 'rgba(254, 226, 226, 0.95)', text: isDark ? '#FEE2E2' : '#991B1B', }; } if (type === 'info') { return { border: isDark ? 'rgba(59, 130, 246, 0.45)' : 'rgba(59, 130, 246, 0.35)', background: isDark ? 'rgba(30, 64, 175, 0.65)' : 'rgba(219, 234, 254, 0.95)', text: isDark ? '#DBEAFE' : '#1D4ED8', }; } return { border: isDark ? 'rgba(34, 197, 94, 0.45)' : 'rgba(34, 197, 94, 0.35)', background: isDark ? 'rgba(22, 101, 52, 0.7)' : 'rgba(220, 252, 231, 0.95)', text: isDark ? '#DCFCE7' : '#166534', }; }; if (!list.length) { return null; } return ( {list.map((toast) => { const tone = resolveTone(toast.type); return ( {toast.text} {toast.action ? ( ) : null} ); })} ); }