46 lines
1.4 KiB
TypeScript
46 lines
1.4 KiB
TypeScript
import { useEffect, useState, type ReactNode } from "react";
|
|
import { CheckCircle2, XCircle } from "lucide-react";
|
|
|
|
type Toast = { id: number; kind: "ok" | "err"; text: ReactNode };
|
|
|
|
const items: Toast[] = [];
|
|
const subs = new Set<(t: Toast[]) => void>();
|
|
let nextId = 1;
|
|
|
|
export function toast(kind: Toast["kind"], text: ReactNode): void {
|
|
const t: Toast = { id: nextId++, kind, text };
|
|
items.push(t);
|
|
subs.forEach((f) => f([...items]));
|
|
setTimeout(() => {
|
|
const i = items.findIndex((x) => x.id === t.id);
|
|
if (i >= 0) items.splice(i, 1);
|
|
subs.forEach((f) => f([...items]));
|
|
}, 4000);
|
|
}
|
|
|
|
export function Toaster() {
|
|
const [list, setList] = useState<Toast[]>([]);
|
|
useEffect(() => {
|
|
subs.add(setList);
|
|
return () => {
|
|
subs.delete(setList);
|
|
};
|
|
}, []);
|
|
return (
|
|
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
|
{list.map((t) => (
|
|
<div
|
|
key={t.id}
|
|
role="status"
|
|
className={`fx-rise flex max-w-sm items-center gap-2 rounded-lg border bg-card px-4 py-2.5 text-sm text-card-foreground shadow-lg ${
|
|
t.kind === "ok" ? "border-border" : "border-destructive/40"
|
|
}`}
|
|
>
|
|
{t.kind === "ok" ? <CheckCircle2 className="size-4 shrink-0 text-accent" /> : <XCircle className="size-4 shrink-0 text-destructive" />}
|
|
{t.text}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|