diff --git a/docs/external-downstream-stickerwalls.md b/docs/external-downstream-stickerwalls.md new file mode 100644 index 0000000..7b822f3 --- /dev/null +++ b/docs/external-downstream-stickerwalls.md @@ -0,0 +1,100 @@ +# Sticker Wall Images — Integration Guide + +The vote-app renders per-session sticker walls as 1920×1080 PNG images with transparent backgrounds. These can be loaded directly as `` sources to display a session's sticker wall. + +## Endpoints + +### Retrieve Sticker Wall Image + +``` +GET /stickerwalls/{sessionID}.png +``` + +**Authentication:** None (public endpoint) + +**Response:** +- `200 OK` — image/png body, `Cache-Control: public, max-age=3600` +- `404 Not Found` — no render has been performed for this session yet + +**Usage:** Load directly as an image source: +```html + +``` + +### Trigger Render + +``` +POST /api/stickerwalls/{sessionID}/render +``` + +**Authentication:** Required — `X-API-Key` header with the upstream API key. + +**Response:** +- `200 OK` — image/png body with the rendered sticker wall +- `403 Forbidden` — missing or invalid API key +- `404 Not Found` — session has no placed stickers + +**Response Headers:** +- `X-Sticker-Wall-Rendered: true` — a fresh render was performed +- `X-Sticker-Wall-Rendered: false` — served from cache (no new stickers since last render) + +## Behavior + +### Active Sessions + +During an active session, stickers are placed by voters and stored in the database. The sticker wall image is **not** automatically re-rendered on each sticker purchase (to avoid thrashing). + +To get a fresh image during an active session: +1. Call `POST /api/stickerwalls/{sessionID}/render` +2. If new stickers were placed since the last render, a fresh image is generated +3. The response contains the image bytes directly + +### Session End + +When a session ends, the vote-app automatically renders the final sticker wall image. After this point: +- The image is **immutable** — it will never be re-rendered +- `GET /stickerwalls/{sessionID}.png` returns the final image directly +- `POST /api/stickerwalls/{sessionID}/render` returns the cached final image (with `X-Sticker-Wall-Rendered: false`) + +### Historical Sessions + +Sessions that existed before this feature was deployed will be rendered on first request (via the render endpoint). Once rendered, they are cached and served normally. + +## Suggested Integration + +### Post-Session Retrieval (most common) + +After a session ends, load the sticker wall for display or archival: + +```javascript +const sessionId = 42; +const img = new Image(); +img.src = `https://vote-app.example.com/stickerwalls/${sessionId}.png`; +img.onload = () => { /* display it */ }; +img.onerror = () => { /* no sticker wall for this session */ }; +``` + +### Active Session Preview + +To show a "live preview" of the current sticker wall during a session: + +```javascript +async function refreshStickerWall(sessionId) { + const res = await fetch(`/api/stickerwalls/${sessionId}/render`, { + method: 'POST', + headers: { 'X-API-Key': API_KEY } + }); + if (res.ok) { + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + document.getElementById('sticker-wall-preview').src = url; + } +} +``` + +## Image Format + +- **Dimensions:** 1920×1080 pixels +- **Format:** PNG with alpha transparency +- **Background:** Fully transparent — overlay on any background +- **Sticker rendering:** Positioned, scaled, and rotated to match the live display diff --git a/docs/superpowers/plans/2026-08-24-sticker-wall-preview.md b/docs/superpowers/plans/2026-08-24-sticker-wall-preview.md new file mode 100644 index 0000000..d9b2f39 --- /dev/null +++ b/docs/superpowers/plans/2026-08-24-sticker-wall-preview.md @@ -0,0 +1,589 @@ +# Sticker Wall Preview Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Display sticker wall images as previews in the live Picker view and as historical records in the SessionDetail view, fetched through backend proxy endpoints. + +**Architecture:** Two Express proxy routes forward sticker wall requests to the downstream vote-app (keeping the API key server-side). A `useStickerWall` React hook encapsulates the shared two-step fetch logic (GET cached → POST render on 404). Two new reusable components (`StickerWallThumbnail`, `StickerWallModal`) are consumed by both `Picker.jsx` and `SessionDetail.jsx`. + +**Tech Stack:** Node.js/Express (backend proxy), React 18 with hooks (frontend), Axios (HTTP), Tailwind CSS (styling) + +## Global Constraints + +- No TypeScript — all files are `.js` / `.jsx` +- Tailwind CSS for all styling — no CSS modules or styled-components +- Dark mode via `dark:` variants on all visual elements +- Shared axios instance at `frontend/src/api/axios.js` for all frontend HTTP calls (it sets `baseURL: '/api'` and injects JWT via interceptor) +- Backend auth middleware: `const { authenticateToken } = require('../middleware/auth')` — use on all new routes +- Modal pattern: `fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50`, close on Escape + overlay click +- No inline imports — all imports at top of file +- Exhaustive switch defaults with `never` check where applicable +- Blob URLs must be revoked on unmount and replacement to prevent memory leaks + +--- + +### Task 1: Backend Proxy Endpoints + +**Files:** +- Create: `backend/routes/stickerwalls.js` +- Modify: `backend/server.js` (lines 22–37, route registration block) + +**Interfaces:** +- Consumes: `authenticateToken` from `../middleware/auth` +- Produces: + - `GET /api/stickerwalls/:sessionId` — proxies to vote-app `GET /stickerwalls/{sessionID}.png`, streams PNG response. Returns 200 (image/png) or 404. + - `POST /api/stickerwalls/:sessionId/render` — proxies to vote-app `POST /api/stickerwalls/{sessionID}/render` with `X-API-Key` header. Streams PNG response. Returns 200 (image/png) or 404. Forwards `X-Sticker-Wall-Rendered` response header. + - Reads env vars: `VOTE_APP_URL` (base URL), `VOTE_APP_API_KEY` (render auth key) + +- [ ] **Step 1: Create `backend/routes/stickerwalls.js` with GET endpoint** + +```javascript +const express = require('express'); +const { authenticateToken } = require('../middleware/auth'); + +const router = express.Router(); + +const VOTE_APP_URL = process.env.VOTE_APP_URL; +const VOTE_APP_API_KEY = process.env.VOTE_APP_API_KEY; + +router.get('/:sessionId', authenticateToken, async (req, res) => { + const { sessionId } = req.params; + if (!VOTE_APP_URL) { + return res.status(503).json({ error: 'Vote app not configured' }); + } + + try { + const upstream = await fetch(`${VOTE_APP_URL}/stickerwalls/${sessionId}.png`); + if (!upstream.ok) { + return res.status(upstream.status).end(); + } + res.set('Content-Type', 'image/png'); + const cacheControl = upstream.headers.get('cache-control'); + if (cacheControl) { + res.set('Cache-Control', cacheControl); + } + const buffer = Buffer.from(await upstream.arrayBuffer()); + res.send(buffer); + } catch (err) { + console.error('Sticker wall GET proxy error:', err.message); + res.status(502).json({ error: 'Failed to fetch sticker wall' }); + } +}); + +module.exports = router; +``` + +- [ ] **Step 2: Add POST render endpoint to the same file** + +Add below the GET route, before `module.exports`: + +```javascript +router.post('/:sessionId/render', authenticateToken, async (req, res) => { + const { sessionId } = req.params; + if (!VOTE_APP_URL || !VOTE_APP_API_KEY) { + return res.status(503).json({ error: 'Vote app not configured' }); + } + + try { + const upstream = await fetch(`${VOTE_APP_URL}/api/stickerwalls/${sessionId}/render`, { + method: 'POST', + headers: { 'X-API-Key': VOTE_APP_API_KEY }, + }); + if (!upstream.ok) { + return res.status(upstream.status).end(); + } + res.set('Content-Type', 'image/png'); + const rendered = upstream.headers.get('x-sticker-wall-rendered'); + if (rendered) { + res.set('X-Sticker-Wall-Rendered', rendered); + } + const buffer = Buffer.from(await upstream.arrayBuffer()); + res.send(buffer); + } catch (err) { + console.error('Sticker wall render proxy error:', err.message); + res.status(502).json({ error: 'Failed to render sticker wall' }); + } +}); +``` + +- [ ] **Step 3: Register the route in `backend/server.js`** + +Add to the imports block (after line 29, the `webhooksRoutes` line): + +```javascript +const stickerwallsRoutes = require('./routes/stickerwalls'); +``` + +Add to the route registration block (after line 37, the webhooks mount): + +```javascript +app.use('/api/stickerwalls', stickerwallsRoutes); +``` + +- [ ] **Step 4: Verify the server starts without errors** + +Run: `cd backend && node -e "require('./routes/stickerwalls')"` +Expected: No errors (clean require) + +- [ ] **Step 5: Commit** + +```bash +git add backend/routes/stickerwalls.js backend/server.js +git commit -m "feat: add backend proxy endpoints for sticker wall retrieval and rendering" +``` + +--- + +### Task 2: `useStickerWall` Hook + +**Files:** +- Create: `frontend/src/hooks/useStickerWall.js` + +**Interfaces:** +- Consumes: `api` from `../api/axios` (axios instance with `baseURL: '/api'` and JWT interceptor) +- Produces: `useStickerWall(sessionId)` hook returning `{ imageUrl, loading, error, refresh }` + - `imageUrl` — blob URL string or `null` + - `loading` — boolean + - `error` — boolean (true when no sticker wall exists) + - `refresh()` — async function, calls POST render endpoint, updates `imageUrl` + +- [ ] **Step 1: Create `frontend/src/hooks/useStickerWall.js`** + +```jsx +import { useState, useEffect, useRef, useCallback } from 'react'; +import api from '../api/axios'; + +export function useStickerWall(sessionId) { + const [imageUrl, setImageUrl] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(false); + const urlRef = useRef(null); + + const revokeUrl = useCallback(() => { + if (urlRef.current) { + URL.revokeObjectURL(urlRef.current); + urlRef.current = null; + } + }, []); + + const setBlobUrl = useCallback((blob) => { + revokeUrl(); + const url = URL.createObjectURL(blob); + urlRef.current = url; + setImageUrl(url); + }, [revokeUrl]); + + const fetchWall = useCallback(async (id) => { + setLoading(true); + setError(false); + try { + const res = await api.get(`/stickerwalls/${id}`, { responseType: 'blob' }); + setBlobUrl(res.data); + } catch (err) { + if (err.response?.status === 404) { + try { + const renderRes = await api.post(`/stickerwalls/${id}/render`, null, { responseType: 'blob' }); + setBlobUrl(renderRes.data); + } catch (renderErr) { + revokeUrl(); + setImageUrl(null); + setError(true); + } + } else { + revokeUrl(); + setImageUrl(null); + setError(true); + } + } finally { + setLoading(false); + } + }, [setBlobUrl, revokeUrl]); + + const refresh = useCallback(async () => { + if (!sessionId) return; + setLoading(true); + setError(false); + try { + const res = await api.post(`/stickerwalls/${sessionId}/render`, null, { responseType: 'blob' }); + setBlobUrl(res.data); + } catch { + setError(true); + } finally { + setLoading(false); + } + }, [sessionId, setBlobUrl]); + + useEffect(() => { + if (sessionId) { + fetchWall(sessionId); + } else { + revokeUrl(); + setImageUrl(null); + setError(false); + } + return revokeUrl; + }, [sessionId, fetchWall, revokeUrl]); + + return { imageUrl, loading, error, refresh }; +} +``` + +- [ ] **Step 2: Verify the file can be imported without syntax errors** + +Run: `cd frontend && node -e "try { require('./src/hooks/useStickerWall.js') } catch(e) { if (e.code === 'ERR_REQUIRE_ESM' || e.message.includes('Cannot use import')) { console.log('ESM syntax OK'); process.exit(0); } throw e; }"` +Expected: "ESM syntax OK" (the file uses ESM imports so it won't work with `require`, but it proves no syntax errors prevent parsing) + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/hooks/useStickerWall.js +git commit -m "feat: add useStickerWall hook for shared fetch/render logic" +``` + +--- + +### Task 3: `StickerWallModal` Component + +**Files:** +- Create: `frontend/src/components/StickerWallModal.jsx` + +**Interfaces:** +- Consumes: nothing external (self-contained presentational component) +- Produces: `StickerWallModal({ isOpen, onClose, imageUrl })` — default export + - `isOpen` — boolean, controls rendering + - `onClose` — callback + - `imageUrl` — string (blob URL) + +- [ ] **Step 1: Create `frontend/src/components/StickerWallModal.jsx`** + +```jsx +import React, { useEffect } from 'react'; + +function StickerWallModal({ isOpen, onClose, imageUrl }) { + useEffect(() => { + if (!isOpen) return; + const handleEscape = (e) => { + if (e.key === 'Escape') onClose(); + }; + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, [isOpen, onClose]); + + if (!isOpen || !imageUrl) return null; + + const handleOverlayClick = (e) => { + if (e.target === e.currentTarget) onClose(); + }; + + return ( +
+
+ + Sticker Wall +
+
+ ); +} + +export default StickerWallModal; +``` + +- [ ] **Step 2: Commit** + +```bash +git add frontend/src/components/StickerWallModal.jsx +git commit -m "feat: add StickerWallModal component for full-size sticker wall viewing" +``` + +--- + +### Task 4: `StickerWallThumbnail` Component + +**Files:** +- Create: `frontend/src/components/StickerWallThumbnail.jsx` + +**Interfaces:** +- Consumes: nothing external (presentational component) +- Produces: `StickerWallThumbnail({ imageUrl, loading, error, onClick })` — default export + - `imageUrl` — string or null + - `loading` — boolean + - `error` — boolean + - `onClick` — callback (for enlarging) + +- [ ] **Step 1: Create `frontend/src/components/StickerWallThumbnail.jsx`** + +```jsx +import React from 'react'; + +function StickerWallThumbnail({ imageUrl, loading, error, onClick }) { + const baseClasses = 'rounded-lg border border-gray-300 dark:border-gray-600 overflow-hidden flex items-center justify-center bg-gray-100 dark:bg-gray-700/50 flex-shrink-0'; + + if (loading) { + return ( +
+ + + + +
+ ); + } + + if (error || !imageUrl) { + return ( +
+ No stickers +
+ ); + } + + return ( + + ); +} + +export default StickerWallThumbnail; +``` + +- [ ] **Step 2: Commit** + +```bash +git add frontend/src/components/StickerWallThumbnail.jsx +git commit -m "feat: add StickerWallThumbnail component for clickable sticker wall previews" +``` + +--- + +### Task 5: Integrate Sticker Wall Preview into Picker + +**Files:** +- Modify: `frontend/src/pages/Picker.jsx` + - Import block (top of file): add imports for `useStickerWall`, `StickerWallThumbnail`, `StickerWallModal` + - State declarations (~line 35–38): add `showStickerWallModal` state + - Sticker toggle card (~lines 892–931): restructure layout to add thumbnail + refresh button on the left + +**Interfaces:** +- Consumes: + - `useStickerWall(sessionId)` from `../hooks/useStickerWall` — returns `{ imageUrl, loading, error, refresh }` + - `StickerWallThumbnail` from `../components/StickerWallThumbnail` + - `StickerWallModal` from `../components/StickerWallModal` + - `sessionId` — already available in Picker's state (the active session's ID) +- Produces: Updated Picker UI with sticker wall preview in the sticker toggle card + +- [ ] **Step 1: Add imports to `Picker.jsx`** + +Add these three imports to the import block at the top of `frontend/src/pages/Picker.jsx` (after the existing component imports): + +```jsx +import { useStickerWall } from '../hooks/useStickerWall'; +import StickerWallThumbnail from '../components/StickerWallThumbnail'; +import StickerWallModal from '../components/StickerWallModal'; +``` + +- [ ] **Step 2: Add hook call and modal state** + +In the Picker function body, near the other sticker-related state (after line 38, the `prevStickerModeRef` line), add: + +```jsx +const [showStickerWallModal, setShowStickerWallModal] = useState(false); +``` + +The active session is stored as `activeSession` (object, line 14). Add the hook call near the other sticker state: + +```jsx +const { imageUrl: stickerWallUrl, loading: stickerWallLoading, error: stickerWallError, refresh: refreshStickerWall } = useStickerWall(activeSession?.id); +``` + +- [ ] **Step 3: Restructure the sticker toggle card** + +Replace the sticker toggle card block (the `{/* Sticker Visibility Toggle */}` section, lines ~892–931) with this updated layout: + +```jsx + {/* Sticker Visibility Toggle */} +
+ stickerWallUrl && setShowStickerWallModal(true)} + /> + +
+ Stickers +
+ {[ + { value: 'auto', label: 'Auto', activeClass: 'bg-gray-500 text-white' }, + { value: 'show', label: 'Show', activeClass: 'bg-green-500 text-white' }, + { value: 'hide', label: 'Hide', activeClass: 'bg-red-500 text-white' }, + ].map((opt, i) => ( + + ))} +
+
+``` + +- [ ] **Step 4: Add the StickerWallModal render** + +Near the other modals in Picker (e.g. near the `RoomCodeModal` around line 877), add: + +```jsx + setShowStickerWallModal(false)} + imageUrl={stickerWallUrl} + /> +``` + +- [ ] **Step 5: Verify the app builds without errors** + +Run: `cd frontend && npx vite build 2>&1 | tail -5` +Expected: Build succeeds + +- [ ] **Step 6: Commit** + +```bash +git add frontend/src/pages/Picker.jsx +git commit -m "feat: integrate sticker wall preview into Picker's sticker toggle card" +``` + +--- + +### Task 6: Integrate Sticker Wall into SessionDetail + +**Files:** +- Modify: `frontend/src/pages/SessionDetail.jsx` + - Import block (top of file): add imports for `useStickerWall`, `StickerWallThumbnail`, `StickerWallModal` + - State declarations (~line 25): add `showStickerWallModal` state + - JSX (~line 286, after the NotesSection card): add sticker wall section + +**Interfaces:** +- Consumes: + - `useStickerWall(sessionId)` from `../hooks/useStickerWall` — returns `{ imageUrl, loading, error }` + - `StickerWallThumbnail` from `../components/StickerWallThumbnail` + - `StickerWallModal` from `../components/StickerWallModal` + - `id` — from `useParams()`, already available in SessionDetail +- Produces: Updated SessionDetail UI with sticker wall section between notes and games list + +- [ ] **Step 1: Add imports to `SessionDetail.jsx`** + +Add these three imports to the import block at the top of `frontend/src/pages/SessionDetail.jsx` (after the existing component imports): + +```jsx +import { useStickerWall } from '../hooks/useStickerWall'; +import StickerWallThumbnail from '../components/StickerWallThumbnail'; +import StickerWallModal from '../components/StickerWallModal'; +``` + +- [ ] **Step 2: Add hook call and modal state** + +Inside the `SessionDetail` function, after the existing state declarations (after line 25, the `closingSession` state), add: + +```jsx +const [showStickerWallModal, setShowStickerWallModal] = useState(false); +const { imageUrl: stickerWallUrl, loading: stickerWallLoading, error: stickerWallError } = useStickerWall(id); +``` + +Note: `id` comes from `useParams()` on line 11 and is the session ID. + +- [ ] **Step 3: Add sticker wall section in JSX** + +After the NotesSection card closing `
` (line 286) and before the `{showChatImport && (` block (line 288), insert: + +```jsx +
+

Sticker Wall

+ {stickerWallLoading ? ( +
+ + + + + Loading sticker wall... +
+ ) : stickerWallError || !stickerWallUrl ? ( +

No sticker wall for this session

+ ) : ( + setShowStickerWallModal(true)} + /> + )} +
+ + setShowStickerWallModal(false)} + imageUrl={stickerWallUrl} + /> +``` + +- [ ] **Step 4: Verify the app builds without errors** + +Run: `cd frontend && npx vite build 2>&1 | tail -5` +Expected: Build succeeds + +- [ ] **Step 5: Commit** + +```bash +git add frontend/src/pages/SessionDetail.jsx +git commit -m "feat: add sticker wall section to SessionDetail view" +``` diff --git a/docs/superpowers/specs/2026-08-24-sticker-wall-preview-design.md b/docs/superpowers/specs/2026-08-24-sticker-wall-preview-design.md new file mode 100644 index 0000000..62a620f --- /dev/null +++ b/docs/superpowers/specs/2026-08-24-sticker-wall-preview-design.md @@ -0,0 +1,158 @@ +# 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 | diff --git a/frontend/src/config/branding.js b/frontend/src/config/branding.js index e237eb3..585651b 100644 --- a/frontend/src/config/branding.js +++ b/frontend/src/config/branding.js @@ -2,7 +2,7 @@ export const branding = { app: { name: 'HSO Jackbox Game Picker', shortName: 'Jackbox Game Picker', - version: '0.8.1.1 - Stickers That Stick Edition', + version: '8.1.2 - Stickers That Stick Edition', description: 'Spicing up Hyper Spaceout game nights!', }, meta: {