Skip to content

Commit 9cbb432

Browse files
committed
feat: add delete account section
1 parent c2b685d commit 9cbb432

File tree

7 files changed

+214
-9
lines changed

7 files changed

+214
-9
lines changed

actions/open-customer-portal.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { redirect } from "next/navigation";
44
import { auth } from "@/auth";
55

66
import { stripe } from "@/lib/stripe";
7-
import { getUserSubscriptionPlan } from "@/lib/subscription";
87
import { absoluteUrl } from "@/lib/utils";
98

109
export type responseAction = {
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { CardSkeleton } from "@/components/shared/card-skeleton"
2-
import { DashboardHeader } from "@/components/dashboard/header"
3-
import { DashboardShell } from "@/components/dashboard/shell"
1+
import { DashboardHeader } from "@/components/dashboard/header";
2+
import { DashboardShell } from "@/components/dashboard/shell";
3+
import { CardSkeleton } from "@/components/shared/card-skeleton";
44

55
export default function DashboardSettingsLoading() {
66
return (
@@ -9,9 +9,10 @@ export default function DashboardSettingsLoading() {
99
heading="Settings"
1010
text="Manage account and website settings."
1111
/>
12-
<div className="grid gap-10">
12+
<div className="grid gap-6">
13+
<CardSkeleton />
1314
<CardSkeleton />
1415
</div>
1516
</DashboardShell>
16-
)
17+
);
1718
}

app/(dashboard)/dashboard/settings/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { redirect } from "next/navigation";
22

33
import { getCurrentUser } from "@/lib/session";
44
import { constructMetadata } from "@/lib/utils";
5+
import { DeleteAccountSection } from "@/components/dashboard/delete-account";
56
import { DashboardHeader } from "@/components/dashboard/header";
67
import { DashboardShell } from "@/components/dashboard/shell";
78
import { UserNameForm } from "@/components/forms/user-name-form";
@@ -24,8 +25,9 @@ export default async function SettingsPage() {
2425
heading="Settings"
2526
text="Manage account and website settings."
2627
/>
27-
<div className="grid gap-10">
28+
<div className="grid gap-6">
2829
<UserNameForm user={{ id: user.id, name: user.name || "" }} />
30+
<DeleteAccountSection />
2931
</div>
3032
</DashboardShell>
3133
);

app/api/user/route.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { auth } from "@/auth";
2+
3+
import { prisma } from "@/lib/db";
4+
5+
export const DELETE = auth(async (req) => {
6+
if (!req.auth) {
7+
return new Response("Not authenticated", { status: 401 });
8+
}
9+
10+
const currentUser = req.auth.user;
11+
if (!currentUser) {
12+
return new Response("Invalid user", { status: 401 });
13+
}
14+
15+
try {
16+
await prisma.user.delete({
17+
where: {
18+
id: currentUser.id,
19+
},
20+
});
21+
} catch (error) {
22+
return new Response("Internal server error", { status: 500 });
23+
}
24+
25+
return new Response("User deleted successfully!", { status: 200 });
26+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"use client";
2+
3+
import { useDeleteAccountModal } from "@/components/modals/delete-account-modal";
4+
import { Button } from "@/components/ui/button";
5+
import {
6+
Card,
7+
CardDescription,
8+
CardFooter,
9+
CardHeader,
10+
CardTitle,
11+
} from "@/components/ui/card";
12+
import { siteConfig } from "@/config/site";
13+
14+
export function DeleteAccountSection() {
15+
const { setShowDeleteAccountModal, DeleteAccountModal } =
16+
useDeleteAccountModal();
17+
18+
return (
19+
<>
20+
<DeleteAccountModal />
21+
<Card className="border border-red-600">
22+
<CardHeader className="space-y-2">
23+
<CardTitle>Delete Account</CardTitle>
24+
<CardDescription className="text-pretty text-[15px] lg:text-balance">
25+
Permanently delete your {siteConfig.name} account and your
26+
subscription. This action cannot be undone - please proceed with
27+
caution.
28+
</CardDescription>
29+
</CardHeader>
30+
<CardFooter className="mt-2 flex justify-end border-t border-red-600 bg-red-500/5 py-2">
31+
<Button
32+
type="submit"
33+
variant="destructive"
34+
onClick={() => setShowDeleteAccountModal(true)}
35+
>
36+
<span>Delete Account</span>
37+
</Button>
38+
</CardFooter>
39+
</Card>
40+
</>
41+
);
42+
}

components/forms/user-name-form.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ export function UserNameForm({ user }: UserNameFormProps) {
6767
return (
6868
<form onSubmit={onSubmit}>
6969
<Card>
70-
<CardHeader>
70+
<CardHeader className="space-y-2">
7171
<CardTitle>Your Name</CardTitle>
72-
<CardDescription>
72+
<CardDescription className="text-[15px]">
7373
Please enter your full name or a display name you are comfortable
7474
with.
7575
</CardDescription>
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import {
2+
Dispatch,
3+
SetStateAction,
4+
useCallback,
5+
useMemo,
6+
useState,
7+
} from "react";
8+
import { signOut, useSession } from "next-auth/react";
9+
import { toast } from "sonner";
10+
11+
import { Button } from "@/components/ui/button";
12+
import { Input } from "@/components/ui/input";
13+
import { Modal } from "@/components/ui/modal";
14+
import { UserAvatar } from "@/components/shared/user-avatar";
15+
16+
function DeleteAccountModal({
17+
showDeleteAccountModal,
18+
setShowDeleteAccountModal,
19+
}: {
20+
showDeleteAccountModal: boolean;
21+
setShowDeleteAccountModal: Dispatch<SetStateAction<boolean>>;
22+
}) {
23+
const { data: session } = useSession();
24+
const [deleting, setDeleting] = useState(false);
25+
26+
async function deleteAccount() {
27+
setDeleting(true);
28+
await fetch(`/api/user`, {
29+
method: "DELETE",
30+
headers: {
31+
"Content-Type": "application/json",
32+
},
33+
}).then(async (res) => {
34+
if (res.status === 200) {
35+
// delay to allow for the route change to complete
36+
await new Promise((resolve) =>
37+
setTimeout(() => {
38+
signOut({
39+
callbackUrl: `${window.location.origin}/`,
40+
});
41+
resolve(null);
42+
}, 500),
43+
);
44+
} else {
45+
setDeleting(false);
46+
const error = await res.text();
47+
throw error;
48+
}
49+
});
50+
}
51+
52+
return (
53+
<Modal
54+
showModal={showDeleteAccountModal}
55+
setShowModal={setShowDeleteAccountModal}
56+
className="gap-0"
57+
>
58+
<div className="flex flex-col items-center justify-center space-y-3 border-b p-4 pt-8 sm:px-16">
59+
<UserAvatar
60+
user={{
61+
name: session?.user?.name || null,
62+
image: session?.user?.image || null,
63+
}}
64+
/>
65+
<h3 className="text-lg font-semibold">Delete Account</h3>
66+
<p className="text-center text-sm text-muted-foreground">
67+
<b>Warning:</b> This will permanently delete your account and your
68+
active subscription!
69+
</p>
70+
71+
{/* TODO: Use getUserSubscriptionPlan(session.user.id) to display the user's subscription if he have a paid plan */}
72+
</div>
73+
74+
<form
75+
onSubmit={async (e) => {
76+
e.preventDefault();
77+
toast.promise(deleteAccount(), {
78+
loading: "Deleting account...",
79+
success: "Account deleted successfully!",
80+
error: (err) => err,
81+
});
82+
}}
83+
className="flex flex-col space-y-6 bg-accent px-4 py-8 text-left sm:px-16"
84+
>
85+
<div>
86+
<label htmlFor="verification" className="block text-sm">
87+
To verify, type{" "}
88+
<span className="font-semibold text-black dark:text-white">
89+
confirm delete account
90+
</span>{" "}
91+
below
92+
</label>
93+
<Input
94+
type="text"
95+
name="verification"
96+
id="verification"
97+
pattern="confirm delete account"
98+
required
99+
autoFocus={false}
100+
autoComplete="off"
101+
className="mt-1 w-full border bg-background"
102+
/>
103+
</div>
104+
105+
<Button
106+
variant={deleting ? "disable" : "destructive"}
107+
disabled={deleting}
108+
>
109+
Confirm delete account
110+
</Button>
111+
</form>
112+
</Modal>
113+
);
114+
}
115+
116+
export function useDeleteAccountModal() {
117+
const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false);
118+
119+
const DeleteAccountModalCallback = useCallback(() => {
120+
return (
121+
<DeleteAccountModal
122+
showDeleteAccountModal={showDeleteAccountModal}
123+
setShowDeleteAccountModal={setShowDeleteAccountModal}
124+
/>
125+
);
126+
}, [showDeleteAccountModal, setShowDeleteAccountModal]);
127+
128+
return useMemo(
129+
() => ({
130+
setShowDeleteAccountModal,
131+
DeleteAccountModal: DeleteAccountModalCallback,
132+
}),
133+
[setShowDeleteAccountModal, DeleteAccountModalCallback],
134+
);
135+
}

0 commit comments

Comments
 (0)