From 53e999f5a95010e1012fa2832ee1ca9b4955793b Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Mon, 22 Dec 2025 02:58:50 +0330 Subject: [PATCH 1/3] feat: enhance profile editing with occupation and interests selectors - Updated SectionPanel component to include icon rendering in the header. - Adjusted TabManager component to increase height for better layout. - Modified NavbarLayout to improve backdrop blur effect. - Refactored ProfileDatePicker to streamline date selection and improve usability. - Introduced InterestsSelector and OccupationSelector components for better user interaction. - Enhanced ProfileEditForm to integrate new selectors and manage user interests and occupations. - Added hooks for fetching profile meta data (occupations and interests). - Updated user profile interface to include occupation and interests fields. --- src/components/modal.tsx | 4 +- src/components/section-panel.tsx | 4 +- src/components/tab-manager.tsx | 2 +- src/layouts/navbar/navbar.layout.tsx | 2 +- .../account/components/ProfileDatePicker.tsx | 406 +++++++++++------- .../account/components/interests-selector.tsx | 92 ++++ .../components/occupation-selector.tsx | 88 ++++ .../account/components/profile-edit-form.tsx | 399 ++++++++++------- .../hooks/profile/getProfileMeta.hook.ts | 40 ++ src/services/hooks/user/userService.hook.ts | 3 + 10 files changed, 738 insertions(+), 302 deletions(-) create mode 100644 src/layouts/setting/tabs/account/components/interests-selector.tsx create mode 100644 src/layouts/setting/tabs/account/components/occupation-selector.tsx create mode 100644 src/services/hooks/profile/getProfileMeta.hook.ts diff --git a/src/components/modal.tsx b/src/components/modal.tsx index 2cd16a83..54140818 100644 --- a/src/components/modal.tsx +++ b/src/components/modal.tsx @@ -28,7 +28,7 @@ const sizeClasses = { h: 'max-h-[85vh]', }, xl: { - w: 'max-w-4xl', + w: 'max-w-6xl h-[90%]', h: 'max-h-[90vh]', }, } as const @@ -73,7 +73,7 @@ export default function Modal({ aria-labelledby={typeof title === 'string' ? title : undefined} aria-modal="true" onClick={() => closeOnBackdropClick && onClose()} - className={`modal modal-middle flex items-center justify-center transition-opacity duration-200 ${ + className={`modal modal-middle flex items-center justify-center transition-opacity duration-200 ${ isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none' }`} > diff --git a/src/components/section-panel.tsx b/src/components/section-panel.tsx index 464d5fe5..eb6e6e57 100644 --- a/src/components/section-panel.tsx +++ b/src/components/section-panel.tsx @@ -47,11 +47,11 @@ export function SectionPanel({ title, children, size = 'md', icon }: SectionPane return (
-
+
+ {icon && React.cloneElement(icon, {})}

{title}

- {icon && React.cloneElement(icon, {})}
{children}
diff --git a/src/components/tab-manager.tsx b/src/components/tab-manager.tsx index aa146e1c..7ef7798d 100644 --- a/src/components/tab-manager.tsx +++ b/src/components/tab-manager.tsx @@ -65,7 +65,7 @@ export const TabManager = ({ const headClass = tabPosition === 'top' ? 'flex-col gap-1 h-[80vh]' - : 'flex-col md:flex-row gap-4 h-[60vh]' + : 'flex-col md:flex-row gap-4 h-[80vh]' const contentClass = tabPosition === 'top' ? 'shrink-0 md:overflow-y-auto w-full' diff --git a/src/layouts/navbar/navbar.layout.tsx b/src/layouts/navbar/navbar.layout.tsx index f7477a06..76b6591d 100644 --- a/src/layouts/navbar/navbar.layout.tsx +++ b/src/layouts/navbar/navbar.layout.tsx @@ -123,7 +123,7 @@ export function NavbarLayout(): JSX.Element { : '-bottom-32 opacity-0 scale-95 pointer-events-none' }`} > -
+ } + position="bottom" + offset={8} + contentClassName="p-4 border shadow-xl bg-glass bg-content border-base-300/20 rounded-2xl max-w-none" + closeOnClickOutside={true} + /> + + ) +} + +interface ScrollWheelProps { + label: string + value: number + max: number + onChange: (value: number) => void + type: 'number' | 'month' | 'year' + startYear?: number +} + +function ScrollWheel({ label, value, max, onChange, type, startYear }: ScrollWheelProps) { + const containerRef = useRef(null) + const scrollTimeoutRef = useRef(null) + const ITEM_HEIGHT = 40 + + const items = + type === 'number' + ? Array.from({ length: max }, (_, i) => i + 1) + : type === 'month' + ? PERSIAN_MONTHS + : Array.from({ length: max }, (_, i) => (startYear || 1403) - i) + + useEffect(() => { + if (!containerRef.current) return + + let index = 0 + if (type === 'number') { + index = value - 1 + } else if (type === 'month') { + index = value - 1 + } else if (type === 'year') { + // @ts-expect-error + index = items.indexOf(value) } - } - const handleYearChange = (selectedYear: string) => { - setYear(selectedYear) + containerRef.current.scrollTop = index * ITEM_HEIGHT + }, [value, type, items]) - if (selectedYear && month && day) { - const maxDays = getJalaliDaysInMonth( - parseInt(selectedYear, 10), - parseInt(month, 10) - ) - const currentDay = parseInt(day, 10) + const handleScroll = () => { + if (!containerRef.current) return - if (currentDay > maxDays) { - setDay('1') - updateDate(selectedYear, month, '1') - } else { - updateDate(selectedYear, month, day) - } + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current) } - } - const handleMonthChange = (selectedMonth: string) => { - setMonth(selectedMonth) + scrollTimeoutRef.current = setTimeout(() => { + if (!containerRef.current) return - if (year && selectedMonth && day) { - const maxDays = getJalaliDaysInMonth( - parseInt(year, 10), - parseInt(selectedMonth, 10) - ) - const currentDay = parseInt(day, 10) + const scrollTop = containerRef.current.scrollTop + const index = Math.round(scrollTop / ITEM_HEIGHT) + const clampedIndex = Math.max(0, Math.min(index, items.length - 1)) - if (currentDay > maxDays) { - setDay('1') - updateDate(year, selectedMonth, '1') + let newValue: number + if (type === 'number' || type === 'month') { + newValue = clampedIndex + 1 } else { - updateDate(year, selectedMonth, day) + newValue = items[clampedIndex] as number } - } - } - const handleDayChange = (selectedDay: string) => { - setDay(selectedDay) - updateDate(year, month, selectedDay) + onChange(newValue) + + containerRef.current.scrollTo({ + top: clampedIndex * ITEM_HEIGHT, + behavior: 'smooth', + }) + }, 150) } - const availableDays = - year && month - ? getJalaliDaysInMonth(Number.parseInt(year, 10), Number.parseInt(month, 10)) - : 31 - - const yearOptions = [ - { value: '', label: 'انتخاب سال' }, - ...JALALI_YEARS.map((y) => ({ value: y.toString(), label: y.toString() })), - ] - - const monthOptions = [ - { value: '', label: 'انتخاب ماه' }, - ...JALALI_MONTHS.map((monthName, index) => ({ - value: (index + 1).toString(), - label: monthName, - })), - ] - - const dayOptions = [ - { value: '', label: 'انتخاب روز' }, - ...Array.from({ length: availableDays }, (_, i) => ({ - value: (i + 1).toString(), - label: (i + 1).toString(), - })), - ] + const handleItemClick = (index: number) => { + let newValue: number + if (type === 'number' || type === 'month') { + newValue = index + 1 + } else { + newValue = items[index] as number + } + onChange(newValue) + } return ( -
- - -
- - - - - +
+
+ +
e.stopPropagation()} + > +
+ {items.map((item, index) => { + let isActive = false + if (type === 'number' || type === 'month') { + isActive = index + 1 === value + } else { + isActive = item === value + } + + return ( +
handleItemClick(index)} + className="flex items-center justify-center transition-all cursor-pointer" + style={{ height: `${ITEM_HEIGHT}px` }} + > + + {item} + +
+ ) + })} +
+ +
+
) } diff --git a/src/layouts/setting/tabs/account/components/interests-selector.tsx b/src/layouts/setting/tabs/account/components/interests-selector.tsx new file mode 100644 index 00000000..267972c2 --- /dev/null +++ b/src/layouts/setting/tabs/account/components/interests-selector.tsx @@ -0,0 +1,92 @@ +import { useState, useRef } from 'react' +import { ClickableTooltip } from '@/components/clickableTooltip' +import type { ProfileMetaItem } from '@/services/hooks/profile/getProfileMeta.hook' + +interface InterestsSelectorProps { + interests: ProfileMetaItem[] + selectedInterests: string[] + onSelect: (interestIds: string[]) => void + isLoading?: boolean + triggerElement: React.ReactNode +} + +export const InterestsSelector = ({ + interests, + selectedInterests, + onSelect, + isLoading = false, + triggerElement, +}: InterestsSelectorProps) => { + const [isOpen, setIsOpen] = useState(false) + const triggerRef = useRef(null) + + const handleInterestToggle = (interestId: string) => { + const isSelected = selectedInterests.includes(interestId) + if (isSelected) { + onSelect(selectedInterests.filter((id) => id !== interestId)) + } else if (selectedInterests.length < 3) { + onSelect([...selectedInterests, interestId]) + } + } + + const content = ( +
+ {isLoading ? ( +
+ صبر کنید... +
+ ) : ( +
+ {interests.map((interest) => { + const isSelected = selectedInterests.includes(interest.id) + const canSelect = selectedInterests.length < 3 || isSelected + + return ( + + ) + })} +
+ )} +
+ ) + + return ( + <> + + + + + ) +} diff --git a/src/layouts/setting/tabs/account/components/occupation-selector.tsx b/src/layouts/setting/tabs/account/components/occupation-selector.tsx new file mode 100644 index 00000000..6d5d5e5a --- /dev/null +++ b/src/layouts/setting/tabs/account/components/occupation-selector.tsx @@ -0,0 +1,88 @@ +import { useState, useRef } from 'react' +import { ClickableTooltip } from '@/components/clickableTooltip' +import type { ProfileMetaItem } from '@/services/hooks/profile/getProfileMeta.hook' + +interface OccupationSelectorProps { + occupations: ProfileMetaItem[] + selectedOccupation: string | null + onSelect: (occupationId: string | null) => void + isLoading?: boolean + triggerElement: React.ReactNode +} + +export const OccupationSelector = ({ + occupations, + selectedOccupation, + onSelect, + isLoading = false, + triggerElement, +}: OccupationSelectorProps) => { + const [isOpen, setIsOpen] = useState(false) + const triggerRef = useRef(null) + + const handleSelect = (occupationId: string) => { + if (selectedOccupation === occupationId) { + onSelect(null) + } else { + onSelect(occupationId) + } + setIsOpen(false) + } + + const content = ( +
+ {isLoading ? ( +
+ درحال بارگذاری... +
+ ) : ( +
+ {occupations.map((occupation) => { + const isActive = selectedOccupation === occupation.id + return ( + + ) + })} +
+ )} +
+ ) + + return ( + <> + + + + + ) +} diff --git a/src/layouts/setting/tabs/account/components/profile-edit-form.tsx b/src/layouts/setting/tabs/account/components/profile-edit-form.tsx index aea38552..00b7b326 100644 --- a/src/layouts/setting/tabs/account/components/profile-edit-form.tsx +++ b/src/layouts/setting/tabs/account/components/profile-edit-form.tsx @@ -1,19 +1,28 @@ import type { AxiosError } from 'axios' import moment from 'jalali-moment' import { useEffect, useRef, useState } from 'react' +import { LuCamera, LuUser, LuBriefcase, LuHeart, LuChevronRight } from 'react-icons/lu' import { AvatarComponent } from '@/components/avatar.component' import { Button } from '@/components/button/button' import { ItemSelector } from '@/components/item-selector' import { TextInput } from '@/components/text-input' +import { OccupationSelector } from '@/layouts/setting/tabs/account/components/occupation-selector' +import { InterestsSelector } from '@/layouts/setting/tabs/account/components/interests-selector' import { safeAwait } from '@/services/api' import { useUpdateUsername, useUpdateUserProfile, } from '@/services/hooks/auth/authService.hook' import type { UserProfile } from '@/services/hooks/user/userService.hook' +import { + useGetOccupations, + useGetInterests, +} from '@/services/hooks/profile/getProfileMeta.hook' import { translateError } from '@/utils/translate-error' import JalaliDatePicker from './ProfileDatePicker' import { showToast } from '@/common/toast' +import { SectionPanel } from '@/components/section-panel' +import { FaMars, FaQuestion, FaVenus } from 'react-icons/fa' interface ProfileEditFormProps { profile?: UserProfile @@ -21,6 +30,21 @@ interface ProfileEditFormProps { onSuccess: () => void } +const options = { + MALE: { + label: 'آقا هستم', + icon: , + }, + FEMALE: { + label: 'خانم هستم', + icon: , + }, + OTHER: { + label: 'بماند', + icon: , + }, +} + export const ProfileEditForm = ({ profile, onCancel, @@ -32,13 +56,17 @@ export const ProfileEditForm = ({ birthdate: '', avatar: null as File | null, username: null as string | null, + occupation: null as string | null, + interests: [] as string[], }) const [error, setError] = useState(null) const updateProfileMutation = useUpdateUserProfile() const updateUsernameMutation = useUpdateUsername() - + const { data: occupations = [], isLoading: occupationsLoading } = useGetOccupations() + const { data: interests = [], isLoading: interestsLoading } = useGetInterests() const avatarRef = useRef(null) + useEffect(() => { setFormData({ name: profile?.name || '', @@ -46,6 +74,8 @@ export const ProfileEditForm = ({ birthdate: profile?.birthDate || '', avatar: null, username: profile?.username || null, + occupation: profile?.occupation || null, + interests: profile?.interests || [], }) }, [profile]) @@ -53,7 +83,6 @@ export const ProfileEditForm = ({ e.preventDefault() const data = new FormData() data.append('name', formData.name) - if (formData.gender) data.append('gender', formData.gender) if (formData.birthdate) { const gregorianDate = moment(formData.birthdate, 'jYYYY-jMM-jDD') @@ -62,31 +91,25 @@ export const ProfileEditForm = ({ data.append('birthdate', gregorianDate) } if (formData.avatar) data.append('avatar', formData.avatar) + if (formData.occupation) data.append('occupationId', formData.occupation) + + formData.interests.map((id) => data.append('interestIds[]', id)) try { if (formData.username && formData.username !== profile?.username) { const usernameRegex = /^[a-zA-Z0-9_]{4,250}$/ if (!usernameRegex.test(formData.username)) { - setError( - 'نام کاربری باید فقط شامل حروف انگلیسی، اعداد و زیرخط باشد و بین 4 تا 250 کاراکتر باشد.' - ) + setError('نام کاربری نامعتبر است') return } - - const [err, response] = await safeAwait( + const [err, _] = await safeAwait( updateUsernameMutation.mutateAsync(formData.username) ) if (err) { errorHandling(err, setError) return - } else { - setFormData((prev) => ({ - ...prev, - username: response.username || '', - })) } } - await updateProfileMutation.mutateAsync(data) setError(null) onSuccess() @@ -98,162 +121,245 @@ export const ProfileEditForm = ({ const onChangeAvatar = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file && file.size > 1024 * 1024) { - showToast('فایل باید کمتر از 1 مگابایت باشد', 'error') + showToast('فایل بزرگتر از ۱ مگابایت است', 'error') return } - const validTypes = ['image/png', 'image/jpeg', 'image/webp'] if (file && !validTypes.includes(file.type)) { - showToast('فقط فرمت‌های png، jpeg و webp مجاز هستند', 'error') + showToast('فرمت فایل نامعتبر است', 'error') return } - - setFormData((prev) => ({ - ...prev, - avatar: file || null, - })) + setFormData((prev) => ({ ...prev, avatar: file || null })) } - const onChangeGender = (gender: 'MALE' | 'FEMALE' | 'OTHER') => { - setFormData((prev) => ({ - ...prev, - gender, - })) - } + return ( +
+
- const onChangeBirthdate = (date: any) => { - setFormData((prev) => ({ - ...prev, - birthdate: date, - })) - } +
+
+
+
+ avatarRef.current?.click()} + /> +
+ + +
+
- return ( -
-
{error && ( -
-

{error}

+
+ {error}
)} +
-
-
- - - setFormData((prev) => ({ - ...prev, - name: value, - })) - } - className="w-full" - /> + } + size="xs" + > +
+
+ + + setFormData((p) => ({ ...p, name: val })) + } + /> +
+
+ + + setFormData((p) => ({ ...p, username: val })) + } + /> +
-
-
-
- onChangeBirthdate(val)} - /> - {!profile?.isBirthDateEditable && ( -

- تاریخ تولد شما به تازگی ویرایش شده، کمی صبر کنید. -

- )} -
-
- -
- onChangeGender('MALE')} - /> - onChangeGender('FEMALE')} - /> - onChangeGender('OTHER')} />
-
+
-
- - - { - avatarRef.current?.click() - }} - /> -
-
+
@@ -264,16 +370,11 @@ export const ProfileEditForm = ({ function errorHandling(err: AxiosError, setError: any) { if (err.response) { - const translate: string | Record = translateError(err) - if (typeof translate === 'string') { - setError(translate) - } else { - const keys = Object.keys(translate) - const messages: Array = [] - keys.forEach((key) => { - messages.push(`${key}: ${translate[key]}`) - }) - setError(messages.join('\n')) - } + const translate = translateError(err) + setError( + typeof translate === 'string' + ? translate + : Object.values(translate).join('\n') + ) } } diff --git a/src/services/hooks/profile/getProfileMeta.hook.ts b/src/services/hooks/profile/getProfileMeta.hook.ts new file mode 100644 index 00000000..229e1dd9 --- /dev/null +++ b/src/services/hooks/profile/getProfileMeta.hook.ts @@ -0,0 +1,40 @@ +import { useQuery } from '@tanstack/react-query' +import { getMainClient } from '@/services/api' + +export interface ProfileMetaItem { + id: string + type: 'OCCUPATION' | 'INTEREST' + title: string + slug: string + isActive: boolean + order: number + createdAt: string +} + +async function fetchProfileMeta( + type: 'OCCUPATION' | 'INTEREST' +): Promise { + const client = await getMainClient() + const response = await client.get(`/profile-meta?type=${type}`) + return response.data +} + +export function useGetOccupations(enabled: boolean = true) { + return useQuery({ + queryKey: ['profile-meta-occupations'], + queryFn: () => fetchProfileMeta('OCCUPATION'), + enabled, + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes + }) +} + +export function useGetInterests(enabled: boolean = true) { + return useQuery({ + queryKey: ['profile-meta-interests'], + queryFn: () => fetchProfileMeta('INTEREST'), + enabled, + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes + }) +} diff --git a/src/services/hooks/user/userService.hook.ts b/src/services/hooks/user/userService.hook.ts index fafa15d1..6829358a 100644 --- a/src/services/hooks/user/userService.hook.ts +++ b/src/services/hooks/user/userService.hook.ts @@ -33,6 +33,9 @@ interface FetchedProfile { city?: { id: string } + + occupation: string + interests: string[] } export interface UserProfile extends FetchedProfile { From fea1ed0e133a9d1581fc88c07205309ba72d2dc5 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Mon, 22 Dec 2025 02:59:17 +0330 Subject: [PATCH 2/3] feat: streamline imports in ProfileDatePicker and ProfileEditForm components --- .../tabs/account/components/ProfileDatePicker.tsx | 11 ++--------- .../tabs/account/components/profile-edit-form.tsx | 3 +-- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/layouts/setting/tabs/account/components/ProfileDatePicker.tsx b/src/layouts/setting/tabs/account/components/ProfileDatePicker.tsx index b1482411..4eab16ae 100644 --- a/src/layouts/setting/tabs/account/components/ProfileDatePicker.tsx +++ b/src/layouts/setting/tabs/account/components/ProfileDatePicker.tsx @@ -2,12 +2,7 @@ import { Button } from '@/components/button/button' import { ClickableTooltip } from '@/components/clickableTooltip' import { useRef, useState, useEffect } from 'react' import { FiCheck } from 'react-icons/fi' -import { - LuBriefcase, - LuCalendarDays, - LuChevronDown, - LuChevronRight, -} from 'react-icons/lu' +import { LuCalendarDays, LuChevronRight } from 'react-icons/lu' const PERSIAN_MONTHS = [ 'فروردین', @@ -35,8 +30,6 @@ interface JalaliDatePickerProps { } export default function JalaliDatePicker({ - id, - label, value, onChange, enable, @@ -196,7 +189,7 @@ interface ScrollWheelProps { startYear?: number } -function ScrollWheel({ label, value, max, onChange, type, startYear }: ScrollWheelProps) { +function ScrollWheel({ value, max, onChange, type, startYear }: ScrollWheelProps) { const containerRef = useRef(null) const scrollTimeoutRef = useRef(null) const ITEM_HEIGHT = 40 diff --git a/src/layouts/setting/tabs/account/components/profile-edit-form.tsx b/src/layouts/setting/tabs/account/components/profile-edit-form.tsx index 00b7b326..d1f4f1ae 100644 --- a/src/layouts/setting/tabs/account/components/profile-edit-form.tsx +++ b/src/layouts/setting/tabs/account/components/profile-edit-form.tsx @@ -1,10 +1,9 @@ import type { AxiosError } from 'axios' import moment from 'jalali-moment' import { useEffect, useRef, useState } from 'react' -import { LuCamera, LuUser, LuBriefcase, LuHeart, LuChevronRight } from 'react-icons/lu' +import { LuCamera, LuUser, LuBriefcase, LuChevronRight } from 'react-icons/lu' import { AvatarComponent } from '@/components/avatar.component' import { Button } from '@/components/button/button' -import { ItemSelector } from '@/components/item-selector' import { TextInput } from '@/components/text-input' import { OccupationSelector } from '@/layouts/setting/tabs/account/components/occupation-selector' import { InterestsSelector } from '@/layouts/setting/tabs/account/components/interests-selector' From 67c24ca6f5dffafd63233a1d6ff79bb8690dbf6f Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Dec 2025 01:59:23 +0330 Subject: [PATCH 3/3] feat: enhance profile display and edit form with additional fields and improved layout --- .../account/components/profile-display.tsx | 257 +++++++++--------- .../account/components/profile-edit-form.tsx | 4 +- src/services/hooks/user/userService.hook.ts | 11 +- 3 files changed, 142 insertions(+), 130 deletions(-) diff --git a/src/layouts/setting/tabs/account/components/profile-display.tsx b/src/layouts/setting/tabs/account/components/profile-display.tsx index 35955433..dcd24291 100644 --- a/src/layouts/setting/tabs/account/components/profile-display.tsx +++ b/src/layouts/setting/tabs/account/components/profile-display.tsx @@ -1,6 +1,7 @@ import moment from 'jalali-moment' import { BsGenderAmbiguous, BsGenderFemale, BsGenderMale } from 'react-icons/bs' -import { FiCalendar, FiEdit, FiMail } from 'react-icons/fi' +import { FiCalendar, FiEdit, FiMail, FiUser } from 'react-icons/fi' +import { LuBriefcase, LuHeart } from 'react-icons/lu' import { AvatarComponent } from '@/components/avatar.component' import { Button } from '@/components/button/button' import { OfflineIndicator } from '@/components/offline-indicator' @@ -13,151 +14,155 @@ interface ProfileDisplayProps { } const getGenderInfo = (gender: 'MALE' | 'FEMALE' | 'OTHER' | null | undefined) => { - if (gender === 'MALE') - return { - label: 'مذکر', - icon: , - } - if (gender === 'FEMALE') - return { - label: 'مؤنث', - icon: , - } - if (gender === 'OTHER') - return { - label: 'نامشخص', - icon: , - } - - return { - label: 'نامشخص', - icon: , - } + if (gender === 'MALE') return { label: 'مذکر', icon: } + if (gender === 'FEMALE') return { label: 'مؤنث', icon: } + return { label: 'نامشخص', icon: } } const formatJalaliDate = (dateString: string | null | undefined): string => { - if (!dateString) return '-' + if (!dateString) return 'تنظیم نشده' try { const jalaliDate = moment(dateString, 'jYYYY-jMM-jDD') - - if (!jalaliDate.isValid()) { - return dateString - } - - return jalaliDate.locale('fa').format('jD jMMMM jYYYY') + return jalaliDate.isValid() + ? jalaliDate.locale('fa').format('jD jMMMM jYYYY') + : dateString } catch { - return dateString + return dateString || '-' } } export const ProfileDisplay = ({ profile, onEditToggle }: ProfileDisplayProps) => { - return ( -
-
-
-
-
+ const genderInfo = getGenderInfo(profile?.gender) -
-
-
-
- + return ( +
+
+ {profile?.joinedAt && ( +
+
+ +
+ + شروعِ ماجرا از + + + {moment(profile.joinedAt) + .locale('fa') + .format('jMMMM jYYYY')} + +
-
+ )} +
+
+ +
+
+
-
-
-
-
-

- {profile?.name || 'کاربر'} -

-

- @{profile?.username || '-'} -

-
- {profile?.coins !== undefined && ( -
- -
- )} -
-
+

+ {profile?.name || 'کاربر'} +

+

+ @{profile?.username || 'username'} +

-
-
-
- -
-
-

- ایمیل -

-

- {profile?.email || '-'} -

-
-
+ {profile?.coins !== undefined && ( +
+ +
+ )} +
-
-
- {getGenderInfo(profile?.gender).icon && ( -
- {getGenderInfo(profile?.gender).icon} -
- )} -
-
-

- جنسیت -

-

- {getGenderInfo(profile?.gender).label} -

-
-
+
+ } + label="نام و نام خانوادگی" + value={profile?.name} + /> -
-
- -
-
-

- تاریخ تولد -

-

- {formatJalaliDate(profile?.birthDate)} -

-
-
-
+ } + label="ایمیل" + value={profile?.email} + isLtr + /> -
- -
-
-
+ {genderInfo.icon}
} + label="جنسیت" + value={genderInfo.label} + /> - {profile?.inCache && ( -
- -
- )} + } + label="تاریخ تولد" + value={formatJalaliDate(profile?.birthDate)} + /> + + } + label="شغل" + value={profile?.occupation?.label} + /> + + } + label="علایق" + value={ + profile?.interests?.map((i) => i.label).join('، ') || + 'انتخاب نشده' + } + />
+ + {/* دکمه ویرایش */} + + + {profile?.inCache && ( +
+ +
+ )}
) } + +const DisplayRow = ({ + icon, + label, + value, + isLtr = false, +}: { + icon: React.ReactNode + label: string + value?: string + isLtr?: boolean +}) => ( +
+
+
+ {icon} +
+ {label} +
+ + {value || '-'} + +
+) diff --git a/src/layouts/setting/tabs/account/components/profile-edit-form.tsx b/src/layouts/setting/tabs/account/components/profile-edit-form.tsx index 9f718c02..05ede312 100644 --- a/src/layouts/setting/tabs/account/components/profile-edit-form.tsx +++ b/src/layouts/setting/tabs/account/components/profile-edit-form.tsx @@ -73,8 +73,8 @@ export const ProfileEditForm = ({ birthdate: profile?.birthDate || '', avatar: null, username: profile?.username || null, - occupation: profile?.occupation || null, - interests: profile?.interests || [], + occupation: profile?.occupation?.id || null, + interests: profile?.interests.map((i) => i.id) || [], }) }, [profile]) diff --git a/src/services/hooks/user/userService.hook.ts b/src/services/hooks/user/userService.hook.ts index 6829358a..49544b7e 100644 --- a/src/services/hooks/user/userService.hook.ts +++ b/src/services/hooks/user/userService.hook.ts @@ -34,8 +34,15 @@ interface FetchedProfile { id: string } - occupation: string - interests: string[] + occupation: { + id: string + label: string + } + interests: Array<{ + id: string + label: string + }> + joinedAt: string } export interface UserProfile extends FetchedProfile {