198 lines
8.4 KiB
TypeScript
198 lines
8.4 KiB
TypeScript
import { Swiper, SwiperSlide } from 'swiper/react';
|
|
import { Autoplay, Pagination, Navigation } from 'swiper/modules';
|
|
import { Link } from 'react-router-dom';
|
|
import type { EditorialCarouselSectionPayload } from '@homaroma/types';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { getLocalizedField } from '../utils/localization';
|
|
|
|
// Import Swiper styles
|
|
import 'swiper/css';
|
|
import 'swiper/css/pagination';
|
|
import 'swiper/css/navigation';
|
|
|
|
interface DynamicEditorialCarouselProps {
|
|
payload: EditorialCarouselSectionPayload;
|
|
}
|
|
|
|
export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCarouselProps) {
|
|
const { i18n } = useTranslation();
|
|
const lang = i18n.language;
|
|
const title = getLocalizedField(payload.title, lang);
|
|
const slides = (payload.slides || []).map((slide) => ({
|
|
...slide,
|
|
title: getLocalizedField(slide.title, lang),
|
|
description: getLocalizedField(slide.description, lang),
|
|
buttonText: getLocalizedField(slide.buttonText, lang),
|
|
category: getLocalizedField(slide.category, lang)
|
|
}));
|
|
|
|
if (slides.length === 0) return null;
|
|
|
|
const renderTitle = (text: string) => {
|
|
if (!text) return null;
|
|
return text.split('[').map((part, i) => {
|
|
if (part.includes(']')) {
|
|
const [accent, rest] = part.split(']');
|
|
return (
|
|
<span key={i}>
|
|
<span className="text-pop-pink font-medium italic">
|
|
{accent}
|
|
</span>
|
|
{rest}
|
|
</span>
|
|
);
|
|
}
|
|
return <span key={i}>{part}</span>;
|
|
});
|
|
};
|
|
|
|
const bgMap: Record<string, string> = {
|
|
bg: 'bg-pop-bg',
|
|
black: 'bg-pop-black',
|
|
pink: 'bg-pop-pink',
|
|
yellow: 'bg-pop-yellow',
|
|
orange: 'bg-pop-orange',
|
|
red: 'bg-pop-red',
|
|
cyan: 'bg-pop-cyan',
|
|
green: 'bg-pop-green',
|
|
purple: 'bg-pop-purple',
|
|
};
|
|
|
|
const textMap: Record<string, string> = {
|
|
bg: 'text-pop-bg',
|
|
black: 'text-pop-black',
|
|
pink: 'text-pop-pink',
|
|
yellow: 'text-pop-yellow',
|
|
orange: 'text-pop-orange',
|
|
red: 'text-pop-red',
|
|
cyan: 'text-pop-cyan',
|
|
green: 'text-pop-green',
|
|
purple: 'text-pop-purple',
|
|
};
|
|
|
|
const bgClass = payload.backgroundColor && bgMap[payload.backgroundColor] ? bgMap[payload.backgroundColor] : 'bg-pop-bg';
|
|
const textClass = payload.textColor && textMap[payload.textColor] ? textMap[payload.textColor] : 'text-pop-black';
|
|
|
|
const isSplitScreen = payload.layout === 'split_screen';
|
|
const imagePosition = payload.imagePosition || 'left';
|
|
|
|
return (
|
|
<section className={`overflow-hidden py-16 md:py-24 ${bgClass} ${textClass}`}>
|
|
<div className={isSplitScreen ? "w-full space-y-12" : "ui-section-inner space-y-12"}>
|
|
<div className={`space-y-3 text-center ${isSplitScreen ? 'px-4 md:px-8' : ''}`}>
|
|
<h2 className="font-serif text-3xl font-light tracking-wide text-pop-pink md:text-5xl">
|
|
{renderTitle(title) || title}
|
|
</h2>
|
|
<div className="w-16 h-[1px] bg-pop-pink mx-auto"></div>
|
|
</div>
|
|
|
|
<div className="relative select-none pb-12">
|
|
<Swiper
|
|
modules={[Autoplay, Pagination, Navigation]}
|
|
spaceBetween={isSplitScreen ? 0 : 30}
|
|
slidesPerView={1}
|
|
breakpoints={isSplitScreen ? undefined : {
|
|
640: { slidesPerView: 2 },
|
|
1024: { slidesPerView: 3 }
|
|
}}
|
|
pagination={{ clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-pop-pink' }}
|
|
navigation={true}
|
|
autoplay={{ delay: 5000, disableOnInteraction: false }}
|
|
className="w-full"
|
|
>
|
|
{slides.map((slide, idx) => (
|
|
<SwiperSlide key={idx} className="h-auto flex flex-col">
|
|
{isSplitScreen ? (
|
|
<div className={`group grid grid-cols-1 bg-night-900 overflow-hidden transition-all duration-300 ${
|
|
imagePosition === 'left' ? 'md:grid-cols-[65%_35%]' : 'md:grid-cols-[35%_65%]'
|
|
}`}>
|
|
<div className={`overflow-hidden relative ${
|
|
imagePosition === 'left' ? 'order-1 md:order-1' : 'order-1 md:order-2'
|
|
} aspect-[4/3] md:aspect-auto h-64 md:h-full min-h-[300px] md:min-h-[500px] lg:min-h-[600px]`}>
|
|
<img
|
|
src={slide.imageUrl}
|
|
alt={slide.title}
|
|
className="w-full h-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
|
|
onError={(e) => {
|
|
(e.target as HTMLImageElement).style.display = 'none';
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className={`p-6 md:p-16 lg:p-24 flex flex-col justify-center space-y-8 ${
|
|
imagePosition === 'left' ? 'order-2 md:order-2' : 'order-2 md:order-1'
|
|
}`}>
|
|
<div className="space-y-3">
|
|
<span className="block text-[10px] font-bold uppercase tracking-[0.2em] text-pop-pink">
|
|
{slide.category}
|
|
</span>
|
|
<h3 className="font-serif text-xl font-light tracking-wide text-pop-black">
|
|
{renderTitle(slide.title) || slide.title}
|
|
</h3>
|
|
<p className="line-clamp-6 text-xs font-light leading-relaxed text-pop-black/70 md:text-sm">
|
|
{slide.description}
|
|
</p>
|
|
</div>
|
|
|
|
{slide.buttonText && slide.buttonLink && (
|
|
<div className="pt-2">
|
|
<Link
|
|
to={slide.buttonLink}
|
|
className="inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-pop-pink hover:text-pop-black transition-colors group/link"
|
|
>
|
|
{slide.buttonText}
|
|
<span className="group-hover/link:translate-x-1 transition-transform">→</span>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="ui-dark-card group flex h-full flex-col transition-all duration-300 hover:border-pop-pink/30">
|
|
<div className="aspect-[4/3] overflow-hidden relative">
|
|
<img
|
|
src={slide.imageUrl}
|
|
alt={slide.title}
|
|
className="w-full h-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
|
|
onError={(e) => {
|
|
(e.target as HTMLImageElement).style.display = 'none';
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className="p-6 md:p-8 flex flex-col flex-1 justify-between space-y-6">
|
|
<div className="space-y-3">
|
|
<span className="block text-[10px] font-bold uppercase tracking-[0.2em] text-pop-pink">
|
|
{slide.category}
|
|
</span>
|
|
<h3 className="font-serif text-xl font-light tracking-wide text-pop-black">
|
|
{renderTitle(slide.title) || slide.title}
|
|
</h3>
|
|
<p className="line-clamp-3 text-xs font-light leading-relaxed text-pop-black/70 md:text-sm">
|
|
{slide.description}
|
|
</p>
|
|
</div>
|
|
|
|
{slide.buttonText && slide.buttonLink && (
|
|
<div className="pt-2">
|
|
<Link
|
|
to={slide.buttonLink}
|
|
className="inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-pop-pink hover:text-pop-black transition-colors group/link"
|
|
>
|
|
{slide.buttonText}
|
|
<span className="group-hover/link:translate-x-1 transition-transform">→</span>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</SwiperSlide>
|
|
))}
|
|
</Swiper>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|