- {/* Sidebar Toggle */}
-
- {isMobileOpen ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
{/* Mobile 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' ? (
-
- {nextStep.label}
-
-
-
-
- ) : 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}
+
+
+
+
+ ) : (
+
+ {nextAction.label}
+
+
+
+
+ )}
+
+ )}