From 8dc704c466695974b5c1767fc931cdb10da1f38f Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Thu, 20 Nov 2025 15:42:09 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D0=BA=D1=83=20main-page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/App.css | 42 ++++ front/src/api/catalogApi.ts | 14 ++ front/src/app/hooks.ts | 7 + front/src/app/routes.tsx | 20 ++ front/src/app/store.ts | 15 ++ front/src/assets/images/Logo.jpg | Bin 0 -> 34268 bytes front/src/components/Banner/Banner.module.css | 95 ++++++++ front/src/components/Banner/Banner.tsx | 73 +++++++ front/src/components/Button/Button.module.css | 22 ++ front/src/components/Button/Button.tsx | 21 ++ .../Categories/Categories.module.css | 127 +++++++++++ .../src/components/Categories/Categories.tsx | 85 ++++++++ .../ContactProduct/ContactProduct.module.css | 81 +++++++ .../ContactProduct/ContactProduct.tsx | 88 ++++++++ front/src/components/Footer/Footer.module.css | 92 ++++++++ front/src/components/Footer/Footer.tsx | 32 +++ front/src/components/Header/Header.module.css | 146 +++++++++++++ front/src/components/Header/Header.tsx | 26 +++ .../ModalRequestForm.module.css | 68 ++++++ .../ModalRequestForm/ModalRequestForm.tsx | 202 ++++++++++++++++++ .../QuestionsBlock/QuestionsBlock.module.css | 125 +++++++++++ .../QuestionsBlock/QuestionsBlock.tsx | 59 +++++ front/src/features/catalog/Catalog.module.css | 64 ++++++ .../src/features/catalog/CategoryFilters.tsx | 22 ++ front/src/features/catalog/ProductCard.tsx | 19 ++ front/src/features/catalog/ProductGrid.tsx | 20 ++ front/src/features/catalog/catalogSlice.ts | 78 +++++++ .../src/features/catalog/catalogeSidebar.tsx | 69 ++++++ front/src/features/catalog/categoriesSlice.ts | 51 +++++ front/src/index.css | 77 +++++++ front/src/main.tsx | 17 ++ front/src/pages/About/About.module.css | 0 front/src/pages/About/About.tsx | 9 + front/src/pages/Layout/Layout.module.css | 4 + front/src/pages/Layout/Layout.tsx | 24 +++ front/src/pages/Main/Main.module.css | 13 ++ front/src/pages/Main/Main.tsx | 26 +++ front/src/pages/Products/Products.module.css | 0 front/src/pages/Products/Products.tsx | 31 +++ front/src/types/categories.ts | 16 ++ front/src/types/product.ts | 8 + 41 files changed, 1988 insertions(+) create mode 100644 front/src/App.css create mode 100644 front/src/api/catalogApi.ts create mode 100644 front/src/app/hooks.ts create mode 100644 front/src/app/routes.tsx create mode 100644 front/src/app/store.ts create mode 100644 front/src/assets/images/Logo.jpg create mode 100644 front/src/components/Banner/Banner.module.css create mode 100644 front/src/components/Banner/Banner.tsx create mode 100644 front/src/components/Button/Button.module.css create mode 100644 front/src/components/Button/Button.tsx create mode 100644 front/src/components/Categories/Categories.module.css create mode 100644 front/src/components/Categories/Categories.tsx create mode 100644 front/src/components/ContactProduct/ContactProduct.module.css create mode 100644 front/src/components/ContactProduct/ContactProduct.tsx create mode 100644 front/src/components/Footer/Footer.module.css create mode 100644 front/src/components/Footer/Footer.tsx create mode 100644 front/src/components/Header/Header.module.css create mode 100644 front/src/components/Header/Header.tsx create mode 100644 front/src/components/ModalRequestForm/ModalRequestForm.module.css create mode 100644 front/src/components/ModalRequestForm/ModalRequestForm.tsx create mode 100644 front/src/components/QuestionsBlock/QuestionsBlock.module.css create mode 100644 front/src/components/QuestionsBlock/QuestionsBlock.tsx create mode 100644 front/src/features/catalog/Catalog.module.css create mode 100644 front/src/features/catalog/CategoryFilters.tsx create mode 100644 front/src/features/catalog/ProductCard.tsx create mode 100644 front/src/features/catalog/ProductGrid.tsx create mode 100644 front/src/features/catalog/catalogSlice.ts create mode 100644 front/src/features/catalog/catalogeSidebar.tsx create mode 100644 front/src/features/catalog/categoriesSlice.ts create mode 100644 front/src/index.css create mode 100644 front/src/main.tsx create mode 100644 front/src/pages/About/About.module.css create mode 100644 front/src/pages/About/About.tsx create mode 100644 front/src/pages/Layout/Layout.module.css create mode 100644 front/src/pages/Layout/Layout.tsx create mode 100644 front/src/pages/Main/Main.module.css create mode 100644 front/src/pages/Main/Main.tsx create mode 100644 front/src/pages/Products/Products.module.css create mode 100644 front/src/pages/Products/Products.tsx create mode 100644 front/src/types/categories.ts create mode 100644 front/src/types/product.ts 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 0000000000000000000000000000000000000000..9c792124a198f52355d97eb47c76e51514f999d2 GIT binary patch literal 34268 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 literal 0 HcmV?d00001 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}} +
+ +