diff --git a/public/images/license-dtr-eac.png b/public/images/license-dtr-eac.png new file mode 100644 index 0000000..fd78b4b Binary files /dev/null and b/public/images/license-dtr-eac.png differ diff --git a/public/images/license-mcs.jpg b/public/images/license-mcs.jpg new file mode 100644 index 0000000..1f10f69 Binary files /dev/null and b/public/images/license-mcs.jpg differ diff --git a/src/core/providers/index.ts b/src/core/providers/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/core/providers/modal-provider.tsx b/src/core/providers/modal-provider.tsx new file mode 100644 index 0000000..af19951 --- /dev/null +++ b/src/core/providers/modal-provider.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { + useState, + useContext, + useCallback, + ReactNode, + createContext, +} from 'react'; +import { Modal } from '@shared/ui/modal'; + +const ModalContext = createContext({ + hideModal: () => {}, + showModal: (content: ReactNode) => {}, +}); + +const useModal = () => useContext(ModalContext); + +const ModalProvider = ({ children }: { children: ReactNode }) => { + const [modalContent, setModalContent] = useState(null); + + const showModal = useCallback((content: ReactNode) => { + setModalContent(content); + }, []); + + const hideModal = useCallback(() => { + setModalContent(null); + }, []); + + return ( + + {children} + {/* Ваш Modal компонент здесь */} + + {modalContent} + + + ); +}; + +export { useModal, ModalProvider }; diff --git a/src/core/styles/variables.scss b/src/core/styles/variables.scss index c518824..de38fbb 100644 --- a/src/core/styles/variables.scss +++ b/src/core/styles/variables.scss @@ -1,4 +1,3 @@ - //frontend breakpoint $mobile: 360px; $tablet: 768px; @@ -24,5 +23,6 @@ $color-darkgray: #999999; $color-text: #333333; $color-text-light: #222222; $color-mark: #E96526; -$color-error: #FF0000; +$color-error: #ff0000; +$color-error-light: #ff9191; $color-gray-border: #555555; \ No newline at end of file diff --git a/src/shared/lib/clickOutside/index.ts b/src/shared/lib/clickOutside/index.ts new file mode 100644 index 0000000..145b15f --- /dev/null +++ b/src/shared/lib/clickOutside/index.ts @@ -0,0 +1 @@ +export { useClickOutside } from './useClickOutside'; diff --git a/src/shared/lib/clickOutside/useClickOutside.tsx b/src/shared/lib/clickOutside/useClickOutside.tsx new file mode 100644 index 0000000..aab6bb7 --- /dev/null +++ b/src/shared/lib/clickOutside/useClickOutside.tsx @@ -0,0 +1,28 @@ +import { RefObject, useEffect } from 'react'; + +export function useClickOutside( + ref: RefObject, + fn: (event: Event) => void, +) { + useEffect(() => { + const handleClickOutside = (event: Event) => { + const el = ref?.current; + if (event instanceof MouseEvent && window !== undefined) { + const x = event?.offsetX || 0; + const width = window?.innerWidth - 18; + if (x >= width) { + return; + } + } + if (!el || el.contains((event?.target as Node) || null)) { + return; + } + fn(event); + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [ref, fn]); +} diff --git a/src/shared/lib/detectIOS/detectIOS.ts b/src/shared/lib/detectIOS/detectIOS.ts new file mode 100644 index 0000000..1084da2 --- /dev/null +++ b/src/shared/lib/detectIOS/detectIOS.ts @@ -0,0 +1,15 @@ +function detectIOS() { + const iosQuirkPresent = () => { + const audio = new Audio(); + audio.volume = 0.5; + return audio.volume === 1; // volume cannot be changed from "1" on iOS 12 and below + }; + + const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); + const isAppleDevice = navigator.userAgent.includes('Macintosh'); + const isTouchScreen = navigator.maxTouchPoints >= 1; // true for iOS 13 (and hopefully beyond) + + return isIOS || (isAppleDevice && (isTouchScreen || iosQuirkPresent())); +} + +export { detectIOS }; diff --git a/src/shared/lib/detectIOS/index.ts b/src/shared/lib/detectIOS/index.ts new file mode 100644 index 0000000..544abe3 --- /dev/null +++ b/src/shared/lib/detectIOS/index.ts @@ -0,0 +1 @@ +export { detectIOS } from './detectIOS'; diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts new file mode 100644 index 0000000..c2178b3 --- /dev/null +++ b/src/shared/lib/index.ts @@ -0,0 +1,2 @@ +export { useClickOutside } from './clickOutside'; +export { detectIOS } from './detectIOS'; diff --git a/src/shared/ui/advanced-phone-input/advanced-phone-input.tsx b/src/shared/ui/advanced-phone-input/advanced-phone-input.tsx deleted file mode 100644 index 1db9c5e..0000000 --- a/src/shared/ui/advanced-phone-input/advanced-phone-input.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import s from './advancedPhoneInput.module.scss'; -import { DetailedHTMLProps, forwardRef, InputHTMLAttributes, Ref } from 'react'; -import { clsx } from 'clsx'; -import { Button, Input } from '@shared/ui'; - -type AdvancedPhoneInputProps = { - containerClassName?: string; - inputClassName?: string; - buttonClassName?: string; - onClick?: () => void; - text: string; -} & DetailedHTMLProps, HTMLInputElement>; - -const AdvancedPhoneInput = forwardRef(function AdvancedPhoneInput( - { - containerClassName, - inputClassName, - buttonClassName, - onClick, - text, - ...props - }: AdvancedPhoneInputProps, - ref: Ref, -) { - return ( -
- - -
- ); -}); - -export default AdvancedPhoneInput; diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index 53fd45e..17eb134 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -1,6 +1,5 @@ export { Button } from './button'; export { Mark } from './mark'; export { Input } from './input'; -export { AdvancedPhoneInput } from './advanced-phone-input'; export { TextArea } from './text-area'; export { PhoneInput } from './phone-input'; diff --git a/src/shared/ui/input/input.module.scss b/src/shared/ui/input/input.module.scss index 2b96349..1b5905c 100644 --- a/src/shared/ui/input/input.module.scss +++ b/src/shared/ui/input/input.module.scss @@ -1,3 +1,8 @@ +.Container { + position: relative; + display: block; +} + .Input { display: flex; background: $color-white; @@ -13,16 +18,16 @@ color: $color-text; width: max-content; - @include iftablet{ + @include iftablet { font-size: rem(18px); } - @include iflaptop{ + @include iflaptop { font-size: rem(20px); padding: rem(10px) rem(20px); } - @include ifdesktop{ + @include ifdesktop { font-size: rem(24px); } @@ -45,7 +50,7 @@ border-color: $color-error; } - &_fullWidth{ + &_fullWidth { width: 100%; } @@ -68,4 +73,16 @@ transition: border-color ease .5s; } } +} + +.Error { + position: absolute; + z-index: 2; + left: rem(8px); + bottom: rem(-16px); + font-family: $font-open-sans; + font-weight: $font-light; + font-size: rem(12px); + line-height: 100%; + color: $color-error; } \ No newline at end of file diff --git a/src/shared/ui/input/input.tsx b/src/shared/ui/input/input.tsx index 21375b1..c20feab 100644 --- a/src/shared/ui/input/input.tsx +++ b/src/shared/ui/input/input.tsx @@ -11,6 +11,7 @@ type InputProps = { fullWidth?: boolean; variant?: 'default' | 'ghost'; error?: string | boolean; + errorTextColor?: string; } & DetailedHTMLProps, HTMLInputElement>; const Input = forwardRef(function Input( @@ -19,22 +20,30 @@ const Input = forwardRef(function Input( fullWidth = false, variant = 'default', error = false, + errorTextColor, ...props }: InputProps, ref: Ref, ) { return ( - + + {error && ( + + {error} + )} - /> + ); }); diff --git a/src/shared/ui/modal/index.ts b/src/shared/ui/modal/index.ts new file mode 100644 index 0000000..2e8af51 --- /dev/null +++ b/src/shared/ui/modal/index.ts @@ -0,0 +1 @@ +export { Modal, ModalContent } from './modal'; diff --git a/src/shared/ui/modal/modal.module.scss b/src/shared/ui/modal/modal.module.scss new file mode 100644 index 0000000..0808bb2 --- /dev/null +++ b/src/shared/ui/modal/modal.module.scss @@ -0,0 +1,144 @@ +.ModalBackdrop { + z-index: 1000; + position: absolute; + top: 0; + left: 0; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + background: rgba(0, 0, 0, 0.5); + padding: 0; + + @include iftablet { + position: fixed; + height: 100vh; + padding: 0 rem(20px); + } + + @include iflaptop { + padding: 0 rem(48px); + } + animation: fadeIn ease 0.3s; + + &_open { + animation: fadeOut ease 0.3s; + } + + &_isIOS { + height: 150vh !important; + + @include iftablet { + height: unset; + } + } +} + +.ModalContent { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: white; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); + //max-height: 90%; + overflow: auto; + width: 100%; + //border-radius: 8px; + animation: fadeIn ease 0.3s; + + @include iftablet { + position: relative; + top: unset; + bottom: unset; + left: unset; + right: unset; + height: 100%; + border-radius: rem(8px); + } + + &_open { + animation: fadeOut ease 0.3s; + } + + &_isIOS { + height: 100vh !important; + + @include iftablet { + height: 100%; + } + } + + &__Inner { + position: relative; + overflow: hidden; + height: fit-content; + } +} + +.Modal { + position: relative; + + &__Close { + position: absolute; + top: rem(5px); + right: rem(5px); + width: rem(36px); + height: rem(36px); + padding: rem(10px); + cursor: pointer; + margin-bottom: rem(4px); + transform: rotate(0deg); + transition: transform 0.3s; + + @include iftablet { + top: rem(35px); + right: rem(35px); + margin-bottom: 0; + } + + @include iflaptop { + top: rem(50px); + right: rem(50px); + } + + &:hover, + &:active { + transform: rotate(45deg); + transition: transform 0.3s; + } + + svg { + width: rem(17px); + height: rem(17px); + } + + //svg { + // transform: rotate(0deg); + // transition: transform 0.3s; + // &:hover, + // &:active { + // transform: rotate(45deg); + // transition: transform 0.3s; + // } + //} + } +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes fadeOut { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} diff --git a/src/shared/ui/modal/modal.tsx b/src/shared/ui/modal/modal.tsx new file mode 100644 index 0000000..0b048cd --- /dev/null +++ b/src/shared/ui/modal/modal.tsx @@ -0,0 +1,109 @@ +'use client'; + +import s from './modal.module.scss'; +import { ReactNode, useEffect, useRef } from 'react'; +import ReactDOM from 'react-dom'; +import clsx from 'clsx'; +import { useModal } from '@core/providers/modal-provider'; +import { detectIOS, useClickOutside } from '@shared/lib'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + children: ReactNode; +} + +const Modal = ({ children, isOpen, onClose }: ModalProps) => { + useEffect(() => { + const html = document.documentElement; + if (isOpen) { + html.style.overflow = 'hidden'; + } + return () => { + html.style.overflow = ''; + }; + }, [isOpen]); + + if (!isOpen) return null; + + const isIOS = detectIOS(); + + return ReactDOM.createPortal( +
+ {children} +
, + document.body, + ); +}; + +function ModalContent(props: { + children: ReactNode; + className?: string; + innerClassName?: string; + closeByClickOutside?: boolean; +}) { + const { + children, + className, + innerClassName, + closeByClickOutside = false, + } = props; + + const modal = useModal(); + + const hideModal = () => modal.hideModal(); + + const modalRef = useRef(null); + + const handleClickOutside = () => { + if (closeByClickOutside) { + hideModal(); + } + }; + + useClickOutside(modalRef, handleClickOutside); + + const isIOS = detectIOS(); + + return ( +
e.stopPropagation()} + ref={modalRef} + > +
+
+ +
+ {children} +
+
+ ); +} + +export { Modal, ModalContent }; + +const CloseIcon = () => ( + + + +); diff --git a/src/shared/ui/phone-input/phone-input.tsx b/src/shared/ui/phone-input/phone-input.tsx index f520349..8aafbdb 100644 --- a/src/shared/ui/phone-input/phone-input.tsx +++ b/src/shared/ui/phone-input/phone-input.tsx @@ -1,13 +1,15 @@ -//import s from './phone-input.module.scss'; import { Input } from '@shared/ui'; import { useMaskito } from '@maskito/react'; - import { maskitoPhoneOptionsGenerator } from '@maskito/phone'; import metadata from 'libphonenumber-js/min/metadata'; import { DetailedHTMLProps, InputHTMLAttributes } from 'react'; type PhoneInput = { className?: string; + variant?: 'default' | 'ghost'; + error?: string | boolean; + errorTextColor?: string; + fullWidth?: boolean; } & DetailedHTMLProps, HTMLInputElement>; export default function PhoneInput({ ...props }: PhoneInput) { @@ -17,9 +19,5 @@ export default function PhoneInput({ ...props }: PhoneInput) { }); const maskedInputRef = useMaskito({ options }); - return ( - <> - - - ); + return ; } diff --git a/src/shared/ui/text-area/text-area.module.scss b/src/shared/ui/text-area/text-area.module.scss index dbcb201..b2c9a34 100644 --- a/src/shared/ui/text-area/text-area.module.scss +++ b/src/shared/ui/text-area/text-area.module.scss @@ -1,4 +1,5 @@ .Container { + position: relative; display: flex; flex-direction: column; box-sizing: border-box; @@ -6,7 +7,7 @@ /* Allows the `resize` property to work on the div */ overflow: hidden; - &_fullWidth{ + &_fullWidth { width: 100%; } } @@ -29,16 +30,16 @@ width: 100%; flex-grow: 1; - @include iftablet{ + @include iftablet { font-size: rem(18px); } - @include iflaptop{ + @include iflaptop { font-size: rem(20px); padding: rem(10px) rem(20px); } - @include ifdesktop{ + @include ifdesktop { font-size: rem(24px); } @@ -47,27 +48,33 @@ /* Arrow mouse cursor over the scrollbar */ cursor: auto; } + &::-webkit-scrollbar { width: rem(12px); height: rem(12px); } + &::-webkit-scrollbar-track, &::-webkit-scrollbar-thumb { background-clip: content-box; border: rem(4px) solid transparent; border-radius: rem(12px); } + &::-webkit-scrollbar-track { background-color: #333; // цвет дорожки } + &::-webkit-scrollbar-thumb { background-color: #999; // цвет указателя } + @media (hover: hover) { :where(&:not(:disabled))::-webkit-scrollbar-thumb:hover { background-color: #999; } } + &:where(:autofill) { /* Reliably removes native autofill colors */ background-clip: text; @@ -113,4 +120,18 @@ transition: border-color ease .5s; } } + + &_error { + border: 1px solid $color-error; + } +} + +.Error { + margin-left: 8px; + margin-top: 4px; + font-family: $font-open-sans; + font-weight: $font-light; + font-size: rem(12px); + line-height: 100%; + color: $color-error; } \ No newline at end of file diff --git a/src/shared/ui/text-area/text-area.tsx b/src/shared/ui/text-area/text-area.tsx index 1513a2f..7c92abb 100644 --- a/src/shared/ui/text-area/text-area.tsx +++ b/src/shared/ui/text-area/text-area.tsx @@ -13,6 +13,8 @@ type TextAreaProps = { children?: ReactNode; variant?: 'default' | 'ghost'; fullWidth?: boolean; + error?: string | boolean; + errorTextColor?: string; } & DetailedHTMLProps< TextareaHTMLAttributes, HTMLTextAreaElement @@ -24,6 +26,8 @@ const TextArea = forwardRef(function TextArea( children, variant = 'default', fullWidth = false, + error = false, + errorTextColor, ...props }: TextAreaProps, ref: Ref, @@ -33,10 +37,20 @@ const TextArea = forwardRef(function TextArea( + {error && ( + + {error} + + )} ); }); diff --git a/src/shared/ui/advanced-phone-input/advancedPhoneInput.module.scss b/src/widgets/advanced-phone-input/advanced-phone-input.module.scss similarity index 100% rename from src/shared/ui/advanced-phone-input/advancedPhoneInput.module.scss rename to src/widgets/advanced-phone-input/advanced-phone-input.module.scss diff --git a/src/widgets/advanced-phone-input/advanced-phone-input.tsx b/src/widgets/advanced-phone-input/advanced-phone-input.tsx new file mode 100644 index 0000000..2a870c5 --- /dev/null +++ b/src/widgets/advanced-phone-input/advanced-phone-input.tsx @@ -0,0 +1,84 @@ +'use client'; + +import s from './advanced-phone-input.module.scss'; +import { clsx } from 'clsx'; +import { Button, PhoneInput } from '@shared/ui'; +import { z } from 'zod'; +import { Controller, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import toast from 'react-hot-toast'; +import { sendFormFn } from '@shared/api/api.service'; +import { isValidPhoneNumber } from 'libphonenumber-js/min'; + +type AdvancedPhoneInputProps = { + containerClassName?: string; +}; + +const FormSchema = z.object({ + phone: z.string().refine(isValidPhoneNumber, 'Некорректный номер телефона'), +}); +type TForm = z.infer; + +const defaultValues = { + phone: '', +}; + +export default function AdvancedPhoneInput({ + containerClassName, +}: AdvancedPhoneInputProps) { + const { + handleSubmit, + control, + reset, + clearErrors, + formState: { errors }, + } = useForm({ + resolver: zodResolver(FormSchema), + defaultValues, + }); + + const onSubmit = async (data: TForm) => { + const payload = { + ...data, + form: 'offer-request-form-desktop', + }; + + try { + await sendFormFn(payload); + toast.success('Заявка на консультацию принята'); + reset(defaultValues); + } catch (e) { + toast.error('Ошибка при отправке заявки...', { + duration: 3000, + }); + } + }; + + return ( +
+ ( + { + clearErrors('phone'); + field.onChange(e); + }} + error={errors && errors.phone?.message} + errorTextColor={'#ff9191'} + /> + )} + /> + + + ); +} diff --git a/src/shared/ui/advanced-phone-input/index.ts b/src/widgets/advanced-phone-input/index.ts similarity index 100% rename from src/shared/ui/advanced-phone-input/index.ts rename to src/widgets/advanced-phone-input/index.ts diff --git a/src/widgets/contacts-form/ui.tsx b/src/widgets/contacts-form/ui.tsx index 2021003..c42241b 100644 --- a/src/widgets/contacts-form/ui.tsx +++ b/src/widgets/contacts-form/ui.tsx @@ -1,7 +1,7 @@ 'use client'; import s from './styles.module.scss'; -import { Button, Input } from '@shared/ui'; +import { Button, Input, PhoneInput } from '@shared/ui'; import Image from 'next/image'; import toast from 'react-hot-toast'; import { Controller, useForm } from 'react-hook-form'; @@ -10,10 +10,16 @@ import { z } from 'zod'; import bgForm from '@public/images/bg-form.jpg'; import { sendFormFn } from '@shared/api/api.service'; +import { isValidPhoneNumber } from 'libphonenumber-js/min'; const FormSchema = z.object({ - name: z.string().min(3), - phone: z.string(), + name: z + .string() + .min(3, { message: 'Поле должно содержать не менее 3-х букв' }) + .regex(/^[A-Za-zА-Яа-яЁё]+(?:[ '-][A-Za-zА-Яа-яЁё]+)*$/, { + message: 'Поле содержит некорректные символы', + }), + phone: z.string().refine(isValidPhoneNumber, 'Некорректный номер телефона'), }); type TForm = z.infer; @@ -27,6 +33,7 @@ export default function ContactsForm() { handleSubmit, control, reset, + clearErrors, formState: { errors }, } = useForm({ mode: 'onSubmit', @@ -75,18 +82,33 @@ export default function ContactsForm() { control={control} name={'name'} render={({ field }) => ( - + { + clearErrors('name'); + field.onChange(e); + }} + error={errors && errors.name?.message} + errorTextColor={'#ff9191'} + /> )} /> ( - { + clearErrors('phone'); + field.onChange(e); + }} + error={errors && errors.phone?.message} + errorTextColor={'#ff9191'} /> )} /> diff --git a/src/widgets/footer-form/ui.tsx b/src/widgets/footer-form/ui.tsx index dd7739a..e5cf40f 100644 --- a/src/widgets/footer-form/ui.tsx +++ b/src/widgets/footer-form/ui.tsx @@ -1,7 +1,7 @@ 'use client'; import s from './styles.module.scss'; -import { Button, Input, Mark, TextArea } from '@shared/ui'; +import { Button, Input, Mark, PhoneInput, TextArea } from '@shared/ui'; import toast from 'react-hot-toast'; import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -9,11 +9,19 @@ import { z } from 'zod'; import man from '@public/images/footer-man.png'; import { sendFormFn } from '@shared/api/api.service'; +import { isValidPhoneNumber } from 'libphonenumber-js/min'; const FormSchema = z.object({ - name: z.string().min(3), - phone: z.string(), - message: z.string(), + name: z + .string() + .min(3, { message: 'Поле должно содержать не менее 3-х букв' }) + .regex(/^[A-Za-zА-Яа-яЁё]+(?:[ '-][A-Za-zА-Яа-яЁё]+)*$/, { + message: 'Поле содержит некорректные символы', + }), + phone: z.string().refine(isValidPhoneNumber, 'Некорректный номер телефона'), + message: z + .string() + .min(21, { message: 'Оставьте сообщение мин. 20 символов' }), }); type TForm = z.infer; @@ -28,6 +36,7 @@ export default function FooterForm() { handleSubmit, control, reset, + clearErrors, formState: { errors }, } = useForm({ mode: 'onSubmit', @@ -67,6 +76,12 @@ export default function FooterForm() { variant='ghost' placeholder={'Ваше имя'} fullWidth + onChange={(e) => { + clearErrors('name'); + field.onChange(e); + }} + error={errors && errors.name?.message} + errorTextColor={'#ff9191'} /> )} /> @@ -74,11 +89,17 @@ export default function FooterForm() { control={control} name={'phone'} render={({ field }) => ( - { + clearErrors('phone'); + field.onChange(e); + }} + error={errors && errors.phone?.message} + errorTextColor={'#ff9191'} /> )} /> @@ -94,6 +115,8 @@ export default function FooterForm() { id='story' name='story' rows={6} + error={errors && errors.message?.message} + errorTextColor={'#ff9191'} /> )} /> diff --git a/src/widgets/index.ts b/src/widgets/index.ts index 7fc6c26..af29f90 100644 --- a/src/widgets/index.ts +++ b/src/widgets/index.ts @@ -4,3 +4,4 @@ export { LicenseForm } from './license-form'; export { LicenseSlider } from './license-slider'; export { OfferForm } from './offer-form'; export { OfferRequestForm } from './offer-request'; +export { AdvancedPhoneInput } from './advanced-phone-input'; diff --git a/src/widgets/license-form/ui.tsx b/src/widgets/license-form/ui.tsx index fd7f033..9b4599f 100644 --- a/src/widgets/license-form/ui.tsx +++ b/src/widgets/license-form/ui.tsx @@ -1,7 +1,7 @@ 'use client'; import s from './styles.module.scss'; -import { Button, Input } from '@shared/ui'; +import { Button, Input, PhoneInput } from '@shared/ui'; import Image from 'next/image'; import toast from 'react-hot-toast'; @@ -10,11 +10,18 @@ import { z } from 'zod'; import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { sendFormFn } from '@shared/api/api.service'; +import { isValidPhoneNumber } from 'libphonenumber-js/min'; const FormSchema = z.object({ - name: z.string().min(3), - phone: z.string(), + name: z + .string() + .min(3, { message: 'Поле должно содержать не менее 3-х букв' }) + .regex(/^[A-Za-zА-Яа-яЁё]+(?:[ '-][A-Za-zА-Яа-яЁё]+)*$/, { + message: 'Поле содержит некорректные символы', + }), + phone: z.string().refine(isValidPhoneNumber, 'Некорректный номер телефона'), }); + type TForm = z.infer; const defaultValues = { @@ -27,6 +34,7 @@ export default function LicenseForm() { handleSubmit, control, reset, + clearErrors, formState: { errors }, } = useForm({ mode: 'onSubmit', @@ -77,14 +85,34 @@ export default function LicenseForm() { control={control} name={'name'} render={({ field }) => ( - + { + clearErrors('name'); + field.onChange(e); + }} + error={errors && errors.name?.message} + errorTextColor={'#ff9191'} + /> )} /> ( - + { + clearErrors('phone'); + field.onChange(e); + }} + error={errors && errors.phone?.message} + errorTextColor={'#ff9191'} + /> )} />