ce4d6025d7
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>
406 lines
14 KiB
Markdown
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**
|