diff --git a/spa/src/locales/en.json b/spa/src/locales/en.json index cc7d097..3c03fc2 100644 --- a/spa/src/locales/en.json +++ b/spa/src/locales/en.json @@ -23,7 +23,9 @@ "run": "Run", "reviews": "{{count}} reviews", "films": "{{count}} films", - "filmsAvg": "{{count}} films, avg {{avg}}" + "filmsAvg": "{{count}} films, avg {{avg}}", + "more": "Show more", + "less": "Show less" }, "nav": { "home": "Home", diff --git a/spa/src/routes/_app/people.$id.tsx b/spa/src/routes/_app/people.$id.tsx index 1150e85..6736823 100644 --- a/spa/src/routes/_app/people.$id.tsx +++ b/spa/src/routes/_app/people.$id.tsx @@ -1,10 +1,15 @@ import { createFileRoute } from "@tanstack/react-router" +import { useState } from "react" import { useTranslation } from "react-i18next" -import { Calendar, ExternalLink, Film, Globe, MapPin, User } from "lucide-react" +import { Calendar, ChevronDown, ExternalLink, Film, Globe, MapPin, User } from "lucide-react" import { BackButton } from "@/components/back-button" import { MovieCard } from "@/components/movie-card" import { EmptyState } from "@/components/empty-state" +import { SwipeTabs } from "@/components/swipe-tabs" import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Separator } from "@/components/ui/separator" import { Skeleton } from "@/components/ui/skeleton" import { tmdbProfileUrl } from "@/lib/api/client" import { usePersonCredits } from "@/hooks/use-search" @@ -27,152 +32,212 @@ function PersonDetailPage() { const { person, cast, crew } = data + const age = person.birthday + ? differenceInYears( + person.deathday ? parseISO(person.deathday) : new Date(), + parseISO(person.birthday), + ) + : null + + const creditTabs = [ + ...(cast.length > 0 ? [{ value: "cast", label: t("movie.cast") + ` (${cast.length})` }] : []), + ...(crew.length > 0 ? [{ value: "crew", label: t("movie.crew") + ` (${crew.length})` }] : []), + ] as const + return (
{/* Header */}
-
+
{person.profile_path ? ( ) : (
- +
)}
-
+

{person.name}

{person.known_for_department && ( -

{person.known_for_department}

+ {person.known_for_department} )} - {person.birthday && ( -

- - {shortDate(person.birthday)} - {person.deathday && ` — ${shortDate(person.deathday)}`} - {` (${differenceInYears(person.deathday ? parseISO(person.deathday) : new Date(), parseISO(person.birthday))})`} -

- )} - {person.place_of_birth && ( -

- - {person.place_of_birth} -

+ {(person.homepage || person.imdb_url) && ( +
+ {person.imdb_url && ( + + + + IMDb + + + )} + {person.homepage && ( + + + + {t("person.homepage")} + + + )} +
)}
- {/* Links */} - {(person.homepage || person.imdb_url) && ( -
- {person.imdb_url && ( - - - - {t("person.imdb")} - - - )} - {person.homepage && ( - - - - {t("person.homepage")} - - - )} -
+ {/* Details card */} + {(person.birthday || person.place_of_birth) && ( + + + {person.birthday && ( +
+ + {shortDate(person.birthday)} + {age != null && ( + ({age}) + )} +
+ )} + {person.deathday && ( +
+ + {shortDate(person.deathday)} + ({t("person.deathday").toLowerCase()}) +
+ )} + {person.place_of_birth && ( +
+ + {person.place_of_birth} +
+ )} +
+
)} {/* Biography */} - {person.biography && ( -

{person.biography}

- )} + {person.biography && } {/* Also known as */} {person.also_known_as?.length > 0 && ( -
-

{t("person.alsoKnownAs")}

-
- {person.also_known_as.map((name) => ( - - {name} - - ))} -
-
+ + + {t("person.alsoKnownAs")} + + +
+ {person.also_known_as.map((name) => ( + + {name} + + ))} +
+
+
)} - {/* Cast credits */} - {cast.length > 0 && ( -
-

- {t("movie.castCredits", { count: cast.length })} -

-
- {cast.map((c) => ( - - ))} -
-
- )} + - {/* Crew credits */} - {crew.length > 0 && ( -
-

- {t("movie.crewCredits", { count: crew.length })} -

-
- {crew.map((c) => ( - - ))} -
-
- )} - - {cast.length === 0 && crew.length === 0 && ( + {/* Credits */} + {creditTabs.length > 0 ? ( + + {(tab) => ( + <> + {tab === "cast" && ( +
+ {cast.map((c) => ( + + ))} +
+ )} + {tab === "crew" && ( +
+ {crew.map((c) => ( + + ))} +
+ )} + + )} +
+ ) : ( )}
) } +const BIO_COLLAPSE_THRESHOLD = 300 + +function BiographySection({ text }: { text: string }) { + const { t } = useTranslation() + const isLong = text.length > BIO_COLLAPSE_THRESHOLD + const [expanded, setExpanded] = useState(!isLong) + + return ( + + + {t("person.biography")} + + +

+ {text} +

+ {isLong && ( + + )} +
+
+ ) +} + function PersonSkeleton() { return (
-
- -
+
+ +
- + +
+ + +
{[1, 2, 3, 4].map((i) => (