The danger zone card, with Hold to Delete
app/settings/danger-zone.tsx
"use client";
import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import HoldToDelete from "@/components/HoldToDelete";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { TypedDeleteDialog } from "@/components/typed-delete-dialog";
import { deleteWorkspace } from "./actions";
type Workspace = { id: string; name: string; members: number; created: string };
// Hold to Delete draws on a 1000 x 660 stage; scale it to the card's width
function useStageScale() {
const ref = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(0);
useEffect(() => {
const el = ref.current;
if (!el) return;
const ro = new ResizeObserver(([entry]) => setScale(entry.contentRect.width / 1000));
ro.observe(el);
return () => ro.disconnect();
}, []);
return [ref, scale] as const;
}
export function DangerZone({ workspace }: { workspace: Workspace }) {
const router = useRouter();
const [stageRef, scale] = useStageScale();
const [error, setError] = useState<string | null>(null);
// runs once: after the hold and the 5 s undo window, or after the typed dialog
const remove = async () => {
const res = await deleteWorkspace(workspace.id);
if (res.error) setError(res.error);
else router.push("/");
};
return (
<Card className="border-destructive/50">
<CardHeader>
<CardTitle className="text-destructive">Danger zone</CardTitle>
<CardDescription>
Deleting {workspace.name} removes its projects, files and members. You get 5 seconds to undo.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div ref={stageRef} className="relative hidden aspect-[1000/660] w-full overflow-hidden rounded-xl sm:block">
{scale > 0 && (
<HoldToDelete
fileName={workspace.name}
fileMeta={`${workspace.members} members · since ${workspace.created}`}
fileType="WS"
tags={[]}
holdLabel="Hold to delete workspace"
toastText={`${workspace.name} deleted`}
restoreLabel={null}
onCommit={remove}
style={{ position: "absolute", left: 0, top: 0, transform: `scale(${scale})`, transformOrigin: "0 0" }}
/>
)}
</div>
{/* phones, voice control and switch access get the typed dialog */}
<TypedDeleteDialog name={workspace.name} onConfirm={remove} />
{error && <p role="alert" className="text-sm text-destructive">{error}</p>}
</CardContent>
</Card>
);
}
app/settings/actions.ts
"use server";
import { auth } from "@/lib/auth"; // your session helper
import { db } from "@/lib/db"; // your database client
export async function deleteWorkspace(id: string): Promise<{ error?: string }> {
const session = await auth();
if (!session) return { error: "Sign in first." };
// only the owner can delete; never trust an id from the browser on its own
const ws = await db.workspace.findFirst({ where: { id, ownerId: session.user.id } });
if (!ws) return { error: "Only the workspace owner can delete it." };
await db.workspace.delete({ where: { id } });
return {};
}