Merge pull request 'feat: rewrite the form to react-hook-form and add validation' (#80) from feature/react-hook-form into main
All checks were successful
it-academy/dry-wash-pl/pipeline/head This commit looks good

Reviewed-on: #80
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
This commit is contained in:
Ильназ 2025-02-04 19:38:09 +03:00
commit bd66f00b81
5 changed files with 137 additions and 64 deletions

View File

@ -86,6 +86,15 @@
"dry-wash.arm.master.drawer.inputPhone.placeholder": "Enter Phone Number", "dry-wash.arm.master.drawer.inputPhone.placeholder": "Enter Phone Number",
"dry-wash.arm.master.drawer.button.save": "Save", "dry-wash.arm.master.drawer.button.save": "Save",
"dry-wash.arm.master.drawer.button.cancel": "Cancel", "dry-wash.arm.master.drawer.button.cancel": "Cancel",
"dry-wash.arm.master.drawer.toast.create-master": "Master created",
"dry-wash.arm.master.drawer.toast.error.empty-fields": "Fields cannot be empty",
"dry-wash.arm.master.drawer.toast.error.base": "Error",
"dry-wash.arm.master.drawer.toast.error.create-master": "Error creating master",
"dry-wash.arm.master.drawer.toast.error.create-master-details": "Failed to add master. Please try again",
"dry-wash.arm.master.drawer.form.name.required": "Master name is required",
"dry-wash.arm.master.drawer.form.phone.required": "Phone number is required",
"dry-wash.arm.master.drawer.form.phone.pattern": "Invalid phone number",
"dry-wash.arm.master.drawer.form.name.minLength": "Name must contain at least 2 characters",
"dry-wash.arm.master.sideBar.orders": "Orders", "dry-wash.arm.master.sideBar.orders": "Orders",
"dry-wash.arm.master.sideBar.master": "Masters", "dry-wash.arm.master.sideBar.master": "Masters",
"dry-wash.arm.master.sideBar.title": "Dry Master", "dry-wash.arm.master.sideBar.title": "Dry Master",

View File

@ -36,6 +36,15 @@
"dry-wash.arm.master.drawer.inputPhone.placeholder": "Введите номер телефона", "dry-wash.arm.master.drawer.inputPhone.placeholder": "Введите номер телефона",
"dry-wash.arm.master.drawer.button.save": "Сохранить", "dry-wash.arm.master.drawer.button.save": "Сохранить",
"dry-wash.arm.master.drawer.button.cancel": "Отменить", "dry-wash.arm.master.drawer.button.cancel": "Отменить",
"dry-wash.arm.master.drawer.toast.create-master": "Мастер создан",
"dry-wash.arm.master.drawer.toast.error.empty-fields": "Поля не могут быть пустыми",
"dry-wash.arm.master.drawer.toast.error.base": "Ошибка",
"dry-wash.arm.master.drawer.toast.error.create-master": "Ошибка при создании мастера",
"dry-wash.arm.master.drawer.toast.error.create-master-details": "Не удалось добавить мастера. Попробуйте еще раз",
"dry-wash.arm.master.drawer.form.name.required": "Имя мастера обязательно",
"dry-wash.arm.master.drawer.form.phone.required": "Телефон обязателен",
"dry-wash.arm.master.drawer.form.phone.pattern": "Некорректный номер телефона",
"dry-wash.arm.master.drawer.form.name.minLength": "Имя должно содержать минимум 2 символа",
"dry-wash.arm.master.sideBar.orders": "Заказы", "dry-wash.arm.master.sideBar.orders": "Заказы",
"dry-wash.arm.master.sideBar.master": "Мастера", "dry-wash.arm.master.sideBar.master": "Мастера",
"dry-wash.arm.master.sideBar.title": "Сухой мастер", "dry-wash.arm.master.sideBar.title": "Сухой мастер",

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import { import {
Button, Button,
FormControl, FormControl,
@ -14,107 +15,136 @@ import {
useToast, useToast,
InputGroup, InputGroup,
InputLeftElement, InputLeftElement,
FormErrorMessage,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PhoneIcon } from '@chakra-ui/icons'; import { PhoneIcon } from '@chakra-ui/icons';
import { api } from '../../__data__/service/api'; import { api } from '../../__data__/service/api';
import showToast from '../../helpers/showToast';
import { DrawerInputs } from '../../models/arm/form';
const MasterDrawer = ({ isOpen, onClose }) => { interface MasterDrawerProps {
const [addMaster, { error, isSuccess }] = api.useAddMasterMutation(); isOpen: boolean;
const toast = useToast(); onClose: () => void;
}
const [newMaster, setNewMaster] = useState({ name: '', phone: '' }); const MasterDrawer = ({ isOpen, onClose }: MasterDrawerProps) => {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<DrawerInputs>();
const handleSave = async () => { const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master.drawer',
});
const onSubmit: SubmitHandler<DrawerInputs> = async (data) => {
const trimMaster = { const trimMaster = {
phone: newMaster.phone.trim(), name: data.name.trim(),
name: newMaster.name.trim(), phone: data.phone.trim(),
}; };
if (trimMaster.name === '' || trimMaster.phone === '') { const isEmptyFields = trimMaster.name === '' || trimMaster.phone === '';
if (isEmptyFields) {
showToast({
toast,
title: t('toast.error.base'),
description: t('toast.error.empty-fields'),
status: 'error',
});
return; return;
} }
addMaster(trimMaster); await addMaster(trimMaster);
}; };
const [addMaster, { error, isSuccess }] = api.useAddMasterMutation();
const toast = useToast();
useEffect(() => { useEffect(() => {
if (isSuccess) { if (isSuccess) {
toast({ showToast({
title: 'Мастер создан.', toast,
description: `Мастер "${newMaster.name}" успешно добавлен.`, title: t('toast.create-master'),
status: 'success', status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
}); });
reset();
onClose(); onClose();
} }
}, [isSuccess]); }, [isSuccess]);
useEffect(() => { useEffect(() => {
if (error) { if (error) {
toast({ showToast({
title: 'Ошибка при создании мастера.', toast,
description: 'Не удалось добавить мастера. Попробуйте еще раз.', title: t('toast.error.create-master'),
description: t('toast.error.create-master-details'),
status: 'error', status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
}); });
console.error(error); console.error(error);
} }
}, [error]); }, [error]);
const { t } = useTranslation('~', {
keyPrefix: 'dry-wash.arm.master.drawer',
});
return ( return (
<Drawer isOpen={isOpen} onClose={onClose} size='md'> <Drawer isOpen={isOpen} onClose={onClose} size='md'>
<DrawerOverlay /> <DrawerOverlay />
<DrawerContent> <DrawerContent>
<DrawerCloseButton /> <form onSubmit={handleSubmit(onSubmit)}>
<DrawerHeader>{t('title')}</DrawerHeader> <DrawerCloseButton />
<DrawerBody> <DrawerHeader>{t('title')}</DrawerHeader>
<FormControl mb='4'> <DrawerBody>
<FormLabel>{t('inputName.label')}</FormLabel> <FormControl mb='4' isInvalid={!!errors.name}>
<Input <FormLabel>{t('inputName.label')}</FormLabel>
// isInvalid
value={newMaster.name}
onChange={(e) =>
setNewMaster({ ...newMaster, name: e.target.value })
}
placeholder={t('inputName.placeholder')}
/>
</FormControl>
<FormControl>
<FormLabel>{t('inputPhone.label')}</FormLabel>
<InputGroup>
<InputLeftElement pointerEvents='none'>
<PhoneIcon color='gray.300' />
</InputLeftElement>
<Input <Input
// isInvalid {...register('name', {
value={newMaster.phone} required: t('form.name.required'),
onChange={(e) => minLength: {
setNewMaster({ ...newMaster, phone: e.target.value }) value: 2,
} message: t('form.name.minLength'),
placeholder={t('inputPhone.placeholder')} },
})}
placeholder={t('inputName.placeholder')}
/> />
</InputGroup> <FormErrorMessage>
</FormControl> {errors.name && errors.name.message}
</DrawerBody> </FormErrorMessage>
<DrawerFooter> </FormControl>
<Button colorScheme='teal' mr={3} onClick={handleSave}> <FormControl isInvalid={!!errors.phone}>
{t('button.save')} <FormLabel>{t('inputPhone.label')}</FormLabel>
</Button> <InputGroup>
<Button variant='ghost' onClick={onClose}> <InputLeftElement pointerEvents='none'>
{t('button.cancel')} <PhoneIcon color='gray.300' />
</Button> </InputLeftElement>
</DrawerFooter> <Input
{...register('phone', {
required: t('form.phone.required'),
pattern: {
value: /^(\+7|8)\d{10}$/,
message: t('form.phone.pattern'),
},
setValueAs: (value) => value.replace(/[^\d+]/g, ''),
})}
placeholder={t('inputPhone.placeholder')}
/>
</InputGroup>
<FormErrorMessage>
{errors.phone && errors.phone.message}
</FormErrorMessage>
</FormControl>
</DrawerBody>
<DrawerFooter>
<Button colorScheme='teal' mr={3} type='submit'>
{t('button.save')}
</Button>
<Button variant='ghost' onClick={onClose}>
{t('button.cancel')}
</Button>
</DrawerFooter>
</form>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
); );

21
src/helpers/showToast.ts Normal file
View File

@ -0,0 +1,21 @@
import { UseToastOptions } from '@chakra-ui/react';
interface ShowToast {
toast: (options: UseToastOptions) => void;
title: string;
description?: string;
status: 'info' | 'warning' | 'success' | 'error';
}
const showToast = ({ toast, title, description, status }: ShowToast) => {
toast({
title,
description,
status,
duration: 5000,
isClosable: true,
position: 'top-right',
});
};
export default showToast;

4
src/models/arm/form.ts Normal file
View File

@ -0,0 +1,4 @@
export type DrawerInputs = {
phone: string;
name: string;
};