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:
1 parent
fd6f0e3353
commit
2117ec6181
13 files changed
+751
-352
No files matched your search
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.0
|
||||
// sqlc v1.31.1
|
||||
|
||||
package database
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.0
|
||||
// sqlc v1.31.1
|
||||
|
||||
package database
|
||||
|
||||
@@ -352,6 +352,15 @@ type MediaRatings struct {
|
||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type MediaReviews struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
|
||||
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||
Review string `db:"review" json:"review"`
|
||||
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type OpdsTokens struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
DeviceID pgtype.UUID `db:"device_id" json:"device_id"`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.0
|
||||
// sqlc v1.31.1
|
||||
|
||||
package database
|
||||
|
||||
@@ -142,6 +142,7 @@ type Querier interface {
|
||||
DeleteMediaItemFormat(ctx context.Context, id pgtype.UUID) error
|
||||
DeleteMediaNote(ctx context.Context, arg DeleteMediaNoteParams) error
|
||||
DeleteMediaRating(ctx context.Context, arg DeleteMediaRatingParams) error
|
||||
DeleteMediaReview(ctx context.Context, arg DeleteMediaReviewParams) error
|
||||
DeleteProcessingIssue(ctx context.Context, id pgtype.UUID) (ProcessingIssues, error)
|
||||
DeleteReadingProgress(ctx context.Context, arg DeleteReadingProgressParams) error
|
||||
DeleteSavedFilter(ctx context.Context, arg DeleteSavedFilterParams) (SavedFilters, error)
|
||||
@@ -299,6 +300,7 @@ type Querier interface {
|
||||
GetMediaNotes(ctx context.Context, arg GetMediaNotesParams) ([]MediaNotes, error)
|
||||
GetMediaRating(ctx context.Context, arg GetMediaRatingParams) (MediaRatings, error)
|
||||
GetMediaRatings(ctx context.Context, mediaItemID pgtype.UUID) ([]GetMediaRatingsRow, error)
|
||||
GetMediaReview(ctx context.Context, arg GetMediaReviewParams) (MediaReviews, error)
|
||||
GetNextRetryTime(ctx context.Context) (interface{}, error)
|
||||
GetNotStartedItems(ctx context.Context, arg GetNotStartedItemsParams) ([]MediaItems, error)
|
||||
// Get OPDS token
|
||||
@@ -573,6 +575,7 @@ type Querier interface {
|
||||
UpdateUsername(ctx context.Context, arg UpdateUsernameParams) error
|
||||
UpsertDashboardPreferences(ctx context.Context, arg UpsertDashboardPreferencesParams) (UserDashboardPreferences, error)
|
||||
UpsertGlobalDashboardPreferences(ctx context.Context, arg UpsertGlobalDashboardPreferencesParams) (UserDashboardPreferencesGlobal, error)
|
||||
UpsertMediaReview(ctx context.Context, arg UpsertMediaReviewParams) (MediaReviews, error)
|
||||
UpsertPanelData(ctx context.Context, arg UpsertPanelDataParams) (PanelData, error)
|
||||
UpsertReaderSettings(ctx context.Context, arg UpsertReaderSettingsParams) (ReaderSettings, error)
|
||||
UpsertSystemSetting(ctx context.Context, arg UpsertSystemSettingParams) (SystemSettings, error)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.0
|
||||
// sqlc v1.31.1
|
||||
// source: queries.sql
|
||||
|
||||
package database
|
||||
@@ -2262,6 +2262,20 @@ func (q *Queries) DeleteMediaRating(ctx context.Context, arg DeleteMediaRatingPa
|
||||
return err
|
||||
}
|
||||
|
||||
const DeleteMediaReview = `-- name: DeleteMediaReview :exec
|
||||
DELETE FROM media_reviews WHERE media_item_id = $1 AND user_id = $2
|
||||
`
|
||||
|
||||
type DeleteMediaReviewParams struct {
|
||||
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
|
||||
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteMediaReview(ctx context.Context, arg DeleteMediaReviewParams) error {
|
||||
_, err := q.db.Exec(ctx, DeleteMediaReview, arg.MediaItemID, arg.UserID)
|
||||
return err
|
||||
}
|
||||
|
||||
const DeleteProcessingIssue = `-- name: DeleteProcessingIssue :one
|
||||
DELETE FROM processing_issues
|
||||
WHERE id = $1
|
||||
@@ -6518,6 +6532,29 @@ func (q *Queries) GetMediaRatings(ctx context.Context, mediaItemID pgtype.UUID)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetMediaReview = `-- name: GetMediaReview :one
|
||||
SELECT id, media_item_id, user_id, review, created_at, updated_at FROM media_reviews WHERE media_item_id = $1 AND user_id = $2
|
||||
`
|
||||
|
||||
type GetMediaReviewParams struct {
|
||||
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
|
||||
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetMediaReview(ctx context.Context, arg GetMediaReviewParams) (MediaReviews, error) {
|
||||
row := q.db.QueryRow(ctx, GetMediaReview, arg.MediaItemID, arg.UserID)
|
||||
var i MediaReviews
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.MediaItemID,
|
||||
&i.UserID,
|
||||
&i.Review,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetNextRetryTime = `-- name: GetNextRetryTime :one
|
||||
SELECT
|
||||
CASE
|
||||
@@ -14976,6 +15013,36 @@ func (q *Queries) UpsertGlobalDashboardPreferences(ctx context.Context, arg Upse
|
||||
return i, err
|
||||
}
|
||||
|
||||
const UpsertMediaReview = `-- name: UpsertMediaReview :one
|
||||
INSERT INTO media_reviews (media_item_id, user_id, review)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (media_item_id, user_id)
|
||||
DO UPDATE SET
|
||||
review = EXCLUDED.review,
|
||||
updated_at = NOW()
|
||||
RETURNING id, media_item_id, user_id, review, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpsertMediaReviewParams struct {
|
||||
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
|
||||
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||
Review string `db:"review" json:"review"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertMediaReview(ctx context.Context, arg UpsertMediaReviewParams) (MediaReviews, error) {
|
||||
row := q.db.QueryRow(ctx, UpsertMediaReview, arg.MediaItemID, arg.UserID, arg.Review)
|
||||
var i MediaReviews
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.MediaItemID,
|
||||
&i.UserID,
|
||||
&i.Review,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const UpsertPanelData = `-- name: UpsertPanelData :one
|
||||
INSERT INTO panel_data (media_item_id, page_number, detection_method, panels)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
|
||||
@@ -550,6 +550,21 @@ RETURNING *;
|
||||
-- name: DeleteMediaRating :exec
|
||||
DELETE FROM media_ratings WHERE media_item_id = $1 AND user_id = $2;
|
||||
|
||||
-- name: UpsertMediaReview :one
|
||||
INSERT INTO media_reviews (media_item_id, user_id, review)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (media_item_id, user_id)
|
||||
DO UPDATE SET
|
||||
review = EXCLUDED.review,
|
||||
updated_at = NOW()
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetMediaReview :one
|
||||
SELECT * FROM media_reviews WHERE media_item_id = $1 AND user_id = $2;
|
||||
|
||||
-- name: DeleteMediaReview :exec
|
||||
DELETE FROM media_reviews WHERE media_item_id = $1 AND user_id = $2;
|
||||
|
||||
-- Search Media Items queries
|
||||
-- name: SearchMediaItems :many
|
||||
SELECT mi.*, l.name as library_name, lt.name as library_type_name
|
||||
|
||||
Reference in new issue
Block a user