Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: change login form to react hook form #333

Merged
merged 6 commits into from
Dec 19, 2024
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
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 */}
Comment on lines +97 to +98
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is planned as the immediate follow-up PR to update the Checkbox component, I'm fine with that being done separately.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not immediate but I saw that the only other place we used was the SiteSummaryAction. I was planning on probably doing a refactoring of that next so that I can refactor the component. Is that ok with you?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah ok that sounds good. I just didn't want to end up with two ways of handling checkboxes for too long. Or forgetting about them.

<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
Loading