112 lines
2.4 KiB
TypeScript
112 lines
2.4 KiB
TypeScript
"use client";
|
|
|
|
import { Portal } from "@/lib/portal/components";
|
|
import { atom, useAtom } from "jotai";
|
|
import React, { useCallback, useEffect, useState } from "react";
|
|
import { ReactNode } from "react";
|
|
|
|
type toastMessage = {
|
|
msg: ReactNode;
|
|
type?: "error" | "default";
|
|
fading: boolean;
|
|
duration?: number;
|
|
};
|
|
|
|
type IDToastMessage = toastMessage & {
|
|
id: string;
|
|
};
|
|
|
|
const toastAtom = atom<IDToastMessage[]>([]);
|
|
|
|
export function useToast() {
|
|
const [_, setToasts] = useAtom(toastAtom);
|
|
|
|
const createToast = useCallback(
|
|
(t: toastMessage) => {
|
|
const idd = { ...t, id: crypto.randomUUID() };
|
|
setToasts((toasts) => {
|
|
return [...toasts, idd];
|
|
});
|
|
|
|
return idd;
|
|
},
|
|
[setToasts]
|
|
);
|
|
|
|
const clearToast = useCallback(
|
|
(t: toastMessage) => setToasts((toasts) => toasts.filter((to) => to != t)),
|
|
[setToasts]
|
|
);
|
|
|
|
return {
|
|
createToast,
|
|
clearToast,
|
|
};
|
|
}
|
|
|
|
export function Toaster() {
|
|
const [toasts, setToasts] = useAtom(toastAtom);
|
|
|
|
const clearToast = useCallback(
|
|
(t: toastMessage) => {
|
|
setToasts((toasts) => {
|
|
return toasts.filter((to) => to !== t);
|
|
});
|
|
},
|
|
[setToasts]
|
|
);
|
|
|
|
if (!toasts.length) return <></>;
|
|
return (
|
|
<Portal>
|
|
<div className="fixed bottom-12 left-1/2 -translate-x-1/2 max-w-[95vw] flex flex-col gap-4">
|
|
{toasts.map((t) => (
|
|
<Toast key={"toast " + t.id} toast={t} clearToast={clearToast} />
|
|
))}
|
|
</div>
|
|
</Portal>
|
|
);
|
|
}
|
|
|
|
function Toast(props: {
|
|
toast: toastMessage;
|
|
clearToast: (t: toastMessage) => void;
|
|
}) {
|
|
const { toast, clearToast } = props;
|
|
const [fading, setFading] = useState(false);
|
|
|
|
const clear = useCallback(() => {
|
|
setFading(true);
|
|
setTimeout(() => {
|
|
clearToast(toast);
|
|
}, 300);
|
|
}, [clearToast, toast]);
|
|
|
|
const fadeOut = useCallback(() => {
|
|
setTimeout(clear, toast.duration ?? 3000);
|
|
}, [clear, toast]);
|
|
|
|
useEffect(() => {
|
|
if (!toast.fading) return;
|
|
fadeOut();
|
|
}, [fadeOut, toast]);
|
|
|
|
return (
|
|
<div
|
|
data-fading={fading}
|
|
data-type={toast.type}
|
|
className="relative p-6 px-16 toast data-[fading=true]:fade-toast rounded-md bg-mixed-300 data-[type=error]:bg-red-900 border-2 border-mixed-400 data-[type=error]:border-red-700"
|
|
>
|
|
{toast.msg}
|
|
{!toast.fading && (
|
|
<button
|
|
className="top-2 right-2 text-xs absolute"
|
|
onClick={() => clear()}
|
|
>
|
|
Dismiss
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|