419 lines
14 KiB
TypeScript
419 lines
14 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
import type { Banner } from '../../content/services/bannerService';
|
|
|
|
interface BannerCarouselProps {
|
|
banners: Banner[];
|
|
children?: React.ReactNode;
|
|
}
|
|
|
|
const BannerCarousel: React.FC<BannerCarouselProps> = ({
|
|
banners,
|
|
children
|
|
}) => {
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
const [isAnimating, setIsAnimating] = useState(false);
|
|
|
|
|
|
useEffect(() => {
|
|
if (banners.length <= 1) return;
|
|
|
|
const interval = setInterval(() => {
|
|
setCurrentIndex((prev) =>
|
|
prev === banners.length - 1 ? 0 : prev + 1
|
|
);
|
|
}, 5000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [banners.length]);
|
|
|
|
const goToPrevious = () => {
|
|
if (isAnimating) return;
|
|
setIsAnimating(true);
|
|
setCurrentIndex((prev) =>
|
|
prev === 0 ? banners.length - 1 : prev - 1
|
|
);
|
|
setTimeout(() => setIsAnimating(false), 800);
|
|
};
|
|
|
|
const goToNext = () => {
|
|
if (isAnimating) return;
|
|
setIsAnimating(true);
|
|
setCurrentIndex((prev) =>
|
|
prev === banners.length - 1 ? 0 : prev + 1
|
|
);
|
|
setTimeout(() => setIsAnimating(false), 800);
|
|
};
|
|
|
|
const goToSlide = (index: number) => {
|
|
if (isAnimating || index === currentIndex) return;
|
|
setIsAnimating(true);
|
|
setCurrentIndex(index);
|
|
setTimeout(() => setIsAnimating(false), 800);
|
|
};
|
|
|
|
|
|
// Don't render if no banners - only show banners from API
|
|
if (banners.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const currentBanner = banners[currentIndex];
|
|
|
|
return (
|
|
<div
|
|
className="relative w-full h-[400px] sm:h-[500px] md:h-[600px] lg:h-[700px] xl:h-[800px] overflow-hidden"
|
|
>
|
|
{}
|
|
<div className="relative w-full h-full">
|
|
{banners.map((banner, index) => (
|
|
<div
|
|
key={banner.id || index}
|
|
className={`absolute inset-0 transition-opacity duration-1000 ease-in-out ${
|
|
index === currentIndex ? 'opacity-100 z-0 pointer-events-auto' : 'opacity-0 z-0 pointer-events-none'
|
|
}`}
|
|
>
|
|
{banner.link_url ? (
|
|
<a
|
|
href={banner.link_url}
|
|
target={banner.link_url.startsWith('http') ? '_blank' : '_self'}
|
|
rel={banner.link_url.startsWith('http') ? 'noopener noreferrer' : undefined}
|
|
className="block w-full h-full"
|
|
>
|
|
<img
|
|
src={banner.image_url}
|
|
alt={banner.title}
|
|
className="w-full h-full object-cover object-center transition-transform duration-1000 ease-out hover:scale-105"
|
|
/>
|
|
</a>
|
|
) : (
|
|
<img
|
|
src={banner.image_url}
|
|
alt={banner.title}
|
|
className="w-full h-full object-cover object-center transition-transform duration-1000 ease-out hover:scale-105"
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{}
|
|
<div
|
|
className="absolute inset-0 bg-gradient-to-t
|
|
from-black/70 via-black/30 via-black/15 to-black/5
|
|
transition-opacity duration-1000 ease-in-out"
|
|
/>
|
|
|
|
{}
|
|
<div
|
|
className="absolute inset-0 bg-gradient-to-br
|
|
from-transparent via-transparent to-black/10
|
|
animate-pulse"
|
|
style={{
|
|
animation: 'luxuryGlow 8s ease-in-out infinite',
|
|
}}
|
|
/>
|
|
|
|
{}
|
|
{currentBanner.title && (
|
|
<div
|
|
key={currentIndex}
|
|
className={`absolute ${children ? 'top-[25%] sm:top-[28%] md:top-[30%] lg:top-[32%]' : 'top-[30%] sm:top-[35%] md:top-[38%] lg:top-[40%]'}
|
|
left-1/2 -translate-x-1/2
|
|
text-white z-10 flex flex-col items-center justify-center
|
|
w-full max-w-5xl px-4 sm:px-6 md:px-8 lg:px-12
|
|
animate-fadeInUp`}
|
|
style={{
|
|
animation: 'luxuryFadeInUp 1s cubic-bezier(0.4, 0, 0.2, 1) forwards',
|
|
}}
|
|
>
|
|
|
|
{}
|
|
<div
|
|
className="absolute inset-0
|
|
rounded-2xl
|
|
-mx-2 sm:-mx-4 md:-mx-6 lg:-mx-8
|
|
pointer-events-none
|
|
opacity-0 animate-borderGlow"
|
|
style={{
|
|
background: 'linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.3), transparent)',
|
|
animation: 'borderGlow 3s ease-in-out infinite',
|
|
}}
|
|
/>
|
|
|
|
<div className="relative w-full flex flex-col items-center">
|
|
{}
|
|
<div
|
|
className="w-0 h-0.5 bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent mb-4 sm:mb-6 opacity-90
|
|
animate-lineExpand"
|
|
style={{
|
|
animation: 'lineExpand 1.2s cubic-bezier(0.4, 0, 0.2, 1) 0.3s forwards',
|
|
maxWidth: '120px',
|
|
}}
|
|
/>
|
|
|
|
<h2
|
|
className={`${children ? 'text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl' : 'text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl'}
|
|
font-serif font-light tracking-[0.02em] sm:tracking-[0.03em] md:tracking-[0.04em] lg:tracking-[0.05em]
|
|
text-center leading-[1.1] sm:leading-[1.15] md:leading-[1.2]
|
|
drop-shadow-[0_2px_8px_rgba(0,0,0,0.5)]
|
|
[text-shadow:_0_2px_20px_rgba(0,0,0,0.8),_0_4px_40px_rgba(0,0,0,0.6)]
|
|
mb-3 sm:mb-4
|
|
transform transition-all duration-700 ease-out
|
|
px-2 sm:px-4 md:px-6
|
|
opacity-0 animate-textReveal`}
|
|
style={{
|
|
letterSpacing: '0.08em',
|
|
fontWeight: 300,
|
|
animation: 'textReveal 1s cubic-bezier(0.4, 0, 0.2, 1) 0.5s forwards',
|
|
}}
|
|
>
|
|
<span
|
|
className="bg-gradient-to-b from-white via-white via-[var(--luxury-gold-light)] to-[var(--luxury-gold)] bg-clip-text text-transparent
|
|
animate-gradientShift"
|
|
style={{
|
|
backgroundSize: '200% 200%',
|
|
animation: 'gradientShift 5s ease infinite',
|
|
}}
|
|
>
|
|
{currentBanner.title}
|
|
</span>
|
|
</h2>
|
|
|
|
{}
|
|
{currentBanner.description && (
|
|
<p
|
|
className="text-sm sm:text-base md:text-lg lg:text-xl xl:text-2xl
|
|
font-light text-white/95 text-center
|
|
max-w-3xl mx-auto
|
|
leading-relaxed sm:leading-relaxed md:leading-relaxed
|
|
drop-shadow-[0_2px_6px_rgba(0,0,0,0.5)]
|
|
[text-shadow:_0_1px_10px_rgba(0,0,0,0.7)]
|
|
mt-2 sm:mt-3 md:mt-4
|
|
px-2 sm:px-4 md:px-6
|
|
opacity-0 animate-textReveal"
|
|
style={{
|
|
animation: 'textReveal 1s cubic-bezier(0.4, 0, 0.2, 1) 0.7s forwards',
|
|
}}
|
|
>
|
|
{currentBanner.description}
|
|
</p>
|
|
)}
|
|
|
|
{}
|
|
<div
|
|
className="w-0 h-0.5 bg-gradient-to-r from-transparent via-[var(--luxury-gold)] to-transparent mt-3 sm:mt-4 opacity-90
|
|
animate-lineExpand"
|
|
style={{
|
|
animation: 'lineExpand 1.2s cubic-bezier(0.4, 0, 0.2, 1) 0.9s forwards',
|
|
maxWidth: '120px',
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{}
|
|
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
|
<div
|
|
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
|
|
w-full max-w-5xl h-32 sm:h-40 md:h-48 lg:h-56
|
|
blur-3xl opacity-0
|
|
animate-glowPulse"
|
|
style={{
|
|
background: 'radial-gradient(circle, rgba(212, 175, 55, 0.25) 0%, rgba(212, 175, 55, 0.12) 40%, transparent 70%)',
|
|
animation: 'glowPulse 4s ease-in-out infinite',
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{}
|
|
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
|
{[...Array(3)].map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="absolute w-1 h-1 bg-[var(--luxury-gold)] rounded-full opacity-40"
|
|
style={{
|
|
left: `${20 + i * 30}%`,
|
|
top: `${30 + i * 20}%`,
|
|
animation: `floatParticle ${3 + i}s ease-in-out infinite`,
|
|
animationDelay: `${i * 0.5}s`,
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{}
|
|
{children && (
|
|
<div className="absolute inset-0 flex items-end justify-center z-50 px-1.5 sm:px-3 md:px-6 lg:px-8 pb-1 sm:pb-2 md:pb-6 lg:pb-12 xl:pb-16 pointer-events-none">
|
|
<div className="w-full max-w-6xl pointer-events-auto">
|
|
<div className="bg-white/95 rounded-md sm:rounded-lg shadow-2xl border border-white/20 p-1.5 sm:p-2.5 md:p-4 lg:p-6">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{}
|
|
{banners.length > 1 && (
|
|
<>
|
|
<button
|
|
onClick={goToPrevious}
|
|
type="button"
|
|
className={`absolute left-2 sm:left-4
|
|
-translate-y-1/2
|
|
bg-white/90
|
|
hover:bg-white text-gray-800
|
|
p-2 sm:p-2.5 md:p-3
|
|
rounded-full
|
|
shadow-xl border border-white/20
|
|
transition-all duration-300 z-40
|
|
hover:scale-110 hover:shadow-2xl
|
|
active:scale-95 cursor-pointer
|
|
group ${children ? 'top-[35%] sm:top-[40%] md:top-1/2' : 'top-1/2'}`}
|
|
aria-label="Previous banner"
|
|
>
|
|
<ChevronLeft className="w-5 h-5 sm:w-6 sm:h-6 transition-transform duration-300 group-hover:-translate-x-1" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={goToNext}
|
|
type="button"
|
|
className={`absolute right-2 sm:right-4
|
|
-translate-y-1/2
|
|
bg-white/90
|
|
hover:bg-white text-gray-800
|
|
p-2 sm:p-2.5 md:p-3
|
|
rounded-full
|
|
shadow-xl border border-white/20
|
|
transition-all duration-300 z-40
|
|
hover:scale-110 hover:shadow-2xl
|
|
active:scale-95 cursor-pointer
|
|
group ${children ? 'top-[35%] sm:top-[40%] md:top-1/2' : 'top-1/2'}`}
|
|
aria-label="Next banner"
|
|
>
|
|
<ChevronRight className="w-5 h-5 sm:w-6 sm:h-6 transition-transform duration-300 group-hover:translate-x-1" />
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
{}
|
|
{banners.length > 1 && (
|
|
<div
|
|
className={`absolute left-1/2
|
|
-translate-x-1/2 flex gap-2 sm:gap-2.5 z-40 pointer-events-auto
|
|
bg-black/40 px-3 py-2 rounded-full
|
|
border border-white/10 ${children ? 'bottom-16 sm:bottom-20 md:bottom-24 lg:bottom-28' : 'bottom-2 sm:bottom-4'}`}
|
|
>
|
|
{banners.map((_, index) => (
|
|
<button
|
|
key={index}
|
|
type="button"
|
|
onClick={() => goToSlide(index)}
|
|
className={`h-2 sm:h-2.5 rounded-full
|
|
transition-all duration-300 cursor-pointer
|
|
${
|
|
index === currentIndex
|
|
? 'bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-light)] w-8 sm:w-10 shadow-lg shadow-[var(--luxury-gold)]/50'
|
|
: 'bg-white/40 hover:bg-white/70 w-2 sm:h-2.5 hover:scale-125'
|
|
}`}
|
|
aria-label={`Go to banner ${index + 1}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{}
|
|
<style>{`
|
|
@keyframes luxuryFadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translate(-50%, 30px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translate(-50%, 0);
|
|
}
|
|
}
|
|
|
|
@keyframes textReveal {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px) scale(0.95);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
}
|
|
}
|
|
|
|
@keyframes lineExpand {
|
|
from {
|
|
width: 0;
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
width: 100%;
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes gradientShift {
|
|
0%, 100% {
|
|
background-position: 0% 50%;
|
|
}
|
|
50% {
|
|
background-position: 100% 50%;
|
|
}
|
|
}
|
|
|
|
@keyframes glowPulse {
|
|
0%, 100% {
|
|
opacity: 0.2;
|
|
transform: translate(-50%, -50%) scale(1);
|
|
}
|
|
50% {
|
|
opacity: 0.4;
|
|
transform: translate(-50%, -50%) scale(1.1);
|
|
}
|
|
}
|
|
|
|
@keyframes borderGlow {
|
|
0%, 100% {
|
|
opacity: 0;
|
|
}
|
|
50% {
|
|
opacity: 0.6;
|
|
}
|
|
}
|
|
|
|
@keyframes floatParticle {
|
|
0%, 100% {
|
|
transform: translateY(0) translateX(0);
|
|
opacity: 0.3;
|
|
}
|
|
33% {
|
|
transform: translateY(-20px) translateX(10px);
|
|
opacity: 0.6;
|
|
}
|
|
66% {
|
|
transform: translateY(-10px) translateX(-10px);
|
|
opacity: 0.4;
|
|
}
|
|
}
|
|
|
|
@keyframes luxuryGlow {
|
|
0%, 100% {
|
|
opacity: 0.3;
|
|
}
|
|
50% {
|
|
opacity: 0.6;
|
|
}
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BannerCarousel;
|