From 3c857856c5e7917ee82eb09e51f150d2fb35da3c Mon Sep 17 00:00:00 2001 From: fahed Date: Wed, 11 Mar 2026 17:38:45 +0300 Subject: [PATCH] docs: add UX/UI overhaul implementation plan (6 phases, 20 tasks) Fixes from plan review: corrected nocodb API patterns (nocodb.list/create/ update/get/delete), fixed REQUIRED_TABLES format (array not object), added KanbanBoard statusField prop, fixed variable shadowing, corrected api.get return value, added missing i18n keys, clarified route removal. Co-Authored-By: Claude Opus 4.6 --- .../plans/2026-03-11-ux-ui-overhaul.md | 1670 +++++++++++++++++ 1 file changed, 1670 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-11-ux-ui-overhaul.md diff --git a/docs/superpowers/plans/2026-03-11-ux-ui-overhaul.md b/docs/superpowers/plans/2026-03-11-ux-ui-overhaul.md new file mode 100644 index 0000000..99885eb --- /dev/null +++ b/docs/superpowers/plans/2026-03-11-ux-ui-overhaul.md @@ -0,0 +1,1670 @@ +# UX/UI Overhaul 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:** Restructure navigation (17→9 items), unify content pipeline, redesign dashboard, standardize UX patterns, and add premium polish across the marketing app. + +**Architecture:** 6 independent phases, each deployable on its own. Phase 1 restructures nav + consistency. Phase 2-3 build the Content pipeline. Phase 4 enhances campaigns. Phase 5 redesigns the dashboard. Phase 6 adds premium animations and effects. + +**Tech Stack:** React (Vite), Tailwind CSS, Express.js, NocoDB REST API, Lucide icons, CSS animations (no motion libraries). + +**Spec:** `docs/superpowers/specs/2026-03-11-ux-ui-overhaul-design.md` + +--- + +## Chunk 1: Phase 1 — Navigation Reorganization + +### Task 1.1: Rewrite Sidebar to Flat Nav + +**Files:** +- Modify: `client/src/components/Sidebar.jsx` +- Modify: `client/src/i18n/en.json` +- Modify: `client/src/i18n/ar.json` + +- [ ] **Step 1: Replace nav data structure** + +Replace the `standaloneTop`, `moduleGroups`, `standaloneBottom` arrays (lines 11-63) with a flat nav config: + +```jsx +const navItems = [ + { to: '/', icon: LayoutDashboard, labelKey: 'nav.dashboard', end: true }, + 'divider', + { to: '/campaigns', icon: Calendar, labelKey: 'nav.campaigns', module: 'marketing' }, + { to: '/content', icon: FileEdit, labelKey: 'nav.content', module: 'marketing' }, + 'divider', + { to: '/projects', icon: FolderKanban, labelKey: 'nav.projects', module: 'projects', hint: 'nav.projectsHint' }, + { to: '/issues', icon: AlertCircle, labelKey: 'nav.issues', module: 'issues' }, + 'divider', + { to: '/finance', icon: BarChart3, labelKey: 'nav.finance', module: 'finance', minRole: 'manager', hint: 'nav.financeHint' }, + 'divider', + { to: '/team', icon: Users, labelKey: 'nav.team' }, + { to: '/settings', icon: Settings, labelKey: 'nav.settings' }, +] +``` + +- [ ] **Step 2: Rewrite nav rendering** + +Remove the module group expand/collapse logic (`expandedGroups`, `toggleGroup`, the `visibleGroups` map). Replace with a flat list renderer: + +```jsx +const filteredItems = navItems.filter(item => { + if (item === 'divider') return true + if (item.module && !hasModule(item.module)) return false + if (item.minRole && userLevel < (ROLE_LEVEL[item.minRole] ?? 0)) return false + return true +}) + +// In JSX, replace the nav section: +{filteredItems.map((item, i) => + item === 'divider' + ?
+ : navLink(item) +)} +``` + +- [ ] **Step 3: Remove sub-item indentation from navLink** + +Update the `navLink` function (line 83) — remove the `sub` parameter and sub-item styles. All items are top-level now: + +```jsx +const navLink = ({ to, icon: Icon, labelKey, end, hint }) => ( + + `flex items-center gap-3 rounded-lg font-medium transition-all duration-200 group px-3 py-2 text-sm ${ + isActive + ? 'bg-white/15 text-white shadow-sm sidebar-active-glow' + : 'text-text-on-dark-muted hover:bg-white/8 hover:text-white' + }` + } + > + + {!collapsed && ( + {t(labelKey)} + )} + {!collapsed && hint && ( + {t(hint)} + )} + +) +``` + +- [ ] **Step 4: Remove unused imports and state** + +Remove: `ChevronDown` from lucide imports, `useState` for `expandedGroups`, the `toggleGroup` function, the `visibleGroups` computation. + +Add to imports if missing: `Settings` from lucide-react. + +- [ ] **Step 5: Add i18n keys** + +In `en.json`, add: +```json +"nav.content": "Content", +"nav.projectsHint": "+ tasks", +"nav.financeHint": "+ budgets" +``` + +In `ar.json`, add: +```json +"nav.content": "المحتوى", +"nav.projectsHint": "+ المهام", +"nav.financeHint": "+ الميزانيات" +``` + +- [ ] **Step 6: Test manually — verify sidebar renders** + +Run: `cd /home/fahed/clawd/projects/marketing-app && npm run dev` + +Check: +- Dashboard, Campaigns, Content, Projects, Issues, Finance, Team, Settings all visible +- Dividers appear between groups +- No collapsible module headers +- Collapsed sidebar shows icons correctly +- Active glow works on each item + +- [ ] **Step 7: Commit** + +```bash +git add client/src/components/Sidebar.jsx client/src/i18n/en.json client/src/i18n/ar.json +git commit -m "refactor: flatten sidebar nav — 17 items to 9, remove collapsible groups" +``` + +### Task 1.2: Add Route Redirects for Old URLs + +**Files:** +- Modify: `client/src/App.jsx` + +- [ ] **Step 1: Add Navigate import and redirect routes** + +At the top of App.jsx, ensure `Navigate` is imported from react-router-dom. + +Inside the protected `}>` block, add redirects for removed routes: + +```jsx +{/* Redirects for old routes */} +} /> +} /> +} /> +} /> +} /> +} /> +} /> +} /> +``` + +- [ ] **Step 2: Add Content route placeholder** + +Add a route for the Content page (will be a placeholder until Phase 2): + +```jsx +const Content = lazy(() => import('./pages/Content')) + +// Inside protected routes — add BEFORE the redirect routes: +}>} /> +}>} /> +``` + +**Also remove the old standalone route definitions** for `/posts`, `/calendar`, `/artefacts`, `/translations`, `/assets`, `/brands`, `/tasks`, `/budgets` — the redirects replace them. + +- [ ] **Step 3: Create placeholder Content page** + +Create `client/src/pages/Content.jsx`: + +```jsx +import { useParams, useSearchParams } from 'react-router-dom' +import { useLanguage } from '../i18n/LanguageContext' + +const TABS = ['pipeline', 'copy', 'translations', 'design', 'posts'] + +export default function Content() { + const { tab = 'pipeline' } = useParams() + const [searchParams] = useSearchParams() + const { t } = useLanguage() + + return ( +
+

{t('nav.content')}

+ +

Content pipeline — coming soon.

+
+ ) +} +``` + +- [ ] **Step 4: Update Header page title map** + +In `client/src/components/Header.jsx`, find the `PAGE_TITLE_KEYS` map and add: + +```js +'/content': 'header.content', +``` + +Add to both i18n files: `"header.content": "Content"` (en), `"header.content": "المحتوى"` (ar). + +Remove entries for `/posts`, `/calendar`, `/artefacts`, `/translations`, `/assets`, `/brands`, `/tasks`, `/budgets` (these now redirect). + +- [ ] **Step 5: Test redirects** + +Navigate to `/posts` → should redirect to `/content/posts` +Navigate to `/artefacts` → should redirect to `/content/design` +Navigate to `/content` → should show placeholder with tabs + +- [ ] **Step 6: Commit** + +```bash +git add client/src/App.jsx client/src/pages/Content.jsx client/src/components/Header.jsx +git commit -m "feat: add route redirects for nav reorg, placeholder Content page" +``` + +### Task 1.3: Absorb Budgets into Finance Page + +**Files:** +- Modify: `client/src/pages/Finance.jsx` +- Modify: `client/src/i18n/en.json` +- Modify: `client/src/i18n/ar.json` + +- [ ] **Step 1: Read Finance.jsx and Budgets.jsx** + +Read both files to understand current structure and plan the merge. + +- [ ] **Step 2: Add tab toggle to Finance page** + +At the top of the Finance page, add a tab bar switching between "Dashboard" and "Budgets": + +```jsx +const [searchParams, setSearchParams] = useSearchParams() +const activeTab = searchParams.get('tab') || 'dashboard' +``` + +Render tabs: +```jsx +
+ + +
+``` + +- [ ] **Step 3: Conditionally render content** + +Wrap the existing Finance dashboard content in `{activeTab === 'dashboard' && (...)}`. + +Import the Budgets page component and render: `{activeTab === 'budgets' && }`. + +Alternatively, if Budgets is too large to import directly, lazy-load it. + +- [ ] **Step 4: Test both tabs work** + +- [ ] **Step 5: Commit** + +```bash +git add client/src/pages/Finance.jsx client/src/i18n/en.json client/src/i18n/ar.json +git commit -m "feat: absorb Budgets as tab inside Finance page" +``` + +### Task 1.4: Absorb Brands & Assets into Settings Page + +**Files:** +- Modify: `client/src/pages/Settings.jsx` +- Modify: `client/src/i18n/en.json` +- Modify: `client/src/i18n/ar.json` + +- [ ] **Step 1: Read Settings.jsx, Brands.jsx, Assets.jsx** + +Understand current Settings structure and what needs to be added. + +- [ ] **Step 2: Add tab navigation to Settings** + +Same pattern as Finance: tabs for "General", "Brands", "Assets" using `useSearchParams`. + +```jsx +const tabs = [ + { key: 'general', labelKey: 'settings.general' }, + { key: 'brands', labelKey: 'nav.brands' }, + { key: 'assets', labelKey: 'nav.assets' }, +] +``` + +- [ ] **Step 3: Render tab content** + +- General tab: existing Settings content +- Brands tab: import and render Brands component +- Assets tab: import and render Assets component + +- [ ] **Step 4: Add i18n key for "General"** + +```json +"settings.general": "General" // en +"settings.general": "عام" // ar +``` + +- [ ] **Step 5: Test all three tabs** + +- [ ] **Step 6: Commit** + +```bash +git add client/src/pages/Settings.jsx client/src/i18n/en.json client/src/i18n/ar.json +git commit -m "feat: absorb Brands & Assets as tabs inside Settings page" +``` + +### Task 1.5: Absorb Tasks into Projects Page + +**Files:** +- Modify: `client/src/pages/Projects.jsx` +- Modify: `client/src/i18n/en.json` +- Modify: `client/src/i18n/ar.json` + +- [ ] **Step 1: Read Projects.jsx and Tasks.jsx** + +- [ ] **Step 2: Add "Projects" / "All Tasks" tab toggle to Projects page** + +Same `useSearchParams` pattern. Default tab is "projects". + +- [ ] **Step 3: Render Tasks component in "All Tasks" tab** + +Import Tasks and render when `tab === 'tasks'`. + +- [ ] **Step 4: Add i18n keys** + +```json +"projects.allTasks": "All Tasks" // en +"projects.allTasks": "جميع المهام" // ar +``` + +- [ ] **Step 5: Test both tabs** + +- [ ] **Step 6: Commit** + +```bash +git add client/src/pages/Projects.jsx client/src/i18n/en.json client/src/i18n/ar.json +git commit -m "feat: absorb Tasks as tab inside Projects page" +``` + +### Task 1.6: Standardize Page Headers + +**Files:** +- Modify: All page files (PostProduction/Content, Issues, Projects, Finance, Campaigns, Team, Settings, Dashboard) +- Create: `client/src/components/PageHeader.jsx` + +- [ ] **Step 1: Create shared PageHeader component** + +```jsx +import { Search, X } from 'lucide-react' +import { useState } from 'react' +import { useLanguage } from '../i18n/LanguageContext' + +export default function PageHeader({ + title, + searchTerm, + onSearchChange, + filters, // ReactNode — inline filter dropdowns + viewToggle, // ReactNode — view mode buttons + actions, // ReactNode — create button, bulk actions +}) { + const [searchOpen, setSearchOpen] = useState(!!searchTerm) + const { t } = useLanguage() + + return ( +
+

{title}

+
+ + {onSearchChange && ( + searchOpen ? ( +
+ + onSearchChange(e.target.value)} + placeholder={t('common.search')} + className="pl-9 pr-8 py-1.5 text-sm border border-border rounded-lg bg-surface-primary focus:outline-none focus:ring-2 focus:ring-brand-primary/20 w-48" + autoFocus + /> + +
+ ) : ( + + ) + )} + + {filters} + {viewToggle} + {actions} +
+ ) +} +``` + +- [ ] **Step 2: Adopt PageHeader in Issues.jsx as first migration** + +Replace the existing header JSX in Issues.jsx with ``, passing the existing search, filters, view toggle, and create button as props. + +- [ ] **Step 3: Adopt PageHeader in remaining pages** + +Apply the same pattern to: PostProduction (now Content/Posts tab), Projects, Campaigns, Finance, Team. + +- [ ] **Step 4: Standardize filter visibility** + +Ensure all pages show filters inline (always visible). Remove any toggle buttons that show/hide the filter bar. + +- [ ] **Step 5: Commit** + +```bash +git add client/src/components/PageHeader.jsx client/src/pages/*.jsx +git commit -m "feat: standardize page headers with shared PageHeader component" +``` + +### Task 1.7: Standardize Detail Panel Layout + +**Files:** +- Modify: `client/src/components/SlidePanel.jsx` +- Modify: All detail panel components + +- [ ] **Step 1: Update SlidePanel default maxWidth** + +In `SlidePanel.jsx`, change default `maxWidth` from `'420px'` to `'480px'`. + +- [ ] **Step 2: Standardize header pattern across panels** + +Each detail panel should use this header pattern: +```jsx +
+ +

{title}

+ + +
+``` + +- [ ] **Step 3: Move delete buttons into overflow menus** + +In each detail panel (PostDetailPanel, ArtefactDetailPanel, IssueDetailPanel, TaskDetailPanel, TranslationDetailPanel, CampaignDetailPanel), move the delete button from standalone position into an overflow `⋯` menu. + +Create a small `OverflowMenu` component if one doesn't exist: + +```jsx +// client/src/components/OverflowMenu.jsx +import { useState, useRef, useEffect } from 'react' +import { MoreVertical } from 'lucide-react' + +export default function OverflowMenu({ items }) { + const [open, setOpen] = useState(false) + const ref = useRef() + + useEffect(() => { + const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false) } + document.addEventListener('mousedown', handler) + return () => document.removeEventListener('mousedown', handler) + }, []) + + return ( +
+ + {open && ( +
+ {items.map((item, i) => ( + + ))} +
+ )} +
+ ) +} +``` + +- [ ] **Step 4: Standardize tab order in detail panels** + +Ensure every detail panel with tabs uses: Details → Activity → Approval (in that order). Rename any "Discussion" or "Comments" tabs to "Activity". + +- [ ] **Step 5: Test each detail panel opens and functions correctly** + +- [ ] **Step 6: Commit** + +```bash +git add client/src/components/SlidePanel.jsx client/src/components/OverflowMenu.jsx client/src/components/*Panel*.jsx client/src/components/*DetailPanel*.jsx +git commit -m "feat: standardize detail panels — 480px width, overflow menu, consistent tabs" +``` + +--- + +## Chunk 2: Phase 2 — Content Page with Tabs + +### Task 2.1: Build Content Page Shell with Tab Routing + +**Files:** +- Modify: `client/src/pages/Content.jsx` (replace placeholder) +- Modify: `client/src/App.jsx` + +- [ ] **Step 1: Update Content.jsx with full tab system** + +```jsx +import { useParams, useNavigate, useSearchParams } from 'react-router-dom' +import { useLanguage } from '../i18n/LanguageContext' +import { Layers, PenLine, Languages, Palette, Send } from 'lucide-react' + +const TABS = [ + { key: 'pipeline', labelKey: 'content.pipeline', icon: Layers }, + { key: 'copy', labelKey: 'content.copy', icon: PenLine }, + { key: 'translations', labelKey: 'content.translations', icon: Languages }, + { key: 'design', labelKey: 'content.design', icon: Palette }, + { key: 'posts', labelKey: 'content.posts', icon: Send }, +] + +export default function Content() { + const { tab = 'pipeline' } = useParams() + const navigate = useNavigate() + const [searchParams] = useSearchParams() + const { t } = useLanguage() + + return ( +
+ {/* Tab bar */} +
+ {TABS.map(({ key, labelKey, icon: Icon }) => ( + + ))} +
+ + {/* Tab content */} + {tab === 'pipeline' && } + {tab === 'copy' && } + {tab === 'translations' && } + {tab === 'design' && } + {tab === 'posts' && } +
+ ) +} + +// Placeholder sub-components — replaced in subsequent tasks +function PipelineTab() { + return

Pipeline view — coming in Phase 3.

+} + +function CopyTab() { + return

Copy tab — loading existing translations (originals).

+} + +function TranslationsTab() { + return

Translations tab — loading.

+} + +function DesignTab() { + return

Design tab — loading artefacts.

+} + +function PostsTab() { + return

Posts tab — loading.

+} +``` + +- [ ] **Step 2: Add i18n keys** + +In `en.json`: +```json +"content.pipeline": "Pipeline", +"content.copy": "Copy", +"content.translations": "Translations", +"content.design": "Design", +"content.posts": "Posts" +``` + +In `ar.json`: +```json +"content.pipeline": "خط الإنتاج", +"content.copy": "النسخ", +"content.translations": "الترجمات", +"content.design": "التصميم", +"content.posts": "المنشورات" +``` + +- [ ] **Step 3: Test tab navigation** + +Navigate to `/content` → Pipeline tab active +Click each tab → URL updates, correct tab shows + +- [ ] **Step 4: Commit** + +```bash +git add client/src/pages/Content.jsx client/src/i18n/en.json client/src/i18n/ar.json +git commit -m "feat: Content page shell with 5-tab routing" +``` + +### Task 2.2: Wire Posts Tab (Migrate PostProduction) + +**Files:** +- Modify: `client/src/pages/Content.jsx` +- Read: `client/src/pages/PostProduction.jsx` + +- [ ] **Step 1: Read PostProduction.jsx fully** + +Understand all state, effects, handlers, and JSX. + +- [ ] **Step 2: Import and render PostProduction inside PostsTab** + +The simplest approach: import the existing page component and render it directly in the Posts tab. This preserves all existing functionality with zero risk: + +```jsx +import PostProduction from './PostProduction' + +function PostsTab() { + const [searchParams] = useSearchParams() + const defaultView = searchParams.get('view') + return +} +``` + +If PostProduction renders its own page title/header, either: +- Add a `embedded` prop to suppress the title when rendered inside Content +- Or remove the title from PostProduction and let Content handle it + +- [ ] **Step 3: Handle Calendar view toggle** + +If URL is `/content/posts?view=calendar`, ensure PostProduction starts in calendar view. Pass `defaultView` prop. + +- [ ] **Step 4: Test** + +- All post functionality works: create, edit, kanban drag, list view, calendar view +- Detail panel opens correctly +- Filters work + +- [ ] **Step 5: Commit** + +```bash +git add client/src/pages/Content.jsx client/src/pages/PostProduction.jsx +git commit -m "feat: wire Posts tab in Content page — migrates PostProduction" +``` + +### Task 2.3: Wire Translations & Copy Tabs + +**Files:** +- Modify: `client/src/pages/Content.jsx` +- Modify: `client/src/pages/Translations.jsx` + +- [ ] **Step 1: Read Translations.jsx fully** + +Understand the data model — particularly how originals vs translations are currently distinguished. + +- [ ] **Step 2: Add `is_original` filter prop to Translations component** + +Modify Translations.jsx to accept an optional `filterOriginal` prop: +- `filterOriginal={true}` → show only original copy (Copy tab) +- `filterOriginal={false}` → show only translations (Translations tab) +- `filterOriginal={undefined}` → show all (backwards compat) + +The filter applies client-side on the loaded data, filtering by `is_original` field. Until the server adds `is_original`, all items show as originals (safe default). + +- [ ] **Step 3: Wire into Content.jsx** + +```jsx +import Translations from './Translations' + +function CopyTab() { + return +} + +function TranslationsTab() { + return +} +``` + +- [ ] **Step 4: Test both tabs show data** + +- [ ] **Step 5: Commit** + +```bash +git add client/src/pages/Content.jsx client/src/pages/Translations.jsx +git commit -m "feat: wire Copy & Translations tabs in Content page" +``` + +### Task 2.4: Wire Design Tab (Migrate Artefacts) + +**Files:** +- Modify: `client/src/pages/Content.jsx` +- Modify: `client/src/pages/Artefacts.jsx` + +- [ ] **Step 1: Import Artefacts into Design tab** + +Same approach as Posts — import and render with `embedded` prop: + +```jsx +import Artefacts from './Artefacts' + +function DesignTab() { + return +} +``` + +- [ ] **Step 2: Add `embedded` prop handling to Artefacts.jsx** + +When `embedded={true}`, suppress the page title. + +- [ ] **Step 3: Test design tab** + +- Grid and list views work +- Detail panel opens +- Create artefact works + +- [ ] **Step 4: Commit** + +```bash +git add client/src/pages/Content.jsx client/src/pages/Artefacts.jsx +git commit -m "feat: wire Design tab in Content page — migrates Artefacts" +``` + +--- + +## Chunk 3: Phase 3 — Content Item Model + Pipeline + +### Task 3.1: Server — ContentItems Table Schema + +**Files:** +- Modify: `server/server.js` + +- [ ] **Step 1: Add ContentItems to REQUIRED_TABLES** + +Add inside the `REQUIRED_TABLES` object (around line 181-445). The value must be an **array** of column objects (not an object with `table_name`/`columns` keys — that's not the pattern used): + +```js +ContentItems: [ + { title: 'title', uidt: 'SingleLineText' }, + { title: 'stage', uidt: 'SingleLineText' }, // copy, translate, design, post, published + { title: 'created_by', uidt: 'SingleLineText' }, +], +``` + +- [ ] **Step 2: Add FK columns for ContentItems** + +In `FK_COLUMNS`, add: + +```js +ContentItems: ['campaign_id', 'brand_id', 'assignee_id'], +Translations: [...existing, 'content_item_id'], +Artefacts: [...existing, 'content_item_id'], +Posts: [...existing, 'content_item_id'], +``` + +- [ ] **Step 3: Add is_original to Translations TEXT_COLUMNS** + +```js +Translations: [...existing, { name: 'is_original', uidt: 'Checkbox' }], +``` + +- [ ] **Step 4: Test — restart server, verify table + columns created** + +Run: `cd /home/fahed/clawd/projects/marketing-app && node server/server.js` + +Check logs for: +- `✓ Created table ContentItems` +- `✓ Created column content_item_id` on Translations, Artefacts, Posts +- `✓ Created column is_original` on Translations + +- [ ] **Step 5: Commit** + +```bash +git add server/server.js +git commit -m "feat: add ContentItems table schema + FK columns + is_original flag" +``` + +### Task 3.2: Server — ContentItems CRUD Routes + +**Files:** +- Modify: `server/server.js` + +- [ ] **Step 1: Add GET /api/content-items** + +Follow the existing route pattern (e.g., GET /api/issues). The `nocodb` client uses `nocodb.list('TableName', opts)` — no tableId resolution needed. Server has no `normalize()` — normalization happens client-side in `api.js`. + +```js +app.get('/api/content-items', requireAuth, async (req, res) => { + try { + const { stage, campaign_id } = req.query + const conditions = [] + if (stage) conditions.push({ field: 'stage', op: 'eq', value: sanitizeWhereValue(stage) }) + if (campaign_id) conditions.push({ field: 'campaign_id', op: 'eq', value: sanitizeWhereValue(campaign_id) }) + + const items = await nocodb.list('ContentItems', { + where: conditions, + sort: '-CreatedAt', + }) + + // Enrich with names using batchResolveNames pattern + const brandIds = [...new Set(items.filter(i => i.brand_id).map(i => i.brand_id))] + const userIds = [...new Set(items.filter(i => i.assignee_id).map(i => i.assignee_id))] + const brandMap = await batchResolveNames('Brands', brandIds) + const userMap = await batchResolveNames('Users', userIds) + + for (const item of items) { + if (item.brand_id) item.brand_name = brandMap[item.brand_id] || '' + if (item.assignee_id) item.assignee_name = userMap[item.assignee_id] || '' + } + + res.json(items) + } catch (err) { + console.error('[GET /content-items]', err) + res.status(500).json({ error: 'Failed to fetch content items' }) + } +}) +``` + +Note: Check if `batchResolveNames` exists in server.js. If not, use the same enrichment pattern as GET /api/issues (which reads users/brands from cached data). + +- [ ] **Step 2: Add POST /api/content-items** + +```js +app.post('/api/content-items', requireAuth, async (req, res) => { + try { + const created = await nocodb.create('ContentItems', { + title: req.body.title, + stage: req.body.stage || 'copy', + campaign_id: req.body.campaign_id || null, + brand_id: req.body.brand_id || null, + assignee_id: req.body.assignee_id || null, + created_by: String(req.session.userId), + }) + res.status(201).json(created) + } catch (err) { + console.error('[POST /content-items]', err) + res.status(500).json({ error: 'Failed to create content item' }) + } +}) +``` + +- [ ] **Step 3: Add PATCH /api/content-items/:id** + +```js +app.patch('/api/content-items/:id', requireAuth, async (req, res) => { + try { + const allowed = ['title', 'stage', 'campaign_id', 'brand_id', 'assignee_id'] + const data = {} + for (const key of allowed) { + if (req.body[key] !== undefined) data[key] = req.body[key] + } + await nocodb.update('ContentItems', req.params.id, data) + const updated = await nocodb.get('ContentItems', req.params.id) + res.json(updated) + } catch (err) { + console.error('[PATCH /content-items]', err) + res.status(500).json({ error: 'Failed to update content item' }) + } +}) +``` + +- [ ] **Step 4: Add DELETE /api/content-items/:id** + +```js +app.delete('/api/content-items/:id', requireAuth, async (req, res) => { + try { + await nocodb.delete('ContentItems', req.params.id) + res.json({ success: true }) + } catch (err) { + console.error('[DELETE /content-items]', err) + res.status(500).json({ error: 'Failed to delete content item' }) + } +}) +``` + +- [ ] **Step 5: Test with curl** + +```bash +# Create +curl -X POST http://localhost:3001/api/content-items -H 'Content-Type: application/json' -H 'Cookie: ...' -d '{"title":"Test Content"}' + +# List +curl http://localhost:3001/api/content-items -H 'Cookie: ...' + +# Update +curl -X PATCH http://localhost:3001/api/content-items/1 -H 'Content-Type: application/json' -H 'Cookie: ...' -d '{"stage":"translate"}' +``` + +- [ ] **Step 6: Commit** + +```bash +git add server/server.js +git commit -m "feat: add ContentItems CRUD API routes" +``` + +### Task 3.3: Pipeline Kanban Board + +**Files:** +- Create: `client/src/components/ContentPipelineBoard.jsx` +- Modify: `client/src/pages/Content.jsx` + +- [ ] **Step 1: Create ContentPipelineBoard component** + +Uses the existing `KanbanBoard` component with pipeline-specific columns: + +```jsx +import { useLanguage } from '../i18n/LanguageContext' +import KanbanBoard from './KanbanBoard' +import StatusBadge from './StatusBadge' + +const PIPELINE_COLUMNS = [ + { id: 'copy', labelKey: 'content.copy', color: '#818cf8' }, + { id: 'translate', labelKey: 'content.translations', color: '#f59e0b' }, + { id: 'design', labelKey: 'content.design', color: '#db2777' }, + { id: 'post', labelKey: 'content.posts', color: '#059669' }, + { id: 'published', labelKey: 'content.published', color: '#6366f1' }, +] + +export default function ContentPipelineBoard({ items, onMove, onSelect }) { + const { t } = useLanguage() + + const columns = PIPELINE_COLUMNS.map(col => ({ + id: col.id, + label: t(col.labelKey), + color: col.color, + })) + + const renderCard = (item) => ( +
onSelect(item)} + className="bg-surface-primary border border-border rounded-lg p-3 cursor-pointer card-hover" + > +

{item.title}

+
+ {item.brand_name && ( + + {item.brand_name} + + )} + {item.assignee_name && ( + {item.assignee_name} + )} +
+
+ ) + + return ( + item.id || item.Id} + onMove={(itemId, newStage) => onMove(itemId, newStage)} + emptyLabel={t('content.emptyStage')} + /> + ) +} +``` + +**IMPORTANT: KanbanBoard compatibility fix needed.** KanbanBoard hardcodes `item.status` (line 38, 47, 48) and expects `col.color` as a Tailwind class (e.g., `bg-indigo-400`), not a hex color. Two fixes: + +**Fix A — Add `statusField` prop to KanbanBoard.jsx** (preferred, backwards-compatible): +```jsx +// Change signature: +export default function KanbanBoard({ columns, items, renderCard, getItemId, onMove, emptyLabel, statusField = 'status' }) + +// Change line 38: draggedItem.status → draggedItem[statusField] +// Change line 47: item.status === col.id → item[statusField] === col.id +// Change line 48: draggedItem?.status !== col.id → draggedItem?.[statusField] !== col.id +``` + +Then pass `statusField="stage"` from ContentPipelineBoard. + +**Fix B — Use Tailwind classes for column colors** (not hex): +```jsx +const PIPELINE_COLUMNS = [ + { id: 'copy', labelKey: 'content.copy', color: 'bg-indigo-400' }, + { id: 'translate', labelKey: 'content.translations', color: 'bg-amber-400' }, + { id: 'design', labelKey: 'content.design', color: 'bg-pink-500' }, + { id: 'post', labelKey: 'content.posts', color: 'bg-emerald-500' }, + { id: 'published', labelKey: 'content.published', color: 'bg-violet-500' }, +] +``` + +- [ ] **Step 2: Wire PipelineTab in Content.jsx** + +```jsx +import ContentPipelineBoard from '../components/ContentPipelineBoard' +import api from '../utils/api' + +function PipelineTab() { + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + api.get('/content-items').then(data => { + setItems(data) + setLoading(false) + }) + }, []) + + const handleMove = async (itemId, newStage) => { + // Optimistic update + setItems(prev => prev.map(i => + (i.id || i.Id) === itemId ? { ...i, stage: newStage } : i + )) + await api.patch(`/content-items/${itemId}`, { stage: newStage }) + } + + if (loading) return + + return {}} /> +} +``` + +- [ ] **Step 3: Add i18n keys** + +```json +"content.translate": "Translate", +"content.post": "Post", +"content.published": "Published", +"content.emptyStage": "No items at this stage" +``` + +- [ ] **Step 4: Test pipeline kanban renders and drag works** + +- [ ] **Step 5: Commit** + +```bash +git add client/src/components/ContentPipelineBoard.jsx client/src/pages/Content.jsx client/src/i18n/en.json client/src/i18n/ar.json +git commit -m "feat: pipeline kanban board for Content page" +``` + +### Task 3.4: Content Item Detail Panel + +**Files:** +- Create: `client/src/components/ContentDetailPanel.jsx` +- Modify: `client/src/pages/Content.jsx` + +- [ ] **Step 1: Create ContentDetailPanel** + +Panel showing pipeline breadcrumb + linked items per stage: + +```jsx +import SlidePanel from './SlidePanel' +import OverflowMenu from './OverflowMenu' +import { ArrowLeft, Check, Circle, Clock } from 'lucide-react' +import { useLanguage } from '../i18n/LanguageContext' + +// Stage keys match i18n: content.copy, content.translate, content.design, content.post, content.published +const STAGES = ['copy', 'translate', 'design', 'post', 'published'] + +function StageBreadcrumb({ currentStage }) { + const { t } = useLanguage() + return ( +
+ {STAGES.map((stage, i) => { + const stageIndex = STAGES.indexOf(currentStage) + const isDone = i < stageIndex + const isCurrent = i === stageIndex + return ( +
+ {i > 0 && } + + {isDone ? : isCurrent ? : } + {t(`content.${stage}`)} + +
+ ) + })} +
+ ) +} + +export default function ContentDetailPanel({ item, onClose, onSave, onDelete }) { + const { t } = useLanguage() + + if (!item) return null + + const header = ( +
+ +

{item.title}

+ + +
+ ) + + return ( + + +
+ {/* Title edit */} +
+ + +
+ {/* Campaign + Brand + Assignee selectors would go here */} + {/* Linked items per stage would go here — Phase 3 continued */} +
+
+ ) +} +``` + +- [ ] **Step 2: Wire into Content.jsx PipelineTab** + +Add state for `selectedItem` and render `ContentDetailPanel` when an item is selected. + +- [ ] **Step 3: Test panel opens on card click with breadcrumb** + +- [ ] **Step 4: Commit** + +```bash +git add client/src/components/ContentDetailPanel.jsx client/src/pages/Content.jsx +git commit -m "feat: content detail panel with pipeline stage breadcrumb" +``` + +--- + +## Chunk 4: Phase 4 — Campaign Brief Enhancement + +### Task 4.1: Server — Campaign Brief Columns + +**Files:** +- Modify: `server/server.js` + +- [ ] **Step 1: Add campaign brief columns to TEXT_COLUMNS** + +```js +Campaigns: [ + { name: 'target_audience', uidt: 'LongText' }, + { name: 'key_messages', uidt: 'LongText' }, + { name: 'reach_target', uidt: 'Number' }, + { name: 'engagement_target', uidt: 'Number' }, + { name: 'conversion_target', uidt: 'Number' }, + { name: 'approval_status', uidt: 'SingleLineText' }, + { name: 'approved_by', uidt: 'SingleLineText' }, + { name: 'approved_at', uidt: 'SingleLineText' }, +], +``` + +Note: **Skip `goals`** — it already exists as `LongText` in `REQUIRED_TABLES` (line 209). It stores JSON-stringified arrays and is already parsed client-side in `api.js`. Reuse it for the goal tags (store as `["awareness","engagement"]`). + +- [ ] **Step 2: Update PATCH /api/campaigns to handle new fields** + +Add the new fields to the allowed update fields list in the PATCH route. + +- [ ] **Step 3: Add campaign approval endpoint** + +```js +app.post('/api/campaigns/:id/approve', requireAuth, async (req, res) => { + try { + if (req.session.userRole !== 'superadmin' && req.session.userRole !== 'manager') { + return res.status(403).json({ error: 'Only managers can approve campaigns' }) + } + await nocodb.update('Campaigns', req.params.id, { + approval_status: 'approved', + approved_by: String(req.session.userId), + approved_at: new Date().toISOString(), + }) + const updated = await nocodb.get('Campaigns', req.params.id) + res.json(updated) + } catch (err) { + res.status(500).json({ error: 'Failed to approve campaign' }) + } +}) +``` + +- [ ] **Step 4: Restart server, verify columns created** + +- [ ] **Step 5: Commit** + +```bash +git add server/server.js +git commit -m "feat: campaign brief columns + approval endpoint" +``` + +### Task 4.2: Campaign Detail — Brief Section UI + +**Files:** +- Modify: `client/src/pages/CampaignDetail.jsx` +- Modify: `client/src/i18n/en.json` +- Modify: `client/src/i18n/ar.json` + +- [ ] **Step 1: Read CampaignDetail.jsx** + +- [ ] **Step 2: Add Brief section to campaign detail** + +Above or below the existing timeline, add a "Brief" section with: +- Goals: tag/chip selector from predefined list (awareness, engagement, conversions, brand_building, lead_generation) +- Target audience: textarea +- Key messages: textarea +- Metrics targets: 3 number inputs (reach, engagement rate, conversions) +- Approval status badge + "Request Approval" / "Approve" button + +- [ ] **Step 3: Add "Content Items" section** + +Below the brief, show linked content items as small cards with pipeline stage badges. Include "Create Content" button (only visible when `approval_status === 'approved'`). + +- [ ] **Step 4: Add i18n keys for all new labels** + +- [ ] **Step 5: Test campaign brief editing and approval flow** + +- [ ] **Step 6: Commit** + +```bash +git add client/src/pages/CampaignDetail.jsx client/src/i18n/en.json client/src/i18n/ar.json +git commit -m "feat: campaign brief section with goals, metrics, approval gate" +``` + +--- + +## Chunk 5: Phase 5 — Dashboard Redesign + +### Task 5.1: Redesign Dashboard Layout + +**Files:** +- Modify: `client/src/pages/Dashboard.jsx` +- Modify: `client/src/i18n/en.json` +- Modify: `client/src/i18n/ar.json` + +- [ ] **Step 1: Read Dashboard.jsx fully** + +Understand all existing widgets, data fetching, and state. + +- [ ] **Step 2: Restructure to new layout** + +Replace the existing multi-grid layout with: + +``` +Top row: 4 metric cards (active campaigns, content in pipeline, awaiting approval, published) +Middle left: Pipeline funnel (horizontal bars per stage) +Middle right: My Tasks (assigned items needing action) +Bottom left: Recent Activity feed +Bottom right: Upcoming Deadlines +``` + +- [ ] **Step 3: Build metric cards** + +4 cards using the existing `StatCard` component or a new variant: +- Active Campaigns: `GET /api/campaigns?status=active` count +- Content in Pipeline: `GET /api/content-items` count, grouped by stage +- Awaiting Your Approval: count of items where current user is approver and status is pending +- Published This Period: count of posts with status=published in date range + +- [ ] **Step 4: Build Pipeline Funnel widget** + +Horizontal stacked bars showing count per stage. Each bar is clickable → navigates to `/content/{stage}`. + +```jsx +function PipelineFunnel({ stageCounts }) { + const maxCount = Math.max(...Object.values(stageCounts), 1) + return ( +
+ {STAGES.map(({ key, label, color }) => ( + + {label} + + ) +} +``` + +- [ ] **Step 5: Build My Tasks widget** + +Aggregates: content items assigned to user, pending approvals, tasks, issues. Show as a simple list with type icon + title + urgency indicator. + +- [ ] **Step 6: Build Recent Activity widget** + +There is no `/api/activity` endpoint. Compose the feed client-side from recent items across entities: +- Fetch recent posts (sorted by UpdatedAt, limit 5) +- Fetch recent content items (sorted by UpdatedAt, limit 5) +- Fetch recent issues (sorted by updated_at, limit 5) +- Merge, sort by date descending, take top 10 +- Display: avatar + "User updated Post Title" + relative timestamp + +Alternative (simpler): Skip this widget for now and add a `GET /api/activity` server endpoint later. Show a placeholder "Activity feed — coming soon" instead. **Ask the user which approach they prefer during implementation.** + +- [ ] **Step 7: Build Upcoming Deadlines widget** + +Items with due dates approaching. Color-coded: red (overdue), amber (this week), gray (later). + +- [ ] **Step 8: Add i18n keys** + +```json +"dashboard.activeCampaigns": "Active Campaigns", +"dashboard.contentInPipeline": "Content in Pipeline", +"dashboard.awaitingApproval": "Awaiting Your Approval", +"dashboard.publishedThisPeriod": "Published This Period", +"dashboard.pipelineOverview": "Pipeline Overview", +"dashboard.myTasks": "My Tasks", +"dashboard.recentActivity": "Recent Activity", +"dashboard.upcomingDeadlines": "Upcoming Deadlines" +``` + +(+ Arabic equivalents) + +- [ ] **Step 9: Test dashboard loads with all widgets** + +- [ ] **Step 10: Commit** + +```bash +git add client/src/pages/Dashboard.jsx client/src/i18n/en.json client/src/i18n/ar.json +git commit -m "feat: redesign dashboard — pipeline funnel, my tasks, activity feed, deadlines" +``` + +--- + +## Chunk 6: Phase 6 — Premium Polish + +### Task 6.1: Enhanced Animations & Transitions + +**Files:** +- Modify: `client/src/index.css` + +- [ ] **Step 1: Add spring-like bezier curves** + +```css +:root { + --spring-bounce: cubic-bezier(0.34, 1.56, 0.64, 1); + --spring-smooth: cubic-bezier(0.22, 1, 0.36, 1); +} +``` + +- [ ] **Step 2: Add new keyframes** + +```css +@keyframes slideInRight { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +@keyframes countUp { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes barGrow { + from { width: 0; } +} + +@keyframes fadeSlideUp { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +``` + +- [ ] **Step 3: Add prefers-reduced-motion override** + +```css +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} +``` + +- [ ] **Step 4: Enhance existing card-hover** + +```css +.card-hover { + transition: transform 0.2s var(--spring-smooth), box-shadow 0.2s ease; +} +.card-hover:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px -5px rgba(0,0,0,0.1), 0 0 0 1px rgba(79,70,229,0.08); +} +``` + +- [ ] **Step 5: Add button press effect** + +```css +.btn-press { + transition: transform 0.1s ease; +} +.btn-press:hover { transform: scale(1.02); } +.btn-press:active { transform: scale(0.98); } +``` + +- [ ] **Step 6: Add tab indicator slide** + +```css +.tab-indicator { + transition: left 0.3s var(--spring-bounce), width 0.3s var(--spring-bounce); +} +``` + +- [ ] **Step 7: Commit** + +```bash +git add client/src/index.css +git commit -m "feat: premium animations — spring curves, card hover, button press, tab slide" +``` + +### Task 6.2: Glass Effects & Visual Depth + +**Files:** +- Modify: `client/src/index.css` +- Modify: `client/src/components/SlidePanel.jsx` +- Modify: `client/src/components/Modal.jsx` +- Modify: `client/src/components/StatusBadge.jsx` + +- [ ] **Step 1: SlidePanel — frosted glass header** + +Update SlidePanel so the header section has: +```css +backdrop-filter: blur(20px); +background: rgba(255, 255, 255, 0.85); +``` +(Dark mode: `rgba(21, 21, 30, 0.85)`) + +Make header sticky (`sticky top-0 z-10`) so it stays visible while content scrolls. + +- [ ] **Step 2: Modal — deeper backdrop blur** + +Update Modal backdrop from `backdrop-blur-sm` to `backdrop-blur-md` (8px → 12px). + +- [ ] **Step 3: StatusBadge — glass morphism** + +Add a `.badge-glass` variant class: +```css +.badge-glass { + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.15); +} +``` + +Apply to StatusBadge in dark mode. + +- [ ] **Step 4: Add layered shadows for depth** + +```css +.shadow-layered { + box-shadow: + 0 1px 2px rgba(0,0,0,0.04), + 0 4px 8px rgba(0,0,0,0.04), + 0 16px 32px rgba(0,0,0,0.04); +} +``` + +- [ ] **Step 5: Commit** + +```bash +git add client/src/index.css client/src/components/SlidePanel.jsx client/src/components/Modal.jsx client/src/components/StatusBadge.jsx +git commit -m "feat: glass effects — frosted panel header, deeper blur, layered shadows" +``` + +### Task 6.3: Typography & Spacing Refinement + +**Files:** +- Modify: `client/src/index.css` +- Modify: `client/src/components/PageHeader.jsx` + +- [ ] **Step 1: Update page title typography** + +In PageHeader, update the h1: +```jsx +

{title}

+``` + +- [ ] **Step 2: Add tabular-nums utility** + +In index.css: +```css +.tabular-nums { font-variant-numeric: tabular-nums; } +``` + +Apply to all metric/count displays in Dashboard, KanbanBoard column counts, etc. + +- [ ] **Step 3: Enforce text-text-secondary for all secondary text** + +Search for any raw `text-gray-*` classes used for secondary text and replace with `text-text-secondary`. This ensures dark mode consistency. + +- [ ] **Step 4: Commit** + +```bash +git add client/src/index.css client/src/components/PageHeader.jsx +git commit -m "feat: typography refinement — lighter titles, tabular nums, consistent text colors" +``` + +### Task 6.4: Premium Empty States + +**Files:** +- Modify: `client/src/components/EmptyState.jsx` + +- [ ] **Step 1: Enhance EmptyState with gradient icon background** + +Update the icon container in the full (non-compact) variant: + +```jsx +
+ +
+``` + +- [ ] **Step 2: Update copy tone** + +Add a `friendlyCopy` prop or update existing description strings across all pages to use warmer language. Example: "No artefacts found" → "No designs yet — create your first one to get started." + +- [ ] **Step 3: Add subtle entrance animation** + +```jsx +
+``` + +- [ ] **Step 4: Commit** + +```bash +git add client/src/components/EmptyState.jsx +git commit -m "feat: premium empty states — gradient icon, warmer copy, entrance animation" +``` + +### Task 6.5: Dashboard Widget Animations + +**Files:** +- Modify: `client/src/pages/Dashboard.jsx` + +- [ ] **Step 1: Add count-up animation to metric cards** + +Use a simple `useCountUp` hook: +```jsx +function useCountUp(target, duration = 800) { + const [value, setValue] = useState(0) + useEffect(() => { + if (!target) return + const start = Date.now() + const tick = () => { + const progress = Math.min((Date.now() - start) / duration, 1) + setValue(Math.round(progress * target)) + if (progress < 1) requestAnimationFrame(tick) + } + requestAnimationFrame(tick) + }, [target, duration]) + return value +} +``` + +- [ ] **Step 2: Add staggered fade-in to activity feed items** + +```jsx +{activities.map((item, i) => ( +
+ ... +
+))} +``` + +- [ ] **Step 3: Add bar grow animation to pipeline funnel** + +```jsx +
+``` + +- [ ] **Step 4: Commit** + +```bash +git add client/src/pages/Dashboard.jsx +git commit -m "feat: dashboard widget animations — count-up, staggered feed, bar grow" +``` + +--- + +## Execution Notes + +- **Phase order matters loosely:** Phase 1 should go first (nav reorg creates the Content route). Phases 2-5 can be done in any order after Phase 1. Phase 6 (polish) should go last. +- **Each commit is independently deployable** — the app works after every commit. +- **Test after every task** — run `npm run dev` and verify the changes visually. +- **NocoDB columns auto-create on restart** — after any server.js schema change, restart the server and check logs. +- **i18n:** Always add keys to both `en.json` and `ar.json` in the same commit.