From cf7ec070fd12d0e158452376151e51fc9595a1f0 Mon Sep 17 00:00:00 2001 From: Norbert Maciaszek Date: Mon, 18 Aug 2025 00:43:59 +0200 Subject: [PATCH] feat: add ActorHero component and actor details page; implement API integration for fetching actor information and enhance MovieCast with links to actor profiles --- src/app/aktor/[id]/page.tsx | 18 ++ src/components/molecules/ActorHero/index.tsx | 270 +++++++++++++++++++ src/components/molecules/MovieCast/index.tsx | 20 +- src/lib/tmdb/index.ts | 4 +- src/lib/tmdb/server.ts | 18 +- src/lib/tmdb/types.ts | 92 +++++++ 6 files changed, 412 insertions(+), 10 deletions(-) create mode 100644 src/app/aktor/[id]/page.tsx create mode 100644 src/components/molecules/ActorHero/index.tsx diff --git a/src/app/aktor/[id]/page.tsx b/src/app/aktor/[id]/page.tsx new file mode 100644 index 0000000..b153a40 --- /dev/null +++ b/src/app/aktor/[id]/page.tsx @@ -0,0 +1,18 @@ +import { ActorHero } from "@/components/molecules/ActorHero"; +import { TMDB } from "@/lib/tmdb"; + +export default async function Page({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const actorId = Number((await params).id); + + const personDetails = await TMDB.getPersonDetails(actorId); + + return ( +
+ +
+ ); +} diff --git a/src/components/molecules/ActorHero/index.tsx b/src/components/molecules/ActorHero/index.tsx new file mode 100644 index 0000000..ef51540 --- /dev/null +++ b/src/components/molecules/ActorHero/index.tsx @@ -0,0 +1,270 @@ +"use client"; + +import { BackButton } from "@/components/atoms/BackButton"; +import { PersonDetailsRich } from "@/lib/tmdb/types"; +import { FC } from "react"; +import { + FaCalendarAlt, + FaMapMarkerAlt, + FaStar, + FaTheaterMasks, + FaImdb, + FaFacebook, + FaInstagram, + FaTwitter, + FaYoutube, + FaTiktok, +} from "react-icons/fa"; + +type Props = { + personDetails: PersonDetailsRich; +}; + +export const ActorHero: FC = ({ personDetails }) => { + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("pl-PL", { + year: "numeric", + month: "long", + day: "numeric", + }); + }; + + const calculateAge = (birthday: string, deathday?: string | null) => { + const birth = new Date(birthday); + const end = deathday ? new Date(deathday) : new Date(); + const age = end.getFullYear() - birth.getFullYear(); + const monthDiff = end.getMonth() - birth.getMonth(); + + if (monthDiff < 0 || (monthDiff === 0 && end.getDate() < birth.getDate())) { + return age - 1; + } + return age; + }; + + const getGenderText = (gender: number) => { + switch (gender) { + case 1: + return "Kobieta"; + case 2: + return "Mężczyzna"; + case 3: + return "Niebinarne"; + default: + return "Nie określono"; + } + }; + + return ( +
+
+ {/* Navigation */} +
+ +
+ + {/* Main content */} +
+
+
+ {/* Profile photo */} +
+
+ {personDetails.name} +
+
+
+ + {/* Actor details */} +
+
+ {/* Name and known for */} +
+

+ {personDetails.name} +

+ {personDetails.birthday && ( + + ( + {calculateAge( + personDetails.birthday, + personDetails.deathday + )}{" "} + lat + {personDetails.deathday && " w chwili śmierci"}) + + )} + +
+
+ + + {personDetails.known_for_department} + +
+ +
+ + + {Math.round(personDetails.popularity)} popularność + +
+
+ + {/* Also known as */} + {personDetails.also_known_as.length > 0 && ( +
+

+ Znany również jako:{" "} + {personDetails.also_known_as.slice(0, 3).join(", ")} +

+
+ )} +
+ + {/* Personal info */} +
+ {/* Gender */} +
+ Płeć: + {getGenderText(personDetails.gender)} +
+ + {/* Birthday */} + {personDetails.birthday && ( +
+ +
+ {formatDate(personDetails.birthday)} +
+
+ )} + + {/* Deathday */} + {personDetails.deathday && ( +
+ +
+ Data śmierci: + {formatDate(personDetails.deathday)} +
+
+ )} + + {/* Place of birth */} + {personDetails.place_of_birth && ( +
+ + {personDetails.place_of_birth} +
+ )} +
+ + {/* Biography */} + {personDetails.biography && ( +
+

+ Biografia +

+
+ {personDetails.biography + .split("\n\n") + .map((paragraph, index) => ( +

{paragraph}

+ ))} +
+
+ )} + + {/* External links */} + {personDetails.external_ids && ( +
+

+ Linki +

+
+ {Object.entries(personDetails.external_ids).map( + ([key, value]) => { + if (!(key in externalIdsMap) || !value) { + return null; + } + + const { label, icon, url } = + externalIdsMap[ + key as keyof typeof externalIdsMap + ]; + return ( + + {icon} + {label} + + ); + } + )} +
+
+ )} +
+
+
+
+
+
+
+ ); +}; + +const externalIdsMap = { + facebook_id: { + label: "Facebook", + icon: , + url: (id: string) => `https://www.facebook.com/${id}`, + }, + instagram_id: { + label: "Instagram", + icon: , + url: (id: string) => `https://www.instagram.com/${id}`, + }, + twitter_id: { + label: "Twitter", + icon: , + url: (id: string) => `https://www.twitter.com/${id}`, + }, + tiktok_id: { + label: "TikTok", + icon: , + url: (id: string) => `https://www.tiktok.com/${id}`, + }, + youtube_id: { + label: "YouTube", + icon: , + url: (id: string) => `https://www.youtube.com/${id}`, + }, + imdb_id: { + label: "IMDb", + icon: , + url: (id: string) => `https://www.imdb.com/name/${id}`, + }, + tvrage_id: { + label: "TVRage", + icon: null, + url: (id: string) => `https://www.tvrage.com/people/${id}`, + }, + wikidata_id: { + label: "Wikidata", + icon: null, + url: (id: string) => `https://www.wikidata.org/wiki/${id}`, + }, +}; diff --git a/src/components/molecules/MovieCast/index.tsx b/src/components/molecules/MovieCast/index.tsx index 034139e..97461be 100644 --- a/src/components/molecules/MovieCast/index.tsx +++ b/src/components/molecules/MovieCast/index.tsx @@ -1,4 +1,5 @@ "use client"; +import Link from "next/link"; import { Button } from "@/components/atoms/Button"; import { MovieDetailsRich } from "@/lib/tmdb/types"; import { FC, useState } from "react"; @@ -31,7 +32,11 @@ export const MovieCast: FC = ({ movieDetails }) => {

Obsada

{mainCast.map((actor) => ( -
+
= ({ movieDetails }) => { className="w-full object-cover group-hover:scale-110 transition-transform duration-500 bg-gradient-to-br from-purple-500/20 to-cyan-500/20" />
+ + {/* Hover overlay with link indication */} +
+
+ Zobacz profil +
+
-

{actor.name}

+

+ {actor.name} +

{actor.character}

-
+ ))}
diff --git a/src/lib/tmdb/index.ts b/src/lib/tmdb/index.ts index dc8e78e..afbd357 100644 --- a/src/lib/tmdb/index.ts +++ b/src/lib/tmdb/index.ts @@ -2,12 +2,12 @@ import { search, getMovieDetails, getMovieCredits, - getMovieDetailsRich, + getPersonDetails, } from "./server"; export const TMDB = { search, getMovieDetails, getMovieCredits, - getMovieDetailsRich, + getPersonDetails, }; diff --git a/src/lib/tmdb/server.ts b/src/lib/tmdb/server.ts index fdc5753..cbbd23d 100644 --- a/src/lib/tmdb/server.ts +++ b/src/lib/tmdb/server.ts @@ -5,6 +5,10 @@ import { MovieDetails, MovieCredits, MovieDetailsRich, + PersonDetails, + PersonMovieCredits, + PersonImages, + PersonDetailsRich, } from "./types"; const url = "https://api.themoviedb.org/3"; @@ -65,11 +69,7 @@ export async function getUpcomingMovies(): Promise { return await fetchTmbd("/movie/upcoming?language=pl®ion=PL"); } -export async function getMovieDetails(movieId: number): Promise { - return await fetchTmbd(`/movie/${movieId}?language=pl&`); -} - -export async function getMovieDetailsRich( +export async function getMovieDetails( movieId: number ): Promise { return await fetchTmbd( @@ -80,3 +80,11 @@ export async function getMovieDetailsRich( export async function getMovieCredits(movieId: number): Promise { return await fetchTmbd(`/movie/${movieId}/credits?language=pl`); } + +export async function getPersonDetails( + personId: number +): Promise { + return await fetchTmbd( + `/person/${personId}?language=pl&append_to_response=movie_credits,images,external_ids` + ); +} diff --git a/src/lib/tmdb/types.ts b/src/lib/tmdb/types.ts index 0d35353..004064c 100644 --- a/src/lib/tmdb/types.ts +++ b/src/lib/tmdb/types.ts @@ -111,6 +111,98 @@ export type MovieCredits = { crew: CrewMember[]; }; +export type PersonDetails = { + adult: boolean; + also_known_as: string[]; + biography: string; + birthday: string | null; + deathday: string | null; + gender: number; + homepage: string | null; + id: number; + imdb_id: string | null; + known_for_department: string; + name: string; + place_of_birth: string | null; + popularity: number; + profile_path: string | null; +}; + +export type PersonExternalIds = { + id: number; + freebase_mid: string | null; + freebase_id: string | null; + imdb_id: string | null; + tvrage_id: number | null; + wikidata_id: string | null; + facebook_id: string | null; + instagram_id: string | null; + twitter_id: string | null; + tiktok_id: string | null; + youtube_id: string | null; +}; + +export type PersonMovieCredits = { + cast: { + adult: boolean; + backdrop_path: string | null; + genre_ids: number[]; + id: number; + original_language: string; + original_title: string; + overview: string; + popularity: number; + poster_path: string | null; + release_date: string; + title: string; + video: boolean; + vote_average: number; + vote_count: number; + character: string; + credit_id: string; + order: number; + }[]; + crew: { + adult: boolean; + backdrop_path: string | null; + genre_ids: number[]; + id: number; + original_language: string; + original_title: string; + overview: string; + popularity: number; + poster_path: string | null; + release_date: string; + title: string; + video: boolean; + vote_average: number; + vote_count: number; + credit_id: string; + department: string; + job: string; + }[]; + id: number; +}; + +export type PersonImages = { + id: number; + profiles: { + aspect_ratio: number; + height: number; + iso_639_1: string | null; + file_path: string; + vote_average: number; + vote_count: number; + width: number; + }[]; +}; + +export type PersonDetailsRich = PersonDetails & { + movie_credits: PersonMovieCredits; + images: PersonImages; + external_ids: PersonExternalIds; +}; + export type MovieDetailsRich = MovieDetails & { credits: MovieCredits; similar: SearchResult;