From 9aa23aedfb4d69178fe60fd60ad7bc61b801ee38 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Mon, 3 Feb 2025 13:13:11 +0300 Subject: [PATCH] F: Rework signup form using react-hook-form and zod --- rsconcept/frontend/src/backend/auth/api.ts | 6 +- rsconcept/frontend/src/backend/users/api.ts | 37 +++++-- .../frontend/src/backend/users/useSignup.tsx | 4 +- .../frontend/src/components/ui/Checkbox.tsx | 2 +- rsconcept/frontend/src/pages/LoginPage.tsx | 4 +- .../src/pages/RegisterPage/FormSignup.tsx | 98 ++++++++----------- rsconcept/frontend/src/utils/labels.ts | 7 +- 7 files changed, 84 insertions(+), 74 deletions(-) diff --git a/rsconcept/frontend/src/backend/auth/api.ts b/rsconcept/frontend/src/backend/auth/api.ts index a31f4c72..340a6eed 100644 --- a/rsconcept/frontend/src/backend/auth/api.ts +++ b/rsconcept/frontend/src/backend/auth/api.ts @@ -4,14 +4,14 @@ import { z } from 'zod'; import { axiosGet, axiosPost } from '@/backend/apiTransport'; import { DELAYS } from '@/backend/configuration'; import { ICurrentUser } from '@/models/user'; -import { information } from '@/utils/labels'; +import { errors, information } from '@/utils/labels'; /** * Represents login data, used to authenticate users. */ export const UserLoginSchema = z.object({ - username: z.string().nonempty('Поле логина обязательно для заполнения'), - password: z.string().nonempty('Поле пароля обязательно для заполнения') + username: z.string().nonempty(errors.requiredField), + password: z.string().nonempty(errors.requiredField) }); /** diff --git a/rsconcept/frontend/src/backend/users/api.ts b/rsconcept/frontend/src/backend/users/api.ts index 8ceefbad..968c0507 100644 --- a/rsconcept/frontend/src/backend/users/api.ts +++ b/rsconcept/frontend/src/backend/users/api.ts @@ -1,22 +1,41 @@ import { queryOptions } from '@tanstack/react-query'; +import { z } from 'zod'; import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport'; import { DELAYS } from '@/backend/configuration'; -import { IUser, IUserInfo, IUserProfile } from '@/models/user'; -import { information } from '@/utils/labels'; +import { IUserInfo, IUserProfile } from '@/models/user'; +import { patterns } from '@/utils/constants'; +import { errors, information } from '@/utils/labels'; /** * Represents signup data, used to create new users. */ -export interface IUserSignupData extends Omit { - password: string; - password2: string; -} +export const UserSignupSchema = z + .object({ + username: z.string().nonempty(errors.requiredField).regex(RegExp(patterns.login), errors.loginFormat), + email: z.string().email(errors.emailField), + first_name: z.string(), + last_name: z.string(), + + password: z.string().nonempty(errors.requiredField), + password2: z.string().nonempty(errors.requiredField) + }) + .refine(schema => schema.password === schema.password2, { path: ['password2'], message: errors.passwordsMismatch }); + +/** + * Represents signup data, used to create new users. + */ +export type IUserSignupDTO = z.infer; /** * Represents user data, intended to update user profile in persistent storage. */ -export interface IUpdateProfileDTO extends Omit {} +export interface IUpdateProfileDTO { + username: string; + email: string; + first_name: string; + last_name: string; +} export const usersApi = { baseKey: 'users', @@ -41,8 +60,8 @@ export const usersApi = { }) }), - signup: (data: IUserSignupData) => - axiosPost({ + signup: (data: IUserSignupDTO) => + axiosPost({ endpoint: '/users/api/signup', request: { data: data, diff --git a/rsconcept/frontend/src/backend/users/useSignup.tsx b/rsconcept/frontend/src/backend/users/useSignup.tsx index 32a108f6..0a1b924c 100644 --- a/rsconcept/frontend/src/backend/users/useSignup.tsx +++ b/rsconcept/frontend/src/backend/users/useSignup.tsx @@ -1,7 +1,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { DataCallback } from '@/backend/apiTransport'; -import { IUserSignupData, usersApi } from '@/backend/users/api'; +import { IUserSignupDTO, usersApi } from '@/backend/users/api'; import { IUserProfile } from '@/models/user'; export const useSignup = () => { @@ -13,7 +13,7 @@ export const useSignup = () => { }); return { signup: ( - data: IUserSignupData, // + data: IUserSignupDTO, // onSuccess?: DataCallback ) => mutation.mutate(data, { onSuccess }), isPending: mutation.isPending, diff --git a/rsconcept/frontend/src/components/ui/Checkbox.tsx b/rsconcept/frontend/src/components/ui/Checkbox.tsx index 784bf3bd..7756f52a 100644 --- a/rsconcept/frontend/src/components/ui/Checkbox.tsx +++ b/rsconcept/frontend/src/components/ui/Checkbox.tsx @@ -12,7 +12,7 @@ export interface CheckboxProps extends Omit disabled?: boolean; /** Current value - `true` or `false`. */ - value: boolean; + value?: boolean; /** Callback to set the `value`. */ setValue?: (newValue: boolean) => void; diff --git a/rsconcept/frontend/src/pages/LoginPage.tsx b/rsconcept/frontend/src/pages/LoginPage.tsx index f5ae48b9..e6334a2f 100644 --- a/rsconcept/frontend/src/pages/LoginPage.tsx +++ b/rsconcept/frontend/src/pages/LoginPage.tsx @@ -77,7 +77,7 @@ function LoginPage() { id='username' autoComplete='username' label='Логин или email' - {...register('username', { required: true })} + {...register('username')} autoFocus allowEnter spellCheck={false} @@ -86,7 +86,7 @@ function LoginPage() { /> ({ + resolver: zodResolver(UserSignupSchema) + }); - useEffect(() => { + const isValid = acceptPrivacy && acceptRules; + + function resetErrors() { reset(); - }, [username, email, password, password2, reset]); + clearErrors(); + } function handleCancel() { if (router.canBack()) { @@ -49,25 +54,15 @@ function FormSignup() { } } - function handleSubmit(event: React.FormEvent) { - event.preventDefault(); - if (isPending) { - return; - } - signup( - { - username, - email, - password, - password2, - first_name: firstName, - last_name: lastName - }, - createdUser => router.push(urls.login_hint(createdUser.username)) - ); + function onSubmit(data: IUserSignupDTO) { + signup(data, createdUser => router.push(urls.login_hint(createdUser.username))); } return ( -
+ void handleSubmit(onSubmit)(event)} + onChange={resetErrors} + >

Новый пользователь @@ -87,68 +82,59 @@ function FormSignup() { setUsername(event.target.value)} + error={errors.username} /> setPassword(event.target.value)} + error={errors.password} /> setPassword2(event.target.value)} + error={errors.password2} /> setEmail(event.target.value)} + error={errors.email} /> setFirstName(event.target.value)} + error={errors.first_name} /> setLastName(event.target.value)} + error={errors.last_name} /> @@ -163,10 +149,10 @@ function FormSignup() {
- +
- {error ? : null} + {serverError ? : null} ); } @@ -174,17 +160,17 @@ function FormSignup() { export default FormSignup; // ====== Internals ========= -function ProcessError({ error }: { error: ErrorData }): React.ReactElement { +function ServerError({ error }: { error: ErrorData }): React.ReactElement { if (axios.isAxiosError(error) && error.response && error.response.status === 400) { if ('email' in error.response.data) { return ( // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -
{error.response.data.email}.
+
{error.response.data.email}
); } else if ('username' in error.response.data) { return ( // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -
{error.response.data.username}.
+
{error.response.data.username}
); } else { return ( diff --git a/rsconcept/frontend/src/utils/labels.ts b/rsconcept/frontend/src/utils/labels.ts index cab16f5c..e7676422 100644 --- a/rsconcept/frontend/src/utils/labels.ts +++ b/rsconcept/frontend/src/utils/labels.ts @@ -985,7 +985,12 @@ export const errors = { imageFailed: 'Ошибка при создании изображения', reuseOriginal: 'Повторное использование удаляемой конституенты при отождествлении', substituteInherited: 'Нельзя удалять наследованные конституенты при отождествлении', - inputAlreadyExists: 'Концептуальная схема с таким именем уже существует' + inputAlreadyExists: 'Концептуальная схема с таким именем уже существует', + requiredField: 'Поле обязательно для заполнения', + emailField: 'Введите корректный адрес электронной почты', + rulesNotAccepted: 'Примите условия пользования Порталом', + privacyNotAccepted: 'Примите политику обработки персональных данных', + loginFormat: 'Имя пользователя должно содержать только буквы и цифры' }; /**