Files
Epicnabbo-Catalogus-Updated…/src/features/history/history-restore.tsx
T

159 lines
4.2 KiB
TypeScript

"use client";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useRef, useState } from "react";
import { previewHistoryRestore, restoreHistory } from "@/actions/history";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from "@/components/ui/dialog";
export function HistoryRestore({ id }: { id: number }) {
const t = useTranslations("pages.admin.logs.history");
const router = useRouter();
const [open, setOpen] = useState(false);
const pending = useRef<"preview" | "restore" | null>(null);
const [busy, setBusy] = useState<"preview" | "restore" | null>(null);
const [error, setError] = useState("");
const [preview, setPreview] = useState<Awaited<
ReturnType<typeof previewHistoryRestore>
> | null>(null);
async function load() {
if (pending.current) return;
pending.current = "preview";
setOpen(true);
setBusy("preview");
setError("");
setPreview(null);
try {
setPreview(await previewHistoryRestore(id));
} catch {
setError(t("unavailable"));
} finally {
pending.current = null;
setBusy(null);
}
}
async function restore() {
if (pending.current || !preview?.canRestore || !preview.changes.length)
return;
pending.current = "restore";
setBusy("restore");
setError("");
try {
const result = await restoreHistory(id);
if (!result.ok) {
setError(t(result.code));
setPreview(null);
return;
}
setOpen(false);
router.refresh();
} catch {
setError(t("failed"));
} finally {
pending.current = null;
setBusy(null);
}
}
return (
<>
<Button
size="sm"
variant="outline"
onClick={load}
disabled={busy !== null}
>
{t("preview")}
</Button>
<Dialog
open={open}
onOpenChange={(nextOpen, details) => {
if (!nextOpen && pending.current === "restore") {
details.cancel();
return;
}
setOpen(nextOpen);
}}
>
<DialogContent
className="w-[calc(100%-2rem)] min-w-0 overflow-x-hidden sm:max-w-3xl"
showCloseButton={busy !== "restore"}
>
<DialogTitle className="pr-6 leading-snug">{t("title")}</DialogTitle>
<DialogDescription>{t("description")}</DialogDescription>
{busy && (
<p role="status">
{t(busy === "restore" ? "restoring" : "loading")}
</p>
)}
{error && (
<p role="alert" className="text-destructive">
{error}
</p>
)}
{preview && (
<>
{!preview.canRestore && (
<p role="alert" className="text-destructive">
{t("conflict")}
</p>
)}
<div className="max-h-[50dvh] min-w-0 space-y-4 overflow-y-auto pr-1">
{preview.changes.map((change) => (
<section
key={change.key}
className="min-w-0 rounded-lg border p-3"
>
<h3 className="mb-2 text-sm font-semibold">
{t.has(`fields.${change.key}`)
? t(`fields.${change.key}`)
: t("field")}
</h3>
<dl className="grid min-w-0 gap-3 sm:grid-cols-2">
<div className="min-w-0">
<dt className="mb-1 text-xs text-muted-foreground">
{t("current")}
</dt>
<dd className="min-w-0">
<pre className="max-h-52 overflow-y-auto whitespace-pre-wrap [overflow-wrap:anywhere] rounded-md bg-muted/40 p-2 text-xs">
{String(change.current ?? "—")}
</pre>
</dd>
</div>
<div className="min-w-0">
<dt className="mb-1 text-xs text-muted-foreground">
{t("restore")}
</dt>
<dd className="min-w-0">
<pre className="max-h-52 overflow-y-auto whitespace-pre-wrap [overflow-wrap:anywhere] rounded-md bg-muted/40 p-2 text-xs">
{String(change.restore ?? "—")}
</pre>
</dd>
</div>
</dl>
</section>
))}
</div>
<Button
disabled={
busy !== null ||
!preview.canRestore ||
!preview.changes.length
}
onClick={restore}
>
{t("confirm")}
</Button>
</>
)}
</DialogContent>
</Dialog>
</>
);
}