From 03d18af0e49f626ace7f515f79369baaadb04233 Mon Sep 17 00:00:00 2001 From: Koosha Lahouti Date: Sat, 26 Jul 2025 17:55:43 +0330 Subject: [PATCH 1/6] feat: security section in profile setting --- package-lock.json | 13 ++ package.json | 1 + public/locales/fa/security.json | 11 ++ src/App.tsx | 2 + .../profile/components/UserSecurity.tsx | 124 ++++++++++++++++++ 5 files changed, 151 insertions(+) create mode 100644 public/locales/fa/security.json create mode 100644 src/features/profile/components/UserSecurity.tsx diff --git a/package-lock.json b/package-lock.json index 1204723..fe1023c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "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-dom": "^19.1.0", "react-i18next": "^15.6.0", @@ -3102,6 +3103,18 @@ "cross-fetch": "4.0.0" } }, + "node_modules/iconsax-react": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/iconsax-react/-/iconsax-react-0.0.8.tgz", + "integrity": "sha512-l3dVk4zGtkkJHgvNYqAf0wDKqnKxXykee5/DoESGo2JvSYwaxajJUHSX2YrPRXSov8Hd8ClGFwJxCEaEjrFD1Q==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", diff --git a/package.json b/package.json index 666890c..91571b6 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "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-dom": "^19.1.0", "react-i18next": "^15.6.0", diff --git a/public/locales/fa/security.json b/public/locales/fa/security.json new file mode 100644 index 0000000..a2aae2a --- /dev/null +++ b/public/locales/fa/security.json @@ -0,0 +1,11 @@ +{ + "securityForm": { + "password": "رمز عبور", + "determinePassword": "با تعیین یک رمز عبور قوی راحت تر به اکانت هارمونی خود وارد شوید", + "addPassword": "افزودن رمز عبور", + "notDeterminedPassword": "هنوز رمز عبوری برای این حساب کاربری تعیین نکرده اید", + "newPassword": "رمز عبور جدید", + "confirmPassword": "تکرار رمز عبور", + "confirm": "تایید" + } +} diff --git a/src/App.tsx b/src/App.tsx index 43fe290..957efc5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { import './App.css'; import { useTranslation } from 'react-i18next'; import { LanguageManager } from './components/LanguageManager'; +import { UserSecurity } from './features/profile/components/UserSecurity'; function App() { const { t } = useTranslation(); @@ -17,6 +18,7 @@ function App() { <> +
{t('helloWorld')} setOpen(true); + const handleClose = () => setOpen(false); + + return ( + + + + {t('securityForm.password')} + + {t('securityForm.determinePassword')} + + + + + + + {t('securityForm.notDeterminedPassword')} + + + + + + + + {t('securityForm.addPassword')} + + + setPassword(e.target.value)} + sx={{ width: '364px' }} + /> + setConfirmPassword(e.target.value)} + sx={{ width: '364px' }} + /> + + + + + + + ); +} From b96855fa286b8bc520786e1780ac6e6e4ea90a3b Mon Sep 17 00:00:00 2001 From: Koosha Lahouti Date: Sun, 27 Jul 2025 18:33:43 +0330 Subject: [PATCH 2/6] feat: add security section in setting and make it responsive --- public/locales/fa/security.json | 8 +- src/App.tsx | 2 - src/components/Toast.tsx | 23 ++ .../profile/components/PasswordValidation.tsx | 41 ++ .../profile/components/UserSecurity.tsx | 370 ++++++++++++++---- 5 files changed, 355 insertions(+), 89 deletions(-) create mode 100644 src/components/Toast.tsx create mode 100644 src/features/profile/components/PasswordValidation.tsx diff --git a/public/locales/fa/security.json b/public/locales/fa/security.json index a2aae2a..e7d47dc 100644 --- a/public/locales/fa/security.json +++ b/public/locales/fa/security.json @@ -6,6 +6,12 @@ "notDeterminedPassword": "هنوز رمز عبوری برای این حساب کاربری تعیین نکرده اید", "newPassword": "رمز عبور جدید", "confirmPassword": "تکرار رمز عبور", - "confirm": "تایید" + "confirm": "تایید", + "hasNumber": "شامل عدد", + "hasMinLength": "حداقل 8 کاراکتر", + "hasUpperAndLower": "شامل یک حرف کوچک و بزرگ", + "hasSpecialChar": "شامل علامت (!@#$%^&*)", + "notCompatibility": "تکرار رمز عبور با رمز عبور یکسان نمی باشد", + "alertSuccess": "رمز عبور با موفقیت تعویض شد" } } diff --git a/src/App.tsx b/src/App.tsx index 957efc5..43fe290 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,7 +9,6 @@ import { import './App.css'; import { useTranslation } from 'react-i18next'; import { LanguageManager } from './components/LanguageManager'; -import { UserSecurity } from './features/profile/components/UserSecurity'; function App() { const { t } = useTranslation(); @@ -18,7 +17,6 @@ function App() { <> -
{t('helloWorld')} void; +} + +export const Toast = ({ color, open, onClose, children }: ToastProps) => { + return ( + + + {children} + + + ); +}; diff --git a/src/features/profile/components/PasswordValidation.tsx b/src/features/profile/components/PasswordValidation.tsx new file mode 100644 index 0000000..7d76d2b --- /dev/null +++ b/src/features/profile/components/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/UserSecurity.tsx b/src/features/profile/components/UserSecurity.tsx index 4881f04..887bf95 100644 --- a/src/features/profile/components/UserSecurity.tsx +++ b/src/features/profile/components/UserSecurity.tsx @@ -10,115 +10,313 @@ import { IconButton, } from '@mui/material'; import { useTranslation } from 'react-i18next'; -import { useState } from 'react'; -import { CloseCircle } from 'iconsax-react'; +import { useState, useEffect } from 'react'; +import { CloseCircle, Refresh } from 'iconsax-react'; +import { PasswordValidationItem } from './PasswordValidation'; +import { Toast } from '@/components/Toast'; export function UserSecurity() { 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.password')} - - {t('securityForm.determinePassword')} - - - - - - - {t('securityForm.notDeterminedPassword')} - - - - - - - - {t('securityForm.addPassword')} - - - setPassword(e.target.value)} - sx={{ width: '364px' }} - /> - setConfirmPassword(e.target.value)} - sx={{ width: '364px' }} - /> - - - - - + + {t('securityForm.password')} + + {t('securityForm.determinePassword')} + + + + + + + {changePassword ? ( + + رمز عبور فعال است + + آخرین تغییر چند ثانیه پیش + + + ) : ( + + + {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')} + + + ); } From 32ea4dceebf9e89de73a66ead643ceebb9df7b35 Mon Sep 17 00:00:00 2001 From: Koosha Lahouti Date: Tue, 29 Jul 2025 15:48:30 +0330 Subject: [PATCH 3/6] feat: add setting and Active devices --- public/locales/en/activeDevices.json | 9 + public/locales/en/security.json | 17 + public/locales/en/setting.json | 14 + public/locales/fa/activeDevices.json | 9 + public/locales/fa/setting.json | 14 + src/App.tsx | 12 +- src/assets/logo.svg | 30 ++ src/components/CardContainer.tsx | 63 ++++ src/components/Logo.tsx | 7 + src/components/ThemToggle.tsx | 68 ++++ .../profile/components/UserSecurity.tsx | 322 ----------------- .../activeDevices/ActiveDevices.tsx | 175 ++++++++++ .../{ => security}/PasswordValidation.tsx | 0 .../components/security/UserSecurity.tsx | 324 ++++++++++++++++++ .../profile/components/setting/Setting.tsx | 202 +++++++++++ .../components/setting/data/Languages.tsx | 44 +++ 16 files changed, 985 insertions(+), 325 deletions(-) create mode 100644 public/locales/en/activeDevices.json create mode 100644 public/locales/en/security.json create mode 100644 public/locales/en/setting.json create mode 100644 public/locales/fa/activeDevices.json create mode 100644 public/locales/fa/setting.json create mode 100644 src/assets/logo.svg create mode 100644 src/components/CardContainer.tsx create mode 100644 src/components/Logo.tsx create mode 100644 src/components/ThemToggle.tsx delete mode 100644 src/features/profile/components/UserSecurity.tsx create mode 100644 src/features/profile/components/activeDevices/ActiveDevices.tsx rename src/features/profile/components/{ => security}/PasswordValidation.tsx (100%) create mode 100644 src/features/profile/components/security/UserSecurity.tsx create mode 100644 src/features/profile/components/setting/Setting.tsx create mode 100644 src/features/profile/components/setting/data/Languages.tsx 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..7024be4 --- /dev/null +++ b/public/locales/en/setting.json @@ -0,0 +1,14 @@ +{ + "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" + } +} 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/setting.json b/public/locales/fa/setting.json new file mode 100644 index 0000000..948eb7a --- /dev/null +++ b/public/locales/fa/setting.json @@ -0,0 +1,14 @@ +{ + "settings": { + "title": "تنظیمات پایه", + "description": "تنظیمات پایه‌ای حساب خود را تغییر دهید", + "editButton": "ویرایش", + "rejectButton": "لغو", + "saveButton": "ذخیره", + "theme": "تم و رنگ", + "light": "روشن", + "dark": "تاریک", + "language": "زبان/language", + "calendar": "فرمت تقویم و تاریخ" + } +} diff --git a/src/App.tsx b/src/App.tsx index 43fe290..bd3de8a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,7 +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 { Setting } from './features/profile/components/setting/Setting'; function App() { const { t } = useTranslation(); @@ -17,7 +19,10 @@ function App() { <> -
+ + + + {/*
{t('helloWorld')} -
+
*/} ); } @@ -48,6 +53,7 @@ function App() { export default App; import { Button } from '@mui/material'; +import { Avalanche } from 'iconsax-react'; export const ThemeToggleButton = () => { const { mode, setMode } = useColorScheme(); diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..6f53ef6 --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/CardContainer.tsx b/src/components/CardContainer.tsx new file mode 100644 index 0000000..798b25a --- /dev/null +++ b/src/components/CardContainer.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Box, Typography } from '@mui/material'; + +export function CardContainer({ + title, + subtitle, + action, + children, + highlighted, +}: { + title: string; + subtitle: string; + action?: React.ReactNode; + children?: React.ReactNode; + highlighted?: boolean; +}) { + return ( + + + + + + {title} + + + {subtitle} + + + {action} + + + {children} + + + ); +} 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/features/profile/components/UserSecurity.tsx b/src/features/profile/components/UserSecurity.tsx deleted file mode 100644 index 887bf95..0000000 --- a/src/features/profile/components/UserSecurity.tsx +++ /dev/null @@ -1,322 +0,0 @@ -import { - Box, - Typography, - Button, - Dialog, - DialogTitle, - DialogContent, - TextField, - DialogActions, - IconButton, -} from '@mui/material'; -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'; - -export function UserSecurity() { - 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.password')} - - {t('securityForm.determinePassword')} - - - - - - - {changePassword ? ( - - رمز عبور فعال است - - آخرین تغییر چند ثانیه پیش - - - ) : ( - - - {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/activeDevices/ActiveDevices.tsx b/src/features/profile/components/activeDevices/ActiveDevices.tsx new file mode 100644 index 0000000..350bb2e --- /dev/null +++ b/src/features/profile/components/activeDevices/ActiveDevices.tsx @@ -0,0 +1,175 @@ +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 ? ( + + ) : null} + + + + + + + ))} + + + + ); +} diff --git a/src/features/profile/components/PasswordValidation.tsx b/src/features/profile/components/security/PasswordValidation.tsx similarity index 100% rename from src/features/profile/components/PasswordValidation.tsx rename to src/features/profile/components/security/PasswordValidation.tsx diff --git a/src/features/profile/components/security/UserSecurity.tsx b/src/features/profile/components/security/UserSecurity.tsx new file mode 100644 index 0000000..ae794a4 --- /dev/null +++ b/src/features/profile/components/security/UserSecurity.tsx @@ -0,0 +1,324 @@ +import { + Box, + Typography, + Button, + Dialog, + DialogTitle, + DialogContent, + TextField, + DialogActions, + IconButton, +} from '@mui/material'; +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 UserSecurity() { + 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.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/setting/Setting.tsx b/src/features/profile/components/setting/Setting.tsx new file mode 100644 index 0000000..6ed6926 --- /dev/null +++ b/src/features/profile/components/setting/Setting.tsx @@ -0,0 +1,202 @@ +import { + Box, + Typography, + Button, + useColorScheme, + TextField, + Autocomplete, +} from '@mui/material'; +import { CardContainer } from '@/components/CardContainer'; +import { useTranslation } from 'react-i18next'; +import { useState } from 'react'; +import { ThemeToggleButton } from '@/components/ThemToggle'; +import { Languages, CountryFlag } from './data/Languages'; +import Logo from '@/components/Logo'; + +export function Setting() { + const { t } = useTranslation('setting'); + const [isEditing, setIsEditing] = useState(false); + const { mode } = useColorScheme(); + const [selectedLang, setSelectedLang] = useState('fa'); + const selectedLanguage = Languages.find((lang) => lang.code === selectedLang); + const calendar = ['میلادی', 'شمسی']; + const [selectedCalendar, setSelectedCalendar] = useState('شمسی'); + + const toggleEdit = () => { + setIsEditing((prev) => !prev); + }; + + return ( + + + + + + + + {isEditing && ( + + )} + + + } + /> + + + + {isEditing ? ( + + {t('settings.theme')} + + + ) : ( + <> + + {t('settings.theme')} + + + {mode === 'light' + ? t('settings.light') + : t('settings.dark')} + + + )} + + + + {isEditing ? ( + option.fa} + value={selectedLanguage || null} + onChange={(_, newValue) => { + if (newValue) setSelectedLang(newValue.code); + }} + renderOption={(props, option) => ( + + + + )} + renderInput={(params) => ( + + )} + /> + ) : ( + <> + + {t('settings.language')} + + + + )} + + + + + {isEditing ? ( + { + if (newValue) setSelectedCalendar(newValue); + }} + renderInput={(params) => ( + + )} + sx={{ width: '337px' }} + /> + ) : ( + <> + + {t('settings.calendar')} + + {selectedCalendar} + + )} + + + + + ); +} diff --git a/src/features/profile/components/setting/data/Languages.tsx b/src/features/profile/components/setting/data/Languages.tsx new file mode 100644 index 0000000..1c95f00 --- /dev/null +++ b/src/features/profile/components/setting/data/Languages.tsx @@ -0,0 +1,44 @@ +import { Box, Typography } from '@mui/material'; + +export const Languages = [ + { code: 'en', en: 'English', fa: 'انگلیسی' }, + { code: 'fa', en: 'Persian (Farsi)', fa: 'فارسی' }, + { code: 'ar', en: 'Arabic', fa: 'عربی' }, + { code: 'fr', en: 'French', fa: 'فرانسوی' }, + { code: 'de', en: 'German', fa: 'آلمانی' }, + { code: 'es', en: 'Spanish', fa: 'اسپانیایی' }, + { code: 'it', en: 'Italian', fa: 'ایتالیایی' }, + { code: 'ru', en: 'Russian', fa: 'روسی' }, + { code: 'zh', en: 'Chinese', fa: 'چینی' }, + { code: 'ja', en: 'Japanese', fa: 'ژاپنی' }, + { code: 'ko', en: 'Korean', fa: 'کره‌ای' }, + { code: 'hi', en: 'Hindi', fa: 'هندی' }, + { code: 'tr', en: 'Turkish', fa: 'ترکی استانبولی' }, + { code: 'pt', en: 'Portuguese', fa: 'پرتغالی' }, + { code: 'ur', en: 'Urdu', fa: 'اردو' }, + { code: 'nl', en: 'Dutch', fa: 'هلندی' }, + { code: 'sv', en: 'Swedish', fa: 'سوئدی' }, + { code: 'pl', en: 'Polish', fa: 'لهستانی' }, + { code: 'th', en: 'Thai', fa: 'تایلندی' }, + { code: 'id', en: 'Indonesian', fa: 'اندونزیایی' }, +]; + +export function CountryFlag({ country }: { country: string }) { + const lang = Languages.find((lang) => lang.code === country); + const countryCode = lang?.code || 'un'; + + const flagUrl = `https://flagcdn.com/w40/${countryCode}.png`; + + return ( + + {country} + {lang ? lang.fa : 'نامشخص'} + + ); +} From 064b3f66dc999c2b608febeaf41325efab563b28 Mon Sep 17 00:00:00 2001 From: Koosha Lahouti Date: Wed, 30 Jul 2025 19:44:08 +0330 Subject: [PATCH 4/6] feat: language can be changed between persian and english and everything got responsive --- package-lock.json | 30 +- package.json | 2 + public/locales/en/setting.json | 6 +- public/locales/fa/setting.json | 6 +- .../activeDevices/ActiveDevices.tsx | 152 +++++----- .../components/security/UserSecurity.tsx | 167 ++++------- .../profile/components/setting/Setting.tsx | 264 +++++++++--------- .../components/setting/data/Languages.tsx | 44 --- 8 files changed, 306 insertions(+), 365 deletions(-) delete mode 100644 src/features/profile/components/setting/data/Languages.tsx diff --git a/package-lock.json b/package-lock.json index fe1023c..24da072 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,10 @@ "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" }, @@ -1067,9 +1069,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": { @@ -3722,6 +3724,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", @@ -3792,6 +3806,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", diff --git a/package.json b/package.json index 91571b6..b2ffcf4 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,10 @@ "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" }, diff --git a/public/locales/en/setting.json b/public/locales/en/setting.json index 7024be4..c5c78f4 100644 --- a/public/locales/en/setting.json +++ b/public/locales/en/setting.json @@ -9,6 +9,10 @@ "light": "Light", "dark": "Dark", "language": "زبان/language", - "calendar": "Calendar and date format" + "calendar": "Calendar and date format", + "solar": "Solar", + "lunar": "Lunar", + "christian": "Christian", + "iran": "Iran" } } diff --git a/public/locales/fa/setting.json b/public/locales/fa/setting.json index 948eb7a..1b07a7a 100644 --- a/public/locales/fa/setting.json +++ b/public/locales/fa/setting.json @@ -9,6 +9,10 @@ "light": "روشن", "dark": "تاریک", "language": "زبان/language", - "calendar": "فرمت تقویم و تاریخ" + "calendar": "فرمت تقویم و تاریخ", + "solar": "شمسی", + "lunar": "قمری", + "christian": "میلادی", + "iran": "ایران" } } diff --git a/src/features/profile/components/activeDevices/ActiveDevices.tsx b/src/features/profile/components/activeDevices/ActiveDevices.tsx index 350bb2e..f4b4bb6 100644 --- a/src/features/profile/components/activeDevices/ActiveDevices.tsx +++ b/src/features/profile/components/activeDevices/ActiveDevices.tsx @@ -45,6 +45,7 @@ export function ActiveDevices() { maxWidth: '834px', mx: 'auto', px: { xs: 2, sm: 3 }, + py: 4, }} > - - + + - + {t('active.activeDevices')} {t('active.activeDevicesCaption')} @@ -88,87 +82,111 @@ export function ActiveDevices() { borderColor: 'error.main', color: 'error.main', border: '1px solid', + textTransform: 'none', }} > {t('active.deletDevicesButton')} - - {devices.map((device) => ( + {devices.map((device) => ( + + + {device.timeAndDate} + + - - {device.timeAndDate} + + + {device.deviceModel} + - - - - {device.deviceModel} - - + + {device.ip} + - - {device.ip} - - - - {device.current ? ( - - ) : null} - - - + + {device.current && ( - + )} - ))} - + + + + + + ))} ); diff --git a/src/features/profile/components/security/UserSecurity.tsx b/src/features/profile/components/security/UserSecurity.tsx index ae794a4..578e0bf 100644 --- a/src/features/profile/components/security/UserSecurity.tsx +++ b/src/features/profile/components/security/UserSecurity.tsx @@ -8,7 +8,9 @@ import { 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'; @@ -18,6 +20,8 @@ import Logo from '@/components/Logo'; import { CardContainer } from '@/components/CardContainer'; export function UserSecurity() { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); const { t } = useTranslation('security'); const [open, setOpen] = useState(false); const [password, setPassword] = useState(''); @@ -63,14 +67,14 @@ export function UserSecurity() { }, [password, validPassword]); return ( - + - + @@ -119,30 +110,22 @@ export function UserSecurity() { } > - + {changePassword ? ( - + رمز عبور فعال است آخرین تغییر چند ثانیه پیش ) : ( - - - {t('securityForm.notDeterminedPassword')} - - + {t('securityForm.notDeterminedPassword')} + )} @@ -150,21 +133,13 @@ export function UserSecurity() { open={open} onClose={handleClose} fullWidth + fullScreen={fullScreen} maxWidth="xs" scroll="body" - PaperProps={{ - sx: { mx: 1 }, - }} + PaperProps={{ sx: { mx: 1 } }} > - + @@ -182,48 +157,18 @@ export function UserSecurity() { px: { xs: 2, sm: 3 }, }} > - - setPassword(e.target.value)} - sx={{ - '& .MuiOutlinedInput-root': { - borderRadius: 2, - }, - }} - /> - + 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, - }, - }} - /> - + setConfirmPassword(e.target.value)} + error={confirmPassword.length > 0 && !matchPassword} + helperText={ + confirmPassword.length > 0 && !matchPassword + ? t('securityForm.notCompatibility') + : ' ' + } + sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }} + /> - + )} + ) : ( + + )} + + + ))} + + + + + + ); +} 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} From 3f5f8e1aaf6299e1b339cd8f99aa07c9bed20f40 Mon Sep 17 00:00:00 2001 From: Koosha Lahouti Date: Sat, 2 Aug 2025 12:48:02 +0330 Subject: [PATCH 6/6] fix: delete additional things --- package-lock.json | 8 ++++++++ package.json | 1 + src/App.tsx | 14 +------------- src/components/Toast.tsx | 2 +- src/features/profile/Settings.tsx | 1 - .../profile/components/setting/Setting.tsx | 2 +- vite.config.d.ts | 2 ++ vite.config.js | 12 ++++++++++++ 8 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 vite.config.d.ts create mode 100644 vite.config.js diff --git a/package-lock.json b/package-lock.json index fd2c54d..fcab9b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,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", @@ -1895,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", diff --git a/package.json b/package.json index 7724dc2..3f7bdc0 100644 --- a/package.json +++ b/package.json @@ -33,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/src/App.tsx b/src/App.tsx index 56e8e0a..50ac647 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,20 +1,9 @@ -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 { Settings } from './features/profile/Settings'; -import { Setting } from './features/profile/components/setting/Setting'; function App() { - const { t } = useTranslation(); - return ( <> @@ -56,7 +45,6 @@ function App() { export default App; import { Button } from '@mui/material'; -import { Avalanche } from 'iconsax-react'; export const ThemeToggleButton = () => { const { mode, setMode } = useColorScheme(); diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index 947194f..713c36f 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -1,5 +1,5 @@ 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 index 27bdb53..269f01f 100644 --- a/src/features/profile/Settings.tsx +++ b/src/features/profile/Settings.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { createBrowserRouter, RouterProvider, diff --git a/src/features/profile/components/setting/Setting.tsx b/src/features/profile/components/setting/Setting.tsx index 850d1cd..9776049 100644 --- a/src/features/profile/components/setting/Setting.tsx +++ b/src/features/profile/components/setting/Setting.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { Box, Typography, 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'), + }, + }, +});