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,31 +74,40 @@ export function ActiveDevices() {
> >
{t('active.deletDevicesButton')} {t('active.deletDevicesButton')}
</Button> </Button>
</Box> }
>
<Box
sx={{
px: { xs: 2, sm: 3, md: 4 },
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
flex: 1,
}}
>
<Box sx={{ width: '100%', maxWidth: '754px', px: 4 }}>
{devices.map((device) => ( {devices.map((device) => (
<Box <Box
key={device.id} key={device.id}
sx={{ sx={{
display: 'flex', display: 'flex',
flexWrap: 'wrap',
alignItems: 'center', alignItems: 'center',
py: 1, gap: 1,
width: '690px', height: 50,
}} }}
> >
<Typography <Typography
variant="body2" variant="body2"
sx={{ width: { xs: '100%', sm: '138px' } }} sx={{ width: { xs: '100%', sm: '172.5px' } }}
> >
{device.timeAndDate} {device.timeAndDate}
</Typography> </Typography>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
width: { xs: '100%', sm: '138px' }, width: { xs: '100%', sm: '172.5px' },
gap: 1, gap: 1,
}} }}
> >
@@ -124,17 +120,15 @@ export function ActiveDevices() {
{device.deviceModel} {device.deviceModel}
</Typography> </Typography>
</Box> </Box>
<Typography <Typography
variant="body2" variant="body2"
sx={{ width: { xs: '100%', sm: '138px' } }} sx={{ width: { xs: '100%', sm: '172.5px' } }}
> >
{device.ip} {device.ip}
</Typography> </Typography>
<Box <Box
sx={{ sx={{
width: { xs: '100%', sm: '138px' }, width: { xs: '100%', sm: '172.5px' },
textAlign: { xs: 'left', sm: 'center' }, textAlign: { xs: 'left', sm: 'center' },
}} }}
> >
@@ -148,7 +142,7 @@ export function ActiveDevices() {
height: '30px', height: '30px',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
color: 'success.main', color: 'success.main',
width: '75%', width: '93px',
textTransform: 'none', textTransform: 'none',
}} }}
> >
@@ -156,10 +150,9 @@ export function ActiveDevices() {
</Button> </Button>
)} )}
</Box> </Box>
<Box <Box
sx={{ sx={{
width: { xs: '100%', sm: '138px' }, width: { xs: '100%', sm: '172.5px' },
textAlign: { xs: 'left', sm: 'center' }, textAlign: { xs: 'left', sm: 'center' },
}} }}
> >
@@ -193,5 +186,7 @@ export function ActiveDevices() {
))} ))}
</Box> </Box>
</Box> </Box>
</CardContainer>
</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,30 +67,18 @@ export function PasswordSecurity() {
}, [password, validPassword]); }, [password, validPassword]);
return ( return (
<Box sx={{ overflowX: 'hidden', px: { xs: 2, sm: 3 } }}>
<Box <Box
sx={{ sx={{
backgroundColor: 'background.paper',
width: '100%',
maxWidth: '796px',
mx: 'auto', mx: 'auto',
px: { xs: 1, sm: 2 }, backgroundColor: 'background.paper',
}}
>
<Box
sx={{
display: 'flex', display: 'flex',
justifyContent: 'flex-start', flexDirection: 'column',
alignItems: 'center',
py: 2,
}} }}
> >
<Box sx={{ display: 'flex', alignItems: 'center', py: 2, height: 84 }}>
<Logo /> <Logo />
</Box> </Box>
<Box <Box sx={{ width: '100%', height: 1, bgcolor: 'divider' }} />
sx={{ width: '100%', height: '1px', backgroundColor: 'divider' }}
/>
<Box>
<CardContainer <CardContainer
title={t('securityForm.password')} title={t('securityForm.password')}
subtitle={t('securityForm.determinePassword')} subtitle={t('securityForm.determinePassword')}
@@ -109,6 +97,17 @@ export function PasswordSecurity() {
{t('securityForm.addPassword')} {t('securityForm.addPassword')}
</Button> </Button>
} }
>
<Box
sx={{
px: { xs: 2, sm: 3, md: 4 },
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
bgcolor: 'background.paper',
flex: 1,
}}
> >
<Box sx={{ height: 'auto', py: { xs: 3, sm: 4 } }}> <Box sx={{ height: 'auto', py: { xs: 3, sm: 4 } }}>
{changePassword ? ( {changePassword ? (
@@ -143,7 +142,7 @@ export function PasswordSecurity() {
<DialogTitle sx={{ p: 2 }}> <DialogTitle sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton onClick={handleClose}> <IconButton onClick={handleClose}>
<CloseCircle size={24} color="#82B1FF" /> <CloseSquare size={24} color="#82B1FF" />
</IconButton> </IconButton>
<Typography variant="h6" fontWeight="bold"> <Typography variant="h6" fontWeight="bold">
{t('securityForm.addPassword')} {t('securityForm.addPassword')}
@@ -213,9 +212,7 @@ export function PasswordSecurity() {
sx={{ maxWidth: '364px', height: 48, textTransform: 'none' }} sx={{ maxWidth: '364px', height: 48, textTransform: 'none' }}
variant="contained" variant="contained"
onClick={handleShowAlert} onClick={handleShowAlert}
disabled={ disabled={!password || password !== confirmPassword || loading}
!password || password !== confirmPassword || loading
}
> >
{loading ? ( {loading ? (
<Box <Box
@@ -245,9 +242,8 @@ export function PasswordSecurity() {
> >
{t('securityForm.alertSuccess')} {t('securityForm.alertSuccess')}
</Toast> </Toast>
</Box>
</CardContainer> </CardContainer>
</Box> </Box>
</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,27 +23,34 @@ export function RecentLogins() {
]; ];
return ( return (
<Box sx={{ overflowX: 'hidden', px: { xs: 2, sm: 3 } }}>
<Box <Box
sx={{ sx={{
backgroundColor: 'background.paper',
width: '100%',
maxWidth: '796px',
mx: 'auto', mx: 'auto',
px: { xs: 1, sm: 2 }, backgroundColor: 'background.paper',
display: 'flex',
flexDirection: 'column',
}} }}
> >
<Box>
<CardContainer <CardContainer
title={t('securityForm.recentLogins')} title={t('securityForm.recentLogins')}
subtitle={t('securityForm.description')} subtitle={t('securityForm.description')}
>
<Box
sx={{
px: { xs: 2, sm: 3, md: 4 },
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
bgcolor: 'background.paper',
flex: 1,
}}
> >
<Box sx={{ width: '100%', maxWidth: '754px', px: 4 }}> <Box sx={{ width: '100%', maxWidth: '754px', px: 4 }}>
{data.map((d) => ( {data.map((d) => (
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
// flexWrap: 'wrap',
alignItems: 'center', alignItems: 'center',
gap: 1, gap: 1,
height: '50px', height: '50px',
@@ -68,7 +76,7 @@ export function RecentLogins() {
{d.ip} {d.ip}
</Typography> </Typography>
<Box sx={{ width: { xs: '100%', sm: '172.5px' } }}> <Box sx={{ width: { xs: '100%', sm: '172.5px' } }}>
{d.current ? ( {d.current && (
<Button <Button
variant="outlined" variant="outlined"
sx={{ sx={{
@@ -84,16 +92,13 @@ export function RecentLogins() {
> >
{t('securityForm.currentDevice')} {t('securityForm.currentDevice')}
</Button> </Button>
) : (
<Typography></Typography>
)} )}
</Box> </Box>
</Box> </Box>
))} ))}
</Box> </Box>
</Box>
</CardContainer> </CardContainer>
</Box> </Box>
</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,16 +57,15 @@ 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' }} />
@@ -82,7 +80,12 @@ export function Setting() {
variant="text" variant="text"
onClick={handleCancel} onClick={handleCancel}
size="large" size="large"
sx={{ textTransform: 'none' }} sx={{
color: 'primary.main',
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
fontSize: { xs: '0.85rem', sm: '1rem' },
}}
> >
{t('settings.rejectButton')} {t('settings.rejectButton')}
</Button> </Button>
@@ -91,15 +94,34 @@ export function Setting() {
onClick={handleEditToggle} onClick={handleEditToggle}
size="large" size="large"
variant="outlined" variant="outlined"
sx={{ textTransform: 'none' }} 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 {isEditing ? t('settings.saveButton') : t('settings.editButton')}
? t('settings.saveButton')
: t('settings.editButton')}
</Button> </Button>
</Box> </Box>
} }
/> >
<Box
sx={{
px: { xs: 2, sm: 3, md: 4 },
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
bgcolor: 'background.paper',
}}
>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -108,7 +130,7 @@ export function Setting() {
mt: 2, mt: 2,
mx: 'auto', mx: 'auto',
width: { xs: '100%', md: 700 }, width: { xs: '100%', md: 700 },
px: 4, // px: 4,
}} }}
> >
<Box sx={{ flex: 1, maxWidth: { sm: 337 }, width: '100%' }}> <Box sx={{ flex: 1, maxWidth: { sm: 337 }, width: '100%' }}>
@@ -123,7 +145,9 @@ export function Setting() {
{t('settings.theme')} {t('settings.theme')}
</Typography> </Typography>
<Typography variant="body1"> <Typography variant="body1">
{mode === 'light' ? t('settings.light') : t('settings.dark')} {mode === 'light'
? t('settings.light')
: t('settings.dark')}
</Typography> </Typography>
</Box> </Box>
)} )}
@@ -134,7 +158,8 @@ export function Setting() {
options={languageOptions} options={languageOptions}
getOptionLabel={(o) => o.label} getOptionLabel={(o) => o.label}
value={ value={
languageOptions.find((o) => o.code === draftLanguage) || null languageOptions.find((o) => o.code === draftLanguage) ||
null
} }
onChange={handleDraftLanguageChange} onChange={handleDraftLanguageChange}
renderInput={(p) => ( renderInput={(p) => (
@@ -149,14 +174,16 @@ export function Setting() {
{t('settings.language')} {t('settings.language')}
</Typography> </Typography>
<Typography variant="body1"> <Typography variant="body1">
{languageOptions.find((o) => o.code === savedLanguage)?.label} {
languageOptions.find((o) => o.code === savedLanguage)
?.label
}
</Typography> </Typography>
</Box> </Box>
)} )}
</Box> </Box>
</Box> </Box>
<Box sx={{ mt: 2, px: 4 }}> <Box sx={{ mt: 2, maxWidth: { sm: 310 }, width: '100%' }}>
<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} />;
}