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