fix: completion page apis, validation, email states

This commit is contained in:
Sajad Mirjalili
2025-08-20 20:55:24 +03:30
parent 58c044542a
commit b02f655d4d
38 changed files with 935 additions and 1203 deletions

View File

@@ -61,8 +61,13 @@ export default function DateOfBirth({ value, onChange }: DateOfBirthProps) {
slotProps={{
textField: {
fullWidth: true,
},
popper: {
sx: {
flex: '1 1 260px',
'& .MuiDateCalendar-root': {
// TODO: fix this to use textfield width instead of defining hardcode
width: '309px',
},
},
},
}}

View File

@@ -8,41 +8,45 @@ import {
Typography,
InputAdornment,
IconButton,
CircularProgress,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { TickCircle, Edit } from 'iconsax-react';
import { Icon } from '@rkheftan/harmony-ui';
import { type EmailSectionProps } from '../../types/settingForm';
import { toLocaleDigits } from '@/utils/persianDigit';
export function EmailSection({
showEmail,
setShowEmail,
email,
setEmail,
correctEmail,
codeSent,
verificationCode,
setVerificationCode,
buttonState,
getButtonLabel,
handleSendCode,
handleVerifyCode,
emailVerified,
loading,
isVerifyingCode,
isSendingCode,
handleEditEmail,
errors,
touched,
handleBlur,
countdown,
}: EmailSectionProps) {
const { t } = useTranslation('completionForm');
const onSendCodeClick = () => {
if (!correctEmail) return;
handleSendCode();
};
const { t, i18n } = useTranslation('completionForm');
const handleToggleEmail = (e: React.ChangeEvent<HTMLInputElement>) => {
setShowEmail(e.target.checked);
};
const formatTimerValue = () => {
const m = String(Math.floor(countdown / 60)).padStart(2, '0');
const s = String(countdown % 60).padStart(2, '0');
return toLocaleDigits(`${m}:${s}`, i18n.language);
};
return (
<>
<FormGroup>
@@ -51,7 +55,6 @@ export function EmailSection({
display: 'flex',
alignItems: 'center',
gap: 1,
px: { xs: 2, sm: 0 },
}}
>
<Switch checked={showEmail} onChange={handleToggleEmail} />
@@ -84,13 +87,16 @@ export function EmailSection({
variant="outlined"
fullWidth
value={email}
autoFocus
onChange={(e) => setEmail(e.target.value)}
error={email.length > 0 && !correctEmail}
onBlur={() => handleBlur('email')}
error={touched.email && !!errors.email}
helperText={touched.email && errors.email}
sx={{ flex: '1 1 260px' }}
slotProps={{
input: {
startAdornment:
!loading && emailVerified ? (
!isVerifyingCode && emailVerified ? (
<InputAdornment position="end">
<Icon
Component={TickCircle}
@@ -118,31 +124,36 @@ export function EmailSection({
},
}}
/>
{!loading && !emailVerified && (
<Button
type="button"
variant="text"
onClick={onSendCodeClick}
sx={{
minWidth: '140px',
width: { xs: '100%', sm: '156px' },
alignSelf: 'center',
textTransform: 'none',
}}
>
{getButtonLabel()}
</Button>
)}
{!isVerifyingCode &&
!emailVerified &&
(buttonState === 'counting' ? (
<Typography
sx={{
minWidth: '140px',
width: { xs: '100%', sm: '156px' },
alignSelf: 'center',
textAlign: 'center',
}}
color="primary"
variant="body1"
>
{formatTimerValue()}
</Typography>
) : (
<Button
variant="text"
onClick={handleSendCode}
loading={isSendingCode}
sx={{
width: { xs: '100%', sm: '156px' },
alignSelf: 'center',
}}
>
{t('completion.vericationCodeButton')}
</Button>
))}
</Box>
{email && (
<Typography
sx={{ color: correctEmail ? 'success.main' : 'error.main' }}
variant="caption"
>
{correctEmail ? '' : t('completion.emailCorrectForm')}
</Typography>
)}
{!emailVerified && codeSent && correctEmail && (
{!emailVerified && codeSent && (
<Box
sx={{
display: 'flex',
@@ -157,23 +168,21 @@ export function EmailSection({
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
sx={{ flex: '1 1 260px' }}
disabled={loading}
disabled={isVerifyingCode}
onBlur={() => handleBlur('verificationCode')}
error={touched.verificationCode && !!errors.verificationCode}
helperText={touched.verificationCode && errors.verificationCode}
/>
<Button
variant="contained"
variant="outlined"
onClick={handleVerifyCode}
disabled={loading}
loading={isVerifyingCode}
sx={{
width: { xs: '100%', sm: '156px' },
border: 0.5,
textTransform: 'none',
alignSelf: 'center',
}}
>
{loading ? (
<CircularProgress size={20} />
) : (
t('completion.checkCodeButton')
)}
{t('completion.checkCodeButton')}
</Button>
</Box>
)}

View File

@@ -28,6 +28,9 @@ export function PasswordSection({
hasSpecialChar,
validPassword,
showValidations,
errors,
touched,
handleBlur,
}: PasswordSectionProps) {
const { t } = useTranslation('completionForm');
const [showPasswordText, setShowPasswordText] = useState(false);
@@ -51,7 +54,6 @@ export function PasswordSection({
sx={{
display: 'flex',
gap: 0.5,
px: { xs: 2, sm: -4 },
alignItems: 'center',
}}
>
@@ -75,7 +77,6 @@ export function PasswordSection({
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 0 },
}}
>
<Box
@@ -92,12 +93,17 @@ export function PasswordSection({
onChange={(e) => setPassword(e.target.value)}
variant="outlined"
type={showPasswordText ? 'text' : 'password'}
autoFocus
onBlur={() => handleBlur('password')}
error={touched.password && !!errors.password}
helperText={touched.password && errors.password}
sx={{
flex: '1 1 260px',
'& .MuiInputBase-input': {
pr: 8, // Increased padding to accommodate both icons
},
}}
// FIXME: deprecated
InputProps={{
endAdornment: (
<InputAdornment position="end">
@@ -146,12 +152,9 @@ export function PasswordSection({
variant="outlined"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
error={confirmPassword.length > 0 && !matchPassword}
helperText={
confirmPassword.length > 0 && !matchPassword
? t('completion.notCompatibility')
: ' '
}
onBlur={() => handleBlur('confirmPassword')}
error={touched.confirmPassword && !!errors.confirmPassword}
helperText={touched.confirmPassword && errors.confirmPassword}
type={showPasswordRepetitionText ? 'text' : 'password'}
sx={{
flex: '1 1 260px',
@@ -205,7 +208,7 @@ export function PasswordSection({
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 2,
gap: { xs: 0, sm: 2 },
justifyContent: { xs: 'center', sm: 'flex-start' },
}}
>

View File

@@ -30,12 +30,15 @@ export function PersonalInfoFields({
setSex,
country,
setCountry,
errors,
touched,
handleBlur,
}: PersonalInfoFieldsProps) {
const { t } = useTranslation('completionForm');
const { t } = useTranslation(['completionForm', 'country']);
const countryOptions = countries.map((c) => ({
code: c.code,
label: t(c.label, { ns: 'countries' }),
label: t(c.label, { ns: 'country' }),
}));
const currentCountry = countryOptions.find((c) => c.code === country) || null;
@@ -67,7 +70,6 @@ export function PersonalInfoFields({
flexDirection: 'column',
gap: 2,
width: '100%',
px: { xs: 2, sm: 4, md: 1 },
}}
>
<Box
@@ -85,6 +87,9 @@ export function PersonalInfoFields({
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')}
@@ -93,6 +98,9 @@ export function PersonalInfoFields({
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>
@@ -107,7 +115,7 @@ export function PersonalInfoFields({
<FormControl sx={{ flex: '1 1 260px' }}>
<InputLabel>{t('completion.gender')}</InputLabel>
<Select
value={sex}
value={sex || ''}
label={t('completion.gender')}
onChange={handleChangeSex}
>
@@ -129,6 +137,9 @@ export function PersonalInfoFields({
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>
@@ -146,6 +157,7 @@ export function PersonalInfoFields({
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
@@ -157,13 +169,19 @@ export function PersonalInfoFields({
// TODO: Check alignment for better styling definition
marginTop: '-2px',
marginRight: '4px',
marginLeft: '8px',
}}
/>
{option.label}
</Box>
)}
renderInput={(params) => (
<TextField {...params} label={t('completion.country')} />
<TextField
{...params}
label={t('completion.country')}
error={touched.country && !!errors.country}
helperText={touched.country && errors.country}
/>
)}
clearOnEscape
/>

View File

@@ -11,7 +11,7 @@ import {
import { useTranslation } from 'react-i18next';
import { type SubmitProps } from '../../types/settingForm';
export function SubmitSection({ onSubmit, loading, error }: SubmitProps) {
export function SubmitSection({ onSubmit, loading }: SubmitProps) {
const { t, i18n } = useTranslation('completionForm');
const [openDialog, setOpenDialog] = useState(false);
@@ -63,7 +63,6 @@ export function SubmitSection({ onSubmit, loading, error }: SubmitProps) {
? t('completion.submitting')
: t('completion.registerButton')}
</Button>
{error && <Typography color="error">{error}</Typography>}
</Box>
<Dialog