update profile

This commit is contained in:
hamid zarghami
2025-12-14 11:07:03 +03:30
parent ec400802d2
commit 2ce6ec6245
4 changed files with 192 additions and 71 deletions
+171 -69
View File
@@ -6,55 +6,161 @@ import ComboBox from '@/components/combobox/Combobox';
import InputField from '@/components/input/InputField'; import InputField from '@/components/input/InputField';
import { PORFILE_EDIT_PAGE_ELEMENT } from '@/enums'; import { PORFILE_EDIT_PAGE_ELEMENT } from '@/enums';
import { Popover, PopoverTrigger, PopoverContent } from '@radix-ui/react-popover'; import { Popover, PopoverTrigger, PopoverContent } from '@radix-ui/react-popover';
// import { useProfile } from '@/hooks/auth/useProfile';
// import { useAuthStore } from '@/zustand/authStore';
import { Calendar2, Camera } from 'iconsax-react'; import { Calendar2, Camera } from 'iconsax-react';
import Image from 'next/image'; import Image from 'next/image';
import React, { ChangeEvent, useState } from 'react' import React, { useEffect, useMemo } from 'react'
import Calendar from '@/components/ui/calendar'; import Calendar from '@/components/ui/calendar';
import { getDateLib } from "react-day-picker/persian"; import { getDateLib } from "react-day-picker/persian";
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useGetProfile, useUpdateProfile } from '../hooks/userProfileData';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { UpdateProfileType } from '../types/Types';
import { toast } from '@/components/Toast';
import { extractErrorMessage } from '@/lib/func';
type Props = object type Props = object
function ProfileIndex({ }: Props) { function ProfileIndex({ }: Props) {
const [fullname, setFullName] = useState("");
const [email, setEmail] = useState(""); const { data, isPending } = useGetProfile();
const [phoneNumber, setPhoneNumber] = useState(""); const { mutate: updateProfile, isPending: isUpdating } = useUpdateProfile();
const [dateOfBirth, setDateOfBirth] = React.useState<Date | undefined>(new Date()); const [showCalendar, setShowCalendar] = React.useState(false);
const [selectedGender, setSelectedGender] = useState("");
const [showCalendar, setShowCalendar] = useState(false);
const dateLib = getDateLib() const dateLib = getDateLib()
const router = useRouter(); const router = useRouter();
const userData = data?.data;
// تابع برای بررسی معتبر بودن Date
const isValidDate = (date: Date | null | undefined): date is Date => {
return date instanceof Date && !isNaN(date.getTime());
};
// تابع برای تبدیل string تاریخ به Date بدون مشکل timezone
const parseLocalDate = (dateString: string): Date | undefined => {
try {
if (!dateString) return undefined;
// اگر تاریخ به فرمت ISO یا کامل است، ابتدا به Date تبدیل می‌کنیم
const date = new Date(dateString);
if (isValidDate(date)) {
// برای جلوگیری از مشکل timezone، فقط سال، ماه و روز را می‌گیریم
const year = date.getFullYear();
const month = date.getMonth();
const day = date.getDate();
return new Date(year, month, day);
}
// اگر فرمت ISO کار نکرد، سعی می‌کنیم فرمت YYYY-MM-DD را parse کنیم
const parts = dateString.split('T')[0].split('-');
if (parts.length === 3) {
const [year, month, day] = parts.map(Number);
if (!isNaN(year) && !isNaN(month) && !isNaN(day)) {
const parsedDate = new Date(year, month - 1, day);
if (isValidDate(parsedDate)) {
return parsedDate;
}
}
}
return undefined;
} catch {
return undefined;
}
};
// تابع برای تبدیل Date به string تاریخ
const formatDateToString = (date: Date): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
// تابع برای format کردن تاریخ با dateLib
const formatDateForDisplay = (date: Date | null | undefined): string => {
if (!date || !isValidDate(date)) {
return "انتخاب کنید";
}
try {
return dateLib.format(date, "yyyy/MM/dd");
} catch {
return "انتخاب کنید";
}
};
type FormValues = {
firstName: string;
lastName: string;
birthDate?: Date | null;
gender?: boolean;
};
const validationSchema: Yup.ObjectSchema<FormValues> = Yup.object({
firstName: Yup.string().required('نام اجباری است'),
lastName: Yup.string().required('نام خانوادگی اجباری است'),
}) as Yup.ObjectSchema<FormValues>;
const formik = useFormik<FormValues>({
initialValues: {
firstName: '',
lastName: '',
birthDate: undefined,
gender: undefined,
},
validationSchema,
enableReinitialize: true,
onSubmit: (values) => {
const { birthDate, ...submitValues } = values;
const payload: UpdateProfileType = {
firstName: submitValues.firstName,
lastName: submitValues.lastName,
birthDate: birthDate ? formatDateToString(birthDate) : undefined,
gender: submitValues.gender,
};
updateProfile(payload, {
onSuccess: () => {
toast('اطلاعات با موفقیت به‌روزرسانی شد', 'success');
router.back();
},
onError: (error) => {
toast(extractErrorMessage(error), 'error');
},
});
},
});
useEffect(() => {
if (userData) {
const birthDate = userData.birthDate ? parseLocalDate(userData.birthDate) : undefined;
const values: FormValues = {
firstName: userData.firstName || '',
lastName: userData.lastName || '',
birthDate: birthDate,
gender: userData.gender !== undefined ? userData.gender : undefined,
};
formik.setValues(values);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userData]);
const selectedGenderId = useMemo(() => {
if (formik.values.gender === true) return '0';
if (formik.values.gender === false) return '1';
return '';
}, [formik.values.gender]);
const changeGender = (e: React.MouseEvent<HTMLDivElement, MouseEvent>, index: number) => { const changeGender = (e: React.MouseEvent<HTMLDivElement, MouseEvent>, index: number) => {
setSelectedGender(() => String(index)); formik.setFieldValue('gender', index === 0);
} }
// const isAuthenticated = useAuthStore((state) => state.isAuthenticated); if (isPending) {
return (
// const { mutate, data, isPending } = useProfile(); <div className='h-full noscrollbar overflow-y-auto flex flex-col items-center justify-center'>
<p className='text-sm2 text-disabled-text'>در حال بارگذاری...</p>
// useEffect(() => { </div>
// if (isAuthenticated) { );
// mutate();
// }
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [isAuthenticated])
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.name == PORFILE_EDIT_PAGE_ELEMENT.INPUT_FULLNAME) {
setFullName(() => e.target.value)
}
else if (e.target.name == PORFILE_EDIT_PAGE_ELEMENT.INPUT_EMAIL) {
setEmail(() => e.target.value)
}
else if (e.target.name == PORFILE_EDIT_PAGE_ELEMENT.INPUT_PHONE) {
setPhoneNumber(() => e.target.value)
}
// else if (e.target.name == PORFILE_EDIT_PAGE_ELEMENT.INPUT_DOB) {
// setDateOfBirth(() => e.target.value)
// }
} }
return ( return (
@@ -64,16 +170,13 @@ function ProfileIndex({ }: Props) {
<h1 className='text-sm2 place-self-center font-medium'>ویرایش اطلاعات</h1> <h1 className='text-sm2 place-self-center font-medium'>ویرایش اطلاعات</h1>
</div> </div>
<div className="mt-8 flex-1 bg-container rounded-container w-full box-shadow-normal py-6 px-4 flex flex-col justify-between"> <form onSubmit={formik.handleSubmit} className="mt-8 flex-1 bg-container rounded-container w-full box-shadow-normal py-6 px-4 flex flex-col justify-between">
<div className="bg-inherit"> <div className="bg-inherit">
<div className="justify-self-center w-fit relative text-center pb-10" <div className="justify-self-center w-fit relative text-center pb-10">
> <button type="button">
<button>
<Image <Image
src={'/assets/images/user-avatar.png'} src={'/assets/images/user-avatar.png'}
className='rounded-full' className='rounded-full'
alt='user avatar' alt='user avatar'
width={96} width={96}
height={96} height={96}
@@ -90,39 +193,37 @@ function ProfileIndex({ }: Props) {
<div className="grid gap-6 mt-6 bg-inherit"> <div className="grid gap-6 mt-6 bg-inherit">
<InputField <InputField
onChange={onChange} name="firstName"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
className='bg-inherit' className='bg-inherit'
labelText='نام و نام خانوادگی' labelText='نام'
htmlFor={PORFILE_EDIT_PAGE_ELEMENT.INPUT_FULLNAME} htmlFor="firstName"
value={fullname} value={formik.values.firstName}
valid={!(formik.touched.firstName && formik.errors.firstName)}
aria-errormessage={formik.touched.firstName && formik.errors.firstName ? formik.errors.firstName : undefined}
/> />
<InputField <InputField
onChange={onChange} name="lastName"
dir='ltr' onChange={formik.handleChange}
type='email' onBlur={formik.handleBlur}
className='bg-inherit' className='bg-inherit'
labelText='ایمیل' labelText='نام خانوادگی'
htmlFor={PORFILE_EDIT_PAGE_ELEMENT.INPUT_EMAIL} htmlFor="lastName"
value={email} value={formik.values.lastName}
/> valid={!(formik.touched.lastName && formik.errors.lastName)}
<InputField aria-errormessage={formik.touched.lastName && formik.errors.lastName ? formik.errors.lastName : undefined}
onChange={onChange}
dir='ltr'
type='number'
className='bg-inherit'
labelText='شماره همراه'
htmlFor={PORFILE_EDIT_PAGE_ELEMENT.INPUT_PHONE}
value={phoneNumber}
/> />
<Popover open={showCalendar} onOpenChange={setShowCalendar}> <Popover open={showCalendar} onOpenChange={setShowCalendar}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<ShadButton <ShadButton
type="button"
variant="outline" variant="outline"
id="date" id="date"
className="w-full bg-container text-border dark:text-border justify-between font-normal relative h-11 rounded-normal hover:bg-white" className="w-full bg-container text-border dark:text-border justify-between font-normal relative h-11 rounded-normal hover:bg-white"
> >
<div className='text-foreground'> <div className='text-foreground'>
{dateOfBirth ? dateLib.format(dateOfBirth, "yyyy/MM/dd") : "انتخاب کنید"} {formatDateForDisplay(formik.values.birthDate)}
</div> </div>
<Calendar2 className='stroke-disabled2 dark:stroke-foreground' size={24} /> <Calendar2 className='stroke-disabled2 dark:stroke-foreground' size={24} />
<span className='absolute text-foreground text-xs top-0 -translate-y-2 right-2 bg-container px-2'>تاریخ تولد</span> <span className='absolute text-foreground text-xs top-0 -translate-y-2 right-2 bg-container px-2'>تاریخ تولد</span>
@@ -132,26 +233,24 @@ function ProfileIndex({ }: Props) {
<Calendar <Calendar
className='bg-white dark:bg-background z-20 min-w-64 rounded-lg' className='bg-white dark:bg-background z-20 min-w-64 rounded-lg'
mode="single" mode="single"
defaultMonth={dateOfBirth} defaultMonth={formik.values.birthDate || undefined}
selected={dateOfBirth} selected={formik.values.birthDate || undefined}
captionLayout="dropdown" captionLayout="dropdown"
onSelect={(date) => { onSelect={(date) => {
setDateOfBirth(date) formik.setFieldValue('birthDate', date || undefined);
setShowCalendar(false) setShowCalendar(false);
}} }}
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<div className='relative'> <div className='relative'>
<ComboBox <ComboBox
searchable={false} searchable={false}
title="" title=""
options={[{ id: '0', title: 'آقا', label: 'آقا' }, { id: '1', title: 'خانوم', label: 'خانوم' }]} options={[{ id: '0', title: 'آقا', label: 'آقا' }, { id: '1', title: 'خانوم', label: 'خانوم' }]}
id={PORFILE_EDIT_PAGE_ELEMENT.INPUT_GENDER} id={PORFILE_EDIT_PAGE_ELEMENT.INPUT_GENDER}
selectedId={selectedGender} selectedId={selectedGenderId}
onSelectionChange={changeGender} /> onSelectionChange={changeGender} />
<span className='absolute text-xs top-0 -translate-y-2 right-2 bg-container px-2'>جنسیت</span> <span className='absolute text-xs top-0 -translate-y-2 right-2 bg-container px-2'>جنسیت</span>
</div> </div>
@@ -159,15 +258,18 @@ function ProfileIndex({ }: Props) {
</div> </div>
<div className='w-full text-center mt-16 grid grid-cols-2 gap-4'> <div className='w-full text-center mt-16 grid grid-cols-2 gap-4'>
<Button>ذخیره</Button> <Button type="submit" disabled={isUpdating || !formik.isValid}>
{isUpdating ? 'در حال ذخیره...' : 'ذخیره'}
</Button>
<Button <Button
type="button"
className='bg-neutral-200! dark:bg-neutral-600! text-disabled-text!' className='bg-neutral-200! dark:bg-neutral-600! text-disabled-text!'
onClick={() => router.back()} onClick={() => router.back()}
> >
انصراف انصراف
</Button> </Button>
</div> </div>
</div> </form>
</div> </div>
) )
} }
@@ -1,4 +1,4 @@
import { useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import * as api from "../service/ProfileService"; import * as api from "../service/ProfileService";
import { getToken } from "@/lib/api/func"; import { getToken } from "@/lib/api/func";
@@ -10,3 +10,9 @@ export const useGetProfile = () => {
enabled: !!getToken(), enabled: !!getToken(),
}); });
}; };
export const useUpdateProfile = () => {
return useMutation({
mutationFn: api.updateProfile,
});
};
@@ -1,7 +1,12 @@
import { api } from "@/config/axios"; import { api } from "@/config/axios";
import { GetProfileResponse } from "../types/Types"; import { GetProfileResponse, UpdateProfileType } from "../types/Types";
export const getProfile = async (): Promise<GetProfileResponse> => { export const getProfile = async (): Promise<GetProfileResponse> => {
const { data } = await api.get<GetProfileResponse>("/public/user/me"); const { data } = await api.get<GetProfileResponse>("/public/user/me");
return data; return data;
}; };
export const updateProfile = async (params: UpdateProfileType) => {
const { data } = await api.patch("/public/user/update", params);
return data;
};
@@ -21,3 +21,11 @@ export interface GetProfileResponse {
success: boolean; success: boolean;
data: UserProfile; data: UserProfile;
} }
export interface UpdateProfileType {
firstName: string;
lastName: string;
birthDate?: string;
marriageDate?: string;
gender?: boolean;
}