From a9b5d57fc935e7bd331b9e3d36453f8b67151de8 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Fri, 21 Nov 2025 18:32:59 +0300 Subject: [PATCH 1/9] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D0=BB=D0=B8=D1=89=D0=B5?= =?UTF-8?q?=20=D0=B8=20=D1=81=D0=BB=D0=B0=D0=B9=D1=81=D1=8B=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20?= =?UTF-8?q?=D0=BA=D0=B0=D0=B8=D0=B0=D0=BB=D0=BE=D0=B3=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/app/store.ts | 7 ++-- front/src/features/catalog/catalogSlice.ts | 35 +++++++++++-------- front/src/features/catalog/categoriesSlice.ts | 7 ++-- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/front/src/app/store.ts b/front/src/app/store.ts index 11ebdd1..b7c77eb 100644 --- a/front/src/app/store.ts +++ b/front/src/app/store.ts @@ -3,12 +3,13 @@ import catalogReducer from "../features/catalog/catalogSlice"; export const store = configureStore({ reducer: { - catalog: catalogReducer, + categories: categoriesReducer, + products: productsReducer, }, - middleware: (getDefaultMiddleware) => + /*middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false, // отключаем для async thunk - }), + }),*/ }); export type RootState = ReturnType; diff --git a/front/src/features/catalog/catalogSlice.ts b/front/src/features/catalog/catalogSlice.ts index 5e0c8e2..bfa06a0 100644 --- a/front/src/features/catalog/catalogSlice.ts +++ b/front/src/features/catalog/catalogSlice.ts @@ -2,6 +2,7 @@ import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import type {PayloadAction} from "@reduxjs/toolkit"; import { fetchCategoryProducts, fetchCategories } from "../../api/catalogApi"; import type { Product } from "../../types/product"; +import axios from "axios"; interface CatalogState { products: Product[]; @@ -21,21 +22,21 @@ const initialState: CatalogState = { currentCategory: null, }; -export const getCategories = createAsyncThunk( - "catalog/getCategories", - async (categoryid: string) => { - const data = await fetchCategories(categoryid); - return data; +export const fetchProductsByCategory = createAsyncThunk( + "products/fetchProductsByCategory", + async (categoryId: number) => { + const res = await axios.get(`/api/products?category=${categoryId}`); + return res.data; } ); -export const getCategoryProducts = createAsyncThunk( - "catalog/getCategoryProducts", - async (category: string) => { - const data = await fetchCategoryProducts(category); - return data; +export const fetchCategoryParameters = createAsyncThunk( + "products/fetchCategoryParametrs", + async (categoryId: number) => { + const res = await axios.get(`/api/category-params/${categoryId}`); + return res.data; } -); +) const catalogSlice = createSlice({ name: "catalog", @@ -58,19 +59,23 @@ const catalogSlice = createSlice({ }, extraReducers: (builder) => { builder - .addCase(getCategoryProducts.pending, (state) => { + .addCase(fetchProductsByCategory.pending, (state) => { state.loading = true; state.error = null; }) - .addCase(getCategoryProducts.fulfilled, (state, action) => { + .addCase(fetchProductsByCategory.fulfilled, (state, action) => { state.loading = false; state.products = action.payload; state.filtered = action.payload; }) - .addCase(getCategoryProducts.rejected, (state, action) => { + .addCase(fetchCategoryParameters.fulfilled, (state, action) => { + state.loading = false; + state.filters = action.payload + }) + /*.addCase(getCategoryProducts.rejected, (state, action) => { state.loading = false; state.error = action.error.message || "Ошибка загрузки данных"; - }); + });*/ }, }); diff --git a/front/src/features/catalog/categoriesSlice.ts b/front/src/features/catalog/categoriesSlice.ts index ecd87d1..4c38a87 100644 --- a/front/src/features/catalog/categoriesSlice.ts +++ b/front/src/features/catalog/categoriesSlice.ts @@ -1,11 +1,12 @@ import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import type { Category } from "../../types/categories"; +import axios from "axios"; export const fetchCategories = createAsyncThunk( - "categories/fetch", + "categories/fetchCategories", async () => { - const res = await fetch("/api/categories"); - return await res.json(); + const res = await axios.get("/api/categories"); + return res.data; } ); From 86f53ba61fef8d2015cdc3126c501282a6c6c886 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Sat, 22 Nov 2025 21:46:41 +0300 Subject: [PATCH 2/9] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D1=81=D0=BB=D1=83=D1=87=D0=B0=D0=B5=D0=B2=20=D0=B2=20extrar?= =?UTF-8?q?educers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/features/catalog/catalogSlice.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/front/src/features/catalog/catalogSlice.ts b/front/src/features/catalog/catalogSlice.ts index bfa06a0..06dbda4 100644 --- a/front/src/features/catalog/catalogSlice.ts +++ b/front/src/features/catalog/catalogSlice.ts @@ -67,15 +67,27 @@ const catalogSlice = createSlice({ state.loading = false; state.products = action.payload; state.filtered = action.payload; + state.error = null; + }) + .addCase(fetchProductsByCategory.rejected, (state, action) => { + state.loading = false; + state.error = action.error.message || "Ошибка загрузки товаров"; + state.products = []; + state.filtered = []; + }) + .addCase(fetchCategoryParameters.pending, (state) => { + state.loading = true; + state.error = null; }) .addCase(fetchCategoryParameters.fulfilled, (state, action) => { state.loading = false; state.filters = action.payload }) - /*.addCase(getCategoryProducts.rejected, (state, action) => { + .addCase(fetchCategoryParameters.rejected, (state, action) => { state.loading = false; - state.error = action.error.message || "Ошибка загрузки данных"; - });*/ + state.error = action.error.message || "Ошибка загрузки параметров категории"; + state.filters = {}; + }); }, }); From 181a24571c143fb4851eb34633ad04f962c1d275 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Sun, 23 Nov 2025 23:31:55 +0300 Subject: [PATCH 3/9] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20api=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82,=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D1=81=D1=82=D0=BE=D1=80?= =?UTF-8?q?,=20=D1=81=D0=BB=D0=B0=D0=B9=D1=81=D1=8B=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B4=D1=83=D0=BA=D1=82=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=B0=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/api/client.ts | 9 ++ front/src/app/store.ts | 4 +- .../features/catalog/SidebarCategories.tsx | 12 +++ front/src/features/catalog/catalogSlice.ts | 95 ------------------- .../src/features/catalog/catalogeSidebar.tsx | 69 -------------- front/src/features/catalog/productsSlice.ts | 95 +++++++++++++++++++ front/src/pages/Products/Products.tsx | 2 +- front/src/types/categories.ts | 3 +- 8 files changed, 122 insertions(+), 167 deletions(-) create mode 100644 front/src/api/client.ts create mode 100644 front/src/features/catalog/SidebarCategories.tsx delete mode 100644 front/src/features/catalog/catalogSlice.ts delete mode 100644 front/src/features/catalog/catalogeSidebar.tsx create mode 100644 front/src/features/catalog/productsSlice.ts diff --git a/front/src/api/client.ts b/front/src/api/client.ts new file mode 100644 index 0000000..4188ab4 --- /dev/null +++ b/front/src/api/client.ts @@ -0,0 +1,9 @@ +import axios from "axios"; + +export const API_BASE = g;/* process.env.REACT_APP_API_BASE || "";*/ +const client = axios.create({ + baseURL: API_BASE, + headers: { "Content-Type": "application/json" }, +}); + +export default client; \ No newline at end of file diff --git a/front/src/app/store.ts b/front/src/app/store.ts index b7c77eb..8bd0c79 100644 --- a/front/src/app/store.ts +++ b/front/src/app/store.ts @@ -1,5 +1,7 @@ import { configureStore } from "@reduxjs/toolkit"; -import catalogReducer from "../features/catalog/catalogSlice"; +import catalogReducer from "../features/catalog/productsSlice"; +import categoriesReducer from "../features/catalog/categoriesSlice" +import productsReducer from "../features/catalog/productsSlice" export const store = configureStore({ reducer: { diff --git a/front/src/features/catalog/SidebarCategories.tsx b/front/src/features/catalog/SidebarCategories.tsx new file mode 100644 index 0000000..bbcdad2 --- /dev/null +++ b/front/src/features/catalog/SidebarCategories.tsx @@ -0,0 +1,12 @@ +import React, { useEffect } from "react"; +import styles from "./SidebarCategories.module.css"; +import { useAppDispatch, useAppSelector } from "../../app/hooks"; +import { fetchCategories, setSelectedCategory } from "./categoriesSlice"; +import { fetchProductsByCategory, setFilters, setPage } from "./productsSlice"; + +const SidebarCategories: React.FC = () => { + return( + <> + + ) +} \ No newline at end of file diff --git a/front/src/features/catalog/catalogSlice.ts b/front/src/features/catalog/catalogSlice.ts deleted file mode 100644 index 06dbda4..0000000 --- a/front/src/features/catalog/catalogSlice.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; -import type {PayloadAction} from "@reduxjs/toolkit"; -import { fetchCategoryProducts, fetchCategories } from "../../api/catalogApi"; -import type { Product } from "../../types/product"; -import axios from "axios"; - -interface CatalogState { - products: Product[]; - filtered: Product[]; - loading: boolean; - error: string | null; - filters: Record; - currentCategory: string | null; -} - -const initialState: CatalogState = { - products: [], - filtered: [], - loading: false, - error: null, - filters: {}, - currentCategory: null, -}; - -export const fetchProductsByCategory = createAsyncThunk( - "products/fetchProductsByCategory", - async (categoryId: number) => { - const res = await axios.get(`/api/products?category=${categoryId}`); - return res.data; - } -); - -export const fetchCategoryParameters = createAsyncThunk( - "products/fetchCategoryParametrs", - async (categoryId: number) => { - const res = await axios.get(`/api/category-params/${categoryId}`); - return res.data; - } -) - -const catalogSlice = createSlice({ - name: "catalog", - initialState, - reducers: { - setFilter(state, action: PayloadAction<{ name: string; value: string }>) { - const { name, value } = action.payload; - const active = state.filters[name] || []; - state.filters[name] = active.includes(value) - ? active.filter((v) => v !== value) - : [...active, value]; - }, - applyFilters(state) { - state.filtered = state.products.filter((p) => - Object.entries(state.filters).every(([key, values]) => - values.length ? values.includes(p[key as keyof Product] as string) : true - ) - ); - }, - }, - extraReducers: (builder) => { - builder - .addCase(fetchProductsByCategory.pending, (state) => { - state.loading = true; - state.error = null; - }) - .addCase(fetchProductsByCategory.fulfilled, (state, action) => { - state.loading = false; - state.products = action.payload; - state.filtered = action.payload; - state.error = null; - }) - .addCase(fetchProductsByCategory.rejected, (state, action) => { - state.loading = false; - state.error = action.error.message || "Ошибка загрузки товаров"; - state.products = []; - state.filtered = []; - }) - .addCase(fetchCategoryParameters.pending, (state) => { - state.loading = true; - state.error = null; - }) - .addCase(fetchCategoryParameters.fulfilled, (state, action) => { - state.loading = false; - state.filters = action.payload - }) - .addCase(fetchCategoryParameters.rejected, (state, action) => { - state.loading = false; - state.error = action.error.message || "Ошибка загрузки параметров категории"; - state.filters = {}; - }); - }, -}); - -export const { setFilter, applyFilters } = catalogSlice.actions; -export default catalogSlice.reducer; \ No newline at end of file diff --git a/front/src/features/catalog/catalogeSidebar.tsx b/front/src/features/catalog/catalogeSidebar.tsx deleted file mode 100644 index 4ce1209..0000000 --- a/front/src/features/catalog/catalogeSidebar.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from "react"; -import { useDispatch } from "react-redux"; -import { setFilter, applyFilters } from "./catalogSlice"; -import styles from "./Catalog.module.css"; -import type { Category } from "../../types/categories"; -import type { CategoryAtributes } from "../../types/categories"; -import type { CategoryAtrVals } from "../../types/categories"; - -interface Props{ - categories: Category[]; - atributes: CategoryAtributes[] - atrVals: CategoryAtrVals[]; -} - -const CatalogSidebar: React.FC = ({categories, atributes, atrVals}) => { - const dispatch = useDispatch(); - - const handleFilterChange = (name: string, value: string) => { - dispatch(setFilter({ name, value })); - dispatch(applyFilters()); - }; - - return ( - - ); -}; - -export default CatalogSidebar; \ No newline at end of file diff --git a/front/src/features/catalog/productsSlice.ts b/front/src/features/catalog/productsSlice.ts new file mode 100644 index 0000000..17c4591 --- /dev/null +++ b/front/src/features/catalog/productsSlice.ts @@ -0,0 +1,95 @@ +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; + page?: number; + limit?: number; + filters?: Record; +} + +export const fetchProductsByCategory = createAsyncThunk( + "products/fetchByCategory", + async (params: FetchParams) => { + const { categoryId, page = 1, limit = 9, filters} = params; + const qp = new URLSearchParams(); + qp.append("categoryId", String(categoryId)); + qp.append("page", String(page)); + qp.append("limit", String(limit)); + if (filters) { + Object.keys(filters).forEach((k) => { + const v = filters[k]; + if (Array.isArray(v)) qp.append(k, v.join(",")); + else qp.append(k, String(v)); + }); + } + const res = await client.get(`/products?${qp.toString()}`); + // ожидаем { items: Product[], total: number, page: number, limit: number } + return res.data; + } +); + +interface ProductsState { + items: Product[]; + total: number; + page: number; + limit: number; + loading: boolean; + error: string | null; + filtersApplied: Record; +} + +const initialState: ProductsState = { + items: [], + total: 0, + page: 1, + limit: 9, + loading: false, + error: null, + filtersApplied: {}, +}; + +const slice = createSlice({ + name: "products", + initialState, + reducers: { + setPage(state, action: PayloadAction) { + state.page = action.payload; + }, + setLimit(state, action: PayloadAction) { + state.limit = action.payload; + }, + setFilters(state, action: PayloadAction) { + state.filtersApplied = action.payload; + }, + resetProducts(state) { + state.items = []; + state.total = 0; + state.page = 1; + } + }, + extraReducers: (builder) => { + builder + .addCase(fetchProductsByCategory.pending, (s) => { + s.loading = true; + s.error = null; + }) + .addCase(fetchProductsByCategory.fulfilled, (s, a) => { + s.loading = false; + s.items = a.payload.items; + s.total = a.payload.total; + s.page = a.payload.page ?? s.page; + s.limit = a.payload.limit ?? s.limit; + }) + .addCase(fetchProductsByCategory.rejected, (s, a) => { + s.loading = false; + s.error = a.error.message ?? "Ошибка"; + }); + } +}); + +export const { setPage, setLimit, setFilters, resetProducts } = slice.actions; +export default slice.reducer; \ No newline at end of file diff --git a/front/src/pages/Products/Products.tsx b/front/src/pages/Products/Products.tsx index e85c1ad..d0a2c3c 100644 --- a/front/src/pages/Products/Products.tsx +++ b/front/src/pages/Products/Products.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from "react"; import { useParams } from "react-router-dom"; import type { RootState } from "../../app/store"; -import { getCategoryProducts } from "../../features/catalog/catalogSlice"; +import { getCategoryProducts } from "../../features/catalog/productsSlice"; import CatalogSidebar from "../../features/catalog/catalogeSidebar" import ProductGrid from "../../features/catalog/ProductGrid"; import styles from "../../features/catalog/catalog.module.css"; diff --git a/front/src/types/categories.ts b/front/src/types/categories.ts index e0c1813..9b332c5 100644 --- a/front/src/types/categories.ts +++ b/front/src/types/categories.ts @@ -13,4 +13,5 @@ export interface Category { id: number; name: string; atributes: CategoryAtributes[]; -} \ No newline at end of file +} + From 951003952e654feb9f2dd0edbfba06a964f12f93 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Mon, 24 Nov 2025 02:03:50 +0300 Subject: [PATCH 4/9] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B1=D0=BE=D1=80=D0=B0=20=D0=BA=D0=B0?= =?UTF-8?q?=D1=80=D1=82=D0=BE=D1=87=D0=B5=D0=BA=20=D1=81=20=D0=BA=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D1=8F=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/features/catalog/CategoryCards.tsx | 23 ++++++++++++++++++++ front/src/types/categories.ts | 1 + 2 files changed, 24 insertions(+) create mode 100644 front/src/features/catalog/CategoryCards.tsx diff --git a/front/src/features/catalog/CategoryCards.tsx b/front/src/features/catalog/CategoryCards.tsx new file mode 100644 index 0000000..8f07c5d --- /dev/null +++ b/front/src/features/catalog/CategoryCards.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import styles from "./CategoryCards.module.css"; +import { useAppSelector } from "../../app/hooks"; + +interface Props { + onSelect?: (id:number) => void +} + +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}
+
+ ))} +
+ ); +}; +export default CategoryCards; \ No newline at end of file diff --git a/front/src/types/categories.ts b/front/src/types/categories.ts index 9b332c5..3e813a7 100644 --- a/front/src/types/categories.ts +++ b/front/src/types/categories.ts @@ -12,6 +12,7 @@ export interface CategoryAtributes { export interface Category { id: number; name: string; + image?: string; atributes: CategoryAtributes[]; } From 15e45f2196d03e22e92687a61f162c2ec4626fd6 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Tue, 25 Nov 2025 17:35:37 +0300 Subject: [PATCH 5/9] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B1=D0=BE=D1=80=D0=B0=20=D0=BA=D0=B0?= =?UTF-8?q?=D1=80=D1=82=D0=BE=D1=87=D0=B5=D0=BA=20=D0=BA=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=D1=80=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/features/catalog/CategoriesGrid.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 front/src/features/catalog/CategoriesGrid.tsx diff --git a/front/src/features/catalog/CategoriesGrid.tsx b/front/src/features/catalog/CategoriesGrid.tsx new file mode 100644 index 0000000..7eab034 --- /dev/null +++ b/front/src/features/catalog/CategoriesGrid.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import styles from "./CategoryCards.module.css"; +import { useAppSelector } from "../../app/hooks"; + +interface Props { onSelect?: (id:number) => void } +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}
+
+ ))} +
+ ); +}; +export default CategoryCards; \ No newline at end of file From 88ff1d8b992ee0ee07f176a77f5c446f28900745 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Tue, 25 Nov 2025 17:38:23 +0300 Subject: [PATCH 6/9] =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BB=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B1=D0=BE=D1=80=20?= =?UTF-8?q?=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=BE=D0=B2=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=20=D0=BD=D0=BE=D0=B2=D1=83=D1=8E=20=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=D0=B8=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/features/catalog/CategoryFilters.tsx | 64 +++++++++++++++---- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/front/src/features/catalog/CategoryFilters.tsx b/front/src/features/catalog/CategoryFilters.tsx index 98cbbf2..a086911 100644 --- a/front/src/features/catalog/CategoryFilters.tsx +++ b/front/src/features/catalog/CategoryFilters.tsx @@ -1,22 +1,58 @@ import { useAppSelector, useAppDispatch } from "../../app/hooks"; -import { setSelectedCategory } from "../store/categoriesSlice"; +import { setSelectedCategory } from "./categoriesSlice"; import styles from "./CategoriesSidebar.module.css"; +import { setFilters, fetchProductsByCategory, setPage } from "./productsSlice"; +import { useState } from "react"; -export const CategoriesSidebar = () => { - const { items, selectedCategoryId } = useAppSelector(s => s.categories); +export const CategoryFilters = () => { const dispatch = useAppDispatch(); + const { items: cats, selectedCategoryId } = useAppSelector(s => s.categories); + const products = useAppSelector(s => s.products); + const cat = cats.find(x => x.id === selectedCategoryId); + const [local, setLocal] = useState>({}); + const apply = () => { + dispatch(setFilters(local)); + dispatch(setPage(1)); + if (selectedCategoryId) dispatch(fetchProductsByCategory({ categoryId: selectedCategoryId, page: 1, limit: products.limit, filters: local })); + }; + + if (!cat) return null; return ( -
- {items.map(cat => ( -
dispatch(setSelectedCategory(cat.id))} - > - {cat.name} -
- ))} +
+

ПАРАМЕТРЫ

+
+ {cat.atributes.map(attr => ( +
+
{`${attr.name}:`}
+
+ {attr.vals.map(v => { + const key = `attr_${attr.id}`; + const active = local[key]?.includes(v.val); + return ( + + ); + })} +
+
+ ))} +
+ +
+ + +
); -}; \ No newline at end of file +}; + +export default CategoryFilters; \ No newline at end of file From c1adee646f077325d5e9594cb2fd7d5018ae1977 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Tue, 25 Nov 2025 17:39:03 +0300 Subject: [PATCH 7/9] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B1=D0=BE=D1=80=D0=B0=20=D0=BA=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D1=81=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/catalog/SidebarCategories.tsx | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/front/src/features/catalog/SidebarCategories.tsx b/front/src/features/catalog/SidebarCategories.tsx index bbcdad2..3844280 100644 --- a/front/src/features/catalog/SidebarCategories.tsx +++ b/front/src/features/catalog/SidebarCategories.tsx @@ -5,8 +5,33 @@ import { fetchCategories, setSelectedCategory } from "./categoriesSlice"; import { fetchProductsByCategory, setFilters, setPage } from "./productsSlice"; const SidebarCategories: React.FC = () => { + const dispatch = useAppDispatch(); + const { items, selectedCategoryId, loading } = useAppSelector(s => s.categories); + + useEffect(() => { + dispatch(fetchCategories()); + }, [dispatch]); + + const onClick = (catId: number) => { + dispatch(setSelectedCategory(catId)); + dispatch(setFilters({})); // сброс фильтров + dispatch(setPage(1)); + dispatch(fetchProductsByCategory({ categoryId: catId, page: 1, limit: 9 })); + }; return( - <> - + ) } \ No newline at end of file From 223ec865e0e8383732f510a9f1ebc66db88cb4d0 Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Tue, 25 Nov 2025 17:39:26 +0300 Subject: [PATCH 8/9] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=82=D0=B8=D0=BF=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=80?= =?UTF-8?q?=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/types/product.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/front/src/types/product.ts b/front/src/types/product.ts index 801ce97..eaf7d03 100644 --- a/front/src/types/product.ts +++ b/front/src/types/product.ts @@ -3,6 +3,4 @@ export interface Product { name: string; image: string; description: string; - size: string; - category: string; } \ No newline at end of file From e517c58d57bf3addaec23ca54702abacb6610dea Mon Sep 17 00:00:00 2001 From: Oleg Urin Date: Fri, 5 Dec 2025 00:10:47 +0300 Subject: [PATCH 9/9] =?UTF-8?q?=D0=B4=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BB?= =?UTF-8?q?=20=D0=BA=D0=B0=D1=82=D0=B0=D0=BB=D0=BE=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/features/catalog/Pagination.tsx | 34 +++++++++++++ front/src/features/catalog/ProductGrid.tsx | 9 ++-- .../features/catalog/SidebarCategories.tsx | 4 +- front/src/pages/Products/Products.tsx | 49 ++++++++++--------- 4 files changed, 68 insertions(+), 28 deletions(-) create mode 100644 front/src/features/catalog/Pagination.tsx diff --git a/front/src/features/catalog/Pagination.tsx b/front/src/features/catalog/Pagination.tsx new file mode 100644 index 0000000..939d2a9 --- /dev/null +++ b/front/src/features/catalog/Pagination.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import styles from "./Pagination.module.css"; +import { useAppSelector, useAppDispatch } from "../../app/hooks"; +import { setPage, fetchProductsByCategory } from "./productsSlice"; + +interface Props { categoryId: number } + +const Pagination: React.FC = ({ categoryId }) => { + const dispatch = useAppDispatch(); + const { total, page, limit } = useAppSelector(s => s.products); + const totalPages = Math.ceil(total / limit); + if (totalPages <= 1) return null; + + const go = (p:number) => { + dispatch(setPage(p)); + dispatch(fetchProductsByCategory({ categoryId, page: p, limit })); + window.scrollTo({ top: 200, behavior: "smooth" }); + } + + return ( +
+ + +
+ {Array.from({length: totalPages}, (_,i) => i+1).map(p => ( + + ))} +
+ + +
+ ); +}; +export default Pagination; \ No newline at end of file diff --git a/front/src/features/catalog/ProductGrid.tsx b/front/src/features/catalog/ProductGrid.tsx index 0ddbd2c..f02ad5b 100644 --- a/front/src/features/catalog/ProductGrid.tsx +++ b/front/src/features/catalog/ProductGrid.tsx @@ -1,13 +1,12 @@ import React from "react"; -import type { Product } from "../../types/product"; import ProductCard from "./ProductCard"; import styles from "./Catalog.module.css"; +import { useAppSelector } from "../../app/hooks"; -interface Props { - products: Product[]; -} -const ProductGrid: React.FC = ({ products }) => { +const ProductGrid: React.FC = () => { + const {items: products} = useAppSelector(p => p.products) + return (
{products.map((p) => ( diff --git a/front/src/features/catalog/SidebarCategories.tsx b/front/src/features/catalog/SidebarCategories.tsx index 3844280..b573e26 100644 --- a/front/src/features/catalog/SidebarCategories.tsx +++ b/front/src/features/catalog/SidebarCategories.tsx @@ -34,4 +34,6 @@ const SidebarCategories: React.FC = () => { )} ) -} \ No newline at end of file +} + +export default SidebarCategories; \ No newline at end of file diff --git a/front/src/pages/Products/Products.tsx b/front/src/pages/Products/Products.tsx index d0a2c3c..a4bf779 100644 --- a/front/src/pages/Products/Products.tsx +++ b/front/src/pages/Products/Products.tsx @@ -1,31 +1,36 @@ -import React, { useEffect } from "react"; -import { useParams } from "react-router-dom"; -import type { RootState } from "../../app/store"; -import { getCategoryProducts } from "../../features/catalog/productsSlice"; -import CatalogSidebar from "../../features/catalog/catalogeSidebar" +import React from "react"; +import styles from "./ProductsPage.module.css"; +import SidebarCategories from "../../features/catalog/SidebarCategories"; +import CategoryCards from "../../features/catalog/CategoryCards"; +import Filters from "../../features/catalog/CategoryFilters"; import ProductGrid from "../../features/catalog/ProductGrid"; -import styles from "../../features/catalog/catalog.module.css"; -import { useAppDispatch, useAppSelector } from "../../app/hooks"; +import Pagination from "../../features/catalog/Pagination"; +import { useAppSelector } from "../../app/hooks"; -const CatalogPage: React.FC = () => { - const { category } = useParams<{ category: string }>(); - const dispatch = useAppDispatch() - const { loading, filtered, error } = useAppSelector((s: RootState) => s.catalog); - - useEffect(() => { - if (category) dispatch(getCategoryProducts(category)); - }, [category]); +const ProductsPage: React.FC = () => { + const { selectedCategoryId } = useAppSelector(s => s.categories); return ( -
- -
- {loading &&

Загрузка...

} - {error &&

Ошибка: {error}

} - +
+ +
+ {!selectedCategoryId ? ( + {}} /> + ) : ( + <> +
+ +
+ +
+ + +
+ + )}
); }; -export default CatalogPage; \ No newline at end of file +export default ProductsPage; \ No newline at end of file