waveform/components/settings/SettingsDeleteAccountCard.tsx
Chneemann 0d695f9fc0
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 1m12s
feat(settings): add SettingsDeleteAccountCard component with two-step confirmation
2026-09-27 13:06:26 +02:00

137 lines
4.3 KiB
TypeScript

/**
* @file components/settings/SettingsDeleteAccountCard.tsx
* @description Card component for handling account deletion with dynamic button inline confirmation and guest protection.
*/
"use client";
import { useState } from "react";
import { useUser } from "@/lib/context/UserContext";
import { Trash2, AlertTriangle, ChevronDown } from "lucide-react";
import { ActionButton } from "@/components/ui/ActionButton";
import { signOut } from "next-auth/react";
interface SettingsDeleteAccountCardProps {
defaultOpen?: boolean;
}
export function SettingsDeleteAccountCard({
defaultOpen = false,
}: SettingsDeleteAccountCardProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);
const [isDeleting, setIsDeleting] = useState(false);
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false);
const [error, setError] = useState<string | null>(null);
const { currentUser } = useUser();
if (!currentUser) return null;
const guestEmail = process.env.NEXT_PUBLIC_GUEST_EMAIL;
const isGuest = Boolean(
guestEmail && currentUser.email?.toLowerCase() === guestEmail.toLowerCase(),
);
/** Handles the two-step delete trigger and API request. */
const handleDelete = async () => {
if (!isConfirmingDelete) {
setIsConfirmingDelete(true);
return;
}
if (isDeleting) return;
try {
setIsDeleting(true);
setError(null);
const res = await fetch("/api/users/profile", {
method: "DELETE",
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Failed to delete account");
}
await signOut({ callbackUrl: "/login" });
} catch (err) {
setError(err instanceof Error ? err.message : "An error occurred");
setIsDeleting(false);
setIsConfirmingDelete(false);
}
};
return (
<div className="bg-surface border border-rose-500/20 rounded-xl overflow-hidden transition-all shrink-0">
{/* Header / Toggle Button */}
<button
type="button"
onClick={() => setIsOpen((prev) => !prev)}
className="w-full p-6 flex items-center justify-between hover:bg-white/5 transition-colors cursor-pointer text-left"
>
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-rose-500/10 text-rose-400">
<Trash2 className="w-5 h-5" />
</div>
<div>
<h2 className="text-lg font-bold text-rose-400">Delete Account</h2>
<p className="text-sm text-muted">
Permanently delete your account and all associated data.
</p>
</div>
</div>
<ChevronDown
className={`w-5 h-5 text-muted transition-transform duration-200 ${
isOpen ? "rotate-180" : "rotate-0"
}`}
/>
</button>
{/* Collapsible Body */}
{isOpen && (
<div className="p-6 pt-0 space-y-4 border-t border-rose-500/10 mt-2">
{isGuest ? (
<div className="flex items-center gap-2 p-3 mt-4 rounded-lg bg-amber-500/10 text-amber-400 text-sm">
<AlertTriangle className="w-4 h-4 shrink-0" />
<span>Guest accounts cannot be deleted.</span>
</div>
) : (
<div className="pt-4 space-y-4">
<p className="text-sm text-muted">
Once you delete your account, there is no going back. Please be
certain.
</p>
{error && <p className="text-xs text-red-400">{error}</p>}
<div className="flex items-center gap-3">
<ActionButton
type="button"
variant="danger"
onClick={handleDelete}
isLoading={isDeleting}
icon={Trash2}
>
{isConfirmingDelete ? "Sure?" : "Delete Account"}
</ActionButton>
{isConfirmingDelete && (
<ActionButton
type="button"
variant="secondary"
onClick={() => setIsConfirmingDelete(false)}
disabled={isDeleting}
>
Cancel
</ActionButton>
)}
</div>
</div>
)}
</div>
)}
</div>
);
}