Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions e-commerce-app/src/hooks/useValidate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { useState } from 'react';
import { IRegistrationFormData } from '../interfaces/IRegistrationFormData';
import { IValues } from '../interfaces/IValues';

export const useValidate = () => {
const [errors, setErrors] = useState<IRegistrationFormData>({});

const validateEmail = (email: string) => {
const trimmedEmail = email.trim();

const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/i;

if (!emailRegex.test(trimmedEmail)) {
return 'A properly formatted email address is required';
}

return '';
};

const validatePassword = (password: string) => {
const trimmedPassword = password.trim();
const minLength = 8;

if (trimmedPassword.length < minLength) {
return 'Password must contain at least 8 characters';
}

const hasLowercase = /[a-z]/.test(trimmedPassword);
const hasUppercase = /[A-Z]/.test(trimmedPassword);
const hasDigit = /[0-9]/.test(trimmedPassword);
const hasSpecialCharacter = /[!@#$%^&*]/.test(trimmedPassword);

if (!(hasLowercase && hasUppercase && hasDigit) && !hasSpecialCharacter) {
return 'Password must contain at least one lowercase letter, one uppercase letter, and one number, or use special characters';
}

return '';
};

const validateConfirmPassword = (password: string, confirmPassword: string) => {
if (password !== confirmPassword) {
return 'Passwords do not match';
}

return '';
};

const validateName = (name: string) => {
if (!/^[A-Za-z\s]+$/.test(name)) {
return 'The field must not contain any special characters';
}

return '';
};

const validateBirthDate = (birthDate: string) => {
const minAge = 13;
const currentDate = new Date();
const selectedDate = new Date(birthDate);

const ageDifference = currentDate.getFullYear() - selectedDate.getFullYear();

if (ageDifference < minAge) {
return `You must be at least ${minAge} years old to register`;
}

return '';
};

const validateStreet = (value: string) => {
if (value.trim() === '') {
return 'The street field must contain at least one character';
}
return '';
};

const validateCity = (city: string) => {
if (!/^[A-Za-z\s]+$/.test(city)) {
return 'The field must not contain any special characters';
}

return '';
};

const validatePostalCode = (postalCode: string, country: string) => {
if (country === 'USA' || country === 'Canada') {
if (!/^[A-Za-z]\d[A-Za-z] \d[A-Za-z]\d$/.test(postalCode)) {
return 'Invalid postal code format! Correct format A1B 2C3 for the USA and Canada';
} else {
return '';
}
} else if (!/^\d{5}$/.test(postalCode)) {
return 'Invalid postal code format! Correct format: 12345';
} else {
return '';
}
};

const validateCountry = (country: string) => {
if (!/^[A-Za-z\s]+$/.test(country)) {
return 'Enter a valid country';
}

return '';
};

const validateField = (fieldName: string, value: string, values?: IValues) => {
let errorMessage = '';

switch (fieldName) {
case 'email':
errorMessage = validateEmail(value);
break;
case 'password':
errorMessage = validatePassword(value);
break;
case 'confirmPassword':
if (values) {
errorMessage = validateConfirmPassword(values.password || '', value);
}
break;
case 'firstName':
case 'lastName':
errorMessage = validateName(value);
break;
case 'birthDate':
errorMessage = validateBirthDate(value);
break;
case 'streetAddress':
errorMessage = validateStreet(value);
break;
case 'city':
errorMessage = validateCity(value);
break;
case 'postalCode':
if (values) {
errorMessage = validatePostalCode(value, values.country || '');
}
break;
case 'country':
errorMessage = validateCountry(value);
break;
default:
break;
}

setErrors((prevErrors) => ({ ...prevErrors, [fieldName]: errorMessage }));
};

return { errors, validateField };
};
12 changes: 12 additions & 0 deletions e-commerce-app/src/interfaces/IRegistrationFormData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface IRegistrationFormData {
email?: string;
password?: string;
confirmPassword?: string;
firstName?: string;
lastName?: string;
birthDate?: string;
streetAddress?: string;
country?: string;
city?: string;
postalCode?: string;
}
4 changes: 4 additions & 0 deletions e-commerce-app/src/interfaces/IValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface IValues {
password?: string;
country?: string;
}
213 changes: 150 additions & 63 deletions e-commerce-app/src/pages/LoginPage/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,156 @@
import { Grid, Box, TextField, Button, Alert, Typography } from '@mui/material';
import {
Grid,
Box,
TextField,
Button,
Alert,
Typography,
InputAdornment,
IconButton,
} from '@mui/material';
import LoginImage from '../../assets/images/ImgLoginPage.png';
import { NavLink } from 'react-router-dom';
import { useState } from 'react';
import React, { useState } from 'react';
import { IRegistrationFormData } from '../../interfaces/IRegistrationFormData';
import { useValidate } from '../../hooks/useValidate';
import { useForm } from 'react-hook-form';
import VisibilityIcon from '@mui/icons-material/Visibility';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';

export const LoginPage: React.FC = () => {
const [error, setError] = useState({
status: false,
message: '',
type: ''
});
const handleSubmit=(e: React.FormEvent<HTMLFormElement>)=>{
e.preventDefault();
const data=new FormData(e.currentTarget);
const actualData = {
email: data.get('email'),
password: data.get('password'),
};
if (actualData.email && actualData.password) {
console.log(actualData);
(document.getElementById('login-form') as HTMLFormElement).reset();
setError({
status: true,
message: 'Successful!',
type: 'success'
});
} else {
setError({
status: true,
message: 'All fields are required',
type: 'error'
});
}
const [error, setError] = useState({
status: false,
message: '',
type: '',
});

const {
register,
formState: { errors },
} = useForm<IRegistrationFormData>();

const [showPassword, setShowPassword] = React.useState(false);

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
const actualData = {
email: data.get('email'),
password: data.get('password'),
};
return (
<Grid container sx={{ height: '80vh'}}>
<Grid item lg={7} sm={5} sx={{
backgroundImage: `url(${LoginImage})`,
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}>
</Grid>
<Grid item lg={5} sm={7}>
<Box component='form' noValidate sx={{ mt: 5, ml:8, mr:8}} id='login-form' onSubmit={handleSubmit}>
<Box>
{error.status ? <Alert severity={'error'}>{error.message}</Alert> : ''}
</Box>
<TextField sx={{mt: 2}} required fullWidth id='email' name='email' label='Email Address' type='email' />
<TextField required fullWidth margin='normal' id='password' name='password' label='Password' type='password'/>
<Box textAlign='center'>
<Button type='submit' variant='contained' sx={{px:5, mt: 2, backgroundColor: 'green'}}>Login</Button>
<Typography component='p' color='gray'>Remember me</Typography>
</Box>
<Box>
<NavLink to='/'>Forgot Password?</NavLink>
</Box>
<Box sx={{mt: 10}}>
<Typography component='p' textAlign='center'>Don&apos;t have account yet? Sign up</Typography>
</Box>
<Box textAlign='center'>
<Button type='submit' variant='contained' sx={{px:2, mt: 2, backgroundColor: 'green'}}>Registration</Button>
</Box>
</Box>
</Grid>
</Grid>
if (actualData.email && actualData.password) {
(document.getElementById('login-form') as HTMLFormElement).reset();
setError({
status: true,
message: 'Successful!',
type: 'success',
});
} else {
setError({
status: true,
message: 'All fields are required',
type: 'error',
});
}
};

const { errors: validationErrors, validateField } = useValidate();

);
};
return (
<Grid container sx={{ height: '80vh' }}>
<Grid
item
lg={7}
sm={5}
sx={{
backgroundImage: `url(${LoginImage})`,
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
></Grid>
<Grid item lg={5} sm={7}>
<Box
component="form"
noValidate
sx={{ mt: 5, ml: 8, mr: 8 }}
id="login-form"
onSubmit={handleSubmit}
>
<Box>{error.status ? <Alert severity={'error'}>{error.message}</Alert> : ''}</Box>
<TextField
sx={{ mt: 2 }}
{...register('email', { required: 'Email is required' })}
required
fullWidth
id="email"
name="email"
label="Email Address"
type="email"
error={!!validationErrors.email}
helperText={validationErrors.email || errors.email?.message}
onChange={(e) => validateField('email', e.target.value)}
/>
<TextField
{...register('password', {
required: 'Password is required',
})}
fullWidth
margin="normal"
id="password"
name="password"
label="Password"
type={showPassword ? 'text' : 'password'}
error={!!validationErrors.password}
helperText={validationErrors.password || errors.password?.message}
onChange={(e) => validateField('password', e.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{showPassword ? (
<IconButton onClick={() => setShowPassword(false)}>
<VisibilityIcon />
</IconButton>
) : (
<IconButton onClick={() => setShowPassword(true)}>
<VisibilityOffIcon />
</IconButton>
)}
</InputAdornment>
),
}}
/>
<Box textAlign="center">
<Button
type="submit"
variant="contained"
sx={{ px: 5, mt: 2, backgroundColor: 'green' }}
>
Login
</Button>
<Typography component="p" color="gray">
Remember me
</Typography>
</Box>
<Box>
<NavLink to="/">Forgot Password?</NavLink>
</Box>
<Box sx={{ mt: 10 }}>
<Typography component="p" textAlign="center">
Don&apos;t have account yet? Sign up
</Typography>
</Box>
<Box textAlign="center">
<Button
type="submit"
variant="contained"
sx={{ px: 2, mt: 2, backgroundColor: 'green' }}
>
Registration
</Button>
</Box>
</Box>
</Grid>
</Grid>
);
};