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

@@ -5,6 +5,7 @@ import {
} from '../types/completionFormApiTypes';
import apiClient from '@/lib/apiClient';
// TODO: define generic type
export const sendEmailOtpApi = async (payload: SendEmailOtpPayload) => {
return apiClient.post('/User/SendEmailOtp', payload);
};

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 = () => {
@@ -10,6 +8,7 @@ export const AccountCreatedClubBanner = () => {
return (
<Stack direction="row" spacing={2} sx={{ pb: 1 }} alignItems="start">
{/* FIXME: Replace with Club Logo (this icon is placeholder) */}
<Profile2User size={80} color={theme.palette.club.main} />
<Stack spacing={0.5}>

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

@@ -1,20 +1,22 @@
import { Paper } from '@mui/material';
import { Paper, type SxProps, type Theme } from '@mui/material';
import { type PropsWithChildren } from 'react';
export interface AuthenticationCardProps extends PropsWithChildren {
maxWidth?: string;
sx?: SxProps<Theme>;
}
// Beacuse in the otp verify there is a element outside of the authentication card
export const AuthenticationCard = ({
children,
maxWidth,
sx,
}: AuthenticationCardProps) => {
return (
<Paper
elevation={0}
sx={{
borderRadius: 4,
borderRadius: 2,
p: {
xs: 4,
md: 6,
@@ -22,6 +24,8 @@ export const AuthenticationCard = ({
marginInline: 2,
width: (t) => `calc(100% - ${t.spacing(2)})`,
maxWidth: maxWidth ?? '552px',
...sx,
}}
>
{children}

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

@@ -40,7 +40,7 @@ export function CountryCodeSelector({
const open = Boolean(anchorEl);
const searchInputRef = useRef<HTMLInputElement>(null);
const menuWidth = menuAnchor ? menuAnchor.clientWidth : 'auto';
const { t, i18n } = useTranslation();
const { t, i18n } = useTranslation(['country', 'common']);
const selectedCountry =
countries.find((c) => c.phone === value) || countries[0];
@@ -176,7 +176,7 @@ export function CountryCodeSelector({
inputRef={searchInputRef}
size="small"
fullWidth
label={t('labels.search')}
label={t('labels.search', { ns: 'common' })}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
@@ -207,7 +207,7 @@ export function CountryCodeSelector({
}}
/>
</ListItemIcon>
<ListItemText primary={t(country.label)} />
<ListItemText primary={t(country.label, { ns: 'country' })} />
<Typography color="text.secondary">
{country.phone}
</Typography>

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

View File

@@ -7,8 +7,6 @@ import { PasswordSection } from '../components/UserCompletionForm/PasswordSectio
import { EmailSection } from '../components/UserCompletionForm/EmailSection';
import { SubmitSection } from '../components/UserCompletionForm/SubmitSection';
import { useToast } from '@rkheftan/harmony-ui';
import { toLocaleDigits } from '../../../utils/persianDigit';
import i18n from '@/config/i18n';
import { Gender } from '../types/settingForm';
import { useApi } from '@/hooks/useApi';
import {
@@ -22,6 +20,9 @@ import { containsNumber } from '@/utils/regexes/containsNumber';
import { hasUpperAndLowerLetter } from '@/utils/regexes/hasUpperAndLowerLetter';
import { isEmail } from '@/utils/regexes/isEmail';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { FlexBox } from '@/components/common/FlexBox';
import { AuthenticationCard } from '../components/AuthenticationCard';
import { nationalIdRegex } from '@/utils/regexes/nationalId';
export function UserCompletionPage() {
const [params] = useSearchParams();
@@ -33,7 +34,7 @@ export function UserCompletionPage() {
const [lastName, setLastName] = useState('');
const [nationalId, setNationalId] = useState('');
const [birthDate, setBirthDate] = useState<Date | null>(null);
const [sex, setSex] = useState<Gender>(Gender.Female);
const [sex, setSex] = useState<Gender | null>(null);
const [country, setCountry] = useState('');
const [showPasswordSection, setShowPasswordSection] = useState(false);
const [password, setPassword] = useState('');
@@ -50,6 +51,9 @@ export function UserCompletionPage() {
const [emailVerified, setEmailVerified] = useState(false);
const [showPasswordValidations, setShowPasswordValidations] = useState(false);
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const [touched, setTouched] = useState<{ [key: string]: boolean }>({});
const hasNumber = containsNumber(password);
const hasMinLength = least8Chars(password);
const hasUpperAndLower = hasUpperAndLowerLetter(password);
@@ -60,94 +64,14 @@ export function UserCompletionPage() {
const matchPassword = password === confirmPassword;
const correctEmail = isEmail(email);
const { execute: sendCode, data: sendCodeData } = useApi(sendEmailOtpApi);
const { execute: sendCode, loading: isSendingCode } = useApi(sendEmailOtpApi);
const {
execute: verifyCode,
data: verifyCodeData,
loading: isVerifyingCode,
} = useApi(confirmEmailOtpApi);
const { execute: verifyCode, loading: isVerifyingCode } =
useApi(confirmEmailOtpApi);
const {
execute: submitForm,
data: submitData,
loading: isSubmitting,
error: submitError,
} = useApi(completeUserInformationApi);
const getErrorMessage = (error: unknown): string | null => {
if (!error) return null;
if (error instanceof Error) return error.message;
return String(error);
};
useEffect(() => {
if (sendCodeData) {
if (sendCodeData.data.success) {
showToast({
message:
sendCodeData.data.message || t('completion.successfullCodeSent'),
severity: 'success',
});
setCodeSent(true);
setButtonState('counting');
setCountdown(120);
} else {
showToast({
message: sendCodeData.data.message || t('completion.problem'),
severity: 'error',
});
}
}
}, [sendCodeData, showToast, t]);
useEffect(() => {
if (verifyCodeData) {
if (verifyCodeData.data.success) {
setEmailVerified(true);
showToast({
message: verifyCodeData.data.message || t('completion.codeVerified'),
severity: 'success',
});
} else {
showToast({
message: verifyCodeData.data.message || t('completion.invalidCode'),
severity: 'error',
});
setEmailVerified(false);
}
}
}, [verifyCodeData, showToast, t]);
useEffect(() => {
if (submitData) {
showToast({
message:
submitData.data.message ||
t(
submitData.data.success
? 'completion.submitSuccess'
: 'completion.submitError',
),
severity: submitData.data.success ? 'success' : 'error',
});
if (submitData.data.success) {
const returnUrl = params.get('returnUrl');
navigate(
returnUrl
? `/account-created?returnUrl=${returnUrl}`
: '`/account-created',
);
}
} else if (submitError) {
showToast({
message: getErrorMessage(submitError) || t('completion.problem'),
severity: 'error',
});
}
}, [submitData, submitError, showToast, t, navigate, params]);
const { execute: submitForm, loading: isSubmitting } = useApi(
completeUserInformationApi,
);
useEffect(() => {
setShowPasswordValidations(password ? !validPassword : false);
@@ -170,26 +94,84 @@ export function UserCompletionPage() {
return () => clearInterval(timer);
}, [buttonState, countdown]);
const handleSendCode = () => {
sendCode({ email });
};
const handleVerifyCode = () => {
if (!verificationCode.trim()) {
showToast({
message: 'Please enter the verification code',
severity: 'warning',
});
const handleSendCode = async () => {
if (!isEmail(email)) {
setTouched((prev) => ({ ...prev, email: true }));
setErrors((prev) => ({ ...prev, email: t('validation.emailInvalid') }));
return;
}
verifyCode({ email, otpCode: verificationCode });
const res = await sendCode({ email });
if (res) {
if (res.success) {
showToast({
message: res.message || t('completion.successfulCodeSent'),
severity: 'success',
});
setCodeSent(true);
setButtonState('counting');
setCountdown(120);
} else {
showToast({
message: res.message || t('completion.problem'),
severity: 'error',
});
}
}
};
const handleSubmit = () => {
submitForm({
const handleVerifyCode = async () => {
if (!verificationCode.trim()) {
// Manually trigger the validation error and stop
setTouched((prev) => ({ ...prev, verificationCode: true }));
setErrors((prev) => ({
...prev,
verificationCode: t('validation.verificationCodeRequired'),
}));
return;
}
const res = await verifyCode({ email, otpCode: verificationCode });
if (res) {
if (res.success) {
setEmailVerified(true);
showToast({
message: res.message || t('completion.codeVerified'),
severity: 'success',
});
} else {
showToast({
message: res.message || t('completion.invalidCode'),
severity: 'error',
});
setEmailVerified(false);
}
}
};
const handleSubmit = async () => {
setTouched({
firstName: true,
lastName: true,
country: true,
email: showEmail,
nationalId: true,
verificationCode: showEmail,
password: showPasswordSection,
confirmPassword: showPasswordSection,
});
const isValid = validateForm();
if (!isValid) {
return; // Stop the submission
}
const res = await submitForm({
firstName,
lastName,
gender: sex,
gender: sex || 0,
nationalId,
savePassword: showPasswordSection,
password: showPasswordSection ? password : undefined,
@@ -198,15 +180,26 @@ export function UserCompletionPage() {
birthDate,
country,
});
};
const getButtonLabel = () => {
if (buttonState === 'counting') {
const m = String(Math.floor(countdown / 60)).padStart(2, '0');
const s = String(countdown % 60).padStart(2, '0');
return toLocaleDigits(`${m}:${s}`, i18n.language);
if (res) {
if (res.success) {
showToast({
message: res.message || t('completion.submitSuccess'),
severity: 'success',
});
const returnUrl = params.get('returnUrl');
navigate(
returnUrl ? `/account-created?returnUrl=${returnUrl}` : '/setting',
);
} else {
showToast({
message: res.message || t('completion.submitError'),
severity: 'error',
});
}
}
return t('completion.vericationCodeButton');
};
const handleEditEmail = () => {
@@ -216,99 +209,189 @@ export function UserCompletionPage() {
setVerificationCode('');
};
const validateForm = () => {
// TODO: check if need separate validation or same common validation
const newErrors: { [key: string]: string } = {};
// Rule 1: First Name is required
if (!firstName.trim())
newErrors.firstName = t('validation.firstNameRequired');
// Rule 2: Last Name is required
if (!lastName.trim()) newErrors.lastName = t('validation.lastNameRequired');
// Rule 3: Country is required
if (!country) newErrors.country = t('validation.countryRequired');
// Rule 4: Email is required and must be valid IF the section is shown
if (showEmail && !isEmail(email)) {
newErrors.email = t('validation.emailInvalid');
}
// Rule 5: National ID must be 10 digits IF it's not empty
if (nationalId && !nationalIdRegex.test(nationalId)) {
newErrors.nationalId = t('validation.nationalIdInvalid');
}
// Rule 6: If verification code sent and email section is active
if (showEmail && codeSent && !emailVerified) {
if (!verificationCode.trim()) {
// Case 1: The code is required but the field is empty.
newErrors.verificationCode = t('validation.verificationCodeRequired');
} else {
// Case 2: The user has typed a code but hasn't clicked "Verify" yet.
newErrors.verificationCode = t('validation.mustVerifyCode');
}
}
// Rule 7: Password validation
if (showPasswordSection) {
// Rule 1: Check if the main password is valid
if (!password.trim()) {
newErrors.password = t('validation.passwordRequired');
} else if (!validPassword) {
// 'validPassword' is the boolean you already calculate
newErrors.password = t('validation.passwordInvalid');
}
// Rule 2: Check if the confirmation password matches
if (!confirmPassword.trim()) {
newErrors.confirmPassword = t('validation.confirmPasswordRequired');
} else if (!matchPassword) {
// 'matchPassword' is the boolean you already calculate
newErrors.confirmPassword = t('validation.passwordsDoNotMatch');
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0; // Returns true if form is valid
};
const handleBlur = (field: string) => {
setTouched((prev) => ({ ...prev, [field]: true }));
};
// re-validate whenever a field the user has touched changes value
useEffect(() => {
// Only run validation if at least one field has been touched
if (Object.keys(touched).length > 0) {
validateForm();
}
}, [
firstName,
lastName,
country,
email,
nationalId,
verificationCode,
codeSent,
emailVerified,
password,
confirmPassword,
showPasswordSection,
touched,
]);
return (
<Box
<FlexBox
direction="column"
align="center"
justify="center"
sx={{
backgroundColor: 'background.default',
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
p: { xs: 1, sm: 2, md: 3 },
gap: 3,
// TODO: check if this padding needed for mobile view
py: { sm: 0, xs: 2 },
}}
>
<Box sx={{ mb: 2 }}>
<Logo />
</Box>
<Box
<Logo />
<AuthenticationCard
// TODO: check styles
sx={{
width: '100%',
maxWidth: 730,
backgroundColor: 'background.paper',
border: '1px solid #ccc',
display: 'flex',
flexDirection: 'column',
gap: 2,
px: { xs: 2, sm: 4, md: 6 },
py: { xs: 3, sm: 4 },
maxHeight: { sm: '80vh', xs: 'unset' },
flex: { sm: 'unset', xs: 1 },
overflowY: 'auto',
}}
maxWidth="730px"
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="h5" color="text.primary">
{t('completion.title')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('completion.description')}
</Typography>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="h5" color="text.primary">
{t('completion.title')}
</Typography>
<Typography variant="body2" color="text.secondary">
{t('completion.description')}
</Typography>
</Box>
<PersonalInfoFields
firstName={firstName}
setFirstName={setFirstName}
lastName={lastName}
setLastName={setLastName}
nationalId={nationalId}
setNationalId={setNationalId}
birthDate={birthDate}
setBirthDate={setBirthDate}
sex={sex}
setSex={setSex}
country={country}
setCountry={setCountry}
errors={errors}
touched={touched}
handleBlur={handleBlur}
/>
<PasswordSection
showPasswordSection={showPasswordSection}
setShowPasswordSection={setShowPasswordSection}
password={password}
setPassword={setPassword}
confirmPassword={confirmPassword}
setConfirmPassword={setConfirmPassword}
matchPassword={matchPassword}
hasNumber={hasNumber}
hasMinLength={hasMinLength}
hasUpperAndLower={hasUpperAndLower}
hasSpecialChar={hasSpecialChar}
validPassword={validPassword}
showValidations={showPasswordValidations}
errors={errors}
touched={touched}
handleBlur={handleBlur}
/>
<EmailSection
showEmail={showEmail}
setShowEmail={setShowEmail}
email={email}
setEmail={setEmail}
correctEmail={correctEmail}
codeSent={codeSent}
verificationCode={verificationCode}
setVerificationCode={setVerificationCode}
buttonState={buttonState}
countdown={countdown}
handleSendCode={handleSendCode}
handleVerifyCode={handleVerifyCode}
emailVerified={emailVerified}
isVerifyingCode={isVerifyingCode}
isSendingCode={isSendingCode}
handleEditEmail={handleEditEmail}
errors={errors}
touched={touched}
handleBlur={handleBlur}
/>
<SubmitSection onSubmit={handleSubmit} loading={isSubmitting} />
</Box>
<PersonalInfoFields
firstName={firstName}
setFirstName={setFirstName}
lastName={lastName}
setLastName={setLastName}
nationalId={nationalId}
setNationalId={setNationalId}
birthDate={birthDate}
setBirthDate={setBirthDate}
sex={sex}
setSex={setSex}
country={country}
setCountry={setCountry}
/>
<PasswordSection
showPasswordSection={showPasswordSection}
setShowPasswordSection={setShowPasswordSection}
password={password}
setPassword={setPassword}
confirmPassword={confirmPassword}
setConfirmPassword={setConfirmPassword}
matchPassword={matchPassword}
hasNumber={hasNumber}
hasMinLength={hasMinLength}
hasUpperAndLower={hasUpperAndLower}
hasSpecialChar={hasSpecialChar}
validPassword={validPassword}
showValidations={showPasswordValidations}
/>
<EmailSection
showEmail={showEmail}
setShowEmail={setShowEmail}
email={email}
setEmail={setEmail}
correctEmail={correctEmail}
codeSent={codeSent}
verificationCode={verificationCode}
setVerificationCode={setVerificationCode}
buttonState={buttonState}
getButtonLabel={getButtonLabel}
handleSendCode={handleSendCode}
handleVerifyCode={handleVerifyCode}
emailVerified={emailVerified}
loading={isVerifyingCode}
handleEditEmail={handleEditEmail}
/>
<SubmitSection
onSubmit={handleSubmit}
loading={isSubmitting}
error={getErrorMessage(submitError)}
success={!!submitData?.data.success}
/>
</Box>
</Box>
</AuthenticationCard>
</FlexBox>
);
}

View File

@@ -16,6 +16,7 @@ export interface ConfirmEmailOtpPayload {
export interface CompleteUserInfoPayload {
firstName: string;
lastName: string;
// TODO: fix this
gender: 0 | 1 | 2;
nationalId: string;
birthDate: Date | null;

View File

@@ -6,12 +6,18 @@ export enum Gender {
Male = 2,
}
export interface ValidationProps {
errors: { [key: string]: string };
touched: { [key: string]: boolean };
handleBlur: (field: string) => void;
}
export interface DateOfBirthProps {
value: Date | null;
onChange: (date: Date | null) => void;
}
export interface EmailSectionProps {
export interface EmailSectionProps extends ValidationProps {
showEmail: boolean;
setShowEmail: (checked: boolean) => void;
email: string;
@@ -21,15 +27,16 @@ export interface EmailSectionProps {
verificationCode: string;
setVerificationCode: (code: string) => void;
buttonState: 'default' | 'counting' | 'sent';
getButtonLabel: () => string;
countdown: number;
handleSendCode: () => void;
handleVerifyCode: () => void;
emailVerified: boolean;
loading: boolean;
isVerifyingCode: boolean;
isSendingCode: boolean;
handleEditEmail: () => void;
}
export interface PasswordSectionProps {
export interface PasswordSectionProps extends ValidationProps {
showPasswordSection: boolean;
setShowPasswordSection: (checked: boolean) => void;
password: string;
@@ -50,13 +57,13 @@ export interface ValidationItemProps {
label: string;
}
export interface PersonalInfoFieldsProps {
export interface PersonalInfoFieldsProps extends ValidationProps {
firstName: string;
setFirstName: (v: string) => void;
lastName: string;
setLastName: (v: string) => void;
sex: Gender;
setSex: Dispatch<SetStateAction<Gender>>;
sex: Gender | null;
setSex: Dispatch<SetStateAction<Gender | null>>;
country: string;
setCountry: (country: string) => void;
nationalId: string;
@@ -68,6 +75,4 @@ export interface PersonalInfoFieldsProps {
export interface SubmitProps {
onSubmit: () => void;
loading: boolean;
error: string | null;
success: boolean;
}

View File

@@ -54,7 +54,7 @@ export function PersonalInformation() {
setData(fetchedData);
setOriginalData(fetchedData);
const imageBaseUrl = process.env.IMAGE_BASE_URL;
const imageBaseUrl = import.meta.env.IMAGE_BASE_URL;
if (profileData.profileImageUrl) {
setUploadedImageUrl(`${imageBaseUrl}${profileData.profileImageUrl}`);

View File

@@ -59,7 +59,7 @@ export function SocialMedia() {
} = useApi(changeEmail);
const fullScreen = useMediaQuery((theme: Theme) =>
theme.breakpoints.down('sm'),
theme?.breakpoints.down('sm'),
);
const downMd = useMediaQuery((theme: Theme) => theme.breakpoints.down('md'));
const computedMaxWidth = (fullScreen ? 'xs' : downMd ? 'sm' : 'md') as