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>
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
# 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**
|
||||
@@ -0,0 +1,226 @@
|
||||
# Post Composition Redesign — Post as Orchestrator
|
||||
|
||||
**Date:** 2026-03-15
|
||||
**Status:** Draft
|
||||
|
||||
## Problem
|
||||
|
||||
The current Post model is flat — a post has a title, description, status, attachments, and platforms. There's no structure showing that a social media post is actually a **composition** of distinct production pieces (caption, in-design copy, design assets, video). The Post detail panel is a disorganized form with tabs that don't map to how content is actually produced.
|
||||
|
||||
Additionally, ContentItems (from the UX overhaul pipeline) duplicates Post metadata and adds confusion about where to create content.
|
||||
|
||||
## Design
|
||||
|
||||
### Post = Orchestrator
|
||||
|
||||
A Post is a container that assembles independently-produced pieces into a publishable unit:
|
||||
|
||||
```
|
||||
Post "Summer Sale Launch"
|
||||
├─ Caption (text field on Post, one base version, minor platform tweaks)
|
||||
├─ Copy (in-design text): linked Translation(s) — approved via Translation flow
|
||||
├─ Design(s): linked Artefact(s) — approved via Artefact flow
|
||||
├─ Video: linked Artefact (optional) — approved via Artefact flow
|
||||
├─ Platforms: [IG, TikTok, YouTube]
|
||||
└─ Format checklist: auto-derived from platforms
|
||||
```
|
||||
|
||||
### Composition Pieces
|
||||
|
||||
| Piece | Storage | Approval | Notes |
|
||||
|-------|---------|----------|-------|
|
||||
| **Caption** | `Post.caption` field (text) | Part of final Post sign-off | The text posted WITH the content (IG caption, tweet text, etc.). One base version with minor platform tweaks (hashtags). Multilingual via existing Translation system if needed. |
|
||||
| **In-design copy** | Translation record (`post_id` FK, `is_original=true`) | Translation approval flow | Text that goes INSIDE the design (overlaid on image/video). Already exists. |
|
||||
| **Design(s)** | Artefact(s) linked via `post_id` FK, `type='design'` | Artefact approval flow | 1..N designs per post (carousel = multiple). Each artefact can have versions. |
|
||||
| **Video** | Artefact linked via `post_id` FK, `type='video'` | Artefact approval flow | 0..1 video per post. Has its own versions/approval. |
|
||||
| **Format specs** | Derived from `Post.platforms` | None (production checklist) | System maps platforms → required formats (IG→1:1, TikTok→9:16, etc.). Designer uses as a guide. |
|
||||
|
||||
### Platform → Format Mapping
|
||||
|
||||
```javascript
|
||||
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 or 16:9)', 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 or 1.91:1)', ratio: '1:1' },
|
||||
],
|
||||
snapchat: [
|
||||
{ key: 'sc_snap', label: 'Snap (9:16)', ratio: '9:16' },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
This is a **checklist** for the designer, not enforced entities. The Post detail panel shows "Formats needed" based on selected platforms. No separate database records per format.
|
||||
|
||||
### Post Status & Readiness
|
||||
|
||||
**Post status field** (unchanged): `idea` | `in_progress` | `in_review` | `approved` | `rejected` | `scheduled` | `published`
|
||||
|
||||
**Readiness is auto-computed** from pieces:
|
||||
- `pieces_ready`: true when ALL linked Translations are approved AND ALL linked Artefacts are approved
|
||||
- Displayed as: "Ready for sign-off" or "Waiting on: Copy (AR), Video"
|
||||
|
||||
**Final publish flow:**
|
||||
1. All pieces get approved through their own flows
|
||||
2. Post auto-shows "All pieces ready — awaiting sign-off"
|
||||
3. Someone manually moves Post to `approved` or `scheduled`
|
||||
4. Published when scheduled date arrives (or manually)
|
||||
|
||||
### ContentItems Merge
|
||||
|
||||
ContentItems table is removed. Its fields map to Post:
|
||||
- `ContentItems.stage` → `Post.stage` (copy / translate / design / post / published)
|
||||
- `ContentItems.title` → already `Post.title`
|
||||
- `ContentItems.campaign_id` → already `Post.campaign_id`
|
||||
- `ContentItems.brand_id` → already `Post.brand_id`
|
||||
- `ContentItems.assignee_id` → already `Post.assigned_to`
|
||||
|
||||
Stage auto-advances based on what exists:
|
||||
- Post created → stage = `copy`
|
||||
- Translation linked → stage = `translate` (if multiple languages)
|
||||
- Artefact (design) linked → stage = `design`
|
||||
- All pieces approved → stage = `post`
|
||||
- Published → stage = `published`
|
||||
|
||||
### Post Detail Panel — Composition View
|
||||
|
||||
Replace the current tabbed panel with a **composition workspace**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Header: Title, Status, Brand, Campaign │
|
||||
│ Platforms: [IG] [TikTok] [YouTube] │
|
||||
├─────────────────────────────────────────┤
|
||||
│ │
|
||||
│ CAPTION │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ Textarea: "🔥 Summer deals..." │ │
|
||||
│ │ Platform hashtags: #summer #sale │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ COPY (in-design text) │
|
||||
│ ┌────────┐ ┌────────┐ ┌────────┐ │
|
||||
│ │ EN ✓ │ │ AR ✓ │ │ FR ⏳ │ │
|
||||
│ └────────┘ └────────┘ └────────┘ │
|
||||
│ [Link Translation] or [Create New] │
|
||||
│ │
|
||||
│ DESIGNS │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Slide 1 │ │ Slide 2 │ │
|
||||
│ │ [thumbnail] │ │ [thumbnail] │ │
|
||||
│ │ ✓ Approved │ │ ✓ Approved │ │
|
||||
│ └──────────────┘ └──────────────┘ │
|
||||
│ [Link Artefact] or [Create New] │
|
||||
│ │
|
||||
│ VIDEO (optional) │
|
||||
│ ┌──────────────────────────────────┐ │
|
||||
│ │ [video thumbnail] Reel v2 │ │
|
||||
│ │ ⏳ In Review │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
│ [Link Artefact] or [Create New] │
|
||||
│ │
|
||||
│ FORMAT CHECKLIST │
|
||||
│ ☑ IG Feed 1:1 ☑ IG Story 9:16 │
|
||||
│ ☑ TikTok 9:16 ☐ YT 16:9 │
|
||||
│ │
|
||||
│ READINESS │
|
||||
│ ● Copy: 2/3 languages approved │
|
||||
│ ● Design: 2/2 approved │
|
||||
│ ● Video: In review │
|
||||
│ [Approve & Schedule] (disabled until │
|
||||
│ all pieces ready) │
|
||||
│ │
|
||||
│ DISCUSSION │
|
||||
│ [comments section] │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
This is a **single scrollable view**, not tabs. Each section is a collapsible card. The readiness summary at the bottom gives a clear picture of what's blocking publication.
|
||||
|
||||
### Schema Changes
|
||||
|
||||
**Post table — add:**
|
||||
- `caption` (LongText) — the social media caption
|
||||
- `stage` (SingleLineText) — pipeline stage: copy/translate/design/post/published
|
||||
|
||||
**Post table — remove:**
|
||||
- `description` (deprecated — copy lives in Translations)
|
||||
|
||||
**Artefact table — ensure:**
|
||||
- `post_id` FK already exists
|
||||
- `type` field already exists (design/video/copy)
|
||||
|
||||
**Translation table — ensure:**
|
||||
- `post_id` FK already exists
|
||||
|
||||
**ContentItems table:**
|
||||
- Delete after migration
|
||||
|
||||
### Migration
|
||||
|
||||
1. For each ContentItem: if no Post exists with matching title + campaign_id, create a Post from it
|
||||
2. Move `stage` values to the new Post.stage field
|
||||
3. Relink any Translations/Artefacts that referenced ContentItem IDs
|
||||
4. Drop ContentItems table (or leave empty, mark deprecated)
|
||||
|
||||
### API Changes
|
||||
|
||||
**POST /api/posts** — add `caption` field
|
||||
**PATCH /api/posts/:id** — add `caption` field, auto-update `stage` based on linked pieces
|
||||
**GET /api/posts/:id** — include linked Translations (with approval status), linked Artefacts (with type + approval status), computed `pieces_ready` boolean, computed `waiting_on` array
|
||||
|
||||
**New helper endpoint:**
|
||||
**GET /api/posts/:id/composition** — returns the full composition view:
|
||||
```json
|
||||
{
|
||||
"caption": "🔥 Summer deals...",
|
||||
"copy": [
|
||||
{ "id": 1, "language": "EN", "status": "approved" },
|
||||
{ "id": 2, "language": "AR", "status": "approved" },
|
||||
{ "id": 3, "language": "FR", "status": "in_review" }
|
||||
],
|
||||
"designs": [
|
||||
{ "id": 10, "title": "Slide 1", "status": "approved", "thumbnail_url": "..." },
|
||||
{ "id": 11, "title": "Slide 2", "status": "approved", "thumbnail_url": "..." }
|
||||
],
|
||||
"video": { "id": 20, "title": "Reel v2", "status": "in_review", "thumbnail_url": "..." },
|
||||
"platforms": ["instagram", "tiktok", "youtube"],
|
||||
"formats_needed": ["ig_feed", "ig_story", "ig_reel", "tt_video", "yt_video", "yt_short", "yt_thumb"],
|
||||
"pieces_ready": false,
|
||||
"waiting_on": ["Copy (FR)", "Video"]
|
||||
}
|
||||
```
|
||||
|
||||
### What Stays the Same
|
||||
|
||||
- Artefact approval flow (unchanged)
|
||||
- Translation approval flow (unchanged)
|
||||
- Post review via public link (unchanged — now reviews the full composition)
|
||||
- Campaign/brand/platform selection on Posts (unchanged)
|
||||
- KanbanBoard for pipeline view (unchanged — works with `stage` or `status`)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Auto-publishing to social media platforms
|
||||
- Caption AI generation
|
||||
- Design template system
|
||||
- Format-specific cropping tool
|
||||
- Per-platform caption variations (just one caption with manual tweaks)
|
||||
Reference in New Issue
Block a user