Files
bookhoard/internal/handlers/media_detail.go
T
John O'Keefe 2117ec6181 feat(web): book reviews — markdown, sibling of ratings, full API + page surface
media_reviews table mirrors media_ratings' shape (unique per user+item,
cascade deletes, updated_at for LWW) but standalone: a review without a
rating must work. Body is RAW MARKDOWN — storage and every payload carry
the source; rendering happens only at display sites (same doctrine as
notes, same sanitization pipeline: markdown-it + DOMPurify).

API: GET/PUT/DELETE /api/media-items/:id/review (PUT upserts; an empty
body deletes — clearing via an editor is a delete, not an empty row).
Book-detail page payload now carries the review beside the rating.

Page: the review renders above the synopsis as a scrolling card (same
max-height box as the summary), hydrated client-side through
renderNoteHtml; 'Write review'/'Edit review' sits next to the rating
stars and opens a raw-markdown editor dialog (save / delete / cancel).

sqlc regenerated; schema applied to the dev DB. Live-verified: upsert,
get, empty-delete, page markup, and the app round-trip against the dev
server.
2026-10-15 10:53:53 -04:00

47 lines
1.7 KiB
Go

package handlers
import "bookhoard/internal/database"
// 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
// Absolute on-disk location (library folder + relative path), resolved by
// the page handler so the UI can show where the file lives.
FileLocation string `json:"file_location"`
// User-specific data
Rating *database.MediaRatings `json:"rating,omitempty"`
Review *database.MediaReviews `json:"review,omitempty"` // raw markdown
Collections []database.Collections `json:"collections"`
ReadingProgress *database.ReadingProgress `json:"reading_progress,omitempty"`
// The viewer's own reading-history card for this book (nil when the
// user has no sessions here); rendered as the "Your Reading Stats"
// section of the book page.
BookStats *BookStatsData `json:"book_stats,omitempty"`
// Conflict data (if exists)
ActiveConflict *ConflictDetailResponse `json:"active_conflict,omitempty"`
// Computed counts
NotesCount int `json:"notes_count"`
HighlightsCount int `json:"highlights_count"`
// Deleted-annotation history (tombstoned rows, newest first) — the book
// page's "recently deleted" list with restore/permanent-delete actions.
DeletedAnnotations []DeletedAnnotationResponse `json:"deleted_annotations"`
}
// BookStatsData is the per-book reading-stats card payload.
type BookStatsData struct {
TotalMinutes int
TotalPages int
SessionCount int
MaxProgress float64 // 0..1
Finished bool
LastSession string
FirstSession string
}