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