# Post Composition Redesign — Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Transform Posts from flat entities into composition orchestrators that assemble Caption + Copy (Translations) + Designs (Artefacts) + Video into a publishable unit with auto-computed readiness. **Architecture:** Add `caption` and `stage` fields to Posts table. New `/api/posts/:id/composition` endpoint aggregates linked Translations + Artefacts with approval statuses. PostDetailPanel is rewritten as a composition workspace (single scroll, no tabs). Platform→format mapping is a client-side constant. **Tech Stack:** Express.js, NocoDB, React, Tailwind CSS **Spec:** `docs/superpowers/specs/2026-03-15-post-composition-redesign.md` --- ## File Structure **Server:** - Modify: `server/server.js` — add `caption`/`stage` to Posts TEXT_COLUMNS, new composition endpoint, update POST/PATCH handlers - Create: `server/post-composition.js` — helper to compute composition, readiness, and stage auto-advance **Client — New:** - Create: `client/src/components/PostCompositionPanel.jsx` — the new composition workspace (replaces PostDetailPanel usage) - Create: `client/src/components/PostCompositionCaption.jsx` — caption section - Create: `client/src/components/PostCompositionCopy.jsx` — linked translations section - Create: `client/src/components/PostCompositionDesigns.jsx` — linked design artefacts section - Create: `client/src/components/PostCompositionVideo.jsx` — linked video artefact section - Create: `client/src/components/PostCompositionFormats.jsx` — platform format checklist - Create: `client/src/components/PostCompositionReadiness.jsx` — readiness summary + sign-off - Create: `client/src/utils/platformFormats.js` — PLATFORM_FORMATS constant **Client — Modify:** - Modify: `client/src/pages/PostProduction.jsx` — use PostCompositionPanel instead of PostDetailPanel - Modify: `client/src/pages/CampaignDetail.jsx` — same - Modify: `client/src/i18n/en.json` / `ar.json` — new i18n keys **Client — Keep (unchanged):** - `PostDetailVersions.jsx`, `PostDetailPlatforms.jsx`, `PostDetailApproval.jsx`, `PostDetailAttachments.jsx` — kept for backward compat, old PostDetailPanel still importable --- ## Chunk 1: Server — Schema + Composition Endpoint ### Task 1: Add caption and stage to Posts schema **Files:** - Modify: `server/server.js` — TEXT_COLUMNS for Posts (~line 520) - [ ] **Step 1: Add new columns to TEXT_COLUMNS** Add to the Posts array in TEXT_COLUMNS: ```javascript { name: 'caption', uidt: 'LongText' }, { name: 'stage', uidt: 'SingleLineText' }, ``` - [ ] **Step 2: Update POST /api/posts to accept caption and stage** In the POST handler, add `caption` and `stage` to the create payload: ```javascript caption: caption || '', stage: stage || 'copy', ``` - [ ] **Step 3: Update PATCH /api/posts/:id to accept caption** Add `caption` to the allowed update fields. - [ ] **Step 4: Commit** ```bash git add server/server.js git commit -m "feat: add caption and stage fields to Posts schema" ``` ### Task 2: Create post-composition helper **Files:** - Create: `server/post-composition.js` - [ ] **Step 1: Create the helper module** ```javascript // server/post-composition.js const nocodb = require('./nocodb'); // Compute full composition for a post async function getPostComposition(postId) { const post = await nocodb.get('Posts', postId); if (!post) return null; // Linked translations (copy) const allTranslations = await nocodb.list('Translations', { where: `(post_id,eq,${postId})`, limit: 100, }); const copy = allTranslations.map(t => ({ id: t.Id, language: t.language, status: t.status || 'draft', is_original: t.is_original, title: t.title, })); // Linked artefacts (designs + video) const allArtefacts = await nocodb.list('Artefacts', { where: `(post_id,eq,${postId})`, limit: 100, }); const designs = allArtefacts .filter(a => (a.type || 'design') === 'design') .map(a => ({ id: a.Id, title: a.title, status: a.status || 'draft', thumbnail_url: a.thumbnail_url || null, })); const videoArtefact = allArtefacts.find(a => a.type === 'video'); const video = videoArtefact ? { id: videoArtefact.Id, title: videoArtefact.title, status: videoArtefact.status || 'draft', thumbnail_url: videoArtefact.thumbnail_url || null, } : null; // Platforms and formats let platforms = []; try { platforms = JSON.parse(post.platforms || '[]'); } catch { platforms = post.platform ? [post.platform] : []; } // Readiness const waitingOn = []; const copyNotApproved = copy.filter(c => c.status !== 'approved'); if (copyNotApproved.length > 0) waitingOn.push(...copyNotApproved.map(c => `Copy (${c.language})`)); const designsNotApproved = designs.filter(d => d.status !== 'approved'); if (designsNotApproved.length > 0) waitingOn.push(...designsNotApproved.map(d => `Design: ${d.title}`)); if (video && video.status !== 'approved') waitingOn.push('Video'); const piecesReady = copy.length > 0 && waitingOn.length === 0; return { caption: post.caption || '', copy, designs, video, platforms, pieces_ready: piecesReady, waiting_on: waitingOn, }; } // Auto-compute stage from linked pieces function computeStage(composition) { const { copy, designs, video, pieces_ready } = composition; if (pieces_ready) return 'post'; if (designs.length > 0 || video) return 'design'; if (copy.length > 1 || copy.some(c => !c.is_original)) return 'translate'; return 'copy'; } module.exports = { getPostComposition, computeStage }; ``` - [ ] **Step 2: Commit** ```bash git add server/post-composition.js git commit -m "feat: add post composition helper (readiness, stage auto-compute)" ``` ### Task 3: Add composition API endpoint **Files:** - Modify: `server/server.js` — add GET /api/posts/:id/composition - [ ] **Step 1: Add the endpoint** After the existing GET /api/posts/:id route, add: ```javascript app.get('/api/posts/:id/composition', requireAuth, async (req, res) => { try { const { getPostComposition } = require('./post-composition'); const composition = await getPostComposition(req.params.id); if (!composition) return res.status(404).json({ error: 'Post not found' }); res.json(composition); } catch (err) { console.error('Composition error:', err); res.status(500).json({ error: 'Failed to load composition' }); } }); ``` - [ ] **Step 2: Auto-update stage on PATCH /api/posts/:id** In the existing PATCH handler, after saving, re-compute and update stage: ```javascript const { getPostComposition, computeStage } = require('./post-composition'); const composition = await getPostComposition(req.params.id); if (composition) { const newStage = computeStage(composition); await nocodb.update('Posts', Number(req.params.id), { stage: newStage }); } ``` - [ ] **Step 3: Also auto-update post stage when Translation or Artefact status changes** In PATCH /api/translations/:id and PATCH /api/artefacts/:id — if the record has a `post_id`, re-compute the post's stage after saving. - [ ] **Step 4: Commit** ```bash git add server/server.js git commit -m "feat: add /posts/:id/composition endpoint + stage auto-update" ``` --- ## Chunk 2: Client — Platform Formats + Composition Sub-Components ### Task 4: Create platform formats constant **Files:** - Create: `client/src/utils/platformFormats.js` - [ ] **Step 1: Create the file** ```javascript export const PLATFORM_FORMATS = { instagram: [ { key: 'ig_feed', label: 'Feed (1:1)', ratio: '1:1' }, { key: 'ig_story', label: 'Story (9:16)', ratio: '9:16' }, { key: 'ig_reel', label: 'Reel (9:16)', ratio: '9:16' }, ], tiktok: [ { key: 'tt_video', label: 'TikTok (9:16)', ratio: '9:16' }, ], youtube: [ { key: 'yt_video', label: 'YouTube (16:9)', ratio: '16:9' }, { key: 'yt_short', label: 'Short (9:16)', ratio: '9:16' }, { key: 'yt_thumb', label: 'Thumbnail (16:9)', ratio: '16:9' }, ], facebook: [ { key: 'fb_post', label: 'Post (1:1)', ratio: '1:1' }, { key: 'fb_story', label: 'Story (9:16)', ratio: '9:16' }, ], twitter: [ { key: 'tw_post', label: 'Post (16:9)', ratio: '16:9' }, ], linkedin: [ { key: 'li_post', label: 'Post (1:1)', ratio: '1:1' }, ], snapchat: [ { key: 'sc_snap', label: 'Snap (9:16)', ratio: '9:16' }, ], } export function getFormatsForPlatforms(platforms = []) { const formats = [] const seen = new Set() for (const p of platforms) { for (const f of (PLATFORM_FORMATS[p] || [])) { if (!seen.has(f.key)) { seen.add(f.key); formats.push(f) } } } return formats } ``` - [ ] **Step 2: Commit** ```bash git add client/src/utils/platformFormats.js git commit -m "feat: add platform format mapping constant" ``` ### Task 5: Create composition sub-components **Files:** - Create: `client/src/components/PostCompositionCaption.jsx` - Create: `client/src/components/PostCompositionCopy.jsx` - Create: `client/src/components/PostCompositionDesigns.jsx` - Create: `client/src/components/PostCompositionVideo.jsx` - Create: `client/src/components/PostCompositionFormats.jsx` - Create: `client/src/components/PostCompositionReadiness.jsx` Each is a small focused component (~40-80 lines) rendering one section of the composition workspace. - [ ] **Step 1: Caption section** PostCompositionCaption.jsx — textarea for the social media caption. Props: `caption`, `onChange`, `disabled`. - [ ] **Step 2: Copy section** PostCompositionCopy.jsx — shows linked translations as language pills with status icons. Props: `copy` (array from composition), `onLink` (opens translation picker), `onCreate` (creates new translation for this post). Each pill is clickable to open the TranslationDetailPanel. - [ ] **Step 3: Designs section** PostCompositionDesigns.jsx — shows linked design artefacts as thumbnail cards with status badges. Props: `designs` (array), `onLink`, `onCreate`, `onOpen` (opens ArtefactDetailPanel). Shows "+ Add Design" button. - [ ] **Step 4: Video section** PostCompositionVideo.jsx — shows linked video artefact (0 or 1) as a card. Props: `video` (object or null), `onLink`, `onCreate`, `onOpen`. - [ ] **Step 5: Formats checklist** PostCompositionFormats.jsx — reads `platforms` from the post, computes needed formats via `getFormatsForPlatforms()`, renders as a checkbox list. This is informational only — checkboxes are manual (designer checks off what they've produced). No database storage for checked state (tracked visually only). - [ ] **Step 6: Readiness summary** PostCompositionReadiness.jsx — shows bullet list of what's ready and what's blocking. Props: `piecesReady`, `waitingOn` (array of strings), `onSignOff` (callback for approve/schedule button). Sign-off button disabled until `piecesReady` is true. - [ ] **Step 7: Commit** ```bash git add client/src/components/PostComposition*.jsx git commit -m "feat: add composition sub-components (caption, copy, designs, video, formats, readiness)" ``` --- ## Chunk 3: Client — Main Composition Panel + Page Integration ### Task 6: Create PostCompositionPanel **Files:** - Create: `client/src/components/PostCompositionPanel.jsx` - [ ] **Step 1: Build the panel** This is a SlidePanel-based component (like existing detail panels) but with a composition layout instead of tabs: ``` Header (title input, status, brand, campaign, platforms, assigned_to, close/save/delete) ───────── Scrollable body: PostCompositionCaption PostCompositionCopy PostCompositionDesigns PostCompositionVideo PostCompositionFormats PostCompositionReadiness CommentsSection ``` Key behavior: - On mount: fetches composition via `GET /api/posts/:id/composition` - Caption changes are saved with the post (dirty tracking + save button) - Copy/Design/Video sections have "Link existing" and "Create new" actions - "Link existing" opens a small picker modal (list of unlinked translations/artefacts) - "Create new" calls the create API with `post_id` pre-set, then refreshes composition - Readiness section shows sign-off button (sets post status to `approved`) - Each section is a collapsible card (use CollapsibleSection component) - [ ] **Step 2: Add i18n keys** Add to en.json and ar.json: - `post.caption`, `post.captionPlaceholder`, `post.copy`, `post.copyInDesign`, `post.designs`, `post.video`, `post.formatChecklist`, `post.formatsNeeded`, `post.readiness`, `post.allPiecesReady`, `post.waitingOn`, `post.signOff`, `post.approveAndSchedule`, `post.linkExisting`, `post.createNew`, `post.addDesign`, `post.addVideo`, `post.linkTranslation`, `post.noCopyLinked`, `post.noDesignsLinked`, `post.noVideoLinked` - [ ] **Step 3: Commit** ```bash git add client/src/components/PostCompositionPanel.jsx client/src/i18n/en.json client/src/i18n/ar.json git commit -m "feat: add PostCompositionPanel — composition workspace" ``` ### Task 7: Wire up PostCompositionPanel in pages **Files:** - Modify: `client/src/pages/PostProduction.jsx` - Modify: `client/src/pages/CampaignDetail.jsx` - [ ] **Step 1: Update PostProduction.jsx** Replace `PostDetailPanel` import and usage with `PostCompositionPanel`. The panel receives the same props (post, onClose, onSave, onDelete, brands, teamMembers, campaigns) plus the new composition data fetching happens inside the panel. - [ ] **Step 2: Update CampaignDetail.jsx** Same — replace PostDetailPanel with PostCompositionPanel for post detail views. - [ ] **Step 3: Commit** ```bash git add client/src/pages/PostProduction.jsx client/src/pages/CampaignDetail.jsx git commit -m "feat: wire PostCompositionPanel into PostProduction and CampaignDetail" ``` ### Task 8: Final verification - [ ] **Step 1: Build check** ```bash cd client && npx vite build --logLevel error ``` - [ ] **Step 2: Manual test checklist** 1. Open a post → composition panel shows caption, copy, designs, video, formats, readiness 2. Edit caption → save → caption persists 3. Link an existing translation → appears in copy section with status 4. Link an existing artefact → appears in designs section with thumbnail 5. Create new design artefact from panel → auto-linked to post 6. Select platforms → format checklist updates 7. Approve all pieces → readiness shows "All pieces ready" 8. Sign off → post status changes to approved 9. Stage auto-advances as pieces are linked - [ ] **Step 3: Commit any fixes**