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.
47 lines
1.7 KiB
Go
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
|
|
}
|