Files
Cupcake/app/components/ui/notice/index.tsx
T

99 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Hooks
import { useEffect, useState } from "react";
// UI
import styles from "./Notice.module.css";
import { createPortal } from "react-dom";
// Tools
import { type NoticeItem, addFn, removeFn } from "./utils";
import { useHydrated } from "~/hooks/use-hydrated";
import { clsn } from "~/utils";
// Components
function Notice() {
const [items, setItems] = useState<NoticeItem[]>([]);
useEffect(() => {
const fn = (notice: NoticeItem) => {
const key = `${Math.ceil(performance.now())}-${Math.round(Math.random() * 100)}`;
setItems((prevItems) => [...prevItems, { ...notice, key }]);
if (notice.duration && notice.duration > 0) {
setTimeout(() => {
onCloseWithKey(key);
}, notice.duration);
}
};
addFn(fn);
return () => {
removeFn(fn);
};
}, []);
const onCloseWithKey = (key: string) => {
setItems((prevItems) => {
const index = prevItems.findIndex((item) => item.key === key);
const nextItems = [...prevItems];
if (index > -1) {
nextItems.splice(index, 1);
}
return nextItems;
});
}
const hydrated = useHydrated();
if (typeof document === "undefined") {
return null;
}
if (!hydrated) {
return null;
}
return createPortal((
<div className="fixed top-24 right-0 z-2 mx-4 flex max-w-[30em] flex-col items-end">
{items.map((item) => {
const showProgress = !!item.duration && item.duration > 0;
const theme = {
default: { bg: "bg-pink-100", text: "text-pink-700", bar: "bg-pink-500" },
success: { bg: "bg-cyan-100", text: "text-cyan-700", bar: "bg-cyan-500" },
error: { bg: "bg-pink-100", text: "text-pink-700", bar: "bg-pink-500" },
warning: { bg: "bg-yellow-100", text: "text-yellow-700", bar: "bg-yellow-500" },
}[item.type || "default"];
return (
<div key={item.key} className={clsn("relative overflow-hidden px-5 py-4 pr-14 mb-4 min-w-[20em] rounded-xl animate-fade-in-left", theme.bg, theme.text)}>
<button
className={clsn("absolute text-xl font-semibold right-6 cursor-pointer", theme.text)}
onClick={() => onCloseWithKey(item.key as string)}
>
×
</button>
<h4 className={clsn("text-lg font-semibold", theme.text)}>{item.title}</h4>
<p className="text-sm mt-4">
{item.content}
</p>
{showProgress && (
<div className={clsn("absolute h-1 left-0 right-0 bottom-0", styles.progress, theme.bar)} style={{ animationDuration: `${(item.duration || 5000) / 1000}s` }}></div>
)}
</div>
);
})}
</div>
), document.body);
}
export default Notice;