# 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`.