256 lines
9.9 KiB
TypeScript
256 lines
9.9 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useRef, useEffect } from "react"
|
||
import Image from "next/image"
|
||
import { Heart, ChevronLeft, ChevronRight } from "lucide-react"
|
||
import Link from "next/link"
|
||
import { Product, formatPrice } from "../data/products"
|
||
import { motion } from "framer-motion"
|
||
|
||
interface NewArrivalsProps {
|
||
products: Product[]
|
||
}
|
||
|
||
export default function NewArrivals({ products }: NewArrivalsProps) {
|
||
const [hoveredProduct, setHoveredProduct] = useState<number | null>(null)
|
||
const [favorites, setFavorites] = useState<number[]>([])
|
||
const sliderRef = useRef<HTMLDivElement>(null)
|
||
const [showLeftArrow, setShowLeftArrow] = useState(false)
|
||
const [showRightArrow, setShowRightArrow] = useState(true)
|
||
const [isDragging, setIsDragging] = useState(false)
|
||
const [startX, setStartX] = useState(0)
|
||
const [scrollLeftValue, setScrollLeftValue] = useState(0)
|
||
const [currentSlide, setCurrentSlide] = useState(0)
|
||
const [slidesPerView, setSlidesPerView] = useState(4)
|
||
|
||
// Функция для добавления/удаления товара из избранного
|
||
const toggleFavorite = (id: number, e: React.MouseEvent) => {
|
||
e.stopPropagation()
|
||
e.preventDefault()
|
||
setFavorites((prev) => (prev.includes(id) ? prev.filter((itemId) => itemId !== id) : [...prev, id]))
|
||
}
|
||
|
||
// Определение количества слайдов на экране в зависимости от размера экрана
|
||
useEffect(() => {
|
||
const handleResize = () => {
|
||
const width = window.innerWidth;
|
||
if (width < 640) {
|
||
setSlidesPerView(1);
|
||
} else if (width < 768) {
|
||
setSlidesPerView(2);
|
||
} else if (width < 1024) {
|
||
setSlidesPerView(3);
|
||
} else {
|
||
setSlidesPerView(4);
|
||
}
|
||
};
|
||
|
||
handleResize();
|
||
window.addEventListener("resize", handleResize);
|
||
return () => window.removeEventListener("resize", handleResize);
|
||
}, []);
|
||
|
||
// Обновление состояния стрелок при скролле
|
||
const updateArrowVisibility = () => {
|
||
if (sliderRef.current) {
|
||
const { scrollLeft, scrollWidth, clientWidth } = sliderRef.current
|
||
setShowLeftArrow(scrollLeft > 0)
|
||
setShowRightArrow(scrollLeft < scrollWidth - clientWidth - 5) // 5px buffer
|
||
|
||
// Обновление текущего слайда
|
||
if (clientWidth > 0) {
|
||
const slideWidth = scrollWidth / products.length;
|
||
const newCurrentSlide = Math.round(scrollLeft / slideWidth);
|
||
setCurrentSlide(newCurrentSlide);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Инициализация и обработка изменений размера
|
||
useEffect(() => {
|
||
updateArrowVisibility()
|
||
window.addEventListener("resize", updateArrowVisibility)
|
||
return () => window.removeEventListener("resize", updateArrowVisibility)
|
||
}, [])
|
||
|
||
// Обработчики скролла
|
||
const handleScroll = () => {
|
||
updateArrowVisibility()
|
||
}
|
||
|
||
const scrollLeft = () => {
|
||
if (sliderRef.current) {
|
||
const itemWidth = sliderRef.current.scrollWidth / products.length;
|
||
const newScrollLeft = Math.max(0, sliderRef.current.scrollLeft - (itemWidth * slidesPerView));
|
||
sliderRef.current.scrollTo({ left: newScrollLeft, behavior: "smooth" });
|
||
}
|
||
}
|
||
|
||
const scrollRight = () => {
|
||
if (sliderRef.current) {
|
||
const itemWidth = sliderRef.current.scrollWidth / products.length;
|
||
const newScrollLeft = Math.min(
|
||
sliderRef.current.scrollWidth - sliderRef.current.clientWidth,
|
||
sliderRef.current.scrollLeft + (itemWidth * slidesPerView)
|
||
);
|
||
sliderRef.current.scrollTo({ left: newScrollLeft, behavior: "smooth" });
|
||
}
|
||
}
|
||
|
||
// Обработчики перетаскивания (для мобильных)
|
||
const handleMouseDown = (e: React.MouseEvent) => {
|
||
setIsDragging(true)
|
||
setStartX(e.pageX - (sliderRef.current?.offsetLeft || 0))
|
||
setScrollLeftValue(sliderRef.current?.scrollLeft || 0)
|
||
}
|
||
|
||
const handleMouseUp = () => {
|
||
setIsDragging(false)
|
||
}
|
||
|
||
const handleMouseMove = (e: React.MouseEvent) => {
|
||
if (!isDragging) return
|
||
e.preventDefault()
|
||
if (sliderRef.current) {
|
||
const x = e.pageX - (sliderRef.current.offsetLeft || 0)
|
||
const walk = (x - startX) * 2 // Скорость скролла
|
||
sliderRef.current.scrollLeft = scrollLeftValue - walk
|
||
}
|
||
}
|
||
|
||
const handleMouseLeave = () => {
|
||
setIsDragging(false)
|
||
}
|
||
|
||
// Обработчики тач-событий
|
||
const handleTouchStart = (e: React.TouchEvent) => {
|
||
if (sliderRef.current) {
|
||
setStartX(e.touches[0].pageX - (sliderRef.current.offsetLeft || 0))
|
||
setScrollLeftValue(sliderRef.current.scrollLeft)
|
||
}
|
||
}
|
||
|
||
const handleTouchMove = (e: React.TouchEvent) => {
|
||
if (sliderRef.current) {
|
||
const x = e.touches[0].pageX - (sliderRef.current.offsetLeft || 0)
|
||
const walk = (x - startX) * 2
|
||
sliderRef.current.scrollLeft = scrollLeftValue - walk
|
||
}
|
||
}
|
||
|
||
// Переход к определенному слайду
|
||
const goToSlide = (index: number) => {
|
||
if (sliderRef.current) {
|
||
const itemWidth = sliderRef.current.scrollWidth / products.length;
|
||
sliderRef.current.scrollTo({ left: itemWidth * index, behavior: "smooth" });
|
||
}
|
||
};
|
||
|
||
return (
|
||
<section id="new-arrivals" className="my-12 px-4 md:px-8 max-w-7xl mx-auto relative">
|
||
<h2 className="text-2xl md:text-3xl font-bold mb-6 font-['Playfair_Display']">Новинки</h2>
|
||
|
||
{/* Контейнер слайдера с кнопками навигации */}
|
||
<div className="relative">
|
||
{/* Кнопка влево */}
|
||
{showLeftArrow && (
|
||
<button
|
||
onClick={scrollLeft}
|
||
className="absolute -left-4 top-1/2 -translate-y-1/2 z-10 bg-white/80 hover:bg-white rounded-full p-2 shadow-md transition-all"
|
||
aria-label="Предыдущие товары"
|
||
>
|
||
<ChevronLeft className="w-6 h-6" />
|
||
</button>
|
||
)}
|
||
|
||
{/* Слайдер продуктов */}
|
||
<div
|
||
ref={sliderRef}
|
||
className="flex overflow-x-auto gap-4 md:gap-6 pb-6 scrollbar-hide snap-x snap-mandatory"
|
||
onScroll={handleScroll}
|
||
onMouseDown={handleMouseDown}
|
||
onMouseUp={handleMouseUp}
|
||
onMouseMove={handleMouseMove}
|
||
onMouseLeave={handleMouseLeave}
|
||
onTouchStart={handleTouchStart}
|
||
onTouchMove={handleTouchMove}
|
||
onTouchEnd={handleMouseUp}
|
||
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
|
||
>
|
||
{products.map((product) => (
|
||
<motion.div
|
||
key={product.id}
|
||
className="flex-none w-[280px] sm:w-[320px] md:w-[300px] lg:w-[280px] xl:w-[300px] snap-start"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.5, delay: product.id * 0.05 }}
|
||
whileHover={{ y: -5, transition: { duration: 0.2 } }}
|
||
>
|
||
<Link
|
||
href={`/product/${product.slug}`}
|
||
className="block h-full"
|
||
onMouseEnter={() => setHoveredProduct(product.id)}
|
||
onMouseLeave={() => setHoveredProduct(null)}
|
||
>
|
||
<div className="relative overflow-hidden rounded-xl">
|
||
<div className="aspect-[3/4] relative overflow-hidden rounded-xl">
|
||
<Image
|
||
src={
|
||
hoveredProduct === product.id && product.images.length > 1 ? product.images[1] : product.images[0]
|
||
}
|
||
alt={product.name}
|
||
fill
|
||
sizes="(max-width: 640px) 280px, (max-width: 768px) 320px, (max-width: 1024px) 300px, 280px"
|
||
className="object-cover transition-all duration-500 hover:scale-105"
|
||
/>
|
||
{product.isNew && (
|
||
<span className="absolute top-4 left-4 bg-black text-white text-sm py-1 px-3 rounded">Новинка</span>
|
||
)}
|
||
<button
|
||
onClick={(e) => toggleFavorite(product.id, e)}
|
||
className="absolute top-4 right-4 bg-white/80 hover:bg-white rounded-full p-2 transition-all"
|
||
aria-label={favorites.includes(product.id) ? "Удалить из избранного" : "Добавить в избранное"}
|
||
>
|
||
<Heart
|
||
className={`w-5 h-5 ${favorites.includes(product.id) ? "fill-red-500 text-red-500" : "text-gray-700"}`}
|
||
/>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="mt-4">
|
||
<h3 className="text-lg font-medium">{product.name}</h3>
|
||
<p className="mt-1 text-lg font-bold">{formatPrice(product.price)} ₽</p>
|
||
</div>
|
||
</Link>
|
||
</motion.div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Кнопка вправо */}
|
||
{showRightArrow && (
|
||
<button
|
||
onClick={scrollRight}
|
||
className="absolute -right-4 top-1/2 -translate-y-1/2 z-10 bg-white/80 hover:bg-white rounded-full p-2 shadow-md transition-all"
|
||
aria-label="Следующие товары"
|
||
>
|
||
<ChevronRight className="w-6 h-6" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Индикаторы слайдов (точки) */}
|
||
<div className="flex justify-center mt-6 space-x-2">
|
||
{Array.from({ length: Math.ceil(products.length / slidesPerView) }).map((_, index) => (
|
||
<button
|
||
key={index}
|
||
onClick={() => goToSlide(index * slidesPerView)}
|
||
className={`w-2 h-2 rounded-full transition-all ${
|
||
Math.floor(currentSlide / slidesPerView) === index ? "bg-black scale-150" : "bg-gray-300"
|
||
}`}
|
||
aria-label={`Перейти к слайду ${index + 1}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|