fix: comprehensive bookings admin UX improvements
- #1 Delete confirmation dialog before removing bookings - #2 Error toasts instead of silent .catch(() => {}) - #3 Optimistic rollback — UI reverts on API failure - #4 Loading indicator on reminder status buttons - #5 Search results are now actionable (status change + delete) - #6 New bookings banner instead of full tab remount - #7 Error states for failed data loads - #8 InlineNotes only saves on blur when value changed - #9 AddBookingModal supports Instagram/Telegram fields - #10 Polling pauses when browser tab is hidden - #11 Enter key submits ConfirmModal
This commit is contained in:
68
src/app/admin/bookings/Toast.tsx
Normal file
68
src/app/admin/bookings/Toast.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, createContext, useContext } from "react";
|
||||
import { X, AlertCircle, CheckCircle2 } from "lucide-react";
|
||||
|
||||
interface ToastItem {
|
||||
id: number;
|
||||
message: string;
|
||||
type: "error" | "success";
|
||||
}
|
||||
|
||||
interface ToastContextValue {
|
||||
showError: (message: string) => void;
|
||||
showSuccess: (message: string) => void;
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastContextValue>({
|
||||
showError: () => {},
|
||||
showSuccess: () => {},
|
||||
});
|
||||
|
||||
export function useToast() {
|
||||
return useContext(ToastContext);
|
||||
}
|
||||
|
||||
let nextId = 0;
|
||||
|
||||
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||
const [toasts, setToasts] = useState<ToastItem[]>([]);
|
||||
|
||||
const addToast = useCallback((message: string, type: "error" | "success") => {
|
||||
const id = ++nextId;
|
||||
setToasts((prev) => [...prev, { id, message, type }]);
|
||||
setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== id)), 4000);
|
||||
}, []);
|
||||
|
||||
const showError = useCallback((message: string) => addToast(message, "error"), [addToast]);
|
||||
const showSuccess = useCallback((message: string) => addToast(message, "success"), [addToast]);
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ showError, showSuccess }}>
|
||||
{children}
|
||||
{toasts.length > 0 && (
|
||||
<div className="fixed bottom-4 right-4 z-[60] flex flex-col gap-2 max-w-sm">
|
||||
{toasts.map((t) => (
|
||||
<div
|
||||
key={t.id}
|
||||
className={`flex items-center gap-2 rounded-lg border px-3 py-2.5 text-sm shadow-lg animate-in slide-in-from-right ${
|
||||
t.type === "error"
|
||||
? "bg-red-950/90 border-red-500/30 text-red-200"
|
||||
: "bg-emerald-950/90 border-emerald-500/30 text-emerald-200"
|
||||
}`}
|
||||
>
|
||||
{t.type === "error" ? <AlertCircle size={14} className="shrink-0" /> : <CheckCircle2 size={14} className="shrink-0" />}
|
||||
<span className="flex-1">{t.message}</span>
|
||||
<button
|
||||
onClick={() => setToasts((prev) => prev.filter((tt) => tt.id !== t.id))}
|
||||
className="shrink-0 text-neutral-400 hover:text-white"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user