constants
import type { NotifyPosition, NotifyVariant } from "./types";
export const notifyPositionClasses: Record<NotifyPosition, string> = {
"top-left": "top-4 left-4",
"top-center": "top-4 left-1/2 -translate-x-1/2",
"top-right": "top-4 right-4",
"bottom-left": "bottom-4 left-4",
"bottom-center": "bottom-4 left-1/2 -translate-x-1/2",
"bottom-right": "bottom-4 right-4",
};
export const notifyVariantClasses: Record<NotifyVariant, string> = {
default: "border-neutral-200 bg-white text-black",
success: "border-green-200 bg-green-50 text-green-950",
error: "border-red-200 bg-red-50 text-red-950",
warning: "border-yellow-200 bg-yellow-50 text-yellow-950",
info: "border-blue-200 bg-blue-50 text-blue-950",
};
INDEX
export { NotifyContainer } from "./notify-container";
export { notify, dismiss } from "./store";
card
import { dismiss } from "./store";
import type { NotifyItem } from "./types";
import { notifyVariantClasses } from "./constants";
import { cn } from "@/src/utils";
interface NotifyCardProps {
item: NotifyItem;
}
export function NotifyCard({ item }: NotifyCardProps) {
return (
<div
className={cn(
"shadow-xl p-4 border rounded-xl min-w-[320px]",
notifyVariantClasses[item?.variant || "default"],
)}
>
<div className="flex justify-between items-start gap-4">
<div>
{item.title && <h3 className="font-semibold">{item.title}</h3>}
{item.description && (
<p className="text-neutral-500 text-sm">{item.description}</p>
)}
</div>
<button onClick={() => dismiss()}>✕</button>
</div>
</div>
);
}
container
import { useEffect, useState } from "react";
import type { NotifyItem } from "./types";
import { subscribe } from "./store";
import { notifyPositionClasses } from "./constants";
import { cn } from "@/src/utils";
import { NotifyCard } from "./notify-card";
import { Potral } from "../overlay";
export function NotifyContainer() {
const [item, setItem] = useState<NotifyItem | null>(null);
const [visible, setVisible] = useState(false);
const [renderItem, setRenderItem] = useState<NotifyItem | null>(null);
useEffect(() => {
return subscribe(setItem);
}, []);
useEffect(() => {
if (item) {
setRenderItem(item);
requestAnimationFrame(() => {
setVisible(true);
});
} else {
setVisible(false);
const timeout = setTimeout(() => {
setRenderItem(null);
}, 300);
return () => clearTimeout(timeout);
}
}, [item]);
if (!renderItem) return null;
return (
<Potral>
<div
key={renderItem.id}
className={cn(
"z-[9999] fixed transition-all duration-300",
visible ? "translate-y-0 opacity-100" : "-translate-y-2 opacity-0",
notifyPositionClasses[renderItem.position || "top-center"],
)}
>
<NotifyCard item={renderItem} />
</div>
</Potral>
);
}
Store
import type { NotifyItem } from "./types";
type Listener = (item: NotifyItem | null) => void;
let memoryState: NotifyItem | null = null;
let listener: Listener | null = null;
function dispatch() {
listener?.(memoryState);
}
export function subscribe(l: Listener) {
listener = l;
listener(memoryState);
return () => {
listener = null;
};
}
export function notify(item: Omit<NotifyItem, "id">) {
const newItem: NotifyItem = {
id: crypto.randomUUID(),
...item,
};
memoryState = newItem;
dispatch();
return newItem.id;
}
export function dismiss() {
memoryState = null;
dispatch();
}
types
export type NotifyPosition =
| "top-left"
| "top-center"
| "top-right"
| "bottom-left"
| "bottom-center"
| "bottom-right";
export type NotifyVariant =
| "default"
| "success"
| "error"
| "info"
| "warning";
export interface NotifyItem {
id: string;
title?: string;
description?: string;
position?: NotifyPosition;
variant?: NotifyVariant;
}
calling
import { RouterProvider } from "react-router-dom";
import { router } from "./router";
import { NotifyContainer, ToastContainer } from "./components/base";
function App() {
return (
<>
<RouterProvider router={router} />
<ToastContainer />
<NotifyContainer />
</>
);
}
export default App;
Top comments (0)