Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | 2x 24x 24x 24x 24x 24x 24x 24x 24x 24x 15x 3x 3x 3x 3x 3x 3x 3x 2x 2x 1x 1x 1x 24x 7x 5x 1x 1x 1x | "use client";
import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Button, Card, Input } from "@/components/ui";
interface DeleteCollectionSectionProps {
collectionId: string;
collectionName: string;
}
// Owner-only permanent delete for a whole collection. Render only for the owner
// (the purge route is owner-gated). Strong guard: the user must type the exact
// collection name, matching POST /api/collections/[id]/purge's confirm contract.
export function DeleteCollectionSection({
collectionId,
collectionName,
}: DeleteCollectionSectionProps) {
const router = useRouter();
const [confirming, setConfirming] = useState(false);
const [typed, setTyped] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const inFlight = useRef(false);
const inputRef = useRef<HTMLInputElement>(null);
const matches = typed === collectionName;
// When the confirm step appears, move focus into the name field so a keyboard
// user isn't dropped and a screen reader announces the new step.
useEffect(() => {
if (confirming) inputRef.current?.focus();
}, [confirming]);
async function purge() {
Iif (inFlight.current || !matches) return;
inFlight.current = true;
setBusy(true);
setError("");
try {
const res = await fetch(`/api/collections/${collectionId}/purge`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ confirm: typed }),
});
if (!res.ok) throw new Error("purge failed");
// The collection is gone — leave the now-dead page.
router.push("/collections");
router.refresh();
} catch {
setError("That didn’t go through — your collection is safe; try again in a moment.");
setBusy(false);
inFlight.current = false;
}
}
return (
<Card variant="danger" padding="lg" className="mt-10">
<h2 className="text-lg text-ink">Delete this collection</h2>
<p className="mt-1 text-sm text-ink-soft">
Permanently removes this collection and everything in it — items, photos, and sorts. This
can’t be undone.
</p>
{!confirming ? (
<Button variant="danger" className="mt-4" onClick={() => setConfirming(true)}>
Delete this collection
</Button>
) : (
<div role="group" aria-label="Delete this collection" className="mt-4 flex flex-col gap-3">
<label htmlFor="confirm-delete-collection" className="text-sm text-ink">
Type <span className="font-semibold">{collectionName}</span> to confirm
</label>
<Input
id="confirm-delete-collection"
ref={inputRef}
type="text"
value={typed}
onChange={(e) => setTyped(e.target.value)}
autoComplete="off"
aria-label={`Type ${collectionName} to confirm`}
/>
<div className="flex gap-2">
<Button
variant="secondary"
onClick={() => {
setConfirming(false);
setTyped("");
setError("");
}}
disabled={busy}
>
Cancel
</Button>
<Button variant="danger" onClick={purge} disabled={busy || !matches}>
{busy ? "Deleting…" : "Delete collection"}
</Button>
</div>
{error && (
<span role="status" className="text-sm text-error-deep">
{error}
</span>
)}
</div>
)}
</Card>
);
}
|