fix: completion page apis, validation, email states
This commit is contained in:
@@ -33,7 +33,7 @@ export function CardContainer({
|
||||
justifyContent: 'space-between',
|
||||
alignItems: { xs: 'flex-start', sm: 'center' },
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
bgcolor: highlighted ? 'primary.light' : 'background.default',
|
||||
bgcolor: highlighted ? 'background.primary' : 'background.default',
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
gap: { xs: 1, sm: 0 },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, IconButton, Skeleton, Typography } from '@mui/material';
|
||||
import { Box, IconButton, Typography } from '@mui/material';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { More } from 'iconsax-react';
|
||||
import type { UserInfo } from '@/contexts/AuthContext';
|
||||
@@ -7,7 +7,7 @@ interface HeaderProps {
|
||||
user: UserInfo;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({ user, loading }) => {
|
||||
export const Header: React.FC<HeaderProps> = ({ user }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
IconButton,
|
||||
Toolbar as MuiToolbar,
|
||||
Skeleton,
|
||||
} from '@mui/material';
|
||||
import { Avatar, Box, IconButton, Toolbar as MuiToolbar } from '@mui/material';
|
||||
import { Icon } from '@rkheftan/harmony-ui';
|
||||
import { HambergerMenu, Menu } from 'iconsax-react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import type { UserInfo } from '@/contexts/AuthContext';
|
||||
import Logo from '../Logo';
|
||||
|
||||
interface ToolbarProps {
|
||||
sideNavOpen: boolean;
|
||||
@@ -22,12 +17,12 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
||||
setSideNavOpen,
|
||||
isMobile,
|
||||
user,
|
||||
loading,
|
||||
}) => {
|
||||
return (
|
||||
<MuiToolbar
|
||||
sx={{
|
||||
height: (t) => t.spacing(isMobile ? 8 : 10.5),
|
||||
bgcolor: 'background.paper',
|
||||
px: isMobile ? 3 : 2,
|
||||
borderBottom: (t) => `1px solid ${t.palette.divider}`,
|
||||
boxSizing: 'content-box',
|
||||
|
||||
@@ -10,8 +10,9 @@ const getIcon = (icon?: Iconsax) => (isSelected: boolean) =>
|
||||
<Icon Component={icon} variant={isSelected ? 'Bold' : 'Broken'} />
|
||||
) : undefined;
|
||||
|
||||
// TODO: make this a hook
|
||||
export function buildNavItems(routes: RouteConfig[]): NavItemConfig[] {
|
||||
const { t } = useTranslation();
|
||||
const { t } = useTranslation('sideNav');
|
||||
|
||||
return routes.flatMap((route) => {
|
||||
// Check if route itself does not have a navItem but its child has
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { CircularProgress, Stack } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
export const Loading = () => {
|
||||
return (
|
||||
@@ -7,7 +6,7 @@ export const Loading = () => {
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: (t) => t.palette.background.default,
|
||||
backgroundColor: 'background.default',
|
||||
position: 'fixed',
|
||||
inset: '0',
|
||||
zIndex: (t) => t.zIndex.tooltip + 1,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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' },
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,19 +33,29 @@ export function useApi<T, P extends any[]>(
|
||||
setData(response.data);
|
||||
return response.data;
|
||||
} catch (err: unknown) {
|
||||
const axiosError: AxiosError = err as AxiosError;
|
||||
const axiosError = err as AxiosError;
|
||||
console.log(axiosError);
|
||||
|
||||
if (axiosError.response?.status === 401) {
|
||||
navigate('/login');
|
||||
}
|
||||
|
||||
if (axiosError.response?.status === 500) {
|
||||
// Check for network error first
|
||||
if (axiosError.code === 'ERR_NETWORK') {
|
||||
toast({
|
||||
message: t('messages.serverError'),
|
||||
message: t('messages.networkError'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
// Check for HTTP status codes on the response object
|
||||
else if (axiosError.response) {
|
||||
if (axiosError.response.status === 401) {
|
||||
navigate('/login');
|
||||
}
|
||||
|
||||
if (axiosError.response.status === 500) {
|
||||
toast({
|
||||
message: t('messages.serverError'),
|
||||
severity: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setError(err);
|
||||
} finally {
|
||||
|
||||
@@ -26,6 +26,9 @@ export const CustomThemeProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
colorSchemeSelector: 'class',
|
||||
},
|
||||
spacing: 8,
|
||||
shape: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
typography: typography,
|
||||
components: {
|
||||
MuiButton: {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { AccountCreatedPage } from '@/features/authorization/routes/AccountCreat
|
||||
import {
|
||||
Calendar,
|
||||
Devices,
|
||||
LocationTick,
|
||||
Mobile,
|
||||
PasswordCheck,
|
||||
Personalcard,
|
||||
@@ -89,6 +88,10 @@ export const appRoutes: RouteConfig[] = [
|
||||
element: <Layout />,
|
||||
authorize: true,
|
||||
children: [
|
||||
{
|
||||
path: '/setting/',
|
||||
element: <Navigate to="/setting/profile" replace />,
|
||||
},
|
||||
{
|
||||
path: '/setting/profile',
|
||||
element: <UserInformationPage />,
|
||||
@@ -135,13 +138,6 @@ export const appRoutes: RouteConfig[] = [
|
||||
icon: PasswordCheck,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/setting/security#confirmed-ips',
|
||||
navConfig: {
|
||||
title: 'side.confirmedIps',
|
||||
icon: LocationTick,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/setting/security#recent-sessions',
|
||||
navConfig: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
TypeBackground,
|
||||
TypeText,
|
||||
} from '@mui/material';
|
||||
import type { PaletteColorOptions } from 'node_modules/@mui/material';
|
||||
import type { PaletteColorOptions } from '@mui/material';
|
||||
|
||||
export interface Palette {
|
||||
primary: {
|
||||
|
||||
@@ -117,10 +117,12 @@ export const PALETTE: Palette = {
|
||||
light: {
|
||||
default: grey[100],
|
||||
paper: '#FFFFFF',
|
||||
primary: blue[50],
|
||||
},
|
||||
dark: {
|
||||
default: '#121212',
|
||||
paper: grey[900],
|
||||
primary: grey[800],
|
||||
},
|
||||
},
|
||||
// Text colors
|
||||
|
||||
@@ -8,6 +8,10 @@ declare module '@mui/material/styles' {
|
||||
interface PaletteOptions {
|
||||
club?: PaletteOptions['primary']; // Extend PaletteOptions for optional 'club'
|
||||
}
|
||||
|
||||
interface TypeBackground {
|
||||
primary: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/Button' {
|
||||
@@ -16,6 +20,8 @@ declare module '@mui/material/Button' {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move this to types
|
||||
// has typo
|
||||
export type PalleteColor =
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
|
||||
1
src/utils/regexes/nationalId.ts
Normal file
1
src/utils/regexes/nationalId.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const nationalIdRegex = /^\d{10}$/;
|
||||
Reference in New Issue
Block a user