feat(sections): show episode context in cards
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" : ""}
|
||||
|
||||
Reference in New Issue
Block a user