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:
Norbert Maciaszek
2025-08-15 17:14:16 +02:00
parent 54e2e74e3a
commit 03b00ad399
2 changed files with 248 additions and 15 deletions

View 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>
);
};