feat: add book detail page with comprehensive metadata display

Implement SSR-first book detail page at /media/:uuid with complete
book information, progress tracking, and interactivity.

Features:
- Cover image (256x384px) with responsive layout
- Complete metadata: title, author, description, publisher, ISBN,
  language, edition, page count, genre, copyright year, format
- External service links (Goodreads, Open Library, Google Books, Amazon)
  with smart URL fallback: ID → ISBN → Title+Author
- Reading progress display with device sources (web/kobo/koreader)
- Sync progress modal for conflict resolution
- Collections display as clickable badges
- Notes/highlights counter with placeholder modal
- Rating display (1-10 scale with star rendering)
- HTML sanitization for book descriptions using bluemonday

Data Structure:
- handlers.MediaDetail embeds database.MediaItems for zero duplication
- Uses existing database queries (GetMediaItem, GetMediaRating, etc.)
- Follows project pattern: no parallel type systems

Frontend:
- TypeScript modal triggers (book-detail.ts)
- Alpine.js for modal interactions
- TailwindCSS styling with theme variables
- Responsive: cover-left layout, mobile stacks vertically

Backend:
- Route: GET /media/:uuid (protected)
- Handler: inline function in frontend.go following existing pattern
- Template: SSR-first with progressive enhancement
- Returns HTML only (API uses separate /api/media-items/:id endpoint)

Files created:
- internal/handlers/media_detail.go
- templates/book_detail.templ
- templates/book_detail_modals.templ
- web/src/book-detail.ts

Files modified:
- internal/router/frontend.go (add route)
- web/src/main.ts (import module)
This commit is contained in:
2026-03-28 23:54:39 -04:00
parent 1c52903172
commit eb349cbc95
8 changed files with 1867 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
import { Alpine } from "./alpine";
import { showToast } from "./toast";
function showReaderPlaceholder(): void {
showToast("Ebook reader coming soon!", "info");
}
function showMetadataEditorPlaceholder(): void {
showToast("Metadata editor coming soon!", "info");
}
function showProgressSyncModal(): void {
const modal = document.getElementById("progress-sync-modal");
if (modal) {
modal.classList.remove("hidden");
}
}
function showNotesModal(): void {
const modal = document.getElementById("notes-modal");
if (modal) {
modal.classList.remove("hidden");
}
}
function hideProgressSyncModal(): void {
const modal = document.getElementById("progress-sync-modal");
if (modal) {
modal.classList.add("hidden");
}
}
function hideNotesModal(): void {
const modal = document.getElementById("notes-modal");
if (modal) {
modal.classList.add("hidden");
}
}
export {
showReaderPlaceholder,
showMetadataEditorPlaceholder,
showProgressSyncModal,
showNotesModal,
hideProgressSyncModal,
hideNotesModal,
};
Alpine.data("bookDetail", () => ({
showReaderPlaceholder,
showMetadataEditorPlaceholder,
showProgressSyncModal,
showNotesModal,
hideProgressSyncModal,
hideNotesModal,
}));
+1
View File
@@ -8,6 +8,7 @@ import "./api-explorer";
import "./api-explorer-docs";
import "./bookPicker";
import "./bookshelf";
import "./book-detail";
import "./collection-rules";
import "./collections";
import "./conflicts";