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