From 4fa8895556a1db5c17a5533dc5c655c30be5bfa9 Mon Sep 17 00:00:00 2001 From: RXWatcher <14085001+RXWatcher@users.noreply.github.com> Date: Sun, 24 May 2026 14:51:56 +0200 Subject: [PATCH] feat(audiobooks): library grid page at /audiobooks Renders a paginated grid of audiobook cards using the useAudiobookLibrary hook. Each card links to /audiobooks/book/{id}. Cards show poster, title, and year; falls back to a "No cover" placeholder when the audiobook has no poster_url. Empty state hints to operators that they need to set a library's type to 'audiobooks'. Routes themselves are wired in Task 8 (navigation integration). Co-Authored-By: Claude Opus 4.7 (1M context) --- web/src/pages/audiobooks/AudiobookLibrary.tsx | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 web/src/pages/audiobooks/AudiobookLibrary.tsx diff --git a/web/src/pages/audiobooks/AudiobookLibrary.tsx b/web/src/pages/audiobooks/AudiobookLibrary.tsx new file mode 100644 index 00000000..5825604b --- /dev/null +++ b/web/src/pages/audiobooks/AudiobookLibrary.tsx @@ -0,0 +1,123 @@ +import { useState } from "react"; + +import ScrollToTopButton from "@/components/ScrollToTopButton"; +import { Skeleton } from "@/components/ui/skeleton"; +import ViewTransitionLink from "@/components/ViewTransitionLink"; +import { useAudiobookLibrary } from "@/hooks/audiobooks/useAudiobookLibrary"; + +const PAGE_SIZE = 60; + +const GRID_CLASSES = + "grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-7 xl:grid-cols-8 gap-3"; + +export default function AudiobookLibrary() { + const [offset, setOffset] = useState(0); + const { data, isLoading, error } = useAudiobookLibrary({ limit: PAGE_SIZE, offset }); + + if (isLoading && !data) { + return ( +
+

Audiobooks

+
+ {Array.from({ length: 24 }).map((_, i) => ( +
+ + +
+ ))} +
+
+ ); + } + + if (error) { + return ( +
+

Audiobooks

+
+ Failed to load audiobooks: {error instanceof Error ? error.message : "Unknown error"} +
+
+ ); + } + + const items = data?.items ?? []; + const total = data?.total ?? 0; + + return ( +
+

Audiobooks

+ {items.length === 0 ? ( +
+ No audiobooks indexed yet. Set a library's type to{" "} + audiobooks{" "} + and rescan. +
+ ) : ( +
+ {items.map((item) => ( +
+ +
+ {item.poster_url ? ( + {item.title} + ) : ( +
+ {item.title || "No Cover"} +
+ )} +
+
+ + +
+ {item.title} +
+ {item.year > 0 && ( +
+ {item.year} +
+ )} +
+
+ ))} +
+ )} + {total > PAGE_SIZE && ( +
+ + + {offset + 1}–{Math.min(offset + PAGE_SIZE, total)} of {total} + + +
+ )} + +
+ ); +}