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 fe1023c..d0ad8a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,13 +12,16 @@ "@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", "iconsax-react": "^0.0.8", "react": "^19.1.0", + "react-country-flag": "^3.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", + "react-virtuoso": "^4.13.0", "stylis": "^4.3.6", "stylis-plugin-rtl": "^2.1.1" }, @@ -27,6 +30,7 @@ "@types/node": "^24.0.10", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "@types/stylis": "^4.2.7", "@typescript-eslint/eslint-plugin": "^8.35.1", "@typescript-eslint/parser": "^8.35.1", "@vitejs/plugin-react": "^4.5.2", @@ -1067,9 +1071,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1482,6 +1486,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", @@ -1878,6 +1896,13 @@ "@types/react": "*" } }, + "node_modules/@types/stylis": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz", + "integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.35.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.35.1.tgz", @@ -2401,6 +2426,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", @@ -3115,6 +3150,18 @@ "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", @@ -3722,6 +3769,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-country-flag": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-country-flag/-/react-country-flag-3.1.0.tgz", + "integrity": "sha512-JWQFw1efdv9sTC+TGQvTKXQg1NKbDU2mBiAiRWcKM9F1sK+/zjhP2yGmm8YDddWyZdXVkR8Md47rPMJmo4YO5g==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", @@ -3776,6 +3835,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", @@ -3792,6 +3891,16 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-virtuoso": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.13.0.tgz", + "integrity": "sha512-XHv2Fglpx80yFPdjZkV9d1baACKghg/ucpDFEXwaix7z0AfVQj+mF6lM+YQR6UC/TwzXG2rJKydRMb3+7iV3PA==", + "license": "MIT", + "peerDependencies": { + "react": ">=16 || >=17 || >= 18 || >= 19", + "react-dom": ">=16 || >=17 || >= 18 || >=19" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -3912,6 +4021,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 91571b6..3f7bdc0 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,16 @@ "@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", "iconsax-react": "^0.0.8", "react": "^19.1.0", + "react-country-flag": "^3.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.6.0", + "react-virtuoso": "^4.13.0", "stylis": "^4.3.6", "stylis-plugin-rtl": "^2.1.1" }, @@ -30,6 +33,7 @@ "@types/node": "^24.0.10", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "@types/stylis": "^4.2.7", "@typescript-eslint/eslint-plugin": "^8.35.1", "@typescript-eslint/parser": "^8.35.1", "@vitejs/plugin-react": "^4.5.2", diff --git a/public/locales/en/activeDevices.json b/public/locales/en/activeDevices.json new file mode 100644 index 0000000..917b53a --- /dev/null +++ b/public/locales/en/activeDevices.json @@ -0,0 +1,9 @@ +{ + "active": { + "activeDevices": "Active devices", + "activeDevicesCaption": "Watch and manage all your active devices", + "deletDevicesButton": "Remove rest of the devices", + "deleteDevice": "Remove device", + "currentDevice": "Current device" + } +} diff --git a/public/locales/en/security.json b/public/locales/en/security.json new file mode 100644 index 0000000..920f3fe --- /dev/null +++ b/public/locales/en/security.json @@ -0,0 +1,17 @@ +{ + "securityForm": { + "password": "Password", + "determinePassword": "Log in to your Harmony account more easily by setting a strong password.", + "addPassword": "Add password", + "notDeterminedPassword": "You have not set a password for this account yet.", + "newPassword": "New password", + "confirmPassword": "Confirm password", + "confirm": "Confirm", + "hasNumber": "Contains number", + "hasMinLength": "at least 8 character", + "hasUpperAndLower": "Contains a lowercase and uppercase letter", + "hasSpecialChar": "Contains sign (!@#$%^&*)", + "notCompatibility": "Confirm password is not the same as password.", + "alertSuccess": "Password has successfully changed" + } +} diff --git a/public/locales/en/setting.json b/public/locales/en/setting.json new file mode 100644 index 0000000..c5c78f4 --- /dev/null +++ b/public/locales/en/setting.json @@ -0,0 +1,18 @@ +{ + "settings": { + "title": "Base settings", + "description": "Change your base settings", + "editButton": "Edit", + "rejectButton": "Reject", + "saveButton": "Save", + "theme": "Theme and color", + "light": "Light", + "dark": "Dark", + "language": "زبان/language", + "calendar": "Calendar and date format", + "solar": "Solar", + "lunar": "Lunar", + "christian": "Christian", + "iran": "Iran" + } +} diff --git a/public/locales/fa/activeDevices.json b/public/locales/fa/activeDevices.json new file mode 100644 index 0000000..929b2dd --- /dev/null +++ b/public/locales/fa/activeDevices.json @@ -0,0 +1,9 @@ +{ + "active": { + "activeDevices": "نشست های فعال", + "activeDevicesCaption": "مشاهده و مدیریت تمام نشست های فعال شما", + "deletDevicesButton": "حذف بقیه نشست ها", + "deleteDevice": "حذف نشست", + "currentDevice": "دستگاه فعلی" + } +} diff --git a/public/locales/fa/security.json b/public/locales/fa/security.json new file mode 100644 index 0000000..8ff49f8 --- /dev/null +++ b/public/locales/fa/security.json @@ -0,0 +1,22 @@ +{ + "securityForm": { + "password": "رمز عبور", + "determinePassword": "با تعیین یک رمز عبور قوی راحت تر به اکانت هارمونی خود وارد شوید", + "addPassword": "افزودن رمز عبور", + "notDeterminedPassword": "هنوز رمز عبوری برای این حساب کاربری تعیین نکرده اید", + "newPassword": "رمز عبور جدید", + "confirmPassword": "تکرار رمز عبور", + "confirm": "تایید", + "hasNumber": "شامل عدد", + "hasMinLength": "حداقل 8 کاراکتر", + "hasUpperAndLower": "شامل یک حرف کوچک و بزرگ", + "hasSpecialChar": "شامل علامت (!@#$%^&*)", + "notCompatibility": "تکرار رمز عبور با رمز عبور یکسان نمی باشد", + "alertSuccess": "رمز عبور با موفقیت تعویض شد", + "lastChange": "آخرین تغییر چند ثانیه پیش", + "activePassword": "رمز عبور فعال است", + "recentLogins": "ورود های اخیر", + "description": "در این بخش از ورود های اخیر به اکانت هارمونی خود را مشاهده می کنید", + "currentDevice": "دستگاه فعلی" + } +} diff --git a/public/locales/fa/setting.json b/public/locales/fa/setting.json new file mode 100644 index 0000000..1b07a7a --- /dev/null +++ b/public/locales/fa/setting.json @@ -0,0 +1,18 @@ +{ + "settings": { + "title": "تنظیمات پایه", + "description": "تنظیمات پایه‌ای حساب خود را تغییر دهید", + "editButton": "ویرایش", + "rejectButton": "لغو", + "saveButton": "ذخیره", + "theme": "تم و رنگ", + "light": "روشن", + "dark": "تاریک", + "language": "زبان/language", + "calendar": "فرمت تقویم و تاریخ", + "solar": "شمسی", + "lunar": "قمری", + "christian": "میلادی", + "iran": "ایران" + } +} diff --git a/src/App.tsx b/src/App.tsx index bf30764..b1c1097 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,25 +1,14 @@ -import { - Alert, - Box, - CssBaseline, - TextField, - Typography, - useColorScheme, -} from '@mui/material'; +import { CssBaseline, useColorScheme } from '@mui/material'; import './App.css'; -import { useTranslation } from 'react-i18next'; import { LanguageManager } from './components/LanguageManager'; -import { UserForm } from './features/profile/components/UserForm'; + function App() { - const { t } = useTranslation(); - return ( <> - - + {/*
{t('helloWorld')} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx new file mode 100644 index 0000000..dbba376 --- /dev/null +++ b/src/components/Logo.tsx @@ -0,0 +1,7 @@ +import LogoSvg from '@/assets/logo.svg'; + +function Logo() { + return ; +} + +export default Logo; diff --git a/src/components/ThemToggle.tsx b/src/components/ThemToggle.tsx new file mode 100644 index 0000000..22f0b1c --- /dev/null +++ b/src/components/ThemToggle.tsx @@ -0,0 +1,68 @@ +import { ToggleButtonGroup, ToggleButton, Box } from '@mui/material'; +import { useColorScheme } from '@mui/material/styles'; +import { Sun1, Moon } from 'iconsax-react'; +import { useTranslation } from 'react-i18next'; + +export const ThemeToggleButton = () => { + const { mode, setMode } = useColorScheme(); + const { t } = useTranslation('setting'); + + const handleChange = (_: any, newMode: 'light' | 'dark' | null) => { + if (newMode !== null) { + setMode(newMode); + localStorage.setItem('theme', newMode); + } + }; + + return ( + + + + + {t('settings.light')} + + + + + {t('settings.dark')} + + + + ); +}; diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index 947194f..0f0927e 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -1,5 +1,7 @@ import { Alert, Snackbar, type AlertColor } from '@mui/material'; -import React, { type PropsWithChildren } from 'react'; + +import { type PropsWithChildren } from 'react'; + export interface ToastProps extends PropsWithChildren { color: AlertColor | undefined; diff --git a/src/features/profile/Settings.tsx b/src/features/profile/Settings.tsx new file mode 100644 index 0000000..269f01f --- /dev/null +++ b/src/features/profile/Settings.tsx @@ -0,0 +1,176 @@ +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 }) => ( +
+ + {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 new file mode 100644 index 0000000..ae8982e --- /dev/null +++ b/src/features/profile/components/activeDevices/ActiveDevices.tsx @@ -0,0 +1,197 @@ +import { Box, Typography, Button } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { DeviceMessage, Logout } from 'iconsax-react'; +import Logo from '@/components/Logo'; + +export function ActiveDevices() { + const { t } = useTranslation('activeDevices'); + + const devices = [ + { + id: 0, + timeAndDate: 'دقایقی پیش', + deviceModel: 'asus i5 24i', + ip: '192.168.1.1', + current: true, + }, + { + id: 1, + timeAndDate: '۲۲:۱۳ - ۱۴۰۴/۰۹/۰۹', + deviceModel: 'Dell XPS 15', + ip: '89.165.23.12', + current: false, + }, + { + id: 2, + timeAndDate: '۲۲:۱۳ - ۱۴۰۴/۰۹/۰۹', + deviceModel: 'Samsung Galaxy S22', + ip: '10.0.0.5', + current: false, + }, + { + id: 3, + timeAndDate: '۲۲:۱۳ - ۱۴۰۴/۰۹/۰۹', + deviceModel: 'MacBook Pro 14-inch', + ip: '172.16.0.101', + current: false, + }, + ]; + + return ( + + + + + + + + + {t('active.activeDevices')} + + {t('active.activeDevicesCaption')} + + + + + + {devices.map((device) => ( + + + {device.timeAndDate} + + + + + + {device.deviceModel} + + + + + {device.ip} + + + + {device.current && ( + + )} + + + + + + + ))} + + + ); +} diff --git a/src/features/profile/components/security/PasswordSecurity.tsx b/src/features/profile/components/security/PasswordSecurity.tsx new file mode 100644 index 0000000..9f3a619 --- /dev/null +++ b/src/features/profile/components/security/PasswordSecurity.tsx @@ -0,0 +1,253 @@ +import { + Box, + Typography, + Button, + Dialog, + DialogTitle, + DialogContent, + TextField, + DialogActions, + IconButton, + useMediaQuery, +} from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import { useTranslation } from 'react-i18next'; +import { useState, useEffect } from 'react'; +import { CloseCircle, Refresh } from 'iconsax-react'; +import { PasswordValidationItem } from './PasswordValidation'; +import { Toast } from '@/components/Toast'; +import Logo from '@/components/Logo'; +import { CardContainer } from '@/components/CardContainer'; + +export function PasswordSecurity() { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + const { t } = useTranslation('security'); + const [open, setOpen] = useState(false); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showValidation, setShowValidation] = useState(false); + const [showPasswordAlert, setShowPasswordAlert] = useState(false); + const [changePassword, setChangePassword] = useState(false); + const [loading, setLoading] = useState(false); + + const hasNumber = /\d/.test(password); + const hasMinLength = password.length >= 8; + const hasUpperAndLower = /[A-Z]/.test(password) && /[a-z]/.test(password); + const hasSpecialChar = /[!@#$%^&*]/.test(password); + const validPassword = + hasNumber && hasMinLength && hasUpperAndLower && hasSpecialChar; + const matchPassword = password === confirmPassword; + + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + const handleShowAlert = () => { + setLoading(true); + setTimeout(() => { + setLoading(false); + setShowPasswordAlert(true); + setChangePassword(true); + handleClose(); + setPassword(''); + setConfirmPassword(''); + }, 1500); + }; + + useEffect(() => { + if (password) { + if (!validPassword) { + setShowValidation(true); + } else { + const timer = setTimeout(() => setShowValidation(false), 1000); + return () => clearTimeout(timer); + } + } else { + setShowValidation(false); + } + }, [password, validPassword]); + + return ( + + + + + + + + + {t('securityForm.addPassword')} + + } + > + + {changePassword ? ( + + + {t('securityForm.activePassword')} + + + {t('securityForm.lastChange')} + + + ) : ( + + {t('securityForm.notDeterminedPassword')} + + )} + + + + + + + + + + {t('securityForm.addPassword')} + + + + + + setPassword(e.target.value)} + sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }} + /> + + {password && showValidation && ( + + + + + + + + + )} + + setConfirmPassword(e.target.value)} + error={confirmPassword.length > 0 && !matchPassword} + helperText={ + confirmPassword.length > 0 && !matchPassword + ? t('securityForm.notCompatibility') + : ' ' + } + sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }} + /> + + + + + + + + setShowPasswordAlert(false)} + > + {t('securityForm.alertSuccess')} + + + + + + ); +} diff --git a/src/features/profile/components/security/PasswordValidation.tsx b/src/features/profile/components/security/PasswordValidation.tsx new file mode 100644 index 0000000..7d76d2b --- /dev/null +++ b/src/features/profile/components/security/PasswordValidation.tsx @@ -0,0 +1,41 @@ +import { Box, Typography } from '@mui/material'; +import { TickCircle } from 'iconsax-react'; + +interface ValidationItemProps { + isValid: boolean; + label: string; +} + +export function PasswordValidationItem({ + isValid, + label, +}: ValidationItemProps) { + return ( + + + + {label} + + + ); +} 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 new file mode 100644 index 0000000..9776049 --- /dev/null +++ b/src/features/profile/components/setting/Setting.tsx @@ -0,0 +1,184 @@ +import { useState } from 'react'; +import { + Box, + Typography, + Button, + useColorScheme, + Autocomplete, + TextField, +} from '@mui/material'; +import { CardContainer } from '@/components/CardContainer'; +import { useTranslation } from 'react-i18next'; +import { ThemeToggleButton } from '@/components/ThemToggle'; +import Logo from '@/components/Logo'; + +export function Setting() { + const { t, i18n } = useTranslation(['setting']); + const { mode } = useColorScheme(); + + const [savedLanguage, setSavedLanguage] = useState( + i18n.language || 'en', + ); + const [draftLanguage, setDraftLanguage] = useState(savedLanguage); + const [isEditing, setIsEditing] = useState(false); + + const languageOptions = [ + { code: 'en', label: 'English' }, + { code: 'fa', label: 'فارسی' }, + ]; + + const calendarOptions = [ + t('settings.christian'), + t('settings.solar'), + t('settings.lunar'), + ]; + const [selectedCalendar, setSelectedCalendar] = useState( + t('settings.solar'), + ); + + const handleDraftLanguageChange = ( + _: any, + v: { code: string; label: string } | null, + ) => v && setDraftLanguage(v.code); + + const handleCancel = () => { + setDraftLanguage(savedLanguage); + setIsEditing(false); + }; + + const handleSave = () => { + if (draftLanguage !== savedLanguage) { + i18n.changeLanguage(draftLanguage); + setSavedLanguage(draftLanguage); + } + setIsEditing(false); + }; + + const handleEditToggle = () => + isEditing ? handleSave() : setIsEditing(true); + + return ( + + + + + + + + {isEditing && ( + + )} + + + } + /> + + + {isEditing ? ( + + {t('settings.theme')} + + + ) : ( + + + {t('settings.theme')} + + + {mode === 'light' ? t('settings.light') : t('settings.dark')} + + + )} + + + {isEditing ? ( + o.label} + value={ + languageOptions.find((o) => o.code === draftLanguage) || null + } + onChange={handleDraftLanguageChange} + renderInput={(p) => ( + + )} + size="medium" + fullWidth + /> + ) : ( + + + {t('settings.language')} + + + {languageOptions.find((o) => o.code === savedLanguage)?.label} + + + )} + + + + + {isEditing ? ( + v && setSelectedCalendar(v)} + renderInput={(p) => ( + + )} + size="medium" + fullWidth + /> + ) : ( + + + {t('settings.calendar')} + + {selectedCalendar} + + )} + + + + + ); +} diff --git a/vite.config.d.ts b/vite.config.d.ts new file mode 100644 index 0000000..340562a --- /dev/null +++ b/vite.config.d.ts @@ -0,0 +1,2 @@ +declare const _default: import("vite").UserConfig; +export default _default; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..70ef262 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +});