feat: add navigation to forger password page, show password icon, toasts for different situations and changes of some styles

This commit is contained in:
Koosha Lahouti
2025-08-20 01:00:20 +03:30
parent 58c044542a
commit 8ed4aab666
23 changed files with 503 additions and 376 deletions

View File

@@ -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"
}
}

View File

@@ -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": "تعویض رمز عبور با مشکل مواجه شد"
}
}

View File

@@ -25,6 +25,7 @@ export function CardContainer({
display: 'flex',
flexDirection: 'column',
gap: 2,
borderRadius: 1,
}}
>
<Box

View File

@@ -1,4 +1,4 @@
import { Box, IconButton, Skeleton, Typography } from '@mui/material';
import { Box, IconButton, Typography } from '@mui/material';
import { Icon } from '@rkheftan/harmony-ui';
import { More } from 'iconsax-react';
import type { UserInfo } from '@/contexts/AuthContext';
@@ -7,7 +7,7 @@ interface HeaderProps {
user: UserInfo;
}
export const Header: React.FC<HeaderProps> = ({ user, loading }) => {
export const Header: React.FC<HeaderProps> = ({ user }) => {
return (
<Box
sx={{

View File

@@ -1,14 +1,9 @@
import {
Avatar,
Box,
IconButton,
Toolbar as MuiToolbar,
Skeleton,
} from '@mui/material';
import { Avatar, Box, IconButton, Toolbar as MuiToolbar } from '@mui/material';
import { Icon } from '@rkheftan/harmony-ui';
import { HambergerMenu, Menu } from 'iconsax-react';
import type { Dispatch, SetStateAction } from 'react';
import type { UserInfo } from '@/contexts/AuthContext';
import Logo from '../Logo';
interface ToolbarProps {
sideNavOpen: boolean;
@@ -22,7 +17,6 @@ export const Toolbar: React.FC<ToolbarProps> = ({
setSideNavOpen,
isMobile,
user,
loading,
}) => {
return (
<MuiToolbar

View File

@@ -1,5 +1,4 @@
import { CircularProgress, Stack } from '@mui/material';
import React from 'react';
export const Loading = () => {
return (

View File

@@ -34,7 +34,9 @@ export const ThemeToggleButton = ({
<ToggleButtonGroup
value={value}
exclusive
color="primary"
onChange={handleChange}
size="medium"
sx={{
borderRadius: 1.5,
border: '1px solid',
@@ -51,10 +53,6 @@ export const ThemeToggleButton = ({
gap: 1,
px: 2,
py: 1,
'&.Mui-selected': {
bgcolor: 'primary.light',
color: 'primary.main',
},
}}
>
<Icon Component={Sun1} color="primary.main" variant="Bold" />
@@ -70,10 +68,6 @@ export const ThemeToggleButton = ({
gap: 1,
px: 2,
py: 1,
'&.Mui-selected': {
bgcolor: 'primary.light',
color: 'primary.main',
},
}}
>
<Icon Component={Moon} size="medium" color="primary.light" />

View File

@@ -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';

View File

@@ -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 = () => {

View File

@@ -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';

View File

@@ -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';

View File

@@ -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 (
<Dialog
open={open}
@@ -80,13 +87,25 @@ export function PasswordDialog({
<>
<TextField
label={t('securityForm.currentPassword')}
type="password"
type={showCurrent ? 'text' : 'password'}
fullWidth
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowCurrent(!showCurrent)}>
<Icon Component={showCurrent ? Eye : EyeSlash} />
</IconButton>
</InputAdornment>
),
}}
/>
<Link sx={{ fontSize: '15px', cursor: 'pointer' }}>
<Link
sx={{ fontSize: '15px', cursor: 'pointer' }}
onClick={() => navigate('/forget-password')}
>
{t('securityForm.forgetPassword')}
</Link>
</>
@@ -94,11 +113,20 @@ export function PasswordDialog({
<TextField
label={t('securityForm.newPassword')}
type="password"
type={showNew ? 'text' : 'password'}
fullWidth
value={password}
onChange={(e) => setPassword(e.target.value)}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 }, mt: 2 }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowNew(!showNew)}>
<Icon Component={showNew ? Eye : EyeSlash} />
</IconButton>
</InputAdornment>
),
}}
/>
{showValidation && (
@@ -124,7 +152,7 @@ export function PasswordDialog({
<TextField
label={t('securityForm.confirmPassword')}
type="password"
type={showConfirm ? 'text' : 'password'}
fullWidth
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
@@ -135,6 +163,15 @@ export function PasswordDialog({
: ' '
}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setShowConfirm(!showConfirm)}>
<Icon Component={showConfirm ? Eye : EyeSlash} />
</IconButton>
</InputAdornment>
),
}}
/>
</DialogContent>
@@ -144,7 +181,6 @@ export function PasswordDialog({
sx={{ height: 48, textTransform: 'none' }}
variant="contained"
onClick={handleSubmit}
disabled={!validPassword || !matchPassword || loading}
>
{loading ? (
<CircularProgress size={24} color="inherit" />

View File

@@ -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,

View File

@@ -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<InfoRowData | null>(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={
<Box
sx={{
display: 'flex',
gap: 1,
}}
>
<Box sx={{ display: 'flex', gap: 1 }}>
{isEditing ? (
<>
<Button
@@ -159,7 +145,6 @@ export function PersonalInformation() {
textTransform: 'none',
width: { xs: '100%', sm: 'auto' },
}}
disabled={isSavingProfile}
>
{isSavingProfile ? (
<CircularProgress size={24} color="inherit" />
@@ -174,7 +159,6 @@ export function PersonalInformation() {
size="large"
variant="outlined"
sx={{ borderRadius: 1 }}
disabled={isLoadingProfile}
>
{t('settingForm.editButton')}
</Button>
@@ -210,46 +194,48 @@ export function PersonalInformation() {
</Typography>
</Box>
) : (
<>
<Box
sx={{
mx: { xs: 2, sm: 3, md: 4 },
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
bgcolor: 'background.paper',
}}
>
{isEditing && (
<ProfileImage
initials={initials}
uploadedImageUrl={uploadedImageUrl}
onImageChange={(file) => {
setUploadedImageFile(file);
const reader = new FileReader();
reader.onload = () =>
setUploadedImageUrl(reader.result as string);
reader.readAsDataURL(file);
}}
onRemoveImage={() => {
setUploadedImageFile(null);
setUploadedImageUrl(null);
}}
<Box
sx={{
mx: { xs: 2, sm: 3, md: 4 },
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
bgcolor: 'background.paper',
}}
>
{isEditing && (
<ProfileImage
initials={initials}
uploadedImageUrl={uploadedImageUrl}
onImageChange={(file) => {
setUploadedImageFile(file);
const reader = new FileReader();
reader.onload = () =>
setUploadedImageUrl(reader.result as string);
reader.readAsDataURL(file);
}}
onRemoveImage={() => {
setUploadedImageFile(null);
setUploadedImageUrl(null);
}}
/>
)}
{data &&
(isEditing ? (
<InfoRowEdit
ref={infoRowEditRef}
data={data}
setData={setData}
/>
)}
{data &&
(isEditing ? (
<InfoRowEdit data={data} setData={setData} />
) : (
<InfoRowDisplay
data={data}
uploadedImageUrl={uploadedImageUrl}
initials={initials}
/>
))}
</Box>
</>
) : (
<InfoRowDisplay
data={data}
uploadedImageUrl={uploadedImageUrl}
initials={initials}
/>
))}
</Box>
)}
</CardContainer>
</PageWrapper>

View File

@@ -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/, ''),
});
};

View File

@@ -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({
<Typography variant="caption" color="text.secondary">
{t('settingForm.country')}
</Typography>
<Box sx={{ mt: 0.5 }}>
<Box
sx={{ mt: 0.5, display: 'flex', alignItems: 'center', gap: 1 }}
>
{data.country ? (
<ReactCountryFlag
countryCode={data.country}
svg
style={{
height: '1.5rem',
width: '1.5rem',
// TODO: Check alignment for better styling definition
marginTop: '-2px',
marginRight: '4px',
}}
/>
<>
<ReactCountryFlag
countryCode={data.country}
svg
style={{ height: '1.5rem', width: '1.5rem' }}
/>
<Typography variant="body1" color="text.primary">
{countryLabel}
</Typography>
</>
) : (
<Typography variant="body1" color="text.primary">
{t('settingForm.notDetermined')}

View File

@@ -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 (
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
{fields.map(({ name, label, value }) => (
<Box
key={name}
sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}
>
<TextField
fullWidth
name={name}
value={value}
onChange={(e) =>
setData((prev) => ({
...prev,
[name]: e.target.value,
}))
}
label={label}
/>
</Box>
))}
return (
data.firstName.trim() !== '' &&
isValidName(data.firstName) &&
data.lastName.trim() !== '' &&
isValidName(data.lastName) &&
data.gender !== Gender.None &&
data.country !== ''
);
},
}));
<Box sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}>
<FormControl fullWidth>
<InputLabel>
{t('settingForm.genderPlaceholder', { ns: 'setting' })}
</InputLabel>
<Select
value={data.gender === Gender.None ? '' : data.gender}
label={t('settingForm.genderPlaceholder', { ns: 'setting' })}
onChange={(e) =>
setData((prev) => ({
...prev,
gender: e.target.value as Gender,
}))
}
const countryOptions = countries.map((c) => ({
code: c.code,
label: t(c.label, { ns: 'country' }),
}));
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.optionalNationalCode', { ns: 'setting' }),
value: data.nationalCode,
},
];
return (
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
{fields.map(({ name, label, value }) => (
<Box
key={name}
sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}
>
<MenuItem value={Gender.Male}>
{t('settingForm.man', { ns: 'setting' })}
</MenuItem>
<MenuItem value={Gender.Female}>
{t('settingForm.woman', { ns: 'setting' })}
</MenuItem>
</Select>
</FormControl>
</Box>
<Autocomplete
sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}
options={countryOptions}
getOptionLabel={(option) => option.label}
value={currentCountry}
onChange={(_, newValue) =>
setData((prev) => ({
...prev,
country: newValue?.code || '',
}))
}
renderOption={(props, option) => (
<Box component="li" {...props} key={option.code}>
<ReactCountryFlag
countryCode={option.code}
svg
style={{
height: '1.5rem',
width: '1.5rem',
// TODO: Check alignment for better styling definition
marginTop: '-2px',
marginRight: '4px',
}}
<TextField
fullWidth
name={name}
value={value}
onChange={(e) =>
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}
</Box>
)}
renderInput={(params) => (
<TextField
{...params}
label={t('settingForm.country', { ns: 'setting' })}
/>
)}
clearOnEscape
/>
</Box>
);
}
))}
<Box sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}>
<FormControl
fullWidth
error={touched.gender && data.gender === Gender.None}
>
<InputLabel>
{t('settingForm.genderPlaceholder', { ns: 'setting' })}
</InputLabel>
<Select
value={data.gender === Gender.None ? '' : data.gender}
label={t('settingForm.genderPlaceholder', { ns: 'setting' })}
onChange={(e) =>
setData((prev) => ({
...prev,
gender: e.target.value as Gender,
}))
}
>
<MenuItem value={Gender.Male}>
{t('settingForm.man', { ns: 'setting' })}
</MenuItem>
<MenuItem value={Gender.Female}>
{t('settingForm.woman', { ns: 'setting' })}
</MenuItem>
</Select>
<FormHelperText>
{touched.gender && data.gender === Gender.None
? t('settingForm.thisFieldIsRequired', { ns: 'setting' })
: ''}
</FormHelperText>
</FormControl>
</Box>
<Autocomplete
sx={{ width: { xs: '100%', sm: '48%', md: 'calc(50% - 8px)' } }}
options={countryOptions}
getOptionLabel={(option) => option.label}
value={currentCountry}
onChange={(_, newValue) =>
setData((prev) => ({ ...prev, country: newValue?.code || '' }))
}
renderOption={(props, option) => (
<Box
component="li"
{...props}
key={option.code}
sx={{ gap: 1, alignItems: 'center' }}
>
<ReactCountryFlag
countryCode={option.code}
svg
style={{ height: '1.5rem', width: '1.5rem' }}
/>
{option.label}
</Box>
)}
renderInput={(params) => (
<TextField
{...params}
label={t('settingForm.country', { ns: 'setting' })}
error={touched.country && data.country === ''}
helperText={
touched.country && data.country === ''
? t('settingForm.thisFieldIsRequired', { ns: 'setting' })
: ''
}
/>
)}
clearOnEscape
/>
</Box>
);
},
);

View File

@@ -45,7 +45,7 @@ export default function PhoneEditForm({
return (
<>
<Box sx={{ width: '100%' }}>
<Box sx={{ mb: 2, mx: 3 }}>
<Box sx={{ mb: 2, mx: 6 }}>
<Typography variant="h6">
{t('settingForm.editPhoneNumber')}
</Typography>
@@ -59,10 +59,12 @@ export default function PhoneEditForm({
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
flexWrap: 'nowrap',
gap: 2,
alignItems: 'center',
mx: 3,
mx: 6,
flexDirection: { xs: 'column', sm: 'row' },
mb: 1,
}}
>
<TextField
@@ -76,8 +78,8 @@ export default function PhoneEditForm({
error={inputError}
helperText={inputError ? error : ''}
onChange={(e) => 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({
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
flexWrap: 'nowrap',
gap: 2,
mt: 2,
alignItems: 'center',
mx: 3,
mx: 6,
flexDirection: { xs: 'column', sm: 'row' },
mb: 1,
}}
>
<TextField
@@ -180,7 +184,7 @@ export default function PhoneEditForm({
onChange={(e) =>
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',
}}

View File

@@ -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<unknown, ElementType> },
@@ -30,57 +31,58 @@ export default function SocialMediaDialog({
emailInput,
setEmailInput,
fullScreen,
computedMaxWidth,
verificationCode,
setVerificationCode,
apiError,
isLoading,
dialogStep,
onSendCode,
onConfirmEmail,
}: SocialMediaDialogProps) {
const [emailError, setEmailError] = useState<string | undefined>();
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 (
<Dialog
open={open}
onClose={onClose}
fullWidth
fullScreen={fullScreen}
maxWidth={computedMaxWidth}
scroll="paper"
maxWidth="xs"
scroll="body"
keepMounted
TransitionComponent={fullScreen ? MobileSlide : undefined}
PaperProps={{
sx: {
borderRadius: { xs: 0, sm: 2 },
width: { xs: '100%', sm: '30%' },
height: 'auto',
},
}}
PaperProps={{ sx: { mx: 1 } }}
>
<DialogTitle
sx={{
display: 'flex',
alignItems: 'center',
fontWeight: 'bold',
fontSize: '16px',
gap: 1,
p: { xs: 1.5, sm: 2 },
bgcolor: 'background.default',
}}
>
<IconButton onClick={onClose} aria-label="Close" disabled={isLoading}>
<Icon Component={CloseCircle} size="medium" color="primary.main" />
</IconButton>
{t('settingForm.addEmailButton')}
<DialogTitle sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton onClick={onClose} disabled={isLoading}>
<Icon Component={CloseCircle} size="large" color="primary.main" />
</IconButton>
<Box component="span" fontWeight="bold" fontSize={18}>
{t('settingForm.addEmailButton')}
</Box>
</Box>
</DialogTitle>
<DialogContent
dividers
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
py: { xs: 1.5, sm: 2 },
px: { xs: 2, sm: 3 },
}}
>
<Box>
@@ -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' && (
<>
<TextField
fullWidth
type="text"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
label={t('settingForm.verificationCode')}
autoComplete="one-time-code"
inputMode="numeric"
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
autoFocus
disabled={isLoading}
/>
</>
)}
{apiError && (
<Typography color="error" variant="caption" sx={{ mt: 1 }}>
{apiError}
</Typography>
<TextField
fullWidth
type="text"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
label={t('settingForm.verificationCode')}
autoComplete="one-time-code"
inputMode="numeric"
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 2 } }}
autoFocus
disabled={isLoading}
/>
)}
</DialogContent>
<Box sx={{ px: 3, pb: 2 }}>
<Button
variant="contained"
fullWidth
sx={{ textTransform: 'none', borderRadius: 2, mt: 1 }}
sx={{ height: 48, textTransform: 'none', borderRadius: 2 }}
variant="contained"
disabled={isLoading || (dialogStep === 'enterEmail' && !emailInput)}
onClick={dialogStep === 'enterEmail' ? onSendCode : onConfirmEmail}
>
@@ -142,7 +147,7 @@ export default function SocialMediaDialog({
t('settingForm.confirmAndSave')
)}
</Button>
</DialogContent>
</Box>
</Dialog>
);
}

View File

@@ -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) {
</Typography>
</Box>
</Box>
<IconButton aria-label="Delete">
<Icon Component={Trash} size="medium" variant="Outline" />
</IconButton>
</Box>
))}
</Box>

View File

@@ -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' }}

View File

@@ -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',

View File

@@ -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 ? (
<CircularProgress size={24} />
) : isEditing ? (
t('settings.saveButton')
) : (
t('settings.editButton')
)}
</Button>
</Box>
}
@@ -211,40 +210,54 @@ export function SettingPage() {
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
gap: 4,
mt: 2,
}}
>
<Box sx={{ flex: 1 }}>
<Typography variant="caption" color="text.secondary">
{t('settings.theme')}
</Typography>
{isEditing ? (
<ThemeToggleButton
value={draftSettings.theme}
onChange={(newTheme) => {
setDraftSettings((prev) => ({
...prev,
theme: newTheme,
}));
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
}}
/>
) : (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Icon
Component={savedSettings.theme === 'light' ? Sun1 : Moon}
size="medium"
variant="Bold"
/>
<Typography variant="body1">
{t(`settings.${savedSettings.theme}`)}
>
<Typography variant="body1" color="text.primary">
{t('settings.theme')}
</Typography>
<ThemeToggleButton
value={draftSettings.theme}
onChange={(newTheme) => {
if (newTheme) {
setMode(newTheme);
setDraftSettings((prev) => ({
...prev,
theme: newTheme,
}));
}
}}
/>
</Box>
) : (
<>
<Typography variant="caption" color="text.secondary">
{t('settings.theme')}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Icon
Component={
savedSettings.theme === 'light' ? Sun1 : Moon
}
size="medium"
variant="Bold"
/>
<Typography variant="body1">
{t(`settings.${savedSettings.theme}`)}
</Typography>
</Box>
</>
)}
</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="caption" color="text.secondary">
{t('settings.language')}
</Typography>
{isEditing ? (
<Autocomplete
options={languageOptions}
@@ -259,26 +272,30 @@ export function SettingPage() {
language: v.code,
}))
}
renderInput={(p) => <TextField {...p} />}
renderInput={(p) => (
<TextField {...p} label={t('settings.language')} />
)}
size="medium"
fullWidth
disableClearable
/>
) : (
<Typography variant="body1">
{
languageOptions.find(
(o) => o.code === savedSettings.language,
)?.label
}
</Typography>
<>
<Typography variant="caption" color="text.secondary">
{t('settings.language')}
</Typography>
<Typography variant="body1">
{
languageOptions.find(
(o) => o.code === savedSettings.language,
)?.label
}
</Typography>
</>
)}
</Box>
</Box>
<Box sx={{ mt: 2, width: { xs: '100%', sm: 'calc(50% - 16px)' } }}>
<Typography variant="caption" color="text.secondary">
{t('settings.calendar')}
</Typography>
{isEditing ? (
<Autocomplete
options={calendarOptions.map((c) => c.key)}
@@ -287,18 +304,25 @@ export function SettingPage() {
onChange={(_, v) =>
v && setDraftSettings((prev) => ({ ...prev, calendar: v }))
}
renderInput={(params) => <TextField {...params} />}
renderInput={(params) => (
<TextField {...params} label={t('settings.calendar')} />
)}
size="medium"
fullWidth
disableClearable
/>
) : (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Icon Component={Calendar1} size="medium" variant="Bold" />
<Typography variant="body1">
{t(`settings.${savedSettings.calendar}`)}
<>
<Typography variant="caption" color="text.secondary">
{t('settings.calendar')}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Icon Component={Calendar1} size="medium" variant="Bold" />
<Typography variant="body1">
{t(`settings.${savedSettings.calendar}`)}
</Typography>
</Box>
</>
)}
</Box>
</Box>