feat(settings): add SettingsDeleteAccountCard component with two-step confirmation
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 1m12s
All checks were successful
Deploy Waveform to VPS / deploy (push) Successful in 1m12s
This commit is contained in:
parent
fabd11e9e0
commit
0d695f9fc0
2 changed files with 140 additions and 1 deletions
137
components/settings/SettingsDeleteAccountCard.tsx
Normal file
137
components/settings/SettingsDeleteAccountCard.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
/**
|
||||||
|
* @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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import { useUser } from "@/lib/context/UserContext";
|
import { useUser } from "@/lib/context/UserContext";
|
||||||
import { SettingsProfileCard } from "./SettingsProfileCard";
|
import { SettingsProfileCard } from "./SettingsProfileCard";
|
||||||
|
import { SettingsDeleteAccountCard } from "./SettingsDeleteAccountCard";
|
||||||
|
|
||||||
/** Renders the settings view layout for managing user profile settings. */
|
/** Renders the settings view layout for managing user profile settings. */
|
||||||
export function SettingsView() {
|
export function SettingsView() {
|
||||||
|
|
@ -25,8 +26,9 @@ export function SettingsView() {
|
||||||
if (!currentUser) return null;
|
if (!currentUser) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto flex flex-col min-h-0 py-2 mt-4">
|
<div className="flex-1 flex flex-col gap-4 py-4 min-h-0">
|
||||||
<SettingsProfileCard onUpdate={updateProfile} />
|
<SettingsProfileCard onUpdate={updateProfile} />
|
||||||
|
<SettingsDeleteAccountCard />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue