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

159 lines
7.3 KiB
Markdown
Raw Normal View 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 |