feat: admin UX — shared input classes, autocomplete role, auto-save team, video improvements
- Extract base input classes (baseInput, textAreaInput, smallInput, dashedInput) with gold hover - Move AutocompleteMulti to shared FormField, support · separator - Team editor: auto-save with toast, split name into first/last, autocomplete role from class styles - Team photo: click-to-upload overlay, smaller 130px thumbnail - Hero videos: play on hover, file size display, 8MB warning, total size performance table - Remove ctaHref field from Hero admin (unused on frontend) - Move Toast to shared _components for reuse across admin pages
This commit is contained in:
@@ -1,68 +1,2 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
// Re-export from shared location
|
||||
export { ToastProvider, useToast } from "../_components/Toast";
|
||||
|
||||
Reference in New Issue
Block a user