feat: add sidebar and change the style of that

This commit is contained in:
2025-08-03 12:13:53 +03:30
parent ed347d8b61
commit bf87dd8123
22 changed files with 2430 additions and 788 deletions

2
.npmrc
View File

@@ -1,2 +0,0 @@
@rkheftan:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=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

1593
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@mui/material": "^7.2.0", "@mui/material": "^7.2.0",
"@mui/stylis-plugin-rtl": "^7.2.0", "@mui/stylis-plugin-rtl": "^7.2.0",
"@rkheftan/harmony-ui": "^0.0.3", "@rkheftan/harmony-ui": "^0.1.2",
"i18next": "^25.3.0", "i18next": "^25.3.0",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",

View File

@@ -0,0 +1,14 @@
{
"side": {
"account": "Account",
"personalInfo": "Personal information",
"phoneNumber": "Phone number",
"email": "Email",
"security": "Security",
"password": "Password",
"verifiedAddress": "Verified addresses",
"recentLogins": "Recent logins",
"activeDevices": "Active devices",
"settings": "Settings"
}
}

View File

@@ -0,0 +1,14 @@
{
"side": {
"account": "حساب کاربری",
"personalInfo": "اطلاعات شخصی",
"phoneNumber": "شماره تماس",
"email": "ایمیل",
"security": "امنیت",
"password": "رمز عبور",
"verifiedAddress": "آدرس های تایید شده",
"recentLogins": "ورود های اخیر",
"activeDevices": "دستگاه های فعال",
"settings": "تنظیمات"
}
}

View File

@@ -1,14 +1,14 @@
import { CssBaseline, useColorScheme } from '@mui/material'; import { CssBaseline, useColorScheme } from '@mui/material';
import './App.css'; import './App.css';
import { LanguageManager } from './components/LanguageManager'; import { LanguageManager } from './components/LanguageManager';
import { Settings } from './features/profile/routes/SettingPage';
function App() { function App() {
return ( return (
<> <>
<CssBaseline /> <CssBaseline />
<LanguageManager /> <LanguageManager />
<Settings />
{/* <div style={{ padding: '16px' }}> {/* <div style={{ padding: '16px' }}>
<Typography variant="h3">{t('helloWorld')}</Typography> <Typography variant="h3">{t('helloWorld')}</Typography>
<Box <Box
@@ -39,6 +39,13 @@ function App() {
export default App; export default App;
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import { PersonalInformation } from './features/profile/components/userInformation/PersonalInformation';
import { PhoneNumber } from './features/profile/components/userInformation/PhoneNumber';
import { SocialMedia } from './features/profile/components/userInformation/SocialMedia';
import { PasswordSecurity } from './features/profile/components/security/PasswordSecurity';
import { RecentLogins } from './features/profile/components/security/RecentLogins';
import { ActiveDevices } from './features/profile/components/activeDevices/ActiveDevices';
import { Setting } from './features/profile/components/setting/Setting';
export const ThemeToggleButton = () => { export const ThemeToggleButton = () => {
const { mode, setMode } = useColorScheme(); const { mode, setMode } = useColorScheme();

View File

@@ -1,176 +0,0 @@
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,
} from 'iconsax-react';
import { Box, Typography, useTheme, useMediaQuery } 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';
interface DummyPageProp {
sections: { title: string; hash: string }[];
}
function DummyPage({ sections }: DummyPageProp) {
return (
<>
{sections.map(({ title, hash }) => (
<div key={hash} id={hash} style={{ height: '50vh', margin: '3rem' }}>
<Box p={3}>
<Typography variant="h4">{title}</Typography>
</Box>
</div>
))}
</>
);
}
function Header() {
return (
<Box
sx={{
height: 84,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
px: 2,
}}
>
<Typography variant="body2">محمدحسین برزهگر</Typography>
<Typography variant="body2" color="text.secondary">
09123456789
</Typography>
</Box>
);
}
function Layout() {
const theme = useTheme();
const isMdUp = useMediaQuery(theme.breakpoints.up('md'));
const location = useLocation();
const navConfig: NavItemConfig[] = [
{
text: 'حساب کاربری',
icon: <ProfileCircle size={24} />,
path: '/profile',
children: [
{
text: 'اطلاعات شخصی',
icon: <Personalcard size={24} />,
path: '/profile#info',
},
{
text: 'شماره تماس',
icon: <Mobile size={24} />,
path: '/profile#contact-info',
},
{ text: 'ایمیل', icon: <Sms size={24} />, path: '/profile#email' },
],
},
{
text: 'امنیت',
icon: <Shield size={24} />,
path: '/security',
children: [
{
text: 'رمز عبور',
icon: <PasswordCheck size={24} />,
path: '/security#password',
},
{
text: 'آدرس‌های تایید شده',
icon: <LocationTick size={24} />,
path: '/security#locations',
},
{
text: 'ورودهای اخیر',
icon: <Devices size={24} />,
path: '/security#sessions',
},
],
},
{ text: 'دستگاه‌های فعال', icon: <Devices size={24} />, path: '/devices' },
{ text: 'تنظیمات', icon: <SettingIcon size={24} />, path: '/setting' },
];
return (
<Box display="flex" flexDirection="column" minHeight="100vh">
<Box display="flex" flex={1} overflow="hidden">
<SideNav
navConfig={navConfig}
header={<Header />}
activePath={location.pathname + location.hash}
sideNavVariant={isMdUp ? 'full' : 'minimized'}
drawerWidth={274}
minimizedWidth={50}
/>
<Box
flex={1}
display="flex"
justifyContent="center"
px={{ xs: 2, sm: 3 }}
>
<Box width="100%" maxWidth={790}>
<Outlet />
</Box>
</Box>
</Box>
</Box>
);
}
const profileSections = [
{ title: 'اطلاعات شخصی', hash: 'info' },
{ title: 'شماره تماس', hash: 'contact-info' },
{ title: 'ایمیل', hash: 'email' },
];
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ path: '/', element: <Navigate to="/profile" replace /> },
{ path: '/profile', element: <DummyPage sections={profileSections} /> },
{
path: '/security',
element: (
<>
<div id="password">
<PasswordSecurity />
</div>
<div id="locations"></div>
<div id="sessions">
<RecentLogins />
</div>
</>
),
},
{ path: '/devices', element: <ActiveDevices /> },
{ path: '/setting', element: <Setting /> },
],
},
]);
export function Settings() {
useTranslation();
return <RouterProvider router={router} />;
}

View File

@@ -1,129 +0,0 @@
import React, { useState } from 'react';
import { Box, Button } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { CardContainer } from '@/components/CardContainer';
import { ProfileImage } from './personlInformation/ProfileImage';
import { InfoRowDisplay } from './personlInformation/InfoRowDisplay';
import { InfoRowEdit } from './personlInformation/InfoRowEdit';
export function PersonalInformation() {
const { t } = useTranslation('profileSetting');
const [isEditing, setIsEditing] = useState(false);
const [gender, setGender] = useState('');
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
const initialData = {
firstName: 'محمد حسین',
lastName: 'برزه‌گر',
country: 'قطر',
gender: 'مرد',
nationalCode: '',
};
const [data, setData] = useState(initialData);
const initials = `${data.firstName?.trim()[0] || ''}${data.lastName?.trim()[0] || ''}`;
const toggleEdit = () => {
setIsEditing((prev) => !prev);
if (isEditing) {
setData((prev) => ({
...prev,
gender: gender === 'male' ? 'مرد' : gender === 'female' ? 'زن' : '',
}));
} else {
setGender(
data.gender === 'مرد' ? 'male' : data.gender === 'زن' ? 'female' : '',
);
}
};
return (
<CardContainer
title={t('settingForm.titlePersonalInfo')}
subtitle={t('settingForm.descriptionPersonalInfo')}
highlighted={isEditing}
action={
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{isEditing && (
<Button
variant="text"
onClick={() => setIsEditing(false)}
size="large"
sx={{
color: 'primary.main',
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
fontSize: { xs: '0.85rem', sm: '1rem' },
}}
>
{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',
px: { xs: 2, sm: '22px' },
py: { xs: '6px', sm: '8px' },
width: { xs: '100%', sm: isEditing ? '85px' : '93px' },
fontSize: { xs: '0.9rem', sm: '1rem' },
}}
>
{isEditing
? t('settingForm.saveButton')
: t('settingForm.editButton')}
</Button>
</Box>
}
>
<Box
sx={{
px: { xs: 2, sm: 3, md: 4 },
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
bgcolor: 'background.paper',
}}
>
{isEditing && (
<ProfileImage
initials={initials}
uploadedImageUrl={uploadedImageUrl}
onImageChange={(e) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () =>
setUploadedImageUrl(reader.result as string);
reader.readAsDataURL(file);
}
}}
/>
)}
{isEditing ? (
<InfoRowEdit
data={data}
setData={setData}
gender={gender}
setGender={setGender}
/>
) : (
<InfoRowDisplay
data={data}
uploadedImageUrl={uploadedImageUrl}
initials={initials}
/>
)}
</Box>
</CardContainer>
);
}

View File

@@ -2,6 +2,7 @@ import { Box, Typography, Button } from '@mui/material';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DeviceMessage, Logout } from 'iconsax-react'; import { DeviceMessage, Logout } from 'iconsax-react';
import Logo from '@/components/Logo'; import Logo from '@/components/Logo';
import { CardContainer } from '@/components/CardContainer';
export function ActiveDevices() { export function ActiveDevices() {
const { t } = useTranslation('activeDevices'); const { t } = useTranslation('activeDevices');
@@ -40,43 +41,29 @@ export function ActiveDevices() {
return ( return (
<Box <Box
sx={{ sx={{
backgroundColor: 'background.paper',
width: '100%',
maxWidth: '834px',
mx: 'auto', mx: 'auto',
px: { xs: 2, sm: 3 }, backgroundColor: 'background.paper',
py: 4, display: 'flex',
flexDirection: 'column',
}} }}
> >
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center', alignItems: 'center',
py: 2, py: 2,
height: 84,
}} }}
> >
<Logo /> <Logo />
</Box> </Box>
<Box sx={{ width: '100%', height: '1px', backgroundColor: 'divider' }} /> <Box sx={{ width: '100%', height: 1, bgcolor: 'divider' }} />
<Box sx={{ px: { xs: 2, sm: 3 } }}> <CardContainer
<Box title={t('active.activeDevices')}
sx={{ subtitle={t('active.activeDevicesCaption')}
display: 'flex', action={
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
py: 1,
mb: 2,
}}
>
<Box>
<Typography variant="h6">{t('active.activeDevices')}</Typography>
<Typography variant="body2" color="text.secondary">
{t('active.activeDevicesCaption')}
</Typography>
</Box>
<Button <Button
variant="outlined"
sx={{ sx={{
borderRadius: '10px', borderRadius: '10px',
borderColor: 'error.main', borderColor: 'error.main',
@@ -87,111 +74,119 @@ export function ActiveDevices() {
> >
{t('active.deletDevicesButton')} {t('active.deletDevicesButton')}
</Button> </Button>
</Box> }
>
{devices.map((device) => ( <Box
<Box sx={{
key={device.id} px: { xs: 2, sm: 3, md: 4 },
sx={{ py: 2,
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexDirection: 'column',
alignItems: 'center', gap: 2,
py: 1, flex: 1,
width: '690px', }}
}} >
> <Box sx={{ width: '100%', maxWidth: '754px', px: 4 }}>
<Typography {devices.map((device) => (
variant="body2" <Box
sx={{ width: { xs: '100%', sm: '138px' } }} key={device.id}
>
{device.timeAndDate}
</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'center',
width: { xs: '100%', sm: '138px' },
gap: 1,
}}
>
<DeviceMessage size={24} color="#82B1FF" />
<Typography
variant="body2"
noWrap
sx={{ width: { xs: '100%', sm: '138px' } }}
>
{device.deviceModel}
</Typography>
</Box>
<Typography
variant="body2"
sx={{ width: { xs: '100%', sm: '138px' } }}
>
{device.ip}
</Typography>
<Box
sx={{
width: { xs: '100%', sm: '138px' },
textAlign: { xs: 'left', sm: 'center' },
}}
>
{device.current && (
<Button
variant="outlined"
sx={{
borderRadius: '15px',
border: '2px solid',
borderColor: 'success.main',
height: '30px',
whiteSpace: 'nowrap',
color: 'success.main',
width: '75%',
textTransform: 'none',
}}
>
{t('active.currentDevice')}
</Button>
)}
</Box>
<Box
sx={{
width: { xs: '100%', sm: '138px' },
textAlign: { xs: 'left', sm: 'center' },
}}
>
<Button
size="small"
variant="outlined"
startIcon={<Logout size={18} color="#E53935" />}
disabled={device.current}
sx={{ sx={{
color: 'error.main',
width: '80%',
minWidth: 0,
borderRadius: '15px',
borderColor: 'error.main',
p: '4px 8px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', gap: 1,
whiteSpace: 'nowrap', height: 50,
textTransform: 'none',
'& .MuiButton-startIcon': {
marginRight: '4px',
marginLeft: 0,
},
}} }}
> >
{t('active.deleteDevice')} <Typography
</Button> variant="body2"
</Box> sx={{ width: { xs: '100%', sm: '172.5px' } }}
>
{device.timeAndDate}
</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'center',
width: { xs: '100%', sm: '172.5px' },
gap: 1,
}}
>
<DeviceMessage size={24} color="#82B1FF" />
<Typography
variant="body2"
noWrap
sx={{ width: { xs: '100%', sm: '138px' } }}
>
{device.deviceModel}
</Typography>
</Box>
<Typography
variant="body2"
sx={{ width: { xs: '100%', sm: '172.5px' } }}
>
{device.ip}
</Typography>
<Box
sx={{
width: { xs: '100%', sm: '172.5px' },
textAlign: { xs: 'left', sm: 'center' },
}}
>
{device.current && (
<Button
variant="outlined"
sx={{
borderRadius: '15px',
border: '2px solid',
borderColor: 'success.main',
height: '30px',
whiteSpace: 'nowrap',
color: 'success.main',
width: '93px',
textTransform: 'none',
}}
>
{t('active.currentDevice')}
</Button>
)}
</Box>
<Box
sx={{
width: { xs: '100%', sm: '172.5px' },
textAlign: { xs: 'left', sm: 'center' },
}}
>
<Button
size="small"
variant="outlined"
startIcon={<Logout size={18} color="#E53935" />}
disabled={device.current}
sx={{
color: 'error.main',
width: '80%',
minWidth: 0,
borderRadius: '15px',
borderColor: 'error.main',
p: '4px 8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
whiteSpace: 'nowrap',
textTransform: 'none',
'& .MuiButton-startIcon': {
marginRight: '4px',
marginLeft: 0,
},
}}
>
{t('active.deleteDevice')}
</Button>
</Box>
</Box>
))}
</Box> </Box>
))} </Box>
</Box> </CardContainer>
</Box> </Box>
); );
} }

View File

@@ -13,7 +13,7 @@ import {
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { CloseCircle, Refresh } from 'iconsax-react'; import { CloseSquare, Refresh } from 'iconsax-react';
import { PasswordValidationItem } from './PasswordValidation'; import { PasswordValidationItem } from './PasswordValidation';
import { Toast } from '@/components/Toast'; import { Toast } from '@/components/Toast';
import Logo from '@/components/Logo'; import Logo from '@/components/Logo';
@@ -67,187 +67,183 @@ export function PasswordSecurity() {
}, [password, validPassword]); }, [password, validPassword]);
return ( return (
<Box sx={{ overflowX: 'hidden', px: { xs: 2, sm: 3 } }}> <Box
<Box sx={{
sx={{ mx: 'auto',
backgroundColor: 'background.paper', backgroundColor: 'background.paper',
width: '100%', display: 'flex',
maxWidth: '796px', flexDirection: 'column',
mx: 'auto', }}
px: { xs: 1, sm: 2 }, >
}} <Box sx={{ display: 'flex', alignItems: 'center', py: 2, height: 84 }}>
<Logo />
</Box>
<Box sx={{ width: '100%', height: 1, bgcolor: 'divider' }} />
<CardContainer
title={t('securityForm.password')}
subtitle={t('securityForm.determinePassword')}
action={
<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>
}
> >
<Box <Box
sx={{ sx={{
display: 'flex', px: { xs: 2, sm: 3, md: 4 },
justifyContent: 'flex-start',
alignItems: 'center',
py: 2, py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
bgcolor: 'background.paper',
flex: 1,
}} }}
> >
<Logo /> <Box sx={{ height: 'auto', py: { xs: 3, sm: 4 } }}>
</Box> {changePassword ? (
<Box <Box sx={{ flexDirection: 'column', px: 2, py: 2 }}>
sx={{ width: '100%', height: '1px', backgroundColor: 'divider' }} <Typography variant="h6">
/> {t('securityForm.activePassword')}
<Box>
<CardContainer
title={t('securityForm.password')}
subtitle={t('securityForm.determinePassword')}
action={
<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>
}
>
<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')}
</Typography> </Typography>
)} <Typography variant="caption" color="text.secondary">
</Box> {t('securityForm.lastChange')}
</Typography>
<Dialog </Box>
open={open} ) : (
onClose={handleClose} <Typography
fullWidth variant="body1"
fullScreen={fullScreen} color="text.secondary"
maxWidth="xs" sx={{ textAlign: 'center', py: { xs: 4, sm: '43.5px' } }}
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>
<DialogContent
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 3 },
}}
> >
<TextField {t('securityForm.notDeterminedPassword')}
label={t('securityForm.newPassword')} </Typography>
type="password" )}
fullWidth </Box>
value={password}
onChange={(e) => setPassword(e.target.value)}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
/>
{password && showValidation && ( <Dialog
<Box sx={{ display: 'flex', justifyContent: 'center' }}> open={open}
<Box sx={{ maxWidth: '364px', width: '100%' }}> onClose={handleClose}
<PasswordValidationItem fullWidth
isValid={hasNumber} fullScreen={fullScreen}
label={t('securityForm.hasNumber')} maxWidth="xs"
/> scroll="body"
<PasswordValidationItem PaperProps={{ sx: { mx: 1 } }}
isValid={hasMinLength} >
label={t('securityForm.hasMinLength')} <DialogTitle sx={{ p: 2 }}>
/> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<PasswordValidationItem <IconButton onClick={handleClose}>
isValid={hasUpperAndLower} <CloseSquare size={24} color="#82B1FF" />
label={t('securityForm.hasUpperAndLower')} </IconButton>
/> <Typography variant="h6" fontWeight="bold">
<PasswordValidationItem {t('securityForm.addPassword')}
isValid={hasSpecialChar} </Typography>
label={t('securityForm.hasSpecialChar')} </Box>
/> </DialogTitle>
</Box>
</Box>
)}
<TextField <DialogContent
label={t('securityForm.confirmPassword')} sx={{
type="password" display: 'flex',
fullWidth flexDirection: 'column',
value={confirmPassword} gap: 2,
onChange={(e) => setConfirmPassword(e.target.value)} px: { xs: 2, sm: 3 },
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>
<Toast
color="success"
open={showPasswordAlert}
onClose={() => setShowPasswordAlert(false)}
> >
{t('securityForm.alertSuccess')} <TextField
</Toast> label={t('securityForm.newPassword')}
</CardContainer> 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>
<Toast
color="success"
open={showPasswordAlert}
onClose={() => setShowPasswordAlert(false)}
>
{t('securityForm.alertSuccess')}
</Toast>
</Box> </Box>
</Box> </CardContainer>
</Box> </Box>
); );
} }

View File

@@ -1,6 +1,7 @@
import { Box, Typography, Button } from '@mui/material'; import { Box, Typography, Button } from '@mui/material';
import { CardContainer } from '@/components/CardContainer';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Logo from '@/components/Logo';
import { CardContainer } from '@/components/CardContainer';
export function RecentLogins() { export function RecentLogins() {
const { t } = useTranslation('security'); const { t } = useTranslation('security');
@@ -22,78 +23,82 @@ export function RecentLogins() {
]; ];
return ( return (
<Box sx={{ overflowX: 'hidden', px: { xs: 2, sm: 3 } }}> <Box
<Box sx={{
sx={{ mx: 'auto',
backgroundColor: 'background.paper', backgroundColor: 'background.paper',
width: '100%', display: 'flex',
maxWidth: '796px', flexDirection: 'column',
mx: 'auto', }}
px: { xs: 1, sm: 2 }, >
}} <CardContainer
title={t('securityForm.recentLogins')}
subtitle={t('securityForm.description')}
> >
<Box> <Box
<CardContainer sx={{
title={t('securityForm.recentLogins')} px: { xs: 2, sm: 3, md: 4 },
subtitle={t('securityForm.description')} py: 2,
> display: 'flex',
<Box sx={{ width: '100%', maxWidth: '754px', px: 4 }}> flexDirection: 'column',
{data.map((d) => ( gap: 2,
<Box bgcolor: 'background.paper',
sx={{ flex: 1,
display: 'flex', }}
// flexWrap: 'wrap', >
alignItems: 'center', <Box sx={{ width: '100%', maxWidth: '754px', px: 4 }}>
gap: 1, {data.map((d) => (
height: '50px', <Box
}} sx={{
key={d.id} display: 'flex',
alignItems: 'center',
gap: 1,
height: '50px',
}}
key={d.id}
>
<Typography
variant="body2"
sx={{ width: { xs: '100%', sm: '172.5px' } }}
> >
<Typography {d.time}
variant="body2" </Typography>
sx={{ width: { xs: '100%', sm: '172.5px' } }} <Typography
> variant="body2"
{d.time} sx={{ width: { xs: '100%', sm: '172.5px' } }}
</Typography> >
<Typography {d.device}
variant="body2" </Typography>
sx={{ width: { xs: '100%', sm: '172.5px' } }} <Typography
> variant="body2"
{d.device} sx={{ width: { xs: '100%', sm: '172.5px' } }}
</Typography> >
<Typography {d.ip}
variant="body2" </Typography>
sx={{ width: { xs: '100%', sm: '172.5px' } }} <Box sx={{ width: { xs: '100%', sm: '172.5px' } }}>
> {d.current && (
{d.ip} <Button
</Typography> variant="outlined"
<Box sx={{ width: { xs: '100%', sm: '172.5px' } }}> sx={{
{d.current ? ( borderRadius: '15px',
<Button border: '2px solid',
variant="outlined" borderColor: 'success.main',
sx={{ height: '30px',
borderRadius: '15px', whiteSpace: 'nowrap',
border: '2px solid', color: 'success.main',
borderColor: 'success.main', width: '93px',
height: '30px', textTransform: 'none',
whiteSpace: 'nowrap', }}
color: 'success.main', >
width: '93px', {t('securityForm.currentDevice')}
textTransform: 'none', </Button>
}} )}
>
{t('securityForm.currentDevice')}
</Button>
) : (
<Typography></Typography>
)}
</Box>
</Box> </Box>
))} </Box>
</Box> ))}
</CardContainer> </Box>
</Box> </Box>
</Box> </CardContainer>
</Box> </Box>
); );
} }

View File

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

View File

@@ -21,20 +21,19 @@ export function Setting() {
); );
const [draftLanguage, setDraftLanguage] = useState<string>(savedLanguage); const [draftLanguage, setDraftLanguage] = useState<string>(savedLanguage);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [selectedCalendar, setSelectedCalendar] = useState<string>(
t('settings.solar'),
);
const languageOptions = [ const languageOptions = [
{ code: 'en', label: 'English' }, { code: 'en', label: 'English' },
{ code: 'fa', label: 'فارسی' }, { code: 'fa', label: 'فارسی' },
]; ];
const calendarOptions = [ const calendarOptions = [
t('settings.christian'), t('settings.christian'),
t('settings.solar'), t('settings.solar'),
t('settings.lunar'), t('settings.lunar'),
]; ];
const [selectedCalendar, setSelectedCalendar] = useState<string>(
t('settings.solar'),
);
const handleDraftLanguageChange = ( const handleDraftLanguageChange = (
_: any, _: any,
@@ -58,105 +57,133 @@ export function Setting() {
isEditing ? handleSave() : setIsEditing(true); isEditing ? handleSave() : setIsEditing(true);
return ( return (
<Box sx={{ px: { xs: 2, sm: 3 }, py: 4, bgcolor: 'background.default' }}> <Box sx={{ backgroundColor: 'background.paper' }}>
<Box <Box
sx={{ sx={{
maxWidth: 790, display: 'flex',
mx: 'auto', alignItems: 'center',
bgcolor: 'background.paper', py: 2,
px: { xs: 1, sm: 2 }, height: 84,
}} }}
> >
<Box sx={{ display: 'flex', alignItems: 'center', py: 2 }}> <Logo />
<Logo /> </Box>
</Box> <Box sx={{ width: '100%', height: 1, bgcolor: 'divider' }} />
<Box sx={{ width: '100%', height: 1, bgcolor: 'divider' }} /> <CardContainer
<CardContainer title={t('settings.title')}
title={t('settings.title')} subtitle={t('settings.description')}
subtitle={t('settings.description')} highlighted={isEditing}
highlighted={isEditing} action={
action={ <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}> {isEditing && (
{isEditing && (
<Button
variant="text"
onClick={handleCancel}
size="large"
sx={{ textTransform: 'none' }}
>
{t('settings.rejectButton')}
</Button>
)}
<Button <Button
onClick={handleEditToggle} variant="text"
onClick={handleCancel}
size="large" size="large"
variant="outlined" sx={{
sx={{ textTransform: 'none' }} color: 'primary.main',
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
fontSize: { xs: '0.85rem', sm: '1rem' },
}}
> >
{isEditing {t('settings.rejectButton')}
? t('settings.saveButton')
: t('settings.editButton')}
</Button> </Button>
</Box> )}
} <Button
/> onClick={handleEditToggle}
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',
px: { xs: 2, sm: '22px' },
py: { xs: '6px', sm: '8px' },
width: { xs: '100%', sm: isEditing ? '85px' : '93px' },
fontSize: { xs: '0.9rem', sm: '1rem' },
}}
>
{isEditing ? t('settings.saveButton') : t('settings.editButton')}
</Button>
</Box>
}
>
<Box <Box
sx={{ sx={{
px: { xs: 2, sm: 3, md: 4 },
py: 2,
display: 'flex', display: 'flex',
flexDirection: { xs: 'column', sm: 'row' }, flexDirection: 'column',
gap: 2, gap: 2,
mt: 2, bgcolor: 'background.paper',
mx: 'auto',
width: { xs: '100%', md: 700 },
px: 4,
}} }}
> >
<Box sx={{ flex: 1, maxWidth: { sm: 337 }, width: '100%' }}> <Box
{isEditing ? ( sx={{
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> display: 'flex',
<Typography variant="body1">{t('settings.theme')}</Typography> flexDirection: { xs: 'column', sm: 'row' },
<ThemeToggleButton /> gap: 2,
</Box> mt: 2,
) : ( mx: 'auto',
<Box> width: { xs: '100%', md: 700 },
<Typography variant="caption" color="text.secondary"> // px: 4,
{t('settings.theme')} }}
</Typography> >
<Typography variant="body1"> <Box sx={{ flex: 1, maxWidth: { sm: 337 }, width: '100%' }}>
{mode === 'light' ? t('settings.light') : t('settings.dark')} {isEditing ? (
</Typography> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
</Box> <Typography variant="body1">{t('settings.theme')}</Typography>
)} <ThemeToggleButton />
</Box>
) : (
<Box>
<Typography variant="caption" color="text.secondary">
{t('settings.theme')}
</Typography>
<Typography variant="body1">
{mode === 'light'
? t('settings.light')
: t('settings.dark')}
</Typography>
</Box>
)}
</Box>
<Box sx={{ flex: 1, maxWidth: { sm: 337 }, width: '100%' }}>
{isEditing ? (
<Autocomplete
options={languageOptions}
getOptionLabel={(o) => o.label}
value={
languageOptions.find((o) => o.code === draftLanguage) ||
null
}
onChange={handleDraftLanguageChange}
renderInput={(p) => (
<TextField {...p} label={t('settings.language')} />
)}
size="medium"
fullWidth
/>
) : (
<Box>
<Typography variant="caption" color="text.secondary">
{t('settings.language')}
</Typography>
<Typography variant="body1">
{
languageOptions.find((o) => o.code === savedLanguage)
?.label
}
</Typography>
</Box>
)}
</Box>
</Box> </Box>
<Box sx={{ flex: 1, maxWidth: { sm: 337 }, width: '100%' }}> <Box sx={{ mt: 2, maxWidth: { sm: 310 }, width: '100%' }}>
{isEditing ? (
<Autocomplete
options={languageOptions}
getOptionLabel={(o) => o.label}
value={
languageOptions.find((o) => o.code === draftLanguage) || null
}
onChange={handleDraftLanguageChange}
renderInput={(p) => (
<TextField {...p} label={t('settings.language')} />
)}
size="medium"
fullWidth
/>
) : (
<Box>
<Typography variant="caption" color="text.secondary">
{t('settings.language')}
</Typography>
<Typography variant="body1">
{languageOptions.find((o) => o.code === savedLanguage)?.label}
</Typography>
</Box>
)}
</Box>
</Box>
<Box sx={{ mt: 2, px: 4 }}>
<Box sx={{ maxWidth: { sm: 310 }, width: '100%' }}>
{isEditing ? ( {isEditing ? (
<Autocomplete <Autocomplete
options={calendarOptions} options={calendarOptions}
@@ -178,7 +205,7 @@ export function Setting() {
)} )}
</Box> </Box>
</Box> </Box>
</Box> </CardContainer>
</Box> </Box>
); );
} }

View File

@@ -0,0 +1,143 @@
import { useState } from 'react';
import { Box, Button } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { CardContainer } from '@/components/CardContainer';
import { ProfileImage } from './personlInformation/ProfileImage';
import { InfoRowDisplay } from './personlInformation/InfoRowDisplay';
import { InfoRowEdit } from './personlInformation/InfoRowEdit';
import Logo from '@/components/Logo';
export function PersonalInformation() {
const { t } = useTranslation('profileSetting');
const [isEditing, setIsEditing] = useState(false);
const [gender, setGender] = useState('');
const [uploadedImageUrl, setUploadedImageUrl] = useState<string | null>(null);
const initialData = {
firstName: 'محمد حسین',
lastName: 'برزه‌گر',
country: 'قطر',
gender: 'مرد',
nationalCode: '',
};
const [data, setData] = useState(initialData);
const initials = `${data.firstName?.trim()[0] || ''}${data.lastName?.trim()[0] || ''}`;
const toggleEdit = () => {
setIsEditing((prev) => !prev);
if (isEditing) {
setData((prev) => ({
...prev,
gender: gender === 'male' ? 'مرد' : gender === 'female' ? 'زن' : '',
}));
} else {
setGender(
data.gender === 'مرد' ? 'male' : data.gender === 'زن' ? 'female' : '',
);
}
};
return (
<Box sx={{ backgroundColor: 'background.paper' }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
py: 2,
height: 84,
}}
>
<Logo />
</Box>
<Box sx={{ width: '100%', height: 1, bgcolor: 'divider' }} />
<CardContainer
title={t('settingForm.titlePersonalInfo')}
subtitle={t('settingForm.descriptionPersonalInfo')}
highlighted={isEditing}
action={
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{isEditing && (
<Button
variant="text"
onClick={() => setIsEditing(false)}
size="large"
sx={{
color: 'primary.main',
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
fontSize: { xs: '0.85rem', sm: '1rem' },
}}
>
{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',
px: { xs: 2, sm: '22px' },
py: { xs: '6px', sm: '8px' },
width: { xs: '100%', sm: isEditing ? '85px' : '93px' },
fontSize: { xs: '0.9rem', sm: '1rem' },
}}
>
{isEditing
? t('settingForm.saveButton')
: t('settingForm.editButton')}
</Button>
</Box>
}
>
<Box
sx={{
px: { xs: 2, sm: 3, md: 4 },
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
bgcolor: 'background.paper',
}}
>
{isEditing && (
<ProfileImage
initials={initials}
uploadedImageUrl={uploadedImageUrl}
onImageChange={(e) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () =>
setUploadedImageUrl(reader.result as string);
reader.readAsDataURL(file);
}
}}
/>
)}
{isEditing ? (
<InfoRowEdit
data={data}
setData={setData}
gender={gender}
setGender={setGender}
/>
) : (
<InfoRowDisplay
data={data}
uploadedImageUrl={uploadedImageUrl}
initials={initials}
/>
)}
</Box>
</CardContainer>
</Box>
);
}

View File

@@ -222,8 +222,8 @@ export function SocialMedia() {
gap: 1, gap: 1,
}} }}
> >
<IconButton onClick={handleCloseDialog} size="small"> <IconButton onClick={handleCloseDialog}>
<CloseSquare size="24" color="#F5F5F5" /> <CloseSquare size={24} color="#82B1FF" />
</IconButton> </IconButton>
{t('settingForm.addEmailButton')} {t('settingForm.addEmailButton')}
</DialogTitle> </DialogTitle>

View File

@@ -0,0 +1,203 @@
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,
} from 'iconsax-react';
import { Box, Typography, useTheme, useMediaQuery } 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 drawerWidth = 274;
const minimizedWidth = 64;
const navWidth = isMdUp ? drawerWidth : minimizedWidth;
const contentWidth = 790;
const totalWidth = navWidth + contentWidth;
const navConfig: NavItemConfig[] = [
{
text: t('side.account'),
icon: <ProfileCircle size={24} />,
path: '/profile',
children: [
{
text: t('side.personalInfo'),
icon: <Personalcard size={24} />,
path: '/profile#info',
},
{
text: t('side.phoneNumber'),
icon: <Mobile size={24} />,
path: '/profile#contact-info',
},
{
text: t('side.email'),
icon: <Sms size={24} />,
path: '/profile#email',
},
],
},
{
text: t('side.security'),
icon: <Shield size={24} />,
path: '/security',
children: [
{
text: t('side.password'),
icon: <PasswordCheck size={24} />,
path: '/security#password',
},
{
text: t('side.verifiedAddress'),
icon: <LocationTick size={24} />,
path: '/security#locations',
},
{
text: t('side.recentLogins'),
icon: <Devices size={24} />,
path: '/security#sessions',
},
],
},
{
text: t('side.activeDevices'),
icon: <Devices size={24} />,
path: '/devices',
},
{
text: t('side.settings'),
icon: <SettingIcon size={24} />,
path: '/setting',
},
];
return (
<Box display="flex" flexDirection="column" minHeight="100vh">
<Box>
<Box
width={totalWidth}
maxWidth="100%"
mx="auto"
display="flex"
flexDirection="row"
>
<Box position="relative" width={navWidth}>
<SideNav
navConfig={navConfig}
header={<Header />}
activePath={location.pathname + location.hash}
sideNavVariant={isMdUp ? 'full' : 'minimized'}
positioning="absolute"
// drawerWidth={drawerWidth}
// minimizedWidth={minimizedWidth}
/>
</Box>
<Box
// flex={1}
maxWidth={contentWidth}
width="100%"
// px={{ xs: 2, sm: 3 }}
>
<Outlet />
</Box>
</Box>
</Box>
</Box>
);
}
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ path: '/', element: <Navigate to="/profile" replace /> },
{
path: '/profile',
element: (
<>
<div id="info">
<PersonalInformation />
</div>
<div id="contact-info">
<PhoneNumber />
</div>
<div id="email">
<SocialMedia />
</div>
</>
),
},
{
path: '/security',
element: (
<>
<div id="password">
<PasswordSecurity />
</div>
<div id="locations"></div>
<div id="sessions">
<RecentLogins />
</div>
</>
),
},
{ path: '/devices', element: <ActiveDevices /> },
{ path: '/setting', element: <Setting /> },
],
},
]);
export function Settings() {
return <RouterProvider router={router} />;
}