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 : 'نامشخص'} + + ); +}