Merge pull request #29 from rkheftan/fix/profile
fix: dashboard api calls, multi profile fetch
This commit is contained in:
@@ -6,7 +6,6 @@ export const Loading = () => {
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'background.default',
|
||||
position: 'fixed',
|
||||
inset: '0',
|
||||
zIndex: (t) => t.zIndex.tooltip + 1,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useToast, type ToastOptions } from '@rkheftan/harmony-ui';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ i18n
|
||||
.use(initReactI18next) // Passes i18n down to react-i18next
|
||||
.init({
|
||||
supportedLngs: ['en', 'fa'], // Supported languages
|
||||
fallbackLng: 'en',
|
||||
fallbackLng: 'fa',
|
||||
detection: {
|
||||
order: ['localStorage', 'cookie', 'navigator'],
|
||||
order: ['localStorage', 'cookie'],
|
||||
caches: ['localStorage', 'cookie'],
|
||||
lookupLocalStorage: 'language', // The key to use in localStorage
|
||||
},
|
||||
|
||||
@@ -188,13 +188,13 @@ export function UserCompletionPage() {
|
||||
firstName,
|
||||
lastName,
|
||||
gender: sex || 0,
|
||||
nationalId,
|
||||
nationalCode: nationalId,
|
||||
savePassword: showPasswordSection,
|
||||
password: showPasswordSection ? password : undefined,
|
||||
saveEmail: showEmail,
|
||||
email: showEmail ? email : undefined,
|
||||
birthDate,
|
||||
country,
|
||||
countryCode: country,
|
||||
});
|
||||
|
||||
if (res) {
|
||||
@@ -351,6 +351,7 @@ export function UserCompletionPage() {
|
||||
if (Object.keys(touched).length > 0) {
|
||||
validateForm();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
firstName,
|
||||
lastName,
|
||||
|
||||
@@ -18,9 +18,9 @@ export interface CompleteUserInfoPayload {
|
||||
lastName: string;
|
||||
// TODO: fix this
|
||||
gender: 0 | 1 | 2;
|
||||
nationalId: string;
|
||||
nationalCode: string;
|
||||
birthDate: Date | null;
|
||||
country: string;
|
||||
countryCode: string;
|
||||
savePassword?: boolean;
|
||||
password?: string;
|
||||
saveEmail?: boolean;
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Button,
|
||||
Link,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
InputAdornment,
|
||||
} from '@mui/material';
|
||||
import { CloseCircle, Eye, EyeSlash } from 'iconsax-react';
|
||||
@@ -18,6 +17,7 @@ import { type PasswordDialogProps } from '../../types/settingsType';
|
||||
import { PasswordValidationItem } from './PasswordValidation';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function PasswordDialog({
|
||||
open,
|
||||
@@ -34,13 +34,13 @@ export function PasswordDialog({
|
||||
loading,
|
||||
handleSubmit,
|
||||
changePassword,
|
||||
apiError,
|
||||
t,
|
||||
hasMinLength,
|
||||
hasNumber,
|
||||
hasUpperAndLower,
|
||||
hasSpecialChar,
|
||||
}: PasswordDialogProps) {
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
const [showCurrent, setShowCurrent] = useState(false);
|
||||
const [showNew, setShowNew] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
@@ -77,12 +77,6 @@ export function PasswordDialog({
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
{apiError && (
|
||||
<Typography color="error" variant="body2" textAlign="center">
|
||||
{apiError}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{changePassword && (
|
||||
<>
|
||||
<TextField
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@@ -12,16 +12,13 @@ import { CardContainer } from '@/components/CardContainer';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
import { PasswordDialog } from './PasswordDialog';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import {
|
||||
addPassword,
|
||||
changePassword,
|
||||
fetchProfile,
|
||||
} from '../../api/settingsApi';
|
||||
import { addPassword, changePassword } from '../../api/settingsApi';
|
||||
import { containsNumber } from '@/utils/regexes/containsNumber';
|
||||
import { least8Chars } from '@/utils/regexes/least8Chars';
|
||||
import { hasUpperAndLowerLetter } from '@/utils/regexes/hasUpperAndLowerLetter';
|
||||
import { containsSymbol } from '@/utils/regexes/containsSymbol';
|
||||
import { useToast } from '@rkheftan/harmony-ui';
|
||||
import { useProfile } from '../../hooks/useProfile';
|
||||
|
||||
export function PasswordSecurity() {
|
||||
const theme = useTheme();
|
||||
@@ -45,31 +42,33 @@ export function PasswordSecurity() {
|
||||
const matchPassword = password === confirmPassword;
|
||||
const showToast = useToast();
|
||||
|
||||
const {
|
||||
data: profileData,
|
||||
loading: isFetching,
|
||||
error: fetchError,
|
||||
} = useApi(fetchProfile, { immediate: true });
|
||||
const { isLoadingProfile, refetchProfile } = useProfile();
|
||||
|
||||
const {
|
||||
data: addData,
|
||||
loading: isAdding,
|
||||
error: addError,
|
||||
execute: executeAddPassword,
|
||||
} = useApi(addPassword);
|
||||
const { loading: isAdding, execute: executeAddPassword } =
|
||||
useApi(addPassword);
|
||||
|
||||
const {
|
||||
data: changeData,
|
||||
loading: isChanging,
|
||||
error: changeError,
|
||||
execute: executeChangePassword,
|
||||
} = useApi(changePassword);
|
||||
const { loading: isChanging, execute: executeChangePassword } =
|
||||
useApi(changePassword);
|
||||
|
||||
useEffect(() => {
|
||||
if (profileData?.success) {
|
||||
setChangePasswordUI(profileData.hasPassword || false);
|
||||
}
|
||||
}, [profileData]);
|
||||
const loadProfile = async () => {
|
||||
const profileData = await refetchProfile();
|
||||
|
||||
if (!profileData) return;
|
||||
|
||||
if (profileData.success) {
|
||||
setChangePasswordUI(profileData.hasPassword || false);
|
||||
} else {
|
||||
showToast({
|
||||
message: profileData.message || t('message.serverError'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadProfile();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (password) {
|
||||
@@ -79,30 +78,6 @@ export function PasswordSecurity() {
|
||||
}
|
||||
}, [password, validPassword]);
|
||||
|
||||
useEffect(() => {
|
||||
if (addData?.success || changeData?.success) {
|
||||
showToast({
|
||||
message: changePasswordUI
|
||||
? t('securityForm.passwordChanged')
|
||||
: t('securityForm.passwordAdded'),
|
||||
severity: 'success',
|
||||
});
|
||||
if (!changePasswordUI) {
|
||||
setChangePasswordUI(true);
|
||||
}
|
||||
setOpen(false);
|
||||
setPassword('');
|
||||
setConfirmPassword('');
|
||||
setCurrentPassword('');
|
||||
} else if (addData || changeData) {
|
||||
showToast({
|
||||
message:
|
||||
addData?.message || changeData?.message || t('securityForm.error'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
}, [addData, changeData, changePasswordUI, showToast, t]);
|
||||
|
||||
const handleOpen = () => setOpen(true);
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
@@ -115,26 +90,48 @@ export function PasswordSecurity() {
|
||||
return;
|
||||
}
|
||||
if (changePasswordUI) {
|
||||
await executeChangePassword({
|
||||
const changeData = await executeChangePassword({
|
||||
oldPassword: currentPassword,
|
||||
newPassword: password,
|
||||
});
|
||||
|
||||
if (!changeData) return;
|
||||
|
||||
if (!changeData.success) {
|
||||
showToast({
|
||||
message: changeData?.message || t('securityForm.error'),
|
||||
severity: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await executeAddPassword({ newPassword: password });
|
||||
const addData = await executeAddPassword({ newPassword: password });
|
||||
|
||||
if (!addData) return;
|
||||
|
||||
if (!addData.success) {
|
||||
showToast({
|
||||
message: addData?.message || t('securityForm.error'),
|
||||
severity: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
showToast({
|
||||
message: changePasswordUI
|
||||
? t('securityForm.passwordChanged')
|
||||
: t('securityForm.passwordAdded'),
|
||||
severity: 'success',
|
||||
});
|
||||
if (!changePasswordUI) {
|
||||
setChangePasswordUI(true);
|
||||
}
|
||||
setOpen(false);
|
||||
setPassword('');
|
||||
setConfirmPassword('');
|
||||
setCurrentPassword('');
|
||||
};
|
||||
|
||||
const getErrorMessage = (error: unknown): string | null => {
|
||||
if (!error) return null;
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
};
|
||||
|
||||
const apiError = useMemo(
|
||||
() => addError || changeError,
|
||||
[addError, changeError],
|
||||
);
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<CardContainer
|
||||
@@ -153,7 +150,7 @@ export function PasswordSecurity() {
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{isFetching ? (
|
||||
{isLoadingProfile ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -165,12 +162,6 @@ export function PasswordSecurity() {
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : fetchError ? (
|
||||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||
<Typography color="error.main">
|
||||
{getErrorMessage(fetchError)}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ px: 4, py: 2 }}>
|
||||
{changePasswordUI ? (
|
||||
@@ -208,8 +199,6 @@ export function PasswordSecurity() {
|
||||
loading={isAdding || isChanging}
|
||||
handleSubmit={handlePasswordSubmit}
|
||||
changePassword={changePasswordUI}
|
||||
apiError={getErrorMessage(apiError)}
|
||||
t={t}
|
||||
hasMinLength={hasMinLength}
|
||||
hasNumber={hasNumber}
|
||||
hasUpperAndLower={hasUpperAndLower}
|
||||
|
||||
@@ -9,11 +9,10 @@ import {
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
import { fetchProfile } from '../../api/settingsApi';
|
||||
import type { LoginLog } from '../../types/settingsApiType';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import { formatDate } from '@/utils/formatSessionDate';
|
||||
import { useToast } from '@rkheftan/harmony-ui';
|
||||
import { useProfile } from '../../hooks/useProfile';
|
||||
|
||||
export function RecentLogins() {
|
||||
const { t, i18n } = useTranslation('setting');
|
||||
@@ -22,31 +21,43 @@ export function RecentLogins() {
|
||||
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
||||
const showToast = useToast();
|
||||
|
||||
const {
|
||||
data: profileData,
|
||||
loading: isLoading,
|
||||
error: fetchError,
|
||||
} = useApi(fetchProfile, { immediate: true });
|
||||
const { isLoadingProfile, refetchProfile } = useProfile();
|
||||
|
||||
// useEffect(() => {
|
||||
// if (profileData?.success && Array.isArray(profileData.loginLogs)) {
|
||||
// setLogs(profileData.loginLogs);
|
||||
// }
|
||||
// }, [profileData]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (fetchError) {
|
||||
// showToast({
|
||||
// message: getErrorMessage(fetchError) || t('active.errorFetch'),
|
||||
// severity: 'error',
|
||||
// });
|
||||
// }
|
||||
// }, [fetchError, t, showToast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (profileData?.success && Array.isArray(profileData.loginLogs)) {
|
||||
setLogs(profileData.loginLogs);
|
||||
}
|
||||
}, [profileData]);
|
||||
const loadProfile = async () => {
|
||||
const profileData = await refetchProfile();
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchError) {
|
||||
showToast({
|
||||
message: getErrorMessage(fetchError) || t('active.errorFetch'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
}, [fetchError, t, showToast]);
|
||||
const getErrorMessage = (error: unknown): string | null => {
|
||||
if (!error) return null;
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
};
|
||||
if (!profileData) return;
|
||||
|
||||
if (profileData.success) {
|
||||
if (!Array.isArray(profileData.loginLogs)) return;
|
||||
setLogs(profileData.loginLogs);
|
||||
} else {
|
||||
showToast({
|
||||
message: profileData.message || t('active.errorFetch'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadProfile();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
@@ -54,7 +65,7 @@ export function RecentLogins() {
|
||||
title={t('securityForm.recentLogins')}
|
||||
subtitle={t('securityForm.description')}
|
||||
>
|
||||
{isLoading ? (
|
||||
{isLoadingProfile ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -66,12 +77,6 @@ export function RecentLogins() {
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : fetchError ? (
|
||||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||
<Typography color="error.main">
|
||||
{getErrorMessage(fetchError) || t('active.errorFetch')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ px: 4 }}>
|
||||
{logs.map((log, index) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Box, Button, Typography, CircularProgress } from '@mui/material';
|
||||
import { Box, Button, CircularProgress } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { ProfileImage } from './personalInformation/ProfileImage';
|
||||
@@ -8,10 +8,13 @@ import { InfoRowEdit } from './personalInformation/InfoRowEdit';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import { Gender, type InfoRowData } from '../../types/settingsType';
|
||||
import { fetchProfile, saveProfile } from '../../api/settingsApi';
|
||||
import { saveProfile } from '../../api/settingsApi';
|
||||
import { useToast } from '@rkheftan/harmony-ui';
|
||||
import { useProfile } from '../../hooks/useProfile';
|
||||
|
||||
export function PersonalInformation() {
|
||||
const imageBaseUrl = import.meta.env.IMAGE_BASE_URL;
|
||||
|
||||
const { t } = useTranslation('setting');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
|
||||
@@ -27,52 +30,46 @@ export function PersonalInformation() {
|
||||
const showToast = useToast();
|
||||
const infoRowEditRef = useRef<{ validateFields: () => boolean }>(null);
|
||||
|
||||
const {
|
||||
data: profileData,
|
||||
loading: isLoadingProfile,
|
||||
error: fetchProfileError,
|
||||
} = useApi(fetchProfile, { immediate: true });
|
||||
const {
|
||||
data: saveData,
|
||||
loading: isSavingProfile,
|
||||
error: saveProfileError,
|
||||
execute: executeSaveProfile,
|
||||
} = useApi(saveProfile);
|
||||
const { isLoadingProfile, refetchProfile } = useProfile();
|
||||
|
||||
const { loading: isSavingProfile, execute: executeSaveProfile } =
|
||||
useApi(saveProfile);
|
||||
|
||||
useEffect(() => {
|
||||
if (profileData?.success) {
|
||||
const fetchedData = {
|
||||
firstName: profileData.firstName ?? '',
|
||||
lastName: profileData.lastName ?? '',
|
||||
nationalCode: profileData.nationalCode ?? '',
|
||||
gender: Object.values(Gender).includes(profileData.gender as Gender)
|
||||
? (profileData.gender as Gender)
|
||||
: Gender.None,
|
||||
country: profileData.countryCode ?? '',
|
||||
};
|
||||
setData(fetchedData);
|
||||
setOriginalData(fetchedData);
|
||||
const imageBaseUrl = import.meta.env.IMAGE_BASE_URL;
|
||||
setUploadedImageUrl(
|
||||
profileData.profileImageUrl
|
||||
? `${imageBaseUrl}${profileData.profileImageUrl}`
|
||||
: null,
|
||||
);
|
||||
setUploadedImageFile(null);
|
||||
}
|
||||
}, [profileData]);
|
||||
const loadProfile = async () => {
|
||||
const profileData = await refetchProfile();
|
||||
|
||||
useEffect(() => {
|
||||
if (saveData?.success) {
|
||||
showToast({
|
||||
message: t('settingForm.successSaveProfile'),
|
||||
severity: 'success',
|
||||
});
|
||||
setIsEditing(false);
|
||||
setOriginalData(data);
|
||||
setUploadedImageFile(null);
|
||||
}
|
||||
}, [saveData, data, showToast, t]);
|
||||
if (!profileData) return;
|
||||
|
||||
if (profileData.success) {
|
||||
const fetchedData = {
|
||||
firstName: profileData.firstName ?? '',
|
||||
lastName: profileData.lastName ?? '',
|
||||
nationalCode: profileData.nationalCode ?? '',
|
||||
gender: Object.values(Gender).includes(profileData.gender as Gender)
|
||||
? (profileData.gender as Gender)
|
||||
: Gender.None,
|
||||
country: profileData.countryCode ?? '',
|
||||
};
|
||||
setData(fetchedData);
|
||||
setOriginalData(fetchedData);
|
||||
setUploadedImageUrl(
|
||||
profileData.profileImageUrl
|
||||
? `${imageBaseUrl}${profileData.profileImageUrl}`
|
||||
: null,
|
||||
);
|
||||
setUploadedImageFile(null);
|
||||
} else {
|
||||
showToast({
|
||||
message: profileData.message || t('message.serverError'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadProfile();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const initials = `${data?.firstName?.trim()[0] || ''}${data?.lastName?.trim()[0] || ''}`;
|
||||
|
||||
@@ -84,36 +81,35 @@ export function PersonalInformation() {
|
||||
setUploadedImageFile(null);
|
||||
};
|
||||
|
||||
const handleSaveClick = () => {
|
||||
const handleSaveClick = async () => {
|
||||
if (!data) return;
|
||||
const isValid = infoRowEditRef.current?.validateFields?.();
|
||||
if (!isValid) return;
|
||||
executeSaveProfile({ data, imageUrl: uploadedImageFile });
|
||||
};
|
||||
|
||||
const getErrorMessage = (error: unknown): string | null => {
|
||||
if (!error) return null;
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
};
|
||||
const saveData = await executeSaveProfile({
|
||||
data,
|
||||
imageUrl: uploadedImageFile,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (saveProfileError)
|
||||
if (!saveData) return;
|
||||
|
||||
if (saveData?.success) {
|
||||
showToast({
|
||||
message:
|
||||
getErrorMessage(saveProfileError) || t('settingForm.errorSave'),
|
||||
message: t('settingForm.successSaveProfile'),
|
||||
severity: 'success',
|
||||
});
|
||||
setIsEditing(false);
|
||||
setOriginalData(data);
|
||||
setUploadedImageFile(null);
|
||||
// Force a refetch to update the cache with the new data
|
||||
refetchProfile({ force: true });
|
||||
} else {
|
||||
showToast({
|
||||
message: saveData.message || t('message.serverError'),
|
||||
severity: 'error',
|
||||
});
|
||||
}, [saveProfileError, showToast, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchProfileError)
|
||||
showToast({
|
||||
message:
|
||||
getErrorMessage(fetchProfileError) || t('settingForm.errorFetch'),
|
||||
severity: 'error',
|
||||
});
|
||||
}, [fetchProfileError, showToast, t]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
@@ -163,14 +159,6 @@ export function PersonalInformation() {
|
||||
{t('settingForm.editButton')}
|
||||
</Button>
|
||||
)}
|
||||
{getErrorMessage(saveProfileError) && (
|
||||
<Typography
|
||||
color="error"
|
||||
sx={{ mt: 1, textAlign: 'right', width: '100%' }}
|
||||
>
|
||||
{getErrorMessage(saveProfileError)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
@@ -186,13 +174,6 @@ export function PersonalInformation() {
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : fetchProfileError ? (
|
||||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||
<Typography color="error">
|
||||
{getErrorMessage(fetchProfileError) ||
|
||||
t('settingForm.errorFetch')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import parsePhoneNumberFromString from 'libphonenumber-js';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
@@ -6,17 +6,17 @@ import { CardContainer } from '@/components/CardContainer';
|
||||
import PhoneDisplay from './phoneNumber/PhoneDisplay';
|
||||
import PhoneEditForm from './phoneNumber/PhoneEditForm';
|
||||
import PhoneActionButtons from './phoneNumber/PhoneActionButtons';
|
||||
import { CircularProgress, Box, Typography } from '@mui/material';
|
||||
import { CircularProgress, Box } from '@mui/material';
|
||||
import { toLocaleDigits } from '@/utils/persianDigit';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import {
|
||||
fetchProfile,
|
||||
sendVerificationCode,
|
||||
confirmPhoneNumberCode,
|
||||
changePhoneNumber,
|
||||
} from '../../api/settingsApi';
|
||||
import { type Phone } from '../../types/settingsType';
|
||||
import { useToast } from '@rkheftan/harmony-ui';
|
||||
import { useProfile } from '../../hooks/useProfile';
|
||||
|
||||
export function PhoneNumber() {
|
||||
const { t, i18n } = useTranslation('setting');
|
||||
@@ -24,167 +24,87 @@ export function PhoneNumber() {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [phoneNumber, setPhoneNumber] = useState('');
|
||||
const [verificationCode, setVerificationCode] = useState('');
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [buttonState, setButtonState] = useState<'default' | 'counting'>(
|
||||
'default',
|
||||
);
|
||||
const [isVerified, setIsVerified] = useState(false);
|
||||
const [phones, setPhones] = useState<Phone[]>([]);
|
||||
const [countryCode, setCountryCode] = useState('+98');
|
||||
const [formError, setFormError] = useState<string>();
|
||||
const [touched, setTouched] = useState<boolean>(false);
|
||||
const [phoneNumberError, setPhoneNumberError] = useState<string>();
|
||||
const [verificationCodeError, setVerificationCodeError] = useState<string>();
|
||||
const [phoneNumberTouched, setPhoneNumberTouched] = useState<boolean>(false);
|
||||
const [verificationCodeTouched, setVerificationCodeTouched] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { isLoadingProfile, refetchProfile } = useProfile();
|
||||
|
||||
const {
|
||||
data: profileData,
|
||||
loading: isLoading,
|
||||
error: fetchError,
|
||||
} = useApi(fetchProfile, { immediate: true });
|
||||
const { loading: isSendingCode, execute: executeSendCode } =
|
||||
useApi(sendVerificationCode);
|
||||
|
||||
const {
|
||||
data: sendCodeData,
|
||||
loading: isSendingCode,
|
||||
error: sendCodeError,
|
||||
execute: executeSendCode,
|
||||
} = useApi(sendVerificationCode);
|
||||
|
||||
const {
|
||||
data: confirmData,
|
||||
loading: isVerifying,
|
||||
error: confirmError,
|
||||
execute: executeConfirmCode,
|
||||
} = useApi(confirmPhoneNumberCode);
|
||||
|
||||
const {
|
||||
data: changePhoneData,
|
||||
loading: isChangingPhone,
|
||||
error: changePhoneError,
|
||||
execute: executeChangePhone,
|
||||
} = useApi(changePhoneNumber);
|
||||
|
||||
useEffect(() => {
|
||||
if (profileData?.success && profileData.phoneNumber) {
|
||||
setPhones([
|
||||
{
|
||||
phone: profileData.phoneNumber,
|
||||
time: '',
|
||||
withCode: profileData.phoneNumber,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}, [profileData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchError) {
|
||||
toast({
|
||||
message:
|
||||
getErrorMessage(fetchError) || t('settingForm.errorFetchPhoneNumber'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
}, [fetchError, toast, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sendCodeData?.success) {
|
||||
setButtonState('counting');
|
||||
setIsVerified(false);
|
||||
toast({
|
||||
message: t('settingForm.codeSentSuccessfully'),
|
||||
severity: 'success',
|
||||
});
|
||||
}
|
||||
}, [sendCodeData, t, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sendCodeError) {
|
||||
toast({
|
||||
message:
|
||||
getErrorMessage(sendCodeError) || t('settingForm.errorSendCode'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
}, [sendCodeError, toast, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (confirmData?.success && confirmData.confirm) {
|
||||
setIsVerified(true);
|
||||
setShowToast(true);
|
||||
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
|
||||
executeChangePhone({ phoneNumber: fullPhoneNumber });
|
||||
toast({
|
||||
message: t('settingForm.phoneVerified'),
|
||||
severity: 'success',
|
||||
});
|
||||
}
|
||||
}, [confirmData, countryCode, phoneNumber, executeChangePhone, t, toast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (confirmError) {
|
||||
toast({
|
||||
message:
|
||||
getErrorMessage(confirmError) || t('settingForm.errorConfirmCode'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
}, [confirmError, toast, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (changePhoneData?.success) {
|
||||
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
|
||||
setPhones([
|
||||
{
|
||||
phone: phoneNumber,
|
||||
time: t('settingForm.justNow'),
|
||||
withCode: fullPhoneNumber,
|
||||
},
|
||||
]);
|
||||
setIsEditing(false);
|
||||
}
|
||||
}, [changePhoneData, countryCode, phoneNumber, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (changePhoneError) {
|
||||
toast({
|
||||
message:
|
||||
getErrorMessage(changePhoneError) ||
|
||||
t('settingForm.errorChangePhone'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
}, [changePhoneError, toast, t]);
|
||||
|
||||
const apiError = useMemo(
|
||||
() => sendCodeError || confirmError || changePhoneError,
|
||||
[sendCodeError, confirmError, changePhoneError],
|
||||
const { loading: isVerifying, execute: executeConfirmCode } = useApi(
|
||||
confirmPhoneNumberCode,
|
||||
);
|
||||
|
||||
const getErrorMessage = (error: unknown): string | undefined => {
|
||||
if (!error) return undefined;
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
};
|
||||
const { loading: isChangingPhone, execute: executeChangePhone } =
|
||||
useApi(changePhoneNumber);
|
||||
|
||||
useEffect(() => {
|
||||
const loadProfile = async () => {
|
||||
const profileData = await refetchProfile();
|
||||
|
||||
if (!profileData) return;
|
||||
|
||||
if (profileData?.success) {
|
||||
setPhones([
|
||||
{
|
||||
phone: profileData.phoneNumber,
|
||||
time: '',
|
||||
withCode: profileData.phoneNumber,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
toast({
|
||||
message: profileData.message,
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
loadProfile();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!phoneNumber) {
|
||||
setPhoneNumberError(t('settingForm.thisFieldIsRequired'));
|
||||
return;
|
||||
}
|
||||
if (!isPhoneValid(countryCode, phoneNumber)) {
|
||||
setPhoneNumberError(t('settingForm.phoneNumberIsInvalid'));
|
||||
} else {
|
||||
setPhoneNumberError(undefined);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [countryCode, phoneNumber, phoneNumberTouched]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!verificationCode) {
|
||||
setVerificationCodeError(t('settingForm.verificationCodeRequired'));
|
||||
return;
|
||||
}
|
||||
setVerificationCodeError(undefined);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [verificationCode, verificationCodeTouched]);
|
||||
|
||||
const isPhoneValid = (code: string, phone: string) => {
|
||||
const phoneNum = parsePhoneNumberFromString(code + phone);
|
||||
return phoneNum?.isValid();
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setTouched(true);
|
||||
if (!phoneNumber) {
|
||||
toast({
|
||||
message: t('settingForm.phoneNumberIsInvalid'),
|
||||
severity: 'error',
|
||||
});
|
||||
setFormError(t('settingForm.thisFieldIsRequired'));
|
||||
return;
|
||||
}
|
||||
if (!isPhoneValid(countryCode, phoneNumber)) {
|
||||
setFormError(t('settingForm.phoneNumberIsInvalid'));
|
||||
const handleBlur = (key: string) => {
|
||||
if (key === 'phoneNumber') {
|
||||
setPhoneNumberTouched(true);
|
||||
} else {
|
||||
setFormError(undefined);
|
||||
setVerificationCodeTouched(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -195,36 +115,91 @@ export function PhoneNumber() {
|
||||
setVerificationCode('');
|
||||
setIsVerified(false);
|
||||
setButtonState('default');
|
||||
setShowToast(false);
|
||||
setFormError(undefined);
|
||||
setTouched(false);
|
||||
setPhoneNumberError(undefined);
|
||||
setPhoneNumberTouched(false);
|
||||
setVerificationCodeError(undefined);
|
||||
setVerificationCodeTouched(false);
|
||||
}
|
||||
return !prev;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSendCode = async () => {
|
||||
handleBlur();
|
||||
if (formError || !isPhoneValid(countryCode, phoneNumber)) return;
|
||||
return executeSendCode({
|
||||
setPhoneNumberTouched(true);
|
||||
if (phoneNumberError) return;
|
||||
|
||||
const result = await executeSendCode({
|
||||
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
|
||||
});
|
||||
|
||||
if (!result) return;
|
||||
|
||||
if (result?.success) {
|
||||
setButtonState('counting');
|
||||
setIsVerified(false);
|
||||
toast({
|
||||
message: result.message || t('settingForm.codeSentSuccessfully'),
|
||||
severity: 'success',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
message: result.message || t('message.serverError'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyCode = () => {
|
||||
if (!verificationCode) {
|
||||
setFormError(t('settingForm.verificationCodeRequired'));
|
||||
return;
|
||||
}
|
||||
setFormError(undefined);
|
||||
executeConfirmCode({
|
||||
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
|
||||
const handleVerifyCode = async () => {
|
||||
setVerificationCodeTouched(true);
|
||||
|
||||
if (verificationCodeError) return;
|
||||
|
||||
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
|
||||
|
||||
const confirmResult = await executeConfirmCode({
|
||||
phoneNumber: fullPhoneNumber,
|
||||
verifyCode: verificationCode,
|
||||
});
|
||||
};
|
||||
|
||||
const combinedError = formError || getErrorMessage(apiError);
|
||||
const inputError: boolean = touched && !!combinedError;
|
||||
if (!confirmResult) return;
|
||||
|
||||
if (!confirmResult.success || !confirmResult.confirm) {
|
||||
toast({
|
||||
message: confirmResult?.message || t('message.serverError'),
|
||||
severity: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsVerified(true);
|
||||
toast({
|
||||
message: t('settingForm.phoneVerified'),
|
||||
severity: 'success',
|
||||
});
|
||||
|
||||
const changeResult = await executeChangePhone({
|
||||
phoneNumber: fullPhoneNumber,
|
||||
});
|
||||
|
||||
if (!changeResult) return;
|
||||
|
||||
if (changeResult?.success) {
|
||||
setPhones([
|
||||
{
|
||||
phone: phoneNumber,
|
||||
time: t('settingForm.justNow'),
|
||||
withCode: fullPhoneNumber,
|
||||
},
|
||||
]);
|
||||
setIsEditing(false);
|
||||
refetchProfile({ force: true });
|
||||
} else {
|
||||
toast({
|
||||
message: changeResult?.message || t('message.serverError'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
@@ -240,7 +215,7 @@ export function PhoneNumber() {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{isLoading ? (
|
||||
{isLoadingProfile ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -252,13 +227,6 @@ export function PhoneNumber() {
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : fetchError ? (
|
||||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||
<Typography color="error">
|
||||
{getErrorMessage(fetchError) ||
|
||||
t('settingForm.errorFetchPhoneNumber')}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : isEditing ? (
|
||||
<PhoneEditForm
|
||||
phoneNumber={phoneNumber}
|
||||
@@ -269,19 +237,17 @@ export function PhoneNumber() {
|
||||
setVerificationCode={setVerificationCode}
|
||||
isVerified={isVerified}
|
||||
isVerifying={isVerifying || isSendingCode || isChangingPhone}
|
||||
isSendingCode={isSendingCode}
|
||||
buttonState={buttonState}
|
||||
setButtonState={setButtonState}
|
||||
handleSendCode={handleSendCode}
|
||||
handleVerifyClick={handleVerifyCode}
|
||||
error={combinedError}
|
||||
inputError={inputError}
|
||||
phoneNumberError={phoneNumberTouched ? phoneNumberError : undefined}
|
||||
verificationCodeError={
|
||||
verificationCodeTouched ? verificationCodeError : undefined
|
||||
}
|
||||
handleBlur={handleBlur}
|
||||
textFieldRef={textFieldRef as React.RefObject<HTMLDivElement>}
|
||||
inputRef={inputRef as React.RefObject<HTMLInputElement>}
|
||||
phones={phones}
|
||||
showToast={showToast}
|
||||
setShowToast={setShowToast}
|
||||
t={t}
|
||||
/>
|
||||
) : (
|
||||
<PhoneDisplay
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { PageWrapper } from '../PageWrapper';
|
||||
@@ -7,16 +7,16 @@ import SocialMediaMenu from './socialMedia/SocialMediaMenu';
|
||||
import SocialMediaDialog from './socialMedia/SocialMediaDialog';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
import type { Theme } from '@mui/material/styles';
|
||||
import { Box, CircularProgress, Typography } from '@mui/material';
|
||||
import { Box, CircularProgress } from '@mui/material';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import {
|
||||
fetchProfile,
|
||||
sendEmailCode,
|
||||
confirmEmailCode,
|
||||
changeEmail,
|
||||
} from '../../api/settingsApi';
|
||||
import { type EmailAccount } from '../../types/settingsType';
|
||||
import { useToast } from '@rkheftan/harmony-ui';
|
||||
import { useProfile } from '../../hooks/useProfile';
|
||||
|
||||
export function SocialMedia() {
|
||||
const { t } = useTranslation('setting');
|
||||
@@ -29,39 +29,23 @@ export function SocialMedia() {
|
||||
);
|
||||
const [emailList, setEmailList] = useState<EmailAccount[]>([]);
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
const showToast = useToast();
|
||||
const toast = useToast();
|
||||
|
||||
const {
|
||||
data: profileData,
|
||||
loading: isFetching,
|
||||
error: fetchError,
|
||||
} = useApi(fetchProfile, { immediate: true });
|
||||
const { isLoadingProfile, refetchProfile } = useProfile();
|
||||
|
||||
const {
|
||||
data: sendCodeData,
|
||||
loading: isSendingCode,
|
||||
error: sendCodeError,
|
||||
execute: executeSendCode,
|
||||
} = useApi(sendEmailCode);
|
||||
const { loading: isSendingCode, execute: executeSendCode } =
|
||||
useApi(sendEmailCode);
|
||||
|
||||
const {
|
||||
data: confirmData,
|
||||
loading: isConfirming,
|
||||
error: confirmError,
|
||||
execute: executeConfirmCode,
|
||||
} = useApi(confirmEmailCode);
|
||||
const { loading: isConfirming, execute: executeConfirmCode } =
|
||||
useApi(confirmEmailCode);
|
||||
|
||||
const {
|
||||
data: changeEmailData,
|
||||
loading: isChangingEmail,
|
||||
error: changeEmailError,
|
||||
execute: executeChangeEmail,
|
||||
} = useApi(changeEmail);
|
||||
const { loading: isChangingEmail, execute: executeChangeEmail } =
|
||||
useApi(changeEmail);
|
||||
|
||||
const fullScreen = useMediaQuery((theme: Theme) =>
|
||||
theme?.breakpoints.down('sm'),
|
||||
);
|
||||
const downMd = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'));
|
||||
const downMd = useMediaQuery((theme: Theme) => theme?.breakpoints.down('md'));
|
||||
const computedMaxWidth = (fullScreen ? 'xs' : downMd ? 'sm' : 'md') as
|
||||
| 'xs'
|
||||
| 'sm'
|
||||
@@ -70,76 +54,34 @@ export function SocialMedia() {
|
||||
| 'xl';
|
||||
|
||||
useEffect(() => {
|
||||
if (profileData?.success && profileData.email) {
|
||||
const { email } = profileData;
|
||||
setEmailList([
|
||||
{
|
||||
email,
|
||||
provider: email.includes('@gmail.') ? 'google' : 'email',
|
||||
time: '',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}, [profileData]);
|
||||
const loadProfile = async () => {
|
||||
const profileData = await refetchProfile();
|
||||
|
||||
useEffect(() => {
|
||||
if (sendCodeData?.success) {
|
||||
showToast({
|
||||
message: t('settingForm.verificationCodeSent'),
|
||||
severity: 'success',
|
||||
});
|
||||
setDialogStep('enterCode');
|
||||
}
|
||||
}, [sendCodeData, t, showToast]);
|
||||
if (!profileData) return;
|
||||
|
||||
useEffect(() => {
|
||||
if (confirmData?.success && confirmData.confirm) {
|
||||
showToast({
|
||||
message: t('settingForm.verificationSuccessful'),
|
||||
severity: 'success',
|
||||
});
|
||||
executeChangeEmail({ email: emailInput });
|
||||
}
|
||||
}, [confirmData, emailInput, executeChangeEmail, t, showToast]);
|
||||
if (profileData?.success) {
|
||||
const { email } = profileData;
|
||||
|
||||
useEffect(() => {
|
||||
if (changeEmailData?.success) {
|
||||
setEmailList((prev) => [
|
||||
...prev,
|
||||
{
|
||||
email: emailInput,
|
||||
provider: emailInput.includes('@gmail.') ? 'google' : 'email',
|
||||
time: t('settingForm.justNow'),
|
||||
},
|
||||
]);
|
||||
showToast({
|
||||
message: t('settingForm.emailChangedSuccessfully'),
|
||||
severity: 'success',
|
||||
});
|
||||
resetDialog();
|
||||
}
|
||||
}, [changeEmailData, emailInput, t, showToast]);
|
||||
if (!email) return;
|
||||
|
||||
useEffect(() => {
|
||||
if (sendCodeError) {
|
||||
showToast({
|
||||
message: getErrorMessage(sendCodeError) || t('settingForm.error'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
if (confirmError) {
|
||||
showToast({
|
||||
message: getErrorMessage(confirmError) || t('settingForm.error'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
if (changeEmailError) {
|
||||
showToast({
|
||||
message: getErrorMessage(changeEmailError) || t('settingForm.error'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
}, [sendCodeError, confirmError, changeEmailError, t, showToast]);
|
||||
setEmailList([
|
||||
{
|
||||
email,
|
||||
provider: email.includes('@gmail.') ? 'google' : 'email',
|
||||
time: '',
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
toast({
|
||||
message: profileData.message,
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadProfile();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const resetDialog = () => {
|
||||
setOpenDialog(false);
|
||||
@@ -149,34 +91,74 @@ export function SocialMedia() {
|
||||
setDialogStep('enterEmail');
|
||||
};
|
||||
|
||||
const handleSendCode = () => {
|
||||
const handleSendCode = async () => {
|
||||
if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(emailInput)) {
|
||||
setFormError(t('settingForm.emailIsInvalid'));
|
||||
return;
|
||||
}
|
||||
setFormError(null);
|
||||
executeSendCode({ email: emailInput });
|
||||
const sendCodeData = await executeSendCode({ email: emailInput });
|
||||
|
||||
if (!sendCodeData) return;
|
||||
|
||||
if (sendCodeData.success) {
|
||||
toast({
|
||||
message: sendCodeData.message || t('settingForm.verificationCodeSent'),
|
||||
severity: 'success',
|
||||
});
|
||||
setDialogStep('enterCode');
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmAndChangeEmail = () => {
|
||||
const handleConfirmAndChangeEmail = async () => {
|
||||
if (verificationCode.length < 4) {
|
||||
setFormError(t('settingForm.verificationCodeRequired'));
|
||||
return;
|
||||
}
|
||||
setFormError(null);
|
||||
executeConfirmCode({ email: emailInput, verifyCode: verificationCode });
|
||||
};
|
||||
const confirmData = await executeConfirmCode({
|
||||
email: emailInput,
|
||||
verifyCode: verificationCode,
|
||||
});
|
||||
|
||||
const getErrorMessage = (error: unknown): string | null => {
|
||||
if (!error) return null;
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
};
|
||||
if (!confirmData) return;
|
||||
|
||||
const apiError = useMemo(
|
||||
() => getErrorMessage(sendCodeError || confirmError || changeEmailError),
|
||||
[sendCodeError, confirmError, changeEmailError],
|
||||
);
|
||||
if (!confirmData.success || !confirmData.confirm) {
|
||||
toast({
|
||||
message: confirmData.message || t('settingForm.error'),
|
||||
severity: 'error',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
message: confirmData.message || t('settingForm.verificationSuccessful'),
|
||||
severity: 'success',
|
||||
});
|
||||
|
||||
const changeEmailData = await executeChangeEmail({ email: emailInput });
|
||||
|
||||
if (!changeEmailData) return;
|
||||
|
||||
if (changeEmailData.success) {
|
||||
setEmailList((prev) => [
|
||||
...prev,
|
||||
{
|
||||
email: emailInput,
|
||||
provider: emailInput.includes('@gmail.') ? 'google' : 'email',
|
||||
time: t('settingForm.justNow'),
|
||||
},
|
||||
]);
|
||||
toast({
|
||||
message: t('settingForm.emailChangedSuccessfully'),
|
||||
severity: 'success',
|
||||
});
|
||||
resetDialog();
|
||||
|
||||
refetchProfile({ force: true });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
@@ -187,7 +169,7 @@ export function SocialMedia() {
|
||||
<SocialMediaMenu t={t} onOpenDialog={() => setOpenDialog(true)} />
|
||||
}
|
||||
>
|
||||
{isFetching ? (
|
||||
{isLoadingProfile ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -199,12 +181,6 @@ export function SocialMedia() {
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : fetchError ? (
|
||||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||
<Typography color="error.main">
|
||||
{getErrorMessage(fetchError)}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<SocialMediaList t={t} emailList={emailList} />
|
||||
)}
|
||||
@@ -216,7 +192,7 @@ export function SocialMedia() {
|
||||
setEmailInput={setEmailInput}
|
||||
verificationCode={verificationCode}
|
||||
setVerificationCode={setVerificationCode}
|
||||
apiError={formError || apiError}
|
||||
apiError={formError}
|
||||
isLoading={isSendingCode || isConfirming || isChangingEmail}
|
||||
dialogStep={dialogStep}
|
||||
onSendCode={handleSendCode}
|
||||
|
||||
@@ -110,11 +110,11 @@ export const InfoRowEdit = forwardRef(
|
||||
error={touched.gender && data.gender === Gender.None}
|
||||
>
|
||||
<InputLabel>
|
||||
{t('settingForm.genderPlaceholder', { ns: 'setting' })}
|
||||
{t('settingForm.gender', { ns: 'setting' })}
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={data.gender === Gender.None ? '' : data.gender}
|
||||
label={t('settingForm.genderPlaceholder', { ns: 'setting' })}
|
||||
label={t('settingForm.gender', { ns: 'setting' })}
|
||||
onChange={(e) =>
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
|
||||
@@ -13,8 +13,8 @@ export default function PhoneDisplay({ phones }: PhoneDisplayProps) {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
py: 2,
|
||||
mx: 3,
|
||||
py: 6,
|
||||
mx: 2,
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -12,7 +12,8 @@ import { CountDownTimer } from '@/components/CountDownTimer';
|
||||
import { CountryCodeSelector } from '../../CountryCodeSelector';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type PhoneEditFormProps } from '@/features/profile/types/settingsType';
|
||||
import { useState } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function PhoneEditForm({
|
||||
phoneNumber,
|
||||
@@ -23,63 +24,51 @@ export default function PhoneEditForm({
|
||||
setVerificationCode,
|
||||
isVerified,
|
||||
isVerifying,
|
||||
isSendingCode,
|
||||
buttonState,
|
||||
setButtonState,
|
||||
handleSendCode,
|
||||
handleVerifyClick,
|
||||
error,
|
||||
inputError,
|
||||
handleBlur,
|
||||
textFieldRef,
|
||||
inputRef,
|
||||
phones,
|
||||
t,
|
||||
phoneNumberError,
|
||||
verificationCodeError,
|
||||
}: PhoneEditFormProps) {
|
||||
const isValidPhoneNumber = (phone: string) => {
|
||||
const digitsOnly = phone.replace(/\D/g, '');
|
||||
return digitsOnly.length >= 8 && digitsOnly.length <= 15;
|
||||
};
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Box sx={{ mb: 2, mx: 6 }}>
|
||||
<Typography variant="h6">
|
||||
{t('settingForm.editPhoneNumber')}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('settingForm.phoneNumberText')}({phones.map((p) => p.withCode)})
|
||||
{t('settingForm.verb')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ px: { sm: 4, xs: 2 }, my: 4 }}>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h6">{t('settingForm.editPhoneNumber')}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('settingForm.phoneNumberText')}({phones.map((p) => p.withCode)})
|
||||
{t('settingForm.verb')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
mx: 6,
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
name="phoneNumber"
|
||||
label={t('settingForm.newPhoneNumber')}
|
||||
type="tel"
|
||||
value={phoneNumber}
|
||||
ref={textFieldRef}
|
||||
inputRef={inputRef}
|
||||
onBlur={handleBlur}
|
||||
error={inputError}
|
||||
helperText={inputError ? error : ''}
|
||||
onBlur={() => handleBlur('phoneNumber')}
|
||||
error={!!phoneNumberError}
|
||||
helperText={phoneNumberError}
|
||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||
placeholder="09123456789"
|
||||
sx={{ width: { xs: '100%', sm: 474 } }}
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
buttonState === 'counting' ? (
|
||||
@@ -127,26 +116,14 @@ export default function PhoneEditForm({
|
||||
) : (
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={async () => {
|
||||
if (isValidPhoneNumber(phoneNumber)) {
|
||||
setIsSending(true);
|
||||
try {
|
||||
await handleSendCode();
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
loading={isSendingCode}
|
||||
onClick={handleSendCode}
|
||||
sx={{
|
||||
whiteSpace: 'nowrap',
|
||||
color: 'primary.main',
|
||||
textTransform: 'none',
|
||||
width: { xs: '100%', sm: 210 },
|
||||
width: { xs: '100%', sm: 208 },
|
||||
}}
|
||||
>
|
||||
{isSending ? (
|
||||
<CircularProgress size={20} />
|
||||
) : buttonState === 'counting' ? (
|
||||
{buttonState === 'counting' ? (
|
||||
<CountDownTimer
|
||||
initialSeconds={60}
|
||||
onComplete={() => setButtonState('default')}
|
||||
@@ -157,28 +134,29 @@ export default function PhoneEditForm({
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* buttonState === 'counting' && !isVerified && */}
|
||||
{buttonState === 'counting' && !isVerified && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
mx: 6,
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
name="verificationCode"
|
||||
label={t('settingForm.verificationCode')}
|
||||
type="tel"
|
||||
value={verificationCode}
|
||||
error={!!verificationCodeError}
|
||||
helperText={verificationCodeError}
|
||||
onBlur={() => handleBlur('verificationCode')}
|
||||
onChange={(e) =>
|
||||
setVerificationCode(e.target.value.replace(/\D/g, ''))
|
||||
}
|
||||
sx={{ width: { xs: '100%', sm: 474 } }}
|
||||
placeholder={t('settingForm.verificationCode')}
|
||||
/>
|
||||
|
||||
@@ -187,9 +165,8 @@ export default function PhoneEditForm({
|
||||
onClick={handleVerifyClick}
|
||||
disabled={isVerifying || verificationCode.length === 0}
|
||||
sx={{
|
||||
width: { xs: '100%', sm: 210 },
|
||||
bgcolor: 'primary.main',
|
||||
textTransform: 'none',
|
||||
width: { xs: '100%', sm: 200 },
|
||||
}}
|
||||
>
|
||||
{isVerifying ? (
|
||||
@@ -200,6 +177,6 @@ export default function PhoneEditForm({
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
13
src/features/profile/contexts/ProfileContext.ts
Normal file
13
src/features/profile/contexts/ProfileContext.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createContext } from 'react';
|
||||
import type { GetProfileApiResponse } from '../types/settingsApiType';
|
||||
|
||||
export interface ProfileContextType {
|
||||
isLoadingProfile: boolean;
|
||||
refetchProfile: (options?: {
|
||||
force?: boolean;
|
||||
}) => Promise<GetProfileApiResponse | undefined>;
|
||||
}
|
||||
|
||||
export const ProfileContext = createContext<ProfileContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
10
src/features/profile/hooks/useProfile.ts
Normal file
10
src/features/profile/hooks/useProfile.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import { ProfileContext } from '../contexts/ProfileContext';
|
||||
|
||||
export const useProfile = () => {
|
||||
const context = useContext(ProfileContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useProfile must be used within a ProfileProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
62
src/features/profile/providers/ProfileProvider.tsx
Normal file
62
src/features/profile/providers/ProfileProvider.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import { fetchProfile } from '../api/settingsApi';
|
||||
import { useCallback, useRef, useMemo, type ReactNode } from 'react';
|
||||
import { ProfileContext } from '../contexts/ProfileContext';
|
||||
import type { GetProfileApiResponse } from '../types/settingsApiType';
|
||||
|
||||
export const ProfileProvider = ({ children }: { children: ReactNode }) => {
|
||||
const profileCache = useRef<GetProfileApiResponse | null>(null);
|
||||
const pendingRequestRef = useRef<Promise<
|
||||
GetProfileApiResponse | undefined
|
||||
> | null>(null);
|
||||
|
||||
const { loading: isLoadingProfile, execute } = useApi(fetchProfile);
|
||||
|
||||
const refetchProfile = useCallback(
|
||||
async (options?: {
|
||||
force?: boolean;
|
||||
}): Promise<GetProfileApiResponse | undefined> => {
|
||||
// If forcing a refetch, clear the cache first
|
||||
if (options?.force) {
|
||||
profileCache.current = null;
|
||||
}
|
||||
|
||||
// Return cached data if it exists
|
||||
if (profileCache.current) {
|
||||
return profileCache.current;
|
||||
}
|
||||
|
||||
if (pendingRequestRef.current) {
|
||||
return pendingRequestRef.current;
|
||||
}
|
||||
|
||||
// If no request is pending, start a new one and store its promise
|
||||
try {
|
||||
pendingRequestRef.current = execute();
|
||||
const result = await pendingRequestRef.current;
|
||||
|
||||
// Update cache on success
|
||||
if (result?.success) {
|
||||
profileCache.current = result;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
// Clear the pending request ref once the request is complete
|
||||
pendingRequestRef.current = null;
|
||||
}
|
||||
},
|
||||
[execute],
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
isLoadingProfile,
|
||||
refetchProfile,
|
||||
}),
|
||||
[isLoadingProfile, refetchProfile],
|
||||
);
|
||||
|
||||
return (
|
||||
<ProfileContext.Provider value={value}>{children}</ProfileContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -12,12 +12,13 @@ import { DeviceMessage, Logout } from 'iconsax-react';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { PageWrapper } from '../components/PageWrapper';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { fetchProfile, deleteSessions } from '../api/settingsApi';
|
||||
import { deleteSessions } from '../api/settingsApi';
|
||||
import { type ApiSession } from '../types/settingsApiType';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import { formatDate } from '@/utils/formatSessionDate';
|
||||
import { type Device } from '../types/settingsType';
|
||||
import { useToast } from '@rkheftan/harmony-ui';
|
||||
import { useProfile } from '../hooks/useProfile';
|
||||
|
||||
export function ActiveDevicesPage() {
|
||||
const { t, i18n } = useTranslation('setting');
|
||||
@@ -27,50 +28,56 @@ export function ActiveDevicesPage() {
|
||||
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
||||
const showToast = useToast();
|
||||
|
||||
const {
|
||||
data: profileData,
|
||||
loading: isLoading,
|
||||
error: fetchError,
|
||||
} = useApi(fetchProfile, { immediate: true });
|
||||
const { isLoadingProfile, refetchProfile } = useProfile();
|
||||
|
||||
const {
|
||||
data: terminateData,
|
||||
loading: isTerminating,
|
||||
execute: executeTerminateAll,
|
||||
} = useApi(deleteSessions);
|
||||
const { loading: isTerminating, execute: executeTerminate } =
|
||||
useApi(deleteSessions);
|
||||
|
||||
useEffect(() => {
|
||||
if (profileData?.success && profileData.activeSessions) {
|
||||
const { sessions, currentKey } = profileData.activeSessions;
|
||||
const formattedDevices = sessions.map((session: ApiSession) => ({
|
||||
id: session.key,
|
||||
timeAndDate: formatDate(session.created, i18n.language, t),
|
||||
deviceModel: `${session.deviceOs} ${session.deviceName}`,
|
||||
ip: session.ipAddress,
|
||||
current: session.key === currentKey,
|
||||
}));
|
||||
const loadProfile = async () => {
|
||||
const profileData = await refetchProfile();
|
||||
|
||||
const sortedDevices = formattedDevices.sort((a, b) => {
|
||||
if (a.current && !b.current) return -1;
|
||||
if (!a.current && b.current) return 1;
|
||||
return 0;
|
||||
});
|
||||
if (!profileData) return;
|
||||
|
||||
setDevices(sortedDevices);
|
||||
}
|
||||
}, [profileData, i18n.language, t]);
|
||||
if (profileData.success) {
|
||||
if (!profileData.activeSessions) return;
|
||||
|
||||
useEffect(() => {
|
||||
if (terminateData?.success) {
|
||||
setDevices((prev) => prev.filter((d) => d.current));
|
||||
}
|
||||
}, [terminateData]);
|
||||
const { sessions, currentKey } = profileData.activeSessions;
|
||||
const formattedDevices = sessions.map((session: ApiSession) => ({
|
||||
id: session.key,
|
||||
timeAndDate: formatDate(session.created, i18n.language, t),
|
||||
deviceModel: `${session.deviceOs} ${session.deviceName}`,
|
||||
ip: session.ipAddress,
|
||||
current: session.key === currentKey,
|
||||
}));
|
||||
|
||||
const sortedDevices = formattedDevices.sort((a, b) => {
|
||||
if (a.current && !b.current) return -1;
|
||||
if (!a.current && b.current) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
setDevices(sortedDevices);
|
||||
} else {
|
||||
showToast({
|
||||
message: profileData.message || t('message.serverError'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadProfile();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleDeleteDevice = async (id: string) => {
|
||||
if (loadingDeleteIds.includes(id)) return;
|
||||
setLoadingDeleteIds((prev) => [...prev, id]);
|
||||
try {
|
||||
const { data } = await deleteSessions({ keys: [id] });
|
||||
const data = await executeTerminate({ keys: [id] });
|
||||
|
||||
if (!data) return;
|
||||
|
||||
if (data.success) {
|
||||
setDevices((prevDevices) => prevDevices.filter((d) => d.id !== id));
|
||||
showToast({ message: t('active.successDelete'), severity: 'success' });
|
||||
@@ -80,11 +87,6 @@ export function ActiveDevicesPage() {
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
showToast({
|
||||
message: t('active.deleteFailed'),
|
||||
severity: 'error',
|
||||
});
|
||||
} finally {
|
||||
setLoadingDeleteIds((prev) =>
|
||||
prev.filter((loadingId) => loadingId !== id),
|
||||
@@ -92,17 +94,22 @@ export function ActiveDevicesPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleTerminateAllOtherSessions = () => {
|
||||
const handleTerminateAllOtherSessions = async () => {
|
||||
const otherSessionKeys = devices.filter((d) => !d.current).map((d) => d.id);
|
||||
if (otherSessionKeys.length > 0) {
|
||||
executeTerminateAll({ keys: otherSessionKeys });
|
||||
}
|
||||
};
|
||||
const terminateData = await executeTerminate({ keys: otherSessionKeys });
|
||||
|
||||
const getErrorMessage = (error: unknown): string | null => {
|
||||
if (!error) return null;
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
if (!terminateData) return;
|
||||
|
||||
if (terminateData?.success) {
|
||||
setDevices((prev) => prev.filter((d) => d.current));
|
||||
} else {
|
||||
showToast({
|
||||
message: terminateData.message || t('active.deleteFailed'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -124,7 +131,7 @@ export function ActiveDevicesPage() {
|
||||
flexGrow: 0,
|
||||
width: 'auto',
|
||||
}}
|
||||
disabled={isLoading || isTerminating}
|
||||
disabled={isLoadingProfile || isTerminating}
|
||||
>
|
||||
{isTerminating
|
||||
? t('active.deleting')
|
||||
@@ -132,7 +139,7 @@ export function ActiveDevicesPage() {
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{isLoading ? (
|
||||
{isLoadingProfile ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -144,12 +151,6 @@ export function ActiveDevicesPage() {
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : fetchError ? (
|
||||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||
<Typography color="error.main">
|
||||
{getErrorMessage(fetchError)}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -12,10 +12,11 @@ import { CardContainer } from '@/components/CardContainer';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ThemeToggleButton } from '@/components/ThemToggle';
|
||||
import { PageWrapper } from '../components/PageWrapper';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { Icon, useToast } from '@rkheftan/harmony-ui';
|
||||
import { Sun1, Moon, Calendar1 } from 'iconsax-react';
|
||||
import { useApi } from '@/hooks/useApi';
|
||||
import { fetchProfile, saveSettings } from '../api/settingsApi';
|
||||
import { saveSettings } from '../api/settingsApi';
|
||||
import { useProfile } from '../hooks/useProfile';
|
||||
|
||||
type ThemeMode = 'light' | 'dark';
|
||||
type CalendarType = 'christian' | 'solar' | 'lunar';
|
||||
@@ -51,40 +52,50 @@ export function SettingPage() {
|
||||
const [draftSettings, setDraftSettings] =
|
||||
useState<SettingsState>(savedSettings);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const showToast = useToast();
|
||||
|
||||
const {
|
||||
data: profileData,
|
||||
loading: isFetching,
|
||||
error: fetchError,
|
||||
} = useApi(fetchProfile);
|
||||
const { isLoadingProfile, refetchProfile } = useProfile();
|
||||
|
||||
const {
|
||||
loading: isSaving,
|
||||
error: saveError,
|
||||
execute: executeSaveSettings,
|
||||
} = useApi(saveSettings);
|
||||
const { loading: isSaving, execute: executeSaveSettings } =
|
||||
useApi(saveSettings);
|
||||
|
||||
useEffect(() => {
|
||||
if (profileData?.success && profileData.userSettings) {
|
||||
const { theme, calendarType, language } = profileData.userSettings;
|
||||
const themeReverseMap: { [key: number]: ThemeMode | undefined } = {
|
||||
1: 'light',
|
||||
2: 'dark',
|
||||
};
|
||||
const newSettings: SettingsState = {
|
||||
theme: themeReverseMap[theme] || 'light',
|
||||
calendar:
|
||||
calendarOptions.find((c) => c.apiValue === calendarType)?.key ||
|
||||
'solar',
|
||||
language:
|
||||
languageOptions.find((l) => l.apiValue === language)?.code || 'en',
|
||||
};
|
||||
setSavedSettings(newSettings);
|
||||
setDraftSettings(newSettings);
|
||||
setMode(newSettings.theme);
|
||||
i18n.changeLanguage(newSettings.language);
|
||||
}
|
||||
}, [profileData, setMode, i18n]);
|
||||
const loadProfile = async () => {
|
||||
const profileData = await refetchProfile();
|
||||
|
||||
if (!profileData) return;
|
||||
|
||||
if (profileData.success) {
|
||||
if (!profileData.userSettings) return;
|
||||
|
||||
const { theme, calendarType, language } = profileData.userSettings;
|
||||
|
||||
const themeReverseMap: { [key: number]: ThemeMode | undefined } = {
|
||||
1: 'light',
|
||||
2: 'dark',
|
||||
};
|
||||
const newSettings: SettingsState = {
|
||||
theme: themeReverseMap[theme] || 'light',
|
||||
calendar:
|
||||
calendarOptions.find((c) => c.apiValue === calendarType)?.key ||
|
||||
'solar',
|
||||
language:
|
||||
languageOptions.find((l) => l.apiValue === language)?.code || 'en',
|
||||
};
|
||||
setSavedSettings(newSettings);
|
||||
setDraftSettings(newSettings);
|
||||
setMode(newSettings.theme);
|
||||
i18n.changeLanguage(newSettings.language);
|
||||
} else {
|
||||
showToast({
|
||||
message: profileData.message || t('message.serverError'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
loadProfile();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
@@ -115,22 +126,27 @@ export function SettingPage() {
|
||||
language: languageObj.apiValue,
|
||||
});
|
||||
|
||||
if (!result) return;
|
||||
|
||||
if (result?.success) {
|
||||
setMode(draftSettings.theme);
|
||||
setSavedSettings(draftSettings);
|
||||
i18n.changeLanguage(draftSettings.language);
|
||||
setIsEditing(false);
|
||||
|
||||
refetchProfile({ force: true });
|
||||
} else {
|
||||
// Handle save failure
|
||||
showToast({
|
||||
message: result.message || t('message.serverError'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setIsEditing(true);
|
||||
}
|
||||
};
|
||||
const getErrorMessage = (error: unknown): string | null => {
|
||||
if (!error) return null;
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
@@ -150,7 +166,7 @@ export function SettingPage() {
|
||||
textTransform: 'none',
|
||||
fontSize: { xs: '0.85rem', sm: '1rem' },
|
||||
}}
|
||||
disabled={isSaving || isFetching}
|
||||
disabled={isSaving || isLoadingProfile}
|
||||
>
|
||||
{t('settings.rejectButton')}
|
||||
</Button>
|
||||
@@ -159,7 +175,7 @@ export function SettingPage() {
|
||||
onClick={handleEditToggle}
|
||||
size="large"
|
||||
variant={isEditing ? 'contained' : 'outlined'}
|
||||
disabled={isSaving || isFetching}
|
||||
disabled={isSaving || isLoadingProfile}
|
||||
>
|
||||
{isSaving ? (
|
||||
<CircularProgress size={24} />
|
||||
@@ -172,7 +188,7 @@ export function SettingPage() {
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{isFetching ? (
|
||||
{isLoadingProfile ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -184,12 +200,6 @@ export function SettingPage() {
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : fetchError ? (
|
||||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||
<Typography color="error.main">
|
||||
{getErrorMessage(fetchError)}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -200,11 +210,6 @@ export function SettingPage() {
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{getErrorMessage(saveError) && (
|
||||
<Typography color="error.main" variant="body2" mb={2}>
|
||||
{getErrorMessage(saveError)}
|
||||
</Typography>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
|
||||
@@ -27,14 +27,14 @@ export interface LoginLog {
|
||||
/* Profile API Types */
|
||||
export interface GetProfileApiResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
message: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
nationalCode?: string;
|
||||
gender?: Gender;
|
||||
countryCode?: string;
|
||||
profileImageUrl?: string;
|
||||
phoneNumber?: string;
|
||||
phoneNumber: string;
|
||||
userSettings?: UserSettingsFromApi;
|
||||
activeSessions?: ActiveSessionsData;
|
||||
loginLogs?: LoginLog[];
|
||||
|
||||
@@ -36,8 +36,6 @@ export interface PasswordDialogProps {
|
||||
loading: boolean;
|
||||
handleSubmit: () => void;
|
||||
changePassword: boolean;
|
||||
apiError: string | null;
|
||||
t: (key: string) => string;
|
||||
hasMinLength: boolean;
|
||||
hasNumber: boolean;
|
||||
hasUpperAndLower: boolean;
|
||||
@@ -111,19 +109,15 @@ export interface PhoneEditFormProps {
|
||||
setVerificationCode: (v: string) => void;
|
||||
isVerified: boolean;
|
||||
isVerifying: boolean;
|
||||
isSendingCode: boolean;
|
||||
buttonState: 'default' | 'counting';
|
||||
setButtonState: (v: 'default' | 'counting') => void;
|
||||
handleSendCode: () => void;
|
||||
handleVerifyClick: () => void;
|
||||
error?: string;
|
||||
inputError: boolean;
|
||||
handleBlur: () => void;
|
||||
textFieldRef: React.RefObject<HTMLDivElement>;
|
||||
inputRef: React.RefObject<HTMLInputElement>;
|
||||
phoneNumberError?: string;
|
||||
verificationCodeError?: string;
|
||||
handleBlur: (key: string) => void;
|
||||
phones: { phone: string; time: string; withCode: string }[];
|
||||
showToast: boolean;
|
||||
setShowToast: (v: boolean) => void;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
export interface DisplayFieldProps {
|
||||
|
||||
@@ -17,7 +17,7 @@ export const RtlProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
key: isRtl ? 'muirtl' : 'muiltr',
|
||||
stylisPlugins: isRtl ? [prefixer, rtlPlugin] : [],
|
||||
});
|
||||
}, [i18n]);
|
||||
}, [i18n, i18n.language]);
|
||||
|
||||
return <CacheProvider value={cacheRtl}>{children}</CacheProvider>;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Layout } from '@/components';
|
||||
import { NavigateWithToast } from '@/components/NavigateWithToast';
|
||||
import { ProfileProvider } from '@/features/profile/providers/ProfileProvider';
|
||||
import {
|
||||
Calendar,
|
||||
Devices,
|
||||
@@ -93,7 +94,7 @@ export const appRoutes: RouteConfig[] = [
|
||||
element: <AccountCreatedPage />,
|
||||
authorize: true,
|
||||
},
|
||||
{ path: '/signup', element: <UserCompletionPage />, authorize: true },
|
||||
{ path: '/signup', element: <UserCompletionPage /> },
|
||||
{
|
||||
path: '/forget-password',
|
||||
element: <ForgetPasswordPage />,
|
||||
@@ -101,7 +102,11 @@ export const appRoutes: RouteConfig[] = [
|
||||
|
||||
{
|
||||
path: '/setting',
|
||||
element: <Layout />,
|
||||
element: (
|
||||
<ProfileProvider>
|
||||
<Layout />
|
||||
</ProfileProvider>
|
||||
),
|
||||
authorize: true,
|
||||
children: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user