diff --git a/front/src/App.css b/front/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/front/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/front/src/api/catalogApi.ts b/front/src/api/catalogApi.ts new file mode 100644 index 0000000..e145965 --- /dev/null +++ b/front/src/api/catalogApi.ts @@ -0,0 +1,14 @@ +import axios from "axios"; +import type { Product } from "../types/product"; +import type { Category } from "../types/categories"; + +export const fetchCategoryProducts = async (category: string): Promise => { + const response = await axios.get(`https://your-api.com/api/products?category=${category}`); + return response.data; +}; + +export const fetchCategories = async (categoryid: string): Promise => { + const response = await axios.get(`https://your-api.com/api/category=${categoryid}`); + return response.data; +}; + diff --git a/front/src/app/hooks.ts b/front/src/app/hooks.ts new file mode 100644 index 0000000..b218738 --- /dev/null +++ b/front/src/app/hooks.ts @@ -0,0 +1,7 @@ +import { useDispatch, useSelector } from "react-redux"; +import type {TypedUseSelectorHook} from "react-redux"; +import type { RootState, AppDispatch } from "./store"; + +// Используем вместо обычных useDispatch и useSelector +export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppSelector: TypedUseSelectorHook = useSelector; \ No newline at end of file diff --git a/front/src/app/routes.tsx b/front/src/app/routes.tsx new file mode 100644 index 0000000..dc58d6f --- /dev/null +++ b/front/src/app/routes.tsx @@ -0,0 +1,20 @@ +import Layout from "../pages/Layout/Layout"; +import { Routes } from "react-router"; +import { Route } from "react-router"; +import Main from "../pages/Main/Main"; +import About from "../pages/About/About"; +import Products from "../pages/Products/Products"; + +function AppRoutes() { + return ( + + }> + } /> + } /> + } /> + + + ) +} + +export default AppRoutes; \ No newline at end of file diff --git a/front/src/app/store.ts b/front/src/app/store.ts new file mode 100644 index 0000000..11ebdd1 --- /dev/null +++ b/front/src/app/store.ts @@ -0,0 +1,15 @@ +import { configureStore } from "@reduxjs/toolkit"; +import catalogReducer from "../features/catalog/catalogSlice"; + +export const store = configureStore({ + reducer: { + catalog: catalogReducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: false, // отключаем для async thunk + }), +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; \ No newline at end of file diff --git a/front/src/assets/images/Logo.jpg b/front/src/assets/images/Logo.jpg new file mode 100644 index 0000000..9c79212 Binary files /dev/null and b/front/src/assets/images/Logo.jpg differ diff --git a/front/src/components/Banner/Banner.module.css b/front/src/components/Banner/Banner.module.css new file mode 100644 index 0000000..3bfecbb --- /dev/null +++ b/front/src/components/Banner/Banner.module.css @@ -0,0 +1,95 @@ +.wrapper { +width: 100%; +position: relative; +} + + +.carousel { +width: 100%; +height: 100%; +} + + +.slide { +position: relative; +width: 100%; +height: 700px; +overflow: hidden; +display: flex; +justify-content: space-between; +align-items: center; +background: var(--darkblue); +} + + +.textBlock { +position: absolute; +left: 80px; +top: 180px; +color: white; +max-width: 500px; +z-index: 2; +} + + +.textBlock h1 { +font-size: 36px; +font-weight: 700; +line-height: 1.3; +} + + +.textBlock h2 { +font-size: 20px; +font-weight: 400; +opacity: 0.8; +} + + +.button { +margin-top: 30px; +background: #ffd400 !important; +border: none !important; +color: black !important; +font-weight: 600; +padding: 0 26px; +height: 48px; +} + + +.image { +width: 100%; +height: 100%; +object-fit: cover; +object-position: center; +} + +/* --- Mobile Adaptive --- */ +@media (max-width: 768px) { +.slide { +height: 500px; +} + + +.textBlock { +left: 20px; +top: 120px; +max-width: 300px; +} + + +.textBlock h1 { +font-size: 22px; +} + + +.textBlock h2 { +font-size: 16px; +} + + +.button { +height: 42px; +padding: 0 20px; +} +} \ No newline at end of file diff --git a/front/src/components/Banner/Banner.tsx b/front/src/components/Banner/Banner.tsx new file mode 100644 index 0000000..712f97b --- /dev/null +++ b/front/src/components/Banner/Banner.tsx @@ -0,0 +1,73 @@ +import styles from './Banner.module.css' +import React from "react"; +import { Carousel } from "antd"; +import Button from '../Button/Button.tsx'; + +export interface SlideItem { +id: number; +image: string; +title: string; +subtitle: string; +description: string; +} + + +const slides: SlideItem[] = [ + { + id: 1, + image: "/images/banner1.jpg", + title: "ПРОИЗВОДСТВО", + subtitle: "ЛЫЖНЫХ СМАЗОК, АКСЕСУАРОВ И ПРОДУКЦИИ", + description: "ДЛЯ ЗИМНИХ ВИДОВ СПОРТА", + }, + { + id: 2, + image: "/images/banner2.jpg", + title: "ПРОИЗВОДСТВО", + subtitle: "ЛЫЖНЫХ СМАЗОК, АКСЕСУАРОВ И ПРОДУКЦИИ", + description: "ДЛЯ ЗИМНИХ ВИДОВ СПОРТА", + }, + { + id: 3, + image: "/images/banner3.jpg", + title: "ПРОИЗВОДСТВО", + subtitle: "ЛЫЖНЫХ СМАЗОК, АКСЕСУАРОВ И ПРОДУКЦИИ", + description: "ДЛЯ ЗИМНИХ ВИДОВ СПОРТА", + }, +]; + + +const BannerSlider: React.FC = () => { + +const handleClick = () => { + console.log('Кнопка нажата!'); + }; + +return ( +
+ + {slides.map((slide) => ( +
+
+
+

{slide.title}

+

+ {slide.subtitle} +
{slide.description} +

+
+ banner +
+
+ ))} +
+
+ ); +}; + + +export default BannerSlider; \ No newline at end of file diff --git a/front/src/components/Button/Button.module.css b/front/src/components/Button/Button.module.css new file mode 100644 index 0000000..36e82e0 --- /dev/null +++ b/front/src/components/Button/Button.module.css @@ -0,0 +1,22 @@ +.button { +background: #ffd400; +border: none; +color: #000; +font-weight: 600; +padding: 0 50px; +height: 50px; +border-radius: 50px; +cursor: pointer; +transition: 0.25s ease; +font-size: 15px; +} + + +.button:hover { +background: #ffdf3e; +} + + +.button:active { +transform: scale(0.97); +} \ No newline at end of file diff --git a/front/src/components/Button/Button.tsx b/front/src/components/Button/Button.tsx new file mode 100644 index 0000000..4080234 --- /dev/null +++ b/front/src/components/Button/Button.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import styles from "./Button.module.css"; + + +interface ButtonProps { + label: string; + onClick?: () => void; + className?: string; +} + + +const Button: React.FC = ({ label, onClick, className }) => { + return ( + + ); +}; + + +export default Button; \ No newline at end of file diff --git a/front/src/components/Categories/Categories.module.css b/front/src/components/Categories/Categories.module.css new file mode 100644 index 0000000..753bbab --- /dev/null +++ b/front/src/components/Categories/Categories.module.css @@ -0,0 +1,127 @@ +.wrapper { +width: 100%; +margin-top: 40px; +} + +.header { +font-size: 24px; +font-weight: 700; +margin-bottom: 20px; +} + + +.grid { +display: grid; +grid-template-columns: repeat(4, 1fr); +grid-auto-rows: 230px; +gap: 0; +} + +.card { +position: relative; +overflow: hidden; +border: 1px solid #ececec; +} + + +.image { +width: 100%; +height: 100%; +object-fit: cover; +} + + +.highlight { +grid-column: span 2; +grid-row: span 2; +} + + +.overlay { +position: absolute; +left: 40px; +bottom: 40px; +color: white; +max-width: 320px; +} + + +.overlay h3 { +font-size: 22px; +font-weight: 700; +} + + +.overlay h4 { +margin-top: 4px; +font-size: 16px; +opacity: 0.9; +} + + +.button { +margin-top: 20px; +background: #ffd400 !important; +color: black !important; +border: none !important; +font-weight: 600; +} + + +.titleSmall { +position: absolute; +bottom: 15px; +left: 15px; +font-size: 15px; +font-weight: 600; +} + +.card:hover .image { +transform: scale(1.05); +transition: transform 0.4s ease; +} + + +.card:hover { +cursor: pointer; +background-color: #ffd400; +} + + +.highlight:hover .overlay h3, +.highlight:hover .overlay h4 { +transform: translateX(4px); +transition: all 0.3s ease; +} + + +.titleSmall { +transition: transform 0.3s ease, opacity 0.3s ease; +} + + +.card:hover .titleSmall { +transform: translateY(-4px); +opacity: 0.9; +} + +/* Mobile */ +@media (max-width: 900px) { +.grid { +grid-template-columns: repeat(2, 1fr); +} +.highlight { +grid-column: span 2; +} +} + + +@media (max-width: 600px) { +.grid { +grid-template-columns: 1fr; +} +.highlight { +grid-column: span 1; +grid-row: span 1; +} +} \ No newline at end of file diff --git a/front/src/components/Categories/Categories.tsx b/front/src/components/Categories/Categories.tsx new file mode 100644 index 0000000..c270aca --- /dev/null +++ b/front/src/components/Categories/Categories.tsx @@ -0,0 +1,85 @@ +import React from "react"; +import styles from "./Categories.module.css"; +import Button from "../Button/Button"; + +interface CategoryItem { +id: number; +title: string; +subtitle?: string; +image: string; +highlight?: boolean; +} + +const categories: CategoryItem[] = [ +{ +id: 1, +title: "СРЕДСТВА", +subtitle: "ДЛЯ УХОДА", +image: "/images/cat-care.jpg", +highlight: true, +}, +{ id: 2, title: "СМАЗКИ СКОЛЬЖЕНИЯ", subtitle: "(ПАРАФИНЫ)", image: "/images/cat-paraffin.jpg" }, +{ id: 3, title: "МАЗИ ДЕРЖАНИЯ", image: "/images/cat-hold.jpg" }, +{ id: 4, title: "СМЫВКА", image: "/images/cat-wash.jpg" }, +{ id: 5, title: "СМОЛА", image: "/images/cat-resin.jpg" }, +{ id: 6, title: "СКРЕБКИ", image: "/images/cat-scraper.jpg" }, +{ id: 7, title: "ПРОБКИ / РАСТИРКИ", image: "/images/cat-cork.jpg" }, +{ +id: 8, +title: "ПОЛЕЗНЫЕ", +subtitle: "АКССЕСУАРЫ", +highlight: true, +image: "/images/cat-accessories.jpg", +}, +{ id: 9, title: "СВЯЗКИ И СКРЕПКИ ДЛЯ ЛЫЖ", image: "/images/cat-straps.jpg" }, +{ id: 10, title: "ЧЕХЛЫ, СУМКИ", image: "/images/cat-bags.jpg" }, +]; + + +const CategoriesGrid: React.FC = () => { + +const handleClick = () => { + console.log('Кнопка нажата!'); + }; + +return ( +
+

КАТЕГОРИИ ТОВАРОВ

+ + +
+{categories.map((cat) => ( +
+{cat.title} + + +{cat.highlight && ( +
+

{cat.title}

+{cat.subtitle &&

{cat.subtitle}

} +
+)} + + +{!cat.highlight && ( +
+{cat.title} +{cat.subtitle && {cat.subtitle}} +
+)} +
+))} +
+
+); +}; + + +export default CategoriesGrid; \ No newline at end of file diff --git a/front/src/components/ContactProduct/ContactProduct.module.css b/front/src/components/ContactProduct/ContactProduct.module.css new file mode 100644 index 0000000..c25e673 --- /dev/null +++ b/front/src/components/ContactProduct/ContactProduct.module.css @@ -0,0 +1,81 @@ +.wrapper { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0px 0; +} + +.left { + flex: 1; + padding-left: 0px; +} + +.right { + flex: 1; + position: relative; + + width: 60%; + height: 100%; + max-height: 600px; +} + +.carousel { + width: 100%; + height: 100%; /* Добавьте высоту 100% */ +} + +.slide { +position: relative; + width: 100%; + height: 100%; +} + +.image { + width: 100%; + height: 100%; + object-fit: cover; + object-position: center bottom; + display: block; +} + +.label { + position: absolute; + bottom: 20px; + left: 40px; + color: white; + font-size: 22px; +} + +.arrows { + position: absolute; + right: 30px; + bottom: 30px; + display: flex; + gap: 16px; +} + +.arrow { + width: 48px; + height: 48px; + background: white; + border: 1px solid #ccc; + font-size: 22px; + cursor: pointer; +} + +@media (max-width: 900px) { + .wrapper { + flex-direction: column; + text-align: center; + } + + .left { + padding-left: 0; + } + + .right { + margin-top: 30px; + height: 300px; /* Меньшая высота для мобильных */ + width: 100%; + } +} \ No newline at end of file diff --git a/front/src/components/ContactProduct/ContactProduct.tsx b/front/src/components/ContactProduct/ContactProduct.tsx new file mode 100644 index 0000000..b1e9cde --- /dev/null +++ b/front/src/components/ContactProduct/ContactProduct.tsx @@ -0,0 +1,88 @@ +import React, { useRef, useState } from "react"; +import styles from "./ContactProduct.module.css"; +import Button from "../Button/Button"; +import { Carousel } from "antd"; +import ModalRequestForm from "../ModalRequestForm/ModalRequestForm"; +const imageUrl = "http://sprint-pro.com/images/banners/slide1/t1.png"; + +interface ProductionSlide { + id: number; + image: string; + label: string; +} + +const slides: ProductionSlide[] = [ + { + id: 1, + image: "/images/prod1.jpg", + label: "Аэрозольная упаковка", + }, + { + id: 2, + image: "/images/prod2.jpg", + label: "Металлические баллоны", + }, +]; + + +const ProductionBanner: React.FC = () => { + + const [open, setOpen] = useState(false); + + const handleClick = () => { + console.log('Кнопка нажата!'); + }; + + const carouselRef = useRef(null); + + console.log('ProductionBanner render, open:', open); + + return ( +
+ setOpen(false)} /> +
+

КОНТРАКТНОЕ

+

ПРОИЗВОДСТВО

+ +
+ +
+ + {slides.map((s) => ( +
+ prod +
{s.label}
+
+ ))} +
+ +
+ + +
+
+
+ ); +}; + + +export default ProductionBanner; \ No newline at end of file diff --git a/front/src/components/Footer/Footer.module.css b/front/src/components/Footer/Footer.module.css new file mode 100644 index 0000000..378c530 --- /dev/null +++ b/front/src/components/Footer/Footer.module.css @@ -0,0 +1,92 @@ +.footer { +color: #bfbfbf; +background: #000; +padding: 60px 0; +} + + +.container { +width: 90%; +margin: 0 auto; +display: flex; +align-items: flex-start; +justify-content: space-between; +} + + +.left { +display: flex; +flex-direction: column; +gap: 25px; +} + + +.logo { +width: 160px; +opacity: 0.9; +} + + +.copy { +font-size: 14px; +opacity: 0.7; +} + + +.center { +display: flex; +flex-direction: column; +gap: 14px; +font-size: 16px; +} + + +.center a { +cursor: pointer; +transition: 0.3s ease; +} + + +.center a:hover { +color: #fff; +} + + +.right { +text-align: right; +} + + +.phone { +color: #fff; +font-size: 20px; +font-weight: 600; +} + + +.mail { +margin-top: 6px; +display: inline-block; +font-size: 16px; +color: #bfbfbf; +} + + +.mail:hover { +color: #fff; +} + + +/* Mobile */ +@media (max-width: 900px) { +.container { +flex-direction: column; +gap: 40px; +text-align: center; +} + + +.right { +text-align: center; +} +} \ No newline at end of file diff --git a/front/src/components/Footer/Footer.tsx b/front/src/components/Footer/Footer.tsx new file mode 100644 index 0000000..ba75749 --- /dev/null +++ b/front/src/components/Footer/Footer.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import styles from "./Footer.module.css"; +import { NavLink } from "react-router-dom"; + +const Footer: React.FC = () => { +return ( + +); +}; + + +export default Footer; \ No newline at end of file diff --git a/front/src/components/Header/Header.module.css b/front/src/components/Header/Header.module.css new file mode 100644 index 0000000..556adcd --- /dev/null +++ b/front/src/components/Header/Header.module.css @@ -0,0 +1,146 @@ +.header { + /*background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);*/ + background-color: white; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + padding: 0; + position: fixed; + top: 0; + z-index: 1000; + backdrop-filter: blur(10px); + width: 100%; +} + +.phoneBlock a{ + font-size: 10px; +} + +.headerMenu { + display: flex; + justify-content: space-between; + align-items: center; + margin: 0 auto; + padding: 0 20px; +} + +.headerMenu a { + position: relative; + color: black; + text-decoration: none; + padding: 1.8rem 2rem; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-weight: 500; + font-size: 1.1rem; + transition: all 0.3s ease; + overflow: hidden; +} + +.headerMenu a::before { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 3px; + background: linear-gradient(90deg, var(--lavander), var(--light-blue)); + transition: all 0.3s ease; + transform: translateX(-50%); +} + +.headerMenu a::after { + content: ''; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); + transition: left 0.5s ease; +} + +.headeLogo { + display: flex; + align-items: center; + gap: 12px; + color: white; + text-decoration: none; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-weight: 700; + font-size: 1.4rem; + transition: all 0.3s ease; +} + +.logoImage { + height: 80px; + border-radius: 8px; + object-fit: contain; + transition: all 0.3s ease; + /*filter: brightness(0) invert(1);*/ +} + +.headerMenu a:hover { + background: rgba(255, 255, 255, 0.1); + transform: translateY(-2px); +} + +.headerMenu a:hover::before { + width: 80%; +} + +.headerMenu a:hover::after { + left: 100%; +} + +.headerMenu a.active { + background: rgba(255, 255, 255, 0.15); + font-weight: 600; +} + +.headerMenu a.active::before { + width: 100%; + background: linear-gradient(90deg, var(--lavander), var(--light-blue)); +} + +/* Анимация при загрузке */ +.headerMenu a { + animation: slideIn 0.6s ease forwards; + opacity: 0; + transform: translateY(-20px); +} + +.headerMenu a:nth-child(1) { animation-delay: 0.1s; } +.headerMenu a:nth-child(2) { animation-delay: 0.2s; } +.headerMenu a:nth-child(3) { animation-delay: 0.3s; } + +@keyframes slideIn { + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Адаптивность */ +@media (max-width: 768px) { + .headerMenu { + flex-direction: column; + padding: 0; + } + + .headerMenu a { + width: 100%; + text-align: center; + padding: 1rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + + .headerMenu a:last-child { + border-bottom: none; + } +} + +/* Эффект пульсации при наведении на кнопку выхода */ +.headerMenu a[onclick]:hover { + background: linear-gradient(135deg, #ff6b6b, #ee5a52); +} + +.headerMenu a[onclick]:hover::before { + background: linear-gradient(90deg, #ffd89b, #ff6b6b); +} \ No newline at end of file diff --git a/front/src/components/Header/Header.tsx b/front/src/components/Header/Header.tsx new file mode 100644 index 0000000..5974e1e --- /dev/null +++ b/front/src/components/Header/Header.tsx @@ -0,0 +1,26 @@ +import { NavLink } from "react-router-dom"; +import styles from './Header.module.css' +import logo from '../../assets/images/Logo.jpg' + +const Header = () =>{ + + return( + + ) +} + +export default Header; \ No newline at end of file diff --git a/front/src/components/ModalRequestForm/ModalRequestForm.module.css b/front/src/components/ModalRequestForm/ModalRequestForm.module.css new file mode 100644 index 0000000..223bef9 --- /dev/null +++ b/front/src/components/ModalRequestForm/ModalRequestForm.module.css @@ -0,0 +1,68 @@ +.modalBox { + padding: 10px 20px 30px; +} + +.title { + text-align: center; + font-size: 28px; + font-weight: 700; +} + +.subtitle { + text-align: center; + margin-top: 6px; + margin-bottom: 26px; + color: #666; + font-size: 16px; +} + +.form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.field { + position: relative; +} + +.input { + width: 93%; + padding: 18px; + font-size: 18px; + border-radius: 12px; + border: 1px solid #ddd; + background: #f9f9f9; + outline: none; + transition: 0.2s; +} + +.input:focus { + border-color: #ffd400; + background: white; +} + +.textarea { + height: 120px; + resize: none; + padding: 18px; + border-radius: 12px; + border: 1px solid #ddd; + background: #f9f9f9; + font-size: 18px; +} + +.error { + font-size: 13px; + color: #d33; + position: absolute; + bottom: -18px; + left: 5px; +} + +.button { + margin-top: 10px; + width: 100%; + height: 58px; + font-size: 18px; +} \ No newline at end of file diff --git a/front/src/components/ModalRequestForm/ModalRequestForm.tsx b/front/src/components/ModalRequestForm/ModalRequestForm.tsx new file mode 100644 index 0000000..f197dd7 --- /dev/null +++ b/front/src/components/ModalRequestForm/ModalRequestForm.tsx @@ -0,0 +1,202 @@ +import React from "react"; +import { Modal } from "antd"; +import { motion } from "framer-motion"; +import InputMask from "react-input-mask"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { yupResolver } from "@hookform/resolvers/yup"; +import styles from "./ModalRequestForm.module.css"; +import Button from "../Button/Button"; + +// ✅ Схема валидации +const schema = yup.object().shape({ + name: yup.string().required("Введите имя"), + email: yup.string().email("Некорректный Email").required("Введите Email"), + phone: yup.string().min(18, "Введите корректный номер").required("Введите номер"), + message: yup.string().nullable(), +}); + +interface Props { + open: boolean; + onClose: () => void; +} + +interface FormData { + name: string; + email: string; + phone: string; + message?: string | null; +} + +const ModalRequestForm: React.FC = ({ open, onClose }) => { + console.log('ModalRequestForm render, open:', open); + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + reset, + } = useForm({ resolver: yupResolver(schema) }); + + const handlePhoneInput = (e: React.ChangeEvent) => { + let value = e.target.value.replace(/\D/g, ''); + + if (value.startsWith('7') || value.startsWith('8')) { + value = value.substring(1); + } + + let formattedValue = '+7 '; + + if (value.length > 0) { + formattedValue += '(' + value.substring(0, 3); + } + if (value.length > 3) { + formattedValue += ') ' + value.substring(3, 6); + } + if (value.length > 6) { + formattedValue += '-' + value.substring(6, 8); + } + if (value.length > 8) { + formattedValue += '-' + value.substring(8, 10); + } + + e.target.value = formattedValue; + }; + + // ✅ Отправка данных на бэкенд + const sendToBackend = async (data: FormData): Promise => { + try { + const response = await fetch(`/api/requests`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...data, + source: "website-form", + timestamp: new Date().toISOString(), + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + return result.success; + } catch (error) { + console.error("Error sending request:", error); + throw new Error("Не удалось отправить заявку. Попробуйте позже."); + } + }; + + // ✅ Submit + const onSubmit = async (data: FormData) => { + try { + await sendToBackend(data); + + // Показываем уведомление об успехе + Modal.success({ + title: "Заявка отправлена!", + content: "Наш менеджер свяжется с вами в ближайшее время.", + okText: "Хорошо", + }); + + reset(); + onClose(); + } catch (error) { + // Показываем ошибку + Modal.error({ + title: "Ошибка", + content: error instanceof Error ? error.message : "Произошла неизвестная ошибка", + okText: "Понятно", + }); + } + }; + + return ( + + +

Контрактное производство

+

+ Заполните форму ниже, и наш менеджер свяжется с Вами +

+ +
+
+ + {errors.name && {errors.name.message}} +
+ +
+ + {errors.email && {errors.email.message}} +
+ + {/*
+ + {(inputProps: any) => ( + + )} + + {errors.phone && {errors.phone.message}} +
*/} +
+ + {errors.phone && {errors.phone.message}} +
+ +