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:
@@ -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,
|
||||
}));
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user