fix: dashboard api calls, multi profile fetch
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user