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:
parent
59444e131a
commit
36cd1582cf
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -139,7 +139,7 @@ export const HeroMovie: FC<Props> = ({ movieDetails }) => {
|
|||
<div className="flex items-center gap-2 text-gray-300">
|
||||
<FaCalendar className="text-purple-400" />
|
||||
<span>
|
||||
{new Date(movieDetails.release_date).getFullYear()}
|
||||
{formatter.formatDate(movieDetails.release_date)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -98,11 +98,10 @@ export const MovieList: FC<Props> = ({
|
|||
return 0;
|
||||
});
|
||||
|
||||
sortedMovies = sortedMovies.slice(0, loaded);
|
||||
|
||||
if (sortDirection === "desc") {
|
||||
sortedMovies = sortedMovies.reverse();
|
||||
}
|
||||
sortedMovies = sortedMovies.slice(0, loaded);
|
||||
|
||||
const handleFilter = (key?: keyof typeof filter) => {
|
||||
setFilter({
|
||||
|
|
|
|||
|
|
@ -1,12 +1,26 @@
|
|||
"use client";
|
||||
import { FC } from "react";
|
||||
import { useGlobalStore } from "@/app/store/globalStore";
|
||||
import Link from "next/link";
|
||||
import { FaCalendar, FaClock, FaStar } from "react-icons/fa";
|
||||
import { FaCalendar, FaClock } from "react-icons/fa";
|
||||
import { MovieRow } from "@/components/atoms/MovieRow";
|
||||
import { Movie } from "@/types/global";
|
||||
|
||||
export const TrackedMovies: FC = () => {
|
||||
const { movies } = useGlobalStore();
|
||||
type Props = {
|
||||
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) {
|
||||
return null;
|
||||
|
|
@ -24,7 +38,7 @@ export const TrackedMovies: FC = () => {
|
|||
|
||||
return (
|
||||
new Date(movie.release_date) <= today &&
|
||||
daysSinceRelease <= 30 &&
|
||||
daysSinceRelease <= daysLimit &&
|
||||
!movie.seen
|
||||
);
|
||||
});
|
||||
|
|
@ -48,7 +62,7 @@ export const TrackedMovies: FC = () => {
|
|||
<div>
|
||||
<h3 className="text-green-400 font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<FaClock className="w-4 h-4" />
|
||||
Aktualnie w kinach ({sortedInCinema.length})
|
||||
{labelCurrent} ({sortedInCinema.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{sortedInCinema.map((movie) => (
|
||||
|
|
@ -62,7 +76,7 @@ export const TrackedMovies: FC = () => {
|
|||
<div>
|
||||
<h3 className="text-blue-400 font-medium text-sm mb-3 flex items-center gap-2">
|
||||
<FaCalendar className="w-4 h-4" />
|
||||
Nadchodzące premiery ({sortedUpcoming.length})
|
||||
{labelUpcoming} ({sortedUpcoming.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{sortedUpcoming.map((movie) => (
|
||||
|
|
|
|||
|
|
@ -89,3 +89,37 @@ export async function getPersonDetails(
|
|||
export async function getMovieGenres(): Promise<{ genres: Genre[] }> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue