Files
marketing-app/docs/superpowers/plans/2026-03-11-ux-ui-overhaul.md
T
fahed 3c857856c5 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 <noreply@anthropic.com>
2026-03-11 17:38:45 +03:00

52 KiB

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:

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:

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'
    ? <div key={`d-${i}`} className="border-t border-white/6 my-2 mx-3" />
    : 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:

const navLink = ({ to, icon: Icon, labelKey, end, hint }) => (
  <NavLink
    key={to}
    to={to}
    end={end}
    className={({ isActive }) =>
      `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'
      }`
    }
  >
    <Icon className="w-5 h-5 shrink-0" />
    {!collapsed && (
      <span className="animate-fade-in whitespace-nowrap flex-1">{t(labelKey)}</span>
    )}
    {!collapsed && hint && (
      <span className="text-[10px] text-text-on-dark-muted/50 animate-fade-in">{t(hint)}</span>
    )}
  </NavLink>
)
  • 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:

"nav.content": "Content",
"nav.projectsHint": "+ tasks",
"nav.financeHint": "+ budgets"

In ar.json, add:

"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

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 <Route path="/" element={<Layout />}> block, add redirects for removed routes:

{/* Redirects for old routes */}
<Route path="/posts" element={<Navigate to="/content/posts" replace />} />
<Route path="/calendar" element={<Navigate to="/content/posts?view=calendar" replace />} />
<Route path="/translations" element={<Navigate to="/content/translations" replace />} />
<Route path="/artefacts" element={<Navigate to="/content/design" replace />} />
<Route path="/assets" element={<Navigate to="/settings?tab=assets" replace />} />
<Route path="/brands" element={<Navigate to="/settings?tab=brands" replace />} />
<Route path="/tasks" element={<Navigate to="/projects?tab=tasks" replace />} />
<Route path="/budgets" element={<Navigate to="/finance?tab=budgets" replace />} />
  • Step 2: Add Content route placeholder

Add a route for the Content page (will be a placeholder until Phase 2):

const Content = lazy(() => import('./pages/Content'))

// Inside protected routes — add BEFORE the redirect routes:
<Route path="/content" element={<Suspense fallback={<PageLoader />}><Content /></Suspense>} />
<Route path="/content/:tab" element={<Suspense fallback={<PageLoader />}><Content /></Suspense>} />

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:

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 (
    <div className="space-y-6">
      <h1 className="text-2xl font-semibold text-text-primary">{t('nav.content')}</h1>
      <div className="flex gap-1 border-b border-border">
        {TABS.map(tabKey => (
          <a
            key={tabKey}
            href={`/content/${tabKey}`}
            className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
              tab === tabKey
                ? 'border-brand-primary text-brand-primary'
                : 'border-transparent text-text-secondary hover:text-text-primary'
            }`}
          >
            {tabKey.charAt(0).toUpperCase() + tabKey.slice(1)}
          </a>
        ))}
      </div>
      <p className="text-text-secondary">Content pipeline  coming soon.</p>
    </div>
  )
}
  • Step 4: Update Header page title map

In client/src/components/Header.jsx, find the PAGE_TITLE_KEYS map and add:

'/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
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":

const [searchParams, setSearchParams] = useSearchParams()
const activeTab = searchParams.get('tab') || 'dashboard'

Render tabs:

<div className="flex gap-1 border-b border-border mb-6">
  <button
    onClick={() => setSearchParams({})}
    className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
      activeTab === 'dashboard'
        ? 'border-brand-primary text-brand-primary'
        : 'border-transparent text-text-secondary hover:text-text-primary'
    }`}
  >
    {t('nav.financeDashboard')}
  </button>
  <button
    onClick={() => setSearchParams({ tab: 'budgets' })}
    className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
      activeTab === 'budgets'
        ? 'border-brand-primary text-brand-primary'
        : 'border-transparent text-text-secondary hover:text-text-primary'
    }`}
  >
    {t('nav.budgets')}
  </button>
</div>
  • Step 3: Conditionally render content

Wrap the existing Finance dashboard content in {activeTab === 'dashboard' && (...)}.

Import the Budgets page component and render: {activeTab === 'budgets' && <Budgets />}.

Alternatively, if Budgets is too large to import directly, lazy-load it.

  • Step 4: Test both tabs work

  • Step 5: Commit

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.

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"

"settings.general": "General"  // en
"settings.general": "عام"      // ar
  • Step 5: Test all three tabs

  • Step 6: Commit

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
"projects.allTasks": "All Tasks"  // en
"projects.allTasks": "جميع المهام"  // ar
  • Step 5: Test both tabs

  • Step 6: Commit

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

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 (
    <div className="flex items-center gap-3 flex-wrap">
      <h1 className="text-2xl font-semibold text-text-primary flex-shrink-0">{title}</h1>
      <div className="flex-1" />

      {onSearchChange && (
        searchOpen ? (
          <div className="relative">
            <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-tertiary" />
            <input
              type="text"
              value={searchTerm}
              onChange={e => 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
            />
            <button
              onClick={() => { onSearchChange(''); setSearchOpen(false) }}
              className="absolute right-2 top-1/2 -translate-y-1/2 text-text-tertiary hover:text-text-primary"
            >
              <X className="w-3.5 h-3.5" />
            </button>
          </div>
        ) : (
          <button
            onClick={() => setSearchOpen(true)}
            className="p-2 text-text-secondary hover:text-text-primary hover:bg-surface-secondary rounded-lg transition-colors"
          >
            <Search className="w-4.5 h-4.5" />
          </button>
        )
      )}

      {filters}
      {viewToggle}
      {actions}
    </div>
  )
}
  • Step 2: Adopt PageHeader in Issues.jsx as first migration

Replace the existing header JSX in Issues.jsx with <PageHeader>, 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
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:

<div className="flex items-center gap-3 px-6 py-4 border-b border-border">
  <button onClick={onClose} className="p-1 hover:bg-surface-secondary rounded-lg">
    <ArrowLeft className="w-5 h-5" />
  </button>
  <h2 className="text-lg font-semibold text-text-primary flex-1 truncate">{title}</h2>
  <button onClick={onSave} className="px-3 py-1.5 text-sm bg-brand-primary text-white rounded-lg hover:bg-brand-primary-light">
    {t('common.save')}
  </button>
  <OverflowMenu items={[{ label: t('common.delete'), onClick: onDelete, danger: true }]} />
</div>
  • 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:

// 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 (
    <div className="relative" ref={ref}>
      <button
        onClick={() => setOpen(!open)}
        className="p-1.5 hover:bg-surface-secondary rounded-lg transition-colors"
      >
        <MoreVertical className="w-4.5 h-4.5 text-text-secondary" />
      </button>
      {open && (
        <div className="absolute right-0 top-full mt-1 bg-surface-primary border border-border rounded-lg shadow-lg py-1 min-w-[160px] z-50 animate-scale-in">
          {items.map((item, i) => (
            <button
              key={i}
              onClick={() => { item.onClick(); setOpen(false) }}
              className={`w-full text-left px-3 py-2 text-sm hover:bg-surface-secondary transition-colors ${
                item.danger ? 'text-red-600 hover:bg-red-50' : 'text-text-primary'
              }`}
            >
              {item.label}
            </button>
          ))}
        </div>
      )}
    </div>
  )
}
  • 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

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

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 (
    <div className="space-y-6">
      {/* Tab bar */}
      <div className="flex gap-1 border-b border-border">
        {TABS.map(({ key, labelKey, icon: Icon }) => (
          <button
            key={key}
            onClick={() => navigate(`/content/${key}`)}
            className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
              tab === key
                ? 'border-brand-primary text-brand-primary'
                : 'border-transparent text-text-secondary hover:text-text-primary hover:border-border'
            }`}
          >
            <Icon className="w-4 h-4" />
            {t(labelKey)}
          </button>
        ))}
      </div>

      {/* Tab content */}
      {tab === 'pipeline' && <PipelineTab />}
      {tab === 'copy' && <CopyTab />}
      {tab === 'translations' && <TranslationsTab />}
      {tab === 'design' && <DesignTab />}
      {tab === 'posts' && <PostsTab />}
    </div>
  )
}

// Placeholder sub-components — replaced in subsequent tasks
function PipelineTab() {
  return <p className="text-text-secondary">Pipeline view  coming in Phase 3.</p>
}

function CopyTab() {
  return <p className="text-text-secondary">Copy tab  loading existing translations (originals).</p>
}

function TranslationsTab() {
  return <p className="text-text-secondary">Translations tab  loading.</p>
}

function DesignTab() {
  return <p className="text-text-secondary">Design tab  loading artefacts.</p>
}

function PostsTab() {
  return <p className="text-text-secondary">Posts tab  loading.</p>
}
  • Step 2: Add i18n keys

In en.json:

"content.pipeline": "Pipeline",
"content.copy": "Copy",
"content.translations": "Translations",
"content.design": "Design",
"content.posts": "Posts"

In ar.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
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:

import PostProduction from './PostProduction'

function PostsTab() {
  const [searchParams] = useSearchParams()
  const defaultView = searchParams.get('view')
  return <PostProduction defaultView={defaultView} />
}

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

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
import Translations from './Translations'

function CopyTab() {
  return <Translations filterOriginal={true} embedded />
}

function TranslationsTab() {
  return <Translations filterOriginal={false} embedded />
}
  • Step 4: Test both tabs show data

  • Step 5: Commit

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:

import Artefacts from './Artefacts'

function DesignTab() {
  return <Artefacts embedded />
}
  • 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

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):

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:

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

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.

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

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) => (
    <div
      onClick={() => onSelect(item)}
      className="bg-surface-primary border border-border rounded-lg p-3 cursor-pointer card-hover"
    >
      <h4 className="text-sm font-medium text-text-primary truncate">{item.title}</h4>
      <div className="flex items-center gap-2 mt-2">
        {item.brand_name && (
          <span className="text-xs px-2 py-0.5 bg-surface-tertiary rounded-full text-text-secondary">
            {item.brand_name}
          </span>
        )}
        {item.assignee_name && (
          <span className="text-xs text-text-tertiary ml-auto">{item.assignee_name}</span>
        )}
      </div>
    </div>
  )

  return (
    <KanbanBoard
      columns={columns}
      items={items}
      renderCard={renderCard}
      getItemId={(item) => 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):

// 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):

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
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 <SkeletonKanbanBoard />

  return <ContentPipelineBoard items={items} onMove={handleMove} onSelect={() => {}} />
}
  • Step 3: Add i18n keys
"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

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:

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 (
    <div className="flex items-center gap-1 px-6 py-3 bg-surface-secondary border-b border-border">
      {STAGES.map((stage, i) => {
        const stageIndex = STAGES.indexOf(currentStage)
        const isDone = i < stageIndex
        const isCurrent = i === stageIndex
        return (
          <div key={stage} className="flex items-center gap-1">
            {i > 0 && <span className="text-text-tertiary mx-1"></span>}
            <span className={`flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full ${
              isDone ? 'bg-emerald-100 text-emerald-700' :
              isCurrent ? 'bg-brand-primary/10 text-brand-primary' :
              'bg-surface-tertiary text-text-tertiary'
            }`}>
              {isDone ? <Check className="w-3 h-3" /> : isCurrent ? <Clock className="w-3 h-3" /> : <Circle className="w-3 h-3" />}
              {t(`content.${stage}`)}
            </span>
          </div>
        )
      })}
    </div>
  )
}

export default function ContentDetailPanel({ item, onClose, onSave, onDelete }) {
  const { t } = useLanguage()

  if (!item) return null

  const header = (
    <div className="flex items-center gap-3 px-6 py-4 border-b border-border">
      <button onClick={onClose} className="p-1 hover:bg-surface-secondary rounded-lg">
        <ArrowLeft className="w-5 h-5" />
      </button>
      <h2 className="text-lg font-semibold text-text-primary flex-1 truncate">{item.title}</h2>
      <button onClick={onSave} className="px-3 py-1.5 text-sm bg-brand-primary text-white rounded-lg hover:bg-brand-primary-light">
        {t('common.save')}
      </button>
      <OverflowMenu items={[{ label: t('common.delete'), onClick: onDelete, danger: true }]} />
    </div>
  )

  return (
    <SlidePanel onClose={onClose} header={header}>
      <StageBreadcrumb currentStage={item.stage} />
      <div className="p-6 space-y-6">
        {/* Title edit */}
        <div>
          <label className="text-sm font-medium text-text-secondary">{t('common.title')}</label>
          <input
            className="w-full mt-1 px-3 py-2 border border-border rounded-lg text-sm"
            defaultValue={item.title}
          />
        </div>
        {/* Campaign + Brand + Assignee selectors would go here */}
        {/* Linked items per stage would go here — Phase 3 continued */}
      </div>
    </SlidePanel>
  )
}
  • 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

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

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

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

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}.

function PipelineFunnel({ stageCounts }) {
  const maxCount = Math.max(...Object.values(stageCounts), 1)
  return (
    <div className="space-y-3">
      {STAGES.map(({ key, label, color }) => (
        <a key={key} href={`/content/${key}`} className="flex items-center gap-3 group">
          <span className="text-xs font-medium text-text-secondary w-20">{label}</span>
          <div className="flex-1 bg-surface-tertiary rounded-full h-6 overflow-hidden">
            <div
              className="h-full rounded-full transition-all duration-700"
              style={{
                width: `${(stageCounts[key] || 0) / maxCount * 100}%`,
                backgroundColor: color,
              }}
            />
          </div>
          <span className="text-sm font-medium text-text-primary w-8 text-right tabular-nums">
            {stageCounts[key] || 0}
          </span>
        </a>
      ))}
    </div>
  )
}
  • 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
"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

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

: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
@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
@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
.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
.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
.tab-indicator {
  transition: left 0.3s var(--spring-bounce), width 0.3s var(--spring-bounce);
}
  • Step 7: Commit
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:

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:

.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
.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
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:

<h1 className="text-3xl font-light tracking-tight text-text-primary">{title}</h1>
  • Step 2: Add tabular-nums utility

In index.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
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:

<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-brand-primary/10 via-brand-secondary/5 to-transparent flex items-center justify-content shadow-sm">
  <Icon className="w-10 h-10 text-brand-primary/60" />
</div>
  • 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
<div className="animate-fade-in text-center py-16">
  • Step 4: Commit
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:

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
{activities.map((item, i) => (
  <div
    key={item.id}
    className="animate-fade-in"
    style={{ animationDelay: `${i * 50}ms`, animationFillMode: 'backwards' }}
  >
    ...
  </div>
))}
  • Step 3: Add bar grow animation to pipeline funnel
<div
  className="h-full rounded-full"
  style={{
    width: `${percentage}%`,
    backgroundColor: color,
    animation: 'barGrow 0.7s var(--spring-smooth) forwards',
    animationDelay: `${i * 100}ms`,
  }}
/>
  • Step 4: Commit
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.