From a17afb39ba9ae094b4e049ce75716069e0bdc88e Mon Sep 17 00:00:00 2001 From: RedrockJS Date: Mon, 9 Jun 2025 14:11:36 +0300 Subject: [PATCH 01/16] feat: add forms logic --- eslint.config.mjs | 5 + package-lock.json | 48 +++++++++- package.json | 5 +- public/svg/email-icon.svg | 5 + .../advanced-phone-input.tsx | 27 +++--- src/shared/ui/input/input.tsx | 15 ++- src/shared/ui/text-area/text-area.tsx | 30 ++++-- .../home/ui/contacts/contacts.module.scss | 5 + src/views/home/ui/contacts/contacts.tsx | 4 +- src/views/home/ui/footer/footer.tsx | 20 +++- src/views/home/ui/home.tsx | 2 +- src/views/home/ui/license/license.module.scss | 6 +- src/views/home/ui/main/main.tsx | 20 +++- src/widgets/contacts-form/ui.tsx | 64 ++++++++++++- src/widgets/footer-form/ui.tsx | 92 ++++++++++++++++--- src/widgets/license-form/ui.tsx | 59 +++++++++++- src/widgets/offer-form/ui.tsx | 75 ++++++++++++--- src/widgets/offer-request/ui.tsx | 83 +++++++++++++++-- 18 files changed, 481 insertions(+), 84 deletions(-) create mode 100644 public/svg/email-icon.svg diff --git a/eslint.config.mjs b/eslint.config.mjs index 7f86eca..c765879 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,6 +11,11 @@ const compat = new FlatCompat({ const eslintConfig = [ ...compat.extends('next/core-web-vitals', 'next/typescript'), + { + rules: { + "@typescript-eslint/no-unused-vars": "off", + }, + }, ]; export default eslintConfig; diff --git a/package-lock.json b/package-lock.json index df7ece4..a98e970 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,14 @@ "name": "fire-exam", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^5.1.1", "next": "15.3.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.57.0", "react-hot-toast": "^2.5.2", - "swiper": "^11.2.8" + "swiper": "^11.2.8", + "zod": "^3.25.56" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -203,6 +206,18 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hookform/resolvers": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.1.1.tgz", + "integrity": "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -884,6 +899,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -4768,6 +4789,22 @@ "react": "^19.1.0" } }, + "node_modules/react-hook-form": { + "version": "7.57.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz", + "integrity": "sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-hot-toast": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", @@ -5998,6 +6035,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.56", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.56.tgz", + "integrity": "sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index b7dc14a..895cbf8 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,14 @@ "prepare": "husky" }, "dependencies": { + "@hookform/resolvers": "^5.1.1", "next": "15.3.2", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-hook-form": "^7.57.0", "react-hot-toast": "^2.5.2", - "swiper": "^11.2.8" + "swiper": "^11.2.8", + "zod": "^3.25.56" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/public/svg/email-icon.svg b/public/svg/email-icon.svg new file mode 100644 index 0000000..622dddd --- /dev/null +++ b/public/svg/email-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/shared/ui/advanced-phone-input/advanced-phone-input.tsx b/src/shared/ui/advanced-phone-input/advanced-phone-input.tsx index 179f8d1..1db9c5e 100644 --- a/src/shared/ui/advanced-phone-input/advanced-phone-input.tsx +++ b/src/shared/ui/advanced-phone-input/advanced-phone-input.tsx @@ -1,5 +1,5 @@ import s from './advancedPhoneInput.module.scss'; -import { DetailedHTMLProps, InputHTMLAttributes } from 'react'; +import { DetailedHTMLProps, forwardRef, InputHTMLAttributes, Ref } from 'react'; import { clsx } from 'clsx'; import { Button, Input } from '@shared/ui'; @@ -11,17 +11,20 @@ type AdvancedPhoneInputProps = { text: string; } & DetailedHTMLProps, HTMLInputElement>; -export default function AdvancedPhoneInput({ - containerClassName, - inputClassName, - buttonClassName, - onClick, - text, - ...props -}: AdvancedPhoneInputProps) { +const AdvancedPhoneInput = forwardRef(function AdvancedPhoneInput( + { + containerClassName, + inputClassName, + buttonClassName, + onClick, + text, + ...props + }: AdvancedPhoneInputProps, + ref: Ref, +) { return (
- +
); -} +}); + +export default AdvancedPhoneInput; diff --git a/src/shared/ui/input/input.tsx b/src/shared/ui/input/input.tsx index 8f1beb2..3f9d697 100644 --- a/src/shared/ui/input/input.tsx +++ b/src/shared/ui/input/input.tsx @@ -2,7 +2,7 @@ import s from './input.module.scss'; -import { DetailedHTMLProps, InputHTMLAttributes } from 'react'; +import { DetailedHTMLProps, forwardRef, InputHTMLAttributes, Ref } from 'react'; import { clsx } from 'clsx'; type InputProps = { @@ -12,15 +12,14 @@ type InputProps = { variant?: 'default' | 'ghost'; } & DetailedHTMLProps, HTMLInputElement>; -const Input = ({ - className, - fullWidth = false, - variant = 'default', - ...props -}: InputProps) => { +const Input = forwardRef(function Input( + { className, fullWidth = false, variant = 'default', ...props }: InputProps, + ref: Ref, +) { return ( ); -}; +}); export default Input; diff --git a/src/shared/ui/text-area/text-area.tsx b/src/shared/ui/text-area/text-area.tsx index ce89539..1513a2f 100644 --- a/src/shared/ui/text-area/text-area.tsx +++ b/src/shared/ui/text-area/text-area.tsx @@ -1,5 +1,11 @@ import s from './text-area.module.scss'; -import { DetailedHTMLProps, ReactNode, TextareaHTMLAttributes } from 'react'; +import { + DetailedHTMLProps, + forwardRef, + ReactNode, + Ref, + TextareaHTMLAttributes, +} from 'react'; import { clsx } from 'clsx'; type TextAreaProps = { @@ -12,21 +18,27 @@ type TextAreaProps = { HTMLTextAreaElement >; -export default function TextArea({ - className, - children, - variant = 'default', - fullWidth = false, - ...props -}: TextAreaProps) { +const TextArea = forwardRef(function TextArea( + { + className, + children, + variant = 'default', + fullWidth = false, + ...props + }: TextAreaProps, + ref: Ref, +) { return (
); -} +}); + +export default TextArea; diff --git a/src/views/home/ui/contacts/contacts.module.scss b/src/views/home/ui/contacts/contacts.module.scss index e25a581..17c2c02 100644 --- a/src/views/home/ui/contacts/contacts.module.scss +++ b/src/views/home/ui/contacts/contacts.module.scss @@ -236,6 +236,11 @@ margin-left: rem(42px); } + a:hover { + color: #163055; + text-decoration: underline; + } + .Icon { position: absolute; top: 0; diff --git a/src/views/home/ui/contacts/contacts.tsx b/src/views/home/ui/contacts/contacts.tsx index 3bbc273..d244574 100644 --- a/src/views/home/ui/contacts/contacts.tsx +++ b/src/views/home/ui/contacts/contacts.tsx @@ -53,11 +53,11 @@ export default function Contacts() {

- +7 (988) 400 93 93 + +7 (988) 400-93-93

- office@firecheck.ru + spo-71@yandex.ru

diff --git a/src/views/home/ui/footer/footer.tsx b/src/views/home/ui/footer/footer.tsx index d4de59d..f10b6ae 100644 --- a/src/views/home/ui/footer/footer.tsx +++ b/src/views/home/ui/footer/footer.tsx @@ -10,9 +10,23 @@ export default function Footer() {
- - - + + + + + + + + +

Политика конфиденциальности

diff --git a/src/views/home/ui/home.tsx b/src/views/home/ui/home.tsx index f5ac30d..71b993e 100644 --- a/src/views/home/ui/home.tsx +++ b/src/views/home/ui/home.tsx @@ -10,7 +10,7 @@ export default function HomePage() { <>
- + {/**/}
diff --git a/src/views/home/ui/license/license.module.scss b/src/views/home/ui/license/license.module.scss index 3f7eb35..ebfd1d8 100644 --- a/src/views/home/ui/license/license.module.scss +++ b/src/views/home/ui/license/license.module.scss @@ -5,17 +5,17 @@ @include iftablet { margin: 0 auto; width: rem(712px); - padding: 0 0 rem(40px); + padding: rem(40px) 0 rem(40px); } @include iflaptop { width: rem(930px); - padding: 0 0 rem(60px); + padding: rem(60px) 0 rem(60px); } @include ifdesktop { width: rem(1340px); - padding: 0 0 rem(160px); + padding: rem(160px) 0 rem(160px); } .Header { diff --git a/src/views/home/ui/main/main.tsx b/src/views/home/ui/main/main.tsx index 7127e71..2079ab9 100644 --- a/src/views/home/ui/main/main.tsx +++ b/src/views/home/ui/main/main.tsx @@ -6,7 +6,7 @@ import Image from 'next/image'; import bgStart from '@public/images/bg-start-desktop.jpg'; import waIcon from '@public/svg/whatsapp.svg'; -import tgIcon from '@public/svg/telegram.svg'; +import emailIcon from '@public/svg/email-icon.svg'; import callBtn from '@public/svg/phone-calling.svg'; export default function Main() { @@ -26,8 +26,20 @@ export default function Main() {
Пожарная экспертиза
- whatsapp - telegram + + email + + + whatsapp +
-

+7 988 400 93 93

+

+7 (988) 400-93-93

diff --git a/src/widgets/contacts-form/ui.tsx b/src/widgets/contacts-form/ui.tsx index d5d9db8..24a168f 100644 --- a/src/widgets/contacts-form/ui.tsx +++ b/src/widgets/contacts-form/ui.tsx @@ -4,11 +4,48 @@ import s from './styles.module.scss'; import { Button, Input } from '@shared/ui'; import Image from 'next/image'; import toast from 'react-hot-toast'; +import { Controller, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; import bgForm from '@public/images/bg-form.jpg'; +const FormSchema = z.object({ + name: z.string().min(3), + phone: z.string(), +}); +type TForm = z.infer; + +const defaultValues = { + name: '', + phone: '', +}; + export default function ContactsForm() { - const notify = () => toast.success('Заявка на консультацию принята'); + const { + handleSubmit, + control, + reset, + formState: { errors }, + clearErrors, + } = useForm({ + mode: 'onSubmit', + reValidateMode: 'onBlur', + resolver: zodResolver(FormSchema), + defaultValues, + }); + + const onSubmit = async (data: TForm) => { + try { + console.log('Form', data); + toast.success('Заявка на консультацию принята'); + reset(defaultValues); + } catch (e) { + toast.error('Ошибка при отправке заявки...', { + duration: 3000, + }); + } + }; return (
@@ -28,10 +65,27 @@ export default function ContactsForm() { точной стоимости работ

-
- - -
diff --git a/src/widgets/footer-form/ui.tsx b/src/widgets/footer-form/ui.tsx index b8ae3f0..4b47241 100644 --- a/src/widgets/footer-form/ui.tsx +++ b/src/widgets/footer-form/ui.tsx @@ -3,26 +3,94 @@ import s from './styles.module.scss'; import { Button, Input, Mark, TextArea } from '@shared/ui'; import toast from 'react-hot-toast'; +import { Controller, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; + +const FormSchema = z.object({ + name: z.string().min(3), + phone: z.string(), + message: z.string(), +}); +type TForm = z.infer; + +const defaultValues = { + name: '', + phone: '', + message: '', +}; export default function FooterForm() { - const notify = () => toast.success('Заявка на консультацию принята'); + const { + handleSubmit, + control, + reset, + formState: { errors }, + clearErrors, + } = useForm({ + mode: 'onSubmit', + reValidateMode: 'onBlur', + resolver: zodResolver(FormSchema), + defaultValues, + }); + + const onSubmit = async (data: TForm) => { + try { + console.log('Form', data); + toast.success('Заявка на консультацию принята'); + reset(defaultValues); + } catch (e) { + toast.error('Ошибка при отправке заявки...', { + duration: 3000, + }); + } + }; return ( -
+

Давайте обсудим ваши задачи

- - - + {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'} + /> )} /> - ); -} diff --git a/src/entities/home/callback-order/index.ts b/src/entities/home/callback-order/index.ts new file mode 100644 index 0000000..84a1b20 --- /dev/null +++ b/src/entities/home/callback-order/index.ts @@ -0,0 +1 @@ +export { CallbackOrder } from './ui'; diff --git a/src/entities/home/callback-order/styles.module.scss b/src/entities/home/callback-order/styles.module.scss new file mode 100644 index 0000000..5435b1a --- /dev/null +++ b/src/entities/home/callback-order/styles.module.scss @@ -0,0 +1,35 @@ +.Container { + display: none; + @include iftablet { + display: block; + height: min-content; + } +} + +.Btn { + @include iflaptop { + display: flex; + flex-direction: row; + justify-items: center; + align-items: center; + gap: rem(16px); + height: rem(40px); + padding: rem(20px)!important; + } + + @include ifdesktop { + gap: rem(16px); + height: rem(48px); + padding: rem(24px); + } + + img { + width: rem(30px); + height: rem(30px); + + @include ifdesktop { + width: rem(40px); + height: rem(40px); + } + } +} \ No newline at end of file diff --git a/src/entities/home/callback-order/ui.tsx b/src/entities/home/callback-order/ui.tsx new file mode 100644 index 0000000..aed940a --- /dev/null +++ b/src/entities/home/callback-order/ui.tsx @@ -0,0 +1,25 @@ +'use client'; + +import s from './styles.module.scss'; +import Image from 'next/image'; +import { Button } from '@shared/ui'; +import { useModal } from '@core/providers/modal-provider'; +import { ConsultationModal } from '@/entities/home/consultation-modal'; + +import callBtn from '@public/svg/phone-calling.svg'; + +function CallbackOrder() { + const modal = useModal(); + const openModal = () => modal.showModal(); + + return ( +
+ +
+ ); +} + +export { CallbackOrder }; diff --git a/src/entities/home/consultation-modal/index.ts b/src/entities/home/consultation-modal/index.ts new file mode 100644 index 0000000..e9f2654 --- /dev/null +++ b/src/entities/home/consultation-modal/index.ts @@ -0,0 +1 @@ +export { ConsultationModal } from './ui'; diff --git a/src/entities/home/consultation-modal/styles.module.scss b/src/entities/home/consultation-modal/styles.module.scss new file mode 100644 index 0000000..141e086 --- /dev/null +++ b/src/entities/home/consultation-modal/styles.module.scss @@ -0,0 +1,29 @@ +.Form { + display: flex; + flex-direction: column; + gap: rem(16px); + + @include iflaptop { + gap: rem(20px); + } +} + +.Title { + font-family: $font-open-sans; + font-weight: $font-medium; + font-size: rem(20px); + line-height: 130%; + color: $color-text; + + @include ifdesktop { + font-size: rem(24px); + } +} + +.Description { + font-family: $font-open-sans; + font-weight: $font-regular; + font-size: rem(16px); + line-height: 100%; + color: $color-text; +} \ No newline at end of file diff --git a/src/entities/home/consultation-modal/ui.tsx b/src/entities/home/consultation-modal/ui.tsx new file mode 100644 index 0000000..9a26d42 --- /dev/null +++ b/src/entities/home/consultation-modal/ui.tsx @@ -0,0 +1,137 @@ +import s from './styles.module.scss'; +import { Button, Input } from '@/shared/ui'; +import { PhoneInput, TextArea } from '@shared/ui'; +import { z } from 'zod'; +import { isValidPhoneNumber } from 'libphonenumber-js/min'; +import { Controller, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { sendFormFn } from '@shared/api/api.service'; +import toast from 'react-hot-toast'; +import { ModalContent } from '@shared/ui/modal/modal-content'; +import { useModal } from '@core/providers/modal-provider'; + +const FormSchema = z.object({ + 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; + +const defaultValues = { + name: '', + phone: '', + message: '', +}; + +type ConsultationModalProps = { + className?: string; +}; + +function ConsultationModal({}: ConsultationModalProps) { + const { + handleSubmit, + control, + reset, + clearErrors, + formState: { errors }, + } = useForm({ + resolver: zodResolver(FormSchema), + defaultValues, + }); + + const modal = useModal(); + + const onSubmit = async (data: TForm) => { + const payload = { + ...data, + form: 'consultation-modal-form', + }; + + try { + await sendFormFn(payload); + toast.success('Заявка на консультацию принята'); + } catch (e) { + toast.error('Ошибка при отправке заявки...', { + duration: 3000, + }); + } finally { + modal.hideModal(); + reset(defaultValues); + } + }; + + return ( + + +

+ Мы подскажем, как решить ваши вопросы по пожарной безопасности. +

+ ( + { + clearErrors('name'); + field.onChange(e); + }} + error={errors && errors.name?.message} + /> + )} + /> + ( + { + clearErrors('phone'); + field.onChange(e); + }} + error={errors && errors.phone?.message} + /> + )} + /> +

Кратко опишите интересующий Вас вопрос

+ ( +