diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..e301f93 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +@rkheftan:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=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 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 24da072..fd2c54d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@emotion/styled": "^11.14.1", "@mui/material": "^7.2.0", "@mui/stylis-plugin-rtl": "^7.2.0", + "@rkheftan/harmony-ui": "^0.0.3", "i18next": "^25.3.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", @@ -1484,6 +1485,20 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@rkheftan/harmony-ui": { + "version": "0.0.3", + "resolved": "https://npm.pkg.github.com/download/@rkheftan/harmony-ui/0.0.3/68af74cdbd6fce03e7d7fba0dee51f95bccd87ad", + "integrity": "sha512-ZH9zroehY4tVszlcz04L9X32AhgsXGiLSUbnYDL7I80DI5VcRYS0EirQkwIun4gnZI0ncoTX6pKlQOwCFK+8bw==", + "peerDependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "^7.2.0", + "iconsax-reactjs": "^0.0.8", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.7.1" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.19", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", @@ -2403,6 +2418,16 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -3117,6 +3142,16 @@ "react": "*" } }, + "node_modules/iconsax-reactjs": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/iconsax-reactjs/-/iconsax-reactjs-0.0.8.tgz", + "integrity": "sha512-cb+uTMxbkSFNbu8ZclX7BWQVfOWQt8+m/PsDjnsm/H+mcYrnfTYMjHxiof1FB43k7UAgt1ds+0oFeMVKdqyslw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "*" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3790,6 +3825,46 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.1.tgz", + "integrity": "sha512-jVKHXoWRIsD/qS6lvGveckwb862EekvapdHJN/cGmzw40KnJH5gg53ujOJ4qX6EKIK9LSBfFed/xiQ5yeXNrUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.7.1.tgz", + "integrity": "sha512-bavdk2BA5r3MYalGKZ01u8PGuDBloQmzpBZVhDLrOOv1N943Wq6dcM9GhB3x8b7AbqPMEezauv4PeGkAJfy7FQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "react-router": "7.7.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -3936,6 +4011,13 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT", + "peer": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index b2ffcf4..7724dc2 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@emotion/styled": "^11.14.1", "@mui/material": "^7.2.0", "@mui/stylis-plugin-rtl": "^7.2.0", + "@rkheftan/harmony-ui": "^0.0.3", "i18next": "^25.3.0", "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", diff --git a/public/locales/fa/security.json b/public/locales/fa/security.json index e7d47dc..8ff49f8 100644 --- a/public/locales/fa/security.json +++ b/public/locales/fa/security.json @@ -12,6 +12,11 @@ "hasUpperAndLower": "شامل یک حرف کوچک و بزرگ", "hasSpecialChar": "شامل علامت (!@#$%^&*)", "notCompatibility": "تکرار رمز عبور با رمز عبور یکسان نمی باشد", - "alertSuccess": "رمز عبور با موفقیت تعویض شد" + "alertSuccess": "رمز عبور با موفقیت تعویض شد", + "lastChange": "آخرین تغییر چند ثانیه پیش", + "activePassword": "رمز عبور فعال است", + "recentLogins": "ورود های اخیر", + "description": "در این بخش از ورود های اخیر به اکانت هارمونی خود را مشاهده می کنید", + "currentDevice": "دستگاه فعلی" } } diff --git a/src/App.tsx b/src/App.tsx index bd3de8a..56e8e0a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,9 +9,9 @@ import { import './App.css'; import { useTranslation } from 'react-i18next'; import { LanguageManager } from './components/LanguageManager'; -import { UserSecurity } from './features/profile/components/security/UserSecurity'; -import { ActiveDevices } from './features/profile/components/activeDevices/ActiveDevices'; +import { Settings } from './features/profile/Settings'; import { Setting } from './features/profile/components/setting/Setting'; + function App() { const { t } = useTranslation(); @@ -19,9 +19,12 @@ function App() { <> - + {/* */} + {/* */} + + {/* - + */} {/*
{t('helloWorld')} + {sections.map(({ title, hash }) => ( +
+ + {title} + +
+ ))} + + ); +} + +function Header() { + return ( + + محمدحسین برزه‌گر + + 09123456789 + + + ); +} + +function Layout() { + const theme = useTheme(); + const isMdUp = useMediaQuery(theme.breakpoints.up('md')); + const location = useLocation(); + + const navConfig: NavItemConfig[] = [ + { + text: 'حساب کاربری', + icon: , + path: '/profile', + children: [ + { + text: 'اطلاعات شخصی', + icon: , + path: '/profile#info', + }, + { + text: 'شماره تماس', + icon: , + path: '/profile#contact-info', + }, + { text: 'ایمیل', icon: , path: '/profile#email' }, + ], + }, + { + text: 'امنیت', + icon: , + path: '/security', + children: [ + { + text: 'رمز عبور', + icon: , + path: '/security#password', + }, + { + text: 'آدرس‌های تایید شده', + icon: , + path: '/security#locations', + }, + { + text: 'ورودهای اخیر', + icon: , + path: '/security#sessions', + }, + ], + }, + { text: 'دستگاه‌های فعال', icon: , path: '/devices' }, + { text: 'تنظیمات', icon: , path: '/setting' }, + ]; + + return ( + + + } + activePath={location.pathname + location.hash} + sideNavVariant={isMdUp ? 'full' : 'minimized'} + drawerWidth={274} + minimizedWidth={50} + /> + + + + + + + + ); +} + +const profileSections = [ + { title: 'اطلاعات شخصی', hash: 'info' }, + { title: 'شماره تماس', hash: 'contact-info' }, + { title: 'ایمیل', hash: 'email' }, +]; + +const router = createBrowserRouter([ + { + path: '/', + element: , + children: [ + { path: '/', element: }, + { path: '/profile', element: }, + { + path: '/security', + element: ( + <> +
+ +
+
+
+ +
+ + ), + }, + { path: '/devices', element: }, + { path: '/setting', element: }, + ], + }, +]); + +export function Settings() { + useTranslation(); + return ; +} diff --git a/src/features/profile/components/activeDevices/ActiveDevices.tsx b/src/features/profile/components/activeDevices/ActiveDevices.tsx index f4b4bb6..ae8982e 100644 --- a/src/features/profile/components/activeDevices/ActiveDevices.tsx +++ b/src/features/profile/components/activeDevices/ActiveDevices.tsx @@ -66,7 +66,7 @@ export function ActiveDevices() { justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', - gap: 2, + py: 1, mb: 2, }} > @@ -96,8 +96,8 @@ export function ActiveDevices() { display: 'flex', flexWrap: 'wrap', alignItems: 'center', - gap: 1, py: 1, + width: '690px', }} > - + {device.deviceModel}
diff --git a/src/features/profile/components/security/UserSecurity.tsx b/src/features/profile/components/security/PasswordSecurity.tsx similarity index 96% rename from src/features/profile/components/security/UserSecurity.tsx rename to src/features/profile/components/security/PasswordSecurity.tsx index 578e0bf..9f3a619 100644 --- a/src/features/profile/components/security/UserSecurity.tsx +++ b/src/features/profile/components/security/PasswordSecurity.tsx @@ -19,7 +19,7 @@ import { Toast } from '@/components/Toast'; import Logo from '@/components/Logo'; import { CardContainer } from '@/components/CardContainer'; -export function UserSecurity() { +export function PasswordSecurity() { const theme = useTheme(); const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); const { t } = useTranslation('security'); @@ -67,7 +67,7 @@ export function UserSecurity() { }, [password, validPassword]); return ( - + - + {changePassword ? ( - رمز عبور فعال است + + {t('securityForm.activePassword')} + - آخرین تغییر چند ثانیه پیش + {t('securityForm.lastChange')} ) : ( diff --git a/src/features/profile/components/security/RecentLogins.tsx b/src/features/profile/components/security/RecentLogins.tsx new file mode 100644 index 0000000..1528593 --- /dev/null +++ b/src/features/profile/components/security/RecentLogins.tsx @@ -0,0 +1,99 @@ +import { Box, Typography, Button } from '@mui/material'; +import { CardContainer } from '@/components/CardContainer'; +import { useTranslation } from 'react-i18next'; + +export function RecentLogins() { + const { t } = useTranslation('security'); + const data = [ + { + id: 0, + time: 'دقایقی پیش', + device: 'asus i5 24i', + ip: '192.168.1.1', + current: true, + }, + { + id: 1, + time: '۲۲:۱۳ - ۱۴۰۴/۰۹/۰۹', + device: 'samsung s23 ultra', + ip: '192.220.1.1', + current: false, + }, + ]; + + return ( + + + + + + {data.map((d) => ( + + + {d.time} + + + {d.device} + + + {d.ip} + + + {d.current ? ( + + ) : ( + + )} + + + ))} + + + + + + ); +} diff --git a/src/features/profile/components/security/Security.tsx b/src/features/profile/components/security/Security.tsx new file mode 100644 index 0000000..08a81ec --- /dev/null +++ b/src/features/profile/components/security/Security.tsx @@ -0,0 +1,12 @@ +import { PasswordSecurity } from './PasswordSecurity'; +import { RecentLogins } from './RecentLogins'; +import { Box } from '@mui/material'; + +export function Security() { + return ( + + + + + ); +} diff --git a/src/features/profile/components/setting/Setting.tsx b/src/features/profile/components/setting/Setting.tsx index 06ec217..850d1cd 100644 --- a/src/features/profile/components/setting/Setting.tsx +++ b/src/features/profile/components/setting/Setting.tsx @@ -27,15 +27,6 @@ export function Setting() { { code: 'fa', label: 'فارسی' }, ]; - const handleDraftLanguageChange = ( - _: any, - newValue: { code: string; label: string } | null, - ) => { - if (newValue) { - setDraftLanguage(newValue.code); - } - }; - const calendarOptions = [ t('settings.christian'), t('settings.solar'), @@ -45,6 +36,11 @@ export function Setting() { t('settings.solar'), ); + const handleDraftLanguageChange = ( + _: any, + v: { code: string; label: string } | null, + ) => v && setDraftLanguage(v.code); + const handleCancel = () => { setDraftLanguage(savedLanguage); setIsEditing(false); @@ -58,38 +54,23 @@ export function Setting() { setIsEditing(false); }; - const handleEditToggle = () => { - if (isEditing) { - handleSave(); - } else { - setDraftLanguage(savedLanguage); - setIsEditing(true); - } - }; + const handleEditToggle = () => + isEditing ? handleSave() : setIsEditing(true); return ( - + - - + } /> - - + {isEditing ? ( {t('settings.theme')} ) : ( - + {t('settings.theme')} @@ -146,55 +128,49 @@ export function Setting() { )} - - + {isEditing ? ( option.label} + getOptionLabel={(o) => o.label} value={ - languageOptions.find((opt) => opt.code === draftLanguage) || - null + languageOptions.find((o) => o.code === draftLanguage) || null } onChange={handleDraftLanguageChange} - renderInput={(params) => ( - + renderInput={(p) => ( + )} - size="small" - sx={{ width: 337, height: '56px' }} + size="medium" + fullWidth /> ) : ( - + {t('settings.language')} - { - languageOptions.find((opt) => opt.code === savedLanguage) - ?.label - } + {languageOptions.find((o) => o.code === savedLanguage)?.label} )} - - - + + {isEditing ? ( newVal && setSelectedCalendar(newVal)} - renderInput={(params) => ( - + onChange={(_, v) => v && setSelectedCalendar(v)} + renderInput={(p) => ( + )} - size="small" - sx={{ width: 337, height: 56 }} + size="medium" + fullWidth /> ) : ( - - + + {t('settings.calendar')} {selectedCalendar}