13 Commits

Author SHA1 Message Date
95e6b33b56 catalog and bags 2025-07-30 13:57:16 +03:00
a8f783767f Merge pull request 'catalog' (#31) from catalog into main
Reviewed-on: #31
2025-07-30 00:25:53 +03:00
b363b88e33 catalog 2025-07-30 00:25:14 +03:00
72a9772934 parvki 2025-07-29 18:55:22 +03:00
1da9c6ac09 Merge branch 'main' of https://gittea.biveki.ru/BivekiGroup/protekauto-frontend 2025-07-18 19:29:53 +03:00
649ddbfa8a prices and shit 2025-07-18 19:15:25 +03:00
f3d21959c9 prices optimised 2025-07-18 18:12:42 +03:00
61b50d10ba Merge pull request 'сonfidentiality' (#30) from сonfidentiality into main
Reviewed-on: #30
2025-07-18 13:43:52 +03:00
ea76106caa Confidentiality 2025-07-18 13:42:48 +03:00
b7edd73ce0 fixed prices, but still working on filters 2025-07-18 04:22:37 +03:00
b6f9d017d6 catalog prices fix 2025-07-17 21:22:45 +03:00
27d378154f fix1707 2025-07-17 16:35:45 +03:00
5fd2cf1b8c Merge pull request 'fix1607' (#29) from fix1607 into main
Reviewed-on: #29
2025-07-16 14:29:58 +03:00
37 changed files with 1993 additions and 892 deletions

View File

@ -10,6 +10,7 @@ services:
NEXT_PUBLIC_UPLOAD_URL: ${NEXT_PUBLIC_UPLOAD_URL:-http://localhost:4000/upload} NEXT_PUBLIC_UPLOAD_URL: ${NEXT_PUBLIC_UPLOAD_URL:-http://localhost:4000/upload}
NEXT_PUBLIC_MAINTENANCE_MODE: ${NEXT_PUBLIC_MAINTENANCE_MODE:-false} NEXT_PUBLIC_MAINTENANCE_MODE: ${NEXT_PUBLIC_MAINTENANCE_MODE:-false}
NEXT_PUBLIC_YANDEX_MAPS_API_KEY: ${NEXT_PUBLIC_YANDEX_MAPS_API_KEY} NEXT_PUBLIC_YANDEX_MAPS_API_KEY: ${NEXT_PUBLIC_YANDEX_MAPS_API_KEY}
PARTSAPI_URL: ${PARTSAPI_URL:-https://api.parts-index.com}
FRONTEND_PORT: ${FRONTEND_PORT:-3000} FRONTEND_PORT: ${FRONTEND_PORT:-3000}
NODE_ENV: ${NODE_ENV:-production} NODE_ENV: ${NODE_ENV:-production}
container_name: protekauto-frontend container_name: protekauto-frontend
@ -26,6 +27,7 @@ services:
- NEXT_PUBLIC_CMS_GRAPHQL_URL=${NEXT_PUBLIC_CMS_GRAPHQL_URL:-http://localhost:4000/graphql} - NEXT_PUBLIC_CMS_GRAPHQL_URL=${NEXT_PUBLIC_CMS_GRAPHQL_URL:-http://localhost:4000/graphql}
- NEXT_PUBLIC_UPLOAD_URL=${NEXT_PUBLIC_UPLOAD_URL:-http://localhost:4000/upload} - NEXT_PUBLIC_UPLOAD_URL=${NEXT_PUBLIC_UPLOAD_URL:-http://localhost:4000/upload}
- NEXT_PUBLIC_MAINTENANCE_MODE=${NEXT_PUBLIC_MAINTENANCE_MODE:-false} - NEXT_PUBLIC_MAINTENANCE_MODE=${NEXT_PUBLIC_MAINTENANCE_MODE:-false}
- PARTSAPI_URL=${PARTSAPI_URL:-https://api.parts-index.com}
# Yandex Maps API # Yandex Maps API
- NEXT_PUBLIC_YANDEX_MAPS_API_KEY=${NEXT_PUBLIC_YANDEX_MAPS_API_KEY} - NEXT_PUBLIC_YANDEX_MAPS_API_KEY=${NEXT_PUBLIC_YANDEX_MAPS_API_KEY}

BIN
public/images/noimage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -72,6 +72,10 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
return parseFloat(cleanPrice) || 0; return parseFloat(cleanPrice) || 0;
}; };
// Note: BestPriceCard doesn't receive isInCart flags from backend
// Since it's a summary component, we'll remove cart state checking for now
const inCart = false; // Disabled for BestPriceCard
// Обработчик добавления в корзину // Обработчик добавления в корзину
const handleAddToCart = async (e: React.MouseEvent) => { const handleAddToCart = async (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
@ -108,10 +112,14 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
}); });
if (result.success) { if (result.success) {
// Показываем тоастер об успешном добавлении // Показываем тоастер с разным текстом в зависимости от того, был ли товар уже в корзине
const toastMessage = inCart
? `Количество увеличено (+${count} шт.)`
: 'Товар добавлен в корзину!';
toast.success( toast.success(
<div> <div>
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div> <div className="font-semibold" style={{ color: '#fff' }}>{toastMessage}</div>
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${offer.brand} ${offer.articleNumber} (${count} шт.)`}</div> <div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${offer.brand} ${offer.articleNumber} (${count} шт.)`}</div>
</div>, </div>,
{ {
@ -176,17 +184,55 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
</div> </div>
</div> </div>
<div className="w-layout-hflex flex-block-42"> <div className="w-layout-hflex flex-block-42">
<div style={{ position: 'relative', display: 'inline-block' }}>
<button <button
type="button" type="button"
onClick={handleAddToCart} onClick={handleAddToCart}
className="button-icon w-inline-block" className={`button-icon w-inline-block ${inCart ? 'in-cart' : ''}`}
style={{ cursor: 'pointer', textDecoration: 'none' }} style={{
aria-label="Добавить в корзину" cursor: 'pointer',
textDecoration: 'none',
opacity: inCart ? 0.5 : 1,
backgroundColor: inCart ? '#9ca3af' : undefined
}}
aria-label={inCart ? "Товар уже в корзине" : "Добавить в корзину"}
title={inCart ? "Товар уже в корзине - нажмите для добавления еще" : "Добавить в корзину"}
> >
<div className="div-block-26"> <div className="div-block-26">
<div className="icon-setting w-embed"><svg width="currentWidht" height="currentHeight" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.1998 22.2C8.8798 22.2 7.81184 23.28 7.81184 24.6C7.81184 25.92 8.8798 27 10.1998 27C11.5197 27 12.5997 25.92 12.5997 24.6C12.5997 23.28 11.5197 22.2 10.1998 22.2ZM3 3V5.4H5.39992L9.71977 14.508L8.09982 17.448C7.90783 17.784 7.79984 18.18 7.79984 18.6C7.79984 19.92 8.8798 21 10.1998 21H24.5993V18.6H10.7037C10.5357 18.6 10.4037 18.468 10.4037 18.3L10.4397 18.156L11.5197 16.2H20.4594C21.3594 16.2 22.1513 15.708 22.5593 14.964L26.8552 7.176C26.9542 6.99286 27.004 6.78718 26.9997 6.57904C26.9955 6.37089 26.9373 6.16741 26.8309 5.98847C26.7245 5.80952 26.5736 5.66124 26.3927 5.55809C26.2119 5.45495 26.0074 5.40048 25.7992 5.4H8.05183L6.92387 3H3ZM22.1993 22.2C20.8794 22.2 19.8114 23.28 19.8114 24.6C19.8114 25.92 20.8794 27 22.1993 27C23.5193 27 24.5993 25.92 24.5993 24.6C24.5993 23.28 23.5193 22.2 22.1993 22.2Z" fill="currentColor"/></svg></div> <div
className="icon-setting w-embed"
style={{
filter: inCart ? 'brightness(0.7)' : undefined
}}
>
<svg width="currentWidht" height="currentHeight" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.1998 22.2C8.8798 22.2 7.81184 23.28 7.81184 24.6C7.81184 25.92 8.8798 27 10.1998 27C11.5197 27 12.5997 25.92 12.5997 24.6C12.5997 23.28 11.5197 22.2 10.1998 22.2ZM3 3V5.4H5.39992L9.71977 14.508L8.09982 17.448C7.90783 17.784 7.79984 18.18 7.79984 18.6C7.79984 19.92 8.8798 21 10.1998 21H24.5993V18.6H10.7037C10.5357 18.6 10.4037 18.468 10.4037 18.3L10.4397 18.156L11.5197 16.2H20.4594C21.3594 16.2 22.1513 15.708 22.5593 14.964L26.8552 7.176C26.9542 6.99286 27.004 6.78718 26.9997 6.57904C26.9955 6.37089 26.9373 6.16741 26.8309 5.98847C26.7245 5.80952 26.5736 5.66124 26.3927 5.55809C26.2119 5.45495 26.0074 5.40048 25.7992 5.4H8.05183L6.92387 3H3ZM22.1993 22.2C20.8794 22.2 19.8114 23.28 19.8114 24.6C19.8114 25.92 20.8794 27 22.1993 27C23.5193 27 24.5993 25.92 24.5993 24.6C24.5993 23.28 23.5193 22.2 22.1993 22.2Z" fill="currentColor"/></svg>
</div>
</div> </div>
</button> </button>
{inCart && (
<div
style={{
position: 'absolute',
top: '-8px',
right: '-8px',
backgroundColor: '#22c55e',
color: 'white',
borderRadius: '50%',
width: '16px',
height: '16px',
fontSize: '10px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold',
zIndex: 1
}}
title="В корзине"
>
</div>
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { useState } from "react";
import { useCart } from "@/contexts/CartContext"; import { useCart } from "@/contexts/CartContext";
import { useFavorites } from "@/contexts/FavoritesContext"; import { useFavorites } from "@/contexts/FavoritesContext";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
@ -13,6 +13,7 @@ interface BestPriceItemProps {
article?: string; article?: string;
productId?: string; productId?: string;
onAddToCart?: (e: React.MouseEvent) => void; onAddToCart?: (e: React.MouseEvent) => void;
isInCart?: boolean;
} }
const BestPriceItem: React.FC<BestPriceItemProps> = ({ const BestPriceItem: React.FC<BestPriceItemProps> = ({
@ -25,9 +26,11 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
article, article,
productId, productId,
onAddToCart, onAddToCart,
isInCart = false,
}) => { }) => {
const { addItem } = useCart(); const { addItem } = useCart();
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites(); const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
const [localInCart, setLocalInCart] = useState(false);
// Проверяем, есть ли товар в избранном // Проверяем, есть ли товар в избранном
const isItemFavorite = isFavorite(productId, undefined, article, brand); const isItemFavorite = isFavorite(productId, undefined, article, brand);
@ -42,6 +45,9 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
const handleAddToCart = async (e: React.MouseEvent) => { const handleAddToCart = async (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (!localInCart) {
setLocalInCart(true);
}
// Если передан кастомный обработчик, используем его // Если передан кастомный обработчик, используем его
if (onAddToCart) { if (onAddToCart) {
@ -122,7 +128,6 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
currency: 'RUB', currency: 'RUB',
image: image image: image
}); });
toast.success('Товар добавлен в избранное');
} }
}; };
@ -168,8 +173,13 @@ const BestPriceItem: React.FC<BestPriceItemProps> = ({
<a <a
href="#" href="#"
className="button-icon w-inline-block" className="button-icon w-inline-block"
onClick={handleAddToCart} onClick={isInCart ? undefined : handleAddToCart}
style={{ cursor: 'pointer' }} style={{
cursor: isInCart ? 'default' : (localInCart ? 'default' : 'pointer'),
background: isInCart ? '#9ca3af' : (localInCart ? '#2563eb' : undefined),
opacity: isInCart || localInCart ? 0.5 : 1,
filter: isInCart || localInCart ? 'grayscale(1)' : 'none'
}}
aria-label="Добавить в корзину" aria-label="Добавить в корзину"
> >
<div className="div-block-26"> <div className="div-block-26">

View File

@ -277,8 +277,7 @@
} }
} }
} }
const activeCatalog = catalogsData?.partsIndexCategoriesWithGroups?.[tabData.findIndex(tab => tab === mobileCategory)]; const catalogId = mobileCategory.catalogId || 'fallback';
const catalogId = activeCatalog?.id || 'fallback';
handleCategoryClick(catalogId, mobileCategory.links[0], subcategoryId); handleCategoryClick(catalogId, mobileCategory.links[0], subcategoryId);
}} }}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
@ -306,8 +305,7 @@
} }
} }
} }
const activeCatalog = catalogsData?.partsIndexCategoriesWithGroups?.[tabData.findIndex(tab => tab === mobileCategory)]; const catalogId = mobileCategory.catalogId || 'fallback';
const catalogId = activeCatalog?.id || 'fallback';
handleCategoryClick(catalogId, link, subcategoryId); handleCategoryClick(catalogId, link, subcategoryId);
}} }}
> >
@ -333,7 +331,7 @@
{tabData.map((cat, index) => { {tabData.map((cat, index) => {
// Получаем ID каталога из данных PartsIndex или создаем fallback ID // Получаем ID каталога из данных PartsIndex или создаем fallback ID
const catalogId = catalogsData?.partsIndexCategoriesWithGroups?.[index]?.id || `fallback_${index}`; const catalogId = catalogsData?.partsIndexCategoriesWithGroups?.[index]?.id || `fallback_${index}`;
const groups = catalogsData?.partsIndexCategoriesWithGroups?.[index]?.groups || [];
return ( return (
<div <div
className="mobile-subcategory" className="mobile-subcategory"
@ -343,7 +341,7 @@
const categoryWithData = { const categoryWithData = {
...cat, ...cat,
catalogId, catalogId,
groups: catalogsData?.partsIndexCategoriesWithGroups?.[index]?.groups groups
}; };
setMobileCategory(categoryWithData); setMobileCategory(categoryWithData);
}} }}

View File

@ -1,79 +1,54 @@
import React, { useState, useEffect } from 'react'; import React from 'react';
import { useCart } from '@/contexts/CartContext'; import { useCart } from '@/contexts/CartContext';
const CartDebug: React.FC = () => { const CartDebug: React.FC = () => {
const { state, addItem, clearCart } = useCart(); const { state, isInCart } = useCart();
const [debugInfo, setDebugInfo] = useState<any>({});
useEffect(() => { if (process.env.NODE_ENV !== 'development') {
if (typeof window !== 'undefined') { return null;
const cartState = localStorage.getItem('cartState');
const cartSummaryState = localStorage.getItem('cartSummaryState');
const oldCart = localStorage.getItem('cart');
setDebugInfo({
cartState: cartState ? JSON.parse(cartState) : null,
cartSummaryState: cartSummaryState ? JSON.parse(cartSummaryState) : null,
oldCart: oldCart ? JSON.parse(oldCart) : null,
currentItems: state.items.length
});
} }
}, [state.items]);
const addTestItem = () => { // Test the isInCart function with some example values from the cart
addItem({ const testItem = state.items[0];
name: 'Тестовый товар', const testResult = testItem ? isInCart(testItem.productId, testItem.offerKey, testItem.article, testItem.brand) : false;
description: 'Описание тестового товара',
article: 'TEST123',
brand: 'TestBrand',
price: 1000,
currency: 'RUB',
quantity: 1,
image: '',
productId: 'test-product',
offerKey: 'test-offer',
isExternal: false
});
};
const clearStorage = () => {
if (typeof window !== 'undefined') {
localStorage.removeItem('cartState');
localStorage.removeItem('cartSummaryState');
localStorage.removeItem('cart');
window.location.reload();
}
};
return ( return (
<div style={{ <div
style={{
position: 'fixed', position: 'fixed',
top: '10px', top: '10px',
right: '10px', right: '10px',
background: 'white', background: 'rgba(0,0,0,0.9)',
border: '1px solid #ccc', color: 'white',
padding: '10px', padding: '10px',
borderRadius: '5px', borderRadius: '5px',
maxWidth: '300px', fontSize: '11px',
fontSize: '12px', maxWidth: '350px',
zIndex: 9999 zIndex: 9999,
}}> maxHeight: '400px',
<h4>Cart Debug</h4> overflow: 'auto'
<button onClick={addTestItem} style={{ marginBottom: '5px', marginRight: '5px' }}> }}
Добавить товар >
</button> <div style={{ fontWeight: 'bold', marginBottom: '5px' }}>🛒 Cart Debug: {state.items.length} items</div>
<button onClick={clearCart} style={{ marginBottom: '5px', marginRight: '5px' }}> {testItem && (
Очистить корзину <div style={{ background: 'rgba(255,255,255,0.1)', padding: '5px', marginBottom: '5px', fontSize: '10px' }}>
</button> <div>Testing isInCart for first item:</div>
<button onClick={clearStorage} style={{ marginBottom: '10px' }}> <div>Brand: {testItem.brand}, Article: {testItem.article}</div>
Очистить localStorage <div>Result: {testResult ? '✅ Found' : '❌ Not found'}</div>
</button>
<div>
<strong>Товаров в корзине:</strong> {state.items.length}
</div> </div>
<pre style={{ fontSize: '10px', maxHeight: '200px', overflow: 'auto' }}> )}
{JSON.stringify(debugInfo, null, 2)} {state.items.slice(0, 6).map((item, idx) => (
</pre> <div key={idx} style={{ fontSize: '9px', marginTop: '3px', borderBottom: '1px solid rgba(255,255,255,0.2)', paddingBottom: '2px' }}>
{item.brand} {item.article}
{item.productId && <div style={{ color: '#90EE90' }}>PID: {item.productId.substring(0, 8)}...</div>}
{item.offerKey && <div style={{ color: '#87CEEB' }}>OK: {item.offerKey.substring(0, 15)}...</div>}
</div>
))}
{state.items.length > 6 && (
<div style={{ fontSize: '9px', marginTop: '3px', opacity: 0.7 }}>
...и еще {state.items.length - 6} товаров
</div>
)}
</div> </div>
); );
}; };

View File

@ -1,5 +1,4 @@
import Link from "next/link"; import React, { useState } from "react";
import React from "react";
import { useFavorites } from "@/contexts/FavoritesContext"; import { useFavorites } from "@/contexts/FavoritesContext";
interface CatalogProductCardProps { interface CatalogProductCardProps {
@ -15,8 +14,9 @@ interface CatalogProductCardProps {
productId?: string; productId?: string;
offerKey?: string; offerKey?: string;
currency?: string; currency?: string;
priceElement?: React.ReactNode; // Элемент для отображения цены (например, скелетон) priceElement?: React.ReactNode;
onAddToCart?: (e: React.MouseEvent) => void | Promise<void>; onAddToCart?: (e: React.MouseEvent) => void | Promise<void>;
isInCart?: boolean;
} }
const CatalogProductCard: React.FC<CatalogProductCardProps> = ({ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
@ -34,43 +34,34 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
currency = 'RUB', currency = 'RUB',
priceElement, priceElement,
onAddToCart, onAddToCart,
isInCart = false,
}) => { }) => {
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites(); const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
const [localInCart, setLocalInCart] = useState(false);
// Обрабатываем пустое изображение - используем SVG-заглушку вместо мокап-фотки
const displayImage = image || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjE5MCIgdmlld0JveD0iMCAwIDIxMCAxOTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMTAiIGhlaWdodD0iMTkwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik04NSA5NUw5NSA4NUwxMjUgMTE1TDE0MCA5NUwxNjUgMTIwSDE2NVY5MEg0NVY5MEw4NSA5NVoiIGZpbGw9IiNEMUQ1REIiLz4KPGNpcmNsZSBjeD0iNzUiIGN5PSI3NSIgcj0iMTAiIGZpbGw9IiNEMUQ1REIiLz4KPHRleHQgeD0iMTA1IiB5PSIxNTAiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzlDQTNBRiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Tm8gaW1hZ2U8L3RleHQ+Cjwvc3ZnPgo='; const displayImage = image || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjE5MCIgdmlld0JveD0iMCAwIDIxMCAxOTAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMTAiIGhlaWdodD0iMTkwIiBmaWxsPSIjRjNGNEY2Ii8+CjxwYXRoIGQ9Ik04NSA5NUw5NSA4NUwxMjUgMTE1TDE0MCA5NUwxNjUgMTIwSDE2NVY5MEg0NVY5MEw4NSA5NVoiIGZpbGw9IiNEMUQ1REIiLz4KPGNpcmNsZSBjeD0iNzUiIGN5PSI3NSIgcj0iMTAiIGZpbGw9IiNEMUQ1REIiLz4KPHRleHQgeD0iMTA1IiB5PSIxNTAiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzlDQTNBRiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+Tm8gaW1hZ2U8L3RleHQ+Cjwvc3ZnPgo=';
// Создаем ссылку на card с параметрами товара
const cardUrl = articleNumber && brandName const cardUrl = articleNumber && brandName
? `/card?article=${encodeURIComponent(articleNumber)}&brand=${encodeURIComponent(brandName)}${artId ? `&artId=${artId}` : ''}` ? `/card?article=${encodeURIComponent(articleNumber)}&brand=${encodeURIComponent(brandName)}${artId ? `&artId=${artId}` : ''}`
: '/card'; // Fallback на card если нет данных : '/card';
// Проверяем, есть ли товар в избранном
const isItemFavorite = isFavorite(productId, offerKey, articleNumber, brandName || brand); const isItemFavorite = isFavorite(productId, offerKey, articleNumber, brandName || brand);
// Обработчик клика по сердечку
const handleFavoriteClick = (e: React.MouseEvent) => { const handleFavoriteClick = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// Извлекаем цену как число
const numericPrice = parseFloat(price.replace(/[^\d.,]/g, '').replace(',', '.')) || 0; const numericPrice = parseFloat(price.replace(/[^\d.,]/g, '').replace(',', '.')) || 0;
if (isItemFavorite) { if (isItemFavorite) {
// Находим товар в избранном по правильному ID
const favoriteItem = favorites.find((fav: any) => { const favoriteItem = favorites.find((fav: any) => {
// Проверяем по разным комбинациям идентификаторов
if (productId && fav.productId === productId) return true; if (productId && fav.productId === productId) return true;
if (offerKey && fav.offerKey === offerKey) return true; if (offerKey && fav.offerKey === offerKey) return true;
if (fav.article === articleNumber && fav.brand === (brandName || brand)) return true; if (fav.article === articleNumber && fav.brand === (brandName || brand)) return true;
return false; return false;
}); });
if (favoriteItem) { if (favoriteItem) {
removeFromFavorites(favoriteItem.id); removeFromFavorites(favoriteItem.id);
} }
} else { } else {
// Добавляем в избранное
addToFavorites({ addToFavorites({
productId, productId,
offerKey, offerKey,
@ -84,30 +75,25 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
} }
}; };
// Обработчик клика по кнопке "Купить"
const handleBuyClick = (e: React.MouseEvent) => { const handleBuyClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!isInCart && !localInCart) {
setLocalInCart(true);
}
if (onAddToCart) { if (onAddToCart) {
onAddToCart(e); onAddToCart(e);
} else { } else {
// Fallback - переходим на страницу товара
window.location.href = cardUrl; window.location.href = cardUrl;
} }
}; };
return ( return (
<div className="w-layout-vflex flex-block-15-copy" data-article-card="visible" itemScope itemType="https://schema.org/Product">
<div <div
className="w-layout-vflex flex-block-15-copy" className={`favcardcat${isItemFavorite ? ' favorite-active' : ''}`}
data-article-card="visible"
itemScope
itemType="https://schema.org/Product"
>
<div
className={`favcardcat ${isItemFavorite ? 'favorite-active' : ''}`}
onClick={handleFavoriteClick} onClick={handleFavoriteClick}
style={{ style={{ cursor: 'pointer', color: isItemFavorite ? '#ff4444' : '#ccc' }}
cursor: 'pointer',
color: isItemFavorite ? '#ff4444' : '#ccc'
}}
> >
<div className="icon-setting w-embed"> <div className="icon-setting w-embed">
<svg width="currentwidth" height="currentheight" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="currentwidth" height="currentheight" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -115,9 +101,7 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
</svg> </svg>
</div> </div>
</div> </div>
<div className="div-block-4">
{/* Делаем картинку и контент кликабельными для перехода на card */}
<Link href={cardUrl} className="div-block-4" style={{ textDecoration: 'none', color: 'inherit' }}>
<img <img
src={displayImage} src={displayImage}
loading="lazy" loading="lazy"
@ -127,10 +111,18 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
className="image-5" className="image-5"
itemProp="image" itemProp="image"
/> />
<div className="text-block-7">{discount}</div> <div
</Link> className="text-block-7"
style={{
<Link href={cardUrl} className="div-block-3" style={{ textDecoration: 'none', color: 'inherit' }}> background: discount ? undefined : 'transparent',
color: discount ? undefined : 'transparent',
border: discount ? undefined : 'none',
}}
>
{discount || ''}
</div>
</div>
<div className="div-block-3">
<div className="w-layout-hflex flex-block-16"> <div className="w-layout-hflex flex-block-16">
{priceElement ? ( {priceElement ? (
<div className="text-block-8">{priceElement}</div> <div className="text-block-8">{priceElement}</div>
@ -142,23 +134,36 @@ const CatalogProductCard: React.FC<CatalogProductCardProps> = ({
)} )}
<div className="text-block-9">{oldPrice}</div> <div className="text-block-9">{oldPrice}</div>
</div> </div>
<div className="w-layout-hflex flex-block-122">
<div className="w-layout-vflex">
<div className="text-block-10" itemProp="name">{title}</div> <div className="text-block-10" itemProp="name">{title}</div>
<div className="text-block-11" itemProp="brand" itemScope itemType="https://schema.org/Brand"> <div className="text-block-11" itemProp="brand" itemScope itemType="https://schema.org/Brand">
<span itemProp="name">{brand}</span> <span itemProp="name">{brand}</span>
</div> </div>
<meta itemProp="sku" content={articleNumber || ''} /> </div>
</Link> <a
href="#"
{/* Обновляем кнопку купить */} className="button-icon w-inline-block"
<div className="catc w-inline-block" onClick={handleBuyClick} style={{ cursor: 'pointer' }}> onClick={handleBuyClick}
<div className="div-block-25"> style={{
cursor: isInCart || localInCart ? 'default' : 'pointer',
opacity: isInCart || localInCart ? 0.5 : 1,
filter: isInCart || localInCart ? 'grayscale(1)' : 'none',
background: isInCart || localInCart ? '#2563eb' : undefined
}}
aria-label={isInCart || localInCart ? 'В корзине' : 'Купить'}
tabIndex={0}
>
<div className="div-block-26">
<div className="icon-setting w-embed"> <div className="icon-setting w-embed">
<svg width="currentWidht" height="currentHeight" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="currentWidht" height="currentHeight" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.1998 22.2C8.8798 22.2 7.81184 23.28 7.81184 24.6C7.81184 25.92 8.8798 27 10.1998 27C11.5197 27 12.5997 25.92 12.5997 24.6C12.5997 23.28 11.5197 22.2 10.1998 22.2ZM3 3V5.4H5.39992L9.71977 14.508L8.09982 17.448C7.90783 17.784 7.79984 18.18 7.79984 18.6C7.79984 19.92 8.8798 21 10.1998 21H24.5993V18.6H10.7037C10.5357 18.6 10.4037 18.468 10.4037 18.3L10.4397 18.156L11.5197 16.2H20.4594C21.3594 16.2 22.1513 15.708 22.5593 14.964L26.8552 7.176C26.9542 6.99286 27.004 6.78718 26.9997 6.57904C26.9955 6.37089 26.9373 6.16741 26.8309 5.98847C26.7245 5.80952 26.5736 5.66124 26.3927 5.55809C26.2119 5.45495 26.0074 5.40048 25.7992 5.4H8.05183L6.92387 3H3ZM22.1993 22.2C20.8794 22.2 19.8114 23.28 19.8114 24.6C19.8114 25.92 20.8794 27 22.1993 27C23.5193 27 24.5993 25.92 24.5993 24.6C24.5993 23.28 23.5193 22.2 22.1993 22.2Z" fill="currentColor"></path> <path d="M10.1998 22.2C8.8798 22.2 7.81184 23.28 7.81184 24.6C7.81184 25.92 8.8798 27 10.1998 27C11.5197 27 12.5997 25.92 12.5997 24.6C12.5997 23.28 11.5197 22.2 10.1998 22.2ZM3 3V5.4H5.39992L9.71977 14.508L8.09982 17.448C7.90783 17.784 7.79984 18.18 7.79984 18.6C7.79984 19.92 8.8798 21 10.1998 21H24.5993V18.6H10.7037C10.5357 18.6 10.4037 18.468 10.4037 18.3L10.4397 18.156L11.5197 16.2H20.4594C21.3594 16.2 22.1513 15.708 22.5593 14.964L26.8552 7.176C26.9542 6.99286 27.004 6.78718 26.9997 6.57904C26.9955 6.37089 26.9373 6.16741 26.8309 5.98847C26.7245 5.80952 26.5736 5.66124 26.3927 5.55809C26.2119 5.45495 26.0074 5.40048 25.7992 5.4H8.05183L6.92387 3H3ZM22.1993 22.2C20.8794 22.2 19.8114 23.28 19.8114 24.6C19.8114 25.92 20.8794 27 22.1993 27C23.5193 27 24.5993 25.92 24.5993 24.6C24.5993 23.28 23.5193 22.2 22.1993 22.2Z" fill="currentColor"></path>
</svg> </svg>
</div> </div>
</div> </div>
<div className="text-block-6">Купить</div> </a>
</div>
<meta itemProp="sku" content={articleNumber || ''} />
</div> </div>
</div> </div>
); );

View File

@ -21,6 +21,8 @@ interface CoreProductCardOffer {
warehouse?: string; warehouse?: string;
supplier?: string; supplier?: string;
deliveryTime?: number; deliveryTime?: number;
hasStock?: boolean;
isInCart?: boolean;
} }
interface CoreProductCardProps { interface CoreProductCardProps {
@ -34,6 +36,7 @@ interface CoreProductCardProps {
isLoadingOffers?: boolean; isLoadingOffers?: boolean;
onLoadOffers?: () => void; onLoadOffers?: () => void;
partsIndexPowered?: boolean; partsIndexPowered?: boolean;
hasStock?: boolean;
} }
const CoreProductCard: React.FC<CoreProductCardProps> = ({ const CoreProductCard: React.FC<CoreProductCardProps> = ({
@ -46,7 +49,8 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
isAnalog = false, isAnalog = false,
isLoadingOffers = false, isLoadingOffers = false,
onLoadOffers, onLoadOffers,
partsIndexPowered = false partsIndexPowered = false,
hasStock = true
}) => { }) => {
const { addItem } = useCart(); const { addItem } = useCart();
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites(); const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
@ -59,6 +63,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
offers.reduce((acc, _, index) => ({ ...acc, [index]: "1" }), {}) offers.reduce((acc, _, index) => ({ ...acc, [index]: "1" }), {})
); );
const [quantityErrors, setQuantityErrors] = useState<{ [key: number]: string }>({}); const [quantityErrors, setQuantityErrors] = useState<{ [key: number]: string }>({});
const [localInCart, setLocalInCart] = useState<{ [key: number]: boolean }>({});
useEffect(() => { useEffect(() => {
setInputValues(offers.reduce((acc, _, index) => ({ ...acc, [index]: "1" }), {})); setInputValues(offers.reduce((acc, _, index) => ({ ...acc, [index]: "1" }), {}));
@ -120,6 +125,8 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
brand brand
); );
// Теперь используем isInCart флаг из backend вместо frontend проверки
const handleInputChange = (idx: number, val: string) => { const handleInputChange = (idx: number, val: string) => {
setInputValues(prev => ({ ...prev, [idx]: val })); setInputValues(prev => ({ ...prev, [idx]: val }));
if (val === "") return; if (val === "") return;
@ -152,8 +159,10 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
}; };
const handleAddToCart = async (offer: CoreProductCardOffer, index: number) => { const handleAddToCart = async (offer: CoreProductCardOffer, index: number) => {
setLocalInCart(prev => ({ ...prev, [index]: true }));
const quantity = quantities[index] || 1; const quantity = quantities[index] || 1;
const availableStock = parseStock(offer.pcs); const availableStock = parseStock(offer.pcs);
const inCart = offer.isInCart || false; // Use backend flag
const numericPrice = parsePrice(offer.price); const numericPrice = parsePrice(offer.price);
@ -176,10 +185,14 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
}); });
if (result.success) { if (result.success) {
// Показываем тоастер вместо alert // Показываем тоастер с разным текстом в зависимости от того, был ли товар уже в корзине
const toastMessage = inCart
? `Количество увеличено (+${quantity} шт.)`
: 'Товар добавлен в корзину!';
toast.success( toast.success(
<div> <div>
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div> <div className="font-semibold" style={{ color: '#fff' }}>{toastMessage}</div>
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${brand} ${article} (${quantity} шт.)`}</div> <div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${brand} ${article} (${quantity} шт.)`}</div>
</div>, </div>,
{ {
@ -256,7 +269,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
if (!offers || offers.length === 0) { if (!offers || offers.length === 0) {
return ( return (
<div className="w-layout-hflex core-product-search-s1"> <div className={`w-layout-hflex core-product-search-s1 ${!hasStock ? 'out-of-stock-highlight' : ''}`} style={!hasStock ? { backgroundColor: '#fee', borderColor: '#f87171' } : {}}>
<div className="w-layout-vflex core-product-s1"> <div className="w-layout-vflex core-product-s1">
<div className="w-layout-vflex flex-block-47"> <div className="w-layout-vflex flex-block-47">
<div className="div-block-19"> <div className="div-block-19">
@ -266,6 +279,19 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
<div className="w-layout-hflex flex-block-79"> <div className="w-layout-hflex flex-block-79">
<h3 className="heading-10 name">{brand}</h3> <h3 className="heading-10 name">{brand}</h3>
<h3 className="heading-10">{article}</h3> <h3 className="heading-10">{article}</h3>
{!hasStock && (
<span className="out-of-stock-badge" style={{
backgroundColor: '#dc2626',
color: 'white',
padding: '2px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: '500',
marginLeft: '8px'
}}>
Нет в наличии
</span>
)}
</div> </div>
<div className="text-block-21">{name}</div> <div className="text-block-21">{name}</div>
</div> </div>
@ -299,7 +325,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
return ( return (
<> <>
<div className="w-layout-hflex core-product-search-s1"> <div className={`w-layout-hflex core-product-search-s1 ${!hasStock ? 'out-of-stock-highlight' : ''}`} style={!hasStock ? { backgroundColor: '#fee', borderColor: '#f87171' } : {}}>
<div className="w-layout-vflex flex-block-48-copy"> <div className="w-layout-vflex flex-block-48-copy">
<div className="w-layout-vflex product-list-search-s1"> <div className="w-layout-vflex product-list-search-s1">
@ -312,9 +338,22 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
<img src="/images/info.svg" loading="lazy" alt="info" className="image-9" /> <img src="/images/info.svg" loading="lazy" alt="info" className="image-9" />
</div> </div>
<div className="w-layout-vflex flex-block-50"> <div className="w-layout-vflex flex-block-50">
<div className="w-layout-hflex flex-block-79"> <div className="flex flex-row flex-nowrap items-center gap-2">
<h3 className="heading-10 name">{brand}</h3> <h3 className="heading-10 name" style={{marginRight: 8}}>{brand}</h3>
<h3 className="heading-10">{article}</h3> <h3 className="heading-10" style={{marginRight: 8}}>{article}</h3>
{!hasStock && (
<span className="out-of-stock-badge" style={{
backgroundColor: '#dc2626',
color: 'white',
padding: '2px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: '500',
marginLeft: '8px'
}}>
Нет в наличии
</span>
)}
<div <div
className="favorite-icon w-embed" className="favorite-icon w-embed"
onClick={handleFavoriteClick} onClick={handleFavoriteClick}
@ -328,7 +367,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
</svg> </svg>
</div> </div>
</div> </div>
<div className="text-block-21">{name}</div> <div className="text-block-21 mt-1">{name}</div>
</div> </div>
</div> </div>
{image && ( {image && (
@ -370,6 +409,11 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
{displayedOffers.map((offer, idx) => { {displayedOffers.map((offer, idx) => {
const isLast = idx === displayedOffers.length - 1; const isLast = idx === displayedOffers.length - 1;
const maxCount = parseStock(offer.pcs); const maxCount = parseStock(offer.pcs);
const inCart = offer.isInCart || false;
const isLocallyInCart = !!localInCart[idx];
// Backend now provides isInCart flag directly
return ( return (
<div <div
className="w-layout-hflex product-item-search-s1" className="w-layout-hflex product-item-search-s1"
@ -439,17 +483,56 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
</div> </div>
</div> </div>
</div> </div>
<div style={{ position: 'relative', display: 'inline-block' }}>
<button <button
type="button" type="button"
onClick={() => handleAddToCart(offer, idx)} onClick={() => handleAddToCart(offer, idx)}
className="button-icon w-inline-block" className={`button-icon w-inline-block ${inCart || isLocallyInCart ? 'in-cart' : ''}`}
style={{ cursor: 'pointer' }} style={{
aria-label="Добавить в корзину" cursor: 'pointer',
opacity: inCart || isLocallyInCart ? 0.5 : 1,
backgroundColor: inCart || isLocallyInCart ? '#2563eb' : undefined
}}
aria-label={inCart || isLocallyInCart ? "Товар уже в корзине" : "Добавить в корзину"}
title={inCart || isLocallyInCart ? "Товар уже в корзине - нажмите для добавления еще" : "Добавить в корзину"}
> >
<div className="div-block-26"> <div className="div-block-26">
<img loading="lazy" src="/images/cart_icon.svg" alt="В корзину" className="image-11" /> <img
loading="lazy"
src="/images/cart_icon.svg"
alt={inCart || isLocallyInCart ? "В корзине" : "В корзину"}
className="image-11"
style={{
filter: inCart || isLocallyInCart ? 'brightness(0.7)' : undefined
}}
/>
</div> </div>
</button> </button>
{inCart && (
<div
style={{
position: 'absolute',
top: '-8px',
right: '-8px',
backgroundColor: '#22c55e',
color: 'white',
borderRadius: '50%',
width: '16px',
height: '16px',
fontSize: '10px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold',
zIndex: 1
}}
title="В корзине"
>
</div>
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -63,6 +63,7 @@ const FiltersPanelMobile: React.FC<FiltersPanelMobileProps> = ({
setLocalFilterValues({}); setLocalFilterValues({});
onSearchChange(''); onSearchChange('');
// Сбрасываем фильтры в родительском компоненте // Сбрасываем фильтры в родительском компоненте
// Используем пустые массивы для правильной очистки
Object.keys(filterValues).forEach(key => { Object.keys(filterValues).forEach(key => {
onFilterChange?.(key, []); onFilterChange?.(key, []);
}); });

View File

@ -115,7 +115,7 @@ const Footer = () => (
<button className="bg-[#23407A] rounded-lg py-2 px-6 font-medium mt-1 mb-2">Напиши нам</button> <button className="bg-[#23407A] rounded-lg py-2 px-6 font-medium mt-1 mb-2">Напиши нам</button>
</div> </div>
{/* Центр: меню */} {/* Центр: меню */}
<div className="hidden md:flex flex-1 flex-wrap gap-10 justify-center min-w-[400px]"> <div className="hidden md:flex flex-1 flex-wrap gap-30 justify-center min-w-[400px]">
<div className="flex flex-col gap-3 min-w-[150px]"> <div className="flex flex-col gap-3 min-w-[150px]">
<div className="link">Подбор по марке авто</div> <div className="link">Подбор по марке авто</div>
<a href="#" className="link">Поиск по VIN</a> <a href="#" className="link">Поиск по VIN</a>
@ -178,7 +178,7 @@ const Footer = () => (
</a> </a>
</div> </div>
<div className="flex flex-col items-center md:flex-row md:items-start md:justify-center flex-1 flex-wrap gap-4 md:gap-20 md:mt-6 md:min-w-[400px]"> <div className="flex flex-col items-center md:flex-row md:items-start md:justify-center flex-1 flex-wrap gap-4 md:gap-37 md:mt-6 md:min-w-[400px]">
<a href="#" className=" hover:underline text-xs opacity-70 text-center md:w-auto md:text-left">Политика конфиденциальности</a> <a href="#" className=" hover:underline text-xs opacity-70 text-center md:w-auto md:text-left">Политика конфиденциальности</a>
<a href="#" className=" hover:underline text-xs opacity-70 text-center md:w-auto md:text-left">Согласие на обработку персональных данных</a> <a href="#" className=" hover:underline text-xs opacity-70 text-center md:w-auto md:text-left">Согласие на обработку персональных данных</a>

View File

@ -54,7 +54,13 @@ const SearchHistoryDropdown: React.FC<SearchHistoryDropdownProps> = ({
{uniqueQueries.map((item) => ( {uniqueQueries.map((item) => (
<button <button
key={item.id} key={item.id}
onClick={() => onItemClick(item.searchQuery)} onClick={() => {
if ((item.searchType === 'ARTICLE' || item.searchType === 'OEM') && item.articleNumber) {
onItemClick(item.articleNumber);
} else {
onItemClick(item.searchQuery);
}
}}
className="search-history-item-custom" className="search-history-item-custom"
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
> >

View File

@ -1,5 +1,5 @@
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React, { useState } from "react";
import { useCart } from "@/contexts/CartContext"; import { useCart } from "@/contexts/CartContext";
import { useFavorites } from "@/contexts/FavoritesContext"; import { useFavorites } from "@/contexts/FavoritesContext";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
@ -13,6 +13,7 @@ interface TopSalesItemProps {
productId?: string; productId?: string;
onAddToCart?: (e: React.MouseEvent) => void; onAddToCart?: (e: React.MouseEvent) => void;
discount?: string; // Новый пропс для лейбла/скидки discount?: string; // Новый пропс для лейбла/скидки
// isInCart?: boolean; // Удаляем из пропсов
} }
const TopSalesItem: React.FC<TopSalesItemProps> = ({ const TopSalesItem: React.FC<TopSalesItemProps> = ({
@ -24,11 +25,17 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
productId, productId,
onAddToCart, onAddToCart,
discount = 'Топ продаж', // По умолчанию как раньше discount = 'Топ продаж', // По умолчанию как раньше
// isInCart = false, // Удаляем из пропсов
}) => { }) => {
const { addItem } = useCart(); const { addItem, cartItems = [] } = useCart();
const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites(); const { addToFavorites, removeFromFavorites, isFavorite, favorites } = useFavorites();
const [localInCart, setLocalInCart] = useState(false);
const isItemFavorite = isFavorite(productId, undefined, article, brand); const isItemFavorite = isFavorite(productId, undefined, article, brand);
const isInCart = cartItems.some(item =>
(productId && item.productId === productId) ||
(article && brand && item.article === article && item.brand === brand)
);
const parsePrice = (priceStr: string): number => { const parsePrice = (priceStr: string): number => {
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.'); const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
@ -38,6 +45,9 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
const handleAddToCart = (e: React.MouseEvent) => { const handleAddToCart = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (!localInCart) {
setLocalInCart(true);
}
if (onAddToCart) { if (onAddToCart) {
onAddToCart(e); onAddToCart(e);
return; return;
@ -89,7 +99,6 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
currency: 'RUB', currency: 'RUB',
image: image image: image
}); });
toast.success('Товар добавлен в избранное');
} }
}; };
@ -135,9 +144,14 @@ const TopSalesItem: React.FC<TopSalesItemProps> = ({
<a <a
href="#" href="#"
className="button-icon w-inline-block" className="button-icon w-inline-block"
onClick={handleAddToCart} onClick={isInCart ? undefined : handleAddToCart}
style={{ cursor: 'pointer' }} style={{
aria-label="Добавить в корзину" cursor: isInCart ? 'default' : (localInCart ? 'default' : 'pointer'),
background: isInCart ? '#9ca3af' : (localInCart ? '#2563eb' : undefined),
opacity: isInCart || localInCart ? 0.5 : 1,
filter: isInCart || localInCart ? 'grayscale(1)' : 'none'
}}
aria-label={isInCart ? 'В корзине' : (localInCart ? 'Добавлено' : 'Добавить в корзину')}
> >
<div className="div-block-26"> <div className="div-block-26">
<div className="icon-setting w-embed"> <div className="icon-setting w-embed">

View File

@ -4,9 +4,10 @@ interface FilterRangeProps {
title: string; title: string;
min?: number; min?: number;
max?: number; max?: number;
isMobile?: boolean; // Добавляем флаг для мобильной версии isMobile?: boolean;
value?: [number, number] | null; // Текущее значение диапазона value?: [number, number] | null;
onChange?: (value: [number, number]) => void; onChange?: (value: [number, number]) => void;
defaultOpen?: boolean; // Добавляем параметр defaultOpen
} }
const DEFAULT_MIN = 1; const DEFAULT_MIN = 1;
@ -14,14 +15,22 @@ const DEFAULT_MAX = 32000;
const clamp = (v: number, min: number, max: number) => Math.max(min, Math.min(v, max)); const clamp = (v: number, min: number, max: number) => Math.max(min, Math.min(v, max));
const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max = DEFAULT_MAX, isMobile = false, value = null, onChange }) => { const FilterRange: React.FC<FilterRangeProps> = ({
title,
min = DEFAULT_MIN,
max = DEFAULT_MAX,
isMobile = false,
value = null,
onChange,
defaultOpen = false // Устанавливаем по умолчанию false
}) => {
const [from, setFrom] = useState<string>(value ? String(value[0]) : String(min)); const [from, setFrom] = useState<string>(value ? String(value[0]) : String(min));
const [to, setTo] = useState<string>(value ? String(value[1]) : String(max)); const [to, setTo] = useState<string>(value ? String(value[1]) : String(max));
const [confirmedFrom, setConfirmedFrom] = useState<number>(value ? value[0] : min); const [confirmedFrom, setConfirmedFrom] = useState<number>(value ? value[0] : min);
const [confirmedTo, setConfirmedTo] = useState<number>(value ? value[1] : max); const [confirmedTo, setConfirmedTo] = useState<number>(value ? value[1] : max);
const [dragging, setDragging] = useState<null | "from" | "to">(null); const [dragging, setDragging] = useState<null | "from" | "to">(null);
const [trackWidth, setTrackWidth] = useState(0); const [trackWidth, setTrackWidth] = useState(0);
const [open, setOpen] = useState(true); const [open, setOpen] = useState(isMobile || defaultOpen); // Учитываем isMobile и defaultOpen
const trackRef = useRef<HTMLDivElement>(null); const trackRef = useRef<HTMLDivElement>(null);
// Обновляем локальное состояние при изменении внешнего значения или границ // Обновляем локальное состояние при изменении внешнего значения или границ
@ -199,7 +208,7 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
style={{ style={{
position: 'absolute', position: 'absolute',
top: 6, top: 6,
left: pxFrom , left: pxFrom,
zIndex: 3, zIndex: 3,
cursor: 'pointer' cursor: 'pointer'
}} }}

View File

@ -111,12 +111,62 @@ const BestPriceSection: React.FC = () => {
<div className="text-block-58">Подборка лучших предложенийпо цене</div> <div className="text-block-58">Подборка лучших предложенийпо цене</div>
<a href="#" className="button-24 w-button">Показать все</a> <a href="#" className="button-24 w-button">Показать все</a>
</div> </div>
<div className="carousel-row"> <div className="carousel-row" style={{ position: 'relative' }}>
{/* Стили для стрелок как в ProductOfDayBanner, но без абсолютного позиционирования */}
<style>{`
.carousel-arrow {
width: 40px;
height: 40px;
border: none;
background: none;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 1;
transition: opacity 0.2s;
cursor: pointer;
margin: 0 8px;
}
.carousel-arrow-left {}
.carousel-arrow-right {}
.carousel-arrow .arrow-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(255,255,255,0.85);
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.carousel-arrow:hover .arrow-circle,
.carousel-arrow:focus .arrow-circle {
background: #ec1c24;
}
.carousel-arrow .arrow-svg {
width: 20px;
height: 20px;
display: block;
transition: stroke 0.2s;
stroke: #222;
}
.carousel-arrow:hover .arrow-svg,
.carousel-arrow:focus .arrow-svg {
stroke: #fff;
}
.carousel-row {
display: flex;
align-items: center;
justify-content: flex-start;
}
`}</style>
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево"> <button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <span className="arrow-circle">
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/> <svg className="arrow-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</span>
</button> </button>
<div className="w-layout-hflex flex-block-121 carousel-scroll" ref={scrollRef}> <div className="w-layout-hflex flex-block-121 carousel-scroll" ref={scrollRef}>
{bestPriceItems.map((item, i) => ( {bestPriceItems.map((item, i) => (
@ -124,10 +174,11 @@ const BestPriceSection: React.FC = () => {
))} ))}
</div> </div>
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо"> <button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <span className="arrow-circle">
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/> <svg className="arrow-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M3.33398 10H16.6673M16.6673 10L11.6673 5M16.6673 10L11.6673 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -84,16 +84,66 @@ const NewArrivalsSection: React.FC = () => {
<h2 className="heading-4">Новое поступление</h2> <h2 className="heading-4">Новое поступление</h2>
</div> </div>
<div className="carousel-row"> <div className="carousel-row">
{/* Стили для стрелок как в BestPriceSection и TopSalesSection */}
<style>{`
.carousel-arrow {
width: 40px;
height: 40px;
border: none;
background: none;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 1;
transition: opacity 0.2s;
cursor: pointer;
margin: 0 8px;
}
.carousel-arrow-left {}
.carousel-arrow-right {}
.carousel-arrow .arrow-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(255,255,255,0.85);
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.carousel-arrow:hover .arrow-circle,
.carousel-arrow:focus .arrow-circle {
background: #ec1c24;
}
.carousel-arrow .arrow-svg {
width: 20px;
height: 20px;
display: block;
transition: stroke 0.2s;
stroke: #222;
}
.carousel-arrow:hover .arrow-svg,
.carousel-arrow:focus .arrow-svg {
stroke: #fff;
}
.carousel-row {
display: flex;
align-items: center;
justify-content: flex-start;
}
`}</style>
<button <button
className="carousel-arrow carousel-arrow-left" className="carousel-arrow carousel-arrow-left"
onClick={scrollLeft} onClick={scrollLeft}
aria-label="Прокрутить влево" aria-label="Прокрутить влево"
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
> >
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <span className="arrow-circle">
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/> <svg className="arrow-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</span>
</button> </button>
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}> <div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
@ -149,10 +199,11 @@ const NewArrivalsSection: React.FC = () => {
aria-label="Прокрутить вправо" aria-label="Прокрутить вправо"
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
> >
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <span className="arrow-circle">
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/> <svg className="arrow-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M3.33398 10H16.6673M16.6673 10L11.6673 5M16.6673 10L11.6673 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -32,11 +32,61 @@ const NewsAndPromos = () => {
</div> </div>
</div> </div>
<div className="carousel-row"> <div className="carousel-row">
{/* Стили для стрелок как в других секциях */}
<style>{`
.carousel-arrow {
width: 40px;
height: 40px;
border: none;
background: none;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 1;
transition: opacity 0.2s;
cursor: pointer;
margin: 0 8px;
}
.carousel-arrow-left {}
.carousel-arrow-right {}
.carousel-arrow .arrow-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(255,255,255,0.85);
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.carousel-arrow:hover .arrow-circle,
.carousel-arrow:focus .arrow-circle {
background: #ec1c24;
}
.carousel-arrow .arrow-svg {
width: 20px;
height: 20px;
display: block;
transition: stroke 0.2s;
stroke: #222;
}
.carousel-arrow:hover .arrow-svg,
.carousel-arrow:focus .arrow-svg {
stroke: #fff;
}
.carousel-row {
display: flex;
align-items: center;
justify-content: flex-start;
}
`}</style>
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево"> <button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <span className="arrow-circle">
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/> <svg className="arrow-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</span>
</button> </button>
<div className="w-layout-hflex flex-block-6-copy-copy carousel-scroll" ref={scrollRef}> <div className="w-layout-hflex flex-block-6-copy-copy carousel-scroll" ref={scrollRef}>
<NewsCard <NewsCard
@ -69,10 +119,11 @@ const NewsAndPromos = () => {
/> />
</div> </div>
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо"> <button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <span className="arrow-circle">
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/> <svg className="arrow-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M3.33398 10H16.6673M16.6673 10L11.6673 5M16.6673 10L11.6673 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -110,7 +110,12 @@ const ProductOfDaySection: React.FC = () => {
}; };
} }
return null; // Если нет ни одной картинки, возвращаем noimage.png
return {
url: '/images/noimage.png',
alt: product.name,
source: 'noimage'
};
}; };
// Обработчики для навигации по товарам дня // Обработчики для навигации по товарам дня
@ -209,6 +214,11 @@ const ProductOfDaySection: React.FC = () => {
Parts Index Parts Index
</div> </div>
)} )}
{productImage.source === 'noimage' && (
<div className="absolute bottom-0 right-0 bg-gray-400 text-white text-xs px-2 py-1 rounded-tl">
Нет изображения
</div>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -2,6 +2,7 @@ import React, { useRef } from "react";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import TopSalesItem from "../TopSalesItem"; import TopSalesItem from "../TopSalesItem";
import { GET_TOP_SALES_PRODUCTS } from "../../lib/graphql"; import { GET_TOP_SALES_PRODUCTS } from "../../lib/graphql";
import { useCart } from "@/contexts/CartContext";
interface TopSalesProductData { interface TopSalesProductData {
id: string; id: string;
@ -22,6 +23,7 @@ const SCROLL_AMOUNT = 340; // px, ширина одной карточки + о
const TopSalesSection: React.FC = () => { const TopSalesSection: React.FC = () => {
const { data, loading, error } = useQuery(GET_TOP_SALES_PRODUCTS); const { data, loading, error } = useQuery(GET_TOP_SALES_PRODUCTS);
const { cartItems = [] } = useCart();
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const scrollLeft = () => { const scrollLeft = () => {
@ -143,11 +145,61 @@ const TopSalesSection: React.FC = () => {
<h2 className="heading-4">Топ продаж</h2> <h2 className="heading-4">Топ продаж</h2>
</div> </div>
<div className="carousel-row"> <div className="carousel-row">
{/* Стили для стрелок как в BestPriceSection */}
<style>{`
.carousel-arrow {
width: 40px;
height: 40px;
border: none;
background: none;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 1;
transition: opacity 0.2s;
cursor: pointer;
margin: 0 8px;
}
.carousel-arrow-left {}
.carousel-arrow-right {}
.carousel-arrow .arrow-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(255,255,255,0.85);
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.carousel-arrow:hover .arrow-circle,
.carousel-arrow:focus .arrow-circle {
background: #ec1c24;
}
.carousel-arrow .arrow-svg {
width: 20px;
height: 20px;
display: block;
transition: stroke 0.2s;
stroke: #222;
}
.carousel-arrow:hover .arrow-svg,
.carousel-arrow:focus .arrow-svg {
stroke: #fff;
}
.carousel-row {
display: flex;
align-items: center;
justify-content: flex-start;
}
`}</style>
<button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево"> <button className="carousel-arrow carousel-arrow-left" onClick={scrollLeft} aria-label="Прокрутить влево">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <span className="arrow-circle">
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/> <svg className="arrow-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.5 24L12.5 16L19.5 8" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M16.6673 10H3.33398M3.33398 10L8.33398 5M3.33398 10L8.33398 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</span>
</button> </button>
<div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}> <div className="w-layout-hflex core-product-search carousel-scroll" ref={scrollRef}>
{activeTopSalesProducts.map((item: TopSalesProductData) => { {activeTopSalesProducts.map((item: TopSalesProductData) => {
@ -162,6 +214,7 @@ const TopSalesSection: React.FC = () => {
const title = product.name; const title = product.name;
const brand = product.brand || 'Неизвестный бренд'; const brand = product.brand || 'Неизвестный бренд';
const isInCart = cartItems.some(cartItem => cartItem.productId === product.id);
return ( return (
<TopSalesItem <TopSalesItem
@ -172,15 +225,17 @@ const TopSalesSection: React.FC = () => {
brand={brand} brand={brand}
article={product.article} article={product.article}
productId={product.id} productId={product.id}
isInCart={isInCart}
/> />
); );
})} })}
</div> </div>
<button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо"> <button className="carousel-arrow carousel-arrow-right" onClick={scrollRight} aria-label="Прокрутить вправо">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <span className="arrow-circle">
<circle cx="16" cy="16" r="16" fill="#F3F4F6"/> <svg className="arrow-svg" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 8L19.5 16L12.5 24" stroke="#222" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/> <path d="M3.33398 10H16.6673M16.6673 10L11.6673 5M16.6673 10L11.6673 15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -112,6 +112,19 @@ const LegalEntityFormBlock: React.FC<LegalEntityFormBlockProps> = ({
onAdd, onAdd,
onCancel, onCancel,
}) => { }) => {
// Состояния для отображения ошибок валидации
const [validationErrors, setValidationErrors] = React.useState({
inn: false,
shortName: false,
jurAddress: false,
form: false,
taxSystem: false,
});
// Функция для очистки ошибки при изменении поля
const clearError = (field: keyof typeof validationErrors) => {
setValidationErrors(prev => ({ ...prev, [field]: false }));
};
const [createLegalEntity, { loading: createLoading }] = useMutation(CREATE_CLIENT_LEGAL_ENTITY, { const [createLegalEntity, { loading: createLoading }] = useMutation(CREATE_CLIENT_LEGAL_ENTITY, {
onCompleted: () => { onCompleted: () => {
console.log('Юридическое лицо создано'); console.log('Юридическое лицо создано');
@ -137,29 +150,27 @@ const LegalEntityFormBlock: React.FC<LegalEntityFormBlockProps> = ({
const loading = createLoading || updateLoading; const loading = createLoading || updateLoading;
const handleSave = async () => { const handleSave = async () => {
// Валидация // Сброс предыдущих ошибок
if (!inn || inn.length < 10) { setValidationErrors({
alert('Введите корректный ИНН'); inn: false,
return; shortName: false,
} jurAddress: false,
form: false,
taxSystem: false,
});
if (!shortName.trim()) { // Валидация с установкой ошибок
alert('Введите краткое наименование'); const errors = {
return; inn: !inn || inn.length < 10,
} shortName: !shortName.trim(),
jurAddress: !jurAddress.trim(),
form: form === 'Выбрать',
taxSystem: taxSystem === 'Выбрать',
};
if (!jurAddress.trim()) { // Если есть ошибки, устанавливаем их и прерываем выполнение
alert('Введите юридический адрес'); if (Object.values(errors).some(error => error)) {
return; setValidationErrors(errors);
}
if (form === 'Выбрать') {
alert('Выберите форму организации');
return;
}
if (taxSystem === 'Выбрать') {
alert('Выберите систему налогообложения');
return; return;
} }
@ -238,13 +249,18 @@ const LegalEntityFormBlock: React.FC<LegalEntityFormBlockProps> = ({
<div className="flex flex-wrap gap-5 items-start w-full whitespace-nowrap max-md:max-w-full"> <div className="flex flex-wrap gap-5 items-start w-full whitespace-nowrap max-md:max-w-full">
<div className="flex flex-col flex-1 shrink basis-0 min-w-[240px]"> <div className="flex flex-col flex-1 shrink basis-0 min-w-[240px]">
<div className="text-gray-950">ИНН</div> <div className="text-gray-950">ИНН</div>
<div className="gap-2.5 self-stretch px-6 py-3.5 mt-1.5 w-full bg-white rounded border border-solid border-stone-300 min-h-[46px] max-md:px-5"> <div className={`gap-2.5 self-stretch px-6 py-3.5 mt-1.5 w-full bg-white rounded border border-solid min-h-[46px] max-md:px-5 ${
validationErrors.inn ? 'border-red-500' : 'border-stone-300'
}`}>
<input <input
type="text" type="text"
placeholder="ИНН" placeholder="ИНН"
className="w-full bg-transparent outline-none text-gray-600" className="w-full bg-transparent outline-none text-gray-600"
value={inn} value={inn}
onChange={e => setInn(e.target.value)} onChange={e => {
setInn(e.target.value);
clearError('inn');
}}
/> />
</div> </div>
</div> </div>
@ -252,7 +268,9 @@ const LegalEntityFormBlock: React.FC<LegalEntityFormBlockProps> = ({
<div className="text-gray-950">Форма</div> <div className="text-gray-950">Форма</div>
<div className="relative mt-1.5"> <div className="relative mt-1.5">
<div <div
className="flex gap-10 justify-between items-center px-6 py-3.5 w-full bg-white rounded border border-solid border-stone-300 min-h-[46px] text-neutral-500 max-md:px-5 cursor-pointer select-none" className={`flex gap-10 justify-between items-center px-6 py-3.5 w-full bg-white rounded border border-solid min-h-[46px] text-neutral-500 max-md:px-5 cursor-pointer select-none ${
validationErrors.form ? 'border-red-500' : 'border-stone-300'
}`}
onClick={() => setIsFormOpen((prev: boolean) => !prev)} onClick={() => setIsFormOpen((prev: boolean) => !prev)}
tabIndex={0} tabIndex={0}
onBlur={() => setIsFormOpen(false)} onBlur={() => setIsFormOpen(false)}
@ -266,7 +284,11 @@ const LegalEntityFormBlock: React.FC<LegalEntityFormBlockProps> = ({
<li <li
key={option} key={option}
className={`px-6 py-3.5 cursor-pointer hover:bg-blue-100 ${option === form ? 'bg-blue-50 font-semibold' : ''}`} className={`px-6 py-3.5 cursor-pointer hover:bg-blue-100 ${option === form ? 'bg-blue-50 font-semibold' : ''}`}
onMouseDown={() => { setForm(option); setIsFormOpen(false); }} onMouseDown={() => {
setForm(option);
setIsFormOpen(false);
clearError('form');
}}
> >
{option} {option}
</li> </li>
@ -303,25 +325,35 @@ const LegalEntityFormBlock: React.FC<LegalEntityFormBlockProps> = ({
<div className="flex flex-wrap gap-5 items-start mt-5 w-full max-md:max-w-full"> <div className="flex flex-wrap gap-5 items-start mt-5 w-full max-md:max-w-full">
<div className="flex flex-col flex-1 shrink basis-0 min-w-[240px]"> <div className="flex flex-col flex-1 shrink basis-0 min-w-[240px]">
<div className="text-gray-950">Юридический адрес</div> <div className="text-gray-950">Юридический адрес</div>
<div className="gap-2.5 self-stretch px-6 py-3.5 mt-1.5 w-full bg-white rounded border border-solid border-stone-300 min-h-[46px] text-neutral-500 max-md:px-5"> <div className={`gap-2.5 self-stretch px-6 py-3.5 mt-1.5 w-full bg-white rounded border border-solid min-h-[46px] text-neutral-500 max-md:px-5 ${
validationErrors.jurAddress ? 'border-red-500' : 'border-stone-300'
}`}>
<input <input
type="text" type="text"
placeholder="Юридический адрес" placeholder="Юридический адрес"
className="w-full bg-transparent outline-none text-neutral-500" className="w-full bg-transparent outline-none text-neutral-500"
value={jurAddress} value={jurAddress}
onChange={e => setJurAddress(e.target.value)} onChange={e => {
setJurAddress(e.target.value);
clearError('jurAddress');
}}
/> />
</div> </div>
</div> </div>
<div className="flex flex-col flex-1 shrink basis-0 min-w-[240px]"> <div className="flex flex-col flex-1 shrink basis-0 min-w-[240px]">
<div className="text-gray-950">Краткое наименование</div> <div className="text-gray-950">Краткое наименование</div>
<div className="gap-2.5 self-stretch px-6 py-3.5 mt-1.5 w-full bg-white rounded border border-solid border-stone-300 min-h-[46px] text-neutral-500 max-md:px-5"> <div className={`gap-2.5 self-stretch px-6 py-3.5 mt-1.5 w-full bg-white rounded border border-solid min-h-[46px] text-neutral-500 max-md:px-5 ${
validationErrors.shortName ? 'border-red-500' : 'border-stone-300'
}`}>
<input <input
type="text" type="text"
placeholder="Краткое наименование" placeholder="Краткое наименование"
className="w-full bg-transparent outline-none text-neutral-500" className="w-full bg-transparent outline-none text-neutral-500"
value={shortName} value={shortName}
onChange={e => setShortName(e.target.value)} onChange={e => {
setShortName(e.target.value);
clearError('shortName');
}}
/> />
</div> </div>
</div> </div>
@ -355,7 +387,9 @@ const LegalEntityFormBlock: React.FC<LegalEntityFormBlockProps> = ({
<div className="text-gray-950">Система налогоблажения</div> <div className="text-gray-950">Система налогоблажения</div>
<div className="relative mt-1.5"> <div className="relative mt-1.5">
<div <div
className="flex gap-10 justify-between items-center px-6 py-3.5 w-full whitespace-nowrap bg-white rounded border border-solid border-stone-300 min-h-[46px] text-neutral-500 max-md:px-5 cursor-pointer select-none" className={`flex gap-10 justify-between items-center px-6 py-3.5 w-full whitespace-nowrap bg-white rounded border border-solid min-h-[46px] text-neutral-500 max-md:px-5 cursor-pointer select-none ${
validationErrors.taxSystem ? 'border-red-500' : 'border-stone-300'
}`}
onClick={() => setIsTaxSystemOpen((prev: boolean) => !prev)} onClick={() => setIsTaxSystemOpen((prev: boolean) => !prev)}
tabIndex={0} tabIndex={0}
onBlur={() => setIsTaxSystemOpen(false)} onBlur={() => setIsTaxSystemOpen(false)}
@ -369,7 +403,11 @@ const LegalEntityFormBlock: React.FC<LegalEntityFormBlockProps> = ({
<li <li
key={option} key={option}
className={`px-6 py-3.5 cursor-pointer hover:bg-blue-100 ${option === taxSystem ? 'bg-blue-50 font-semibold' : ''}`} className={`px-6 py-3.5 cursor-pointer hover:bg-blue-100 ${option === taxSystem ? 'bg-blue-50 font-semibold' : ''}`}
onMouseDown={() => { setTaxSystem(option); setIsTaxSystemOpen(false); }} onMouseDown={() => {
setTaxSystem(option);
setIsTaxSystemOpen(false);
clearError('taxSystem');
}}
> >
{option} {option}
</li> </li>

View File

@ -19,6 +19,7 @@ interface VehicleAttributesTooltipProps {
const VehicleAttributesTooltip: React.FC<VehicleAttributesTooltipProps> = ({ const VehicleAttributesTooltip: React.FC<VehicleAttributesTooltipProps> = ({
show, show,
position, position,
vehicleName,
vehicleAttributes, vehicleAttributes,
onMouseEnter, onMouseEnter,
onMouseLeave, onMouseLeave,
@ -27,7 +28,7 @@ const VehicleAttributesTooltip: React.FC<VehicleAttributesTooltipProps> = ({
if (!show) return null; if (!show) return null;
return ( return (
<div <div
className="flex overflow-hidden flex-col items-center px-8 py-8 bg-slate-50 shadow-[0px_0px_20px_rgba(0,0,0,0.15)] rounded-2xl w-[450px] min-h-[365px] max-w-full fixed z-[9999]" className="flex overflow-hidden flex-col items-center px-8 py-8 bg-slate-50 shadow-[0px_0px_20px_rgba(0,0,0,0.15)] rounded-2xl w-[450px] max-w-full fixed z-[9999]"
style={{ style={{
left: `${position.x + 120}px`, left: `${position.x + 120}px`,
top: `${position.y}px`, top: `${position.y}px`,
@ -45,16 +46,33 @@ const VehicleAttributesTooltip: React.FC<VehicleAttributesTooltipProps> = ({
/> />
)} )}
<div className="flex relative flex-col w-full"> <div className="flex relative flex-col w-full">
{vehicleAttributes.map((attr, idx) => ( {/* Заголовок */}
<div key={idx} className="flex gap-5 items-center mt-2 w-full whitespace-nowrap first:mt-0"> {vehicleName && (
<div className="self-stretch my-auto text-gray-400 w-[150px] truncate"> <div className="font-semibold text-lg text-black mb-3 truncate">{vehicleName}</div>
)}
{/* Список характеристик или сообщение */}
{vehicleAttributes.length > 0 ? (
vehicleAttributes.map((attr, idx) => (
<div
key={idx}
className="grid grid-cols-[150px_1fr] gap-x-5 items-start mt-2 w-full first:mt-0"
>
<div className="text-gray-400 break-words whitespace-normal text-left">
{attr.name} {attr.name}
</div> </div>
<div className="self-stretch my-auto font-medium text-black truncate"> <div
className="font-medium text-black break-words whitespace-normal text-left justify-self-start"
style={{ textAlign: 'left' }}
>
{attr.value} {attr.value}
</div> </div>
</div> </div>
))} ))
) : (
<div className="flex flex-col items-center justify-center w-full py-8">
<div className="text-gray-400 mb-2">Дополнительная информация недоступна</div>
</div>
)}
</div> </div>
</div> </div>
); );

View File

@ -88,10 +88,9 @@ const VinQuick: React.FC<VinQuickProps> = ({ quickGroup, catalogCode, vehicleId,
))} ))}
{total > 3 && shownCount < total && ( {total > 3 && shownCount < total && (
<div className="flex gap-2 mt-2 w-full"> <div className="flex gap-2 mt-2 w-full">
{shownCount + 3 < total && (
<button <button
className="expand-btn" className="expand-btn"
onClick={() => setShownCounts(prev => ({ ...prev, [unit.unitid]: shownCount + 3 }))} onClick={() => setShownCounts(prev => ({ ...prev, [unit.unitid]: total }))}
style={{ border: '1px solid #EC1C24', borderRadius: 8, background: '#fff', color: '#222', padding: '6px 18px', minWidth: 180 }} style={{ border: '1px solid #EC1C24', borderRadius: 8, background: '#fff', color: '#222', padding: '6px 18px', minWidth: 180 }}
> >
Развернуть Развернуть
@ -99,10 +98,9 @@ const VinQuick: React.FC<VinQuickProps> = ({ quickGroup, catalogCode, vehicleId,
<path d="M4 6l4 4 4-4" stroke="#222" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"/> <path d="M4 6l4 4 4-4" stroke="#222" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
</svg> </svg>
</button> </button>
)}
<button <button
className="showall-btn" className="showall-btn"
onClick={() => setShownCounts(prev => ({ ...prev, [unit.unitid]: total }))} onClick={() => handleUnitClick(unit)}
style={{ background: '#e9eef5', borderRadius: 8, color: '#222', padding: '6px 18px', border: 'none'}} style={{ background: '#e9eef5', borderRadius: 8, color: '#222', padding: '6px 18px', border: 'none'}}
> >
Показать все Показать все

View File

@ -1,7 +1,10 @@
'use client' 'use client'
import React, { createContext, useContext, useReducer, useEffect, useState } from 'react' import React, { createContext, useContext, useState, useEffect } from 'react'
import { useMutation, useQuery } from '@apollo/client'
import { CartState, CartContextType, CartItem, DeliveryInfo } from '@/types/cart' import { CartState, CartContextType, CartItem, DeliveryInfo } from '@/types/cart'
import { ADD_TO_CART, REMOVE_FROM_CART, UPDATE_CART_ITEM_QUANTITY, CLEAR_CART, GET_CART } from '@/lib/graphql'
import { toast } from 'react-hot-toast'
// Начальное состояние корзины // Начальное состояние корзины
const initialState: CartState = { const initialState: CartState = {
@ -22,51 +25,53 @@ const initialState: CartState = {
isLoading: false isLoading: false
} }
// Типы действий // Создаем контекст
type CartAction = const CartContext = createContext<CartContextType | undefined>(undefined)
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'id' | 'selected' | 'favorite'> }
| { type: 'ADD_ITEM_SUCCESS'; payload: { items: CartItem[]; summary: any } }
| { type: 'ADD_ITEM_ERROR'; payload: string }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'TOGGLE_SELECT'; payload: string }
| { type: 'TOGGLE_FAVORITE'; payload: string }
| { type: 'UPDATE_COMMENT'; payload: { id: string; comment: string } }
| { type: 'UPDATE_ORDER_COMMENT'; payload: string }
| { type: 'SELECT_ALL' }
| { type: 'REMOVE_ALL' }
| { type: 'REMOVE_SELECTED' }
| { type: 'UPDATE_DELIVERY'; payload: Partial<DeliveryInfo> }
| { type: 'CLEAR_CART' }
| { type: 'LOAD_CART'; payload: CartItem[] }
| { type: 'LOAD_FULL_STATE'; payload: { items: CartItem[]; delivery: DeliveryInfo; orderComment: string } }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string }
// Функция для генерации ID
const generateId = () => Math.random().toString(36).substr(2, 9)
// Утилитарная функция для парсинга количества в наличии // Утилитарная функция для парсинга количества в наличии
const parseStock = (stockStr: string | number | undefined): number => { const parseStock = (stockStr: string | number | undefined): number => {
if (typeof stockStr === 'number') return stockStr; if (stockStr === undefined || stockStr === null) return 0
if (typeof stockStr === 'number') return stockStr
if (typeof stockStr === 'string') { if (typeof stockStr === 'string') {
const match = stockStr.match(/\d+/); // Извлекаем числа из строки типа "10 шт" или "В наличии: 5"
return match ? parseInt(match[0]) : 0; const match = stockStr.match(/\d+/)
return match ? parseInt(match[0], 10) : 0
} }
return 0; return 0
}; }
// Функция для расчета итогов // Функция для преобразования backend cart items в frontend format
const calculateSummary = (items: CartItem[], deliveryPrice: number) => { const transformBackendItems = (backendItems: any[]): CartItem[] => {
const selectedItems = items.filter(item => item.selected) return backendItems.map(item => ({
const totalItems = selectedItems.reduce((sum, item) => sum + item.quantity, 0) id: item.id,
const totalPrice = selectedItems.reduce((sum, item) => sum + (item.price * item.quantity), 0) productId: item.productId,
const totalDiscount = selectedItems.reduce((sum, item) => { offerKey: item.offerKey,
const discount = item.originalPrice ? (item.originalPrice - item.price) * item.quantity : 0 name: item.name,
return sum + discount description: item.description,
}, 0) brand: item.brand,
// Доставка включена в стоимость товаров, поэтому добавляем её только если есть товары article: item.article,
const finalPrice = totalPrice + (totalPrice > 0 ? 0 : 0) // Доставка всегда включена в цену товаров price: item.price,
currency: item.currency || 'RUB',
quantity: item.quantity,
stock: item.stock,
deliveryTime: item.deliveryTime,
warehouse: item.warehouse,
supplier: item.supplier,
isExternal: item.isExternal,
image: item.image,
selected: true,
favorite: false,
comment: ''
}))
}
// Функция для подсчета статистики корзины
const calculateSummary = (items: CartItem[]) => {
const totalItems = items.reduce((sum, item) => sum + item.quantity, 0)
const totalPrice = items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
const totalDiscount = 0 // TODO: Implement discount logic
const deliveryPrice = 39
const finalPrice = totalPrice + deliveryPrice - totalDiscount
return { return {
totalItems, totalItems,
@ -77,373 +82,317 @@ const calculateSummary = (items: CartItem[], deliveryPrice: number) => {
} }
} }
// Редьюсер корзины // Провайдер контекста
const cartReducer = (state: CartState, action: CartAction): CartState => {
switch (action.type) {
case 'ADD_ITEM': {
const existingItemIndex = state.items.findIndex(
item =>
(item.productId && item.productId === action.payload.productId) ||
(item.offerKey && item.offerKey === action.payload.offerKey)
)
let newItems: CartItem[]
if (existingItemIndex >= 0) {
// Увеличиваем количество существующего товара
const existingItem = state.items[existingItemIndex];
const totalQuantity = existingItem.quantity + action.payload.quantity;
newItems = state.items.map((item, index) =>
index === existingItemIndex
? { ...item, quantity: totalQuantity }
: item
)
} else {
// Добавляем новый товар
const newItem: CartItem = {
...action.payload,
id: generateId(),
selected: true,
favorite: false
}
newItems = [...state.items, newItem]
}
const newSummary = calculateSummary(newItems, state.delivery.price)
return {
...state,
items: newItems,
summary: newSummary
}
}
case 'REMOVE_ITEM': {
const newItems = state.items.filter(item => item.id !== action.payload)
const newSummary = calculateSummary(newItems, state.delivery.price)
return {
...state,
items: newItems,
summary: newSummary
}
}
case 'UPDATE_QUANTITY': {
const newItems = state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: Math.max(1, action.payload.quantity) }
: item
)
const newSummary = calculateSummary(newItems, state.delivery.price)
return {
...state,
items: newItems,
summary: newSummary
}
}
case 'TOGGLE_SELECT': {
const newItems = state.items.map(item =>
item.id === action.payload
? { ...item, selected: !item.selected }
: item
)
const newSummary = calculateSummary(newItems, state.delivery.price)
return {
...state,
items: newItems,
summary: newSummary
}
}
case 'TOGGLE_FAVORITE': {
const newItems = state.items.map(item =>
item.id === action.payload
? { ...item, favorite: !item.favorite }
: item
)
return {
...state,
items: newItems
}
}
case 'UPDATE_COMMENT': {
const newItems = state.items.map(item =>
item.id === action.payload.id
? { ...item, comment: action.payload.comment }
: item
)
return {
...state,
items: newItems
}
}
case 'UPDATE_ORDER_COMMENT': {
return {
...state,
orderComment: action.payload
}
}
case 'SELECT_ALL': {
const allSelected = state.items.every(item => item.selected)
const newItems = state.items.map(item => ({
...item,
selected: !allSelected
}))
const newSummary = calculateSummary(newItems, state.delivery.price)
return {
...state,
items: newItems,
summary: newSummary
}
}
case 'REMOVE_ALL': {
const newSummary = calculateSummary([], state.delivery.price)
return {
...state,
items: [],
summary: newSummary
}
}
case 'REMOVE_SELECTED': {
const newItems = state.items.filter(item => !item.selected)
const newSummary = calculateSummary(newItems, state.delivery.price)
return {
...state,
items: newItems,
summary: newSummary
}
}
case 'UPDATE_DELIVERY': {
const newDelivery = { ...state.delivery, ...action.payload }
const newSummary = calculateSummary(state.items, newDelivery.price)
return {
...state,
delivery: newDelivery,
summary: newSummary
}
}
case 'CLEAR_CART': {
const newSummary = calculateSummary([], state.delivery.price)
return {
...state,
items: [],
summary: newSummary
}
}
case 'LOAD_CART': {
const newSummary = calculateSummary(action.payload, state.delivery.price)
return {
...state,
items: action.payload,
summary: newSummary
}
}
case 'LOAD_FULL_STATE': {
const newSummary = calculateSummary(action.payload.items, action.payload.delivery.price || state.delivery.price)
return {
...state,
items: action.payload.items,
delivery: action.payload.delivery,
orderComment: action.payload.orderComment,
summary: newSummary
}
}
case 'SET_LOADING': {
return {
...state,
isLoading: action.payload
}
}
case 'SET_ERROR': {
return {
...state,
error: action.payload,
isLoading: false
}
}
default:
return state
}
}
// Создание контекста
const CartContext = createContext<CartContextType | undefined>(undefined)
// Провайдер корзины
export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(cartReducer, initialState) const [state, setState] = useState<CartState>(initialState)
const [isInitialized, setIsInitialized] = useState(false) const [error, setError] = useState<string>('')
// Загрузка корзины из localStorage при инициализации // GraphQL operations
const { data: cartData, loading: cartLoading, refetch: refetchCart } = useQuery(GET_CART, {
errorPolicy: 'ignore' // Don't show errors for unauthenticated users
})
const [addToCartMutation] = useMutation(ADD_TO_CART)
const [removeFromCartMutation] = useMutation(REMOVE_FROM_CART)
const [updateQuantityMutation] = useMutation(UPDATE_CART_ITEM_QUANTITY)
const [clearCartMutation] = useMutation(CLEAR_CART)
// Load cart from backend when component mounts or cart data changes
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined') return if (cartData?.getCart) {
const backendItems = transformBackendItems(cartData.getCart.items)
const summary = calculateSummary(backendItems)
console.log('🔄 Загружаем состояние корзины из localStorage...') setState(prev => ({
...prev,
const savedCartState = localStorage.getItem('cartState') items: backendItems,
if (savedCartState) { summary,
try { isLoading: false
const cartState = JSON.parse(savedCartState) }))
console.log('✅ Найдено сохраненное состояние корзины:', cartState)
// Загружаем полное состояние корзины
dispatch({ type: 'LOAD_FULL_STATE', payload: cartState })
} catch (error) {
console.error('❌ Ошибка загрузки корзины из localStorage:', error)
// Попытаемся загрузить старый формат (только товары)
const savedCart = localStorage.getItem('cart')
if (savedCart) {
try {
const cartItems = JSON.parse(savedCart)
console.log('✅ Найдены товары в старом формате:', cartItems)
dispatch({ type: 'LOAD_CART', payload: cartItems })
} catch (error) {
console.error('❌ Ошибка загрузки старой корзины:', error)
}
}
}
} else { } else {
console.log(' Сохраненное состояние корзины не найдено') setState(prev => ({
...prev,
items: [],
summary: calculateSummary([]),
isLoading: false
}))
} }
}, [cartData])
setIsInitialized(true) // Set loading state
}, [])
// Сохранение полного состояния корзины в localStorage при изменении (только после инициализации)
useEffect(() => { useEffect(() => {
if (!isInitialized || typeof window === 'undefined') return setState(prev => ({
...prev,
isLoading: cartLoading
}))
}, [cartLoading])
const stateToSave = { // GraphQL-based cart operations
items: state.items,
delivery: state.delivery,
orderComment: state.orderComment
}
console.log('💾 Сохраняем состояние корзины:', stateToSave)
localStorage.setItem('cartState', JSON.stringify(stateToSave))
// Сохраняем также старый формат для совместимости
localStorage.setItem('cart', JSON.stringify(state.items))
}, [state.items, state.delivery, state.orderComment, isInitialized])
// Функции для работы с корзиной
const addItem = async (item: Omit<CartItem, 'id' | 'selected' | 'favorite'>) => { const addItem = async (item: Omit<CartItem, 'id' | 'selected' | 'favorite'>) => {
// Проверяем наличие товара на складе перед добавлением try {
const existingItemIndex = state.items.findIndex( setError('')
existingItem => setState(prev => ({ ...prev, isLoading: true }))
(existingItem.productId && existingItem.productId === item.productId) ||
(existingItem.offerKey && existingItem.offerKey === item.offerKey)
)
let totalQuantity = item.quantity; console.log('🛒 Adding item to backend cart:', item)
if (existingItemIndex >= 0) {
const existingItem = state.items[existingItemIndex]; const { data } = await addToCartMutation({
totalQuantity = existingItem.quantity + item.quantity; variables: {
input: {
productId: item.productId || null,
offerKey: item.offerKey || null,
name: item.name,
description: item.description,
brand: item.brand,
article: item.article,
price: item.price,
currency: item.currency || 'RUB',
quantity: item.quantity,
stock: item.stock || null,
deliveryTime: item.deliveryTime || null,
warehouse: item.warehouse || null,
supplier: item.supplier || null,
isExternal: item.isExternal || false,
image: item.image || null
}
}
})
if (data?.addToCart?.success) {
// Update local state with backend response
if (data.addToCart.cart) {
const backendItems = transformBackendItems(data.addToCart.cart.items)
const summary = calculateSummary(backendItems)
setState(prev => ({
...prev,
items: backendItems,
summary,
isLoading: false
}))
} }
// Проверяем наличие товара на складе
const availableStock = parseStock(item.stock);
if (availableStock > 0 && totalQuantity > availableStock) {
const errorMessage = `Недостаточно товара в наличии. Доступно: ${availableStock} шт., запрошено: ${totalQuantity} шт.`;
dispatch({ type: 'SET_ERROR', payload: errorMessage });
return { success: false, error: errorMessage };
}
// Если проверка прошла успешно, добавляем товар
dispatch({ type: 'ADD_ITEM', payload: item }) // Refetch to ensure data consistency
refetchCart()
return { success: true } return { success: true }
} else {
const errorMessage = data?.addToCart?.error || 'Ошибка добавления товара'
setError(errorMessage)
setState(prev => ({ ...prev, isLoading: false }))
toast.error(errorMessage)
return { success: false, error: errorMessage }
} }
} catch (error) {
const removeItem = (id: string) => { console.error('❌ Error adding item to cart:', error)
dispatch({ type: 'REMOVE_ITEM', payload: id }) const errorMessage = 'Ошибка добавления товара в корзину'
} setError(errorMessage)
setState(prev => ({ ...prev, isLoading: false }))
const updateQuantity = (id: string, quantity: number) => { toast.error(errorMessage)
// Найдем товар для проверки наличия return { success: false, error: errorMessage }
const item = state.items.find(item => item.id === id);
if (item) {
const availableStock = parseStock(item.stock);
if (availableStock > 0 && quantity > availableStock) {
// Показываем ошибку, но не изменяем количество
dispatch({ type: 'SET_ERROR', payload: `Недостаточно товара в наличии. Доступно: ${availableStock} шт.` });
return;
} }
} }
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } }) const removeItem = async (id: string) => {
try {
setError('')
setState(prev => ({ ...prev, isLoading: true }))
console.log('🗑️ Removing item from backend cart:', id)
const { data } = await removeFromCartMutation({
variables: { itemId: id }
})
if (data?.removeFromCart?.success) {
// Update local state
if (data.removeFromCart.cart) {
const backendItems = transformBackendItems(data.removeFromCart.cart.items)
const summary = calculateSummary(backendItems)
setState(prev => ({
...prev,
items: backendItems,
summary,
isLoading: false
}))
} }
toast.success(data.removeFromCart.message || 'Товар удален из корзины')
refetchCart()
} else {
const errorMessage = data?.removeFromCart?.error || 'Ошибка удаления товара'
setError(errorMessage)
setState(prev => ({ ...prev, isLoading: false }))
toast.error(errorMessage)
}
} catch (error) {
console.error('❌ Error removing item from cart:', error)
const errorMessage = 'Ошибка удаления товара из корзины'
setError(errorMessage)
setState(prev => ({ ...prev, isLoading: false }))
toast.error(errorMessage)
}
}
const updateQuantity = async (id: string, quantity: number) => {
try {
if (quantity < 1) return
setError('')
setState(prev => ({ ...prev, isLoading: true }))
console.log('📝 Updating item quantity in backend cart:', id, quantity)
const { data } = await updateQuantityMutation({
variables: { itemId: id, quantity }
})
if (data?.updateCartItemQuantity?.success) {
// Update local state
if (data.updateCartItemQuantity.cart) {
const backendItems = transformBackendItems(data.updateCartItemQuantity.cart.items)
const summary = calculateSummary(backendItems)
setState(prev => ({
...prev,
items: backendItems,
summary,
isLoading: false
}))
}
toast.success(data.updateCartItemQuantity.message || 'Количество обновлено')
refetchCart()
} else {
const errorMessage = data?.updateCartItemQuantity?.error || 'Ошибка обновления количества'
setError(errorMessage)
setState(prev => ({ ...prev, isLoading: false }))
toast.error(errorMessage)
}
} catch (error) {
console.error('❌ Error updating item quantity:', error)
const errorMessage = 'Ошибка обновления количества товара'
setError(errorMessage)
setState(prev => ({ ...prev, isLoading: false }))
toast.error(errorMessage)
}
}
const clearCart = async () => {
try {
setError('')
setState(prev => ({ ...prev, isLoading: true }))
console.log('🧹 Clearing backend cart')
const { data } = await clearCartMutation()
if (data?.clearCart?.success) {
setState(prev => ({
...prev,
items: [],
summary: calculateSummary([]),
isLoading: false
}))
toast.success(data.clearCart.message || 'Корзина очищена')
refetchCart()
} else {
const errorMessage = data?.clearCart?.error || 'Ошибка очистки корзины'
setError(errorMessage)
setState(prev => ({ ...prev, isLoading: false }))
toast.error(errorMessage)
}
} catch (error) {
console.error('❌ Error clearing cart:', error)
const errorMessage = 'Ошибка очистки корзины'
setError(errorMessage)
setState(prev => ({ ...prev, isLoading: false }))
toast.error(errorMessage)
}
}
// Local-only operations (not synced with backend)
const toggleSelect = (id: string) => { const toggleSelect = (id: string) => {
dispatch({ type: 'TOGGLE_SELECT', payload: id }) setState(prev => ({
...prev,
items: prev.items.map(item =>
item.id === id ? { ...item, selected: !item.selected } : item
)
}))
} }
const toggleFavorite = (id: string) => { const toggleFavorite = (id: string) => {
dispatch({ type: 'TOGGLE_FAVORITE', payload: id }) setState(prev => ({
...prev,
items: prev.items.map(item =>
item.id === id ? { ...item, favorite: !item.favorite } : item
)
}))
} }
const updateComment = (id: string, comment: string) => { const updateComment = (id: string, comment: string) => {
dispatch({ type: 'UPDATE_COMMENT', payload: { id, comment } }) setState(prev => ({
...prev,
items: prev.items.map(item =>
item.id === id ? { ...item, comment } : item
)
}))
} }
const updateOrderComment = (comment: string) => { const updateOrderComment = (comment: string) => {
dispatch({ type: 'UPDATE_ORDER_COMMENT', payload: comment }) setState(prev => ({
...prev,
orderComment: comment
}))
} }
const selectAll = () => { const selectAll = () => {
dispatch({ type: 'SELECT_ALL' }) setState(prev => ({
...prev,
items: prev.items.map(item => ({ ...item, selected: true }))
}))
} }
const removeAll = () => { const removeAll = () => {
dispatch({ type: 'REMOVE_ALL' }) clearCart()
} }
const removeSelected = () => { const removeSelected = async () => {
dispatch({ type: 'REMOVE_SELECTED' }) const selectedItems = state.items.filter(item => item.selected)
for (const item of selectedItems) {
await removeItem(item.id)
}
} }
const updateDelivery = (delivery: Partial<DeliveryInfo>) => { const updateDelivery = (delivery: Partial<DeliveryInfo>) => {
dispatch({ type: 'UPDATE_DELIVERY', payload: delivery }) setState(prev => ({
} ...prev,
delivery: { ...prev.delivery, ...delivery }
const clearCart = () => { }))
dispatch({ type: 'CLEAR_CART' })
// Очищаем localStorage при очистке корзины
if (typeof window !== 'undefined') {
localStorage.removeItem('cartState')
localStorage.removeItem('cart')
}
} }
const clearError = () => { const clearError = () => {
dispatch({ type: 'SET_ERROR', payload: '' }) setError('')
}
// Check if item is in cart (using backend data)
const isInCart = (productId?: string, offerKey?: string, article?: string, brand?: string): boolean => {
return state.items.some(item => {
if (productId && item.productId === productId) return true
if (offerKey && item.offerKey === offerKey) return true
if (article && brand && item.article === article && item.brand === brand) return true
return false
})
} }
const contextValue: CartContextType = { const contextValue: CartContextType = {
state, state: {
...state,
error
},
addItem, addItem,
removeItem, removeItem,
updateQuantity, updateQuantity,
@ -456,7 +405,8 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
removeSelected, removeSelected,
updateDelivery, updateDelivery,
clearCart, clearCart,
clearError clearError,
isInCart
} }
return ( return (
@ -466,7 +416,6 @@ export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children
) )
} }
// Хук для использования контекста корзины // Хук для использования контекста корзины
export const useCart = (): CartContextType => { export const useCart = (): CartContextType => {
const context = useContext(CartContext) const context = useContext(CartContext)

View File

@ -1,6 +1,7 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { useLazyQuery } from '@apollo/client'; import { useLazyQuery } from '@apollo/client';
import { SEARCH_PRODUCT_OFFERS } from '@/lib/graphql'; import { SEARCH_PRODUCT_OFFERS } from '@/lib/graphql';
import { useCart } from '@/contexts/CartContext';
interface ProductOffer { interface ProductOffer {
offerKey: string; offerKey: string;
@ -15,6 +16,7 @@ interface ProductOffer {
warehouse: string; warehouse: string;
supplier: string; supplier: string;
canPurchase: boolean; canPurchase: boolean;
isInCart: boolean;
} }
interface ProductPriceData { interface ProductPriceData {
@ -25,12 +27,22 @@ interface ProductPriceData {
externalOffers: ProductOffer[]; externalOffers: ProductOffer[];
analogs: number; analogs: number;
hasInternalStock: boolean; hasInternalStock: boolean;
isInCart: boolean;
}; };
} }
interface CartItemInput {
productId?: string;
offerKey?: string;
article: string;
brand: string;
quantity: number;
}
interface ProductPriceVariables { interface ProductPriceVariables {
articleNumber: string; articleNumber: string;
brand: string; brand: string;
cartItems?: CartItemInput[];
} }
export const useProductPrices = () => { export const useProductPrices = () => {
@ -38,6 +50,7 @@ export const useProductPrices = () => {
const [loadingPrices, setLoadingPrices] = useState<Set<string>>(new Set()); const [loadingPrices, setLoadingPrices] = useState<Set<string>>(new Set());
const [loadedPrices, setLoadedPrices] = useState<Set<string>>(new Set()); const [loadedPrices, setLoadedPrices] = useState<Set<string>>(new Set());
const { state: cartState } = useCart();
const [searchOffers] = useLazyQuery<ProductPriceData, ProductPriceVariables>(SEARCH_PRODUCT_OFFERS); const [searchOffers] = useLazyQuery<ProductPriceData, ProductPriceVariables>(SEARCH_PRODUCT_OFFERS);
const loadPrice = useCallback(async (product: { code: string; brand: string; id: string }) => { const loadPrice = useCallback(async (product: { code: string; brand: string; id: string }) => {
@ -52,10 +65,22 @@ export const useProductPrices = () => {
setLoadingPrices(prev => new Set([...prev, key])); setLoadingPrices(prev => new Set([...prev, key]));
try { try {
// Преобразуем товары корзины в формат для запроса
const cartItems: CartItemInput[] = cartState.items
.filter(item => item.article && item.brand) // Фильтруем товары с обязательными полями
.map(item => ({
productId: item.productId,
offerKey: item.offerKey,
article: item.article!,
brand: item.brand!,
quantity: item.quantity
}));
const result = await searchOffers({ const result = await searchOffers({
variables: { variables: {
articleNumber: product.code, articleNumber: product.code,
brand: product.brand brand: product.brand,
cartItems
} }
}); });

View File

@ -20,16 +20,25 @@ const authLink = setContext((_, { headers }) => {
const user = JSON.parse(userData); const user = JSON.parse(userData);
// Создаем токен в формате, который ожидает CMS // Создаем токен в формате, который ожидает CMS
token = `client_${user.id}`; token = `client_${user.id}`;
console.log('Apollo Client: создан токен:', token); console.log('Apollo Client: создан токен для авторизованного пользователя:', token);
console.log('Apollo Client: user data:', user);
console.log('Apollo Client: заголовки:', { authorization: `Bearer ${token}` });
} catch (error) { } catch (error) {
console.error('Apollo Client: ошибка парсинга userData:', error); console.error('Apollo Client: ошибка парсинга userData:', error);
localStorage.removeItem('userData'); localStorage.removeItem('userData');
localStorage.removeItem('authToken'); localStorage.removeItem('authToken');
} }
} else { }
console.log('Apollo Client: userData не найден в localStorage');
// Если нет авторизованного пользователя, создаем анонимную сессию для корзины
if (!token) {
let sessionId = localStorage.getItem('anonymousSessionId');
if (!sessionId) {
// Генерируем уникальный ID сессии
sessionId = 'anon_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
localStorage.setItem('anonymousSessionId', sessionId);
console.log('Apollo Client: создана новая анонимная сессия:', sessionId);
}
token = `client_${sessionId}`;
console.log('Apollo Client: используется анонимная сессия:', token);
} }
} }

View File

@ -1125,14 +1125,25 @@ export const GET_LAXIMO_UNIT_IMAGE_MAP = gql`
` `
export const SEARCH_PRODUCT_OFFERS = gql` export const SEARCH_PRODUCT_OFFERS = gql`
query SearchProductOffers($articleNumber: String!, $brand: String!) { query SearchProductOffers($articleNumber: String!, $brand: String!, $cartItems: [CartItemInput!]) {
searchProductOffers(articleNumber: $articleNumber, brand: $brand) { searchProductOffers(articleNumber: $articleNumber, brand: $brand, cartItems: $cartItems) {
articleNumber articleNumber
brand brand
name name
description description
hasInternalStock hasInternalStock
totalOffers totalOffers
isInCart
stockCalculation {
totalInternalStock
totalExternalStock
availableInternalOffers
availableExternalOffers
hasInternalStock
hasExternalStock
totalStock
hasAnyStock
}
images { images {
id id
url url
@ -1168,6 +1179,7 @@ export const SEARCH_PRODUCT_OFFERS = gql`
available available
rating rating
supplier supplier
isInCart
} }
externalOffers { externalOffers {
offerKey offerKey
@ -1185,6 +1197,7 @@ export const SEARCH_PRODUCT_OFFERS = gql`
weight weight
volume volume
canPurchase canPurchase
isInCart
} }
analogs { analogs {
brand brand
@ -1192,6 +1205,16 @@ export const SEARCH_PRODUCT_OFFERS = gql`
name name
type type
} }
stockCalculation {
totalInternalStock
totalExternalStock
availableInternalOffers
availableExternalOffers
hasInternalStock
hasExternalStock
totalStock
hasAnyStock
}
} }
} }
` `
@ -1738,4 +1761,164 @@ export const GET_NEW_ARRIVALS = gql`
} }
} }
} }
` `;
// Cart mutations and queries
export const GET_CART = gql`
query GetCart {
getCart {
id
clientId
items {
id
productId
offerKey
name
description
brand
article
price
currency
quantity
stock
deliveryTime
warehouse
supplier
isExternal
image
createdAt
updatedAt
}
createdAt
updatedAt
}
}
`;
export const ADD_TO_CART = gql`
mutation AddToCart($input: AddToCartInput!) {
addToCart(input: $input) {
success
message
error
cart {
id
clientId
items {
id
productId
offerKey
name
description
brand
article
price
currency
quantity
stock
deliveryTime
warehouse
supplier
isExternal
image
createdAt
updatedAt
}
createdAt
updatedAt
}
}
}
`;
export const REMOVE_FROM_CART = gql`
mutation RemoveFromCart($itemId: ID!) {
removeFromCart(itemId: $itemId) {
success
message
error
cart {
id
clientId
items {
id
productId
offerKey
name
description
brand
article
price
currency
quantity
stock
deliveryTime
warehouse
supplier
isExternal
image
}
createdAt
updatedAt
}
}
}
`;
export const UPDATE_CART_ITEM_QUANTITY = gql`
mutation UpdateCartItemQuantity($itemId: ID!, $quantity: Int!) {
updateCartItemQuantity(itemId: $itemId, quantity: $quantity) {
success
message
error
cart {
id
clientId
items {
id
productId
offerKey
name
description
brand
article
price
currency
quantity
stock
deliveryTime
warehouse
supplier
isExternal
image
}
createdAt
updatedAt
}
}
}
`;
export const CLEAR_CART = gql`
mutation ClearCart {
clearCart {
success
message
error
cart {
id
clientId
items {
id
name
brand
article
quantity
price
}
createdAt
updatedAt
}
}
}
`;

View File

@ -1,8 +1,14 @@
import { PartsIndexCatalogsResponse, PartsIndexGroup, PartsIndexEntityInfoResponse } from '@/types/partsindex'; import { PartsIndexCatalogsResponse, PartsIndexGroup, PartsIndexEntityInfoResponse } from '@/types/partsindex';
const PARTS_INDEX_API_BASE = 'https://api.parts-index.com'; const PARTS_INDEX_API_BASE = process.env.PARTSAPI_URL || 'https://api.parts-index.com';
const API_KEY = 'PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE'; const API_KEY = 'PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE';
// Debug logging for development
if (process.env.NODE_ENV === 'development') {
console.log('🔍 PartsIndex API Base URL:', PARTS_INDEX_API_BASE);
console.log('🔍 Environment variable NEXT_PUBLIC_PARTSAPI_URL:', process.env.NEXT_PUBLIC_PARTSAPI_URL);
}
class PartsIndexService { class PartsIndexService {
/** /**
* Получить список каталогов * Получить список каталогов

View File

@ -58,14 +58,12 @@ export default function App({ Component, pageProps }: AppProps) {
style: { style: {
background: '#363636', background: '#363636',
color: '#fff', color: '#fff',
marginTop: '80px', // Отступ сверху, чтобы не закрывать кнопки меню
}, },
success: { success: {
duration: 3000, duration: 3000,
style: { style: {
background: '#22c55e', // Зеленый фон для успешных уведомлений background: '#22c55e', // Зеленый фон для успешных уведомлений
color: '#fff', // Белый текст color: '#fff', // Белый текст
marginTop: '80px', // Отступ сверху для успешных уведомлений
}, },
iconTheme: { iconTheme: {
primary: '#22c55e', primary: '#22c55e',
@ -75,7 +73,8 @@ export default function App({ Component, pageProps }: AppProps) {
error: { error: {
duration: 5000, duration: 5000,
style: { style: {
marginTop: '80px', // Отступ сверху для ошибок background: '#ef4444',
color: '#fff',
}, },
iconTheme: { iconTheme: {
primary: '#ef4444', primary: '#ef4444',
@ -83,6 +82,9 @@ export default function App({ Component, pageProps }: AppProps) {
}, },
}, },
}} }}
containerStyle={{
top: '80px', // Отступ для всего контейнера toast'ов
}}
/> />
<Script src="/js/webflow.js" strategy="beforeInteractive" /> <Script src="/js/webflow.js" strategy="beforeInteractive" />
<Script <Script

View File

@ -38,11 +38,11 @@ const mockData = Array(12).fill({
brand: "Borsehung", brand: "Borsehung",
}); });
const ITEMS_PER_PAGE = 50; // Целевое количество товаров на странице
const PARTSINDEX_PAGE_SIZE = 25; // Размер страницы PartsIndex API (фиксированный)
const MAX_BRANDS_DISPLAY = 10; // Сколько брендов показывать изначально
export default function Catalog() { export default function Catalog() {
const ITEMS_PER_PAGE = 24; // Показывать 12 карточек за раз
const PARTSINDEX_PAGE_SIZE = 25; // Синхронизировано для оптимальной скорости
const MAX_BRANDS_DISPLAY = 24; // Сколько брендов показывать изначально
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);
const router = useRouter(); const router = useRouter();
const { addItem } = useCart(); const { addItem } = useCart();
const { const {
@ -56,6 +56,36 @@ export default function Catalog() {
const [showSortMobile, setShowSortMobile] = useState(false); const [showSortMobile, setShowSortMobile] = useState(false);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [selectedFilters, setSelectedFilters] = useState<{[key: string]: string[]}>({}); const [selectedFilters, setSelectedFilters] = useState<{[key: string]: string[]}>({});
// Инициализация фильтров из URL при загрузке
useEffect(() => {
if (router.isReady) {
const urlFilters: {[key: string]: string[]} = {};
const urlSearchQuery = router.query.q as string || '';
// Восстанавливаем фильтры из URL
Object.keys(router.query).forEach(key => {
if (key.startsWith('filter_')) {
const filterName = key.replace('filter_', '');
const filterValue = router.query[key];
if (typeof filterValue === 'string') {
urlFilters[filterName] = [filterValue];
} else if (Array.isArray(filterValue)) {
urlFilters[filterName] = filterValue;
}
}
});
console.log('🔗 Восстанавливаем фильтры из URL:', { urlFilters, urlSearchQuery });
if (Object.keys(urlFilters).length > 0) {
setSelectedFilters(urlFilters);
}
if (urlSearchQuery) {
setSearchQuery(urlSearchQuery);
}
}
}, [router.isReady]);
const [visibleArticles, setVisibleArticles] = useState<PartsAPIArticle[]>([]); const [visibleArticles, setVisibleArticles] = useState<PartsAPIArticle[]>([]);
const [visibleEntities, setVisibleEntities] = useState<PartsIndexEntity[]>([]); const [visibleEntities, setVisibleEntities] = useState<PartsIndexEntity[]>([]);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@ -80,6 +110,7 @@ export default function Catalog() {
const [isAutoLoading, setIsAutoLoading] = useState(false); // Автоматическая подгрузка в процессе const [isAutoLoading, setIsAutoLoading] = useState(false); // Автоматическая подгрузка в процессе
const [currentUserPage, setCurrentUserPage] = useState(1); // Текущая пользовательская страница const [currentUserPage, setCurrentUserPage] = useState(1); // Текущая пользовательская страница
const [entitiesCache, setEntitiesCache] = useState<Map<number, PartsIndexEntity[]>>(new Map()); // Кэш страниц const [entitiesCache, setEntitiesCache] = useState<Map<number, PartsIndexEntity[]>>(new Map()); // Кэш страниц
const [isFilterChanging, setIsFilterChanging] = useState(false); // Флаг изменения фильтров
// Карта видимости товаров по индексу // Карта видимости товаров по индексу
const [visibilityMap, setVisibilityMap] = useState<Map<number, boolean>>(new Map()); const [visibilityMap, setVisibilityMap] = useState<Map<number, boolean>>(new Map());
@ -175,16 +206,17 @@ export default function Catalog() {
// Хук для загрузки цен товаров PartsIndex // Хук для загрузки цен товаров PartsIndex
const { getPrice, isLoadingPrice, ensurePriceLoaded } = useProductPrices(); const { getPrice, isLoadingPrice, ensurePriceLoaded } = useProductPrices();
// Загружаем цены для видимых товаров PartsIndex // Загружаем цены для видимых товаров PartsIndex (для отображения конкретных цен)
useEffect(() => { useEffect(() => {
if (isPartsIndexMode && visibleEntities.length > 0) { if (isPartsIndexMode && visibleEntities.length > 0) {
// Загружаем цены только для видимых товаров для отображения точных цен
visibleEntities.forEach((entity, index) => { visibleEntities.forEach((entity, index) => {
const productForPrice = { const productForPrice = {
id: entity.id, id: entity.id,
code: entity.code, code: entity.code,
brand: entity.brand.name brand: entity.brand.name
}; };
// Загружаем с небольшой задержкой чтобы не перегружать сервер // Загружаем с небольшой задержкой
setTimeout(() => { setTimeout(() => {
ensurePriceLoaded(productForPrice); ensurePriceLoaded(productForPrice);
}, index * 50); }, index * 50);
@ -208,9 +240,16 @@ export default function Catalog() {
console.log('📊 Обновляем entitiesData:', { console.log('📊 Обновляем entitiesData:', {
listLength: entitiesData.partsIndexCatalogEntities.list.length, listLength: entitiesData.partsIndexCatalogEntities.list.length,
pagination: entitiesData.partsIndexCatalogEntities.pagination, pagination: entitiesData.partsIndexCatalogEntities.pagination,
currentPage: entitiesData.partsIndexCatalogEntities.pagination?.page?.current || 1 currentPage: entitiesData.partsIndexCatalogEntities.pagination?.page?.current || 1,
isFilterChanging
}); });
// Если изменяются фильтры, сбрасываем флаг после получения новых данных
if (isFilterChanging) {
setIsFilterChanging(false);
console.log('🔄 Сброшен флаг isFilterChanging - получены новые отфильтрованные данные');
}
const newEntities = entitiesData.partsIndexCatalogEntities.list; const newEntities = entitiesData.partsIndexCatalogEntities.list;
const pagination = entitiesData.partsIndexCatalogEntities.pagination; const pagination = entitiesData.partsIndexCatalogEntities.pagination;
@ -228,9 +267,13 @@ export default function Catalog() {
// Если это первая страница или сброс, заменяем накопленные товары // Если это первая страница или сброс, заменяем накопленные товары
if (currentPage === 1) { if (currentPage === 1) {
setAccumulatedEntities(newEntities); setAccumulatedEntities(newEntities);
// Устанавливаем visibleEntities сразу, не дожидаясь проверки цен // Устанавливаем visibleEntities сразу, только если не идет изменение фильтров
if (!isFilterChanging) {
setVisibleEntities(newEntities); setVisibleEntities(newEntities);
console.log('✅ Установлены visibleEntities для первой страницы:', newEntities.length); console.log('✅ Установлены visibleEntities для первой страницы:', newEntities.length);
} else {
console.log('🔄 Пропускаем установку visibleEntities - фильтры изменяются');
}
} else { } else {
// Добавляем к накопленным товарам // Добавляем к накопленным товарам
setAccumulatedEntities(prev => [...prev, ...newEntities]); setAccumulatedEntities(prev => [...prev, ...newEntities]);
@ -245,7 +288,7 @@ export default function Catalog() {
console.log('✅ Пагинация обновлена:', { currentPage, hasNext, hasPrev }); console.log('✅ Пагинация обновлена:', { currentPage, hasNext, hasPrev });
} }
}, [entitiesData]); }, [entitiesData, isFilterChanging]);
// Преобразование выбранных фильтров в формат PartsIndex API // Преобразование выбранных фильтров в формат PartsIndex API
const convertFiltersToPartsIndexParams = useMemo((): Record<string, any> => { const convertFiltersToPartsIndexParams = useMemo((): Record<string, any> => {
@ -284,28 +327,15 @@ export default function Catalog() {
// Восстанавливаем автоподгрузку // Восстанавливаем автоподгрузку
console.log('🔄 Автоподгрузка активна'); console.log('🔄 Автоподгрузка активна');
// Подсчитываем текущее количество товаров с предложениями // Подсчитываем текущее количество товаров (все уже отфильтрованы на сервере)
const currentEntitiesWithOffers = accumulatedEntities.filter(entity => { const currentEntitiesCount = accumulatedEntities.length;
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
const priceData = getPrice(productForPrice);
const isLoadingPriceData = isLoadingPrice(productForPrice);
// Товар считается "с предложениями" если у него есть реальная цена (не null и не undefined)
return (priceData && priceData.price && priceData.price > 0) || isLoadingPriceData;
});
console.log('📊 Автоподгрузка: текущее состояние:', { console.log('📊 Автоподгрузка: текущее состояние:', {
накопленоТоваров: accumulatedEntities.length, накопленоТоваров: currentEntitiesCount,
сПредложениями: currentEntitiesWithOffers.length,
целевоеКоличество: ITEMS_PER_PAGE, целевоеКоличество: ITEMS_PER_PAGE,
естьЕщеТовары: hasMoreEntities естьЕщеТовары: hasMoreEntities
}); });
// Если у нас уже достаточно товаров с предложениями, не загружаем
if (currentEntitiesWithOffers.length >= ITEMS_PER_PAGE) {
console.log('✅ Автоподгрузка: достаточно товаров с предложениями');
return;
}
// Даем время на загрузку цен товаров, если их слишком много загружается // Даем время на загрузку цен товаров, если их слишком много загружается
const loadingCount = accumulatedEntities.filter(entity => { const loadingCount = accumulatedEntities.filter(entity => {
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name }; const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
@ -371,7 +401,8 @@ export default function Catalog() {
type: 'range' as const, type: 'range' as const,
title: param.name, title: param.name,
min, min,
max max,
defaultOpen: false,
}; };
} else { } else {
// Для dropdown фильтров // Для dropdown фильтров
@ -382,7 +413,8 @@ export default function Catalog() {
.filter((value: any) => value.available) // Показываем только доступные .filter((value: any) => value.available) // Показываем только доступные
.map((value: any) => value.title || value.value), .map((value: any) => value.title || value.value),
multi: true, multi: true,
showAll: true showAll: true,
defaultOpen: false,
}; };
} }
}); });
@ -444,27 +476,26 @@ export default function Catalog() {
} }
}, [isPartsIndexMode, entitiesWithOffers.length, hasMoreEntities, isAutoLoading]); }, [isPartsIndexMode, entitiesWithOffers.length, hasMoreEntities, isAutoLoading]);
// Обновляем список товаров с предложениями при изменении накопленных товаров или цен // Обновляем список товаров при изменении накопленных товаров (серверная фильтрация)
useEffect(() => { useEffect(() => {
if (!isPartsIndexMode) { if (!isPartsIndexMode) {
return; return;
} }
// Показываем все товары, но отдельно считаем те, у которых есть цены // Если фильтры изменяются, не обновляем отображение старых данных
if (isFilterChanging) {
console.log('🔄 Пропускаем обновление entitiesWithOffers - фильтры изменяются');
return;
}
// Все товары уже отфильтрованы на сервере - показываем все накопленные
const entitiesWithOffers = accumulatedEntities; const entitiesWithOffers = accumulatedEntities;
// Подсчитываем количество товаров с реальными ценами для автоподгрузки console.log('📊 Обновляем entitiesWithOffers (серверная фильтрация):', {
const entitiesWithRealPrices = accumulatedEntities.filter(entity => {
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
const priceData = getPrice(productForPrice);
return priceData && priceData.price && priceData.price > 0;
});
console.log('📊 Обновляем entitiesWithOffers:', {
накопленоТоваров: accumulatedEntities.length, накопленоТоваров: accumulatedEntities.length,
отображаемыхТоваров: entitiesWithOffers.length, отображаемыхТоваров: entitiesWithOffers.length,
сРеальнымиЦенами: entitiesWithRealPrices.length, целевоеКоличество: ITEMS_PER_PAGE,
целевоеКоличество: ITEMS_PER_PAGE isFilterChanging
}); });
setEntitiesWithOffers(entitiesWithOffers); setEntitiesWithOffers(entitiesWithOffers);
@ -484,31 +515,9 @@ export default function Catalog() {
setVisibleEntities(visibleForCurrentPage); setVisibleEntities(visibleForCurrentPage);
}, [isPartsIndexMode, accumulatedEntities, currentUserPage]); }, [isPartsIndexMode, accumulatedEntities, currentUserPage, isFilterChanging]);
// Отдельный useEffect для обновления статистики цен (без влияния на visibleEntities)
useEffect(() => {
if (!isPartsIndexMode || accumulatedEntities.length === 0) {
return;
}
// Обновляем статистику каждые 2 секунды
const timer = setTimeout(() => {
const entitiesWithRealPrices = accumulatedEntities.filter(entity => {
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
const priceData = getPrice(productForPrice);
return priceData && priceData.price && priceData.price > 0;
});
console.log('💰 Обновление статистики цен:', {
накопленоТоваров: accumulatedEntities.length,
сРеальнымиЦенами: entitiesWithRealPrices.length,
процентЗагрузки: Math.round((entitiesWithRealPrices.length / accumulatedEntities.length) * 100)
});
}, 2000);
return () => clearTimeout(timer);
}, [isPartsIndexMode, accumulatedEntities.length, getPrice]);
// Генерируем динамические фильтры для PartsAPI // Генерируем динамические фильтры для PartsAPI
const generatePartsAPIFilters = useCallback((): FilterConfig[] => { const generatePartsAPIFilters = useCallback((): FilterConfig[] => {
@ -551,7 +560,7 @@ export default function Catalog() {
options: brandsToShow.sort(), // Сортируем по алфавиту для удобства options: brandsToShow.sort(), // Сортируем по алфавиту для удобства
multi: true, multi: true,
showAll: true, showAll: true,
defaultOpen: true, defaultOpen: false,
hasMore: !showAllBrands && sortedBrands.length > MAX_BRANDS_DISPLAY, hasMore: !showAllBrands && sortedBrands.length > MAX_BRANDS_DISPLAY,
onShowMore: () => setShowAllBrands(true) onShowMore: () => setShowAllBrands(true)
}); });
@ -564,7 +573,7 @@ export default function Catalog() {
options: Array.from(productGroups).sort(), options: Array.from(productGroups).sort(),
multi: true, multi: true,
showAll: true, showAll: true,
defaultOpen: true, defaultOpen: false,
}); });
} }
@ -595,27 +604,94 @@ export default function Catalog() {
// Функция для обновления URL с фильтрами
const updateUrlWithFilters = useCallback((filters: {[key: string]: string[]}, search: string) => {
const query: any = { ...router.query };
// Удаляем старые фильтры из URL
Object.keys(query).forEach(key => {
if (key.startsWith('filter_') || key === 'q') {
delete query[key];
}
});
// Добавляем новые фильтры
Object.entries(filters).forEach(([filterName, values]) => {
if (values.length > 0) {
query[`filter_${filterName}`] = values.length === 1 ? values[0] : values;
}
});
// Добавляем поисковый запрос
if (search.trim()) {
query.q = search;
}
// Обновляем URL без перезагрузки страницы
router.push({
pathname: router.pathname,
query
}, undefined, { shallow: true });
}, [router]);
const handleDesktopFilterChange = (filterTitle: string, value: string | string[]) => { const handleDesktopFilterChange = (filterTitle: string, value: string | string[]) => {
setSelectedFilters(prev => ({ setSelectedFilters(prev => {
...prev, const newFilters = { ...prev };
[filterTitle]: Array.isArray(value) ? value : [value]
})); // Если значение пустое (пустой массив или пустая строка), удаляем фильтр
if (Array.isArray(value) && value.length === 0) {
delete newFilters[filterTitle];
} else if (!value || (typeof value === 'string' && value.trim() === '')) {
delete newFilters[filterTitle];
} else {
// Иначе устанавливаем значение
newFilters[filterTitle] = Array.isArray(value) ? value : [value];
}
// Обновляем URL
updateUrlWithFilters(newFilters, searchQuery);
return newFilters;
});
}; };
const handleMobileFilterChange = (type: string, value: any) => { const handleMobileFilterChange = (type: string, value: any) => {
setSelectedFilters(prev => ({ setSelectedFilters(prev => {
...prev, const newFilters = { ...prev };
[type]: Array.isArray(value) ? value : [value]
})); // Если значение пустое (пустой массив или пустая строка), удаляем фильтр
if (Array.isArray(value) && value.length === 0) {
delete newFilters[type];
} else if (!value || (typeof value === 'string' && value.trim() === '')) {
delete newFilters[type];
} else {
// Иначе устанавливаем значение
newFilters[type] = Array.isArray(value) ? value : [value];
}
// Обновляем URL
updateUrlWithFilters(newFilters, searchQuery);
return newFilters;
});
}; };
// Обработчик изменения поискового запроса
const handleSearchChange = useCallback((value: string) => {
setSearchQuery(value);
updateUrlWithFilters(selectedFilters, value);
}, [selectedFilters, updateUrlWithFilters]);
// Функция для сброса всех фильтров // Функция для сброса всех фильтров
const handleResetFilters = useCallback(() => { const handleResetFilters = useCallback(() => {
setSearchQuery(''); setSearchQuery('');
setSelectedFilters({}); setSelectedFilters({});
setShowAllBrands(false); setShowAllBrands(false);
setPartsIndexPage(1); // Сбрасываем страницу PartsIndex на первую setPartsIndexPage(1); // Сбрасываем страницу PartsIndex на первую
}, []);
// Очищаем URL от фильтров
updateUrlWithFilters({}, '');
}, [updateUrlWithFilters]);
// Фильтрация по поиску и фильтрам для PartsAPI // Фильтрация по поиску и фильтрам для PartsAPI
const filteredArticles = useMemo(() => { const filteredArticles = useMemo(() => {
@ -672,6 +748,10 @@ export default function Catalog() {
// Если изменился поисковый запрос или фильтры, нужно перезагрузить данные с сервера // Если изменился поисковый запрос или фильтры, нужно перезагрузить данные с сервера
if (searchQuery.trim() || Object.keys(selectedFilters).length > 0) { if (searchQuery.trim() || Object.keys(selectedFilters).length > 0) {
console.log('🔍 Поисковый запрос или фильтры изменились, сбрасываем пагинацию'); console.log('🔍 Поисковый запрос или фильтры изменились, сбрасываем пагинацию');
// Устанавливаем флаг изменения фильтров
setIsFilterChanging(true);
setPartsIndexPage(1); setPartsIndexPage(1);
setCurrentUserPage(1); setCurrentUserPage(1);
setHasMoreEntities(true); setHasMoreEntities(true);
@ -679,10 +759,36 @@ export default function Catalog() {
setEntitiesWithOffers([]); setEntitiesWithOffers([]);
setEntitiesCache(new Map()); setEntitiesCache(new Map());
// Перезагружаем данные с новыми параметрами фильтрации // Вычисляем параметры фильтрации прямо здесь, чтобы избежать зависимости от useMemo
const apiParams = convertFiltersToPartsIndexParams; let apiParams: Record<string, any> = {};
if (paramsData?.partsIndexCatalogParams?.list && Object.keys(selectedFilters).length > 0) {
paramsData.partsIndexCatalogParams.list.forEach((param: any) => {
const selectedValues = selectedFilters[param.name];
if (selectedValues && selectedValues.length > 0) {
// Находим соответствующие значения из API данных
const matchingValues = param.values.filter((value: any) =>
selectedValues.includes(value.title || value.value)
);
if (matchingValues.length > 0) {
// Используем ID параметра из API и значения
apiParams[param.id] = matchingValues.map((v: any) => v.value);
}
}
});
}
const paramsString = Object.keys(apiParams).length > 0 ? JSON.stringify(apiParams) : undefined; const paramsString = Object.keys(apiParams).length > 0 ? JSON.stringify(apiParams) : undefined;
console.log('🔄 Запуск refetch с новыми фильтрами:', {
searchQuery,
selectedFilters,
apiParams,
paramsString,
catalogId,
groupId
});
// Также обновляем параметры фильтрации // Также обновляем параметры фильтрации
refetchParams({ refetchParams({
catalogId: catalogId as string, catalogId: catalogId as string,
@ -690,6 +796,10 @@ export default function Catalog() {
lang: 'ru', lang: 'ru',
q: searchQuery || undefined, q: searchQuery || undefined,
params: paramsString params: paramsString
}).then(result => {
console.log('✅ refetchParams результат:', result);
}).catch(error => {
console.error('❌ refetchParams ошибка:', error);
}); });
refetchEntities({ refetchEntities({
@ -700,11 +810,20 @@ export default function Catalog() {
page: 1, page: 1,
q: searchQuery || undefined, q: searchQuery || undefined,
params: paramsString params: paramsString
}).then(result => {
console.log('✅ refetchEntities результат:', result.data?.partsIndexCatalogEntities?.list?.length || 0, 'товаров');
}).catch(error => {
console.error('❌ refetchEntities ошибка:', error);
}); });
} else {
// Если нет активных фильтров, сбрасываем флаг
if (isFilterChanging) {
setIsFilterChanging(false);
}
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isPartsIndexMode, searchQuery, JSON.stringify(selectedFilters), refetchEntities, refetchParams, convertFiltersToPartsIndexParams]); }, [isPartsIndexMode, searchQuery, JSON.stringify(selectedFilters), paramsData]);
// Управляем показом пустого состояния с задержкой // Управляем показом пустого состояния с задержкой
useEffect(() => { useEffect(() => {
@ -724,12 +843,18 @@ export default function Catalog() {
} else if (isPartsIndexMode && !entitiesLoading && !entitiesError) { } else if (isPartsIndexMode && !entitiesLoading && !entitiesError) {
// Для PartsIndex показываем пустое состояние если нет товаров И данные уже загружены // Для PartsIndex показываем пустое состояние если нет товаров И данные уже загружены
const hasLoadedData = accumulatedEntities.length > 0 || Boolean(entitiesData?.partsIndexCatalogEntities?.list); const hasLoadedData = accumulatedEntities.length > 0 || Boolean(entitiesData?.partsIndexCatalogEntities?.list);
setShowEmptyState(hasLoadedData && visibleEntities.length === 0);
console.log('📊 Определяем showEmptyState для PartsIndex:', { // Показываем пустое состояние если данные загружены и нет видимых товаров
// (товары уже отфильтрованы на сервере, поэтому не нужно ждать загрузки цен)
const shouldShowEmpty = hasLoadedData && visibleEntities.length === 0;
setShowEmptyState(shouldShowEmpty);
console.log('📊 Определяем showEmptyState для PartsIndex (серверная фильтрация):', {
hasLoadedData, hasLoadedData,
visibleEntitiesLength: visibleEntities.length, visibleEntitiesLength: visibleEntities.length,
accumulatedEntitiesLength: accumulatedEntities.length, accumulatedEntitiesLength: accumulatedEntities.length,
showEmptyState: hasLoadedData && visibleEntities.length === 0 shouldShowEmpty,
showEmptyState: shouldShowEmpty
}); });
} else { } else {
setShowEmptyState(false); setShowEmptyState(false);
@ -804,6 +929,20 @@ export default function Catalog() {
return false; return false;
}, [isPartsAPIMode, loadedArticlesCount, filteredArticles.length]); }, [isPartsAPIMode, loadedArticlesCount, filteredArticles.length]);
useEffect(() => {
// Сбросить все состояния при смене каталога или подкатегории
setAccumulatedEntities([]);
setVisibleEntities([]);
setEntitiesWithOffers([]);
setEntitiesCache(new Map());
setCurrentUserPage(1);
setPartsIndexPage(1);
setHasMoreEntities(true);
setShowEmptyState(false);
setIsFilterChanging(false);
setVisibleCount(ITEMS_PER_PAGE);
}, [catalogId, groupId]);
if (filtersLoading) { if (filtersLoading) {
return <div className="py-8 text-center">Загрузка фильтров...</div>; return <div className="py-8 text-center">Загрузка фильтров...</div>;
} }
@ -878,35 +1017,35 @@ export default function Catalog() {
</div> </div>
</div> </div>
{isPartsAPIMode ? ( {isPartsAPIMode ? (
<div className="filters-desktop"> <div className="filters-desktop" style={{ width: '300px', marginRight: '20px', marginBottom: '80px' }}>
<Filters <Filters
filters={dynamicFilters} filters={dynamicFilters}
onFilterChange={handleDesktopFilterChange} onFilterChange={handleDesktopFilterChange}
filterValues={selectedFilters} filterValues={selectedFilters}
searchQuery={searchQuery} searchQuery={searchQuery}
onSearchChange={setSearchQuery} onSearchChange={handleSearchChange}
isLoading={filtersGenerating} isLoading={filtersGenerating}
/> />
</div> </div>
) : isPartsIndexMode ? ( ) : isPartsIndexMode ? (
<div className="filters-desktop"> <div className="filters-desktop" style={{ width: '300px', marginRight: '20px', marginBottom: '80px' }}>
<Filters <Filters
filters={catalogFilters} filters={catalogFilters}
onFilterChange={handleDesktopFilterChange} onFilterChange={handleDesktopFilterChange}
filterValues={selectedFilters} filterValues={selectedFilters}
searchQuery={searchQuery} searchQuery={searchQuery}
onSearchChange={setSearchQuery} onSearchChange={handleSearchChange}
isLoading={filtersLoading} isLoading={filtersLoading}
/> />
</div> </div>
) : ( ) : (
<div className="filters-desktop"> <div className="filters-desktop" style={{ width: '300px', marginRight: '20px', marginBottom: '80px' }}>
<Filters <Filters
filters={catalogFilters} filters={catalogFilters}
onFilterChange={handleDesktopFilterChange} onFilterChange={handleDesktopFilterChange}
filterValues={selectedFilters} filterValues={selectedFilters}
searchQuery={searchQuery} searchQuery={searchQuery}
onSearchChange={setSearchQuery} onSearchChange={handleSearchChange}
isLoading={filtersLoading} isLoading={filtersLoading}
/> />
</div> </div>
@ -916,7 +1055,7 @@ export default function Catalog() {
onClose={() => setShowFiltersMobile(false)} onClose={() => setShowFiltersMobile(false)}
filters={isPartsAPIMode ? dynamicFilters : catalogFilters} filters={isPartsAPIMode ? dynamicFilters : catalogFilters}
searchQuery={searchQuery} searchQuery={searchQuery}
onSearchChange={setSearchQuery} onSearchChange={handleSearchChange}
filterValues={selectedFilters} filterValues={selectedFilters}
onFilterChange={handleMobileFilterChange} onFilterChange={handleMobileFilterChange}
/> />
@ -936,6 +1075,8 @@ export default function Catalog() {
</div> </div>
)} )}
{/* Сообщение об ошибке */} {/* Сообщение об ошибке */}
{isPartsAPIMode && articlesError && ( {isPartsAPIMode && articlesError && (
<div className="flex justify-center items-center py-8"> <div className="flex justify-center items-center py-8">
@ -985,32 +1126,36 @@ export default function Catalog() {
</> </>
)} )}
{/* Показываем индикатор загрузки при изменении фильтров */}
{isPartsIndexMode && isFilterChanging && (
<div className="flex flex-col items-center justify-center py-12">
<LoadingSpinner />
<div className="text-gray-500 text-lg mt-4">Применяем фильтры...</div>
</div>
)}
{/* Отображение товаров PartsIndex */} {/* Отображение товаров PartsIndex */}
{isPartsIndexMode && (() => { {isPartsIndexMode && !isFilterChanging && accumulatedEntities.length > 0 && (
console.log('🎯 Проверяем отображение PartsIndex товаров:', {
isPartsIndexMode,
visibleEntitiesLength: visibleEntities.length,
visibleEntities: visibleEntities.map(e => ({ id: e.id, code: e.code, brand: e.brand.name }))
});
return visibleEntities.length > 0;
})() && (
<> <>
{visibleEntities {accumulatedEntities.slice(0, visibleCount).map((entity, idx) => {
.map((entity, idx) => {
const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name }; const productForPrice = { id: entity.id, code: entity.code, brand: entity.brand.name };
const priceData = getPrice(productForPrice); const priceData = getPrice(productForPrice);
const isLoadingPriceData = isLoadingPrice(productForPrice); const isLoadingPriceData = isLoadingPrice(productForPrice);
// Определяем цену для отображения // Определяем цену для отображения (все товары уже отфильтрованы на сервере)
let displayPrice = "Цена по запросу"; let displayPrice = "";
let displayCurrency = "RUB"; let displayCurrency = "RUB";
let priceElement; let priceElement;
if (isLoadingPriceData) { if (isLoadingPriceData) {
// Показываем скелетон загрузки вместо текста
priceElement = <PriceSkeleton />; priceElement = <PriceSkeleton />;
} else if (priceData && priceData.price) { } else if (priceData && priceData.price) {
displayPrice = `${priceData.price.toLocaleString('ru-RU')}`; displayPrice = `${priceData.price.toLocaleString('ru-RU')}`;
displayCurrency = priceData.currency || "RUB"; displayCurrency = priceData.currency || "RUB";
} else {
// Если нет данных о цене, показываем скелетон (товар должен загрузиться)
priceElement = <PriceSkeleton />;
} }
return ( return (
@ -1021,7 +1166,7 @@ export default function Catalog() {
articleNumber={entity.code} articleNumber={entity.code}
brandName={entity.brand.name} brandName={entity.brand.name}
image={entity.images?.[0] || ''} image={entity.images?.[0] || ''}
price={isLoadingPriceData ? "" : displayPrice} price={priceElement ? "" : displayPrice}
priceElement={priceElement} priceElement={priceElement}
oldPrice="" oldPrice=""
discount="" discount=""
@ -1029,6 +1174,7 @@ export default function Catalog() {
productId={entity.id} productId={entity.id}
artId={entity.id} artId={entity.id}
offerKey={priceData?.offerKey} offerKey={priceData?.offerKey}
isInCart={priceData?.isInCart}
onAddToCart={async () => { onAddToCart={async () => {
// Если цена не загружена, загружаем её и добавляем в корзину // Если цена не загружена, загружаем её и добавляем в корзину
if (!priceData && !isLoadingPriceData) { if (!priceData && !isLoadingPriceData) {
@ -1082,40 +1228,19 @@ export default function Catalog() {
); );
})} })}
{/* Пагинация для PartsIndex */} {/* Кнопка "Показать еще" */}
{visibleCount < accumulatedEntities.length && (
<div className="w-layout-hflex pagination"> <div className="w-layout-hflex pagination">
<button <button
onClick={() => { onClick={() => setVisibleCount(c => Math.min(c + ITEMS_PER_PAGE, accumulatedEntities.length))}
console.log('🖱️ Клик по кнопке "Назад"'); className="button_strock w-button"
handlePrevPage();
}}
disabled={currentUserPage <= 1}
className="button_strock w-button mr-2"
> >
Назад Показать еще
</button>
<span className="flex items-center px-4 text-gray-600">
Страница {currentUserPage} из {Math.ceil(accumulatedEntities.length / ITEMS_PER_PAGE) || 1}
{isAutoLoading && ' (загружаем...)'}
<span className="ml-2 text-xs text-gray-400">
(товаров: {accumulatedEntities.length})
</span>
</span>
<button
onClick={() => {
console.log('🖱️ Клик по кнопке "Вперед"');
handleNextPage();
}}
disabled={currentUserPage >= Math.ceil(accumulatedEntities.length / ITEMS_PER_PAGE)}
className="button_strock w-button ml-2"
>
Вперед
</button> </button>
</div> </div>
)}
{/* Отладочная информация */} {/* Отладочная информация
{isPartsIndexMode && ( {isPartsIndexMode && (
<div className="text-xs text-gray-500 mt-4 p-2 bg-gray-100 rounded"> <div className="text-xs text-gray-500 mt-4 p-2 bg-gray-100 rounded">
<div>🔍 Отладка PartsIndex (исправленная логика):</div> <div>🔍 Отладка PartsIndex (исправленная логика):</div>
@ -1141,7 +1266,7 @@ export default function Catalog() {
{isAutoLoading ? 'Загружаем...' : 'Загрузить еще'} {isAutoLoading ? 'Загружаем...' : 'Загрузить еще'}
</button> </button>
</div> </div>
)} )} */}
</> </>
)} )}

View File

@ -0,0 +1,146 @@
import React from 'react';
import Head from 'next/head';
import CatalogSubscribe from "@/components/CatalogSubscribe";
import MobileMenuBottomSection from "@/components/MobileMenuBottomSection";
import NewsAndPromos from "@/components/index/NewsAndPromos";
import Footer from "@/components/Footer";
import IndexTopMenuNav from "@/components/index/IndexTopMenuNav";
import MetaTags from "@/components/MetaTags";
import { getMetaByPath } from "@/lib/meta-config";
import JsonLdScript from "@/components/JsonLdScript";
import { generateOrganizationSchema, generateWebSiteSchema, PROTEK_ORGANIZATION } from "@/lib/schema";
export default function Confidentiality() {
const metaData = getMetaByPath('/');
// Добавьте эти строки:
const organizationSchema = generateOrganizationSchema(PROTEK_ORGANIZATION);
const websiteSchema = generateWebSiteSchema(
"Protek - Автозапчасти и аксессуары",
"https://protek.ru",
"https://protek.ru/search"
);
return (
<>
<MetaTags {...metaData} />
<JsonLdScript schema={organizationSchema} />
<JsonLdScript schema={websiteSchema} />
<section className="section-info">
<div className="w-layout-blockcontainer container info w-container">
<div className="w-layout-vflex flex-block-9">
<div className="w-layout-hflex flex-block-7">
<a href="#" className="link-block w-inline-block">
<div>Главная</div>
</a>
<div className="text-block-3"></div>
<a href="#" className="link-block-2 w-inline-block">
<div>Политика конфиденциальности</div>
</a>
</div>
<div className="w-layout-hflex flex-block-8">
<div className="w-layout-hflex flex-block-10">
<h1 className="heading">Политика конфиденциальности</h1>
</div>
</div>
</div>
</div>
</section>
<div className="flex relative gap-8 items-start self-stretch pt-10 pb-20 max-md:p-8 max-sm:gap-5 max-sm:p-5">
<div className="flex relative flex-col gap-8 items-start p-10 bg-white rounded-3xl flex-[1_0_0] max-w-[1580px] mx-auto max-md:p-8 max-sm:gap-5 max-sm:p-5">
<div className="flex relative flex-col gap-5 items-start self-stretch max-sm:gap-4">
<div
layer-name="Объявлен старт продаж электрических насосов"
className="relative self-stretch text-3xl font-bold leading-9 text-gray-950"
>
Объявлен старт продаж электрических насосов
</div>
<div
layer-name="Бренд вывел на рынок сразу широкий ассортимент, уже на старте продаж - более 100 артикулов и включает в себя позиции для брендов-лидеров автомобильного рынка, например: артикул 77WPE080 для Mercedes-Benz S-CLASS (W221, C216), артикул 77WPE096 Land Rover DISCOVERY V (L462) / Jaguar F-PACE (X761), артикул 77WPE014 Audi Q5 (8RB) / Volkswagen TOUAREG (7P5, 7P6)."
className="relative self-stretch text-base leading-6 text-gray-600 max-sm:text-sm"
>
Бренд вывел на рынок сразу широкий ассортимент, уже на старте
продаж - более 100 артикулов и включает в себя позиции для
брендов-лидеров автомобильного рынка, например: артикул 77WPE080
для Mercedes-Benz S-CLASS (W221, C216), артикул 77WPE096 Land
Rover DISCOVERY V (L462) / Jaguar F-PACE (X761), артикул 77WPE014
Audi Q5 (8RB) / Volkswagen TOUAREG (7P5, 7P6).
</div>
</div>
<div className="flex relative flex-col gap-8 items-start self-stretch max-sm:gap-5">
<div
layer-name="Преимущества электрических насосов охлаждающей жидкости MasterKit Electro:"
className="relative self-stretch text-3xl font-medium leading-9 text-gray-950"
>
Преимущества электрических насосов охлаждающей жидкости MasterKit
Electro:
</div>
<div className="flex relative flex-col gap-3.5 items-start self-stretch">
<div className="flex relative gap-10 items-start w-full max-md:gap-5 max-sm:gap-4">
<div className="relative shrink-0 mt-2 w-2 h-2 bg-gray-600 rounded-full" />
<div
layer-name="Отличная производительность за счёт применения компонентов известных мировых брендов."
className="relative text-base leading-6 text-gray-600 flex-[1_0_0] max-sm:text-sm"
>
Отличная производительность за счёт применения компонентов
известных мировых брендов.
</div>
</div>
<div className="flex relative gap-10 items-start w-full max-md:gap-5 max-sm:gap-4">
<div className="relative shrink-0 mt-2 w-2 h-2 bg-gray-600 rounded-full" />
<div
layer-name="Герметичность и устойчивость к коррозии"
className="relative text-base leading-6 text-gray-600 flex-[1_0_0] max-sm:text-sm"
>
Герметичность и устойчивость к коррозии
</div>
</div>
<div className="flex relative gap-10 items-start w-full max-md:gap-5 max-sm:gap-4">
<div className="relative shrink-0 mt-2 w-2 h-2 bg-gray-600 rounded-full" />
<div
layer-name="Высококачественные материалы компонентов, обеспечивающие долгий срок службы"
className="relative text-base leading-6 text-gray-600 flex-[1_0_0] max-sm:text-sm"
>
Высококачественные материалы компонентов, обеспечивающие
долгий срок службы
</div>
</div>
<div className="flex relative gap-10 items-start w-full max-md:gap-5 max-sm:gap-4">
<div className="relative shrink-0 mt-2 w-2 h-2 bg-gray-600 rounded-full" />
<div
layer-name="Широкий ассортимент более 100 артикулов"
className="relative text-base leading-6 text-gray-600 flex-[1_0_0] max-sm:text-sm"
>
Широкий ассортимент более 100 артикулов
</div>
</div>
</div>
<div
layer-name="На электрические насосы системы охлаждения MasterKit Electro предоставляется гарантия 1 год или 30.000 км пробега, в зависимости от того, что наступит раньше. Все новинки уже внесены в каталог подбора продукции и доступны для заказа."
className="relative self-stretch text-base leading-6 text-gray-600 max-sm:text-sm"
>
На электрические насосы системы охлаждения MasterKit Electro
предоставляется гарантия 1 год или 30.000 км пробега, в
зависимости от того, что наступит раньше. Все новинки уже внесены
в каталог подбора продукции и доступны для заказа.
</div>
<div
layer-name="ABig_Button"
data-component-name="ABig_Button"
data-variant-name="Button big=Default"
className="relative gap-2.5 px-10 py-6 text-lg font-medium leading-5 text-center text-white no-underline bg-red-600 rounded-xl transition-all cursor-pointer border-[none] duration-[0.2s] ease-[ease] w-fit max-sm:px-8 max-sm:py-5 max-sm:w-full hover:bg-red-700"
>
Перейти к товару
</div>
</div>
</div>
</div>
<section className="section-3">
<CatalogSubscribe />
</section>
<Footer />
<MobileMenuBottomSection />
</>
);
}

View File

@ -16,6 +16,7 @@ import MobileMenuBottomSection from '../components/MobileMenuBottomSection';
import { SEARCH_PRODUCT_OFFERS, GET_ANALOG_OFFERS } from "@/lib/graphql"; import { SEARCH_PRODUCT_OFFERS, GET_ANALOG_OFFERS } from "@/lib/graphql";
import { useArticleImage } from "@/hooks/useArticleImage"; import { useArticleImage } from "@/hooks/useArticleImage";
import { usePartsIndexEntityInfo } from "@/hooks/usePartsIndex"; import { usePartsIndexEntityInfo } from "@/hooks/usePartsIndex";
import { useCart } from "@/contexts/CartContext";
import MetaTags from "@/components/MetaTags"; import MetaTags from "@/components/MetaTags";
import { createProductMeta } from "@/lib/meta-config"; import { createProductMeta } from "@/lib/meta-config";
@ -189,7 +190,28 @@ const getBestOffers = (offers: any[]) => {
// Убрано: функция сортировки теперь в CoreProductCard // Убрано: функция сортировки теперь в CoreProductCard
const transformOffersForCard = (offers: any[]) => { // Функция для проверки наличия товара на складе
const checkProductStock = (result: any): boolean => {
if (!result) return false;
// Используем новые данные stockCalculation если доступны
if (result.stockCalculation) {
return result.stockCalculation.hasAnyStock;
}
// Fallback к старой логике для обратной совместимости
const hasInternalStock = result.internalOffers?.some((offer: any) =>
offer.quantity > 0 && offer.available
);
const hasExternalStock = result.externalOffers?.some((offer: any) =>
offer.quantity > 0
);
return hasInternalStock || hasExternalStock;
};
const transformOffersForCard = (offers: any[], hasStock: boolean = true) => {
return offers.map(offer => { return offers.map(offer => {
const isExternal = offer.type === 'external'; const isExternal = offer.type === 'external';
const deliveryDays = isExternal ? offer.deliveryTime : offer.deliveryDays; const deliveryDays = isExternal ? offer.deliveryTime : offer.deliveryDays;
@ -207,6 +229,7 @@ const transformOffersForCard = (offers: any[]) => {
warehouse: offer.warehouse, warehouse: offer.warehouse,
supplier: offer.supplier, supplier: offer.supplier,
deliveryTime: deliveryDays, deliveryTime: deliveryDays,
hasStock, // Добавляем информацию о наличии
}; };
}); });
}; };
@ -214,6 +237,7 @@ const transformOffersForCard = (offers: any[]) => {
export default function SearchResult() { export default function SearchResult() {
const router = useRouter(); const router = useRouter();
const { article, brand, q, artId } = router.query; const { article, brand, q, artId } = router.query;
const { state: cartState } = useCart();
// Убрано: глобальная сортировка теперь не используется // Убрано: глобальная сортировка теперь не используется
const [showFiltersMobile, setShowFiltersMobile] = useState(false); const [showFiltersMobile, setShowFiltersMobile] = useState(false);
@ -241,10 +265,20 @@ export default function SearchResult() {
setVisibleAnalogsCount(ANALOGS_CHUNK_SIZE); setVisibleAnalogsCount(ANALOGS_CHUNK_SIZE);
}, [article, brand]); }, [article, brand]);
// Подготавливаем данные корзины для отправки на backend
const cartItems = cartState.items.map(item => ({
productId: item.productId,
offerKey: item.offerKey,
article: item.article || '',
brand: item.brand || '',
quantity: item.quantity
}));
const { data, loading, error } = useQuery(SEARCH_PRODUCT_OFFERS, { const { data, loading, error } = useQuery(SEARCH_PRODUCT_OFFERS, {
variables: { variables: {
articleNumber: searchQuery, articleNumber: searchQuery,
brand: brandQuery || '' // Используем пустую строку если бренд не указан brand: brandQuery || '', // Используем пустую строку если бренд не указан
cartItems: cartItems
}, },
skip: !searchQuery, skip: !searchQuery,
errorPolicy: 'all' errorPolicy: 'all'
@ -668,8 +702,10 @@ export default function SearchResult() {
{/* Основной товар */} {/* Основной товар */}
<div className="w-layout-vflex flex-block-14-copy"> <div className="w-layout-vflex flex-block-14-copy">
{hasOffers && result && (() => { {hasOffers && result && (() => {
const hasMainProductStock = checkProductStock(result);
const mainProductOffers = transformOffersForCard( const mainProductOffers = transformOffersForCard(
filteredOffers.filter(o => !o.isAnalog) filteredOffers.filter(o => !o.isAnalog),
hasMainProductStock
); );
// Не показываем основной товар, если у него нет предложений // Не показываем основной товар, если у него нет предложений
@ -690,6 +726,7 @@ export default function SearchResult() {
offers={mainProductOffers} offers={mainProductOffers}
showMoreText={mainProductOffers.length < filteredOffers.filter(o => !o.isAnalog).length ? "Показать еще" : undefined} showMoreText={mainProductOffers.length < filteredOffers.filter(o => !o.isAnalog).length ? "Показать еще" : undefined}
partsIndexPowered={!!partsIndexImage} partsIndexPowered={!!partsIndexImage}
hasStock={hasMainProductStock}
/> />
</> </>
); );
@ -795,9 +832,11 @@ export default function SearchResult() {
return true; return true;
}); });
return transformOffersForCard(filteredAnalogOffers); return transformOffersForCard(filteredAnalogOffers, checkProductStock(loadedAnalogData));
})() : []; })() : [];
const hasAnalogStock = loadedAnalogData ? checkProductStock(loadedAnalogData) : true;
return ( return (
<CoreProductCard <CoreProductCard
key={analogKey} key={analogKey}
@ -807,6 +846,7 @@ export default function SearchResult() {
offers={analogOffers} offers={analogOffers}
isAnalog isAnalog
isLoadingOffers={!loadedAnalogData} isLoadingOffers={!loadedAnalogData}
hasStock={hasAnalogStock}
/> />
) )
})} })}

View File

@ -379,3 +379,40 @@ button,
} }
} }
/* Стили для состояния "товар в корзине" */
.button-icon.in-cart {
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.button-icon.in-cart::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(156, 163, 175, 0.2);
pointer-events: none;
border-radius: inherit;
}
.button-icon.in-cart:hover {
opacity: 0.8 !important;
background-color: #6b7280 !important;
transform: scale(0.98);
}
/* Анимация для добавления в корзину */
.button-icon:active {
transform: scale(0.95);
transition: transform 0.1s ease;
}
/* Убеждаемся, что иконка корзины видна в сером состоянии */
.button-icon.in-cart .image-11,
.button-icon.in-cart svg {
filter: brightness(0.7) contrast(1.2);
}

View File

@ -51,8 +51,14 @@
} }
.flex-block-39-copy {
max-width: 300px;
min-width: 0;
}
.flex-block-40 { .flex-block-40 {
background-color: #fff;
padding-top: 10px; padding-top: 10px;
} }
@ -266,7 +272,18 @@ header.section-4 {
display: none; /* Chrome, Safari, Opera */ display: none; /* Chrome, Safari, Opera */
} }
.heading-2 {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
min-width: 0;
}
.dropdown-toggle.w-dropdown-toggle {
min-width: 0;
}
.mobile-category-overlay { .mobile-category-overlay {
position: fixed; position: fixed;
@ -348,6 +365,12 @@ input.input-receiver:focus {
box-shadow: none; box-shadow: none;
} }
.flex-block-122 {
width: 100% !important;
}
.button-icon.w-inline-block {
margin-left: auto;
}
.text-block-10 { .text-block-10 {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
@ -403,6 +426,25 @@ input.input-receiver:focus {
width: 100% !important; width: 100% !important;
} }
.text-block-7 {
border-radius: var(--_round---small-8);
background-color: var(--green);
color: var(--_fonts---color--white);
padding: 5px;
font-weight: 600;
position: relative;
top: -35px;
height: 30px;
}
.div-block-3 {
grid-column-gap: 5px;
grid-row-gap: 5px;
flex-flow: column;
align-self: auto;
margin-top: -30px;
display: flex;
}
.sort-item.active { .sort-item.active {
color: #111; color: #111;
font-weight: 700; font-weight: 700;
@ -491,20 +533,20 @@ input#VinSearchInput {
line-height: 1.4em; line-height: 1.4em;
} }
.heading-9-copy, .text-block-21-copy,
.text-block-21-copy { .heading-9-copy {
width: 250px; width: 250px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.heading-9-copy { /* .heading-9-copy {
text-align: right; text-align: right;
margin-left: auto; margin-left: auto;
display: block; display: block;
} } */
.pcs-search { .pcs-search {
color: var(--_fonts---color--black); color: var(--_fonts---color--black);
font-size: var(--_fonts---font-size--core); font-size: var(--_fonts---font-size--core);
@ -514,11 +556,11 @@ input#VinSearchInput {
@media (max-width: 767px) { @media (max-width: 767px) {
.heading-9-copy { /* .heading-9-copy {
text-align: left; text-align: left;
display: block; display: block;
} } */
.w-layout-hflex.flex-block-6 { .w-layout-hflex.flex-block-6 {
flex-direction: column !important; flex-direction: column !important;
@ -757,7 +799,7 @@ a.link-block-2.w-inline-block {
} }
@media screen and (max-width: 991px) { @media screen and (max-width: 991px) {
.flex-block-108, .flex-block-14-copy-copy { .flex-block-108 {
flex-flow: column; flex-flow: column;
justify-content: space-between; justify-content: space-between;
@ -939,15 +981,15 @@ a.link-block-2.w-inline-block {
max-width: 100%; max-width: 100%;
} }
.heading-9-copy {
min-width: 100px;
.flex-block-36 {
width: 100%;
} }
.flex-block-15-copy { .flex-block-15-copy {
width: 232px!important; width: 240px!important;
min-width: 232px!important; height: 315px;
min-width: 240px!important;
} }
.nameitembp { .nameitembp {
@ -961,8 +1003,8 @@ a.link-block-2.w-inline-block {
.flex-block-15-copy { .flex-block-15-copy {
grid-column-gap: 5px; grid-column-gap: 5px;
grid-row-gap: 5px; grid-row-gap: 5px;
width: 160px !important; width: 210px !important;
min-width: 160px !important; min-width: 210px !important;
padding: 15px; padding: 15px;
} }
.div-block-3 { .div-block-3 {
@ -980,8 +1022,8 @@ a.link-block-2.w-inline-block {
.flex-block-15-copy { .flex-block-15-copy {
grid-column-gap: 5px; grid-column-gap: 5px;
grid-row-gap: 5px; grid-row-gap: 5px;
width: 160px !important; width: 180px !important;
min-width: 160px !important; min-width: 180px !important;
padding: 15px; padding: 15px;
} }
.nameitembp { .nameitembp {
@ -1194,3 +1236,96 @@ a.link-block-2.w-inline-block {
box-shadow: 0 8px 32px rgba(44,62,80,0.10), 0 1.5px 4px rgba(44,62,80,0.08) !important; box-shadow: 0 8px 32px rgba(44,62,80,0.10), 0 1.5px 4px rgba(44,62,80,0.08) !important;
} }
} }
.pricecartbp {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px; /* или другой нужный вам отступ */
}
.bestpriceitem {
height: 279px;
}
.flex-block-49 {
gap: 15px;
}
.pcs-search-s1,
.sort-item.first {
width: 100px;
}
@media (max-width: 991px) {
.pcs-search-s1,
.sort-item.first {
width: 60px;
}
}
@media (max-width: 479px) {
.pcs-search-s1,
.sort-item.first {
width: 50px;
}
}
.w-layout-vflex.flex-block-36 {
display: flex;
flex-wrap: wrap;
gap: 24px;
justify-content: flex-start;
align-items: stretch;
}
.w-layout-vflex.flex-block-44 {
flex: 1 1 calc(33.333% - 16px);
max-width: calc(33.333% - 16px);
min-width: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
@media (max-width: 991px) {
.w-layout-vflex.flex-block-44 {
flex: 1 1 calc(50% - 12px);
max-width: calc(50% - 12px);
}
}
@media (max-width: 600px) {
.w-layout-vflex.flex-block-44 {
flex: 1 1 100%;
max-width: 100%;
}
}
@media (max-width: 767px) {
.w-layout-vflex.flex-block-36 {
flex-wrap: nowrap;
overflow-x: auto;
gap: 12px;
-webkit-overflow-scrolling: touch;
}
.w-layout-vflex.flex-block-44 {
min-width: 160px;
max-width: 160px;
flex: 0 0 160px;
}
/* .heading-9-copy {
text-align: left !important;
margin-left: 0 !important;
} */
}
@media (max-width: 1200px) {
.pcs-cart-s1 {
display: none !important;
}
}
@media (max-width: 767px) {
.filters-desktop {
display: none !important;
}
}

View File

@ -1539,7 +1539,7 @@ body {
grid-row-gap: 5px; grid-row-gap: 5px;
flex-flow: column; flex-flow: column;
align-self: auto; align-self: auto;
margin-top: -30px; /* margin-top: -30px; */
display: flex; display: flex;
} }

View File

@ -66,4 +66,5 @@ export interface CartContextType {
updateDelivery: (delivery: Partial<DeliveryInfo>) => void updateDelivery: (delivery: Partial<DeliveryInfo>) => void
clearCart: () => void clearCart: () => void
clearError: () => void clearError: () => void
isInCart: (productId?: string, offerKey?: string, article?: string, brand?: string) => boolean
} }

View File

@ -5,6 +5,7 @@ FRONTEND_PORT=3000
NEXT_PUBLIC_CMS_GRAPHQL_URL=https://cms.protekauto.ru/api/graphql NEXT_PUBLIC_CMS_GRAPHQL_URL=https://cms.protekauto.ru/api/graphql
NEXT_PUBLIC_UPLOAD_URL=https://cms.protekauto.ru/upload NEXT_PUBLIC_UPLOAD_URL=https://cms.protekauto.ru/upload
NEXT_PUBLIC_MAINTENANCE_MODE=true NEXT_PUBLIC_MAINTENANCE_MODE=true
NEXT_PUBLIC_PARTSAPI_URL=https://api.parts-index.com
# Build Configuration # Build Configuration
NODE_ENV=production NODE_ENV=production

View File

@ -9,7 +9,7 @@ async function testPartsIndexAPI() {
// Получаем каталоги // Получаем каталоги
console.log('\n📦 Получаем список каталогов...'); console.log('\n📦 Получаем список каталогов...');
const catalogsResponse = await fetch('https://api.parts-index.com/v1/catalogs?lang=ru', { const catalogsResponse = await fetch(process.env.PARTSAPI_URL+"/v1/catalogs?lang=ru", {
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
}, },
@ -31,7 +31,7 @@ async function testPartsIndexAPI() {
console.log(`\n🎯 Получаем группы для каталога "${firstCatalog.name}"...`); console.log(`\n🎯 Получаем группы для каталога "${firstCatalog.name}"...`);
const groupsResponse = await fetch( const groupsResponse = await fetch(
`https://api.parts-index.com/v1/catalogs/${firstCatalog.id}/groups?lang=ru`, `${process.env.PARTSAPI_URL}/v1/catalogs/${firstCatalog.id}/groups?lang=ru`,
{ {
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',

21
user_input.py Normal file
View File

@ -0,0 +1,21 @@
def main():
while True:
print("\n" + "="*50)
user_input = input("Please provide feedback or next task (type 'stop' to exit): ").strip()
if user_input.lower() == 'stop':
print("Exiting task loop. Thank you!")
break
elif user_input.lower() == '':
print("Please provide some input or type 'stop' to exit.")
continue
else:
print(f"\nReceived input: {user_input}")
print("Processing your request...")
# Here the main process would handle the user's input
return user_input
if __name__ == "__main__":
result = main()
if result and result.lower() != 'stop':
print(f"Next task received: {result}")