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">
|
<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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue