ReviewOS

stacks/bench-review

identity: reviewer public profile page

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

/profile is the signed-in user's own dashboard. resources/views/user/[id].stx exists as a bare shell. A reader looking at a review can't click through to see who wrote it, what else they've reviewed, or whether they're a credible voice.

For a Yelp-style trust model, reviewer-history is core — "this reviewer has 14 published reviews and an Elite badge" is what makes a single review interpretable.

Scope

  • /user/{id} — public profile view:
    • avatar, display name, member-since date, optional bio
    • badges: verified-credentials (see sibling issue), review-count milestone (10/50/100 published reviews)
    • list of published reviews authored by this user, paginated
    • if logged-in viewer is not this user: "Follow" button (future — start with read-only)
  • API: GET /api/users/{id} — public-safe user payload (no email, no internal IDs from other tables).
  • API: GET /api/users/{id}/reviews — paginated list of the user's published reviews (status=published only; pending/rejected stay private).
  • Cross-link: clicking the reviewer name on any /article/{id} page navigates to /user/{reviewer.id}.

Acceptance

  • A logged-out visitor can view a reviewer's public profile and their published reviews.
  • Email and other PII are never exposed via the public endpoint.
  • Reviewer names on articles are clickable.
9 days ago

Shipped.

Server:

  • `app/Actions/Users/UserShowAction.ts` + `GET /api/users/{id}` — public-safe profile payload. Returns `{ id, name, created_at, review_count, judges_reviewed, total_likes, avg_rating }`. No email, no role memberships, no PII. Aggregates over published reviews only.
  • `app/Actions/Users/UserReviewsAction.ts` + `GET /api/users/{id}/reviews` — paginated list of the user's published reviews, hydrated with judge name/image. Returns canonical paginator shape via the shim from #28. `status=published` is hardcoded; pending/rejected stay private.
  • `ShowReviewAction` extended to return a public-safe `author: { id, name }` block so the article page can show "by Reviewer Name" without a second round-trip.

View — `resources/views/user/[id].stx` rewritten from the hardcoded fake-data shell into a thin `@include('Bench/User/ReviewerProfile')` host.

Component — `resources/components/Bench/User/ReviewerProfile.stx` (new):

  • Identity header (initials avatar + name + member-since + review count) on a dark hero card.
  • 4 aggregate stat tiles: Reviews / Judges Reviewed / Avg Rating / Total Likes.
  • Paginated published-reviews list (max 20 per page; uses the new endpoint).
  • Loading skeletons during the fetch.
  • 404 empty state inline (no full-page 404 swap).
  • Owner-only "Edit your profile" + "Manage your reviews" CTAs when the viewer is the same user.

Cross-links:

  • `ReviewsFeed.stx` — reviewer avatars + names now link to `/user/{id}` instead of the dead `/profile/{id}` path that was 404ing.
  • `ArticleView.stx` — added a "by <reviewer>" cross-link in the article meta row (next to date + read-time). Renders only when the review has a non-null `user_id` AND the server resolved the author name; seeded/factory reviews without an author stay anonymous.

Privacy guardrails kept tight: every server-side query that returns a user filters to a tiny whitelist of fields. Email never appears in any response under `/api/users/{id}` or `/api/users/{id}/reviews`.

Notes:

  • bqb's `.select(['COUNT(DISTINCT col) as ...'])` chokes at the prepare step — worked around by splitting into two narrow queries (count + groupBy on `judge_id` to count distinct). Likely the same identifier-quoting gap that's bitten us in other actions. Worth filing upstream on bqb if it hasn't been already.
  • `likes` lives on the `judge_reviews_likes` pivot, not a denormalized column on `judge_reviews`, so the total-likes stat does an extra JOIN to the pivot. Cheap at our scale.

Sign in to comment on this issue.