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() {
-
-
-
+
+
+
+
+
+
+
+
+
Политика конфиденциальности
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() {
Пожарная экспертиза
-
-
+
+
+
+
+
+
-
+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() {
точной стоимости работ
-
);
--
2.49.1
From 02415d92311cece706ee8805a675cdc675a9e035 Mon Sep 17 00:00:00 2001
From: RedrockJS