Merge pull request #42 from rkheftan/fix/profile-bugs

Fix/profile bugs
This commit is contained in:
SajadMRjl
2025-09-29 19:38:02 +03:30
committed by GitHub
22 changed files with 276 additions and 244 deletions

View File

@@ -2,7 +2,7 @@
"loginForm": {
"title": "Login/Register",
"description": "Please enter your email/password to start",
"emailOrPhoneLabel": "Email/Password",
"emailOrPhoneLabel": "Email/Phone number",
"submitButton": "Login/Register",
"loginWithGoogle": "Login with google",
"emailIsInvalid": "Email is invalid",
@@ -23,6 +23,7 @@
"moreMinute": "minute",
"resendCode": "Resend code",
"confirmAndLogin": "Confirm & login",
"confirmAndContinue": "Confirm & continue",
"loginWithPassword": "Login with password"
},
"completeSignUp": {

View File

@@ -102,8 +102,7 @@
"dark": "Dark",
"language": "زبان/language",
"calendar": "Calendar and date format",
"solar": "Solar",
"lunar": "Lunar",
"jalali": "jalali",
"christian": "Christian",
"iran": "Iran",
"saving": "Saving...",

View File

@@ -102,8 +102,7 @@
"dark": "تاریک",
"language": "زبان/language",
"calendar": "فرمت تقویم و تاریخ",
"solar": "شمسی",
"lunar": "قمری",
"jalali": "شمسی",
"christian": "میلادی",
"iran": "ایران",
"saving": "در حال ذخیره‌سازی...",

View File

@@ -8,6 +8,7 @@ import React, {
} from 'react';
import { TextField, Stack } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
interface DigitInputProps {
error: boolean;
@@ -34,6 +35,8 @@ const DigitInput: React.FC<DigitInputProps> = ({
};
const handleChange = (value: string, index: number) => {
value = replacePersianWithRealNumbers(value);
if (!/^\d$/.test(value) && value !== '') return;
const newCode = [...code];
@@ -91,6 +94,7 @@ const DigitInput: React.FC<DigitInputProps> = ({
color={success ? 'success' : 'primary'}
key={index}
inputRef={(el) => (inputRefs.current[index] = el)}
autoFocus={index === 0}
value={digit}
onChange={(e) => handleChange(e.target.value, index)}
onKeyDown={(e) => e.key === 'Backspace' && handleBackspace(e, index)}
@@ -115,7 +119,7 @@ const DigitInput: React.FC<DigitInputProps> = ({
variant="standard"
size="medium"
sx={{
width: '83px',
width: { md: '83px', xs: '20%' },
}}
/>
))}

View File

@@ -1,4 +1,5 @@
import {
Avatar,
Box,
IconButton,
ListItemIcon,
@@ -9,23 +10,19 @@ import {
} from '@mui/material';
import { Icon } from '@rkheftan/harmony-ui';
import { Logout, More } from 'iconsax-react';
import type { UserInfo } from '@/contexts/AuthContext';
import { LTRTypography } from '../common/LTRTypography';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAuth } from '@/hooks/useAuth';
import { FlexBox } from '../common/FlexBox';
interface HeaderProps {
user: UserInfo;
}
export const Header: React.FC<HeaderProps> = ({ user }) => {
export const Header: React.FC = () => {
const { t, i18n } = useTranslation('sideNav');
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const { logout } = useAuth();
const { logout, userInfo: user } = useAuth();
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation();
@@ -52,15 +49,26 @@ export const Header: React.FC<HeaderProps> = ({ user }) => {
height: (t) => t.spacing(10.5),
}}
>
<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 sx={{ alignItems: 'center', gap: 1 }}>
<Avatar
sx={{ width: 32, height: 32, fontSize: '14px' }}
src={
user.picture &&
import.meta.env.VITE_IMAGE_BASE_URL + '/' + user.picture
}
>
{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">
<Icon Component={More} />

View File

@@ -4,9 +4,10 @@ import { appRoutes } from '@/routes/config';
import { Outlet, useLocation } from 'react-router-dom';
import { Box, useMediaQuery, useTheme } from '@mui/material';
import { Header } from './Header';
import { useState } from 'react';
import { Suspense, useState } from 'react';
import { Toolbar } from './Toolbar';
import { useAuth } from '@/hooks/useAuth';
import { Loading } from '../routes/Loading';
export const Layout = () => {
const navItemConfigs = buildNavItems(appRoutes);
@@ -53,7 +54,9 @@ export const Layout = () => {
overflowInline: 'auto',
}}
>
<Outlet />
<Suspense fallback={<Loading />}>
<Outlet />
</Suspense>
</Box>
</Box>

View File

@@ -63,21 +63,9 @@ export const Toolbar: React.FC<ToolbarProps> = ({
)}
<Logo />
</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}>
<Icon Component={Menu} variant="Bold" />
</IconButton>
</Box>
<ProductsMenu anchorEl={anchorEl} onClose={handleClose} open={open} />
</MuiToolbar>
);

View File

@@ -95,9 +95,10 @@ export const AuthenticationSteps = (): JSX.Element => {
tokenResponse: GenerateTokenResponse,
) => {
setMemoryTokenRes(tokenResponse);
if (authFactory.isCurrentApplication()) {
login(tokenResponse);
}
// TODO: For now both application scopes can have their tokens
// later we need to discuss this base on business plan and change it
// if (authFactory.isCurrentApplication()) {}
login(tokenResponse);
if (loginResult.registeredWithOutPhoneNumber) {
setCurrentStep('addPhoneNumber');

View File

@@ -1,12 +1,13 @@
import { Box, Button, TextField, Typography } from '@mui/material';
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 { AuthenticationCard } from '../AuthenticationCard';
import { CountryCodeSelector } from '../../../../components/CountryCodeSelector';
import { sendSmsOtp } from '../../api/authorizationAPI';
import type { CountryCode } from '@/types/commonTypes';
import { useApi } from '@/hooks/useApi';
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
export interface CompleteSignUpProps {
email: string;
@@ -25,7 +26,7 @@ export const CompleteSignUp = ({
setCountryCode,
onCompleteSignUp,
}: CompleteSignUpProps) => {
const { t } = useTranslation('authentication');
const { t, i18n } = useTranslation('authentication');
const [error, setError] = useState<string>();
const textFieldRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
@@ -45,6 +46,12 @@ export const CompleteSignUp = ({
handleValueError();
};
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const value = replacePersianWithRealNumbers(event.target.value);
setValue(value);
};
const handleValueError = () => {
if (!value) {
setError(t('loginForm.thisFieldIsRequired'));
@@ -62,8 +69,13 @@ export const CompleteSignUp = ({
if (!value || !isPhoneValid(countryCode, value)) {
inputRef.current?.focus();
} else {
await sendSmsCall({ phoneNumber: countryCode + value });
onCompleteSignUp(countryCode, value);
let newValue = 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}
label={t('completeSignUp.phoneNumber')}
value={value}
onChange={(e) => setValue(e.target.value)}
onChange={handleChange}
onBlur={handleBlur}
error={inputError}
helperText={inputError ? error : ''}
@@ -101,7 +113,16 @@ export const CompleteSignUp = ({
slotProps={{
htmlInput: { dir: 'auto', sx: { lineHeight: 1.5 } },
input: {
endAdornment: (
endAdornment: i18n.dir() === 'rtl' && (
<CountryCodeSelector
value={countryCode}
onChange={setCountryCode}
show={true}
menuAnchor={textFieldRef.current}
onCloseFocusRef={inputRef}
/>
),
startAdornment: i18n.dir() === 'ltr' && (
<CountryCodeSelector
value={countryCode}
onChange={setCountryCode}

View File

@@ -67,9 +67,6 @@ export function LoginRegisterForm({
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
let newValue = event.target.value;
newValue = replacePersianWithRealNumbers(newValue);
if (newValue.startsWith('09')) {
newValue = newValue.substring(1);
}
setLoginRegisterValue(newValue);
@@ -112,10 +109,20 @@ export function LoginRegisterForm({
const handleSubmit = async () => {
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({
phoneNumber:
authType === 'phone' ? countryCode + loginRegisterValue : undefined,
email: authType === 'email' ? loginRegisterValue : undefined,
phoneNumber: authType === 'phone' ? countryCode + newValue : undefined,
email: authType === 'email' ? newValue : undefined,
});
if (!res) {
@@ -123,7 +130,7 @@ export function LoginRegisterForm({
}
if (res.success) {
onLoginRegisterSubmit(loginRegisterValue, res.userStatus);
onLoginRegisterSubmit(newValue, res.userStatus);
} else {
toast({ message: res.message, severity: 'error' });
}

View File

@@ -191,7 +191,6 @@ export function OtpVerifyForm({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 4,
mb: 0.5,
}}
>

View File

@@ -24,7 +24,7 @@ import { useApi } from '@/hooks/useApi';
export interface ChangePasswordProps {
onEditInfo: () => void;
onPasswordChanged: () => void;
forgettedPasswordInfo: string;
forgetPasswordInfo: string;
infoType: AuthType;
countryCode: CountryCode;
}
@@ -32,7 +32,7 @@ export interface ChangePasswordProps {
export const ChangePassword = ({
onEditInfo,
onPasswordChanged,
forgettedPasswordInfo,
forgetPasswordInfo,
infoType,
countryCode,
}: ChangePasswordProps) => {
@@ -78,11 +78,9 @@ export const ChangePassword = ({
confirmInputRef.current?.focus();
} else {
const apiRequest: ResetPasswordRequest = {
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
email: infoType === 'email' ? forgetPasswordInfo : undefined,
phoneNumber:
infoType === 'phone'
? countryCode + forgettedPasswordInfo
: undefined,
infoType === 'phone' ? countryCode + forgetPasswordInfo : undefined,
newPassword: passValue,
confirmNewPassword: confirmPassValue,
};
@@ -138,7 +136,7 @@ export const ChangePassword = ({
endIcon={<Icon Component={Edit2} />}
onClick={onEditInfo}
>
{forgettedPasswordInfo}
{forgetPasswordInfo}
</Button>
</Box>

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import type { AuthType } from '../../types/authTypes';
import { ForgettedPasswordInfo } from './ForgettedPasswordInfo';
import { ForgetPasswordInfo } from './ForgetPasswordInfo';
import { ForgetPasswordOtp } from './ForgetPasswordOtp';
import { ChangePassword } from './ChangePassword';
import type { CountryCode } from '@/types/commonTypes';
@@ -11,8 +11,7 @@ export const ForgetPasswordContainer = () => {
const [forgetPassCurrentStep, setForgetPassCurrentStep] = useState<
'enterInfo' | 'verifyOtp' | 'setPassword'
>('enterInfo');
const [forgettedPasswordInfo, setForgettedPasswordInfo] =
useState<string>('');
const [forgetPasswordInfo, setForgetPasswordInfo] = useState<string>('');
const [infoCountryCode, setInfoCountryCode] = useState<CountryCode>('+98');
const [infoType, setInfoType] = useState<AuthType>('email');
@@ -35,11 +34,11 @@ export const ForgetPasswordContainer = () => {
return (
<>
{forgetPassCurrentStep === 'enterInfo' && (
<ForgettedPasswordInfo
<ForgetPasswordInfo
infoType={infoType}
setInfoType={setInfoType}
forgettedPasswordInfo={forgettedPasswordInfo}
setForgettedPasswordInfo={setForgettedPasswordInfo}
forgetPasswordInfo={forgetPasswordInfo}
setForgetPasswordInfo={setForgetPasswordInfo}
onVerifyOtp={handleVerifyOtp}
countryCode={infoCountryCode}
setCountryCode={setInfoCountryCode}
@@ -51,7 +50,7 @@ export const ForgetPasswordContainer = () => {
countryCode={infoCountryCode}
infoType={infoType}
onEditInfo={handleEditInfo}
forgettedPasswordInfo={forgettedPasswordInfo}
forgetPasswordInfo={forgetPasswordInfo}
onOTPVerified={handleOtpVerified}
/>
)}
@@ -59,7 +58,7 @@ export const ForgetPasswordContainer = () => {
{forgetPassCurrentStep === 'setPassword' && (
<ChangePassword
onEditInfo={handleEditInfo}
forgettedPasswordInfo={forgettedPasswordInfo}
forgetPasswordInfo={forgetPasswordInfo}
onPasswordChanged={handlePasswordChanged}
infoType={infoType}
countryCode={infoCountryCode}

View File

@@ -14,9 +14,9 @@ import { useToast } from '@rkheftan/harmony-ui';
import { useApi } from '@/hooks/useApi';
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
export interface ForgettedPasswordInfoProps {
forgettedPasswordInfo: string;
setForgettedPasswordInfo: Dispatch<string>;
export interface ForgetPasswordInfoProps {
forgetPasswordInfo: string;
setForgetPasswordInfo: Dispatch<string>;
infoType: AuthType;
setInfoType: Dispatch<AuthType>;
onVerifyOtp: (value: string) => void;
@@ -24,15 +24,15 @@ export interface ForgettedPasswordInfoProps {
setCountryCode: Dispatch<CountryCode>;
}
export function ForgettedPasswordInfo({
forgettedPasswordInfo,
setForgettedPasswordInfo,
export function ForgetPasswordInfo({
forgetPasswordInfo,
setForgetPasswordInfo,
infoType,
setInfoType,
onVerifyOtp,
countryCode,
setCountryCode,
}: ForgettedPasswordInfoProps) {
}: ForgetPasswordInfoProps) {
const { t } = useTranslation('authentication');
const textFieldRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
@@ -51,7 +51,7 @@ export function ForgettedPasswordInfo({
if (newValue.startsWith('09')) {
newValue = newValue.substring(1);
}
setForgettedPasswordInfo(newValue);
setForgetPasswordInfo(newValue);
// If the new value contains only digits (or is empty), it's a phone number
if (isNumeric(newValue)) {
@@ -63,7 +63,7 @@ export function ForgettedPasswordInfo({
const handleBlur = () => {
setTouched(true);
validateInput(forgettedPasswordInfo, infoType);
validateInput(forgetPasswordInfo, infoType);
};
const validateInput = (
@@ -87,13 +87,11 @@ export function ForgettedPasswordInfo({
};
const handleSubmit = async () => {
if (validateInput(forgettedPasswordInfo, infoType, false)) {
if (validateInput(forgetPasswordInfo, infoType, false)) {
const sendCodeRequest: SendForgetPassCodeRequest = {
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
email: infoType === 'email' ? forgetPasswordInfo : undefined,
phoneNumber:
infoType === 'phone'
? countryCode + forgettedPasswordInfo
: undefined,
infoType === 'phone' ? countryCode + forgetPasswordInfo : undefined,
};
const res = await sendForgetPassCodeCall(sendCodeRequest);
@@ -106,19 +104,18 @@ export function ForgettedPasswordInfo({
});
}
onVerifyOtp(forgettedPasswordInfo);
onVerifyOtp(forgetPasswordInfo);
} else {
inputRef.current?.focus();
validateInput(forgettedPasswordInfo, infoType);
validateInput(forgetPasswordInfo, infoType);
}
};
const showAdornment =
infoType === 'phone' && forgettedPasswordInfo.length > 0;
const showAdornment = infoType === 'phone' && forgetPasswordInfo.length > 0;
return (
<AuthenticationCard>
<Stack spacing={1}>
<Stack component="form" onSubmit={handleSubmit} spacing={1}>
<Typography variant="h5">
{t('forgetPassword.forgetPassword')}
</Typography>
@@ -133,7 +130,7 @@ export function ForgettedPasswordInfo({
ref={textFieldRef}
inputRef={inputRef}
label={t('loginForm.emailOrPhoneLabel')}
value={forgettedPasswordInfo}
value={forgetPasswordInfo}
onChange={handleInputChange}
onBlur={handleBlur}
error={inputError}
@@ -157,7 +154,7 @@ export function ForgettedPasswordInfo({
/>
<Stack spacing={2}>
<Button loading={sendForgetPassCodeLoading} onClick={handleSubmit}>
<Button loading={sendForgetPassCodeLoading} type="submit">
{t('forgetPassword.confirm')}
</Button>
</Stack>

View File

@@ -18,7 +18,7 @@ import { Icon, useToast } from '@rkheftan/harmony-ui';
import { useApi } from '@/hooks/useApi';
interface ForgetPasswordOtpProps {
forgettedPasswordInfo: string;
forgetPasswordInfo: string;
infoType: AuthType;
countryCode: CountryCode;
onEditInfo: () => void;
@@ -26,7 +26,7 @@ interface ForgetPasswordOtpProps {
}
export function ForgetPasswordOtp({
forgettedPasswordInfo,
forgetPasswordInfo,
infoType,
countryCode,
onEditInfo,
@@ -69,9 +69,9 @@ export function ForgetPasswordOtp({
const handleResendOTPCode = async () => {
const sendCodeRequest: SendForgetPassCodeRequest = {
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
email: infoType === 'email' ? forgetPasswordInfo : undefined,
phoneNumber:
infoType === 'phone' ? countryCode + forgettedPasswordInfo : undefined,
infoType === 'phone' ? countryCode + forgetPasswordInfo : undefined,
};
await sendForgetPassCodeCall(sendCodeRequest);
@@ -93,11 +93,9 @@ export function ForgetPasswordOtp({
// Change setTimeout to api call
const apiRequest: ConfirmForgetPassCodeRequest = {
email: infoType === 'email' ? forgettedPasswordInfo : undefined,
email: infoType === 'email' ? forgetPasswordInfo : undefined,
phoneNumber:
infoType === 'phone'
? countryCode + forgettedPasswordInfo
: undefined,
infoType === 'phone' ? countryCode + forgetPasswordInfo : undefined,
code: otpCode,
};
@@ -153,8 +151,8 @@ export function ForgetPasswordOtp({
onClick={onEditInfo}
>
{infoType === 'phone'
? countryCode + forgettedPasswordInfo
: forgettedPasswordInfo}
? countryCode + forgetPasswordInfo
: forgetPasswordInfo}
</Button>
</Box>

View File

@@ -118,6 +118,7 @@ export function PhoneNumber() {
setPhoneNumberTouched(false);
setVerificationCodeError(undefined);
setVerificationCodeTouched(false);
setIsCodeSent(false);
}
return !prev;
});

View File

@@ -11,10 +11,11 @@ import { Edit2, TickCircle } from 'iconsax-react';
import { CountDownTimer } from '@/components/CountDownTimer';
import { Icon } from '@rkheftan/harmony-ui';
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 { CountryCodeSelector } from '@/components/CountryCodeSelector';
import { LTRTypography } from '@/components/common/LTRTypography';
import { replacePersianWithRealNumbers } from '@/utils/replacePersianWithRealNumbers';
export default function PhoneEditForm({
phoneNumber,
@@ -42,6 +43,20 @@ export default function PhoneEditForm({
const textFieldRef = useRef<HTMLDivElement>(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 (
<Box sx={{ px: { sm: 4, xs: 2 }, my: 4 }}>
<Box sx={{ mb: 4 }}>
@@ -70,6 +85,7 @@ export default function PhoneEditForm({
>
<TextField
fullWidth
autoFocus
name="phoneNumber"
label={t('settingForm.newPhoneNumber')}
disabled={isCodeSent}
@@ -80,30 +96,12 @@ export default function PhoneEditForm({
onBlur={() => handleBlur('phoneNumber')}
error={!!phoneNumberError}
helperText={phoneNumberError}
onChange={(e) => setPhoneNumber(e.target.value)}
onChange={handleChange}
placeholder="09123456789"
slotProps={{
input: {
endAdornment: 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>
) : (
i18n.dir() === 'rtl' && (
endAdornment:
i18n.dir() === 'rtl' ? (
<CountryCodeSelector
value={countryCode}
onChange={setCountryCode}
@@ -112,18 +110,58 @@ export default function PhoneEditForm({
menuAnchor={textFieldRef.current}
onCloseFocusRef={inputRef}
/>
)
),
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>
)
),
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
variant="text"
loading={isSendingCode}
onClick={handleSendCode}
onClick={onSendCode}
disabled={buttonState === 'counting'}
sx={{
color: 'primary.main',
width: { xs: '100%', sm: 208 },

View File

@@ -1,14 +1,13 @@
import React, { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Button,
useTheme,
useMediaQuery,
CircularProgress,
Stack,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { DeviceMessage, Logout } from 'iconsax-react';
import { DeviceMessage, Logout, Mobile } from 'iconsax-react';
import { CardContainer } from '@/components/CardContainer';
import { PageWrapper } from '../components/PageWrapper';
import { Icon } from '@rkheftan/harmony-ui';
@@ -24,8 +23,6 @@ export function ActiveDevicesPage() {
const { t, i18n } = useTranslation('setting');
const [devices, setDevices] = useState<Device[]>([]);
const [loadingDeleteIds, setLoadingDeleteIds] = useState<string[]>([]);
const theme = useTheme();
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
const showToast = useToast();
const { isLoadingProfile, refetchProfile } = useProfile();
@@ -46,7 +43,8 @@ export function ActiveDevicesPage() {
const formattedDevices = sessions.map((session: ApiSession) => ({
id: session.key,
timeAndDate: formatDate(session.created, i18n.language, t),
deviceModel: `${session.deviceOs} ${session.deviceName}`,
deviceOs: session.deviceOs,
deviceName: session.deviceName,
ip: session.ipAddress,
current: session.key === currentKey,
}));
@@ -132,7 +130,9 @@ export function ActiveDevicesPage() {
flexGrow: 0,
width: 'auto',
}}
disabled={isLoadingProfile || isTerminating}
disabled={
isLoadingProfile || isTerminating || devices.length === 1
}
>
{isTerminating
? t('active.deleting')
@@ -158,20 +158,23 @@ export function ActiveDevicesPage() {
sx={{
mx: { xs: 2, sm: 3, md: 4 },
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
bgcolor: 'background.paper',
}}
>
{devices.map((device) => (
<React.Fragment key={device.id}>
<Stack
alignItems="center"
justifyContent="space-between"
flexDirection="row"
key={device.id}
>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
alignItems: { xs: 'flex-start', sm: 'center' },
minHeight: 50,
gap: 0.5,
py: 1.5,
}}
>
@@ -197,12 +200,16 @@ export function ActiveDevicesPage() {
}}
>
<Icon
Component={DeviceMessage}
Component={
device.deviceName === 'smartphone'
? Mobile
: DeviceMessage
}
size="medium"
color="primary.main"
/>
<Typography variant="body2" noWrap>
{device.deviceModel}
{`${device.deviceOs} ${device.deviceName}`}
</Typography>
</Box>
@@ -217,85 +224,55 @@ export function ActiveDevicesPage() {
>
{device.ip}
</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>
{isXsup && (
<Box sx={{ borderBottom: 1, borderColor: 'divider' }} />
{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>
)}
</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>
)}

View File

@@ -19,7 +19,7 @@ import { saveSettings } from '../api/settingsApi';
import { useProfile } from '../hooks/useProfile';
type ThemeMode = 'light' | 'dark';
type CalendarType = 'christian' | 'solar' | 'lunar';
type CalendarType = 'christian' | 'jalali';
interface SettingsState {
language: string;
@@ -33,9 +33,8 @@ const languageOptions = [
];
const calendarOptions: { key: CalendarType; apiValue: number }[] = [
{ key: 'christian', apiValue: 1 },
{ key: 'solar', apiValue: 2 },
{ key: 'lunar', apiValue: 3 },
{ key: 'jalali', apiValue: 1 },
{ key: 'christian', apiValue: 2 },
];
const themeApiMap: Record<ThemeMode, number> = { light: 1, dark: 2 };
@@ -46,7 +45,7 @@ export function SettingPage() {
const [savedSettings, setSavedSettings] = useState<SettingsState>({
language: i18n.language,
calendar: 'solar',
calendar: 'jalali',
theme: mode === 'light' || mode === 'dark' ? mode : 'light',
});
const [draftSettings, setDraftSettings] =
@@ -78,9 +77,9 @@ export function SettingPage() {
theme: themeReverseMap[theme] || 'light',
calendar:
calendarOptions.find((c) => c.apiValue === calendarType)?.key ||
'solar',
'jalali',
language:
languageOptions.find((l) => l.apiValue === language)?.code || 'en',
languageOptions.find((l) => l.apiValue === language)?.code || 'fa',
};
setSavedSettings(newSettings);
setDraftSettings(newSettings);

View File

@@ -19,7 +19,8 @@ export interface InfoRowData {
export interface Device {
id: string;
timeAndDate: string;
deviceModel: string;
deviceName: 'smartphone' | 'desktop' | string;
deviceOs: string;
ip: string;
current: boolean;
}

View File

@@ -1,8 +1,7 @@
import { Suspense, type ReactNode } from 'react';
import { type ReactNode } from 'react';
import { createBrowserRouter, type RouteObject } from 'react-router-dom';
import { appRoutes, type RouteConfig } from './config';
import { ProtectedRoute } from '@/components/routes/ProtectedRoute';
import { Loading } from '@/components/routes/Loading';
/**
* 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[] {
return routes.map((route) => {
// Start with the base element, wrapped in Suspense for lazy loading
let element: ReactNode = (
<Suspense fallback={<Loading />}>{route.element}</Suspense>
);
// Conditionally wrap the element in the specified layout
// if (route.layout) {
// element = <route.layout>{element}</route.layout>;
// }
// Remove the suspense from here and move to layout outlet
// Avoid loading layout for rendering different children
let element: ReactNode = route.element;
if (route.authorize) {
element = <ProtectedRoute>{element}</ProtectedRoute>;