diff --git a/web/src/lib/audiobooks/types.ts b/web/src/lib/audiobooks/types.ts index a3973791..1b2a37c1 100644 --- a/web/src/lib/audiobooks/types.ts +++ b/web/src/lib/audiobooks/types.ts @@ -43,10 +43,29 @@ export interface AudiobookDetailItem { genres?: string[]; } +export interface AudiobookRelatedItem { + content_id: string; + title: string; + poster_url?: string; + year?: number; +} + +export interface AudiobookSeriesEntry { + content_id: string; + title: string; + poster_url?: string; + series_index?: number; +} + export interface AudiobookDetailResponse { audiobook: AudiobookDetailItem; author?: string; narrator?: string; files: AudiobookFile[]; progress?: AudiobookProgress; + also_by_author?: AudiobookRelatedItem[]; + in_series?: { + name?: string; + entries: AudiobookSeriesEntry[]; + }; } diff --git a/web/src/pages/audiobooks/AudiobookDetail.tsx b/web/src/pages/audiobooks/AudiobookDetail.tsx index fad17287..19cbbf96 100644 --- a/web/src/pages/audiobooks/AudiobookDetail.tsx +++ b/web/src/pages/audiobooks/AudiobookDetail.tsx @@ -7,6 +7,7 @@ import { Play } from "lucide-react"; import AudiobookPlayer from "./player/AudiobookPlayer"; import { ChaptersSection } from "./components/ChaptersSection"; import { NarratorCard } from "./components/NarratorCard"; +import { RelatedRail } from "./components/RelatedRail"; import DetailHero from "@/pages/ItemDetail/DetailHero"; import MetadataBadges from "@/pages/ItemDetail/components/MetadataBadges"; import type { AudiobookChapter, AudiobookFile } from "@/lib/audiobooks/types"; @@ -231,6 +232,31 @@ export default function AudiobookDetail() { onSelect={(s) => openPlayer(s)} /> {narrator && } + + {data.also_by_author && data.also_by_author.length > 0 && ( + ({ + content_id: it.content_id, + title: it.title, + poster_url: it.poster_url, + subtitle: it.year ? String(it.year) : undefined, + }))} + /> + )} + + {data.in_series && data.in_series.entries.length > 0 && ( + ({ + content_id: it.content_id, + title: it.title, + poster_url: it.poster_url, + subtitle: typeof it.series_index === "number" ? `Book ${it.series_index}` : undefined, + highlight: it.content_id === contentId, + }))} + /> + )} ); diff --git a/web/src/pages/audiobooks/components/RelatedRail.tsx b/web/src/pages/audiobooks/components/RelatedRail.tsx new file mode 100644 index 00000000..28408132 --- /dev/null +++ b/web/src/pages/audiobooks/components/RelatedRail.tsx @@ -0,0 +1,49 @@ +import ViewTransitionLink from "@/components/ViewTransitionLink"; + +interface RelatedRailItem { + content_id: string; + title: string; + poster_url?: string; + subtitle?: string; + highlight?: boolean; +} + +interface RelatedRailProps { + heading: string; + items: RelatedRailItem[]; +} + +export function RelatedRail({ heading, items }: RelatedRailProps) { + if (items.length === 0) return null; + return ( + + {heading} + + {items.map((item) => ( + + + {item.poster_url ? ( + + ) : null} + + {item.title} + {item.subtitle && ( + {item.subtitle} + )} + + ))} + + + ); +}