From b96855fa286b8bc520786e1780ac6e6e4ea90a3b Mon Sep 17 00:00:00 2001 From: Koosha Lahouti Date: Sun, 27 Jul 2025 18:33:43 +0330 Subject: [PATCH] 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')} + + + ); }