Files
jackboxpartypack-gamepicker/docs/superpowers/specs/2026-08-24-sticker-wall-preview-design.md

7.3 KiB
Raw Blame History

Sticker Wall Preview — Design Spec

Display sticker wall images in two places: as a live preview in the Picker's sticker toggle card, and as a historical record in the SessionDetail view.

Backend — Proxy Endpoints

Two new Express routes in backend/routes/stickerwalls.js, both behind JWT auth middleware.

GET /api/stickerwalls/:sessionId

Proxies to the vote-app's GET /stickerwalls/{sessionID}.png.

  • Streams the PNG response through to the client.
  • Passes through status codes: 200 with image/png body, 404 if no render exists.
  • Passes through Cache-Control header from upstream.

POST /api/stickerwalls/:sessionId/render

Proxies to the vote-app's POST /api/stickerwalls/{sessionID}/render.

  • Injects the X-API-Key header server-side (from env var).
  • Streams back the PNG response.
  • Forwards the X-Sticker-Wall-Rendered response header (indicates fresh render vs cached).
  • Passes through status codes: 200 with image, 404 if session has no placed stickers.

Configuration

Two new environment variables:

  • VOTE_APP_URL — base URL of the vote-app (e.g. https://vote-app.example.com)
  • VOTE_APP_API_KEY — API key for the render endpoint

Frontend — Shared Components

StickerWallThumbnail (frontend/src/components/StickerWallThumbnail.jsx)

Clickable image thumbnail for sticker walls. Used in both Picker and SessionDetail.

Props:

  • imageUrl — blob URL or null. When set, displays this image.
  • loading — boolean, shows a spinner overlay while fetching.
  • error — boolean, shows placeholder state ("No sticker wall").
  • onClick — callback for enlarging (opens modal).

Rendering:

  • Fixed small size, 16:9 aspect ratio. Scaled-down from 1920×1080.
  • Rounded corners, subtle border matching surrounding card style.
  • Three visual states: loading (spinner), error/empty (muted placeholder), loaded (image).

StickerWallModal (frontend/src/components/StickerWallModal.jsx)

Full-size image viewer modal. Used in both Picker and SessionDetail.

Props:

  • isOpen — boolean controlling visibility.
  • onClose — callback to close.
  • imageUrl — blob URL of the sticker wall image.

Behavior:

  • Displays the sticker wall image at full/large size, constrained to viewport (max-w-screen, max-h-screen, object-contain).
  • PNG with transparent background rendered over a dark backdrop so stickers are visible.
  • Closes on: Escape key, overlay click, explicit close button (top-right ×).
  • Follows existing Tailwind modal pattern (fixed inset-0, z-50), built as a reusable component like RoomCodeModal.

Shared Fetch Logic — useStickerWall Hook

frontend/src/hooks/useStickerWall.js — custom hook used by both Picker and SessionDetail.

Input: sessionId (triggers fetch when set/changed).

Returns: { imageUrl, loading, error, refresh } where refresh() manually calls the render endpoint.

Fetch sequence (runs on mount / sessionId change):

  1. GET /api/stickerwalls/:sessionId — try the cached image.
  2. If 404 → POST /api/stickerwalls/:sessionId/render — attempt to generate one.
  3. If that also 404s (no stickers exist) → set error state.

Both endpoints return image bytes. The hook fetches as blob, creates a blob URL via URL.createObjectURL, and revokes the previous URL to prevent memory leaks. Cleanup on unmount revokes the current URL.

refresh() method: Calls POST /api/stickerwalls/:sessionId/render directly (skipping the GET), updates imageUrl with the result. Used only by the Picker's refresh button.

This catches three cases:

  • Sessions with a final rendered image (normal post-feature sessions) — served from step 1.
  • Sessions with sticker data but no render yet (played between wall archival and retrieval deployment) — rendered in step 2.
  • Sessions with no stickers at all — error state after step 2 fails.

Frontend — Picker (Live Session Sticker Card)

The existing sticker toggle card in Picker.jsx (~line 892) is expanded to include the sticker wall preview.

Layout (left to right)

  1. Thumbnail — StickerWallThumbnail on the left side. Sized to fit the card height (~80–90px tall, ~142–160px wide at 16:9). Rounded corners, subtle border.
  2. Refresh button — Immediately right of the thumbnail. Small, subtle icon button (refresh/sync icon). Muted styling (text-gray-400 hover:text-indigo-500) so it doesn't compete with the mode toggle. Visually separated from the segmented control.
  3. Flex spacer — Pushes the toggle control to the right edge.
  4. "Stickers" label + auto/show/hide segmented control — Unchanged, stays on the right.

Fetch behavior

  • On session load (when sessionId is available), run the shared fetch logic.
  • The refresh button calls POST /api/stickerwalls/:sessionId/render and updates the thumbnail with the response. Shows a spinner on the button while rendering.

Click to enlarge

Clicking the thumbnail opens StickerWallModal with the current image.

State

Managed by the useStickerWall(sessionId) hook. Picker destructures { imageUrl, loading, error, refresh } — no additional local state needed beyond the modal open/close boolean.

Frontend — SessionDetail (History View)

A new "Sticker Wall" section in SessionDetail.jsx, positioned after the notes section and before the games list.

Card layout

  • Section heading "Sticker Wall" matching existing heading style (Notes, Games, etc.).
  • Contains StickerWallThumbnail, slightly larger than in Picker (~120px tall).
  • No refresh button — historical sessions have immutable final images; the one-time render-on-404 from the shared fetch logic handles the edge case of pre-existing sessions.

Fetch behavior

On load, run the shared fetch logic (GET, then POST on 404, then placeholder on second 404).

Placeholder state

If no sticker wall exists (both GET and POST return 404), the card displays "No sticker wall for this session" in muted text.

Click to enlarge

Clicking the thumbnail opens StickerWallModal.

Active sessions in SessionDetail

If the session is still active (is_active === 1), the sticker wall section still appears with read-only fetch-on-load behavior. No refresh button — the Picker is the intended live interface.

Error Handling

  • Network errors on proxy requests: Show a brief toast or inline error state in the thumbnail. Don't block the rest of the page.
  • Render endpoint returns cached (X-Sticker-Wall-Rendered: false): No special UI treatment — just display the image. The admin can infer from visual inspection whether anything changed.
  • Blob URL cleanup: Revoke object URLs on unmount and when replaced by a new fetch, to avoid memory leaks.

Files Changed

File Change
backend/routes/stickerwalls.js New file — proxy endpoints
backend/server.js (or wherever routes mount) Register stickerwalls routes
frontend/src/hooks/useStickerWall.js New file — shared fetch/render hook
frontend/src/components/StickerWallThumbnail.jsx New file — reusable thumbnail
frontend/src/components/StickerWallModal.jsx New file — reusable full-size modal
frontend/src/pages/Picker.jsx Expand sticker card with thumbnail, refresh button, modal
frontend/src/pages/SessionDetail.jsx Add sticker wall section after notes