import "./Toaster.css"; import { ReactNode, useSyncExternalStore } from "react"; import { createPortal } from "react-dom"; import { v4 as uuid } from "uuid"; import { ExternalStore, unixNow } from "@snort/shared"; import Icon from "@/Icons/Icon"; interface ToastNotification { element: ReactNode | ((remove: () => void) => ReactNode); expire?: number; icon?: string; id?: string; } class ToasterSlots extends ExternalStore> { #stack: Array = []; #cleanup = setInterval(() => this.#eatToast(), 1000); push(n: ToastNotification) { n.expire ??= unixNow() + 10; n.id ??= uuid(); this.#stack.push(n); this.notifyChange(); } takeSnapshot(): ToastNotification[] { return [...this.#stack]; } remove(id?: string) { this.#stack = this.#stack.filter(a => a.id !== id); this.notifyChange(); } #eatToast() { const now = unixNow(); this.#stack = this.#stack.filter(a => (a.expire ?? 0) > now); this.notifyChange(); } } export const Toastore = new ToasterSlots(); export default function Toaster() { const toast = useSyncExternalStore( c => Toastore.hook(c), () => Toastore.snapshot(), ); return createPortal(
{toast.map(a => (
{a.icon && } {typeof a.element === "function" ? a.element(() => Toastore.remove(a.id)) : a.element}
))}
, document.body, ); }