// Product card component + data const PRODUCTS = [ { id:'p1', name:'Печь для бани Торнадо-14 сетка дровяная', brand:'Торнадо', art:'УП-00032226', price:113400, oldPrice:null, stock:13, rating:4.5, reviews:23, badges:[], category:'Печи для бани дровяные', specs:['Объём парилки: 8–14 м³','Сталь топки: 6 мм','Камни: до 60 кг','Дымоход: ∅115'] }, { id:'p2', name:'Печь для бани GFS Авангард Ураган 25П2С ЗК круглая чугунная', brand:'GFS', art:'УП-00032227', price:103400, oldPrice:133800, stock:5, rating:4.8, reviews:41, badges:['hit','new','sale'], category:'Печи для бани дровяные', specs:['Объём парилки: 14–25 м³','Чугун, панорамное стекло','Камни: до 90 кг','Гарантия 10 лет'] }, { id:'p3', name:'Печь для бани Сибирь Копеечка', brand:'Сибирь', art:'УП-00032228', price:113400, oldPrice:null, stock:8, rating:4.3, reviews:12, badges:['hit'], category:'Печи для бани дровяные', specs:['Объём парилки: 8–16 м³','Сталь: 8 мм','Конвекционный кожух','Нагрев за 45 мин'] }, { id:'p4', name:'Термокрышка POLAR Spa COMFORT 160', brand:'POLAR', art:'УП-00032229', price:41000, oldPrice:null, stock:1, rating:5.0, reviews:7, badges:['new'], category:'Банные аксессуары', specs:['Диаметр: Ø160 см','Изоляция: 80 мм','Удержание тепла: 12 ч','Антискользящее покрытие'] }, { id:'p5', name:'Дымоход нержавеющая сталь AISI 439 ∅115', brand:'Феррум', art:'УП-00030014', price:4280, oldPrice:4890, stock:47, rating:4.6, reviews:58, badges:['sale'], category:'Дымоходы нержавеющие', specs:['∅ внутренний: 115 мм','Длина: 1000 мм','AISI 439, 0,8 мм','До 750 °C'] }, { id:'p6', name:'Камни для каменки Габбро-диабаз колотый 20 кг', brand:'КАМ', art:'УП-00030215', price:980, oldPrice:null, stock:120, rating:4.9, reviews:204, badges:['hit'], category:'Камни для каменки', specs:['Фракция: 70–120 мм','Упаковка: 20 кг','Колотый, отмытый','Сертификат в комплекте'] }, { id:'p7', name:'Дверь для бани стеклянная Эстет 700×1900', brand:'Эстет', art:'УП-00028811', price:18900, oldPrice:null, stock:6, rating:4.7, reviews:19, badges:[], category:'Двери для бани стеклянные', specs:['Размер: 700×1900 мм','Стекло: закалённое 8 мм','Коробка: осина','Ручка-скоба в комплекте'] }, { id:'p8', name:'Печь банная Везувий Скиф Стандарт 22 антрацит', brand:'Везувий', art:'УП-00031902', price:52700, oldPrice:58900, stock:9, rating:4.4, reviews:33, badges:['sale'], category:'Печи для бани дровяные', specs:['Объём парилки: 12–22 м³','Сталь: 4 мм','Бак для воды: 65 л','Ковка «антрацит»'] }, { id:'p9', name:'Огнезащитный экран ЗВИ-У 1000×600', brand:'ЗВИ', art:'УП-00030507', price:6450, oldPrice:null, stock:23, rating:4.5, reviews:14, badges:['new'], category:'Огнезащита', specs:['Размер: 1000×600 мм','Базальт + нержавейка','До 900 °C','Сертификат МЧС'] }, { id:'p10', name:'Чугунная плита одноконфорочная 410×360', brand:'ЛИТКОМ', art:'УП-00029118', price:3200, oldPrice:null, stock:0, rating:4.2, reviews:8, badges:[], category:'Чугунное литьё для печей', specs:['Размер: 410×360 мм','Вес: 8,2 кг','Съёмное кольцо','Чугун СЧ-20'] }, { id:'p11', name:'Бондарное изделие: ушат кедр 15 л', brand:'БаняКедр', art:'УП-00028322', price:4100, oldPrice:null, stock:14, rating:4.8, reviews:27, badges:['hit'], category:'Бондарные изделия для бани', specs:['Объём: 15 л','Материал: алтайский кедр','Нерж. обручи','Льняное масло'] }, { id:'p12', name:'Обливное устройство «Водопад» 15 л нерж.', brand:'БаняПро', art:'УП-00028655', price:5980, oldPrice:null, stock:0, rating:4.6, reviews:11, badges:[], category:'Обливное устройство', specs:['Объём: 15 л','Нержавеющая сталь','Пуск за цепочку','Монтажные петли'] }, ]; function fmtPrice(n) { return n.toLocaleString('ru-RU') + ' ₽'; } function StockLabel({ stock }) { if (stock === 0) return ( Под заказ ); const color = stock > 5 ? 'var(--green)' : 'var(--orange)'; return ( В наличии: {stock} ); } function Bages({ list, dense = false }) { if (!list || !list.length) return null; const map = { hit: { label: 'ХИТ', cls: 'badge-hit' }, new: { label: 'НОВИНКА', cls: 'badge-new' }, sale: { label: '-10%', cls: 'badge-sale' } }; return (
{list.map((b,i) => {map[b].label})}
); } function QtyControl({ qty, onChange, size = 'md', style }) { const h = size === 'sm' ? 48 : 56; const inner = h - 8; // 2px border + 2px gap on each side const minusActive = qty > 0; return (
{qty}
); } function ProductCard({ p, idx = 0, variant = 'grid', favSet, compSet, onFav, onComp, cartMap, onQty, onOrder }) { const qty = cartMap[p.id] || 0; const isFav = favSet.has(p.id); const isComp = compSet.has(p.id); const [copied, setCopied] = useState(false); const [imgIdx, setImgIdx] = useState(0); const imgCount = 5; const handleImgMove = (e) => { const r = e.currentTarget.getBoundingClientRect(); const x = e.clientX - r.left; const zone = Math.min(imgCount - 1, Math.max(0, Math.floor((x / r.width) * imgCount))); setImgIdx(zone); }; const handleImgLeave = () => setImgIdx(0); // Тач-эквивалент hover-zones для мобиля: на мобильниках нет курсора, // поэтому проводим пальцем слева-направо по фото — миниатюры // переключаются как при hover-навигации. После окончания тача // последний индекс сохраняется (без handleImgLeave), как у Wildberries/OZON. const handleImgTouch = (e) => { const r = e.currentTarget.getBoundingClientRect(); const t = e.touches?.[0] || e.changedTouches?.[0]; if (!t) return; const x = t.clientX - r.left; const zone = Math.min(imgCount - 1, Math.max(0, Math.floor((x / r.width) * imgCount))); setImgIdx(zone); }; const copyArt = (e) => { e.preventDefault(); e.stopPropagation(); navigator.clipboard?.writeText(p.art); setCopied(true); setTimeout(() => setCopied(false), 1200); }; const bagesMap = { hit:{label:'ХИТ',cls:'badge-hit'}, new:{label:'НОВИНКА',cls:'badge-new'}, sale:{label:'-10%',cls:'badge-sale'} }; if (variant === 'table') { return (
{e.currentTarget.style.borderColor='var(--line)';e.currentTarget.style.background='var(--bg-soft)';}} onMouseLeave={e=>{e.currentTarget.style.borderColor='var(--line-2)';e.currentTarget.style.background='#FFFFFF';}}>
{p.name}
{p.brand} Арт. {p.art} {!!p.badges.length && p.badges.map((b,i)=>{bagesMap[b].label})}
{p.rating} · {p.reviews}
{fmtPrice(p.price)}
{p.oldPrice &&
{fmtPrice(p.oldPrice)}
}
{p.stock > 0 ? ( qty > 0 ? (
{qty}
) : ( ) ) : ( )}
); } if (variant === 'list') { return (
e.currentTarget.style.borderColor='var(--line)'} onMouseLeave={e=>e.currentTarget.style.borderColor='transparent'}>
{Array.from({length:imgCount}).map((_,i) => (
))}
{!!p.badges.length &&
{p.badges.map((b,i)=>{bagesMap[b].label})}
}
{Array.from({length:imgCount}).map((_,i) => ( ))}
{p.name} {p.specs && p.specs.length > 0 && (
    {p.specs.map((s,i) => (
  • {s}
  • ))}
)}
Бренд: {e.currentTarget.style.color='var(--red)';e.currentTarget.style.borderBottomColor='var(--red)';}} onMouseLeave={e=>{e.currentTarget.style.color='var(--text)';e.currentTarget.style.borderBottomColor='var(--text-3)';}}>{p.brand} Арт.: {p.art}
{p.rating} · {p.reviews} отзывов
{p.price.toLocaleString('ru-RU')} ₽/шт. {p.oldPrice && {fmtPrice(p.oldPrice)}}
{p.stock > 0 ? (qty > 0 ? onQty(p.id,n)} size="sm"/> : ) : }
); } // GRID — matches Figma mockup return (
{e.currentTarget.style.boxShadow='0 4px 16px rgba(0,0,0,0.06)';e.currentTarget.style.borderColor='var(--text-3)';}} onMouseLeave={e=>{e.currentTarget.style.boxShadow='none';e.currentTarget.style.borderColor='var(--line)';}}>
{Array.from({length:imgCount}).map((_,i) => (
))}
b!=='sale')}/> {p.badges.includes('sale') &&
-10%
}
{/* pagination dots — bottom-center, wrapped in pill (visual only; image switches on hover zones) */}
{Array.from({length:imgCount}).map((_,i) => ( ))}
{p.name}
{p.price.toLocaleString('ru-RU')} ₽/шт. {p.oldPrice && {fmtPrice(p.oldPrice)}}
Арт.: {p.art}
{p.rating}
{p.stock > 0 ? ( qty > 0 ? ( onQty(p.id,n)} style={{marginTop:'auto'}}/> ) : ( ) ) : ( )}
); } Object.assign(window, { PRODUCTS, fmtPrice, StockLabel, QtyControl, ProductCard });