feat: add navigation to forger password page, show password icon, toasts for different situations and changes of some styles
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
"familyName": "Family Name",
|
||||
"country": "Country",
|
||||
"gender": "Gender",
|
||||
"nationalCode": "National code",
|
||||
"nationalCode": "National code(Op",
|
||||
"man": "Male",
|
||||
"woman": "Female",
|
||||
"genderPlaceholder": "Male",
|
||||
@@ -62,7 +62,14 @@
|
||||
"emailIsInvalid": "Email is invalid",
|
||||
"changeEmailFailed": "Change of email failed",
|
||||
"anErrorOccurred": "An error occurred.",
|
||||
"saving": "Saving..."
|
||||
"saving": "Saving...",
|
||||
"successSaveProfile": "Profile saved successfully",
|
||||
"errorSave": "Saving failed",
|
||||
"codeSentSuccessfully": "Code sent successfully",
|
||||
"errorSendCode": "Failed to send code",
|
||||
"phoneVerified": "Phone number verified",
|
||||
"errorConfirmCode": "Failed to confirm code",
|
||||
"errorChangePhone": "Failed to change phone number"
|
||||
},
|
||||
|
||||
"active": {
|
||||
@@ -76,7 +83,9 @@
|
||||
"notLoggedIn": "You are not logged in",
|
||||
"failFetchActiveSessions": "Failed to fetch active sessions.",
|
||||
"errorFetch": "An error occurred while fetching your active sessions.",
|
||||
"deleting": "Deleting..."
|
||||
"deleting": "Deleting...",
|
||||
"successDelete": "Deleted successfully",
|
||||
"deleteFailed": "Deletion failed"
|
||||
},
|
||||
|
||||
"settings": {
|
||||
@@ -123,6 +132,9 @@
|
||||
"currentDevice": "Current device",
|
||||
"changePassword": "Change password",
|
||||
"currentPassword": "Current password",
|
||||
"forgetPassword": "Forgot your password?"
|
||||
"forgetPassword": "Forgot your password?",
|
||||
"passwordChanged": "Password changed",
|
||||
"passwordAdded": "Password added",
|
||||
"error": "Password change failed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"country": "کشور",
|
||||
"gender": "جنسیت",
|
||||
"nationalCode": "کد ملی",
|
||||
"optionalNationalCode": "کد ملی(اختیاری)",
|
||||
"man": "مرد",
|
||||
"woman": "زن",
|
||||
"genderPlaceholder": "مرد",
|
||||
@@ -62,7 +63,14 @@
|
||||
"emailIsInvalid": "ایمیل نامعتبر است",
|
||||
"changeEmailFailed": "تغییر ایمیل با خطا مواجه شد",
|
||||
"anErrorOccurred": "خطایی رخ داد",
|
||||
"saving": "در حال ذخیرهسازی..."
|
||||
"saving": "در حال ذخیرهسازی...",
|
||||
"successSaveProfile": "پروفایل با موفقیت ذخیره شد",
|
||||
"errorSave": "ذخیره با مشکل مواجه شد",
|
||||
"codeSentSuccessfully": "کد با موفقیت ارسال شد",
|
||||
"errorSendCode": "ارسال کد با خطا مواجه شد",
|
||||
"phoneVerified": "تلفن همراه تایید شد",
|
||||
"errorConfirmCode": "تایید کد با خطا مواجه شد",
|
||||
"errorChangePhone": "تغییر تلفن همراه با خطا مواجه شد"
|
||||
},
|
||||
|
||||
"active": {
|
||||
@@ -76,7 +84,9 @@
|
||||
"notLoggedIn": "شما وارد سیستم نشدهاید",
|
||||
"failFetchActiveSessions": "دریافت نشست های فعال ناموفق بود.",
|
||||
"errorFetch": "هنگام دریافت جلسات فعال شما خطایی روی داد.",
|
||||
"deleting": "در حال حذف..."
|
||||
"deleting": "در حال حذف...",
|
||||
"successDelete": "با موفقیت حذف شد",
|
||||
"deleteFailed": "حذف با مشکل مواجه شد"
|
||||
},
|
||||
|
||||
"settings": {
|
||||
@@ -123,6 +133,9 @@
|
||||
"currentDevice": "دستگاه فعلی",
|
||||
"changePassword": "تغییر رمز عبور",
|
||||
"currentPassword": "رمز عبور فعلی",
|
||||
"forgetPassword": "رمز عبور را فراموش کرده اید؟"
|
||||
"forgetPassword": "رمز عبور را فراموش کرده اید؟",
|
||||
"passwordChanged": "رمز عبور تعویض شد",
|
||||
"passwordAdded": "رمز عبور اضافه شد",
|
||||
"error": "تعویض رمز عبور با مشکل مواجه شد"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export function CardContainer({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
borderRadius: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, IconButton, Skeleton, Typography } from '@mui/material';
|
||||
import { Box, IconButton, Typography } from '@mui/material';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { More } from 'iconsax-react';
|
||||
import type { UserInfo } from '@/contexts/AuthContext';
|
||||
@@ -7,7 +7,7 @@ interface HeaderProps {
|
||||
user: UserInfo;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({ user, loading }) => {
|
||||
export const Header: React.FC<HeaderProps> = ({ user }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
IconButton,
|
||||
Toolbar as MuiToolbar,
|
||||
Skeleton,
|
||||
} from '@mui/material';
|
||||
import { Avatar, Box, IconButton, Toolbar as MuiToolbar } from '@mui/material';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { HambergerMenu, Menu } from 'iconsax-react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import type { UserInfo } from '@/contexts/AuthContext';
|
||||
import Logo from '../Logo';
|
||||
|
||||
interface ToolbarProps {
|
||||
sideNavOpen: boolean;
|
||||
@@ -22,7 +17,6 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
||||
setSideNavOpen,
|
||||
isMobile,
|
||||
user,
|
||||
loading,
|
||||
}) => {
|
||||
return (
|
||||
<MuiToolbar
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { CircularProgress, Stack } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
export const Loading = () => {
|
||||
return (
|
||||
|
||||
@@ -34,7 +34,9 @@ export const ThemeToggleButton = ({
|
||||
<ToggleButtonGroup
|
||||
value={value}
|
||||
exclusive
|
||||
color="primary"
|
||||
onChange={handleChange}
|
||||
size="medium"
|
||||
sx={{
|
||||
borderRadius: 1.5,
|
||||
border: '1px solid',
|
||||
@@ -51,10 +53,6 @@ export const ThemeToggleButton = ({
|
||||
gap: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
'&.Mui-selected': {
|
||||
bgcolor: 'primary.light',
|
||||
color: 'primary.main',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Icon Component={Sun1} color="primary.main" variant="Bold" />
|
||||
@@ -70,10 +68,6 @@ export const ThemeToggleButton = ({
|
||||
gap: 1,
|
||||
px: 2,
|
||||
py: 1,
|
||||
'&.Mui-selected': {
|
||||
bgcolor: 'primary.light',
|
||||
color: 'primary.main',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Icon Component={Moon} size="medium" color="primary.light" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { AuthenticationCard } from '../AuthenticationCard';
|
||||
import { Box, CardHeader, Divider, Stack, Typography } from '@mui/material';
|
||||
import { Box, Divider, Stack, Typography } from '@mui/material';
|
||||
import AccountCreatedIcon from '@/assets/account-created.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link as RouterLink, useSearchParams } from 'react-router-dom';
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Stack, Typography, useTheme } from '@mui/material';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { Box, Profile2User } from 'iconsax-react';
|
||||
import React from 'react';
|
||||
import { Profile2User } from 'iconsax-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const AccountCreatedClubBanner = () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { RequestedApplication } from './AccountCreated';
|
||||
import { Box, Button, useTheme } from '@mui/material';
|
||||
import { Box, Button } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CountDownTimer } from '@/components/CountDownTimer';
|
||||
import type { PalleteColor } from '@/theme/palette';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { isNumeric } from '@/utils/regexes/isNumeric';
|
||||
import { CompleteSignUp } from './CompleteSignUp';
|
||||
import { EnterPasswordForm } from './EnterPasswordForm';
|
||||
import { UserStatus } from '../../types/userTypes';
|
||||
import type { CountryCode, GUID } from '@/types/commonTypes';
|
||||
import type { CountryCode } from '@/types/commonTypes';
|
||||
import { VerifyPhoneNumber } from './VerifyPhoneNumber';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
|
||||
@@ -10,11 +10,14 @@ import {
|
||||
Link,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
InputAdornment,
|
||||
} from '@mui/material';
|
||||
import { CloseCircle } from 'iconsax-react';
|
||||
import { CloseCircle, Eye, EyeSlash } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type PasswordDialogProps } from '../../types/settingsType';
|
||||
import { PasswordValidationItem } from './PasswordValidation';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export function PasswordDialog({
|
||||
open,
|
||||
@@ -27,7 +30,6 @@ export function PasswordDialog({
|
||||
currentPassword,
|
||||
setCurrentPassword,
|
||||
showValidation,
|
||||
validPassword,
|
||||
matchPassword,
|
||||
loading,
|
||||
handleSubmit,
|
||||
@@ -39,6 +41,11 @@ export function PasswordDialog({
|
||||
hasUpperAndLower,
|
||||
hasSpecialChar,
|
||||
}: PasswordDialogProps) {
|
||||
const [showCurrent, setShowCurrent] = useState(false);
|
||||
const [showNew, setShowNew] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
@@ -80,13 +87,25 @@ export function PasswordDialog({
|
||||
<>
|
||||
<TextField
|
||||
label={t('securityForm.currentPassword')}
|
||||
type="password"
|
||||
type={showCurrent ? 'text' : 'password'}
|
||||
fullWidth
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowCurrent(!showCurrent)}>
|
||||
<Icon Component={showCurrent ? Eye : EyeSlash} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Link sx={{ fontSize: '15px', cursor: 'pointer' }}>
|
||||
<Link
|
||||
sx={{ fontSize: '15px', cursor: 'pointer' }}
|
||||
onClick={() => navigate('/forget-password')}
|
||||
>
|
||||
{t('securityForm.forgetPassword')}
|
||||
</Link>
|
||||
</>
|
||||
@@ -94,11 +113,20 @@ export function PasswordDialog({
|
||||
|
||||
<TextField
|
||||
label={t('securityForm.newPassword')}
|
||||
type="password"
|
||||
type={showNew ? 'text' : 'password'}
|
||||
fullWidth
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowNew(!showNew)}>
|
||||
<Icon Component={showNew ? Eye : EyeSlash} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
{showValidation && (
|
||||
@@ -124,7 +152,7 @@ export function PasswordDialog({
|
||||
|
||||
<TextField
|
||||
label={t('securityForm.confirmPassword')}
|
||||
type="password"
|
||||
type={showConfirm ? 'text' : 'password'}
|
||||
fullWidth
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
@@ -135,6 +163,15 @@ export function PasswordDialog({
|
||||
: ' '
|
||||
}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowConfirm(!showConfirm)}>
|
||||
<Icon Component={showConfirm ? Eye : EyeSlash} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
@@ -144,7 +181,6 @@ export function PasswordDialog({
|
||||
sx={{ height: 48, textTransform: 'none' }}
|
||||
variant="contained"
|
||||
onClick={handleSubmit}
|
||||
disabled={!validPassword || !matchPassword || loading}
|
||||
>
|
||||
{loading ? (
|
||||
<CircularProgress size={24} color="inherit" />
|
||||
|
||||
@@ -94,23 +94,26 @@ export function PasswordSecurity() {
|
||||
setPassword('');
|
||||
setConfirmPassword('');
|
||||
setCurrentPassword('');
|
||||
}
|
||||
}, [addData, changeData, changePasswordUI, showToast, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (addError || changeError) {
|
||||
} else if (addData || changeData) {
|
||||
showToast({
|
||||
message:
|
||||
getErrorMessage(addError || changeError) || t('securityForm.general'),
|
||||
addData?.message || changeData?.message || t('securityForm.error'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
}, [addError, changeError, t, showToast]);
|
||||
}, [addData, changeData, changePasswordUI, showToast, t]);
|
||||
|
||||
const handleOpen = () => setOpen(true);
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
const handlePasswordSubmit = async () => {
|
||||
if (!matchPassword) {
|
||||
showToast({
|
||||
message: t('securityForm.notCompatibility'),
|
||||
severity: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (changePasswordUI) {
|
||||
await executeChangePassword({
|
||||
oldPassword: currentPassword,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Box, Button, Typography, CircularProgress } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
@@ -25,13 +25,13 @@ export function PersonalInformation() {
|
||||
});
|
||||
const [originalData, setOriginalData] = useState<InfoRowData | null>(null);
|
||||
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,
|
||||
@@ -50,18 +50,14 @@ export function PersonalInformation() {
|
||||
: Gender.None,
|
||||
country: profileData.countryCode ?? '',
|
||||
};
|
||||
|
||||
setData(fetchedData);
|
||||
setOriginalData(fetchedData);
|
||||
|
||||
const imageBaseUrl = process.env.IMAGE_BASE_URL;
|
||||
|
||||
if (profileData.profileImageUrl) {
|
||||
setUploadedImageUrl(`${imageBaseUrl}${profileData.profileImageUrl}`);
|
||||
} else {
|
||||
setUploadedImageUrl(null);
|
||||
}
|
||||
|
||||
const imageBaseUrl = import.meta.env.IMAGE_BASE_URL;
|
||||
setUploadedImageUrl(
|
||||
profileData.profileImageUrl
|
||||
? `${imageBaseUrl}${profileData.profileImageUrl}`
|
||||
: null,
|
||||
);
|
||||
setUploadedImageFile(null);
|
||||
}
|
||||
}, [profileData]);
|
||||
@@ -80,21 +76,18 @@ export function PersonalInformation() {
|
||||
|
||||
const initials = `${data?.firstName?.trim()[0] || ''}${data?.lastName?.trim()[0] || ''}`;
|
||||
|
||||
const handleEditClick = () => {
|
||||
setIsEditing(true);
|
||||
setOriginalData(data);
|
||||
};
|
||||
const handleEditClick = () => setIsEditing(true);
|
||||
|
||||
const handleCancelClick = () => {
|
||||
setIsEditing(false);
|
||||
if (originalData) {
|
||||
setData(originalData);
|
||||
}
|
||||
if (originalData) setData(originalData);
|
||||
setUploadedImageFile(null);
|
||||
};
|
||||
|
||||
const handleSaveClick = async () => {
|
||||
const handleSaveClick = () => {
|
||||
if (!data) return;
|
||||
const isValid = infoRowEditRef.current?.validateFields?.();
|
||||
if (!isValid) return;
|
||||
executeSaveProfile({ data, imageUrl: uploadedImageFile });
|
||||
};
|
||||
|
||||
@@ -105,23 +98,21 @@ export function PersonalInformation() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (saveProfileError) {
|
||||
if (saveProfileError)
|
||||
showToast({
|
||||
message:
|
||||
getErrorMessage(saveProfileError) || t('settingForm.errorSave'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
}, [saveProfileError, showToast, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (fetchProfileError) {
|
||||
if (fetchProfileError)
|
||||
showToast({
|
||||
message:
|
||||
getErrorMessage(fetchProfileError) || t('settingForm.errorFetch'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
}, [fetchProfileError, showToast, t]);
|
||||
|
||||
return (
|
||||
@@ -131,12 +122,7 @@ export function PersonalInformation() {
|
||||
subtitle={t('settingForm.descriptionPersonalInfo')}
|
||||
highlighted={isEditing}
|
||||
action={
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Button
|
||||
@@ -159,7 +145,6 @@ export function PersonalInformation() {
|
||||
textTransform: 'none',
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
}}
|
||||
disabled={isSavingProfile}
|
||||
>
|
||||
{isSavingProfile ? (
|
||||
<CircularProgress size={24} color="inherit" />
|
||||
@@ -174,7 +159,6 @@ export function PersonalInformation() {
|
||||
size="large"
|
||||
variant="outlined"
|
||||
sx={{ borderRadius: 1 }}
|
||||
disabled={isLoadingProfile}
|
||||
>
|
||||
{t('settingForm.editButton')}
|
||||
</Button>
|
||||
@@ -210,46 +194,48 @@ export function PersonalInformation() {
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
mx: { xs: 2, sm: 3, md: 4 },
|
||||
py: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
{isEditing && (
|
||||
<ProfileImage
|
||||
initials={initials}
|
||||
uploadedImageUrl={uploadedImageUrl}
|
||||
onImageChange={(file) => {
|
||||
setUploadedImageFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = () =>
|
||||
setUploadedImageUrl(reader.result as string);
|
||||
reader.readAsDataURL(file);
|
||||
}}
|
||||
onRemoveImage={() => {
|
||||
setUploadedImageFile(null);
|
||||
setUploadedImageUrl(null);
|
||||
}}
|
||||
<Box
|
||||
sx={{
|
||||
mx: { xs: 2, sm: 3, md: 4 },
|
||||
py: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
{isEditing && (
|
||||
<ProfileImage
|
||||
initials={initials}
|
||||
uploadedImageUrl={uploadedImageUrl}
|
||||
onImageChange={(file) => {
|
||||
setUploadedImageFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = () =>
|
||||
setUploadedImageUrl(reader.result as string);
|
||||
reader.readAsDataURL(file);
|
||||
}}
|
||||
onRemoveImage={() => {
|
||||
setUploadedImageFile(null);
|
||||
setUploadedImageUrl(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{data &&
|
||||
(isEditing ? (
|
||||
<InfoRowEdit
|
||||
ref={infoRowEditRef}
|
||||
data={data}
|
||||
setData={setData}
|
||||
/>
|
||||
)}
|
||||
{data &&
|
||||
(isEditing ? (
|
||||
<InfoRowEdit data={data} setData={setData} />
|
||||
) : (
|
||||
<InfoRowDisplay
|
||||
data={data}
|
||||
uploadedImageUrl={uploadedImageUrl}
|
||||
initials={initials}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<InfoRowDisplay
|
||||
data={data}
|
||||
uploadedImageUrl={uploadedImageUrl}
|
||||
initials={initials}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</CardContainer>
|
||||
</PageWrapper>
|
||||
|
||||
@@ -141,12 +141,8 @@ export function PhoneNumber() {
|
||||
},
|
||||
]);
|
||||
setIsEditing(false);
|
||||
toast({
|
||||
message: t('settingForm.phoneChangedSuccessfully'),
|
||||
severity: 'success',
|
||||
});
|
||||
}
|
||||
}, [changePhoneData, countryCode, phoneNumber, t, toast]);
|
||||
}, [changePhoneData, countryCode, phoneNumber, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (changePhoneError) {
|
||||
@@ -203,10 +199,10 @@ export function PhoneNumber() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSendCode = () => {
|
||||
const handleSendCode = async () => {
|
||||
handleBlur();
|
||||
if (formError || !isPhoneValid(countryCode, phoneNumber)) return;
|
||||
executeSendCode({
|
||||
return executeSendCode({
|
||||
phoneNumber: countryCode + phoneNumber.replace(/^0/, ''),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,15 +4,21 @@ import { DisplayField } from './DisplayField';
|
||||
import { Gender } from '@/features/profile/types/settingsType';
|
||||
import { type InfoRowDisplayProps } from '@/features/profile/types/settingsType';
|
||||
import ReactCountryFlag from 'react-country-flag';
|
||||
import { countries } from '@/data/countries';
|
||||
|
||||
export function InfoRowDisplay({
|
||||
data,
|
||||
uploadedImageUrl,
|
||||
initials,
|
||||
}: InfoRowDisplayProps) {
|
||||
const { t } = useTranslation('setting');
|
||||
const { t } = useTranslation(['setting', 'country']);
|
||||
const displayValue = (value: string) =>
|
||||
value?.trim() || t('settingForm.notDetermined');
|
||||
const countryLabel =
|
||||
data.country &&
|
||||
t(countries.find((c) => c.code === data.country)?.label || '', {
|
||||
ns: 'country',
|
||||
});
|
||||
|
||||
const getGenderLabel = (gender: Gender | '') => {
|
||||
switch (gender) {
|
||||
@@ -70,19 +76,20 @@ export function InfoRowDisplay({
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t('settingForm.country')}
|
||||
</Typography>
|
||||
<Box sx={{ mt: 0.5 }}>
|
||||
<Box
|
||||
sx={{ mt: 0.5, display: 'flex', alignItems: 'center', gap: 1 }}
|
||||
>
|
||||
{data.country ? (
|
||||
<ReactCountryFlag
|
||||
countryCode={data.country}
|
||||
svg
|
||||
style={{
|
||||
height: '1.5rem',
|
||||
width: '1.5rem',
|
||||
// TODO: Check alignment for better styling definition
|
||||
marginTop: '-2px',
|
||||
marginRight: '4px',
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<ReactCountryFlag
|
||||
countryCode={data.country}
|
||||
svg
|
||||
style={{ height: '1.5rem', width: '1.5rem' }}
|
||||
/>
|
||||
<Typography variant="body1" color="text.primary">
|
||||
{countryLabel}
|
||||
</Typography>
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="body1" color="text.primary">
|
||||
{t('settingForm.notDetermined')}
|
||||
|
||||
@@ -6,123 +6,175 @@ import {
|
||||
MenuItem,
|
||||
Select,
|
||||
Autocomplete,
|
||||
FormHelperText,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { countries } from '@/data/countries';
|
||||
import { Gender } from '@/features/profile/types/settingsType';
|
||||
import { type InfoRowEditProps } from '@/features/profile/types/settingsType';
|
||||
import ReactCountryFlag from 'react-country-flag';
|
||||
import { useState, forwardRef, useImperativeHandle } from 'react';
|
||||
|
||||
export function InfoRowEdit({ data, setData }: InfoRowEditProps) {
|
||||
const { t } = useTranslation(['countries', 'setting']);
|
||||
export const InfoRowEdit = forwardRef(
|
||||
({ data, setData }: InfoRowEditProps, ref) => {
|
||||
const { t } = useTranslation(['country', 'setting']);
|
||||
const [touched, setTouched] = useState({
|
||||
firstName: false,
|
||||
lastName: false,
|
||||
gender: false,
|
||||
country: false,
|
||||
});
|
||||
|
||||
const countryOptions = countries.map((c) => ({
|
||||
code: c.code,
|
||||
label: t(c.label, { ns: 'countries' }),
|
||||
}));
|
||||
const isValidName = (str: string) => /[A-Za-z\u0600-\u06FF]+/.test(str);
|
||||
|
||||
const currentCountry =
|
||||
countryOptions.find((c) => c.code === data.country) || null;
|
||||
const fields = [
|
||||
{
|
||||
name: 'firstName' as const,
|
||||
label: t('settingForm.name', { ns: 'setting' }),
|
||||
value: data.firstName,
|
||||
},
|
||||
{
|
||||
name: 'lastName' as const,
|
||||
label: t('settingForm.familyName', { ns: 'setting' }),
|
||||
value: data.lastName,
|
||||
},
|
||||
{
|
||||
name: 'nationalCode' as const,
|
||||
label: t('settingForm.nationalCode', { ns: 'setting' }),
|
||||
value: data.nationalCode,
|
||||
},
|
||||
];
|
||||
useImperativeHandle(ref, () => ({
|
||||
validateFields: () => {
|
||||
const newTouched = {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
gender: true,
|
||||
country: true,
|
||||
};
|
||||
setTouched(newTouched);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
{fields.map(({ name, label, value }) => (
|
||||
<Box
|
||||
key={name}
|
||||
sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
[name]: e.target.value,
|
||||
}))
|
||||
}
|
||||
label={label}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
return (
|
||||
data.firstName.trim() !== '' &&
|
||||
isValidName(data.firstName) &&
|
||||
data.lastName.trim() !== '' &&
|
||||
isValidName(data.lastName) &&
|
||||
data.gender !== Gender.None &&
|
||||
data.country !== ''
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
<Box sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>
|
||||
{t('settingForm.genderPlaceholder', { ns: 'setting' })}
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={data.gender === Gender.None ? '' : data.gender}
|
||||
label={t('settingForm.genderPlaceholder', { ns: 'setting' })}
|
||||
onChange={(e) =>
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
gender: e.target.value as Gender,
|
||||
}))
|
||||
}
|
||||
const countryOptions = countries.map((c) => ({
|
||||
code: c.code,
|
||||
label: t(c.label, { ns: 'country' }),
|
||||
}));
|
||||
const currentCountry =
|
||||
countryOptions.find((c) => c.code === data.country) || null;
|
||||
|
||||
const fields = [
|
||||
{
|
||||
name: 'firstName' as const,
|
||||
label: t('settingForm.name', { ns: 'setting' }),
|
||||
value: data.firstName,
|
||||
},
|
||||
{
|
||||
name: 'lastName' as const,
|
||||
label: t('settingForm.familyName', { ns: 'setting' }),
|
||||
value: data.lastName,
|
||||
},
|
||||
{
|
||||
name: 'nationalCode' as const,
|
||||
label: t('settingForm.optionalNationalCode', { ns: 'setting' }),
|
||||
value: data.nationalCode,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
{fields.map(({ name, label, value }) => (
|
||||
<Box
|
||||
key={name}
|
||||
sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}
|
||||
>
|
||||
<MenuItem value={Gender.Male}>
|
||||
{t('settingForm.man', { ns: 'setting' })}
|
||||
</MenuItem>
|
||||
<MenuItem value={Gender.Female}>
|
||||
{t('settingForm.woman', { ns: 'setting' })}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Autocomplete
|
||||
sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}
|
||||
options={countryOptions}
|
||||
getOptionLabel={(option) => option.label}
|
||||
value={currentCountry}
|
||||
onChange={(_, newValue) =>
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
country: newValue?.code || '',
|
||||
}))
|
||||
}
|
||||
renderOption={(props, option) => (
|
||||
<Box component="li" {...props} key={option.code}>
|
||||
<ReactCountryFlag
|
||||
countryCode={option.code}
|
||||
svg
|
||||
style={{
|
||||
height: '1.5rem',
|
||||
width: '1.5rem',
|
||||
// TODO: Check alignment for better styling definition
|
||||
marginTop: '-2px',
|
||||
marginRight: '4px',
|
||||
}}
|
||||
<TextField
|
||||
fullWidth
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setData((prev) => ({ ...prev, [name]: e.target.value }))
|
||||
}
|
||||
label={label}
|
||||
error={
|
||||
name !== 'nationalCode' &&
|
||||
touched[name] &&
|
||||
(value.trim() === '' || !isValidName(value))
|
||||
}
|
||||
helperText={
|
||||
name !== 'nationalCode' &&
|
||||
touched[name] &&
|
||||
(value.trim() === '' || !isValidName(value))
|
||||
? t('settingForm.thisFieldIsRequired', { ns: 'setting' })
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{option.label}
|
||||
</Box>
|
||||
)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={t('settingForm.country', { ns: 'setting' })}
|
||||
/>
|
||||
)}
|
||||
clearOnEscape
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
))}
|
||||
|
||||
<Box sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}>
|
||||
<FormControl
|
||||
fullWidth
|
||||
error={touched.gender && data.gender === Gender.None}
|
||||
>
|
||||
<InputLabel>
|
||||
{t('settingForm.genderPlaceholder', { ns: 'setting' })}
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={data.gender === Gender.None ? '' : data.gender}
|
||||
label={t('settingForm.genderPlaceholder', { ns: 'setting' })}
|
||||
onChange={(e) =>
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
gender: e.target.value as Gender,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<MenuItem value={Gender.Male}>
|
||||
{t('settingForm.man', { ns: 'setting' })}
|
||||
</MenuItem>
|
||||
<MenuItem value={Gender.Female}>
|
||||
{t('settingForm.woman', { ns: 'setting' })}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
{touched.gender && data.gender === Gender.None
|
||||
? t('settingForm.thisFieldIsRequired', { ns: 'setting' })
|
||||
: ''}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Autocomplete
|
||||
sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}
|
||||
options={countryOptions}
|
||||
getOptionLabel={(option) => option.label}
|
||||
value={currentCountry}
|
||||
onChange={(_, newValue) =>
|
||||
setData((prev) => ({ ...prev, country: newValue?.code || '' }))
|
||||
}
|
||||
renderOption={(props, option) => (
|
||||
<Box
|
||||
component="li"
|
||||
{...props}
|
||||
key={option.code}
|
||||
sx={{ gap: 1, alignItems: 'center' }}
|
||||
>
|
||||
<ReactCountryFlag
|
||||
countryCode={option.code}
|
||||
svg
|
||||
style={{ height: '1.5rem', width: '1.5rem' }}
|
||||
/>
|
||||
{option.label}
|
||||
</Box>
|
||||
)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={t('settingForm.country', { ns: 'setting' })}
|
||||
error={touched.country && data.country === ''}
|
||||
helperText={
|
||||
touched.country && data.country === ''
|
||||
? t('settingForm.thisFieldIsRequired', { ns: 'setting' })
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
)}
|
||||
clearOnEscape
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function PhoneEditForm({
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Box sx={{ mb: 2, mx: 3 }}>
|
||||
<Box sx={{ mb: 2, mx: 6 }}>
|
||||
<Typography variant="h6">
|
||||
{t('settingForm.editPhoneNumber')}
|
||||
</Typography>
|
||||
@@ -59,10 +59,12 @@ export default function PhoneEditForm({
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
flexWrap: 'nowrap',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
mx: 3,
|
||||
mx: 6,
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
@@ -76,8 +78,8 @@ export default function PhoneEditForm({
|
||||
error={inputError}
|
||||
helperText={inputError ? error : ''}
|
||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
||||
sx={{ flex: '1 1 220px' }}
|
||||
placeholder="09123456789"
|
||||
sx={{ width: { xs: '100%', sm: 474 } }}
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
buttonState === 'counting' ? (
|
||||
@@ -142,9 +144,10 @@ export default function PhoneEditForm({
|
||||
!isValidPhoneNumber(phoneNumber)
|
||||
}
|
||||
sx={{
|
||||
minWidth: { xs: '100%', sm: 220 },
|
||||
whiteSpace: 'nowrap',
|
||||
color: 'primary.main',
|
||||
textTransform: 'none',
|
||||
width: { xs: '100%', sm: 210 },
|
||||
}}
|
||||
>
|
||||
{isSending ? (
|
||||
@@ -165,11 +168,12 @@ export default function PhoneEditForm({
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
flexWrap: 'nowrap',
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
alignItems: 'center',
|
||||
mx: 3,
|
||||
mx: 6,
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
@@ -180,7 +184,7 @@ export default function PhoneEditForm({
|
||||
onChange={(e) =>
|
||||
setVerificationCode(e.target.value.replace(/\D/g, ''))
|
||||
}
|
||||
sx={{ flex: '1 1 240px', minWidth: 0 }}
|
||||
sx={{ width: { xs: '100%', sm: 474 } }}
|
||||
placeholder={t('settingForm.verificationCode')}
|
||||
/>
|
||||
|
||||
@@ -189,7 +193,7 @@ export default function PhoneEditForm({
|
||||
onClick={handleVerifyClick}
|
||||
disabled={isVerifying || verificationCode.length === 0}
|
||||
sx={{
|
||||
minWidth: { xs: '100%', sm: 220 },
|
||||
width: { xs: '100%', sm: 210 },
|
||||
bgcolor: 'primary.main',
|
||||
textTransform: 'none',
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { type ReactElement, type ElementType } from 'react';
|
||||
import React, { type ReactElement, type ElementType, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -15,6 +15,7 @@ import type { TransitionProps } from '@mui/material/transitions';
|
||||
import { CloseCircle } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type SocialMediaDialogProps } from '@/features/profile/types/settingsType';
|
||||
import { isEmail } from '@/utils/regexes/isEmail';
|
||||
|
||||
const MobileSlide = React.forwardRef(function MobileSlide(
|
||||
props: TransitionProps & { children: ReactElement<unknown, ElementType> },
|
||||
@@ -30,57 +31,58 @@ export default function SocialMediaDialog({
|
||||
emailInput,
|
||||
setEmailInput,
|
||||
fullScreen,
|
||||
computedMaxWidth,
|
||||
verificationCode,
|
||||
setVerificationCode,
|
||||
apiError,
|
||||
isLoading,
|
||||
dialogStep,
|
||||
onSendCode,
|
||||
onConfirmEmail,
|
||||
}: SocialMediaDialogProps) {
|
||||
const [emailError, setEmailError] = useState<string | undefined>();
|
||||
const [touched, setTouched] = useState(false);
|
||||
|
||||
const validateEmail = (value: string) => {
|
||||
if (!value) {
|
||||
setEmailError(t('settingForm.thisFieldIsRequired'));
|
||||
return false;
|
||||
}
|
||||
if (!isEmail(value)) {
|
||||
setEmailError(t('settingForm.emailIsInvalid'));
|
||||
return false;
|
||||
}
|
||||
setEmailError(undefined);
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
fullWidth
|
||||
fullScreen={fullScreen}
|
||||
maxWidth={computedMaxWidth}
|
||||
scroll="paper"
|
||||
maxWidth="xs"
|
||||
scroll="body"
|
||||
keepMounted
|
||||
TransitionComponent={fullScreen ? MobileSlide : undefined}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: { xs: 0, sm: 2 },
|
||||
width: { xs: '100%', sm: '30%' },
|
||||
height: 'auto',
|
||||
},
|
||||
}}
|
||||
PaperProps={{ sx: { mx: 1 } }}
|
||||
>
|
||||
<DialogTitle
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '16px',
|
||||
gap: 1,
|
||||
p: { xs: 1.5, sm: 2 },
|
||||
bgcolor: 'background.default',
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={onClose} aria-label="Close" disabled={isLoading}>
|
||||
<Icon Component={CloseCircle} size="medium" color="primary.main" />
|
||||
</IconButton>
|
||||
{t('settingForm.addEmailButton')}
|
||||
<DialogTitle sx={{ p: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton onClick={onClose} disabled={isLoading}>
|
||||
<Icon Component={CloseCircle} size="large" color="primary.main" />
|
||||
</IconButton>
|
||||
<Box component="span" fontWeight="bold" fontSize={18}>
|
||||
{t('settingForm.addEmailButton')}
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent
|
||||
dividers
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
py: { xs: 1.5, sm: 2 },
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
@@ -94,43 +96,46 @@ export default function SocialMediaDialog({
|
||||
fullWidth
|
||||
type="email"
|
||||
value={emailInput}
|
||||
onChange={(e) => setEmailInput(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setEmailInput(e.target.value);
|
||||
if (touched) validateEmail(e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setTouched(true);
|
||||
validateEmail(emailInput);
|
||||
}}
|
||||
label={t('settingForm.email')}
|
||||
placeholder="abc@email.com"
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
|
||||
autoFocus
|
||||
disabled={isLoading || dialogStep === 'enterCode'}
|
||||
error={touched && !!emailError}
|
||||
helperText={touched && emailError ? emailError : ' '}
|
||||
/>
|
||||
|
||||
{dialogStep === 'enterCode' && (
|
||||
<>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="text"
|
||||
value={verificationCode}
|
||||
onChange={(e) => setVerificationCode(e.target.value)}
|
||||
label={t('settingForm.verificationCode')}
|
||||
autoComplete="one-time-code"
|
||||
inputMode="numeric"
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
autoFocus
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{apiError && (
|
||||
<Typography color="error" variant="caption" sx={{ mt: 1 }}>
|
||||
{apiError}
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="text"
|
||||
value={verificationCode}
|
||||
onChange={(e) => setVerificationCode(e.target.value)}
|
||||
label={t('settingForm.verificationCode')}
|
||||
autoComplete="one-time-code"
|
||||
inputMode="numeric"
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
autoFocus
|
||||
disabled={isLoading}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<Box sx={{ px: 3, pb: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
sx={{ textTransform: 'none', borderRadius: 2, mt: 1 }}
|
||||
sx={{ height: 48, textTransform: 'none', borderRadius: 2 }}
|
||||
variant="contained"
|
||||
disabled={isLoading || (dialogStep === 'enterEmail' && !emailInput)}
|
||||
onClick={dialogStep === 'enterEmail' ? onSendCode : onConfirmEmail}
|
||||
>
|
||||
@@ -142,7 +147,7 @@ export default function SocialMediaDialog({
|
||||
t('settingForm.confirmAndSave')
|
||||
)}
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Typography, IconButton } from '@mui/material';
|
||||
import { Google, Sms, Trash } from 'iconsax-react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { Google, Sms } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type SocialMediaListProps } from '@/features/profile/types/settingsType';
|
||||
|
||||
@@ -64,10 +64,6 @@ export default function SocialMediaList({ emailList }: SocialMediaListProps) {
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<IconButton aria-label="Delete">
|
||||
<Icon Component={Trash} size="medium" variant="Outline" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
@@ -74,8 +74,8 @@ export default function SocialMediaMenu({
|
||||
onClose={handleCloseMenu}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
minWidth: 187,
|
||||
maxWidth: '90vw',
|
||||
width: anchor ? anchor.offsetWidth : 'auto',
|
||||
// maxWidth: '90vw',
|
||||
},
|
||||
}}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
|
||||
@@ -49,7 +49,14 @@ export function ActiveDevicesPage() {
|
||||
ip: session.ipAddress,
|
||||
current: session.key === currentKey,
|
||||
}));
|
||||
setDevices(formattedDevices);
|
||||
|
||||
const sortedDevices = formattedDevices.sort((a, b) => {
|
||||
if (a.current && !b.current) return -1;
|
||||
if (!a.current && b.current) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
setDevices(sortedDevices);
|
||||
}
|
||||
}, [profileData, i18n.language, t]);
|
||||
|
||||
@@ -66,6 +73,7 @@ export function ActiveDevicesPage() {
|
||||
const { data } = await deleteSessions({ keys: [id] });
|
||||
if (data.success) {
|
||||
setDevices((prevDevices) => prevDevices.filter((d) => d.id !== id));
|
||||
showToast({ message: t('active.successDelete'), severity: 'success' });
|
||||
} else {
|
||||
showToast({
|
||||
message: data.message || t('active.deleteFailed'),
|
||||
@@ -73,7 +81,6 @@ export function ActiveDevicesPage() {
|
||||
});
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// console.error('Delete error:', error);
|
||||
showToast({
|
||||
message: t('active.deleteFailed'),
|
||||
severity: 'error',
|
||||
|
||||
@@ -59,7 +59,6 @@ export function SettingPage() {
|
||||
} = useApi(fetchProfile);
|
||||
|
||||
const {
|
||||
data: saveData,
|
||||
loading: isSaving,
|
||||
error: saveError,
|
||||
execute: executeSaveSettings,
|
||||
@@ -87,15 +86,6 @@ export function SettingPage() {
|
||||
}
|
||||
}, [profileData, setMode, i18n]);
|
||||
|
||||
useEffect(() => {
|
||||
if (saveData?.success) {
|
||||
setMode(draftSettings.theme);
|
||||
setSavedSettings(draftSettings);
|
||||
i18n.changeLanguage(draftSettings.language);
|
||||
setIsEditing(false);
|
||||
}
|
||||
}, [saveData, draftSettings, setMode, i18n]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
const resolvedMode = mode === 'light' || mode === 'dark' ? mode : 'light';
|
||||
@@ -109,7 +99,7 @@ export function SettingPage() {
|
||||
setMode(savedSettings.theme);
|
||||
};
|
||||
|
||||
const handleEditToggle = () => {
|
||||
const handleEditToggle = async () => {
|
||||
if (isEditing) {
|
||||
const languageObj = languageOptions.find(
|
||||
(o) => o.code === draftSettings.language,
|
||||
@@ -119,11 +109,18 @@ export function SettingPage() {
|
||||
);
|
||||
|
||||
if (languageObj && calendarObj) {
|
||||
executeSaveSettings({
|
||||
const result = await executeSaveSettings({
|
||||
theme: themeApiMap[draftSettings.theme],
|
||||
calendarType: calendarObj.apiValue,
|
||||
language: languageObj.apiValue,
|
||||
});
|
||||
|
||||
if (result?.success) {
|
||||
setMode(draftSettings.theme);
|
||||
setSavedSettings(draftSettings);
|
||||
i18n.changeLanguage(draftSettings.language);
|
||||
setIsEditing(false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setIsEditing(true);
|
||||
@@ -164,11 +161,13 @@ export function SettingPage() {
|
||||
variant={isEditing ? 'contained' : 'outlined'}
|
||||
disabled={isSaving || isFetching}
|
||||
>
|
||||
{isSaving
|
||||
? t('settings.saving')
|
||||
: isEditing
|
||||
? t('settings.saveButton')
|
||||
: t('settings.editButton')}
|
||||
{isSaving ? (
|
||||
<CircularProgress size={24} />
|
||||
) : isEditing ? (
|
||||
t('settings.saveButton')
|
||||
) : (
|
||||
t('settings.editButton')
|
||||
)}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
@@ -211,40 +210,54 @@ export function SettingPage() {
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
gap: 4,
|
||||
mt: 2,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t('settings.theme')}
|
||||
</Typography>
|
||||
{isEditing ? (
|
||||
<ThemeToggleButton
|
||||
value={draftSettings.theme}
|
||||
onChange={(newTheme) => {
|
||||
setDraftSettings((prev) => ({
|
||||
...prev,
|
||||
theme: newTheme,
|
||||
}));
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Icon
|
||||
Component={savedSettings.theme === 'light' ? Sun1 : Moon}
|
||||
size="medium"
|
||||
variant="Bold"
|
||||
/>
|
||||
<Typography variant="body1">
|
||||
{t(`settings.${savedSettings.theme}`)}
|
||||
>
|
||||
<Typography variant="body1" color="text.primary">
|
||||
{t('settings.theme')}
|
||||
</Typography>
|
||||
<ThemeToggleButton
|
||||
value={draftSettings.theme}
|
||||
onChange={(newTheme) => {
|
||||
if (newTheme) {
|
||||
setMode(newTheme);
|
||||
setDraftSettings((prev) => ({
|
||||
...prev,
|
||||
theme: newTheme,
|
||||
}));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t('settings.theme')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Icon
|
||||
Component={
|
||||
savedSettings.theme === 'light' ? Sun1 : Moon
|
||||
}
|
||||
size="medium"
|
||||
variant="Bold"
|
||||
/>
|
||||
<Typography variant="body1">
|
||||
{t(`settings.${savedSettings.theme}`)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t('settings.language')}
|
||||
</Typography>
|
||||
{isEditing ? (
|
||||
<Autocomplete
|
||||
options={languageOptions}
|
||||
@@ -259,26 +272,30 @@ export function SettingPage() {
|
||||
language: v.code,
|
||||
}))
|
||||
}
|
||||
renderInput={(p) => <TextField {...p} />}
|
||||
renderInput={(p) => (
|
||||
<TextField {...p} label={t('settings.language')} />
|
||||
)}
|
||||
size="medium"
|
||||
fullWidth
|
||||
disableClearable
|
||||
/>
|
||||
) : (
|
||||
<Typography variant="body1">
|
||||
{
|
||||
languageOptions.find(
|
||||
(o) => o.code === savedSettings.language,
|
||||
)?.label
|
||||
}
|
||||
</Typography>
|
||||
<>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t('settings.language')}
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{
|
||||
languageOptions.find(
|
||||
(o) => o.code === savedSettings.language,
|
||||
)?.label
|
||||
}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ mt: 2, width: { xs: '100%', sm: 'calc(50% - 16px)' } }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t('settings.calendar')}
|
||||
</Typography>
|
||||
{isEditing ? (
|
||||
<Autocomplete
|
||||
options={calendarOptions.map((c) => c.key)}
|
||||
@@ -287,18 +304,25 @@ export function SettingPage() {
|
||||
onChange={(_, v) =>
|
||||
v && setDraftSettings((prev) => ({ ...prev, calendar: v }))
|
||||
}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label={t('settings.calendar')} />
|
||||
)}
|
||||
size="medium"
|
||||
fullWidth
|
||||
disableClearable
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Icon Component={Calendar1} size="medium" variant="Bold" />
|
||||
<Typography variant="body1">
|
||||
{t(`settings.${savedSettings.calendar}`)}
|
||||
<>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{t('settings.calendar')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Icon Component={Calendar1} size="medium" variant="Bold" />
|
||||
<Typography variant="body1">
|
||||
{t(`settings.${savedSettings.calendar}`)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user