fix: validation states, responsive ui, ui states

This commit is contained in:
Sajad Mirjalili
2025-08-21 01:55:09 +03:30
parent b02f655d4d
commit 476ec953b2
10 changed files with 312 additions and 261 deletions

View File

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

View File

@@ -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": "کد تایید باید ۴ رقم باشد."
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -49,7 +49,6 @@ export interface PasswordSectionProps extends ValidationProps {
hasUpperAndLower: boolean;
hasSpecialChar: boolean;
validPassword: boolean;
showValidations: boolean;
}
export interface ValidationItemProps {