Files
lunchtime-web/docs/superpowers/specs/2026-11-05-participation-fun-design.md
T
2026-11-05 23:28:21 +00:00

48 lines
3.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.