feat: country selection, add birthdate and add error messages for textfields

This commit is contained in:
2025-07-22 17:36:35 +03:30
committed by Koosha Lahouti
parent 9e471670d3
commit 09e4dfa917
4 changed files with 220 additions and 209 deletions

View File

@@ -21,13 +21,14 @@
"hasMinLength": "حداقل 8 کاراکتر", "hasMinLength": "حداقل 8 کاراکتر",
"hasUpperAndLower": "شامل یک حرف کوچک و بزرگ", "hasUpperAndLower": "شامل یک حرف کوچک و بزرگ",
"hasSpecialChar": "شامل علامت (!@#$%^&*)", "hasSpecialChar": "شامل علامت (!@#$%^&*)",
"notCompatibility": "مطابقت ندارد", "notCompatibility": "تکرار رمز عبور با رمز عبور یکسان نمی باشد",
"emailCorrectForm": "فرم درست ایمیل را وارد کنید", "emailCorrectForm": "ساختار ایمیل صحیح نیست",
"agreementPart1": " ادامه فرایند ثبت نام به منزله تایید و قبول", "agreementPart1": " ادامه فرایند ثبت نام به منزله تایید و قبول",
"agreementLinkText": " قوانین و مقررات هارمونی", "agreementLinkText": " قوانین و مقررات هارمونی",
"agreementPart2": "می باشد.", "agreementPart2": "می باشد.",
"sent": "ارسال شد!", "sent": "ارسال شد!",
"country": "کشور", "country": "کشور",
"dateOfBirth": "تاریخ تولد(اختیاری)" "dateOfBirth": "تاریخ تولد(اختیاری)",
"invalidCountry": "کشور انتخاب شده صحیح نیست"
} }
} }

View File

@@ -72,6 +72,7 @@ export function EmailSection({
variant="outlined" variant="outlined"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
error={!correctEmail}
sx={{ sx={{
width: !isVerifyingCode && !emailVerified ? '446px' : '634px', width: !isVerifyingCode && !emailVerified ? '446px' : '634px',
transition: 'width 0.3s', transition: 'width 0.3s',
@@ -111,17 +112,25 @@ export function EmailSection({
}, },
}} }}
/> />
{email && (
<Typography
sx={{ color: correctEmail ? 'green' : 'red' }}
variant="caption"
>
{correctEmail ? '' : t('completion.emailCorrectForm')}
</Typography>
)}
</Box> </Box>
{!isVerifyingCode && !emailVerified && ( {!isVerifyingCode && !emailVerified && (
<Button <Button
variant="text" variant="text"
onClick={handleSendCode} onClick={handleSendCode}
sx={{ width: '156px' }} sx={{ width: '156px' }}
disabled={ // disabled={
buttonState === 'sent' || // buttonState === 'sent' ||
buttonState === 'counting' || // buttonState === 'counting' ||
!correctEmail // !correctEmail
} // }
> >
{getButtonLabel()} {getButtonLabel()}
</Button> </Button>

View File

@@ -9,7 +9,7 @@ import {
InputAdornment, InputAdornment,
} from '@mui/material'; } from '@mui/material';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TickCircle, Eye, EyeSlash } from 'iconsax-react'; import { TickCircle, Eye, EyeSlash, CloseCircle } from 'iconsax-react';
import { PasswordValidationItem } from './PasswordValidation'; import { PasswordValidationItem } from './PasswordValidation';
interface PasswordSectionProps { interface PasswordSectionProps {
@@ -66,10 +66,12 @@ export function PasswordSection({
<> <>
<FormGroup> <FormGroup>
<Box sx={{ display: 'flex', gap: 0.5, px: 6, alignItems: 'center' }}> <Box sx={{ display: 'flex', gap: 0.5, px: 6, alignItems: 'center' }}>
<Switch <Box dir="ltr">
checked={showPasswordSection} <Switch
onChange={handleTogglePasswordSection} checked={showPasswordSection}
/> onChange={handleTogglePasswordSection}
/>
</Box>
<Typography <Typography
sx={{ color: showPasswordSection ? '#2979FF' : 'text.primary' }} sx={{ color: showPasswordSection ? '#2979FF' : 'text.primary' }}
> >
@@ -79,8 +81,8 @@ export function PasswordSection({
</FormGroup> </FormGroup>
{showPasswordSection && ( {showPasswordSection && (
<Box sx={{ display: 'flex', gap: 2, px: 6 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, px: 6 }}>
<Box sx={{ display: 'flex', flexDirection: 'column' }}> <Box sx={{ display: 'flex', gap: 2 }}>
<TextField <TextField
label={t('completion.password')} label={t('completion.password')}
value={password} value={password}
@@ -131,87 +133,121 @@ export function PasswordSection({
}} }}
/> />
{password && ( <TextField
<Box sx={{ mt: 1 }}> label={t('completion.passwordRepetition')}
{showValidations && ( variant="outlined"
<Box sx={{ mt: 1 }}> value={confirmPassword}
<PasswordValidationItem onChange={(e) => setConfirmPassword(e.target.value)}
isValid={hasNumber} error={confirmPassword.length > 0 && !matchPassword}
label={t('completion.hasNumber')} helperText={
/> confirmPassword.length > 0 && !matchPassword
<PasswordValidationItem ? t('completion.notCompatibility')
isValid={hasMinLength} : ' '
label={t('completion.hasMinLength')} }
/> sx={{ width: '309px' }}
<PasswordValidationItem type={showPasswordRepititonText ? 'text' : 'password'}
isValid={hasUpperAndLower} InputProps={{
label={t('completion.hasUpperAndLower')} endAdornment: (
/> <InputAdornment position="end" sx={{ height: 'unset' }}>
<PasswordValidationItem <Box
isValid={hasSpecialChar} sx={{
label={t('completion.hasSpecialChar')} display: 'flex',
/> alignItems: 'center',
</Box> width: '100%',
)} }}
</Box> >
)} {confirmPassword.length > 0 ? (
matchPassword ? (
<TickCircle
size="24"
color="#14AE5C"
variant="Bold"
style={{
position: 'absolute',
right: 0,
marginRight: '16px',
}}
/>
) : (
<CloseCircle
size="24"
color="red"
variant="Bold"
style={{
position: 'absolute',
right: 0,
marginRight: '16px',
}}
/>
)
) : null}
<IconButton
onClick={handleTogglePasswordRepetitionEye}
sx={{
ml:
confirmPassword.length > 0 && matchPassword
? 0.5
: 'auto',
}}
>
{showPasswordRepititonText ? (
<Eye size="24" color="#2979FF" />
) : (
<EyeSlash size="24" color="#2979FF" />
)}
</IconButton>
</Box>
</InputAdornment>
),
}}
inputProps={{
style: {
paddingRight:
confirmPassword.length > 0 && matchPassword
? '48px'
: '45px',
},
}}
/>
</Box> </Box>
<TextField {password && showValidations && (
label={t('completion.passwordRepetition')} <Box sx={{ display: 'flex', gap: 2 }}>
variant="outlined" <Box
value={confirmPassword} sx={{
onChange={(e) => setConfirmPassword(e.target.value)} width: '317px',
error={confirmPassword.length > 0 && !matchPassword} display: 'flex',
helperText={ flexDirection: 'column',
confirmPassword.length > 0 && !matchPassword }}
? t('completion.notCompatibility') >
: ' ' <PasswordValidationItem
} isValid={hasNumber}
sx={{ width: '309px' }} label={t('completion.hasNumber')}
type={showPasswordRepititonText ? 'text' : 'password'} />
InputProps={{ <PasswordValidationItem
endAdornment: ( isValid={hasMinLength}
<InputAdornment position="end" sx={{ height: 'unset' }}> label={t('completion.hasMinLength')}
<Box />
sx={{ </Box>
display: 'flex', <Box
alignItems: 'center', sx={{
width: '100%', width: '317px',
}} display: 'flex',
> flexDirection: 'column',
{confirmPassword.length > 0 && matchPassword && ( }}
<TickCircle >
size="24" <PasswordValidationItem
color="#14AE5C" isValid={hasUpperAndLower}
variant="Bold" label={t('completion.hasUpperAndLower')}
style={{ />
position: 'absolute', <PasswordValidationItem
right: 0, isValid={hasSpecialChar}
marginRight: '16px', label={t('completion.hasSpecialChar')}
}} />
/> </Box>
)} </Box>
<IconButton )}
onClick={handleTogglePasswordRepetitionEye}
sx={{ ml: validPassword ? 0.5 : 'auto' }}
>
{showPasswordRepititonText ? (
<Eye size="24" color="#2979FF" />
) : (
<EyeSlash size="24" color="#2979FF" />
)}
</IconButton>
</Box>
</InputAdornment>
),
}}
inputProps={{
style: {
paddingRight: validPassword ? '48px' : '20px',
},
}}
/>
</Box> </Box>
)} )}
</> </>

View File

@@ -5,9 +5,11 @@ import {
MenuItem, MenuItem,
Select, Select,
Box, Box,
type SelectChangeEvent, Autocomplete,
} from '@mui/material'; } from '@mui/material';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Woman, Man } from 'iconsax-react';
import { useState } from 'react';
interface PersonalInfoFieldsProps { interface PersonalInfoFieldsProps {
sex: string; sex: string;
@@ -23,116 +25,65 @@ export function PersonalInfoFields({
setCountry, setCountry,
}: PersonalInfoFieldsProps) { }: PersonalInfoFieldsProps) {
const { t } = useTranslation('completionForm'); const { t } = useTranslation('completionForm');
const [countryError, setCountryError] = useState(false);
const countries = [ const countries = [
{ name: 'Afghanistan', fa: 'افغانستان', flag: 'af' }, { name: 'Afghanistan', fa: 'افغانستان', flag: 'af' },
{ name: 'Albania', fa: 'آلبانی', flag: 'al' }, { name: 'Albania', fa: 'آلبانی', flag: 'al' },
{ name: 'Algeria', fa: 'الجزایر', flag: 'dz' }, { name: 'Algeria', fa: 'الجزایر', flag: 'dz' },
{ name: 'Andorra', fa: 'آندورا', flag: 'ad' },
{ name: 'Angola', fa: 'آنگولا', flag: 'ao' },
{ name: 'Argentina', fa: 'آرژانتین', flag: 'ar' }, { name: 'Argentina', fa: 'آرژانتین', flag: 'ar' },
{ name: 'Armenia', fa: 'ارمنستان', flag: 'am' }, { name: 'Armenia', fa: 'ارمنستان', flag: 'am' },
{ name: 'Australia', fa: 'استرالیا', flag: 'au' }, { name: 'Australia', fa: 'استرالیا', flag: 'au' },
{ name: 'Austria', fa: 'اتریش', flag: 'at' }, { name: 'Austria', fa: 'اتریش', flag: 'at' },
{ name: 'Azerbaijan', fa: 'آذربایجان', flag: 'az' },
{ name: 'Bahamas', fa: 'باهاما', flag: 'bs' },
{ name: 'Bahrain', fa: 'بحرین', flag: 'bh' }, { name: 'Bahrain', fa: 'بحرین', flag: 'bh' },
{ name: 'Bangladesh', fa: 'بنگلادش', flag: 'bd' },
{ name: 'Barbados', fa: 'باربادوس', flag: 'bb' },
{ name: 'Belarus', fa: 'بلاروس', flag: 'by' },
{ name: 'Belgium', fa: 'بلژیک', flag: 'be' },
{ name: 'Belize', fa: 'بلیز', flag: 'bz' },
{ name: 'Benin', fa: 'بنین', flag: 'bj' },
{ name: 'Bhutan', fa: 'بوتان', flag: 'bt' },
{ name: 'Bolivia', fa: 'بولیوی', flag: 'bo' },
{ name: 'Bosnia and Herzegovina', fa: 'بوسنی و هرزگوین', flag: 'ba' },
{ name: 'Botswana', fa: 'بوتسوانا', flag: 'bw' },
{ name: 'Brazil', fa: 'برزیل', flag: 'br' },
{ name: 'Brunei', fa: 'برونئی', flag: 'bn' },
{ name: 'Bulgaria', fa: 'بلغارستان', flag: 'bg' },
{ name: 'Burkina Faso', fa: 'بورکینافاسو', flag: 'bf' },
{ name: 'Burundi', fa: 'بوروندی', flag: 'bi' },
{ name: 'Cambodia', fa: 'کامبوج', flag: 'kh' },
{ name: 'Cameroon', fa: 'کامرون', flag: 'cm' },
{ name: 'Canada', fa: 'کانادا', flag: 'ca' }, { name: 'Canada', fa: 'کانادا', flag: 'ca' },
{ name: 'Cape Verde', fa: 'کیپ ورد', flag: '🇨🇻' },
{
name: 'Central African Republic',
fa: 'جمهوری آفریقای مرکزی',
flag: 'cf',
},
{ name: 'Chad', fa: 'چاد', flag: 'td' },
{ name: 'Chile', fa: 'شیلی', flag: 'cl' },
{ name: 'China', fa: 'چین', flag: 'cn' }, { name: 'China', fa: 'چین', flag: 'cn' },
{ name: 'Colombia', fa: 'کلمبیا', flag: 'co' },
{ name: 'Comoros', fa: 'کومور', flag: 'km' },
{ name: 'Congo (Brazzaville)', fa: 'کنگو (برازاویل)', flag: 'cg' },
{ name: 'Congo (Kinshasa)', fa: 'کنگو (کینشاسا)', flag: 'cd' },
{ name: 'Costa Rica', fa: 'کاستاریکا', flag: 'cr' },
{ name: 'Croatia', fa: 'کرواسی', flag: 'hr' },
{ name: 'Cuba', fa: 'کوبا', flag: 'cu' },
{ name: 'Cyprus', fa: 'قبرس', flag: 'cy' },
{ name: 'Czech Republic', fa: 'جمهوری چک', flag: 'cz' },
{ name: 'Denmark', fa: 'دانمارک', flag: 'dk' },
{ name: 'Djibouti', fa: 'جیبوتی', flag: 'dj' },
{ name: 'Dominica', fa: 'دومینیکا', flag: 'dm' },
{ name: 'Dominican Republic', fa: 'جمهوری دومینیکن', flag: 'do' },
{ name: 'Ecuador', fa: 'اکوادور', flag: 'ec' },
{ name: 'Egypt', fa: 'مصر', flag: 'eg' },
{ name: 'El Salvador', fa: 'السالوادور', flag: 'sv' },
{ name: 'Equatorial Guinea', fa: 'گینه استوایی', flag: 'gq' },
{ name: 'Eritrea', fa: 'اریتره', flag: 'er' },
{ name: 'Estonia', fa: 'استونی', flag: 'ee' },
{ name: 'Eswatini', fa: 'اسواتینی', flag: 'az' },
{ name: 'Ethiopia', fa: 'اتیوپی', flag: 'et' },
{ name: 'Fiji', fa: 'فیجی', flag: 'fj' },
{ name: 'Finland', fa: 'فنلاند', flag: 'fi' },
{ name: 'France', fa: 'فرانسه', flag: 'fr' }, { name: 'France', fa: 'فرانسه', flag: 'fr' },
{ name: 'Gabon', fa: 'گابون', flag: 'ga' },
{ name: 'Gambia', fa: 'گامبیا', flag: 'gm' },
{ name: 'Georgia', fa: 'گرجستان', flag: 'ge' },
{ name: 'Germany', fa: 'آلمان', flag: 'de' }, { name: 'Germany', fa: 'آلمان', flag: 'de' },
{ name: 'Ghana', fa: 'غنا', flag: 'gh' },
{ name: 'Greece', fa: 'یونان', flag: 'gr' },
{ name: 'Guatemala', fa: 'گواتمالا', flag: 'gt' },
{ name: 'India', fa: 'هند', flag: 'in' }, { name: 'India', fa: 'هند', flag: 'in' },
{ name: 'Indonesia', fa: 'اندونزی', flag: 'id' },
{ name: 'Iran', fa: 'ایران', flag: 'ir' }, { name: 'Iran', fa: 'ایران', flag: 'ir' },
{ name: 'Iraq', fa: 'عراق', flag: 'iq' }, { name: 'Iraq', fa: 'عراق', flag: 'iq' },
{ name: 'Ireland', fa: 'ایرلند', flag: 'ie' },
{ name: 'Israel', fa: 'اسرائیل', flag: 'il' },
{ name: 'Italy', fa: 'ایتالیا', flag: 'it' }, { name: 'Italy', fa: 'ایتالیا', flag: 'it' },
{ name: 'Japan', fa: 'ژاپن', flag: 'jp' }, { name: 'Japan', fa: 'ژاپن', flag: 'jp' },
{ name: 'Jordan', fa: 'اردن', flag: 'jo' },
{ name: 'Kazakhstan', fa: 'قزاقستان', flag: 'kz' },
{ name: 'Kuwait', fa: 'کویت', flag: 'kw' },
{ name: 'Lebanon', fa: 'لبنان', flag: 'lb' },
{ name: 'Malaysia', fa: 'مالزی', flag: 'my' },
{ name: 'Netherlands', fa: 'هلند', flag: 'nl' }, { name: 'Netherlands', fa: 'هلند', flag: 'nl' },
{ name: 'Norway', fa: 'نروژ', flag: 'no' },
{ name: 'Oman', fa: 'عمان', flag: 'om' },
{ name: 'Pakistan', fa: 'پاکستان', flag: 'pk' }, { name: 'Pakistan', fa: 'پاکستان', flag: 'pk' },
{ name: 'Palestine', fa: 'فلسطین', flag: 'ps' },
{ name: 'Qatar', fa: 'قطر', flag: 'qa' }, { name: 'Qatar', fa: 'قطر', flag: 'qa' },
{ name: 'Russia', fa: 'روسیه', flag: 'ru' }, { name: 'Russia', fa: 'روسیه', flag: 'ru' },
{ name: 'Saudi Arabia', fa: 'عربستان سعودی', flag: 'sa' }, { name: 'Saudi Arabia', fa: 'عربستان سعودی', flag: 'sa' },
{ name: 'Spain', fa: 'اسپانیا', flag: 'es' }, { name: 'Spain', fa: 'اسپانیا', flag: 'es' },
{ name: 'Sweden', fa: 'سوئد', flag: 'se' }, { name: 'Sweden', fa: 'سوئد', flag: 'se' },
{ name: 'Switzerland', fa: 'سوئیس', flag: 'ch' }, { name: 'Switzerland', fa: 'سوئیس', flag: 'ch' },
{ name: 'Syria', fa: 'سوریه', flag: 'sy' },
{ name: 'Turkey', fa: 'ترکیه', flag: 'tr' }, { name: 'Turkey', fa: 'ترکیه', flag: 'tr' },
{ name: 'Ukraine', fa: 'اوکراین', flag: 'ua' },
{ name: 'United Arab Emirates', fa: 'امارات متحده عربی', flag: 'ae' }, { name: 'United Arab Emirates', fa: 'امارات متحده عربی', flag: 'ae' },
{ name: 'United Kingdom', fa: 'بریتانیا', flag: 'gb' }, { name: 'United Kingdom', fa: 'بریتانیا', flag: 'gb' },
{ name: 'United States', fa: 'ایالات متحده آمریکا', flag: 'us' }, { name: 'United States', fa: 'ایالات متحده آمریکا', flag: 'us' },
{ name: 'Yemen', fa: 'یمن', flag: 'ye' }, { name: 'Yemen', fa: 'یمن', flag: 'ye' },
]; ];
const handleChange = (e: SelectChangeEvent) => { const handleChangeSex = (e: any) => {
setSex(e.target.value); setSex(e.target.value);
}; };
const handleChangeCountry = (e: SelectChangeEvent) => { const handleChangeCountry = (_: any, newValue: any) => {
setCountry(e.target.value); setCountry(newValue ? newValue.name : '');
setCountryError(false);
};
const handleCountryBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const inputValue = e.target.value.trim();
const exists = countries.some((c) => c.fa === inputValue);
if (inputValue && !exists) {
setCountryError(true);
} else {
setCountryError(false);
}
};
const handleInputChange = (_: any, value: string) => {
const exists = countries.some((c) => c.fa === value.trim());
if (value.trim() && !exists) {
setCountryError(true);
} else {
setCountryError(false);
}
}; };
return ( return (
@@ -142,17 +93,13 @@ export function PersonalInfoFields({
label={t('completion.name')} label={t('completion.name')}
placeholder={t('completion.name')} placeholder={t('completion.name')}
variant="outlined" variant="outlined"
sx={{ sx={{ width: '309px' }}
width: '309px',
}}
/> />
<TextField <TextField
label={t('completion.familyName')} label={t('completion.familyName')}
placeholder={t('completion.familyName')} placeholder={t('completion.familyName')}
variant="outlined" variant="outlined"
sx={{ sx={{ width: '309px' }}
width: '309px',
}}
/> />
</Box> </Box>
@@ -162,10 +109,20 @@ export function PersonalInfoFields({
<Select <Select
value={sex} value={sex}
label={t('completion.gender')} label={t('completion.gender')}
onChange={handleChange} onChange={handleChangeSex}
> >
<MenuItem value="female">{t('completion.man')}</MenuItem> <MenuItem value="female">
<MenuItem value="male">{t('completion.woman')}</MenuItem> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Woman size={20} color="#F50057" />
{t('completion.woman')}
</Box>
</MenuItem>
<MenuItem value="male">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Man size={20} color="#0091EA" />
{t('completion.man')}
</Box>
</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
@@ -173,45 +130,53 @@ export function PersonalInfoFields({
label={t('completion.optionalNationalCode')} label={t('completion.optionalNationalCode')}
placeholder={t('completion.optionalNationalCode')} placeholder={t('completion.optionalNationalCode')}
variant="outlined" variant="outlined"
sx={{ sx={{ width: '309px' }}
width: '309px',
}}
/> />
</Box> </Box>
<Box sx={{ display: 'flex', gap: 2 }}> <Box sx={{ display: 'flex', gap: 2 }}>
<FormControl sx={{ width: '309px' }}> <Autocomplete
<InputLabel>{t('completion.country')}</InputLabel> options={countries}
<Select getOptionLabel={(c) => (typeof c === 'string' ? c : c.fa)}
value={country} value={countries.find((c) => c.name === country) || null}
label={t('completion.country')} onChange={handleChangeCountry}
onChange={handleChangeCountry} onInputChange={handleInputChange}
> freeSolo
{countries.map((c) => ( noOptionsText=""
<MenuItem key={c.name} value={c.name}> renderOption={(props, c) => (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box
<span> component="li"
<img {...props}
src={`https://flagcdn.com/w40/${c.flag}.png`} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
alt={c.name} >
width="24" <img
height="16" src={`https://flagcdn.com/w40/${c.flag}.png`}
style={{ borderRadius: '2px', border: '1px solid #ccc' }} width="24"
/> height="24"
</span> style={{ borderRadius: '2px', border: '1px solid #ccc' }}
<span>{c.fa}</span> alt={c.name}
</Box> />
</MenuItem> {c.fa}
))} </Box>
</Select> )}
</FormControl> renderInput={(params) => (
<TextField
{...params}
label={t('completion.country')}
variant="outlined"
error={countryError}
helperText={countryError ? t('completion.invalidCountry') : ''}
onBlur={handleCountryBlur}
/>
)}
sx={{ width: '309px' }}
/>
<TextField <TextField
label={t('completion.dateOfBirth')} label={t('completion.dateOfBirth')}
placeholder={t('completion.dateOfBirth')} placeholder={t('completion.dateOfBirth')}
variant="outlined" variant="outlined"
sx={{ sx={{ width: '309px' }}
width: '309px',
}}
/> />
</Box> </Box>
</Box> </Box>