diff --git a/public/locales/en/setting.json b/public/locales/en/setting.json index 458690b..b2162f4 100644 --- a/public/locales/en/setting.json +++ b/public/locales/en/setting.json @@ -16,7 +16,7 @@ "familyName": "Family Name", "country": "Country", "gender": "Gender", - "nationalCode": "National code", + "nationalCode": "National code(Op", "man": "Male", "woman": "Female", "genderPlaceholder": "Male", @@ -62,7 +62,14 @@ "emailIsInvalid": "Email is invalid", "changeEmailFailed": "Change of email failed", "anErrorOccurred": "An error occurred.", - "saving": "Saving..." + "saving": "Saving...", + "successSaveProfile": "Profile saved successfully", + "errorSave": "Saving failed", + "codeSentSuccessfully": "Code sent successfully", + "errorSendCode": "Failed to send code", + "phoneVerified": "Phone number verified", + "errorConfirmCode": "Failed to confirm code", + "errorChangePhone": "Failed to change phone number" }, "active": { @@ -76,7 +83,9 @@ "notLoggedIn": "You are not logged in", "failFetchActiveSessions": "Failed to fetch active sessions.", "errorFetch": "An error occurred while fetching your active sessions.", - "deleting": "Deleting..." + "deleting": "Deleting...", + "successDelete": "Deleted successfully", + "deleteFailed": "Deletion failed" }, "settings": { @@ -123,6 +132,9 @@ "currentDevice": "Current device", "changePassword": "Change password", "currentPassword": "Current password", - "forgetPassword": "Forgot your password?" + "forgetPassword": "Forgot your password?", + "passwordChanged": "Password changed", + "passwordAdded": "Password added", + "error": "Password change failed" } } diff --git a/public/locales/fa/setting.json b/public/locales/fa/setting.json index eec7e03..5a33613 100644 --- a/public/locales/fa/setting.json +++ b/public/locales/fa/setting.json @@ -17,6 +17,7 @@ "country": "کشور", "gender": "جنسیت", "nationalCode": "کد ملی", + "optionalNationalCode": "کد ملی(اختیاری)", "man": "مرد", "woman": "زن", "genderPlaceholder": "مرد", @@ -62,7 +63,14 @@ "emailIsInvalid": "ایمیل نامعتبر است", "changeEmailFailed": "تغییر ایمیل با خطا مواجه شد", "anErrorOccurred": "خطایی رخ داد", - "saving": "در حال ذخیره‌سازی..." + "saving": "در حال ذخیره‌سازی...", + "successSaveProfile": "پروفایل با موفقیت ذخیره شد", + "errorSave": "ذخیره با مشکل مواجه شد", + "codeSentSuccessfully": "کد با موفقیت ارسال شد", + "errorSendCode": "ارسال کد با خطا مواجه شد", + "phoneVerified": "تلفن همراه تایید شد", + "errorConfirmCode": "تایید کد با خطا مواجه شد", + "errorChangePhone": "تغییر تلفن همراه با خطا مواجه شد" }, "active": { @@ -76,7 +84,9 @@ "notLoggedIn": "شما وارد سیستم نشده‌اید", "failFetchActiveSessions": "دریافت نشست های فعال ناموفق بود.", "errorFetch": "هنگام دریافت جلسات فعال شما خطایی روی داد.", - "deleting": "در حال حذف..." + "deleting": "در حال حذف...", + "successDelete": "با موفقیت حذف شد", + "deleteFailed": "حذف با مشکل مواجه شد" }, "settings": { @@ -123,6 +133,9 @@ "currentDevice": "دستگاه فعلی", "changePassword": "تغییر رمز عبور", "currentPassword": "رمز عبور فعلی", - "forgetPassword": "رمز عبور را فراموش کرده اید؟" + "forgetPassword": "رمز عبور را فراموش کرده اید؟", + "passwordChanged": "رمز عبور تعویض شد", + "passwordAdded": "رمز عبور اضافه شد", + "error": "تعویض رمز عبور با مشکل مواجه شد" } } diff --git a/src/components/CardContainer.tsx b/src/components/CardContainer.tsx index 2159df7..24fd92f 100644 --- a/src/components/CardContainer.tsx +++ b/src/components/CardContainer.tsx @@ -25,6 +25,7 @@ export function CardContainer({ display: 'flex', flexDirection: 'column', gap: 2, + borderRadius: 1, }} > = ({ user, loading }) => { +export const Header: React.FC = ({ user }) => { return ( = ({ setSideNavOpen, isMobile, user, - loading, }) => { return ( { return ( diff --git a/src/components/ThemToggle.tsx b/src/components/ThemToggle.tsx index 0149f26..36ef2fd 100644 --- a/src/components/ThemToggle.tsx +++ b/src/components/ThemToggle.tsx @@ -34,7 +34,9 @@ export const ThemeToggleButton = ({ @@ -70,10 +68,6 @@ export const ThemeToggleButton = ({ gap: 1, px: 2, py: 1, - '&.Mui-selected': { - bgcolor: 'primary.light', - color: 'primary.main', - }, }} > diff --git a/src/features/authorization/components/AccountCreated/AccountCreated.tsx b/src/features/authorization/components/AccountCreated/AccountCreated.tsx index ba49095..bee3d4d 100644 --- a/src/features/authorization/components/AccountCreated/AccountCreated.tsx +++ b/src/features/authorization/components/AccountCreated/AccountCreated.tsx @@ -1,6 +1,6 @@ -import React, { useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { AuthenticationCard } from '../AuthenticationCard'; -import { Box, CardHeader, Divider, Stack, Typography } from '@mui/material'; +import { Box, Divider, Stack, Typography } from '@mui/material'; import AccountCreatedIcon from '@/assets/account-created.svg'; import { useTranslation } from 'react-i18next'; import { Link as RouterLink, useSearchParams } from 'react-router-dom'; diff --git a/src/features/authorization/components/AccountCreated/AccountCreatedClubBanner.tsx b/src/features/authorization/components/AccountCreated/AccountCreatedClubBanner.tsx index 213e17c..bc77e4b 100644 --- a/src/features/authorization/components/AccountCreated/AccountCreatedClubBanner.tsx +++ b/src/features/authorization/components/AccountCreated/AccountCreatedClubBanner.tsx @@ -1,7 +1,5 @@ import { Stack, Typography, useTheme } from '@mui/material'; -import { Icon } from '@rkheftan/harmony-ui'; -import { Box, Profile2User } from 'iconsax-react'; -import React from 'react'; +import { Profile2User } from 'iconsax-react'; import { useTranslation } from 'react-i18next'; export const AccountCreatedClubBanner = () => { diff --git a/src/features/authorization/components/AccountCreated/AccountCreatedRedirectButton.tsx b/src/features/authorization/components/AccountCreated/AccountCreatedRedirectButton.tsx index 1cd78cb..199652c 100644 --- a/src/features/authorization/components/AccountCreated/AccountCreatedRedirectButton.tsx +++ b/src/features/authorization/components/AccountCreated/AccountCreatedRedirectButton.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import type { RequestedApplication } from './AccountCreated'; -import { Box, Button, useTheme } from '@mui/material'; +import { Box, Button } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { CountDownTimer } from '@/components/CountDownTimer'; import type { PalleteColor } from '@/theme/palette'; diff --git a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx index 344427b..bd5ca98 100644 --- a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx +++ b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx @@ -6,7 +6,7 @@ import { isNumeric } from '@/utils/regexes/isNumeric'; import { CompleteSignUp } from './CompleteSignUp'; import { EnterPasswordForm } from './EnterPasswordForm'; import { UserStatus } from '../../types/userTypes'; -import type { CountryCode, GUID } from '@/types/commonTypes'; +import type { CountryCode } from '@/types/commonTypes'; import { VerifyPhoneNumber } from './VerifyPhoneNumber'; import { useNavigate, useSearchParams } from 'react-router-dom'; diff --git a/src/features/profile/components/security/PasswordDialog.tsx b/src/features/profile/components/security/PasswordDialog.tsx index a0bdf2d..2aefd9b 100644 --- a/src/features/profile/components/security/PasswordDialog.tsx +++ b/src/features/profile/components/security/PasswordDialog.tsx @@ -10,11 +10,14 @@ import { Link, CircularProgress, Typography, + InputAdornment, } from '@mui/material'; -import { CloseCircle } from 'iconsax-react'; +import { CloseCircle, Eye, EyeSlash } from 'iconsax-react'; import { Icon } from '@rkheftan/harmony-ui'; import { type PasswordDialogProps } from '../../types/settingsType'; import { PasswordValidationItem } from './PasswordValidation'; +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; export function PasswordDialog({ open, @@ -27,7 +30,6 @@ export function PasswordDialog({ currentPassword, setCurrentPassword, showValidation, - validPassword, matchPassword, loading, handleSubmit, @@ -39,6 +41,11 @@ export function PasswordDialog({ hasUpperAndLower, hasSpecialChar, }: PasswordDialogProps) { + const [showCurrent, setShowCurrent] = useState(false); + const [showNew, setShowNew] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + const navigate = useNavigate(); + return ( setCurrentPassword(e.target.value)} sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }} + InputProps={{ + endAdornment: ( + + setShowCurrent(!showCurrent)}> + + + + ), + }} /> - + navigate('/forget-password')} + > {t('securityForm.forgetPassword')} @@ -94,11 +113,20 @@ export function PasswordDialog({ setPassword(e.target.value)} sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }} + InputProps={{ + endAdornment: ( + + setShowNew(!showNew)}> + + + + ), + }} /> {showValidation && ( @@ -124,7 +152,7 @@ export function PasswordDialog({ setConfirmPassword(e.target.value)} @@ -135,6 +163,15 @@ export function PasswordDialog({ : ' ' } sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }} + InputProps={{ + endAdornment: ( + + setShowConfirm(!showConfirm)}> + + + + ), + }} /> @@ -144,7 +181,6 @@ export function PasswordDialog({ sx={{ height: 48, textTransform: 'none' }} variant="contained" onClick={handleSubmit} - disabled={!validPassword || !matchPassword || loading} > {loading ? ( diff --git a/src/features/profile/components/security/PasswordSecurity.tsx b/src/features/profile/components/security/PasswordSecurity.tsx index 825f2af..4a12176 100644 --- a/src/features/profile/components/security/PasswordSecurity.tsx +++ b/src/features/profile/components/security/PasswordSecurity.tsx @@ -94,23 +94,26 @@ export function PasswordSecurity() { setPassword(''); setConfirmPassword(''); setCurrentPassword(''); - } - }, [addData, changeData, changePasswordUI, showToast, t]); - - useEffect(() => { - if (addError || changeError) { + } else if (addData || changeData) { showToast({ message: - getErrorMessage(addError || changeError) || t('securityForm.general'), + addData?.message || changeData?.message || t('securityForm.error'), severity: 'error', }); } - }, [addError, changeError, t, showToast]); + }, [addData, changeData, changePasswordUI, showToast, t]); const handleOpen = () => setOpen(true); const handleClose = () => setOpen(false); const handlePasswordSubmit = async () => { + if (!matchPassword) { + showToast({ + message: t('securityForm.notCompatibility'), + severity: 'error', + }); + return; + } if (changePasswordUI) { await executeChangePassword({ oldPassword: currentPassword, diff --git a/src/features/profile/components/userInformation/PersonalInformation.tsx b/src/features/profile/components/userInformation/PersonalInformation.tsx index 32e077d..eaf6078 100644 --- a/src/features/profile/components/userInformation/PersonalInformation.tsx +++ b/src/features/profile/components/userInformation/PersonalInformation.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Box, Button, Typography, CircularProgress } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { CardContainer } from '@/components/CardContainer'; @@ -25,13 +25,13 @@ export function PersonalInformation() { }); const [originalData, setOriginalData] = useState(null); const showToast = useToast(); + const infoRowEditRef = useRef<{ validateFields: () => boolean }>(null); const { data: profileData, loading: isLoadingProfile, error: fetchProfileError, } = useApi(fetchProfile, { immediate: true }); - const { data: saveData, loading: isSavingProfile, @@ -50,18 +50,14 @@ export function PersonalInformation() { : Gender.None, country: profileData.countryCode ?? '', }; - setData(fetchedData); setOriginalData(fetchedData); - - const imageBaseUrl = process.env.IMAGE_BASE_URL; - - if (profileData.profileImageUrl) { - setUploadedImageUrl(`${imageBaseUrl}${profileData.profileImageUrl}`); - } else { - setUploadedImageUrl(null); - } - + const imageBaseUrl = import.meta.env.IMAGE_BASE_URL; + setUploadedImageUrl( + profileData.profileImageUrl + ? `${imageBaseUrl}${profileData.profileImageUrl}` + : null, + ); setUploadedImageFile(null); } }, [profileData]); @@ -80,21 +76,18 @@ export function PersonalInformation() { const initials = `${data?.firstName?.trim()[0] || ''}‌${data?.lastName?.trim()[0] || ''}`; - const handleEditClick = () => { - setIsEditing(true); - setOriginalData(data); - }; + const handleEditClick = () => setIsEditing(true); const handleCancelClick = () => { setIsEditing(false); - if (originalData) { - setData(originalData); - } + if (originalData) setData(originalData); setUploadedImageFile(null); }; - const handleSaveClick = async () => { + const handleSaveClick = () => { if (!data) return; + const isValid = infoRowEditRef.current?.validateFields?.(); + if (!isValid) return; executeSaveProfile({ data, imageUrl: uploadedImageFile }); }; @@ -105,23 +98,21 @@ export function PersonalInformation() { }; useEffect(() => { - if (saveProfileError) { + if (saveProfileError) showToast({ message: getErrorMessage(saveProfileError) || t('settingForm.errorSave'), severity: 'error', }); - } }, [saveProfileError, showToast, t]); useEffect(() => { - if (fetchProfileError) { + if (fetchProfileError) showToast({ message: getErrorMessage(fetchProfileError) || t('settingForm.errorFetch'), severity: 'error', }); - } }, [fetchProfileError, showToast, t]); return ( @@ -131,12 +122,7 @@ export function PersonalInformation() { subtitle={t('settingForm.descriptionPersonalInfo')} highlighted={isEditing} action={ - + {isEditing ? ( <> @@ -210,46 +194,48 @@ export function PersonalInformation() { ) : ( - <> - - {isEditing && ( - { - setUploadedImageFile(file); - const reader = new FileReader(); - reader.onload = () => - setUploadedImageUrl(reader.result as string); - reader.readAsDataURL(file); - }} - onRemoveImage={() => { - setUploadedImageFile(null); - setUploadedImageUrl(null); - }} + + {isEditing && ( + { + setUploadedImageFile(file); + const reader = new FileReader(); + reader.onload = () => + setUploadedImageUrl(reader.result as string); + reader.readAsDataURL(file); + }} + onRemoveImage={() => { + setUploadedImageFile(null); + setUploadedImageUrl(null); + }} + /> + )} + {data && + (isEditing ? ( + - )} - {data && - (isEditing ? ( - - ) : ( - - ))} - - + ) : ( + + ))} + )} diff --git a/src/features/profile/components/userInformation/PhoneNumber.tsx b/src/features/profile/components/userInformation/PhoneNumber.tsx index 266f64e..2ff5f75 100644 --- a/src/features/profile/components/userInformation/PhoneNumber.tsx +++ b/src/features/profile/components/userInformation/PhoneNumber.tsx @@ -141,12 +141,8 @@ export function PhoneNumber() { }, ]); setIsEditing(false); - toast({ - message: t('settingForm.phoneChangedSuccessfully'), - severity: 'success', - }); } - }, [changePhoneData, countryCode, phoneNumber, t, toast]); + }, [changePhoneData, countryCode, phoneNumber, t]); useEffect(() => { if (changePhoneError) { @@ -203,10 +199,10 @@ export function PhoneNumber() { }); }; - const handleSendCode = () => { + const handleSendCode = async () => { handleBlur(); if (formError || !isPhoneValid(countryCode, phoneNumber)) return; - executeSendCode({ + return executeSendCode({ phoneNumber: countryCode + phoneNumber.replace(/^0/, ''), }); }; diff --git a/src/features/profile/components/userInformation/personalInformation/InfoRowDisplay.tsx b/src/features/profile/components/userInformation/personalInformation/InfoRowDisplay.tsx index 9bb46a2..408f514 100644 --- a/src/features/profile/components/userInformation/personalInformation/InfoRowDisplay.tsx +++ b/src/features/profile/components/userInformation/personalInformation/InfoRowDisplay.tsx @@ -4,15 +4,21 @@ import { DisplayField } from './DisplayField'; import { Gender } from '@/features/profile/types/settingsType'; import { type InfoRowDisplayProps } from '@/features/profile/types/settingsType'; import ReactCountryFlag from 'react-country-flag'; +import { countries } from '@/data/countries'; export function InfoRowDisplay({ data, uploadedImageUrl, initials, }: InfoRowDisplayProps) { - const { t } = useTranslation('setting'); + const { t } = useTranslation(['setting', 'country']); const displayValue = (value: string) => value?.trim() || t('settingForm.notDetermined'); + const countryLabel = + data.country && + t(countries.find((c) => c.code === data.country)?.label || '', { + ns: 'country', + }); const getGenderLabel = (gender: Gender | '') => { switch (gender) { @@ -70,19 +76,20 @@ export function InfoRowDisplay({ {t('settingForm.country')} - + {data.country ? ( - + <> + + + {countryLabel} + + ) : ( {t('settingForm.notDetermined')} diff --git a/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx b/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx index f05dfd6..5bf9d27 100644 --- a/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx +++ b/src/features/profile/components/userInformation/personalInformation/InfoRowEdit.tsx @@ -6,123 +6,175 @@ import { MenuItem, Select, Autocomplete, + FormHelperText, } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { countries } from '@/data/countries'; import { Gender } from '@/features/profile/types/settingsType'; import { type InfoRowEditProps } from '@/features/profile/types/settingsType'; import ReactCountryFlag from 'react-country-flag'; +import { useState, forwardRef, useImperativeHandle } from 'react'; -export function InfoRowEdit({ data, setData }: InfoRowEditProps) { - const { t } = useTranslation(['countries', 'setting']); +export const InfoRowEdit = forwardRef( + ({ data, setData }: InfoRowEditProps, ref) => { + const { t } = useTranslation(['country', 'setting']); + const [touched, setTouched] = useState({ + firstName: false, + lastName: false, + gender: false, + country: false, + }); - const countryOptions = countries.map((c) => ({ - code: c.code, - label: t(c.label, { ns: 'countries' }), - })); + const isValidName = (str: string) => /[A-Za-z\u0600-\u06FF]+/.test(str); - const currentCountry = - countryOptions.find((c) => c.code === data.country) || null; - const fields = [ - { - name: 'firstName' as const, - label: t('settingForm.name', { ns: 'setting' }), - value: data.firstName, - }, - { - name: 'lastName' as const, - label: t('settingForm.familyName', { ns: 'setting' }), - value: data.lastName, - }, - { - name: 'nationalCode' as const, - label: t('settingForm.nationalCode', { ns: 'setting' }), - value: data.nationalCode, - }, - ]; + useImperativeHandle(ref, () => ({ + validateFields: () => { + const newTouched = { + firstName: true, + lastName: true, + gender: true, + country: true, + }; + setTouched(newTouched); - return ( - - {fields.map(({ name, label, value }) => ( - - - setData((prev) => ({ - ...prev, - [name]: e.target.value, - })) - } - label={label} - /> - - ))} + return ( + data.firstName.trim() !== '' && + isValidName(data.firstName) && + data.lastName.trim() !== '' && + isValidName(data.lastName) && + data.gender !== Gender.None && + data.country !== '' + ); + }, + })); - - - - {t('settingForm.genderPlaceholder', { ns: 'setting' })} - - - - - - option.label} - value={currentCountry} - onChange={(_, newValue) => - setData((prev) => ({ - ...prev, - country: newValue?.code || '', - })) - } - renderOption={(props, option) => ( - - + setData((prev) => ({ ...prev, [name]: e.target.value })) + } + label={label} + error={ + name !== 'nationalCode' && + touched[name] && + (value.trim() === '' || !isValidName(value)) + } + helperText={ + name !== 'nationalCode' && + touched[name] && + (value.trim() === '' || !isValidName(value)) + ? t('settingForm.thisFieldIsRequired', { ns: 'setting' }) + : undefined + } /> - {option.label} - )} - renderInput={(params) => ( - - )} - clearOnEscape - /> - - ); -} + ))} + + + + + {t('settingForm.genderPlaceholder', { ns: 'setting' })} + + + + {touched.gender && data.gender === Gender.None + ? t('settingForm.thisFieldIsRequired', { ns: 'setting' }) + : ''} + + + + + option.label} + value={currentCountry} + onChange={(_, newValue) => + setData((prev) => ({ ...prev, country: newValue?.code || '' })) + } + renderOption={(props, option) => ( + + + {option.label} + + )} + renderInput={(params) => ( + + )} + clearOnEscape + /> + + ); + }, +); diff --git a/src/features/profile/components/userInformation/phoneNumber/PhoneEditForm.tsx b/src/features/profile/components/userInformation/phoneNumber/PhoneEditForm.tsx index 081a7ef..2ecf6c1 100644 --- a/src/features/profile/components/userInformation/phoneNumber/PhoneEditForm.tsx +++ b/src/features/profile/components/userInformation/phoneNumber/PhoneEditForm.tsx @@ -45,7 +45,7 @@ export default function PhoneEditForm({ return ( <> - + {t('settingForm.editPhoneNumber')} @@ -59,10 +59,12 @@ export default function PhoneEditForm({ setPhoneNumber(e.target.value)} - sx={{ flex: '1 1 220px' }} placeholder="09123456789" + sx={{ width: { xs: '100%', sm: 474 } }} InputProps={{ endAdornment: buttonState === 'counting' ? ( @@ -142,9 +144,10 @@ export default function PhoneEditForm({ !isValidPhoneNumber(phoneNumber) } sx={{ - minWidth: { xs: '100%', sm: 220 }, + whiteSpace: 'nowrap', color: 'primary.main', textTransform: 'none', + width: { xs: '100%', sm: 210 }, }} > {isSending ? ( @@ -165,11 +168,12 @@ export default function PhoneEditForm({ setVerificationCode(e.target.value.replace(/\D/g, '')) } - sx={{ flex: '1 1 240px', minWidth: 0 }} + sx={{ width: { xs: '100%', sm: 474 } }} placeholder={t('settingForm.verificationCode')} /> @@ -189,7 +193,7 @@ export default function PhoneEditForm({ onClick={handleVerifyClick} disabled={isVerifying || verificationCode.length === 0} sx={{ - minWidth: { xs: '100%', sm: 220 }, + width: { xs: '100%', sm: 210 }, bgcolor: 'primary.main', textTransform: 'none', }} diff --git a/src/features/profile/components/userInformation/socialMedia/SocialMediaDialog.tsx b/src/features/profile/components/userInformation/socialMedia/SocialMediaDialog.tsx index 7d7b48a..7715624 100644 --- a/src/features/profile/components/userInformation/socialMedia/SocialMediaDialog.tsx +++ b/src/features/profile/components/userInformation/socialMedia/SocialMediaDialog.tsx @@ -1,4 +1,4 @@ -import React, { type ReactElement, type ElementType } from 'react'; +import React, { type ReactElement, type ElementType, useState } from 'react'; import { Box, Button, @@ -15,6 +15,7 @@ import type { TransitionProps } from '@mui/material/transitions'; import { CloseCircle } from 'iconsax-react'; import { Icon } from '@rkheftan/harmony-ui'; import { type SocialMediaDialogProps } from '@/features/profile/types/settingsType'; +import { isEmail } from '@/utils/regexes/isEmail'; const MobileSlide = React.forwardRef(function MobileSlide( props: TransitionProps & { children: ReactElement }, @@ -30,57 +31,58 @@ export default function SocialMediaDialog({ emailInput, setEmailInput, fullScreen, - computedMaxWidth, verificationCode, setVerificationCode, - apiError, isLoading, dialogStep, onSendCode, onConfirmEmail, }: SocialMediaDialogProps) { + const [emailError, setEmailError] = useState(); + const [touched, setTouched] = useState(false); + + const validateEmail = (value: string) => { + if (!value) { + setEmailError(t('settingForm.thisFieldIsRequired')); + return false; + } + if (!isEmail(value)) { + setEmailError(t('settingForm.emailIsInvalid')); + return false; + } + setEmailError(undefined); + return true; + }; + return ( - - - - - {t('settingForm.addEmailButton')} + + + + + + + {t('settingForm.addEmailButton')} + + @@ -94,43 +96,46 @@ export default function SocialMediaDialog({ fullWidth type="email" value={emailInput} - onChange={(e) => setEmailInput(e.target.value)} + onChange={(e) => { + setEmailInput(e.target.value); + if (touched) validateEmail(e.target.value); + }} + onBlur={() => { + setTouched(true); + validateEmail(emailInput); + }} label={t('settingForm.email')} placeholder="abc@email.com" autoComplete="email" inputMode="email" - sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }} + sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }} autoFocus disabled={isLoading || dialogStep === 'enterCode'} + error={touched && !!emailError} + helperText={touched && emailError ? emailError : ' '} /> {dialogStep === 'enterCode' && ( - <> - setVerificationCode(e.target.value)} - label={t('settingForm.verificationCode')} - autoComplete="one-time-code" - inputMode="numeric" - sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }} - autoFocus - disabled={isLoading} - /> - - )} - - {apiError && ( - - {apiError} - + setVerificationCode(e.target.value)} + label={t('settingForm.verificationCode')} + autoComplete="one-time-code" + inputMode="numeric" + sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }} + autoFocus + disabled={isLoading} + /> )} + + - + ); } diff --git a/src/features/profile/components/userInformation/socialMedia/SocialMediaList.tsx b/src/features/profile/components/userInformation/socialMedia/SocialMediaList.tsx index 85782c2..2f0a0e0 100644 --- a/src/features/profile/components/userInformation/socialMedia/SocialMediaList.tsx +++ b/src/features/profile/components/userInformation/socialMedia/SocialMediaList.tsx @@ -1,5 +1,5 @@ -import { Box, Typography, IconButton } from '@mui/material'; -import { Google, Sms, Trash } from 'iconsax-react'; +import { Box, Typography } from '@mui/material'; +import { Google, Sms } from 'iconsax-react'; import { Icon } from '@rkheftan/harmony-ui'; import { type SocialMediaListProps } from '@/features/profile/types/settingsType'; @@ -64,10 +64,6 @@ export default function SocialMediaList({ emailList }: SocialMediaListProps) { - - - - ))} diff --git a/src/features/profile/components/userInformation/socialMedia/SocialMediaMenu.tsx b/src/features/profile/components/userInformation/socialMedia/SocialMediaMenu.tsx index ebc3f9c..0716d57 100644 --- a/src/features/profile/components/userInformation/socialMedia/SocialMediaMenu.tsx +++ b/src/features/profile/components/userInformation/socialMedia/SocialMediaMenu.tsx @@ -74,8 +74,8 @@ export default function SocialMediaMenu({ onClose={handleCloseMenu} PaperProps={{ sx: { - minWidth: 187, - maxWidth: '90vw', + width: anchor ? anchor.offsetWidth : 'auto', + // maxWidth: '90vw', }, }} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} diff --git a/src/features/profile/routes/ActiveDevicesPage.tsx b/src/features/profile/routes/ActiveDevicesPage.tsx index 9c6fcca..5ce46d5 100644 --- a/src/features/profile/routes/ActiveDevicesPage.tsx +++ b/src/features/profile/routes/ActiveDevicesPage.tsx @@ -49,7 +49,14 @@ export function ActiveDevicesPage() { ip: session.ipAddress, current: session.key === currentKey, })); - setDevices(formattedDevices); + + const sortedDevices = formattedDevices.sort((a, b) => { + if (a.current && !b.current) return -1; + if (!a.current && b.current) return 1; + return 0; + }); + + setDevices(sortedDevices); } }, [profileData, i18n.language, t]); @@ -66,6 +73,7 @@ export function ActiveDevicesPage() { const { data } = await deleteSessions({ keys: [id] }); if (data.success) { setDevices((prevDevices) => prevDevices.filter((d) => d.id !== id)); + showToast({ message: t('active.successDelete'), severity: 'success' }); } else { showToast({ message: data.message || t('active.deleteFailed'), @@ -73,7 +81,6 @@ export function ActiveDevicesPage() { }); } } catch (error: unknown) { - // console.error('Delete error:', error); showToast({ message: t('active.deleteFailed'), severity: 'error', diff --git a/src/features/profile/routes/SettingPage.tsx b/src/features/profile/routes/SettingPage.tsx index b1da343..7651cbe 100644 --- a/src/features/profile/routes/SettingPage.tsx +++ b/src/features/profile/routes/SettingPage.tsx @@ -59,7 +59,6 @@ export function SettingPage() { } = useApi(fetchProfile); const { - data: saveData, loading: isSaving, error: saveError, execute: executeSaveSettings, @@ -87,15 +86,6 @@ export function SettingPage() { } }, [profileData, setMode, i18n]); - useEffect(() => { - if (saveData?.success) { - setMode(draftSettings.theme); - setSavedSettings(draftSettings); - i18n.changeLanguage(draftSettings.language); - setIsEditing(false); - } - }, [saveData, draftSettings, setMode, i18n]); - useEffect(() => { if (isEditing) { const resolvedMode = mode === 'light' || mode === 'dark' ? mode : 'light'; @@ -109,7 +99,7 @@ export function SettingPage() { setMode(savedSettings.theme); }; - const handleEditToggle = () => { + const handleEditToggle = async () => { if (isEditing) { const languageObj = languageOptions.find( (o) => o.code === draftSettings.language, @@ -119,11 +109,18 @@ export function SettingPage() { ); if (languageObj && calendarObj) { - executeSaveSettings({ + const result = await executeSaveSettings({ theme: themeApiMap[draftSettings.theme], calendarType: calendarObj.apiValue, language: languageObj.apiValue, }); + + if (result?.success) { + setMode(draftSettings.theme); + setSavedSettings(draftSettings); + i18n.changeLanguage(draftSettings.language); + setIsEditing(false); + } } } else { setIsEditing(true); @@ -164,11 +161,13 @@ export function SettingPage() { variant={isEditing ? 'contained' : 'outlined'} disabled={isSaving || isFetching} > - {isSaving - ? t('settings.saving') - : isEditing - ? t('settings.saveButton') - : t('settings.editButton')} + {isSaving ? ( + + ) : isEditing ? ( + t('settings.saveButton') + ) : ( + t('settings.editButton') + )} } @@ -211,40 +210,54 @@ export function SettingPage() { display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 4, - mt: 2, }} > - - {t('settings.theme')} - {isEditing ? ( - { - setDraftSettings((prev) => ({ - ...prev, - theme: newTheme, - })); + - ) : ( - - - - {t(`settings.${savedSettings.theme}`)} + > + + {t('settings.theme')} + { + if (newTheme) { + setMode(newTheme); + setDraftSettings((prev) => ({ + ...prev, + theme: newTheme, + })); + } + }} + /> + ) : ( + <> + + {t('settings.theme')} + + + + + {t(`settings.${savedSettings.theme}`)} + + + )} - - {t('settings.language')} - {isEditing ? ( } + renderInput={(p) => ( + + )} size="medium" fullWidth disableClearable /> ) : ( - - { - languageOptions.find( - (o) => o.code === savedSettings.language, - )?.label - } - + <> + + {t('settings.language')} + + + { + languageOptions.find( + (o) => o.code === savedSettings.language, + )?.label + } + + )} - - {t('settings.calendar')} - {isEditing ? ( c.key)} @@ -287,18 +304,25 @@ export function SettingPage() { onChange={(_, v) => v && setDraftSettings((prev) => ({ ...prev, calendar: v })) } - renderInput={(params) => } + renderInput={(params) => ( + + )} size="medium" fullWidth disableClearable /> ) : ( - - - - {t(`settings.${savedSettings.calendar}`)} + <> + + {t('settings.calendar')} - + + + + {t(`settings.${savedSettings.calendar}`)} + + + )}