ifixkart-storefront/sections/NewArrivalsSection.tsx

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