many changes for modules widgets and colors and styling

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-31 23:52:43 +00:00
parent b61bd6e64d
commit 89b64cd737
34 changed files with 2450 additions and 1985 deletions

View File

@@ -500,8 +500,14 @@ export default function SignUpFormUnified({
<div className="flex items-start gap-3 pt-2">
<Checkbox className="w-5 h-5 mt-0.5" checked={isChecked} onChange={setIsChecked} />
<p className="text-sm text-gray-500 dark:text-gray-400">
By creating an account means you agree to the <span className="text-gray-800 dark:text-white/90">Terms and Conditions</span>, and our{' '}
<span className="text-gray-800 dark:text-white">Privacy Policy</span>
By creating an account means you agree to the{' '}
<Link to="/terms" className="text-brand-500 hover:text-brand-600 dark:text-brand-400 hover:underline">
Terms and Conditions
</Link>
, and our{' '}
<Link to="/privacy" className="text-brand-500 hover:text-brand-600 dark:text-brand-400 hover:underline">
Privacy Policy
</Link>
</p>
</div>

View File

@@ -1,8 +1,10 @@
/**
* ColumnSelector Component
* Dropdown with checkboxes to show/hide table columns
* Dropdown opens in the direction with most available space
*/
import React, { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { ChevronDownIcon } from '../../icons';
import Checkbox from '../form/input/Checkbox';
@@ -22,6 +24,7 @@ export default function ColumnSelector({
compact = false,
}: ColumnSelectorProps) {
const [isOpen, setIsOpen] = useState(false);
const [dropdownStyle, setDropdownStyle] = useState<React.CSSProperties>({});
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
@@ -46,6 +49,98 @@ export default function ColumnSelector({
}
}, [isOpen]);
// Calculate dropdown position when opened
useEffect(() => {
if (isOpen && buttonRef.current) {
const buttonRect = buttonRef.current.getBoundingClientRect();
const dropdownHeight = 384; // max-h-96 = 24rem = 384px
const dropdownWidth = 224; // w-56 = 14rem = 224px
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
const margin = 8;
// Calculate space above and below
const spaceBelow = viewportHeight - buttonRect.bottom - margin;
const spaceAbove = buttonRect.top - margin;
// Determine vertical position - prefer below if enough space
let top: number;
if (spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove) {
// Open below
top = buttonRect.bottom + margin;
} else {
// Open above
top = buttonRect.top - dropdownHeight - margin;
}
// Ensure dropdown doesn't go off top of screen
top = Math.max(margin, top);
// Calculate horizontal position - align to right edge of button
let left = buttonRect.right - dropdownWidth;
// Ensure dropdown doesn't go off left edge
left = Math.max(margin, left);
// Ensure dropdown doesn't go off right edge
if (left + dropdownWidth > viewportWidth - margin) {
left = viewportWidth - dropdownWidth - margin;
}
setDropdownStyle({
position: 'fixed',
top: `${top}px`,
left: `${left}px`,
width: `${dropdownWidth}px`,
});
}
}, [isOpen]);
// Recalculate on scroll/resize
useEffect(() => {
if (!isOpen) return;
const handleReposition = () => {
if (buttonRef.current) {
const buttonRect = buttonRef.current.getBoundingClientRect();
const dropdownHeight = 384;
const dropdownWidth = 224;
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
const margin = 8;
const spaceBelow = viewportHeight - buttonRect.bottom - margin;
const spaceAbove = buttonRect.top - margin;
let top: number;
if (spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove) {
top = buttonRect.bottom + margin;
} else {
top = buttonRect.top - dropdownHeight - margin;
}
top = Math.max(margin, top);
let left = buttonRect.right - dropdownWidth;
left = Math.max(margin, left);
if (left + dropdownWidth > viewportWidth - margin) {
left = viewportWidth - dropdownWidth - margin;
}
setDropdownStyle({
position: 'fixed',
top: `${top}px`,
left: `${left}px`,
width: `${dropdownWidth}px`,
});
}
};
window.addEventListener('scroll', handleReposition, true);
window.addEventListener('resize', handleReposition);
return () => {
window.removeEventListener('scroll', handleReposition, true);
window.removeEventListener('resize', handleReposition);
};
}, [isOpen]);
const visibleCount = visibleColumns.size;
const totalCount = columns.length;
@@ -90,10 +185,11 @@ export default function ColumnSelector({
)}
</button>
{isOpen && (
{isOpen && createPortal(
<div
ref={dropdownRef}
className="absolute right-0 mt-2 w-56 rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800 z-50 max-h-96 overflow-y-auto"
style={dropdownStyle}
className="rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800 z-[9999] max-h-96 overflow-y-auto"
>
<div className="p-2">
<div className="px-3 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide border-b border-gray-200 dark:border-gray-700 mb-1">
@@ -137,7 +233,8 @@ export default function ColumnSelector({
</button>
</div>
</div>
</div>
</div>,
document.body
)}
</div>
);

View File

@@ -35,15 +35,22 @@ interface AIOperationsWidgetProps {
loading?: boolean;
}
const operationConfig: Record<string, { label: string; icon: typeof GroupIcon; color: string }> = {
clustering: { label: 'Clustering', icon: GroupIcon, color: 'text-purple-600 dark:text-purple-400' },
ideas: { label: 'Ideas', icon: BoltIcon, color: 'text-warning-600 dark:text-warning-400' },
content: { label: 'Content', icon: FileTextIcon, color: 'text-success-600 dark:text-success-400' },
images: { label: 'Images', icon: FileIcon, color: 'text-purple-600 dark:text-purple-400' },
/**
* Operation config with solid colored backgrounds matching Automation/Usage pages:
* - Clustering: purple
* - Ideas: warning/amber
* - Content: success/green
* - Images: purple
*/
const operationConfig: Record<string, { label: string; icon: typeof GroupIcon; gradient: string }> = {
clustering: { label: 'Clustering', icon: GroupIcon, gradient: 'from-purple-500 to-purple-600' },
ideas: { label: 'Ideas', icon: BoltIcon, gradient: 'from-warning-500 to-warning-600' },
content: { label: 'Content', icon: FileTextIcon, gradient: 'from-success-500 to-success-600' },
images: { label: 'Images', icon: FileIcon, gradient: 'from-purple-500 to-purple-600' },
};
// Default config for unknown operation types
const defaultConfig = { label: 'Other', icon: BoltIcon, color: 'text-gray-600 dark:text-gray-400' };
const defaultConfig = { label: 'Other', icon: BoltIcon, gradient: 'from-gray-500 to-gray-600' };
const periods = [
{ value: '7d', label: '7 days' },
@@ -117,7 +124,9 @@ export default function AIOperationsWidget({ data, onPeriodChange, loading }: AI
className="flex items-center py-2 border-b border-gray-100 dark:border-gray-800"
>
<div className="flex items-center gap-2.5 flex-1">
<Icon className={`w-5 h-5 ${config.color}`} />
<div className={`p-1.5 rounded-lg bg-gradient-to-br ${config.gradient} shadow-sm`}>
<Icon className="w-4 h-4 text-white" />
</div>
<span className="text-base text-gray-800 dark:text-gray-200">
{config.label}
</span>

View File

@@ -1,85 +0,0 @@
/**
* ModuleMetricsFooter - Compact metrics footer for table pages
* Shows module-specific metrics at the bottom of table pages
* Uses standard EnhancedMetricCard and ProgressBar components
* Follows standard app design system and color scheme
*/
import React from 'react';
import EnhancedMetricCard, { MetricCardProps } from './EnhancedMetricCard';
import { ProgressBar } from '../ui/progress';
export interface MetricItem {
title: string;
value: string | number;
subtitle?: string;
icon: React.ReactNode;
accentColor: MetricCardProps['accentColor'];
href?: string;
onClick?: () => void;
}
export interface ProgressMetric {
label: string;
value: number; // 0-100
color?: 'primary' | 'success' | 'warning' | 'purple';
}
interface ModuleMetricsFooterProps {
metrics: MetricItem[];
progress?: ProgressMetric;
className?: string;
}
export default function ModuleMetricsFooter({
metrics,
progress,
className = ''
}: ModuleMetricsFooterProps) {
if (metrics.length === 0 && !progress) return null;
const progressColors = {
primary: 'bg-[var(--color-primary)]',
success: 'bg-[var(--color-success)]',
warning: 'bg-[var(--color-warning)]',
purple: 'bg-[var(--color-purple)]',
};
return (
<div className={`mt-8 pt-6 border-t border-gray-200 dark:border-gray-800 ${className}`}>
<div className="space-y-4">
{/* Metrics Grid */}
{metrics.length > 0 && (
<div className={`grid grid-cols-1 sm:grid-cols-2 ${metrics.length > 2 ? 'lg:grid-cols-3' : 'lg:grid-cols-2'} ${metrics.length > 3 ? 'xl:grid-cols-4' : ''} gap-4`}>
{metrics.map((metric, index) => (
<EnhancedMetricCard
key={index}
title={metric.title}
value={metric.value}
subtitle={metric.subtitle}
icon={metric.icon}
accentColor={metric.accentColor}
href={metric.href}
onClick={metric.onClick}
/>
))}
</div>
)}
{/* Progress Bar */}
{progress && (
<div className="space-y-2">
<ProgressBar
value={progress.value}
color={progress.color === 'success' ? 'success' : progress.color === 'warning' ? 'warning' : 'primary'}
size="md"
showLabel={true}
label={progress.label}
/>
</div>
)}
</div>
</div>
);
}

View File

@@ -20,6 +20,17 @@ interface QuickActionsWidgetProps {
onAddKeywords?: () => void;
}
/**
* Workflow steps with solid colored backgrounds matching Automation/Usage pages:
* - Keywords: brand/primary (blue)
* - Clusters: purple
* - Ideas: warning/amber
* - Tasks: brand/primary (blue)
* - Content: success/green
* - Images: purple
* - Review: warning/amber
* - Publish: success/green
*/
const workflowSteps = [
{
num: 1,
@@ -28,7 +39,7 @@ const workflowSteps = [
description: 'Import your target keywords manually or from CSV',
href: '/planner/keyword-opportunities',
actionLabel: 'Add',
color: 'text-brand-600 dark:text-brand-400',
gradient: 'from-brand-500 to-brand-600',
},
{
num: 2,
@@ -37,7 +48,7 @@ const workflowSteps = [
description: 'AI groups related keywords into content clusters',
href: '/planner/clusters',
actionLabel: 'Cluster',
color: 'text-purple-600 dark:text-purple-400',
gradient: 'from-purple-500 to-purple-600',
},
{
num: 3,
@@ -46,7 +57,7 @@ const workflowSteps = [
description: 'Create content ideas from your keyword clusters',
href: '/planner/ideas',
actionLabel: 'Ideas',
color: 'text-warning-600 dark:text-warning-400',
gradient: 'from-warning-500 to-warning-600',
},
{
num: 4,
@@ -55,7 +66,7 @@ const workflowSteps = [
description: 'Convert approved ideas into content tasks',
href: '/writer/tasks',
actionLabel: 'Tasks',
color: 'text-purple-600 dark:text-purple-400',
gradient: 'from-brand-500 to-brand-600',
},
{
num: 5,
@@ -64,7 +75,7 @@ const workflowSteps = [
description: 'AI writes SEO-optimized articles from tasks',
href: '/writer/content',
actionLabel: 'Write',
color: 'text-success-600 dark:text-success-400',
gradient: 'from-success-500 to-success-600',
},
{
num: 6,
@@ -73,7 +84,7 @@ const workflowSteps = [
description: 'Create featured images and media for articles',
href: '/writer/images',
actionLabel: 'Images',
color: 'text-purple-600 dark:text-purple-400',
gradient: 'from-purple-500 to-purple-600',
},
{
num: 7,
@@ -82,7 +93,7 @@ const workflowSteps = [
description: 'Quality check and approve generated content',
href: '/writer/review',
actionLabel: 'Review',
color: 'text-warning-600 dark:text-warning-400',
gradient: 'from-warning-500 to-warning-600',
},
{
num: 8,
@@ -91,7 +102,7 @@ const workflowSteps = [
description: 'Push approved content to your WordPress site',
href: '/writer/published',
actionLabel: 'Publish',
color: 'text-purple-600 dark:text-purple-400',
gradient: 'from-success-500 to-success-600',
},
];
@@ -128,13 +139,13 @@ export default function QuickActionsWidget({ onAddKeywords }: QuickActionsWidget
className="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors group"
>
{/* Step Number */}
<span className="w-6 h-6 flex items-center justify-center rounded-full bg-brand-100 dark:bg-brand-900/30 text-sm font-semibold text-brand-600 dark:text-brand-400 flex-shrink-0">
<span className={`w-6 h-6 flex items-center justify-center rounded-full bg-gradient-to-br ${step.gradient} text-sm font-semibold text-white flex-shrink-0 shadow-sm`}>
{step.num}
</span>
{/* Icon */}
<div className={`flex-shrink-0 ${step.color}`}>
<Icon className="w-5 h-5" />
{/* Icon with solid gradient background */}
<div className={`flex-shrink-0 p-1.5 rounded-lg bg-gradient-to-br ${step.gradient} shadow-sm`}>
<Icon className="w-4 h-4 text-white" />
</div>
{/* Text Content */}
@@ -172,13 +183,13 @@ export default function QuickActionsWidget({ onAddKeywords }: QuickActionsWidget
className="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors group"
>
{/* Step Number */}
<span className="w-6 h-6 flex items-center justify-center rounded-full bg-brand-100 dark:bg-brand-900/30 text-sm font-semibold text-brand-600 dark:text-brand-400 flex-shrink-0">
<span className={`w-6 h-6 flex items-center justify-center rounded-full bg-gradient-to-br ${step.gradient} text-sm font-semibold text-white flex-shrink-0 shadow-sm`}>
{step.num}
</span>
{/* Icon */}
<div className={`flex-shrink-0 ${step.color}`}>
<Icon className="w-5 h-5" />
{/* Icon with solid gradient background */}
<div className={`flex-shrink-0 p-1.5 rounded-lg bg-gradient-to-br ${step.gradient} shadow-sm`}>
<Icon className="w-4 h-4 text-white" />
</div>
{/* Text Content */}
@@ -216,13 +227,13 @@ export default function QuickActionsWidget({ onAddKeywords }: QuickActionsWidget
className="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors group"
>
{/* Step Number */}
<span className="w-6 h-6 flex items-center justify-center rounded-full bg-brand-100 dark:bg-brand-900/30 text-sm font-semibold text-brand-600 dark:text-brand-400 flex-shrink-0">
<span className={`w-6 h-6 flex items-center justify-center rounded-full bg-gradient-to-br ${step.gradient} text-sm font-semibold text-white flex-shrink-0 shadow-sm`}>
{step.num}
</span>
{/* Icon */}
<div className={`flex-shrink-0 ${step.color}`}>
<Icon className="w-5 h-5" />
{/* Icon with solid gradient background */}
<div className={`flex-shrink-0 p-1.5 rounded-lg bg-gradient-to-br ${step.gradient} shadow-sm`}>
<Icon className="w-4 h-4 text-white" />
</div>
{/* Text Content */}

View File

@@ -30,19 +30,19 @@ interface RecentActivityWidgetProps {
loading?: boolean;
}
const activityConfig: Record<string, { icon: typeof GroupIcon; color: string; bgColor: string }> = {
clustering: { icon: GroupIcon, color: 'text-purple-600 dark:text-purple-400', bgColor: 'bg-purple-100 dark:bg-purple-900/40' },
ideas: { icon: BoltIcon, color: 'text-warning-600 dark:text-warning-400', bgColor: 'bg-warning-100 dark:bg-warning-900/40' },
content: { icon: FileTextIcon, color: 'text-success-600 dark:text-success-400', bgColor: 'bg-success-100 dark:bg-success-900/40' },
images: { icon: FileIcon, color: 'text-purple-600 dark:text-purple-400', bgColor: 'bg-purple-100 dark:bg-purple-900/40' },
published: { icon: PaperPlaneIcon, color: 'text-purple-600 dark:text-purple-400', bgColor: 'bg-purple-100 dark:bg-purple-900/40' },
keywords: { icon: ListIcon, color: 'text-brand-600 dark:text-brand-400', bgColor: 'bg-brand-100 dark:bg-brand-900/40' },
error: { icon: AlertIcon, color: 'text-error-600 dark:text-error-400', bgColor: 'bg-error-100 dark:bg-error-900/40' },
sync: { icon: CheckCircleIcon, color: 'text-purple-600 dark:text-purple-400', bgColor: 'bg-purple-100 dark:bg-purple-900/40' },
const activityConfig: Record<string, { icon: typeof GroupIcon; gradient: string }> = {
clustering: { icon: GroupIcon, gradient: 'from-purple-500 to-purple-600' },
ideas: { icon: BoltIcon, gradient: 'from-warning-500 to-warning-600' },
content: { icon: FileTextIcon, gradient: 'from-success-500 to-success-600' },
images: { icon: FileIcon, gradient: 'from-purple-500 to-purple-600' },
published: { icon: PaperPlaneIcon, gradient: 'from-success-500 to-success-600' },
keywords: { icon: ListIcon, gradient: 'from-brand-500 to-brand-600' },
error: { icon: AlertIcon, gradient: 'from-error-500 to-error-600' },
sync: { icon: CheckCircleIcon, gradient: 'from-success-500 to-success-600' },
};
// Default config for unknown activity types
const defaultActivityConfig = { icon: BoltIcon, color: 'text-gray-600 dark:text-gray-400', bgColor: 'bg-gray-100 dark:bg-gray-900/40' };
const defaultActivityConfig = { icon: BoltIcon, gradient: 'from-gray-500 to-gray-600' };
function formatRelativeTime(date: Date): string {
const now = new Date();
@@ -95,8 +95,8 @@ export default function RecentActivityWidget({ activities, loading }: RecentActi
const content = (
<div className="flex items-start gap-3">
<div className={`w-9 h-9 rounded-lg ${config.bgColor} flex items-center justify-center flex-shrink-0`}>
<Icon className={`w-5 h-5 ${config.color}`} />
<div className={`w-9 h-9 rounded-lg bg-gradient-to-br ${config.gradient} flex items-center justify-center flex-shrink-0 shadow-md`}>
<Icon className="w-5 h-5 text-white" />
</div>
<div className="flex-1 min-w-0">
<p className="text-base text-gray-800 dark:text-gray-200 line-clamp-1">

View File

@@ -1,27 +1,23 @@
/**
* ThreeWidgetFooter - 3-Column Layout for Table Page Footers
* StandardThreeWidgetFooter - Enhanced 3-Column Layout with Standardized Widgets
*
* Design from Section 3 of COMPREHENSIVE-AUDIT-REPORT.md:
*
* WIDGET 1: PAGE METRICS WIDGET 2: MODULE STATS WIDGET 3: COMPLETION
* (Current Page Progress) (Full Module Overview) (Both Modules Stats)
* ~33.3% width ~33.3% width ~33.3% width
*
* This component provides a consistent 3-widget footer layout:
* - Widget 1: Page Progress (page-specific, passed as prop)
* - Widget 2: Module Stats (standardized via useModuleStats hook)
* - Widget 3: Workflow Completion (standardized via useWorkflowStats hook)
*
* STYLING: Uses CSS tokens from styles/tokens.css:
* - --color-primary: Brand blue for primary actions/bars
* - --color-success: Green for success states
* - --color-warning: Amber for warnings
* - --color-purple: Purple accent
* Use this component for consistent data across all Planner and Writer module pages.
*/
import React from 'react';
import { Link } from 'react-router-dom';
import { Card } from '../ui/card/Card';
import { Link } from 'react-router-dom';
import { LightBulbIcon, ChevronRightIcon } from '@heroicons/react/24/solid';
import WorkflowCompletionWidget from './WorkflowCompletionWidget';
import StandardizedModuleWidget, { ModuleType } from './StandardizedModuleWidget';
// ============================================================================
// TYPE DEFINITIONS
// TYPE DEFINITIONS (moved from ThreeWidgetFooter.tsx)
// ============================================================================
/** Submodule color type - matches headerMetrics accentColor */
@@ -35,6 +31,10 @@ export interface PageProgressWidget {
hint?: string;
/** The submodule's accent color - progress bar uses this */
submoduleColor?: SubmoduleColor;
/** Optional credits consumed for AI functions on this page */
creditsConsumed?: number;
/** Contextual status insight - guidance message based on current state */
statusInsight?: string;
}
/** Widget 2: Module Stats - Pipeline flow with arrows and progress bars */
@@ -57,28 +57,21 @@ export interface ModuleStatsWidget {
links: Array<{ label: string; href: string }>;
}
/** Widget 3: Completion - Tree structure with bars for both modules */
export interface CompletionItem {
label: string;
value: number;
color?: SubmoduleColor;
}
// ============================================================================
// TYPE DEFINITIONS
// ============================================================================
export interface CompletionWidget {
title: string;
plannerItems: CompletionItem[];
writerItems: CompletionItem[];
creditsUsed?: number;
operationsCount?: number;
analyticsHref?: string;
}
/** Main component props */
export interface ThreeWidgetFooterProps {
export interface StandardThreeWidgetFooterProps {
pageProgress: PageProgressWidget;
moduleStats: ModuleStatsWidget;
completion: CompletionWidget;
/** @deprecated Use `module` prop instead for standardized data. This is kept for backward compatibility. */
moduleStats?: ModuleStatsWidget;
/** Module type for standardized module stats widget */
module?: ModuleType;
submoduleColor?: SubmoduleColor;
/** Show credits consumption in workflow widget */
showCredits?: boolean;
/** Analytics href for the workflow widget */
analyticsHref?: string;
className?: string;
}
@@ -104,7 +97,7 @@ function PageProgressCard({ widget, submoduleColor = 'blue' }: { widget: PagePro
const progressColor = widget.submoduleColor || widget.progress.color || submoduleColor;
return (
<Card className="p-5 bg-white dark:bg-gray-900 border border-[color:var(--color-stroke)] dark:border-gray-700">
<Card className="p-5 bg-white dark:bg-gray-900 border border-[color:var(--color-stroke)] dark:border-gray-700 min-w-0 overflow-hidden">
{/* Header */}
<h3 className="text-sm font-semibold text-[color:var(--color-text)] dark:text-gray-200 uppercase tracking-wide mb-4">
{widget.title}
@@ -144,14 +137,31 @@ function PageProgressCard({ widget, submoduleColor = 'blue' }: { widget: PagePro
</div>
</div>
{/* Credits consumed (for AI function pages) */}
{widget.creditsConsumed !== undefined && widget.creditsConsumed > 0 && (
<div className="flex items-center justify-between pt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800 mb-3">
<span className="text-sm font-medium text-[color:var(--color-text-dim)] dark:text-gray-400">Credits Consumed</span>
<span className="text-sm font-bold" style={{ color: 'var(--color-warning)' }}>{widget.creditsConsumed.toLocaleString()}</span>
</div>
)}
{/* Hint with icon */}
{widget.hint && (
<div className="flex items-start gap-2 pt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800">
<div className={`flex items-start gap-2 ${widget.creditsConsumed === undefined || widget.creditsConsumed === 0 ? 'pt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800' : ''}`}>
<LightBulbIcon className="w-5 h-5 flex-shrink-0 mt-0.5 text-warning-500" />
<span className="text-sm font-medium text-brand-500">{widget.hint}</span>
</div>
)}
{/* Status Insight - contextual guidance */}
{widget.statusInsight && (
<div className="mt-3 pt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800">
<p className="text-xs text-[color:var(--color-text-dim)] dark:text-gray-400 leading-relaxed">
<span className="font-semibold" style={{ color: 'var(--color-primary)' }}>Next:</span>{' '}
{widget.statusInsight}
</p>
</div>
)}
</Card>
);
}
@@ -162,7 +172,7 @@ function PageProgressCard({ widget, submoduleColor = 'blue' }: { widget: PagePro
function ModuleStatsCard({ widget }: { widget: ModuleStatsWidget }) {
return (
<Card className="p-5 bg-white dark:bg-gray-900 border border-[color:var(--color-stroke)] dark:border-gray-700">
<Card className="p-5 bg-white dark:bg-gray-900 border border-[color:var(--color-stroke)] dark:border-gray-700 min-w-0 overflow-hidden">
{/* Header */}
<h3 className="text-sm font-semibold text-[color:var(--color-text)] dark:text-gray-200 uppercase tracking-wide mb-4">
{widget.title}
@@ -245,137 +255,43 @@ function ModuleStatsCard({ widget }: { widget: ModuleStatsWidget }) {
);
}
// ============================================================================
// WIDGET 3: COMPLETION
// ============================================================================
function CompletionCard({ widget }: { widget: CompletionWidget }) {
// Calculate max for proportional bars (across both columns)
const allValues = [...widget.plannerItems, ...widget.writerItems].map(i => i.value);
const maxValue = Math.max(...allValues, 1);
const renderItem = (item: CompletionItem, isLast: boolean) => {
const barWidth = (item.value / maxValue) * 100;
const prefix = isLast ? '└─' : '├─';
const color = item.color || 'blue';
return (
<div key={item.label} className="flex items-center gap-2 py-1">
{/* Tree prefix */}
<span className="text-[color:var(--color-text-dim)] dark:text-gray-500 font-mono text-xs w-5 flex-shrink-0">{prefix}</span>
{/* Label */}
<span className="text-sm text-[color:var(--color-text)] dark:text-gray-300 flex-1 truncate">{item.label}</span>
{/* Progress bar */}
<div className="w-16 h-2.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden flex-shrink-0">
<div
className="h-full rounded-full transition-all duration-500"
style={{
...getProgressBarStyle(color),
width: `${Math.min(100, barWidth)}%`
}}
/>
</div>
{/* Value */}
<span className="text-sm font-bold text-[color:var(--color-text)] dark:text-white tabular-nums w-10 text-right flex-shrink-0">
{item.value}
</span>
</div>
);
};
return (
<Card className="p-5 bg-white dark:bg-gray-900 border border-[color:var(--color-stroke)] dark:border-gray-700">
{/* Header */}
<h3 className="text-sm font-semibold text-[color:var(--color-text)] dark:text-gray-200 uppercase tracking-wide mb-4">
{widget.title}
</h3>
{/* Two-column layout: Planner | Writer */}
<div className="grid grid-cols-2 gap-6 mb-4">
{/* Planner Column */}
<div>
<div className="text-xs font-bold uppercase tracking-wide mb-2 text-brand-500">
Planner
</div>
<div className="space-y-0.5">
{widget.plannerItems.map((item, idx) =>
renderItem(item, idx === widget.plannerItems.length - 1)
)}
</div>
</div>
{/* Writer Column */}
<div>
<div className="text-xs font-bold uppercase tracking-wide mb-2 text-success-500">
Writer
</div>
<div className="space-y-0.5">
{widget.writerItems.map((item, idx) =>
renderItem(item, idx === widget.writerItems.length - 1)
)}
</div>
</div>
</div>
{/* Footer Stats - Credits Used & Operations */}
{(widget.creditsUsed !== undefined || widget.operationsCount !== undefined) && (
<div className="flex items-center gap-4 pt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800 text-sm">
{widget.creditsUsed !== undefined && (
<span className="text-[color:var(--color-text-dim)] dark:text-gray-400">
Credits Used: <strong className="text-[color:var(--color-text)] dark:text-white font-bold">{widget.creditsUsed.toLocaleString()}</strong>
</span>
)}
{widget.creditsUsed !== undefined && widget.operationsCount !== undefined && (
<span className="text-[color:var(--color-stroke)] dark:text-gray-600"></span>
)}
{widget.operationsCount !== undefined && (
<span className="text-[color:var(--color-text-dim)] dark:text-gray-400">
Operations: <strong className="text-[color:var(--color-text)] dark:text-white font-bold">{widget.operationsCount}</strong>
</span>
)}
</div>
)}
{/* Analytics Link */}
{widget.analyticsHref && (
<div className="pt-3 mt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800">
<Link
to={widget.analyticsHref}
className="text-sm font-medium hover:underline flex items-center gap-1 text-brand-500"
>
View Full Analytics
<ChevronRightIcon className="w-4 h-4" />
</Link>
</div>
)}
</Card>
);
}
// ============================================================================
// MAIN COMPONENT
// ============================================================================
export default function ThreeWidgetFooter({
export default function StandardThreeWidgetFooter({
pageProgress,
moduleStats,
completion,
module,
submoduleColor = 'blue',
showCredits = true,
analyticsHref = '/account/usage',
className = '',
}: ThreeWidgetFooterProps) {
}: StandardThreeWidgetFooterProps) {
// Determine which module to show - prefer explicit module prop
const moduleType: ModuleType = module || (moduleStats?.title?.toLowerCase().includes('planner') ? 'planner' : 'writer');
return (
<div className={`mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 ${className}`}>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div className={`mt-8 pt-6 border-t border-gray-200 dark:border-gray-700 w-full max-w-full overflow-hidden ${className}`}>
{/*
* Widget widths adjusted per requirements:
* - Widget 1 (Page Progress): 28.3% (reduced by 5%)
* - Widget 2 (Module Stats): 28.3% (reduced by 5%)
* - Widget 3 (Workflow Completion): 43.4% (increased by 10%)
* Using fr units to prevent overflow
*/}
<div className="grid grid-cols-1 lg:grid-cols-[minmax(0,28.3fr)_minmax(0,28.3fr)_minmax(0,43.4fr)] gap-4 w-full">
<PageProgressCard widget={pageProgress} submoduleColor={submoduleColor} />
<ModuleStatsCard widget={moduleStats} />
<CompletionCard widget={completion} />
{/* Use standardized module widget for consistent data */}
<StandardizedModuleWidget module={moduleType} />
<WorkflowCompletionWidget
showCredits={showCredits}
analyticsHref={analyticsHref}
/>
</div>
</div>
);
}
// Also export sub-components for flexibility
export { PageProgressCard, ModuleStatsCard, CompletionCard };
// Export sub-components
export { PageProgressCard, ModuleStatsCard };

View File

@@ -0,0 +1,279 @@
/**
* StandardizedModuleWidget - Centralized Module Stats Widget
*
* A standardized widget that displays consistent module pipeline stats
* using the useModuleStats hook. This ensures all pages show the same
* data for module-level statistics.
*
* Usage in Planner pages: Shows Planner pipeline (Keywords → Clusters → Ideas → Tasks)
* Usage in Writer pages: Shows Writer pipeline (Tasks → Content → Images → Published)
*
* Also displays credits consumed per AI function:
* - Planner: Clustering credits, Idea Generation credits
* - Writer: Content Generation credits, Image Generation credits
*/
import React, { useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Card } from '../ui/card/Card';
import { ChevronRightIcon } from '@heroicons/react/24/solid';
import { useModuleStats } from '../../hooks/useModuleStats';
import { getPipelineColors } from '../../config/colors.config';
// ============================================================================
// TYPE DEFINITIONS
// ============================================================================
export type ModuleType = 'planner' | 'writer';
export interface StandardizedModuleWidgetProps {
/** Which module pipeline to display */
module: ModuleType;
/** Additional CSS classes */
className?: string;
}
// ============================================================================
// MAIN COMPONENT
// ============================================================================
export default function StandardizedModuleWidget({
module,
className = '',
}: StandardizedModuleWidgetProps) {
const { planner, writer, credits, loading } = useModuleStats();
// Get CSS variable colors at runtime for inline styles
const pipelineColors = useMemo(() => getPipelineColors(module), [module]);
// Define Planner pipeline using colors from config
const plannerPipeline = [
{
fromLabel: 'Keywords',
fromValue: planner.totalKeywords,
fromHref: '/planner/keywords',
toLabel: 'Clusters',
toValue: planner.totalClusters,
toHref: '/planner/clusters',
progress: planner.totalKeywords > 0
? Math.round((planner.keywordsMapped / planner.totalKeywords) * 100)
: 0,
color: pipelineColors[0], // primary
},
{
fromLabel: 'Clusters',
fromValue: planner.totalClusters,
fromHref: '/planner/clusters',
toLabel: 'Ideas',
toValue: planner.totalIdeas,
toHref: '/planner/ideas',
progress: planner.totalClusters > 0
? Math.min(100, Math.round((planner.totalIdeas / planner.totalClusters) * 100))
: 0,
color: pipelineColors[1], // purple
},
{
fromLabel: 'Ideas',
fromValue: planner.totalIdeas,
fromHref: '/planner/ideas',
toLabel: 'Tasks',
toValue: planner.ideasConverted,
toHref: '/writer/tasks',
progress: planner.totalIdeas > 0
? Math.round((planner.ideasConverted / planner.totalIdeas) * 100)
: 0,
color: pipelineColors[2], // warning
},
];
// Define Writer pipeline - using correct content structure
// Content has status: draft, review, published
// totalContent = drafts + review + published
// Get writer colors from config
const writerColors = useMemo(() => getPipelineColors('writer'), []);
const writerPipeline = [
{
fromLabel: 'Tasks',
fromValue: writer.totalTasks,
fromHref: '/writer/tasks',
toLabel: 'Content',
toValue: writer.totalContent,
toHref: '/writer/content',
progress: writer.totalTasks > 0
? Math.min(100, Math.round((writer.tasksCompleted / writer.totalTasks) * 100))
: 0,
color: writerColors[0], // primary
},
{
fromLabel: 'Content',
fromValue: writer.totalContent,
fromHref: '/writer/content',
toLabel: 'Images',
toValue: writer.totalImages,
toHref: '/writer/images',
progress: writer.totalContent > 0
? Math.min(100, Math.round((writer.totalImages / writer.totalContent) * 100))
: 0,
color: writerColors[1], // purple
},
{
fromLabel: 'Review',
fromValue: writer.contentReview,
fromHref: '/writer/review',
toLabel: 'Published',
toValue: writer.contentPublished,
toHref: '/writer/approved',
progress: (writer.contentReview + writer.contentPublished) > 0
? Math.round((writer.contentPublished / (writer.contentReview + writer.contentPublished)) * 100)
: 0,
color: writerColors[2], // success
},
];
const pipeline = module === 'planner' ? plannerPipeline : writerPipeline;
const title = module === 'planner' ? 'Planner Module' : 'Writer Module';
const links = module === 'planner'
? [
{ label: 'Keywords', href: '/planner/keywords' },
{ label: 'Clusters', href: '/planner/clusters' },
{ label: 'Ideas', href: '/planner/ideas' },
]
: [
{ label: 'Tasks', href: '/writer/tasks' },
{ label: 'Content', href: '/writer/content' },
{ label: 'Images', href: '/writer/images' },
{ label: 'Approved', href: '/writer/approved' },
];
return (
<Card className={`p-5 bg-white dark:bg-gray-900 border border-[color:var(--color-stroke)] dark:border-gray-700 min-w-0 overflow-hidden ${className}`}>
{/* Header */}
<h3 className="text-sm font-semibold text-[color:var(--color-text)] dark:text-gray-200 uppercase tracking-wide mb-4">
{title}
</h3>
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-pulse flex flex-col items-center gap-2">
<div className="w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700" />
<div className="text-sm text-gray-400">Loading...</div>
</div>
</div>
) : (
<>
{/* Pipeline Rows */}
<div className="space-y-4 mb-4">
{pipeline.map((row, idx) => (
<div key={idx}>
{/* Row header: FromLabel Value ► ToLabel Value */}
<div className="flex items-center justify-between mb-2">
{/* From side */}
<div className="flex items-center gap-2 min-w-0">
<Link
to={row.fromHref}
className="text-sm font-medium hover:underline truncate"
style={{ color: row.color }}
>
{row.fromLabel}
</Link>
<span className="text-lg font-bold tabular-nums" style={{ color: row.color }}>
{row.fromValue}
</span>
</div>
{/* Arrow icon */}
<ChevronRightIcon
className="w-5 h-5 flex-shrink-0 mx-2"
style={{ color: row.color }}
/>
{/* To side */}
<div className="flex items-center gap-2 min-w-0">
<Link
to={row.toHref}
className="text-sm font-medium hover:underline truncate"
style={{ color: row.color }}
>
{row.toLabel}
</Link>
<span className="text-lg font-bold text-[color:var(--color-text)] dark:text-white tabular-nums">
{row.toValue}
</span>
</div>
</div>
{/* Progress bar */}
<div className="h-2.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-500"
style={{
backgroundColor: row.color,
width: `${Math.min(100, Math.max(0, row.progress))}%`
}}
/>
</div>
</div>
))}
</div>
{/* Navigation Links */}
<div className="flex flex-wrap gap-3 pt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800">
{links.map((link, idx) => (
<Link
key={idx}
to={link.href}
className="text-sm font-medium hover:underline flex items-center gap-1 text-brand-500"
>
<ChevronRightIcon className="w-4 h-4" />
<span>{link.label}</span>
</Link>
))}
</div>
{/* Credits Consumed Section - inline layout */}
{module === 'planner' && credits.plannerTotal > 0 && (
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 mt-3 pt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800 text-sm">
<span className="text-xs font-semibold text-[color:var(--color-text-dim)] dark:text-gray-400 uppercase tracking-wide">
Credits:
</span>
{credits.clusteringCredits > 0 && (
<span className="text-[color:var(--color-text-dim)] dark:text-gray-400">
Clustering{' '}
<strong className="font-bold" style={{ color: 'var(--color-purple)' }}>{credits.clusteringCredits}</strong>
</span>
)}
{credits.ideaGenerationCredits > 0 && (
<span className="text-[color:var(--color-text-dim)] dark:text-gray-400">
Ideas{' '}
<strong className="font-bold" style={{ color: 'var(--color-warning)' }}>{credits.ideaGenerationCredits}</strong>
</span>
)}
</div>
)}
{module === 'writer' && credits.writerTotal > 0 && (
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 mt-3 pt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800 text-sm">
<span className="text-xs font-semibold text-[color:var(--color-text-dim)] dark:text-gray-400 uppercase tracking-wide">
Credits:
</span>
{credits.contentGenerationCredits > 0 && (
<span className="text-[color:var(--color-text-dim)] dark:text-gray-400">
Content{' '}
<strong className="font-bold" style={{ color: 'var(--color-success)' }}>{credits.contentGenerationCredits}</strong>
</span>
)}
{credits.imageGenerationCredits > 0 && (
<span className="text-[color:var(--color-text-dim)] dark:text-gray-400">
Images{' '}
<strong className="font-bold" style={{ color: 'var(--color-purple)' }}>{credits.imageGenerationCredits}</strong>
</span>
)}
</div>
)}
</>
)}
</Card>
);
}
export { StandardizedModuleWidget };

View File

@@ -0,0 +1,267 @@
/**
* WorkflowCompletionWidget - Standardized Workflow Stats Widget
*
* A centralized widget component that displays consistent workflow completion
* stats across all Planner and Writer module pages.
*
* Features:
* - Unified data fetching via useWorkflowStats hook
* - Time-based filtering (Today, 7, 30, 90 days)
* - Credits consumption display
* - Consistent appearance across all pages
*
* IMPORTANT: Uses "Content Pages" not "Articles" per design requirements
*/
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { Card } from '../ui/card/Card';
import { ChevronRightIcon } from '@heroicons/react/24/solid';
import { useWorkflowStats, TimeFilter } from '../../hooks/useWorkflowStats';
import { WORKFLOW_COLORS } from '../../config/colors.config';
// ============================================================================
// TYPE DEFINITIONS
// ============================================================================
export type SubmoduleColor = 'blue' | 'green' | 'amber' | 'purple';
export interface WorkflowCompletionWidgetProps {
/** Show credits consumption section */
showCredits?: boolean;
/** Link to analytics page */
analyticsHref?: string;
/** Additional CSS classes */
className?: string;
}
// ============================================================================
// TIME FILTER COMPONENT
// ============================================================================
interface TimeFilterProps {
value: TimeFilter;
onChange: (value: TimeFilter) => void;
}
function TimeFilterButtons({ value, onChange }: TimeFilterProps) {
const options: { value: TimeFilter; label: string }[] = [
{ value: 'today', label: 'Today' },
{ value: '7', label: '7d' },
{ value: '30', label: '30d' },
{ value: '90', label: '90d' },
];
return (
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 rounded-lg p-0.5">
{options.map((option) => (
<button
key={option.value}
onClick={() => onChange(option.value)}
className={`px-2 py-1 text-xs font-medium rounded-md transition-all ${
value === option.value
? 'bg-brand-500 text-white shadow-sm'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
>
{option.label}
</button>
))}
</div>
);
}
// ============================================================================
// COMPLETION ITEM COMPONENT
// ============================================================================
interface CompletionItemProps {
label: string;
value: number;
barColor: string;
maxValue: number;
isLast: boolean;
}
function CompletionItem({ label, value, barColor, maxValue, isLast }: CompletionItemProps) {
const barWidth = maxValue > 0 ? (value / maxValue) * 100 : 0;
const prefix = isLast ? '└─' : '├─';
return (
<div className="flex items-center gap-2 py-1">
{/* Tree prefix */}
<span className="text-[color:var(--color-text-dim)] dark:text-gray-500 font-mono text-xs w-5 flex-shrink-0">
{prefix}
</span>
{/* Label */}
<span className="text-sm text-[color:var(--color-text)] dark:text-gray-300 flex-1 truncate">
{label}
</span>
{/* Progress bar */}
<div className="w-16 h-2.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden flex-shrink-0">
<div
className="h-full rounded-full transition-all duration-500"
style={{
backgroundColor: barColor,
width: `${Math.min(100, barWidth)}%`
}}
/>
</div>
{/* Value */}
<span className="text-sm font-bold text-[color:var(--color-text)] dark:text-white tabular-nums w-10 text-right flex-shrink-0">
{value.toLocaleString()}
</span>
</div>
);
}
// ============================================================================
// MAIN COMPONENT
// ============================================================================
export default function WorkflowCompletionWidget({
showCredits = true,
analyticsHref = '/account/usage',
className = '',
}: WorkflowCompletionWidgetProps) {
const [timeFilter, setTimeFilter] = useState<TimeFilter>('30');
const { planner, writer, credits, loading } = useWorkflowStats(timeFilter);
// Define planner items with unified colors from config
// WORKFLOW_COLORS contains CSS variable names, so wrap with var()
const plannerItems = [
{ label: 'Keywords Clustered', value: planner.keywordsClustered, barColor: `var(${WORKFLOW_COLORS.planner.keywordsClustered})` },
{ label: 'Clusters Created', value: planner.clustersCreated, barColor: `var(${WORKFLOW_COLORS.planner.clustersCreated})` },
{ label: 'Ideas Generated', value: planner.ideasGenerated, barColor: `var(${WORKFLOW_COLORS.planner.ideasGenerated})` },
];
// Define writer items - using "Content Pages" not "Articles"
// Total content = drafts + review + published
const totalContent = writer.contentDrafts + writer.contentReview + writer.contentPublished;
const writerItems = [
{ label: 'Content Pages', value: totalContent, barColor: `var(${WORKFLOW_COLORS.writer.contentPages})` },
{ label: 'Images Created', value: writer.imagesCreated, barColor: `var(${WORKFLOW_COLORS.writer.imagesCreated})` },
{ label: 'Pages Published', value: writer.contentPublished, barColor: `var(${WORKFLOW_COLORS.writer.pagesPublished})` },
];
// Calculate max value for proportional bars (across both columns)
const allValues = [...plannerItems, ...writerItems].map(i => i.value);
const maxValue = Math.max(...allValues, 1);
return (
<Card className={`p-5 bg-white dark:bg-gray-900 border border-[color:var(--color-stroke)] dark:border-gray-700 min-w-0 ${className}`}>
{/* Header with Time Filter */}
<div className="flex items-center justify-between mb-4 gap-2">
<h3 className="text-sm font-semibold text-[color:var(--color-text)] dark:text-gray-200 uppercase tracking-wide truncate">
Workflow Completion
</h3>
<TimeFilterButtons value={timeFilter} onChange={setTimeFilter} />
</div>
{/* Loading State */}
{loading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-pulse flex flex-col items-center gap-2">
<div className="w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700" />
<div className="text-sm text-gray-400">Loading stats...</div>
</div>
</div>
) : (
<>
{/* Two-column layout: Planner | Writer */}
<div className="grid grid-cols-2 gap-4 mb-4">
{/* Planner Column */}
<div className="min-w-0">
<div className="text-xs font-bold uppercase tracking-wide mb-2" style={{ color: 'var(--color-primary)' }}>
Planner
</div>
<div className="space-y-0.5">
{plannerItems.map((item, idx) => (
<CompletionItem
key={item.label}
label={item.label}
value={item.value}
barColor={item.barColor}
maxValue={maxValue}
isLast={idx === plannerItems.length - 1}
/>
))}
</div>
</div>
{/* Writer Column */}
<div className="min-w-0">
<div className="text-xs font-bold uppercase tracking-wide mb-2" style={{ color: 'var(--color-success)' }}>
Writer
</div>
<div className="space-y-0.5">
{writerItems.map((item, idx) => (
<CompletionItem
key={item.label}
label={item.label}
value={item.value}
barColor={item.barColor}
maxValue={maxValue}
isLast={idx === writerItems.length - 1}
/>
))}
</div>
</div>
</div>
{/* Credits Used Section - Always show */}
{showCredits && (
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 pt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800 text-sm">
<span className="text-[color:var(--color-text-dim)] dark:text-gray-400">
Credits Used:{' '}
<strong className="text-[color:var(--color-text)] dark:text-white font-bold">
{credits.totalCreditsUsed.toLocaleString()}
</strong>
</span>
{credits.plannerCreditsUsed > 0 && (
<>
<span className="text-[color:var(--color-stroke)] dark:text-gray-600"></span>
<span className="text-[color:var(--color-text-dim)] dark:text-gray-400">
Planner:{' '}
<strong className="font-bold" style={{ color: 'var(--color-primary)' }}>
{credits.plannerCreditsUsed.toLocaleString()}
</strong>
</span>
</>
)}
{credits.writerCreditsUsed > 0 && (
<>
<span className="text-[color:var(--color-stroke)] dark:text-gray-600"></span>
<span className="text-[color:var(--color-text-dim)] dark:text-gray-400">
Writer:{' '}
<strong className="font-bold" style={{ color: 'var(--color-success)' }}>
{credits.writerCreditsUsed.toLocaleString()}
</strong>
</span>
</>
)}
</div>
)}
{/* Analytics Link */}
{analyticsHref && (
<div className="pt-3 mt-3 border-t border-[color:var(--color-stroke)] dark:border-gray-800">
<Link
to={analyticsHref}
className="text-sm font-medium hover:underline flex items-center gap-1 text-brand-500"
>
View Full Analytics
<ChevronRightIcon className="w-4 h-4" />
</Link>
</div>
)}
</>
)}
</Card>
);
}
export { WorkflowCompletionWidget };

View File

@@ -33,14 +33,24 @@ interface WorkflowPipelineWidgetProps {
loading?: boolean;
}
/**
* Pipeline stages with solid colored backgrounds matching Automation/Usage pages:
* - Sites: brand/primary (blue)
* - Keywords: brand/primary (blue)
* - Clusters: purple
* - Ideas: warning/amber
* - Tasks: brand/primary (blue)
* - Drafts/Content: success/green
* - Published: success/green
*/
const stages = [
{ key: 'sites', label: 'Sites', icon: GridIcon, href: '/sites', color: 'text-brand-600 dark:text-brand-400' },
{ key: 'keywords', label: 'Keywords', icon: ListIcon, href: '/planner/keywords', color: 'text-brand-600 dark:text-brand-400' },
{ key: 'clusters', label: 'Clusters', icon: GroupIcon, href: '/planner/clusters', color: 'text-purple-600 dark:text-purple-400' },
{ key: 'ideas', label: 'Ideas', icon: BoltIcon, href: '/planner/ideas', color: 'text-warning-600 dark:text-warning-400' },
{ key: 'tasks', label: 'Tasks', icon: CheckCircleIcon, href: '/writer/tasks', color: 'text-purple-600 dark:text-purple-400' },
{ key: 'drafts', label: 'Drafts', icon: FileTextIcon, href: '/writer/content', color: 'text-success-600 dark:text-success-400' },
{ key: 'published', label: 'Published', icon: PaperPlaneIcon, href: '/writer/published', color: 'text-purple-600 dark:text-purple-400' },
{ key: 'sites', label: 'Sites', icon: GridIcon, href: '/sites', gradient: 'from-brand-500 to-brand-600' },
{ key: 'keywords', label: 'Keywords', icon: ListIcon, href: '/planner/keywords', gradient: 'from-brand-500 to-brand-600' },
{ key: 'clusters', label: 'Clusters', icon: GroupIcon, href: '/planner/clusters', gradient: 'from-purple-500 to-purple-600' },
{ key: 'ideas', label: 'Ideas', icon: BoltIcon, href: '/planner/ideas', gradient: 'from-warning-500 to-warning-600' },
{ key: 'tasks', label: 'Tasks', icon: CheckCircleIcon, href: '/writer/tasks', gradient: 'from-brand-500 to-brand-600' },
{ key: 'drafts', label: 'Drafts', icon: FileTextIcon, href: '/writer/content', gradient: 'from-success-500 to-success-600' },
{ key: 'published', label: 'Published', icon: PaperPlaneIcon, href: '/writer/published', gradient: 'from-success-500 to-success-600' },
] as const;
// Small filled arrow triangle component
@@ -79,13 +89,13 @@ export default function WorkflowPipelineWidget({ data, loading }: WorkflowPipeli
to={stage.href}
className="flex flex-col items-center group min-w-[60px]"
>
<div className="p-2.5 rounded-lg bg-gray-50 dark:bg-gray-800 group-hover:bg-brand-50 dark:group-hover:bg-brand-900/20 transition-colors border border-transparent group-hover:border-brand-200 dark:group-hover:border-brand-800">
<Icon className={`w-6 h-6 ${stage.color}`} />
<div className={`p-2.5 rounded-xl bg-gradient-to-br ${stage.gradient} shadow-md group-hover:shadow-lg group-hover:scale-105 transition-all`}>
<Icon className="w-6 h-6 text-white" />
</div>
<span className="text-sm text-gray-700 dark:text-gray-300 mt-1.5 font-medium">
{stage.label}
</span>
<span className={`text-lg font-bold ${stage.color}`}>
<span className="text-lg font-bold text-gray-900 dark:text-white">
{loading ? '—' : typeof count === 'number' ? count.toLocaleString() : count}
</span>
</Link>