Files
marketing-app/docs/superpowers/plans/2026-03-15-post-composition-redesign.md
T
fahed ce4d6025d7 feat: post composition redesign + budget allocation + brand identity (Rawaj)
Post Workflow:
- PostDetail full page (/posts/:id) replaces slide panel approach
- Post = 1 Caption Copy + 1 Body Copy + 1 Design + 0-1 Video
- copy_type field on Translations (caption/body)
- Composition endpoint returns rich data (content preview, languages, thumbnails)
- Stage auto-advances on translation/artefact changes (both link and unlink)
- "Translations" renamed to "Copy" in navigation
- GET /api/posts/:id, /api/translations/:id, /api/artefacts/:id routes added
- PostProduction: "New Post" creates → navigates to full page
- CampaignDetail: click post → navigates to full page
- Inline link picker (no modals) with search + rich item display
- PostComposition sub-components for caption, copy, designs, video, formats, readiness

Budget Allocation:
- Single source of truth: BudgetEntries (Campaign.budget deprecated)
- Budget mutex for race conditions
- Validation at all levels (main → campaign → track, expenses)
- CEO approval workflow: BudgetRequests table, public approval page
- Finance page: request budget UI, budget requests section
- Settings: CEO email field
- All emails branded with "Rawaj —" prefix

Brand Identity:
- Name: Rawaj (رواج) — trending/virality
- Deep teal palette (#0d9488), forest-tinted dark mode
- DM Sans font, custom SVG logo
- Consistent across login, sidebar, emails, public pages

Approval Workflow:
- Single reviewer per artefact (not multi-select)
- Reviewer redirect on public review page
- Server blocks submit-review without reviewer
- Review URLs use APP_URL (not server URL)

UI/UX:
- Scroll clipping fix: Modal, TabbedModal, SlidePanel restructured
  to avoid overflow-y-auto clipping native select dropdowns
- section-card overflow-hidden → overflow-clip
- All page titles via Header.jsx (removed duplicate h1s)
- CampaignDetail redesigned: prominent budget card, compact team

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:02:29 +03:00

406 lines
14 KiB
Markdown

# 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**