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

@ -1,30 +1,84 @@
import { getNowPlayingMovies } from "@/lib/tmdb/server";
import {
getNowPlayingMovies,
getPopularMovies,
getTrendingMovies,
getUpcomingMovies,
} from "@/lib/tmdb/server";
import { Hero } from "@/components/organisms/Hero";
import { Carousel } from "@/components/molecules/Carousel";
import { MovieCard } from "@/components/atoms/MovieCard";
// 12 hours
export const revalidate = 43200;
export default async function Home() {
// Get trending movies and popular movies for hero carousel.
const data = await getNowPlayingMovies();
// Combine first 3 trending and first 2 popular movies for variety.
const dataRaw = data.results.slice(0, 5);
const [nowPlayingData, popularData, trendingData, upcomingData] =
await Promise.all([
getNowPlayingMovies(),
getPopularMovies(),
getTrendingMovies(),
getUpcomingMovies(),
]);
// Convert TMDB movie format to our Movie type.
const heroMovies = dataRaw.map((movie) => ({
const convertMovies = (movies: any[]) =>
movies.map((movie) => ({
...movie,
genre_ids: JSON.stringify(movie.genre_ids),
seen: false,
favorite: false,
}));
const heroMovies = convertMovies(trendingData.results.slice(0, 5));
const popularMovies = convertMovies(popularData.results);
const trendingMovies = convertMovies(trendingData.results).slice(5);
const nowPlayingMovies = convertMovies(nowPlayingData.results);
const upcomingMovies = convertMovies(upcomingData.results);
return (
<>
<Hero movies={heroMovies} autoRotate />
<div className="container py-16">
<h2 className="text-3xl font-bold text-white mb-8">Popularne filmy</h2>
{/* Add other homepage content here */}
<div className="container py-16 space-y-16">
<section>
<h2 className="text-3xl font-bold text-white mb-8">Teraz w kinach</h2>
<Carousel>
{nowPlayingMovies.map((movie) => (
<MovieCard key={movie.id} {...movie} layout="zeus" simpleToggle />
))}
</Carousel>
</section>
<section>
<h2 className="text-3xl font-bold text-white mb-8">
Nadchodzące filmy
</h2>
<Carousel>
{upcomingMovies.map((movie) => (
<MovieCard key={movie.id} {...movie} layout="zeus" simpleToggle />
))}
</Carousel>
</section>
<section>
<h2 className="text-3xl font-bold text-white mb-8">
Popularne filmy
</h2>
<Carousel>
{popularMovies.map((movie) => (
<MovieCard key={movie.id} {...movie} layout="zeus" simpleToggle />
))}
</Carousel>
</section>
<section>
<h2 className="text-3xl font-bold text-white mb-8">Trendy dnia</h2>
<Carousel>
{trendingMovies.map((movie) => (
<MovieCard key={movie.id} {...movie} layout="zeus" simpleToggle />
))}
</Carousel>
</section>
</div>
</>
);

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