Files
Hotel-Booking/Frontend/src/features/content/components/PartnersCarousel.tsx
Iliyan Angelov e1988fe37a update
2025-12-05 01:50:38 +02:00

287 lines
11 KiB
TypeScript

import React, { useState, useEffect, useMemo } from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
interface Partner {
name?: string;
logo?: string;
link?: string;
}
interface PartnersCarouselProps {
partners: Partner[];
autoSlideInterval?: number;
showNavigation?: boolean;
itemsPerView?: number;
}
const PartnersCarousel: React.FC<PartnersCarouselProps> = ({
partners,
autoSlideInterval = 5000,
showNavigation = true,
itemsPerView = 6,
}) => {
const [currentIndex, setCurrentIndex] = useState(0);
const [windowWidth, setWindowWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1920);
// Handle window resize
useEffect(() => {
if (typeof window === 'undefined') return;
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Calculate responsive items per view
const responsiveItemsPerView = useMemo(() => {
if (windowWidth < 640) return 2; // sm: 2 items
if (windowWidth < 768) return 3; // md: 3 items
if (windowWidth < 1024) return 4; // lg: 4 items
return itemsPerView; // xl: 6 items
}, [windowWidth, itemsPerView]);
// Calculate max index (how many slides we can have)
const maxIndex = Math.max(0, partners.length - responsiveItemsPerView);
const canNavigate = partners.length > responsiveItemsPerView;
const allItemsFit = partners.length <= responsiveItemsPerView;
// Auto-slide functionality
useEffect(() => {
if (!canNavigate) return;
const interval = setInterval(() => {
setCurrentIndex((prev) => {
if (prev >= maxIndex) {
return 0; // Loop back to start
}
return prev + 1;
});
}, autoSlideInterval);
return () => clearInterval(interval);
}, [canNavigate, maxIndex, autoSlideInterval]);
// Reset index when it exceeds max
useEffect(() => {
if (currentIndex > maxIndex) {
setCurrentIndex(0);
}
}, [maxIndex, currentIndex]);
const goToPrevious = () => {
if (!canNavigate) return;
setCurrentIndex((prev) => (prev === 0 ? maxIndex : prev - 1));
};
const goToNext = () => {
if (!canNavigate) return;
setCurrentIndex((prev) => (prev >= maxIndex ? 0 : prev + 1));
};
const goToSlide = (index: number) => {
if (index < 0 || index > maxIndex) return;
setCurrentIndex(index);
};
if (partners.length === 0) {
return null;
}
const itemWidth = 100 / responsiveItemsPerView;
const translateX = allItemsFit ? 0 : -(currentIndex * itemWidth);
return (
<div className="relative w-full max-w-7xl mx-auto">
{/* Carousel Container */}
<div className={`relative ${allItemsFit ? '' : 'overflow-hidden'} px-4 sm:px-6 lg:px-8`}>
{allItemsFit ? (
// Centered layout when all items fit - show only logos without containers
<div className="flex justify-center items-center flex-wrap gap-4 md:gap-6 lg:gap-8">
{partners.map((partner, index) => {
return (
<div
key={`partner-${index}`}
className="flex-shrink-0"
>
{partner.link ? (
<a
href={partner.link}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center group"
>
{partner.logo ? (
<img
src={partner.logo}
alt={partner.name || `Partner ${index + 1}`}
className="max-w-[80px] md:max-w-[100px] lg:max-w-[120px] max-h-12 md:max-h-16 lg:max-h-20 object-contain opacity-60 group-hover:opacity-100 transition-opacity duration-300"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
}}
/>
) : (
<span className="text-gray-600 font-semibold text-xs md:text-sm text-center">
{partner.name || `Partner ${index + 1}`}
</span>
)}
</a>
) : (
<>
{partner.logo ? (
<img
src={partner.logo}
alt={partner.name || `Partner ${index + 1}`}
className="max-w-[80px] md:max-w-[100px] lg:max-w-[120px] max-h-12 md:max-h-16 lg:max-h-20 object-contain opacity-60 hover:opacity-100 transition-opacity duration-300"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
}}
/>
) : (
<span className="text-gray-600 font-semibold text-xs md:text-sm text-center">
{partner.name || `Partner ${index + 1}`}
</span>
)}
</>
)}
</div>
);
})}
</div>
) : (
// Carousel layout when items don't all fit
<div
className="flex transition-transform duration-500 ease-in-out"
style={{
transform: `translateX(${translateX}%)`,
willChange: 'transform',
}}
>
{partners.map((partner, index) => (
<div
key={`partner-${index}`}
className="flex-shrink-0 px-2 sm:px-3 md:px-4 flex items-center justify-center"
style={{ width: `${itemWidth}%` }}
>
{partner.link ? (
<a
href={partner.link}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center group"
>
{partner.logo ? (
<img
src={partner.logo}
alt={partner.name || `Partner ${index + 1}`}
className="max-w-[80px] md:max-w-[100px] lg:max-w-[120px] max-h-12 md:max-h-16 lg:max-h-20 object-contain opacity-60 group-hover:opacity-100 transition-opacity duration-300"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
}}
/>
) : (
<span className="text-gray-600 font-semibold text-xs md:text-sm text-center">
{partner.name || `Partner ${index + 1}`}
</span>
)}
</a>
) : (
<>
{partner.logo ? (
<img
src={partner.logo}
alt={partner.name || `Partner ${index + 1}`}
className="max-w-[80px] md:max-w-[100px] lg:max-w-[120px] max-h-12 md:max-h-16 lg:max-h-20 object-contain opacity-60 hover:opacity-100 transition-opacity duration-300"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.style.display = 'none';
}}
/>
) : (
<span className="text-gray-600 font-semibold text-xs md:text-sm text-center">
{partner.name || `Partner ${index + 1}`}
</span>
)}
</>
)}
</div>
))}
</div>
)}
</div>
{/* Navigation Buttons */}
{showNavigation && canNavigate && (
<>
<button
onClick={goToPrevious}
disabled={currentIndex === 0}
className="absolute left-0 top-1/2 -translate-y-1/2 z-10
w-10 h-10 sm:w-12 sm:h-12
bg-gradient-to-r from-[#d4af37] to-[#c9a227]
text-white rounded-full
flex items-center justify-center
shadow-lg shadow-[#d4af37]/40
hover:shadow-xl hover:shadow-[#d4af37]/50
hover:scale-110
active:scale-95
transition-all duration-300
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100
group backdrop-blur-sm"
aria-label="Previous partners"
>
<ChevronLeft className="w-5 h-5 sm:w-6 sm:h-6 group-hover:-translate-x-0.5 transition-transform" />
</button>
<button
onClick={goToNext}
disabled={currentIndex >= maxIndex}
className="absolute right-0 top-1/2 -translate-y-1/2 z-10
w-10 h-10 sm:w-12 sm:h-12
bg-gradient-to-r from-[#d4af37] to-[#c9a227]
text-white rounded-full
flex items-center justify-center
shadow-lg shadow-[#d4af37]/40
hover:shadow-xl hover:shadow-[#d4af37]/50
hover:scale-110
active:scale-95
transition-all duration-300
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100
group backdrop-blur-sm"
aria-label="Next partners"
>
<ChevronRight className="w-5 h-5 sm:w-6 sm:h-6 group-hover:translate-x-0.5 transition-transform" />
</button>
</>
)}
{/* Dots Indicator */}
{canNavigate && maxIndex > 0 && (
<div className="flex justify-center items-center gap-1.5 sm:gap-2 mt-6 md:mt-8">
{Array.from({ length: maxIndex + 1 }).map((_, index) => (
<button
key={index}
onClick={() => goToSlide(index)}
className={`transition-all duration-300 rounded-full
${
index === currentIndex
? 'w-8 h-2 sm:w-10 sm:h-2.5 bg-gradient-to-r from-[#d4af37] to-[#c9a227] shadow-lg shadow-[#d4af37]/40'
: 'w-2 h-2 sm:w-2.5 sm:h-2.5 bg-gray-300 hover:bg-[#d4af37]/50'
}
`}
aria-label={`Go to partners slide ${index + 1}`}
/>
))}
</div>
)}
</div>
);
};
export default PartnersCarousel;