Files
snort/packages/app/src/Components/Toaster/Toaster.tsx
2024-01-04 19:01:18 +02:00

64 lines
1.5 KiB
TypeScript

import "./Toaster.css";
import { ExternalStore, unixNow } from "@snort/shared";
import { ReactNode, useSyncExternalStore } from "react";
import { createPortal } from "react-dom";
import { v4 as uuid } from "uuid";
import Icon from "@/Components/Icons/Icon";
interface ToastNotification {
element: ReactNode | ((remove: () => void) => ReactNode);
expire?: number;
icon?: string;
id?: string;
}
class ToasterSlots extends ExternalStore<Array<ToastNotification>> {
#stack: Array<ToastNotification> = [];
#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(
<div className="toaster">
{toast.map(a => (
<div className="p br b flex bg-dark g8 fade-in" key={a.id}>
{a.icon && <Icon name={a.icon} />}
{typeof a.element === "function" ? a.element(() => Toastore.remove(a.id)) : a.element}
</div>
))}
</div>,
document.body,
);
}