diff --git a/www/app/[roomName]/page.tsx b/www/app/[roomName]/page.tsx index bf4c3e24..b03a7e4f 100644 --- a/www/app/[roomName]/page.tsx +++ b/www/app/[roomName]/page.tsx @@ -118,7 +118,7 @@ const useConsentDialog = ( return ( + diff --git a/www/app/components/ui/toaster.tsx b/www/app/components/ui/toaster.tsx index 1e4a47df..20ab1f7b 100644 --- a/www/app/components/ui/toaster.tsx +++ b/www/app/components/ui/toaster.tsx @@ -1,7 +1,14 @@ "use client"; -// Simple toaster implementation for migration -// This is a temporary solution until we properly configure Chakra UI v3 toasts +import { + createContext, + useContext, + useState, + useEffect, + useCallback, +} from "react"; +import { createPortal } from "react-dom"; +import { Box } from "@chakra-ui/react"; interface ToastOptions { placement?: string; @@ -9,41 +16,162 @@ interface ToastOptions { render: (props: { dismiss: () => void }) => React.ReactNode; } +interface Toast extends ToastOptions { + id: string; +} + +interface ToasterContextType { + toasts: Toast[]; + addToast: (options: ToastOptions) => string; + removeToast: (id: string) => void; +} + +const ToasterContext = createContext(null); + +export const ToasterProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [toasts, setToasts] = useState([]); + + const addToast = useCallback((options: ToastOptions) => { + const id = String(Date.now() + Math.random()); + setToasts((prev) => [...prev, { ...options, id }]); + + if (options.duration !== null) { + setTimeout(() => { + removeToast(id); + }, options.duration || 5000); + } + + return id; + }, []); + + const removeToast = useCallback((id: string) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }, []); + + return ( + + {children} + + + ); +}; + +const ToastContainer = () => { + const context = useContext(ToasterContext); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!context || !mounted) return null; + + return createPortal( + + {context.toasts.map((toast) => ( + + {toast.render({ dismiss: () => context.removeToast(toast.id) })} + + ))} + , + document.body, + ); +}; + class ToasterClass { - private toasts: Map = new Map(); + private listeners: ((action: { type: string; payload: any }) => void)[] = []; private nextId = 1; + private toastsMap: Map = new Map(); + + subscribe(listener: (action: { type: string; payload: any }) => void) { + this.listeners.push(listener); + return () => { + this.listeners = this.listeners.filter((l) => l !== listener); + }; + } + + private notify(action: { type: string; payload: any }) { + this.listeners.forEach((listener) => listener(action)); + } create(options: ToastOptions): Promise { const id = String(this.nextId++); - this.toasts.set(id, options); + this.toastsMap.set(id, true); + this.notify({ type: "ADD_TOAST", payload: { ...options, id } }); - // For now, we'll render toasts using a portal or modal - // This is a simplified implementation - if (typeof window !== "undefined") { - console.log("Toast created:", id, options); - - // Auto-dismiss after duration if specified - if (options.duration !== null) { - setTimeout(() => { - this.dismiss(id); - }, options.duration || 5000); - } + if (options.duration !== null) { + setTimeout(() => { + this.dismiss(id); + }, options.duration || 5000); } return Promise.resolve(id); } dismiss(id: string) { - this.toasts.delete(id); - console.log("Toast dismissed:", id); + this.toastsMap.delete(id); + this.notify({ type: "REMOVE_TOAST", payload: id }); } isActive(id: string): boolean { - return this.toasts.has(id); + return this.toastsMap.has(id); } } export const toaster = new ToasterClass(); -// Empty Toaster component for now -export const Toaster = () => null; +// Bridge component to connect the class-based API with React +export const Toaster = () => { + const [toasts, setToasts] = useState([]); + + useEffect(() => { + const unsubscribe = toaster.subscribe((action) => { + if (action.type === "ADD_TOAST") { + setToasts((prev) => [...prev, action.payload]); + } else if (action.type === "REMOVE_TOAST") { + setToasts((prev) => + prev.filter((toast) => toast.id !== action.payload), + ); + } + }); + + return unsubscribe; + }, []); + + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) return null; + + return createPortal( + + {toasts.map((toast) => ( + + {toast.render({ dismiss: () => toaster.dismiss(toast.id) })} + + ))} + , + document.body, + ); +}; diff --git a/www/app/providers.tsx b/www/app/providers.tsx index 08242122..dbab9d29 100644 --- a/www/app/providers.tsx +++ b/www/app/providers.tsx @@ -4,11 +4,15 @@ import { ChakraProvider } from "@chakra-ui/react"; import system from "./styles/theme"; import { WherebyProvider } from "@whereby.com/browser-sdk/react"; +import { Toaster } from "./components/ui/toaster"; export function Providers({ children }: { children: React.ReactNode }) { return ( - {children} + + {children} + + ); }