Merge pull request #13 from redrockjs/dev

Dev
This commit was merged in pull request #13.
This commit is contained in:
Valeriy G.
2025-06-18 15:49:49 +03:00
committed by GitHub
96 changed files with 2904 additions and 229 deletions

View File

@@ -1,4 +1,5 @@
npx lint-staged
echo "Running test build..."
echo ""
echo "🚀 Running test build..."
npm run build

View File

@@ -1,2 +1,3 @@
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

View File

@@ -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;

117
package-lock.json generated
View File

@@ -8,15 +8,24 @@
"name": "fire-exam",
"version": "0.1.0",
"dependencies": {
"@hookform/resolvers": "^5.1.1",
"@maskito/core": "^3.9.0",
"@maskito/phone": "^3.9.0",
"@maskito/react": "^3.9.0",
"libphonenumber-js": "^1.12.9",
"next": "15.3.2",
"nodemailer": "^7.0.3",
"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",
"@types/node": "^20",
"@types/nodemailer": "^6.4.17",
"@types/react": "^19",
"@types/react-dom": "^19",
"clsx": "^2.1.1",
@@ -203,6 +212,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",
@@ -665,6 +686,44 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@maskito/core": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@maskito/core/-/core-3.9.0.tgz",
"integrity": "sha512-OgzzgzJTXFZH79mqyHFVUZ5/bUhSW147+JzYVX+DdmQ5zc+mxmFQqsUS5ffVxd2C7/bnEmC7+savYbcae2IhBw==",
"license": "Apache-2.0"
},
"node_modules/@maskito/kit": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-3.9.0.tgz",
"integrity": "sha512-CD7TQ7WUMtZ8jkhOsislbqht1gMuNHVQsLJG9tXcGvZbegkgJ6wdkggkol1y1/0F5eh/fT+RzzKD9dVjSQon2g==",
"license": "Apache-2.0",
"peer": true,
"peerDependencies": {
"@maskito/core": "^3.9.0"
}
},
"node_modules/@maskito/phone": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@maskito/phone/-/phone-3.9.0.tgz",
"integrity": "sha512-EUCOmOscoQM+vnJwOAiBXVpZVVYkHc7rhnqLqfkslXsZCg5VLNNpzAb8SuFQMxYJYM2NnMawErvL7CjOdVDmvQ==",
"license": "Apache-2.0",
"peerDependencies": {
"@maskito/core": "^3.9.0",
"@maskito/kit": "^3.9.0",
"libphonenumber-js": ">=1.0.0"
}
},
"node_modules/@maskito/react": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@maskito/react/-/react-3.9.0.tgz",
"integrity": "sha512-qYGncdyaPbi50rDkg0gwh64DHPBCT6YMdkWsMbqG57bVjE1S0X9zbZQICieRQXGVkRjlgJenoMu3b5svdH4ysQ==",
"license": "Apache-2.0",
"peerDependencies": {
"@maskito/core": "^3.9.0",
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.10.tgz",
@@ -884,6 +943,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",
@@ -941,6 +1006,16 @@
"undici-types": "~6.19.2"
}
},
"node_modules/@types/nodemailer": {
"version": "6.4.17",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
"integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/react": {
"version": "19.1.6",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz",
@@ -3992,6 +4067,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/libphonenumber-js": {
"version": "1.12.9",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.9.tgz",
"integrity": "sha512-VWwAdNeJgN7jFOD+wN4qx83DTPMVPPAUyx9/TUkBXKLiNkuWWk6anV0439tgdtwaJDrEdqkvdN22iA6J4bUCZg==",
"license": "MIT"
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -4351,6 +4432,15 @@
}
}
},
"node_modules/nodemailer": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz",
"integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -4768,6 +4858,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 +6104,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"
}
}
}
}

View File

@@ -11,15 +11,24 @@
"prepare": "husky"
},
"dependencies": {
"@hookform/resolvers": "^5.1.1",
"@maskito/core": "^3.9.0",
"@maskito/phone": "^3.9.0",
"@maskito/react": "^3.9.0",
"libphonenumber-js": "^1.12.9",
"next": "15.3.2",
"nodemailer": "^7.0.3",
"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",
"@types/node": "^20",
"@types/nodemailer": "^6.4.17",
"@types/react": "^19",
"@types/react-dom": "^19",
"clsx": "^2.1.1",

BIN
public/images/dtr-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

5
public/svg/checkbox.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg width="10" height="8" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M9.86467 0.994471C9.94004 1.07208 9.94004 1.19555 9.86467 1.27315L3.47494 7.85228C3.39639 7.93315 3.26655 7.93315 3.188 7.85228L0.13533 4.70913C0.0599584 4.63152 0.0599584 4.50805 0.13533 4.43044L0.957702 3.5837C1.03625 3.50282 1.16609 3.50282 1.24464 3.5837L3.33147 5.73237L8.75535 0.147724C8.8339 0.0668455 8.96375 0.0668453 9.0423 0.147724L9.86467 0.994471Z"
fill="white" />
</svg>

After

Width:  |  Height:  |  Size: 537 B

View File

@@ -0,0 +1,5 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="30" cy="30" r="30" fill="#D9D9D9"/>
<path d="M14.1666 23.3751L26.825 32.8687C29.0028 34.5021 31.9971 34.5021 34.175 32.8687L46.8333 23.375" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.125 23.736C12.125 22.253 12.929 20.8867 14.2253 20.1665L28.5169 12.2267C29.7503 11.5415 31.2497 11.5415 32.4831 12.2267L46.7747 20.1665C48.071 20.8867 48.875 22.253 48.875 23.736V39.7083C48.875 41.9636 47.0469 43.7917 44.7917 43.7917H16.2083C13.9532 43.7917 12.125 41.9636 12.125 39.7083V23.736Z" stroke="black" stroke-width="3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 715 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -0,0 +1,9 @@
export async function GET(request: Request) {
return new Response('Heartbeat is OK!', {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
},
});
}

View File

@@ -0,0 +1,66 @@
import nodemailer from 'nodemailer';
import { TBaseForm } from '@shared/api/api.types';
import { CORE } from '@shared/config/core';
async function sendMail(data: TBaseForm) {
const { name, phone, message, form } = data;
const formattedBody = `
<html lang="ru-RU">
<body>
<p>Сообщение с сайта "Пожарная экспертиза"</p>
<p>Форма отправки: ${form}</p>
<p>Имя отправителя: ${name ?? 'не указано'}</p>
<p>Номер телефона: ${phone}</p>
<p>Сообщение: ${message ?? 'отсутствует'}</p>
</body>
</html>
`;
const transporter = nodemailer.createTransport({
service: 'yandex',
auth: {
user: CORE.MAIL_USER,
pass: CORE.MAIL_PASS,
},
});
return await transporter.sendMail({
from: CORE.MAIL_FROM,
to: CORE.MAIL_TO,
subject: 'Заявка с сайта FireExams',
html: formattedBody,
});
}
export async function POST(request: Request) {
try {
const payload = await request.json();
if (payload.secure !== CORE.MAIL_SECURE_KEY) {
await Promise.reject('Request failure!');
}
const sendResult = await sendMail({ ...payload });
const data = { message: 'Form accepted' };
const headers = new Headers({
'Content-Type': 'application/json',
});
const options = {
status: 200,
statusText: 'OK',
headers: headers,
};
if (sendResult?.messageId) {
return new Response(JSON.stringify(data), options);
} else {
await Promise.reject('Sending request failure!');
}
} catch (error) {
return new Response(`Api error: ${error}`, {
status: 400,
});
}
}

9
src/app/cookie/page.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { Cookie } from '@views/cookie';
export default function CookiePage() {
return (
<main>
<Cookie />
</main>
);
}

View File

@@ -3,6 +3,8 @@ import { Open_Sans } from 'next/font/google';
import '@core/styles/reset.scss';
import '@core/styles/globals.scss';
import { Toaster } from 'react-hot-toast';
import { ModalProvider } from '@core/providers/modal-provider';
import { CookieNotice } from '@/widgets';
const openSans = Open_Sans({
subsets: ['cyrillic'],
@@ -34,8 +36,9 @@ export default function RootLayout({
return (
<html lang='en'>
<body className={`${openSans.variable}`}>
{children}
<ModalProvider>{children}</ModalProvider>
<Toaster />
<CookieNotice />
</body>
</html>
);

View File

@@ -1,9 +1,9 @@
import { HomePage } from '@views/home';
import { Home } from '@views/home';
export default function Home() {
export default function HomePage() {
return (
<main>
<HomePage />
<Home />
</main>
);
}

View File

@@ -0,0 +1,9 @@
import { PrivacyPolicy } from '@views/privacy-policy';
export default function PrivacyPolicyPage() {
return (
<main>
<PrivacyPolicy />
</main>
);
}

View File

@@ -0,0 +1,9 @@
import { UserAgreement } from '@views/user-agreement';
export default function UserAgreementPage() {
return (
<main>
<UserAgreement />
</main>
);
}

View File

@@ -0,0 +1,5 @@
const COMPANY = '«ООО ДИТРАСО»';
const WEB = 'https://www.fire-expert.ru/';
const EMAIL = 'spo-71@yandex.ru';
export { COMPANY, WEB, EMAIL };

View File

@@ -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<ReactNode>(null);
const showModal = useCallback((content: ReactNode) => {
setModalContent(content);
}, []);
const hideModal = useCallback(() => {
setModalContent(null);
}, []);
return (
<ModalContext.Provider value={{ hideModal, showModal }}>
{children}
{/* Ваш Modal компонент здесь */}
<Modal isOpen={modalContent !== null} onClose={hideModal}>
{modalContent}
</Modal>
</ModalContext.Provider>
);
};
export { useModal, ModalProvider };

View File

@@ -1,4 +1,3 @@
//frontend breakpoint
$mobile: 360px;
$tablet: 768px;
@@ -19,10 +18,12 @@ $font-semi-bold: 600;
$color-white: #FFFFFF;
$color-black: #000000;
$color-orange: #E96526;
$color-orange-hover: #ea4b05;
$color-lightgray: #E4E1E1;
$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;

View File

@@ -1 +0,0 @@
export { default as ConsultationOrder } from './ui';

View File

@@ -1,13 +0,0 @@
'use client';
import { Button } from '@shared/ui';
import toast from 'react-hot-toast';
export default function ConsultationOrder() {
const notify = () => toast.success('Заявка на консультацию принята');
return (
<Button variant='orange' onClick={notify}>
Получить консультацию
</Button>
);
}

View File

@@ -0,0 +1 @@
export { CallbackOrder } from './ui';

View File

@@ -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);
}
}
}

View File

@@ -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(<ConsultationModal />);
return (
<div className={s.Container}>
<Button className={s.Btn} onClick={openModal}>
<Image src={callBtn} alt='Call' />
Обратный звонок
</Button>
</div>
);
}
export { CallbackOrder };

View File

@@ -0,0 +1 @@
export { ConsultationModal } from './ui';

View File

@@ -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;
}

View File

@@ -0,0 +1,138 @@
import s from './styles.module.scss';
import { Button, Input } from '@/shared/ui';
import { Checkbox, 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<typeof FormSchema>;
const defaultValues = {
name: '',
phone: '',
message: '',
};
type ConsultationModalProps = {
className?: string;
};
function ConsultationModal({}: ConsultationModalProps) {
const {
handleSubmit,
control,
reset,
clearErrors,
formState: { errors },
} = useForm<TForm>({
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 (
<ModalContent closeByClickOutside>
<form
className={s.Form}
id='consultation-modal-form'
onSubmit={handleSubmit(onSubmit)}
>
<h2 className={s.Title}>
Мы подскажем, как решить ваши вопросы по пожарной безопасности.
</h2>
<Controller
control={control}
name={'name'}
render={({ field }) => (
<Input
{...field}
placeholder={'Ваше имя'}
fullWidth
onChange={(e) => {
clearErrors('name');
field.onChange(e);
}}
error={errors && errors.name?.message}
/>
)}
/>
<Controller
control={control}
name={'phone'}
render={({ field }) => (
<PhoneInput
{...field}
placeholder={'+7 (999) 123-45-67'}
fullWidth
onChange={(e) => {
clearErrors('phone');
field.onChange(e);
}}
error={errors && errors.phone?.message}
/>
)}
/>
<p className={s.Description}>Кратко опишите интересующий Вас вопрос</p>
<Controller
control={control}
name={'message'}
render={({ field }) => (
<TextArea
{...field}
placeholder={''}
fullWidth
id='story'
name='story'
rows={6}
error={errors && errors.message?.message}
errorTextColor={'#ff9191'}
/>
)}
/>
<Checkbox label={'asdasdasdad'} />
<Button className={s.SendBtn} variant='orange'>
Отправить
</Button>
</form>
</ModalContent>
);
}
export { ConsultationModal };

View File

@@ -0,0 +1 @@
export { ConsultationOrder } from './ui';

View File

@@ -0,0 +1,18 @@
'use client';
import { Button } from '@shared/ui';
import { useModal } from '@core/providers/modal-provider';
import { ConsultationModal } from '@/entities/home/consultation-modal';
function ConsultationOrder() {
const modal = useModal();
const openModal = () => modal.showModal(<ConsultationModal />);
return (
<Button variant='orange' onClick={openModal}>
Получить консультацию
</Button>
);
}
export { ConsultationOrder };

View File

@@ -0,0 +1,35 @@
import { API_ROUTES } from '@shared/config/routes';
import { TBaseForm } from '@shared/api/api.types';
import { CORE } from '@shared/config/core';
type TRequest = TBaseForm;
const sendFormFn = async ({ ...props }: TRequest) => {
try {
const response = await fetch('/api' + API_ROUTES.SEND_FORM, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...props,
secure: CORE.MAIL_SECURE_KEY,
}),
});
if (response.ok) {
return response;
} else {
if (response.status === 400) throw new Error(`400 Bad request`);
if (response.status === 401) throw new Error(`401 Unauthorized`);
if (response.status === 500) throw new Error('500 Internal server error');
throw new Error(`${response.status} - Network response failure`);
}
} catch (e) {
console.error(e);
}
};
export { sendFormFn };

View File

@@ -0,0 +1,6 @@
export type TBaseForm = {
form: string;
name?: string;
phone: string;
message?: string;
};

View File

@@ -0,0 +1,7 @@
export const CORE = {
MAIL_USER: process.env.MAIL_USER,
MAIL_PASS: process.env.MAIL_PASS,
MAIL_FROM: process.env.MAIL_FROM,
MAIL_TO: process.env.MAIL_TO,
MAIL_SECURE_KEY: process.env.NEXT_PUBLIC_MAIL_SECURE_KEY,
} as const;

View File

@@ -0,0 +1,4 @@
export const API_ROUTES = {
SEND_FORM: '/sendform',
HEARTBEAT: '/heartbeat',
} as const;

View File

@@ -0,0 +1 @@
export { useClickOutside } from './useClickOutside';

View File

@@ -0,0 +1,28 @@
import { RefObject, useEffect } from 'react';
export function useClickOutside(
ref: RefObject<HTMLDivElement | null>,
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]);
}

View File

@@ -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 };

View File

@@ -0,0 +1 @@
export { detectIOS } from './detectIOS';

2
src/shared/lib/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { useClickOutside } from './clickOutside';
export { detectIOS } from './detectIOS';

View File

@@ -1,34 +0,0 @@
import s from './advancedPhoneInput.module.scss';
import { DetailedHTMLProps, InputHTMLAttributes } 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<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
export default function AdvancedPhoneInput({
containerClassName,
inputClassName,
buttonClassName,
onClick,
text,
...props
}: AdvancedPhoneInputProps) {
return (
<div className={clsx(containerClassName, s.Container)}>
<Input {...props} className={clsx(inputClassName, s.Phone)} />
<Button
className={clsx(buttonClassName, s.Button)}
onClick={onClick}
variant='orange'
>
{text}
</Button>
</div>
);
}

View File

@@ -34,11 +34,11 @@ export default function Button({
return (
<button
className={clsx(
className,
s.Button,
disabled && s.Button_disabled,
s['Button_' + variant],
fullWidth && s.Button_fullWidth,
className,
)}
onClick={onClick}
disabled={disabled}

View File

@@ -0,0 +1,72 @@
.Container {
display: flex;
align-items: center;
width: fit-content;
cursor: pointer;
}
.Input {
border: none;
position: absolute;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: none;
opacity: 0;
}
.Checkbox {
width: 1rem;
height: 1rem;
flex-shrink: 0;
border-radius: 0.125rem;
border: 1px solid #e3e4e5;
}
.CheckboxError {
composes: Checkbox;
border: 1px solid #ff6969 !important;
}
.Input:hover + .Checkbox {
background: #f0faff;
border: 1px solid #94dfff;
}
.Input:focus + .Checkbox {
background: #f0faff;
border: 1px solid #94dfff;
}
.Input:checked + .Checkbox {
background: url(/svg/checkbox.svg) center no-repeat, #00affa;
}
.Input:checked:hover + .Checkbox {
background: url(/svg/checkbox.svg) center no-repeat, #00a4eb;
}
.Input:disabled + .Checkbox {
background: #f5f6f7;
border: 1px solid #e3e4e5;
}
.Input:disabled:checked + .Checkbox {
background: url(/svg/checkbox.svg) center no-repeat, #e3e4e5;
border: 1px solid transparent;
}
.Label {
font-family: $font-open-sans;
font-weight: $font-regular;
font-size: rem(12px);
line-height: 100%;
color: #333333;
margin-left: 0.5rem;
}
.LabelError {
composes: Label;
color: #ff6969 !important;
}

View File

@@ -0,0 +1,20 @@
import s from './checkbox.module.scss';
import { DetailedHTMLProps, InputHTMLAttributes } from 'react';
type CheckboxProps = {
className?: string;
label?: string;
error?: string | boolean;
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
export default function Checkbox({ ...props }: CheckboxProps) {
const { label, error } = props;
return (
<div className={s.Container}>
<input {...props} className={s.Input} type='checkbox' />
<div className={!error ? s.Checkbox : s.CheckboxError} />
<div className={!error ? s.Label : s.LabelError}>{label}</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export { default as Checkbox } from './checkbox';

View File

@@ -1,5 +1,7 @@
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';
export { Modal, ModalContent } from './modal';
export { Checkbox } from './checkbox';

View File

@@ -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);
}
@@ -41,7 +46,11 @@
transition: border-color ease .5s;
}
&_fullWidth{
&_error {
border-color: $color-error;
}
&_fullWidth {
width: 100%;
}
@@ -64,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;
}

View File

@@ -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 = {
@@ -10,25 +10,41 @@ type InputProps = {
className?: string;
fullWidth?: boolean;
variant?: 'default' | 'ghost';
error?: string | boolean;
errorTextColor?: string;
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
const Input = ({
className,
fullWidth = false,
variant = 'default',
...props
}: InputProps) => {
const Input = forwardRef(function Input(
{
className,
fullWidth = false,
variant = 'default',
error = false,
errorTextColor,
...props
}: InputProps,
ref: Ref<HTMLInputElement>,
) {
return (
<input
{...props}
className={clsx(
s.Input,
s['Input_' + variant],
fullWidth && s.Input_fullWidth,
className,
<div className={s.Container}>
<input
{...props}
ref={ref}
className={clsx(
s.Input,
s['Input_' + variant],
fullWidth && s.Input_fullWidth,
error && s.Input_error,
className,
)}
/>
{error && (
<span className={s.Error} style={{ color: errorTextColor }}>
{error}
</span>
)}
/>
</div>
);
};
});
export default Input;

View File

@@ -0,0 +1,23 @@
import { SVGProps } from 'react';
const CloseIcon = ({ ...props }: SVGProps<SVGSVGElement>) => (
<svg
{...props}
width='17'
height='17'
viewBox='0 0 17 17'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M2.5 15L15.5 2M2.5 2L15.5 15'
stroke='#053635'
strokeOpacity='0.3'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
export { CloseIcon };

View File

@@ -0,0 +1,2 @@
export { Modal } from './modal';
export { ModalContent } from './modal-content';

View File

@@ -0,0 +1,79 @@
.ModalContent {
position: absolute;
width: rem(360px);
padding: rem(40px) rem(20px) rem(20px);
border-radius: rem(20px);
background: white;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
overflow: auto;
animation: fadeIn ease 0.3s;
@include iftablet {
top: unset;
width: rem(540px);
padding: rem(40px) rem(24px) rem(24px);
}
@include iflaptop {
padding: rem(40px) rem(30px) rem(30px);
}
@include ifdesktop {
width: rem(640px);
padding: rem(60px) rem(40px) rem(40px);
}
&_open {
animation: fadeOut ease 0.3s;
}
&_isIOS {
height: 100vh !important;
@include iftablet {
height: 100%;
}
}
}
.Inner {
position: relative;
overflow: hidden;
}
.CloseBtn {
position: absolute;
top: rem(20px);
right: rem(20px);
width: rem(24px);
height: rem(24px);
cursor: pointer;
transform: rotate(0deg);
transition: transform 0.3s;
&:hover,
&:active {
transform: rotate(90deg);
transition: transform 0.3s;
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

View File

@@ -0,0 +1,49 @@
'use client';
import s from './modal-content.module.scss';
import { MouseEvent, ReactNode, useRef } from 'react';
import clsx from 'clsx';
import { useModal } from '@core/providers/modal-provider';
import { detectIOS, useClickOutside } from '@shared/lib';
import { CloseIcon } from '@shared/ui/modal/close-icon';
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<HTMLDivElement | null>(null);
const handleClickOutside = () => closeByClickOutside && hideModal();
useClickOutside(modalRef, handleClickOutside);
const isIOS = detectIOS();
const disableClick = (e: MouseEvent<HTMLDivElement>) => e.stopPropagation();
return (
<div
className={clsx(className, s.ModalContent, isIOS && s.ModalContent_isIOS)}
ref={modalRef}
onClick={disableClick}
>
<CloseIcon className={s.CloseBtn} onClick={hideModal} />
<div className={clsx(s.Inner, innerClassName)}>{children}</div>
</div>
);
}
export { ModalContent };

View File

@@ -0,0 +1,54 @@
.ModalBackdrop {
z-index: 1000;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.5);
padding: 0;
animation: fadeIn ease 0.3s;
@include iftablet {
position: fixed;
height: 100vh;
padding: 0 rem(20px);
}
@include iflaptop {
padding: 0 rem(48px);
}
&_open {
animation: fadeOut ease 0.3s;
}
&_isIOS {
height: 150vh !important;
@include iftablet {
height: unset;
}
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

View File

@@ -0,0 +1,44 @@
'use client';
import s from './modal.module.scss';
import { ReactNode, useEffect } from 'react';
import clsx from 'clsx';
import { detectIOS } from '@shared/lib';
import { createPortal } from 'react-dom';
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 createPortal(
<div
className={clsx(
s.ModalBackdrop,
isOpen && s.ModalBackdrop__open,
isIOS && s.ModalBackdrop_isIOS,
)}
>
{children}
</div>,
document.body,
);
};
export { Modal };

View File

@@ -0,0 +1 @@
export { default as PhoneInput } from './phone-input';

View File

@@ -0,0 +1,23 @@
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<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
export default function PhoneInput({ ...props }: PhoneInput) {
const options = maskitoPhoneOptionsGenerator({
countryIsoCode: 'RU',
metadata,
});
const maskedInputRef = useMaskito({ options });
return <Input {...props} ref={maskedInputRef} type='tel' />;
}

View File

@@ -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%;
}
}
@@ -14,13 +15,13 @@
.Area {
background: $color-white;
border: 1px solid $color-darkgray;
border-radius: rem(20px);
padding: rem(10px) rem(24px);
border-radius: rem(16px);
padding: rem(10px) rem(10px);
transition: border ease .5s;
font-family: $font-open-sans;
font-weight: $font-regular;
font-size: rem(18px);
font-size: rem(16px);
line-height: 100%;
color: $color-text;
@@ -29,16 +30,18 @@
width: 100%;
flex-grow: 1;
@include iftablet{
@include iftablet {
font-size: rem(18px);
border-radius: rem(20px);
padding: rem(10px) rem(24px);
}
@include iflaptop{
@include iflaptop {
font-size: rem(20px);
padding: rem(10px) rem(20px);
}
@include ifdesktop{
@include ifdesktop {
font-size: rem(24px);
}
@@ -47,27 +50,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 +122,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;
}

View File

@@ -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 = {
@@ -7,26 +13,46 @@ type TextAreaProps = {
children?: ReactNode;
variant?: 'default' | 'ghost';
fullWidth?: boolean;
error?: string | boolean;
errorTextColor?: string;
} & DetailedHTMLProps<
TextareaHTMLAttributes<HTMLTextAreaElement>,
HTMLTextAreaElement
>;
export default function TextArea({
className,
children,
variant = 'default',
fullWidth = false,
...props
}: TextAreaProps) {
const TextArea = forwardRef(function TextArea(
{
className,
children,
variant = 'default',
fullWidth = false,
error = false,
errorTextColor,
...props
}: TextAreaProps,
ref: Ref<HTMLTextAreaElement>,
) {
return (
<div className={clsx(s.Container, fullWidth && s.Container_fullWidth)}>
<textarea
{...props}
className={clsx(className, s.Area, s['Area_' + variant])}
ref={ref}
className={clsx(
className,
s.Area,
s['Area_' + variant],
error && s.Area_error,
)}
>
{children}
</textarea>
{error && (
<span className={s.Error} style={{ color: errorTextColor }}>
{error}
</span>
)}
</div>
);
}
});
export default TextArea;

View File

@@ -0,0 +1 @@
export { default as Cookie } from './ui';

View File

@@ -0,0 +1,79 @@
.Cookie {
position: relative;
margin: rem(60px) auto rem(20px);
width: rem(360px);
background: #EEE;
padding: rem(20px);
border-radius: rem(28px);
@include iftablet{
width: rem(600px);
margin: rem(40px) auto rem(20px);
padding: rem(20px);
}
@include iflaptop{
width: rem(800px);
margin: rem(60px) auto rem(20px);
padding: rem(60px);
}
@include ifdesktop{
width: rem(1200px);
margin: rem(60px) auto rem(20px);
padding: rem(60px);
}
h2 {
font-family: $font-open-sans;
font-weight: $font-regular;
font-size: rem(20px);
line-height: 130%;
color: $color-text;
@include iftablet{
font-size: rem(24px);
}
}
p {
font-family: $font-open-sans;
font-weight: $font-regular;
font-size: rem(14px);
line-height: 130%;
color: $color-text;
margin: 8px 0;
@include iftablet{
font-size: rem(16px);
}
}
a {
color: $color-orange;
}
}
.FloatBtn {
position: fixed;
top: rem(10px);
right: 50%;
transform: translateX(50%);
@include iftablet{
position: absolute;
top: rem(-20px);
right: rem(-100px);
transform: none;
}
@include iflaptop{
top: rem(-20px);
right: rem(-100px);
}
@include ifdesktop{
top: rem(-20px);
right: rem(-150px);
}
}

50
src/views/cookie/ui.tsx Normal file
View File

@@ -0,0 +1,50 @@
import s from './styles.module.scss';
import { Button } from '@shared/ui';
import Link from 'next/link';
import { WEB } from '@core/constants/privacy-policy';
export default function Cookie() {
return (
<section className={s.Cookie}>
<Link href={'/'}>
<Button className={s.FloatBtn} variant={'orange'}>
Вернутся на главную
</Button>
</Link>
<h2>Мы используем cookie</h2>
<p>
Когда вы посещаете сайт {WEB}, наша компания может использовать
общеотраслевую технологию, называемую cookie. Файлы cookie представляют
собой небольшие фрагменты данных, которые временно сохраняются на вашем
компьютере или мобильном устройстве и обеспечивают более эффективную
работу сайта.
</p>
<p>
Сайт {WEB} использует метрические системы для сбора статистики:
«Яндекс.Метрика». На основе этих данных мы делаем наш сайт лучше и
эффективнее для пользователей.
</p>
<p>
Продолжая пользоваться этим сайтом, вы соглашаетесь на использование
cookie и обработку данных в соответствии с Политикой сайта в области
обработки и защиты персональных данных.
</p>
<p>
<Link href={'/privacy-policy'}>
Согласие на обработку персональных данных посетителей сайта
</Link>
</p>
<p>
Если вы не хотите использовать cookie, вы можете отключить их в
настройках безопасности вашего браузера. Отключение cookie следует
выполнить для каждого браузера и устройства, с помощью которого
осуществляется вход на сайт.
</p>
<p>
Обратите внимание, что в случае, если использование сайтом файлов cookie
отключено, некоторые возможности и услуги сайта могут быть недоступны.
</p>
</section>
);
}

View File

@@ -1 +1 @@
export { default as HomePage } from './ui/home';
export { default as Home } from './ui/home';

View File

@@ -236,6 +236,11 @@
margin-left: rem(42px);
}
a:hover {
color: #163055;
text-decoration: underline;
}
.Icon {
position: absolute;
top: 0;

View File

@@ -8,6 +8,7 @@ import sochiparkLogo from '@public/images/logo-sochipark.png';
import chateauLogo from '@public/images/logo-chateau-de-talu.png';
import gazpromLogo from '@public/images/logo-gazprom.png';
import kraspolLogo from '@public/images/logo-kraspol.png';
import sochiAdmLogo from '@public/svg/sochi-adm-logo.svg';
import ledOn from '@public/svg/led-on.svg';
import phoneCall from '@public/svg/phone-call.svg';
import map from '@public/svg/map.svg';
@@ -53,11 +54,11 @@ export default function Contacts() {
</p>
<p className={s.Address}>
<Image className={s.Icon} src={phoneCall} alt='' />
+7 (988) 400 93 93
<a href='tel:+79884009393'>+7 (988) 400-93-93</a>
</p>
<p className={s.Address}>
<Image className={s.Icon} src={email} alt='' />
office@firecheck.ru
<a href='mailto:spo-71@yandex.ru'>spo-71@yandex.ru</a>
</p>
</div>
</div>
@@ -69,7 +70,7 @@ export default function Contacts() {
const clientsLogos = [
{ logo: bogatyrLogo },
{ logo: sochiparkLogo },
{ logo: sochiAdmLogo },
{ logo: chateauLogo },
{ logo: gazpromLogo },
{ logo: kraspolLogo },
];

View File

@@ -39,6 +39,29 @@
@include ifdesktop {
width: rem(1340px);
}
.Picture {
display: none;
position: absolute;
top: 0;
height: auto;
@include iftablet{
display: block;
right: rem(40px);
width: rem(130px);
}
@include iflaptop{
right: rem(80px);
width: rem(150px);
}
@include ifdesktop{
right: rem(160px);
width: rem(180px);
}
}
}
.Bottom {

View File

@@ -2,19 +2,37 @@ import s from './footer.module.scss';
import { Button } from '@shared/ui';
import { FooterForm } from '@/widgets';
import Link from 'next/link';
import Image from 'next/image';
import man from '@public/images/footer-man.png';
export default function Footer() {
return (
<section className={s.Container}>
<div className={s.Footer}>
<FooterForm />
<Image className={s.Picture} src={man} alt='' quality={100} />
<div className={s.Bottom}>
<Button variant='ghost'>Telegram</Button>
<Button variant='ghost'>WhatsApp</Button>
<Button variant='ghost'>+7 (999) 123 45 67</Button>
<a
href='https://wa.me/?phone=79884009393&text=Консультация+по+пожарной+безопасности'
target='_blank'
rel='noopener noreferrer'
>
<Button variant='ghost'>WhatsApp</Button>
</a>
<a
href='mailto: spo-71@yandex.ru'
target='_blank'
rel='noopener noreferrer'
>
<Button variant='ghost'>spo-71@yandex.ru</Button>
</a>
<a href='tel:+79884009393' target='_blank' rel='noopener noreferrer'>
<Button variant='ghost'>+7 (988) 400-93-93</Button>
</a>
<p className={s.Policy}>
<Link href='#'>Политика конфиденциальности</Link>
<Link href='/privacy-policy'>Политика конфиденциальности</Link>
</p>
</div>
</div>

View File

@@ -10,7 +10,7 @@ export default function HomePage() {
<>
<Main />
<Offer />
<Result />
{/*<Result />*/}
<License />
<Contacts />
<Footer />

View File

@@ -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 {

View File

@@ -3,6 +3,8 @@ import { Mark } from '@shared/ui';
import { LicenseForm, LicenseSlider } from '@/widgets';
import emptyPaper from '@public/svg/empty-paper.svg';
import mcsLicense from '@public/images/license-mcs.jpg';
import eacLicense from '@public/images/license-dtr-eac.png';
export default function License() {
return (
@@ -20,21 +22,21 @@ const slides = [
{
id: '0',
name: '',
image: emptyPaper,
image: eacLicense,
},
{
id: '1',
name: '',
image: emptyPaper,
},
{
id: '2',
name: '',
image: emptyPaper,
},
{
id: '3',
name: '',
image: emptyPaper,
image: mcsLicense,
},
// {
// id: '2',
// name: '',
// image: emptyPaper,
// },
// {
// id: '3',
// name: '',
// image: emptyPaper,
// },
];

View File

@@ -41,6 +41,59 @@
.Logo {
color: $color-white;
display: flex;
flex-direction: row;
img {
width: rem(64px);
height: rem(64px);
}
.Block {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: rem(16px);
}
.Title {
font-family: $font-open-sans;
font-weight: $font-semi-bold;
font-size: rem(26px);
line-height: 100%;
color: $color-white;
text-transform: uppercase;
border-bottom: 1px solid $color-white;
padding-bottom: rem(6px);
margin-bottom: rem(6px);
width: fit-content;
@include iftablet{
font-size: rem(28px);
}
@include ifdesktop{
font-size: rem(32px);
}
}
.Subtitle {
font-family: $font-open-sans;
font-weight: $font-light;
font-size: rem(11px);
line-height: 100%;
color: $color-white;
text-transform: uppercase;
@include iftablet{
font-size: rem(12px);
}
@include ifdesktop{
font-size: rem(14px);
}
}
}
.Buttons {
@@ -65,33 +118,6 @@
height: rem(60px);
}
}
.Button {
display: none;
@include iflaptop {
display: flex;
flex-direction: row;
gap: rem(16px);
height: rem(40px);
padding: rem(20px);
}
@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);
}
}
}
}
}
@@ -102,7 +128,7 @@
padding: rem(64px) 0 rem(160px);
gap: rem(100px);
@include iftablet {
@include iftablet {
flex-direction: row;
gap: unset;
padding: rem(130px) 0 rem(160px);

View File

@@ -1,13 +1,13 @@
import s from './main.module.scss';
import { Button } from '@shared/ui';
import { ConsultationOrder } from '@/entities/home/ConsultationOrder';
import Image from 'next/image';
import { ConsultationOrder } from '@/entities/home/consultation-order';
import { CallbackOrder } from '@/entities/home/callback-order';
import bgStart from '@public/images/bg-start-desktop.jpg';
import logo from '@public/images/dtr-logo.png';
import waIcon from '@public/svg/whatsapp.svg';
import tgIcon from '@public/svg/telegram.svg';
import callBtn from '@public/svg/phone-calling.svg';
import emailIcon from '@public/svg/email-icon.svg';
export default function Main() {
return (
@@ -24,14 +24,30 @@ export default function Main() {
/>
</div>
<div className={s.Header}>
<div className={s.Logo}>Пожарная экспертиза</div>
<div className={s.Logo}>
<Image className={s.Image} src={logo} alt='ДиТрасо' />
<div className={s.Block}>
<p className={s.Title}>ДИ ТРАСО</p>
<p className={s.Subtitle}>Пожарная экспертиза</p>
</div>
</div>
<div className={s.Buttons}>
<Image className={s.Icon} src={waIcon} alt='whatsapp' />
<Image className={s.Icon} src={tgIcon} alt='telegram' />
<Button className={s.Button}>
<Image src={callBtn} alt='Call' />
Обратный звонок
</Button>
<a
href='mailto: spo-71@yandex.ru'
target='_blank'
rel='noopener noreferrer'
>
<Image className={s.Icon} src={emailIcon} alt='email' />
</a>
<a
href='https://wa.me/?phone=79884009393&text=Консультация+по+пожарной+безопасности'
target='_blank'
rel='noopener noreferrer'
>
<Image className={s.Icon} src={waIcon} alt='whatsapp' />
</a>
<CallbackOrder />
</div>
</div>
<div className={s.Info}>
@@ -51,7 +67,7 @@ export default function Main() {
</ul>
</div>
<div className={s.Phone}>
<p className={s.Title}>+7 988 400 93 93</p>
<p className={s.Title}>+7 (988) 400-93-93</p>
<ConsultationOrder />
</div>
</div>

View File

@@ -1 +0,0 @@
export { default as PolicyPage } from './ui/policy';

View File

@@ -1,2 +0,0 @@
.Policy {
}

View File

@@ -1,5 +0,0 @@
import s from './policy.module.scss';
export default function Policy() {
return <section className={s.Policy}>policy</section>;
}

View File

@@ -0,0 +1 @@
export { default as PrivacyPolicy } from './ui';

View File

@@ -0,0 +1,101 @@
.Policy {
position: relative;
margin: rem(60px) auto rem(20px);
width: rem(360px);
background: #EEE;
padding: rem(20px);
border-radius: rem(28px);
@include iftablet{
width: rem(600px);
margin: rem(40px) auto rem(20px);
padding: rem(20px);
}
@include iflaptop{
width: rem(800px);
margin: rem(60px) auto rem(20px);
padding: rem(60px);
}
@include ifdesktop{
width: rem(1200px);
margin: rem(60px) auto rem(20px);
padding: rem(60px);
}
h2 {
font-family: $font-open-sans;
font-weight: $font-regular;
font-size: rem(20px);
line-height: 130%;
color: $color-text;
@include iftablet{
font-size: rem(24px);
}
}
h3 {
font-family: $font-open-sans;
font-weight: $font-regular;
font-size: rem(16px);
line-height: 130%;
color: $color-text;
margin: 8px 0;
@include iftablet{
font-size: rem(20px);
}
}
p {
font-family: $font-open-sans;
font-weight: $font-regular;
font-size: rem(14px);
line-height: 130%;
color: $color-text;
margin: 8px 0;
@include iftablet{
font-size: rem(16px);
}
}
li {
font-family: $font-open-sans;
font-weight: $font-regular;
font-size: rem(14px);
line-height: 130%;
color: $color-text;
margin: 8px 0;
@include iftablet{
font-size: rem(16px);
}
}
}
.FloatBtn {
position: fixed;
top: rem(10px);
right: 50%;
transform: translateX(50%);
@include iftablet{
position: absolute;
top: rem(-20px);
right: rem(-100px);
transform: none;
}
@include iflaptop{
top: rem(-20px);
right: rem(-100px);
}
@include ifdesktop{
top: rem(-20px);
right: rem(-150px);
}
}

View File

@@ -0,0 +1,510 @@
import s from './styles.module.scss';
import Link from 'next/link';
import { Button } from '@shared/ui';
import { COMPANY, EMAIL, WEB } from '@core/constants/privacy-policy';
export default function PrivacyPolicy() {
return (
<section className={s.Policy}>
<Link href={'/'}>
<Button className={s.FloatBtn} variant={'orange'}>
Вернутся на главную
</Button>
</Link>
<h2>Политика в отношении обработки персональных данных</h2>
<h3>1. Общие положения</h3>
<p>
Настоящая политика обработки персональных данных составлена в
соответствии с требованиями Федерального закона от 27.07.2006. 152-ФЗ
«О персональных данных» (далее Закон о персональных данных) и
определяет порядок обработки персональных данных и меры по обеспечению
безопасности персональных данных, предпринимаемые {COMPANY} (далее
Оператор).
</p>
<ul>
<li>
1.1. Оператор ставит своей важнейшей целью и условием осуществления
своей деятельности соблюдение прав и свобод человека и гражданина при
обработке его персональных данных, в том числе защиты прав на
неприкосновенность частной жизни, личную и семейную тайну.
</li>
<li>
1.2. Настоящая политика Оператора в отношении обработки персональных
данных (далее Политика) применяется ко всей информации, которую
Оператор может получить о посетителях веб-сайта {WEB}.
</li>
</ul>
<h3>2. Основные понятия, используемые в Политике</h3>
<ul>
<li>
2.1. Автоматизированная обработка персональных данных обработка
персональных данных с помощью средств вычислительной техники.
</li>
<li>
2.2. Блокирование персональных данных временное прекращение
обработки персональных данных (за исключением случаев, если обработка
необходима для уточнения персональных данных).
</li>
<li>
2.3. Веб-сайт совокупность графических и информационных материалов,
а также программ для ЭВМ и баз данных, обеспечивающих их доступность в
сети интернет по сетевому адресу {WEB}.
</li>
<li>
2.4. Информационная система персональных данных совокупность
содержащихся в базах данных персональных данных и обеспечивающих их
обработку информационных технологий и технических средств.
</li>
<li>
2.5. Обезличивание персональных данных действия, в результате
которых невозможно определить без использования дополнительной
информации принадлежность персональных данных конкретному Пользователю
или иному субъекту персональных данных.
</li>
<li>
2.6. Обработка персональных данных любое действие (операция) или
совокупность действий (операций), совершаемых с использованием средств
автоматизации или без использования таких средств с персональными
данными, включая сбор, запись, систематизацию, накопление, хранение,
уточнение (обновление, изменение), извлечение, использование, передачу
(распространение, предоставление, доступ), обезличивание,
блокирование, удаление, уничтожение персональных данных.
</li>
<li>
2.7. Оператор государственный орган, муниципальный орган,
юридическое или физическое лицо, самостоятельно или совместно с
другими лицами организующие и/или осуществляющие обработку
персональных данных, а также определяющие цели обработки персональных
данных, состав персональных данных, подлежащих обработке, действия
(операции), совершаемые с персональными данными.
</li>
<li>
2.8. Персональные данные любая информация, относящаяся прямо или
косвенно к определенному или определяемому Пользователю веб-сайта{' '}
{WEB}.
</li>
<li>
2.9. Персональные данные, разрешенные субъектом персональных данных
для распространения, персональные данные, доступ неограниченного
круга лиц к которым предоставлен субъектом персональных данных путем
дачи согласия на обработку персональных данных, разрешенных субъектом
персональных данных для распространения в порядке, предусмотренном
Законом о персональных данных (далее персональные данные,
разрешенные для распространения).
</li>
<li>2.10. Пользователь любой посетитель веб-сайта {WEB}.</li>
<li>
2.11. Предоставление персональных данных действия, направленные на
раскрытие персональных данных определенному лицу или определенному
кругу лиц.
</li>
<li>
2.12. Распространение персональных данных любые действия,
направленные на раскрытие персональных данных неопределенному кругу
лиц (передача персональных данных) или на ознакомление с персональными
данными неограниченного круга лиц, в том числе обнародование
персональных данных в средствах массовой информации, размещение в
информационно-телекоммуникационных сетях или предоставление доступа к
персональным данным каким-либо иным способом.
</li>
<li>
2.13. Трансграничная передача персональных данных передача
персональных данных на территорию иностранного государства органу
власти иностранного государства, иностранному физическому или
иностранному юридическому лицу.
</li>
<li>
2.14. Уничтожение персональных данных любые действия, в результате
которых персональные данные уничтожаются безвозвратно с невозможностью
дальнейшего восстановления содержания персональных данных в
информационной системе персональных данных и/или уничтожаются
материальные носители персональных данных.
</li>
</ul>
<h3>3. Основные права и обязанности Оператора</h3>
<ul>
<li>
3.1. Оператор имеет право:
<ol>
<li>
получать от субъекта персональных данных достоверные информацию
и/или документы, содержащие персональные данные;
</li>
<li>
в случае отзыва субъектом персональных данных согласия на
обработку персональных данных, а также, направления обращения с
требованием о прекращении обработки персональных данных, Оператор
вправе продолжить обработку персональных данных без согласия
субъекта персональных данных при наличии оснований, указанных в
Законе о персональных данных;
</li>
<li>
самостоятельно определять состав и перечень мер, необходимых и
достаточных для обеспечения выполнения обязанностей,
предусмотренных Законом о персональных данных и принятыми в
соответствии с ним нормативными правовыми актами, если иное не
предусмотрено Законом о персональных данных или другими
федеральными законами.
</li>
</ol>
</li>
<li>
3.2. Оператор обязан:
<ol>
<li>
предоставлять субъекту персональных данных по его просьбе
информацию, касающуюся обработки его персональных данных;
</li>
<li>
организовывать обработку персональных данных в порядке,
установленном действующим законодательством РФ;
</li>
<li>
отвечать на обращения и запросы субъектов персональных данных и
их законных представителей в соответствии с требованиями Закона о
персональных данных;
</li>
<li>
сообщать в уполномоченный орган по защите прав субъектов
персональных данных по запросу этого органа необходимую информацию
в течение 10 дней с даты получения такого запроса;
</li>
<li>
публиковать или иным образом обеспечивать неограниченный доступ
к настоящей Политике в отношении обработки персональных данных;
</li>
<li>
принимать правовые, организационные и технические меры для
защиты персональных данных от неправомерного или случайного
доступа к ним, уничтожения, изменения, блокирования, копирования,
предоставления, распространения персональных данных, а также от
иных неправомерных действий в отношении персональных данных;
</li>
<li>
прекратить передачу (распространение, предоставление, доступ)
персональных данных, прекратить обработку и уничтожить
персональные данные в порядке и случаях, предусмотренных Законом о
персональных данных;
</li>
<li>
исполнять иные обязанности, предусмотренные Законом о
персональных данных.
</li>
</ol>
</li>
</ul>
<h3>4. Основные права и обязанности субъектов персональных данных</h3>
<ul>
<li>
4.1. Субъекты персональных данных имеют право:
<ol>
<li>
получать информацию, касающуюся обработки его персональных
данных, за исключением случаев, предусмотренных федеральными
законами. Сведения предоставляются субъекту персональных данных
Оператором в доступной форме, и в них не должны содержаться
персональные данные, относящиеся к другим субъектам персональных
данных, за исключением случаев, когда имеются законные основания
для раскрытия таких персональных данных. Перечень информации и
порядок ее получения установлен Законом о персональных данных;
</li>
<li>
требовать от оператора уточнения его персональных данных, их
блокирования или уничтожения в случае, если персональные данные
являются неполными, устаревшими, неточными, незаконно полученными
или не являются необходимыми для заявленной цели обработки, а
также принимать предусмотренные законом меры по защите своих прав;
</li>
<li>
выдвигать условие предварительного согласия при обработке
персональных данных в целях продвижения на рынке товаров, работ и
услуг;
</li>
<li>
на отзыв согласия на обработку персональных данных, а также, на
направление требования о прекращении обработки персональных
данных;
</li>
<li>
обжаловать в уполномоченный орган по защите прав субъектов
персональных данных или в судебном порядке неправомерные действия
или бездействие Оператора при обработке его персональных данных;
</li>
<li>
на осуществление иных прав, предусмотренных законодательством
РФ.
</li>
</ol>
</li>
<li>
4.2. Субъекты персональных данных обязаны:
<ol>
<li> предоставлять Оператору достоверные данные о себе;</li>
<li>
сообщать Оператору об уточнении (обновлении, изменении) своих
персональных данных.
</li>
</ol>
</li>
<li>
4.3. Лица, передавшие Оператору недостоверные сведения о себе, либо
сведения о другом субъекте персональных данных без согласия
последнего, несут ответственность в соответствии с законодательством
РФ.
</li>
</ul>
<h3>5. Принципы обработки персональных данных</h3>
<ul>
<li>
5.1. Обработка персональных данных осуществляется на законной и
справедливой основе.
</li>
<li>
5.2. Обработка персональных данных ограничивается достижением
конкретных, заранее определенных и законных целей. Не допускается
обработка персональных данных, несовместимая с целями сбора
персональных данных.
</li>
<li>
5.3. Не допускается объединение баз данных, содержащих персональные
данные, обработка которых осуществляется в целях, несовместимых между
собой.
</li>
<li>
5.4. Обработке подлежат только персональные данные, которые отвечают
целям их обработки.
</li>
<li>
5.5. Содержание и объем обрабатываемых персональных данных
соответствуют заявленным целям обработки. Не допускается избыточность
обрабатываемых персональных данных по отношению к заявленным целям их
обработки.
</li>
<li>
5.6. При обработке персональных данных обеспечивается точность
персональных данных, их достаточность, а в необходимых случаях и
актуальность по отношению к целям обработки персональных данных.
Оператор принимает необходимые меры и/или обеспечивает их принятие по
удалению или уточнению неполных или неточных данных.
</li>
<li>
5.7. Хранение персональных данных осуществляется в форме, позволяющей
определить субъекта персональных данных, не дольше, чем этого требуют
цели обработки персональных данных, если срок хранения персональных
данных не установлен федеральным законом, договором, стороной
которого, выгодоприобретателем или поручителем по которому является
субъект персональных данных. Обрабатываемые персональные данные
уничтожаются либо обезличиваются по достижении целей обработки или в
случае утраты необходимости в достижении этих целей, если иное не
предусмотрено федеральным законом.
</li>
</ul>
<h3>6. Цели обработки персональных данных</h3>
<p>
Цель обработки: информирование Пользователя посредством отправки
электронных писем
</p>
<p>
Персональные данные: фамилия, имя, отчество электронный адрес номера
телефонов
</p>
<p>
Правовые основания: уставные (учредительные) документы Оператора
договоры, заключаемые между оператором и субъектом персональных данных
</p>
<p>
Виды обработки персональных данных: Сбор, запись, систематизация,
накопление, хранение, уничтожение и обезличивание персональных данных
Отправка информационных писем на адрес электронной почты
</p>
<h3>7. Условия обработки персональных данных</h3>
<ul>
<li>
7.1. Обработка персональных данных осуществляется с согласия субъекта
персональных данных на обработку его персональных данных.
</li>
<li>
7.2. Обработка персональных данных необходима для достижения целей,
предусмотренных международным договором Российской Федерации или
законом, для осуществления возложенных законодательством Российской
Федерации на оператора функций, полномочий и обязанностей.
</li>
<li>
7.3. Обработка персональных данных необходима для осуществления
правосудия, исполнения судебного акта, акта другого органа или
должностного лица, подлежащих исполнению в соответствии с
законодательством Российской Федерации об исполнительном производстве.
</li>
<li>
7.4. Обработка персональных данных необходима для исполнения договора,
стороной которого либо выгодоприобретателем или поручителем по
которому является субъект персональных данных, а также для заключения
договора по инициативе субъекта персональных данных или договора, по
которому субъект персональных данных будет являться
выгодоприобретателем или поручителем.
</li>
<li>
7.5. Обработка персональных данных необходима для осуществления прав и
законных интересов оператора или третьих лиц либо для достижения
общественно значимых целей при условии, что при этом не нарушаются
права и свободы субъекта персональных данных.
</li>
<li>
7.6. Осуществляется обработка персональных данных, доступ
неограниченного круга лиц к которым предоставлен субъектом
персональных данных либо по его просьбе (далее общедоступные
персональные данные).
</li>
<li>
7.7. Осуществляется обработка персональных данных, подлежащих
опубликованию или обязательному раскрытию в соответствии с федеральным
законом.
</li>
</ul>
<h3>
8. Порядок сбора, хранения, передачи и других видов обработки
персональных данных
</h3>
<p>
Безопасность персональных данных, которые обрабатываются Оператором,
обеспечивается путем реализации правовых, организационных и технических
мер, необходимых для выполнения в полном объеме требований действующего
законодательства в области защиты персональных данных.
</p>
<ul>
<li>
8.1. Оператор обеспечивает сохранность персональных данных и принимает
все возможные меры, исключающие доступ к персональным данным
неуполномоченных лиц.
</li>
<li>
8.2. Персональные данные Пользователя никогда, ни при каких условиях
не будут переданы третьим лицам, за исключением случаев, связанных с
исполнением действующего законодательства либо в случае, если
субъектом персональных данных дано согласие Оператору на передачу
данных третьему лицу для исполнения обязательств по
гражданско-правовому договору.
</li>
<li>
8.3. В случае выявления неточностей в персональных данных,
Пользователь может актуализировать их самостоятельно, путем
направления Оператору уведомление на адрес электронной почты Оператора{' '}
{EMAIL} с пометкой «Актуализация персональных данных».
</li>
<li>
8.4. Срок обработки персональных данных определяется достижением
целей, для которых были собраны персональные данные, если иной срок не
предусмотрен договором или действующим законодательством. Пользователь
может в любой момент отозвать свое согласие на обработку персональных
данных, направив Оператору уведомление посредством электронной почты
на электронный адрес Оператора {EMAIL} с пометкой «Отзыв согласия на
обработку персональных данных».
</li>
<li>
8.5. Вся информация, которая собирается сторонними сервисами, в том
числе платежными системами, средствами связи и другими поставщиками
услуг, хранится и обрабатывается указанными лицами (Операторами) в
соответствии с их Пользовательским соглашением и Политикой
конфиденциальности. Субъект персональных данных и/или с указанными
документами. Оператор не несет ответственность за действия третьих
лиц, в том числе указанных в настоящем пункте поставщиков услуг.
</li>
<li>
8.6. Установленные субъектом персональных данных запреты на передачу
(кроме предоставления доступа), а также на обработку или условия
обработки (кроме получения доступа) персональных данных, разрешенных
для распространения, не действуют в случаях обработки персональных
данных в государственных, общественных и иных публичных интересах,
определенных законодательством РФ.
</li>
<li>
8.7. Оператор при обработке персональных данных обеспечивает
конфиденциальность персональных данных.
</li>
<li>
8.8. Оператор осуществляет хранение персональных данных в форме,
позволяющей определить субъекта персональных данных, не дольше, чем
этого требуют цели обработки персональных данных, если срок хранения
персональных данных не установлен федеральным законом, договором,
стороной которого, выгодоприобретателем или поручителем по которому
является субъект персональных данных.
</li>
<li>
8.9. Условием прекращения обработки персональных данных может являться
достижение целей обработки персональных данных, истечение срока
действия согласия субъекта персональных данных, отзыв согласия
субъектом персональных данных или требование о прекращении обработки
персональных данных, а также выявление неправомерной обработки
персональных данных.
</li>
</ul>
<h3>
9. Перечень действий, производимых Оператором с полученными
персональными данными{' '}
</h3>
<ul>
<li>
9.1. Оператор осуществляет сбор, запись, систематизацию, накопление,
хранение, уточнение (обновление, изменение), извлечение,
использование, передачу (распространение, предоставление, доступ),
обезличивание, блокирование, удаление и уничтожение персональных
данных.
</li>
<li>
9.2. Оператор осуществляет автоматизированную обработку персональных
данных с получением и/или передачей полученной информации по
информационно-телекоммуникационным сетям или без таковой.
</li>
</ul>
<h3>10. Трансграничная передача персональных данных</h3>
<ul>
<li>
10.1. Оператор до начала осуществления деятельности по трансграничной
передаче персональных данных обязан уведомить уполномоченный орган по
защите прав субъектов персональных данных о своем намерении
осуществлять трансграничную передачу персональных данных (такое
уведомление направляется отдельно от уведомления о намерении
осуществлять обработку персональных данных).
</li>
<li>
10.2. Оператор до подачи вышеуказанного уведомления, обязан получить
от органов власти иностранного государства, иностранных физических
лиц, иностранных юридических лиц, которым планируется трансграничная
передача персональных данных, соответствующие сведения.
</li>
</ul>
<h3>11. Конфиденциальность персональных данных</h3>
<p>
Оператор и иные лица, получившие доступ к персональным данным, обязаны
не раскрывать третьим лицам и не распространять персональные данные без
согласия субъекта персональных данных, если иное не предусмотрено
федеральным законом.
</p>
<h3>12. Заключительные положения</h3>
<ul>
<li>
12.1. Пользователь может получить любые разъяснения по интересующим
вопросам, касающимся обработки его персональных данных, обратившись к
Оператору с помощью электронной почты {EMAIL}.
</li>
<li>
12.2. В данном документе будут отражены любые изменения политики
обработки персональных данных Оператором. Политика действует бессрочно
до замены ее новой версией.
</li>
<li>
12.3. Актуальная версия Политики в свободном доступе расположена в
сети Интернет по адресу{' '}
<Link href={'/privacy-policy'}>{WEB + '/privacy-policy'}</Link>.
</li>
</ul>
</section>
);
}

View File

@@ -0,0 +1 @@
export { default as UserAgreement } from './ui';

View File

@@ -0,0 +1,75 @@
.Agreement {
position: relative;
margin: rem(60px) auto rem(20px);
width: rem(360px);
background: #EEE;
padding: rem(20px);
border-radius: rem(28px);
@include iftablet{
width: rem(600px);
margin: rem(40px) auto rem(20px);
padding: rem(20px);
}
@include iflaptop{
width: rem(800px);
margin: rem(60px) auto rem(20px);
padding: rem(60px);
}
@include ifdesktop{
width: rem(1200px);
margin: rem(60px) auto rem(20px);
padding: rem(60px);
}
h2 {
font-family: $font-open-sans;
font-weight: $font-regular;
font-size: rem(20px);
line-height: 130%;
color: $color-text;
@include iftablet{
font-size: rem(24px);
}
}
p {
font-family: $font-open-sans;
font-weight: $font-regular;
font-size: rem(14px);
line-height: 130%;
color: $color-text;
margin: 8px 0;
@include iftablet{
font-size: rem(16px);
}
}
}
.FloatBtn {
position: fixed;
top: rem(10px);
right: 50%;
transform: translateX(50%);
@include iftablet{
position: absolute;
top: rem(-20px);
right: rem(-100px);
transform: none;
}
@include iflaptop{
top: rem(-20px);
right: rem(-100px);
}
@include ifdesktop{
top: rem(-20px);
right: rem(-150px);
}
}

View File

@@ -0,0 +1,69 @@
import s from './styles.module.scss';
import Link from 'next/link';
import { Button } from '@shared/ui';
import { COMPANY, EMAIL, WEB } from '@core/constants/privacy-policy';
export default function UserAgreement() {
return (
<section className={s.Agreement}>
<Link href={'/'}>
<Button className={s.FloatBtn} variant={'orange'}>
Вернутся на главную
</Button>
</Link>
<h2>Согласие на обработку персональных данных {COMPANY}</h2>
<p>
Настоящим Я, в соответствии со ст. 9 ФЗ от 27.07.2006 N 152-ФЗ «О
персональных данных», предоставляю {COMPANY} (ИНН 2320219187, ОГРН
1142366003010, 354000, Краснодарский край, г.Сочи, ул.Труда, д.15, офис
64, e-mail: {EMAIL}) согласие на обработку моих персональных данных:
фамилии, имени, отчества, номера телефона, адреса электронной почты,
года, месяца, даты рождения, почтового адреса.
</p>
<p>
Цель обработки персональных данных: обратный звонок субъекту
персональных данных и связь с ним, предоставление информации, оказание
услуг, отправка информационных, рекламно-информационных сообщений.
</p>
<p>
Категории субъектов персональных данных: Пользователи сайта, которые
оставляют заявку на обратный звонок, оказание услуг.
</p>
<p>
Способы обработки персональных данных: сбор, запись, систематизация,
накопление, хранение, уточнение (обновление, изменение), извлечение,
использование, передача (распространение, предоставление, доступ),
обезличивание, блокирование, удаление, уничтожение персональных данных,
в том числе в информационных системах персональных данных с
использованием средств автоматизации или без использования таких
средств.
</p>
<p>
Порядок уничтожения персональных данных при достижении цели их обработки
или при наступлении иных законных оснований: лицо, ответственное за
обработку персональных данных, производит удаление данных методом
перезаписи (замена всех единиц хранения информации на «0») с
составлением акта об уничтожении персональных данных.
</p>
<p>
Передача персональных данных может быть осуществлена при необходимости в
случаях, предусмотренных российским законодательством.
</p>
<p>
Настоящее согласие действует со дня его предоставления до дня отзыва
путём его направления на адрес электронной почты: {EMAIL} или путем
письменного обращения по юридическому адресу: 354000, Краснодарский
край, г.Сочи, ул.Труда, д.15, офис 64
</p>
<p>
Я согласен/согласна квалифицировать в качестве своей простой электронной
подписи под настоящим Согласием и под Политикой обработки персональных
данных выполнение мною следующего действия на сайте: {WEB}: простановка
мною символа в чек-боксе (в поле для ввода) рядом с текстом: «Я
ознакомлен(-а) с Политикой обработки персональных данных и даю свое
согласие на обработку персональных данных».
</p>
</section>
);
}

View File

@@ -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<typeof FormSchema>;
const defaultValues = {
phone: '',
};
export default function AdvancedPhoneInput({
containerClassName,
}: AdvancedPhoneInputProps) {
const {
handleSubmit,
control,
reset,
clearErrors,
formState: { errors },
} = useForm<TForm>({
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 (
<form
className={clsx(containerClassName, s.Container)}
onSubmit={handleSubmit(onSubmit)}
>
<Controller
control={control}
name={'phone'}
render={({ field }) => (
<PhoneInput
{...field}
className={s.Phone}
placeholder={'+7 (999) 123-45-67'}
onChange={(e) => {
clearErrors('phone');
field.onChange(e);
}}
error={errors && errors.phone?.message}
errorTextColor={'#ff9191'}
/>
)}
/>
<Button className={s.Button} variant='orange'>
Отправить заявку
</Button>
</form>
);
}

View File

@@ -1,14 +1,63 @@
'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';
import { zodResolver } from '@hookform/resolvers/zod';
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, { message: 'Поле должно содержать не менее 3-х букв' })
.regex(/^[A-Za-zА-Яа-яЁё]+(?:[ '-][A-Za-zА-Яа-яЁё]+)*$/, {
message: 'Поле содержит некорректные символы',
}),
phone: z.string().refine(isValidPhoneNumber, 'Некорректный номер телефона'),
});
type TForm = z.infer<typeof FormSchema>;
const defaultValues = {
name: '',
phone: '',
};
export default function ContactsForm() {
const notify = () => toast.success('Заявка на консультацию принята');
const {
handleSubmit,
control,
reset,
clearErrors,
formState: { errors },
} = useForm<TForm>({
mode: 'onSubmit',
reValidateMode: 'onBlur',
resolver: zodResolver(FormSchema),
defaultValues,
});
const onSubmit = async (data: TForm) => {
const payload = {
...data,
form: 'contacts-form',
};
try {
await sendFormFn(payload);
toast.success('Заявка на консультацию принята');
reset(defaultValues);
} catch (e) {
toast.error('Ошибка при отправке заявки...', {
duration: 3000,
});
}
};
return (
<div className={s.Form}>
@@ -28,10 +77,42 @@ export default function ContactsForm() {
точной стоимости работ
</p>
</div>
<form className={s.Inputs}>
<Input placeholder='Ваше имя' fullWidth />
<Input type='text' placeholder='+7 (999) 123 45 67' fullWidth />
<Button variant='orange' fullWidth onClick={notify}>
<form className={s.Inputs} onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name={'name'}
render={({ field }) => (
<Input
{...field}
placeholder={'Ваше имя'}
fullWidth
onChange={(e) => {
clearErrors('name');
field.onChange(e);
}}
error={errors && errors.name?.message}
errorTextColor={'#ff9191'}
/>
)}
/>
<Controller
control={control}
name={'phone'}
render={({ field }) => (
<PhoneInput
{...field}
placeholder={'+7 (999) 123-45-67'}
fullWidth
onChange={(e) => {
clearErrors('phone');
field.onChange(e);
}}
error={errors && errors.phone?.message}
errorTextColor={'#ff9191'}
/>
)}
/>
<Button variant='orange' fullWidth>
Получить консультацию
</Button>
</form>

View File

@@ -0,0 +1 @@
export { default as CookieNotice } from './ui';

View File

@@ -0,0 +1,97 @@
.Cookies {
position: fixed;
z-index: 10;
width: rem(360px);
bottom: rem(10px);
left: 50%;
transform: translateX(-50%);
background-color: #d6d6d6;
transition: opacity 0.25s ease-in-out;
border: 1px solid $color-lightgray;
border-radius: 28px;
box-shadow: 2px 2px 2px 2px #CCCCCC;
@include iftablet {
width: rem(700px);
bottom: rem(20px);
}
@include iflaptop {
width: rem(1000px);
}
@include ifdesktop {
width: rem(1200px);
}
}
.Container {
display: flex;
flex-direction: column;
gap: rem(20px);
padding: rem(20px);
@include iftablet {
justify-content: space-between;
flex-direction: row;
align-items: center;
gap: rem(25px);
padding: rem(20px) rem(40px);
}
}
.Text {
font-family: $font-open-sans;
font-weight: $font-regular;
font-size: rem(16px);
line-height: 130%;
opacity: 0.7;
@include iftablet {
max-width: rem(350px);
}
@include iflaptop {
max-width: unset;
}
@include ifdesktop {
font-size: rem(18px);
}
a {
color: $color-orange;
&:hover {
color: $color-orange-hover;
}
}
}
.ButtonBox {
display: flex;
justify-content: center;
align-items: center;
gap: rem(32px);
@include iftablet {
flex-direction: column-reverse;
gap: rem(20px);
max-width: max-content;
}
@include iflaptop {
flex-direction: row;
justify-content: flex-end;
max-width: unset;
}
@include ifdesktop {
}
}
.Hide {
opacity: 0;
user-select: none;
pointer-events: none;
}

View File

@@ -0,0 +1,49 @@
'use client';
import s from './styles.module.scss';
import { useEffect, useState } from 'react';
import { Button } from '@shared/ui';
import { clsx } from 'clsx';
import Link from 'next/link';
export default function CookiesNotice() {
const [seenCookie, setSeenCookie] = useState(true);
const [clicked, setClicked] = useState(false);
useEffect(() => {
const seenCookie = localStorage.getItem('seen_cookie');
if (!seenCookie) setSeenCookie(false);
}, []);
const handleClickAgree = () => {
localStorage.setItem('seen_cookie', 'true');
setClicked(true);
};
const handleClickCancel = () => {
setClicked(true);
};
return seenCookie ? null : (
<div className={clsx(s.Cookies, clicked && s.Hide)}>
<div className={s.Container}>
<span className={s.Text}>
Мы используем cookie. <br /> Во время посещения этого сайта вы
соглашаетесь с тем, что мы обрабатываем ваши персональные данные с
использованием метрических программ. <br />
<Link href={'/cookie'}>Подробнее</Link>
</span>
<div className={s.ButtonBox}>
<Button onClick={handleClickCancel} fullWidth>
Отмена
</Button>
<Button variant={'ghost'} onClick={handleClickAgree} fullWidth>
Согласится
</Button>
</div>
</div>
</div>
);
}

View File

@@ -1,28 +1,122 @@
'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';
import { z } from 'zod';
import { sendFormFn } from '@shared/api/api.service';
import { isValidPhoneNumber } from 'libphonenumber-js/min';
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<typeof FormSchema>;
const defaultValues = {
name: '',
phone: '',
message: '',
};
export default function FooterForm() {
const notify = () => toast.success('Заявка на консультацию принята');
const {
handleSubmit,
control,
reset,
clearErrors,
formState: { errors },
} = useForm<TForm>({
resolver: zodResolver(FormSchema),
defaultValues,
});
const onSubmit = async (data: TForm) => {
const payload = {
...data,
form: 'footer-form',
};
try {
await sendFormFn(payload);
toast.success('Заявка на консультацию принята');
reset(defaultValues);
} catch (e) {
toast.error('Ошибка при отправке заявки...', {
duration: 3000,
});
}
};
return (
<form className={s.Form}>
<form className={s.Form} onSubmit={handleSubmit(onSubmit)}>
<h2 className={s.Header}>
Давайте <Mark>обсудим</Mark> ваши задачи
</h2>
<Input variant='ghost' placeholder={'Ваше имя'} fullWidth />
<Input variant='ghost' placeholder={'+7 999 1234567'} fullWidth />
<TextArea
variant='ghost'
placeholder={'Кратко опишите вашу задачу'}
fullWidth
id='story'
name='story'
rows={6}
<Controller
control={control}
name={'name'}
render={({ field }) => (
<Input
{...field}
variant='ghost'
placeholder={'Ваше имя'}
fullWidth
onChange={(e) => {
clearErrors('name');
field.onChange(e);
}}
error={errors && errors.name?.message}
errorTextColor={'#ff9191'}
/>
)}
/>
<Button className={s.SendBtn} variant='orange' fullWidth onClick={notify}>
<Controller
control={control}
name={'phone'}
render={({ field }) => (
<PhoneInput
{...field}
variant='ghost'
placeholder={'+7 (999) 123-45-67'}
fullWidth
onChange={(e) => {
clearErrors('phone');
field.onChange(e);
}}
error={errors && errors.phone?.message}
errorTextColor={'#ff9191'}
/>
)}
/>
<Controller
control={control}
name={'message'}
render={({ field }) => (
<TextArea
{...field}
variant='ghost'
placeholder={'Кратко опишите вашу задачу'}
fullWidth
id='story'
name='story'
rows={6}
error={errors && errors.message?.message}
errorTextColor={'#ff9191'}
/>
)}
/>
<Button className={s.SendBtn} variant='orange' fullWidth>
Отправить
</Button>
</form>

View File

@@ -4,3 +4,5 @@ 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';
export { CookieNotice } from './cookie-notice';

View File

@@ -1,14 +1,64 @@
'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 bgForm from '@public/images/bg-form.jpg';
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, { message: 'Поле должно содержать не менее 3-х букв' })
.regex(/^[A-Za-zА-Яа-яЁё]+(?:[ '-][A-Za-zА-Яа-яЁё]+)*$/, {
message: 'Поле содержит некорректные символы',
}),
phone: z.string().refine(isValidPhoneNumber, 'Некорректный номер телефона'),
});
type TForm = z.infer<typeof FormSchema>;
const defaultValues = {
name: '',
phone: '',
};
export default function LicenseForm() {
const notify = () => toast.success('Заявка на консультацию принята');
const {
handleSubmit,
control,
reset,
clearErrors,
formState: { errors },
} = useForm<TForm>({
mode: 'onSubmit',
reValidateMode: 'onBlur',
resolver: zodResolver(FormSchema),
defaultValues,
});
const onSubmit = async (data: TForm) => {
const payload = {
...data,
form: 'license-form',
};
try {
await sendFormFn(payload);
toast.success('Заявка на консультацию принята');
reset(defaultValues);
} catch (e) {
toast.error('Ошибка при отправке заявки...', {
duration: 3000,
});
}
};
return (
<div className={s.Form}>
@@ -30,10 +80,42 @@ export default function LicenseForm() {
Оставьте свои контактные данные и мы закрепим скидку до 1 июля за вами
</p>
</div>
<form className={s.Inputs}>
<Input placeholder={'Ваше имя'} fullWidth />
<Input placeholder={'+7 (999) 123 45 67'} fullWidth />
<Button variant='orange' fullWidth onClick={notify}>
<form className={s.Inputs} onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name={'name'}
render={({ field }) => (
<Input
{...field}
placeholder={'Ваше имя'}
fullWidth
onChange={(e) => {
clearErrors('name');
field.onChange(e);
}}
error={errors && errors.name?.message}
errorTextColor={'#ff9191'}
/>
)}
/>
<Controller
control={control}
name={'phone'}
render={({ field }) => (
<PhoneInput
{...field}
placeholder={'+7 (999) 123-45-67'}
fullWidth
onChange={(e) => {
clearErrors('phone');
field.onChange(e);
}}
error={errors && errors.phone?.message}
errorTextColor={'#ff9191'}
/>
)}
/>
<Button variant='orange' fullWidth>
Получить консультацию
</Button>
</form>

View File

@@ -3,7 +3,7 @@
flex-direction: row;
justify-content: space-between;
margin-bottom: rem(40px);
padding: 0 rem(20px) 0;
padding: 0 rem(20px) 0!important;
@include iftablet {
padding: unset;
@@ -15,6 +15,14 @@
}
.Slide {
cursor: pointer;
width: 100%;
display: flex!important;
justify-content: center;
.Image {
cursor: pointer;
width: auto;
height: rem(400px);
display: flex;
align-items: center;
}
}

View File

@@ -32,11 +32,11 @@ const swiperBreakpoints = {
spaceBetween: 30,
},
1024: {
slidesPerView: 3,
slidesPerView: 2,
spaceBetween: 30,
},
1440: {
slidesPerView: 3,
slidesPerView: 2,
spaceBetween: 30,
},
};
@@ -47,13 +47,11 @@ export default function Ui({ className, images }: LicenseSliderProps) {
className={clsx(s.Slider, className)}
modules={[Grid]}
breakpoints={swiperBreakpoints}
// onSlideChange={() => console.log('slide change')}
// onSwiper={(swiper) => console.log(swiper)}
>
{images.map(({ id, name, image }) => (
<SwiperSlide key={id}>
<SwiperSlide key={id} className={s.Slide}>
<a href={mockFullSizeImage} target='_blank'>
<Image className={s.Slide} src={image} alt={name} />
<Image className={s.Image} src={image} alt={name} quality={75} />
</a>
</SwiperSlide>
))}

View File

@@ -1,25 +1,96 @@
'use client';
import s from './styles.module.scss';
import { Button, Input } from '@shared/ui';
import { useState } from 'react';
import { Button, Input, PhoneInput } from '@shared/ui';
import { Controller, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import toast from 'react-hot-toast';
import { sendFormFn } from '@shared/api/api.service';
import { isValidPhoneNumber } from 'libphonenumber-js/min';
const FormSchema = z.object({
name: z
.string()
.min(3, { message: 'Поле должно содержать не менее 3-х букв' })
.regex(/^[A-Za-zА-Яа-яЁё]+(?:[ '-][A-Za-zА-Яа-яЁё]+)*$/, {
message: 'Поле содержит некорректные символы',
}),
phone: z.string().refine(isValidPhoneNumber, 'Некорректный номер телефона'),
});
type TForm = z.infer<typeof FormSchema>;
const defaultValues = {
name: '',
phone: '',
};
export default function OfferForm() {
const [name, setName] = useState('');
const notify = () => toast.success('Заявка на консультацию принята');
const {
handleSubmit,
control,
reset,
clearErrors,
formState: { errors },
} = useForm<TForm>({
resolver: zodResolver(FormSchema),
defaultValues,
});
const onSubmit = async (data: TForm) => {
const payload = {
...data,
form: 'offer-form',
};
try {
await sendFormFn(payload);
toast.success('Заявка на консультацию принята');
reset(defaultValues);
} catch (e) {
toast.error('Ошибка при отправке заявки...', {
duration: 3000,
});
}
};
return (
<form className={s.RowForm}>
<Input className={s.Unit} type='text' placeholder='+7 (999) 123 45 67' />
<Input
className={s.Unit}
type='text'
placeholder='Ваше имя'
value={name}
onChange={(e) => setName(e.target.value)}
<form className={s.RowForm} onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name={'name'}
render={({ field }) => (
<Input
{...field}
className={s.Unit}
type='text'
placeholder='Ваше имя'
error={errors && errors.name?.message}
onChange={(e) => {
clearErrors('name');
field.onChange(e);
}}
/>
)}
/>
<Button className={s.Unit} variant='orange' onClick={notify}>
<Controller
control={control}
name={'phone'}
render={({ field }) => (
<PhoneInput
{...field}
className={s.Unit}
type='text'
placeholder='+7 999 123-45-67'
error={errors && errors.phone?.message}
onChange={(e) => {
clearErrors('phone');
field.onChange(e);
}}
/>
)}
/>
<Button className={s.Unit} variant='orange'>
Получить консультацию
</Button>
</form>

View File

@@ -69,7 +69,7 @@
position: relative;
display: flex;
flex-direction: column;
gap: rem(12px);
gap: rem(20px);
z-index: 2;
max-width: rem(400px);
@include iftablet{

View File

@@ -1,14 +1,64 @@
'use client';
import s from './styles.module.scss';
import { AdvancedPhoneInput, Button, Input } from '@shared/ui';
import { Button, Input, PhoneInput } from '@shared/ui';
import Image from 'next/image';
import { Controller, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import toast from 'react-hot-toast';
import bgForm from '@public/images/bg-form.jpg';
import { AdvancedPhoneInput } from '@/widgets';
import { sendFormFn } from '@shared/api/api.service';
import { isValidPhoneNumber } from 'libphonenumber-js/min';
const FormSchema = z.object({
name: z
.string()
.min(3, { message: 'Поле должно содержать не менее 3-х букв' })
.regex(/^[A-Za-zА-Яа-яЁё]+(?:[ '-][A-Za-zА-Яа-яЁё]+)*$/, {
message: 'Поле содержит некорректные символы',
}),
phone: z.string().refine(isValidPhoneNumber, 'Некорректный номер телефона'),
});
type TForm = z.infer<typeof FormSchema>;
const defaultValues = {
name: '',
phone: '',
};
export default function OfferRequest() {
const notify = () => toast.success('Заявка на консультацию принята');
const {
handleSubmit,
control,
reset,
clearErrors,
formState: { errors },
} = useForm<TForm>({
mode: 'onSubmit',
reValidateMode: 'onBlur',
resolver: zodResolver(FormSchema),
defaultValues,
});
const onSubmit = async (data: TForm) => {
const payload = {
...data,
form: 'offer-request-form-mobile',
};
try {
await sendFormFn(payload);
toast.success('Заявка на консультацию принята');
reset(defaultValues);
} catch (e) {
toast.error('Ошибка при отправке заявки...', {
duration: 3000,
});
}
};
return (
<div className={s.Form}>
@@ -26,20 +76,46 @@ export default function OfferRequest() {
<h3 className={s.Title}>Оставьте заявку на бесплатную консультацию</h3>
</div>
<div className={s.PanelRight}>
<AdvancedPhoneInput
containerClassName={s.AdvPhoneInput}
text='Отправить заявку'
placeholder={'+7 (999) 123 45 67'}
onClick={notify}
/>
<AdvancedPhoneInput containerClassName={s.AdvPhoneInput} />
<div className={s.MobileBtns}>
<Input placeholder='Ваше имя' fullWidth />
<Input placeholder='+7 999 123 45 67' fullWidth />
<Button variant='orange' fullWidth onClick={notify}>
<form className={s.MobileBtns} onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name={'name'}
render={({ field }) => (
<Input
{...field}
placeholder='Ваше имя'
fullWidth
onChange={(e) => {
clearErrors('name');
field.onChange(e);
}}
error={errors && errors.name?.message}
errorTextColor={'#ff9191'}
/>
)}
/>
<Controller
control={control}
name={'phone'}
render={({ field }) => (
<PhoneInput
{...field}
placeholder='+7 (999) 123-45-67'
onChange={(e) => {
clearErrors('phone');
field.onChange(e);
}}
error={errors && errors.phone?.message}
errorTextColor={'#ff9191'}
/>
)}
/>
<Button variant='orange' fullWidth>
Отправить заявку
</Button>
</div>
</form>
</div>
</div>
);