update
This commit is contained in:
435
Frontend/src/features/rooms/components/BannerCarousel.tsx
Normal file
435
Frontend/src/features/rooms/components/BannerCarousel.tsx
Normal file
@@ -0,0 +1,435 @@
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
const defaultBanner: Banner = {
|
||||
id: 0,
|
||||
title: 'Welcome to Hotel Booking',
|
||||
image_url: '/images/default-banner.jpg',
|
||||
position: 'home',
|
||||
display_order: 0,
|
||||
is_active: true,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
description: undefined,
|
||||
link_url: undefined,
|
||||
};
|
||||
|
||||
const displayBanners = banners.length > 0
|
||||
? banners
|
||||
: [defaultBanner];
|
||||
const currentBanner = displayBanners[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">
|
||||
{displayBanners.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"
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = '/images/default-banner.jpg';
|
||||
}}
|
||||
/>
|
||||
</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"
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = '/images/default-banner.jpg';
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</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-[#d4af37] 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-[#f5d76e] to-[#d4af37] 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-[#d4af37] 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-[#d4af37] 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>
|
||||
|
||||
{}
|
||||
{displayBanners.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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{}
|
||||
{displayBanners.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'}`}
|
||||
>
|
||||
{displayBanners.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-[#d4af37] to-[#f5d76e] w-8 sm:w-10 shadow-lg shadow-[#d4af37]/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;
|
||||
Reference in New Issue
Block a user