chore: optimization of code

This commit is contained in:
Koosha Lahouti
2025-08-09 16:22:15 -07:00
parent f23a8a9fca
commit 57959f39ce
28 changed files with 2586 additions and 1251 deletions

View File

@@ -0,0 +1,254 @@
import {
Box,
InputAdornment,
ListItem,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
TextField,
Typography,
} from '@mui/material';
import { useMemo, useRef, useState, type RefObject } from 'react';
import { ArrowDown2 } from 'iconsax-react';
import ReactCountryFlag from 'react-country-flag';
import { useTranslation } from 'react-i18next';
import { countries, type Country } from '../data/countries';
interface CountryCodeSelectorProps {
show: boolean;
value: string;
onChange: (newValue: string) => void;
menuAnchor: HTMLElement | null;
onCloseFocusRef: RefObject<HTMLInputElement | null>;
}
/**
* An animated country code adornment that fades and slides into view.
* Its visibility is controlled by the `show` prop.
*/
export function CountryCodeSelector({
show,
value,
onChange,
menuAnchor,
onCloseFocusRef,
}: CountryCodeSelectorProps) {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [searchTerm, setSearchTerm] = useState('');
const open = Boolean(anchorEl);
const searchInputRef = useRef<HTMLInputElement>(null);
const menuWidth = menuAnchor ? menuAnchor.clientWidth : 'auto';
const { t, i18n } = useTranslation();
const selectedCountry =
countries.find((c) => c.phone === value) || countries[0];
const handleClick = () => {
setAnchorEl(menuAnchor);
};
const handleClose = () => {
setTimeout(() => {
setAnchorEl(null);
}, 0);
setTimeout(() => {
onCloseFocusRef.current?.focus();
}, 100);
setSearchTerm(''); // Reset search on close
};
const handleSelect = (country: Country) => {
onChange(country.phone);
handleClose();
};
const handleMenuEntered = () => {
// Focus the input field after the menu has finished opening
searchInputRef.current?.focus();
};
const filteredCountries = useMemo(
() =>
countries.filter(
(country) =>
t(country.label).toLowerCase().includes(searchTerm.toLowerCase()) ||
country.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
country.phone.includes(searchTerm),
),
[searchTerm],
);
return (
<InputAdornment
position={i18n.dir() === 'rtl' ? 'start' : 'end'}
sx={{
mx: 0,
}}
>
<Box
onClick={handleClick}
sx={{
// Animate width and opacity based on the 'show' prop
width: show ? 'auto' : 0,
opacity: show ? 1 : 0,
transition: (theme) =>
theme.transitions.create(['width', 'opacity'], {
duration: theme.transitions.duration.standard,
}),
// Prevent content from wrapping or spilling out during animation
overflow: 'hidden',
whiteSpace: 'nowrap',
// layout styles
height: '100%',
display: 'flex',
alignItems: 'center',
gap: 0.25,
pl: show ? 0.25 : 0,
'&:hover': {
cursor: 'pointer',
},
}}
>
{/* This inner Box prevents the content from being squeezed during the transition */}
<ArrowDown2 size="24" variant="Bold" />
<Typography
variant="body1"
color="text.primary"
component="div"
sx={{ direction: 'rtl' }} // TODO: need to fixed for both en and fa
>
{value}
</Typography>
<ReactCountryFlag
countryCode={selectedCountry.code}
svg
style={{
height: '1.5rem',
width: '1.5rem',
// TODO: Check alignment for better styling definition
marginTop: '-2px',
marginRight: '4px',
}}
/>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
slotProps={{
list: {
sx: {
// Set the width to match the anchor element's width
width: menuWidth,
height: '18.75rem',
},
},
transition: {
onEntered: handleMenuEntered,
},
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<Box
sx={{
position: 'sticky',
top: 0,
zIndex: 1,
p: 1,
backgroundColor: 'background.paper',
}}
>
<TextField
inputRef={searchInputRef}
size="small"
fullWidth
label={t('labels.search')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</Box>
{/* Can improve preformance with using virtual scrolling */}
<Box sx={{ width: '100%', maxHeight: '14.75rem', overflow: 'auto' }}>
{filteredCountries.length === 0 ? (
<MenuItem disabled>
<ListItem>
<ListItemText>{t('messages.noResualtFound')}</ListItemText>
</ListItem>
</MenuItem>
) : (
filteredCountries.map((country) => (
<MenuItem
key={country.code}
selected={country.phone === value}
onClick={() => handleSelect(country)}
>
<ListItemIcon>
<ReactCountryFlag
countryCode={country.code}
svg
style={{
height: '1.125rem',
width: '1.125rem',
}}
/>
</ListItemIcon>
<ListItemText primary={t(country.label)} />
<Typography color="text.secondary">
{country.phone}
</Typography>
</MenuItem>
))
)}
</Box>
{/* virtual scrolling */}
{/* <Virtuoso
style={{ height: '14.75rem' }} // Adjust height to account for the search bar
data={filteredCountries}
components={{
EmptyPlaceholder: () => (
<MenuItem disabled>
<ListItemText>{t('messages.noResultFound')}</ListItemText>
</MenuItem>
),
}}
initialTopMostItemIndex={countries.indexOf(selectedCountry)}
itemContent={(_, country) => (
<MenuItem
key={country.code}
selected={country.phone === value}
onClick={() => handleSelect(country)}
>
<ListItemIcon>
<ReactCountryFlag
countryCode={country.code}
svg
style={{ fontSize: '1.25em', lineHeight: '1.25em' }}
/>
</ListItemIcon>
<ListItemText primary={country.label} />
<Typography variant="body2" color="text.secondary">
{country.phone}
</Typography>
</MenuItem>
)}
/> */}
</Menu>
</Box>
</InputAdornment>
);
}

View File

@@ -10,6 +10,7 @@ import { DeviceMessage, Logout } from 'iconsax-react';
import Logo from '@/components/Logo';
import { CardContainer } from '@/components/CardContainer';
import { PageWrapper } from '../PageWrapper';
import React from 'react';
export function ActiveDevices() {
const { t } = useTranslation('activeDevices');
@@ -83,9 +84,8 @@ export function ActiveDevices() {
}}
>
{devices.map((device) => (
<>
<React.Fragment key={device.id}>
<Box
key={device.id}
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
@@ -199,7 +199,7 @@ export function ActiveDevices() {
{isXsup && (
<Box sx={{ color: 'divider', borderBottom: '1px solid' }} />
)}
</>
</React.Fragment>
))}
</Box>
</CardContainer>

View File

@@ -0,0 +1,25 @@
import { Box, Typography } from '@mui/material';
export function Header() {
const headers = [{ name: 'محمدحسین برزه گر', phone: '09123456789' }];
return (
<Box
sx={{
height: 84,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
px: 2,
}}
>
{headers.map((h) => (
<Box key={h.phone}>
<Typography variant="body2">{h.name}</Typography>
<Typography variant="body2" color="text.secondary">
{h.phone}
</Typography>
</Box>
))}
</Box>
);
}

View File

@@ -0,0 +1,127 @@
import { useState } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import {
Box,
useTheme,
useMediaQuery,
Drawer,
AppBar,
Toolbar,
IconButton,
} from '@mui/material';
import { SideNav } from '@rkheftan/harmony-ui';
import { useTranslation } from 'react-i18next';
import { HambergerMenu } from 'iconsax-react';
import { Header } from './Header';
import { getNavConfig } from './navConfigs';
export function Layout() {
const theme = useTheme();
const isMdUp = useMediaQuery(theme.breakpoints.up('md'));
const isLgUp = useMediaQuery(theme.breakpoints.up('lg'));
const location = useLocation();
const { t } = useTranslation('sideMap');
const [drawerOpen, setDrawerOpen] = useState(false);
const navWidthMd = isLgUp ? 274 : 'min(15vw, 180px)';
// const sidebarWidth = isLgUp ? 274 : isMdUp ? 'min(15vw, 180px)' : 0;
const navConfig = getNavConfig(t);
return (
<Box
sx={{
minHeight: '100vh',
width: '100vw',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
px: { xs: 0, sm: 0, md: 'max(1vw, 8px)', lg: '60px' },
py: { xs: 0, sm: 0, md: '40px', lg: '80px' },
boxSizing: 'border-box',
backgroundColor: 'background.default',
}}
>
<Box
sx={{
width: {
xs: '100vw',
md: 'min(90vw, 900px)',
lg: '1100px',
},
height: '100vh',
display: 'flex',
flexDirection: 'column',
bgcolor: 'background.paper',
overflow: 'hidden',
borderRadius: { xs: 0, md: 2 },
boxShadow: { xs: 0, md: 3 },
}}
>
{!isMdUp && (
<AppBar position="static" elevation={0}>
<Toolbar sx={{ backgroundColor: 'background.paper', gap: 2 }}>
<IconButton
edge="start"
onClick={() => setDrawerOpen(true)}
size="medium"
sx={{ backgroundColor: '#2979FF1F' }}
>
<HambergerMenu size={24} color="#1976d2" />
</IconButton>
</Toolbar>
</AppBar>
)}
<Box sx={{ flexGrow: 1, display: 'flex', overflow: 'hidden' }}>
{isMdUp && (
<Box
sx={{
width: navWidthMd,
flexShrink: 0,
transition: 'width 0.3s ease',
}}
>
<SideNav
navConfig={navConfig}
header={<Header />}
activePath={location.pathname + location.hash}
sideNavVariant="full"
positioning="absolute"
/>
</Box>
)}
<Box
sx={{
flexGrow: 1,
overflowY: 'auto',
px: { xs: 2, sm: 3, md: 'max(1.5vw, 12px)' },
py: { xs: 2, sm: 3 },
}}
>
<Outlet />
</Box>
</Box>
{!isMdUp && (
<Drawer
anchor="left"
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
>
<Box width={navWidthMd}>
<SideNav
navConfig={navConfig}
header={<Header />}
activePath={location.pathname + location.hash}
sideNavVariant="full"
/>
</Box>
</Drawer>
)}
</Box>
</Box>
);
}

View File

@@ -0,0 +1,52 @@
import { createBrowserRouter, Navigate } from 'react-router-dom';
import { Layout } from './Layout';
import { PersonalInformation } from '../userInformation/PersonalInformation';
import { PhoneNumber } from '../userInformation/PhoneNumber';
import { SocialMedia } from '../userInformation/SocialMedia';
import { PasswordSecurity } from '../security/PasswordSecurity';
import { RecentLogins } from '../security/RecentLogins';
import { ActiveDevices } from '../activeDevices/ActiveDevices';
import { Setting } from '../setting/Setting';
import { Box } from '@mui/material';
export const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ path: '/', element: <Navigate to="/profile" replace /> },
{
path: '/profile',
element: (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<div id="info">
<PersonalInformation />
</div>
<div id="contact-info">
<PhoneNumber />
</div>
<div id="email">
<SocialMedia />
</div>
</Box>
),
},
{
path: '/security',
element: (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<div id="password">
<PasswordSecurity />
</div>
<div id="locations"></div>
<div id="sessions">
<RecentLogins />
</div>
</Box>
),
},
{ path: '/devices', element: <ActiveDevices /> },
{ path: '/setting', element: <Setting /> },
],
},
]);

View File

@@ -0,0 +1,121 @@
import {
Devices,
LocationTick,
Mobile,
PasswordCheck,
Personalcard,
ProfileCircle,
Setting as SettingIcon,
Shield,
Sms,
} from 'iconsax-react';
import { type TFunction } from 'i18next';
export function getNavConfig(t: TFunction) {
return [
{
text: t('side.account'),
getIcon: (selected: boolean) =>
selected ? (
<ProfileCircle size={24} color="#1976d2" variant="Bold" />
) : (
<ProfileCircle size={24} color="#82B1FF" />
),
path: '/profile',
children: [
{
text: t('side.personalInfo'),
getIcon: (sel: boolean) =>
sel ? (
<Personalcard size={24} color="#1976d2" variant="Bold" />
) : (
<Personalcard size={24} color="#82B1FF" />
),
path: '/profile#info',
},
{
text: t('side.phoneNumber'),
getIcon: (sel: boolean) =>
sel ? (
<Mobile size={24} color="#1976d2" variant="Bold" />
) : (
<Mobile size={24} color="#82B1FF" />
),
path: '/profile#contact-info',
},
{
text: t('side.email'),
getIcon: (sel: boolean) =>
sel ? (
<Sms size={24} color="#1976d2" variant="Bold" />
) : (
<Sms size={24} color="#82B1FF" />
),
path: '/profile#email',
},
],
},
{
text: t('side.security'),
getIcon: (sel: boolean) =>
sel ? (
<Shield size={24} color="#1976d2" variant="Bold" />
) : (
<Shield size={24} color="#82B1FF" />
),
path: '/security',
children: [
{
text: t('side.password'),
getIcon: (sel: boolean) =>
sel ? (
<PasswordCheck size={24} color="#1976d2" variant="Bold" />
) : (
<PasswordCheck size={24} color="#82B1FF" />
),
path: '/security#password',
},
{
text: t('side.verifiedAddress'),
getIcon: (sel: boolean) =>
sel ? (
<LocationTick size={24} color="#1976d2" variant="Bold" />
) : (
<LocationTick size={24} color="#82B1FF" />
),
path: '/security#locations',
},
{
text: t('side.recentLogins'),
getIcon: (sel: boolean) =>
sel ? (
<Devices size={24} color="#1976d2" variant="Bold" />
) : (
<Devices size={24} color="#82B1FF" />
),
path: '/security#sessions',
},
],
},
{
text: t('side.activeDevices'),
getIcon: (sel: boolean) =>
sel ? (
<Devices size={24} color="#1976d2" variant="Bold" />
) : (
<Devices size={24} color="#82B1FF" />
),
path: '/devices',
},
{
text: t('side.settings'),
getIcon: (sel: boolean) =>
sel ? (
<SettingIcon size={24} color="#1976d2" variant="Bold" />
) : (
<SettingIcon size={24} color="#82B1FF" />
),
path: '/setting',
},
];
}

View File

@@ -0,0 +1,179 @@
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
IconButton,
Box,
Button,
Link,
} from '@mui/material';
import { CloseCircle, Refresh } from 'iconsax-react';
import { PasswordValidationItem } from './PasswordValidation';
interface PasswordDialogProps {
open: boolean;
fullScreen: boolean;
handleClose: () => void;
password: string;
setPassword: (val: string) => void;
confirmPassword: string;
setConfirmPassword: (val: string) => void;
currentPassword: string;
setCurrentPassword: (val: string) => void;
showValidation: boolean;
hasNumber: boolean;
hasMinLength: boolean;
hasUpperAndLower: boolean;
hasSpecialChar: boolean;
matchPassword: boolean;
loading: boolean;
handleShowAlert: () => void;
changePassword: boolean;
t: (key: string) => string;
}
export function PasswordDialog({
open,
fullScreen,
handleClose,
password,
setPassword,
confirmPassword,
setConfirmPassword,
currentPassword,
setCurrentPassword,
showValidation,
hasNumber,
hasMinLength,
hasUpperAndLower,
hasSpecialChar,
matchPassword,
loading,
handleShowAlert,
changePassword,
t,
}: PasswordDialogProps) {
return (
<Dialog
open={open}
onClose={handleClose}
fullWidth
fullScreen={fullScreen}
maxWidth="xs"
scroll="body"
PaperProps={{ sx: { mx: 1 } }}
>
<DialogTitle sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton onClick={handleClose}>
<CloseCircle size={24} color="#82B1FF" />
</IconButton>
<Box component="span" fontWeight="bold" fontSize={18}>
{t('securityForm.addPassword')}
</Box>
</Box>
</DialogTitle>
<DialogContent
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 3 },
}}
>
{changePassword && (
<>
<TextField
label={t('securityForm.currentPassword')}
fullWidth
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
/>
<Link sx={{ fontSize: '15px' }}>
{t('securityForm.forgetPassword')}
</Link>
</>
)}
<TextField
label={t('securityForm.newPassword')}
type="password"
fullWidth
value={password}
onChange={(e) => setPassword(e.target.value)}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
/>
{password && showValidation && (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Box sx={{ maxWidth: '364px', width: '100%' }}>
<PasswordValidationItem
isValid={hasNumber}
label={t('securityForm.hasNumber')}
/>
<PasswordValidationItem
isValid={hasMinLength}
label={t('securityForm.hasMinLength')}
/>
<PasswordValidationItem
isValid={hasUpperAndLower}
label={t('securityForm.hasUpperAndLower')}
/>
<PasswordValidationItem
isValid={hasSpecialChar}
label={t('securityForm.hasSpecialChar')}
/>
</Box>
</Box>
)}
<TextField
label={t('securityForm.confirmPassword')}
type="password"
fullWidth
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
error={confirmPassword.length > 0 && !matchPassword}
helperText={
confirmPassword.length > 0 && !matchPassword
? t('securityForm.notCompatibility')
: ' '
}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
/>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2, justifyContent: 'center' }}>
<Button
fullWidth
sx={{ maxWidth: '364px', height: 48, textTransform: 'none' }}
variant="contained"
onClick={handleShowAlert}
disabled={!password || password !== confirmPassword || loading}
>
{loading ? (
<Box
component="span"
sx={{
display: 'flex',
animation: 'spin 1s linear infinite',
'@keyframes spin': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' },
},
}}
>
<Refresh size="20" color="#fff" />
</Box>
) : (
t('securityForm.confirm')
)}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -1,30 +1,23 @@
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Button,
Dialog,
DialogTitle,
DialogContent,
TextField,
DialogActions,
IconButton,
useTheme,
useMediaQuery,
Link,
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';
import { useState, useEffect } from 'react';
import { CloseCircle, Refresh } from 'iconsax-react';
import { PasswordValidationItem } from './PasswordValidation';
import { Toast } from '@/components/Toast';
import Logo from '@/components/Logo';
import { CardContainer } from '@/components/CardContainer';
import { PageWrapper } from '../PageWrapper';
import Logo from '@/components/Logo';
import { PasswordDialog } from './PasswordDialog';
import { Toast } from '@/components/Toast';
export function PasswordSecurity() {
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const { t } = useTranslation('security');
const [open, setOpen] = useState(false);
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
@@ -42,21 +35,6 @@ export function PasswordSecurity() {
hasNumber && hasMinLength && hasUpperAndLower && hasSpecialChar;
const matchPassword = password === confirmPassword;
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const handleShowAlert = () => {
setLoading(true);
setTimeout(() => {
setLoading(false);
setShowPasswordAlert(true);
setChangePassword(true);
handleClose();
setPassword('');
setConfirmPassword('');
setCurrentPassword('');
}, 1500);
};
useEffect(() => {
if (password) {
if (!validPassword) {
@@ -70,6 +48,22 @@ export function PasswordSecurity() {
}
}, [password, validPassword]);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const handleShowAlert = () => {
setLoading(true);
setTimeout(() => {
setLoading(false);
setShowPasswordAlert(true);
setChangePassword(true);
handleClose();
setPassword('');
setConfirmPassword('');
setCurrentPassword('');
}, 1500);
};
return (
<PageWrapper>
<Box sx={{ display: 'flex', alignItems: 'center', py: 2, height: 84 }}>
@@ -80,35 +74,21 @@ export function PasswordSecurity() {
title={t('securityForm.password')}
subtitle={t('securityForm.determinePassword')}
action={
changePassword ? (
<Button
variant="outlined"
onClick={handleOpen}
sx={{
mt: { xs: 2, sm: 0 },
backgroundColor: 'primary.main',
color: 'background.paper',
width: { xs: '100%', sm: '142px' },
textTransform: 'none',
}}
>
{t('securityForm.changePassword')}
</Button>
) : (
<Button
variant="outlined"
onClick={handleOpen}
sx={{
mt: { xs: 2, sm: 0 },
backgroundColor: 'primary.main',
color: 'background.paper',
width: { xs: '100%', sm: '142px' },
textTransform: 'none',
}}
>
{t('securityForm.addPassword')}
</Button>
)
<Button
variant="outlined"
onClick={handleOpen}
sx={{
mt: { xs: 2, sm: 0 },
backgroundColor: 'primary.main',
color: 'background.paper',
width: { xs: '100%', sm: '142px' },
textTransform: 'none',
}}
>
{changePassword
? t('securityForm.changePassword')
: t('securityForm.addPassword')}
</Button>
}
>
<Box
@@ -122,198 +102,48 @@ export function PasswordSecurity() {
flex: 1,
}}
>
<Box sx={{ height: 'auto', py: { xs: 3, sm: 4 } }}>
{changePassword ? (
<Box sx={{ flexDirection: 'column', px: 2, py: 2 }}>
<Typography variant="h6">
{t('securityForm.activePassword')}
</Typography>
<Typography variant="caption" color="text.secondary">
{t('securityForm.lastChange')}
</Typography>
</Box>
) : (
<Typography
variant="body1"
color="text.secondary"
sx={{ textAlign: 'center', py: { xs: 4, sm: '43.5px' } }}
>
{t('securityForm.notDeterminedPassword')}
{changePassword ? (
<Box sx={{ flexDirection: 'column', px: 2, py: 2 }}>
<Typography variant="h6">
{t('securityForm.activePassword')}
</Typography>
)}
</Box>
<Dialog
<Typography variant="caption" color="text.secondary">
{t('securityForm.lastChange')}
</Typography>
</Box>
) : (
<Typography
variant="body1"
color="text.secondary"
sx={{ textAlign: 'center', py: { xs: 4, sm: '43.5px' } }}
>
{t('securityForm.notDeterminedPassword')}
</Typography>
)}
{/* New PasswordDialog component usage */}
<PasswordDialog
open={open}
onClose={handleClose}
fullWidth
fullScreen={fullScreen}
maxWidth="xs"
scroll="body"
PaperProps={{ sx: { mx: 1 } }}
>
<DialogTitle sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton onClick={handleClose}>
<CloseCircle size={24} color="#82B1FF" />
</IconButton>
<Typography variant="h6" fontWeight="bold">
{t('securityForm.addPassword')}
</Typography>
</Box>
</DialogTitle>
{changePassword ? (
<DialogContent
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 3 },
}}
>
<TextField
label={t('securityForm.currentPassword')}
fullWidth
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
sx={{
'& .MuiOutlinedInput-root': { borderRadius: 2 },
mt: 2,
}}
/>
<Link sx={{ fontSize: '15px' }}>
{t('securityForm.forgetPassword')}
</Link>
<TextField
label={t('securityForm.newPassword')}
type="password"
fullWidth
value={password}
onChange={(e) => setPassword(e.target.value)}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
/>
{password && showValidation && (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Box sx={{ maxWidth: '364px', width: '100%' }}>
<PasswordValidationItem
isValid={hasNumber}
label={t('securityForm.hasNumber')}
/>
<PasswordValidationItem
isValid={hasMinLength}
label={t('securityForm.hasMinLength')}
/>
<PasswordValidationItem
isValid={hasUpperAndLower}
label={t('securityForm.hasUpperAndLower')}
/>
<PasswordValidationItem
isValid={hasSpecialChar}
label={t('securityForm.hasSpecialChar')}
/>
</Box>
</Box>
)}
<TextField
label={t('securityForm.confirmPassword')}
type="password"
fullWidth
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
error={confirmPassword.length > 0 && !matchPassword}
helperText={
confirmPassword.length > 0 && !matchPassword
? t('securityForm.notCompatibility')
: ' '
}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
/>
</DialogContent>
) : (
<DialogContent
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 3 },
}}
>
<TextField
label={t('securityForm.newPassword')}
type="password"
fullWidth
value={password}
onChange={(e) => setPassword(e.target.value)}
sx={{
'& .MuiOutlinedInput-root': { borderRadius: 2 },
mt: 2,
}}
/>
{password && showValidation && (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Box sx={{ maxWidth: '364px', width: '100%' }}>
<PasswordValidationItem
isValid={hasNumber}
label={t('securityForm.hasNumber')}
/>
<PasswordValidationItem
isValid={hasMinLength}
label={t('securityForm.hasMinLength')}
/>
<PasswordValidationItem
isValid={hasUpperAndLower}
label={t('securityForm.hasUpperAndLower')}
/>
<PasswordValidationItem
isValid={hasSpecialChar}
label={t('securityForm.hasSpecialChar')}
/>
</Box>
</Box>
)}
<TextField
label={t('securityForm.confirmPassword')}
type="password"
fullWidth
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
error={confirmPassword.length > 0 && !matchPassword}
helperText={
confirmPassword.length > 0 && !matchPassword
? t('securityForm.notCompatibility')
: ' '
}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
/>
</DialogContent>
)}
<DialogActions sx={{ px: 3, pb: 2, justifyContent: 'center' }}>
<Button
fullWidth
sx={{ maxWidth: '364px', height: 48, textTransform: 'none' }}
variant="contained"
onClick={handleShowAlert}
disabled={!password || password !== confirmPassword || loading}
>
{loading ? (
<Box
component="span"
sx={{
display: 'flex',
animation: 'spin 1s linear infinite',
'@keyframes spin': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' },
},
}}
>
<Refresh size="20" color="#fff" />
</Box>
) : (
t('securityForm.confirm')
)}
</Button>
</DialogActions>
</Dialog>
handleClose={handleClose}
password={password}
setPassword={setPassword}
confirmPassword={confirmPassword}
setConfirmPassword={setConfirmPassword}
currentPassword={currentPassword}
setCurrentPassword={setCurrentPassword}
showValidation={showValidation}
hasNumber={hasNumber}
hasMinLength={hasMinLength}
hasUpperAndLower={hasUpperAndLower}
hasSpecialChar={hasSpecialChar}
matchPassword={matchPassword}
loading={loading}
handleShowAlert={handleShowAlert}
changePassword={changePassword}
t={t}
/>
<Toast
color="success"
open={showPasswordAlert}

View File

@@ -2,6 +2,7 @@ import { Box, Typography, Button } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { CardContainer } from '@/components/CardContainer';
import { PageWrapper } from '../PageWrapper';
import React from 'react';
export function RecentLogins() {
const { t } = useTranslation('security');
@@ -46,7 +47,7 @@ export function RecentLogins() {
>
<Box sx={{ width: '100%', maxWidth: '754px', px: 4 }}>
{data.map((d) => (
<>
<React.Fragment key={d.id}>
<Box
sx={{
display: 'flex',
@@ -54,7 +55,6 @@ export function RecentLogins() {
alignItems: { xs: 'flex-start', sm: 'center' },
minHeight: 50,
}}
key={d.id}
>
<Typography
variant="body2"
@@ -117,7 +117,7 @@ export function RecentLogins() {
</Box>
</Box>
<Box sx={{ color: 'divider', borderBottom: '1px solid' }} />
</>
</React.Fragment>
))}
</Box>
</Box>

View File

@@ -1,12 +0,0 @@
import { PasswordSecurity } from './PasswordSecurity';
import { RecentLogins } from './RecentLogins';
import { Box } from '@mui/material';
export function Security() {
return (
<Box sx={{ backgroundColor: 'background.paper', height: '730px' }}>
<PasswordSecurity />
<RecentLogins />
</Box>
);
}

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { Box, Button } from '@mui/material';
import { Box, Button, useTheme, useMediaQuery } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { CardContainer } from '@/components/CardContainer';
import { ProfileImage } from './personlInformation/ProfileImage';
@@ -13,6 +13,8 @@ export function PersonalInformation() {
const [isEditing, setIsEditing] = useState(false);
const [gender, setGender] = useState('');
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
const theme = useTheme();
const isMdUp = useMediaQuery(theme.breakpoints.up('lg'));
const initialData = {
firstName: 'محمد حسین',
@@ -30,28 +32,43 @@ export function PersonalInformation() {
if (isEditing) {
setData((prev) => ({
...prev,
gender: gender === 'male' ? 'مرد' : gender === 'female' ? 'زن' : '',
gender:
gender === 'male'
? t('settingForm.man')
: gender === 'female'
? t('settingForm.woman')
: '',
}));
} else {
setGender(
data.gender === 'مرد' ? 'male' : data.gender === 'زن' ? 'female' : '',
data.gender === t('settingForm.man')
? 'male'
: data.gender === t('settingForm.woman')
? 'female'
: '',
);
}
};
return (
<PageWrapper>
<Box
sx={{
display: 'flex',
alignItems: 'center',
py: 2,
height: 84,
}}
>
<Logo />
</Box>
<Box sx={{ width: '100%', height: 1, bgcolor: 'divider' }} />
{isMdUp && (
<>
<Box
sx={{
display: 'flex',
alignItems: 'center',
py: 2,
height: 84,
}}
>
<Logo />
</Box>
<Box
sx={{ width: '100%', height: 1, bgcolor: 'background.default' }}
/>
</>
)}
<CardContainer
title={t('settingForm.titlePersonalInfo')}
subtitle={t('settingForm.descriptionPersonalInfo')}

View File

@@ -1,11 +1,11 @@
import { useState, type ChangeEvent } from 'react';
import { Box, Typography, Button, TextField, IconButton } from '@mui/material';
import { Edit, Refresh, TickCircle } from 'iconsax-react';
import { useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { CardContainer } from '@/components/CardContainer';
import { CountDownTimer } from '@/components/CountDownTimer';
import { Toast } from '@/components/Toast';
import parsePhoneNumberFromString from 'libphonenumber-js';
import { PageWrapper } from '../PageWrapper';
import { CardContainer } from '@/components/CardContainer';
import PhoneDisplay from './phoneNumber/PhoneDisplay';
import PhoneEditForm from './phoneNumber/PhoneEditForm';
import PhoneActionButtons from './phoneNumber/PhoneActionButtons';
export function PhoneNumber() {
const { t } = useTranslation('profileSetting');
@@ -18,10 +18,32 @@ export function PhoneNumber() {
);
const [isVerifying, setIsVerifying] = useState(false);
const [isVerified, setIsVerified] = useState(false);
const [phones, setPhone] = useState([
{ phone: '09123456789', time: '۱ ماه پیش', withCode: '+989123456789' },
]);
const [countryCode, setCountryCode] = useState('+98');
const textFieldRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [error, setError] = useState<string>();
const [touched, setTouched] = useState<boolean>(false);
const inputError: boolean = touched && !!error;
const isPhoneValid = (code: string, phone: string) => {
const phoneNum = parsePhoneNumberFromString(code + phone);
return phoneNum && phoneNum.isValid();
};
const handleBlur = () => {
setTouched(true);
if (!phoneNumber) {
setError(t('settingForm.thisFieldIsRequired'));
}
if (!isPhoneValid(countryCode, phoneNumber)) {
setError(t('settingForm.phoneNumberIsInvalid'));
} else {
setError(undefined);
}
};
const toggleEdit = () => {
setIsEditing((prev) => {
@@ -37,16 +59,6 @@ export function PhoneNumber() {
});
};
const handlePhoneNumberChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/\D/g, '');
setPhoneNumber(value);
};
const handleVerificationCodeChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.replace(/\D/g, '');
setVerificationCode(value);
};
const handleSendCode = () => {
if (!phoneNumber) return;
setButtonState('counting');
@@ -79,209 +91,39 @@ export function PhoneNumber() {
subtitle={t('settingForm.descriptionPhoneNumber')}
highlighted={isEditing}
action={
<Box sx={{ display: 'flex', gap: 1 }}>
{isEditing && (
<Button
variant="text"
onClick={toggleEdit}
size="large"
sx={{
color: 'primary.main',
textTransform: 'none',
width: '43px',
}}
>
{t('settingForm.rejectButton')}
</Button>
)}
<Button
onClick={toggleEdit}
size="large"
variant="outlined"
sx={{
textTransform: 'none',
border: '1px solid',
borderColor: 'primary.main',
borderRadius: '4px',
bgcolor: isEditing ? 'primary.main' : 'background.paper',
color: isEditing ? 'primary.contrastText' : 'primary.main',
width: { xs: '100%', sm: isEditing ? '85px' : '161px' },
whiteSpace: 'nowrap',
}}
>
{isEditing
? t('settingForm.saveButton')
: t('settingForm.editPhoneNumber')}
</Button>
</Box>
<PhoneActionButtons
isEditing={isEditing}
toggleEdit={toggleEdit}
t={t}
/>
}
>
{isEditing ? (
<Box sx={{ px: { xs: 2, sm: 4 }, bgcolor: 'background.paper' }}>
<Box sx={{ mb: 2 }}>
<Typography variant="h6">
{t('settingForm.editPhoneNumber')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('settingForm.phoneNumberText')}(
{phones.map((p) => p.withCode)}){t('settingForm.verb')}
</Typography>
</Box>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2,
alignItems: 'center',
}}
>
<TextField
name="phoneNumber"
label={t('settingForm.newPhoneNumber')}
type="tel"
value={phoneNumber}
onChange={handlePhoneNumberChange}
sx={{ flex: '1 1 240px', minWidth: 0 }}
placeholder="09123456789"
inputProps={{ dir: 'rtl', style: { textAlign: 'right' } }}
InputProps={{
endAdornment:
buttonState === 'counting' ? (
<IconButton
size="small"
onClick={() => {
setButtonState('default');
setPhoneNumber('');
}}
sx={{ mr: 1 }}
>
<Edit size="24" color="#64B5F6" />
</IconButton>
) : null,
}}
/>
{isVerified ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<TickCircle
size="24"
style={{ color: '#43A047' }}
variant="Bold"
/>
<Typography sx={{ color: 'success.main' }}>
{t('settingForm.successButton')}
</Typography>
</Box>
) : (
<Button
variant="text"
onClick={handleSendCode}
disabled={
buttonState === 'counting' || phoneNumber.length === 0
}
sx={{
textTransform: 'none',
minWidth: { xs: '100%', sm: 170 },
color: 'primary.main',
height: 56,
}}
>
{buttonState === 'counting' ? (
<CountDownTimer
initialSeconds={60}
onComplete={() => setButtonState('default')}
/>
) : (
t('settingForm.verificationCodeButton')
)}
</Button>
)}
</Box>
{buttonState === 'counting' && !isVerified && (
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2,
mt: 2,
alignItems: 'center',
}}
>
<TextField
name="verificationCode"
label={t('settingForm.verificationCode')}
type="tel"
value={verificationCode}
onChange={handleVerificationCodeChange}
sx={{ flex: '1 1 240px', minWidth: 0 }}
placeholder={t('settingForm.verificationCode')}
inputProps={{ dir: 'rtl', style: { textAlign: 'right' } }}
/>
<Button
variant="contained"
onClick={handleVerifyClick}
disabled={isVerifying || verificationCode.length === 0}
sx={{
textTransform: 'none',
minWidth: { xs: '100%', sm: 170 },
bgcolor: 'primary.main',
height: 56,
}}
>
{isVerifying ? (
<Box
component="span"
sx={{
display: 'flex',
animation: 'spin 1s linear infinite',
'@keyframes spin': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' },
},
}}
>
<Refresh size="20" color="white" />
</Box>
) : (
t('settingForm.checkCode')
)}
</Button>
</Box>
)}
<Toast
color="success"
open={showToast}
onClose={() => setShowToast(false)}
>
{t('settingForm.successfulChangePhone')}
</Toast>
</Box>
<PhoneEditForm
phoneNumber={phoneNumber}
setPhoneNumber={setPhoneNumber}
countryCode={countryCode}
setCountryCode={setCountryCode}
verificationCode={verificationCode}
setVerificationCode={setVerificationCode}
isVerified={isVerified}
isVerifying={isVerifying}
buttonState={buttonState}
setButtonState={setButtonState}
handleSendCode={handleSendCode}
handleVerifyClick={handleVerifyClick}
error={error}
inputError={inputError}
handleBlur={handleBlur}
textFieldRef={textFieldRef as React.RefObject<HTMLDivElement>}
inputRef={inputRef as React.RefObject<HTMLInputElement>}
phones={phones}
showToast={showToast}
setShowToast={setShowToast}
t={t}
/>
) : (
<Box sx={{ px: { xs: 2, sm: 4 } }}>
{phones.map((item, index) => (
<Box
key={index}
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
py: 2,
width: '100%',
}}
>
<Typography variant="h6" color="text.primary">
{item.phone}
</Typography>
<Typography variant="caption" color="text.secondary">
{item.time}
</Typography>
</Box>
))}
</Box>
<PhoneDisplay phones={phones} />
)}
</CardContainer>
</PageWrapper>

View File

@@ -1,42 +1,12 @@
import React, { useState } from 'react';
import {
Box,
Button,
Typography,
Dialog,
DialogTitle,
DialogContent,
IconButton,
TextField,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
useMediaQuery,
} from '@mui/material';
import Slide from '@mui/material/Slide';
import type { TransitionProps } from '@mui/material/transitions';
import type { Theme } from '@mui/material/styles';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CardContainer } from '@/components/CardContainer';
import {
Google,
Apple,
Sms,
Trash,
CloseSquare,
Message,
ArrowDown3,
} from 'iconsax-react';
import { PageWrapper } from '../PageWrapper';
const MobileSlide = React.forwardRef(function MobileSlide(
props: TransitionProps & { children: React.ReactElement<any, any> },
ref: React.Ref<unknown>,
) {
return <Slide direction="up" ref={ref} {...props} />;
});
import SocialMediaList from './socialMedia/SocialMediaList';
import SocialMediaMenu from './socialMedia/SocialMediaMenu';
import SocialMediaDialog from './socialMedia/SocialMediaDialog';
import useMediaQuery from '@mui/material/useMediaQuery';
import type { Theme } from '@mui/material/styles';
export function SocialMedia() {
const { t } = useTranslation('profileSetting');
@@ -44,9 +14,7 @@ export function SocialMedia() {
const [openDialog, setOpenDialog] = useState(false);
const [emailInput, setEmailInput] = useState('');
const [emailError, setEmailError] = useState(false);
const [anchor, setAnchor] = useState<null | HTMLElement>(null);
const openMenu = Boolean(anchor);
const fullScreen = useMediaQuery((theme: Theme) =>
theme.breakpoints.down('sm'),
);
@@ -58,19 +26,6 @@ export function SocialMedia() {
| 'lg'
| 'xl';
const handleOpenDialog = () => setOpenDialog(true);
const handleCloseDialog = () => setOpenDialog(false);
const handleClickMenu = (e: React.MouseEvent<HTMLButtonElement>) =>
setAnchor(e.currentTarget);
const handleCloseMenu = () => setAnchor(null);
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setEmailInput(value);
setEmailError(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value));
};
const emailList = [
{ email: 'emailtemp@email.com', provider: 'email', time: '1 ماه پیش' },
{ email: 'emailtemp@gmail.com', provider: 'google', time: '1 ماه پیش' },
@@ -83,286 +38,21 @@ export function SocialMedia() {
title={t('settingForm.titleSocial')}
subtitle={t('settingForm.descriptionSocial')}
action={
<Box
sx={{
display: 'flex',
justifyContent: 'flex-start',
flexWrap: 'wrap',
gap: 1,
}}
>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
alignItems: { xs: 'stretch', sm: 'flex-start' },
gap: 1,
width: '100%',
}}
>
<Button
onClick={handleClickMenu}
variant="outlined"
sx={{
width: '100%',
maxWidth: { sm: 187 },
textTransform: 'none',
border: '1px solid',
borderColor: 'primary.main',
borderRadius: '8px',
color: 'primary.main',
fontSize: '14px',
px: 2,
py: 1,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
whiteSpace: 'nowrap',
}}
>
<Box
component="span"
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
direction: 'rtl',
}}
>
{t('settingForm.addEmailOrSocialButton')}
</Box>
<ArrowDown3 size="20" color="#2979FF" />
</Button>
</Box>
<Menu
anchorEl={anchor}
open={openMenu}
onClose={handleCloseMenu}
PaperProps={{
sx: {
minWidth: 240,
maxWidth: '90vw',
},
}}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
>
<MenuItem
onClick={() => {
handleCloseMenu();
handleOpenDialog();
}}
>
<ListItemIcon>
<Message size={20} color="black" />
</ListItemIcon>
<ListItemText>{t('settingForm.email')}</ListItemText>
</MenuItem>
<MenuItem>
<ListItemIcon>
<Google size={20} color="#4285F4" />
</ListItemIcon>
<ListItemText>{t('settingForm.google')}</ListItemText>
</MenuItem>
<MenuItem>
<ListItemIcon>
<Apple size={20} color="black" />
</ListItemIcon>
<ListItemText>{t('settingForm.apple')}</ListItemText>
</MenuItem>
</Menu>
</Box>
<SocialMediaMenu t={t} onOpenDialog={() => setOpenDialog(true)} />
}
>
<Box sx={{ width: '100%', borderRadius: '8px', p: { xs: 1, sm: 2 } }}>
{emailList.map((item, index) => (
<Box
key={index}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
py: 1,
gap: 1,
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
minWidth: 0,
}}
>
{item.provider === 'google' && (
<Google size="20" variant="Bold" color="#4285F4" />
)}
{item.provider === 'apple' && (
<Apple size="20" variant="Bold" color="black" />
)}
{item.provider === 'email' && (
<Sms size="20" variant="Bold" color="#1976d2" />
)}
<Box sx={{ minWidth: 0 }}>
<Typography variant="h6" noWrap>
{item.email}
</Typography>
<Typography variant="caption" color="text.secondary">
{item.time}
</Typography>
</Box>
</Box>
<IconButton size="small" aria-label="Delete">
<Trash size="20" color="gray" variant="Outline" />
</IconButton>
</Box>
))}
</Box>
<Dialog
<SocialMediaList t={t} emailList={emailList} />
<SocialMediaDialog
open={openDialog}
onClose={handleCloseDialog}
fullWidth
onClose={() => setOpenDialog(false)}
t={t}
emailInput={emailInput}
setEmailInput={setEmailInput}
emailError={emailError}
setEmailError={setEmailError}
fullScreen={fullScreen}
maxWidth={computedMaxWidth}
scroll="paper"
keepMounted
TransitionComponent={fullScreen ? MobileSlide : undefined}
PaperProps={{
sx: {
m: { xs: 0, sm: 2 },
borderRadius: { xs: 0, sm: 2 },
width: { xs: '100%', sm: 'auto' },
height: { xs: '100%', sm: 'auto' },
},
}}
>
<DialogTitle
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
fontWeight: 'bold',
fontSize: '16px',
gap: 1,
p: { xs: 1.5, sm: 2 },
position: { xs: 'sticky', sm: 'static' },
top: 0,
bgcolor: 'background.paper',
zIndex: 1,
}}
>
<IconButton onClick={handleCloseDialog} aria-label="Close">
<CloseSquare size={24} color="#82B1FF" />
</IconButton>
{t('settingForm.addEmailButton')}
</DialogTitle>
<DialogContent
dividers
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 3 },
py: { xs: 1.5, sm: 2 },
}}
>
<Box>
<Typography fontWeight="bold">
{t('settingForm.newEmail')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('settingForm.dialogHeader')}
</Typography>
</Box>
<TextField
fullWidth
type="email"
value={emailInput}
onChange={handleEmailChange}
error={emailError}
helperText={emailError ? t('settingForm.emailError') : ''}
label={t('settingForm.email')}
placeholder="abc@email.com"
autoComplete="email"
inputMode="email"
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
autoFocus
/>
<Button
variant="contained"
fullWidth
sx={{ textTransform: 'none', borderRadius: 2 }}
disabled={emailError || emailInput === ''}
>
{t('settingForm.verificationCodeButton')}
</Button>
<Box sx={{ display: 'flex', alignItems: 'center', my: 2 }}>
<Box sx={{ flex: 1, height: 1, bgcolor: 'divider' }} />
<Typography sx={{ mx: 1, fontSize: 12, color: 'text.secondary' }}>
{t('settingForm.or')}
</Typography>
<Box sx={{ flex: 1, height: 1, bgcolor: 'divider' }} />
</Box>
<Box
sx={{
display: 'flex',
gap: 1,
flexDirection: { xs: 'column', sm: 'row' },
}}
>
<Button
fullWidth
sx={{
textTransform: 'none',
border: 2,
borderColor: 'primary.main',
color: 'primary.main',
borderRadius: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Google
size="20"
color="#4285F4"
style={{ marginInlineStart: 8 }}
/>
{t('settingForm.google')}
</Button>
<Button
fullWidth
sx={{
textTransform: 'none',
border: 2,
borderColor: 'primary.main',
color: 'primary.main',
borderRadius: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Apple
size="20"
color="black"
style={{ marginInlineStart: 8 }}
/>
{t('settingForm.apple')}
</Button>
</Box>
</DialogContent>
</Dialog>
computedMaxWidth={computedMaxWidth}
/>
</CardContainer>
</PageWrapper>
);

View File

@@ -0,0 +1,49 @@
import { Box, Button } from '@mui/material';
export default function PhoneActionButtons({
isEditing,
toggleEdit,
t,
}: {
isEditing: boolean;
toggleEdit: () => void;
t: (key: string) => string;
}) {
return (
<Box sx={{ display: 'flex', gap: 1 }}>
{isEditing && (
<Button
variant="text"
onClick={toggleEdit}
size="large"
sx={{
color: 'primary.main',
textTransform: 'none',
width: '43px',
}}
>
{t('settingForm.rejectButton')}
</Button>
)}
<Button
onClick={toggleEdit}
size="large"
variant="outlined"
sx={{
textTransform: 'none',
border: '1px solid',
borderColor: 'primary.main',
borderRadius: '4px',
bgcolor: isEditing ? 'primary.main' : 'background.paper',
color: isEditing ? 'primary.contrastText' : 'primary.main',
width: { xs: '100%', sm: isEditing ? '85px' : '161px' },
whiteSpace: 'nowrap',
}}
>
{isEditing
? t('settingForm.saveButton')
: t('settingForm.editPhoneNumber')}
</Button>
</Box>
);
}

View File

@@ -0,0 +1,31 @@
import { Box, Typography } from '@mui/material';
export default function PhoneDisplay({
phones,
}: {
phones: { phone: string; time: string }[];
}) {
return (
<Box sx={{ px: { xs: 2, sm: 4 } }}>
{phones.map((item, index) => (
<Box
key={index}
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
py: 2,
width: '100%',
}}
>
<Typography variant="h6" color="text.primary">
{item.phone}
</Typography>
<Typography variant="caption" color="text.secondary">
{item.time}
</Typography>
</Box>
))}
</Box>
);
}

View File

@@ -0,0 +1,208 @@
import { Box, Typography, TextField, IconButton, Button } from '@mui/material';
import { Edit, Refresh, TickCircle } from 'iconsax-react';
import { CountDownTimer } from '@/components/CountDownTimer';
import { Toast } from '@/components/Toast';
import { CountryCodeSelector } from '../../CountryCodeSelector';
export default function PhoneEditForm({
phoneNumber,
setPhoneNumber,
countryCode,
setCountryCode,
verificationCode,
setVerificationCode,
isVerified,
isVerifying,
buttonState,
setButtonState,
handleSendCode,
handleVerifyClick,
error,
inputError,
handleBlur,
textFieldRef,
inputRef,
phones,
showToast,
setShowToast,
t,
}: {
phoneNumber: string;
setPhoneNumber: (v: string) => void;
countryCode: string;
setCountryCode: (v: string) => void;
verificationCode: string;
setVerificationCode: (v: string) => void;
isVerified: boolean;
isVerifying: boolean;
buttonState: 'default' | 'counting';
setButtonState: (v: 'default' | 'counting') => void;
handleSendCode: () => void;
handleVerifyClick: () => void;
error?: string;
inputError: boolean;
handleBlur: () => void;
textFieldRef: React.RefObject<HTMLDivElement>;
inputRef: React.RefObject<HTMLInputElement>;
phones: { phone: string; time: string; withCode: string }[];
showToast: boolean;
setShowToast: (v: boolean) => void;
t: (key: string) => string;
}) {
return (
<Box sx={{ px: { xs: 2, sm: 4 }, bgcolor: 'background.paper' }}>
<Box sx={{ mb: 2 }}>
<Typography variant="h6">{t('settingForm.editPhoneNumber')}</Typography>
<Typography variant="body2" color="text.secondary">
{t('settingForm.phoneNumberText')}({phones.map((p) => p.withCode)})
{t('settingForm.verb')}
</Typography>
</Box>
<Box
sx={{ display: 'flex', flexWrap: 'wrap', gap: 2, alignItems: 'center' }}
>
<TextField
name="phoneNumber"
label={t('settingForm.newPhoneNumber')}
type="tel"
value={phoneNumber}
ref={textFieldRef}
inputRef={inputRef}
onBlur={handleBlur}
error={inputError}
helperText={inputError ? error : ''}
onChange={(e) => setPhoneNumber(e.target.value)}
sx={{ flex: '1 1 240px', minWidth: 0 }}
placeholder="09123456789"
inputProps={{ dir: 'rtl', style: { textAlign: 'right' } }}
InputProps={{
endAdornment:
buttonState === 'counting' ? (
<IconButton
size="small"
onClick={() => {
setButtonState('default');
setPhoneNumber('');
}}
sx={{ mr: 1 }}
>
<Edit size="24" color="#64B5F6" />
</IconButton>
) : null,
}}
slotProps={{
htmlInput: {
dir: 'auto',
sx: { lineHeight: 1.5, paddingX: 0 },
},
input: {
endAdornment: (
<CountryCodeSelector
value={countryCode}
onChange={setCountryCode}
show={true}
menuAnchor={textFieldRef.current}
onCloseFocusRef={inputRef}
/>
),
},
}}
/>
{isVerified ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<TickCircle size="24" style={{ color: '#43A047' }} variant="Bold" />
<Typography sx={{ color: 'success.main' }}>
{t('settingForm.successButton')}
</Typography>
</Box>
) : (
<Button
variant="text"
onClick={handleSendCode}
disabled={buttonState === 'counting' || phoneNumber.length === 0}
sx={{
textTransform: 'none',
minWidth: { xs: '100%', sm: 170 },
color: 'primary.main',
height: 56,
}}
>
{buttonState === 'counting' ? (
<CountDownTimer
initialSeconds={60}
onComplete={() => setButtonState('default')}
/>
) : (
t('settingForm.verificationCodeButton')
)}
</Button>
)}
</Box>
{buttonState === 'counting' && !isVerified && (
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2,
mt: 2,
alignItems: 'center',
}}
>
<TextField
name="verificationCode"
label={t('settingForm.verificationCode')}
type="tel"
value={verificationCode}
onChange={(e) =>
setVerificationCode(e.target.value.replace(/\D/g, ''))
}
sx={{ flex: '1 1 240px', minWidth: 0 }}
placeholder={t('settingForm.verificationCode')}
inputProps={{ dir: 'rtl', style: { textAlign: 'right' } }}
/>
<Button
variant="contained"
onClick={handleVerifyClick}
disabled={isVerifying || verificationCode.length === 0}
sx={{
textTransform: 'none',
minWidth: { xs: '100%', sm: 170 },
bgcolor: 'primary.main',
height: 56,
}}
>
{isVerifying ? (
<Box
component="span"
sx={{
display: 'flex',
animation: 'spin 1s linear infinite',
'@keyframes spin': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' },
},
}}
>
<Refresh size="20" color="white" />
</Box>
) : (
t('settingForm.checkCode')
)}
</Button>
</Box>
)}
<Toast
color="success"
open={showToast}
onClose={() => setShowToast(false)}
>
{t('settingForm.successfulChangePhone')}
</Toast>
</Box>
);
}

View File

@@ -0,0 +1,187 @@
import React from 'react';
import {
Box,
Button,
Dialog,
DialogContent,
DialogTitle,
IconButton,
TextField,
Typography,
} from '@mui/material';
import Slide from '@mui/material/Slide';
import type { TransitionProps } from '@mui/material/transitions';
import { CloseSquare, Google, Apple } from 'iconsax-react';
const MobileSlide = React.forwardRef(function MobileSlide(
props: TransitionProps & { children: React.ReactElement<any, any> },
ref: React.Ref<unknown>,
) {
return <Slide direction="up" ref={ref} {...props} />;
});
export default function SocialMediaDialog({
open,
onClose,
t,
emailInput,
setEmailInput,
emailError,
setEmailError,
fullScreen,
computedMaxWidth,
}: {
open: boolean;
onClose: () => void;
t: (key: string) => string;
emailInput: string;
setEmailInput: (val: string) => void;
emailError: boolean;
setEmailError: (val: boolean) => void;
fullScreen: boolean;
computedMaxWidth: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
}) {
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setEmailInput(value);
setEmailError(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value));
};
return (
<Dialog
open={open}
onClose={onClose}
fullWidth
fullScreen={fullScreen}
maxWidth={computedMaxWidth}
scroll="paper"
keepMounted
TransitionComponent={fullScreen ? MobileSlide : undefined}
PaperProps={{
sx: {
m: { xs: 0, sm: 2 },
borderRadius: { xs: 0, sm: 2 },
width: { xs: '100%', sm: 'auto' },
height: { xs: '100%', sm: 'auto' },
},
}}
>
<DialogTitle
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
fontWeight: 'bold',
fontSize: '16px',
gap: 1,
p: { xs: 1.5, sm: 2 },
position: { xs: 'sticky', sm: 'static' },
top: 0,
bgcolor: 'background.paper',
zIndex: 1,
}}
>
<IconButton onClick={onClose} aria-label="Close">
<CloseSquare size={24} color="#82B1FF" />
</IconButton>
{t('settingForm.addEmailButton')}
</DialogTitle>
<DialogContent
dividers
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 3 },
py: { xs: 1.5, sm: 2 },
}}
>
<Box>
<Typography fontWeight="bold">{t('settingForm.newEmail')}</Typography>
<Typography variant="body2" color="text.secondary">
{t('settingForm.dialogHeader')}
</Typography>
</Box>
<TextField
fullWidth
type="email"
value={emailInput}
onChange={handleEmailChange}
error={emailError}
helperText={emailError ? t('settingForm.emailError') : ''}
label={t('settingForm.email')}
placeholder="abc@email.com"
autoComplete="email"
inputMode="email"
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
autoFocus
/>
<Button
variant="contained"
fullWidth
sx={{ textTransform: 'none', borderRadius: 2 }}
disabled={emailError || emailInput === ''}
>
{t('settingForm.verificationCodeButton')}
</Button>
<Box sx={{ display: 'flex', alignItems: 'center', my: 2 }}>
<Box sx={{ flex: 1, height: 1, bgcolor: 'divider' }} />
<Typography sx={{ mx: 1, fontSize: 12, color: 'text.secondary' }}>
{t('settingForm.or')}
</Typography>
<Box sx={{ flex: 1, height: 1, bgcolor: 'divider' }} />
</Box>
<Box
sx={{
display: 'flex',
gap: 1,
flexDirection: { xs: 'column', sm: 'row' },
}}
>
<Button
fullWidth
sx={{
textTransform: 'none',
border: 2,
borderColor: 'primary.main',
color: 'primary.main',
borderRadius: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Google
size="20"
color="#4285F4"
style={{ marginInlineStart: 8 }}
/>
{t('settingForm.google')}
</Button>
<Button
fullWidth
sx={{
textTransform: 'none',
border: 2,
borderColor: 'primary.main',
color: 'primary.main',
borderRadius: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Apple size="20" color="black" style={{ marginInlineStart: 8 }} />
{t('settingForm.apple')}
</Button>
</Box>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,63 @@
import { Box, Typography, IconButton } from '@mui/material';
import { Google, Apple, Sms, Trash } from 'iconsax-react';
export default function SocialMediaList({
emailList,
}: {
t: (key: string) => string;
emailList: readonly {
email: string;
provider: 'email' | 'google' | 'apple';
time: string;
}[];
}) {
return (
<Box sx={{ width: '100%', borderRadius: '8px', p: { xs: 1, sm: 2 } }}>
{emailList.map((item, index) => (
<Box
key={index}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
py: 1,
gap: 1,
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
minWidth: 0,
}}
>
{item.provider === 'google' && (
<Google size="20" variant="Bold" color="#4285F4" />
)}
{item.provider === 'apple' && (
<Apple size="20" variant="Bold" color="black" />
)}
{item.provider === 'email' && (
<Sms size="20" variant="Bold" color="#1976d2" />
)}
<Box sx={{ minWidth: 0 }}>
<Typography variant="h6" noWrap>
{item.email}
</Typography>
<Typography variant="caption" color="text.secondary">
{item.time}
</Typography>
</Box>
</Box>
<IconButton size="small" aria-label="Delete">
<Trash size="20" color="gray" variant="Outline" />
</IconButton>
</Box>
))}
</Box>
);
}

View File

@@ -0,0 +1,117 @@
import React, { useState } from 'react';
import {
Box,
Button,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
} from '@mui/material';
import { Message, Google, Apple, ArrowDown3 } from 'iconsax-react';
export default function SocialMediaMenu({
t,
onOpenDialog,
}: {
t: (key: string) => string;
onOpenDialog: () => void;
}) {
const [anchor, setAnchor] = useState<null | HTMLElement>(null);
const openMenu = Boolean(anchor);
const handleClickMenu = (e: React.MouseEvent<HTMLButtonElement>) =>
setAnchor(e.currentTarget);
const handleCloseMenu = () => setAnchor(null);
return (
<Box
sx={{
display: 'flex',
justifyContent: 'flex-start',
flexWrap: 'wrap',
gap: 1,
}}
>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
alignItems: { xs: 'stretch', sm: 'flex-start' },
gap: 1,
width: '100%',
}}
>
<Button
onClick={handleClickMenu}
variant="outlined"
sx={{
width: '100%',
maxWidth: { sm: 187 },
textTransform: 'none',
border: '1px solid',
borderColor: 'primary.main',
borderRadius: '8px',
color: 'primary.main',
fontSize: '14px',
px: 2,
py: 1,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
whiteSpace: 'nowrap',
}}
>
<Box
component="span"
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
direction: 'rtl',
}}
>
{t('settingForm.addEmailOrSocialButton')}
</Box>
<ArrowDown3 size="20" color="#2979FF" />
</Button>
</Box>
<Menu
anchorEl={anchor}
open={openMenu}
onClose={handleCloseMenu}
PaperProps={{
sx: {
minWidth: 240,
maxWidth: '90vw',
},
}}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
>
<MenuItem
onClick={() => {
handleCloseMenu();
onOpenDialog();
}}
>
<ListItemIcon>
<Message size={20} color="black" />
</ListItemIcon>
<ListItemText>{t('settingForm.email')}</ListItemText>
</MenuItem>
<MenuItem>
<ListItemIcon>
<Google size={20} color="#4285F4" />
</ListItemIcon>
<ListItemText>{t('settingForm.google')}</ListItemText>
</MenuItem>
<MenuItem>
<ListItemIcon>
<Apple size={20} color="black" />
</ListItemIcon>
<ListItemText>{t('settingForm.apple')}</ListItemText>
</MenuItem>
</Menu>
</Box>
);
}

View File

@@ -0,0 +1,271 @@
export interface Country {
code: string;
label: string;
phone: string;
}
export interface Country {
code: string;
label: string;
phone: string;
}
export const countries: readonly Country[] = [
{ code: 'AF', label: 'country.afghanistan', phone: '+93' },
{ code: 'AX', label: 'country.aland_islands', phone: '+358' },
{ code: 'AL', label: 'country.albania', phone: '+355' },
{ code: 'DZ', label: 'country.algeria', phone: '+213' },
{ code: 'AS', label: 'country.american_samoa', phone: '+1684' },
{ code: 'AD', label: 'country.andorra', phone: '+376' },
{ code: 'AO', label: 'country.angola', phone: '+244' },
{ code: 'AI', label: 'country.anguilla', phone: '+1264' },
{ code: 'AQ', label: 'country.antarctica', phone: '+672' },
{ code: 'AG', label: 'country.antigua_and_barbuda', phone: '+1268' },
{ code: 'AR', label: 'country.argentina', phone: '+54' },
{ code: 'AM', label: 'country.armenia', phone: '+374' },
{ code: 'AW', label: 'country.aruba', phone: '+297' },
{ code: 'AU', label: 'country.australia', phone: '+61' },
{ code: 'AT', label: 'country.austria', phone: '+43' },
{ code: 'AZ', label: 'country.azerbaijan', phone: '+994' },
{ code: 'BS', label: 'country.bahamas', phone: '+1242' },
{ code: 'BH', label: 'country.bahrain', phone: '+973' },
{ code: 'BD', label: 'country.bangladesh', phone: '+880' },
{ code: 'BB', label: 'country.barbados', phone: '+1246' },
{ code: 'BY', label: 'country.belarus', phone: '+375' },
{ code: 'BE', label: 'country.belgium', phone: '+32' },
{ code: 'BZ', label: 'country.belize', phone: '+501' },
{ code: 'BJ', label: 'country.benin', phone: '+229' },
{ code: 'BM', label: 'country.bermuda', phone: '+1441' },
{ code: 'BT', label: 'country.bhutan', phone: '+975' },
{ code: 'BO', label: 'country.bolivia', phone: '+591' },
{ code: 'BA', label: 'country.bosnia_and_herzegovina', phone: '+387' },
{ code: 'BW', label: 'country.botswana', phone: '+267' },
{ code: 'BR', label: 'country.brazil', phone: '+55' },
{
code: 'IO',
label: 'country.british_indian_ocean_territory',
phone: '+246',
},
{ code: 'VG', label: 'country.british_virgin_islands', phone: '+1284' },
{ code: 'BN', label: 'country.brunei', phone: '+673' },
{ code: 'BG', label: 'country.bulgaria', phone: '+359' },
{ code: 'BF', label: 'country.burkina_faso', phone: '+226' },
{ code: 'BI', label: 'country.burundi', phone: '+257' },
{ code: 'KH', label: 'country.cambodia', phone: '+855' },
{ code: 'CM', label: 'country.cameroon', phone: '+237' },
{ code: 'CA', label: 'country.canada', phone: '+1' },
{ code: 'CV', label: 'country.cape_verde', phone: '+238' },
{ code: 'KY', label: 'country.cayman_islands', phone: '+1345' },
{ code: 'CF', label: 'country.central_african_republic', phone: '+236' },
{ code: 'TD', label: 'country.chad', phone: '+235' },
{ code: 'CL', label: 'country.chile', phone: '+56' },
{ code: 'CN', label: 'country.china', phone: '+86' },
{ code: 'CX', label: 'country.christmas_island', phone: '+61' },
{ code: 'CC', label: 'country.cocos_keeling_islands', phone: '+61' },
{ code: 'CO', label: 'country.colombia', phone: '+57' },
{ code: 'KM', label: 'country.comoros', phone: '+269' },
{ code: 'CG', label: 'country.congo_brazzaville', phone: '+242' },
{ code: 'CD', label: 'country.congo_kinshasa', phone: '+243' },
{ code: 'CK', label: 'country.cook_islands', phone: '+682' },
{ code: 'CR', label: 'country.costa_rica', phone: '+506' },
{ code: 'CI', label: 'country.cote_divoire', phone: '+225' },
{ code: 'HR', label: 'country.croatia', phone: '+385' },
{ code: 'CU', label: 'country.cuba', phone: '+53' },
{ code: 'CW', label: 'country.curacao', phone: '+599' },
{ code: 'CY', label: 'country.cyprus', phone: '+357' },
{ code: 'CZ', label: 'country.czech_republic', phone: '+420' },
{ code: 'DK', label: 'country.denmark', phone: '+45' },
{ code: 'DJ', label: 'country.djibouti', phone: '+253' },
{ code: 'DM', label: 'country.dominica', phone: '+1767' },
{ code: 'DO', label: 'country.dominican_republic', phone: '+1' },
{ code: 'EC', label: 'country.ecuador', phone: '+593' },
{ code: 'EG', label: 'country.egypt', phone: '+20' },
{ code: 'SV', label: 'country.el_salvador', phone: '+503' },
{ code: 'GQ', label: 'country.equatorial_guinea', phone: '+240' },
{ code: 'ER', label: 'country.eritrea', phone: '+291' },
{ code: 'EE', label: 'country.estonia', phone: '+372' },
{ code: 'SZ', label: 'country.eswatini', phone: '+268' },
{ code: 'ET', label: 'country.ethiopia', phone: '+251' },
{ code: 'FK', label: 'country.falkland_islands', phone: '+500' },
{ code: 'FO', label: 'country.faroe_islands', phone: '+298' },
{ code: 'FJ', label: 'country.fiji', phone: '+679' },
{ code: 'FI', label: 'country.finland', phone: '+358' },
{ code: 'FR', label: 'country.france', phone: '+33' },
{ code: 'GF', label: 'country.french_guiana', phone: '+594' },
{ code: 'PF', label: 'country.french_polynesia', phone: '+689' },
{ code: 'GA', label: 'country.gabon', phone: '+241' },
{ code: 'GM', label: 'country.gambia', phone: '+220' },
{ code: 'GE', label: 'country.georgia', phone: '+995' },
{ code: 'DE', label: 'country.germany', phone: '+49' },
{ code: 'GH', label: 'country.ghana', phone: '+233' },
{ code: 'GI', label: 'country.gibraltar', phone: '+350' },
{ code: 'GR', label: 'country.greece', phone: '+30' },
{ code: 'GL', label: 'country.greenland', phone: '+299' },
{ code: 'GD', label: 'country.grenada', phone: '+1473' },
{ code: 'GP', label: 'country.guadeloupe', phone: '+590' },
{ code: 'GU', label: 'country.guam', phone: '+1671' },
{ code: 'GT', label: 'country.guatemala', phone: '+502' },
{ code: 'GG', label: 'country.guernsey', phone: '+44' },
{ code: 'GN', label: 'country.guinea', phone: '+224' },
{ code: 'GW', label: 'country.guinea_bissau', phone: '+245' },
{ code: 'GY', label: 'country.guyana', phone: '+592' },
{ code: 'HT', label: 'country.haiti', phone: '+509' },
{ code: 'HN', label: 'country.honduras', phone: '+504' },
{ code: 'HK', label: 'country.hong_kong', phone: '+852' },
{ code: 'HU', label: 'country.hungary', phone: '+36' },
{ code: 'IS', label: 'country.iceland', phone: '+354' },
{ code: 'IN', label: 'country.india', phone: '+91' },
{ code: 'ID', label: 'country.indonesia', phone: '+62' },
{ code: 'IR', label: 'country.iran', phone: '+98' },
{ code: 'IQ', label: 'country.iraq', phone: '+964' },
{ code: 'IE', label: 'country.ireland', phone: '+353' },
{ code: 'IM', label: 'country.isle_of_man', phone: '+44' },
{ code: 'IL', label: 'country.israel', phone: '+972' },
{ code: 'IT', label: 'country.italy', phone: '+39' },
{ code: 'JM', label: 'country.jamaica', phone: '+1876' },
{ code: 'JP', label: 'country.japan', phone: '+81' },
{ code: 'JE', label: 'country.jersey', phone: '+44' },
{ code: 'JO', label: 'country.jordan', phone: '+962' },
{ code: 'KZ', label: 'country.kazakhstan', phone: '+7' },
{ code: 'KE', label: 'country.kenya', phone: '+254' },
{ code: 'KI', label: 'country.kiribati', phone: '+686' },
{ code: 'XK', label: 'country.kosovo', phone: '+383' },
{ code: 'KW', label: 'country.kuwait', phone: '+965' },
{ code: 'KG', label: 'country.kyrgyzstan', phone: '+996' },
{ code: 'LA', label: 'country.laos', phone: '+856' },
{ code: 'LV', label: 'country.latvia', phone: '+371' },
{ code: 'LB', label: 'country.lebanon', phone: '+961' },
{ code: 'LS', label: 'country.lesotho', phone: '+266' },
{ code: 'LR', label: 'country.liberia', phone: '+231' },
{ code: 'LY', label: 'country.libya', phone: '+218' },
{ code: 'LI', label: 'country.liechtenstein', phone: '+423' },
{ code: 'LT', label: 'country.lithuania', phone: '+370' },
{ code: 'LU', label: 'country.luxembourg', phone: '+352' },
{ code: 'MO', label: 'country.macau', phone: '+853' },
{ code: 'MG', label: 'country.madagascar', phone: '+261' },
{ code: 'MW', label: 'country.malawi', phone: '+265' },
{ code: 'MY', label: 'country.malaysia', phone: '+60' },
{ code: 'MV', label: 'country.maldives', phone: '+960' },
{ code: 'ML', label: 'country.mali', phone: '+223' },
{ code: 'MT', label: 'country.malta', phone: '+356' },
{ code: 'MH', label: 'country.marshall_islands', phone: '+692' },
{ code: 'MQ', label: 'country.martinique', phone: '+596' },
{ code: 'MR', label: 'country.mauritania', phone: '+222' },
{ code: 'MU', label: 'country.mauritius', phone: '+230' },
{ code: 'YT', label: 'country.mayotte', phone: '+262' },
{ code: 'MX', label: 'country.mexico', phone: '+52' },
{ code: 'FM', label: 'country.micronesia', phone: '+691' },
{ code: 'MD', label: 'country.moldova', phone: '+373' },
{ code: 'MC', label: 'country.monaco', phone: '+377' },
{ code: 'MN', label: 'country.mongolia', phone: '+976' },
{ code: 'ME', label: 'country.montenegro', phone: '+382' },
{ code: 'MS', label: 'country.montserrat', phone: '+1664' },
{ code: 'MA', label: 'country.morocco', phone: '+212' },
{ code: 'MZ', label: 'country.mozambique', phone: '+258' },
{ code: 'MM', label: 'country.myanmar', phone: '+95' },
{ code: 'NA', label: 'country.namibia', phone: '+264' },
{ code: 'NR', label: 'country.nauru', phone: '+674' },
{ code: 'NP', label: 'country.nepal', phone: '+977' },
{ code: 'NL', label: 'country.netherlands', phone: '+31' },
{ code: 'NC', label: 'country.new_caledonia', phone: '+687' },
{ code: 'NZ', label: 'country.new_zealand', phone: '+64' },
{ code: 'NI', label: 'country.nicaragua', phone: '+505' },
{ code: 'NE', label: 'country.niger', phone: '+227' },
{ code: 'NG', label: 'country.nigeria', phone: '+234' },
{ code: 'NU', label: 'country.niue', phone: '+683' },
{ code: 'NF', label: 'country.norfolk_island', phone: '+672' },
{ code: 'KP', label: 'country.north_korea', phone: '+850' },
{ code: 'MK', label: 'country.north_macedonia', phone: '+389' },
{ code: 'MP', label: 'country.northern_mariana_islands', phone: '+1670' },
{ code: 'NO', label: 'country.norway', phone: '+47' },
{ code: 'OM', label: 'country.oman', phone: '+968' },
{ code: 'PK', label: 'country.pakistan', phone: '+92' },
{ code: 'PW', label: 'country.palau', phone: '+680' },
{ code: 'PS', label: 'country.palestine', phone: '+970' },
{ code: 'PA', label: 'country.panama', phone: '+507' },
{ code: 'PG', label: 'country.papua_new_guinea', phone: '+675' },
{ code: 'PY', label: 'country.paraguay', phone: '+595' },
{ code: 'PE', label: 'country.peru', phone: '+51' },
{ code: 'PH', label: 'country.philippines', phone: '+63' },
{ code: 'PN', label: 'country.pitcairn_islands', phone: '+64' },
{ code: 'PL', label: 'country.poland', phone: '+48' },
{ code: 'PT', label: 'country.portugal', phone: '+351' },
{ code: 'PR', label: 'country.puerto_rico', phone: '+1' },
{ code: 'QA', label: 'country.qatar', phone: '+974' },
{ code: 'RE', label: 'country.reunion', phone: '+262' },
{ code: 'RO', label: 'country.romania', phone: '+40' },
{ code: 'RU', label: 'country.russia', phone: '+7' },
{ code: 'RW', label: 'country.rwanda', phone: '+250' },
{ code: 'BL', label: 'country.saint_barthelemy', phone: '+590' },
{ code: 'SH', label: 'country.saint_helena', phone: '+290' },
{ code: 'KN', label: 'country.saint_kitts_and_nevis', phone: '+1869' },
{ code: 'LC', label: 'country.saint_lucia', phone: '+1758' },
{ code: 'MF', label: 'country.saint_martin', phone: '+590' },
{ code: 'PM', label: 'country.saint_pierre_and_miquelon', phone: '+508' },
{
code: 'VC',
label: 'country.saint_vincent_and_the_grenadines',
phone: '+1784',
},
{ code: 'WS', label: 'country.samoa', phone: '+685' },
{ code: 'SM', label: 'country.san_marino', phone: '+378' },
{ code: 'ST', label: 'country.sao_tome_and_principe', phone: '+239' },
{ code: 'SA', label: 'country.saudi_arabia', phone: '+966' },
{ code: 'SN', label: 'country.senegal', phone: '+221' },
{ code: 'RS', label: 'country.serbia', phone: '+381' },
{ code: 'SC', label: 'country.seychelles', phone: '+248' },
{ code: 'SL', label: 'country.sierra_leone', phone: '+232' },
{ code: 'SG', label: 'country.singapore', phone: '+65' },
{ code: 'SX', label: 'country.sint_maarten', phone: '+1721' },
{ code: 'SK', label: 'country.slovakia', phone: '+421' },
{ code: 'SI', label: 'country.slovenia', phone: '+386' },
{ code: 'SB', label: 'country.solomon_islands', phone: '+677' },
{ code: 'SO', label: 'country.somalia', phone: '+252' },
{ code: 'ZA', label: 'country.south_africa', phone: '+27' },
{
code: 'GS',
label: 'country.south_georgia_and_south_sandwich_islands',
phone: '+500',
},
{ code: 'KR', label: 'country.south_korea', phone: '+82' },
{ code: 'SS', label: 'country.south_sudan', phone: '+211' },
{ code: 'ES', label: 'country.spain', phone: '+34' },
{ code: 'LK', label: 'country.sri_lanka', phone: '+94' },
{ code: 'SD', label: 'country.sudan', phone: '+249' },
{ code: 'SR', label: 'country.suriname', phone: '+597' },
{ code: 'SJ', label: 'country.svalbard_and_jan_mayen', phone: '+47' },
{ code: 'SE', label: 'country.sweden', phone: '+46' },
{ code: 'CH', label: 'country.switzerland', phone: '+41' },
{ code: 'SY', label: 'country.syria', phone: '+963' },
{ code: 'TW', label: 'country.taiwan', phone: '+886' },
{ code: 'TJ', label: 'country.tajikistan', phone: '+992' },
{ code: 'TZ', label: 'country.tanzania', phone: '+255' },
{ code: 'TH', label: 'country.thailand', phone: '+66' },
{ code: 'TL', label: 'country.timor_leste', phone: '+670' },
{ code: 'TG', label: 'country.togo', phone: '+228' },
{ code: 'TK', label: 'country.tokelau', phone: '+690' },
{ code: 'TO', label: 'country.tonga', phone: '+676' },
{ code: 'TT', label: 'country.trinidad_and_tobago', phone: '+1868' },
{ code: 'TN', label: 'country.tunisia', phone: '+216' },
{ code: 'TR', label: 'country.turkey', phone: '+90' },
{ code: 'TM', label: 'country.turkmenistan', phone: '+993' },
{ code: 'TC', label: 'country.turks_and_caicos_islands', phone: '+1649' },
{ code: 'TV', label: 'country.tuvalu', phone: '+688' },
{ code: 'VI', label: 'country.us_virgin_islands', phone: '+1340' },
{ code: 'UG', label: 'country.uganda', phone: '+256' },
{ code: 'UA', label: 'country.ukraine', phone: '+380' },
{ code: 'AE', label: 'country.united_arab_emirates', phone: '+971' },
{ code: 'GB', label: 'country.united_kingdom', phone: '+44' },
{ code: 'US', label: 'country.united_states', phone: '+1' },
{ code: 'UY', label: 'country.uruguay', phone: '+598' },
{ code: 'UZ', label: 'country.uzbekistan', phone: '+998' },
{ code: 'VU', label: 'country.vanuatu', phone: '+678' },
{ code: 'VA', label: 'country.vatican_city', phone: '+39' },
{ code: 'VE', label: 'country.venezuela', phone: '+58' },
{ code: 'VN', label: 'country.vietnam', phone: '+84' },
{ code: 'WF', label: 'country.wallis_and_futuna', phone: '+681' },
{ code: 'EH', label: 'country.western_sahara', phone: '+212' },
{ code: 'YE', label: 'country.yemen', phone: '+967' },
{ code: 'ZM', label: 'country.zambia', phone: '+260' },
{ code: 'ZW', label: 'country.zimbabwe', phone: '+263' },
];

View File

@@ -1,312 +1,5 @@
import { useState } from 'react';
import {
createBrowserRouter,
RouterProvider,
Navigate,
Outlet,
useLocation,
} from 'react-router-dom';
import { SideNav, type NavItemConfig } from '@rkheftan/harmony-ui';
import {
Devices,
LocationTick,
Mobile,
PasswordCheck,
Personalcard,
ProfileCircle,
Setting as SettingIcon,
Shield,
Sms,
Menu,
} from 'iconsax-react';
import {
Box,
Typography,
useTheme,
useMediaQuery,
Drawer,
AppBar,
Toolbar,
IconButton,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { ActiveDevices } from '../components/activeDevices/ActiveDevices';
import { Setting } from '../components/setting/Setting';
import { RecentLogins } from '../components/security/RecentLogins';
import { PasswordSecurity } from '../components/security/PasswordSecurity';
import { PersonalInformation } from '../components/userInformation/PersonalInformation';
import { PhoneNumber } from '../components/userInformation/PhoneNumber';
import { SocialMedia } from '../components/userInformation/SocialMedia';
function Header() {
const headers = [{ name: 'محمدحسین برزه گر', phone: '09123456789' }];
return (
<Box
sx={{
height: 84,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
px: 2,
}}
>
{headers.map((h) => (
<Box key={h.phone}>
<Typography variant="body2">{h.name}</Typography>
<Typography variant="body2" color="text.secondary">
{h.phone}
</Typography>
</Box>
))}
</Box>
);
}
export function Layout() {
const theme = useTheme();
const isMdUp = useMediaQuery(theme.breakpoints.up('md'));
const location = useLocation();
const { t } = useTranslation('sideMap');
const [drawerOpen, setDrawerOpen] = useState(false);
const navWidthMd = '274px';
const navConfig: NavItemConfig[] = [
{
text: t('side.account'),
getIcon: (sel) =>
sel ? (
<ProfileCircle size={24} color="#1976d2" variant="Bold" />
) : (
<ProfileCircle size={24} color="#82B1FF" />
),
path: '/profile',
children: [
{
text: t('side.personalInfo'),
getIcon: (sel) =>
sel ? (
<Personalcard size={24} color="#1976d2" variant="Bold" />
) : (
<Personalcard size={24} color="#82B1FF" />
),
path: '/profile#info',
},
{
text: t('side.phoneNumber'),
getIcon: (sel) =>
sel ? (
<Mobile size={24} color="#1976d2" variant="Bold" />
) : (
<Mobile size={24} color="#82B1FF" />
),
path: '/profile#contact-info',
},
{
text: t('side.email'),
getIcon: (sel) =>
sel ? (
<Sms size={24} color="#1976d2" variant="Bold" />
) : (
<Sms size={24} color="#82B1FF" />
),
path: '/profile#email',
},
],
},
{
text: t('side.security'),
getIcon: (sel) =>
sel ? (
<Shield size={24} color="#1976d2" variant="Bold" />
) : (
<Shield size={24} color="#82B1FF" />
),
path: '/security',
children: [
{
text: t('side.password'),
getIcon: (sel) =>
sel ? (
<PasswordCheck size={24} color="#1976d2" variant="Bold" />
) : (
<PasswordCheck size={24} color="#82B1FF" />
),
path: '/security#password',
},
{
text: t('side.verifiedAddress'),
getIcon: (sel) =>
sel ? (
<LocationTick size={24} color="#1976d2" variant="Bold" />
) : (
<LocationTick size={24} color="#82B1FF" />
),
path: '/security#locations',
},
{
text: t('side.recentLogins'),
getIcon: (sel) =>
sel ? (
<Devices size={24} color="#1976d2" variant="Bold" />
) : (
<Devices size={24} color="#82B1FF" />
),
path: '/security#sessions',
},
],
},
{
text: t('side.activeDevices'),
getIcon: (sel) =>
sel ? (
<Devices size={24} color="#1976d2" variant="Bold" />
) : (
<Devices size={24} color="#82B1FF" />
),
path: '/devices',
},
{
text: t('side.settings'),
getIcon: (sel) =>
sel ? (
<SettingIcon size={24} color="#1976d2" variant="Bold" />
) : (
<SettingIcon size={24} color="#82B1FF" />
),
path: '/setting',
},
];
return (
<Box
sx={{
minHeight: '100vh',
width: '100vw',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
px: { xs: 0, sm: 0, md: '185px' },
py: { xs: 0, sm: 0, md: '105px' },
boxSizing: 'border-box',
backgroundColor: 'background.default',
}}
>
<Box
width={{ xs: '100vw', md: 1100 }}
height="100vh"
display="flex"
flexDirection="column"
bgcolor="background.paper"
overflow="hidden"
borderRadius={{ xs: 0, md: 2 }}
boxShadow={{ xs: 0, md: 3 }}
sx={{
overflow: 'hidden',
}}
>
{!isMdUp && (
<AppBar position="static" elevation={0}>
<Toolbar sx={{ backgroundColor: 'background.paper' }}>
<IconButton edge="start" onClick={() => setDrawerOpen(true)}>
<Menu size={24} color="#1976d2" variant="Bold" />
</IconButton>
</Toolbar>
</AppBar>
)}
<Box sx={{ flexGrow: 1, display: 'flex', overflow: 'hidden' }}>
{isMdUp && (
<Box
sx={{
width: navWidthMd,
flexShrink: 0,
}}
>
<SideNav
navConfig={navConfig}
header={<Header />}
activePath={location.pathname + location.hash}
sideNavVariant="full"
positioning="absolute"
/>
</Box>
)}
<Box
sx={{
flexGrow: 1,
overflowY: 'auto',
px: { xs: 2, sm: 3 },
py: { xs: 2, sm: 3 },
}}
>
<Outlet />
</Box>
</Box>
{!isMdUp && (
<Drawer
anchor="left"
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
>
<Box width={navWidthMd}>
<SideNav
navConfig={navConfig}
header={<Header />}
activePath={location.pathname + location.hash}
sideNavVariant="full"
/>
</Box>
</Drawer>
)}
</Box>
</Box>
);
}
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ path: '/', element: <Navigate to="/profile" replace /> },
{
path: '/profile',
element: (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<div id="info">
<PersonalInformation />
</div>
<div id="contact-info">
<PhoneNumber />
</div>
<div id="email">
<SocialMedia />
</div>
</Box>
),
},
{
path: '/security',
element: (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<div id="password">
<PasswordSecurity />
</div>
<div id="locations"></div>
<div id="sessions">
<RecentLogins />
</div>
</Box>
),
},
{ path: '/devices', element: <ActiveDevices /> },
{ path: '/setting', element: <Setting /> },
],
},
]);
import { RouterProvider } from 'react-router-dom';
import { router } from '../components/layout/Routes';
export function Settings() {
return <RouterProvider router={router} />;