import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { VariableProduct } from '../types';
import { ChevronLeft, ChevronRight, Eye, Sparkles, Shuffle, Star, Heart, FileCode } from 'lucide-react';
interface ProductCascadeWidgetProps {
products: VariableProduct[];
onSelectProduct: (product: VariableProduct) => void;
favoriteIds?: string[];
onToggleFavorite?: (productId: string) => void;
title?: string;
subtitle?: string;
onOpenSnippetModal?: () => void;
}
export const ProductCascadeWidget: React.FC = ({
products,
onSelectProduct,
favoriteIds = [],
onToggleFavorite,
title = 'Особливі та вибрані речі',
subtitle = 'Гортайте пальцем в сторони: плавний рух каскаду з виділеним центральним фото',
onOpenSnippetModal
}) => {
const [filterMode, setFilterMode] = useState<'featured' | 'random' | 'recent'>('featured');
const [activeIndex, setActiveIndex] = useState(0);
const scrollContainerRef = useRef(null);
const isMouseDownRef = useRef(false);
const startMouseXRef = useRef(0);
const startScrollLeftRef = useRef(0);
const hasMovedMouseRef = useRef(false);
const rafIdRef = useRef(null);
// Responsive screen width detection
const [windowWidth, setWindowWidth] = useState(() =>
typeof window !== 'undefined' ? window.innerWidth : 1200
);
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Responsive card dimensions to fill the viewport seamlessly
const cardWidth = useMemo(() => {
if (windowWidth < 640) return 240;
if (windowWidth < 1024) return 270;
if (windowWidth < 1600) return 300;
return 330;
}, [windowWidth]);
// Base filtered products
const baseItems = useMemo(() => {
if (!products || products.length === 0) return [];
if (filterMode === 'recent') {
return [...products].sort((a, b) => b.id.localeCompare(a.id));
}
if (filterMode === 'random') {
return [...products].sort(() => 0.5 - Math.random());
}
return products;
}, [products, filterMode]);
// Repeat items so on ultra-wide screens the carousel extends from edge to edge without empty space
const displayItems = useMemo(() => {
if (baseItems.length === 0) return [];
return [
...baseItems.map((p) => ({ ...p, uniqueId: `${p.id}-s1` })),
...baseItems.map((p) => ({ ...p, uniqueId: `${p.id}-s2` })),
...baseItems.map((p) => ({ ...p, uniqueId: `${p.id}-s3` })),
...baseItems.map((p) => ({ ...p, uniqueId: `${p.id}-s4` }))
];
}, [baseItems]);
const total = displayItems.length;
// Center scroll on item by index
const scrollToItem = useCallback(
(index: number, behavior: ScrollBehavior = 'smooth') => {
const container = scrollContainerRef.current;
if (!container) return;
const cardElements = container.querySelectorAll('.gidna-cascade-card');
if (cardElements[index]) {
const card = cardElements[index];
const containerWidth = container.clientWidth;
const targetScrollLeft = card.offsetLeft - containerWidth / 2 + card.clientWidth / 2;
container.scrollTo({ left: targetScrollLeft, behavior });
}
},
[]
);
// Initial centering on mount or filter change
useEffect(() => {
const timer = setTimeout(() => {
if (baseItems.length > 0) {
// Start near the beginning of set 2
const initialIdx = Math.min(baseItems.length, total - 1);
setActiveIndex(initialIdx);
scrollToItem(initialIdx, 'auto');
}
}, 60);
return () => clearTimeout(timer);
}, [filterMode, baseItems.length, total, scrollToItem]);
// Track center item during scroll with requestAnimationFrame for 60fps performance
const handleScroll = useCallback(() => {
const container = scrollContainerRef.current;
if (!container) return;
const containerCenter = container.scrollLeft + container.clientWidth / 2;
const cardElements = container.querySelectorAll('.gidna-cascade-card');
let closestIndex = 0;
let minDistance = Infinity;
cardElements.forEach((card, index) => {
const cardCenter = card.offsetLeft + card.clientWidth / 2;
const distance = Math.abs(containerCenter - cardCenter);
if (distance < minDistance) {
minDistance = distance;
closestIndex = index;
}
});
if (closestIndex !== activeIndex) {
setActiveIndex(closestIndex);
}
}, [activeIndex]);
const onScrollThrottled = useCallback(() => {
if (rafIdRef.current !== null) return;
rafIdRef.current = requestAnimationFrame(() => {
handleScroll();
rafIdRef.current = null;
});
}, [handleScroll]);
// Previous and Next buttons
const handlePrev = useCallback(() => {
if (activeIndex > 0) {
scrollToItem(activeIndex - 1);
}
}, [activeIndex, scrollToItem]);
const handleNext = useCallback(() => {
if (activeIndex < total - 1) {
scrollToItem(activeIndex + 1);
}
}, [activeIndex, total, scrollToItem]);
// Keyboard navigation
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'ArrowLeft') handlePrev();
if (e.key === 'ArrowRight') handleNext();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handlePrev, handleNext]);
// Desktop Mouse Drag Handlers
const handleMouseDown = (e: React.MouseEvent) => {
const container = scrollContainerRef.current;
if (!container) return;
isMouseDownRef.current = true;
hasMovedMouseRef.current = false;
startMouseXRef.current = e.pageX - container.offsetLeft;
startScrollLeftRef.current = container.scrollLeft;
};
const handleMouseMove = (e: React.MouseEvent) => {
const container = scrollContainerRef.current;
if (!isMouseDownRef.current || !container) return;
e.preventDefault();
const x = e.pageX - container.offsetLeft;
const walk = (x - startMouseXRef.current) * 1.5;
if (Math.abs(walk) > 6) {
hasMovedMouseRef.current = true;
}
container.scrollLeft = startScrollLeftRef.current - walk;
};
const handleMouseUpOrLeave = () => {
isMouseDownRef.current = false;
};
if (!displayItems || displayItems.length === 0) {
return null;
}
const activeProduct = displayItems[activeIndex] || displayItems[0];
const isFavoriteActive = activeProduct ? favoriteIds.includes(activeProduct.id) : false;
return (
{/* 1. Header with Mode Switcher (Особливі / Рандомні / Останні новинки) */}
Добірка речей магазину «Гідна»
{filterMode === 'featured' && 'Особливі та вибрані речі'}
{filterMode === 'random' && 'Випадкова добірка для вас'}
{filterMode === 'recent' && 'Останні надходження сезону'}
{subtitle}
{/* Controls: Mode Tabs & Snippet Button */}
{onOpenSnippetModal && (
Сніпет віджета (PHP)
)}
{/* 3 Quick Mode Tabs */}
setFilterMode('featured')}
className={`px-3 py-1.5 text-xs font-serif rounded-xs transition-colors cursor-pointer flex items-center gap-1.5 ${
filterMode === 'featured'
? 'bg-[#231c16] text-[#faecc8] font-bold shadow-xs'
: 'text-[#61503e] hover:text-[#231c16]'
}`}
>
Особливі
setFilterMode('random')}
className={`px-3 py-1.5 text-xs font-serif rounded-xs transition-colors cursor-pointer flex items-center gap-1.5 ${
filterMode === 'random'
? 'bg-[#231c16] text-[#faecc8] font-bold shadow-xs'
: 'text-[#61503e] hover:text-[#231c16]'
}`}
>
Рандомні
setFilterMode('recent')}
className={`px-3 py-1.5 text-xs font-serif rounded-xs transition-colors cursor-pointer flex items-center gap-1.5 ${
filterMode === 'recent'
? 'bg-[#231c16] text-[#faecc8] font-bold shadow-xs'
: 'text-[#61503e] hover:text-[#231c16]'
}`}
>
Останні
{/* 2. FULL-WIDTH EDGE-TO-EDGE SWIPE / SCROLL STREAM */}
{/* Navigation Arrow Left (Desktop) */}
{/* Navigation Arrow Right (Desktop) */}
{/*
HORIZONTAL SCROLL/SWIPE TRACK:
- Spans 100% of the screen width (edge to edge)
- Native iOS/Android touch swipe with buttery smooth inertia
- Snaps softly to center without abrupt jumps
- Mouse drag supported for desktop
*/}
{displayItems.map((product, index) => {
const isCenter = index === activeIndex;
const activeImage = product.gallery[0];
const isFavorite = favoriteIds.includes(product.id);
return (
{
if (hasMovedMouseRef.current) return;
if (isCenter) {
onSelectProduct(product);
} else {
scrollToItem(index);
}
}}
className={`gidna-cascade-card shrink-0 transition-all duration-300 ease-out ${
isCenter ? 'z-30' : 'z-10'
}`}
style={{
width: `${cardWidth}px`,
scrollSnapAlign: 'center',
transform: isCenter ? 'scale(1.05)' : 'scale(0.92)',
opacity: isCenter ? 1 : 0.85
}}
>
{/*
PHOTO CARD WITH VINTAGE CORNERS:
Notice: NO titles and NO prices on the photo cards!
Clean, luxurious, high-fashion presentation.
*/}
{/* 4 Authentic Photo Album Corners */}
{/* Favorite heart button */}
{onToggleFavorite && (
{
e.stopPropagation();
onToggleFavorite(product.id);
}}
className={`absolute top-3.5 right-3.5 z-20 w-7 h-7 rounded-full flex items-center justify-center transition-all ${
isFavorite
? 'bg-[#fcf7ed] text-[#b33939] border border-[#d6a5a5] shadow-sm'
: 'bg-[#faf6ed]/90 text-[#716150] border border-[#cfc0ab] hover:text-[#b33939] hover:bg-white'
}`}
title={isFavorite ? 'В улюблених' : 'Додати в улюблені'}
>
)}
{/* Photograph */}
{/* Quick view badge overlay ONLY on center card */}
{isCenter && (
Натисніть для вибору кольору
)}
);
})}
{/*
3. DEDICATED HIGHLIGHTED PRESENTATION:
Exclusively displays title, price, and actions ONLY FOR THE CENTRAL HIGHLIGHTED PHOTO
*/}
{activeProduct && (
{activeProduct.category}
✦ Виділений товар
{activeProduct.title}
{activeProduct.basePrice} грн
onSelectProduct(activeProduct)}
className="inline-flex items-center gap-2 px-5 py-2 bg-[#231c16] hover:bg-[#8c714d] text-[#faecc8] text-sm font-serif font-bold rounded-xs shadow-md hover:shadow-lg transition-all duration-200 cursor-pointer"
>
Обрати розмір і колір
)}
{/* 4. Pagination Dots for base unique items */}
{baseItems.length > 1 && (
{baseItems.map((item, idx) => {
const isDotActive = activeProduct?.id === item.id;
return (
{
// Find the closest index matching this base product
const targetIdx = displayItems.findIndex(
(p, i) => p.id === item.id && Math.abs(i - activeIndex) <= baseItems.length / 2 + 1
);
if (targetIdx !== -1) {
scrollToItem(targetIdx);
}
}}
className={`transition-all duration-300 rounded-full cursor-pointer ${
isDotActive
? 'w-7 h-2 bg-[#8c714d]'
: 'w-2 h-2 bg-[#d4c5af] hover:bg-[#a6927c]'
}`}
aria-label={`Перейти до товару: ${item.title}`}
/>
);
})}
)}
);
};
♥
Батник жіночий на флісі з новорічним принтом 856 грн
Оберіть розмір:
📐 Таблиця замірів
Обрано: Не обрано (наявний на складі)
🚀 Швидка доставка Новою Поштою або Укрпоштою
💳 Накладений платіж без будь-яких передоплат
🔄 Гарантований обмін/повернення протягом 14 днів
Артикул: 241R4002
Характеристики моделі:
• Довжина рукава:
З довгими рукавами
• Тип тканини:
Трьохнитка
• Особливості тканини:
Тягнеться
• Сезон:
Осінній; Зимовий; Демісезонний
• Склад:
98% бавовна, 2% спандекс
• Стиль:
Повсякденний (casual)
• Особливості крою:
Вільний
• Розмірна група:
Норма; Напівбатал
Заміри (фактичні заміри можуть відрізнятись +- 2 см)
Розмір
L-XL
S-M
Довжина виробу
54 см
56 см
Довжина рукава (см)
51 см
53 см
Напівобхват грудей
62 см
66 см
Ширина плечей
65 см
66 см
Відгуки
Відгуків немає, поки що.