chore: bump version to 8.1.2, add sticker wall docs and design spec
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
589
docs/superpowers/plans/2026-08-24-sticker-wall-preview.md
Normal file
589
docs/superpowers/plans/2026-08-24-sticker-wall-preview.md
Normal file
@@ -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 (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50 p-4"
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
<div className="relative max-w-[90vw] max-h-[90vh]">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute -top-3 -right-3 w-8 h-8 bg-gray-800 hover:bg-gray-700 text-white rounded-full flex items-center justify-center text-sm font-bold shadow-lg z-10 transition"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Sticker Wall"
|
||||
className="max-w-full max-h-[85vh] object-contain rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={`${baseClasses} w-[142px] h-[80px]`}>
|
||||
<svg className="animate-spin h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !imageUrl) {
|
||||
return (
|
||||
<div className={`${baseClasses} w-[142px] h-[80px]`}>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 text-center px-2">No stickers</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`${baseClasses} w-[142px] h-[80px] cursor-pointer hover:border-indigo-400 dark:hover:border-indigo-500 transition group`}
|
||||
>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Sticker Wall Preview"
|
||||
className="w-full h-full object-cover group-hover:opacity-90 transition"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
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 */}
|
||||
<div
|
||||
className={`flex items-center border rounded-lg p-3 mb-4 transition-all duration-300 ${
|
||||
stickerPulse
|
||||
? 'border-indigo-400 dark:border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20 animate-pulse'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<StickerWallThumbnail
|
||||
imageUrl={stickerWallUrl}
|
||||
loading={stickerWallLoading}
|
||||
error={stickerWallError}
|
||||
onClick={() => stickerWallUrl && setShowStickerWallModal(true)}
|
||||
/>
|
||||
<button
|
||||
onClick={refreshStickerWall}
|
||||
disabled={stickerWallLoading}
|
||||
className="ml-2 p-1.5 text-gray-400 hover:text-indigo-500 dark:text-gray-500 dark:hover:text-indigo-400 transition rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0"
|
||||
title="Refresh sticker wall"
|
||||
>
|
||||
<svg className={`w-4 h-4 ${stickerWallLoading ? 'animate-spin' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="flex-1" />
|
||||
<span className="text-sm font-semibold text-gray-700 dark:text-gray-300 mr-3">Stickers</span>
|
||||
<div className="flex border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
|
||||
{[
|
||||
{ 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) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => {
|
||||
setStickerMode(opt.value);
|
||||
if (opt.value === 'auto') {
|
||||
stickerSendRef.current?.(pollActive ? 'show' : 'hide');
|
||||
}
|
||||
stickerSendRef.current?.(opt.value);
|
||||
}}
|
||||
disabled={!stickerWsReady}
|
||||
className={`px-3 py-1.5 text-xs font-medium transition ${
|
||||
i === 1 ? 'border-x border-gray-300 dark:border-gray-600 ' : ''
|
||||
}${
|
||||
stickerMode === opt.value
|
||||
? opt.activeClass
|
||||
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}${
|
||||
!stickerWsReady ? ' opacity-50 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add the StickerWallModal render**
|
||||
|
||||
Near the other modals in Picker (e.g. near the `RoomCodeModal` around line 877), add:
|
||||
|
||||
```jsx
|
||||
<StickerWallModal
|
||||
isOpen={showStickerWallModal}
|
||||
onClose={() => 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 `</div>` (line 286) and before the `{showChatImport && (` block (line 288), insert:
|
||||
|
||||
```jsx
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-3">Sticker Wall</h2>
|
||||
{stickerWallLoading ? (
|
||||
<div className="flex items-center gap-2 text-gray-500 dark:text-gray-400">
|
||||
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
<span className="text-sm">Loading sticker wall...</span>
|
||||
</div>
|
||||
) : stickerWallError || !stickerWallUrl ? (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 italic">No sticker wall for this session</p>
|
||||
) : (
|
||||
<StickerWallThumbnail
|
||||
imageUrl={stickerWallUrl}
|
||||
loading={false}
|
||||
error={false}
|
||||
onClick={() => setShowStickerWallModal(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<StickerWallModal
|
||||
isOpen={showStickerWallModal}
|
||||
onClose={() => 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"
|
||||
```
|
||||
Reference in New Issue
Block a user