1
.env
1
.env
@@ -4,3 +4,4 @@ VITE_API_URL=https://accounts.business-harmony.com/api/
|
|||||||
VITE_IDENTITY_URL=https://accounts.business-harmony.com/connect/token
|
VITE_IDENTITY_URL=https://accounts.business-harmony.com/connect/token
|
||||||
VITE_IDENTITY_CLIENT_ID=harmony_identity
|
VITE_IDENTITY_CLIENT_ID=harmony_identity
|
||||||
VITE_IDENTITY_SCOPE=openid profile offline_access harmony_identity
|
VITE_IDENTITY_SCOPE=openid profile offline_access harmony_identity
|
||||||
|
IMAGE_BASE_URL=https://accounts.business-harmony.com/uploads/
|
||||||
|
|||||||
@@ -2,31 +2,19 @@ import {
|
|||||||
type SendEmailOtpPayload,
|
type SendEmailOtpPayload,
|
||||||
type ConfirmEmailOtpPayload,
|
type ConfirmEmailOtpPayload,
|
||||||
type CompleteUserInfoPayload,
|
type CompleteUserInfoPayload,
|
||||||
type CompleteUserInfoResponse,
|
|
||||||
type GenericApiResponse,
|
|
||||||
} from '../types/completionFormApiTypes';
|
} from '../types/completionFormApiTypes';
|
||||||
import apiClient from '@/lib/apiClient';
|
import apiClient from '@/lib/apiClient';
|
||||||
|
|
||||||
export const sendEmailOtpApi = async (
|
export const sendEmailOtpApi = async (payload: SendEmailOtpPayload) => {
|
||||||
payload: SendEmailOtpPayload,
|
return apiClient.post('/User/SendEmailOtp', payload);
|
||||||
): Promise<GenericApiResponse & { codeSentAnyway?: boolean }> => {
|
|
||||||
const { data } = await apiClient.post('/User/SendEmailOtp', payload);
|
|
||||||
return data;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const confirmEmailOtpApi = async (
|
export const confirmEmailOtpApi = async (payload: ConfirmEmailOtpPayload) => {
|
||||||
payload: ConfirmEmailOtpPayload,
|
return apiClient.post('/User/ConfirmEmailOtp', payload);
|
||||||
): Promise<GenericApiResponse> => {
|
|
||||||
const { data } = await apiClient.post('/User/ConfirmEmailOtp', payload);
|
|
||||||
return data;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const completeUserInformationApi = async (
|
export const completeUserInformationApi = async (
|
||||||
payload: CompleteUserInfoPayload,
|
payload: CompleteUserInfoPayload,
|
||||||
): Promise<CompleteUserInfoResponse> => {
|
) => {
|
||||||
const { data } = await apiClient.post(
|
return apiClient.post('/User/CompleteUserInformation', payload);
|
||||||
'/User/CompleteUserInformation',
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
return data;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,11 +55,8 @@ export const EnterPasswordForm = ({
|
|||||||
useApi(sendSmsOtp);
|
useApi(sendSmsOtp);
|
||||||
const { loading: emailResendLoading, execute: emailResendCall } =
|
const { loading: emailResendLoading, execute: emailResendCall } =
|
||||||
useApi(sendEmailOtp);
|
useApi(sendEmailOtp);
|
||||||
const {
|
const { loading: loginWithPassLoading, execute: loginWithPassCall } =
|
||||||
data: loginWithPassResult,
|
useApi(loginWithPassword);
|
||||||
loading: loginWithPassLoading,
|
|
||||||
execute: loginWithPassCall,
|
|
||||||
} = useApi(loginWithPassword);
|
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
|
|||||||
@@ -16,12 +16,6 @@ import {
|
|||||||
confirmEmailOtpApi,
|
confirmEmailOtpApi,
|
||||||
completeUserInformationApi,
|
completeUserInformationApi,
|
||||||
} from '../api/userCompletion';
|
} 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 { containsSymbol } from '@/utils/regexes/containsSymbol';
|
||||||
import { least8Chars } from '@/utils/regexes/least8Chars';
|
import { least8Chars } from '@/utils/regexes/least8Chars';
|
||||||
import { containsNumber } from '@/utils/regexes/containsNumber';
|
import { containsNumber } from '@/utils/regexes/containsNumber';
|
||||||
@@ -63,46 +57,20 @@ export function UserCompletionPage() {
|
|||||||
const matchPassword = password === confirmPassword;
|
const matchPassword = password === confirmPassword;
|
||||||
const correctEmail = isEmail(email);
|
const correctEmail = isEmail(email);
|
||||||
|
|
||||||
const { execute: sendCode, data: sendCodeData } = useApi(
|
const { execute: sendCode, data: sendCodeData } = useApi(sendEmailOtpApi);
|
||||||
async (payload: SendEmailOtpPayload) => {
|
|
||||||
const result = await sendEmailOtpApi(payload);
|
|
||||||
const conformingResult: ApiResponse = {
|
|
||||||
...result,
|
|
||||||
errorCode: result.errorCode || 0,
|
|
||||||
validations: [],
|
|
||||||
};
|
|
||||||
return { data: conformingResult };
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
execute: verifyCode,
|
execute: verifyCode,
|
||||||
loading: isVerifyingCode,
|
|
||||||
data: verifyCodeData,
|
data: verifyCodeData,
|
||||||
} = useApi(async (payload: ConfirmEmailOtpPayload) => {
|
loading: isVerifyingCode,
|
||||||
const result = await confirmEmailOtpApi(payload);
|
} = useApi(confirmEmailOtpApi);
|
||||||
const conformingResult: ApiResponse = {
|
|
||||||
...result,
|
|
||||||
errorCode: result.errorCode || 0,
|
|
||||||
validations: [],
|
|
||||||
};
|
|
||||||
return { data: conformingResult };
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
execute: submitForm,
|
execute: submitForm,
|
||||||
|
data: submitData,
|
||||||
loading: isSubmitting,
|
loading: isSubmitting,
|
||||||
error: submitError,
|
error: submitError,
|
||||||
data: submitData,
|
} = useApi(completeUserInformationApi);
|
||||||
} = useApi(async (payload: CompleteUserInfoPayload) => {
|
|
||||||
const result = await completeUserInformationApi(payload);
|
|
||||||
const conformingResult: ApiResponse = {
|
|
||||||
...result,
|
|
||||||
errorCode: result.errorCode || 0,
|
|
||||||
validations: [],
|
|
||||||
};
|
|
||||||
return { data: conformingResult };
|
|
||||||
});
|
|
||||||
|
|
||||||
const getErrorMessage = (error: unknown): string | null => {
|
const getErrorMessage = (error: unknown): string | null => {
|
||||||
if (!error) return null;
|
if (!error) return null;
|
||||||
@@ -112,9 +80,10 @@ export function UserCompletionPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sendCodeData) {
|
if (sendCodeData) {
|
||||||
if (sendCodeData.success) {
|
if (sendCodeData.data.success) {
|
||||||
showToast({
|
showToast({
|
||||||
message: sendCodeData.message || t('completion.successfullCodeSent'),
|
message:
|
||||||
|
sendCodeData.data.message || t('completion.successfullCodeSent'),
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
});
|
});
|
||||||
setCodeSent(true);
|
setCodeSent(true);
|
||||||
@@ -122,7 +91,7 @@ export function UserCompletionPage() {
|
|||||||
setCountdown(120);
|
setCountdown(120);
|
||||||
} else {
|
} else {
|
||||||
showToast({
|
showToast({
|
||||||
message: sendCodeData.message || t('completion.problem'),
|
message: sendCodeData.data.message || t('completion.problem'),
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -131,15 +100,15 @@ export function UserCompletionPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (verifyCodeData) {
|
if (verifyCodeData) {
|
||||||
if (verifyCodeData.success) {
|
if (verifyCodeData.data.success) {
|
||||||
setEmailVerified(true);
|
setEmailVerified(true);
|
||||||
showToast({
|
showToast({
|
||||||
message: verifyCodeData.message || t('completion.codeVerified'),
|
message: verifyCodeData.data.message || t('completion.codeVerified'),
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
showToast({
|
showToast({
|
||||||
message: verifyCodeData.message || t('completion.invalidCode'),
|
message: verifyCodeData.data.message || t('completion.invalidCode'),
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
});
|
});
|
||||||
setEmailVerified(false);
|
setEmailVerified(false);
|
||||||
@@ -151,13 +120,13 @@ export function UserCompletionPage() {
|
|||||||
if (submitData) {
|
if (submitData) {
|
||||||
showToast({
|
showToast({
|
||||||
message:
|
message:
|
||||||
submitData.message ||
|
submitData.data.message ||
|
||||||
t(
|
t(
|
||||||
submitData.success
|
submitData.data.success
|
||||||
? 'completion.submitSuccess'
|
? 'completion.submitSuccess'
|
||||||
: 'completion.submitError',
|
: 'completion.submitError',
|
||||||
),
|
),
|
||||||
severity: submitData.success ? 'success' : 'error',
|
severity: submitData.data.success ? 'success' : 'error',
|
||||||
});
|
});
|
||||||
} else if (submitError) {
|
} else if (submitError) {
|
||||||
showToast({
|
showToast({
|
||||||
@@ -205,7 +174,6 @@ export function UserCompletionPage() {
|
|||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
submitForm({
|
submitForm({
|
||||||
userId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
|
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
gender: sex,
|
gender: sex,
|
||||||
@@ -325,7 +293,7 @@ export function UserCompletionPage() {
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
error={getErrorMessage(submitError)}
|
error={getErrorMessage(submitError)}
|
||||||
success={!!submitData?.success}
|
success={!!submitData?.data.success}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export interface ConfirmEmailOtpPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CompleteUserInfoPayload {
|
export interface CompleteUserInfoPayload {
|
||||||
userId: string;
|
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
gender: 0 | 1 | 2;
|
gender: 0 | 1 | 2;
|
||||||
|
|||||||
@@ -20,11 +20,29 @@ export async function fetchProfile() {
|
|||||||
|
|
||||||
export async function saveProfile(payload: {
|
export async function saveProfile(payload: {
|
||||||
data: InfoRowData;
|
data: InfoRowData;
|
||||||
imageUrl: string | null;
|
imageUrl: File | null;
|
||||||
}) {
|
}) {
|
||||||
|
const { data, imageUrl } = payload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('FirstName', data.firstName || '');
|
||||||
|
formData.append('LastName', data.lastName || '');
|
||||||
|
formData.append('NationalCode', data.nationalCode || '');
|
||||||
|
formData.append('Gender', String(data.gender));
|
||||||
|
formData.append('CountryCode', data.country || '');
|
||||||
|
|
||||||
|
if (imageUrl) {
|
||||||
|
formData.append('Image', imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
return apiClient.post<SaveProfileApiResponse>(
|
return apiClient.post<SaveProfileApiResponse>(
|
||||||
'/Profile/SaveProfilePersonalInforamtion',
|
'/Profile/SaveProfilePersonalInforamtion',
|
||||||
payload,
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +88,7 @@ export async function deleteSessions(payload: { keys: string[] }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addPassword(payload: { password: string }) {
|
export async function addPassword(payload: { newPassword: string }) {
|
||||||
return apiClient.post<PasswordApiResponse>('/Profile/AddPassword', payload);
|
return apiClient.post<PasswordApiResponse>('/Profile/AddPassword', payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { CloseCircle } from 'iconsax-react';
|
import { CloseCircle } from 'iconsax-react';
|
||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
import { type PasswordDialogProps } from '../../types/settingsType';
|
import { type PasswordDialogProps } from '../../types/settingsType';
|
||||||
|
import { PasswordValidationItem } from './PasswordValidation';
|
||||||
|
|
||||||
export function PasswordDialog({
|
export function PasswordDialog({
|
||||||
open,
|
open,
|
||||||
@@ -33,6 +34,10 @@ export function PasswordDialog({
|
|||||||
changePassword,
|
changePassword,
|
||||||
apiError,
|
apiError,
|
||||||
t,
|
t,
|
||||||
|
hasMinLength,
|
||||||
|
hasNumber,
|
||||||
|
hasUpperAndLower,
|
||||||
|
hasSpecialChar,
|
||||||
}: PasswordDialogProps) {
|
}: PasswordDialogProps) {
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -65,7 +70,6 @@ export function PasswordDialog({
|
|||||||
px: { xs: 2, sm: 3 },
|
px: { xs: 2, sm: 3 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* ✅ 4. API ERROR DISPLAY ADDED */}
|
|
||||||
{apiError && (
|
{apiError && (
|
||||||
<Typography color="error" variant="body2" textAlign="center">
|
<Typography color="error" variant="body2" textAlign="center">
|
||||||
{apiError}
|
{apiError}
|
||||||
@@ -94,18 +98,28 @@ export function PasswordDialog({
|
|||||||
fullWidth
|
fullWidth
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showValidation && (
|
{showValidation && (
|
||||||
<Typography
|
<Box sx={{ mt: 1 }}>
|
||||||
variant="body2"
|
<PasswordValidationItem
|
||||||
color={validPassword ? 'success.main' : 'error.main'}
|
isValid={hasNumber}
|
||||||
>
|
label={t('securityForm.hasNumber')}
|
||||||
{validPassword
|
/>
|
||||||
? t('securityForm.passwordIsValid')
|
<PasswordValidationItem
|
||||||
: t('securityForm.passwordIsInvalid')}
|
isValid={hasMinLength}
|
||||||
</Typography>
|
label={t('securityForm.hasMinLength')}
|
||||||
|
/>
|
||||||
|
<PasswordValidationItem
|
||||||
|
isValid={hasUpperAndLower}
|
||||||
|
label={t('securityForm.hasUpperAndLower')}
|
||||||
|
/>
|
||||||
|
<PasswordValidationItem
|
||||||
|
isValid={hasSpecialChar}
|
||||||
|
label={t('securityForm.hasSpecialChar')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ 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';
|
||||||
|
|
||||||
export function PasswordSecurity() {
|
export function PasswordSecurity() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -42,6 +43,7 @@ export function PasswordSecurity() {
|
|||||||
hasNumber && hasMinLength && hasUpperAndLower && hasSpecialChar;
|
hasNumber && hasMinLength && hasUpperAndLower && hasSpecialChar;
|
||||||
|
|
||||||
const matchPassword = password === confirmPassword;
|
const matchPassword = password === confirmPassword;
|
||||||
|
const showToast = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: profileData,
|
data: profileData,
|
||||||
@@ -79,6 +81,12 @@ export function PasswordSecurity() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (addData?.success || changeData?.success) {
|
if (addData?.success || changeData?.success) {
|
||||||
|
showToast({
|
||||||
|
message: changePasswordUI
|
||||||
|
? t('securityForm.passwordChanged')
|
||||||
|
: t('securityForm.passwordAdded'),
|
||||||
|
severity: 'success',
|
||||||
|
});
|
||||||
if (!changePasswordUI) {
|
if (!changePasswordUI) {
|
||||||
setChangePasswordUI(true);
|
setChangePasswordUI(true);
|
||||||
}
|
}
|
||||||
@@ -87,7 +95,17 @@ export function PasswordSecurity() {
|
|||||||
setConfirmPassword('');
|
setConfirmPassword('');
|
||||||
setCurrentPassword('');
|
setCurrentPassword('');
|
||||||
}
|
}
|
||||||
}, [addData, changeData, changePasswordUI]);
|
}, [addData, changeData, changePasswordUI, showToast, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (addError || changeError) {
|
||||||
|
showToast({
|
||||||
|
message:
|
||||||
|
getErrorMessage(addError || changeError) || t('securityForm.general'),
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [addError, changeError, t, showToast]);
|
||||||
|
|
||||||
const handleOpen = () => setOpen(true);
|
const handleOpen = () => setOpen(true);
|
||||||
const handleClose = () => setOpen(false);
|
const handleClose = () => setOpen(false);
|
||||||
@@ -99,7 +117,7 @@ export function PasswordSecurity() {
|
|||||||
newPassword: password,
|
newPassword: password,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await executeAddPassword({ password });
|
await executeAddPassword({ newPassword: password });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,7 +138,12 @@ export function PasswordSecurity() {
|
|||||||
title={t('securityForm.password')}
|
title={t('securityForm.password')}
|
||||||
subtitle={t('securityForm.determinePassword')}
|
subtitle={t('securityForm.determinePassword')}
|
||||||
action={
|
action={
|
||||||
<Button variant="contained" onClick={handleOpen}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleOpen}
|
||||||
|
size="medium"
|
||||||
|
sx={{ flexShrink: 0, flexGrow: 0, width: 'auto' }}
|
||||||
|
>
|
||||||
{changePasswordUI
|
{changePasswordUI
|
||||||
? t('securityForm.changePassword')
|
? t('securityForm.changePassword')
|
||||||
: t('securityForm.addPassword')}
|
: t('securityForm.addPassword')}
|
||||||
@@ -184,6 +207,10 @@ export function PasswordSecurity() {
|
|||||||
changePassword={changePasswordUI}
|
changePassword={changePasswordUI}
|
||||||
apiError={getErrorMessage(apiError)}
|
apiError={getErrorMessage(apiError)}
|
||||||
t={t}
|
t={t}
|
||||||
|
hasMinLength={hasMinLength}
|
||||||
|
hasNumber={hasNumber}
|
||||||
|
hasUpperAndLower={hasUpperAndLower}
|
||||||
|
hasSpecialChar={hasSpecialChar}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import { fetchProfile } from '../../api/settingsApi';
|
|||||||
import type { LoginLog } from '../../types/settingsApiType';
|
import type { LoginLog } from '../../types/settingsApiType';
|
||||||
import { useApi } from '@/hooks/useApi';
|
import { useApi } from '@/hooks/useApi';
|
||||||
import { formatDate } from '@/utils/formatSessionDate';
|
import { formatDate } from '@/utils/formatSessionDate';
|
||||||
|
import { useToast } from '@rkheftan/harmony-ui';
|
||||||
|
|
||||||
export function RecentLogins() {
|
export function RecentLogins() {
|
||||||
const { t, i18n } = useTranslation('setting');
|
const { t, i18n } = useTranslation('setting');
|
||||||
const [logs, setLogs] = useState<LoginLog[]>([]);
|
const [logs, setLogs] = useState<LoginLog[]>([]);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
||||||
|
const showToast = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: profileData,
|
data: profileData,
|
||||||
@@ -32,6 +34,14 @@ export function RecentLogins() {
|
|||||||
}
|
}
|
||||||
}, [profileData]);
|
}, [profileData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fetchError) {
|
||||||
|
showToast({
|
||||||
|
message: getErrorMessage(fetchError) || t('active.errorFetch'),
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [fetchError, t, showToast]);
|
||||||
const getErrorMessage = (error: unknown): string | null => {
|
const getErrorMessage = (error: unknown): string | null => {
|
||||||
if (!error) return null;
|
if (!error) return null;
|
||||||
if (error instanceof Error) return error.message;
|
if (error instanceof Error) return error.message;
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ 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 { fetchProfile, saveProfile } from '../../api/settingsApi';
|
||||||
|
import { useToast } from '@rkheftan/harmony-ui';
|
||||||
|
|
||||||
export function PersonalInformation() {
|
export function PersonalInformation() {
|
||||||
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);
|
||||||
|
const [uploadedImageFile, setUploadedImageFile] = useState<File | null>(null);
|
||||||
const [data, setData] = useState<InfoRowData>({
|
const [data, setData] = useState<InfoRowData>({
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
@@ -22,6 +24,7 @@ export function PersonalInformation() {
|
|||||||
country: '',
|
country: '',
|
||||||
});
|
});
|
||||||
const [originalData, setOriginalData] = useState<InfoRowData | null>(null);
|
const [originalData, setOriginalData] = useState<InfoRowData | null>(null);
|
||||||
|
const showToast = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: profileData,
|
data: profileData,
|
||||||
@@ -47,18 +50,33 @@ export function PersonalInformation() {
|
|||||||
: Gender.None,
|
: Gender.None,
|
||||||
country: profileData.countryCode ?? '',
|
country: profileData.countryCode ?? '',
|
||||||
};
|
};
|
||||||
|
|
||||||
setData(fetchedData);
|
setData(fetchedData);
|
||||||
setOriginalData(fetchedData);
|
setOriginalData(fetchedData);
|
||||||
setUploadedImageUrl(profileData.profileImageUrl || null);
|
|
||||||
|
const imageBaseUrl = process.env.IMAGE_BASE_URL;
|
||||||
|
|
||||||
|
if (profileData.profileImageUrl) {
|
||||||
|
setUploadedImageUrl(`${imageBaseUrl}${profileData.profileImageUrl}`);
|
||||||
|
} else {
|
||||||
|
setUploadedImageUrl(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadedImageFile(null);
|
||||||
}
|
}
|
||||||
}, [profileData]);
|
}, [profileData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (saveData?.success) {
|
if (saveData?.success) {
|
||||||
|
showToast({
|
||||||
|
message: t('settingForm.successSaveProfile'),
|
||||||
|
severity: 'success',
|
||||||
|
});
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
setOriginalData(data);
|
setOriginalData(data);
|
||||||
|
setUploadedImageFile(null);
|
||||||
}
|
}
|
||||||
}, [saveData, data]);
|
}, [saveData, data, showToast, t]);
|
||||||
|
|
||||||
const initials = `${data?.firstName?.trim()[0] || ''}${data?.lastName?.trim()[0] || ''}`;
|
const initials = `${data?.firstName?.trim()[0] || ''}${data?.lastName?.trim()[0] || ''}`;
|
||||||
|
|
||||||
@@ -72,11 +90,12 @@ export function PersonalInformation() {
|
|||||||
if (originalData) {
|
if (originalData) {
|
||||||
setData(originalData);
|
setData(originalData);
|
||||||
}
|
}
|
||||||
|
setUploadedImageFile(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveClick = async () => {
|
const handleSaveClick = async () => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
executeSaveProfile({ data, imageUrl: uploadedImageUrl });
|
executeSaveProfile({ data, imageUrl: uploadedImageFile });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getErrorMessage = (error: unknown): string | null => {
|
const getErrorMessage = (error: unknown): string | null => {
|
||||||
@@ -85,6 +104,26 @@ export function PersonalInformation() {
|
|||||||
return String(error);
|
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 (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
<CardContainer
|
<CardContainer
|
||||||
@@ -95,62 +134,51 @@ export function PersonalInformation() {
|
|||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
gap: 1,
|
||||||
alignItems: 'flex-end',
|
|
||||||
width: '100%',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
{isEditing ? (
|
||||||
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>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Button
|
<Button
|
||||||
onClick={handleEditClick}
|
variant="text"
|
||||||
|
onClick={handleCancelClick}
|
||||||
size="large"
|
size="large"
|
||||||
variant="outlined"
|
sx={{
|
||||||
sx={{ borderRadius: 1 }}
|
color: 'primary.main',
|
||||||
disabled={isLoadingProfile}
|
textTransform: 'none',
|
||||||
|
width: { xs: '100%', sm: 'auto' },
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t('settingForm.editButton')}
|
{t('settingForm.rejectButton')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
<Button
|
||||||
</Box>
|
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) && (
|
{getErrorMessage(saveProfileError) && (
|
||||||
<Typography
|
<Typography
|
||||||
color="error"
|
color="error"
|
||||||
@@ -198,12 +226,16 @@ export function PersonalInformation() {
|
|||||||
initials={initials}
|
initials={initials}
|
||||||
uploadedImageUrl={uploadedImageUrl}
|
uploadedImageUrl={uploadedImageUrl}
|
||||||
onImageChange={(file) => {
|
onImageChange={(file) => {
|
||||||
|
setUploadedImageFile(file);
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () =>
|
reader.onload = () =>
|
||||||
setUploadedImageUrl(reader.result as string);
|
setUploadedImageUrl(reader.result as string);
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}}
|
}}
|
||||||
onRemoveImage={() => setUploadedImageUrl(null)}
|
onRemoveImage={() => {
|
||||||
|
setUploadedImageFile(null);
|
||||||
|
setUploadedImageUrl(null);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{data &&
|
{data &&
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ import {
|
|||||||
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';
|
||||||
|
|
||||||
export function PhoneNumber() {
|
export function PhoneNumber() {
|
||||||
const { t, i18n } = useTranslation('setting');
|
const { t, i18n } = useTranslation('setting');
|
||||||
|
const toast = useToast();
|
||||||
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('');
|
||||||
@@ -74,12 +76,36 @@ export function PhoneNumber() {
|
|||||||
}
|
}
|
||||||
}, [profileData]);
|
}, [profileData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fetchError) {
|
||||||
|
toast({
|
||||||
|
message:
|
||||||
|
getErrorMessage(fetchError) || t('settingForm.errorFetchPhoneNumber'),
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [fetchError, toast, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sendCodeData?.success) {
|
if (sendCodeData?.success) {
|
||||||
setButtonState('counting');
|
setButtonState('counting');
|
||||||
setIsVerified(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (confirmData?.success && confirmData.confirm) {
|
if (confirmData?.success && confirmData.confirm) {
|
||||||
@@ -87,8 +113,22 @@ export function PhoneNumber() {
|
|||||||
setShowToast(true);
|
setShowToast(true);
|
||||||
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
|
const fullPhoneNumber = countryCode + phoneNumber.replace(/^0/, '');
|
||||||
executeChangePhone({ phoneNumber: fullPhoneNumber });
|
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(() => {
|
useEffect(() => {
|
||||||
if (changePhoneData?.success) {
|
if (changePhoneData?.success) {
|
||||||
@@ -101,8 +141,23 @@ export function PhoneNumber() {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
setIsEditing(false);
|
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(
|
const apiError = useMemo(
|
||||||
() => sendCodeError || confirmError || changePhoneError,
|
() => sendCodeError || confirmError || changePhoneError,
|
||||||
@@ -151,7 +206,6 @@ export function PhoneNumber() {
|
|||||||
const handleSendCode = () => {
|
const handleSendCode = () => {
|
||||||
handleBlur();
|
handleBlur();
|
||||||
if (formError || !isPhoneValid(countryCode, phoneNumber)) return;
|
if (formError || !isPhoneValid(countryCode, phoneNumber)) return;
|
||||||
|
|
||||||
executeSendCode({
|
executeSendCode({
|
||||||
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
|
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
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';
|
||||||
|
|
||||||
export function SocialMedia() {
|
export function SocialMedia() {
|
||||||
const { t } = useTranslation('setting');
|
const { t } = useTranslation('setting');
|
||||||
@@ -28,6 +29,7 @@ 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 {
|
const {
|
||||||
data: profileData,
|
data: profileData,
|
||||||
@@ -82,15 +84,23 @@ export function SocialMedia() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sendCodeData?.success) {
|
if (sendCodeData?.success) {
|
||||||
|
showToast({
|
||||||
|
message: t('settingForm.verificationCodeSent'),
|
||||||
|
severity: 'success',
|
||||||
|
});
|
||||||
setDialogStep('enterCode');
|
setDialogStep('enterCode');
|
||||||
}
|
}
|
||||||
}, [sendCodeData]);
|
}, [sendCodeData, t, showToast]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (confirmData?.success && confirmData.confirm) {
|
if (confirmData?.success && confirmData.confirm) {
|
||||||
|
showToast({
|
||||||
|
message: t('settingForm.verificationSuccessful'),
|
||||||
|
severity: 'success',
|
||||||
|
});
|
||||||
executeChangeEmail({ email: emailInput });
|
executeChangeEmail({ email: emailInput });
|
||||||
}
|
}
|
||||||
}, [confirmData, emailInput, executeChangeEmail]);
|
}, [confirmData, emailInput, executeChangeEmail, t, showToast]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (changeEmailData?.success) {
|
if (changeEmailData?.success) {
|
||||||
@@ -102,9 +112,34 @@ export function SocialMedia() {
|
|||||||
time: t('settingForm.justNow'),
|
time: t('settingForm.justNow'),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
showToast({
|
||||||
|
message: t('settingForm.emailChangedSuccessfully'),
|
||||||
|
severity: 'success',
|
||||||
|
});
|
||||||
resetDialog();
|
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 = () => {
|
const resetDialog = () => {
|
||||||
setOpenDialog(false);
|
setOpenDialog(false);
|
||||||
@@ -115,7 +150,7 @@ export function SocialMedia() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSendCode = () => {
|
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'));
|
setFormError(t('settingForm.emailIsInvalid'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ export function ProfileImage({
|
|||||||
sx={{
|
sx={{
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
|
flexShrink: 0,
|
||||||
|
flexGrow: 0,
|
||||||
|
width: 'auto',
|
||||||
}}
|
}}
|
||||||
startIcon={
|
startIcon={
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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';
|
||||||
|
|
||||||
export default function PhoneEditForm({
|
export default function PhoneEditForm({
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
@@ -39,6 +40,8 @@ export default function PhoneEditForm({
|
|||||||
return digitsOnly.length >= 8 && digitsOnly.length <= 15;
|
return digitsOnly.length >= 8 && digitsOnly.length <= 15;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [isSending, setIsSending] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ width: '100%' }}>
|
<Box sx={{ width: '100%' }}>
|
||||||
@@ -73,7 +76,7 @@ export default function PhoneEditForm({
|
|||||||
error={inputError}
|
error={inputError}
|
||||||
helperText={inputError ? error : ''}
|
helperText={inputError ? error : ''}
|
||||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||||
sx={{ flex: '1 1 220px', minWidth: 0 }}
|
sx={{ flex: '1 1 220px' }}
|
||||||
placeholder="09123456789"
|
placeholder="09123456789"
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment:
|
endAdornment:
|
||||||
@@ -122,12 +125,18 @@ export default function PhoneEditForm({
|
|||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant="text"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (isValidPhoneNumber(phoneNumber)) {
|
if (isValidPhoneNumber(phoneNumber)) {
|
||||||
handleSendCode();
|
setIsSending(true);
|
||||||
|
try {
|
||||||
|
await handleSendCode();
|
||||||
|
} finally {
|
||||||
|
setIsSending(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={
|
||||||
|
isSending ||
|
||||||
buttonState === 'counting' ||
|
buttonState === 'counting' ||
|
||||||
phoneNumber.length === 0 ||
|
phoneNumber.length === 0 ||
|
||||||
!isValidPhoneNumber(phoneNumber)
|
!isValidPhoneNumber(phoneNumber)
|
||||||
@@ -138,7 +147,9 @@ export default function PhoneEditForm({
|
|||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{buttonState === 'counting' ? (
|
{isSending ? (
|
||||||
|
<CircularProgress size={20} />
|
||||||
|
) : buttonState === 'counting' ? (
|
||||||
<CountDownTimer
|
<CountDownTimer
|
||||||
initialSeconds={60}
|
initialSeconds={60}
|
||||||
onComplete={() => setButtonState('default')}
|
onComplete={() => setButtonState('default')}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ 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';
|
||||||
|
|
||||||
export function ActiveDevicesPage() {
|
export function ActiveDevicesPage() {
|
||||||
const { t, i18n } = useTranslation('setting');
|
const { t, i18n } = useTranslation('setting');
|
||||||
@@ -24,6 +25,7 @@ export function ActiveDevicesPage() {
|
|||||||
const [loadingDeleteIds, setLoadingDeleteIds] = useState<string[]>([]);
|
const [loadingDeleteIds, setLoadingDeleteIds] = useState<string[]>([]);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
||||||
|
const showToast = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: profileData,
|
data: profileData,
|
||||||
@@ -65,10 +67,17 @@ export function ActiveDevicesPage() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
setDevices((prevDevices) => prevDevices.filter((d) => d.id !== id));
|
setDevices((prevDevices) => prevDevices.filter((d) => d.id !== id));
|
||||||
} else {
|
} else {
|
||||||
console.error('Delete failed:', data.message);
|
showToast({
|
||||||
|
message: data.message || t('active.deleteFailed'),
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error('Delete error:', error);
|
// console.error('Delete error:', error);
|
||||||
|
showToast({
|
||||||
|
message: t('active.deleteFailed'),
|
||||||
|
severity: 'error',
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingDeleteIds((prev) =>
|
setLoadingDeleteIds((prev) =>
|
||||||
prev.filter((loadingId) => loadingId !== id),
|
prev.filter((loadingId) => loadingId !== id),
|
||||||
@@ -104,6 +113,9 @@ export function ActiveDevicesPage() {
|
|||||||
borderColor: 'error.main',
|
borderColor: 'error.main',
|
||||||
color: 'error.main',
|
color: 'error.main',
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
|
flexShrink: 0,
|
||||||
|
flexGrow: 0,
|
||||||
|
width: 'auto',
|
||||||
}}
|
}}
|
||||||
disabled={isLoading || isTerminating}
|
disabled={isLoading || isTerminating}
|
||||||
>
|
>
|
||||||
@@ -215,6 +227,7 @@ export function ActiveDevicesPage() {
|
|||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
color: 'success.main',
|
color: 'success.main',
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
|
maxWidth: '125px',
|
||||||
'&.Mui-disabled': {
|
'&.Mui-disabled': {
|
||||||
color: 'success.main',
|
color: 'success.main',
|
||||||
borderColor: 'success.main',
|
borderColor: 'success.main',
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export function SettingPage() {
|
|||||||
data: profileData,
|
data: profileData,
|
||||||
loading: isFetching,
|
loading: isFetching,
|
||||||
error: fetchError,
|
error: fetchError,
|
||||||
} = useApi(fetchProfile, { immediate: true });
|
} = useApi(fetchProfile);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: saveData,
|
data: saveData,
|
||||||
@@ -142,7 +142,7 @@ export function SettingPage() {
|
|||||||
subtitle={t('settings.description')}
|
subtitle={t('settings.description')}
|
||||||
highlighted={isEditing}
|
highlighted={isEditing}
|
||||||
action={
|
action={
|
||||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant="text"
|
||||||
@@ -260,7 +260,7 @@ export function SettingPage() {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
renderInput={(p) => <TextField {...p} />}
|
renderInput={(p) => <TextField {...p} />}
|
||||||
size="small"
|
size="medium"
|
||||||
fullWidth
|
fullWidth
|
||||||
disableClearable
|
disableClearable
|
||||||
/>
|
/>
|
||||||
@@ -288,7 +288,7 @@ export function SettingPage() {
|
|||||||
v && setDraftSettings((prev) => ({ ...prev, calendar: v }))
|
v && setDraftSettings((prev) => ({ ...prev, calendar: v }))
|
||||||
}
|
}
|
||||||
renderInput={(params) => <TextField {...params} />}
|
renderInput={(params) => <TextField {...params} />}
|
||||||
size="small"
|
size="medium"
|
||||||
fullWidth
|
fullWidth
|
||||||
disableClearable
|
disableClearable
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ export interface PasswordDialogProps {
|
|||||||
changePassword: boolean;
|
changePassword: boolean;
|
||||||
apiError: string | null;
|
apiError: string | null;
|
||||||
t: (key: string) => string;
|
t: (key: string) => string;
|
||||||
|
hasMinLength: boolean;
|
||||||
|
hasNumber: boolean;
|
||||||
|
hasUpperAndLower: boolean;
|
||||||
|
hasSpecialChar: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValidationItemProps {
|
export interface ValidationItemProps {
|
||||||
|
|||||||
Reference in New Issue
Block a user