fix: merge style bugs

This commit is contained in:
Koosha Lahouti
2025-08-17 14:31:18 +03:30
parent dd6e2fdd0d
commit 7e5ed6e2fc
14 changed files with 254 additions and 138 deletions

View File

@@ -2,31 +2,19 @@ import {
type SendEmailOtpPayload,
type ConfirmEmailOtpPayload,
type CompleteUserInfoPayload,
type CompleteUserInfoResponse,
type GenericApiResponse,
} from '../types/completionFormApiTypes';
import apiClient from '@/lib/apiClient';
export const sendEmailOtpApi = async (
payload: SendEmailOtpPayload,
): Promise<GenericApiResponse & { codeSentAnyway?: boolean }> => {
const { data } = await apiClient.post('/User/SendEmailOtp', payload);
return data;
export const sendEmailOtpApi = async (payload: SendEmailOtpPayload) => {
return apiClient.post('/User/SendEmailOtp', payload);
};
export const confirmEmailOtpApi = async (
payload: ConfirmEmailOtpPayload,
): Promise<GenericApiResponse> => {
const { data } = await apiClient.post('/User/ConfirmEmailOtp', payload);
return data;
export const confirmEmailOtpApi = async (payload: ConfirmEmailOtpPayload) => {
return apiClient.post('/User/ConfirmEmailOtp', payload);
};
export const completeUserInformationApi = async (
payload: CompleteUserInfoPayload,
): Promise<CompleteUserInfoResponse> => {
const { data } = await apiClient.post(
'/User/CompleteUserInformation',
payload,
);
return data;
) => {
return apiClient.post('/User/CompleteUserInformation', payload);
};

View File

@@ -16,12 +16,6 @@ import {
confirmEmailOtpApi,
completeUserInformationApi,
} from '../api/userCompletion';
import {
type SendEmailOtpPayload,
type ConfirmEmailOtpPayload,
type CompleteUserInfoPayload,
} from '../types/completionFormApiTypes';
import { type ApiResponse } from '@/types/apiResponse';
import { containsSymbol } from '@/utils/regexes/containsSymbol';
import { least8Chars } from '@/utils/regexes/least8Chars';
import { containsNumber } from '@/utils/regexes/containsNumber';
@@ -63,46 +57,20 @@ export function UserCompletionPage() {
const matchPassword = password === confirmPassword;
const correctEmail = isEmail(email);
const { execute: sendCode, data: sendCodeData } = useApi(
async (payload: SendEmailOtpPayload) => {
const result = await sendEmailOtpApi(payload);
const conformingResult: ApiResponse = {
...result,
errorCode: result.errorCode || 0,
validations: [],
};
return { data: conformingResult };
},
);
const { execute: sendCode, data: sendCodeData } = useApi(sendEmailOtpApi);
const {
execute: verifyCode,
loading: isVerifyingCode,
data: verifyCodeData,
} = useApi(async (payload: ConfirmEmailOtpPayload) => {
const result = await confirmEmailOtpApi(payload);
const conformingResult: ApiResponse = {
...result,
errorCode: result.errorCode || 0,
validations: [],
};
return { data: conformingResult };
});
loading: isVerifyingCode,
} = useApi(confirmEmailOtpApi);
const {
execute: submitForm,
data: submitData,
loading: isSubmitting,
error: submitError,
data: submitData,
} = useApi(async (payload: CompleteUserInfoPayload) => {
const result = await completeUserInformationApi(payload);
const conformingResult: ApiResponse = {
...result,
errorCode: result.errorCode || 0,
validations: [],
};
return { data: conformingResult };
});
} = useApi(completeUserInformationApi);
const getErrorMessage = (error: unknown): string | null => {
if (!error) return null;
@@ -112,9 +80,10 @@ export function UserCompletionPage() {
useEffect(() => {
if (sendCodeData) {
if (sendCodeData.success) {
if (sendCodeData.data.success) {
showToast({
message: sendCodeData.message || t('completion.successfullCodeSent'),
message:
sendCodeData.data.message || t('completion.successfullCodeSent'),
severity: 'success',
});
setCodeSent(true);
@@ -122,7 +91,7 @@ export function UserCompletionPage() {
setCountdown(120);
} else {
showToast({
message: sendCodeData.message || t('completion.problem'),
message: sendCodeData.data.message || t('completion.problem'),
severity: 'error',
});
}
@@ -131,15 +100,15 @@ export function UserCompletionPage() {
useEffect(() => {
if (verifyCodeData) {
if (verifyCodeData.success) {
if (verifyCodeData.data.success) {
setEmailVerified(true);
showToast({
message: verifyCodeData.message || t('completion.codeVerified'),
message: verifyCodeData.data.message || t('completion.codeVerified'),
severity: 'success',
});
} else {
showToast({
message: verifyCodeData.message || t('completion.invalidCode'),
message: verifyCodeData.data.message || t('completion.invalidCode'),
severity: 'error',
});
setEmailVerified(false);
@@ -151,13 +120,13 @@ export function UserCompletionPage() {
if (submitData) {
showToast({
message:
submitData.message ||
submitData.data.message ||
t(
submitData.success
submitData.data.success
? 'completion.submitSuccess'
: 'completion.submitError',
),
severity: submitData.success ? 'success' : 'error',
severity: submitData.data.success ? 'success' : 'error',
});
} else if (submitError) {
showToast({
@@ -205,7 +174,6 @@ export function UserCompletionPage() {
const handleSubmit = () => {
submitForm({
userId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
firstName,
lastName,
gender: sex,
@@ -325,7 +293,7 @@ export function UserCompletionPage() {
onSubmit={handleSubmit}
loading={isSubmitting}
error={getErrorMessage(submitError)}
success={!!submitData?.success}
success={!!submitData?.data.success}
/>
</Box>
</Box>

View File

@@ -14,7 +14,6 @@ export interface ConfirmEmailOtpPayload {
}
export interface CompleteUserInfoPayload {
userId: string;
firstName: string;
lastName: string;
gender: 0 | 1 | 2;

View File

@@ -15,7 +15,7 @@ import {
import { type InfoRowData } from '../types/settingsType';
export async function fetchProfile() {
return apiClient.post<GetProfileApiResponse>('/Profile/GetProfile');
return apiClient.post<GetProfileApiResponse>('/Profile/GetProfile', {});
}
export async function saveProfile(payload: {

View File

@@ -65,7 +65,6 @@ export function PasswordDialog({
px: { xs: 2, sm: 3 },
}}
>
{/* ✅ 4. API ERROR DISPLAY ADDED */}
{apiError && (
<Typography color="error" variant="body2" textAlign="center">
{apiError}

View File

@@ -21,6 +21,7 @@ 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';
export function PasswordSecurity() {
const theme = useTheme();
@@ -42,6 +43,7 @@ export function PasswordSecurity() {
hasNumber && hasMinLength && hasUpperAndLower && hasSpecialChar;
const matchPassword = password === confirmPassword;
const showToast = useToast();
const {
data: profileData,
@@ -79,6 +81,12 @@ export function PasswordSecurity() {
useEffect(() => {
if (addData?.success || changeData?.success) {
showToast({
message: changePasswordUI
? t('securityForm.passwordChanged')
: t('securityForm.passwordAdded'),
severity: 'success',
});
if (!changePasswordUI) {
setChangePasswordUI(true);
}
@@ -87,7 +95,16 @@ export function PasswordSecurity() {
setConfirmPassword('');
setCurrentPassword('');
}
}, [addData, changeData, changePasswordUI]);
}, [addData, changeData, changePasswordUI, showToast, t]);
useEffect(() => {
if (addError || changeError) {
showToast({
message: getErrorMessage(addError || changeError) || t('error.general'),
severity: 'error',
});
}
}, [addError, changeError, t, showToast]);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
@@ -120,7 +137,12 @@ export function PasswordSecurity() {
title={t('securityForm.password')}
subtitle={t('securityForm.determinePassword')}
action={
<Button variant="contained" onClick={handleOpen}>
<Button
variant="contained"
onClick={handleOpen}
size="medium"
sx={{ flexShrink: 0, flexGrow: 0, width: 'auto' }}
>
{changePasswordUI
? t('securityForm.changePassword')
: t('securityForm.addPassword')}

View File

@@ -13,12 +13,14 @@ 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';
export function RecentLogins() {
const { t, i18n } = useTranslation('setting');
const [logs, setLogs] = useState<LoginLog[]>([]);
const theme = useTheme();
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
const showToast = useToast();
const {
data: profileData,
@@ -32,6 +34,14 @@ export function RecentLogins() {
}
}, [profileData]);
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;

View File

@@ -9,6 +9,7 @@ import { PageWrapper } from '../PageWrapper';
import { useApi } from '@/hooks/useApi';
import { Gender, type InfoRowData } from '../../types/settingsType';
import { fetchProfile, saveProfile } from '../../api/settingsApi';
import { useToast } from '@rkheftan/harmony-ui';
export function PersonalInformation() {
const { t } = useTranslation('setting');
@@ -22,6 +23,7 @@ export function PersonalInformation() {
country: '',
});
const [originalData, setOriginalData] = useState<InfoRowData | null>(null);
const showToast = useToast();
const {
data: profileData,
@@ -55,10 +57,14 @@ export function PersonalInformation() {
useEffect(() => {
if (saveData?.success) {
showToast({
message: t('settingForm.successSaveProfile'),
severity: 'success',
});
setIsEditing(false);
setOriginalData(data);
}
}, [saveData, data]);
}, [saveData, data, showToast, t]);
const initials = `${data?.firstName?.trim()[0] || ''}${data?.lastName?.trim()[0] || ''}`;
@@ -84,6 +90,25 @@ export function PersonalInformation() {
if (error instanceof Error) return error.message;
return String(error);
};
useEffect(() => {
if (saveProfileError) {
showToast({
message:
getErrorMessage(saveProfileError) || t('settingForm.errorSave'),
severity: 'error',
});
}
}, [saveProfileError, showToast, t]);
useEffect(() => {
if (fetchProfileError) {
showToast({
message:
getErrorMessage(fetchProfileError) || t('settingForm.errorFetch'),
severity: 'error',
});
}
}, [fetchProfileError, showToast, t]);
return (
<PageWrapper>
@@ -95,62 +120,51 @@ export function PersonalInformation() {
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
width: '100%',
gap: 1,
}}
>
<Box
sx={{
display: 'flex',
gap: 1,
flexWrap: 'wrap',
justifyContent: 'flex-end',
}}
>
{isEditing ? (
<>
<Button
variant="text"
onClick={handleCancelClick}
size="large"
sx={{
color: 'primary.main',
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
}}
>
{t('settingForm.rejectButton')}
</Button>
<Button
onClick={handleSaveClick}
size="large"
variant="contained"
sx={{
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
}}
disabled={isSavingProfile}
>
{isSavingProfile ? (
<CircularProgress size={24} color="inherit" />
) : (
t('settingForm.saveButton')
)}
</Button>
</>
) : (
{isEditing ? (
<>
<Button
onClick={handleEditClick}
variant="text"
onClick={handleCancelClick}
size="large"
variant="outlined"
sx={{ borderRadius: 1 }}
disabled={isLoadingProfile}
sx={{
color: 'primary.main',
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
}}
>
{t('settingForm.editButton')}
{t('settingForm.rejectButton')}
</Button>
)}
</Box>
<Button
onClick={handleSaveClick}
size="large"
variant="contained"
sx={{
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
}}
disabled={isSavingProfile}
>
{isSavingProfile ? (
<CircularProgress size={24} color="inherit" />
) : (
t('settingForm.saveButton')
)}
</Button>
</>
) : (
<Button
onClick={handleEditClick}
size="large"
variant="outlined"
sx={{ borderRadius: 1 }}
disabled={isLoadingProfile}
>
{t('settingForm.editButton')}
</Button>
)}
{getErrorMessage(saveProfileError) && (
<Typography
color="error"

View File

@@ -16,9 +16,11 @@ import {
changePhoneNumber,
} from '../../api/settingsApi';
import { type Phone } from '../../types/settingsType';
import { useToast } from '@rkheftan/harmony-ui';
export function PhoneNumber() {
const { t, i18n } = useTranslation('setting');
const toast = useToast();
const [isEditing, setIsEditing] = useState(false);
const [phoneNumber, setPhoneNumber] = useState('');
const [verificationCode, setVerificationCode] = useState('');
@@ -74,12 +76,36 @@ export function 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]);
}, [sendCodeData, t, toast]);
useEffect(() => {
if (sendCodeError) {
toast({
message:
getErrorMessage(sendCodeError) || t('settingForm.errorSendCode'),
severity: 'error',
});
}
}, [sendCodeError, toast, t]);
useEffect(() => {
if (confirmData?.success && confirmData.confirm) {
@@ -87,8 +113,22 @@ export function PhoneNumber() {
setShowToast(true);
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
executeChangePhone({ phoneNumber: fullPhoneNumber });
toast({
message: t('settingForm.phoneVerified'),
severity: 'success',
});
}
}, [confirmData, countryCode, phoneNumber, executeChangePhone]);
}, [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) {
@@ -101,8 +141,23 @@ export function PhoneNumber() {
},
]);
setIsEditing(false);
toast({
message: t('settingForm.phoneChangedSuccessfully'),
severity: 'success',
});
}
}, [changePhoneData, countryCode, phoneNumber, t]);
}, [changePhoneData, countryCode, phoneNumber, t, toast]);
useEffect(() => {
if (changePhoneError) {
toast({
message:
getErrorMessage(changePhoneError) ||
t('settingForm.errorChangePhone'),
severity: 'error',
});
}
}, [changePhoneError, toast, t]);
const apiError = useMemo(
() => sendCodeError || confirmError || changePhoneError,
@@ -151,7 +206,6 @@ export function PhoneNumber() {
const handleSendCode = () => {
handleBlur();
if (formError || !isPhoneValid(countryCode, phoneNumber)) return;
executeSendCode({
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
});

View File

@@ -16,6 +16,7 @@ import {
changeEmail,
} from '../../api/settingsApi';
import { type EmailAccount } from '../../types/settingsType';
import { useToast } from '@rkheftan/harmony-ui';
export function SocialMedia() {
const { t } = useTranslation('setting');
@@ -28,6 +29,7 @@ export function SocialMedia() {
);
const [emailList, setEmailList] = useState<EmailAccount[]>([]);
const [formError, setFormError] = useState<string | null>(null);
const showToast = useToast();
const {
data: profileData,
@@ -82,15 +84,23 @@ export function SocialMedia() {
useEffect(() => {
if (sendCodeData?.success) {
showToast({
message: t('settingForm.verificationCodeSent'),
severity: 'success',
});
setDialogStep('enterCode');
}
}, [sendCodeData]);
}, [sendCodeData, t, showToast]);
useEffect(() => {
if (confirmData?.success && confirmData.confirm) {
showToast({
message: t('settingForm.verificationSuccessful'),
severity: 'success',
});
executeChangeEmail({ email: emailInput });
}
}, [confirmData, emailInput, executeChangeEmail]);
}, [confirmData, emailInput, executeChangeEmail, t, showToast]);
useEffect(() => {
if (changeEmailData?.success) {
@@ -102,9 +112,34 @@ export function SocialMedia() {
time: t('settingForm.justNow'),
},
]);
showToast({
message: t('settingForm.emailChangedSuccessfully'),
severity: 'success',
});
resetDialog();
}
}, [changeEmailData, emailInput, t]);
}, [changeEmailData, emailInput, t, showToast]);
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]);
const resetDialog = () => {
setOpenDialog(false);
@@ -115,7 +150,7 @@ export function SocialMedia() {
};
const handleSendCode = () => {
if (!/^\S+@\S+\.\S+$/.test(emailInput)) {
if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(emailInput)) {
setFormError(t('settingForm.emailIsInvalid'));
return;
}

View File

@@ -68,6 +68,9 @@ export function ProfileImage({
sx={{
borderRadius: 2,
textTransform: 'none',
flexShrink: 0,
flexGrow: 0,
width: 'auto',
}}
startIcon={
<Icon

View File

@@ -12,6 +12,7 @@ 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';
export default function PhoneEditForm({
phoneNumber,
@@ -39,6 +40,8 @@ export default function PhoneEditForm({
return digitsOnly.length >= 8 && digitsOnly.length <= 15;
};
const [isSending, setIsSending] = useState(false);
return (
<>
<Box sx={{ width: '100%' }}>
@@ -73,7 +76,7 @@ export default function PhoneEditForm({
error={inputError}
helperText={inputError ? error : ''}
onChange={(e) => setPhoneNumber(e.target.value)}
sx={{ flex: '1 1 220px', minWidth: 0 }}
sx={{ flex: '1 1 220px' }}
placeholder="09123456789"
InputProps={{
endAdornment:
@@ -122,12 +125,18 @@ export default function PhoneEditForm({
) : (
<Button
variant="text"
onClick={() => {
onClick={async () => {
if (isValidPhoneNumber(phoneNumber)) {
handleSendCode();
setIsSending(true);
try {
await handleSendCode();
} finally {
setIsSending(false);
}
}
}}
disabled={
isSending ||
buttonState === 'counting' ||
phoneNumber.length === 0 ||
!isValidPhoneNumber(phoneNumber)
@@ -138,7 +147,9 @@ export default function PhoneEditForm({
textTransform: 'none',
}}
>
{buttonState === 'counting' ? (
{isSending ? (
<CircularProgress size={20} />
) : buttonState === 'counting' ? (
<CountDownTimer
initialSeconds={60}
onComplete={() => setButtonState('default')}

View File

@@ -17,6 +17,7 @@ import { type ApiSession } from '../types/settingsApiType';
import { useApi } from '@/hooks/useApi';
import { formatDate } from '@/utils/formatSessionDate';
import { type Device } from '../types/settingsType';
import { useToast } from '@rkheftan/harmony-ui';
export function ActiveDevicesPage() {
const { t, i18n } = useTranslation('setting');
@@ -24,6 +25,7 @@ export function ActiveDevicesPage() {
const [loadingDeleteIds, setLoadingDeleteIds] = useState<string[]>([]);
const theme = useTheme();
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
const showToast = useToast();
const {
data: profileData,
@@ -65,10 +67,17 @@ export function ActiveDevicesPage() {
if (data.success) {
setDevices((prevDevices) => prevDevices.filter((d) => d.id !== id));
} else {
console.error('Delete failed:', data.message);
showToast({
message: data.message || t('active.deleteFailed'),
severity: 'error',
});
}
} catch (error: unknown) {
console.error('Delete error:', error);
// console.error('Delete error:', error);
showToast({
message: t('active.deleteFailed'),
severity: 'error',
});
} finally {
setLoadingDeleteIds((prev) =>
prev.filter((loadingId) => loadingId !== id),
@@ -104,6 +113,9 @@ export function ActiveDevicesPage() {
borderColor: 'error.main',
color: 'error.main',
textTransform: 'none',
flexShrink: 0,
flexGrow: 0,
width: 'auto',
}}
disabled={isLoading || isTerminating}
>
@@ -215,6 +227,7 @@ export function ActiveDevicesPage() {
whiteSpace: 'nowrap',
color: 'success.main',
textTransform: 'none',
maxWidth: '125px',
'&.Mui-disabled': {
color: 'success.main',
borderColor: 'success.main',

View File

@@ -56,7 +56,7 @@ export function SettingPage() {
data: profileData,
loading: isFetching,
error: fetchError,
} = useApi(fetchProfile, { immediate: true });
} = useApi(fetchProfile);
const {
data: saveData,
@@ -142,7 +142,7 @@ export function SettingPage() {
subtitle={t('settings.description')}
highlighted={isEditing}
action={
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Box sx={{ display: 'flex', gap: 1 }}>
{isEditing && (
<Button
variant="text"