new-personal-site/components/lib/elements/Toast.tsx
Benjamin Toby 6d833c7d3b Updates
2025-07-25 19:21:17 +01:00

108 lines
2.9 KiB
TypeScript

import React, { DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge";
import Card from "./Card";
import { X } from "lucide-react";
import ReactDOM from "react-dom";
import Span from "../layout/Span";
export const ToastStyles = ["normal", "success", "error"] as const;
export const ToastColors = ToastStyles;
export type TWUIToastProps = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
open?: boolean;
setOpen?: React.Dispatch<React.SetStateAction<boolean>>;
closeDelay?: number;
color?: (typeof ToastStyles)[number];
};
let interval: any;
let timeout: any;
/**
* # Toast Component
* @className twui-toast-root
* @className twui-toast
* @className twui-toast-success
* @className twui-toast-error
*/
export default function Toast({
open,
setOpen,
closeDelay = 4000,
color,
...props
}: TWUIToastProps) {
const [ready, setReady] = React.useState(false);
const IDName = "twui-toast-root";
React.useEffect(() => {
const toastRoot = document.getElementById(IDName);
if (toastRoot) {
setReady(true);
} else {
const newToastRootEl = document.createElement("div");
newToastRootEl.id = IDName;
document.body.appendChild(newToastRootEl);
setReady(true);
}
}, []);
React.useEffect(() => {
if (!ready || !open) return;
timeout = setTimeout(() => {
setOpen?.(false);
}, closeDelay);
return function () {
setOpen?.(false);
};
}, [ready, open]);
if (!ready) return null;
if (!open) return null;
return ReactDOM.createPortal(
<Card
{...props}
className={twMerge(
"fixed bottom-4 right-4 z-[250] border-none",
"pl-6 pr-8 py-4 bg-primary dark:bg-primary-dark",
color == "success"
? "bg-success dark:bg-success-dark twui-toast-success"
: color == "error"
? "bg-error dark:bg-error-dark twui-toast-error"
: "",
props.className,
"twui-toast"
)}
onMouseEnter={() => {
window.clearTimeout(timeout);
}}
onMouseLeave={(e) => {
timeout = setTimeout(() => {
setOpen?.(false);
}, closeDelay);
}}
>
<Span
className={twMerge(
"absolute top-2 right-2 z-[100] cursor-pointer",
"text-white"
)}
onClick={(e) => {
setOpen?.(false);
}}
>
<X size={15} />
</Span>
<Span className={twMerge("text-white")}>{props.children}</Span>
</Card>,
document.getElementById(IDName) as HTMLElement
);
}