From 0e94b1dba274d29a8db8908fe8382d7215dd766f Mon Sep 17 00:00:00 2001 From: Simon Gruber Date: Thu, 5 Nov 2026 23:28:21 +0000 Subject: [PATCH] Add design for participation stats and podium stamps Co-Authored-By: Claude Sonnet 5.5 --- .../2026-11-05-participation-fun-design.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 docs/superpowers/specs/2026-11-05-participation-fun-design.md diff --git a/docs/superpowers/specs/2026-11-05-participation-fun-design.md b/docs/superpowers/specs/2026-11-05-participation-fun-design.md new file mode 100644 index 0000000..626e93d --- /dev/null +++ b/docs/superpowers/specs/2026-11-05-participation-fun-design.md @@ -0,0 +1,47 @@ +# Participation fun: live stats and podium stamps (short design) + +## Intent +Make the participant order screen (`ParticipantView.tsx`) feel rewarding: an anonymous, live "number go up" panel and a personal stamp for the first three people to submit. + +## Goals +- Live horizontal bar chart per category: how often each item was picked, plus a participant count. +- A personal stamp (🥇🥈🥉) shown only to the participant themself, in their "Your order" card, if they are 1st, 2nd or 3rd to submit. +- Live updates only while the order is open; stop when it closes, resume if reopened. + +## Non-goals / anti-goals +- No usernames, no emails, no way to tell who picked what or to reconstruct a full order. +- No medals for most-picked items or any other item-based reward. +- No websockets/SSE, no new infrastructure. +- The order creator never receives a medal (their submission is skipped in the ranking). + +## Constraints +- Frontend already polls every 5s (`ParticipantView.tsx`); follow that pattern. +- Admin status changes (accepted/paid/handed_out) bump `submissions.updated_at`, so it cannot be the ranking key. + +## Approaches considered +- **Rank key:** new `ranked_at` column (chosen) vs reusing `updated_at` (rejected: admin clicks would reshuffle medals). +- **Delivery:** new `GET /api/orders/{id}/stats` polled only while open (chosen); extending `GET /api/orders/{id}` (rejected: no user identity, and its poll must keep running after close); SSE/websockets (rejected: new infra for a counter). + +## Design +**DB** (`db.py`): add `submissions.ranked_at TEXT` via the existing `PRAGMA table_info` + `ALTER TABLE` pattern; backfill from `created_at`. + +**Backend** +- `create_submission` and `update_submission` (the participant's own PUT) set `ranked_at = now`. `admin_update_submission_status` does not touch it. Deleting a submission removes it from the ranking naturally. +- `GET /api/orders/{order_id}/stats`, public like `get_order`, optional `X-User-Id` header. Returns: + `{ participant_count, categories: [{id, label, items: [{name, count}]}], my_rank: 1|2|3|null }` + - Counts come from every submission's `choices_json`, including the creator's. + - `my_rank` is the caller's position among non-creator submissions ordered by `ranked_at`, only if 1–3. The creator is found via `get_order_creator_info` and their submission via `user_order_tokens`. Null for the creator, for non-submitters, and for rank 4 and above. +- The stats logic goes in a new function in `stats_service.py`, with a small test in `backend/tests/` (ranking: creator skipped, edit moves you back, admin status change does not, delete promotes others). + +**Frontend** +- `statsService` (or `ordersService`) gets a `getOrderStats(orderId)` call. +- `ParticipantView`: a `useEffect` polls it every 5s while `!isClosed` and clears the interval when closed. The effect depends on `isClosed`, so a reopen restarts it. The last result stays on screen, frozen, after close. +- A small stats card (antd horizontal bars per category, participant count) and a stamp inside the "Your order" card driven by `my_rank`. + +## Left to the builder +Bar styling, stamp wording and animation, number-change animation, how to lay out many categories or items (e.g. cap long lists). + +## My calls (not discussed) +- The count and bars include the creator; only the medal skips them. +- After close the last stats stay visible but frozen. +- Backfill `ranked_at` from `created_at`.