You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
111 lines
3.7 KiB
111 lines
3.7 KiB
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { create } from "zustand";
|
|
|
|
interface ToastState {
|
|
message: string | null;
|
|
type: "info" | "success" | "warning" | "error";
|
|
persistent: boolean;
|
|
details: string | null;
|
|
show: (message: string, type?: "info" | "success" | "warning" | "error", persistent?: boolean, details?: string | null) => void;
|
|
hide: () => void;
|
|
}
|
|
|
|
export const useToast = create<ToastState>((set) => ({
|
|
message: null,
|
|
type: "info",
|
|
persistent: false,
|
|
details: null,
|
|
show: (message, type = "info", persistent = false, details = null) => set({ message, type, persistent, details }),
|
|
hide: () => set({ message: null, persistent: false, details: null }),
|
|
}));
|
|
|
|
const typeStyles = {
|
|
info: "bg-neutral-800 border-neutral-600 text-neutral-100",
|
|
success: "bg-green-900 border-green-700 text-green-100",
|
|
warning: "bg-orange-900 border-orange-600 text-orange-100",
|
|
error: "bg-red-900 border-red-700 text-red-100",
|
|
};
|
|
|
|
const typeIcons = {
|
|
info: (
|
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
),
|
|
success: (
|
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
),
|
|
warning: (
|
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
|
|
</svg>
|
|
),
|
|
error: (
|
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
),
|
|
};
|
|
|
|
export function Toast() {
|
|
const { message, type, persistent, details, hide } = useToast();
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Reset expanded state when toast changes
|
|
setIsExpanded(false);
|
|
}, [message]);
|
|
|
|
useEffect(() => {
|
|
if (message && !persistent) {
|
|
const timer = setTimeout(() => {
|
|
hide();
|
|
}, 4000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [message, persistent, hide]);
|
|
|
|
if (!message) return null;
|
|
|
|
return (
|
|
<div className="fixed top-6 right-6 z-[200] animate-in fade-in slide-in-from-top-4 duration-300 max-w-md">
|
|
<div
|
|
className={`flex flex-col rounded-lg border shadow-xl ${typeStyles[type]}`}
|
|
>
|
|
<div className="flex items-center gap-3 px-4 py-3">
|
|
{typeIcons[type]}
|
|
<span className="text-sm font-medium flex-1">{message}</span>
|
|
<button
|
|
onClick={hide}
|
|
className="p-1 rounded hover:bg-white/10 transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
{details && (
|
|
<>
|
|
<button
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
className="px-4 py-1 text-xs opacity-70 hover:opacity-100 transition-opacity text-left border-t border-white/10"
|
|
>
|
|
{isExpanded ? "Hide details" : "Show details"}
|
|
</button>
|
|
{isExpanded && (
|
|
<div className="px-4 pb-3">
|
|
<pre className="bg-black/30 rounded p-2 max-h-40 overflow-auto text-xs font-mono whitespace-pre-wrap break-words">
|
|
{details}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|