Initial commit

This commit is contained in:
admin
2026-05-26 22:46:00 +02:00
commit 7e2c2ff89c
256 changed files with 51523 additions and 0 deletions
+120
View File
@@ -0,0 +1,120 @@
"use client";
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { Undo2, X } from "lucide-react";
import { restoreImages } from "@/app/actions/trash";
interface ToastState {
ids: number[];
visibleAt: number;
failed?: { message: string };
}
interface Ctx {
show: (ids: number[]) => void;
}
const ToastCtx = createContext<Ctx | null>(null);
const VISIBLE_MS = 8000;
export function UndoDeleteToastProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<ToastState | null>(null);
const timerRef = useRef<number | null>(null);
const router = useRouter();
const [pending, start] = useTransition();
const dismiss = useCallback(() => {
setState(null);
if (timerRef.current != null) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}, []);
const show = useCallback((ids: number[]) => {
if (ids.length === 0) return;
setState({ ids, visibleAt: Date.now() });
if (timerRef.current != null) clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => setState(null), VISIBLE_MS);
}, []);
useEffect(() => () => { if (timerRef.current != null) clearTimeout(timerRef.current); }, []);
const undo = () => {
if (!state) return;
const ids = state.ids;
// Hide the trash-confirmation copy while the restore is in flight,
// but DON'T dismiss the toast — if restoreImages throws, the items
// are still in trash and the user has no signal. Re-surface in the
// catch with a retry affordance.
if (timerRef.current != null) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
start(async () => {
try {
await restoreImages(ids);
router.refresh();
setState(null);
} catch (e) {
setState({
ids,
visibleAt: Date.now(),
failed: { message: (e as Error).message || "Restore failed" },
});
timerRef.current = window.setTimeout(() => setState(null), VISIBLE_MS);
}
});
};
const ctx = useMemo<Ctx>(() => ({ show }), [show]);
return (
<ToastCtx.Provider value={ctx}>
{children}
{state && (
<div className="fixed bottom-[80px] left-1/2 -translate-x-1/2 z-[60]">
<div
className="rounded-2xl shadow-2xl px-4 py-2.5 flex items-center gap-3 border border-[var(--color-glass-border-strong)] backdrop-blur-2xl"
style={{ background: "color-mix(in oklch, var(--color-bg-0) 90%, transparent)" }}
>
<span className="text-sm">
{state.failed ? (
<>
<span className="text-[var(--color-red)]">Restore failed</span>
<span className="text-[var(--color-fg-dim)]"> &mdash; {state.failed.message}</span>
</>
) : (
<>
<span className="font-mono text-[var(--color-cyan)]">{state.ids.length}</span>
<span className="text-[var(--color-fg-dim)]"> moved to trash</span>
</>
)}
</span>
<div className="w-px h-5 bg-[var(--color-glass-border)]" />
<button
onClick={undo}
disabled={pending}
className="flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg bg-[var(--color-cyan)]/15 text-[var(--color-cyan)] border border-[var(--color-cyan)]/40 hover:bg-[var(--color-cyan)]/25 disabled:opacity-50"
>
<Undo2 className="w-3.5 h-3.5" /> {state.failed ? "Retry" : "Undo"}
</button>
<button
onClick={dismiss}
aria-label="Dismiss"
className="text-[var(--color-fg-muted)] hover:text-[var(--color-fg)]"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
</div>
)}
</ToastCtx.Provider>
);
}
export function useUndoDeleteToast() {
const ctx = useContext(ToastCtx);
if (!ctx) throw new Error("useUndoDeleteToast must be used within UndoDeleteToastProvider");
return ctx;
}