feat: implement GenrePage component to display movies by genre, including recent, upcoming, and top-rated films; enhance metadata generation for SEO

This commit is contained in:
Norbert Maciaszek 2025-08-22 00:05:27 +02:00
parent 59444e131a
commit 36cd1582cf
5 changed files with 220 additions and 10 deletions

View File

@ -0,0 +1,163 @@
import { getMovieGenres, getMoviesByGenre } from "@/lib/tmdb/server";
import { MovieList } from "@/components/molecules/MovieList";
import { FaCalendar, FaFire } from "react-icons/fa";
import { notFound } from "next/navigation";
import { GenreList } from "@/components/molecules/GenreList";
import { BackButton } from "@/components/atoms/BackButton";
// 6 hours cache for genre pages.
export const revalidate = 21600;
type PageProps = {
params: Promise<{ id: string }>;
};
export default async function GenrePage({ params }: PageProps) {
const { id } = await params;
const genreId = parseInt(id);
if (isNaN(genreId)) {
notFound();
}
const { genres } = await getMovieGenres();
const genre = genres.find((g) => g.id === genreId);
if (!genre) {
notFound();
}
const now = new Date();
// Get genre name and all movie data in parallel.
const [recentData, upcomingData, topRatedData] = await Promise.all([
getMoviesByGenre(genreId, 1, {
sort_by: "release_date.desc",
["release_date.lte"]: `${now.getFullYear()}-${
now.getMonth() + 1
}-${now.getDate()}`,
}),
getMoviesByGenre(genreId, 1, {
sort_by: "release_date.asc",
["release_date.gte"]: `${now.getFullYear()}-${now.getMonth() + 1}-${
now.getDate() + 1
} `,
}),
getMoviesByGenre(genreId, 1, {
sort_by: "",
vote_count_gte: "100",
["release_date.lte"]: `${now.getFullYear()}-${
now.getMonth() + 1
}-${now.getDate()}`,
}),
]);
// Convert TMDB movie format to our Movie type.
const convertMovies = (movies: any[]) =>
movies.map((movie) => ({
...movie,
genre_ids: JSON.stringify(movie.genre_ids),
seen: false,
favorite: false,
}));
const recentMovies = convertMovies(recentData.results);
const upcomingMovies = convertMovies(upcomingData.results);
const topRatedMovies = convertMovies(topRatedData.results);
return (
<>
{/* Hero section with genre name */}
<section className="blockp bg-gradient-to-br from-slate-900/50 via-slate-800/30 to-slate-900/50 border-b border-gray-800">
{/* Navigation */}
<div className="relative">
<div className="absolute top-0 left-0 right-0 z-20 px-6">
<BackButton />
</div>
</div>
<div className="container text-center">
<h1 className="text-4xl md:text-6xl font-bold mb-4 bg-gradient-to-r from-purple-400 via-pink-400 to-cyan-400 bg-clip-text text-transparent">
{genre.name}
</h1>
<p className="text-lg text-gray-300 max-w-2xl mx-auto">
Odkryj najlepsze filmy z kategorii {genre.name.toLowerCase()}
</p>
<div className="mt-8 w-32 h-px bg-gradient-to-r from-transparent via-purple-400/50 to-transparent mx-auto" />
</div>
</section>
{/* Recent movies section */}
<section className="blocks">
<MovieList
overrideMovies={recentMovies}
heading="Najnowsze filmy"
loadMore
icon={<FaCalendar />}
colors="green"
showFilters={false}
displayType="list"
/>
</section>
{/* Top rated section */}
{upcomingMovies.length > 0 && (
<section className="blocks">
<MovieList
overrideMovies={upcomingMovies}
heading="Nadchodzące premiery"
loadMore
icon={<FaCalendar />}
colors="blue"
showFilters={false}
sortDirection="desc"
displayType="list"
/>
</section>
)}
{/* Top rated section */}
{topRatedMovies.length > 0 && (
<section className="blocks">
<MovieList
overrideMovies={topRatedMovies}
heading="Najpopularniejsze filmy"
icon={<FaFire />}
colors="red"
showFilters={false}
displayType="list"
/>
</section>
)}
<GenreList heading="Odkryj inne gatunki" />
</>
);
}
export async function generateMetadata({ params }: PageProps) {
const { id } = await params;
const genreId = parseInt(id);
if (isNaN(genreId)) {
return {
title: "Gatunek nie znaleziony",
};
}
try {
const { genres } = await getMovieGenres();
const genre = genres.find((g) => g.id === genreId);
return {
title: genre ? `${genre.name} - Movie Box` : "Gatunek - Movie Box",
description: genre
? `Odkryj najlepsze filmy z kategorii ${genre.name}. Najnowsze premiery, popularne tytuły i najwyżej oceniane produkcje.`
: "Przeglądaj filmy według gatunków",
};
} catch (error) {
return {
title: "Gatunek - Movie Box",
};
}
}

View File

@ -139,7 +139,7 @@ export const HeroMovie: FC<Props> = ({ movieDetails }) => {
<div className="flex items-center gap-2 text-gray-300"> <div className="flex items-center gap-2 text-gray-300">
<FaCalendar className="text-purple-400" /> <FaCalendar className="text-purple-400" />
<span> <span>
{new Date(movieDetails.release_date).getFullYear()} {formatter.formatDate(movieDetails.release_date)}
</span> </span>
</div> </div>
)} )}

View File

@ -98,11 +98,10 @@ export const MovieList: FC<Props> = ({
return 0; return 0;
}); });
sortedMovies = sortedMovies.slice(0, loaded);
if (sortDirection === "desc") { if (sortDirection === "desc") {
sortedMovies = sortedMovies.reverse(); sortedMovies = sortedMovies.reverse();
} }
sortedMovies = sortedMovies.slice(0, loaded);
const handleFilter = (key?: keyof typeof filter) => { const handleFilter = (key?: keyof typeof filter) => {
setFilter({ setFilter({

View File

@ -1,12 +1,26 @@
"use client"; "use client";
import { FC } from "react"; import { FC } from "react";
import { useGlobalStore } from "@/app/store/globalStore"; import { useGlobalStore } from "@/app/store/globalStore";
import Link from "next/link"; import { FaCalendar, FaClock } from "react-icons/fa";
import { FaCalendar, FaClock, FaStar } from "react-icons/fa";
import { MovieRow } from "@/components/atoms/MovieRow"; import { MovieRow } from "@/components/atoms/MovieRow";
import { Movie } from "@/types/global";
export const TrackedMovies: FC = () => { type Props = {
const { movies } = useGlobalStore(); overrideMovies?: Movie[];
daysLimit?: number;
labelCurrent?: string;
labelUpcoming?: string;
};
export const TrackedMovies: FC<Props> = ({
overrideMovies,
daysLimit = 30,
labelCurrent = "Aktualnie w kinach",
labelUpcoming = "Nadchodzące premiery",
}) => {
const { movies: storeMovies } = useGlobalStore();
const movies = overrideMovies || storeMovies;
if (movies.length === 0) { if (movies.length === 0) {
return null; return null;
@ -24,7 +38,7 @@ export const TrackedMovies: FC = () => {
return ( return (
new Date(movie.release_date) <= today && new Date(movie.release_date) <= today &&
daysSinceRelease <= 30 && daysSinceRelease <= daysLimit &&
!movie.seen !movie.seen
); );
}); });
@ -48,7 +62,7 @@ export const TrackedMovies: FC = () => {
<div> <div>
<h3 className="text-green-400 font-medium text-sm mb-3 flex items-center gap-2"> <h3 className="text-green-400 font-medium text-sm mb-3 flex items-center gap-2">
<FaClock className="w-4 h-4" /> <FaClock className="w-4 h-4" />
Aktualnie w kinach ({sortedInCinema.length}) {labelCurrent} ({sortedInCinema.length})
</h3> </h3>
<div className="space-y-2"> <div className="space-y-2">
{sortedInCinema.map((movie) => ( {sortedInCinema.map((movie) => (
@ -62,7 +76,7 @@ export const TrackedMovies: FC = () => {
<div> <div>
<h3 className="text-blue-400 font-medium text-sm mb-3 flex items-center gap-2"> <h3 className="text-blue-400 font-medium text-sm mb-3 flex items-center gap-2">
<FaCalendar className="w-4 h-4" /> <FaCalendar className="w-4 h-4" />
Nadchodzące premiery ({sortedUpcoming.length}) {labelUpcoming} ({sortedUpcoming.length})
</h3> </h3>
<div className="space-y-2"> <div className="space-y-2">
{sortedUpcoming.map((movie) => ( {sortedUpcoming.map((movie) => (

View File

@ -89,3 +89,37 @@ export async function getPersonDetails(
export async function getMovieGenres(): Promise<{ genres: Genre[] }> { export async function getMovieGenres(): Promise<{ genres: Genre[] }> {
return await fetchTmbd("/genre/movie/list?language=pl"); return await fetchTmbd("/genre/movie/list?language=pl");
} }
type DiscoverMoviesOptions = Record<string, string | number>;
export async function discoverMovies(
options: DiscoverMoviesOptions
): Promise<SearchResult> {
const params = new URLSearchParams();
// Default language and region for Polish users.
params.set("language", "pl-PL");
params.set("region", "PL");
params.set("with_original_language", "en|pl");
Object.entries(options).forEach(([key, value]) => {
if (value !== undefined) {
params.set(key, value.toString());
}
});
return await fetchTmbd(`/discover/movie?${params.toString()}`);
}
export async function getMoviesByGenre(
genreId: number,
page: number = 1,
options: DiscoverMoviesOptions = {}
): Promise<SearchResult> {
return await discoverMovies({
with_genres: genreId,
sort_by: "release_date.desc",
page,
...options,
});
}