@@ -2,7 +2,7 @@
|
|||||||
"loginForm": {
|
"loginForm": {
|
||||||
"title": "Login/Register",
|
"title": "Login/Register",
|
||||||
"description": "Please enter your email/password to start",
|
"description": "Please enter your email/password to start",
|
||||||
"emailOrPhoneLabel": "Email/Password",
|
"emailOrPhoneLabel": "Email/Phone number",
|
||||||
"submitButton": "Login/Register",
|
"submitButton": "Login/Register",
|
||||||
"loginWithGoogle": "Login with google",
|
"loginWithGoogle": "Login with google",
|
||||||
"emailIsInvalid": "Email is invalid",
|
"emailIsInvalid": "Email is invalid",
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
"moreMinute": "minute",
|
"moreMinute": "minute",
|
||||||
"resendCode": "Resend code",
|
"resendCode": "Resend code",
|
||||||
"confirmAndLogin": "Confirm & login",
|
"confirmAndLogin": "Confirm & login",
|
||||||
|
"confirmAndContinue": "Confirm & continue",
|
||||||
"loginWithPassword": "Login with password"
|
"loginWithPassword": "Login with password"
|
||||||
},
|
},
|
||||||
"completeSignUp": {
|
"completeSignUp": {
|
||||||
|
|||||||
@@ -102,8 +102,7 @@
|
|||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"language": "زبان/language",
|
"language": "زبان/language",
|
||||||
"calendar": "Calendar and date format",
|
"calendar": "Calendar and date format",
|
||||||
"solar": "Solar",
|
"jalali": "jalali",
|
||||||
"lunar": "Lunar",
|
|
||||||
"christian": "Christian",
|
"christian": "Christian",
|
||||||
"iran": "Iran",
|
"iran": "Iran",
|
||||||
"saving": "Saving...",
|
"saving": "Saving...",
|
||||||
|
|||||||
@@ -102,8 +102,7 @@
|
|||||||
"dark": "تاریک",
|
"dark": "تاریک",
|
||||||
"language": "زبان/language",
|
"language": "زبان/language",
|
||||||
"calendar": "فرمت تقویم و تاریخ",
|
"calendar": "فرمت تقویم و تاریخ",
|
||||||
"solar": "شمسی",
|
"jalali": "شمسی",
|
||||||
"lunar": "قمری",
|
|
||||||
"christian": "میلادی",
|
"christian": "میلادی",
|
||||||
"iran": "ایران",
|
"iran": "ایران",
|
||||||
"saving": "در حال ذخیرهسازی...",
|
"saving": "در حال ذخیرهسازی...",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import React, {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { TextField, Stack } from '@mui/material';
|
import { TextField, Stack } from '@mui/material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
|
||||||
|
|
||||||
interface DigitInputProps {
|
interface DigitInputProps {
|
||||||
error: boolean;
|
error: boolean;
|
||||||
@@ -34,6 +35,8 @@ const DigitInput: React.FC<DigitInputProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (value: string, index: number) => {
|
const handleChange = (value: string, index: number) => {
|
||||||
|
value = replacePersianWithRealNumbers(value);
|
||||||
|
|
||||||
if (!/^\d$/.test(value) && value !== '') return;
|
if (!/^\d$/.test(value) && value !== '') return;
|
||||||
|
|
||||||
const newCode = [...code];
|
const newCode = [...code];
|
||||||
@@ -91,6 +94,7 @@ const DigitInput: React.FC<DigitInputProps> = ({
|
|||||||
color={success ? 'success' : 'primary'}
|
color={success ? 'success' : 'primary'}
|
||||||
key={index}
|
key={index}
|
||||||
inputRef={(el) => (inputRefs.current[index] = el)}
|
inputRef={(el) => (inputRefs.current[index] = el)}
|
||||||
|
autoFocus={index === 0}
|
||||||
value={digit}
|
value={digit}
|
||||||
onChange={(e) => handleChange(e.target.value, index)}
|
onChange={(e) => handleChange(e.target.value, index)}
|
||||||
onKeyDown={(e) => e.key === 'Backspace' && handleBackspace(e, index)}
|
onKeyDown={(e) => e.key === 'Backspace' && handleBackspace(e, index)}
|
||||||
@@ -115,7 +119,7 @@ const DigitInput: React.FC<DigitInputProps> = ({
|
|||||||
variant="standard"
|
variant="standard"
|
||||||
size="medium"
|
size="medium"
|
||||||
sx={{
|
sx={{
|
||||||
width: '83px',
|
width: { md: '83px', xs: '20%' },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
IconButton,
|
IconButton,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
@@ -9,23 +10,19 @@ import {
|
|||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
import { Logout, More } from 'iconsax-react';
|
import { Logout, More } from 'iconsax-react';
|
||||||
import type { UserInfo } from '@/contexts/AuthContext';
|
|
||||||
import { LTRTypography } from '../common/LTRTypography';
|
import { LTRTypography } from '../common/LTRTypography';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
import { FlexBox } from '../common/FlexBox';
|
||||||
|
|
||||||
interface HeaderProps {
|
export const Header: React.FC = () => {
|
||||||
user: UserInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Header: React.FC<HeaderProps> = ({ user }) => {
|
|
||||||
const { t, i18n } = useTranslation('sideNav');
|
const { t, i18n } = useTranslation('sideNav');
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
const { logout } = useAuth();
|
const { logout, userInfo: user } = useAuth();
|
||||||
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -52,15 +49,26 @@ export const Header: React.FC<HeaderProps> = ({ user }) => {
|
|||||||
height: (t) => t.spacing(10.5),
|
height: (t) => t.spacing(10.5),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<FlexBox sx={{ alignItems: 'center', gap: 1 }}>
|
||||||
<Typography variant="body1" color="textSecondary">
|
<Avatar
|
||||||
{user.fullName}
|
sx={{ width: 32, height: 32, fontSize: '14px' }}
|
||||||
</Typography>
|
src={
|
||||||
{/* TODO: add ternary text color to palette */}
|
user.picture &&
|
||||||
<LTRTypography variant="body2" color="#757575">
|
import.meta.env.VITE_IMAGE_BASE_URL + '/' + user.picture
|
||||||
{user.phoneNumber ?? user.email}
|
}
|
||||||
</LTRTypography>
|
>
|
||||||
</Box>
|
{user.firstName.charAt(0) + ' ' + user.lastName.charAt(0)}
|
||||||
|
</Avatar>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body1" color="textSecondary">
|
||||||
|
{user.fullName}
|
||||||
|
</Typography>
|
||||||
|
{/* TODO: add ternary text color to palette */}
|
||||||
|
<LTRTypography variant="body2" color="#757575">
|
||||||
|
{user.phoneNumber ?? user.email}
|
||||||
|
</LTRTypography>
|
||||||
|
</Box>
|
||||||
|
</FlexBox>
|
||||||
|
|
||||||
<IconButton onClick={handleClick} color="primary">
|
<IconButton onClick={handleClick} color="primary">
|
||||||
<Icon Component={More} />
|
<Icon Component={More} />
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { appRoutes } from '@/routes/config';
|
|||||||
import { Outlet, useLocation } from 'react-router-dom';
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
import { Box, useMediaQuery, useTheme } from '@mui/material';
|
import { Box, useMediaQuery, useTheme } from '@mui/material';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
import { useState } from 'react';
|
import { Suspense, useState } from 'react';
|
||||||
import { Toolbar } from './Toolbar';
|
import { Toolbar } from './Toolbar';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
import { Loading } from '../routes/Loading';
|
||||||
|
|
||||||
export const Layout = () => {
|
export const Layout = () => {
|
||||||
const navItemConfigs = buildNavItems(appRoutes);
|
const navItemConfigs = buildNavItems(appRoutes);
|
||||||
@@ -53,7 +54,9 @@ export const Layout = () => {
|
|||||||
overflowInline: 'auto',
|
overflowInline: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Outlet />
|
<Suspense fallback={<Loading />}>
|
||||||
|
<Outlet />
|
||||||
|
</Suspense>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -63,21 +63,9 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
)}
|
)}
|
||||||
<Logo />
|
<Logo />
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
|
||||||
sx={{ display: 'flex', height: '100%', alignItems: 'center', gap: 1 }}
|
|
||||||
>
|
|
||||||
{isMobile && (
|
|
||||||
<Avatar
|
|
||||||
sx={{ width: 32, height: 32, fontSize: '14px' }}
|
|
||||||
src={user.picture}
|
|
||||||
>
|
|
||||||
{user.firstName.charAt(0) + ' ' + user.lastName.charAt(0)}
|
|
||||||
</Avatar>
|
|
||||||
)}
|
|
||||||
<IconButton color="primary" onClick={handleClick}>
|
<IconButton color="primary" onClick={handleClick}>
|
||||||
<Icon Component={Menu} variant="Bold" />
|
<Icon Component={Menu} variant="Bold" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
|
||||||
<ProductsMenu anchorEl={anchorEl} onClose={handleClose} open={open} />
|
<ProductsMenu anchorEl={anchorEl} onClose={handleClose} open={open} />
|
||||||
</MuiToolbar>
|
</MuiToolbar>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -95,9 +95,10 @@ export const AuthenticationSteps = (): JSX.Element => {
|
|||||||
tokenResponse: GenerateTokenResponse,
|
tokenResponse: GenerateTokenResponse,
|
||||||
) => {
|
) => {
|
||||||
setMemoryTokenRes(tokenResponse);
|
setMemoryTokenRes(tokenResponse);
|
||||||
if (authFactory.isCurrentApplication()) {
|
// TODO: For now both application scopes can have their tokens
|
||||||
login(tokenResponse);
|
// later we need to discuss this base on business plan and change it
|
||||||
}
|
// if (authFactory.isCurrentApplication()) {}
|
||||||
|
login(tokenResponse);
|
||||||
|
|
||||||
if (loginResult.registeredWithOutPhoneNumber) {
|
if (loginResult.registeredWithOutPhoneNumber) {
|
||||||
setCurrentStep('addPhoneNumber');
|
setCurrentStep('addPhoneNumber');
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Box, Button, TextField, Typography } from '@mui/material';
|
import { Box, Button, TextField, Typography } from '@mui/material';
|
||||||
import parsePhoneNumberFromString from 'libphonenumber-js';
|
import parsePhoneNumberFromString from 'libphonenumber-js';
|
||||||
import { useRef, useState, type Dispatch } from 'react';
|
import { useRef, useState, type ChangeEvent, type Dispatch } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AuthenticationCard } from '../AuthenticationCard';
|
import { AuthenticationCard } from '../AuthenticationCard';
|
||||||
import { CountryCodeSelector } from '../../../../components/CountryCodeSelector';
|
import { CountryCodeSelector } from '../../../../components/CountryCodeSelector';
|
||||||
import { sendSmsOtp } from '../../api/authorizationAPI';
|
import { sendSmsOtp } from '../../api/authorizationAPI';
|
||||||
import type { CountryCode } from '@/types/commonTypes';
|
import type { CountryCode } from '@/types/commonTypes';
|
||||||
import { useApi } from '@/hooks/useApi';
|
import { useApi } from '@/hooks/useApi';
|
||||||
|
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
|
||||||
|
|
||||||
export interface CompleteSignUpProps {
|
export interface CompleteSignUpProps {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -25,7 +26,7 @@ export const CompleteSignUp = ({
|
|||||||
setCountryCode,
|
setCountryCode,
|
||||||
onCompleteSignUp,
|
onCompleteSignUp,
|
||||||
}: CompleteSignUpProps) => {
|
}: CompleteSignUpProps) => {
|
||||||
const { t } = useTranslation('authentication');
|
const { t, i18n } = useTranslation('authentication');
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
const textFieldRef = useRef<HTMLDivElement>(null);
|
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -45,6 +46,12 @@ export const CompleteSignUp = ({
|
|||||||
handleValueError();
|
handleValueError();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = replacePersianWithRealNumbers(event.target.value);
|
||||||
|
|
||||||
|
setValue(value);
|
||||||
|
};
|
||||||
|
|
||||||
const handleValueError = () => {
|
const handleValueError = () => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
setError(t('loginForm.thisFieldIsRequired'));
|
setError(t('loginForm.thisFieldIsRequired'));
|
||||||
@@ -62,8 +69,13 @@ export const CompleteSignUp = ({
|
|||||||
if (!value || !isPhoneValid(countryCode, value)) {
|
if (!value || !isPhoneValid(countryCode, value)) {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
} else {
|
} else {
|
||||||
await sendSmsCall({ phoneNumber: countryCode + value });
|
let newValue = value;
|
||||||
onCompleteSignUp(countryCode, value);
|
if (countryCode === '+98' && newValue.startsWith('09')) {
|
||||||
|
newValue = newValue.substring(1);
|
||||||
|
setValue(newValue);
|
||||||
|
}
|
||||||
|
await sendSmsCall({ phoneNumber: countryCode + newValue });
|
||||||
|
onCompleteSignUp(countryCode, newValue);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,7 +105,7 @@ export const CompleteSignUp = ({
|
|||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
label={t('completeSignUp.phoneNumber')}
|
label={t('completeSignUp.phoneNumber')}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={handleChange}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
error={inputError}
|
error={inputError}
|
||||||
helperText={inputError ? error : ''}
|
helperText={inputError ? error : ''}
|
||||||
@@ -101,7 +113,16 @@ export const CompleteSignUp = ({
|
|||||||
slotProps={{
|
slotProps={{
|
||||||
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5 } },
|
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5 } },
|
||||||
input: {
|
input: {
|
||||||
endAdornment: (
|
endAdornment: i18n.dir() === 'rtl' && (
|
||||||
|
<CountryCodeSelector
|
||||||
|
value={countryCode}
|
||||||
|
onChange={setCountryCode}
|
||||||
|
show={true}
|
||||||
|
menuAnchor={textFieldRef.current}
|
||||||
|
onCloseFocusRef={inputRef}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
startAdornment: i18n.dir() === 'ltr' && (
|
||||||
<CountryCodeSelector
|
<CountryCodeSelector
|
||||||
value={countryCode}
|
value={countryCode}
|
||||||
onChange={setCountryCode}
|
onChange={setCountryCode}
|
||||||
|
|||||||
@@ -67,9 +67,6 @@ export function LoginRegisterForm({
|
|||||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
let newValue = event.target.value;
|
let newValue = event.target.value;
|
||||||
newValue = replacePersianWithRealNumbers(newValue);
|
newValue = replacePersianWithRealNumbers(newValue);
|
||||||
if (newValue.startsWith('09')) {
|
|
||||||
newValue = newValue.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoginRegisterValue(newValue);
|
setLoginRegisterValue(newValue);
|
||||||
|
|
||||||
@@ -112,10 +109,20 @@ export function LoginRegisterForm({
|
|||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (validateInput(loginRegisterValue, authType, false)) {
|
if (validateInput(loginRegisterValue, authType, false)) {
|
||||||
|
let newValue = loginRegisterValue;
|
||||||
|
|
||||||
|
if (
|
||||||
|
authType === 'phone' &&
|
||||||
|
countryCode === '+98' &&
|
||||||
|
newValue.startsWith('09')
|
||||||
|
) {
|
||||||
|
newValue = newValue.substring(1);
|
||||||
|
setLoginRegisterValue(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
const res = await execUserStatus({
|
const res = await execUserStatus({
|
||||||
phoneNumber:
|
phoneNumber: authType === 'phone' ? countryCode + newValue : undefined,
|
||||||
authType === 'phone' ? countryCode + loginRegisterValue : undefined,
|
email: authType === 'email' ? newValue : undefined,
|
||||||
email: authType === 'email' ? loginRegisterValue : undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res) {
|
if (!res) {
|
||||||
@@ -123,7 +130,7 @@ export function LoginRegisterForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
onLoginRegisterSubmit(loginRegisterValue, res.userStatus);
|
onLoginRegisterSubmit(newValue, res.userStatus);
|
||||||
} else {
|
} else {
|
||||||
toast({ message: res.message, severity: 'error' });
|
toast({ message: res.message, severity: 'error' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,6 @@ export function OtpVerifyForm({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
gap: 4,
|
|
||||||
mb: 0.5,
|
mb: 0.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { useApi } from '@/hooks/useApi';
|
|||||||
export interface ChangePasswordProps {
|
export interface ChangePasswordProps {
|
||||||
onEditInfo: () => void;
|
onEditInfo: () => void;
|
||||||
onPasswordChanged: () => void;
|
onPasswordChanged: () => void;
|
||||||
forgettedPasswordInfo: string;
|
forgetPasswordInfo: string;
|
||||||
infoType: AuthType;
|
infoType: AuthType;
|
||||||
countryCode: CountryCode;
|
countryCode: CountryCode;
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ export interface ChangePasswordProps {
|
|||||||
export const ChangePassword = ({
|
export const ChangePassword = ({
|
||||||
onEditInfo,
|
onEditInfo,
|
||||||
onPasswordChanged,
|
onPasswordChanged,
|
||||||
forgettedPasswordInfo,
|
forgetPasswordInfo,
|
||||||
infoType,
|
infoType,
|
||||||
countryCode,
|
countryCode,
|
||||||
}: ChangePasswordProps) => {
|
}: ChangePasswordProps) => {
|
||||||
@@ -78,11 +78,9 @@ export const ChangePassword = ({
|
|||||||
confirmInputRef.current?.focus();
|
confirmInputRef.current?.focus();
|
||||||
} else {
|
} else {
|
||||||
const apiRequest: ResetPasswordRequest = {
|
const apiRequest: ResetPasswordRequest = {
|
||||||
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
|
email: infoType === 'email' ? forgetPasswordInfo : undefined,
|
||||||
phoneNumber:
|
phoneNumber:
|
||||||
infoType === 'phone'
|
infoType === 'phone' ? countryCode + forgetPasswordInfo : undefined,
|
||||||
? countryCode + forgettedPasswordInfo
|
|
||||||
: undefined,
|
|
||||||
newPassword: passValue,
|
newPassword: passValue,
|
||||||
confirmNewPassword: confirmPassValue,
|
confirmNewPassword: confirmPassValue,
|
||||||
};
|
};
|
||||||
@@ -138,7 +136,7 @@ export const ChangePassword = ({
|
|||||||
endIcon={<Icon Component={Edit2} />}
|
endIcon={<Icon Component={Edit2} />}
|
||||||
onClick={onEditInfo}
|
onClick={onEditInfo}
|
||||||
>
|
>
|
||||||
{forgettedPasswordInfo}
|
{forgetPasswordInfo}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { AuthType } from '../../types/authTypes';
|
import type { AuthType } from '../../types/authTypes';
|
||||||
import { ForgettedPasswordInfo } from './ForgettedPasswordInfo';
|
import { ForgetPasswordInfo } from './ForgetPasswordInfo';
|
||||||
import { ForgetPasswordOtp } from './ForgetPasswordOtp';
|
import { ForgetPasswordOtp } from './ForgetPasswordOtp';
|
||||||
import { ChangePassword } from './ChangePassword';
|
import { ChangePassword } from './ChangePassword';
|
||||||
import type { CountryCode } from '@/types/commonTypes';
|
import type { CountryCode } from '@/types/commonTypes';
|
||||||
@@ -11,8 +11,7 @@ export const ForgetPasswordContainer = () => {
|
|||||||
const [forgetPassCurrentStep, setForgetPassCurrentStep] = useState<
|
const [forgetPassCurrentStep, setForgetPassCurrentStep] = useState<
|
||||||
'enterInfo' | 'verifyOtp' | 'setPassword'
|
'enterInfo' | 'verifyOtp' | 'setPassword'
|
||||||
>('enterInfo');
|
>('enterInfo');
|
||||||
const [forgettedPasswordInfo, setForgettedPasswordInfo] =
|
const [forgetPasswordInfo, setForgetPasswordInfo] = useState<string>('');
|
||||||
useState<string>('');
|
|
||||||
const [infoCountryCode, setInfoCountryCode] = useState<CountryCode>('+98');
|
const [infoCountryCode, setInfoCountryCode] = useState<CountryCode>('+98');
|
||||||
const [infoType, setInfoType] = useState<AuthType>('email');
|
const [infoType, setInfoType] = useState<AuthType>('email');
|
||||||
|
|
||||||
@@ -35,11 +34,11 @@ export const ForgetPasswordContainer = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{forgetPassCurrentStep === 'enterInfo' && (
|
{forgetPassCurrentStep === 'enterInfo' && (
|
||||||
<ForgettedPasswordInfo
|
<ForgetPasswordInfo
|
||||||
infoType={infoType}
|
infoType={infoType}
|
||||||
setInfoType={setInfoType}
|
setInfoType={setInfoType}
|
||||||
forgettedPasswordInfo={forgettedPasswordInfo}
|
forgetPasswordInfo={forgetPasswordInfo}
|
||||||
setForgettedPasswordInfo={setForgettedPasswordInfo}
|
setForgetPasswordInfo={setForgetPasswordInfo}
|
||||||
onVerifyOtp={handleVerifyOtp}
|
onVerifyOtp={handleVerifyOtp}
|
||||||
countryCode={infoCountryCode}
|
countryCode={infoCountryCode}
|
||||||
setCountryCode={setInfoCountryCode}
|
setCountryCode={setInfoCountryCode}
|
||||||
@@ -51,7 +50,7 @@ export const ForgetPasswordContainer = () => {
|
|||||||
countryCode={infoCountryCode}
|
countryCode={infoCountryCode}
|
||||||
infoType={infoType}
|
infoType={infoType}
|
||||||
onEditInfo={handleEditInfo}
|
onEditInfo={handleEditInfo}
|
||||||
forgettedPasswordInfo={forgettedPasswordInfo}
|
forgetPasswordInfo={forgetPasswordInfo}
|
||||||
onOTPVerified={handleOtpVerified}
|
onOTPVerified={handleOtpVerified}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -59,7 +58,7 @@ export const ForgetPasswordContainer = () => {
|
|||||||
{forgetPassCurrentStep === 'setPassword' && (
|
{forgetPassCurrentStep === 'setPassword' && (
|
||||||
<ChangePassword
|
<ChangePassword
|
||||||
onEditInfo={handleEditInfo}
|
onEditInfo={handleEditInfo}
|
||||||
forgettedPasswordInfo={forgettedPasswordInfo}
|
forgetPasswordInfo={forgetPasswordInfo}
|
||||||
onPasswordChanged={handlePasswordChanged}
|
onPasswordChanged={handlePasswordChanged}
|
||||||
infoType={infoType}
|
infoType={infoType}
|
||||||
countryCode={infoCountryCode}
|
countryCode={infoCountryCode}
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import { useToast } from '@rkheftan/harmony-ui';
|
|||||||
import { useApi } from '@/hooks/useApi';
|
import { useApi } from '@/hooks/useApi';
|
||||||
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
|
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
|
||||||
|
|
||||||
export interface ForgettedPasswordInfoProps {
|
export interface ForgetPasswordInfoProps {
|
||||||
forgettedPasswordInfo: string;
|
forgetPasswordInfo: string;
|
||||||
setForgettedPasswordInfo: Dispatch<string>;
|
setForgetPasswordInfo: Dispatch<string>;
|
||||||
infoType: AuthType;
|
infoType: AuthType;
|
||||||
setInfoType: Dispatch<AuthType>;
|
setInfoType: Dispatch<AuthType>;
|
||||||
onVerifyOtp: (value: string) => void;
|
onVerifyOtp: (value: string) => void;
|
||||||
@@ -24,15 +24,15 @@ export interface ForgettedPasswordInfoProps {
|
|||||||
setCountryCode: Dispatch<CountryCode>;
|
setCountryCode: Dispatch<CountryCode>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ForgettedPasswordInfo({
|
export function ForgetPasswordInfo({
|
||||||
forgettedPasswordInfo,
|
forgetPasswordInfo,
|
||||||
setForgettedPasswordInfo,
|
setForgetPasswordInfo,
|
||||||
infoType,
|
infoType,
|
||||||
setInfoType,
|
setInfoType,
|
||||||
onVerifyOtp,
|
onVerifyOtp,
|
||||||
countryCode,
|
countryCode,
|
||||||
setCountryCode,
|
setCountryCode,
|
||||||
}: ForgettedPasswordInfoProps) {
|
}: ForgetPasswordInfoProps) {
|
||||||
const { t } = useTranslation('authentication');
|
const { t } = useTranslation('authentication');
|
||||||
const textFieldRef = useRef<HTMLDivElement>(null);
|
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -51,7 +51,7 @@ export function ForgettedPasswordInfo({
|
|||||||
if (newValue.startsWith('09')) {
|
if (newValue.startsWith('09')) {
|
||||||
newValue = newValue.substring(1);
|
newValue = newValue.substring(1);
|
||||||
}
|
}
|
||||||
setForgettedPasswordInfo(newValue);
|
setForgetPasswordInfo(newValue);
|
||||||
|
|
||||||
// If the new value contains only digits (or is empty), it's a phone number
|
// If the new value contains only digits (or is empty), it's a phone number
|
||||||
if (isNumeric(newValue)) {
|
if (isNumeric(newValue)) {
|
||||||
@@ -63,7 +63,7 @@ export function ForgettedPasswordInfo({
|
|||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
setTouched(true);
|
setTouched(true);
|
||||||
validateInput(forgettedPasswordInfo, infoType);
|
validateInput(forgetPasswordInfo, infoType);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateInput = (
|
const validateInput = (
|
||||||
@@ -87,13 +87,11 @@ export function ForgettedPasswordInfo({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (validateInput(forgettedPasswordInfo, infoType, false)) {
|
if (validateInput(forgetPasswordInfo, infoType, false)) {
|
||||||
const sendCodeRequest: SendForgetPassCodeRequest = {
|
const sendCodeRequest: SendForgetPassCodeRequest = {
|
||||||
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
|
email: infoType === 'email' ? forgetPasswordInfo : undefined,
|
||||||
phoneNumber:
|
phoneNumber:
|
||||||
infoType === 'phone'
|
infoType === 'phone' ? countryCode + forgetPasswordInfo : undefined,
|
||||||
? countryCode + forgettedPasswordInfo
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
const res = await sendForgetPassCodeCall(sendCodeRequest);
|
const res = await sendForgetPassCodeCall(sendCodeRequest);
|
||||||
|
|
||||||
@@ -106,19 +104,18 @@ export function ForgettedPasswordInfo({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onVerifyOtp(forgettedPasswordInfo);
|
onVerifyOtp(forgetPasswordInfo);
|
||||||
} else {
|
} else {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
validateInput(forgettedPasswordInfo, infoType);
|
validateInput(forgetPasswordInfo, infoType);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showAdornment =
|
const showAdornment = infoType === 'phone' && forgetPasswordInfo.length > 0;
|
||||||
infoType === 'phone' && forgettedPasswordInfo.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthenticationCard>
|
<AuthenticationCard>
|
||||||
<Stack spacing={1}>
|
<Stack component="form" onSubmit={handleSubmit} spacing={1}>
|
||||||
<Typography variant="h5">
|
<Typography variant="h5">
|
||||||
{t('forgetPassword.forgetPassword')}
|
{t('forgetPassword.forgetPassword')}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -133,7 +130,7 @@ export function ForgettedPasswordInfo({
|
|||||||
ref={textFieldRef}
|
ref={textFieldRef}
|
||||||
inputRef={inputRef}
|
inputRef={inputRef}
|
||||||
label={t('loginForm.emailOrPhoneLabel')}
|
label={t('loginForm.emailOrPhoneLabel')}
|
||||||
value={forgettedPasswordInfo}
|
value={forgetPasswordInfo}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
error={inputError}
|
error={inputError}
|
||||||
@@ -157,7 +154,7 @@ export function ForgettedPasswordInfo({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Button loading={sendForgetPassCodeLoading} onClick={handleSubmit}>
|
<Button loading={sendForgetPassCodeLoading} type="submit">
|
||||||
{t('forgetPassword.confirm')}
|
{t('forgetPassword.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -18,7 +18,7 @@ import { Icon, useToast } from '@rkheftan/harmony-ui';
|
|||||||
import { useApi } from '@/hooks/useApi';
|
import { useApi } from '@/hooks/useApi';
|
||||||
|
|
||||||
interface ForgetPasswordOtpProps {
|
interface ForgetPasswordOtpProps {
|
||||||
forgettedPasswordInfo: string;
|
forgetPasswordInfo: string;
|
||||||
infoType: AuthType;
|
infoType: AuthType;
|
||||||
countryCode: CountryCode;
|
countryCode: CountryCode;
|
||||||
onEditInfo: () => void;
|
onEditInfo: () => void;
|
||||||
@@ -26,7 +26,7 @@ interface ForgetPasswordOtpProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ForgetPasswordOtp({
|
export function ForgetPasswordOtp({
|
||||||
forgettedPasswordInfo,
|
forgetPasswordInfo,
|
||||||
infoType,
|
infoType,
|
||||||
countryCode,
|
countryCode,
|
||||||
onEditInfo,
|
onEditInfo,
|
||||||
@@ -69,9 +69,9 @@ export function ForgetPasswordOtp({
|
|||||||
|
|
||||||
const handleResendOTPCode = async () => {
|
const handleResendOTPCode = async () => {
|
||||||
const sendCodeRequest: SendForgetPassCodeRequest = {
|
const sendCodeRequest: SendForgetPassCodeRequest = {
|
||||||
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
|
email: infoType === 'email' ? forgetPasswordInfo : undefined,
|
||||||
phoneNumber:
|
phoneNumber:
|
||||||
infoType === 'phone' ? countryCode + forgettedPasswordInfo : undefined,
|
infoType === 'phone' ? countryCode + forgetPasswordInfo : undefined,
|
||||||
};
|
};
|
||||||
await sendForgetPassCodeCall(sendCodeRequest);
|
await sendForgetPassCodeCall(sendCodeRequest);
|
||||||
|
|
||||||
@@ -93,11 +93,9 @@ export function ForgetPasswordOtp({
|
|||||||
|
|
||||||
// Change setTimeout to api call
|
// Change setTimeout to api call
|
||||||
const apiRequest: ConfirmForgetPassCodeRequest = {
|
const apiRequest: ConfirmForgetPassCodeRequest = {
|
||||||
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
|
email: infoType === 'email' ? forgetPasswordInfo : undefined,
|
||||||
phoneNumber:
|
phoneNumber:
|
||||||
infoType === 'phone'
|
infoType === 'phone' ? countryCode + forgetPasswordInfo : undefined,
|
||||||
? countryCode + forgettedPasswordInfo
|
|
||||||
: undefined,
|
|
||||||
code: otpCode,
|
code: otpCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -153,8 +151,8 @@ export function ForgetPasswordOtp({
|
|||||||
onClick={onEditInfo}
|
onClick={onEditInfo}
|
||||||
>
|
>
|
||||||
{infoType === 'phone'
|
{infoType === 'phone'
|
||||||
? countryCode + forgettedPasswordInfo
|
? countryCode + forgetPasswordInfo
|
||||||
: forgettedPasswordInfo}
|
: forgetPasswordInfo}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ export function PhoneNumber() {
|
|||||||
setPhoneNumberTouched(false);
|
setPhoneNumberTouched(false);
|
||||||
setVerificationCodeError(undefined);
|
setVerificationCodeError(undefined);
|
||||||
setVerificationCodeTouched(false);
|
setVerificationCodeTouched(false);
|
||||||
|
setIsCodeSent(false);
|
||||||
}
|
}
|
||||||
return !prev;
|
return !prev;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ import { Edit2, TickCircle } from 'iconsax-react';
|
|||||||
import { CountDownTimer } from '@/components/CountDownTimer';
|
import { CountDownTimer } from '@/components/CountDownTimer';
|
||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
import { type PhoneEditFormProps } from '@/features/profile/types/settingsType';
|
import { type PhoneEditFormProps } from '@/features/profile/types/settingsType';
|
||||||
import { useRef } from 'react';
|
import { useRef, type ChangeEvent } from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { CountryCodeSelector } from '@/components/CountryCodeSelector';
|
import { CountryCodeSelector } from '@/components/CountryCodeSelector';
|
||||||
import { LTRTypography } from '@/components/common/LTRTypography';
|
import { LTRTypography } from '@/components/common/LTRTypography';
|
||||||
|
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
|
||||||
|
|
||||||
export default function PhoneEditForm({
|
export default function PhoneEditForm({
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
@@ -42,6 +43,20 @@ export default function PhoneEditForm({
|
|||||||
const textFieldRef = useRef<HTMLDivElement>(null);
|
const textFieldRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = replacePersianWithRealNumbers(event.target.value);
|
||||||
|
|
||||||
|
setPhoneNumber(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSendCode = () => {
|
||||||
|
if (countryCode === '+98' && phoneNumber.startsWith('09')) {
|
||||||
|
const newValue = phoneNumber.substring(1);
|
||||||
|
setPhoneNumber(newValue);
|
||||||
|
}
|
||||||
|
handleSendCode();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ px: { sm: 4, xs: 2 }, my: 4 }}>
|
<Box sx={{ px: { sm: 4, xs: 2 }, my: 4 }}>
|
||||||
<Box sx={{ mb: 4 }}>
|
<Box sx={{ mb: 4 }}>
|
||||||
@@ -70,6 +85,7 @@ export default function PhoneEditForm({
|
|||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
|
autoFocus
|
||||||
name="phoneNumber"
|
name="phoneNumber"
|
||||||
label={t('settingForm.newPhoneNumber')}
|
label={t('settingForm.newPhoneNumber')}
|
||||||
disabled={isCodeSent}
|
disabled={isCodeSent}
|
||||||
@@ -80,30 +96,12 @@ export default function PhoneEditForm({
|
|||||||
onBlur={() => handleBlur('phoneNumber')}
|
onBlur={() => handleBlur('phoneNumber')}
|
||||||
error={!!phoneNumberError}
|
error={!!phoneNumberError}
|
||||||
helperText={phoneNumberError}
|
helperText={phoneNumberError}
|
||||||
onChange={(e) => setPhoneNumber(e.target.value)}
|
onChange={handleChange}
|
||||||
placeholder="09123456789"
|
placeholder="09123456789"
|
||||||
slotProps={{
|
slotProps={{
|
||||||
input: {
|
input: {
|
||||||
endAdornment: isCodeSent ? (
|
endAdornment:
|
||||||
<InputAdornment position="end">
|
i18n.dir() === 'rtl' ? (
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
setButtonState('default');
|
|
||||||
setVerificationCode('');
|
|
||||||
setIsCodeSent(false);
|
|
||||||
}}
|
|
||||||
edge="end"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
Component={Edit2}
|
|
||||||
color="primary.main"
|
|
||||||
variant="Bold"
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
) : (
|
|
||||||
i18n.dir() === 'rtl' && (
|
|
||||||
<CountryCodeSelector
|
<CountryCodeSelector
|
||||||
value={countryCode}
|
value={countryCode}
|
||||||
onChange={setCountryCode}
|
onChange={setCountryCode}
|
||||||
@@ -112,18 +110,58 @@ export default function PhoneEditForm({
|
|||||||
menuAnchor={textFieldRef.current}
|
menuAnchor={textFieldRef.current}
|
||||||
onCloseFocusRef={inputRef}
|
onCloseFocusRef={inputRef}
|
||||||
/>
|
/>
|
||||||
)
|
) : (
|
||||||
),
|
isCodeSent && (
|
||||||
startAdornment: i18n.dir() === 'ltr' && (
|
<InputAdornment position="end">
|
||||||
<CountryCodeSelector
|
<IconButton
|
||||||
value={countryCode}
|
size="small"
|
||||||
onChange={setCountryCode}
|
onClick={() => {
|
||||||
disabled={isCodeSent}
|
setButtonState('default');
|
||||||
show={true}
|
setVerificationCode('');
|
||||||
menuAnchor={textFieldRef.current}
|
setIsCodeSent(false);
|
||||||
onCloseFocusRef={inputRef}
|
}}
|
||||||
/>
|
edge="end"
|
||||||
),
|
>
|
||||||
|
<Icon
|
||||||
|
Component={Edit2}
|
||||||
|
color="primary.main"
|
||||||
|
variant="Bold"
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
),
|
||||||
|
startAdornment:
|
||||||
|
i18n.dir() === 'ltr' ? (
|
||||||
|
<CountryCodeSelector
|
||||||
|
value={countryCode}
|
||||||
|
onChange={setCountryCode}
|
||||||
|
disabled={isCodeSent}
|
||||||
|
show={true}
|
||||||
|
menuAnchor={textFieldRef.current}
|
||||||
|
onCloseFocusRef={inputRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
isCodeSent && (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setButtonState('default');
|
||||||
|
setVerificationCode('');
|
||||||
|
setIsCodeSent(false);
|
||||||
|
}}
|
||||||
|
edge="end"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
Component={Edit2}
|
||||||
|
color="primary.main"
|
||||||
|
variant="Bold"
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -144,7 +182,8 @@ export default function PhoneEditForm({
|
|||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant="text"
|
||||||
loading={isSendingCode}
|
loading={isSendingCode}
|
||||||
onClick={handleSendCode}
|
onClick={onSendCode}
|
||||||
|
disabled={buttonState === 'counting'}
|
||||||
sx={{
|
sx={{
|
||||||
color: 'primary.main',
|
color: 'primary.main',
|
||||||
width: { xs: '100%', sm: 208 },
|
width: { xs: '100%', sm: 208 },
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
Button,
|
||||||
useTheme,
|
|
||||||
useMediaQuery,
|
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
|
Stack,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { DeviceMessage, Logout } from 'iconsax-react';
|
import { DeviceMessage, Logout, Mobile } from 'iconsax-react';
|
||||||
import { CardContainer } from '@/components/CardContainer';
|
import { CardContainer } from '@/components/CardContainer';
|
||||||
import { PageWrapper } from '../components/PageWrapper';
|
import { PageWrapper } from '../components/PageWrapper';
|
||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
@@ -24,8 +23,6 @@ export function ActiveDevicesPage() {
|
|||||||
const { t, i18n } = useTranslation('setting');
|
const { t, i18n } = useTranslation('setting');
|
||||||
const [devices, setDevices] = useState<Device[]>([]);
|
const [devices, setDevices] = useState<Device[]>([]);
|
||||||
const [loadingDeleteIds, setLoadingDeleteIds] = useState<string[]>([]);
|
const [loadingDeleteIds, setLoadingDeleteIds] = useState<string[]>([]);
|
||||||
const theme = useTheme();
|
|
||||||
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
|
||||||
const showToast = useToast();
|
const showToast = useToast();
|
||||||
|
|
||||||
const { isLoadingProfile, refetchProfile } = useProfile();
|
const { isLoadingProfile, refetchProfile } = useProfile();
|
||||||
@@ -46,7 +43,8 @@ export function ActiveDevicesPage() {
|
|||||||
const formattedDevices = sessions.map((session: ApiSession) => ({
|
const formattedDevices = sessions.map((session: ApiSession) => ({
|
||||||
id: session.key,
|
id: session.key,
|
||||||
timeAndDate: formatDate(session.created, i18n.language, t),
|
timeAndDate: formatDate(session.created, i18n.language, t),
|
||||||
deviceModel: `${session.deviceOs} ${session.deviceName}`,
|
deviceOs: session.deviceOs,
|
||||||
|
deviceName: session.deviceName,
|
||||||
ip: session.ipAddress,
|
ip: session.ipAddress,
|
||||||
current: session.key === currentKey,
|
current: session.key === currentKey,
|
||||||
}));
|
}));
|
||||||
@@ -132,7 +130,9 @@ export function ActiveDevicesPage() {
|
|||||||
flexGrow: 0,
|
flexGrow: 0,
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
}}
|
}}
|
||||||
disabled={isLoadingProfile || isTerminating}
|
disabled={
|
||||||
|
isLoadingProfile || isTerminating || devices.length === 1
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isTerminating
|
{isTerminating
|
||||||
? t('active.deleting')
|
? t('active.deleting')
|
||||||
@@ -158,20 +158,23 @@ export function ActiveDevicesPage() {
|
|||||||
sx={{
|
sx={{
|
||||||
mx: { xs: 2, sm: 3, md: 4 },
|
mx: { xs: 2, sm: 3, md: 4 },
|
||||||
py: 2,
|
py: 2,
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: 2,
|
|
||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{devices.map((device) => (
|
{devices.map((device) => (
|
||||||
<React.Fragment key={device.id}>
|
<Stack
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
flexDirection="row"
|
||||||
|
key={device.id}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: { xs: 'column', sm: 'row' },
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
alignItems: { xs: 'flex-start', sm: 'center' },
|
alignItems: { xs: 'flex-start', sm: 'center' },
|
||||||
minHeight: 50,
|
minHeight: 50,
|
||||||
|
gap: 0.5,
|
||||||
py: 1.5,
|
py: 1.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -197,12 +200,16 @@ export function ActiveDevicesPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
Component={DeviceMessage}
|
Component={
|
||||||
|
device.deviceName === 'smartphone'
|
||||||
|
? Mobile
|
||||||
|
: DeviceMessage
|
||||||
|
}
|
||||||
size="medium"
|
size="medium"
|
||||||
color="primary.main"
|
color="primary.main"
|
||||||
/>
|
/>
|
||||||
<Typography variant="body2" noWrap>
|
<Typography variant="body2" noWrap>
|
||||||
{device.deviceModel}
|
{`${device.deviceOs} ${device.deviceName}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -217,85 +224,55 @@ export function ActiveDevicesPage() {
|
|||||||
>
|
>
|
||||||
{device.ip}
|
{device.ip}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
flexBasis: { xs: '100%', sm: 'auto' },
|
|
||||||
mb: { xs: 1, sm: 0 },
|
|
||||||
minWidth: { sm: '126px' },
|
|
||||||
order: { xs: 4, sm: 4 },
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{device.current && (
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
size="medium"
|
|
||||||
sx={{
|
|
||||||
borderRadius: 12.5,
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'success.main',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
color: 'success.main',
|
|
||||||
textTransform: 'none',
|
|
||||||
maxWidth: '125px',
|
|
||||||
'&.Mui-disabled': {
|
|
||||||
color: 'success.main',
|
|
||||||
borderColor: 'success.main',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
{t('active.currentDevice')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
flexBasis: { xs: '100%', sm: 'auto' },
|
|
||||||
mb: { xs: 1, sm: 0 },
|
|
||||||
textAlign: { xs: 'left', sm: 'center' },
|
|
||||||
minWidth: { sm: '138px' },
|
|
||||||
order: { xs: 5, sm: 5 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<Icon Component={Logout} size="small" />}
|
|
||||||
disabled={
|
|
||||||
device.current || loadingDeleteIds.includes(device.id)
|
|
||||||
}
|
|
||||||
onClick={() => handleDeleteDevice(device.id)}
|
|
||||||
sx={{
|
|
||||||
color: 'error.main',
|
|
||||||
borderRadius: 1,
|
|
||||||
borderColor: 'error.main',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
textTransform: 'none',
|
|
||||||
'& .MuiButton-startIcon': {
|
|
||||||
marginRight: 0.5,
|
|
||||||
marginLeft: 0,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loadingDeleteIds.includes(device.id) ? (
|
|
||||||
<CircularProgress size={20} color="error" />
|
|
||||||
) : (
|
|
||||||
t('active.deleteDevice')
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
{isXsup && (
|
{device.current && (
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }} />
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="medium"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 12.5,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'success.main',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
color: 'success.main',
|
||||||
|
textTransform: 'none',
|
||||||
|
maxWidth: '125px',
|
||||||
|
'&.Mui-disabled': {
|
||||||
|
color: 'success.main',
|
||||||
|
borderColor: 'success.main',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
{t('active.currentDevice')}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
|
||||||
|
{!device.current && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<Icon Component={Logout} size="small" />}
|
||||||
|
disabled={
|
||||||
|
device.current || loadingDeleteIds.includes(device.id)
|
||||||
|
}
|
||||||
|
loading={loadingDeleteIds.includes(device.id)}
|
||||||
|
onClick={() => handleDeleteDevice(device.id)}
|
||||||
|
fullWidth={false}
|
||||||
|
sx={{
|
||||||
|
color: 'error.main',
|
||||||
|
borderRadius: 1,
|
||||||
|
borderColor: 'error.main',
|
||||||
|
'& .MuiButton-startIcon': {
|
||||||
|
marginRight: 0.5,
|
||||||
|
marginLeft: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('active.deleteDevice')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { saveSettings } from '../api/settingsApi';
|
|||||||
import { useProfile } from '../hooks/useProfile';
|
import { useProfile } from '../hooks/useProfile';
|
||||||
|
|
||||||
type ThemeMode = 'light' | 'dark';
|
type ThemeMode = 'light' | 'dark';
|
||||||
type CalendarType = 'christian' | 'solar' | 'lunar';
|
type CalendarType = 'christian' | 'jalali';
|
||||||
|
|
||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
language: string;
|
language: string;
|
||||||
@@ -33,9 +33,8 @@ const languageOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const calendarOptions: { key: CalendarType; apiValue: number }[] = [
|
const calendarOptions: { key: CalendarType; apiValue: number }[] = [
|
||||||
{ key: 'christian', apiValue: 1 },
|
{ key: 'jalali', apiValue: 1 },
|
||||||
{ key: 'solar', apiValue: 2 },
|
{ key: 'christian', apiValue: 2 },
|
||||||
{ key: 'lunar', apiValue: 3 },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const themeApiMap: Record<ThemeMode, number> = { light: 1, dark: 2 };
|
const themeApiMap: Record<ThemeMode, number> = { light: 1, dark: 2 };
|
||||||
@@ -46,7 +45,7 @@ export function SettingPage() {
|
|||||||
|
|
||||||
const [savedSettings, setSavedSettings] = useState<SettingsState>({
|
const [savedSettings, setSavedSettings] = useState<SettingsState>({
|
||||||
language: i18n.language,
|
language: i18n.language,
|
||||||
calendar: 'solar',
|
calendar: 'jalali',
|
||||||
theme: mode === 'light' || mode === 'dark' ? mode : 'light',
|
theme: mode === 'light' || mode === 'dark' ? mode : 'light',
|
||||||
});
|
});
|
||||||
const [draftSettings, setDraftSettings] =
|
const [draftSettings, setDraftSettings] =
|
||||||
@@ -78,9 +77,9 @@ export function SettingPage() {
|
|||||||
theme: themeReverseMap[theme] || 'light',
|
theme: themeReverseMap[theme] || 'light',
|
||||||
calendar:
|
calendar:
|
||||||
calendarOptions.find((c) => c.apiValue === calendarType)?.key ||
|
calendarOptions.find((c) => c.apiValue === calendarType)?.key ||
|
||||||
'solar',
|
'jalali',
|
||||||
language:
|
language:
|
||||||
languageOptions.find((l) => l.apiValue === language)?.code || 'en',
|
languageOptions.find((l) => l.apiValue === language)?.code || 'fa',
|
||||||
};
|
};
|
||||||
setSavedSettings(newSettings);
|
setSavedSettings(newSettings);
|
||||||
setDraftSettings(newSettings);
|
setDraftSettings(newSettings);
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export interface InfoRowData {
|
|||||||
export interface Device {
|
export interface Device {
|
||||||
id: string;
|
id: string;
|
||||||
timeAndDate: string;
|
timeAndDate: string;
|
||||||
deviceModel: string;
|
deviceName: 'smartphone' | 'desktop' | string;
|
||||||
|
deviceOs: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
current: boolean;
|
current: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Suspense, type ReactNode } from 'react';
|
import { type ReactNode } from 'react';
|
||||||
import { createBrowserRouter, type RouteObject } from 'react-router-dom';
|
import { createBrowserRouter, type RouteObject } from 'react-router-dom';
|
||||||
import { appRoutes, type RouteConfig } from './config';
|
import { appRoutes, type RouteConfig } from './config';
|
||||||
import { ProtectedRoute } from '@/components/routes/ProtectedRoute';
|
import { ProtectedRoute } from '@/components/routes/ProtectedRoute';
|
||||||
import { Loading } from '@/components/routes/Loading';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A recursive function to map our custom route config to the format
|
* A recursive function to map our custom route config to the format
|
||||||
@@ -10,15 +9,9 @@ import { Loading } from '@/components/routes/Loading';
|
|||||||
*/
|
*/
|
||||||
function mapRoutes(routes: RouteConfig[]): RouteObject[] {
|
function mapRoutes(routes: RouteConfig[]): RouteObject[] {
|
||||||
return routes.map((route) => {
|
return routes.map((route) => {
|
||||||
// Start with the base element, wrapped in Suspense for lazy loading
|
// Remove the suspense from here and move to layout outlet
|
||||||
let element: ReactNode = (
|
// Avoid loading layout for rendering different children
|
||||||
<Suspense fallback={<Loading />}>{route.element}</Suspense>
|
let element: ReactNode = route.element;
|
||||||
);
|
|
||||||
|
|
||||||
// Conditionally wrap the element in the specified layout
|
|
||||||
// if (route.layout) {
|
|
||||||
// element = <route.layout>{element}</route.layout>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (route.authorize) {
|
if (route.authorize) {
|
||||||
element = <ProtectedRoute>{element}</ProtectedRoute>;
|
element = <ProtectedRoute>{element}</ProtectedRoute>;
|
||||||
|
|||||||
Reference in New Issue
Block a user