ReviewOS

stacks/bench-review

content: photos in reviews

#31
Closed glennmichael123 opened this 10 days ago ·
10 days ago

Reviews are text + rating only. A photo (courtroom signage, public docket board, building exterior) anchors credibility and gives the content a richer feed shape — Yelp without photos would feel half-finished.

Scope

  • Storage: local filesystem for MVP (storage/uploads/review-photos/<review-uuid>/<photo-uuid>.jpg), S3-compatible adapter for prod. Stacks's storage package already supports both.
  • Upload UI: drop zone in the review composer (/review/{judgeId}), max 4 photos per review, max 5MB each, JPEG/PNG/WebP only.
  • Server: image processing pipeline — strip EXIF, generate 3 sizes (thumb 200w, card 800w, full 1600w), persist all three.
  • New review_photos table: id, judge_review_id, original_filename, mime, sizes (JSON), order, created_at.
  • API: POST /api/reviews/{id}/photos (auth-gated, author-only), DELETE /api/me/reviews/{id}/photos/{photoId}.
  • Render: photo gallery in ArticleView.stx; lightbox on click; card-size thumbs in feed lists.

Acceptance

  • Author can attach up to 4 photos at review submission time, see thumbs render in the published article, delete from their own.
  • Photos drop into the moderation queue alongside the review text (one approve/reject covers both).
  • EXIF (especially GPS) stripped from every saved variant — privacy table-stakes.
  • Page weight stays sane: feed thumbnails load 800w, article body loads up to 1600w on click.
9 days ago

Shipped — full upload + resize + EXIF strip + render pipeline.

Schema — `database/migrations/0000000113-create-review_photos-table.sql`:

  • `id, judge_review_id, user_id, thumb_url, card_url, full_url, mime, width, height, order_index, created_at`.
  • Three URL columns track the 3-size resize ladder; the on-disk variants live under `storage/uploads/review-photos/<review-uuid>/`.
  • Indexes on `(judge_review_id, order_index)` + `user_id` for the listing + ownership lookups.

Pipeline — `app/Helpers/reviewPhotos.ts`:

  • Lazy-imports sharp (added as a direct dep this turn).
  • Per file: `rotate()` (honour EXIF orientation, then drop everything) → resize to thumb 200×200 cover, card 800w, full 1600w → `withMetadata({})` strips ALL metadata (EXIF, GPS, ICC color profiles) → encode as WebP q=80.
  • Accepts JPEG / PNG / WebP up to 8MB raw. Larger / wrong-type rejected before any disk write.
  • Returns the persisted URL triple + dimensions of the full variant.

Why EXIF strip is the load-bearing privacy concern: bench-review's audience is legal professionals whose camera-roll GPS metadata is a real retaliation risk vector. Stripping at upload time means the bytes-on-disk never carry it, period — even if storage is later proxied or downloaded, the original location data is gone. `withMetadata({})` on sharp's pipeline drops every metadata block; the WebP re-encode pass is a second-layer guarantee.

API:

  • `POST /api/reviews/{id}/photos` (`UploadReviewPhotosAction.ts`) — auth-gated, owner-only. Multipart payload with `photos` field. Enforces 4-per-review cap server-side (counts existing + current batch). Per-file failures collected and returned alongside successes so a partial batch doesn't roll back.
  • `DELETE /api/me/photos/{id}` (`DeleteMyReviewPhotoAction.ts`) — owner-only. Hard-deletes the row + unlinks the three on-disk variants (best-effort; missing files don't fail the row delete).
  • `ShowReviewAction` extended to return the `photos` array on the review payload, ordered by `order_index`.

UI:

  • `ReviewForm.stx` post-submit success panel now has an "Attach photos (optional)" section. Drop-zone-style label with a file input, multi-select up to 4. Captures the freshly-created `review_id` from the submit response and posts to the upload endpoint via the store's new `uploadPhotos(reviewId, formData)` helper. Privacy note inline: "Camera metadata (including GPS) is stripped at upload — your photos won't reveal where you were when you took them."
  • `ArticleView.stx` renders a gallery between the byline and the article body. Single photo → 1-col, 2 photos → 2-col, 3+ → 2-col on mobile / 3-col on desktop. Clicking a photo opens the 1600w full variant in a new tab (lightbox proper is a follow-up).

Deferred (call-outs for the next pass):

  • In-feed thumbnails on `/reviews` cards — the data's available (`photos[0].thumb_url`) but `LatestReviewsAction` doesn't return it yet. ~5 LOC to add the JOIN once the feed-card layout is updated to accommodate the thumb.
  • Edit-page photo management — `ArticleEdit.stx` doesn't have the upload panel. Authors who want to add photos to an already-submitted review currently can't. Same UI block as the submit-success panel, dropped into the edit page.
  • Storage backend swap — local FS is fine for MVP; for production, swap `writeFileSync` for a Stacks storage adapter (already abstracted at `@stacksjs/storage`) targeting an S3-compat bucket. The URL-column architecture stays unchanged; only the persistence call site moves.
  • Reorder UI — `order_index` is in the schema but there's no drag-to-reorder UI yet. Authors can effectively reorder by deleting + re-uploading in the desired sequence.
  • Lightbox for the full variant — current behaviour opens a new tab. Inline modal viewer is a polish pass.

Sign in to comment on this issue.