update
This commit is contained in:
286
Frontend/src/features/content/components/PartnersCarousel.tsx
Normal file
286
Frontend/src/features/content/components/PartnersCarousel.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user