From 5d9191ac24811a8507dfa71c9edba979d687c692 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Mon, 9 Feb 2026 21:47:23 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D0=BF=D0=B0=D1=81=D0=BF=D0=BE=D1=80=D1=82?= =?UTF-8?q?=20=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/app/routes.tsx | 2 + front/src/features/catalog/ProductCard.tsx | 8 +- front/src/features/catalog/productsSlice.ts | 8 ++ front/src/pages/ItemPage/ItemPage.module.css | 93 +++++++++++++++++++ front/src/pages/ItemPage/ItemPage.tsx | 96 ++++++++++++++++++++ 5 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 front/src/pages/ItemPage/ItemPage.module.css create mode 100644 front/src/pages/ItemPage/ItemPage.tsx diff --git a/front/src/app/routes.tsx b/front/src/app/routes.tsx index dc58d6f..f7186e4 100644 --- a/front/src/app/routes.tsx +++ b/front/src/app/routes.tsx @@ -4,6 +4,7 @@ import { Route } from "react-router"; import Main from "../pages/Main/Main"; import About from "../pages/About/About"; import Products from "../pages/Products/Products"; +import ItemPage from "../pages/ItemPage/ItemPage"; function AppRoutes() { return ( @@ -12,6 +13,7 @@ function AppRoutes() { } /> } /> } /> + } /> ) diff --git a/front/src/features/catalog/ProductCard.tsx b/front/src/features/catalog/ProductCard.tsx index 2abb1ad..a3a607e 100644 --- a/front/src/features/catalog/ProductCard.tsx +++ b/front/src/features/catalog/ProductCard.tsx @@ -1,14 +1,20 @@ import React from "react"; import type { Product } from "../../types/product"; import styles from "./Catalog.module.css"; +import { useNavigate } from "react-router-dom"; interface Props { product: Product; } const ProductCard: React.FC = ({ product }) => { + const navigate = useNavigate(); + return ( -
+
navigate(`/product/${product.id}`)} + > {product.name}

{product.name}

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 From a18a7063663d49326afddd01a56bbd600330a3b0 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Wed, 11 Feb 2026 17:43:21 +0300 Subject: [PATCH 2/2] =?UTF-8?q?=D0=B4=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BB?= =?UTF-8?q?=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D1=85=D0=B0=D1=80=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=B5=D1=80=D0=B8=D1=81=D1=82=D0=B8=D0=BA=20=D0=BF?= =?UTF-8?q?=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} +
+ ))}
))}