# Book Detail Page Implementation Guide Complete implementation guide for the `/media/:uuid` book detail page. ## ✅ Verified Backend Code All database queries, function signatures, and struct definitions used in this guide have been verified against the actual codebase: - `GetMediaItem` ✓ (line 179 in querier.go) - `GetMediaRating` ✓ (line 201 in querier.go) - `GetCollectionsForBook` ✓ (line 132 in querier.go) - `GetReadingProgress` ✓ (line 213 in querier.go) - `ListSyncConflictsByMediaItem` ✓ (line 267 in querier.go) - `GetMediaNotes` ✓ (line 200 in querier.go) - `GetMediaHighlights` ✓ (line 178 in querier.go) - `database.MediaItems` struct ✓ (lines 182-235 in models.go) - `database.Collections` struct ✓ (lines 20-34 in models.go) - `database.MediaRatings` struct ✓ (lines 254-262 in models.go) - `database.ReadingProgress` struct ✓ (lines 287-312 in models.go) ## Overview - **Route**: `GET /media/:uuid` - **Template**: SSR-first with Alpine.js for modal interactions - **Data Structure**: Embeds `database.MediaItems` to avoid duplication - **Features**: Cover + metadata, progress tracking, sync modal, collections, placeholder buttons --- ## Files to Create ### 1. MediaDetail Data Structure **Location**: `internal/handlers/media_detail.go` (new file) **Full file content**: ```go package handlers import ( "bookhoard/internal/database" "encoding/json" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" ) // MediaDetail embeds database.MediaItems for complete book metadata // No field duplication - template gets direct access to all database fields type MediaDetail struct { database.MediaItems // Embedded - ALL book fields available // User-specific data Rating *database.MediaRatings `json:"rating,omitempty"` Collections []database.Collections `json:"collections"` ReadingProgress *database.ReadingProgress `json:"reading_progress,omitempty"` // Conflict data (if exists) ActiveConflict *ConflictDetailResponse `json:"active_conflict,omitempty"` // Computed counts NotesCount int `json:"notes_count"` HighlightsCount int `json:"highlights_count"} } ``` **Note**: The actual handler is implemented in `internal/router/frontend.go` as an inline function (see step 5), following the pattern used by all other frontend routes in this codebase. --- ### 2. `templates/book_detail.templ` **Location**: `templates/book_detail.templ` (new file) **Full file content**: ```templ package templates import "bookhoard/internal/handlers" templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
{ book.Description.String }
⚠️ Progress conflict detected - Click "Sync Progress" to review and resolve
Progress
{ fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64) }%
Page
{ book.ReadingProgress.CurrentPage.Int32 } / { book.ReadingProgress.TotalPages.Int32 }
Last Read
{ book.ReadingProgress.LastReadAt.Time.Format("2006-01-02 15:04") }
Source
{ book.ReadingProgress.LastSyncSource.String }
Publisher
{ book.Publisher.String }
Published
{ book.DatePublished.Time.Format("2006-01-02") }
ISBN
{ book.ISBN.String }
Language
{ book.Language.String }
Edition
{ book.Edition.String }
Pages
{ book.PageCount.Int32 }
Genre
{ book.Genre.String }
Copyright Year
{ book.CopyrightYear.Int32 }
Format
{ book.MimeType.String }
File Size
{ formatFileSize(book.FileSize.Int64) }
{ book.Title }
⚠️ Conflict Detected
Progress differs between devices. Review the options below and manually resolve via the Conflicts page.
Go to Conflicts Page →{ data.Timestamp.Format("2006-01-02 15:04:05") }
No conflicts detected. Current progress from { book.ReadingProgress.LastSyncSource.String }:
{ book.ReadingProgress.LastSyncSource.String }
if book.ReadingProgress.CurrentPage.Valid && book.ReadingProgress.TotalPages.Valid {Page { book.ReadingProgress.CurrentPage.Int32 } of { book.ReadingProgress.TotalPages.Int32 }
} if book.ReadingProgress.LastReadAt.Valid {Last read: { book.ReadingProgress.LastReadAt.Time.Format("2006-01-02 15:04") }
}This book has { book.NotesCount } notes and { book.HighlightsCount } highlights.
Feature coming soon!