Skip to content

Commit

Permalink
refactor: change login form to react hook form (#333)
Browse files Browse the repository at this point in the history
  • Loading branch information
gab-gil authored Dec 19, 2024
1 parent 81f6382 commit 01f22a2
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 88 deletions.
1 change: 1 addition & 0 deletions canopeum_frontend/src/locale/en/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default {
'password-confirmation-error-required': 'Please re-enter your password',
'password-error-must-match': 'Passwords do not match',
'log-in': 'Log In',
'remember-me': 'Remember me',
'log-out': 'Log Out',
'log-out-confirmation': 'Are you sure you want to log out?',
'sign-up': 'Sign Up',
Expand Down
1 change: 1 addition & 0 deletions canopeum_frontend/src/locale/fr/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default {
'log-out': 'Se Déconnecter',
'log-out-confirmation': 'Est-vous certain de vouloir vous déconnecter?',
'sign-up': "S'inscrire",
'remember-me': 'Se souvenir de moi',
'create-account': 'Créer mon compte',
'already-have-an-account': 'Vous avez déjà un compte?',
'back-to-map': 'Retourner à la carte',
Expand Down
144 changes: 56 additions & 88 deletions canopeum_frontend/src/pages/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,150 +1,118 @@
/* eslint-disable react/jsx-props-no-spreading -- Needed for react hook forms */
import { useContext, useState } from 'react'
import { type SubmitHandler, useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'

import AuthPageLayout from '@components/auth/AuthPageLayout'
import Checkbox from '@components/Checkbox'
import { AuthenticationContext } from '@components/context/AuthenticationContext'
import { appRoutes } from '@constants/routes.constant'
import { formClasses } from '@constants/style'
import useApiClient from '@hooks/ApiClientHook'
import { LoginUser } from '@services/api'
import { storeToken } from '@utils/auth.utils'
import type { InputValidationError } from '@utils/validators'

const Login = () => {
const { authenticate } = useContext(AuthenticationContext)
const { t: translate } = useTranslation()
const { getApiClient } = useApiClient()

const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [rememberMe, setRememberMe] = useState(false)

const [emailError, setEmailError] = useState<InputValidationError | undefined>()
const [passwordError, setPasswordError] = useState<InputValidationError | undefined>()

const [loginError, setLoginError] = useState<string | undefined>()

const validateEmail = () => {
if (!email) {
setEmailError('required')

return false
}

setEmailError(undefined)

return true
}

const validatePassword = () => {
if (!password) {
setPasswordError('required')

return false
}

setPasswordError(undefined)

return true
}

const validateForm = () => {
// Do not return directly the method calls;
// we need each of them to be called before returning the result
const emailValid = validateEmail()
const passwordValid = validatePassword()

return emailValid
&& passwordValid
}
type LoginFormInputs = {
email: string,
password: string,
rememberMe: boolean,
}

const onLoginClick = async () => {
const isFormValid = validateForm()
if (!isFormValid) return
const Login = () => {
const {
register,
handleSubmit,
formState: { errors, touchedFields },
} = useForm<LoginFormInputs>({ mode: 'onTouched' })

const onSubmit: SubmitHandler<LoginFormInputs> = async formData => {
try {
const response = await getApiClient().authenticationClient.login(
new LoginUser({
email: email.trim(),
password,
email: formData.email.trim(),
password: formData.password,
}),
)

storeToken(response.token, rememberMe)
storeToken(response.token, formData.rememberMe)
authenticate(response.user)
} catch {
setLoginError(translate('auth.log-in-error'))
}
}

const { authenticate } = useContext(AuthenticationContext)
const { t: translate } = useTranslation()
const { getApiClient } = useApiClient()

const [loginError, setLoginError] = useState<string | undefined>()

return (
<AuthPageLayout>
<div style={{ flexGrow: '0.4', display: 'flex', alignItems: 'center' }}>
<h1 style={{ textAlign: 'center' }}>{translate('auth.log-in-header-text')}</h1>
</div>

<div className='col-10 col-sm-6 col-md-8 col-xl-6 d-flex flex-column gap-4'>
<form
className='col-10 col-sm-8 col-xl-6 d-flex flex-column gap-4'
onSubmit={handleSubmit(onSubmit)}
>
<div className='w-100'>
<label htmlFor='email-input'>{translate('auth.email-label')}</label>
<input
aria-describedby='email'
className={`form-control ${
emailError
? 'is-invalid'
touchedFields.email && errors.email
? formClasses.invalidFieldClass
: ''
}`}
id='email-input'
onBlur={() => validateEmail()}
onChange={event => setEmail(event.target.value)}
type='email'
{...register('email', {
required: { value: true, message: translate('auth.email-error-required') },
})}
/>
{emailError === 'required' && (
<span className='help-block text-danger'>
{translate('auth.email-error-required')}
</span>
)}
{errors.email && <span className='help-block text-danger'>{errors.email.message}</span>}
</div>

<div className='w-100'>
<label htmlFor='password-input'>{translate('auth.password-label')}</label>
<input
className={`form-control ${
passwordError
? 'is-invalid'
touchedFields.password && errors.password
? formClasses.invalidFieldClass
: ''
}`}
id='password-input'
onBlur={() => validatePassword()}
onChange={event => setPassword(event.target.value)}
type='password'
{...register('password', {
required: { value: true, message: translate('auth.password-error-required') },
})}
/>
{passwordError === 'required' && (
<span className='help-block text-danger'>
{translate('auth.password-error-required')}
</span>
{errors.password && (
<span className='help-block text-danger'>{errors.password.message}</span>
)}
</div>

<div>
<Checkbox
checked={rememberMe}
id='remember-me'
onChange={(_value, isChecked) => setRememberMe(isChecked)}
value='remember-me'
>
Remember Me
</Checkbox>
{/* TODO: Make a component, this is extracted from the Checkbox */}
{/* component until everything uses React Hook Forms */}
<div className='form-check'>
<input
className='form-check-input'
id='remember-me'
type='checkbox'
{...register('rememberMe')}
/>
<label className='form-check-label' htmlFor='remember-me'>
{translate('auth.remember-me')}
</label>
</div>
</div>

{loginError && <span className='help-block text-danger'>{loginError}</span>}

<div className='mt-4'>
<button
className='btn btn-primary w-100 my-2'
onClick={onLoginClick}
type='submit'
>
<button className='btn btn-primary w-100 my-2' type='submit'>
{translate('auth.log-in')}
</button>

Expand All @@ -156,7 +124,7 @@ const Login = () => {
{translate('auth.sign-up')}
</Link>
</div>
</div>
</form>
</AuthPageLayout>
)
}
Expand Down

0 comments on commit 01f22a2

Please sign in to comment.