переделаны счетчки фильтр рэндж, настроены выборы категорий и подкатегорий
This commit is contained in:
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useCart } from "@/contexts/CartContext";
|
import { useCart } from "@/contexts/CartContext";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
@ -27,6 +27,11 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
|
|||||||
const parsedStock = parseInt(stock.replace(/[^\d]/g, ""), 10);
|
const parsedStock = parseInt(stock.replace(/[^\d]/g, ""), 10);
|
||||||
const maxCount = isNaN(parsedStock) ? undefined : parsedStock;
|
const maxCount = isNaN(parsedStock) ? undefined : parsedStock;
|
||||||
const [count, setCount] = useState(1);
|
const [count, setCount] = useState(1);
|
||||||
|
const [inputValue, setInputValue] = useState("1");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputValue(count.toString());
|
||||||
|
}, [count]);
|
||||||
|
|
||||||
const handleMinus = () => setCount(prev => Math.max(1, prev - 1));
|
const handleMinus = () => setCount(prev => Math.max(1, prev - 1));
|
||||||
const handlePlus = () => {
|
const handlePlus = () => {
|
||||||
@ -38,7 +43,13 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
let value = parseInt(e.target.value, 10);
|
const val = e.target.value;
|
||||||
|
setInputValue(val);
|
||||||
|
if (val === "") {
|
||||||
|
// Не обновляем count, пока не будет blur
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let value = parseInt(val, 10);
|
||||||
if (isNaN(value) || value < 1) value = 1;
|
if (isNaN(value) || value < 1) value = 1;
|
||||||
if (maxCount !== undefined && value > maxCount) {
|
if (maxCount !== undefined && value > maxCount) {
|
||||||
toast.error(`Максимум ${maxCount} шт.`);
|
toast.error(`Максимум ${maxCount} шт.`);
|
||||||
@ -47,6 +58,13 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
|
|||||||
setCount(value);
|
setCount(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInputBlur = () => {
|
||||||
|
if (inputValue === "") {
|
||||||
|
setInputValue("1");
|
||||||
|
setCount(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Функция для парсинга цены из строки
|
// Функция для парсинга цены из строки
|
||||||
const parsePrice = (priceStr: string): number => {
|
const parsePrice = (priceStr: string): number => {
|
||||||
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
|
const cleanPrice = priceStr.replace(/[^\d.,]/g, '').replace(',', '.');
|
||||||
@ -144,8 +162,9 @@ const BestPriceCard: React.FC<BestPriceCardProps> = ({
|
|||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={maxCount}
|
max={maxCount}
|
||||||
value={count}
|
value={inputValue}
|
||||||
onChange={handleInput}
|
onChange={handleInput}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
className="text-block-26 w-full text-center outline-none"
|
className="text-block-26 w-full text-center outline-none"
|
||||||
aria-label="Количество"
|
aria-label="Количество"
|
||||||
/>
|
/>
|
||||||
|
@ -38,160 +38,180 @@ const CartItem: React.FC<CartItemProps> = ({
|
|||||||
onRemove,
|
onRemove,
|
||||||
isSummaryStep = false,
|
isSummaryStep = false,
|
||||||
itemNumber,
|
itemNumber,
|
||||||
}) => (
|
}) => {
|
||||||
<div className="w-layout-hflex cart-item">
|
// --- Фикс для input: можно стереть, при blur пустое = 1 ---
|
||||||
<div className="w-layout-hflex info-block-search-copy">
|
const [inputValue, setInputValue] = React.useState(count.toString());
|
||||||
{isSummaryStep ? (
|
React.useEffect(() => {
|
||||||
<div style={{ marginRight: 12, minWidth: 24, textAlign: 'center', fontWeight: 600, fontSize: 14 }}>{itemNumber}</div>
|
setInputValue(count.toString());
|
||||||
) : (
|
}, [count]);
|
||||||
<div
|
|
||||||
className={"div-block-7" + (selected ? " active" : "")}
|
return (
|
||||||
onClick={onSelect}
|
<div className="w-layout-hflex cart-item">
|
||||||
style={{ marginRight: 12, cursor: 'pointer' }}
|
<div className="w-layout-hflex info-block-search-copy">
|
||||||
>
|
|
||||||
{selected && (
|
|
||||||
<svg width="14" height="10" viewBox="0 0 14 10" fill="none">
|
|
||||||
<path d="M2 5.5L6 9L12 2" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="w-layout-hflex block-name">
|
|
||||||
<h4 className="heading-9-copy">{name}</h4>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
"text-block-21-copy" +
|
|
||||||
(isSummaryStep && itemNumber === 1 ? " border-t-0" : "")
|
|
||||||
}
|
|
||||||
style={
|
|
||||||
isSummaryStep && itemNumber === 1
|
|
||||||
? { borderTop: 'none' }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="form-block-copy w-form">
|
|
||||||
<form className="form-copy" onSubmit={e => e.preventDefault()}>
|
|
||||||
<input
|
|
||||||
className="text-field-copy w-input"
|
|
||||||
maxLength={256}
|
|
||||||
name="Search-5"
|
|
||||||
data-name="Search 5"
|
|
||||||
placeholder="Комментарий"
|
|
||||||
type="text"
|
|
||||||
id="Search-5"
|
|
||||||
value={comment}
|
|
||||||
onChange={e => onComment(e.target.value)}
|
|
||||||
disabled={isSummaryStep}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
<div className="success-message w-form-done">
|
|
||||||
<div>Thank you! Your submission has been received!</div>
|
|
||||||
</div>
|
|
||||||
<div className="error-message w-form-fail">
|
|
||||||
<div>Oops! Something went wrong while submitting the form.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex add-to-cart-block">
|
|
||||||
<div className="w-layout-hflex flex-block-39-copy">
|
|
||||||
<h4 className="delivery-cart-s1">{delivery}</h4>
|
|
||||||
<div className="text-block-21-copy-copy">{deliveryDate}</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-layout-hflex pcs-cart-s1">
|
|
||||||
{isSummaryStep ? (
|
{isSummaryStep ? (
|
||||||
<div className="text-block-26" style={{ fontWeight: 600, fontSize: 14 }}>{count} шт.</div>
|
<div style={{ marginRight: 12, minWidth: 24, textAlign: 'center', fontWeight: 600, fontSize: 14 }}>{itemNumber}</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div
|
||||||
<div
|
className={"div-block-7" + (selected ? " active" : "")}
|
||||||
className="minus-plus"
|
onClick={onSelect}
|
||||||
onClick={() => onCountChange && onCountChange(count - 1)}
|
style={{ marginRight: 12, cursor: 'pointer' }}
|
||||||
style={{ cursor: 'pointer' }}
|
>
|
||||||
aria-label="Уменьшить количество"
|
{selected && (
|
||||||
tabIndex={0}
|
<svg width="14" height="10" viewBox="0 0 14 10" fill="none">
|
||||||
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onCountChange && onCountChange(count - 1)}
|
<path d="M2 5.5L6 9L12 2" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
role="button"
|
</svg>
|
||||||
>
|
)}
|
||||||
<div className="pluspcs w-embed">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M6 10.5V9.5H14V10.5H6Z" fill="currentColor"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="input-pcs">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
value={count}
|
|
||||||
onChange={e => {
|
|
||||||
const value = Math.max(1, parseInt(e.target.value, 10) || 1);
|
|
||||||
onCountChange && onCountChange(value);
|
|
||||||
}}
|
|
||||||
className="text-block-26 w-full text-center outline-none"
|
|
||||||
aria-label="Количество"
|
|
||||||
style={{ width: 40 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="minus-plus"
|
|
||||||
onClick={() => onCountChange && onCountChange(count + 1)}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
aria-label="Увеличить количество"
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onCountChange && onCountChange(count + 1)}
|
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
<div className="pluspcs w-embed">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M6 10.5V9.5H14V10.5H6ZM9.5 6H10.5V14H9.5V6Z" fill="currentColor"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
<div className="w-layout-hflex block-name">
|
||||||
<div className="w-layout-hflex flex-block-39-copy-copy">
|
<h4 className="heading-9-copy">{name}</h4>
|
||||||
<h4 className="price-in-cart-s1">{price}</h4>
|
|
||||||
<div className="price-1-pcs-cart-s1">{pricePerItem}</div>
|
|
||||||
</div>
|
|
||||||
{!isSummaryStep && (
|
|
||||||
<div className="w-layout-hflex control-element">
|
|
||||||
<div className="favorite-icon w-embed" onClick={onFavorite} style={{ cursor: 'pointer', color: favorite ? '#e53935' : undefined }}>
|
|
||||||
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M9 16.5L7.84 15.4929C3.72 11.93 1 9.57248 1 6.69619C1 4.33869 2.936 2.5 5.4 2.5C6.792 2.5 8.128 3.11798 9 4.08692C9.872 3.11798 11.208 2.5 12.6 2.5C15.064 2.5 17 4.33869 17 6.69619C17 9.57248 14.28 11.93 10.16 15.4929L9 16.5Z" fill={favorite ? "#e53935" : "currentColor"} />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className="bdel"
|
className={
|
||||||
role="button"
|
"text-block-21-copy" +
|
||||||
tabIndex={0}
|
(isSummaryStep && itemNumber === 1 ? " border-t-0" : "")
|
||||||
aria-label="Удалить из корзины"
|
}
|
||||||
onClick={onRemove}
|
style={
|
||||||
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onRemove && onRemove()}
|
isSummaryStep && itemNumber === 1
|
||||||
style={{ display: 'inline-flex', cursor: 'pointer', transition: 'color 0.2s' }}
|
? { borderTop: 'none' }
|
||||||
onMouseEnter={e => {
|
: undefined
|
||||||
const path = e.currentTarget.querySelector('path');
|
}
|
||||||
if (path) path.setAttribute('fill', '#ec1c24');
|
|
||||||
}}
|
|
||||||
onMouseLeave={e => {
|
|
||||||
const path = e.currentTarget.querySelector('path');
|
|
||||||
if (path) path.setAttribute('fill', '#D0D0D0');
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-block-copy w-form">
|
||||||
|
<form className="form-copy" onSubmit={e => e.preventDefault()}>
|
||||||
|
<input
|
||||||
|
className="text-field-copy w-input"
|
||||||
|
maxLength={256}
|
||||||
|
name="Search-5"
|
||||||
|
data-name="Search 5"
|
||||||
|
placeholder="Комментарий"
|
||||||
|
type="text"
|
||||||
|
id="Search-5"
|
||||||
|
value={comment}
|
||||||
|
onChange={e => onComment(e.target.value)}
|
||||||
|
disabled={isSummaryStep}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<div className="success-message w-form-done">
|
||||||
|
<div>Thank you! Your submission has been received!</div>
|
||||||
|
</div>
|
||||||
|
<div className="error-message w-form-fail">
|
||||||
|
<div>Oops! Something went wrong while submitting the form.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-layout-hflex add-to-cart-block">
|
||||||
|
<div className="w-layout-hflex flex-block-39-copy">
|
||||||
|
<h4 className="delivery-cart-s1">{delivery}</h4>
|
||||||
|
<div className="text-block-21-copy-copy">{deliveryDate}</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-layout-hflex pcs-cart-s1">
|
||||||
|
{isSummaryStep ? (
|
||||||
|
<div className="text-block-26" style={{ fontWeight: 600, fontSize: 14 }}>{count} шт.</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="minus-plus"
|
||||||
|
onClick={() => onCountChange && onCountChange(count - 1)}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
aria-label="Уменьшить количество"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onCountChange && onCountChange(count - 1)}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div className="pluspcs w-embed">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 10.5V9.5H14V10.5H6Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="input-pcs">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={e => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setInputValue(val);
|
||||||
|
if (val === "") {
|
||||||
|
// Не обновляем count, пока не будет blur
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const valueNum = Math.max(1, parseInt(val, 10) || 1);
|
||||||
|
onCountChange && onCountChange(valueNum);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
if (inputValue === "") {
|
||||||
|
setInputValue("1");
|
||||||
|
onCountChange && onCountChange(1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-block-26 w-full text-center outline-none"
|
||||||
|
aria-label="Количество"
|
||||||
|
style={{ width: 40 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="minus-plus"
|
||||||
|
onClick={() => onCountChange && onCountChange(count + 1)}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
aria-label="Увеличить количество"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onCountChange && onCountChange(count + 1)}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<div className="pluspcs w-embed">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 10.5V9.5H14V10.5H6ZM9.5 6H10.5V14H9.5V6Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-layout-hflex flex-block-39-copy-copy">
|
||||||
|
<h4 className="price-in-cart-s1">{price}</h4>
|
||||||
|
<div className="price-1-pcs-cart-s1">{pricePerItem}</div>
|
||||||
|
</div>
|
||||||
|
{!isSummaryStep && (
|
||||||
|
<div className="w-layout-hflex control-element">
|
||||||
|
<div className="favorite-icon w-embed" onClick={onFavorite} style={{ cursor: 'pointer', color: favorite ? '#e53935' : undefined }}>
|
||||||
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path d="M9 16.5L7.84 15.4929C3.72 11.93 1 9.57248 1 6.69619C1 4.33869 2.936 2.5 5.4 2.5C6.792 2.5 8.128 3.11798 9 4.08692C9.872 3.11798 11.208 2.5 12.6 2.5C15.064 2.5 17 4.33869 17 6.69619C17 9.57248 14.28 11.93 10.16 15.4929L9 16.5Z" fill={favorite ? "#e53935" : "currentColor"} />
|
||||||
d="M4.625 17.5C4.14375 17.5 3.73192 17.3261 3.3895 16.9782C3.04708 16.6304 2.87558 16.2117 2.875 15.7222V4.16667H2V2.38889H6.375V1.5H11.625V2.38889H16V4.16667H15.125V15.7222C15.125 16.2111 14.9538 16.6298 14.6114 16.9782C14.269 17.3267 13.8568 17.5006 13.375 17.5H4.625ZM6.375 13.9444H8.125V5.94444H6.375V13.9444ZM9.875 13.9444H11.625V5.94444H9.875V13.9444Z"
|
|
||||||
fill="#D0D0D0"
|
|
||||||
style={{ transition: 'fill 0.2s' }}
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="bdel"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Удалить из корзины"
|
||||||
|
onClick={onRemove}
|
||||||
|
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && onRemove && onRemove()}
|
||||||
|
style={{ display: 'inline-flex', cursor: 'pointer', transition: 'color 0.2s' }}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
const path = e.currentTarget.querySelector('path');
|
||||||
|
if (path) path.setAttribute('fill', '#ec1c24');
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
const path = e.currentTarget.querySelector('path');
|
||||||
|
if (path) path.setAttribute('fill', '#D0D0D0');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M4.625 17.5C4.14375 17.5 3.73192 17.3261 3.3895 16.9782C3.04708 16.6304 2.87558 16.2117 2.875 15.7222V4.16667H2V2.38889H6.375V1.5H11.625V2.38889H16V4.16667H15.125V15.7222C15.125 16.2111 14.9538 16.6298 14.6114 16.9782C14.269 17.3267 13.8568 17.5006 13.375 17.5H4.625ZM6.375 13.9444H8.125V5.94444H6.375V13.9444ZM9.875 13.9444H11.625V5.94444H9.875V13.9444Z"
|
||||||
|
fill="#D0D0D0"
|
||||||
|
style={{ transition: 'fill 0.2s' }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
export default CartItem;
|
export default CartItem;
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } 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";
|
||||||
@ -52,8 +52,16 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
const [quantities, setQuantities] = useState<{ [key: number]: number }>(
|
const [quantities, setQuantities] = useState<{ [key: number]: number }>(
|
||||||
offers.reduce((acc, _, index) => ({ ...acc, [index]: 1 }), {})
|
offers.reduce((acc, _, index) => ({ ...acc, [index]: 1 }), {})
|
||||||
);
|
);
|
||||||
|
const [inputValues, setInputValues] = useState<{ [key: number]: string }>(
|
||||||
|
offers.reduce((acc, _, index) => ({ ...acc, [index]: "1" }), {})
|
||||||
|
);
|
||||||
const [quantityErrors, setQuantityErrors] = useState<{ [key: number]: string }>({});
|
const [quantityErrors, setQuantityErrors] = useState<{ [key: number]: string }>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputValues(offers.reduce((acc, _, index) => ({ ...acc, [index]: "1" }), {}));
|
||||||
|
setQuantities(offers.reduce((acc, _, index) => ({ ...acc, [index]: 1 }), {}));
|
||||||
|
}, [offers.length]);
|
||||||
|
|
||||||
const displayedOffers = offers.slice(0, visibleOffersCount);
|
const displayedOffers = offers.slice(0, visibleOffersCount);
|
||||||
const hasMoreOffers = visibleOffersCount < offers.length;
|
const hasMoreOffers = visibleOffersCount < offers.length;
|
||||||
|
|
||||||
@ -83,16 +91,35 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
return match ? parseInt(match[0]) : 0;
|
return match ? parseInt(match[0]) : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQuantityInput = (index: number, value: string) => {
|
const handleInputChange = (idx: number, val: string) => {
|
||||||
const offer = offers[index];
|
setInputValues(prev => ({ ...prev, [idx]: val }));
|
||||||
const availableStock = parseStock(offer.pcs);
|
if (val === "") return;
|
||||||
let num = parseInt(value, 10);
|
const valueNum = Math.max(1, parseInt(val, 10) || 1);
|
||||||
if (isNaN(num) || num < 1) num = 1;
|
setQuantities(prev => ({ ...prev, [idx]: valueNum }));
|
||||||
if (num > availableStock) {
|
};
|
||||||
toast.error(`Максимум ${availableStock} шт.`);
|
|
||||||
return;
|
const handleInputBlur = (idx: number) => {
|
||||||
|
if (inputValues[idx] === "") {
|
||||||
|
setInputValues(prev => ({ ...prev, [idx]: "1" }));
|
||||||
|
setQuantities(prev => ({ ...prev, [idx]: 1 }));
|
||||||
}
|
}
|
||||||
setQuantities(prev => ({ ...prev, [index]: num }));
|
};
|
||||||
|
|
||||||
|
const handleMinus = (idx: number) => {
|
||||||
|
setQuantities(prev => {
|
||||||
|
const newVal = Math.max(1, (prev[idx] || 1) - 1);
|
||||||
|
setInputValues(vals => ({ ...vals, [idx]: newVal.toString() }));
|
||||||
|
return { ...prev, [idx]: newVal };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlus = (idx: number, maxCount?: number) => {
|
||||||
|
setQuantities(prev => {
|
||||||
|
let newVal = (prev[idx] || 1) + 1;
|
||||||
|
if (maxCount !== undefined) newVal = Math.min(newVal, maxCount);
|
||||||
|
setInputValues(vals => ({ ...vals, [idx]: newVal.toString() }));
|
||||||
|
return { ...prev, [idx]: newVal };
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddToCart = (offer: CoreProductCardOffer, index: number) => {
|
const handleAddToCart = (offer: CoreProductCardOffer, index: number) => {
|
||||||
@ -291,6 +318,7 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
<div className="w-layout-vflex product-list-search-s1">
|
<div className="w-layout-vflex product-list-search-s1">
|
||||||
{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);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-layout-hflex product-item-search-s1"
|
className="w-layout-hflex product-item-search-s1"
|
||||||
@ -317,43 +345,48 @@ const CoreProductCard: React.FC<CoreProductCardProps> = ({
|
|||||||
<div className="w-layout-hflex add-to-cart-block-s1">
|
<div className="w-layout-hflex add-to-cart-block-s1">
|
||||||
<div className="w-layout-hflex flex-block-82">
|
<div className="w-layout-hflex flex-block-82">
|
||||||
<div className="w-layout-hflex pcs-cart-s1">
|
<div className="w-layout-hflex pcs-cart-s1">
|
||||||
<button
|
<div
|
||||||
type="button"
|
|
||||||
className="minus-plus"
|
className="minus-plus"
|
||||||
onClick={() => handleQuantityInput(idx, ((quantities[idx] || 1) - 1).toString())}
|
onClick={() => handleMinus(idx)}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
aria-label="Уменьшить количество"
|
aria-label="Уменьшить количество"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handleMinus(idx)}
|
||||||
|
role="button"
|
||||||
>
|
>
|
||||||
<div className="pluspcs w-embed">
|
<div className="pluspcs w-embed">
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M6 10.5V9.5H14V10.5H6Z" fill="currentColor" />
|
<path d="M6 10.5V9.5H14V10.5H6Z" fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
<div className="input-pcs">
|
<div className="input-pcs">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={parseStock(offer.pcs)}
|
max={maxCount}
|
||||||
value={quantities[idx] || 1}
|
value={inputValues[idx]}
|
||||||
onChange={e => handleQuantityInput(idx, e.target.value)}
|
onChange={e => handleInputChange(idx, e.target.value)}
|
||||||
|
onBlur={() => handleInputBlur(idx)}
|
||||||
className="text-block-26 w-full text-center outline-none"
|
className="text-block-26 w-full text-center outline-none"
|
||||||
aria-label="Количество"
|
aria-label="Количество"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div
|
||||||
type="button"
|
|
||||||
className="minus-plus"
|
className="minus-plus"
|
||||||
onClick={() => handleQuantityInput(idx, ((quantities[idx] || 1) + 1).toString())}
|
onClick={() => handlePlus(idx, maxCount)}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
aria-label="Увеличить количество"
|
aria-label="Увеличить количество"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && handlePlus(idx, maxCount)}
|
||||||
|
role="button"
|
||||||
>
|
>
|
||||||
<div className="pluspcs w-embed">
|
<div className="pluspcs w-embed">
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M6 10.5V9.5H14V10.5H6ZM9.5 6H10.5V14H9.5V6Z" fill="currentColor" />
|
<path d="M6 10.5V9.5H14V10.5H6ZM9.5 6H10.5V14H9.5V6Z" fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -68,49 +68,49 @@ const VehicleSearchResults: React.FC<VehicleSearchResultsProps> = ({
|
|||||||
Найдено автомобилей: {results.length}
|
Найдено автомобилей: {results.length}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="flex flex-wrap flex-1 gap-5 size-full max-md:max-w-full">
|
||||||
{results.map((vehicle, index) => (
|
{results.map((vehicle, index) => (
|
||||||
<div
|
<div
|
||||||
key={`${vehicle.vehicleid}-${index}`}
|
key={`${vehicle.vehicleid}-${index}`}
|
||||||
className="bg-white rounded-lg shadow-md border border-gray-200 p-4 hover:shadow-lg transition-shadow cursor-pointer"
|
className="flex flex-col flex-1 shrink p-8 bg-white rounded-lg border border-solid basis-0 border-stone-300 max-w-[504px] md:min-w-[370px] sm:min-w-[340px] min-w-[200px] max-md:px-5 cursor-pointer transition-shadow hover:shadow-lg"
|
||||||
onClick={() => handleSelectVehicle(vehicle)}
|
onClick={() => handleSelectVehicle(vehicle)}
|
||||||
>
|
>
|
||||||
{/* Заголовок автомобиля */}
|
{/* Заголовок автомобиля */}
|
||||||
<div className="mb-3">
|
<div className="">
|
||||||
<h4 className="text-lg font-semibold text-blue-600 mb-1">
|
<h4 className="text-lg font-semibold text-red-600 mb-1 truncate">
|
||||||
{vehicle.name || `${vehicle.brand} ${vehicle.model}`}
|
{vehicle.name || `${vehicle.brand} ${vehicle.model}`}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-gray-500">
|
{/* <p className="text-sm text-gray-500 truncate">
|
||||||
{vehicle.modification} ({vehicle.year})
|
{vehicle.modification} ({vehicle.year})
|
||||||
</p>
|
</p> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Основные характеристики */}
|
{/* Основные характеристики */}
|
||||||
<div className="space-y-1 mb-4">
|
<div className="space-y-1 mb-4">
|
||||||
<h5 className="text-sm font-semibold text-gray-700 mb-2">Основные характеристики</h5>
|
<h5 className="text-base font-semibold text-gray-900 mb-2">Основные характеристики</h5>
|
||||||
{renderAttribute('Марка', vehicle.brand)}
|
{renderAttribute('Марка', vehicle.brand)}
|
||||||
{renderAttribute('Модель', vehicle.model)}
|
{renderAttribute('Модель', vehicle.model)}
|
||||||
{renderAttribute('Двигатель', vehicle.engine)}
|
{renderAttribute('Двигатель', vehicle.engine)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Все атрибуты из API */}
|
{/* Все атрибуты из API */}
|
||||||
{vehicle.attributes && vehicle.attributes.length > 0 && (
|
{vehicle.attributes && vehicle.attributes.length > 0 && (
|
||||||
<div className="space-y-1 mb-4">
|
<div className="space-y-1 mb-4">
|
||||||
<h5 className="text-sm font-semibold text-gray-700 mb-2">Дополнительные характеристики</h5>
|
<h5 className="text-base font-semibold text-gray-900 mb-2">Дополнительные характеристики</h5>
|
||||||
{vehicle.attributes.map((attr, attrIndex) => (
|
{vehicle.attributes.map((attr, attrIndex) => (
|
||||||
<div key={attrIndex} className="flex justify-between py-1 border-b border-gray-100">
|
<div key={attrIndex} className="flex justify-between py-1 border-b border-gray-100">
|
||||||
<span className="text-sm text-gray-600 font-medium">{attr.name || attr.key}:</span>
|
<span className="text-sm text-gray-600 font-medium">{attr.name || attr.key}:</span>
|
||||||
<span className="text-sm text-gray-900">{attr.value}</span>
|
<span className="text-sm text-gray-900">{attr.value}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Технические характеристики (fallback для старых данных) */}
|
{/* Технические характеристики (fallback для старых данных) */}
|
||||||
{(!vehicle.attributes || vehicle.attributes.length === 0) && (
|
{(!vehicle.attributes || vehicle.attributes.length === 0) && (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-1 mb-4">
|
<div className="space-y-1 mb-4">
|
||||||
<h5 className="text-sm font-semibold text-gray-700 mb-2">Дополнительные характеристики</h5>
|
<h5 className="text-base font-semibold text-gray-900 mb-2">Дополнительные характеристики</h5>
|
||||||
{renderAttribute('Год', vehicle.year)}
|
{renderAttribute('Год', vehicle.year)}
|
||||||
{renderAttribute('Кузов', vehicle.bodytype)}
|
{renderAttribute('Кузов', vehicle.bodytype)}
|
||||||
{renderAttribute('Трансмиссия', vehicle.transmission)}
|
{renderAttribute('Трансмиссия', vehicle.transmission)}
|
||||||
@ -123,7 +123,7 @@ const VehicleSearchResults: React.FC<VehicleSearchResultsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1 mb-4">
|
<div className="space-y-1 mb-4">
|
||||||
<h5 className="text-sm font-semibold text-gray-700 mb-2">Технические характеристики</h5>
|
<h5 className="text-base font-semibold text-gray-900 mb-2">Технические характеристики</h5>
|
||||||
{renderAttribute('Информация о двигателе', vehicle.engine_info)}
|
{renderAttribute('Информация о двигателе', vehicle.engine_info)}
|
||||||
{renderAttribute('Номер двигателя', vehicle.engineno)}
|
{renderAttribute('Номер двигателя', vehicle.engineno)}
|
||||||
{renderAttribute('Дата производства', vehicle.date)}
|
{renderAttribute('Дата производства', vehicle.date)}
|
||||||
@ -133,7 +133,7 @@ const VehicleSearchResults: React.FC<VehicleSearchResultsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1 mb-4">
|
<div className="space-y-1 mb-4">
|
||||||
<h5 className="text-sm font-semibold text-gray-700 mb-2">Даты и периоды</h5>
|
<h5 className="text-base font-semibold text-gray-900 mb-2">Даты и периоды</h5>
|
||||||
{renderAttribute('Дата с', vehicle.datefrom)}
|
{renderAttribute('Дата с', vehicle.datefrom)}
|
||||||
{renderAttribute('Дата по', vehicle.dateto)}
|
{renderAttribute('Дата по', vehicle.dateto)}
|
||||||
{renderAttribute('Модельный год с', vehicle.modelyearfrom)}
|
{renderAttribute('Модельный год с', vehicle.modelyearfrom)}
|
||||||
@ -143,7 +143,7 @@ const VehicleSearchResults: React.FC<VehicleSearchResultsProps> = ({
|
|||||||
{/* Опции и описание */}
|
{/* Опции и описание */}
|
||||||
{(vehicle.options || vehicle.description || vehicle.notes) && (
|
{(vehicle.options || vehicle.description || vehicle.notes) && (
|
||||||
<div className="space-y-1 mb-4">
|
<div className="space-y-1 mb-4">
|
||||||
<h5 className="text-sm font-semibold text-gray-700 mb-2">Опции и описание</h5>
|
<h5 className="text-base font-semibold text-gray-900 mb-2">Опции и описание</h5>
|
||||||
{renderAttribute('Опции', vehicle.options)}
|
{renderAttribute('Опции', vehicle.options)}
|
||||||
{renderAttribute('Описание', vehicle.description)}
|
{renderAttribute('Описание', vehicle.description)}
|
||||||
{renderAttribute('Примечания', vehicle.notes)}
|
{renderAttribute('Примечания', vehicle.notes)}
|
||||||
@ -153,25 +153,7 @@ const VehicleSearchResults: React.FC<VehicleSearchResultsProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Системная информация */}
|
{/* Системная информация */}
|
||||||
<div className="mt-4 pt-3 border-t border-gray-200">
|
|
||||||
<div className="text-xs text-gray-400 space-y-1">
|
|
||||||
<div>ID: {vehicle.vehicleid}</div>
|
|
||||||
{vehicle.catalog && <div>Каталог: {vehicle.catalog}</div>}
|
|
||||||
{vehicle.ssd && (
|
|
||||||
<div>SSD: {vehicle.ssd.length > 50 ? `${vehicle.ssd.substring(0, 50)}...` : vehicle.ssd}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Debug информация (только в development) */}
|
|
||||||
{process.env.NODE_ENV === 'development' && (
|
|
||||||
<div className="mt-4 p-2 bg-gray-100 rounded text-xs">
|
|
||||||
<div className="font-semibold text-gray-700 mb-1">Debug Info:</div>
|
|
||||||
<pre className="text-gray-600 whitespace-pre-wrap">
|
|
||||||
{JSON.stringify(vehicle, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -325,36 +325,36 @@ const WizardSearchForm: React.FC<WizardSearchFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Кнопка поиска автомобилей */}
|
{/* Информация о недостаточности параметров и кнопка поиска */}
|
||||||
{!isLoading && canListVehicles && showSearchButton && (
|
{!isLoading && wizardSteps.length > 0 && (
|
||||||
<div className="pt-4 border-t">
|
<div className="flex flex-row gap-4 items-center w-full mx-auto max-sm:flex-col max-sm:items-stretch">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleFindVehicles();
|
handleFindVehicles();
|
||||||
setShowSearchButton(false);
|
setShowSearchButton(false);
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={!canListVehicles || isLoading}
|
||||||
className="w-full sm:w-auto px-8 py-3 bg-red-600 !text-white font-medium rounded-lg shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
|
className="w-full sm:w-auto px-8 py-3 bg-red-600 !text-white font-medium rounded-lg shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center max-sm:w-full"
|
||||||
|
style={{ minWidth: 180 }}
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
Найти
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
||||||
</svg>
|
|
||||||
Найти автомобили
|
|
||||||
</button>
|
</button>
|
||||||
<div className="mt-3 text-sm text-gray-600">
|
<div
|
||||||
Определено параметров: {wizardSteps.filter(s => s.determined).length} из {wizardSteps.length}
|
layer-name="Выберите больше параметров для поиска автомобилей"
|
||||||
|
className="box-border inline-flex gap-5 items-center px-10 py-4 rounded-xl bg-slate-50 h-[52px] max-md:px-8 max-md:py-3.5 max-md:w-full max-md:h-auto max-md:max-w-[524px] max-md:min-h-[52px] max-sm:gap-3 max-sm:px-5 max-sm:py-3 max-sm:w-full max-sm:rounded-lg max-sm:justify-center"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<img src="/images/info.svg" alt="info" style={{ width: 18, height: 20, flexShrink: 0 }} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
layer-name="Выберите больше параметров для поиска автомобилей"
|
||||||
|
className="relative text-base font-medium leading-5 text-center text-gray-950 max-md:text-sm max-sm:text-sm max-sm:leading-4 max-sm:text-center"
|
||||||
|
>
|
||||||
|
Выберите больше параметров для поиска автомобилей
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Информация о недостаточности параметров */}
|
|
||||||
{!isLoading && !canListVehicles && wizardSteps.length > 0 && (
|
|
||||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
||||||
<p className="text-blue-800 text-sm">
|
|
||||||
Выберите больше параметров для поиска автомобилей
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -15,8 +15,10 @@ 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 }) => {
|
||||||
const [from, setFrom] = useState(value ? value[0] : min);
|
const [from, setFrom] = useState<string>(value ? String(value[0]) : String(min));
|
||||||
const [to, setTo] = useState(value ? value[1] : max);
|
const [to, setTo] = useState<string>(value ? String(value[1]) : String(max));
|
||||||
|
const [confirmedFrom, setConfirmedFrom] = useState<number>(value ? value[0] : min);
|
||||||
|
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(true);
|
||||||
@ -25,11 +27,15 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
|||||||
// Обновляем локальное состояние при изменении внешнего значения
|
// Обновляем локальное состояние при изменении внешнего значения
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value) {
|
if (value) {
|
||||||
setFrom(value[0]);
|
setFrom(String(value[0]));
|
||||||
setTo(value[1]);
|
setTo(String(value[1]));
|
||||||
|
setConfirmedFrom(value[0]);
|
||||||
|
setConfirmedTo(value[1]);
|
||||||
} else {
|
} else {
|
||||||
setFrom(min);
|
setFrom(String(min));
|
||||||
setTo(max);
|
setTo(String(max));
|
||||||
|
setConfirmedFrom(min);
|
||||||
|
setConfirmedTo(max);
|
||||||
}
|
}
|
||||||
}, [value, min, max]);
|
}, [value, min, max]);
|
||||||
|
|
||||||
@ -61,15 +67,15 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
|||||||
x = clamp(x, 0, trackWidth);
|
x = clamp(x, 0, trackWidth);
|
||||||
const value = clamp(pxToValue(x), min, max);
|
const value = clamp(pxToValue(x), min, max);
|
||||||
if (dragging === "from") {
|
if (dragging === "from") {
|
||||||
setFrom(v => clamp(Math.min(value, to), min, to));
|
setFrom(v => String(clamp(Math.min(value, Number(to)), min, Number(to))));
|
||||||
} else {
|
} else {
|
||||||
setTo(v => clamp(Math.max(value, from), from, max));
|
setTo(v => String(clamp(Math.max(value, Number(from)), Number(from), max)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onUp = () => {
|
const onUp = () => {
|
||||||
setDragging(null);
|
setDragging(null);
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange([from, to]);
|
onChange([Number(from), Number(to)]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener("mousemove", onMove);
|
window.addEventListener("mousemove", onMove);
|
||||||
@ -82,25 +88,48 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
|||||||
|
|
||||||
// Input handlers
|
// Input handlers
|
||||||
const handleFromInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFromInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
let v = Number(e.target.value.replace(/\D/g, ""));
|
let v = e.target.value.replace(/\D/g, "");
|
||||||
if (isNaN(v)) v = min;
|
setFrom(v);
|
||||||
setFrom(clamp(Math.min(v, to), min, to));
|
|
||||||
};
|
};
|
||||||
const handleToInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleToInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
let v = Number(e.target.value.replace(/\D/g, ""));
|
let v = e.target.value.replace(/\D/g, "");
|
||||||
if (isNaN(v)) v = max;
|
setTo(v);
|
||||||
setTo(clamp(Math.max(v, from), from, max));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInputBlur = () => {
|
const handleFromBlur = () => {
|
||||||
if (onChange) {
|
let v = Number(from);
|
||||||
onChange([from, to]);
|
if (isNaN(v) || v < min) v = min;
|
||||||
|
// если больше max — оставлять как есть
|
||||||
|
setFrom(String(v));
|
||||||
|
if (onChange) onChange([v, to === "" ? max : Number(to)]);
|
||||||
|
setConfirmedFrom(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToBlur = () => {
|
||||||
|
let v = Number(to);
|
||||||
|
if (isNaN(v) || v < min) v = min;
|
||||||
|
if (v > max) v = max;
|
||||||
|
setTo(String(v));
|
||||||
|
if (onChange) onChange([from === "" ? min : Number(from), v]);
|
||||||
|
setConfirmedTo(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFromKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleFromBlur();
|
||||||
|
(e.target as HTMLInputElement).blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleToKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleToBlur();
|
||||||
|
(e.target as HTMLInputElement).blur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// px позиции для точек
|
// px позиции для точек
|
||||||
const pxFrom = valueToPx(from);
|
const pxFrom = valueToPx(dragging ? Number(from) : confirmedFrom);
|
||||||
const pxTo = valueToPx(to);
|
const pxTo = valueToPx(dragging ? Number(to) : confirmedTo);
|
||||||
|
|
||||||
// Мобильная версия - без dropdown
|
// Мобильная версия - без dropdown
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
@ -124,7 +153,8 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
|||||||
id="from"
|
id="from"
|
||||||
value={from}
|
value={from}
|
||||||
onChange={handleFromInput}
|
onChange={handleFromInput}
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleFromBlur}
|
||||||
|
onKeyDown={handleFromKeyDown}
|
||||||
style={{ padding: '8px 10px 8px 36px', fontSize: 16, width: '100%' }}
|
style={{ padding: '8px 10px 8px 36px', fontSize: 16, width: '100%' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -139,7 +169,8 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
|||||||
id="to"
|
id="to"
|
||||||
value={to}
|
value={to}
|
||||||
onChange={handleToInput}
|
onChange={handleToInput}
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleToBlur}
|
||||||
|
onKeyDown={handleToKeyDown}
|
||||||
style={{ padding: '8px 10px 8px 36px', fontSize: 16, width: '100%' }}
|
style={{ padding: '8px 10px 8px 36px', fontSize: 16, width: '100%' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -214,7 +245,8 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
|||||||
id="from"
|
id="from"
|
||||||
value={from}
|
value={from}
|
||||||
onChange={handleFromInput}
|
onChange={handleFromInput}
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleFromBlur}
|
||||||
|
onKeyDown={handleFromKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="div-block-5">
|
<div className="div-block-5">
|
||||||
@ -228,7 +260,8 @@ const FilterRange: React.FC<FilterRangeProps> = ({ title, min = DEFAULT_MIN, max
|
|||||||
id="to"
|
id="to"
|
||||||
value={to}
|
value={to}
|
||||||
onChange={handleToInput}
|
onChange={handleToInput}
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleToBlur}
|
||||||
|
onKeyDown={handleToKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -137,10 +137,10 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
|||||||
/>
|
/>
|
||||||
{/* Точки/области */}
|
{/* Точки/области */}
|
||||||
{coordinates.map((coord: any, idx: number) => {
|
{coordinates.map((coord: any, idx: number) => {
|
||||||
const scaledX = coord.x * imageScale.x;
|
// Кружки всегда 32x32px, центрируем по координате
|
||||||
const scaledY = coord.y * imageScale.y;
|
const size = 22;
|
||||||
const scaledWidth = coord.width * imageScale.x;
|
const scaledX = coord.x * imageScale.x - size / 2;
|
||||||
const scaledHeight = coord.height * imageScale.y;
|
const scaledY = coord.y * imageScale.y - size / 2;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`coord-${unitId}-${idx}-${coord.x}-${coord.y}`}
|
key={`coord-${unitId}-${idx}-${coord.x}-${coord.y}`}
|
||||||
@ -149,19 +149,29 @@ const KnotIn: React.FC<KnotInProps> = ({ catalogCode, vehicleId, ssd, unitId, un
|
|||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') handlePointClick(coord.codeonimage);
|
if (e.key === 'Enter' || e.key === ' ') handlePointClick(coord.codeonimage);
|
||||||
}}
|
}}
|
||||||
className="absolute flex items-center justify-center border-2 border-red-600 bg-white rounded-full cursor-pointer"
|
className="absolute flex items-center justify-center cursor-pointer transition-colors"
|
||||||
style={{
|
style={{
|
||||||
left: scaledX,
|
left: scaledX,
|
||||||
top: scaledY,
|
top: scaledY,
|
||||||
width: scaledWidth,
|
width: size,
|
||||||
height: scaledHeight,
|
height: size,
|
||||||
|
background: '#B7CAE2',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
|
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
}}
|
}}
|
||||||
title={coord.codeonimage}
|
title={coord.codeonimage}
|
||||||
onClick={() => handlePointClick(coord.codeonimage)}
|
onClick={() => handlePointClick(coord.codeonimage)}
|
||||||
|
onMouseEnter={e => {
|
||||||
|
(e.currentTarget as HTMLDivElement).style.background = '#EC1C24';
|
||||||
|
(e.currentTarget.querySelector('span') as HTMLSpanElement).style.color = '#fff';
|
||||||
|
}}
|
||||||
|
onMouseLeave={e => {
|
||||||
|
(e.currentTarget as HTMLDivElement).style.background = '#B7CAE2';
|
||||||
|
(e.currentTarget.querySelector('span') as HTMLSpanElement).style.color = '#000';
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center justify-center w-full h-full text-black text-sm font-bold select-none pointer-events-none">
|
<span className="flex items-center justify-center w-full h-full text-black text-sm font-bold select-none pointer-events-none" style={{color: '#000'}}>
|
||||||
{coord.codeonimage}
|
{coord.codeonimage}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,18 +10,14 @@ interface VinCategoryProps {
|
|||||||
activeTab?: 'uzly' | 'manufacturer';
|
activeTab?: 'uzly' | 'manufacturer';
|
||||||
onQuickGroupSelect?: (group: any) => void;
|
onQuickGroupSelect?: (group: any) => void;
|
||||||
onCategoryClick?: (e?: React.MouseEvent) => void;
|
onCategoryClick?: (e?: React.MouseEvent) => void;
|
||||||
|
openedPath?: string[];
|
||||||
|
setOpenedPath?: (path: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd, onNodeSelect, activeTab = 'uzly', onQuickGroupSelect, onCategoryClick }) => {
|
const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd, onNodeSelect, activeTab = 'uzly', onQuickGroupSelect, onCategoryClick, openedPath = [], setOpenedPath = () => {} }) => {
|
||||||
const [selectedCategory, setSelectedCategory] = useState<any>(null);
|
|
||||||
const [unitsByCategory, setUnitsByCategory] = useState<{ [key: string]: any[] }>({});
|
const [unitsByCategory, setUnitsByCategory] = useState<{ [key: string]: any[] }>({});
|
||||||
const lastCategoryIdRef = useRef<string | null>(null);
|
const lastCategoryIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
// Сброс выбранной категории при смене вкладки
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedCategory(null);
|
|
||||||
}, [activeTab]);
|
|
||||||
|
|
||||||
// Запрос для "Общие" (QuickGroups)
|
// Запрос для "Общие" (QuickGroups)
|
||||||
const { data: quickGroupsData, loading: quickGroupsLoading, error: quickGroupsError } = useQuery(GET_LAXIMO_QUICK_GROUPS, {
|
const { data: quickGroupsData, loading: quickGroupsLoading, error: quickGroupsError } = useQuery(GET_LAXIMO_QUICK_GROUPS, {
|
||||||
variables: { catalogCode: catalogCode || '', vehicleId: vehicleId || '', ssd: ssd || '' },
|
variables: { catalogCode: catalogCode || '', vehicleId: vehicleId || '', ssd: ssd || '' },
|
||||||
@ -51,50 +47,41 @@ const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const categories = activeTab === 'uzly' ? (quickGroupsData?.laximoQuickGroups || []) : (categoriesData?.laximoCategories || []);
|
// categories теперь зависят от activeTab
|
||||||
|
let categories = activeTab === 'uzly' ? (quickGroupsData?.laximoQuickGroups || []) : (categoriesData?.laximoCategories || []);
|
||||||
|
let selectedCategory: any = null;
|
||||||
|
let currentLevel = 0;
|
||||||
|
let currentList = categories;
|
||||||
|
while (openedPath[currentLevel]) {
|
||||||
|
const found = currentList.find((cat: any) => (cat.quickgroupid || cat.categoryid || cat.id) === openedPath[currentLevel]);
|
||||||
|
if (!found) break;
|
||||||
|
selectedCategory = found;
|
||||||
|
currentList = found.children || [];
|
||||||
|
currentLevel++;
|
||||||
|
}
|
||||||
|
|
||||||
const loading = activeTab === 'uzly' ? quickGroupsLoading : categoriesLoading;
|
const loading = activeTab === 'uzly' ? quickGroupsLoading : categoriesLoading;
|
||||||
const error = activeTab === 'uzly' ? quickGroupsError : categoriesError;
|
const error = activeTab === 'uzly' ? quickGroupsError : categoriesError;
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
setSelectedCategory(null);
|
setOpenedPath(openedPath.slice(0, openedPath.length - 1));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCategoryClick = (category: any) => {
|
const handleCategoryClick = (category: any, level: number) => {
|
||||||
// Если передан onCategoryClick, используем его
|
|
||||||
if (onCategoryClick) {
|
if (onCategoryClick) {
|
||||||
onCategoryClick();
|
onCategoryClick();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (category.children && category.children.length > 0) {
|
||||||
if (activeTab === 'uzly') {
|
if (openedPath[level] === (category.quickgroupid || category.categoryid || category.id)) {
|
||||||
// Логика для вкладки "Общие" (QuickGroups)
|
setOpenedPath(openedPath.slice(0, level));
|
||||||
if (category.children && category.children.length > 0) {
|
|
||||||
setSelectedCategory(category);
|
|
||||||
} else if (category.link && onQuickGroupSelect) {
|
|
||||||
onQuickGroupSelect(category);
|
|
||||||
} else if (onNodeSelect) {
|
|
||||||
onNodeSelect(category);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Логика для вкладки "От производителя" (Categories)
|
|
||||||
if (category.children && category.children.length > 0) {
|
|
||||||
setSelectedCategory(category);
|
|
||||||
} else {
|
} else {
|
||||||
// Если нет children, грузим units (подкатегории)
|
setOpenedPath([...openedPath.slice(0, level), (category.quickgroupid || category.categoryid || category.id)]);
|
||||||
const categoryId = category.categoryid || category.quickgroupid || category.id;
|
|
||||||
if (!unitsByCategory[categoryId] && catalogCode && vehicleId) {
|
|
||||||
lastCategoryIdRef.current = categoryId;
|
|
||||||
getUnits({
|
|
||||||
variables: {
|
|
||||||
catalogCode,
|
|
||||||
vehicleId,
|
|
||||||
ssd: ssd || '',
|
|
||||||
categoryId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setSelectedCategory(category);
|
|
||||||
}
|
}
|
||||||
|
} else if (category.link && onQuickGroupSelect) {
|
||||||
|
onQuickGroupSelect(category);
|
||||||
|
} else if (onNodeSelect) {
|
||||||
|
onNodeSelect(category);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -106,7 +93,7 @@ const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd,
|
|||||||
unitid: subcat.unitid || subcat.categoryid || subcat.quickgroupid || subcat.id
|
unitid: subcat.unitid || subcat.categoryid || subcat.quickgroupid || subcat.id
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
handleCategoryClick(subcat);
|
handleCategoryClick(subcat, 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -150,7 +137,7 @@ const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd,
|
|||||||
<div
|
<div
|
||||||
className="div-block-131"
|
className="div-block-131"
|
||||||
key={cat.quickgroupid || cat.categoryid || cat.id || idx}
|
key={cat.quickgroupid || cat.categoryid || cat.id || idx}
|
||||||
onClick={() => handleCategoryClick(cat)}
|
onClick={() => handleCategoryClick(cat, 0)}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<div className="text-block-57">{cat.name}</div>
|
<div className="text-block-57">{cat.name}</div>
|
||||||
@ -165,32 +152,37 @@ const VinCategory: React.FC<VinCategoryProps> = ({ catalogCode, vehicleId, ssd,
|
|||||||
) : (
|
) : (
|
||||||
// Список подкатегорий
|
// Список подкатегорий
|
||||||
<>
|
<>
|
||||||
{/* <div className="div-block-131" onClick={handleBack} style={{ cursor: "pointer", fontWeight: 500 }}>
|
{(() => {
|
||||||
<div className="text-block-57">← Назад</div>
|
// Найти текущий уровень вложенности для selectedCategory
|
||||||
<div className="w-embed">
|
let level = 0;
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
let list = categories;
|
||||||
<rect x="24" width="24" height="24" rx="12" transform="rotate(90 24 0)" fill="currentcolor"></rect>
|
while (openedPath[level] && list) {
|
||||||
<path fillRule="evenodd" clipRule="evenodd" d="M10.9303 17L10 16.0825L14.1395 12L10 7.91747L10.9303 7L16 12L10.9303 17Z" fill="white"></path>
|
const found = list.find((cat: any) => (cat.quickgroupid || cat.categoryid || cat.id) === openedPath[level]);
|
||||||
</svg>
|
if (!found) break;
|
||||||
</div>
|
if (found === selectedCategory) break;
|
||||||
</div> */}
|
list = found.children || [];
|
||||||
{subcategories.length === 0 && <div style={{ color: "#888", padding: 8 }}>Нет подкатегорий</div>}
|
level++;
|
||||||
{subcategories.map((subcat: any, idx: number) => (
|
}
|
||||||
<div
|
// Теперь level - это уровень selectedCategory, подкатегории будут на level+1
|
||||||
className="div-block-131"
|
const subcategories = selectedCategory.children || [];
|
||||||
key={subcat.quickgroupid || subcat.categoryid || subcat.unitid || subcat.id || idx}
|
if (subcategories.length === 0) return <div style={{ color: "#888", padding: 8 }}>Нет подкатегорий</div>;
|
||||||
onClick={() => handleSubcategoryClick(subcat)}
|
return subcategories.map((subcat: any, idx: number) => (
|
||||||
style={{ cursor: "pointer" }}
|
<div
|
||||||
>
|
className="div-block-131"
|
||||||
<div className="text-block-57">{subcat.name}</div>
|
key={subcat.quickgroupid || subcat.categoryid || subcat.unitid || subcat.id || idx}
|
||||||
<div className="w-embed">
|
onClick={() => handleCategoryClick(subcat, level + 1)}
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
style={{ cursor: "pointer" }}
|
||||||
<rect x="24" width="24" height="24" rx="12" transform="rotate(90 24 0)" fill="currentcolor"></rect>
|
>
|
||||||
<path fillRule="evenodd" clipRule="evenodd" d="M10.9303 17L10 16.0825L14.1395 12L10 7.91747L10.9303 7L16 12L10.9303 17Z" fill="white"></path>
|
<div className="text-block-57">{subcat.name}</div>
|
||||||
</svg>
|
<div className="w-embed">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="24" width="24" height="24" rx="12" transform="rotate(90 24 0)" fill="currentcolor"></rect>
|
||||||
|
<path fillRule="evenodd" clipRule="evenodd" d="M10.9303 17L10 16.0825L14.1395 12L10 7.91747L10.9303 7L16 12L10.9303 17Z" fill="white"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
));
|
||||||
))}
|
})()}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,6 +19,9 @@ interface VinLeftbarProps {
|
|||||||
onNodeSelect?: (node: any) => void;
|
onNodeSelect?: (node: any) => void;
|
||||||
onActiveTabChange?: (tab: 'uzly' | 'manufacturer') => void;
|
onActiveTabChange?: (tab: 'uzly' | 'manufacturer') => void;
|
||||||
onQuickGroupSelect?: (group: any) => void;
|
onQuickGroupSelect?: (group: any) => void;
|
||||||
|
activeTab?: 'uzly' | 'manufacturer';
|
||||||
|
openedPath?: string[];
|
||||||
|
setOpenedPath?: (path: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QuickGroup {
|
interface QuickGroup {
|
||||||
@ -28,13 +31,11 @@ interface QuickGroup {
|
|||||||
children?: QuickGroup[];
|
children?: QuickGroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, onNodeSelect, onActiveTabChange, onQuickGroupSelect }) => {
|
const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, onNodeSelect, onActiveTabChange, onQuickGroupSelect, activeTab: activeTabProp, openedPath = [], setOpenedPath = () => {} }) => {
|
||||||
const catalogCode = vehicleInfo?.catalog || '';
|
const catalogCode = vehicleInfo?.catalog || '';
|
||||||
const vehicleId = vehicleInfo?.vehicleid || '';
|
const vehicleId = vehicleInfo?.vehicleid || '';
|
||||||
const ssd = vehicleInfo?.ssd || '';
|
const ssd = vehicleInfo?.ssd || '';
|
||||||
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly');
|
|
||||||
const [executeSearch, { data, loading, error }] = useLazyQuery(GET_LAXIMO_FULLTEXT_SEARCH, { errorPolicy: 'all' });
|
const [executeSearch, { data, loading, error }] = useLazyQuery(GET_LAXIMO_FULLTEXT_SEARCH, { errorPolicy: 'all' });
|
||||||
|
|
||||||
const { data: categoriesData, loading: categoriesLoading, error: categoriesError } = useQuery(GET_LAXIMO_CATEGORIES, {
|
const { data: categoriesData, loading: categoriesLoading, error: categoriesError } = useQuery(GET_LAXIMO_CATEGORIES, {
|
||||||
@ -58,11 +59,11 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
|||||||
|
|
||||||
const lastCategoryIdRef = React.useRef<string | null>(null);
|
const lastCategoryIdRef = React.useRef<string | null>(null);
|
||||||
|
|
||||||
const handleToggle = (idx: number, categoryId: string) => {
|
const handleToggle = (categoryId: string, level: number) => {
|
||||||
setOpenIndex(openIndex === idx ? null : idx);
|
if (openedPath[level] === categoryId) {
|
||||||
if (openIndex !== idx && !unitsByCategory[categoryId]) {
|
setOpenedPath(openedPath.slice(0, level));
|
||||||
lastCategoryIdRef.current = categoryId;
|
} else {
|
||||||
getUnits({ variables: { catalogCode, vehicleId, ssd, categoryId } });
|
setOpenedPath([...openedPath.slice(0, level), categoryId]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -117,26 +118,11 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
|||||||
});
|
});
|
||||||
const quickGroups = quickGroupsData?.laximoQuickGroups || [];
|
const quickGroups = quickGroupsData?.laximoQuickGroups || [];
|
||||||
|
|
||||||
const [expandedQuickGroup, setExpandedQuickGroup] = useState<string | null>(null);
|
const handleQuickGroupToggle = (groupId: string, level: number) => {
|
||||||
const [expandedSubQuickGroup, setExpandedSubQuickGroup] = useState<string | null>(null);
|
if (openedPath[level] === groupId) {
|
||||||
|
setOpenedPath(openedPath.slice(0, level));
|
||||||
const handleQuickGroupToggle = (groupId: string) => {
|
|
||||||
setExpandedQuickGroup(prev => (prev === groupId ? null : groupId));
|
|
||||||
setExpandedSubQuickGroup(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubQuickGroupToggle = (groupId: string) => {
|
|
||||||
setExpandedSubQuickGroup(prev => (prev === groupId ? null : groupId));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleQuickGroupClick = (group: any) => {
|
|
||||||
if (group.link) {
|
|
||||||
// Передаем выбранную группу в родительский компонент для отображения справа
|
|
||||||
if (onQuickGroupSelect) {
|
|
||||||
onQuickGroupSelect(group);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
handleQuickGroupToggle(group.quickgroupid);
|
setOpenedPath([...openedPath.slice(0, level), groupId]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -207,12 +193,6 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
|||||||
|
|
||||||
const fulltextResults = fulltextData?.laximoFulltextSearch?.details || [];
|
const fulltextResults = fulltextData?.laximoFulltextSearch?.details || [];
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (onActiveTabChange) {
|
|
||||||
onActiveTabChange(activeTab);
|
|
||||||
}
|
|
||||||
}, [activeTab]);
|
|
||||||
|
|
||||||
// Если нет данных о транспортном средстве, показываем заглушку
|
// Если нет данных о транспортном средстве, показываем заглушку
|
||||||
if (!vehicleInfo) {
|
if (!vehicleInfo) {
|
||||||
return (
|
return (
|
||||||
@ -281,18 +261,15 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
|||||||
className={
|
className={
|
||||||
searchQuery
|
searchQuery
|
||||||
? 'button-23 w-button'
|
? 'button-23 w-button'
|
||||||
: activeTab === 'uzly'
|
: activeTabProp === 'uzly'
|
||||||
? 'button-3 w-button'
|
? 'button-3 w-button'
|
||||||
: 'button-23 w-button'
|
: 'button-23 w-button'
|
||||||
}
|
}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (searchQuery) setSearchQuery('');
|
if (searchQuery) setSearchQuery('');
|
||||||
setActiveTab('uzly');
|
if (onActiveTabChange) onActiveTabChange('uzly');
|
||||||
// Очищаем выбранную группу при смене таба
|
if (onQuickGroupSelect) onQuickGroupSelect(null);
|
||||||
if (onQuickGroupSelect) {
|
|
||||||
onQuickGroupSelect(null);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Узлы
|
Узлы
|
||||||
@ -302,25 +279,22 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
|||||||
className={
|
className={
|
||||||
searchQuery
|
searchQuery
|
||||||
? 'button-23 w-button'
|
? 'button-23 w-button'
|
||||||
: activeTab === 'manufacturer'
|
: activeTabProp === 'manufacturer'
|
||||||
? 'button-3 w-button'
|
? 'button-3 w-button'
|
||||||
: 'button-23 w-button'
|
: 'button-23 w-button'
|
||||||
}
|
}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (searchQuery) setSearchQuery('');
|
if (searchQuery) setSearchQuery('');
|
||||||
setActiveTab('manufacturer');
|
if (onActiveTabChange) onActiveTabChange('manufacturer');
|
||||||
// Очищаем выбранную группу при смене таба
|
if (onQuickGroupSelect) onQuickGroupSelect(null);
|
||||||
if (onQuickGroupSelect) {
|
|
||||||
onQuickGroupSelect(null);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
От производителя
|
От производителя
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{/* Tab content start */}
|
{/* Tab content start */}
|
||||||
{activeTab === 'uzly' ? (
|
{activeTabProp === 'uzly' ? (
|
||||||
// Общие (QuickGroups - бывшие "От производителя")
|
// Общие (QuickGroups - бывшие "От производителя")
|
||||||
quickGroupsLoading ? (
|
quickGroupsLoading ? (
|
||||||
<div style={{ padding: 16, textAlign: 'center' }}>Загружаем группы быстрого поиска...</div>
|
<div style={{ padding: 16, textAlign: 'center' }}>Загружаем группы быстрого поиска...</div>
|
||||||
@ -330,7 +304,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
|||||||
<>
|
<>
|
||||||
{(quickGroups as QuickGroup[]).map((group: QuickGroup) => {
|
{(quickGroups as QuickGroup[]).map((group: QuickGroup) => {
|
||||||
const hasChildren = group.children && group.children.length > 0;
|
const hasChildren = group.children && group.children.length > 0;
|
||||||
const isOpen = expandedQuickGroup === group.quickgroupid;
|
const isOpen = openedPath.includes(group.quickgroupid);
|
||||||
|
|
||||||
if (!hasChildren) {
|
if (!hasChildren) {
|
||||||
return (
|
return (
|
||||||
@ -340,7 +314,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
|||||||
className="dropdown-link-3 w-dropdown-link"
|
className="dropdown-link-3 w-dropdown-link"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleQuickGroupClick(group);
|
handleQuickGroupToggle(group.quickgroupid, 0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{group.name}
|
{group.name}
|
||||||
@ -357,7 +331,10 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`dropdown-toggle-3 w-dropdown-toggle${isOpen ? " w--open active" : ""}`}
|
className={`dropdown-toggle-3 w-dropdown-toggle${isOpen ? " w--open active" : ""}`}
|
||||||
onClick={() => handleQuickGroupToggle(group.quickgroupid)}
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleQuickGroupToggle(group.quickgroupid, 0);
|
||||||
|
}}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<div className="w-icon-dropdown-toggle"></div>
|
<div className="w-icon-dropdown-toggle"></div>
|
||||||
@ -366,7 +343,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
|||||||
<nav className={`dropdown-list-4 w-dropdown-list${isOpen ? " w--open" : ""}`}>
|
<nav className={`dropdown-list-4 w-dropdown-list${isOpen ? " w--open" : ""}`}>
|
||||||
{group.children?.map((child: QuickGroup) => {
|
{group.children?.map((child: QuickGroup) => {
|
||||||
const hasSubChildren = child.children && child.children.length > 0;
|
const hasSubChildren = child.children && child.children.length > 0;
|
||||||
const isChildOpen = expandedSubQuickGroup === child.quickgroupid;
|
const isChildOpen = openedPath.includes(child.quickgroupid);
|
||||||
|
|
||||||
if (!hasSubChildren) {
|
if (!hasSubChildren) {
|
||||||
return (
|
return (
|
||||||
@ -376,7 +353,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
|||||||
className="dropdown-link-3 w-dropdown-link"
|
className="dropdown-link-3 w-dropdown-link"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleQuickGroupClick(child);
|
handleQuickGroupToggle(child.quickgroupid, 1);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{child.name}
|
{child.name}
|
||||||
@ -393,7 +370,10 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`dropdown-toggle-card w-dropdown-toggle pl-0${isChildOpen ? " w--open active" : ""}`}
|
className={`dropdown-toggle-card w-dropdown-toggle pl-0${isChildOpen ? " w--open active" : ""}`}
|
||||||
onClick={() => handleSubQuickGroupToggle(child.quickgroupid)}
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleQuickGroupToggle(child.quickgroupid, 2);
|
||||||
|
}}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<div className="w-icon-dropdown-toggle"></div>
|
<div className="w-icon-dropdown-toggle"></div>
|
||||||
@ -407,7 +387,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
|||||||
className="dropdown-link-3 w-dropdown-link"
|
className="dropdown-link-3 w-dropdown-link"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleQuickGroupClick(subChild);
|
handleQuickGroupToggle(subChild.quickgroupid, 3);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{subChild.name}
|
{subChild.name}
|
||||||
@ -434,7 +414,7 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{categories.map((category: any, idx: number) => {
|
{categories.map((category: any, idx: number) => {
|
||||||
const isOpen = openIndex === idx;
|
const isOpen = openedPath.includes(category.quickgroupid);
|
||||||
const subcategories = category.children && category.children.length > 0
|
const subcategories = category.children && category.children.length > 0
|
||||||
? category.children
|
? category.children
|
||||||
: unitsByCategory[category.quickgroupid] || [];
|
: unitsByCategory[category.quickgroupid] || [];
|
||||||
@ -447,7 +427,10 @@ const VinLeftbar: React.FC<VinLeftbarProps> = ({ vehicleInfo, onSearchResults, o
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`dropdown-toggle-3 w-dropdown-toggle${isOpen ? " w--open" : ""}`}
|
className={`dropdown-toggle-3 w-dropdown-toggle${isOpen ? " w--open" : ""}`}
|
||||||
onClick={() => handleToggle(idx, category.quickgroupid)}
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggle(category.quickgroupid, 0);
|
||||||
|
}}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<div className="w-icon-dropdown-toggle"></div>
|
<div className="w-icon-dropdown-toggle"></div>
|
||||||
|
@ -58,6 +58,7 @@ const VehicleDetailsPage = () => {
|
|||||||
const [showKnot, setShowKnot] = useState(false);
|
const [showKnot, setShowKnot] = useState(false);
|
||||||
const [foundParts, setFoundParts] = useState<any[]>([]);
|
const [foundParts, setFoundParts] = useState<any[]>([]);
|
||||||
const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly');
|
const [activeTab, setActiveTab] = useState<'uzly' | 'manufacturer'>('uzly');
|
||||||
|
const [openedPath, setOpenedPath] = useState<string[]>([]);
|
||||||
const [searchState, setSearchState] = useState<{
|
const [searchState, setSearchState] = useState<{
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: any;
|
error: any;
|
||||||
@ -329,6 +330,9 @@ const VehicleDetailsPage = () => {
|
|||||||
onNodeSelect={setSelectedNode}
|
onNodeSelect={setSelectedNode}
|
||||||
onActiveTabChange={(tab) => setActiveTab(tab)}
|
onActiveTabChange={(tab) => setActiveTab(tab)}
|
||||||
onQuickGroupSelect={setSelectedQuickGroup}
|
onQuickGroupSelect={setSelectedQuickGroup}
|
||||||
|
activeTab={activeTab}
|
||||||
|
openedPath={openedPath}
|
||||||
|
setOpenedPath={setOpenedPath}
|
||||||
/>
|
/>
|
||||||
{searchState.isSearching ? (
|
{searchState.isSearching ? (
|
||||||
<div className="knot-parts">
|
<div className="knot-parts">
|
||||||
@ -399,6 +403,8 @@ const VehicleDetailsPage = () => {
|
|||||||
onNodeSelect={setSelectedNode}
|
onNodeSelect={setSelectedNode}
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
onQuickGroupSelect={setSelectedQuickGroup}
|
onQuickGroupSelect={setSelectedQuickGroup}
|
||||||
|
openedPath={openedPath}
|
||||||
|
setOpenedPath={setOpenedPath}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -469,7 +469,13 @@ input#VinSearchInput {
|
|||||||
|
|
||||||
|
|
||||||
.dropdown-toggle-card {
|
.dropdown-toggle-card {
|
||||||
|
align-self: stretch;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
padding: 6px 15px;
|
||||||
padding-left: 0 !important;
|
padding-left: 0 !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-link-3 {
|
.dropdown-link-3 {
|
||||||
@ -482,7 +488,7 @@ input#VinSearchInput {
|
|||||||
|
|
||||||
.dropdown-toggle-3.active, .dropdown-toggle-card.active {
|
.dropdown-toggle-3.active, .dropdown-toggle-card.active {
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-toggle-3.active {
|
.dropdown-toggle-3.active {
|
||||||
@ -689,4 +695,28 @@ body {
|
|||||||
}
|
}
|
||||||
.flex-block-108::-webkit-scrollbar, .flex-block-108-copy::-webkit-scrollbar, .w-layout-hflex.flex-block-121::-webkit-scrollbar, .core-product-search::-webkit-scrollbar {
|
.flex-block-108::-webkit-scrollbar, .flex-block-108-copy::-webkit-scrollbar, .w-layout-hflex.flex-block-121::-webkit-scrollbar, .core-product-search::-webkit-scrollbar {
|
||||||
display: none; /* Chrome, Safari */
|
display: none; /* Chrome, Safari */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.flex-block-44 {
|
||||||
|
max-width: 33%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-block-21 {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-block-45 {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-block-39 {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-9-copy {
|
||||||
|
min-width: 100px;
|
||||||
|
|
||||||
}
|
}
|
Reference in New Issue
Block a user