Skip to content

Commit bf35421

Browse files
Merge pull request #515 from Mkalbani/feat/settings-page
Feat/settings-page
2 parents 8113905 + d5faecb commit bf35421

2 files changed

Lines changed: 260 additions & 1 deletion

File tree

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { useForm } from "react-hook-form";
5+
import { zodResolver } from "@hookform/resolvers/zod";
6+
import { z } from "zod";
7+
import { User, Lock, CheckCircle } from "lucide-react";
8+
import { useAuthStore } from "@/store/auth.store";
9+
import { useUpdateProfile } from "@/lib/query/hooks/query.hook";
10+
11+
// ── Profile schema ──────────────────────────────────────────────
12+
const profileSchema = z.object({
13+
firstName: z.string().min(1, "First name is required"),
14+
lastName: z.string().min(1, "Last name is required"),
15+
});
16+
type ProfileForm = z.infer<typeof profileSchema>;
17+
18+
// ── Password schema ─────────────────────────────────────────────
19+
const passwordSchema = z
20+
.object({
21+
password: z.string().min(8, "Password must be at least 8 characters"),
22+
confirmPassword: z.string(),
23+
})
24+
.refine((d) => d.password === d.confirmPassword, {
25+
message: "Passwords don't match",
26+
path: ["confirmPassword"],
27+
});
28+
type PasswordForm = z.infer<typeof passwordSchema>;
29+
30+
export default function SettingsPage() {
31+
const { user } = useAuthStore();
32+
const updateProfile = useUpdateProfile();
33+
const [profileSaved, setProfileSaved] = useState(false);
34+
const [passwordSaved, setPasswordSaved] = useState(false);
35+
36+
const profileForm = useForm<ProfileForm>({
37+
resolver: zodResolver(profileSchema),
38+
defaultValues: {
39+
firstName: user?.firstName ?? "",
40+
lastName: user?.lastName ?? "",
41+
},
42+
});
43+
44+
const passwordForm = useForm<PasswordForm>({
45+
resolver: zodResolver(passwordSchema),
46+
defaultValues: { password: "", confirmPassword: "" },
47+
});
48+
49+
const onProfileSubmit = (data: ProfileForm) => {
50+
updateProfile.mutate(data, {
51+
onSuccess: () => {
52+
setProfileSaved(true);
53+
setTimeout(() => setProfileSaved(false), 3000);
54+
},
55+
});
56+
};
57+
58+
const onPasswordSubmit = (data: PasswordForm) => {
59+
updateProfile.mutate(
60+
{ password: data.password },
61+
{
62+
onSuccess: () => {
63+
setPasswordSaved(true);
64+
passwordForm.reset();
65+
setTimeout(() => setPasswordSaved(false), 3000);
66+
},
67+
},
68+
);
69+
};
70+
71+
return (
72+
<div className="max-w-2xl">
73+
{/* Header */}
74+
<div className="mb-6">
75+
<h1 className="text-2xl font-bold text-gray-900">Settings</h1>
76+
<p className="text-sm text-gray-500 mt-1">
77+
Manage your profile and account preferences
78+
</p>
79+
</div>
80+
81+
{/* Profile section */}
82+
<div className="bg-white rounded-xl border border-gray-200 p-6 mb-5">
83+
<div className="flex items-center gap-3 mb-5">
84+
<div className="w-8 h-8 rounded-full bg-gray-900 text-white flex items-center justify-center flex-shrink-0">
85+
<User size={15} />
86+
</div>
87+
<div>
88+
<h2 className="text-sm font-semibold text-gray-900">Profile</h2>
89+
<p className="text-xs text-gray-500">Update your display name</p>
90+
</div>
91+
</div>
92+
93+
<form
94+
onSubmit={profileForm.handleSubmit(onProfileSubmit)}
95+
className="space-y-4"
96+
>
97+
<div className="grid grid-cols-2 gap-4">
98+
<div>
99+
<label className="block text-xs font-medium text-gray-700 mb-1">
100+
First name
101+
</label>
102+
<input
103+
{...profileForm.register("firstName")}
104+
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-900"
105+
/>
106+
{profileForm.formState.errors.firstName && (
107+
<p className="text-xs text-red-500 mt-1">
108+
{profileForm.formState.errors.firstName.message}
109+
</p>
110+
)}
111+
</div>
112+
<div>
113+
<label className="block text-xs font-medium text-gray-700 mb-1">
114+
Last name
115+
</label>
116+
<input
117+
{...profileForm.register("lastName")}
118+
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-900"
119+
/>
120+
{profileForm.formState.errors.lastName && (
121+
<p className="text-xs text-red-500 mt-1">
122+
{profileForm.formState.errors.lastName.message}
123+
</p>
124+
)}
125+
</div>
126+
</div>
127+
128+
{/* Email (read-only) */}
129+
<div>
130+
<label className="block text-xs font-medium text-gray-700 mb-1">
131+
Email address
132+
</label>
133+
<input
134+
type="email"
135+
value={user?.email ?? ""}
136+
disabled
137+
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg bg-gray-50 text-gray-400 cursor-not-allowed"
138+
/>
139+
<p className="text-xs text-gray-400 mt-1">
140+
Email cannot be changed.
141+
</p>
142+
</div>
143+
144+
{/* Role (read-only) */}
145+
<div>
146+
<label className="block text-xs font-medium text-gray-700 mb-1">
147+
Role
148+
</label>
149+
<input
150+
value={
151+
user?.role
152+
? user.role.charAt(0).toUpperCase() + user.role.slice(1)
153+
: ""
154+
}
155+
disabled
156+
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg bg-gray-50 text-gray-400 cursor-not-allowed"
157+
/>
158+
</div>
159+
160+
<div className="flex items-center gap-3 pt-1">
161+
<button
162+
type="submit"
163+
disabled={updateProfile.isPending}
164+
className="px-4 py-2 text-sm font-medium bg-gray-900 text-white rounded-lg hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
165+
>
166+
{updateProfile.isPending ? "Saving…" : "Save changes"}
167+
</button>
168+
{profileSaved && (
169+
<span className="flex items-center gap-1.5 text-xs text-green-600">
170+
<CheckCircle size={13} />
171+
Saved successfully
172+
</span>
173+
)}
174+
{updateProfile.isError && (
175+
<span className="text-xs text-red-500">
176+
Failed to save. Try again.
177+
</span>
178+
)}
179+
</div>
180+
</form>
181+
</div>
182+
183+
{/* Password section */}
184+
<div className="bg-white rounded-xl border border-gray-200 p-6">
185+
<div className="flex items-center gap-3 mb-5">
186+
<div className="w-8 h-8 rounded-full bg-gray-100 text-gray-600 flex items-center justify-center flex-shrink-0">
187+
<Lock size={15} />
188+
</div>
189+
<div>
190+
<h2 className="text-sm font-semibold text-gray-900">
191+
Change Password
192+
</h2>
193+
<p className="text-xs text-gray-500">
194+
Choose a strong password (min. 8 characters)
195+
</p>
196+
</div>
197+
</div>
198+
199+
<form
200+
onSubmit={passwordForm.handleSubmit(onPasswordSubmit)}
201+
className="space-y-4"
202+
>
203+
<div>
204+
<label className="block text-xs font-medium text-gray-700 mb-1">
205+
New password
206+
</label>
207+
<input
208+
{...passwordForm.register("password")}
209+
type="password"
210+
placeholder="••••••••"
211+
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-900"
212+
/>
213+
{passwordForm.formState.errors.password && (
214+
<p className="text-xs text-red-500 mt-1">
215+
{passwordForm.formState.errors.password.message}
216+
</p>
217+
)}
218+
</div>
219+
220+
<div>
221+
<label className="block text-xs font-medium text-gray-700 mb-1">
222+
Confirm new password
223+
</label>
224+
<input
225+
{...passwordForm.register("confirmPassword")}
226+
type="password"
227+
placeholder="••••••••"
228+
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-gray-900"
229+
/>
230+
{passwordForm.formState.errors.confirmPassword && (
231+
<p className="text-xs text-red-500 mt-1">
232+
{passwordForm.formState.errors.confirmPassword.message}
233+
</p>
234+
)}
235+
</div>
236+
237+
<div className="flex items-center gap-3 pt-1">
238+
<button
239+
type="submit"
240+
disabled={updateProfile.isPending}
241+
className="px-4 py-2 text-sm font-medium bg-gray-900 text-white rounded-lg hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
242+
>
243+
{updateProfile.isPending ? "Updating…" : "Update password"}
244+
</button>
245+
{passwordSaved && (
246+
<span className="flex items-center gap-1.5 text-xs text-green-600">
247+
<CheckCircle size={13} />
248+
Password updated
249+
</span>
250+
)}
251+
</div>
252+
</form>
253+
</div>
254+
</div>
255+
);
256+
}

frontend/store/auth.store.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { persist } from 'zustand/middleware';
44
interface User {
55
id: string;
66
email: string;
7-
name: string;
7+
firstName: string;
8+
lastName: string;
9+
role: string;
10+
name?: string;
811
}
912

1013
interface AuthStore {

0 commit comments

Comments
 (0)