Compare commits
4 Commits
1da9c6ac09
...
bag3007
Author | SHA1 | Date | |
---|---|---|---|
95e6b33b56 | |||
a8f783767f | |||
b363b88e33 | |||
72a9772934 |
@ -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,7 +27,8 @@ 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}
|
||||||
|
|
||||||
|
@ -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">
|
||||||
<button
|
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||||||
type="button"
|
<button
|
||||||
onClick={handleAddToCart}
|
type="button"
|
||||||
className="button-icon w-inline-block"
|
onClick={handleAddToCart}
|
||||||
style={{ cursor: 'pointer', textDecoration: 'none' }}
|
className={`button-icon w-inline-block ${inCart ? 'in-cart' : ''}`}
|
||||||
aria-label="Добавить в корзину"
|
style={{
|
||||||
>
|
cursor: 'pointer',
|
||||||
<div className="div-block-26">
|
textDecoration: 'none',
|
||||||
<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>
|
opacity: inCart ? 0.5 : 1,
|
||||||
</div>
|
backgroundColor: inCart ? '#9ca3af' : undefined
|
||||||
</button>
|
}}
|
||||||
|
aria-label={inCart ? "Товар уже в корзине" : "Добавить в корзину"}
|
||||||
|
title={inCart ? "Товар уже в корзине - нажмите для добавления еще" : "Добавить в корзину"}
|
||||||
|
>
|
||||||
|
<div className="div-block-26">
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
@ -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">
|
||||||
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
@ -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
|
||||||
position: 'fixed',
|
style={{
|
||||||
top: '10px',
|
position: 'fixed',
|
||||||
right: '10px',
|
top: '10px',
|
||||||
background: 'white',
|
right: '10px',
|
||||||
border: '1px solid #ccc',
|
background: 'rgba(0,0,0,0.9)',
|
||||||
padding: '10px',
|
color: 'white',
|
||||||
borderRadius: '5px',
|
padding: '10px',
|
||||||
maxWidth: '300px',
|
borderRadius: '5px',
|
||||||
fontSize: '12px',
|
fontSize: '11px',
|
||||||
zIndex: 9999
|
maxWidth: '350px',
|
||||||
}}>
|
zIndex: 9999,
|
||||||
<h4>Cart Debug</h4>
|
maxHeight: '400px',
|
||||||
<button onClick={addTestItem} style={{ marginBottom: '5px', marginRight: '5px' }}>
|
overflow: 'auto'
|
||||||
Добавить товар
|
}}
|
||||||
</button>
|
>
|
||||||
<button onClick={clearCart} style={{ marginBottom: '5px', marginRight: '5px' }}>
|
<div style={{ fontWeight: 'bold', marginBottom: '5px' }}>🛒 Cart Debug: {state.items.length} items</div>
|
||||||
Очистить корзину
|
{testItem && (
|
||||||
</button>
|
<div style={{ background: 'rgba(255,255,255,0.1)', padding: '5px', marginBottom: '5px', fontSize: '10px' }}>
|
||||||
<button onClick={clearStorage} style={{ marginBottom: '10px' }}>
|
<div>Testing isInCart for first item:</div>
|
||||||
Очистить localStorage
|
<div>Brand: {testItem.brand}, Article: {testItem.article}</div>
|
||||||
</button>
|
<div>Result: {testResult ? '✅ Found' : '❌ Not found'}</div>
|
||||||
<div>
|
</div>
|
||||||
<strong>Товаров в корзине:</strong> {state.items.length}
|
)}
|
||||||
</div>
|
{state.items.slice(0, 6).map((item, idx) => (
|
||||||
<pre style={{ fontSize: '10px', maxHeight: '200px', overflow: 'auto' }}>
|
<div key={idx} style={{ fontSize: '9px', marginTop: '3px', borderBottom: '1px solid rgba(255,255,255,0.2)', paddingBottom: '2px' }}>
|
||||||
{JSON.stringify(debugInfo, null, 2)}
|
{item.brand} {item.article}
|
||||||
</pre>
|
{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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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,53 +75,54 @@ 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
|
<div className="w-layout-vflex flex-block-15-copy" data-article-card="visible" itemScope itemType="https://schema.org/Product">
|
||||||
className="w-layout-vflex flex-block-15-copy"
|
<div
|
||||||
data-article-card="visible"
|
className={`favcardcat${isItemFavorite ? ' favorite-active' : ''}`}
|
||||||
itemScope
|
onClick={handleFavoriteClick}
|
||||||
itemType="https://schema.org/Product"
|
style={{ cursor: 'pointer', color: isItemFavorite ? '#ff4444' : '#ccc' }}
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`favcardcat ${isItemFavorite ? 'favorite-active' : ''}`}
|
|
||||||
onClick={handleFavoriteClick}
|
|
||||||
style={{
|
|
||||||
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">
|
||||||
<path d="M13.5996 3.5C15.8107 3.5 17.5 5.1376 17.5 7.19629C17.5 8.46211 16.9057 9.65758 15.7451 11.0117C14.8712 12.0314 13.7092 13.1034 12.3096 14.3311L10.833 15.6143L10.832 15.6152L10 16.3369L9.16797 15.6152L9.16699 15.6143L7.69043 14.3311C6.29084 13.1034 5.12883 12.0314 4.25488 11.0117C3.09428 9.65758 2.50003 8.46211 2.5 7.19629C2.5 5.1376 4.18931 3.5 6.40039 3.5C7.6497 3.50012 8.85029 4.05779 9.62793 4.92188L10 5.33398L10.3721 4.92188C11.1497 4.05779 12.3503 3.50012 13.5996 3.5Z" fill="currentColor" ></path>
|
<path d="M13.5996 3.5C15.8107 3.5 17.5 5.1376 17.5 7.19629C17.5 8.46211 16.9057 9.65758 15.7451 11.0117C14.8712 12.0314 13.7092 13.1034 12.3096 14.3311L10.833 15.6143L10.832 15.6152L10 16.3369L9.16797 15.6152L9.16699 15.6143L7.69043 14.3311C6.29084 13.1034 5.12883 12.0314 4.25488 11.0117C3.09428 9.65758 2.50003 8.46211 2.5 7.19629C2.5 5.1376 4.18931 3.5 6.40039 3.5C7.6497 3.50012 8.85029 4.05779 9.62793 4.92188L10 5.33398L10.3721 4.92188C11.1497 4.05779 12.3503 3.50012 13.5996 3.5Z" fill="currentColor" ></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="div-block-4">
|
||||||
{/* Делаем картинку и контент кликабельными для перехода на card */}
|
<img
|
||||||
<Link href={cardUrl} className="div-block-4" style={{ textDecoration: 'none', color: 'inherit' }}>
|
src={displayImage}
|
||||||
<img
|
loading="lazy"
|
||||||
src={displayImage}
|
width="Auto"
|
||||||
loading="lazy"
|
height="Auto"
|
||||||
width="Auto"
|
alt={title}
|
||||||
height="Auto"
|
|
||||||
alt={title}
|
|
||||||
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="text-block-10" itemProp="name">{title}</div>
|
<div className="w-layout-hflex flex-block-122">
|
||||||
<div className="text-block-11" itemProp="brand" itemScope itemType="https://schema.org/Brand">
|
<div className="w-layout-vflex">
|
||||||
<span itemProp="name">{brand}</span>
|
<div className="text-block-10" itemProp="name">{title}</div>
|
||||||
|
<div className="text-block-11" itemProp="brand" itemScope itemType="https://schema.org/Brand">
|
||||||
|
<span itemProp="name">{brand}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="button-icon w-inline-block"
|
||||||
|
onClick={handleBuyClick}
|
||||||
|
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">
|
||||||
|
<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>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<meta itemProp="sku" content={articleNumber || ''} />
|
<meta itemProp="sku" content={articleNumber || ''} />
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Обновляем кнопку купить */}
|
|
||||||
<div className="catc w-inline-block" onClick={handleBuyClick} style={{ cursor: 'pointer' }}>
|
|
||||||
<div className="div-block-25">
|
|
||||||
<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"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-block-6">Купить</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -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">
|
||||||
|
|
||||||
@ -315,6 +341,19 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
<div className="flex flex-row flex-nowrap items-center gap-2">
|
<div className="flex flex-row flex-nowrap items-center gap-2">
|
||||||
<h3 className="heading-10 name" style={{marginRight: 8}}>{brand}</h3>
|
<h3 className="heading-10 name" style={{marginRight: 8}}>{brand}</h3>
|
||||||
<h3 className="heading-10" style={{marginRight: 8}}>{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}
|
||||||
@ -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>
|
||||||
<button
|
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||||||
type="button"
|
<button
|
||||||
onClick={() => handleAddToCart(offer, idx)}
|
type="button"
|
||||||
className="button-icon w-inline-block"
|
onClick={() => handleAddToCart(offer, idx)}
|
||||||
style={{ cursor: 'pointer' }}
|
className={`button-icon w-inline-block ${inCart || isLocallyInCart ? 'in-cart' : ''}`}
|
||||||
aria-label="Добавить в корзину"
|
style={{
|
||||||
>
|
cursor: 'pointer',
|
||||||
<div className="div-block-26">
|
opacity: inCart || isLocallyInCart ? 0.5 : 1,
|
||||||
<img loading="lazy" src="/images/cart_icon.svg" alt="В корзину" className="image-11" />
|
backgroundColor: inCart || isLocallyInCart ? '#2563eb' : undefined
|
||||||
</div>
|
}}
|
||||||
</button>
|
aria-label={inCart || isLocallyInCart ? "Товар уже в корзине" : "Добавить в корзину"}
|
||||||
|
title={inCart || isLocallyInCart ? "Товар уже в корзине - нажмите для добавления еще" : "Добавить в корзину"}
|
||||||
|
>
|
||||||
|
<div className="div-block-26">
|
||||||
|
<img
|
||||||
|
loading="lazy"
|
||||||
|
src="/images/cart_icon.svg"
|
||||||
|
alt={inCart || isLocallyInCart ? "В корзине" : "В корзину"}
|
||||||
|
className="image-11"
|
||||||
|
style={{
|
||||||
|
filter: inCart || isLocallyInCart ? 'brightness(0.7)' : undefined
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
@ -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' }}
|
||||||
>
|
>
|
||||||
|
@ -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">
|
||||||
|
@ -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'
|
||||||
}}
|
}}
|
||||||
|
@ -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 = () => {
|
||||||
@ -212,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
|
||||||
@ -222,6 +225,7 @@ const TopSalesSection: React.FC = () => {
|
|||||||
brand={brand}
|
brand={brand}
|
||||||
article={product.article}
|
article={product.article}
|
||||||
productId={product.id}
|
productId={product.id}
|
||||||
|
isInCart={isInCart}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
items: backendItems,
|
||||||
|
summary,
|
||||||
|
isLoading: false
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
items: [],
|
||||||
|
summary: calculateSummary([]),
|
||||||
|
isLoading: false
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}, [cartData])
|
||||||
|
|
||||||
console.log('🔄 Загружаем состояние корзины из localStorage...')
|
// Set loading state
|
||||||
|
useEffect(() => {
|
||||||
const savedCartState = localStorage.getItem('cartState')
|
setState(prev => ({
|
||||||
if (savedCartState) {
|
...prev,
|
||||||
try {
|
isLoading: cartLoading
|
||||||
const cartState = JSON.parse(savedCartState)
|
}))
|
||||||
console.log('✅ Найдено сохраненное состояние корзины:', cartState)
|
}, [cartLoading])
|
||||||
// Загружаем полное состояние корзины
|
|
||||||
dispatch({ type: 'LOAD_FULL_STATE', payload: cartState })
|
// GraphQL-based cart operations
|
||||||
} catch (error) {
|
const addItem = async (item: Omit<CartItem, 'id' | 'selected' | 'favorite'>) => {
|
||||||
console.error('❌ Ошибка загрузки корзины из localStorage:', error)
|
try {
|
||||||
// Попытаемся загрузить старый формат (только товары)
|
setError('')
|
||||||
const savedCart = localStorage.getItem('cart')
|
setState(prev => ({ ...prev, isLoading: true }))
|
||||||
if (savedCart) {
|
|
||||||
try {
|
console.log('🛒 Adding item to backend cart:', item)
|
||||||
const cartItems = JSON.parse(savedCart)
|
|
||||||
console.log('✅ Найдены товары в старом формате:', cartItems)
|
const { data } = await addToCartMutation({
|
||||||
dispatch({ type: 'LOAD_CART', payload: cartItems })
|
variables: {
|
||||||
} catch (error) {
|
input: {
|
||||||
console.error('❌ Ошибка загрузки старой корзины:', error)
|
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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Refetch to ensure data consistency
|
||||||
|
refetchCart()
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
} else {
|
||||||
|
const errorMessage = data?.addToCart?.error || 'Ошибка добавления товара'
|
||||||
|
setError(errorMessage)
|
||||||
|
setState(prev => ({ ...prev, isLoading: false }))
|
||||||
|
toast.error(errorMessage)
|
||||||
|
return { success: false, error: errorMessage }
|
||||||
}
|
}
|
||||||
} else {
|
} catch (error) {
|
||||||
console.log('ℹ️ Сохраненное состояние корзины не найдено')
|
console.error('❌ Error adding item to cart:', error)
|
||||||
|
const errorMessage = 'Ошибка добавления товара в корзину'
|
||||||
|
setError(errorMessage)
|
||||||
|
setState(prev => ({ ...prev, isLoading: false }))
|
||||||
|
toast.error(errorMessage)
|
||||||
|
return { success: false, error: errorMessage }
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsInitialized(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Сохранение полного состояния корзины в localStorage при изменении (только после инициализации)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isInitialized || typeof window === 'undefined') return
|
|
||||||
|
|
||||||
const stateToSave = {
|
|
||||||
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 existingItemIndex = state.items.findIndex(
|
|
||||||
existingItem =>
|
|
||||||
(existingItem.productId && existingItem.productId === item.productId) ||
|
|
||||||
(existingItem.offerKey && existingItem.offerKey === item.offerKey)
|
|
||||||
)
|
|
||||||
|
|
||||||
let totalQuantity = item.quantity;
|
|
||||||
if (existingItemIndex >= 0) {
|
|
||||||
const existingItem = state.items[existingItemIndex];
|
|
||||||
totalQuantity = existingItem.quantity + item.quantity;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем наличие товара на складе
|
|
||||||
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 })
|
|
||||||
return { success: true }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeItem = (id: string) => {
|
const removeItem = async (id: string) => {
|
||||||
dispatch({ type: 'REMOVE_ITEM', payload: id })
|
try {
|
||||||
}
|
setError('')
|
||||||
|
setState(prev => ({ ...prev, isLoading: true }))
|
||||||
|
|
||||||
const updateQuantity = (id: string, quantity: number) => {
|
console.log('🗑️ Removing item from backend cart:', id)
|
||||||
// Найдем товар для проверки наличия
|
|
||||||
const item = state.items.find(item => item.id === id);
|
const { data } = await removeFromCartMutation({
|
||||||
if (item) {
|
variables: { itemId: id }
|
||||||
const availableStock = parseStock(item.stock);
|
})
|
||||||
if (availableStock > 0 && quantity > availableStock) {
|
|
||||||
// Показываем ошибку, но не изменяем количество
|
if (data?.removeFromCart?.success) {
|
||||||
dispatch({ type: 'SET_ERROR', payload: `Недостаточно товара в наличии. Доступно: ${availableStock} шт.` });
|
// Update local state
|
||||||
return;
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>) => {
|
|
||||||
dispatch({ type: 'UPDATE_DELIVERY', payload: delivery })
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearCart = () => {
|
|
||||||
dispatch({ type: 'CLEAR_CART' })
|
|
||||||
// Очищаем localStorage при очистке корзины
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
localStorage.removeItem('cartState')
|
|
||||||
localStorage.removeItem('cart')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateDelivery = (delivery: Partial<DeliveryInfo>) => {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
delivery: { ...prev.delivery, ...delivery }
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
@ -1,8 +1,14 @@
|
|||||||
import { PartsIndexCatalogsResponse, PartsIndexGroup, PartsIndexEntityInfoResponse } from '@/types/partsindex';
|
import { PartsIndexCatalogsResponse, PartsIndexGroup, PartsIndexEntityInfoResponse } from '@/types/partsindex';
|
||||||
|
|
||||||
const PARTS_INDEX_API_BASE = process.env.PARTSAPI_URL+"/v1" || 'https://api.parts-index.com/v1';
|
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 {
|
||||||
/**
|
/**
|
||||||
* Получить список каталогов
|
* Получить список каталогов
|
||||||
|
@ -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
|
||||||
|
@ -38,11 +38,11 @@ const mockData = Array(12).fill({
|
|||||||
brand: "Borsehung",
|
brand: "Borsehung",
|
||||||
});
|
});
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 50; // Уменьшено для быстрой загрузки и лучшего UX
|
|
||||||
const PARTSINDEX_PAGE_SIZE = 25; // Синхронизировано для оптимальной скорости
|
|
||||||
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 {
|
||||||
@ -336,12 +336,6 @@ export default function Catalog() {
|
|||||||
естьЕщеТовары: hasMoreEntities
|
естьЕщеТовары: hasMoreEntities
|
||||||
});
|
});
|
||||||
|
|
||||||
// Если у нас уже достаточно товаров, не загружаем
|
|
||||||
if (currentEntitiesCount >= 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 };
|
||||||
@ -407,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 фильтров
|
||||||
@ -418,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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -564,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)
|
||||||
});
|
});
|
||||||
@ -577,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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -933,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>;
|
||||||
}
|
}
|
||||||
@ -1007,7 +1017,7 @@ 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}
|
||||||
@ -1018,7 +1028,7 @@ export default function Catalog() {
|
|||||||
/>
|
/>
|
||||||
</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}
|
||||||
@ -1029,7 +1039,7 @@ export default function Catalog() {
|
|||||||
/>
|
/>
|
||||||
</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}
|
||||||
@ -1125,141 +1135,112 @@ export default function Catalog() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Отображение товаров PartsIndex */}
|
{/* Отображение товаров PartsIndex */}
|
||||||
{isPartsIndexMode && !isFilterChanging && (() => {
|
{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 })),
|
|
||||||
isFilterChanging
|
|
||||||
});
|
|
||||||
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 {
|
||||||
} else {
|
// Если нет данных о цене, показываем скелетон (товар должен загрузиться)
|
||||||
// Если нет данных о цене, показываем скелетон (товар должен загрузиться)
|
priceElement = <PriceSkeleton />;
|
||||||
priceElement = <PriceSkeleton />;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CatalogProductCard
|
<CatalogProductCard
|
||||||
key={`${entity.id}_${idx}`}
|
key={`${entity.id}_${idx}`}
|
||||||
title={entity.originalName || entity.name?.name || 'Товар без названия'}
|
title={entity.originalName || entity.name?.name || 'Товар без названия'}
|
||||||
brand={entity.brand.name}
|
brand={entity.brand.name}
|
||||||
articleNumber={entity.code}
|
articleNumber={entity.code}
|
||||||
brandName={entity.brand.name}
|
brandName={entity.brand.name}
|
||||||
image={entity.images?.[0] || ''}
|
image={entity.images?.[0] || ''}
|
||||||
price={priceElement ? "" : displayPrice}
|
price={priceElement ? "" : displayPrice}
|
||||||
priceElement={priceElement}
|
priceElement={priceElement}
|
||||||
oldPrice=""
|
oldPrice=""
|
||||||
discount=""
|
discount=""
|
||||||
currency={displayCurrency}
|
currency={displayCurrency}
|
||||||
productId={entity.id}
|
productId={entity.id}
|
||||||
artId={entity.id}
|
artId={entity.id}
|
||||||
offerKey={priceData?.offerKey}
|
offerKey={priceData?.offerKey}
|
||||||
onAddToCart={async () => {
|
isInCart={priceData?.isInCart}
|
||||||
// Если цена не загружена, загружаем её и добавляем в корзину
|
onAddToCart={async () => {
|
||||||
if (!priceData && !isLoadingPriceData) {
|
// Если цена не загружена, загружаем её и добавляем в корзину
|
||||||
ensurePriceLoaded(productForPrice);
|
if (!priceData && !isLoadingPriceData) {
|
||||||
console.log('🔄 Загружаем цену для:', entity.code, entity.brand.name);
|
ensurePriceLoaded(productForPrice);
|
||||||
return;
|
console.log('🔄 Загружаем цену для:', entity.code, entity.brand.name);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Если цена есть, добавляем в корзину
|
// Если цена есть, добавляем в корзину
|
||||||
if (priceData && priceData.price) {
|
if (priceData && priceData.price) {
|
||||||
const itemToAdd = {
|
const itemToAdd = {
|
||||||
productId: entity.id,
|
productId: entity.id,
|
||||||
offerKey: priceData.offerKey,
|
offerKey: priceData.offerKey,
|
||||||
name: entity.originalName || entity.name?.name || 'Товар без названия',
|
name: entity.originalName || entity.name?.name || 'Товар без названия',
|
||||||
description: `${entity.brand.name} ${entity.code}`,
|
description: `${entity.brand.name} ${entity.code}`,
|
||||||
brand: entity.brand.name,
|
brand: entity.brand.name,
|
||||||
article: entity.code,
|
article: entity.code,
|
||||||
price: priceData.price,
|
price: priceData.price,
|
||||||
currency: priceData.currency || 'RUB',
|
currency: priceData.currency || 'RUB',
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
stock: undefined, // информация о наличии не доступна для PartsIndex
|
stock: undefined, // информация о наличии не доступна для PartsIndex
|
||||||
deliveryTime: '1-3 дня',
|
deliveryTime: '1-3 дня',
|
||||||
warehouse: 'Parts Index',
|
warehouse: 'Parts Index',
|
||||||
supplier: 'Parts Index',
|
supplier: 'Parts Index',
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
image: entity.images?.[0] || '',
|
image: entity.images?.[0] || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await addItem(itemToAdd);
|
const result = await addItem(itemToAdd);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Показываем уведомление
|
// Показываем уведомление
|
||||||
toast.success(
|
toast.success(
|
||||||
<div>
|
<div>
|
||||||
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
|
<div className="font-semibold" style={{ color: '#fff' }}>Товар добавлен в корзину!</div>
|
||||||
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${entity.brand.name} ${entity.code} за ${priceData.price.toLocaleString('ru-RU')} ₽`}</div>
|
<div className="text-sm" style={{ color: '#fff', opacity: 0.9 }}>{`${entity.brand.name} ${entity.code} за ${priceData.price.toLocaleString('ru-RU')} ₽`}</div>
|
||||||
</div>,
|
</div>,
|
||||||
{
|
{
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
icon: <CartIcon size={20} color="#fff" />,
|
icon: <CartIcon size={20} color="#fff" />,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
toast.error(result.error || 'Ошибка при добавлении товара в корзину');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
toast.error('Цена товара еще загружается. Попробуйте снова через несколько секунд.');
|
toast.error(result.error || 'Ошибка при добавлении товара в корзину');
|
||||||
}
|
}
|
||||||
}}
|
} else {
|
||||||
/>
|
toast.error('Цена товара еще загружается. Попробуйте снова через несколько секунд.');
|
||||||
);
|
}
|
||||||
})}
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Пагинация для PartsIndex */}
|
{/* Кнопка "Показать еще" */}
|
||||||
<div className="w-layout-hflex pagination">
|
{visibleCount < accumulatedEntities.length && (
|
||||||
<button
|
<div className="w-layout-hflex pagination">
|
||||||
onClick={() => {
|
<button
|
||||||
console.log('🖱️ Клик по кнопке "Назад"');
|
onClick={() => setVisibleCount(c => Math.min(c + ITEMS_PER_PAGE, accumulatedEntities.length))}
|
||||||
handlePrevPage();
|
className="button_strock w-button"
|
||||||
}}
|
>
|
||||||
disabled={currentUserPage <= 1}
|
Показать еще
|
||||||
className="button_strock w-button mr-2"
|
</button>
|
||||||
>
|
</div>
|
||||||
← Назад
|
)}
|
||||||
</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>
|
|
||||||
</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>
|
||||||
@ -1285,7 +1266,7 @@ export default function Catalog() {
|
|||||||
{isAutoLoading ? 'Загружаем...' : 'Загрузить еще'}
|
{isAutoLoading ? 'Загружаем...' : 'Загрузить еще'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -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,19 +533,20 @@ input#VinSearchInput {
|
|||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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);
|
||||||
@ -513,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;
|
||||||
@ -756,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;
|
||||||
|
|
||||||
@ -944,8 +987,9 @@ a.link-block-2.w-inline-block {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
||||||
@ -959,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 {
|
||||||
@ -978,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 {
|
||||||
@ -1268,8 +1312,20 @@ a.link-block-2.w-inline-block {
|
|||||||
max-width: 160px;
|
max-width: 160px;
|
||||||
flex: 0 0 160px;
|
flex: 0 0 160px;
|
||||||
}
|
}
|
||||||
.heading-9-copy {
|
/* .heading-9-copy {
|
||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.pcs-cart-s1 {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.filters-desktop {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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
|
||||||
|
Reference in New Issue
Block a user