feat: add sidebar and change the style of that
This commit is contained in:
11
src/App.tsx
11
src/App.tsx
@@ -1,14 +1,14 @@
|
||||
import { CssBaseline, useColorScheme } from '@mui/material';
|
||||
import './App.css';
|
||||
import { LanguageManager } from './components/LanguageManager';
|
||||
|
||||
import { Settings } from './features/profile/routes/SettingPage';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<CssBaseline />
|
||||
<LanguageManager />
|
||||
|
||||
<Settings />
|
||||
{/* <div style={{ padding: '16px' }}>
|
||||
<Typography variant="h3">{t('helloWorld')}</Typography>
|
||||
<Box
|
||||
@@ -39,6 +39,13 @@ function App() {
|
||||
export default App;
|
||||
|
||||
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 = () => {
|
||||
const { mode, setMode } = useColorScheme();
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Box, Typography, Button } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DeviceMessage, Logout } from 'iconsax-react';
|
||||
import Logo from '@/components/Logo';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
|
||||
export function ActiveDevices() {
|
||||
const { t } = useTranslation('activeDevices');
|
||||
@@ -40,43 +41,29 @@ export function ActiveDevices() {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'background.paper',
|
||||
width: '100%',
|
||||
maxWidth: '834px',
|
||||
mx: 'auto',
|
||||
px: { xs: 2, sm: 3 },
|
||||
py: 4,
|
||||
backgroundColor: 'background.paper',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
py: 2,
|
||||
height: 84,
|
||||
}}
|
||||
>
|
||||
<Logo />
|
||||
</Box>
|
||||
<Box sx={{ width: '100%', height: '1px', backgroundColor: 'divider' }} />
|
||||
<Box sx={{ px: { xs: 2, sm: 3 } }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
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>
|
||||
<Box sx={{ width: '100%', height: 1, bgcolor: 'divider' }} />
|
||||
<CardContainer
|
||||
title={t('active.activeDevices')}
|
||||
subtitle={t('active.activeDevicesCaption')}
|
||||
action={
|
||||
<Button
|
||||
variant="outlined"
|
||||
sx={{
|
||||
borderRadius: '10px',
|
||||
borderColor: 'error.main',
|
||||
@@ -87,111 +74,119 @@ export function ActiveDevices() {
|
||||
>
|
||||
{t('active.deletDevicesButton')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{devices.map((device) => (
|
||||
<Box
|
||||
key={device.id}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
py: 1,
|
||||
width: '690px',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ width: { xs: '100%', sm: '138px' } }}
|
||||
>
|
||||
{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}
|
||||
}
|
||||
>
|
||||
<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) => (
|
||||
<Box
|
||||
key={device.id}
|
||||
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,
|
||||
},
|
||||
gap: 1,
|
||||
height: 50,
|
||||
}}
|
||||
>
|
||||
{t('active.deleteDevice')}
|
||||
</Button>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
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>
|
||||
</CardContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { CloseCircle, Refresh } from 'iconsax-react';
|
||||
import { CloseSquare, Refresh } from 'iconsax-react';
|
||||
import { PasswordValidationItem } from './PasswordValidation';
|
||||
import { Toast } from '@/components/Toast';
|
||||
import Logo from '@/components/Logo';
|
||||
@@ -67,187 +67,183 @@ export function PasswordSecurity() {
|
||||
}, [password, validPassword]);
|
||||
|
||||
return (
|
||||
<Box sx={{ overflowX: 'hidden', px: { xs: 2, sm: 3 } }}>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'background.paper',
|
||||
width: '100%',
|
||||
maxWidth: '796px',
|
||||
mx: 'auto',
|
||||
px: { xs: 1, sm: 2 },
|
||||
}}
|
||||
<Box
|
||||
sx={{
|
||||
mx: 'auto',
|
||||
backgroundColor: 'background.paper',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<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
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
px: { xs: 2, sm: 3, md: 4 },
|
||||
py: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
bgcolor: 'background.paper',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Logo />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{ width: '100%', height: '1px', backgroundColor: 'divider' }}
|
||||
/>
|
||||
<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')}
|
||||
<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>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<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>
|
||||
<Typography variant="h6" fontWeight="bold">
|
||||
{t('securityForm.addPassword')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
<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' } }}
|
||||
>
|
||||
<TextField
|
||||
label={t('securityForm.newPassword')}
|
||||
type="password"
|
||||
fullWidth
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
|
||||
/>
|
||||
{t('securityForm.notDeterminedPassword')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{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>
|
||||
)}
|
||||
<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}>
|
||||
<CloseSquare size={24} color="#82B1FF" />
|
||||
</IconButton>
|
||||
<Typography variant="h6" fontWeight="bold">
|
||||
{t('securityForm.addPassword')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<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)}
|
||||
<DialogContent
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
px: { xs: 2, sm: 3 },
|
||||
}}
|
||||
>
|
||||
{t('securityForm.alertSuccess')}
|
||||
</Toast>
|
||||
</CardContainer>
|
||||
<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>
|
||||
|
||||
<Toast
|
||||
color="success"
|
||||
open={showPasswordAlert}
|
||||
onClose={() => setShowPasswordAlert(false)}
|
||||
>
|
||||
{t('securityForm.alertSuccess')}
|
||||
</Toast>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Box, Typography, Button } from '@mui/material';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Logo from '@/components/Logo';
|
||||
import { CardContainer } from '@/components/CardContainer';
|
||||
|
||||
export function RecentLogins() {
|
||||
const { t } = useTranslation('security');
|
||||
@@ -22,78 +23,82 @@ export function RecentLogins() {
|
||||
];
|
||||
|
||||
return (
|
||||
<Box sx={{ overflowX: 'hidden', px: { xs: 2, sm: 3 } }}>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: 'background.paper',
|
||||
width: '100%',
|
||||
maxWidth: '796px',
|
||||
mx: 'auto',
|
||||
px: { xs: 1, sm: 2 },
|
||||
}}
|
||||
<Box
|
||||
sx={{
|
||||
mx: 'auto',
|
||||
backgroundColor: 'background.paper',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<CardContainer
|
||||
title={t('securityForm.recentLogins')}
|
||||
subtitle={t('securityForm.description')}
|
||||
>
|
||||
<Box>
|
||||
<CardContainer
|
||||
title={t('securityForm.recentLogins')}
|
||||
subtitle={t('securityForm.description')}
|
||||
>
|
||||
<Box sx={{ width: '100%', maxWidth: '754px', px: 4 }}>
|
||||
{data.map((d) => (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
// flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
height: '50px',
|
||||
}}
|
||||
key={d.id}
|
||||
<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 }}>
|
||||
{data.map((d) => (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
height: '50px',
|
||||
}}
|
||||
key={d.id}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ width: { xs: '100%', sm: '172.5px' } }}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ width: { xs: '100%', sm: '172.5px' } }}
|
||||
>
|
||||
{d.time}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ width: { xs: '100%', sm: '172.5px' } }}
|
||||
>
|
||||
{d.device}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ width: { xs: '100%', sm: '172.5px' } }}
|
||||
>
|
||||
{d.ip}
|
||||
</Typography>
|
||||
<Box sx={{ width: { xs: '100%', sm: '172.5px' } }}>
|
||||
{d.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('securityForm.currentDevice')}
|
||||
</Button>
|
||||
) : (
|
||||
<Typography></Typography>
|
||||
)}
|
||||
</Box>
|
||||
{d.time}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ width: { xs: '100%', sm: '172.5px' } }}
|
||||
>
|
||||
{d.device}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ width: { xs: '100%', sm: '172.5px' } }}
|
||||
>
|
||||
{d.ip}
|
||||
</Typography>
|
||||
<Box sx={{ width: { xs: '100%', sm: '172.5px' } }}>
|
||||
{d.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('securityForm.currentDevice')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</CardContainer>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Box } from '@mui/material';
|
||||
|
||||
export function Security() {
|
||||
return (
|
||||
<Box sx={{ backgroundColor: 'background.paper' }}>
|
||||
<Box sx={{ backgroundColor: 'background.paper', height: '730px' }}>
|
||||
<PasswordSecurity />
|
||||
<RecentLogins />
|
||||
</Box>
|
||||
|
||||
@@ -21,20 +21,19 @@ export function Setting() {
|
||||
);
|
||||
const [draftLanguage, setDraftLanguage] = useState<string>(savedLanguage);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [selectedCalendar, setSelectedCalendar] = useState<string>(
|
||||
t('settings.solar'),
|
||||
);
|
||||
|
||||
const languageOptions = [
|
||||
{ code: 'en', label: 'English' },
|
||||
{ code: 'fa', label: 'فارسی' },
|
||||
];
|
||||
|
||||
const calendarOptions = [
|
||||
t('settings.christian'),
|
||||
t('settings.solar'),
|
||||
t('settings.lunar'),
|
||||
];
|
||||
const [selectedCalendar, setSelectedCalendar] = useState<string>(
|
||||
t('settings.solar'),
|
||||
);
|
||||
|
||||
const handleDraftLanguageChange = (
|
||||
_: any,
|
||||
@@ -58,105 +57,133 @@ export function Setting() {
|
||||
isEditing ? handleSave() : setIsEditing(true);
|
||||
|
||||
return (
|
||||
<Box sx={{ px: { xs: 2, sm: 3 }, py: 4, bgcolor: 'background.default' }}>
|
||||
<Box sx={{ backgroundColor: 'background.paper' }}>
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: 790,
|
||||
mx: 'auto',
|
||||
bgcolor: 'background.paper',
|
||||
px: { xs: 1, sm: 2 },
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
py: 2,
|
||||
height: 84,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', py: 2 }}>
|
||||
<Logo />
|
||||
</Box>
|
||||
<Box sx={{ width: '100%', height: 1, bgcolor: 'divider' }} />
|
||||
<CardContainer
|
||||
title={t('settings.title')}
|
||||
subtitle={t('settings.description')}
|
||||
highlighted={isEditing}
|
||||
action={
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
{isEditing && (
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={handleCancel}
|
||||
size="large"
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
{t('settings.rejectButton')}
|
||||
</Button>
|
||||
)}
|
||||
<Logo />
|
||||
</Box>
|
||||
<Box sx={{ width: '100%', height: 1, bgcolor: 'divider' }} />
|
||||
<CardContainer
|
||||
title={t('settings.title')}
|
||||
subtitle={t('settings.description')}
|
||||
highlighted={isEditing}
|
||||
action={
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
{isEditing && (
|
||||
<Button
|
||||
onClick={handleEditToggle}
|
||||
variant="text"
|
||||
onClick={handleCancel}
|
||||
size="large"
|
||||
variant="outlined"
|
||||
sx={{ textTransform: 'none' }}
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
textTransform: 'none',
|
||||
width: { xs: '100%', sm: 'auto' },
|
||||
fontSize: { xs: '0.85rem', sm: '1rem' },
|
||||
}}
|
||||
>
|
||||
{isEditing
|
||||
? t('settings.saveButton')
|
||||
: t('settings.editButton')}
|
||||
{t('settings.rejectButton')}
|
||||
</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
|
||||
sx={{
|
||||
px: { xs: 2, sm: 3, md: 4 },
|
||||
py: 2,
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
mx: 'auto',
|
||||
width: { xs: '100%', md: 700 },
|
||||
px: 4,
|
||||
bgcolor: 'background.paper',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: 1, maxWidth: { sm: 337 }, width: '100%' }}>
|
||||
{isEditing ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<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
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
mx: 'auto',
|
||||
width: { xs: '100%', md: 700 },
|
||||
// px: 4,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: 1, maxWidth: { sm: 337 }, width: '100%' }}>
|
||||
{isEditing ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<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 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 sx={{ mt: 2, px: 4 }}>
|
||||
<Box sx={{ maxWidth: { sm: 310 }, width: '100%' }}>
|
||||
<Box sx={{ mt: 2, maxWidth: { sm: 310 }, width: '100%' }}>
|
||||
{isEditing ? (
|
||||
<Autocomplete
|
||||
options={calendarOptions}
|
||||
@@ -178,7 +205,7 @@ export function Setting() {
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -222,8 +222,8 @@ export function SocialMedia() {
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={handleCloseDialog} size="small">
|
||||
<CloseSquare size="24" color="#F5F5F5" />
|
||||
<IconButton onClick={handleCloseDialog}>
|
||||
<CloseSquare size={24} color="#82B1FF" />
|
||||
</IconButton>
|
||||
{t('settingForm.addEmailButton')}
|
||||
</DialogTitle>
|
||||
203
src/features/profile/routes/SettingPage.tsx
Normal file
203
src/features/profile/routes/SettingPage.tsx
Normal 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} />;
|
||||
}
|
||||
Reference in New Issue
Block a user