81 أسطر
2.6 KiB
TypeScript
81 أسطر
2.6 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import { X } from "lucide-react";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type ToastVariant = "default" | "success" | "warning" | "danger";
|
|
|
|
type ToastItem = {
|
|
id: string;
|
|
title: string;
|
|
description?: string;
|
|
variant?: ToastVariant;
|
|
};
|
|
|
|
type ToastContextValue = {
|
|
toast: (item: Omit<ToastItem, "id">) => void;
|
|
};
|
|
|
|
const ToastContext = React.createContext<ToastContextValue | null>(null);
|
|
|
|
function getToastId() {
|
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
return crypto.randomUUID();
|
|
}
|
|
return `toast_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
}
|
|
|
|
const variantStyles: Record<ToastVariant, string> = {
|
|
default: "border-border bg-card text-foreground",
|
|
success: "border-emerald-500/40 bg-emerald-500/10 text-emerald-100",
|
|
warning: "border-amber-500/40 bg-amber-500/10 text-amber-100",
|
|
danger: "border-rose-500/40 bg-rose-500/10 text-rose-100",
|
|
};
|
|
|
|
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|
const [toasts, setToasts] = React.useState<ToastItem[]>([]);
|
|
|
|
const toast = React.useCallback((item: Omit<ToastItem, "id">) => {
|
|
const id = getToastId();
|
|
setToasts((prev) => [...prev, { id, ...item }]);
|
|
window.setTimeout(() => {
|
|
setToasts((prev) => prev.filter((toastItem) => toastItem.id !== id));
|
|
}, 3200);
|
|
}, []);
|
|
|
|
const remove = React.useCallback((id: string) => {
|
|
setToasts((prev) => prev.filter((toastItem) => toastItem.id !== id));
|
|
}, []);
|
|
|
|
return (
|
|
<ToastContext.Provider value={{ toast }}>
|
|
{children}
|
|
<div className="fixed bottom-6 left-6 z-50 flex w-[90vw] max-w-sm flex-col gap-3">
|
|
{toasts.map((item) => (
|
|
<div key={item.id} className={cn("frame-panel border px-4 py-3 shadow-glow", variantStyles[item.variant ?? "default"])}>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<p className="text-sm font-semibold">{item.title}</p>
|
|
{item.description ? <p className="mt-1 text-xs text-muted-foreground">{item.description}</p> : null}
|
|
</div>
|
|
<button className="rounded-md p-1 text-muted-foreground hover:text-foreground" onClick={() => remove(item.id)}>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ToastContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useToast() {
|
|
const context = React.useContext(ToastContext);
|
|
if (!context) {
|
|
throw new Error("useToast must be used within ToastProvider");
|
|
}
|
|
return context;
|
|
}
|