feat(sections): show episode context in cards

This commit is contained in:
Quick
2026-05-27 15:48:00 -04:00
parent dc4c20bb0f
commit 71ca2012c5
13 changed files with 485 additions and 86 deletions
+1 -1
View File
@@ -75,7 +75,7 @@ describe("ItemCard SortMeta", () => {
});
expect(markup).toContain("The Last of Us");
expect(markup).toContain("S1 E1");
expect(markup).toContain("S01E01");
expect(markup).toContain("When You're Lost in the Darkness");
});
});
+11 -14
View File
@@ -7,20 +7,12 @@ import { timeAgo } from "@/lib/timeAgo";
import MediaItemMenu from "@/components/MediaItemMenu";
import CardOverlays from "@/components/overlays/CardOverlays";
import { overlayDataFromBrowseItem, type CardOverlayPrefs } from "@/lib/overlays";
import { buildEpisodeCardLabels } from "@/lib/episodeCardLabels";
function SortMeta({ item, sortField }: { item: BrowseItem; sortField?: string }) {
if (
item.type === "episode" &&
item.series_title &&
item.season_number != null &&
item.episode_number != null
) {
return (
<>
S{item.season_number} E{item.episode_number}
{item.title ? ` • ${item.title}` : ""}
</>
);
const episodeLabels = buildEpisodeCardLabels(item);
if (episodeLabels) {
return <>{episodeLabels.episodeCode}</>;
}
const defaultLabel = [item.year || "", item.type === "series" ? "Series" : ""]
@@ -90,8 +82,8 @@ export default function ItemCard({
const [loaded, setLoaded] = useState(false);
const thumbhashUrl = item.poster_thumbhash ? decodeThumbhash(item.poster_thumbhash) : "";
const itemHref = `/item/${item.content_id}${libraryId ? `?libraryId=${libraryId}` : ""}`;
const displayTitle =
item.type === "episode" && item.series_title ? item.series_title : item.title;
const episodeLabels = buildEpisodeCardLabels(item);
const displayTitle = episodeLabels ? episodeLabels.seriesTitle : item.title;
return (
<div className="media-card group/card">
@@ -176,6 +168,11 @@ export default function ItemCard({
</div>
<ViewTransitionLink to={itemHref} className="block px-1 pt-3">
<div className="truncate text-[14px] font-semibold tracking-tight">{displayTitle}</div>
{episodeLabels?.episodeTitle ? (
<div className="text-muted-foreground mt-1 truncate text-[12px] font-medium">
{episodeLabels.episodeTitle}
</div>
) : null}
<div className="text-muted-foreground mt-1 text-[11px] font-medium tracking-[0.14em] uppercase">
<SortMeta item={item} sortField={sortField} />
</div>
@@ -21,6 +21,35 @@ vi.mock("@/hooks/useOverlayPrefs", () => ({
}));
describe("SectionItemCard", () => {
it("renders episode cards with series title, episode title, and episode code", () => {
const markup = renderToStaticMarkup(
<SectionItemCard
item={{
content_id: "episode-1",
type: "episode",
title: "Dumbston Checks In",
series_title: "American Dad!",
season_number: 22,
episode_number: 10,
year: 2025,
genres: ["Animation"],
status: "matched",
rating_imdb: 7.1,
overview: "Overview",
poster_url: "",
poster_thumbhash: "",
backdrop_url: "",
backdrop_thumbhash: "",
logo_url: "",
}}
/>,
);
expect(markup).toContain("American Dad!");
expect(markup).toContain("Dumbston Checks In");
expect(markup).toContain("S22E10");
});
it("renders premiere metadata when an upcoming event is present", () => {
const markup = renderToStaticMarkup(
<SectionItemCard
+16 -1
View File
@@ -5,6 +5,7 @@ import CardOverlays from "@/components/overlays/CardOverlays";
import { decodeThumbhash } from "@/lib/thumbhash";
import { useOverlayPrefs } from "@/hooks/useOverlayPrefs";
import { overlayDataFromSectionItem } from "@/lib/overlays";
import { buildEpisodeCardLabels } from "@/lib/episodeCardLabels";
import {
formatUpcomingDate,
formatUpcomingSubtitle,
@@ -28,6 +29,7 @@ export default function SectionItemCard({ item, libraryId }: SectionItemCardProp
const subtitle = upcomingEvent ? formatUpcomingSubtitle(upcomingEvent) : "";
const airDateLabel = upcomingEvent ? formatUpcomingDate(upcomingEvent.air_date) : "";
const airTimeLabel = upcomingEvent ? formatUpcomingTime(upcomingEvent.air_time) : null;
const episodeLabels = !upcomingEvent ? buildEpisodeCardLabels(item) : null;
return (
<div className="media-card group/card">
@@ -91,7 +93,9 @@ export default function SectionItemCard({ item, libraryId }: SectionItemCardProp
/>
</div>
<ViewTransitionLink to={itemHref} className="block px-1 pt-3">
<div className="truncate text-[14px] font-semibold tracking-tight">{item.title}</div>
<div className="truncate text-[14px] font-semibold tracking-tight">
{episodeLabels ? episodeLabels.seriesTitle : item.title}
</div>
{upcomingEvent ? (
<>
{subtitle && (
@@ -104,6 +108,17 @@ export default function SectionItemCard({ item, libraryId }: SectionItemCardProp
{airTimeLabel && <span className="text-muted-foreground">{airTimeLabel}</span>}
</div>
</>
) : episodeLabels ? (
<>
{episodeLabels.episodeTitle ? (
<div className="text-muted-foreground mt-1 truncate text-[12px] font-medium">
{episodeLabels.episodeTitle}
</div>
) : null}
<div className="text-muted-foreground mt-1 text-[11px] font-medium tracking-[0.14em] uppercase">
{episodeLabels.episodeCode}
</div>
</>
) : (
<div className="text-muted-foreground mt-1 text-[11px] font-medium tracking-[0.14em] uppercase">
{item.year ? `${item.year}` : ""} {item.type === "series" ? "Series" : ""}