Skip to content

Commit 8959ebd

Browse files
committed
feat: update account form.
1 parent e6811a6 commit 8959ebd

File tree

6 files changed

+287
-63
lines changed

6 files changed

+287
-63
lines changed

__tests__/app/api/account.route.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,15 @@ describe("User Profile Update Endpoint", () => {
7777
});
7878
expect(prisma.user.update).toHaveBeenCalledWith({
7979
where: { id: "user123" },
80-
data: { name: "New Name", slug: "new-slug" },
80+
data: {
81+
name: "New Name",
82+
slug: "new-slug",
83+
displayEmail: null,
84+
title: null,
85+
location: null,
86+
siteTitle: null,
87+
siteDescription: null,
88+
},
8189
});
8290
});
8391

src/app/account/AccountForm.tsx

Lines changed: 172 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,40 @@
11
"use client";
22

33
import { CodeInline } from "@/components/CodeInline";
4+
import { LoadingOverlay } from "@/components/LoadingOverlay";
5+
import { MessageDialog } from "@/components/MessageDialog";
46
import { Box, Button, TextField, Typography } from "@mui/material";
57
import { useState } from "react";
68

7-
const AccountForm = ({ name, slug }: { name: string; slug: string }) => {
8-
const [formData, setFormData] = useState({ name, slug });
9-
const [errors, setErrors] = useState<{ name?: string; slug?: string }>({});
9+
const AccountForm = ({
10+
name,
11+
slug,
12+
displayEmail,
13+
title,
14+
location,
15+
siteTitle,
16+
siteDescription,
17+
}: {
18+
name: string;
19+
slug: string;
20+
displayEmail: string;
21+
title: string;
22+
location: string;
23+
siteTitle: string;
24+
siteDescription: string;
25+
}) => {
26+
const [formData, setFormData] = useState({
27+
name,
28+
slug,
29+
displayEmail,
30+
title,
31+
location,
32+
siteTitle,
33+
siteDescription,
34+
});
35+
const [errors, setErrors] = useState<{ name?: string; slug?: string; displayEmail?: string }>({});
36+
const [loading, setLoading] = useState(false);
37+
const [message, setMessage] = useState("");
1038

1139
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
1240
const { name, value } = e.target;
@@ -34,7 +62,7 @@ const AccountForm = ({ name, slug }: { name: string; slug: string }) => {
3462

3563
const handleSubmit = (e: React.FormEvent) => {
3664
e.preventDefault();
37-
const { name, slug } = formData;
65+
const { name, slug, displayEmail } = formData;
3866

3967
if (!name.trim() || !slug.trim()) {
4068
setErrors({
@@ -44,6 +72,13 @@ const AccountForm = ({ name, slug }: { name: string; slug: string }) => {
4472
return;
4573
}
4674

75+
if (displayEmail.trim()?.length > 0 && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(displayEmail)) {
76+
setErrors({ displayEmail: "Invalid email address" });
77+
return;
78+
}
79+
80+
setLoading(true);
81+
4782
// Send the form data to the next.js API route to
4883
// update the user's account information. Use POST method.
4984
fetch("/api/account", {
@@ -53,73 +88,153 @@ const AccountForm = ({ name, slug }: { name: string; slug: string }) => {
5388
},
5489
body: JSON.stringify(formData),
5590
})
56-
.then((res) => {
91+
.then(async (res) => {
5792
if (!res.ok) {
58-
alert("Failed to submit the form!");
93+
const { error } = await res.json();
94+
setMessage(error);
5995
return;
6096
}
6197

62-
alert("Form submitted successfully!");
98+
setMessage("Saved!");
6399
})
64100
.catch((err) => {
65-
// eslint-disable-next-line no-console
66-
console.error(err);
67-
alert("Failed to submit the form!");
101+
setMessage(err.message);
102+
})
103+
.finally(() => {
104+
setLoading(false);
68105
});
69106
};
70107

71108
return (
72-
<Box
73-
component="form"
74-
onSubmit={handleSubmit}
75-
sx={{
76-
display: "flex",
77-
flexDirection: "column",
78-
gap: 2,
79-
mt: 4,
80-
}}
81-
>
82-
<Typography variant="body2" color="textSecondary">
83-
Displayed on your public resume.
84-
</Typography>
85-
<TextField
86-
label="Full Name"
87-
name="name"
88-
value={formData.name}
89-
onChange={handleChange}
90-
error={!!errors.name}
91-
helperText={errors.name ? errors.name : " "}
92-
fullWidth
93-
/>
94-
<Typography variant="body2" color="textSecondary">
95-
Used to generate your public resume URL, e.g.{" "}
96-
<CodeInline>
97-
https://openresume.org/r/<strong>your-custom-slug</strong>/
98-
</CodeInline>
99-
. If you change it, an automatic redirect will <strong>not</strong> be created, so please
100-
update your shared links.
101-
</Typography>
102-
<TextField
103-
label="Slug"
104-
name="slug"
105-
value={formData.slug}
106-
onChange={handleChange}
107-
error={!!errors.slug}
108-
helperText={errors.slug ? errors.slug : " "}
109-
fullWidth
109+
<Box>
110+
<LoadingOverlay open={loading} message="Submitting form..." />
111+
<MessageDialog
112+
open={message?.length > 0}
113+
message={message}
114+
onClose={() => setMessage("")}
115+
onConfirm={() => setMessage("")}
110116
/>
111-
<Button
112-
type="submit"
113-
variant="contained"
114-
color="primary"
115-
fullWidth
117+
<Box
118+
component="form"
119+
onSubmit={handleSubmit}
116120
sx={{
117-
mt: 2,
118-
width: "200px",
121+
display: "flex",
122+
flexDirection: "column",
123+
gap: 2,
124+
mt: 4,
119125
}}
120126
>
121-
Save
122-
</Button>
127+
<Typography variant="body2" color="textSecondary">
128+
Displayed on your public resume.
129+
</Typography>
130+
<TextField
131+
label="Full Name"
132+
name="name"
133+
value={formData.name}
134+
onChange={handleChange}
135+
error={!!errors.name}
136+
helperText={errors.name ? errors.name : " "}
137+
fullWidth
138+
/>
139+
<Typography variant="body2" color="textSecondary">
140+
Used to generate your public resume URL, e.g.{" "}
141+
<CodeInline>
142+
https://openresume.org/r/
143+
<strong>
144+
{formData?.slug ? formData.slug : "your-custom-slug"}
145+
</strong>/
146+
</CodeInline>
147+
. If you change it, an automatic redirect will <strong>not</strong> be created, so please
148+
update your shared links.
149+
</Typography>
150+
<TextField
151+
label="Slug"
152+
name="slug"
153+
value={formData.slug}
154+
onChange={handleChange}
155+
error={!!errors.slug}
156+
helperText={errors.slug ? errors.slug : " "}
157+
fullWidth
158+
/>
159+
<Typography variant="body2" color="textSecondary">
160+
Your display email is publicly visible on your resume!
161+
</Typography>
162+
<TextField
163+
label="Display Email"
164+
name="displayEmail"
165+
value={formData.displayEmail}
166+
onChange={handleChange}
167+
error={!!errors.displayEmail}
168+
helperText={errors.displayEmail ? errors.displayEmail : " "}
169+
fullWidth
170+
/>
171+
<Typography variant="body2" color="textSecondary">
172+
Your Title is the job title you&apos;re looking for, or identify as. Example:{" "}
173+
<strong>Software Engineer</strong>
174+
</Typography>
175+
<TextField
176+
label="Title"
177+
name="title"
178+
value={formData.title}
179+
onChange={handleChange}
180+
fullWidth
181+
/>
182+
<Typography variant="body2" color="textSecondary">
183+
Your Location is where you are currently located. General is advised. Example:{" "}
184+
<strong>Los Angeles, CA</strong>
185+
</Typography>
186+
<TextField
187+
label="Location"
188+
name="location"
189+
value={formData.location}
190+
onChange={handleChange}
191+
fullWidth
192+
/>
193+
<Typography variant="body2" color="textSecondary">
194+
Your Site Title is the title of your personal website. Example:{" "}
195+
<strong>{formData?.name ? formData.name : "John Doe"} | OpenResume</strong>{" "}
196+
<em>
197+
This is used in the title tag of your website as well as for social media sharing.
198+
</em>
199+
</Typography>
200+
<TextField
201+
label="Site Title"
202+
name="siteTitle"
203+
value={formData.siteTitle}
204+
onChange={handleChange}
205+
fullWidth
206+
/>
207+
<Typography variant="body2" color="textSecondary">
208+
Your Site Description is a short description of your personal website. Example:{" "}
209+
<strong>
210+
{formData?.name ? formData.name : "John Doe"} is a seasoned professional with 10 years
211+
experience.
212+
</strong>{" "}
213+
<em>
214+
This is used in the meta description tag of your website as well as for social media
215+
sharing.
216+
</em>
217+
</Typography>
218+
<TextField
219+
label="Site Description"
220+
name="siteDescription"
221+
value={formData.siteDescription}
222+
onChange={handleChange}
223+
fullWidth
224+
/>
225+
<Button
226+
type="submit"
227+
variant="contained"
228+
color="primary"
229+
fullWidth
230+
sx={{
231+
mt: 2,
232+
width: "200px",
233+
}}
234+
>
235+
Save
236+
</Button>
237+
</Box>
123238
</Box>
124239
);
125240
};

src/app/account/page.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,15 @@ const Page = async () => {
3737
<Typography component="h1" variant="h4">
3838
Account Settings
3939
</Typography>
40-
<AccountForm name={user?.name || ""} slug={user?.slug || ""} />
40+
<AccountForm
41+
name={user?.name || ""}
42+
slug={user?.slug || ""}
43+
displayEmail={user?.displayEmail || ""}
44+
title={user?.title || ""}
45+
location={user?.location || ""}
46+
siteTitle={user?.siteTitle || ""}
47+
siteDescription={user?.siteDescription || ""}
48+
/>
4149
</Box>
4250
</Container>
4351
);

src/app/api/account/route.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export async function POST(req: NextRequest) {
1010
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
1111
}
1212

13-
const { name, slug } = await req.json();
13+
const { name, slug, displayEmail, title, location, siteTitle, siteDescription } =
14+
await req.json();
1415
if (!name || !slug) {
1516
return NextResponse.json({ error: "Name and slug are required" }, { status: 400 });
1617
}
@@ -28,8 +29,11 @@ export async function POST(req: NextRequest) {
2829
where: {
2930
slug,
3031
},
32+
select: {
33+
id: true,
34+
},
3135
});
32-
if (existingUser) {
36+
if (existingUser && existingUser.id !== session.user.id) {
3337
return NextResponse.json({ error: "Slug is already taken" }, { status: 400 });
3438
}
3539

@@ -38,8 +42,13 @@ export async function POST(req: NextRequest) {
3842
id: session.user.id,
3943
},
4044
data: {
41-
name,
42-
slug,
45+
name: name || null,
46+
slug: slug || null,
47+
displayEmail: displayEmail || null,
48+
title: title || null,
49+
location: location || null,
50+
siteTitle: siteTitle || null,
51+
siteDescription: siteDescription || null,
4352
},
4453
});
4554

src/components/LoadingOverlay.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from "react";
2+
import { Backdrop, CircularProgress, Typography, Box } from "@mui/material";
3+
4+
export const LoadingOverlay = ({ open = false, message = "Loading...", zIndex = 9999 }) => {
5+
return (
6+
<Backdrop
7+
sx={{
8+
color: "#fff",
9+
zIndex: zIndex,
10+
flexDirection: "column",
11+
backgroundColor: "rgba(0, 0, 0, 0.6)",
12+
}}
13+
open={open}
14+
>
15+
<Box display="flex" flexDirection="column" alignItems="center" justifyContent="center">
16+
<CircularProgress color="inherit" />
17+
<Typography
18+
variant="body1"
19+
sx={{
20+
marginTop: 2,
21+
color: "white",
22+
}}
23+
>
24+
{message}
25+
</Typography>
26+
</Box>
27+
</Backdrop>
28+
);
29+
};

0 commit comments

Comments
 (0)