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:
parent
54e2e74e3a
commit
03b00ad399
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in New Issue