3.3 KiB
3.3 KiB
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_atcolumn (chosen) vs reusingupdated_at(rejected: admin clicks would reshuffle medals). - Delivery: new
GET /api/orders/{id}/statspolled only while open (chosen); extendingGET /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_submissionandupdate_submission(the participant's own PUT) setranked_at = now.admin_update_submission_statusdoes not touch it. Deleting a submission removes it from the ranking naturally.GET /api/orders/{order_id}/stats, public likeget_order, optionalX-User-Idheader. 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_rankis the caller's position among non-creator submissions ordered byranked_at, only if 1–3. The creator is found viaget_order_creator_infoand their submission viauser_order_tokens. Null for the creator, for non-submitters, and for rank 4 and above.
- Counts come from every submission's
- The stats logic goes in a new function in
stats_service.py, with a small test inbackend/tests/(ranking: creator skipped, edit moves you back, admin status change does not, delete promotes others).
Frontend
statsService(orordersService) gets agetOrderStats(orderId)call.ParticipantView: auseEffectpolls it every 5s while!isClosedand clears the interval when closed. The effect depends onisClosed, 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_atfromcreated_at.