206 lines
7.8 KiB
TypeScript
206 lines
7.8 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import Link from 'next/link';
|
|
import { ArrowRight, PackageX, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
|
import { Navigation, A11y } from 'swiper/modules';
|
|
import type { Swiper as SwiperType } from 'swiper';
|
|
import { ProductCard } from '../components/ProductCard';
|
|
import { Product } from '../types';
|
|
import { mapCatalogProductToProduct } from '../utils/productMapper';
|
|
import {
|
|
SectionHeaderSkeleton,
|
|
ProductCardSkeleton,
|
|
} from '../components/ui/Skeleton';
|
|
import { getSectionTitle } from '../lib/homepageDefaults';
|
|
|
|
import 'swiper/css';
|
|
import 'swiper/css/navigation';
|
|
|
|
interface NewArrivalsSectionProps {
|
|
config?: any;
|
|
products?: Product[];
|
|
loading?: boolean;
|
|
error?: boolean;
|
|
}
|
|
|
|
export const NewArrivalsSection: React.FC<NewArrivalsSectionProps> = ({
|
|
config,
|
|
products,
|
|
loading,
|
|
error,
|
|
}) => {
|
|
const [newArrivals, setNewArrivals] = useState<Product[]>([]);
|
|
const [canPrev, setCanPrev] = useState(false);
|
|
const [canNext, setCanNext] = useState(false);
|
|
/** Only true when slides overflow the viewport */
|
|
const [showNav, setShowNav] = useState(false);
|
|
const swiperRef = useRef<SwiperType | null>(null);
|
|
|
|
useEffect(() => {
|
|
const pinnedSlugs: string[] =
|
|
config?.metadata_json?.pinned_product_slugs ||
|
|
config?.metadata?.pinned_product_slugs ||
|
|
[];
|
|
|
|
if (pinnedSlugs.length > 0 && products && products.length > 0) {
|
|
const pinned = pinnedSlugs
|
|
.map((slug) => products.find((p) => p.slug === slug || p.id === slug))
|
|
.filter(Boolean) as Product[];
|
|
setNewArrivals(pinned.length > 0 ? pinned : products.slice(0, 10));
|
|
return;
|
|
}
|
|
|
|
if (products && products.length > 0) {
|
|
const arrivals = products.filter((p) => p.isNewArrival);
|
|
setNewArrivals((arrivals.length >= 5 ? arrivals : products).slice(0, 10));
|
|
return;
|
|
}
|
|
|
|
import('../services/api/catalogService').then(({ catalogService }) => {
|
|
catalogService
|
|
.getProductsPaginated({ sort: 'newest', limit: 10 })
|
|
.then((res: any) => {
|
|
if (res?.products?.length > 0) {
|
|
const mapped = res.products.map((p: any) =>
|
|
mapCatalogProductToProduct(p)
|
|
);
|
|
setNewArrivals(mapped);
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
});
|
|
}, [config, products, error]);
|
|
|
|
const title = getSectionTitle(config, 'editorial_story');
|
|
|
|
|
|
const syncNav = (swiper: SwiperType) => {
|
|
if (!swiper || !swiper.slides) return;
|
|
const overflow = !swiper.isLocked && (swiper.slides?.length || 0) > 0;
|
|
setShowNav(overflow);
|
|
setCanPrev(overflow && !swiper.isBeginning);
|
|
setCanNext(overflow && !swiper.isEnd);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const swiper = swiperRef.current;
|
|
if (!swiper || !swiper.slides) return;
|
|
requestAnimationFrame(() => syncNav(swiper));
|
|
}, [newArrivals.length]);
|
|
|
|
return (
|
|
<section className="py-6 bg-white font-sans">
|
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
|
{loading ? (
|
|
<div>
|
|
<SectionHeaderSkeleton />
|
|
<div className="border border-[#e5e5e5] grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5">
|
|
{[1, 2, 3, 4, 5].map((i) => (
|
|
<div
|
|
key={i}
|
|
className="border-r border-[#e5e5e5] last:border-r-0"
|
|
>
|
|
<ProductCardSkeleton />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : !loading && !error && newArrivals.length === 0 ? (
|
|
<div className="border border-[#e5e5e5] p-10 text-center flex flex-col items-center justify-center">
|
|
<PackageX className="w-10 h-10 text-gray-400 mb-2 stroke-[1.5]" />
|
|
<h3 className="text-sm font-bold text-gray-800">
|
|
No Products Available
|
|
</h3>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Check back later for new arrivals in our catalog.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
{/* Header — TechShop reference */}
|
|
<div className="flex items-center justify-between gap-4 pb-3.5 border-b border-[#e5e5e5]">
|
|
<h2 className="text-[18px] md:text-[20px] font-bold text-[#222] tracking-tight">
|
|
{title}
|
|
</h2>
|
|
<Link
|
|
href="/shop?sort=newest"
|
|
className="text-[13px] font-medium text-[#333] hover:text-primary transition-colors flex items-center gap-1.5 shrink-0"
|
|
>
|
|
View All
|
|
<ArrowRight size={14} strokeWidth={2.5} />
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Slider — both arrows always fully visible on hover (same style) */}
|
|
<div className="relative mt-5 group/arrivals">
|
|
{showNav ? (
|
|
<>
|
|
<button
|
|
type="button"
|
|
aria-label="Previous products"
|
|
onClick={() => swiperRef.current?.slidePrev()}
|
|
className="hidden md:flex absolute -left-4 top-[42%] -translate-y-1/2 z-30 w-10 h-10 rounded-full items-center justify-center shadow-[0_4px_14px_rgba(25,118,243,0.35)] bg-primary text-white hover:bg-primary-hover cursor-pointer opacity-0 pointer-events-none transition-opacity duration-200 group-hover/arrivals:opacity-100 group-hover/arrivals:pointer-events-auto"
|
|
>
|
|
<ChevronLeft size={20} strokeWidth={2.5} />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
aria-label="Next products"
|
|
onClick={() => swiperRef.current?.slideNext()}
|
|
className="hidden md:flex absolute -right-4 top-[42%] -translate-y-1/2 z-30 w-10 h-10 rounded-full items-center justify-center shadow-[0_4px_14px_rgba(25,118,243,0.35)] bg-primary text-white hover:bg-primary-hover cursor-pointer opacity-0 pointer-events-none transition-opacity duration-200 group-hover/arrivals:opacity-100 group-hover/arrivals:pointer-events-auto"
|
|
>
|
|
<ChevronRight size={20} strokeWidth={2.5} />
|
|
</button>
|
|
</>
|
|
) : null}
|
|
|
|
<div className="border border-[#e5e5e5] bg-white overflow-hidden">
|
|
<Swiper
|
|
modules={[Navigation, A11y]}
|
|
noSwiping
|
|
noSwipingSelector="button, a"
|
|
onSwiper={(swiper) => {
|
|
swiperRef.current = swiper;
|
|
requestAnimationFrame(() => syncNav(swiper));
|
|
}}
|
|
onSlideChange={syncNav}
|
|
onResize={syncNav}
|
|
onBreakpoint={syncNav}
|
|
onLock={() => {
|
|
setShowNav(false);
|
|
setCanPrev(false);
|
|
setCanNext(false);
|
|
}}
|
|
onUnlock={(swiper) => syncNav(swiper)}
|
|
watchOverflow
|
|
spaceBetween={0}
|
|
slidesPerView={2}
|
|
breakpoints={{
|
|
640: { slidesPerView: 3 },
|
|
1024: { slidesPerView: 4 },
|
|
1280: { slidesPerView: 5 },
|
|
}}
|
|
>
|
|
{newArrivals.map((product) => (
|
|
<SwiperSlide
|
|
key={product.id || product.slug}
|
|
className="!h-auto border-r border-[#e5e5e5] last:border-r-0"
|
|
>
|
|
<ProductCard
|
|
product={product}
|
|
products={newArrivals}
|
|
variant="techshop"
|
|
/>
|
|
</SwiperSlide>
|
|
))}
|
|
</Swiper>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|