260 lines
9.6 KiB
TypeScript
260 lines
9.6 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import Link from 'next/link';
|
|
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 { ArrowRight, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
import { Product } from '../types';
|
|
import { getImageUrl } from '../lib/utils';
|
|
import { getSectionMeta, getSectionTitle } from '../lib/homepageDefaults';
|
|
|
|
import 'swiper/css';
|
|
import 'swiper/css/navigation';
|
|
|
|
const DEFAULT_BANNER_IMAGE =
|
|
'https://images.unsplash.com/photo-1610945415295-d9bbf067e59c?w=600&auto=format&fit=crop&q=80';
|
|
|
|
interface RecentlyLaunchedSectionProps {
|
|
config?: any;
|
|
products?: Product[];
|
|
loading?: boolean;
|
|
error?: boolean;
|
|
}
|
|
|
|
export const RecentlyLaunchedSection: React.FC<RecentlyLaunchedSectionProps> = ({
|
|
config,
|
|
products,
|
|
error,
|
|
}) => {
|
|
const [carouselProducts, setCarouselProducts] = useState<Product[]>([]);
|
|
const [canSlide, setCanSlide] = 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[];
|
|
setCarouselProducts(pinned.length > 0 ? pinned : products.slice(0, 8));
|
|
return;
|
|
}
|
|
|
|
const rawItems = config?.metadata_json?.items || config?.metadata?.items;
|
|
if (rawItems && rawItems.length > 0) {
|
|
setCarouselProducts(
|
|
rawItems.map((it: any) => ({
|
|
id: it.id || String(Math.random()),
|
|
name: it.title || it.name,
|
|
price: it.price || 0,
|
|
oldPrice: it.oldPrice || it.originalPrice,
|
|
discount: it.discount,
|
|
image: getImageUrl(it.image),
|
|
rating: it.rating || 5.0,
|
|
reviewsCount: it.reviewsCount || 12,
|
|
category: 'Accessories',
|
|
brand: 'Tech',
|
|
slug: it.slug || it.id,
|
|
}))
|
|
);
|
|
} else if (products && products.length > 0) {
|
|
const recent = products.filter((p) => p.isRecentlyLaunched);
|
|
setCarouselProducts(recent.length > 0 ? recent : products.slice(0, 8));
|
|
} else {
|
|
setCarouselProducts([]);
|
|
}
|
|
}, [config, products, error]);
|
|
|
|
const title = getSectionTitle(config, 'recently_launched');
|
|
|
|
const bannerTitle =
|
|
config?.metadata_json?.banner_title ||
|
|
config?.metadata?.banner_title ||
|
|
'iPhone Prodigy';
|
|
const bannerSubtitle =
|
|
config?.metadata_json?.banner_subtitle ||
|
|
config?.metadata?.banner_subtitle ||
|
|
'Performance in Motion';
|
|
const rawBannerImage =
|
|
config?.metadata_json?.banner_image || config?.metadata?.banner_image || '';
|
|
const bannerImage = getImageUrl(rawBannerImage) || DEFAULT_BANNER_IMAGE;
|
|
const bannerLink =
|
|
config?.metadata_json?.banner_link ||
|
|
config?.metadata?.banner_link ||
|
|
'/shop';
|
|
|
|
const syncOverflow = (swiper: SwiperType) => {
|
|
if (!swiper) return;
|
|
setCanSlide(!swiper.isLocked && carouselProducts.length > 0);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const swiper = swiperRef.current;
|
|
if (!swiper) return;
|
|
setTimeout(() => {
|
|
if (swiper.navigation) {
|
|
try {
|
|
swiper.navigation.destroy();
|
|
swiper.navigation.init();
|
|
swiper.navigation.update();
|
|
} catch (e) {}
|
|
}
|
|
syncOverflow(swiper);
|
|
}, 0);
|
|
}, [carouselProducts.length]);
|
|
|
|
if (carouselProducts.length === 0) {
|
|
return (
|
|
<section className="py-8 bg-white font-sans">
|
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
|
<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>
|
|
</div>
|
|
<div className="mt-5 border border-[#e5e5e5] rounded-lg p-10 text-center text-[13px] text-[#777]">
|
|
Recently launched products will appear here once they are in the catalog.
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<section className="py-8 bg-white font-sans">
|
|
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px]">
|
|
{/* Header — title + View All, line underneath (not joined to content) */}
|
|
<div className="flex items-center justify-between gap-4 pb-3.5 border-b border-[#e5e5e5]">
|
|
<div className="text-left">
|
|
<h2 className="text-[18px] md:text-[20px] font-bold text-[#222] tracking-tight">
|
|
{title}
|
|
</h2>
|
|
</div>
|
|
<Link
|
|
href="/shop?highlight=recent"
|
|
className="text-[13px] text-[#333] hover:text-primary transition-colors flex items-center gap-1.5 shrink-0"
|
|
>
|
|
View All
|
|
<ArrowRight size={14} strokeWidth={2} />
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Banner + product strip */}
|
|
<div className="mt-5 flex flex-col lg:flex-row gap-4 lg:gap-5 items-stretch">
|
|
{/* Lavender feature card */}
|
|
<Link
|
|
href={bannerLink}
|
|
className="relative w-full lg:w-[260px] xl:w-[280px] shrink-0 rounded-2xl overflow-hidden bg-[#F3E8FF] min-h-[380px] lg:min-h-0 flex flex-col group/banner"
|
|
>
|
|
<div className="relative z-10 pt-7 px-6 md:px-7 text-left">
|
|
<h3 className="text-[22px] md:text-[24px] font-bold text-[#1a1a1a] leading-tight mb-1.5">
|
|
{bannerTitle}
|
|
</h3>
|
|
<p className="text-[13px] text-[#333] mb-4">{bannerSubtitle}</p>
|
|
<span className="text-[13px] font-bold text-[#1a1a1a] underline underline-offset-4 decoration-1 group-hover/banner:text-primary transition-colors">
|
|
Shop Now
|
|
</span>
|
|
</div>
|
|
|
|
<div className="absolute inset-x-0 bottom-0 top-[38%] flex items-end justify-center pointer-events-none select-none">
|
|
<img
|
|
src={bannerImage}
|
|
alt={bannerTitle}
|
|
className="h-[108%] w-auto max-w-[95%] object-contain object-bottom translate-y-[6%]"
|
|
/>
|
|
</div>
|
|
</Link>
|
|
|
|
{/* Product carousel with vertical dividers */}
|
|
<div className="relative group/rl flex-1 min-w-0 border border-[#e5e5e5] bg-white">
|
|
{/* Blue circular arrows — between banner & strip / far right */}
|
|
<button
|
|
type="button"
|
|
aria-label="Previous products"
|
|
className={`rl-swiper-prev absolute left-0 top-[42%] z-20 -translate-x-1/2 w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center shadow-md transition-opacity duration-200 hover:brightness-95 ${
|
|
canSlide
|
|
? 'opacity-100'
|
|
: 'opacity-0 pointer-events-none invisible'
|
|
}`}
|
|
>
|
|
<ChevronLeft size={20} strokeWidth={2.5} />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
aria-label="Next products"
|
|
className={`rl-swiper-next absolute right-0 top-[42%] z-20 translate-x-1/2 w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center shadow-md transition-opacity duration-200 hover:brightness-95 ${
|
|
canSlide
|
|
? 'opacity-100'
|
|
: 'opacity-0 pointer-events-none invisible'
|
|
}`}
|
|
>
|
|
<ChevronRight size={20} strokeWidth={2.5} />
|
|
</button>
|
|
|
|
<Swiper
|
|
modules={[Navigation, A11y]}
|
|
onSwiper={(swiper) => {
|
|
swiperRef.current = swiper;
|
|
setTimeout(() => {
|
|
if (
|
|
swiper &&
|
|
swiper.params &&
|
|
swiper.params.navigation &&
|
|
typeof swiper.params.navigation !== 'boolean'
|
|
) {
|
|
swiper.params.navigation.prevEl = '.rl-swiper-prev';
|
|
swiper.params.navigation.nextEl = '.rl-swiper-next';
|
|
}
|
|
if (swiper?.navigation) {
|
|
try {
|
|
swiper.navigation.destroy();
|
|
swiper.navigation.init();
|
|
swiper.navigation.update();
|
|
} catch (e) {}
|
|
}
|
|
syncOverflow(swiper);
|
|
}, 0);
|
|
}}
|
|
onResize={syncOverflow}
|
|
onBreakpoint={syncOverflow}
|
|
onLock={() => setCanSlide(false)}
|
|
onUnlock={() => setCanSlide(true)}
|
|
navigation={{
|
|
prevEl: '.rl-swiper-prev',
|
|
nextEl: '.rl-swiper-next',
|
|
}}
|
|
watchOverflow
|
|
spaceBetween={0}
|
|
slidesPerView={2}
|
|
breakpoints={{
|
|
640: { slidesPerView: 2 },
|
|
900: { slidesPerView: 3 },
|
|
1200: { slidesPerView: 4 },
|
|
}}
|
|
className="h-full"
|
|
>
|
|
{carouselProducts.map((product) => (
|
|
<SwiperSlide
|
|
key={product.id || product.slug}
|
|
className="!h-auto border-r border-[#e5e5e5] last:border-r-0"
|
|
>
|
|
<ProductCard
|
|
product={product}
|
|
products={carouselProducts}
|
|
/>
|
|
</SwiperSlide>
|
|
))}
|
|
</Swiper>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|