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.
This commit is contained in:
John O'Keefe committed 2026-10-15 10:53:53 -04:00
1 parent fd6f0e3353
commit 2117ec6181
13 files changed
+751 -352

No files matched your search

+18
View File
@@ -345,6 +345,20 @@ CREATE TABLE IF NOT EXISTS media_ratings (
UNIQUE(media_item_id, user_id)
);
-- Create media_reviews table: the user's markdown book review. Sibling of
-- media_ratings (same uniqueness + cascade shape) but standalone — a review
-- without a rating (and vice versa) must work. Stored as RAW markdown and
-- rendered only at display sites (same doctrine as notes).
CREATE TABLE IF NOT EXISTS media_reviews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
review TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(media_item_id, user_id)
);
-- Create media_notes table for user notes on media items
CREATE TABLE IF NOT EXISTS media_notes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -602,6 +616,10 @@ CREATE INDEX IF NOT EXISTS idx_reading_progress_user_id ON reading_progress(user
CREATE INDEX IF NOT EXISTS idx_media_ratings_media_item_id ON media_ratings(media_item_id);
CREATE INDEX IF NOT EXISTS idx_media_ratings_user_id ON media_ratings(user_id);
-- Reviews indexes
CREATE INDEX IF NOT EXISTS idx_media_reviews_media_item_id ON media_reviews(media_item_id);
CREATE INDEX IF NOT EXISTS idx_media_reviews_user_id ON media_reviews(user_id);
-- Notes and highlights indexes
CREATE INDEX IF NOT EXISTS idx_media_notes_media_item_id ON media_notes(media_item_id);
CREATE INDEX IF NOT EXISTS idx_media_notes_user_id ON media_notes(user_id);