feat(plex): person cards in the grid for the filtered actors (S3)

The actors currently in the filter now render as cards above the poster grid —
headshot + name + in-scope title count + a remove x — so you SEE who you're
exploring, not just chips in the rail (generalises the old search-only person
cards to the applied filter). New GET /plex/people/cards (photo from the
plex_people cache, count live against cast_names); PlexBrowse fetches + renders
them; i18n en/hu. Verified: John Goodman (11) / Douglas M. Griffin (1) / Bradley
Cooper (14) with photos + counts. (Plex cast-explore S3)
This commit is contained in:
2026-07-19 03:46:31 +02:00
parent 6f28ce36a7
commit 05105197e9
5 changed files with 114 additions and 2 deletions
+49
View File
@@ -15,6 +15,8 @@ import {
RotateCcw,
Star,
Tv2,
User,
X,
type LucideIcon,
} from "lucide-react";
import {
@@ -136,6 +138,17 @@ export default function PlexBrowse() {
const items = browseQ.data?.pages.flatMap((p) => p.items) ?? [];
const total = browseQ.data?.pages[0]?.total ?? 0;
// Cards for the actors currently in the filter (name + headshot + in-scope count) — shown above the
// grid so you SEE who you're exploring, not just chips in the rail.
const peopleCardsQ = useQuery({
queryKey: ["plex-people-cards", scope, filters.actors],
queryFn: () => api.plexPeopleCards(filters.actors, scope),
enabled: sub.view.kind === "grid" && filters.actors.length > 0,
staleTime: 60_000,
});
const personCards = peopleCardsQ.data?.people ?? [];
const removeActor = (name: string) =>
setFilters({ ...filters, actors: filters.actors.filter((x) => x !== name) });
// Episode matches (grouped "Episodes" section) — search-only; same set on every page, take page 0.
const episodes = browseQ.data?.pages[0]?.episodes ?? [];
@@ -280,6 +293,42 @@ export default function PlexBrowse() {
</div>
</PageToolbar>
{personCards.length > 0 && (
<div className="mb-4 flex flex-wrap gap-3">
{personCards.map((p) => (
<div
key={p.name}
className="glass-card flex items-center gap-2.5 rounded-xl py-1.5 pl-1.5 pr-2.5"
>
{p.photo ? (
<img
src={p.photo}
alt=""
loading="lazy"
className="w-10 h-10 rounded-full object-cover shrink-0"
/>
) : (
<div className="w-10 h-10 rounded-full grid place-items-center bg-surface text-muted shrink-0">
<User className="w-5 h-5" />
</div>
)}
<div className="min-w-0">
<div className="text-sm font-medium truncate max-w-[150px]">{p.name}</div>
<div className="text-[11px] text-muted">{t("plex.people.count", { count: p.count })}</div>
</div>
<button
onClick={() => removeActor(p.name)}
title={t("plex.people.remove")}
aria-label={t("plex.people.remove")}
className="ml-1 p-1 rounded-full text-muted hover:text-fg hover:bg-card transition"
>
<X className="w-4 h-4" />
</button>
</div>
))}
</div>
)}
{browseQ.isLoading ? (
<p className="text-muted text-sm">{t("plex.loading")}</p>
) : items.length === 0 && episodes.length === 0 ? (