diff --git a/docs/30-FRONTEND/PAGE-REQUIREMENTS.md b/docs/30-FRONTEND/PAGE-REQUIREMENTS.md new file mode 100644 index 00000000..435b1f79 --- /dev/null +++ b/docs/30-FRONTEND/PAGE-REQUIREMENTS.md @@ -0,0 +1,179 @@ +# Page Requirements - Site & Sector Selectors + +This document outlines all pages in the application and their requirements for site/sector selectors. + +## Legend +- **Site Selector**: Whether the page needs a site selector dropdown +- **Sector Selector**: Whether the page needs a sector selector dropdown +- **Implementation**: How the selectors should behave on this page +- **Next Action**: Recommended workflow guidance for Planner/Writer pages + +--- + +## Planner Module (Content Planning) + +| Page | Route | Site Selector | Sector Selector | Implementation | Recommended Next Action | +|------|-------|---------------|-----------------|----------------|-------------------------| +| Keywords | `/planner/keywords` | ✅ Required | ✅ Required | Filter keywords by site/sector | `{count} selected → Auto-Cluster` OR `{clustered} clustered → Generate Ideas` | +| Clusters | `/planner/clusters` | ✅ Required | ✅ Required | Filter clusters by site/sector | `{count} selected → Expand Clusters` OR `{ready} ready → Generate Ideas` | +| Cluster Detail | `/planner/clusters/:id` | ✅ Read-only | ✅ Read-only | Display only (inherited from cluster) | `Back to Clusters` OR `Generate Ideas from Cluster` | +| Ideas | `/planner/ideas` | ✅ Required | ✅ Required | Filter ideas by site/sector | `{count} selected → Create Tasks` OR `{approved} approved → Create Tasks` | + +--- + +## Writer Module (Content Creation) + +| Page | Route | Site Selector | Sector Selector | Implementation | Recommended Next Action | +|------|-------|---------------|-----------------|----------------|-------------------------| +| Tasks | `/writer/tasks` | ✅ Required | ✅ Required | Filter tasks by site/sector | `{count} selected → Generate Content` OR `{ready} ready → Generate Content` | +| Content | `/writer/content` | ✅ Required | ✅ Required | Filter content by site/sector | `{count} selected → Generate Images` OR `{draft} drafts → Add Images` | +| Images | `/writer/images` | ✅ Required | ✅ Required | Filter images by site/sector | `{count} selected → Submit for Review` OR `{ready} ready → Submit for Review` | +| Review | `/writer/review` | ✅ Required | ✅ Required | Filter review items by site/sector | `{count} selected → Publish Selected` OR `{approved} approved → Publish All` | +| Published | `/writer/published` | ✅ Required | ✅ Required | Filter published items by site/sector | `{count} selected → Sync to WordPress` OR `View All Sites` | + +--- + +## Linker Module (Internal Linking) + +| Page | Route | Site Selector | Sector Selector | Implementation | Recommended Next Action | +|------|-------|---------------|-----------------|----------------|-------------------------| +| Content List | `/linker/content` | ✅ Required | ✅ Optional | Filter content by site/sector | N/A | + +--- + +## Optimizer Module (Content Optimization) + +| Page | Route | Site Selector | Sector Selector | Implementation | Recommended Next Action | +|------|-------|---------------|-----------------|----------------|-------------------------| +| Content Selector | `/optimizer/content` | ✅ Required | ✅ Optional | Filter content for optimization | N/A | +| Analysis Preview | `/optimizer/analyze/:id` | ✅ Read-only | ❌ Not needed | Display only (inherited from content) | N/A | + +--- + +## Thinker Module (AI Configuration) - Admin Only + +| Page | Route | Site Selector | Sector Selector | Implementation | Recommended Next Action | +|------|-------|---------------|-----------------|----------------|-------------------------| +| Prompts | `/thinker/prompts` | ❌ Global | ❌ Global | System-wide prompts | N/A | +| Author Profiles | `/thinker/author-profiles` | ❌ Global | ❌ Global | System-wide profiles | N/A | +| Strategies | `/thinker/strategies` | ❌ Global | ❌ Global | System-wide strategies | N/A | +| Image Testing | `/thinker/image-testing` | ❌ Global | ❌ Global | Testing interface | N/A | + +--- + +## Sites Module (Site Management) + +| Page | Route | Site Selector | Sector Selector | Implementation | Recommended Next Action | +|------|-------|---------------|-----------------|----------------|-------------------------| +| Site List | `/sites` | ❌ Shows all | ❌ Not applicable | Lists all sites | N/A | +| Site Dashboard | `/sites/:id` | ✅ Read-only | ❌ Not needed | Inherited from route param | N/A | +| Site Content | `/sites/:id/content` | ✅ Read-only | ✅ Optional | Filter by sector within site | N/A | +| Page Manager | `/sites/:id/pages` | ✅ Read-only | ❌ Not needed | Inherited from route param | N/A | +| Site Settings | `/sites/:id/settings` | ✅ Read-only | ❌ Not needed | Inherited from route param | N/A | +| Sync Dashboard | `/sites/:id/sync` | ✅ Read-only | ❌ Not needed | Inherited from route param | N/A | +| Deployment Panel | `/sites/:id/deploy` | ✅ Read-only | ❌ Not needed | Inherited from route param | N/A | + +--- + +## Account & Billing + +| Page | Route | Site Selector | Sector Selector | Implementation | Recommended Next Action | +|------|-------|---------------|-----------------|----------------|-------------------------| +| Account Settings | `/account/settings` | ❌ Not needed | ❌ Not needed | Account-level settings | N/A | +| Plans & Billing | `/account/plans` | ❌ Not needed | ❌ Not needed | Account-level billing | N/A | +| Purchase Credits | `/account/purchase-credits` | ❌ Not needed | ❌ Not needed | Account-level purchase | N/A | +| Usage Analytics | `/account/usage` | ✅ Optional | ❌ Not needed | Filter usage by site | N/A | +| Content Settings | `/account/content-settings` | ❌ Not needed | ❌ Not needed | Account-level settings | N/A | + +--- + +## Settings (Admin) + +| Page | Route | Site Selector | Sector Selector | Implementation | Recommended Next Action | +|------|-------|---------------|-----------------|----------------|-------------------------| +| General Settings | `/settings` | ❌ Not needed | ❌ Not needed | System-wide settings | N/A | +| Users | `/settings/users` | ❌ Not needed | ❌ Not needed | User management | N/A | +| Subscriptions | `/settings/subscriptions` | ❌ Not needed | ❌ Not needed | Subscription management | N/A | +| System | `/settings/system` | ❌ Not needed | ❌ Not needed | System configuration | N/A | +| Account | `/settings/account` | ❌ Not needed | ❌ Not needed | Account settings | N/A | +| AI Settings | `/settings/ai` | ❌ Not needed | ❌ Not needed | AI model configuration | N/A | +| Plans | `/settings/plans` | ❌ Not needed | ❌ Not needed | Plan management | N/A | +| Industries | `/settings/industries` | ❌ Not needed | ❌ Not needed | Industry reference data | N/A | +| Integration | `/settings/integration` | ❌ Not needed | ❌ Not needed | API integrations | N/A | +| Publishing | `/settings/publishing` | ❌ Not needed | ❌ Not needed | Publishing defaults | N/A | +| Sites | `/settings/sites` | ❌ Not needed | ❌ Not needed | Site configuration | N/A | + +--- + +## Reference Data + +| Page | Route | Site Selector | Sector Selector | Implementation | Recommended Next Action | +|------|-------|---------------|-----------------|----------------|-------------------------| +| Seed Keywords | `/reference/seed-keywords` | ✅ Optional | ✅ Optional | Filter reference keywords | N/A | +| Industries | `/reference/industries` | ❌ Not needed | ❌ Not needed | Global reference data | N/A | + +--- + +## Setup + +| Page | Route | Site Selector | Sector Selector | Implementation | Recommended Next Action | +|------|-------|---------------|-----------------|----------------|-------------------------| +| Add Keywords | `/setup/add-keywords` | ✅ Required | ✅ Required | Target site/sector for import | N/A | + +--- + +## Other Pages + +| Page | Route | Site Selector | Sector Selector | Implementation | Recommended Next Action | +|------|-------|---------------|-----------------|----------------|-------------------------| +| Home Dashboard | `/` | ✅ Optional | ❌ Not needed | Overview all sites or filter | N/A | +| Help | `/help` | ❌ Not needed | ❌ Not needed | Documentation | N/A | +| Components | `/components` | ❌ Not needed | ❌ Not needed | Design system showcase | N/A | + +--- + +## Implementation Notes + +### Site Selector Behavior +- **Required**: User must select a site before content is displayed +- **Optional**: Shows all sites by default, can filter to specific site +- **Read-only**: Shows the current site but cannot be changed (inherited from route/context) +- **Not needed**: Page operates at account/system level + +### Sector Selector Behavior +- **Required**: User must select both site and sector +- **Optional**: Shows all sectors by default within selected site +- **Read-only**: Shows current sector but cannot be changed +- **Not needed**: Page doesn't operate at sector level + +### Next Action Patterns (Planner/Writer) +The next action button should follow this pattern: +1. If items are selected → Action on selected items +2. If no selection but ready items exist → Workflow progression action +3. If nothing actionable → Hide or disable + +Example: +```tsx +nextAction={selectedIds.length > 0 ? { + label: 'Process Selected', + message: `${selectedIds.length} items selected`, + onClick: handleBulkAction, +} : workflowStats.ready > 0 ? { + label: 'Continue to Next Step', + href: '/next/page', + message: `${workflowStats.ready} items ready`, +} : undefined} +``` + +--- + +## Workflow Pipeline (Planner → Writer) + +``` +Keywords → Clusters → Ideas → Tasks → Content → Images → Review → Published + ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ +Cluster Expand Create Generate Generate Submit Publish Sync to +Keywords Ideas Tasks Content Images Review Content WordPress +``` + +Each page's "next action" guides users through this pipeline. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a6c06853..d8a19896 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,6 +7,7 @@ import ProtectedRoute from "./components/auth/ProtectedRoute"; import AdminRoute from "./components/auth/AdminRoute"; import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay"; import LoadingStateMonitor from "./components/common/LoadingStateMonitor"; +import { PageProvider } from "./context/PageContext"; import { useAuthStore } from "./store/authStore"; import { useModuleStore } from "./store/moduleStore"; @@ -118,7 +119,7 @@ export default function App() { }, [loadModuleSettings]); return ( - <> + @@ -265,6 +266,6 @@ export default function App() { - + ); } diff --git a/frontend/src/components/common/PageHeader.tsx b/frontend/src/components/common/PageHeader.tsx index 133e9863..b0b3be25 100644 --- a/frontend/src/components/common/PageHeader.tsx +++ b/frontend/src/components/common/PageHeader.tsx @@ -1,25 +1,27 @@ /** * Standardized Page Header Component - * Simplified version - Site/sector selector moved to AppHeader - * Just shows: breadcrumb (inline), page title with badge, description + * Simplified version - Title shown in AppHeader + * Shows: page title with parent badge, description */ -import React, { ReactNode, useEffect, useRef } from 'react'; +import React, { ReactNode, useEffect, useRef, useMemo } from 'react'; import { useSiteStore } from '../../store/siteStore'; import { useSectorStore } from '../../store/sectorStore'; import { trackLoading } from './LoadingStateMonitor'; import { useErrorHandler } from '../../hooks/useErrorHandler'; +import { usePageContext } from '../../context/PageContext'; interface PageHeaderProps { title: string; description?: string; - breadcrumb?: string; + parent?: string; // Parent module name (e.g., "Planner", "Writer") + breadcrumb?: string; // Deprecated - use parent instead lastUpdated?: Date; showRefresh?: boolean; onRefresh?: () => void; className?: string; badge?: { icon: ReactNode; - color: 'blue' | 'green' | 'purple' | 'orange' | 'red' | 'indigo'; + color: 'blue' | 'green' | 'purple' | 'orange' | 'red' | 'indigo' | 'yellow' | 'pink' | 'emerald' | 'cyan' | 'amber' | 'teal'; }; hideSiteSector?: boolean; navigation?: ReactNode; // Kept for backwards compat but not rendered @@ -31,7 +33,8 @@ interface PageHeaderProps { export default function PageHeader({ title, description, - breadcrumb, + parent, + breadcrumb, // Deprecated alias for parent lastUpdated, showRefresh = false, onRefresh, @@ -43,8 +46,19 @@ export default function PageHeader({ const { activeSite } = useSiteStore(); const { loadSectorsForSite } = useSectorStore(); const { addError } = useErrorHandler('PageHeader'); + const { setPageInfo } = usePageContext(); const lastSiteId = useRef(null); const isLoadingSector = useRef(false); + + // Resolve parent from either prop + const parentModule = parent || breadcrumb; + + // Update page context with title and badge info for AppHeader + const pageInfoKey = useMemo(() => `${title}|${parentModule}`, [title, parentModule]); + useEffect(() => { + setPageInfo({ title, parent: parentModule, badge }); + return () => setPageInfo(null); + }, [pageInfoKey, badge?.color]); // Load sectors when active site changes useEffect(() => { @@ -83,56 +97,27 @@ export default function PageHeader({ }, [activeSite?.id, activeSite?.is_active, hideSiteSector, loadSectorsForSite, addError]); const badgeColors = { - blue: 'bg-blue-600 dark:bg-blue-500', - green: 'bg-green-600 dark:bg-green-500', - purple: 'bg-purple-600 dark:bg-purple-500', - orange: 'bg-orange-600 dark:bg-orange-500', - red: 'bg-red-600 dark:bg-red-500', - indigo: 'bg-indigo-600 dark:bg-indigo-500', + blue: { bg: 'bg-blue-600 dark:bg-blue-500', light: 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300' }, + green: { bg: 'bg-green-600 dark:bg-green-500', light: 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-300' }, + purple: { bg: 'bg-purple-600 dark:bg-purple-500', light: 'bg-purple-100 text-purple-700 dark:bg-purple-500/20 dark:text-purple-300' }, + orange: { bg: 'bg-orange-600 dark:bg-orange-500', light: 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-300' }, + red: { bg: 'bg-red-600 dark:bg-red-500', light: 'bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-300' }, + indigo: { bg: 'bg-indigo-600 dark:bg-indigo-500', light: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300' }, + yellow: { bg: 'bg-yellow-600 dark:bg-yellow-500', light: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-500/20 dark:text-yellow-300' }, + pink: { bg: 'bg-pink-600 dark:bg-pink-500', light: 'bg-pink-100 text-pink-700 dark:bg-pink-500/20 dark:text-pink-300' }, + emerald: { bg: 'bg-emerald-600 dark:bg-emerald-500', light: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300' }, + cyan: { bg: 'bg-cyan-600 dark:bg-cyan-500', light: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-500/20 dark:text-cyan-300' }, + amber: { bg: 'bg-amber-600 dark:bg-amber-500', light: 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300' }, + teal: { bg: 'bg-teal-600 dark:bg-teal-500', light: 'bg-teal-100 text-teal-700 dark:bg-teal-500/20 dark:text-teal-300' }, }; return ( -
- {/* Left: Breadcrumb + Badge + Title */} -
- {breadcrumb && ( - <> - {breadcrumb} - / - - )} - {badge && ( -
- {badge.icon && typeof badge.icon === 'object' && 'type' in badge.icon - ? React.cloneElement(badge.icon as React.ReactElement, { className: 'text-white size-4' }) - : badge.icon} -
- )} -
-

{title}

- {description && ( -

{description}

- )} -
-
- - {/* Right: Actions */} -
- {lastUpdated && ( - - Updated {lastUpdated.toLocaleTimeString()} - - )} - {showRefresh && onRefresh && ( - - )} - {actions} -
+
+ {/* Title now shown in AppHeader - this component only triggers the context update */} + {/* Show description if provided - can be used for additional context */} + {description && ( +

{description}

+ )}
); } diff --git a/frontend/src/context/PageContext.tsx b/frontend/src/context/PageContext.tsx index e98aaddf..7615925c 100644 --- a/frontend/src/context/PageContext.tsx +++ b/frontend/src/context/PageContext.tsx @@ -1,15 +1,15 @@ /** * Page Context - Shares current page info with header - * Allows pages to set title, breadcrumb, badge for display in AppHeader + * Allows pages to set title, parent module, badge for display in AppHeader */ import React, { createContext, useContext, useState, ReactNode } from 'react'; interface PageInfo { title: string; - breadcrumb?: string; + parent?: string; // Parent module name (e.g., "Planner", "Writer") badge?: { icon: ReactNode; - color: 'blue' | 'green' | 'purple' | 'orange' | 'red' | 'indigo'; + color: 'blue' | 'green' | 'purple' | 'orange' | 'red' | 'indigo' | 'yellow' | 'pink' | 'emerald' | 'cyan' | 'amber' | 'teal'; }; } @@ -44,5 +44,5 @@ export function usePage(info: PageInfo) { React.useEffect(() => { setPageInfo(info); return () => setPageInfo(null); - }, [info.title, info.breadcrumb]); + }, [info.title, info.parent]); } diff --git a/frontend/src/layout/AppHeader.tsx b/frontend/src/layout/AppHeader.tsx index a4e21aa2..92493e3d 100644 --- a/frontend/src/layout/AppHeader.tsx +++ b/frontend/src/layout/AppHeader.tsx @@ -1,26 +1,34 @@ import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; -import { useSidebar } from "../context/SidebarContext"; +import { usePageContext } from "../context/PageContext"; import { ThemeToggleButton } from "../components/common/ThemeToggleButton"; import NotificationDropdown from "../components/header/NotificationDropdown"; import UserDropdown from "../components/header/UserDropdown"; import { HeaderMetrics } from "../components/header/HeaderMetrics"; import SearchModal from "../components/common/SearchModal"; import SiteAndSectorSelector from "../components/common/SiteAndSectorSelector"; +import React from "react"; + +// Badge color mappings for light versions +const badgeColors: Record = { + blue: { bg: 'bg-blue-600 dark:bg-blue-500', light: 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300' }, + green: { bg: 'bg-green-600 dark:bg-green-500', light: 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-300' }, + purple: { bg: 'bg-purple-600 dark:bg-purple-500', light: 'bg-purple-100 text-purple-700 dark:bg-purple-500/20 dark:text-purple-300' }, + orange: { bg: 'bg-orange-600 dark:bg-orange-500', light: 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-300' }, + red: { bg: 'bg-red-600 dark:bg-red-500', light: 'bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-300' }, + indigo: { bg: 'bg-indigo-600 dark:bg-indigo-500', light: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-300' }, + yellow: { bg: 'bg-yellow-600 dark:bg-yellow-500', light: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-500/20 dark:text-yellow-300' }, + pink: { bg: 'bg-pink-600 dark:bg-pink-500', light: 'bg-pink-100 text-pink-700 dark:bg-pink-500/20 dark:text-pink-300' }, + emerald: { bg: 'bg-emerald-600 dark:bg-emerald-500', light: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300' }, + cyan: { bg: 'bg-cyan-600 dark:bg-cyan-500', light: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-500/20 dark:text-cyan-300' }, + amber: { bg: 'bg-amber-600 dark:bg-amber-500', light: 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300' }, + teal: { bg: 'bg-teal-600 dark:bg-teal-500', light: 'bg-teal-100 text-teal-700 dark:bg-teal-500/20 dark:text-teal-300' }, +}; const AppHeader: React.FC = () => { const [isApplicationMenuOpen, setApplicationMenuOpen] = useState(false); const [isSearchOpen, setIsSearchOpen] = useState(false); - - const { isMobileOpen, toggleSidebar, toggleMobileSidebar } = useSidebar(); - - const handleToggle = () => { - if (window.innerWidth >= 1024) { - toggleSidebar(); - } else { - toggleMobileSidebar(); - } - }; + const { pageInfo } = usePageContext(); const toggleApplicationMenu = () => { setApplicationMenuOpen(!isApplicationMenuOpen); @@ -44,23 +52,6 @@ const AppHeader: React.FC = () => {
- {/* Sidebar Toggle */} - - {/* Mobile Logo */} Logo @@ -77,8 +68,27 @@ const AppHeader: React.FC = () => { + {/* Page Title with Badge - Desktop */} + {pageInfo && ( +
+ {pageInfo.badge && ( +
+ {pageInfo.badge.icon && typeof pageInfo.badge.icon === 'object' && 'type' in pageInfo.badge.icon + ? React.cloneElement(pageInfo.badge.icon as React.ReactElement, { className: 'text-white size-4' }) + : pageInfo.badge.icon} +
+ )} +

{pageInfo.title}

+ {pageInfo.parent && pageInfo.badge && ( + + {pageInfo.parent} + + )} +
+ )} + {/* Site and Sector Selector - Desktop */} -
+
diff --git a/frontend/src/layout/AppSidebar.tsx b/frontend/src/layout/AppSidebar.tsx index 23a0e2d0..270c4feb 100644 --- a/frontend/src/layout/AppSidebar.tsx +++ b/frontend/src/layout/AppSidebar.tsx @@ -37,7 +37,7 @@ type MenuSection = { }; const AppSidebar: React.FC = () => { - const { isExpanded, isMobileOpen, isHovered, setIsHovered } = useSidebar(); + const { isExpanded, isMobileOpen, isHovered, setIsHovered, toggleSidebar } = useSidebar(); const location = useLocation(); const { user, isAuthenticated } = useAuthStore(); const { isModuleEnabled, settings: moduleSettings } = useModuleStore(); @@ -452,6 +452,22 @@ const AppSidebar: React.FC = () => { onMouseEnter={() => !isExpanded && setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > + {/* Collapse/Expand Toggle Button - Attached to border */} + +
{isExpanded || isHovered || isMobileOpen ? ( diff --git a/frontend/src/pages/Planner/Clusters.tsx b/frontend/src/pages/Planner/Clusters.tsx index 2c77e140..47435979 100644 --- a/frontend/src/pages/Planner/Clusters.tsx +++ b/frontend/src/pages/Planner/Clusters.tsx @@ -4,7 +4,6 @@ */ import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; -import { Link } from 'react-router-dom'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchClusters, @@ -390,20 +389,8 @@ export default function Clusters() { <> , color: 'purple' }} - breadcrumb="Planner" - actions={ - - Generate Ideas - - - - - } + parent="Planner" /> 0 ? { + label: 'Generate Ideas', + message: `${selectedIds.length} selected`, + onClick: () => handleBulkAction('generate_ideas', selectedIds), + } : clusters.length > 0 ? { + label: 'Generate Ideas', + href: '/planner/ideas', + message: `${clusters.length} clusters`, + } : undefined} onFilterChange={(key, value) => { const stringValue = value === null || value === undefined ? '' : String(value); if (key === 'search') { diff --git a/frontend/src/pages/Planner/Ideas.tsx b/frontend/src/pages/Planner/Ideas.tsx index 9790c926..f0287ef5 100644 --- a/frontend/src/pages/Planner/Ideas.tsx +++ b/frontend/src/pages/Planner/Ideas.tsx @@ -4,7 +4,6 @@ */ import { useState, useEffect, useMemo, useCallback } from 'react'; -import { Link } from 'react-router-dom'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchContentIdeas, @@ -301,20 +300,8 @@ export default function Ideas() { <> , color: 'yellow' }} - breadcrumb="Planner" - actions={ - - Start Writing - - - - - } + parent="Planner" /> 0 ? { + label: 'Queue to Writer', + message: `${selectedIds.length} selected`, + onClick: () => handleBulkAction('queue_to_writer', selectedIds), + } : ideas.filter(i => i.status === 'approved').length > 0 ? { + label: 'Start Writing', + href: '/writer/queue', + message: `${ideas.filter(i => i.status === 'approved').length} approved`, + } : undefined} onFilterChange={(key, value) => { const stringValue = value === null || value === undefined ? '' : String(value); if (key === 'search') { diff --git a/frontend/src/pages/Planner/Keywords.tsx b/frontend/src/pages/Planner/Keywords.tsx index 77577e90..b098c7cb 100644 --- a/frontend/src/pages/Planner/Keywords.tsx +++ b/frontend/src/pages/Planner/Keywords.tsx @@ -5,7 +5,6 @@ */ import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; -import { Link } from 'react-router-dom'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchKeywords, @@ -507,20 +506,6 @@ export default function Keywords() { }; }, [keywords, totalCount]); - // Determine next step action - const nextStep = useMemo(() => { - if (totalCount === 0) { - return { label: 'Import Keywords', path: '/add-keywords', disabled: false }; - } - if (workflowStats.unclustered >= 5) { - return { label: 'Auto-Cluster', action: 'cluster', disabled: false }; - } - if (workflowStats.clustered > 0) { - return { label: 'Generate Ideas', path: '/planner/ideas', disabled: false }; - } - return { label: 'Add More Keywords', path: '/add-keywords', disabled: false }; - }, [totalCount, workflowStats]); - // Handle create/edit const handleSave = async () => { try { @@ -594,37 +579,8 @@ export default function Keywords() { <> , color: 'green' }} - breadcrumb="Planner" - actions={ -
- - {workflowStats.clustered}/{workflowStats.total} clustered - - {nextStep.path ? ( - - {nextStep.label} - - - - - ) : nextStep.action === 'cluster' ? ( - - ) : null} -
- } + parent="Planner" /> 0 ? { + label: 'Auto-Cluster Selected', + message: `${selectedIds.length} selected`, + onClick: handleAutoCluster, + } : workflowStats.unclustered >= 5 ? { + label: 'Auto-Cluster All', + message: `${workflowStats.unclustered} unclustered`, + onClick: handleAutoCluster, + } : workflowStats.clustered > 0 ? { + label: 'Generate Ideas', + href: '/planner/ideas', + message: `${workflowStats.clustered} clustered`, + } : undefined} onFilterChange={(key, value) => { // Normalize value to string, preserving empty strings const stringValue = value === null || value === undefined ? '' : String(value); diff --git a/frontend/src/pages/Writer/Content.tsx b/frontend/src/pages/Writer/Content.tsx index c5ec0f2c..e10148f6 100644 --- a/frontend/src/pages/Writer/Content.tsx +++ b/frontend/src/pages/Writer/Content.tsx @@ -14,7 +14,7 @@ import { bulkDeleteContent, } from '../../services/api'; import { optimizerApi } from '../../api/optimizer.api'; -import { useNavigate, Link } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, TaskIcon, CheckCircleIcon } from '../../icons'; import { createContentPageConfig } from '../../config/pages/content.config'; @@ -228,20 +228,8 @@ export default function Content() { <> , color: 'orange' }} - breadcrumb="Writer" - actions={ - - Generate Images - - - - - } + parent="Writer" /> 0 ? { + label: 'Generate Images', + message: `${selectedIds.length} selected`, + onClick: () => handleRowAction('generate_images', { id: selectedIds[0] }), + } : content.filter(c => c.status === 'draft').length > 0 ? { + label: 'Generate Images', + href: '/writer/images', + message: `${content.filter(c => c.status === 'draft').length} drafts`, + } : undefined} onFilterChange={(key: string, value: any) => { if (key === 'search') { setSearchTerm(value); diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx index 22569941..cb88b427 100644 --- a/frontend/src/pages/Writer/Images.tsx +++ b/frontend/src/pages/Writer/Images.tsx @@ -4,7 +4,6 @@ */ import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; -import { Link } from 'react-router-dom'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchContentImages, @@ -452,20 +451,8 @@ export default function Images() { <> , color: 'pink' }} - breadcrumb="Writer" - actions={ - - Review Content - - - - - } + parent="Writer" /> 0 ? { + label: 'Generate Images', + message: `${selectedIds.length} selected`, + onClick: () => handleBulkAction('generate_images', selectedIds), + } : images.filter(i => i.overall_status === 'ready').length > 0 ? { + label: 'Review Content', + href: '/writer/review', + message: `${images.filter(i => i.overall_status === 'ready').length} ready`, + } : undefined} onFilterChange={(key, value) => { const stringValue = value === null || value === undefined ? '' : String(value); if (key === 'search') { diff --git a/frontend/src/pages/Writer/Published.tsx b/frontend/src/pages/Writer/Published.tsx index 1990b2ef..e7269459 100644 --- a/frontend/src/pages/Writer/Published.tsx +++ b/frontend/src/pages/Writer/Published.tsx @@ -4,7 +4,7 @@ */ import { useState, useEffect, useMemo, useCallback } from 'react'; -import { useNavigate, Link } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchContent, @@ -309,20 +309,8 @@ export default function Published() { <> , color: 'green' }} - breadcrumb="Writer" - actions={ - - Create More Content - - - - - } + parent="Writer" /> 0 ? { + label: 'Sync to WordPress', + message: `${selectedIds.length} selected`, + onClick: () => handleBulkAction('publish_to_wordpress', selectedIds), + } : { + label: 'Create More Content', + href: '/planner/keywords', + message: `${content.length} published`, + }} onFilterChange={(key: string, value: any) => { if (key === 'search') { setSearchTerm(value); diff --git a/frontend/src/pages/Writer/Review.tsx b/frontend/src/pages/Writer/Review.tsx index 5a3f2046..a81c7efa 100644 --- a/frontend/src/pages/Writer/Review.tsx +++ b/frontend/src/pages/Writer/Review.tsx @@ -4,7 +4,7 @@ */ import { useState, useEffect, useMemo, useCallback } from 'react'; -import { useNavigate, Link } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchContent, @@ -348,20 +348,8 @@ export default function Review() { <> , color: 'emerald' }} - breadcrumb="Writer" - actions={ - - View Published - - - - - } + parent="Writer" /> 0 ? { + label: 'Publish Selected', + message: `${selectedIds.length} selected`, + onClick: () => handleBulkAction('publish', selectedIds), + } : content.filter(c => c.status === 'review').length > 0 ? { + label: 'View Published', + href: '/writer/published', + message: `${content.filter(c => c.status === 'review').length} in review`, + } : undefined} onFilterChange={(key, value) => { const stringValue = value === null || value === undefined ? '' : String(value); if (key === 'search') { diff --git a/frontend/src/pages/Writer/Tasks.tsx b/frontend/src/pages/Writer/Tasks.tsx index e9398a65..3e7834d1 100644 --- a/frontend/src/pages/Writer/Tasks.tsx +++ b/frontend/src/pages/Writer/Tasks.tsx @@ -4,7 +4,6 @@ */ import { useState, useEffect, useCallback, useRef } from 'react'; -import { Link } from 'react-router-dom'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchTasks, @@ -369,20 +368,8 @@ export default function Tasks() { <> , color: 'blue' }} - breadcrumb="Writer" - actions={ - - View Drafts - - - - - } + parent="Writer" /> 0 ? { + label: 'Generate Content', + message: `${selectedIds.length} selected`, + onClick: () => handleBulkAction('generate_content', selectedIds), + } : tasks.filter(t => t.status === 'queued').length > 0 ? { + label: 'View Drafts', + href: '/writer/content', + message: `${tasks.filter(t => t.status === 'queued').length} queued`, + } : undefined} onFilterChange={(key, value) => { const stringValue = value === null || value === undefined ? '' : String(value); if (key === 'search') { diff --git a/frontend/src/templates/TablePageTemplate.tsx b/frontend/src/templates/TablePageTemplate.tsx index cc93335e..4e48d3e2 100644 --- a/frontend/src/templates/TablePageTemplate.tsx +++ b/frontend/src/templates/TablePageTemplate.tsx @@ -142,6 +142,14 @@ interface TablePageTemplateProps { icon?: ReactNode; variant?: 'primary' | 'success' | 'danger'; }>; + // Next action button for workflow guidance (shown in action bar) + nextAction?: { + label: string; + message?: string; // Message to show above button (e.g., "5 selected") + onClick?: () => void; + href?: string; + disabled?: boolean; + }; } export default function TablePageTemplate({ @@ -178,6 +186,7 @@ export default function TablePageTemplate({ className = '', customActions, bulkActions: customBulkActions, + nextAction, }: TablePageTemplateProps) { const location = useLocation(); const [isBulkActionsDropdownOpen, setIsBulkActionsDropdownOpen] = useState(false); @@ -741,6 +750,44 @@ export default function TablePageTemplate({ {createLabel} )} + {/* Next Action Button - Workflow Guidance */} + {nextAction && ( +
+ {nextAction.message && ( + {nextAction.message} + )} + {nextAction.href ? ( + + {nextAction.label} + + + + + ) : ( + + )} +
+ )}