From 357bbcafc3925b00fc2fa6064346af18077e22bf Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 7 Oct 2025 16:42:49 -0700 Subject: [PATCH 01/11] Switch Login form to useForm --- client/src/Login.jsx | 45 +++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/client/src/Login.jsx b/client/src/Login.jsx index 5dce682..32839c1 100644 --- a/client/src/Login.jsx +++ b/client/src/Login.jsx @@ -1,7 +1,8 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useNavigate, Link, useLocation, useSearchParams } from 'react-router'; import { StatusCodes } from 'http-status-codes'; import { Alert, Box, Button, Container, Group, Stack, TextInput, Title } from '@mantine/core'; +import { hasLength, isEmail, useForm } from '@mantine/form'; import { Head } from '@unhead/react'; import Api from './Api'; @@ -23,21 +24,28 @@ function Login () { } }, [authContext.user, from, navigate]); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); + const form = useForm({ + mode: 'uncontrolled', + initialValues: { + email: '', + password: '', + }, + validate: { + email: isEmail('Please enter a valid email address.'), + password: hasLength({ min: 8 }, 'Passwords must be at least 8 characters.'), + }, + }); - const [showInvalidError, setShowInvalidError] = useState(false); - - async function onSubmit (event) { - event.preventDefault(); - setShowInvalidError(false); + async function onSubmit (values) { try { - const response = await Api.auth.login(email, password); + const response = await Api.auth.login(values.email, values.password); authContext.setUser(response.data); navigate(from, { replace: true }); } catch (error) { if (error.response?.status === StatusCodes.UNPROCESSABLE_ENTITY || error.response?.status === StatusCodes.NOT_FOUND) { - setShowInvalidError(true); + form.setErrors({ + global: 'Invalid email and/or password', + }); } else { console.log(error); } @@ -51,25 +59,20 @@ function Login () { Log in -
+ {location.state?.flash && {location.state?.flash}} - {showInvalidError && Invalid email and/or password.} + {form.errors.global && {form.errors.global}} setEmail(e.target.value)} /> setPassword(e.target.value)} /> diff --git a/client/src/ValidationError.js b/client/src/ValidationError.js index 601e0f1..a9c4c56 100644 --- a/client/src/ValidationError.js +++ b/client/src/ValidationError.js @@ -3,18 +3,13 @@ import { capitalize } from 'inflection'; class ValidationError extends Error { constructor (data) { super(); - this.data = data; - } - - errorsFor (name) { - const errors = this.data.errors.filter((e) => e.path === name); - return errors.length ? errors : null; - } - - errorMessagesHTMLFor (name) { - const errors = this.errorsFor(name); - if (errors) { - return `${capitalize([...new Set(errors.map((e) => e.message))].join(', '))}.`; + this.data = {}; + for (const error of data.errors) { + this.data[error.path] ||= new Set(); + this.data[error.path].add(error.message); + } + for (const key of Object.keys(this.data)) { + this.data[key] = capitalize([...this.data[key]].join(', ')); } } } From cd43b7b71faa828f9d1d25f2e8cf3ff32065d8bb Mon Sep 17 00:00:00 2001 From: Francis Li Date: Tue, 14 Oct 2025 15:27:09 -0700 Subject: [PATCH 03/11] Admin invite form refactor --- client/src/Admin/Invites/AdminInviteForm.jsx | 77 +++++++++----------- client/src/RegistrationForm.jsx | 2 +- 2 files changed, 35 insertions(+), 44 deletions(-) diff --git a/client/src/Admin/Invites/AdminInviteForm.jsx b/client/src/Admin/Invites/AdminInviteForm.jsx index eb4df36..872e91d 100644 --- a/client/src/Admin/Invites/AdminInviteForm.jsx +++ b/client/src/Admin/Invites/AdminInviteForm.jsx @@ -1,38 +1,43 @@ -import { useState } from 'react'; import { useNavigate } from 'react-router'; import { Alert, Button, Container, Fieldset, Group, Stack, Textarea, TextInput, Title } from '@mantine/core'; +import { isEmail, isNotEmpty, useForm } from '@mantine/form'; import { useMutation } from '@tanstack/react-query'; import { Head } from '@unhead/react'; import Api from '../../Api'; +import ValidationError from '../../ValidationError'; function AdminInviteForm () { const navigate = useNavigate(); - const [invite, setInvite] = useState({ - firstName: '', - lastName: '', - email: '', - message: '', + const form = useForm({ + initialValues: { + firstName: '', + lastName: '', + email: '', + message: '', + }, + validate: { + firstName: isNotEmpty('First name is required.'), + email: isEmail('Please enter a valid email address.'), + }, }); const onSubmitMutation = useMutation({ - mutationFn: () => Api.invites.create(invite), + mutationFn: (values) => Api.invites.create(values), onSuccess: () => navigate('/admin/invites', { flash: 'Invite sent!' }), - onError: () => window.scrollTo(0, 0), + onError: (error) => { + if (error instanceof ValidationError) { + form.setErrors(error.data); + } else { + form.setErrors({ + global: error.toString(), + }); + } + window.scrollTo(0, 0); + }, }); - function onChange (event) { - const newInvite = { ...invite }; - newInvite[event.target.name] = event.target.value; - setInvite(newInvite); - } - - function onSubmit (event) { - event.preventDefault(); - onSubmitMutation.mutate(); - } - return ( <> @@ -40,44 +45,30 @@ function AdminInviteForm () { Invite a new User - +
- {onSubmitMutation.error && onSubmitMutation.error.message && {onSubmitMutation.error.message}} + {form.errors.global && {form.errors.global}}