159 lines
7.3 KiB
Markdown
159 lines
7.3 KiB
Markdown
# 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 |
|