;
- if (buttonState === 'counting' && countdown > 0) {
- timer = setInterval(() => {
- setCountdown((prev) => prev - 1);
- }, 1000);
- }
- if (countdown === 0) {
- setButtonState('default');
- }
- return () => clearInterval(timer);
- }, [buttonState, countdown]);
-
- const toPersianDigits = (str: string) =>
- str.replace(/\d/g, (d: string) => '۰۱۲۳۴۵۶۷۸۹'[parseInt(d)]);
-
- const getButtonLabel = () => {
- if (buttonState === 'sent') return 'ارسال شد!';
- if (buttonState === 'counting') {
- const minutes = String(Math.floor(countdown / 60)).padStart(2, '0');
- const seconds = String(countdown % 60).padStart(2, '0');
- const time = `${minutes}:${seconds}`;
- return toPersianDigits(time);
- }
- return 'ارسال کد تایید';
- };
-
- return (
-
-
-
-
- تکمیل اطلاعات حساب کاربری
-
-
- اطلاعات کسب و کار خود را وارد کنید
-
-
-
-
-
-
-
-
- جنسیت
-
-
-
-
-
-
-
-
- تعیین رمز عبور
-
-
- {showPassword && (
-
-
- setPassword(e.target.value)}
- variant="outlined"
- sx={{
- '& .MuiOutlinedInput-root': {
- height: 45,
- },
- }}
- />
- {password && (
-
-
- شامل عدد
-
-
-
- حداقل 8 کاراکتر
-
-
-
- شامل یک حرف بزرگ و کوچک
-
-
-
- شامل علامت(!@#$%^&*)
-
-
- )}
-
- {showPassword && (
- setConfirmPassword(e.target.value)}
- error={confirmPassword.length > 0 && !matchPassword}
- helperText={
- confirmPassword.length > 0 && !matchPassword
- ? 'مطابقت ندارد'
- : ' '
- }
- sx={{
- width: '330px',
- '& .MuiOutlinedInput-root': {
- height: 45,
- },
- }}
- />
- )}
-
- )}
-
-
-
-
- اتصال ایمیل خود
-
-
- {showEmail && (
-
-
-
- setEmail(e.target.value)}
- sx={{
- width: '330px',
- '& .MuiOutlinedInput-root': {
- height: 45,
- },
- }}
- />
- {email && (
-
- فرم درست ایمیل وارد کنید
-
- )}
-
-
-
- {codeSent && (
-
- setVerificationCode(e.target.value)}
- sx={{
- width: '330px',
- '& .MuiOutlinedInput-root': {
- height: 45,
- },
- }}
- />
-
-
- )}
-
- )}
-
-
- ادامه فرایند ثبت نام به منزله تایید و قبول{' '}
-
- قوانین و مقررات هارمونی
- {' '}
- می باشد.
-
-
-
-
-
- );
-}
diff --git a/src/features/profile/components/PageWrapper.tsx b/src/features/profile/components/PageWrapper.tsx
index 629aff8..cbb07c0 100644
--- a/src/features/profile/components/PageWrapper.tsx
+++ b/src/features/profile/components/PageWrapper.tsx
@@ -10,7 +10,6 @@ export function PageWrapper({ children }: PageWrapperProps) {
([]);
+ const [loadingDeleteIds, setLoadingDeleteIds] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [fetchError, setFetchError] = useState(null);
- const devices = [
- {
- id: 0,
- timeAndDate: 'دقایقی پیش',
- deviceModel: 'asus i5 24i',
- ip: '192.168.1.1',
- current: true,
- },
- {
- id: 1,
- timeAndDate: '۲۲:۱۳ - ۱۴۰۴/۰۹/۰۹',
- deviceModel: 'Dell XPS 15',
- ip: '89.165.23.12',
- current: false,
- },
- {
- id: 2,
- timeAndDate: '۲۲:۱۳ - ۱۴۰۴/۰۹/۰۹',
- deviceModel: 'Samsung Galaxy S22',
- ip: '10.0.0.5',
- current: false,
- },
- {
- id: 3,
- timeAndDate: '۲۲:۱۳ - ۱۴۰۴/۰۹/۰۹',
- deviceModel: 'MacBook Pro 14-inch',
- ip: '172.16.0.101',
- current: false,
- },
- ];
const theme = useTheme();
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
+ useEffect(() => {
+ const fetchActiveSessions = async () => {
+ setIsLoading(true);
+ setFetchError(null);
+ if (!token) {
+ setIsLoading(false);
+ setFetchError(t('active.notLoggedIn'));
+ return;
+ }
+ try {
+ const res = await apiClient.post(
+ '/Profile/GetProfile',
+ {},
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+
+ if (res.data.success && res.data.activeSessions) {
+ const { sessions, currentKey } = res.data.activeSessions;
+ const formattedDevices = sessions.map((session: ApiSession) => ({
+ id: session.key,
+ timeAndDate: formatSessionDate(session.created, i18n.language, t),
+ deviceModel: `${session.deviceOs} ${session.deviceName}`,
+ ip: session.ipAddress,
+ current: session.key === currentKey,
+ }));
+ setDevices(formattedDevices);
+ } else {
+ throw new Error(
+ res.data.message || t('active.failFetchActiveSessions'),
+ );
+ }
+ } catch (err: unknown) {
+ console.error(t('active.failFetchActiveSessions'), err);
+ let message = t('active.errorFetch');
+ if (err instanceof Error) {
+ message = err.message;
+ }
+ setFetchError(message);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchActiveSessions();
+ }, [token, i18n.language, t]);
+
+ const handleDeleteDevice = async (id: string) => {
+ if (loadingDeleteIds.includes(id)) return;
+ setLoadingDeleteIds((prev) => [...prev, id]);
+ try {
+ const res = await apiClient.post(
+ '/Profile/DeleteSessions',
+ {
+ keys: [id],
+ },
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+
+ if (res.data.success) {
+ setDevices((prevDevices) => prevDevices.filter((d) => d.id !== id));
+ } else {
+ console.error('Delete failed:', res.data.message);
+ }
+ } catch (error: unknown) {
+ // console.error('Delete error:', error);
+ } finally {
+ setLoadingDeleteIds((prev) =>
+ prev.filter((loadingId) => loadingId !== id),
+ );
+ }
+ };
+
+ const handleTerminateAllOtherSessions = async () => {
+ const otherSessionKeys = devices.filter((d) => !d.current).map((d) => d.id);
+
+ if (otherSessionKeys.length === 0) return;
+
+ try {
+ const res = await apiClient.post(
+ '/Profile/DeleteSessions',
+ {
+ keys: otherSessionKeys,
+ },
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+
+ if (res.data.success) {
+ setDevices((prev) => prev.filter((d) => d.current));
+ } else {
+ console.error('Failed to terminate other sessions:', res.data.message);
+ }
+ } catch (error: unknown) {
+ console.error('Error terminating sessions:', error);
+ }
+ };
+
return (
- {t('active.deletDevicesButton')}
+ {t('active.deleteDevicesButton')}
}
>
-
- {devices.map((device) => (
-
-
-
- {device.timeAndDate}
-
-
+ {isLoading ? (
+
+
+
+ ) : fetchError ? (
+
+ {fetchError}
+
+ ) : (
+
+ {devices.map((device) => (
+
-
-
- {device.deviceModel}
-
-
-
-
- {device.ip}
-
-
-
- {device.current && (
-
- )}
-
-
-
-
- }
- disabled={device.current}
+
- {t('active.deleteDevice')}
-
+ {device.timeAndDate}
+
+
+
+
+
+ {device.deviceModel}
+
+
+
+
+ {device.ip}
+
+
+
+ {device.current && (
+
+ )}
+
+
+
+
+ }
+ disabled={
+ device.current || loadingDeleteIds.includes(device.id)
+ }
+ onClick={() => handleDeleteDevice(device.id)}
+ sx={{
+ color: 'error.main',
+ borderRadius: 1,
+ borderColor: 'error.main',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ whiteSpace: 'nowrap',
+ '& .MuiButton-startIcon': {
+ marginRight: 0.5,
+ marginLeft: 0,
+ },
+ }}
+ >
+ {loadingDeleteIds.includes(device.id)
+ ? t('active.deleting...')
+ : t('active.deleteDevice')}
+
+
-
- {isXsup && (
-
- )}
-
- ))}
-
+ {isXsup && (
+
+ )}
+
+ ))}
+
+ )}
);
diff --git a/src/features/profile/components/security/PasswordSecurity.tsx b/src/features/profile/components/security/PasswordSecurity.tsx
index f60e4c6..060c08d 100644
--- a/src/features/profile/components/security/PasswordSecurity.tsx
+++ b/src/features/profile/components/security/PasswordSecurity.tsx
@@ -11,11 +11,12 @@ import { CardContainer } from '@/components/CardContainer';
import { PageWrapper } from '../PageWrapper';
import { PasswordDialog } from './PasswordDialog';
import { Toast } from '@/components/Toast';
+import { regex } from '@/utils/regex';
export function PasswordSecurity() {
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
- const { t } = useTranslation('security');
+ const { t } = useTranslation('setting');
const [open, setOpen] = useState(false);
const [password, setPassword] = useState('');
@@ -26,12 +27,14 @@ export function PasswordSecurity() {
const [changePassword, setChangePassword] = useState(false);
const [loading, setLoading] = useState(false);
- const hasNumber = /\d/.test(password);
- const hasMinLength = password.length >= 8;
- const hasUpperAndLower = /[A-Z]/.test(password) && /[a-z]/.test(password);
- const hasSpecialChar = /[!@#$%^&*]/.test(password);
- const validPassword =
- hasNumber && hasMinLength && hasUpperAndLower && hasSpecialChar;
+ const {
+ hasNumber,
+ hasMinLength,
+ hasUpperAndLower,
+ hasSpecialChar,
+ validPassword,
+ } = regex(password);
+
const matchPassword = password === confirmPassword;
useEffect(() => {
diff --git a/src/features/profile/components/security/RecentLogins.tsx b/src/features/profile/components/security/RecentLogins.tsx
index 99f14b1..3e83f34 100644
--- a/src/features/profile/components/security/RecentLogins.tsx
+++ b/src/features/profile/components/security/RecentLogins.tsx
@@ -5,7 +5,7 @@ import { PageWrapper } from '../PageWrapper';
import React from 'react';
export function RecentLogins() {
- const { t } = useTranslation('security');
+ const { t } = useTranslation('setting');
const data = [
{
id: 0,
@@ -31,7 +31,6 @@ export function RecentLogins() {
>
= { light: 1, dark: 2 };
export function Setting() {
const { t, i18n } = useTranslation(['setting']);
- const { mode } = useColorScheme();
+ const { mode, setMode } = useColorScheme();
+ const token = localStorage.getItem('authToken');
- const [savedLanguage, setSavedLanguage] = useState(
- i18n.language || 'en',
- );
- const [draftLanguage, setDraftLanguage] = useState(savedLanguage);
+ const [savedSettings, setSavedSettings] = useState({
+ language: i18n.language || 'en',
+ calendar: 'solar',
+ theme: mode === 'light' || mode === 'dark' ? mode : 'light',
+ });
+
+ const [draftSettings, setDraftSettings] =
+ useState(savedSettings);
const [isEditing, setIsEditing] = useState(false);
- const [selectedCalendar, setSelectedCalendar] = useState<
- 'christian' | 'solar' | 'lunar'
- >('solar');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
- const languageOptions = [
- { code: 'en', label: 'English' },
- { code: 'fa', label: 'فارسی' },
- ];
- const calendarOptions: ('christian' | 'solar' | 'lunar')[] = [
- 'christian',
- 'solar',
- 'lunar',
- ];
+ const [isFetching, setIsFetching] = useState(true);
+ const [fetchError, setFetchError] = useState(null);
- const handleDraftLanguageChange = (
- _: React.SyntheticEvent,
- v: { code: string; label: string } | null,
- ) => v && setDraftLanguage(v.code);
+ useEffect(() => {
+ const fetchUserSettings = async () => {
+ setIsFetching(true);
+ setFetchError(null);
+ if (!token) {
+ setIsFetching(false);
+ setFetchError(t('settings.notLoggedIn'));
+ return;
+ }
+ try {
+ const res = await apiClient.post(
+ '/Profile/GetProfile',
+ {},
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+
+ if (res.data.success && res.data.userSettings) {
+ const { theme, calendarType, language } = res.data.userSettings;
+ const themeReverseMap: { [key: number]: ThemeMode | undefined } = {
+ 1: 'light',
+ 2: 'dark',
+ };
+ const themeMode = themeReverseMap[theme] || 'light';
+ const calendarSetting = calendarOptions.find(
+ (c) => c.apiValue === calendarType,
+ );
+ const calendarKey = calendarSetting ? calendarSetting.key : 'solar';
+ const languageSetting = languageOptions.find(
+ (l) => l.apiValue === language,
+ );
+ const languageCode = languageSetting ? languageSetting.code : 'en';
+ const newSettings: SettingsState = {
+ theme: themeMode,
+ calendar: calendarKey,
+ language: languageCode,
+ };
+ setSavedSettings(newSettings);
+ setDraftSettings(newSettings);
+ setMode(themeMode);
+ i18n.changeLanguage(languageCode);
+ } else {
+ throw new Error(res.data.message || t('settings.failRetrieve'));
+ }
+ } catch (e: unknown) {
+ let message = t('settings.errorFetch');
+ if (e instanceof Error) {
+ message = e.message;
+ }
+ setFetchError(message);
+ } finally {
+ setIsFetching(false);
+ }
+ };
+ fetchUserSettings();
+ }, [token, setMode, i18n, t]);
+
+ useEffect(() => {
+ if (isEditing) {
+ setDraftSettings({
+ ...savedSettings,
+ theme: mode === 'light' || mode === 'dark' ? mode : 'light',
+ });
+ }
+ }, [isEditing, savedSettings, mode]);
const handleCancel = () => {
- setDraftLanguage(savedLanguage);
setIsEditing(false);
+ setError(null);
};
- const handleSave = () => {
- if (draftLanguage !== savedLanguage) {
- i18n.changeLanguage(draftLanguage);
- setSavedLanguage(draftLanguage);
+ const handleSave = async () => {
+ setError(null);
+ setLoading(true);
+ try {
+ const languageObj = languageOptions.find(
+ (o) => o.code === draftSettings.language,
+ );
+ const calendarObj = calendarOptions.find(
+ (c) => c.key === draftSettings.calendar,
+ );
+ const apiThemeValue = themeApiMap[draftSettings.theme];
+
+ if (!languageObj || !calendarObj) {
+ setError(t('settings.invalidSelection'));
+ setLoading(false);
+ return;
+ }
+
+ const res = await apiClient.post(
+ '/Profile/SaveSetting',
+ {
+ theme: apiThemeValue,
+ calendarType: calendarObj.apiValue,
+ language: languageObj.apiValue,
+ },
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+
+ if (res.data.success) {
+ setMode(draftSettings.theme);
+ setSavedSettings(draftSettings);
+ await i18n.changeLanguage(draftSettings.language);
+ setIsEditing(false);
+ } else {
+ setError(res.data.message || t('settings.saveFailed'));
+ }
+ } catch (e: unknown) {
+ setError(t('settings.saveFailed'));
+ } finally {
+ setLoading(false);
}
- setIsEditing(false);
};
- const handleEditToggle = () =>
+ const handleEditToggle = () => {
isEditing ? handleSave() : setIsEditing(true);
-
- // useEffect(() => {
- // setSelectedCalendar(t('settings.solar'));
- // }, [i18n.language, t]);
+ };
return (
-
-
+
+
{t('settings.rejectButton')}
@@ -108,80 +228,153 @@ export function Setting() {
bgcolor: isEditing ? 'primary.main' : 'background.default',
color: isEditing ? 'primary.contrastText' : 'primary.main',
}}
+ disabled={loading || isFetching}
>
- {isEditing
- ? t('settings.saveButton')
- : t('settings.editButton')}
+ {loading
+ ? t('settings.saving...')
+ : isEditing
+ ? t('settings.saveButton')
+ : t('settings.editButton')}
}
>
-
+ {isFetching ? (
-
- {isEditing ? (
-
-
- {t('settings.theme')}
-
-
-
- ) : (
-
-
- {t('settings.theme')}
-
-
+
+
+ ) : fetchError ? (
+
+ {fetchError}
+
+ ) : (
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {isEditing ? (
-
+ {t('settings.theme')}
+
+ {
+ setDraftSettings((prev) => ({
+ ...prev,
+ theme: newTheme,
+ }));
+ }}
/>
+
+ ) : (
+
+
+ {t('settings.theme')}
+
+
+
+
+ {t(`settings.${savedSettings.theme}`)}
+
+
+
+ )}
+
+
+ {isEditing ? (
+ o.label}
+ value={
+ languageOptions.find(
+ (o) => o.code === draftSettings.language,
+ ) || null
+ }
+ onChange={(_, v) =>
+ v &&
+ setDraftSettings((prev) => ({
+ ...prev,
+ language: v.code,
+ }))
+ }
+ renderInput={(p) => (
+
+ )}
+ size="medium"
+ fullWidth
+ />
+ ) : (
+
+
+ {t('settings.language')}
+
- {mode === 'light'
- ? t('settings.light')
- : t('settings.dark')}
+ {
+ languageOptions.find(
+ (o) => o.code === savedSettings.language,
+ )?.label
+ }
-
- )}
+ )}
+
-
+
{isEditing ? (
o.label}
- value={
- languageOptions.find((o) => o.code === draftLanguage) ||
- null
+ options={calendarOptions.map((c) => c.key)}
+ getOptionLabel={(key) => t(`settings.${key}`)}
+ value={draftSettings.calendar}
+ onChange={(_, v) =>
+ v &&
+ setDraftSettings((prev) => ({ ...prev, calendar: v }))
}
- onChange={handleDraftLanguageChange}
- renderInput={(p) => (
-
+ renderInput={(params) => (
+
)}
size="medium"
fullWidth
@@ -189,51 +382,24 @@ export function Setting() {
) : (
- {t('settings.language')}
-
-
- {
- languageOptions.find((o) => o.code === savedLanguage)
- ?.label
- }
+ {t('settings.calendar')}
+
+
+
+ {t(`settings.${savedSettings.calendar}`)}
+
+
)}
-
- {isEditing ? (
- t(`settings.${key}`)}
- value={selectedCalendar}
- onChange={(_, v) => v && setSelectedCalendar(v)}
- renderInput={(params) => (
-
- )}
- size="medium"
- fullWidth
- />
- ) : (
-
-
- {t('settings.calendar')}
-
-
-
-
- {t(`settings.${selectedCalendar}`)}
-
-
-
- )}
-
-
+ )}
diff --git a/src/features/profile/components/userInformation/PersonalInformation.tsx b/src/features/profile/components/userInformation/PersonalInformation.tsx
index 3c9dc5d..b97cdaa 100644
--- a/src/features/profile/components/userInformation/PersonalInformation.tsx
+++ b/src/features/profile/components/userInformation/PersonalInformation.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
-import { Box, Button } from '@mui/material';
+import { Box, Button, Typography, CircularProgress } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { CardContainer } from '@/components/CardContainer';
import { ProfileImage } from './personalInformation/ProfileImage';
@@ -7,43 +7,193 @@ import { InfoRowDisplay } from './personalInformation/InfoRowDisplay';
import { InfoRowEdit } from './personalInformation/InfoRowEdit';
import { PageWrapper } from '../PageWrapper';
import { Gender, type InfoRowData } from '../../types';
+import axios, { isAxiosError } from 'axios';
+import apiClient from '@/lib/apiClient';
+
+interface GetProfileApiResponse {
+ success: boolean;
+ message?: string;
+ firstName?: string;
+ lastName?: string;
+ nationalCode?: string;
+ gender?: Gender;
+ countryCode?: string;
+ profileImageUrl?: string;
+}
+
+interface SaveProfileApiResponse {
+ success: boolean;
+ message?: string;
+}
+
+interface TokenApiResponse {
+ access_token: string;
+}
export function PersonalInformation() {
- const { t } = useTranslation('profileSetting');
+ const { t } = useTranslation('setting');
const [isEditing, setIsEditing] = useState(false);
const [uploadedImageUrl, setUploadedImageUrl] = useState(null);
-
- const initialData: InfoRowData = {
- firstName: 'محمد حسین',
- lastName: 'برزهگر',
- country: 'قطر',
+ const [data, setData] = useState({
+ firstName: '',
+ lastName: '',
nationalCode: '',
gender: Gender.None,
- };
+ country: '',
+ });
+ const [originalData, setOriginalData] = useState(null);
+ // const [token, setToken] = useState(null);
+ const [tokenError, setTokenError] = useState(null);
+ const [saveError, setSaveError] = useState(null);
+ const storedToken = localStorage.getItem('authToken');
- const [data, setData] = useState(initialData);
- const [gender, setGender] = useState(Gender.None);
+ const [isLoading, setIsLoading] = useState(true);
+ const [fetchError, setFetchError] = useState(null);
useEffect(() => {
- if (Object.values(Gender).includes(data.gender)) {
- setGender(data.gender);
- }
- }, [data.gender]);
+ const fetchProfile = async () => {
+ setIsLoading(true);
+ setFetchError(null);
+ try {
+ const res = await apiClient.post(
+ '/Profile/GetProfile',
+ {},
+ {
+ headers: {
+ Authorization: `Bearer ${storedToken}`,
+ },
+ },
+ );
+ if (res.data?.success) {
+ const profile = res.data;
+ const fetchedData = {
+ firstName: profile.firstName ?? '',
+ lastName: profile.lastName ?? '',
+ nationalCode: profile.nationalCode ?? '',
+ gender: Object.values(Gender).includes(profile.gender as Gender)
+ ? (profile.gender as Gender)
+ : Gender.None,
+ country: profile.countryCode ?? '',
+ };
+ setData(fetchedData);
+ setOriginalData(fetchedData);
+ setUploadedImageUrl(profile.profileImageUrl || null);
+ } else {
+ throw new Error(res.data.message || t('settingForm.failRetrieve'));
+ }
+ } catch (error: unknown) {
+ let message = t('settingForm.errorFetch');
+ if (error instanceof Error) {
+ message = error.message;
+ }
+ setFetchError(message);
+ } finally {
+ setIsLoading(false);
+ }
+ };
- const initials = `${data.firstName?.trim()[0] || ''}${data.lastName?.trim()[0] || ''}`;
-
- const toggleEdit = () => {
- if (isEditing) {
- setData((prev) => ({
- ...prev,
- gender: gender,
- }));
+ if (storedToken) {
+ fetchProfile();
} else {
- setGender(
- Object.values(Gender).includes(data.gender) ? data.gender : Gender.None,
- );
+ setIsLoading(false);
+ setFetchError(t('settingForm.notLoggedIn'));
+ }
+ }, [storedToken, t]);
+
+ const initials = `${data?.firstName?.trim()[0] || ''}${
+ data?.lastName?.trim()[0] || ''
+ }`;
+
+ const handleEditClick = () => {
+ setIsEditing(true);
+ setSaveError(null);
+ setOriginalData(data);
+ };
+
+ const handleCancelClick = () => {
+ setIsEditing(false);
+ if (originalData) {
+ setData(originalData);
+ }
+ setSaveError(null);
+ };
+
+ const handleSaveClick = async () => {
+ if (!data) return;
+ setSaveError(null);
+ try {
+ const formData = new FormData();
+ formData.append('FirstName', data.firstName || '');
+ formData.append('LastName', data.lastName || '');
+ formData.append('NationalCode', data.nationalCode || '');
+ formData.append('Gender', String(data.gender ?? Gender.None));
+ formData.append('CountryCode', data.country || '');
+
+ if (uploadedImageUrl && uploadedImageUrl.startsWith('data:')) {
+ const response = await fetch(uploadedImageUrl);
+ const blob = await response.blob();
+ formData.append('Image', blob, 'profile.jpg');
+ }
+
+ const res = await apiClient.post(
+ 'Profile/SaveProfilePersonalInforamtion',
+ formData,
+ {
+ headers: {
+ Authorization: `Bearer ${storedToken}`,
+ },
+ },
+ );
+
+ if (res.data.success) {
+ setIsEditing(false);
+ setOriginalData(data);
+ } else {
+ throw new Error(res.data.message || t('settingForm.unknownError'));
+ }
+ } catch (error: unknown) {
+ let message = t('settingForm.checkConnection');
+ if (error instanceof Error) {
+ message = error.message;
+ }
+ setSaveError(message);
+ }
+ };
+
+ const apiUrl = 'https://accounts.business-harmony.com';
+ const tokenEndpoint = `${apiUrl}/connect/token`;
+ const getToken = async () => {
+ setTokenError(null);
+ try {
+ const body = new URLSearchParams();
+ body.set('grant_type', 'password');
+ body.set('username', 'zareian.1381@gmail.com');
+ body.set('password', '123@Qweasd');
+ body.set('client_id', 'harmony_identity');
+ body.set('scope', 'openid harmony_identity profile offline_access');
+ const response = await axios.post(
+ tokenEndpoint,
+ body.toString(),
+ {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ },
+ );
+ if (response.data?.access_token) {
+ localStorage.setItem('authToken', response.data.access_token);
+ } else {
+ throw new Error('No access token in response');
+ }
+ } catch (error: unknown) {
+ let message = 'Failed to get token';
+ if (isAxiosError(error) && error.response) {
+ message = `Request failed with status ${error.response.status}`;
+ } else if (error instanceof Error) {
+ message = error.message;
+ }
+ setTokenError(message);
}
- setIsEditing(!isEditing);
};
return (
@@ -53,78 +203,140 @@ export function PersonalInformation() {
subtitle={t('settingForm.descriptionPersonalInfo')}
highlighted={isEditing}
action={
-
- {isEditing && (
-
- )}
-
+
+ {isEditing ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
+
+ {saveError && (
+
+ {saveError}
+
+ )}
}
>
-
- {isEditing && (
- {
- const reader = new FileReader();
- reader.onload = () =>
- setUploadedImageUrl(reader.result as string);
- reader.readAsDataURL(file);
+ {isLoading ? (
+
+
+
+ ) : fetchError ? (
+
+ {fetchError}
+
+ ) : (
+ <>
+ {tokenError && (
+ Error: {tokenError}
+ )}
+ setUploadedImageUrl(null)}
- />
- )}
-
- {isEditing ? (
-
- ) : (
-
- )}
-
+ >
+ {isEditing && (
+ {
+ const reader = new FileReader();
+ reader.onload = () =>
+ setUploadedImageUrl(reader.result as string);
+ reader.readAsDataURL(file);
+ }}
+ onRemoveImage={() => setUploadedImageUrl(null)}
+ />
+ )}
+ {data &&
+ (isEditing ? (
+
+ ) : (
+
+ ))}
+
+ >
+ )}
);
diff --git a/src/features/profile/components/userInformation/PhoneNumber.tsx b/src/features/profile/components/userInformation/PhoneNumber.tsx
index 4bb170c..1e38410 100644
--- a/src/features/profile/components/userInformation/PhoneNumber.tsx
+++ b/src/features/profile/components/userInformation/PhoneNumber.tsx
@@ -1,4 +1,4 @@
-import { useState, useRef } from 'react';
+import { useState, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import parsePhoneNumberFromString from 'libphonenumber-js';
import { PageWrapper } from '../PageWrapper';
@@ -6,9 +6,32 @@ import { CardContainer } from '@/components/CardContainer';
import PhoneDisplay from './phoneNumber/PhoneDisplay';
import PhoneEditForm from './phoneNumber/PhoneEditForm';
import PhoneActionButtons from './phoneNumber/PhoneActionButtons';
+import apiClient from '@/lib/apiClient';
+import { CircularProgress, Box, Typography } from '@mui/material';
+
+interface Phone {
+ phone: string;
+ time: string;
+ withCode: string;
+}
+
+interface GetProfileApiResponse {
+ success: boolean;
+ message?: string;
+ phoneNumber?: string;
+}
+
+interface ApiResponse {
+ success: boolean;
+ message?: string;
+}
+
+interface ConfirmApiResponse extends ApiResponse {
+ confirm?: boolean;
+}
export function PhoneNumber() {
- const { t } = useTranslation('profileSetting');
+ const { t } = useTranslation('setting');
const [isEditing, setIsEditing] = useState(false);
const [phoneNumber, setPhoneNumber] = useState('');
const [verificationCode, setVerificationCode] = useState('');
@@ -18,15 +41,60 @@ export function PhoneNumber() {
);
const [isVerifying, setIsVerifying] = useState(false);
const [isVerified, setIsVerified] = useState(false);
- const [phones, setPhone] = useState([
- { phone: '09123456789', time: '۱ ماه پیش', withCode: '+989123456789' },
- ]);
+ const [phones, setPhone] = useState([]);
const [countryCode, setCountryCode] = useState('+98');
const textFieldRef = useRef(null);
const inputRef = useRef(null);
const [error, setError] = useState();
const [touched, setTouched] = useState(false);
const inputError: boolean = touched && !!error;
+ const token = localStorage.getItem('authToken');
+
+ const [isLoading, setIsLoading] = useState(true);
+ const [fetchError, setFetchError] = useState(null);
+
+ useEffect(() => {
+ const fetchPhoneNumber = async () => {
+ setIsLoading(true);
+ setFetchError(null);
+ if (!token) {
+ setIsLoading(false);
+ setFetchError(t('settingForm.notLoggedIn'));
+ return;
+ }
+ try {
+ const res = await apiClient.post(
+ '/Profile/GetProfile',
+ {},
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+
+ if (res.data.success && res.data.phoneNumber) {
+ setPhone([
+ {
+ phone: res.data.phoneNumber,
+ time: '',
+ withCode: res.data.phoneNumber,
+ },
+ ]);
+ } else if (!res.data.success) {
+ throw new Error(
+ res.data.message || t('settingForm.failFetchPhoneNumber'),
+ );
+ }
+ } catch (err: unknown) {
+ let message = t('settingForm.errorFetchPhoneNumber');
+ if (err instanceof Error) {
+ message = err.message;
+ }
+ setFetchError(message);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchPhoneNumber();
+ }, [token, t]);
const isPhoneValid = (code: string, phone: string) => {
const phoneNum = parsePhoneNumberFromString(code + phone);
@@ -37,6 +105,7 @@ export function PhoneNumber() {
setTouched(true);
if (!phoneNumber) {
setError(t('settingForm.thisFieldIsRequired'));
+ return;
}
if (!isPhoneValid(countryCode, phoneNumber)) {
setError(t('settingForm.phoneNumberIsInvalid'));
@@ -54,34 +123,100 @@ export function PhoneNumber() {
setIsVerified(false);
setButtonState('default');
setShowToast(false);
+ setError(undefined);
+ setTouched(false);
}
return enteringEditMode;
});
};
- const handleSendCode = () => {
+ const handleSendCode = async () => {
if (!phoneNumber) return;
- setButtonState('counting');
- setIsVerified(false);
+ if (!isPhoneValid(countryCode, phoneNumber)) {
+ setError(t('settingForm.phoneNumberIsInvalid'));
+ return;
+ }
+ setError(undefined);
+
+ try {
+ const res = await apiClient.post(
+ '/Profile/SendVerfiyPhoneNumberCode',
+ {
+ phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
+ },
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+ if (res.data.success) {
+ setButtonState('counting');
+ setIsVerified(false);
+ } else {
+ setError(res.data.message || t('settingForm.sendCodeFailed'));
+ }
+ } catch (error: unknown) {
+ setError(t('settingForm.sendCodeFailed'));
+ }
};
- const handleVerifyCode = () => {
+ const handleVerifyCode = async () => {
+ if (!verificationCode) {
+ setError(t('settingForm.verificationCodeRequired'));
+ return;
+ }
setIsVerifying(true);
- setTimeout(() => {
+ setError(undefined);
+
+ try {
+ const res = await apiClient.post(
+ '/Profile/ConfirmPhoneNumberChangeCode',
+ {
+ phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
+ verifyCode: verificationCode,
+ },
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+
+ if (res.data.success && res.data.confirm) {
+ setIsVerified(true);
+ setShowToast(true);
+ await handleChangePhoneNumber();
+ } else {
+ setError(res.data.message || t('settingForm.verifyCodeFailed'));
+ setIsVerified(false);
+ }
+ } catch (error: unknown) {
+ setError(t('settingForm.verifyCodeFailed'));
+ setIsVerified(false);
+ } finally {
setIsVerifying(false);
- setIsVerified(true);
- setShowToast(true);
- const newPhone = '+98' + phoneNumber.slice(1);
- setPhone([
- { phone: phoneNumber, time: 'چند ثانیه پیش', withCode: newPhone },
- ]);
- }, 1500);
+ }
};
- const handleVerifyClick = () => {
- if (!verificationCode || isVerifying) return;
- handleVerifyCode();
- setTimeout(() => setShowToast(true), 1600);
+ const handleChangePhoneNumber = async () => {
+ try {
+ const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
+ const res = await apiClient.post(
+ '/Profile/ChangePhoneNumber',
+ {
+ phoneNumber: fullPhoneNumber,
+ },
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+
+ if (res.data.success) {
+ setPhone([
+ {
+ phone: phoneNumber,
+ time: t('settingForm.justNow'),
+ withCode: fullPhoneNumber,
+ },
+ ]);
+ setIsEditing(false);
+ } else {
+ setError(res.data.message || t('settingForm.changePhoneFailed'));
+ }
+ } catch (error: unknown) {
+ setError(t('settingForm.changePhoneFailed'));
+ }
};
return (
@@ -98,7 +233,23 @@ export function PhoneNumber() {
/>
}
>
- {isEditing ? (
+ {isLoading ? (
+
+
+
+ ) : fetchError ? (
+
+ {fetchError}
+
+ ) : isEditing ? (
(
+ 'enterEmail',
+ );
+ const [apiError, setApiError] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [emailList, setEmailList] = useState([]);
+
+ const [isFetching, setIsFetching] = useState(true);
+ const [fetchError, setFetchError] = useState(null);
const fullScreen = useMediaQuery((theme: Theme) =>
theme.breakpoints.down('sm'),
@@ -26,10 +66,128 @@ export function SocialMedia() {
| 'lg'
| 'xl';
- const emailList = [
- { email: 'emailtemp@email.com', provider: 'email', time: '1 ماه پیش' },
- { email: 'emailtemp@gmail.com', provider: 'google', time: '1 ماه پیش' },
- ] as const;
+ useEffect(() => {
+ const fetchInitialEmail = async () => {
+ setIsFetching(true);
+ setFetchError(null);
+ if (!token) {
+ setIsFetching(false);
+ setFetchError(t('settingForm.notLoggedIn'));
+ return;
+ }
+ try {
+ const res = await apiClient.post(
+ '/Profile/GetProfile',
+ {},
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+
+ if (res.data.success && res.data.email) {
+ const userEmail = res.data.email;
+ const newAccount: EmailAccount = {
+ email: userEmail,
+ provider: userEmail.includes('gmail.com') ? 'google' : 'email',
+ time: '',
+ };
+ setEmailList([newAccount]);
+ } else if (!res.data.success) {
+ throw new Error(res.data.message || t('settingForm.failFetchEmail'));
+ }
+ } catch (err: unknown) {
+ let message = t('settingForm.errorFetchEmail');
+ if (err instanceof Error) {
+ message = err.message;
+ }
+ setFetchError(message);
+ } finally {
+ setIsFetching(false);
+ }
+ };
+
+ fetchInitialEmail();
+ }, [token, t]);
+
+ const resetDialog = () => {
+ setOpenDialog(false);
+ setEmailInput('');
+ setVerificationCode('');
+ setApiError(null);
+ setIsLoading(false);
+ setDialogStep('enterEmail');
+ };
+
+ const handleSendCode = async () => {
+ if (!/^\S+@\S+\.\S+$/.test(emailInput)) {
+ setApiError(t('settingForm.emailIsInvalid'));
+ return;
+ }
+ setIsLoading(true);
+ setApiError(null);
+ try {
+ const res = await apiClient.post(
+ 'Profile/SendEmailChangeCode',
+ { email: emailInput },
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+ if (res.data.success) {
+ setDialogStep('enterCode');
+ } else {
+ setApiError(res.data.message || t('settingForm.sendCodeFailed'));
+ }
+ } catch (err: unknown) {
+ setApiError(t('settingForm.sendCodeFailed'));
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleConfirmAndChangeEmail = async () => {
+ if (verificationCode.length < 4) {
+ setApiError(t('settingForm.verificationCodeRequired'));
+ return;
+ }
+ setIsLoading(true);
+ setApiError(null);
+ try {
+ const confirmRes = await apiClient.post(
+ 'Profile/ConfirmEmailChangeCode',
+ { email: emailInput, verifyCode: verificationCode },
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+
+ if (confirmRes.data.success && confirmRes.data.confirm) {
+ const changeRes = await apiClient.post(
+ 'Profile/ChangeEmail',
+ { email: emailInput },
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+
+ if (changeRes.data.success) {
+ setEmailList((prevList) => [
+ ...prevList,
+ {
+ email: emailInput,
+ provider: 'email',
+ time: t('settingForm.justNow'),
+ },
+ ]);
+ resetDialog();
+ } else {
+ setApiError(
+ changeRes.data.message || t('settingForm.changeEmailFailed'),
+ );
+ }
+ } else {
+ setApiError(
+ confirmRes.data.message || t('settingForm.verifyCodeFailed'),
+ );
+ }
+ } catch (err: unknown) {
+ setApiError(t('settingForm.anErrorOccurred'));
+ } finally {
+ setIsLoading(false);
+ }
+ };
return (
@@ -40,15 +198,38 @@ export function SocialMedia() {
setOpenDialog(true)} />
}
>
-
+ {isFetching ? (
+
+
+
+ ) : fetchError ? (
+
+ {fetchError}
+
+ ) : (
+
+ )}
setOpenDialog(false)}
+ onClose={resetDialog}
t={t}
emailInput={emailInput}
setEmailInput={setEmailInput}
- emailError={emailError}
- setEmailError={setEmailError}
+ verificationCode={verificationCode}
+ setVerificationCode={setVerificationCode}
+ apiError={apiError}
+ isLoading={isLoading}
+ dialogStep={dialogStep}
+ onSendCode={handleSendCode}
+ onConfirmEmail={handleConfirmAndChangeEmail}
fullScreen={fullScreen}
computedMaxWidth={computedMaxWidth}
/>
diff --git a/src/features/profile/components/userInformation/personalInformation/DisplayField.tsx b/src/features/profile/components/userInformation/personalInformation/DisplayField.tsx
index f74e331..a53025d 100644
--- a/src/features/profile/components/userInformation/personalInformation/DisplayField.tsx
+++ b/src/features/profile/components/userInformation/personalInformation/DisplayField.tsx
@@ -7,7 +7,7 @@ interface DisplayFieldProps {
}
export function DisplayField({ label, value }: DisplayFieldProps) {
- const { t } = useTranslation('profileSetting');
+ const { t } = useTranslation('setting');
const displayValue = value?.trim() || t('settingForm.notDetermined');
return (
diff --git a/src/features/profile/components/userInformation/personalInformation/InfoRowDisplay.tsx b/src/features/profile/components/userInformation/personalInformation/InfoRowDisplay.tsx
index 6fd3cb8..6979ed2 100644
--- a/src/features/profile/components/userInformation/personalInformation/InfoRowDisplay.tsx
+++ b/src/features/profile/components/userInformation/personalInformation/InfoRowDisplay.tsx
@@ -23,7 +23,7 @@ export function InfoRowDisplay({
uploadedImageUrl,
initials,
}: InfoRowDisplayProps) {
- const { t } = useTranslation('profileSetting');
+ const { t } = useTranslation('setting');
const displayValue = (value: string) =>
value?.trim() || t('settingForm.notDetermined');
diff --git a/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx b/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx
index a661093..7d36938 100644
--- a/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx
+++ b/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx
@@ -2,6 +2,7 @@ import {
Box,
TextField,
FormControl,
+ InputLabel,
MenuItem,
Select,
Autocomplete,
@@ -15,17 +16,10 @@ import { type InfoRowData } from '@/features/profile/types';
interface InfoRowEditProps {
data: InfoRowData;
setData: React.Dispatch>;
- gender: Gender;
- setGender: React.Dispatch>;
}
-export function InfoRowEdit({
- data,
- setData,
- gender,
- setGender,
-}: InfoRowEditProps) {
- const { t } = useTranslation(['countries', 'profileSetting']);
+export function InfoRowEdit({ data, setData }: InfoRowEditProps) {
+ const { t } = useTranslation(['countries', 'setting']);
const countryOptions = countries.map((c) => ({
code: c.code,
@@ -34,26 +28,27 @@ export function InfoRowEdit({
const currentCountry =
countryOptions.find((c) => c.code === data.country) || null;
+ const fields = [
+ {
+ name: 'firstName' as const,
+ label: t('settingForm.name', { ns: 'profileSetting' }),
+ value: data.firstName,
+ },
+ {
+ name: 'lastName' as const,
+ label: t('settingForm.familyName', { ns: 'profileSetting' }),
+ value: data.lastName,
+ },
+ {
+ name: 'nationalCode' as const,
+ label: t('settingForm.nationalCode', { ns: 'profileSetting' }),
+ value: data.nationalCode,
+ },
+ ];
return (
- {[
- {
- name: 'firstName' as keyof InfoRowData,
- label: t('settingForm.name', { ns: 'profileSetting' }),
- value: data.firstName,
- },
- {
- name: 'lastName' as keyof InfoRowData,
- label: t('settingForm.familyName', { ns: 'profileSetting' }),
- value: data.lastName,
- },
- {
- name: 'nationalCode' as keyof InfoRowData,
- label: t('settingForm.nationalCode', { ns: 'profileSetting' }),
- value: data.nationalCode,
- },
- ].map(({ name, label, value }) => (
+ {fields.map(({ name, label, value }) => (
+
+ {t('settingForm.genderPlaceholder', { ns: 'profileSetting' })}
+