fix: validation states, responsive ui, ui states
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
"description": "Enter your business information",
|
||||
"name": "Name",
|
||||
"familyName": "Family Name",
|
||||
"gender": "Gender(Optional)",
|
||||
"gender": "Gender",
|
||||
"optionalNationalCode": "National Code(Optional)",
|
||||
"determinePassword": "Determine Password",
|
||||
"password": "Password",
|
||||
@@ -19,7 +19,7 @@
|
||||
"woman": "female",
|
||||
"hasNumber": "includes number",
|
||||
"hasMinLength": "at least 8 characters",
|
||||
"hasUpperAndLower": "includes a lowercase and uppercase letter",
|
||||
"hasUpperAndLower": "includes a lowercase and uppercase English letter",
|
||||
"hasSpecialChar": "includes sign (!@#$%^&*)",
|
||||
"notCompatibility": "does not match",
|
||||
"emailCorrectForm": "Enter the correct email form.",
|
||||
@@ -28,6 +28,7 @@
|
||||
"agreementPart2": ".",
|
||||
"sent": "sent",
|
||||
"country": "country",
|
||||
"noOption": "No option",
|
||||
"dateOfBirth": "Date of birth(optional)",
|
||||
"submitSuccess": "Information successfully registered",
|
||||
"submitError": "Error in registering information",
|
||||
@@ -46,6 +47,8 @@
|
||||
"confirmPasswordRequired": "Please confirm your password.",
|
||||
"passwordsDoNotMatch": "Passwords do not match.",
|
||||
"verificationCodeRequired": "Verification code is required.",
|
||||
"mustVerifyCode": "Please click the verify button to confirm the code."
|
||||
"mustVerifyCode": "Please click the verify button to confirm the code.",
|
||||
"genderRequired": "Selecting a gender is required.",
|
||||
"verificationCodeInvalid": "Verification code must be 4 digits."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "اطلاعات کسب و کار خود را وارد کنید",
|
||||
"name": "نام",
|
||||
"familyName": "نام خانوادگی",
|
||||
"gender": "جنسیت(اختیاری)",
|
||||
"gender": "جنسیت",
|
||||
"optionalNationalCode": "کدملی(اختیاری)",
|
||||
"determinePassword": "تعیین رمز عبور",
|
||||
"password": "رمز عبور",
|
||||
@@ -19,7 +19,7 @@
|
||||
"woman": "زن",
|
||||
"hasNumber": "شامل عدد",
|
||||
"hasMinLength": "حداقل 8 کاراکتر",
|
||||
"hasUpperAndLower": "شامل یک حرف کوچک و بزرگ",
|
||||
"hasUpperAndLower": "شامل یک حرف کوچک و بزرگ انگلیسی",
|
||||
"hasSpecialChar": "شامل علامت (!@#$%^&*)",
|
||||
"notCompatibility": "تکرار رمز عبور با رمز عبور یکسان نمی باشد",
|
||||
"emailCorrectForm": "ساختار ایمیل صحیح نیست",
|
||||
@@ -28,6 +28,7 @@
|
||||
"agreementPart2": " می باشد.",
|
||||
"sent": "ارسال شد!",
|
||||
"country": "کشور",
|
||||
"noOption": "گزینه ای یافت نشد",
|
||||
"dateOfBirth": "تاریخ تولد(اختیاری)",
|
||||
"invalidCountry": "کشور انتخاب شده صحیح نیست",
|
||||
"rules": "قوانین و مقررات",
|
||||
@@ -54,6 +55,8 @@
|
||||
"confirmPasswordRequired": "لطفاً رمز عبور خود را تکرار کنید.",
|
||||
"passwordsDoNotMatch": "رمزهای عبور با یکدیگر مطابقت ندارند.",
|
||||
"verificationCodeRequired": "وارد کردن کد تایید الزامی است.",
|
||||
"mustVerifyCode": "لطفاً برای تایید کد، روی دکمه بررسی کلیک کنید."
|
||||
"mustVerifyCode": "لطفاً برای تایید کد، روی دکمه بررسی کلیک کنید.",
|
||||
"genderRequired": "انتخاب جنسیت الزامی است.",
|
||||
"verificationCodeInvalid": "کد تایید باید ۴ رقم باشد."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,12 @@ export const AuthenticationCard = ({
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
p: {
|
||||
xs: 4,
|
||||
xs: 3,
|
||||
md: 6,
|
||||
},
|
||||
marginInline: 2,
|
||||
width: (t) => `calc(100% - ${t.spacing(2)})`,
|
||||
boxSizing: 'border-box',
|
||||
width: '100%',
|
||||
maxWidth: maxWidth ?? '552px',
|
||||
|
||||
...sx,
|
||||
|
||||
@@ -15,6 +15,8 @@ import { format } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toLocaleDigits } from '@/utils/persianDigit';
|
||||
import { type DateOfBirthProps } from '../../types/settingForm';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { Calendar } from 'iconsax-react';
|
||||
|
||||
export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
|
||||
const { t, i18n } = useTranslation('completionForm');
|
||||
@@ -49,6 +51,10 @@ export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
|
||||
);
|
||||
};
|
||||
|
||||
const CustomCalendarIcon = () => (
|
||||
<Icon Component={Calendar} color="primary.main" variant="Bold" />
|
||||
);
|
||||
|
||||
return (
|
||||
<LocalizationProvider dateAdapter={Adapter} adapterLocale={locale}>
|
||||
<DatePicker
|
||||
@@ -57,7 +63,8 @@ export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
|
||||
onChange={onChange}
|
||||
format={formatString}
|
||||
dayOfWeekFormatter={dayOfWeekFormatter}
|
||||
slots={{ day: CustomDay }}
|
||||
slots={{ day: CustomDay, openPickerIcon: CustomCalendarIcon }}
|
||||
disableFuture
|
||||
slotProps={{
|
||||
textField: {
|
||||
fullWidth: true,
|
||||
|
||||
@@ -10,10 +10,11 @@ import {
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TickCircle, Edit } from 'iconsax-react';
|
||||
import { TickCircle, Edit2 } from 'iconsax-react';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { type EmailSectionProps } from '../../types/settingForm';
|
||||
import { toLocaleDigits } from '@/utils/persianDigit';
|
||||
import { isNumeric } from '@/utils/regexes/isNumeric';
|
||||
|
||||
export function EmailSection({
|
||||
showEmail,
|
||||
@@ -41,6 +42,16 @@ export function EmailSection({
|
||||
setShowEmail(e.target.checked);
|
||||
};
|
||||
|
||||
const handleVerificationCodeChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const value = e.target.value;
|
||||
// Allow only digits and enforce the max length
|
||||
if (isNumeric(value) && value.length <= 4) {
|
||||
setVerificationCode(value);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTimerValue = () => {
|
||||
const m = String(Math.floor(countdown / 60)).padStart(2, '0');
|
||||
const s = String(countdown % 60).padStart(2, '0');
|
||||
@@ -71,7 +82,6 @@ export function EmailSection({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
px: { xs: 2, sm: 0 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@@ -90,6 +100,7 @@ export function EmailSection({
|
||||
autoFocus
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onBlur={() => handleBlur('email')}
|
||||
disabled={isSendingCode || codeSent}
|
||||
error={touched.email && !!errors.email}
|
||||
helperText={touched.email && errors.email}
|
||||
sx={{ flex: '1 1 260px' }}
|
||||
@@ -97,7 +108,7 @@ export function EmailSection({
|
||||
input: {
|
||||
startAdornment:
|
||||
!isVerifyingCode && emailVerified ? (
|
||||
<InputAdornment position="end">
|
||||
<InputAdornment position="start">
|
||||
<Icon
|
||||
Component={TickCircle}
|
||||
size="medium"
|
||||
@@ -106,21 +117,17 @@ export function EmailSection({
|
||||
/>
|
||||
</InputAdornment>
|
||||
) : null,
|
||||
endAdornment:
|
||||
buttonState === 'counting' ? (
|
||||
<InputAdornment position="start">
|
||||
<IconButton onClick={handleEditEmail}>
|
||||
<Icon
|
||||
Component={Edit}
|
||||
color="primary.main"
|
||||
size="medium"
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
) : null,
|
||||
sx: {
|
||||
paddingLeft: buttonState === 'counting' ? 0 : undefined,
|
||||
},
|
||||
endAdornment: codeSent ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={handleEditEmail}>
|
||||
<Icon
|
||||
Component={Edit2}
|
||||
color="primary.main"
|
||||
size="medium"
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
) : null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -164,9 +171,11 @@ export function EmailSection({
|
||||
>
|
||||
<TextField
|
||||
label={t('completion.verificationCode')}
|
||||
autoFocus
|
||||
variant="outlined"
|
||||
type="text"
|
||||
value={verificationCode}
|
||||
onChange={(e) => setVerificationCode(e.target.value)}
|
||||
onChange={handleVerificationCodeChange}
|
||||
sx={{ flex: '1 1 260px' }}
|
||||
disabled={isVerifyingCode}
|
||||
onBlur={() => handleBlur('verificationCode')}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
FormGroup,
|
||||
Typography,
|
||||
InputAdornment,
|
||||
Grid,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TickCircle, Eye, EyeSlash, CloseCircle } from 'iconsax-react';
|
||||
@@ -27,7 +28,6 @@ export function PasswordSection({
|
||||
hasUpperAndLower,
|
||||
hasSpecialChar,
|
||||
validPassword,
|
||||
showValidations,
|
||||
errors,
|
||||
touched,
|
||||
handleBlur,
|
||||
@@ -36,6 +36,7 @@ export function PasswordSection({
|
||||
const [showPasswordText, setShowPasswordText] = useState(false);
|
||||
const [showPasswordRepetitionText, setShowPasswordRepetitionText] =
|
||||
useState(false);
|
||||
const [showValidations, setShowValidation] = useState(false);
|
||||
|
||||
const handleTogglePasswordSection = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
@@ -47,6 +48,11 @@ export function PasswordSection({
|
||||
const handleTogglePasswordRepetitionEye = () =>
|
||||
setShowPasswordRepetitionText((prev) => !prev);
|
||||
|
||||
const handleBlurPassword = () => {
|
||||
handleBlur('password');
|
||||
setShowValidation(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormGroup>
|
||||
@@ -72,29 +78,18 @@ export function PasswordSection({
|
||||
</FormGroup>
|
||||
|
||||
{showPasswordSection && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
justifyContent: { xs: 'center', sm: 'flex-start' },
|
||||
}}
|
||||
>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('completion.password')}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
variant="outlined"
|
||||
type={showPasswordText ? 'text' : 'password'}
|
||||
autoFocus
|
||||
onBlur={() => handleBlur('password')}
|
||||
onBlur={handleBlurPassword}
|
||||
onFocus={() => setShowValidation(true)}
|
||||
error={touched.password && !!errors.password}
|
||||
helperText={touched.password && errors.password}
|
||||
sx={{
|
||||
@@ -103,18 +98,10 @@ export function PasswordSection({
|
||||
pr: 8, // Increased padding to accommodate both icons
|
||||
},
|
||||
}}
|
||||
// FIXME: deprecated
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
minWidth: '64px', // Adjusted to fit both icons
|
||||
}}
|
||||
>
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={handleTogglePasswordEye}
|
||||
sx={{ p: 0.5 }}
|
||||
@@ -133,21 +120,25 @@ export function PasswordSection({
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
{validPassword && (
|
||||
<Icon
|
||||
Component={TickCircle}
|
||||
size="medium"
|
||||
color="success.main"
|
||||
variant="Bold"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</InputAdornment>
|
||||
),
|
||||
</InputAdornment>
|
||||
),
|
||||
startAdornment: validPassword && (
|
||||
<InputAdornment position="start">
|
||||
<Icon
|
||||
Component={TickCircle}
|
||||
size="medium"
|
||||
color="success.main"
|
||||
variant="Bold"
|
||||
/>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('completion.passwordRepetition')}
|
||||
variant="outlined"
|
||||
value={confirmPassword}
|
||||
@@ -157,19 +148,12 @@ export function PasswordSection({
|
||||
helperText={touched.confirmPassword && errors.confirmPassword}
|
||||
type={showPasswordRepetitionText ? 'text' : 'password'}
|
||||
sx={{
|
||||
flex: '1 1 260px',
|
||||
flex: '1 1',
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
minWidth: '64px',
|
||||
}}
|
||||
>
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={handleTogglePasswordRepetitionEye}
|
||||
sx={{ p: 0.5 }}
|
||||
@@ -188,65 +172,55 @@ export function PasswordSection({
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
{confirmPassword.length > 0 && (
|
||||
<Icon
|
||||
Component={matchPassword ? TickCircle : CloseCircle}
|
||||
size="medium"
|
||||
color={matchPassword ? 'success.main' : 'error.main'}
|
||||
variant="Bold"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</InputAdornment>
|
||||
),
|
||||
</InputAdornment>
|
||||
),
|
||||
startAdornment: confirmPassword.length > 0 && (
|
||||
<InputAdornment position="start">
|
||||
<Icon
|
||||
Component={matchPassword ? TickCircle : CloseCircle}
|
||||
size="medium"
|
||||
color={matchPassword ? 'success.main' : 'error.main'}
|
||||
variant="Bold"
|
||||
/>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{password && showValidations && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: { xs: 0, sm: 2 },
|
||||
justifyContent: { xs: 'center', sm: 'flex-start' },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
flex: '1 1 260px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{showValidations && (
|
||||
<>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<PasswordValidationItem
|
||||
isValid={hasNumber}
|
||||
label={t('completion.hasNumber')}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<PasswordValidationItem
|
||||
isValid={hasMinLength}
|
||||
label={t('completion.hasMinLength')}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
flex: '1 1 260px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<PasswordValidationItem
|
||||
isValid={hasUpperAndLower}
|
||||
label={t('completion.hasUpperAndLower')}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<PasswordValidationItem
|
||||
isValid={hasSpecialChar}
|
||||
label={t('completion.hasSpecialChar')}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
Select,
|
||||
Box,
|
||||
Autocomplete,
|
||||
Grid,
|
||||
type SelectChangeEvent,
|
||||
FormHelperText,
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Woman, Man } from 'iconsax-react';
|
||||
@@ -63,134 +65,115 @@ export function PersonalInfoFields({
|
||||
];
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 2,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
justifyContent: { xs: 'center', sm: 'flex-start' },
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label={t('completion.name')}
|
||||
placeholder={t('completion.name')}
|
||||
variant="outlined"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
sx={{ flex: '1 1 260px' }}
|
||||
onBlur={() => handleBlur('firstName')}
|
||||
error={touched.firstName && !!errors.firstName}
|
||||
helperText={touched.firstName && errors.firstName}
|
||||
/>
|
||||
<TextField
|
||||
label={t('completion.familyName')}
|
||||
placeholder={t('completion.familyName')}
|
||||
variant="outlined"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
sx={{ flex: '1 1 260px' }}
|
||||
onBlur={() => handleBlur('lastName')}
|
||||
error={touched.lastName && !!errors.lastName}
|
||||
helperText={touched.lastName && errors.lastName}
|
||||
/>
|
||||
</Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
label={t('completion.name')}
|
||||
placeholder={t('completion.name')}
|
||||
variant="outlined"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
onBlur={() => handleBlur('firstName')}
|
||||
error={touched.firstName && !!errors.firstName}
|
||||
helperText={touched.firstName && errors.firstName}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
justifyContent: { xs: 'center', sm: 'flex-start' },
|
||||
}}
|
||||
>
|
||||
<FormControl sx={{ flex: '1 1 260px' }}>
|
||||
<InputLabel>{t('completion.gender')}</InputLabel>
|
||||
<Select
|
||||
value={sex || ''}
|
||||
label={t('completion.gender')}
|
||||
onChange={handleChangeSex}
|
||||
>
|
||||
{genderOptions.map((g) => (
|
||||
<MenuItem key={g.value} value={g.value}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Icon Component={g.icon} size="small" color={g.color} />
|
||||
{g.label}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('completion.familyName')}
|
||||
placeholder={t('completion.familyName')}
|
||||
variant="outlined"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
onBlur={() => handleBlur('lastName')}
|
||||
error={touched.lastName && !!errors.lastName}
|
||||
helperText={touched.lastName && errors.lastName}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<TextField
|
||||
label={t('completion.optionalNationalCode')}
|
||||
placeholder={t('completion.optionalNationalCode')}
|
||||
value={nationalId}
|
||||
onChange={(e) => setNationalId(e.target.value)}
|
||||
variant="outlined"
|
||||
sx={{ flex: '1 1 260px' }}
|
||||
onBlur={() => handleBlur('nationalId')}
|
||||
error={touched.nationalId && !!errors.nationalId}
|
||||
helperText={touched.nationalId && errors.nationalId}
|
||||
/>
|
||||
</Box>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<FormControl fullWidth error={touched.sex && !!errors.sex}>
|
||||
<InputLabel>{t('completion.gender')}</InputLabel>
|
||||
<Select
|
||||
value={sex || ''}
|
||||
label={t('completion.gender')}
|
||||
onChange={handleChangeSex}
|
||||
onBlur={() => handleBlur('sex')}
|
||||
>
|
||||
{genderOptions.map((g) => (
|
||||
<MenuItem key={g.value} value={g.value}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Icon Component={g.icon} size="small" color={g.color} />
|
||||
{g.label}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
justifyContent: { xs: 'center', sm: 'flex-start' },
|
||||
}}
|
||||
>
|
||||
<Autocomplete
|
||||
sx={{ flex: '1 1 260px' }}
|
||||
options={countryOptions}
|
||||
getOptionLabel={(option) => option.label}
|
||||
value={currentCountry}
|
||||
onChange={(_, newValue) => setCountry(newValue?.code || '')}
|
||||
onBlur={() => handleBlur('country')}
|
||||
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',
|
||||
marginLeft: '8px',
|
||||
}}
|
||||
/>
|
||||
{option.label}
|
||||
</Box>
|
||||
)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={t('completion.country')}
|
||||
error={touched.country && !!errors.country}
|
||||
helperText={touched.country && errors.country}
|
||||
{touched.sex && errors.sex && (
|
||||
<FormHelperText>{errors.sex}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
options={countryOptions}
|
||||
getOptionLabel={(option) => option.label}
|
||||
noOptionsText={t('completion.noOption')}
|
||||
value={currentCountry}
|
||||
onChange={(_, newValue) => setCountry(newValue?.code || '')}
|
||||
onBlur={() => handleBlur('country')}
|
||||
renderOption={(props, option) => (
|
||||
<Box component="li" {...props} key={option.code}>
|
||||
<ReactCountryFlag
|
||||
countryCode={option.code}
|
||||
svg
|
||||
style={{
|
||||
height: '1.5rem',
|
||||
width: '1.5rem',
|
||||
marginTop: '-2px',
|
||||
marginRight: '4px',
|
||||
marginLeft: '8px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
clearOnEscape
|
||||
/>
|
||||
{option.label}
|
||||
</Box>
|
||||
)}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label={t('completion.country')}
|
||||
error={touched.country && !!errors.country}
|
||||
helperText={touched.country && errors.country}
|
||||
/>
|
||||
)}
|
||||
clearOnEscape
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Box sx={{ flex: '1 1 260px' }}>
|
||||
<DateOfBirth value={birthDate} onChange={setBirthDate} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('completion.optionalNationalCode')}
|
||||
placeholder={t('completion.optionalNationalCode')}
|
||||
value={nationalId}
|
||||
onChange={(e) => setNationalId(e.target.value)}
|
||||
variant="outlined"
|
||||
onBlur={() => handleBlur('nationalId')}
|
||||
error={touched.nationalId && !!errors.nationalId}
|
||||
helperText={touched.nationalId && errors.nationalId}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<DateOfBirth value={birthDate} onChange={setBirthDate} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,9 +28,8 @@ export function SubmitSection({ onSubmit, loading }: SubmitProps) {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 2,
|
||||
px: { xs: 2, sm: 0 },
|
||||
mb: 2,
|
||||
justifyContent: { xs: 'center', sm: 'flex-start' },
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
|
||||
@@ -49,7 +49,6 @@ export function UserCompletionPage() {
|
||||
);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [emailVerified, setEmailVerified] = useState(false);
|
||||
const [showPasswordValidations, setShowPasswordValidations] = useState(false);
|
||||
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
const [touched, setTouched] = useState<{ [key: string]: boolean }>({});
|
||||
@@ -73,10 +72,6 @@ export function UserCompletionPage() {
|
||||
completeUserInformationApi,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setShowPasswordValidations(password ? !validPassword : false);
|
||||
}, [password, validPassword]);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (buttonState === 'counting' && countdown > 0) {
|
||||
@@ -101,6 +96,17 @@ export function UserCompletionPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
setTouched((prev) => {
|
||||
const newTouched = { ...prev };
|
||||
delete newTouched.verificationCode;
|
||||
return newTouched;
|
||||
});
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors.verificationCode;
|
||||
return newErrors;
|
||||
});
|
||||
|
||||
const res = await sendCode({ email });
|
||||
|
||||
if (res) {
|
||||
@@ -122,14 +128,23 @@ export function UserCompletionPage() {
|
||||
};
|
||||
|
||||
const handleVerifyCode = async () => {
|
||||
if (!verificationCode.trim()) {
|
||||
// Manually trigger the validation error and stop
|
||||
const trimmedCode = verificationCode.trim();
|
||||
|
||||
// Manually trigger the validation error and stop
|
||||
if (!trimmedCode) {
|
||||
setTouched((prev) => ({ ...prev, verificationCode: true }));
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
verificationCode: t('validation.verificationCodeRequired'),
|
||||
}));
|
||||
return;
|
||||
} else if (trimmedCode.length < 4) {
|
||||
setTouched((prev) => ({ ...prev, verificationCode: true }));
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
verificationCode: t('validation.verificationCodeInvalid'),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await verifyCode({ email, otpCode: verificationCode });
|
||||
@@ -161,6 +176,7 @@ export function UserCompletionPage() {
|
||||
verificationCode: showEmail,
|
||||
password: showPasswordSection,
|
||||
confirmPassword: showPasswordSection,
|
||||
sex: true,
|
||||
});
|
||||
|
||||
const isValid = validateForm();
|
||||
@@ -207,10 +223,22 @@ export function UserCompletionPage() {
|
||||
setCodeSent(false);
|
||||
setEmailVerified(false);
|
||||
setVerificationCode('');
|
||||
// We clear both touched and errors
|
||||
setTouched((prev) => {
|
||||
const newTouched = { ...prev };
|
||||
delete newTouched.email;
|
||||
delete newTouched.verificationCode;
|
||||
return newTouched;
|
||||
});
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors.email;
|
||||
delete newErrors.verificationCode;
|
||||
return newErrors;
|
||||
});
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
// TODO: check if need separate validation or same common validation
|
||||
const newErrors: { [key: string]: string } = {};
|
||||
|
||||
// Rule 1: First Name is required
|
||||
@@ -235,11 +263,16 @@ export function UserCompletionPage() {
|
||||
|
||||
// Rule 6: If verification code sent and email section is active
|
||||
if (showEmail && codeSent && !emailVerified) {
|
||||
if (!verificationCode.trim()) {
|
||||
const trimmedCode = verificationCode.trim();
|
||||
|
||||
if (!trimmedCode) {
|
||||
// Case 1: The code is required but the field is empty.
|
||||
newErrors.verificationCode = t('validation.verificationCodeRequired');
|
||||
} else if (trimmedCode.length < 4) {
|
||||
// Case 2 : The code is entered but is less than 4 digits.
|
||||
newErrors.verificationCode = t('validation.verificationCodeInvalid');
|
||||
} else {
|
||||
// Case 2: The user has typed a code but hasn't clicked "Verify" yet.
|
||||
// Case 3: The user has typed a code but hasn't clicked "Verify" yet.
|
||||
newErrors.verificationCode = t('validation.mustVerifyCode');
|
||||
}
|
||||
}
|
||||
@@ -263,6 +296,10 @@ export function UserCompletionPage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (sex === null) {
|
||||
newErrors.sex = t('validation.genderRequired');
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0; // Returns true if form is valid
|
||||
};
|
||||
@@ -271,6 +308,43 @@ export function UserCompletionPage() {
|
||||
setTouched((prev) => ({ ...prev, [field]: true }));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!showPasswordSection) {
|
||||
// We clear both touched and errors to prevent lingering validation messages
|
||||
setTouched((prev) => {
|
||||
const newTouched = { ...prev };
|
||||
delete newTouched.password;
|
||||
delete newTouched.confirmPassword;
|
||||
return newTouched;
|
||||
});
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors.password;
|
||||
delete newErrors.confirmPassword;
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
}, [showPasswordSection]);
|
||||
|
||||
// This effect resets email fields when the section is hidden
|
||||
useEffect(() => {
|
||||
if (!showEmail) {
|
||||
// We clear both touched and errors
|
||||
setTouched((prev) => {
|
||||
const newTouched = { ...prev };
|
||||
delete newTouched.email;
|
||||
delete newTouched.verificationCode;
|
||||
return newTouched;
|
||||
});
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors.email;
|
||||
delete newErrors.verificationCode;
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
}, [showEmail]);
|
||||
|
||||
// re-validate whenever a field the user has touched changes value
|
||||
useEffect(() => {
|
||||
// Only run validation if at least one field has been touched
|
||||
@@ -289,6 +363,7 @@ export function UserCompletionPage() {
|
||||
password,
|
||||
confirmPassword,
|
||||
showPasswordSection,
|
||||
sex,
|
||||
touched,
|
||||
]);
|
||||
|
||||
@@ -300,17 +375,16 @@ export function UserCompletionPage() {
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
gap: 3,
|
||||
// TODO: check if this padding needed for mobile view
|
||||
py: { sm: 0, xs: 2 },
|
||||
py: { md: 0, xs: 4 },
|
||||
}}
|
||||
>
|
||||
<Logo />
|
||||
<AuthenticationCard
|
||||
// TODO: check styles
|
||||
sx={{
|
||||
maxHeight: { sm: '80vh', xs: 'unset' },
|
||||
flex: { sm: 'unset', xs: 1 },
|
||||
overflowY: 'auto',
|
||||
scrollbarGutter: 'stable both-edges',
|
||||
}}
|
||||
maxWidth="730px"
|
||||
>
|
||||
@@ -361,7 +435,6 @@ export function UserCompletionPage() {
|
||||
hasUpperAndLower={hasUpperAndLower}
|
||||
hasSpecialChar={hasSpecialChar}
|
||||
validPassword={validPassword}
|
||||
showValidations={showPasswordValidations}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
handleBlur={handleBlur}
|
||||
|
||||
@@ -49,7 +49,6 @@ export interface PasswordSectionProps extends ValidationProps {
|
||||
hasUpperAndLower: boolean;
|
||||
hasSpecialChar: boolean;
|
||||
validPassword: boolean;
|
||||
showValidations: boolean;
|
||||
}
|
||||
|
||||
export interface ValidationItemProps {
|
||||
|
||||
Reference in New Issue
Block a user