Files
Hotel-Booking/Frontend/src/features/rooms/components/BannerCarousel.tsx
Iliyan Angelov b818d645a9 updates
2025-12-07 20:36:17 +02:00

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;