From 267ab815b4de8272433c427ac05873021458f228 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Fri, 21 Nov 2025 18:41:48 +0300 Subject: [PATCH 01/67] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=82=D0=B5=D0=BB=D0=B5=D1=84=D0=BE=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D0=B0=20?= =?UTF-8?q?=D0=B8=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=20=D0=B2=20=D0=BE=D1=82?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QuestionsBlock/QuestionsBlock.tsx | 6 +- .../src/features/MainPage/MainPage.module.css | 26 +++++ front/src/features/MainPage/PhoneRequest.tsx | 105 ++++++++++++++++++ 3 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 front/src/features/MainPage/MainPage.module.css create mode 100644 front/src/features/MainPage/PhoneRequest.tsx diff --git a/front/src/components/QuestionsBlock/QuestionsBlock.tsx b/front/src/components/QuestionsBlock/QuestionsBlock.tsx index 8139ead..79fc316 100644 --- a/front/src/components/QuestionsBlock/QuestionsBlock.tsx +++ b/front/src/components/QuestionsBlock/QuestionsBlock.tsx @@ -2,6 +2,7 @@ import React from "react"; import styles from "./QuestionsBlock.module.css"; import Button from "../Button/Button"; import { PhoneOutlined } from "@ant-design/icons"; +import PhoneRequest from "../../features/MainPage/PhoneRequest"; const QuestionsBlock: React.FC = () => { @@ -39,7 +40,7 @@ const QuestionsBlock: React.FC = () => {

{/* FORM */} -
+ {/*
{ label="ОТПРАВИТЬ" onClick={handleClick} /> -
+
*/} + diff --git a/front/src/features/MainPage/MainPage.module.css b/front/src/features/MainPage/MainPage.module.css new file mode 100644 index 0000000..1151440 --- /dev/null +++ b/front/src/features/MainPage/MainPage.module.css @@ -0,0 +1,26 @@ +.form { + margin-top: 20px; + display: flex; + align-items: center; + gap: 20px; +} + +.input { + flex: 1; + padding: 18px 24px; + border-radius: 50px; + border: none; + font-size: 18px; + outline: none; +} + +.error { + color: red; + margin-top: 10px; +} + +.success { + color: green; + margin-top: 10px; +} + diff --git a/front/src/features/MainPage/PhoneRequest.tsx b/front/src/features/MainPage/PhoneRequest.tsx new file mode 100644 index 0000000..228b614 --- /dev/null +++ b/front/src/features/MainPage/PhoneRequest.tsx @@ -0,0 +1,105 @@ +import React, { useState } from "react"; +import axios from "axios"; +import styles from "./MainPage.module.css"; +import Button from "../../components/Button/Button"; + +interface Props { + onSend?: (phone: string) => Promise; +} + +const PhoneRequest: React.FC = ({ onSend }) => { + const [phone, setPhone] = useState(""); + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const [error, setError] = useState(""); + + // Автоформатирование "+7 (999) 999-99-99" + const formatPhone = (value: string) => { + const digits = value.replace(/\D/g, ""); + + let result = "+7 "; + if (digits.length > 1) result += "(" + digits.substring(1, 4); + if (digits.length >= 4) result += ") " + digits.substring(4, 7); + if (digits.length >= 7) result += "-" + digits.substring(7, 9); + if (digits.length >= 9) result += "-" + digits.substring(9, 11); + + return result; + }; + + const handlePhoneChange = (e: React.ChangeEvent) => { + let v = e.target.value; + + if (v.replace(/\D/g, "").length === 0) { + setPhone(""); + return; + } + + if (v[0] !== "+") { + v = "+7" + v; + } + + setPhone(formatPhone(v)); + }; + + const validatePhone = (p: string) => { + return p.replace(/\D/g, "").length === 11; + }; + + const handleSubmit = async () => { + setError(""); + + if (!validatePhone(phone)) { + setError("Введите корректный номер телефона"); + return; + } + + setLoading(true); + + try { + if (onSend) { + await onSend(phone); + } else { + + await axios.post("/api/contact/send-phone", { phone }); + } + + setSuccess(true); + setPhone(""); + + setTimeout(() => setSuccess(false), 3000); + } catch (e) { + setError("Ошибка отправки. Попробуйте позже."); + } + + setLoading(false); + }; + + return ( +
+ + */} + + {error &&

{error}

} + {success &&

Заявка отправлена!

} +
+ ); +}; + +export default PhoneRequest; \ No newline at end of file -- 2.54.0 From 7c14baca77778d700b2ad6db8495d674d4e4f949 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Sat, 22 Nov 2025 23:48:15 +0300 Subject: [PATCH 02/67] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B2=D0=BE=D1=80=D0=BC=D1=83=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B9=D1=81-=D0=BB=D0=B8=D1=81=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PriceLisForm/PriceLisForm.module.css | 0 .../components/PriceLisForm/PriceLisForm.tsx | 149 ++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 front/src/components/PriceLisForm/PriceLisForm.module.css create mode 100644 front/src/components/PriceLisForm/PriceLisForm.tsx diff --git a/front/src/components/PriceLisForm/PriceLisForm.module.css b/front/src/components/PriceLisForm/PriceLisForm.module.css new file mode 100644 index 0000000..e69de29 diff --git a/front/src/components/PriceLisForm/PriceLisForm.tsx b/front/src/components/PriceLisForm/PriceLisForm.tsx new file mode 100644 index 0000000..d7c04e7 --- /dev/null +++ b/front/src/components/PriceLisForm/PriceLisForm.tsx @@ -0,0 +1,149 @@ +import React from "react"; +import { Modal } from "antd"; +import { motion } from "framer-motion"; +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("Введите номер"), +}); + +interface Props { + open: boolean; + onClose: () => void; +} + +interface FormData { + name: string; + email: string; + phone: string; +} + +const PriceLisForm: 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 => { + return true; + }; + + const onSubmit = async (data: FormData) => { + try { + await sendToBackend(data); + + Modal.success({ + title: "Прайс-лист отправлен!", + 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}} +
+ +
+ + {errors.phone && {errors.phone.message}} +
+ +
diff --git a/front/src/features/catalog/productsSlice.ts b/front/src/features/catalog/productsSlice.ts index 17c4591..acbabd6 100644 --- a/front/src/features/catalog/productsSlice.ts +++ b/front/src/features/catalog/productsSlice.ts @@ -32,6 +32,14 @@ export const fetchProductsByCategory = createAsyncThunk( } ); +export const fetchProductById = createAsyncThunk( + "product/fetchById", + async (id: string) => { + const response = await axios.get(`/api/products/${id}`); + return response.data; + } +); + interface ProductsState { items: Product[]; total: number; diff --git a/front/src/pages/ItemPage/ItemPage.module.css b/front/src/pages/ItemPage/ItemPage.module.css new file mode 100644 index 0000000..e627a65 --- /dev/null +++ b/front/src/pages/ItemPage/ItemPage.module.css @@ -0,0 +1,93 @@ +.page { + background: #f5f5f5; + min-height: 100vh; +} + +.top { + background: #000; + padding: 15px 40px; +} + +.back { + color: white; + background: none; + border: none; + cursor: pointer; + font-weight: 600; +} + +.hero { + display: flex; + min-height: 500px; +} + +.left { + flex: 1; + background: #fff; + display: flex; + align-items: center; + justify-content: center; +} + +.left img { + max-width: 400px; +} + +.right { + flex: 1; + background: linear-gradient( + rgba(0, 0, 0, 0.7), + rgba(0, 0, 0, 0.7) + ), + url("/bg.jpg"); + color: white; + padding: 80px; + display: flex; + flex-direction: column; + justify-content: center; +} + +.breadcrumbs { + color: #f5c400; + margin-bottom: 20px; +} + +.download { + margin-top: 30px; + background: #f5c400; + border: none; + padding: 15px 40px; + border-radius: 30px; + font-weight: bold; + cursor: pointer; +} + +.tabs { + display: flex; + border-top: 1px solid #ddd; +} + +.tabs button { + flex: 1; + padding: 20px; + border: none; + background: #eee; + cursor: pointer; +} + +.active { + background: white; + font-weight: bold; +} + +.content { + padding: 40px; + background: white; +} + +.attrRow { + display: flex; + justify-content: space-between; + padding: 10px 0; + border-bottom: 1px solid #eee; +} \ No newline at end of file diff --git a/front/src/pages/ItemPage/ItemPage.tsx b/front/src/pages/ItemPage/ItemPage.tsx new file mode 100644 index 0000000..fb25f80 --- /dev/null +++ b/front/src/pages/ItemPage/ItemPage.tsx @@ -0,0 +1,96 @@ +import { useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { useAppDispatch, useAppSelector } from "../../app/hooks"; +import { fetchProductById } from "../../features/catalog/productsSlice"; +import styles from "./ProductPage.module.css"; +//import type { Product } from "../../types/product"; +import type { CategoryAtributes } from "../../types/categories"; + +const ItemPage = () => { + const { id } = useParams(); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const { items, loading } = useAppSelector((state) => state.products); + const { items: cats, selectedCategoryId } = useAppSelector(s => s.categories); + //const filters = useAppSelector(s => s.products.filtersApplied); + + const [activeTab, setActiveTab] = useState<"desc" | "chars">("desc"); + + useEffect(() => { + if (id) dispatch(fetchProductById(id)); + }, [id]); + + const product = items.find((item) => item.id === Number(id)); + const category = cats.find((cat) => cat.id === Number(selectedCategoryId)) + + if ((loading || !product)) return
Загрузка...
; + if(!category) return
Загрузка...
; + + return ( +
+
+ +
+ +
+
+ {product.name} +
+ +
+
+ ГЛАВНАЯ / {category?.name} / {product.name} +
+ +

{product.name}

+ +

{product.description}

+ + +
+
+ +
+ + +
+ +
+ {activeTab === "desc" && ( +
+

ОПИСАНИЕ

+

{product.description}

+
+ )} + + {activeTab === "chars" && ( +
+

ХАРАКТЕРИСТИКИ

+ {category.atributes?.map((attr: CategoryAtributes) => ( +
+ {attr.name} + {} +
+ ))} +
+ )} +
+
+ ); +} + +export default ItemPage; \ No newline at end of file -- 2.54.0 From 5b32ece367081ec4d16a910f447a00145eeb9898 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Tue, 10 Feb 2026 21:30:43 +0300 Subject: [PATCH 08/67] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20vite=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/vite.config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/front/vite.config.ts b/front/vite.config.ts index 8b0f57b..705517a 100644 --- a/front/vite.config.ts +++ b/front/vite.config.ts @@ -4,4 +4,8 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + server: { + port: 5173, + host: true, // попробуйте добавить эту строку + } }) -- 2.54.0 From e142ee7998435178b9f7641ce9746fb2564ecca5 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Wed, 11 Feb 2026 16:50:36 +0300 Subject: [PATCH 09/67] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D1=8B=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20=D1=81=D0=BE=20=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D0=BB=D1=8F=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/package-lock.json | 7 ++-- front/package.json | 1 + front/src/api/client.ts | 2 +- front/src/features/catalog/Catalog.module.css | 41 ++++++++++++++++++- front/src/features/catalog/CategoriesGrid.tsx | 10 ++--- front/src/features/catalog/CategoryCards.tsx | 2 +- .../src/features/catalog/CategoryFilters.tsx | 4 +- front/src/features/catalog/Pagination.tsx | 2 +- .../features/catalog/SidebarCategories.tsx | 2 +- front/src/pages/Products/Products.tsx | 2 +- 10 files changed, 56 insertions(+), 17 deletions(-) diff --git a/front/package-lock.json b/front/package-lock.json index 89ac0af..f209b10 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -30,6 +30,7 @@ "@types/react-input-mask": "^3.0.6", "@types/react-redux": "^7.1.34", "@vitejs/plugin-react": "^5.0.4", + "baseline-browser-mapping": "^2.9.19", "eslint": "^9.36.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.22", @@ -2346,9 +2347,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.25", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz", - "integrity": "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==", + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/front/package.json b/front/package.json index ec40eb4..44f3321 100644 --- a/front/package.json +++ b/front/package.json @@ -32,6 +32,7 @@ "@types/react-input-mask": "^3.0.6", "@types/react-redux": "^7.1.34", "@vitejs/plugin-react": "^5.0.4", + "baseline-browser-mapping": "^2.9.19", "eslint": "^9.36.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.22", diff --git a/front/src/api/client.ts b/front/src/api/client.ts index 4188ab4..eb9f9fb 100644 --- a/front/src/api/client.ts +++ b/front/src/api/client.ts @@ -1,6 +1,6 @@ import axios from "axios"; -export const API_BASE = g;/* process.env.REACT_APP_API_BASE || "";*/ +export const API_BASE = ""; const client = axios.create({ baseURL: API_BASE, headers: { "Content-Type": "application/json" }, diff --git a/front/src/features/catalog/Catalog.module.css b/front/src/features/catalog/Catalog.module.css index 835d39e..4dcf160 100644 --- a/front/src/features/catalog/Catalog.module.css +++ b/front/src/features/catalog/Catalog.module.css @@ -4,9 +4,9 @@ gap: 40px; } -.sidebar { +/*.sidebar { width: 260px; -} +}*/ .productsSection { flex: 1; @@ -54,6 +54,43 @@ color: #ffcc00; } +/*SidebarCategories*/ +.sidebar { width: 260px; padding: 20px 0; } +.title { font-weight:700; font-size:14px; margin-bottom:16px; } +.list { list-style:none; padding:0; margin:0; } +.item { padding:8px 0; color:#666; cursor:pointer; transition:color .15s; } +.item:hover { color:#000; } +.active { color:#000; font-weight:700; } +/*SidebarCategories*/ + +/*Pagination*/ +.wrap{ display:flex; gap:12px; align-items:center; justify-content:center; margin:30px 0;} +.go{ padding:8px 14px; background:#fff;border:1px solid #ddd; cursor:pointer;} +.pages{ display:flex; gap:8px;} +.page{ padding:8px 12px; background:#fff;border:1px solid #ddd; cursor:pointer;} +.active{ background:#ffd400; font-weight:700; border-color:#ffd400;} +button:disabled{ opacity:.5; cursor:default;} +/*Pagination*/ + +/*CategoryFilters*/ +.filtersWrap { margin:24px 0; } +.title { font-weight:700; margin-bottom:12px; } +.block { margin-bottom:16px; } +.blockTitle{ font-weight:600; margin-bottom:8px; } +.opt { display:block; margin:6px 0; font-size:14px; } +.actions { margin-top:12px; display:flex; gap:12px; } +.apply { background:#ffd400; border:none; padding:10px 22px; border-radius:26px; cursor:pointer;} +.reset { background:#fff; border:1px solid #ddd; padding:8px 16px; cursor:pointer;} +/*CategoryFilters*/ + +.categoryGrid { display:grid; grid-template-columns:repeat(3, 1fr); gap:20px; } +.categoryCard { border:1px solid #e7e7e7; padding:26px; text-align:center; cursor:pointer; transition:box-shadow .15s; } +.categoryCard:hover { box-shadow:0 6px 18px rgba(0,0,0,0.06); } +.categoryImg { max-width:160px; height:auto; display:block; margin:0 auto 14px; } +.categoryName { font-weight:700; } +@media(max-width:900px){ .grid{grid-template-columns:repeat(2,1fr)} } +@media(max-width:600px){ .grid{grid-template-columns:1fr} } + @media (max-width: 900px) { .catalogWrapper { flex-direction: column; diff --git a/front/src/features/catalog/CategoriesGrid.tsx b/front/src/features/catalog/CategoriesGrid.tsx index 7eab034..1240905 100644 --- a/front/src/features/catalog/CategoriesGrid.tsx +++ b/front/src/features/catalog/CategoriesGrid.tsx @@ -1,5 +1,5 @@ import React from "react"; -import styles from "./CategoryCards.module.css"; +import styles from "./Catalog.module.css"; import { useAppSelector } from "../../app/hooks"; interface Props { onSelect?: (id:number) => void } @@ -7,11 +7,11 @@ const CategoryCards: React.FC = ({ onSelect }) => { const categories = useAppSelector(s => s.categories.items); return ( -
+
{categories.map(c => ( -
onSelect?.(c.id)}> - {c.image ? {c.name} : {c.name}} -
{c.name}
+
onSelect?.(c.id)}> + {c.image ? {c.name} : {c.name}} +
{c.name}
))}
diff --git a/front/src/features/catalog/CategoryCards.tsx b/front/src/features/catalog/CategoryCards.tsx index 8f07c5d..22c3f0b 100644 --- a/front/src/features/catalog/CategoryCards.tsx +++ b/front/src/features/catalog/CategoryCards.tsx @@ -1,5 +1,5 @@ import React from "react"; -import styles from "./CategoryCards.module.css"; +import styles from "./Catalog.module.css"; import { useAppSelector } from "../../app/hooks"; interface Props { diff --git a/front/src/features/catalog/CategoryFilters.tsx b/front/src/features/catalog/CategoryFilters.tsx index a086911..b41b607 100644 --- a/front/src/features/catalog/CategoryFilters.tsx +++ b/front/src/features/catalog/CategoryFilters.tsx @@ -1,6 +1,6 @@ import { useAppSelector, useAppDispatch } from "../../app/hooks"; import { setSelectedCategory } from "./categoriesSlice"; -import styles from "./CategoriesSidebar.module.css"; +import styles from "./Catalog.module.css"; import { setFilters, fetchProductsByCategory, setPage } from "./productsSlice"; import { useState } from "react"; @@ -19,7 +19,7 @@ export const CategoryFilters = () => { if (!cat) return null; return ( -
+

ПАРАМЕТРЫ

{cat.atributes.map(attr => ( diff --git a/front/src/features/catalog/Pagination.tsx b/front/src/features/catalog/Pagination.tsx index 939d2a9..9dcff40 100644 --- a/front/src/features/catalog/Pagination.tsx +++ b/front/src/features/catalog/Pagination.tsx @@ -1,5 +1,5 @@ import React from "react"; -import styles from "./Pagination.module.css"; +import styles from "./Catalog.module.css"; import { useAppSelector, useAppDispatch } from "../../app/hooks"; import { setPage, fetchProductsByCategory } from "./productsSlice"; diff --git a/front/src/features/catalog/SidebarCategories.tsx b/front/src/features/catalog/SidebarCategories.tsx index b573e26..33eeae6 100644 --- a/front/src/features/catalog/SidebarCategories.tsx +++ b/front/src/features/catalog/SidebarCategories.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from "react"; -import styles from "./SidebarCategories.module.css"; +import styles from "./Catalog.module.css"; import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { fetchCategories, setSelectedCategory } from "./categoriesSlice"; import { fetchProductsByCategory, setFilters, setPage } from "./productsSlice"; diff --git a/front/src/pages/Products/Products.tsx b/front/src/pages/Products/Products.tsx index a4bf779..9961514 100644 --- a/front/src/pages/Products/Products.tsx +++ b/front/src/pages/Products/Products.tsx @@ -1,5 +1,5 @@ import React from "react"; -import styles from "./ProductsPage.module.css"; +import styles from "./Products.module.css"; import SidebarCategories from "../../features/catalog/SidebarCategories"; import CategoryCards from "../../features/catalog/CategoryCards"; import Filters from "../../features/catalog/CategoryFilters"; -- 2.54.0 From a18a7063663d49326afddd01a56bbd600330a3b0 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Wed, 11 Feb 2026 17:43:21 +0300 Subject: [PATCH 10/67] =?UTF-8?q?=D0=B4=D0=BE=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D0=BB=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D1=85=D0=B0=D1=80?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=B5=D1=80=D0=B8=D1=81=D1=82=D0=B8=D0=BA=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B4=D1=83=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/pages/ItemPage/ItemPage.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/front/src/pages/ItemPage/ItemPage.tsx b/front/src/pages/ItemPage/ItemPage.tsx index fb25f80..3436661 100644 --- a/front/src/pages/ItemPage/ItemPage.tsx +++ b/front/src/pages/ItemPage/ItemPage.tsx @@ -3,8 +3,7 @@ import { useParams, useNavigate } from "react-router-dom"; import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { fetchProductById } from "../../features/catalog/productsSlice"; import styles from "./ProductPage.module.css"; -//import type { Product } from "../../types/product"; -import type { CategoryAtributes } from "../../types/categories"; +import type { CategoryAtributes, CategoryAtrVals } from "../../types/categories"; const ItemPage = () => { const { id } = useParams(); @@ -12,7 +11,6 @@ const ItemPage = () => { const navigate = useNavigate(); const { items, loading } = useAppSelector((state) => state.products); const { items: cats, selectedCategoryId } = useAppSelector(s => s.categories); - //const filters = useAppSelector(s => s.products.filtersApplied); const [activeTab, setActiveTab] = useState<"desc" | "chars">("desc"); @@ -83,7 +81,11 @@ const ItemPage = () => { {category.atributes?.map((attr: CategoryAtributes) => (
{attr.name} - {} + {attr.vals?.map((val: CategoryAtrVals) => ( +
+ {val.val} +
+ ))}
))}
-- 2.54.0 From 5451970f02d74d50e89c34e94469be6fe4e804d0 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Tue, 17 Mar 2026 15:09:45 +0300 Subject: [PATCH 11/67] =?UTF-8?q?md=20=D1=81=20api,=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=B7=D0=B0=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/README.md | 155 +++++++++++++++++- front/src/features/catalog/categoriesSlice.ts | 2 +- front/src/features/catalog/productsSlice.ts | 9 +- 3 files changed, 157 insertions(+), 9 deletions(-) diff --git a/front/README.md b/front/README.md index 4c83312..9b24cc2 100644 --- a/front/README.md +++ b/front/README.md @@ -1,17 +1,158 @@ -# React + TypeScript + Vite +# API -## fetchCategories: +## Категории -* get запрос по /api/categories +### Получение всех категорий +**Запрос:** `GET /api/categories` -* возвращает массив объектов типа Category +**Получаю:** +```json +[ + { + "id": 1, + "name": "для внутренней канализации", + "image": "/images/hghfghgd.jpg", + "atributes": [ + { + "id": 1, + "name": "цвет", + "vals": [ + { "id": 1001, "val": "оранжевый" }, + { "id": 1002, "val": "серый" }, + ] + }, + { + "id": 2, + "name": "размер", + "vals": [ + { "id": 2001, "val": "..." }, + { "id": 2002, "val": "...." }, + { "id": 2003, "val": "...." } + ] + }, + { + "id": 2, + "name": "кол-во в упаковке", + "vals": [ + { "id": 2001, "val": "2" }, + { "id": 2002, "val": "4" }, + ] + } + ] + }, + { + "id": 1, + "name": "для внешней канализации", + "image": "/images/sfjsjsjf.jpg", + "atributes": [ + { + "id": 1, + "name": "цвет", + "vals": [ + { "id": 1001, "val": "оранжевый" }, + { "id": 1002, "val": "серый" }, + ] + }, + { + "id": 2, + "name": "размер", + "vals": [ + { "id": 2001, "val": "..." }, + { "id": 2002, "val": "...." }, + { "id": 2003, "val": "...." } + ] + }, + { + "id": 2, + "name": "кол-во в упаковке", + "vals": [ + { "id": 2001, "val": "2" }, + { "id": 2002, "val": "4" }, + ] + } + ] + } +] +``` +## Товары (Products) -## fetchProductsByCategory +### Получение товаров по id категории с фильтрацией +**Запрос:** `GET /products` -* параметры: categoryId, page = 1, limit = 9, filters: string | number | (string | number)[] +**Параметры запроса (Query Parameters):** -* ожидаю: { items: Product[], total: number, page: number, limit: number } Сериализация превратит это в query-параметры +| Параметр | Тип | Обязательный | Описание | Пример | +|----------|-----|--------------|----------|---------| +| `categoryId` | number | Да | ID категории | `categoryId=1` | +| `page` | number | Нет | Номер страницы (по умолчанию 1) | `page=2` | +| `limit` | number | Нет | Количество товаров на странице (по умолчанию 9) | `limit=12` | +| `filters` | object | Нет | Фильтры по атрибутам | `size=110х2,2х500&color=оранжевый` | +**Примеры запросов:** +1. **Базовый запрос:** +``` +GET /products?categoryId=1&page=1&limit=9 +``` +2. **С фильтрами (одиночные значения):** +``` +GET /products?categoryId=1&page=1&limit=9&size=110х2,2х500&color=оранжевый +``` + +3. **С фильтрами (массив значений):** +``` +GET /products?categoryId=1&page=1&limit=9&size=110х2,2х500,110х2,2х1500&color=серый +``` + +**Получаю:** +```json +{ + "items": [ + { + "id": 101, + "name": "Труба ПВХ", + "description": "бля бля", + "price": 799, + "categoryId": 1, + "images": ["/images/s23-1.jpg", "/images/s23-2.jpg"], + "attributes": { + "color": "серый", + "amount": "4", + "size": "110х2,2х500" + }, + "inStock": true, + } + ], + "total": 45, + "page": 1, + "limit": 9 +} +``` + +### Получение товаров по id +**Запрос:** `GET/product/fetchById` + +**Параметры запроса:** +- `id` (path parameter) - уникальный идентификатор товара + +**Пример запроса:** +``` +GET /api/products/101 +``` + +**Получаю:** +```json +{ + "id": 101, + "name": "ПВХ труба", + "description": "бля бля", + "price": 799, + "categoryId": 1, + "images": [ + "/images/s23-1.jpg", + "/images/s23-2.jpg", + "/images/s23-3.jpg" + ] +} \ No newline at end of file diff --git a/front/src/features/catalog/categoriesSlice.ts b/front/src/features/catalog/categoriesSlice.ts index 4c38a87..4c0685c 100644 --- a/front/src/features/catalog/categoriesSlice.ts +++ b/front/src/features/catalog/categoriesSlice.ts @@ -5,7 +5,7 @@ import axios from "axios"; export const fetchCategories = createAsyncThunk( "categories/fetchCategories", async () => { - const res = await axios.get("/api/categories"); + const res = await axios.get("/api/categories"); return res.data; } ); diff --git a/front/src/features/catalog/productsSlice.ts b/front/src/features/catalog/productsSlice.ts index acbabd6..45b9af2 100644 --- a/front/src/features/catalog/productsSlice.ts +++ b/front/src/features/catalog/productsSlice.ts @@ -11,6 +11,13 @@ interface FetchParams { filters?: Record; } +interface FetchResponce{ + items: Product[], + total: number, + page: number, + limit: number, +} + export const fetchProductsByCategory = createAsyncThunk( "products/fetchByCategory", async (params: FetchParams) => { @@ -26,7 +33,7 @@ export const fetchProductsByCategory = createAsyncThunk( else qp.append(k, String(v)); }); } - const res = await client.get(`/products?${qp.toString()}`); + const res = await client.get(`/products?${qp.toString()}`); // ожидаем { items: Product[], total: number, page: number, limit: number } return res.data; } -- 2.54.0 From 8aa8e0407708699558eca0be852e13970e4faa2d Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 30 Mar 2026 12:28:11 +0300 Subject: [PATCH 12/67] feat(): Added ci restruct project and add dockerfile --- .forgejo/workflows/demo.yaml | 53 ++++++++++++++++++ front/.gitignore => .gitignore | 0 Dockerfile | 10 ++++ front/README.md => README.md | 0 front/eslint.config.js => eslint.config.js | 0 front/vite.config.ts | 11 ---- front/index.html => index.html | 0 front/package-lock.json => package-lock.json | 0 front/package.json => package.json | 0 {front/src => src}/App.css | 0 {front/src => src}/api/catalogApi.ts | 0 {front/src => src}/api/client.ts | 0 {front/src => src}/app/hooks.ts | 0 {front/src => src}/app/routes.tsx | 0 {front/src => src}/app/store.ts | 0 {front/src => src}/assets/images/Logo.jpg | Bin .../images/mainPage/296170994030930.jpg | Bin ...ChatGPT-Image-Oct-1_-2025_-05_29_18-PM.png | Bin .../src => src}/assets/images/mainPage/_.jpg | Bin .../images/mainPage/category_internal1.jpg | Bin .../images/mainPage/category_internal2.jpg | Bin .../assets/images/mainPage/orig.jpg | Bin .../components/Banner/Banner.module.css | 0 .../src => src}/components/Banner/Banner.tsx | 0 .../components/Button/Button.module.css | 0 .../src => src}/components/Button/Button.tsx | 0 .../Categories/Categories.module.css | 0 .../components/Categories/Categories.tsx | 0 .../ContactProduct/ContactProduct.module.css | 0 .../ContactProduct/ContactProduct.tsx | 0 .../components/Footer/Footer.module.css | 0 .../src => src}/components/Footer/Footer.tsx | 0 .../components/Header/Header.module.css | 0 .../src => src}/components/Header/Header.tsx | 0 .../ModalRequestForm.module.css | 0 .../ModalRequestForm/ModalRequestForm.tsx | 0 .../PriceLisForm/PriceLisForm.module.css | 0 .../components/PriceLisForm/PriceLisForm.tsx | 0 .../QuestionsBlock/QuestionsBlock.module.css | 0 .../QuestionsBlock/QuestionsBlock.tsx | 0 .../features/MainPage/MainPage.module.css | 0 .../features/MainPage/PhoneRequest.tsx | 0 .../features/catalog/Catalog.module.css | 0 .../features/catalog/CategoriesGrid.tsx | 0 .../features/catalog/CategoryCards.tsx | 0 .../features/catalog/CategoryFilters.tsx | 0 .../features/catalog/Pagination.tsx | 0 .../features/catalog/ProductCard.tsx | 0 .../features/catalog/ProductGrid.tsx | 0 .../features/catalog/SidebarCategories.tsx | 0 .../features/catalog/categoriesSlice.ts | 0 .../features/catalog/productsSlice.ts | 0 {front/src => src}/index.css | 0 {front/src => src}/main.tsx | 0 .../src => src}/pages/About/About.module.css | 0 {front/src => src}/pages/About/About.tsx | 0 .../pages/ItemPage/ItemPage.module.css | 0 .../src => src}/pages/ItemPage/ItemPage.tsx | 0 .../pages/Layout/Layout.module.css | 0 {front/src => src}/pages/Layout/Layout.tsx | 0 {front/src => src}/pages/Main/Main.module.css | 0 {front/src => src}/pages/Main/Main.tsx | 0 .../pages/Products/Products.module.css | 0 .../src => src}/pages/Products/Products.tsx | 0 {front/src => src}/types/categories.ts | 0 {front/src => src}/types/product.ts | 0 front/tsconfig.app.json => tsconfig.app.json | 0 front/tsconfig.json => tsconfig.json | 0 .../tsconfig.node.json => tsconfig.node.json | 0 vite.config.ts | 18 ++++++ 70 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 .forgejo/workflows/demo.yaml rename front/.gitignore => .gitignore (100%) create mode 100644 Dockerfile rename front/README.md => README.md (100%) rename front/eslint.config.js => eslint.config.js (100%) delete mode 100644 front/vite.config.ts rename front/index.html => index.html (100%) rename front/package-lock.json => package-lock.json (100%) rename front/package.json => package.json (100%) rename {front/src => src}/App.css (100%) rename {front/src => src}/api/catalogApi.ts (100%) rename {front/src => src}/api/client.ts (100%) rename {front/src => src}/app/hooks.ts (100%) rename {front/src => src}/app/routes.tsx (100%) rename {front/src => src}/app/store.ts (100%) rename {front/src => src}/assets/images/Logo.jpg (100%) rename {front/src => src}/assets/images/mainPage/296170994030930.jpg (100%) rename {front/src => src}/assets/images/mainPage/ChatGPT-Image-Oct-1_-2025_-05_29_18-PM.png (100%) rename {front/src => src}/assets/images/mainPage/_.jpg (100%) rename {front/src => src}/assets/images/mainPage/category_internal1.jpg (100%) rename {front/src => src}/assets/images/mainPage/category_internal2.jpg (100%) rename {front/src => src}/assets/images/mainPage/orig.jpg (100%) rename {front/src => src}/components/Banner/Banner.module.css (100%) rename {front/src => src}/components/Banner/Banner.tsx (100%) rename {front/src => src}/components/Button/Button.module.css (100%) rename {front/src => src}/components/Button/Button.tsx (100%) rename {front/src => src}/components/Categories/Categories.module.css (100%) rename {front/src => src}/components/Categories/Categories.tsx (100%) rename {front/src => src}/components/ContactProduct/ContactProduct.module.css (100%) rename {front/src => src}/components/ContactProduct/ContactProduct.tsx (100%) rename {front/src => src}/components/Footer/Footer.module.css (100%) rename {front/src => src}/components/Footer/Footer.tsx (100%) rename {front/src => src}/components/Header/Header.module.css (100%) rename {front/src => src}/components/Header/Header.tsx (100%) rename {front/src => src}/components/ModalRequestForm/ModalRequestForm.module.css (100%) rename {front/src => src}/components/ModalRequestForm/ModalRequestForm.tsx (100%) rename {front/src => src}/components/PriceLisForm/PriceLisForm.module.css (100%) rename {front/src => src}/components/PriceLisForm/PriceLisForm.tsx (100%) rename {front/src => src}/components/QuestionsBlock/QuestionsBlock.module.css (100%) rename {front/src => src}/components/QuestionsBlock/QuestionsBlock.tsx (100%) rename {front/src => src}/features/MainPage/MainPage.module.css (100%) rename {front/src => src}/features/MainPage/PhoneRequest.tsx (100%) rename {front/src => src}/features/catalog/Catalog.module.css (100%) rename {front/src => src}/features/catalog/CategoriesGrid.tsx (100%) rename {front/src => src}/features/catalog/CategoryCards.tsx (100%) rename {front/src => src}/features/catalog/CategoryFilters.tsx (100%) rename {front/src => src}/features/catalog/Pagination.tsx (100%) rename {front/src => src}/features/catalog/ProductCard.tsx (100%) rename {front/src => src}/features/catalog/ProductGrid.tsx (100%) rename {front/src => src}/features/catalog/SidebarCategories.tsx (100%) rename {front/src => src}/features/catalog/categoriesSlice.ts (100%) rename {front/src => src}/features/catalog/productsSlice.ts (100%) rename {front/src => src}/index.css (100%) rename {front/src => src}/main.tsx (100%) rename {front/src => src}/pages/About/About.module.css (100%) rename {front/src => src}/pages/About/About.tsx (100%) rename {front/src => src}/pages/ItemPage/ItemPage.module.css (100%) rename {front/src => src}/pages/ItemPage/ItemPage.tsx (100%) rename {front/src => src}/pages/Layout/Layout.module.css (100%) rename {front/src => src}/pages/Layout/Layout.tsx (100%) rename {front/src => src}/pages/Main/Main.module.css (100%) rename {front/src => src}/pages/Main/Main.tsx (100%) rename {front/src => src}/pages/Products/Products.module.css (100%) rename {front/src => src}/pages/Products/Products.tsx (100%) rename {front/src => src}/types/categories.ts (100%) rename {front/src => src}/types/product.ts (100%) rename front/tsconfig.app.json => tsconfig.app.json (100%) rename front/tsconfig.json => tsconfig.json (100%) rename front/tsconfig.node.json => tsconfig.node.json (100%) create mode 100644 vite.config.ts diff --git a/.forgejo/workflows/demo.yaml b/.forgejo/workflows/demo.yaml new file mode 100644 index 0000000..90a4898 --- /dev/null +++ b/.forgejo/workflows/demo.yaml @@ -0,0 +1,53 @@ +name: Build, Test and Push Flask App + +on: + push: + branches: [ main, master ] + +jobs: + docker-build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Docker CLI + run: | + apt-get update + apt-get install -y docker.io + docker --version + + - name: Configure Docker CLI for insecure registry + run: | + mkdir -p ~/.docker + echo '{"insecure-registries" : ["git.rustprogersteam.ru"]}' > ~/.docker/config.json + cat ~/.docker/config.json + + - name: 'Docker Tags' + id: tags + uses: https://github.com/cssnr/docker-tags-action@v2 + with: + images: 'git.rustprogersteam.ru/pvhproject/pvhfrontend/' + + - name: Login to Docker Registry + uses: docker/login-action@v3 + with: + registry: git.rustprogersteam.ru + username: ${{ vars.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + git.rustprogersteam.ru/pvhproject/pvhfrontend/pvh:latest + git.rustprogersteam.ru/pvhproject/pvhfrontend/pvh:${{ github.sha }} + labels: ${{ steps.tags.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/front/.gitignore b/.gitignore similarity index 100% rename from front/.gitignore rename to .gitignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3b74df8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM mkenney/npm:node-11-alpine + +WORKDIR /web/app + +COPY . . + +RUN npm install +RUN npm cache clean --force + +CMD ["npm", "run", "dev"] diff --git a/front/README.md b/README.md similarity index 100% rename from front/README.md rename to README.md diff --git a/front/eslint.config.js b/eslint.config.js similarity index 100% rename from front/eslint.config.js rename to eslint.config.js diff --git a/front/vite.config.ts b/front/vite.config.ts deleted file mode 100644 index 705517a..0000000 --- a/front/vite.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], - server: { - port: 5173, - host: true, // попробуйте добавить эту строку - } -}) diff --git a/front/index.html b/index.html similarity index 100% rename from front/index.html rename to index.html diff --git a/front/package-lock.json b/package-lock.json similarity index 100% rename from front/package-lock.json rename to package-lock.json diff --git a/front/package.json b/package.json similarity index 100% rename from front/package.json rename to package.json diff --git a/front/src/App.css b/src/App.css similarity index 100% rename from front/src/App.css rename to src/App.css diff --git a/front/src/api/catalogApi.ts b/src/api/catalogApi.ts similarity index 100% rename from front/src/api/catalogApi.ts rename to src/api/catalogApi.ts diff --git a/front/src/api/client.ts b/src/api/client.ts similarity index 100% rename from front/src/api/client.ts rename to src/api/client.ts diff --git a/front/src/app/hooks.ts b/src/app/hooks.ts similarity index 100% rename from front/src/app/hooks.ts rename to src/app/hooks.ts diff --git a/front/src/app/routes.tsx b/src/app/routes.tsx similarity index 100% rename from front/src/app/routes.tsx rename to src/app/routes.tsx diff --git a/front/src/app/store.ts b/src/app/store.ts similarity index 100% rename from front/src/app/store.ts rename to src/app/store.ts diff --git a/front/src/assets/images/Logo.jpg b/src/assets/images/Logo.jpg similarity index 100% rename from front/src/assets/images/Logo.jpg rename to src/assets/images/Logo.jpg diff --git a/front/src/assets/images/mainPage/296170994030930.jpg b/src/assets/images/mainPage/296170994030930.jpg similarity index 100% rename from front/src/assets/images/mainPage/296170994030930.jpg rename to src/assets/images/mainPage/296170994030930.jpg diff --git a/front/src/assets/images/mainPage/ChatGPT-Image-Oct-1_-2025_-05_29_18-PM.png b/src/assets/images/mainPage/ChatGPT-Image-Oct-1_-2025_-05_29_18-PM.png similarity index 100% rename from front/src/assets/images/mainPage/ChatGPT-Image-Oct-1_-2025_-05_29_18-PM.png rename to src/assets/images/mainPage/ChatGPT-Image-Oct-1_-2025_-05_29_18-PM.png diff --git a/front/src/assets/images/mainPage/_.jpg b/src/assets/images/mainPage/_.jpg similarity index 100% rename from front/src/assets/images/mainPage/_.jpg rename to src/assets/images/mainPage/_.jpg diff --git a/front/src/assets/images/mainPage/category_internal1.jpg b/src/assets/images/mainPage/category_internal1.jpg similarity index 100% rename from front/src/assets/images/mainPage/category_internal1.jpg rename to src/assets/images/mainPage/category_internal1.jpg diff --git a/front/src/assets/images/mainPage/category_internal2.jpg b/src/assets/images/mainPage/category_internal2.jpg similarity index 100% rename from front/src/assets/images/mainPage/category_internal2.jpg rename to src/assets/images/mainPage/category_internal2.jpg diff --git a/front/src/assets/images/mainPage/orig.jpg b/src/assets/images/mainPage/orig.jpg similarity index 100% rename from front/src/assets/images/mainPage/orig.jpg rename to src/assets/images/mainPage/orig.jpg diff --git a/front/src/components/Banner/Banner.module.css b/src/components/Banner/Banner.module.css similarity index 100% rename from front/src/components/Banner/Banner.module.css rename to src/components/Banner/Banner.module.css diff --git a/front/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx similarity index 100% rename from front/src/components/Banner/Banner.tsx rename to src/components/Banner/Banner.tsx diff --git a/front/src/components/Button/Button.module.css b/src/components/Button/Button.module.css similarity index 100% rename from front/src/components/Button/Button.module.css rename to src/components/Button/Button.module.css diff --git a/front/src/components/Button/Button.tsx b/src/components/Button/Button.tsx similarity index 100% rename from front/src/components/Button/Button.tsx rename to src/components/Button/Button.tsx diff --git a/front/src/components/Categories/Categories.module.css b/src/components/Categories/Categories.module.css similarity index 100% rename from front/src/components/Categories/Categories.module.css rename to src/components/Categories/Categories.module.css diff --git a/front/src/components/Categories/Categories.tsx b/src/components/Categories/Categories.tsx similarity index 100% rename from front/src/components/Categories/Categories.tsx rename to src/components/Categories/Categories.tsx diff --git a/front/src/components/ContactProduct/ContactProduct.module.css b/src/components/ContactProduct/ContactProduct.module.css similarity index 100% rename from front/src/components/ContactProduct/ContactProduct.module.css rename to src/components/ContactProduct/ContactProduct.module.css diff --git a/front/src/components/ContactProduct/ContactProduct.tsx b/src/components/ContactProduct/ContactProduct.tsx similarity index 100% rename from front/src/components/ContactProduct/ContactProduct.tsx rename to src/components/ContactProduct/ContactProduct.tsx diff --git a/front/src/components/Footer/Footer.module.css b/src/components/Footer/Footer.module.css similarity index 100% rename from front/src/components/Footer/Footer.module.css rename to src/components/Footer/Footer.module.css diff --git a/front/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx similarity index 100% rename from front/src/components/Footer/Footer.tsx rename to src/components/Footer/Footer.tsx diff --git a/front/src/components/Header/Header.module.css b/src/components/Header/Header.module.css similarity index 100% rename from front/src/components/Header/Header.module.css rename to src/components/Header/Header.module.css diff --git a/front/src/components/Header/Header.tsx b/src/components/Header/Header.tsx similarity index 100% rename from front/src/components/Header/Header.tsx rename to src/components/Header/Header.tsx diff --git a/front/src/components/ModalRequestForm/ModalRequestForm.module.css b/src/components/ModalRequestForm/ModalRequestForm.module.css similarity index 100% rename from front/src/components/ModalRequestForm/ModalRequestForm.module.css rename to src/components/ModalRequestForm/ModalRequestForm.module.css diff --git a/front/src/components/ModalRequestForm/ModalRequestForm.tsx b/src/components/ModalRequestForm/ModalRequestForm.tsx similarity index 100% rename from front/src/components/ModalRequestForm/ModalRequestForm.tsx rename to src/components/ModalRequestForm/ModalRequestForm.tsx diff --git a/front/src/components/PriceLisForm/PriceLisForm.module.css b/src/components/PriceLisForm/PriceLisForm.module.css similarity index 100% rename from front/src/components/PriceLisForm/PriceLisForm.module.css rename to src/components/PriceLisForm/PriceLisForm.module.css diff --git a/front/src/components/PriceLisForm/PriceLisForm.tsx b/src/components/PriceLisForm/PriceLisForm.tsx similarity index 100% rename from front/src/components/PriceLisForm/PriceLisForm.tsx rename to src/components/PriceLisForm/PriceLisForm.tsx diff --git a/front/src/components/QuestionsBlock/QuestionsBlock.module.css b/src/components/QuestionsBlock/QuestionsBlock.module.css similarity index 100% rename from front/src/components/QuestionsBlock/QuestionsBlock.module.css rename to src/components/QuestionsBlock/QuestionsBlock.module.css diff --git a/front/src/components/QuestionsBlock/QuestionsBlock.tsx b/src/components/QuestionsBlock/QuestionsBlock.tsx similarity index 100% rename from front/src/components/QuestionsBlock/QuestionsBlock.tsx rename to src/components/QuestionsBlock/QuestionsBlock.tsx diff --git a/front/src/features/MainPage/MainPage.module.css b/src/features/MainPage/MainPage.module.css similarity index 100% rename from front/src/features/MainPage/MainPage.module.css rename to src/features/MainPage/MainPage.module.css diff --git a/front/src/features/MainPage/PhoneRequest.tsx b/src/features/MainPage/PhoneRequest.tsx similarity index 100% rename from front/src/features/MainPage/PhoneRequest.tsx rename to src/features/MainPage/PhoneRequest.tsx diff --git a/front/src/features/catalog/Catalog.module.css b/src/features/catalog/Catalog.module.css similarity index 100% rename from front/src/features/catalog/Catalog.module.css rename to src/features/catalog/Catalog.module.css diff --git a/front/src/features/catalog/CategoriesGrid.tsx b/src/features/catalog/CategoriesGrid.tsx similarity index 100% rename from front/src/features/catalog/CategoriesGrid.tsx rename to src/features/catalog/CategoriesGrid.tsx diff --git a/front/src/features/catalog/CategoryCards.tsx b/src/features/catalog/CategoryCards.tsx similarity index 100% rename from front/src/features/catalog/CategoryCards.tsx rename to src/features/catalog/CategoryCards.tsx diff --git a/front/src/features/catalog/CategoryFilters.tsx b/src/features/catalog/CategoryFilters.tsx similarity index 100% rename from front/src/features/catalog/CategoryFilters.tsx rename to src/features/catalog/CategoryFilters.tsx diff --git a/front/src/features/catalog/Pagination.tsx b/src/features/catalog/Pagination.tsx similarity index 100% rename from front/src/features/catalog/Pagination.tsx rename to src/features/catalog/Pagination.tsx diff --git a/front/src/features/catalog/ProductCard.tsx b/src/features/catalog/ProductCard.tsx similarity index 100% rename from front/src/features/catalog/ProductCard.tsx rename to src/features/catalog/ProductCard.tsx diff --git a/front/src/features/catalog/ProductGrid.tsx b/src/features/catalog/ProductGrid.tsx similarity index 100% rename from front/src/features/catalog/ProductGrid.tsx rename to src/features/catalog/ProductGrid.tsx diff --git a/front/src/features/catalog/SidebarCategories.tsx b/src/features/catalog/SidebarCategories.tsx similarity index 100% rename from front/src/features/catalog/SidebarCategories.tsx rename to src/features/catalog/SidebarCategories.tsx diff --git a/front/src/features/catalog/categoriesSlice.ts b/src/features/catalog/categoriesSlice.ts similarity index 100% rename from front/src/features/catalog/categoriesSlice.ts rename to src/features/catalog/categoriesSlice.ts diff --git a/front/src/features/catalog/productsSlice.ts b/src/features/catalog/productsSlice.ts similarity index 100% rename from front/src/features/catalog/productsSlice.ts rename to src/features/catalog/productsSlice.ts diff --git a/front/src/index.css b/src/index.css similarity index 100% rename from front/src/index.css rename to src/index.css diff --git a/front/src/main.tsx b/src/main.tsx similarity index 100% rename from front/src/main.tsx rename to src/main.tsx diff --git a/front/src/pages/About/About.module.css b/src/pages/About/About.module.css similarity index 100% rename from front/src/pages/About/About.module.css rename to src/pages/About/About.module.css diff --git a/front/src/pages/About/About.tsx b/src/pages/About/About.tsx similarity index 100% rename from front/src/pages/About/About.tsx rename to src/pages/About/About.tsx diff --git a/front/src/pages/ItemPage/ItemPage.module.css b/src/pages/ItemPage/ItemPage.module.css similarity index 100% rename from front/src/pages/ItemPage/ItemPage.module.css rename to src/pages/ItemPage/ItemPage.module.css diff --git a/front/src/pages/ItemPage/ItemPage.tsx b/src/pages/ItemPage/ItemPage.tsx similarity index 100% rename from front/src/pages/ItemPage/ItemPage.tsx rename to src/pages/ItemPage/ItemPage.tsx diff --git a/front/src/pages/Layout/Layout.module.css b/src/pages/Layout/Layout.module.css similarity index 100% rename from front/src/pages/Layout/Layout.module.css rename to src/pages/Layout/Layout.module.css diff --git a/front/src/pages/Layout/Layout.tsx b/src/pages/Layout/Layout.tsx similarity index 100% rename from front/src/pages/Layout/Layout.tsx rename to src/pages/Layout/Layout.tsx diff --git a/front/src/pages/Main/Main.module.css b/src/pages/Main/Main.module.css similarity index 100% rename from front/src/pages/Main/Main.module.css rename to src/pages/Main/Main.module.css diff --git a/front/src/pages/Main/Main.tsx b/src/pages/Main/Main.tsx similarity index 100% rename from front/src/pages/Main/Main.tsx rename to src/pages/Main/Main.tsx diff --git a/front/src/pages/Products/Products.module.css b/src/pages/Products/Products.module.css similarity index 100% rename from front/src/pages/Products/Products.module.css rename to src/pages/Products/Products.module.css diff --git a/front/src/pages/Products/Products.tsx b/src/pages/Products/Products.tsx similarity index 100% rename from front/src/pages/Products/Products.tsx rename to src/pages/Products/Products.tsx diff --git a/front/src/types/categories.ts b/src/types/categories.ts similarity index 100% rename from front/src/types/categories.ts rename to src/types/categories.ts diff --git a/front/src/types/product.ts b/src/types/product.ts similarity index 100% rename from front/src/types/product.ts rename to src/types/product.ts diff --git a/front/tsconfig.app.json b/tsconfig.app.json similarity index 100% rename from front/tsconfig.app.json rename to tsconfig.app.json diff --git a/front/tsconfig.json b/tsconfig.json similarity index 100% rename from front/tsconfig.json rename to tsconfig.json diff --git a/front/tsconfig.node.json b/tsconfig.node.json similarity index 100% rename from front/tsconfig.node.json rename to tsconfig.node.json diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..13122b5 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + host: '0.0.0.0', // Слушаем все интерфейсы + port: 5173, + allowedHosts: ['rustprogersteam.ru'], + strictPort: true, // Запрещаем автоматический выбор порта + hmr: { + clientPort: 5173, // Важно для HMR в Docker + }, + // Разрешаем CORS для внешних запросов + cors: true + } +}) -- 2.54.0 From 1699873ea8912f4544145afd42953702cf6e98e0 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Mon, 30 Mar 2026 22:56:01 +0300 Subject: [PATCH 13/67] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82,=20=D0=BF?= =?UTF-8?q?=D1=83=D1=82=D0=B8=20=D0=B2=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=D1=85,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=83?= =?UTF-8?q?=D1=8E=20=D0=BE=D0=BA=D1=80=D1=83=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 2 - src/api/client.ts | 48 +++++++++++++++++--- src/components/PriceLisForm/PriceLisForm.tsx | 9 ++-- src/features/catalog/categoriesSlice.ts | 4 +- src/features/catalog/productsSlice.ts | 4 +- src/vite-env.d.ts | 8 ++++ tsconfig.app.json | 2 +- 8 files changed, 59 insertions(+), 19 deletions(-) create mode 100644 src/vite-env.d.ts diff --git a/.gitignore b/.gitignore index a547bf3..dc2ec41 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +.env.production # Editor directories and files .vscode/* diff --git a/README.md b/README.md index 9b24cc2..0212a8b 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,6 @@ GET /products?categoryId=1&page=1&limit=9&size=110х2,2х500,110х2,2х1500&colo "id": 101, "name": "Труба ПВХ", "description": "бля бля", - "price": 799, "categoryId": 1, "images": ["/images/s23-1.jpg", "/images/s23-2.jpg"], "attributes": { @@ -148,7 +147,6 @@ GET /api/products/101 "id": 101, "name": "ПВХ труба", "description": "бля бля", - "price": 799, "categoryId": 1, "images": [ "/images/s23-1.jpg", diff --git a/src/api/client.ts b/src/api/client.ts index eb9f9fb..a1d33a0 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -1,9 +1,43 @@ -import axios from "axios"; +import axios, { AxiosInstance } from 'axios'; -export const API_BASE = ""; -const client = axios.create({ - baseURL: API_BASE, - headers: { "Content-Type": "application/json" }, -}); +class ApiClient { + private axiosInstance: AxiosInstance; -export default client; \ No newline at end of file + constructor(baseURL: string = import.meta.env.VITE_API_URL) { + this.axiosInstance = axios.create({ + baseURL, + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, + }); + + // Интерцептор для обработки ошибок + this.axiosInstance.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 404) { + console.error('Resource not found:', error.config?.url); + } else if (error.response?.status === 500) { + console.error('Server error:', error.response?.data); + } + return Promise.reject(error); + } + ); + } + + async get(url: string): Promise<{ data: T }> { + const response = await this.axiosInstance.get(url); + return { data: response.data }; + } + + async post( + url: string, + data?: any, + ): Promise<{ data: T }> { + const response = await this.axiosInstance.post(url, data); + return { data: response.data }; + } +} + +export const client = new ApiClient('/api'); \ No newline at end of file diff --git a/src/components/PriceLisForm/PriceLisForm.tsx b/src/components/PriceLisForm/PriceLisForm.tsx index 88518bc..a0837b0 100644 --- a/src/components/PriceLisForm/PriceLisForm.tsx +++ b/src/components/PriceLisForm/PriceLisForm.tsx @@ -7,10 +7,11 @@ import { yupResolver } from "@hookform/resolvers/yup"; import styles from "./ModalRequestForm.module.css"; import Button from "../Button/Button"; import axios from "axios"; +import { client } from "../../api/client"; // ✅ Схема валидации const schema = yup.object().shape({ - name: yup.string().required("Введите имя"), + name: yup.string().required("Введите ФИО"), email: yup.string().email("Некорректный Email").required("Введите Email"), phone: yup.string().min(18, "Введите корректный номер").required("Введите номер"), }); @@ -62,10 +63,8 @@ const PriceLisForm: React.FC = ({ open, onClose }) => { const sendToBackend = async (data: FormData): Promise => { try{ - const response = await axios.post("/api/requests", { + const response = await client.post<{success: boolean}>("/api/email", { ...data, - source: "website-form", - timestamp: new Date().toISOString(), }) return response.data.success; @@ -121,7 +120,7 @@ const PriceLisForm: React.FC = ({ open, onClose }) => {
diff --git a/src/features/catalog/categoriesSlice.ts b/src/features/catalog/categoriesSlice.ts index 4c0685c..f1b8a81 100644 --- a/src/features/catalog/categoriesSlice.ts +++ b/src/features/catalog/categoriesSlice.ts @@ -1,11 +1,11 @@ import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import type { Category } from "../../types/categories"; -import axios from "axios"; +import {client} from "../../api/client"; export const fetchCategories = createAsyncThunk( "categories/fetchCategories", async () => { - const res = await axios.get("/api/categories"); + const res = await client.get("/categories"); return res.data; } ); diff --git a/src/features/catalog/productsSlice.ts b/src/features/catalog/productsSlice.ts index 45b9af2..a0b3f85 100644 --- a/src/features/catalog/productsSlice.ts +++ b/src/features/catalog/productsSlice.ts @@ -1,6 +1,6 @@ import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import type {PayloadAction} from "@reduxjs/toolkit"; -import client from "../../api/client"; +import {client} from "../../api/client"; import type { Product } from "../../types/product"; import axios from "axios"; @@ -42,7 +42,7 @@ export const fetchProductsByCategory = createAsyncThunk( export const fetchProductById = createAsyncThunk( "product/fetchById", async (id: string) => { - const response = await axios.get(`/api/products/${id}`); + const response = await client.get(`/products/${id}`); return response.data; } ); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..2fb12b0 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,8 @@ +interface ImportMetaEnv { + readonly VITE_API_URL: string + readonly VITE_API_KEY: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} \ No newline at end of file diff --git a/tsconfig.app.json b/tsconfig.app.json index a9b5a59..7d68ebb 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -11,7 +11,7 @@ /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, + "verbatimModuleSyntax": false, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", -- 2.54.0 From fd28afea41841b4d0153ed2ff60483188945f3d3 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Tue, 31 Mar 2026 15:09:35 +0300 Subject: [PATCH 14/67] ... --- src/api/client.ts | 1 - vite.config.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/api/client.ts b/src/api/client.ts index a1d33a0..76e08df 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -12,7 +12,6 @@ class ApiClient { }, }); - // Интерцептор для обработки ошибок this.axiosInstance.interceptors.response.use( (response) => response, (error) => { diff --git a/vite.config.ts b/vite.config.ts index 13122b5..159db8d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ server: { host: '0.0.0.0', // Слушаем все интерфейсы port: 5173, - allowedHosts: ['rustprogersteam.ru'], + allowedHosts: ['pvh.projects.rustprogersteam.ru'], strictPort: true, // Запрещаем автоматический выбор порта hmr: { clientPort: 5173, // Важно для HMR в Docker -- 2.54.0 From ed1c305cbb94ee1faa102c668fac2d65aef7bb1c Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Thu, 2 Apr 2026 11:44:35 +0300 Subject: [PATCH 15/67] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20docker,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20gockerignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 9 +++++++++ Dockerfile | 33 ++++++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5bdd9f0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +.forgejo +dist +.git +.env* +*.log +.DS_Store +coverage +.vite \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 3b74df8..d780e0d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,33 @@ -FROM mkenney/npm:node-11-alpine +# ========== СТАДИЯ 1: СБОРКА ========== +FROM node:20-alpine AS builder -WORKDIR /web/app +WORKDIR /app +# Копируем зависимости +COPY package*.json ./ +RUN npm ci + +# Копируем исходники COPY . . -RUN npm install -RUN npm cache clean --force +# Сборка проекта +RUN npm run build -CMD ["npm", "run", "dev"] +# ========== СТАДИЯ 2: СЕРВЕР ========== +FROM nginx:alpine + +# Копируем собранные файлы из стадии 1 +COPY --from=builder /app/dist /usr/share/nginx/html + +# Конфиг для SPA (чтобы react-router работал) +RUN echo 'server { \ + listen 80; \ + location / { \ + root /usr/share/nginx/html; \ + try_files $uri $uri/ /index.html; \ + } \ +}' > /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] -- 2.54.0 From f6b59e9a23446e46ad2be6617bf46e7f03c1f526 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Thu, 2 Apr 2026 11:56:49 +0300 Subject: [PATCH 16/67] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=83=D0=B6=D0=BD=D1=8B=D0=B5=20=D0=B8=D0=BC=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/store.ts | 1 - src/features/catalog/productsSlice.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/app/store.ts b/src/app/store.ts index 8bd0c79..bed903e 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -1,5 +1,4 @@ import { configureStore } from "@reduxjs/toolkit"; -import catalogReducer from "../features/catalog/productsSlice"; import categoriesReducer from "../features/catalog/categoriesSlice" import productsReducer from "../features/catalog/productsSlice" diff --git a/src/features/catalog/productsSlice.ts b/src/features/catalog/productsSlice.ts index a0b3f85..1126237 100644 --- a/src/features/catalog/productsSlice.ts +++ b/src/features/catalog/productsSlice.ts @@ -2,7 +2,6 @@ import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import type {PayloadAction} from "@reduxjs/toolkit"; import {client} from "../../api/client"; import type { Product } from "../../types/product"; -import axios from "axios"; interface FetchParams { categoryId: number; -- 2.54.0 From f270c6c0fe367bd666328baa662817d9395c0ca0 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Thu, 2 Apr 2026 12:07:06 +0300 Subject: [PATCH 17/67] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20package.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 44f3321..0ad4e49 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -b && vite build", + "build": "vite build", + "build:check": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" }, -- 2.54.0 From df7a3a542cc727d19ce3fd1fe5cf3f55c8b4e9a4 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Thu, 2 Apr 2026 12:18:49 +0300 Subject: [PATCH 18/67] . --- src/pages/ItemPage/ItemPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ItemPage/ItemPage.tsx b/src/pages/ItemPage/ItemPage.tsx index 3436661..9839a1d 100644 --- a/src/pages/ItemPage/ItemPage.tsx +++ b/src/pages/ItemPage/ItemPage.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { fetchProductById } from "../../features/catalog/productsSlice"; -import styles from "./ProductPage.module.css"; +import styles from "./ItemPage.module.css"; import type { CategoryAtributes, CategoryAtrVals } from "../../types/categories"; const ItemPage = () => { -- 2.54.0 From 5de57cc7ad6e6e8b4a6258346d75198f6976720d Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Thu, 2 Apr 2026 12:56:47 +0300 Subject: [PATCH 19/67] =?UTF-8?q?=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=83?= =?UTF-8?q?=D1=8E=20=D0=BE=D0=BA=D1=80=D1=83=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/client.ts | 2 +- src/components/ModalRequestForm/ModalRequestForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/client.ts b/src/api/client.ts index 76e08df..0f4e6ab 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -3,7 +3,7 @@ import axios, { AxiosInstance } from 'axios'; class ApiClient { private axiosInstance: AxiosInstance; - constructor(baseURL: string = import.meta.env.VITE_API_URL) { + constructor(baseURL: string = 'https://pvh.projects.rustprogersteam.ru') { this.axiosInstance = axios.create({ baseURL, timeout: 10000, diff --git a/src/components/ModalRequestForm/ModalRequestForm.tsx b/src/components/ModalRequestForm/ModalRequestForm.tsx index f197dd7..07c2131 100644 --- a/src/components/ModalRequestForm/ModalRequestForm.tsx +++ b/src/components/ModalRequestForm/ModalRequestForm.tsx @@ -65,7 +65,7 @@ const ModalRequestForm: React.FC = ({ open, onClose }) => { // ✅ Отправка данных на бэкенд const sendToBackend = async (data: FormData): Promise => { try { - const response = await fetch(`/api/requests`, { + const response = await fetch(`/api/email`, { method: "POST", headers: { "Content-Type": "application/json", -- 2.54.0 From b205ca92043bf092ac92624a62334268a51fc041 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Thu, 2 Apr 2026 13:23:27 +0300 Subject: [PATCH 20/67] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8E=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=87=D1=82=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ModalRequestForm/ModalRequestForm.tsx | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/src/components/ModalRequestForm/ModalRequestForm.tsx b/src/components/ModalRequestForm/ModalRequestForm.tsx index 07c2131..b6111d2 100644 --- a/src/components/ModalRequestForm/ModalRequestForm.tsx +++ b/src/components/ModalRequestForm/ModalRequestForm.tsx @@ -7,6 +7,7 @@ import * as yup from "yup"; import { yupResolver } from "@hookform/resolvers/yup"; import styles from "./ModalRequestForm.module.css"; import Button from "../Button/Button"; +import { client } from "../../api/client"; // ✅ Схема валидации const schema = yup.object().shape({ @@ -64,30 +65,19 @@ const ModalRequestForm: React.FC = ({ open, onClose }) => { // ✅ Отправка данных на бэкенд const sendToBackend = async (data: FormData): Promise => { - try { - const response = await fetch(`/api/email`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - ...data, - source: "website-form", - timestamp: new Date().toISOString(), - }), - }); + try { + const payload = { + ...data, + }; - 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("Не удалось отправить заявку. Попробуйте позже."); - } - }; + const response = await client.post<{ success: boolean }>("/email", payload); + + return response.data.success; + } catch (error) { + console.error("Error sending request:", error); + throw new Error("Не удалось отправить заявку. Попробуйте позже."); + } +}; // ✅ Submit const onSubmit = async (data: FormData) => { -- 2.54.0 From 053128ca50b688ff1fad6267eb6a1b1b502ad43b Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Thu, 2 Apr 2026 13:23:42 +0300 Subject: [PATCH 21/67] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/client.ts b/src/api/client.ts index 0f4e6ab..3989ee1 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -39,4 +39,4 @@ class ApiClient { } } -export const client = new ApiClient('/api'); \ No newline at end of file +export const client = new ApiClient('https://pvh.projects.rustprogersteam.ru/api'); \ No newline at end of file -- 2.54.0 From cacc69a7d32bed4488a6e66020601ae3b38233b0 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Thu, 2 Apr 2026 13:47:23 +0300 Subject: [PATCH 22/67] ... --- src/components/PriceLisForm/PriceLisForm.tsx | 1 - src/features/MainPage/PhoneRequest.tsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/PriceLisForm/PriceLisForm.tsx b/src/components/PriceLisForm/PriceLisForm.tsx index a0837b0..183925f 100644 --- a/src/components/PriceLisForm/PriceLisForm.tsx +++ b/src/components/PriceLisForm/PriceLisForm.tsx @@ -6,7 +6,6 @@ import * as yup from "yup"; import { yupResolver } from "@hookform/resolvers/yup"; import styles from "./ModalRequestForm.module.css"; import Button from "../Button/Button"; -import axios from "axios"; import { client } from "../../api/client"; // ✅ Схема валидации diff --git a/src/features/MainPage/PhoneRequest.tsx b/src/features/MainPage/PhoneRequest.tsx index 228b614..e35ef02 100644 --- a/src/features/MainPage/PhoneRequest.tsx +++ b/src/features/MainPage/PhoneRequest.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; -import axios from "axios"; import styles from "./MainPage.module.css"; import Button from "../../components/Button/Button"; +import { client } from "../../api/client"; interface Props { onSend?: (phone: string) => Promise; @@ -60,7 +60,7 @@ const PhoneRequest: React.FC = ({ onSend }) => { await onSend(phone); } else { - await axios.post("/api/contact/send-phone", { phone }); + await client.post("/api/contact/send-phone", { phone }); } setSuccess(true); -- 2.54.0 From 941e3ea9df47d0edc2d6cacc57f167e6532301d1 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Thu, 2 Apr 2026 18:18:41 +0300 Subject: [PATCH 23/67] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20http?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/catalogApi.ts | 4 ++-- src/components/ContactProduct/ContactProduct.tsx | 2 +- src/components/PriceLisForm/PriceLisForm.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/catalogApi.ts b/src/api/catalogApi.ts index e145965..8e3960d 100644 --- a/src/api/catalogApi.ts +++ b/src/api/catalogApi.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +/*import axios from "axios"; import type { Product } from "../types/product"; import type { Category } from "../types/categories"; @@ -10,5 +10,5 @@ export const fetchCategoryProducts = async (category: string): Promise => { const response = await axios.get(`https://your-api.com/api/category=${categoryid}`); return response.data; -}; +};*/ diff --git a/src/components/ContactProduct/ContactProduct.tsx b/src/components/ContactProduct/ContactProduct.tsx index bfeedee..82a24e4 100644 --- a/src/components/ContactProduct/ContactProduct.tsx +++ b/src/components/ContactProduct/ContactProduct.tsx @@ -3,7 +3,7 @@ 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"; +const imageUrl = "https://sprint-pro.com/images/banners/slide1/t1.png"; import image1 from "../../assets/images/mainPage/_.jpg" import image2 from "../../assets/images/mainPage/orig.jpg" diff --git a/src/components/PriceLisForm/PriceLisForm.tsx b/src/components/PriceLisForm/PriceLisForm.tsx index 183925f..5771697 100644 --- a/src/components/PriceLisForm/PriceLisForm.tsx +++ b/src/components/PriceLisForm/PriceLisForm.tsx @@ -62,7 +62,7 @@ const PriceLisForm: React.FC = ({ open, onClose }) => { const sendToBackend = async (data: FormData): Promise => { try{ - const response = await client.post<{success: boolean}>("/api/email", { + const response = await client.post<{success: boolean}>("/email", { ...data, }) -- 2.54.0 From e7699bba77af59f24e2f077b8ffd7a5292c19148 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Thu, 2 Apr 2026 19:12:50 +0300 Subject: [PATCH 24/67] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d780e0d..5bc1652 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN npm ci COPY . . # Сборка проекта -RUN npm run build +RUN npx vite build # ========== СТАДИЯ 2: СЕРВЕР ========== FROM nginx:alpine -- 2.54.0 From 8e7a687ab344cdb93a4f339a03ae65606348a67f Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Fri, 3 Apr 2026 13:37:11 +0300 Subject: [PATCH 25/67] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=D1=83=20http=20=D0=BD?= =?UTF-8?q?=D0=B0=20https?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/client.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/api/client.ts b/src/api/client.ts index 3989ee1..fa03c20 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -12,6 +12,17 @@ class ApiClient { }, }); + this.axiosInstance.interceptors.request.use( + (config) => { + if (config.baseURL && config.baseURL.startsWith('http://')) { + console.error('❌ Request uses HTTP instead of HTTPS:', config.baseURL); + config.baseURL = config.baseURL.replace('http://', 'https://'); + } + return config; + }, + (error) => Promise.reject(error) + ); + this.axiosInstance.interceptors.response.use( (response) => response, (error) => { -- 2.54.0 From e75023f15ae6479d2e4fa04ea5d9f7a0b642215c Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Sun, 5 Apr 2026 21:22:42 +0300 Subject: [PATCH 26/67] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D0=B8=D1=80=D0=B5=D0=BA=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/client.ts | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/api/client.ts b/src/api/client.ts index fa03c20..6bd3591 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -10,18 +10,39 @@ class ApiClient { headers: { 'Content-Type': 'application/json', }, + maxRedirects: 0, + validateStatus: (status) => status >= 200 && status < 400, }); - this.axiosInstance.interceptors.request.use( - (config) => { - if (config.baseURL && config.baseURL.startsWith('http://')) { - console.error('❌ Request uses HTTP instead of HTTPS:', config.baseURL); - config.baseURL = config.baseURL.replace('http://', 'https://'); + this.axiosInstance.interceptors.response.use( + (response) => response, + async (error) => { + // Если это редирект (301, 302, 307, 308) + if (error.response && [301, 302, 307, 308].includes(error.response.status)) { + const location = error.response.headers.location; + console.log('🔄 Redirect detected:', location); + + if (location) { + // Исправляем HTTP на HTTPS + let fixedLocation = location; + if (fixedLocation.startsWith('http://')) { + fixedLocation = fixedLocation.replace('http://', 'https://'); + console.log('✅ Fixed URL:', fixedLocation); + } + + // Повторяем запрос на исправленный URL + try { + const response = await this.axiosInstance.get(fixedLocation); + return response; + } catch (retryError) { + return Promise.reject(retryError); + } + } } - return config; - }, - (error) => Promise.reject(error) + return Promise.reject(error); + } ); + this.axiosInstance.interceptors.response.use( (response) => response, -- 2.54.0 From 20afce31dd741063daaa5e7017d7684e0b5e67fd Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Sun, 5 Apr 2026 21:59:59 +0300 Subject: [PATCH 27/67] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20baseURL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/client.ts b/src/api/client.ts index 6bd3591..e0b27e3 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -3,7 +3,7 @@ import axios, { AxiosInstance } from 'axios'; class ApiClient { private axiosInstance: AxiosInstance; - constructor(baseURL: string = 'https://pvh.projects.rustprogersteam.ru') { + constructor(baseURL: string = 'https://pvh.projects.rustprogersteam.ru/api') { this.axiosInstance = axios.create({ baseURL, timeout: 10000, @@ -71,4 +71,4 @@ class ApiClient { } } -export const client = new ApiClient('https://pvh.projects.rustprogersteam.ru/api'); \ No newline at end of file +export const client = new ApiClient(); \ No newline at end of file -- 2.54.0 From d4f9aebf008a55a9f91202e676d08a8b3d999504 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Mon, 6 Apr 2026 21:18:00 +0300 Subject: [PATCH 28/67] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B2=D0=B5=D0=B7=D0=B4=D0=B5=20/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ModalRequestForm/ModalRequestForm.tsx | 2 +- src/components/PriceLisForm/PriceLisForm.tsx | 2 +- src/features/MainPage/PhoneRequest.tsx | 2 +- src/features/catalog/categoriesSlice.ts | 2 +- src/features/catalog/productsSlice.ts | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ModalRequestForm/ModalRequestForm.tsx b/src/components/ModalRequestForm/ModalRequestForm.tsx index b6111d2..5d083cc 100644 --- a/src/components/ModalRequestForm/ModalRequestForm.tsx +++ b/src/components/ModalRequestForm/ModalRequestForm.tsx @@ -70,7 +70,7 @@ const ModalRequestForm: React.FC = ({ open, onClose }) => { ...data, }; - const response = await client.post<{ success: boolean }>("/email", payload); + const response = await client.post<{ success: boolean }>("/email/", payload); return response.data.success; } catch (error) { diff --git a/src/components/PriceLisForm/PriceLisForm.tsx b/src/components/PriceLisForm/PriceLisForm.tsx index 5771697..611d8e5 100644 --- a/src/components/PriceLisForm/PriceLisForm.tsx +++ b/src/components/PriceLisForm/PriceLisForm.tsx @@ -62,7 +62,7 @@ const PriceLisForm: React.FC = ({ open, onClose }) => { const sendToBackend = async (data: FormData): Promise => { try{ - const response = await client.post<{success: boolean}>("/email", { + const response = await client.post<{success: boolean}>("/email/", { ...data, }) diff --git a/src/features/MainPage/PhoneRequest.tsx b/src/features/MainPage/PhoneRequest.tsx index e35ef02..980d8d4 100644 --- a/src/features/MainPage/PhoneRequest.tsx +++ b/src/features/MainPage/PhoneRequest.tsx @@ -60,7 +60,7 @@ const PhoneRequest: React.FC = ({ onSend }) => { await onSend(phone); } else { - await client.post("/api/contact/send-phone", { phone }); + await client.post("/api/contact/send-phone/", { phone }); } setSuccess(true); diff --git a/src/features/catalog/categoriesSlice.ts b/src/features/catalog/categoriesSlice.ts index f1b8a81..9cbc651 100644 --- a/src/features/catalog/categoriesSlice.ts +++ b/src/features/catalog/categoriesSlice.ts @@ -5,7 +5,7 @@ import {client} from "../../api/client"; export const fetchCategories = createAsyncThunk( "categories/fetchCategories", async () => { - const res = await client.get("/categories"); + const res = await client.get("/categories/"); return res.data; } ); diff --git a/src/features/catalog/productsSlice.ts b/src/features/catalog/productsSlice.ts index 1126237..6130241 100644 --- a/src/features/catalog/productsSlice.ts +++ b/src/features/catalog/productsSlice.ts @@ -32,7 +32,7 @@ export const fetchProductsByCategory = createAsyncThunk( else qp.append(k, String(v)); }); } - const res = await client.get(`/products?${qp.toString()}`); + const res = await client.get(`/products?${qp.toString()}/`); // ожидаем { items: Product[], total: number, page: number, limit: number } return res.data; } @@ -41,7 +41,7 @@ export const fetchProductsByCategory = createAsyncThunk( export const fetchProductById = createAsyncThunk( "product/fetchById", async (id: string) => { - const response = await client.get(`/products/${id}`); + const response = await client.get(`/products/${id}/`); return response.data; } ); -- 2.54.0 From 524d9e3deb413d0e2f5e212641b82b96e0f663f7 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Tue, 7 Apr 2026 00:23:49 +0300 Subject: [PATCH 29/67] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE=D0=B4=20=D0=BA=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B4=D1=83=D0=BA=D1=82=D0=B0=D0=BC=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D0=B8?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20=D0=BD=D0=B0=D0=B6=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=B0=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/catalog/CategoryCards.tsx | 19 +++++++++++++------ src/pages/Products/Products.tsx | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/features/catalog/CategoryCards.tsx b/src/features/catalog/CategoryCards.tsx index 22c3f0b..aef5d71 100644 --- a/src/features/catalog/CategoryCards.tsx +++ b/src/features/catalog/CategoryCards.tsx @@ -1,18 +1,25 @@ import React from "react"; import styles from "./Catalog.module.css"; -import { useAppSelector } from "../../app/hooks"; +import { useAppDispatch, useAppSelector } from "../../app/hooks"; +import { setSelectedCategory } from "./categoriesSlice"; +import { fetchProductsByCategory, setFilters, setPage } from "./productsSlice"; -interface Props { - onSelect?: (id:number) => void -} -const CategoryCards: React.FC = ({ onSelect }) => { +const CategoryCards: React.FC = () => { const categories = useAppSelector(s => s.categories.items); + const dispatch = useAppDispatch(); + + const onSelect = (catId:number) => { + dispatch(setSelectedCategory(catId)); + dispatch(setFilters({})); + dispatch(setPage(1)); + dispatch(fetchProductsByCategory({ categoryId: catId, page: 1, limit: 9 })); + } return (
{categories.map(c => ( -
onSelect?.(c.id)}> +
onSelect(c.id)}> {c.image ? {c.name} : {c.name}}
{c.name}
diff --git a/src/pages/Products/Products.tsx b/src/pages/Products/Products.tsx index 9961514..0be6057 100644 --- a/src/pages/Products/Products.tsx +++ b/src/pages/Products/Products.tsx @@ -15,7 +15,7 @@ const ProductsPage: React.FC = () => {
{!selectedCategoryId ? ( - {}} /> + ) : ( <>
-- 2.54.0 From 27523cb2a12595caa5898aee21d3b616db15c937 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Tue, 7 Apr 2026 15:18:09 +0300 Subject: [PATCH 30/67] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BD=D0=B0=D0=B2=D0=B8=D0=B3=D0=B0=D1=86=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Banner/Banner.tsx | 3 +++ src/components/Categories/Categories.tsx | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx index 652bb4b..68d49bf 100644 --- a/src/components/Banner/Banner.tsx +++ b/src/components/Banner/Banner.tsx @@ -4,6 +4,7 @@ import { Carousel } from "antd"; import Button from '../Button/Button.tsx'; import banner_img1 from '../../assets/images/mainPage/ChatGPT-Image-Oct-1_-2025_-05_29_18-PM.png' import banner_img2 from '../../assets/images/mainPage/296170994030930.jpg' +import { useNavigate } from 'react-router-dom'; export interface SlideItem { id: number; @@ -33,8 +34,10 @@ const slides: SlideItem[] = [ const BannerSlider: React.FC = () => { +const navigate = useNavigate(); const handleClick = () => { + navigate('/products') console.log('Кнопка нажата!'); }; diff --git a/src/components/Categories/Categories.tsx b/src/components/Categories/Categories.tsx index 2a72a39..25c29fe 100644 --- a/src/components/Categories/Categories.tsx +++ b/src/components/Categories/Categories.tsx @@ -4,6 +4,7 @@ import Button from "../Button/Button"; import category_internal1 from "../../assets/images/mainPage/category_internal1.jpg" import category_interna2 from "../../assets/images/mainPage/category_internal2.jpg" import { useAppSelector, useAppDispatch } from "../../app/hooks"; +import { useNavigate } from "react-router-dom"; interface CategoryItem { id: number; @@ -40,10 +41,12 @@ image: category_internal1, const CategoriesGrid: React.FC = () => { +const navigate = useNavigate(); const handleClick = () => { - console.log('Кнопка нажата!'); - }; + navigate('/products'); + console.log('Кнопка нажата!'); +}; return (
-- 2.54.0 From ba921e8348c3e8be705eed921f5572facc8f103d Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Tue, 7 Apr 2026 15:20:18 +0300 Subject: [PATCH 31/67] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BC=D0=BE=D0=B4=D0=B0=D0=BB=D0=BA=D1=83=20=D1=81=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B9=D1=81=D1=84=D0=BE=D1=80=D0=BC=D0=BE?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ContactProduct/ContactProduct.tsx | 2 +- src/components/ModalRequestForm/ModalRequestForm.tsx | 1 - src/pages/ItemPage/ItemPage.tsx | 11 ++++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/ContactProduct/ContactProduct.tsx b/src/components/ContactProduct/ContactProduct.tsx index 82a24e4..d50717f 100644 --- a/src/components/ContactProduct/ContactProduct.tsx +++ b/src/components/ContactProduct/ContactProduct.tsx @@ -40,7 +40,7 @@ const ProductionBanner: React.FC = () => { console.log('ProductionBanner render, open:', open); return ( -
+
setOpen(false)} />

КОНТРАКТНОЕ

diff --git a/src/components/ModalRequestForm/ModalRequestForm.tsx b/src/components/ModalRequestForm/ModalRequestForm.tsx index 5d083cc..ce42660 100644 --- a/src/components/ModalRequestForm/ModalRequestForm.tsx +++ b/src/components/ModalRequestForm/ModalRequestForm.tsx @@ -181,7 +181,6 @@ const ModalRequestForm: React.FC = ({ open, onClose }) => { +
-- 2.54.0 From 1cb9f9a509ae71eb5bbcbba5363accc4cc75ef4b Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Tue, 7 Apr 2026 15:43:53 +0300 Subject: [PATCH 32/67] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D1=83,=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20=D1=81=D1=82=D0=B8=D0=BB?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PriceLisForm/PriceLisForm.module.css | 68 +++++++++++++++++++ src/components/PriceLisForm/PriceLisForm.tsx | 2 +- src/pages/ItemPage/ItemPage.tsx | 4 -- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/components/PriceLisForm/PriceLisForm.module.css b/src/components/PriceLisForm/PriceLisForm.module.css index e69de29..223bef9 100644 --- a/src/components/PriceLisForm/PriceLisForm.module.css +++ b/src/components/PriceLisForm/PriceLisForm.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/src/components/PriceLisForm/PriceLisForm.tsx b/src/components/PriceLisForm/PriceLisForm.tsx index 611d8e5..869cd94 100644 --- a/src/components/PriceLisForm/PriceLisForm.tsx +++ b/src/components/PriceLisForm/PriceLisForm.tsx @@ -4,7 +4,7 @@ import { motion } from "framer-motion"; import { useForm } from "react-hook-form"; import * as yup from "yup"; import { yupResolver } from "@hookform/resolvers/yup"; -import styles from "./ModalRequestForm.module.css"; +import styles from "./PriceLisForm.module.css"; import Button from "../Button/Button"; import { client } from "../../api/client"; diff --git a/src/pages/ItemPage/ItemPage.tsx b/src/pages/ItemPage/ItemPage.tsx index aa14dea..c733a39 100644 --- a/src/pages/ItemPage/ItemPage.tsx +++ b/src/pages/ItemPage/ItemPage.tsx @@ -50,10 +50,6 @@ const ItemPage = () => {

{product.name}

{product.description}

- -
diff --git a/src/features/catalog/SidebarCategories.tsx b/src/features/catalog/SidebarCategories.tsx index 33eeae6..1e7cf65 100644 --- a/src/features/catalog/SidebarCategories.tsx +++ b/src/features/catalog/SidebarCategories.tsx @@ -25,7 +25,7 @@ const SidebarCategories: React.FC = () => {
    {items.map(cat => (
  • onClick(cat.id)}> {cat.name}
  • diff --git a/src/features/catalog/productsSlice.ts b/src/features/catalog/productsSlice.ts index 6130241..dd25eb0 100644 --- a/src/features/catalog/productsSlice.ts +++ b/src/features/catalog/productsSlice.ts @@ -54,6 +54,7 @@ interface ProductsState { loading: boolean; error: string | null; filtersApplied: Record; + selectedProductId: number | null; } const initialState: ProductsState = { @@ -64,12 +65,16 @@ const initialState: ProductsState = { loading: false, error: null, filtersApplied: {}, + selectedProductId: null }; const slice = createSlice({ name: "products", initialState, reducers: { + setSelectedProducts(state, action: PayloadAction){ + state.selectedProductId = action.payload; + }, setPage(state, action: PayloadAction) { state.page = action.payload; }, @@ -105,5 +110,5 @@ const slice = createSlice({ } }); -export const { setPage, setLimit, setFilters, resetProducts } = slice.actions; +export const { setPage, setLimit, setFilters, resetProducts, setSelectedProducts } = slice.actions; export default slice.reducer; \ No newline at end of file diff --git a/src/pages/ItemPage/ItemPage.module.css b/src/pages/ItemPage/ItemPage.module.css index e627a65..72830c2 100644 --- a/src/pages/ItemPage/ItemPage.module.css +++ b/src/pages/ItemPage/ItemPage.module.css @@ -6,6 +6,12 @@ .top { background: #000; padding: 15px 40px; + padding-top: 100px; +} + +.short{ + margin-top: 30px; + margin-bottom: 40px; } .back { @@ -33,15 +39,20 @@ max-width: 400px; } +.rightContent{ + padding-right: 100px; + +} + .right { flex: 1; background: linear-gradient( - rgba(0, 0, 0, 0.7), - rgba(0, 0, 0, 0.7) + rgba(44, 44, 44, 0.9), + rgba(44, 44, 44, 0.9) ), - url("/bg.jpg"); + url("../../assets/images/otkrytie-novogo-proizvodstva-v-g.Krymsk.jpeg"); color: white; - padding: 80px; + padding: 90px; display: flex; flex-direction: column; justify-content: center; diff --git a/src/pages/ItemPage/ItemPage.tsx b/src/pages/ItemPage/ItemPage.tsx index c733a39..0c905cd 100644 --- a/src/pages/ItemPage/ItemPage.tsx +++ b/src/pages/ItemPage/ItemPage.tsx @@ -6,6 +6,8 @@ import styles from "./ItemPage.module.css"; import type { CategoryAtributes, CategoryAtrVals } from "../../types/categories"; import Button from "../../components/Button/Button"; import PriceLisForm from "../../components/PriceLisForm/PriceLisForm"; +import ProductAttributes from "../../components/Table/ProducTable"; +import { p } from "framer-motion/client"; const ItemPage = () => { const { id } = useParams(); @@ -39,10 +41,11 @@ const ItemPage = () => {
    - {product.name} + {product.name}
    +
    ГЛАВНАЯ / {category?.name} / {product.name}
    @@ -54,6 +57,7 @@ const ItemPage = () => { label="СКАЧАТЬ ПРАЙС-ЛИСТ" onClick={() => setOpen(true)} /> +
    @@ -77,13 +81,21 @@ const ItemPage = () => {

    ОПИСАНИЕ

    {product.description}

    +

    {product.full_name}

    + {!product.inStock? + ( +

    в наличии

    + ) : ( +

    нет в наличии

    + ) + }
    )} {activeTab === "chars" && (

    ХАРАКТЕРИСТИКИ

    - {category.atributes?.map((attr: CategoryAtributes) => ( + {/*{product.attributes.map((attr) => (
    {attr.name} {attr.vals?.map((val: CategoryAtrVals) => ( @@ -92,7 +104,8 @@ const ItemPage = () => {
    ))}
    - ))} + ))}*/} +
)}
diff --git a/src/pages/Layout/Layout.tsx b/src/pages/Layout/Layout.tsx index 241d865..6788950 100644 --- a/src/pages/Layout/Layout.tsx +++ b/src/pages/Layout/Layout.tsx @@ -8,7 +8,7 @@ const Layout = () =>{ return( /*token ?*/
-
+
diff --git a/src/pages/Products/Products.module.css b/src/pages/Products/Products.module.css index e69de29..6bfd06c 100644 --- a/src/pages/Products/Products.module.css +++ b/src/pages/Products/Products.module.css @@ -0,0 +1,207 @@ +/* ===== ОБЩИЙ ЛЭЙАУТ ===== */ +.wrapper { + display: flex; + margin: 0; + justify-content: center; +} + +.container{ + display: flex; + background: #f3f3f3; + min-height: 100vh; + font-family: Arial, sans-serif; + width: 80%; +} + +/* ===== САЙДБАР ===== */ +.sidebar { + width: 280px; + background: #fff; + padding: 30px 20px; + border-right: 1px solid #e0e0e0; + + display: flex; + flex-direction: column; + padding-top: 70px; +} + +/* ===== ОСНОВНАЯ ЧАСТЬ ===== */ +.main { + flex: 1; + padding: 30px 40px; + padding-top: 100px; +} + +/* ===== ЛЭЙАУТ КАТАЛОГА ===== */ +.catalogLayout { + display: flex; + gap: 30px; + align-items: flex-start; +} + +/* ===== ФИЛЬТРЫ ===== */ +.filters { + width: 100%; /* 🔥 важно */ + border: 1px solid #e5e5e5; +} + +/* ===== БЛОК ТОВАРОВ ===== */ +.products { + flex: 1; + display: flex; + flex-direction: column; + gap: 25px; +} + +/* ===== GRID ===== */ +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + border-top: 1px solid #ddd; + border-left: 1px solid #ddd; +} + +/* ===== КАРТОЧКА ===== */ +.card { + background: #fff; + border-right: 1px solid #ddd; + border-bottom: 1px solid #ddd; + padding: 25px 20px; + text-align: center; + position: relative; + transition: all 0.2s ease; +} + +.card:hover { + box-shadow: 0 8px 20px rgba(0,0,0,0.08); +} + +/* ===== КАРТИНКА ===== */ +.card img { + max-height: 140px; + object-fit: contain; + margin-bottom: 15px; +} + +/* ===== НАЗВАНИЕ ===== */ +.card h3 { + font-size: 14px; + font-weight: 600; + color: #222; + min-height: 40px; +} + +/* ===== ПЛЮС КНОПКА ===== */ +.card::after { + content: "+"; + position: absolute; + right: 15px; + bottom: 15px; + color: #f5c400; + font-size: 22px; + font-weight: bold; +} + +/* ===== ПАГИНАЦИЯ ===== */ +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 5px; +} + +.pageBtn { + width: 40px; + height: 40px; + border: 1px solid #ddd; + background: #fff; + cursor: pointer; + font-weight: 600; +} + +.pageBtn:hover { + background: #f5c400; +} + +.activePage { + background: #f5c400; + border-color: #f5c400; +} + +/* ===== ФИЛЬТРЫ СТИЛИ ===== */ +.filters h4 { + font-size: 14px; + margin-bottom: 10px; + margin-top: 20px; +} + +.filters label { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + margin-bottom: 8px; + cursor: pointer; +} + +.filters input { + cursor: pointer; +} + +/* ===== КАТЕГОРИИ ===== */ +.sidebar h3 { + font-size: 16px; + margin-bottom: 20px; +} + +.sidebar ul { + list-style: none; + padding: 0; + margin: 0; +} + +.sidebar li { + font-size: 14px; + margin-bottom: 12px; + cursor: pointer; + color: #666; + transition: 0.2s; +} + +.sidebar li:hover { + color: #000; +} + +.activeCategory { + font-weight: 700; + color: #000; +} + +/* ===== АДАПТИВ ===== */ +@media (max-width: 1200px) { + .grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .wrapper { + flex-direction: column; + } + + .sidebar { + width: 100%; + } + + .catalogLayout { + flex-direction: column; + } + + .filters { + width: 100%; + } + + .grid { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/src/pages/Products/Products.tsx b/src/pages/Products/Products.tsx index 0be6057..872543c 100644 --- a/src/pages/Products/Products.tsx +++ b/src/pages/Products/Products.tsx @@ -11,7 +11,7 @@ const ProductsPage: React.FC = () => { const { selectedCategoryId } = useAppSelector(s => s.categories); return ( -
+ /*
{!selectedCategoryId ? ( @@ -29,6 +29,27 @@ const ProductsPage: React.FC = () => { )}
+
*/ +
+
+ + +
+ {!selectedCategoryId ? ( + + ) : ( +
+ + +
+ )} +
+
); }; diff --git a/src/types/product.ts b/src/types/product.ts index eaf7d03..611728d 100644 --- a/src/types/product.ts +++ b/src/types/product.ts @@ -1,6 +1,12 @@ export interface Product { id: number; name: string; - image: string; description: string; + images: string[]; + inStock: boolean; + price: number | null; + attributes: Record; + categoryId?: number; + created_at: string; + full_name: string; } \ No newline at end of file -- 2.54.0 From b6f66ab4a70e03e146b68e88b674428c61e492b3 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Thu, 9 Apr 2026 22:10:07 +0300 Subject: [PATCH 34/67] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B8=D0=BA=D0=BE=D0=BD=D0=BA=D1=83=20=D1=81=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D1=83,=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D1=83=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=BE=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 4 +- public/MiniLogo.jpg | Bin 0 -> 10967 bytes .../ModalRequestForm/ModalRequestForm.tsx | 2 +- .../QuestionsBlock/QuestionsBlock.tsx | 17 +- src/features/MainPage/PhoneRequest.tsx | 252 ++++++++++++------ 5 files changed, 180 insertions(+), 95 deletions(-) create mode 100644 public/MiniLogo.jpg diff --git a/index.html b/index.html index a6e9be0..4ae69d1 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,9 @@ - + - front + RK PLASTIC
diff --git a/public/MiniLogo.jpg b/public/MiniLogo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7a964ca0de865e48485f526edb65dfb8a0282bcb GIT binary patch literal 10967 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 = ({ open, onClose }) => {