feat: rich person detail page with bio, dates, links

This commit is contained in:
2026-06-11 13:42:04 +02:00
parent 9b932cde8e
commit b2a41db290
3 changed files with 82 additions and 5 deletions

View File

@@ -45,6 +45,14 @@ export const personDtoSchema = z.object({
name: z.string(), name: z.string(),
known_for_department: z.string().optional(), known_for_department: z.string().optional(),
profile_path: z.string().optional(), profile_path: z.string().optional(),
biography: z.string().optional(),
birthday: z.string().optional(),
deathday: z.string().optional(),
place_of_birth: z.string().optional(),
also_known_as: z.array(z.string()).default([]),
homepage: z.string().optional(),
imdb_url: z.string().optional(),
enriched: z.boolean().default(false),
}) })
export type PersonDto = z.infer<typeof personDtoSchema> export type PersonDto = z.infer<typeof personDtoSchema>

View File

@@ -126,6 +126,16 @@
"searchPlaceholder": "Search entries...", "searchPlaceholder": "Search entries...",
"watchedAgo": "Watched {{when}}" "watchedAgo": "Watched {{when}}"
}, },
"person": {
"biography": "Biography",
"birthday": "Born",
"deathday": "Died",
"placeOfBirth": "Birthplace",
"alsoKnownAs": "Also Known As",
"links": "Links",
"homepage": "Homepage",
"imdb": "IMDb"
},
"social": { "social": {
"title": "Social", "title": "Social",
"following": "Following", "following": "Following",

View File

@@ -1,13 +1,15 @@
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { Film, User } from "lucide-react" import { Calendar, ExternalLink, Film, Globe, MapPin, User } from "lucide-react"
import { BackButton } from "@/components/back-button" import { BackButton } from "@/components/back-button"
import { MovieCard } from "@/components/movie-card" import { MovieCard } from "@/components/movie-card"
import { EmptyState } from "@/components/empty-state" import { EmptyState } from "@/components/empty-state"
import { Badge } from "@/components/ui/badge"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton"
import { tmdbProfileUrl } from "@/lib/api/client" import { tmdbProfileUrl } from "@/lib/api/client"
import { usePersonCredits } from "@/hooks/use-search" import { usePersonCredits } from "@/hooks/use-search"
import { useDocumentTitle } from "@/hooks/use-document-title" import { useDocumentTitle } from "@/hooks/use-document-title"
import { shortDate } from "@/lib/date"
export const Route = createFileRoute("/_app/people/$id")({ export const Route = createFileRoute("/_app/people/$id")({
component: PersonDetailPage, component: PersonDetailPage,
@@ -28,24 +30,80 @@ function PersonDetailPage() {
<div className="space-y-4 p-4"> <div className="space-y-4 p-4">
<BackButton /> <BackButton />
<div className="flex items-center gap-4"> {/* Header */}
<div className="size-16 flex-shrink-0 overflow-hidden rounded-full bg-muted"> <div className="flex gap-4">
<div className="size-20 flex-shrink-0 overflow-hidden rounded-xl bg-muted">
{person.profile_path ? ( {person.profile_path ? (
<img src={tmdbProfileUrl(person.profile_path)} alt="" className="size-full object-cover" /> <img src={tmdbProfileUrl(person.profile_path)} alt="" className="size-full object-cover" />
) : ( ) : (
<div className="flex size-full items-center justify-center"> <div className="flex size-full items-center justify-center">
<User className="size-6 text-muted-foreground/40" /> <User className="size-8 text-muted-foreground/40" />
</div> </div>
)} )}
</div> </div>
<div> <div className="min-w-0 flex-1">
<h1 className="text-xl font-bold">{person.name}</h1> <h1 className="text-xl font-bold">{person.name}</h1>
{person.known_for_department && ( {person.known_for_department && (
<p className="text-sm text-muted-foreground">{person.known_for_department}</p> <p className="text-sm text-muted-foreground">{person.known_for_department}</p>
)} )}
{person.birthday && (
<p className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground">
<Calendar className="size-3" />
{shortDate(person.birthday)}
{person.deathday && `${shortDate(person.deathday)}`}
</p>
)}
{person.place_of_birth && (
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
<MapPin className="size-3" />
{person.place_of_birth}
</p>
)}
</div> </div>
</div> </div>
{/* Links */}
{(person.homepage || person.imdb_url) && (
<div className="flex gap-2">
{person.imdb_url && (
<a href={person.imdb_url} target="_blank" rel="noopener noreferrer">
<Badge variant="secondary" className="gap-1">
<ExternalLink className="size-3" />
{t("person.imdb")}
</Badge>
</a>
)}
{person.homepage && (
<a href={person.homepage} target="_blank" rel="noopener noreferrer">
<Badge variant="secondary" className="gap-1">
<Globe className="size-3" />
{t("person.homepage")}
</Badge>
</a>
)}
</div>
)}
{/* Biography */}
{person.biography && (
<p className="text-sm leading-relaxed text-muted-foreground">{person.biography}</p>
)}
{/* Also known as */}
{person.also_known_as.length > 0 && (
<div>
<p className="mb-1 text-xs font-medium text-muted-foreground">{t("person.alsoKnownAs")}</p>
<div className="flex flex-wrap gap-1">
{person.also_known_as.map((name) => (
<Badge key={name} variant="outline" className="text-xs font-normal">
{name}
</Badge>
))}
</div>
</div>
)}
{/* Cast credits */}
{cast.length > 0 && ( {cast.length > 0 && (
<section> <section>
<h2 className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground"> <h2 className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
@@ -70,6 +128,7 @@ function PersonDetailPage() {
</section> </section>
)} )}
{/* Crew credits */}
{crew.length > 0 && ( {crew.length > 0 && (
<section> <section>
<h2 className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground"> <h2 className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">