Add movie details and cast components: implement Page, HeroMovie, and MovieCast components to display detailed movie information, including cast, genres, and financial data. Integrate new BackButton and GenreLabel components for enhanced navigation and presentation.
This commit is contained in:
parent
b577a79278
commit
61395ca1ec
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { HeroMovie } from "@/components/molecules/HeroMovie";
|
||||||
|
import { MovieCast } from "@/components/molecules/MovieCast";
|
||||||
|
import { TMDB } from "@/lib/tmdb";
|
||||||
|
|
||||||
|
// Main movie details component.
|
||||||
|
export default async function Page({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}) {
|
||||||
|
const movieId = Number((await params).id);
|
||||||
|
|
||||||
|
const movieDetails = await TMDB.getMovieDetailsRich(movieId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen mt-16">
|
||||||
|
<HeroMovie movieDetails={movieDetails} />
|
||||||
|
<MovieCast movieDetails={movieDetails} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
"use client";
|
||||||
|
import { FaArrowLeft } from "react-icons/fa";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BackButton: FC<Props> = ({ label = "Powrót" }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => router.back()}
|
||||||
|
className="flex items-center gap-3 text-white hover:text-purple-300 transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="p-2 rounded-full bg-black/30 group-hover:bg-purple-600/30 transition-colors">
|
||||||
|
<FaArrowLeft size={20} />
|
||||||
|
</div>
|
||||||
|
<span className="font-medium">{label}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
genre: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GenreLabel: FC<Props> = ({ genre }) => {
|
||||||
|
return (
|
||||||
|
<span className="px-3 py-1 bg-gradient-to-r from-purple-600/30 to-pink-600/30 rounded-full text-sm border border-purple-400/30">
|
||||||
|
{genre}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
import { MdFavorite } from "react-icons/md";
|
import { MdFavorite } from "react-icons/md";
|
||||||
import { RxEyeOpen } from "react-icons/rx";
|
import { RxEyeOpen } from "react-icons/rx";
|
||||||
import { FaFire, FaTrash } from "react-icons/fa";
|
import { FaFire, FaTrash, FaInfoCircle } from "react-icons/fa";
|
||||||
import { RiCalendarCheckLine, RiCalendarScheduleLine } from "react-icons/ri";
|
import { RiCalendarCheckLine, RiCalendarScheduleLine } from "react-icons/ri";
|
||||||
import { Movie } from "@/types/global";
|
import { Movie } from "@/types/global";
|
||||||
|
|
||||||
|
|
@ -20,6 +21,7 @@ interface AuroraLayoutProps extends Movie {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuroraLayout: FC<AuroraLayoutProps> = ({
|
export const AuroraLayout: FC<AuroraLayoutProps> = ({
|
||||||
|
id,
|
||||||
title,
|
title,
|
||||||
overview,
|
overview,
|
||||||
popularity,
|
popularity,
|
||||||
|
|
@ -202,22 +204,35 @@ export const AuroraLayout: FC<AuroraLayoutProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{alreadyInStore && (
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex gap-2">
|
{/* Zobacz więcej button */}
|
||||||
{seen && (
|
<Link
|
||||||
<div
|
href={`/film/${id}`}
|
||||||
className="w-3 h-3 bg-gradient-to-r from-emerald-400 to-teal-400 rounded-full shadow-lg shadow-emerald-400/50 animate-pulse"
|
className="opacity-0 group-hover:opacity-100 transition-all duration-300 transform group-hover:scale-105"
|
||||||
title="Watched"
|
>
|
||||||
/>
|
<div className="flex items-center gap-2 bg-gradient-to-r from-purple-600/90 to-pink-600/90 hover:from-purple-500 hover:to-pink-500 px-3 py-2 rounded-lg text-white text-sm font-medium shadow-lg border border-white/10 transition-all duration-300">
|
||||||
)}
|
<FaInfoCircle size={14} />
|
||||||
{favorite && (
|
<span>Zobacz więcej</span>
|
||||||
<div
|
</div>
|
||||||
className="w-3 h-3 bg-gradient-to-r from-rose-400 to-pink-400 rounded-full shadow-lg shadow-rose-400/50 animate-pulse"
|
</Link>
|
||||||
title="Favorite"
|
|
||||||
/>
|
{alreadyInStore && (
|
||||||
)}
|
<div className="flex gap-2">
|
||||||
</div>
|
{seen && (
|
||||||
)}
|
<div
|
||||||
|
className="w-3 h-3 bg-gradient-to-r from-emerald-400 to-teal-400 rounded-full shadow-lg shadow-emerald-400/50 animate-pulse"
|
||||||
|
title="Watched"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{favorite && (
|
||||||
|
<div
|
||||||
|
className="w-3 h-3 bg-gradient-to-r from-rose-400 to-pink-400 rounded-full shadow-lg shadow-rose-400/50 animate-pulse"
|
||||||
|
title="Favorite"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
"use client";
|
||||||
|
import { BackButton } from "@/components/atoms/BackButton";
|
||||||
|
import { Button } from "@/components/atoms/Button";
|
||||||
|
import { GenreLabel } from "@/components/atoms/GenreLabel";
|
||||||
|
import { MovieDetailsRich } from "@/lib/tmdb/types";
|
||||||
|
import { useGlobalStore } from "@/app/store/globalStore";
|
||||||
|
import { FC } from "react";
|
||||||
|
import {
|
||||||
|
FaHeart,
|
||||||
|
FaBookmark,
|
||||||
|
FaClock,
|
||||||
|
FaCalendar,
|
||||||
|
FaGlobe,
|
||||||
|
FaEye,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
movieDetails: MovieDetailsRich;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HeroMovie: FC<Props> = ({ movieDetails }) => {
|
||||||
|
const { movies, addMovie, deleteMovie, updateMovie } = useGlobalStore();
|
||||||
|
|
||||||
|
// Check if movie is in store and get its state.
|
||||||
|
const movieInStore = movies.find((m) => m.id === movieDetails.id);
|
||||||
|
const isInStore = !!movieInStore;
|
||||||
|
const isFavorite = movieInStore?.favorite || false;
|
||||||
|
const isSeen = movieInStore?.seen || false;
|
||||||
|
|
||||||
|
const formatRuntime = (minutes: number) => {
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const mins = minutes % 60;
|
||||||
|
return `${hours}h ${mins}m`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert TMDB movie to our Movie type.
|
||||||
|
const convertToMovie = () => ({
|
||||||
|
id: movieDetails.id,
|
||||||
|
title: movieDetails.title,
|
||||||
|
adult: movieDetails.adult,
|
||||||
|
backdrop_path: movieDetails.backdrop_path || "",
|
||||||
|
genre_ids: movieDetails.genres.map((g) => g.id).join(","),
|
||||||
|
original_language: movieDetails.original_language,
|
||||||
|
original_title: movieDetails.original_title,
|
||||||
|
overview: movieDetails.overview || "",
|
||||||
|
popularity: movieDetails.popularity,
|
||||||
|
poster_path: movieDetails.poster_path || "",
|
||||||
|
release_date: movieDetails.release_date,
|
||||||
|
video: movieDetails.video,
|
||||||
|
vote_average: movieDetails.vote_average,
|
||||||
|
vote_count: movieDetails.vote_count,
|
||||||
|
favorite: false,
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddToList = () => {
|
||||||
|
if (isInStore) {
|
||||||
|
deleteMovie(movieDetails.id);
|
||||||
|
} else {
|
||||||
|
addMovie(convertToMovie());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleFavorite = () => {
|
||||||
|
if (!isInStore) {
|
||||||
|
addMovie({ ...convertToMovie(), favorite: true });
|
||||||
|
} else {
|
||||||
|
updateMovie(movieDetails.id, { favorite: !isFavorite });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleSeen = () => {
|
||||||
|
if (!isInStore) {
|
||||||
|
addMovie({ ...convertToMovie(), seen: true });
|
||||||
|
} else {
|
||||||
|
updateMovie(movieDetails.id, { seen: !isSeen });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="my-16">
|
||||||
|
<div className="relative">
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className="absolute top-0 left-0 right-0 z-20 px-6">
|
||||||
|
<BackButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="relative z-10 px-6 lg:px-8">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="flex flex-col lg:flex-row gap-8">
|
||||||
|
{/* Movie poster */}
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="relative group">
|
||||||
|
<img
|
||||||
|
src={`https://image.tmdb.org/t/p/w500${movieDetails.poster_path}`}
|
||||||
|
alt={movieDetails.title}
|
||||||
|
className="w-80 h-auto rounded-2xl shadow-2xl shadow-purple-500/20 group-hover:shadow-purple-500/40 transition-all duration-500"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 rounded-2xl bg-gradient-to-t from-purple-600/20 to-transparent opacity-100" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Movie details */}
|
||||||
|
<div className="flex-1 text-white">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Title and rating */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-4xl lg:text-5xl font-bold bg-gradient-to-r from-white to-gray-300 bg-clip-text text-transparent mb-3">
|
||||||
|
{movieDetails.title}
|
||||||
|
</h1>
|
||||||
|
{movieDetails.tagline && (
|
||||||
|
<p className="text-xl text-gray-300 italic mb-4">
|
||||||
|
"{movieDetails.tagline}"
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-6 flex-wrap">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className={`text-2xl ${
|
||||||
|
i < Math.round(movieDetails.vote_average / 2)
|
||||||
|
? "text-yellow-400"
|
||||||
|
: "text-gray-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
★
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span className="text-lg font-semibold">
|
||||||
|
{movieDetails.vote_average.toFixed(1)}
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-400">
|
||||||
|
({movieDetails.vote_count} głosów)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Key info */}
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{movieDetails.release_date && (
|
||||||
|
<div className="flex items-center gap-2 text-gray-300">
|
||||||
|
<FaCalendar className="text-purple-400" />
|
||||||
|
<span>
|
||||||
|
{new Date(movieDetails.release_date).getFullYear()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{movieDetails.runtime && (
|
||||||
|
<div className="flex items-center gap-2 text-gray-300">
|
||||||
|
<FaClock className="text-purple-400" />
|
||||||
|
<span>{formatRuntime(movieDetails.runtime)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{movieDetails.spoken_languages[0] && (
|
||||||
|
<div className="flex items-center gap-2 text-gray-300">
|
||||||
|
<FaGlobe className="text-purple-400" />
|
||||||
|
<span>{movieDetails.spoken_languages[0].name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-gray-300">
|
||||||
|
<span className="text-purple-400">Status:</span>
|
||||||
|
<span>{movieDetails.status}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Genres */}
|
||||||
|
{movieDetails.genres.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-3 text-purple-300">
|
||||||
|
Gatunki
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{movieDetails.genres.map((genre) => (
|
||||||
|
<GenreLabel key={genre.id} genre={genre.name} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Synopsis */}
|
||||||
|
{movieDetails.overview && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-3 text-purple-300">
|
||||||
|
Opis
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-300 leading-relaxed text-lg">
|
||||||
|
{movieDetails.overview}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex gap-4 flex-wrap">
|
||||||
|
<Button
|
||||||
|
className={`flex items-center gap-3 ${
|
||||||
|
isInStore
|
||||||
|
? "bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={handleAddToList}
|
||||||
|
>
|
||||||
|
<FaBookmark />
|
||||||
|
{isInStore ? "Usuń z listy" : "Dodaj do listy"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
theme="glass"
|
||||||
|
className={`flex items-center gap-3 ${
|
||||||
|
isFavorite
|
||||||
|
? "bg-gradient-to-r from-rose-600/90 to-pink-600/90 hover:from-rose-500/90 hover:to-pink-500/90 border-rose-400/30"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={handleToggleFavorite}
|
||||||
|
>
|
||||||
|
<FaHeart className={isFavorite ? "text-rose-200" : ""} />
|
||||||
|
{isFavorite ? "Usuń z ulubionych" : "Dodaj do ulubionych"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
theme="glass"
|
||||||
|
className={`flex items-center gap-3 ${
|
||||||
|
isSeen
|
||||||
|
? "bg-gradient-to-r from-emerald-600/90 to-teal-600/90 hover:from-emerald-500/90 hover:to-teal-500/90 border-emerald-400/30"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={handleToggleSeen}
|
||||||
|
>
|
||||||
|
<FaEye className={isSeen ? "text-emerald-200" : ""} />
|
||||||
|
{isSeen
|
||||||
|
? "Oznacz jako nieobejrzany"
|
||||||
|
: "Oznacz jako obejrzany"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
import { MovieDetailsRich } from "@/lib/tmdb/types";
|
||||||
|
import { FC } from "react";
|
||||||
|
import { FaDollarSign } from "react-icons/fa";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
movieDetails: MovieDetailsRich;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MovieCast: FC<Props> = ({ movieDetails }) => {
|
||||||
|
const director = movieDetails?.credits.crew.find(
|
||||||
|
(member) => member.job === "Director"
|
||||||
|
);
|
||||||
|
const mainCast = movieDetails?.credits.cast.slice(0, 6) || [];
|
||||||
|
|
||||||
|
const formatCurrency = (amount: number) =>
|
||||||
|
new Intl.NumberFormat("pl-PL", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD",
|
||||||
|
}).format(amount);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="px-6 lg:px-8 py-16">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="grid lg:grid-cols-3 gap-12">
|
||||||
|
{/* Cast */}
|
||||||
|
{mainCast.length > 0 && (
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-6">Obsada</h2>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
|
||||||
|
{mainCast.map((actor) => (
|
||||||
|
<div key={actor.id} className="text-center group">
|
||||||
|
<div className="relative overflow-hidden rounded-xl mb-3">
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
actor.profile_path
|
||||||
|
? `https://image.tmdb.org/t/p/w185${actor.profile_path}`
|
||||||
|
: "/api/placeholder/185/278"
|
||||||
|
}
|
||||||
|
alt={actor.name}
|
||||||
|
className="w-full h-48 object-cover group-hover:scale-110 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-semibold text-white">{actor.name}</h4>
|
||||||
|
<p className="text-sm text-gray-400">{actor.character}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Additional info */}
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Director */}
|
||||||
|
{director && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-white mb-3">
|
||||||
|
Reżyseria
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-300">{director.name}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Budget & Revenue */}
|
||||||
|
{(movieDetails.budget > 0 || movieDetails.revenue > 0) && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-white mb-3">
|
||||||
|
Finanse
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{movieDetails.budget > 0 && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FaDollarSign className="text-green-400" />
|
||||||
|
<span className="text-gray-400">Budżet:</span>
|
||||||
|
<span className="text-white">
|
||||||
|
{formatCurrency(movieDetails.budget)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{movieDetails.revenue > 0 && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FaDollarSign className="text-green-400" />
|
||||||
|
<span className="text-gray-400">Przychody:</span>
|
||||||
|
<span className="text-white">
|
||||||
|
{formatCurrency(movieDetails.revenue)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Production companies */}
|
||||||
|
{movieDetails.production_companies.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-white mb-3">
|
||||||
|
Produkcja
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{movieDetails.production_companies
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((company) => (
|
||||||
|
<p key={company.id} className="text-gray-300">
|
||||||
|
{company.name}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* External links */}
|
||||||
|
{(movieDetails.homepage || movieDetails.imdb_id) && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-white mb-3">Linki</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{movieDetails.homepage && (
|
||||||
|
<a
|
||||||
|
href={movieDetails.homepage}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="block text-purple-400 hover:text-purple-300 transition-colors"
|
||||||
|
>
|
||||||
|
Oficjalna strona
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{movieDetails.imdb_id && (
|
||||||
|
<a
|
||||||
|
href={`https://www.imdb.com/title/${movieDetails.imdb_id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="block text-purple-400 hover:text-purple-300 transition-colors"
|
||||||
|
>
|
||||||
|
IMDb
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
import { search } from "./server";
|
import {
|
||||||
|
search,
|
||||||
|
getMovieDetails,
|
||||||
|
getMovieCredits,
|
||||||
|
getMovieDetailsRich,
|
||||||
|
} from "./server";
|
||||||
|
|
||||||
export const TMDB = {
|
export const TMDB = {
|
||||||
search,
|
search,
|
||||||
|
getMovieDetails,
|
||||||
|
getMovieCredits,
|
||||||
|
getMovieDetailsRich,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { SearchResult } from "./types";
|
import {
|
||||||
|
SearchResult,
|
||||||
|
MovieDetails,
|
||||||
|
MovieCredits,
|
||||||
|
MovieDetailsRich,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
const url = "https://api.themoviedb.org/3";
|
const url = "https://api.themoviedb.org/3";
|
||||||
const fetchTmbd = async (path: string) => {
|
const fetchTmbd = async (path: string) => {
|
||||||
|
|
@ -41,21 +46,37 @@ export async function search(options: SearchOptions): Promise<SearchResult> {
|
||||||
export async function getPopularMovies(
|
export async function getPopularMovies(
|
||||||
page: number = 1
|
page: number = 1
|
||||||
): Promise<SearchResult> {
|
): Promise<SearchResult> {
|
||||||
return await fetchTmbd(`/movie/popular?language=pl-PL&page=${page}`);
|
return await fetchTmbd(`/movie/popular?language=pl&page=${page}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTrendingMovies(): Promise<SearchResult> {
|
export async function getTrendingMovies(): Promise<SearchResult> {
|
||||||
return await fetchTmbd("/trending/movie/day?language=pl-PL");
|
return await fetchTmbd("/trending/movie/day?language=pl");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNowPlayingMovies(
|
export async function getNowPlayingMovies(
|
||||||
page: number = 1
|
page: number = 1
|
||||||
): Promise<SearchResult> {
|
): Promise<SearchResult> {
|
||||||
return await fetchTmbd(
|
return await fetchTmbd(
|
||||||
`/movie/now_playing?language=pl-PL®ion=PL&page=${page}`
|
`/movie/now_playing?language=pl®ion=PL&page=${page}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUpcomingMovies(): Promise<SearchResult> {
|
export async function getUpcomingMovies(): Promise<SearchResult> {
|
||||||
return await fetchTmbd("/movie/upcoming?language=pl-PL®ion=PL");
|
return await fetchTmbd("/movie/upcoming?language=pl®ion=PL");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMovieDetails(movieId: number): Promise<MovieDetails> {
|
||||||
|
return await fetchTmbd(`/movie/${movieId}?language=pl&`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMovieDetailsRich(
|
||||||
|
movieId: number
|
||||||
|
): Promise<MovieDetailsRich> {
|
||||||
|
return await fetchTmbd(
|
||||||
|
`/movie/${movieId}?language=pl&append_to_response=credits,similar,images,recommendations`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMovieCredits(movieId: number): Promise<MovieCredits> {
|
||||||
|
return await fetchTmbd(`/movie/${movieId}/credits?language=pl`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,120 @@ export type SearchResult = {
|
||||||
total_pages: number;
|
total_pages: number;
|
||||||
total_results: number;
|
total_results: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Genre = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProductionCompany = {
|
||||||
|
id: number;
|
||||||
|
logo_path: string | null;
|
||||||
|
name: string;
|
||||||
|
origin_country: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProductionCountry = {
|
||||||
|
iso_3166_1: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SpokenLanguage = {
|
||||||
|
english_name: string;
|
||||||
|
iso_639_1: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MovieDetails = {
|
||||||
|
adult: boolean;
|
||||||
|
backdrop_path: string | null;
|
||||||
|
belongs_to_collection: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
poster_path: string | null;
|
||||||
|
backdrop_path: string | null;
|
||||||
|
} | null;
|
||||||
|
budget: number;
|
||||||
|
genres: Genre[];
|
||||||
|
homepage: string | null;
|
||||||
|
id: number;
|
||||||
|
imdb_id: string | null;
|
||||||
|
original_language: string;
|
||||||
|
original_title: string;
|
||||||
|
overview: string | null;
|
||||||
|
popularity: number;
|
||||||
|
poster_path: string | null;
|
||||||
|
production_companies: ProductionCompany[];
|
||||||
|
production_countries: ProductionCountry[];
|
||||||
|
release_date: string;
|
||||||
|
revenue: number;
|
||||||
|
runtime: number | null;
|
||||||
|
spoken_languages: SpokenLanguage[];
|
||||||
|
status: string;
|
||||||
|
tagline: string | null;
|
||||||
|
title: string;
|
||||||
|
video: boolean;
|
||||||
|
vote_average: number;
|
||||||
|
vote_count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CastMember = {
|
||||||
|
adult: boolean;
|
||||||
|
gender: number;
|
||||||
|
id: number;
|
||||||
|
known_for_department: string;
|
||||||
|
name: string;
|
||||||
|
original_name: string;
|
||||||
|
popularity: number;
|
||||||
|
profile_path: string | null;
|
||||||
|
cast_id: number;
|
||||||
|
character: string;
|
||||||
|
credit_id: string;
|
||||||
|
order: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CrewMember = {
|
||||||
|
adult: boolean;
|
||||||
|
gender: number;
|
||||||
|
id: number;
|
||||||
|
known_for_department: string;
|
||||||
|
name: string;
|
||||||
|
original_name: string;
|
||||||
|
popularity: number;
|
||||||
|
profile_path: string | null;
|
||||||
|
credit_id: string;
|
||||||
|
department: string;
|
||||||
|
job: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MovieCredits = {
|
||||||
|
id: number;
|
||||||
|
cast: CastMember[];
|
||||||
|
crew: CrewMember[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MovieDetailsRich = MovieDetails & {
|
||||||
|
credits: MovieCredits;
|
||||||
|
similar: SearchResult;
|
||||||
|
recommendations: SearchResult;
|
||||||
|
images: {
|
||||||
|
backdrops: {
|
||||||
|
aspect_ratio: number;
|
||||||
|
file_path: string;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
}[];
|
||||||
|
logos: {
|
||||||
|
aspect_ratio: number;
|
||||||
|
file_path: string;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
}[];
|
||||||
|
posters: {
|
||||||
|
aspect_ratio: number;
|
||||||
|
file_path: string;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue