dressed_for_succes_store/components/NewArrivals.tsx

256 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
)
}