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:
2026-03-24 15:54:22 +03:00
parent 669c4a3023
commit aa0cfe35c3
7 changed files with 315 additions and 75 deletions

View 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>
);
}