Skip to content

Commit f978822

Browse files
authored
Merge pull request #90 from hammercode-dev/feat/user-profile
[FEAT] - mutate update user profile
2 parents 2d13bfa + 15ae38b commit f978822

11 files changed

Lines changed: 278 additions & 56 deletions

File tree

src/components/layout/Navbar/constant.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ export const USER_LINKS: Record<UserRole, LinkItem[]> = {
4444
id: "my-events",
4545
href: "/my-events",
4646
},
47+
{
48+
id: "profile",
49+
href: "/profile",
50+
},
4751
],
4852
};
4953

src/components/provider/TanstackProvider/index.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22

33
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
44

5-
const queryClient = new QueryClient();
5+
const queryClient = new QueryClient({
6+
defaultOptions: {
7+
queries: {
8+
retry: false,
9+
staleTime: 5 * 60 * 1000,
10+
},
11+
mutations: {
12+
retry: false,
13+
},
14+
},
15+
});
616

717
const TanstackProvider = ({ children }: { children: React.ReactNode }) => {
818
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;

src/domains/Profile.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { z } from "zod";
22

3-
export const UserSchema = z.object({
3+
export const profileResSchema = z.object({
44
id: z.number(),
55
username: z.string(),
66
email: z.string(),
@@ -14,21 +14,23 @@ export const UserSchema = z.object({
1414
github: z.string(),
1515
linkedin: z.string(),
1616
personal_web: z.string(),
17-
created_at: z.string(),
18-
updated_at: z.string(),
17+
created_at: z.date(),
18+
updated_at: z.date(),
1919
});
2020

21-
export type UserType = z.infer<typeof UserSchema>;
21+
export type ProfileResType = z.infer<typeof profileResSchema>;
2222

23-
export const profileFormSchema = z.object({
24-
fullname: z.string().min(1, "Fullname is required"),
25-
date_of_birth: z.string().min(1, "Date of birth is required"),
26-
gender: z.enum(["Male", "Female"], { required_error: "Please select a gender" }),
27-
phone_number: z.string().min(1, "Phone number is required"),
28-
address: z.string().min(1, "Address is required"),
29-
github: z.string().url("Please enter a valid GitHub URL").optional().or(z.literal("")),
30-
linkedin: z.string().url("Please enter a valid LinkedIn URL").optional().or(z.literal("")),
31-
personal_web: z.string().url("Please enter a valid personal website URL").optional().or(z.literal("")),
32-
});
23+
export const createProfileFormSchema = (t: (key: string) => string) =>
24+
z.object({
25+
username: z.string().min(1, t("validation.username-required")),
26+
fullname: z.string().min(1, t("validation.fullname-required")),
27+
date_of_birth: z.string().min(1, t("validation.date-of-birth-required")),
28+
gender: z.string().min(1, t("validation.gender-required")),
29+
phone_number: z.string().min(1, t("validation.phone-number-required")),
30+
address: z.string().min(1, t("validation.address-required")),
31+
github: z.string().url(t("validation.github-invalid-url")).optional().or(z.literal("")),
32+
linkedin: z.string().url(t("validation.linkedin-invalid-url")).optional().or(z.literal("")),
33+
personal_web: z.string().url(t("validation.personal-web-invalid-url")).optional().or(z.literal("")),
34+
});
3335

34-
export type ProfileFormType = z.infer<typeof profileFormSchema>;
36+
export type ProfileFormType = z.infer<ReturnType<typeof createProfileFormSchema>>;

src/features/profile/components/ProfileForm.tsx

Lines changed: 90 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,96 @@ import { useForm } from "react-hook-form";
33
import { zodResolver } from "@hookform/resolvers/zod";
44
import { format } from "date-fns";
55
import { CalendarIcon } from "lucide-react";
6+
import { useTranslations } from "next-intl";
67
import { Button } from "@/components/ui/Button";
78
import { Input } from "@/components/ui/Input";
8-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/Select";
9+
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/Select";
910
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/Form";
1011
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/Popover";
1112
import { Calendar } from "@/components/ui/Calendar";
12-
import { profileFormSchema, ProfileFormType } from "@/domains/Profile";
13+
import { createProfileFormSchema, ProfileFormType } from "@/domains/Profile";
1314
import { cn } from "@/lib/utils";
15+
import { useGetProfile, useUpdateProfile } from "../hooks";
16+
import Loader from "@/components/common/Loader";
17+
import { useEffect } from "react";
1418

1519
interface ProfileFormProps {
1620
activeTab: "account" | "information";
1721
}
1822

1923
const ProfileForm = ({ activeTab }: ProfileFormProps) => {
24+
const t = useTranslations("Profile");
25+
const profileFormSchema = createProfileFormSchema(t);
26+
const { data, isLoading } = useGetProfile();
27+
const { mutate } = useUpdateProfile();
28+
2029
const form = useForm<ProfileFormType>({
2130
resolver: zodResolver(profileFormSchema),
2231
defaultValues: {
32+
username: "",
2333
fullname: "",
2434
date_of_birth: "",
2535
phone_number: "",
36+
gender: "",
2637
address: "",
2738
github: "",
2839
linkedin: "",
2940
personal_web: "",
3041
},
3142
});
3243

44+
useEffect(() => {
45+
if (data) {
46+
const resetData = {
47+
username: data.username || "",
48+
fullname: data.fullname || "",
49+
date_of_birth: data.date_of_birth || "",
50+
phone_number: data.phone_number || "",
51+
gender: data.gender || "",
52+
address: data.address || "",
53+
github: data.github || "",
54+
linkedin: data.linkedin || "",
55+
personal_web: data.personal_web || "",
56+
};
57+
58+
form.reset(resetData);
59+
}
60+
}, [data]);
61+
3362
const onSubmit = (data: ProfileFormType) => {
34-
// ! TODO handle query mutation
35-
console.log("Profile data:", data);
63+
mutate(data);
3664
};
3765

66+
if (isLoading) {
67+
return <Loader />;
68+
}
69+
3870
const renderForm = () => {
3971
if (activeTab === "account") {
4072
return (
4173
<>
74+
<FormField
75+
control={form.control}
76+
name="username"
77+
render={({ field }) => (
78+
<FormItem>
79+
<FormLabel>{t("form.label.username")}</FormLabel>
80+
<FormControl>
81+
<Input placeholder={t("form.placeholder.username")} {...field} />
82+
</FormControl>
83+
<FormMessage />
84+
</FormItem>
85+
)}
86+
/>
87+
4288
<FormField
4389
control={form.control}
4490
name="fullname"
4591
render={({ field }) => (
4692
<FormItem>
47-
<FormLabel>Full Name</FormLabel>
93+
<FormLabel>{t("form.label.fullname")}</FormLabel>
4894
<FormControl>
49-
<Input placeholder="Enter your full name" {...field} />
95+
<Input placeholder={t("form.placeholder.fullname")} {...field} />
5096
</FormControl>
5197
<FormMessage />
5298
</FormItem>
@@ -58,15 +104,19 @@ const ProfileForm = ({ activeTab }: ProfileFormProps) => {
58104
name="date_of_birth"
59105
render={({ field }) => (
60106
<FormItem className="flex flex-col">
61-
<FormLabel>Date of Birth</FormLabel>
107+
<FormLabel>{t("form.label.date-of-birth")}</FormLabel>
62108
<Popover>
63109
<PopoverTrigger asChild>
64110
<FormControl>
65111
<Button
66112
variant="outline"
67113
className={cn("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
68114
>
69-
{field.value ? format(new Date(field.value), "PPP") : <span>Pick a date</span>}
115+
{field.value ? (
116+
format(new Date(field.value), "PPP")
117+
) : (
118+
<span>{t("form.placeholder.date-of-birth")}</span>
119+
)}
70120
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
71121
</Button>
72122
</FormControl>
@@ -93,33 +143,37 @@ const ProfileForm = ({ activeTab }: ProfileFormProps) => {
93143
<FormField
94144
control={form.control}
95145
name="gender"
96-
render={({ field }) => (
97-
<FormItem>
98-
<FormLabel>Gender</FormLabel>
99-
<FormControl>
100-
<Select onValueChange={field.onChange} defaultValue={field.value}>
101-
<SelectTrigger>
102-
<SelectValue placeholder="Select gender" />
103-
</SelectTrigger>
104-
<SelectContent>
105-
<SelectItem value="Male">Male</SelectItem>
106-
<SelectItem value="Female">Female</SelectItem>
107-
</SelectContent>
108-
</Select>
109-
</FormControl>
110-
<FormMessage />
111-
</FormItem>
112-
)}
146+
render={({ field }) => {
147+
return (
148+
<FormItem>
149+
<FormLabel>{t("form.label.gender")}</FormLabel>
150+
<FormControl>
151+
<Select defaultValue={data?.gender || ""} onValueChange={field.onChange}>
152+
<SelectTrigger>
153+
<SelectValue placeholder={t("form.placeholder.gender")} />
154+
</SelectTrigger>
155+
<SelectContent>
156+
<SelectGroup>
157+
<SelectItem value="Male">{t("gender.male")}</SelectItem>
158+
<SelectItem value="Female">{t("gender.female")}</SelectItem>
159+
</SelectGroup>
160+
</SelectContent>
161+
</Select>
162+
</FormControl>
163+
<FormMessage />
164+
</FormItem>
165+
);
166+
}}
113167
/>
114168

115169
<FormField
116170
control={form.control}
117171
name="phone_number"
118172
render={({ field }) => (
119173
<FormItem>
120-
<FormLabel>Phone Number</FormLabel>
174+
<FormLabel>{t("form.label.phone-number")}</FormLabel>
121175
<FormControl>
122-
<Input placeholder="Enter your phone number" {...field} />
176+
<Input placeholder={t("form.placeholder.phone-number")} {...field} />
123177
</FormControl>
124178
<FormMessage />
125179
</FormItem>
@@ -131,9 +185,9 @@ const ProfileForm = ({ activeTab }: ProfileFormProps) => {
131185
name="address"
132186
render={({ field }) => (
133187
<FormItem>
134-
<FormLabel>Address</FormLabel>
188+
<FormLabel>{t("form.label.address")}</FormLabel>
135189
<FormControl>
136-
<Input placeholder="Enter your address" {...field} />
190+
<Input placeholder={t("form.placeholder.address")} {...field} />
137191
</FormControl>
138192
<FormMessage />
139193
</FormItem>
@@ -150,9 +204,9 @@ const ProfileForm = ({ activeTab }: ProfileFormProps) => {
150204
name="github"
151205
render={({ field }) => (
152206
<FormItem>
153-
<FormLabel>GitHub</FormLabel>
207+
<FormLabel>{t("form.label.github")}</FormLabel>
154208
<FormControl>
155-
<Input placeholder="Enter your github account" {...field} />
209+
<Input placeholder={t("form.placeholder.github")} {...field} />
156210
</FormControl>
157211
<FormMessage />
158212
</FormItem>
@@ -164,9 +218,9 @@ const ProfileForm = ({ activeTab }: ProfileFormProps) => {
164218
name="linkedin"
165219
render={({ field }) => (
166220
<FormItem>
167-
<FormLabel>LinkedIn</FormLabel>
221+
<FormLabel>{t("form.label.linkedin")}</FormLabel>
168222
<FormControl>
169-
<Input placeholder="Enter your linkedin account" {...field} />
223+
<Input placeholder={t("form.placeholder.linkedin")} {...field} />
170224
</FormControl>
171225
<FormMessage />
172226
</FormItem>
@@ -178,9 +232,9 @@ const ProfileForm = ({ activeTab }: ProfileFormProps) => {
178232
name="personal_web"
179233
render={({ field }) => (
180234
<FormItem>
181-
<FormLabel>Personal Website</FormLabel>
235+
<FormLabel>{t("form.label.personal-web")}</FormLabel>
182236
<FormControl>
183-
<Input placeholder="Enter your personal website" {...field} />
237+
<Input placeholder={t("form.placeholder.personal-web")} {...field} />
184238
</FormControl>
185239
<FormMessage />
186240
</FormItem>
@@ -195,7 +249,7 @@ const ProfileForm = ({ activeTab }: ProfileFormProps) => {
195249
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
196250
{renderForm()}
197251
<Button type="submit" className="bg-hmc-base-darkblue dark:bg-hmc-base-lightblue w-full text-white sm:w-auto">
198-
Save Profile
252+
{t("form.save-button")}
199253
</Button>
200254
</form>
201255
</Form>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as useGetProfile } from "./useGetProfile";
2+
export { default as useUpdateProfile } from "./useUpdateProfile";
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { profileService } from "@/services/profile";
2+
import { useQuery } from "@tanstack/react-query";
3+
4+
const useGetProfile = () =>
5+
useQuery({
6+
queryKey: ["getProfile"],
7+
queryFn: async () => {
8+
const response = await profileService.getProfile();
9+
return response.data;
10+
},
11+
});
12+
13+
export default useGetProfile;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { ProfileFormType } from "@/domains/Profile";
2+
import { profileService } from "@/services/profile";
3+
import { useMutation } from "@tanstack/react-query";
4+
import { toast } from "sonner";
5+
import { useTranslations } from "next-intl";
6+
7+
const useUpdateProfile = () => {
8+
const t = useTranslations("Profile");
9+
10+
return useMutation({
11+
mutationKey: ["updateProfile"],
12+
mutationFn: async (payload: ProfileFormType) => profileService.updateProfile(payload),
13+
onSuccess: () => {
14+
toast.success(t("toast.update-success"));
15+
},
16+
});
17+
};
18+
19+
export default useUpdateProfile;

0 commit comments

Comments
 (0)