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