Refactor homepage to include multiple movie sections: integrate new Carousel component for displaying now playing, upcoming, popular, and trending movies. Update data fetching to utilize multiple TMDB endpoints for enhanced content variety and user experience.
This commit is contained in:
179
src/components/molecules/Carousel/index.tsx
Normal file
179
src/components/molecules/Carousel/index.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
"use client";
|
||||
import { FC, ReactNode, useRef, useState, useCallback, useEffect } from "react";
|
||||
import { FaChevronLeft, FaChevronRight } from "react-icons/fa";
|
||||
|
||||
type CarouselOptions = {
|
||||
itemsPerView?: number;
|
||||
itemsPerViewMobile?: number;
|
||||
itemsPerViewTablet?: number;
|
||||
gap?: number;
|
||||
showArrows?: boolean;
|
||||
showDots?: boolean;
|
||||
autoScroll?: boolean;
|
||||
autoScrollInterval?: number;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
children: ReactNode[];
|
||||
options?: CarouselOptions;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Carousel: FC<Props> = ({
|
||||
children,
|
||||
options = {},
|
||||
className = "",
|
||||
}) => {
|
||||
const {
|
||||
itemsPerView = 4,
|
||||
itemsPerViewMobile = 2,
|
||||
itemsPerViewTablet = 3,
|
||||
gap = 20,
|
||||
showArrows = true,
|
||||
showDots = true,
|
||||
autoScroll = false,
|
||||
autoScrollInterval = 5000,
|
||||
} = options;
|
||||
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
const [itemsVisible, setItemsVisible] = useState(itemsPerView);
|
||||
const carouselRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const totalItems = children.length;
|
||||
const maxIndex = Math.max(0, totalItems - itemsVisible);
|
||||
|
||||
// Responsive items per view.
|
||||
useEffect(() => {
|
||||
const updateItemsPerView = () => {
|
||||
if (window.innerWidth < 640) {
|
||||
setItemsVisible(itemsPerViewMobile);
|
||||
} else if (window.innerWidth < 1024) {
|
||||
setItemsVisible(itemsPerViewTablet);
|
||||
} else {
|
||||
setItemsVisible(itemsPerView);
|
||||
}
|
||||
};
|
||||
|
||||
updateItemsPerView();
|
||||
window.addEventListener("resize", updateItemsPerView);
|
||||
return () => window.removeEventListener("resize", updateItemsPerView);
|
||||
}, [itemsPerView, itemsPerViewMobile, itemsPerViewTablet]);
|
||||
|
||||
const nextSlide = useCallback(() => {
|
||||
if (isTransitioning) return;
|
||||
|
||||
setIsTransitioning(true);
|
||||
setCurrentIndex((prev) => {
|
||||
return Math.min(prev + itemsVisible, maxIndex);
|
||||
});
|
||||
|
||||
setTimeout(() => setIsTransitioning(false), 300);
|
||||
}, [isTransitioning, totalItems, maxIndex]);
|
||||
|
||||
const prevSlide = useCallback(() => {
|
||||
if (isTransitioning) return;
|
||||
|
||||
setIsTransitioning(true);
|
||||
setCurrentIndex((prev) => {
|
||||
return Math.max(prev - itemsVisible, 0);
|
||||
});
|
||||
|
||||
setTimeout(() => setIsTransitioning(false), 300);
|
||||
}, [isTransitioning, totalItems]);
|
||||
|
||||
const goToSlide = useCallback(
|
||||
(index: number) => {
|
||||
if (isTransitioning || index === currentIndex) return;
|
||||
|
||||
setIsTransitioning(true);
|
||||
setCurrentIndex(index);
|
||||
setTimeout(() => setIsTransitioning(false), 300);
|
||||
},
|
||||
[currentIndex, isTransitioning]
|
||||
);
|
||||
|
||||
// Auto-scroll functionality.
|
||||
useEffect(() => {
|
||||
if (!autoScroll || totalItems <= itemsVisible) return;
|
||||
|
||||
const interval = setInterval(nextSlide, autoScrollInterval);
|
||||
return () => clearInterval(interval);
|
||||
}, [autoScroll, autoScrollInterval, nextSlide, totalItems, itemsVisible]);
|
||||
|
||||
// Calculate transform for slide positioning.
|
||||
const getTransform = () => {
|
||||
return `translateX(-${currentIndex * (100 / itemsVisible)}%)`;
|
||||
};
|
||||
|
||||
const canGoPrev = currentIndex > 0;
|
||||
const canGoNext = currentIndex < maxIndex;
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
{/* Carousel container */}
|
||||
<div className="overflow-hidden" ref={carouselRef}>
|
||||
<div
|
||||
className="flex transition-transform duration-300 ease-out"
|
||||
style={{
|
||||
transform: getTransform(),
|
||||
marginRight: `-${gap}px`,
|
||||
}}
|
||||
>
|
||||
{children.map((child, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex-shrink-0 [&_article]:h-full"
|
||||
style={{
|
||||
paddingRight: `${gap}px`,
|
||||
width: `${100 / itemsVisible}%`,
|
||||
}}
|
||||
>
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation arrows */}
|
||||
{showArrows && totalItems > itemsVisible && (
|
||||
<>
|
||||
<button
|
||||
onClick={prevSlide}
|
||||
disabled={!canGoPrev || isTransitioning}
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2 z-10 p-4 bg-primary/50 hover:bg-primary cursor-pointer text-white rounded-full transition-all disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
>
|
||||
<FaChevronLeft size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={nextSlide}
|
||||
disabled={!canGoNext || isTransitioning}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 z-10 p-4 bg-primary/50 hover:bg-primary cursor-pointer text-white rounded-full transition-all disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
>
|
||||
<FaChevronRight size={20} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Dot indicators */}
|
||||
{showDots && totalItems > itemsVisible && (
|
||||
<div className="flex justify-center mt-4 gap-2">
|
||||
{Array.from({ length: Math.ceil(totalItems / itemsVisible) }).map(
|
||||
(_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => goToSlide(index * itemsVisible)}
|
||||
disabled={isTransitioning}
|
||||
className={`w-2 h-2 rounded-full transition-all duration-300 disabled:cursor-not-allowed cursor-pointer ${
|
||||
Math.floor(currentIndex / itemsVisible) === index
|
||||
? "bg-secondary scale-125"
|
||||
: "bg-white/30 hover:bg-secondary/50"
|
||||
}`}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user