diff --git a/frontend/src/components/dashboard/ThreeWidgetFooter.tsx b/frontend/src/components/dashboard/ThreeWidgetFooter.tsx new file mode 100644 index 00000000..01b44910 --- /dev/null +++ b/frontend/src/components/dashboard/ThreeWidgetFooter.tsx @@ -0,0 +1,385 @@ +/** + * ThreeWidgetFooter - 3-Column Layout for Table Page Footers + * + * 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 │ + * └─────────────────────────────────────────────────────────────────────────────────────┘ + * + * 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 + */ + +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Card } from '../ui/card/Card'; +import { LightBulbIcon, ChevronRightIcon } from '@heroicons/react/24/solid'; + +// ============================================================================ +// TYPE DEFINITIONS +// ============================================================================ + +/** Submodule color type - matches headerMetrics accentColor */ +export type SubmoduleColor = 'blue' | 'green' | 'amber' | 'purple'; + +/** Widget 1: Page Progress - metrics in 2x2 grid + progress bar + hint */ +export interface PageProgressWidget { + title: string; + metrics: Array<{ label: string; value: string | number; percentage?: string }>; + progress: { value: number; label: string; color?: SubmoduleColor }; + hint?: string; + /** The submodule's accent color - progress bar uses this */ + submoduleColor?: SubmoduleColor; +} + +/** Widget 2: Module Stats - Pipeline flow with arrows and progress bars */ +export interface ModulePipelineRow { + fromLabel: string; + fromValue: number; + fromHref?: string; + actionLabel: string; + toLabel: string; + toValue: number; + toHref?: string; + progress: number; // 0-100 + /** Color for this pipeline row's progress bar */ + color?: SubmoduleColor; +} + +export interface ModuleStatsWidget { + title: string; + pipeline: ModulePipelineRow[]; + 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; +} + +export interface CompletionWidget { + title: string; + plannerItems: CompletionItem[]; + writerItems: CompletionItem[]; + creditsUsed?: number; + operationsCount?: number; + analyticsHref?: string; +} + +/** Main component props */ +export interface ThreeWidgetFooterProps { + pageProgress: PageProgressWidget; + moduleStats: ModuleStatsWidget; + completion: CompletionWidget; + submoduleColor?: SubmoduleColor; + className?: string; +} + +// ============================================================================ +// COLOR UTILITIES +// ============================================================================ + +const getProgressBarStyle = (color: SubmoduleColor = 'blue'): React.CSSProperties => { + const colorMap: Record = { + blue: 'var(--color-primary)', + green: 'var(--color-success)', + amber: 'var(--color-warning)', + purple: 'var(--color-purple)', + }; + return { backgroundColor: colorMap[color] }; +}; + +// ============================================================================ +// WIDGET 1: PAGE PROGRESS +// ============================================================================ + +function PageProgressCard({ widget, submoduleColor = 'blue' }: { widget: PageProgressWidget; submoduleColor?: SubmoduleColor }) { + const progressColor = widget.submoduleColor || widget.progress.color || submoduleColor; + + return ( + + {/* Header */} +

+ {widget.title} +

+ + {/* 2x2 Metrics Grid */} +
+ {widget.metrics.slice(0, 4).map((metric, idx) => ( +
+ {metric.label} +
+ + {typeof metric.value === 'number' ? metric.value.toLocaleString() : metric.value} + + {metric.percentage && ( + ({metric.percentage}) + )} +
+
+ ))} +
+ + {/* Progress Bar */} +
+
+
+
+
+ {widget.progress.label} + {widget.progress.value}% +
+
+ + {/* Hint with icon */} + {widget.hint && ( +
+ + {widget.hint} +
+ )} + + ); +} + +// ============================================================================ +// WIDGET 2: MODULE STATS +// ============================================================================ + +function ModuleStatsCard({ widget }: { widget: ModuleStatsWidget }) { + return ( + + {/* Header */} +

+ {widget.title} +

+ + {/* Pipeline Rows */} +
+ {widget.pipeline.map((row, idx) => ( +
+ {/* Row header: FromLabel Value ► ToLabel Value */} +
+ {/* From side */} +
+ {row.fromHref ? ( + + {row.fromLabel} + + ) : ( + {row.fromLabel} + )} + + {row.fromValue} + +
+ + {/* Arrow icon */} + + + {/* To side */} +
+ {row.toHref ? ( + + {row.toLabel} + + ) : ( + {row.toLabel} + )} + + {row.toValue} + +
+
+ + {/* Progress bar */} +
+
+
+
+ ))} +
+ + {/* Navigation Links */} +
+ {widget.links.map((link, idx) => ( + + + {link.label} + + ))} +
+ + ); +} + +// ============================================================================ +// 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 ( +
+ {/* Tree prefix */} + {prefix} + + {/* Label */} + {item.label} + + {/* Progress bar */} +
+
+
+ + {/* Value */} + + {item.value} + +
+ ); + }; + + return ( + + {/* Header */} +

+ {widget.title} +

+ + {/* Two-column layout: Planner | Writer */} +
+ {/* Planner Column */} +
+
+ Planner +
+
+ {widget.plannerItems.map((item, idx) => + renderItem(item, idx === widget.plannerItems.length - 1) + )} +
+
+ + {/* Writer Column */} +
+
+ Writer +
+
+ {widget.writerItems.map((item, idx) => + renderItem(item, idx === widget.writerItems.length - 1) + )} +
+
+
+ + {/* Footer Stats - Credits Used & Operations */} + {(widget.creditsUsed !== undefined || widget.operationsCount !== undefined) && ( +
+ {widget.creditsUsed !== undefined && ( + + Credits Used: {widget.creditsUsed.toLocaleString()} + + )} + {widget.creditsUsed !== undefined && widget.operationsCount !== undefined && ( + + )} + {widget.operationsCount !== undefined && ( + + Operations: {widget.operationsCount} + + )} +
+ )} + + {/* Analytics Link */} + {widget.analyticsHref && ( +
+ + View Full Analytics + + +
+ )} +
+ ); +} + +// ============================================================================ +// MAIN COMPONENT +// ============================================================================ + +export default function ThreeWidgetFooter({ + pageProgress, + moduleStats, + completion, + submoduleColor = 'blue', + className = '', +}: ThreeWidgetFooterProps) { + return ( +
+
+ + + +
+
+ ); +} + +// Also export sub-components for flexibility +export { PageProgressCard, ModuleStatsCard, CompletionCard }; diff --git a/frontend/src/hooks/useThreeWidgetFooter.ts b/frontend/src/hooks/useThreeWidgetFooter.ts new file mode 100644 index 00000000..bf93443f --- /dev/null +++ b/frontend/src/hooks/useThreeWidgetFooter.ts @@ -0,0 +1,390 @@ +/** + * useThreeWidgetFooter - Hook to build ThreeWidgetFooter props + * + * Provides helper functions to construct the three widgets: + * - Page Progress (current page metrics) + * - Module Stats (workflow pipeline) + * - Completion Stats (both modules summary) + * + * Usage: + * const footerProps = useThreeWidgetFooter({ + * module: 'planner', + * currentPage: 'keywords', + * plannerData: { keywords: [...], clusters: [...] }, + * completionData: { ... } + * }); + */ + +import { useMemo } from 'react'; +import type { + ThreeWidgetFooterProps, + PageProgressWidget, + ModuleStatsWidget, + CompletionWidget, + SubmoduleColor, +} from '../components/dashboard/ThreeWidgetFooter'; + +// ============================================================================ +// DATA INTERFACES +// ============================================================================ + +interface PlannerPageData { + keywords?: Array<{ cluster_id?: number | null; volume?: number }>; + clusters?: Array<{ ideas_count?: number; keywords_count?: number }>; + ideas?: Array<{ status?: string }>; + totalKeywords?: number; + totalClusters?: number; + totalIdeas?: number; +} + +interface WriterPageData { + tasks?: Array<{ status?: string }>; + content?: Array<{ status?: string; has_generated_images?: boolean }>; + totalTasks?: number; + totalContent?: number; + totalPublished?: number; +} + +interface CompletionData { + keywordsClustered?: number; + clustersCreated?: number; + ideasGenerated?: number; + contentGenerated?: number; + imagesCreated?: number; + articlesPublished?: number; + creditsUsed?: number; + totalOperations?: number; +} + +interface UseThreeWidgetFooterOptions { + module: 'planner' | 'writer'; + currentPage: 'keywords' | 'clusters' | 'ideas' | 'tasks' | 'content' | 'images' | 'review' | 'published'; + plannerData?: PlannerPageData; + writerData?: WriterPageData; + completionData?: CompletionData; +} + +// ============================================================================ +// PLANNER PAGE PROGRESS BUILDERS +// ============================================================================ + +function buildKeywordsPageProgress(data: PlannerPageData): PageProgressWidget { + const keywords = data.keywords || []; + const totalKeywords = data.totalKeywords || keywords.length; + const clusteredCount = keywords.filter(k => k.cluster_id).length; + const unmappedCount = keywords.filter(k => !k.cluster_id).length; + const totalVolume = keywords.reduce((sum, k) => sum + (k.volume || 0), 0); + const clusteredPercent = totalKeywords > 0 ? Math.round((clusteredCount / totalKeywords) * 100) : 0; + + return { + title: 'Page Progress', + metrics: [ + { label: 'Keywords', value: totalKeywords }, + { label: 'Clustered', value: clusteredCount, percentage: `${clusteredPercent}%` }, + { label: 'Unmapped', value: unmappedCount }, + { label: 'Volume', value: totalVolume >= 1000 ? `${(totalVolume / 1000).toFixed(1)}K` : totalVolume }, + ], + progress: { + value: clusteredPercent, + label: `${clusteredPercent}% Clustered`, + color: clusteredPercent >= 80 ? 'green' : 'blue', + }, + hint: unmappedCount > 0 ? `${unmappedCount} keywords ready to cluster` : 'All keywords clustered!', + }; +} + +function buildClustersPageProgress(data: PlannerPageData): PageProgressWidget { + const clusters = data.clusters || []; + const totalClusters = data.totalClusters || clusters.length; + const withIdeas = clusters.filter(c => (c.ideas_count || 0) > 0).length; + const totalKeywords = clusters.reduce((sum, c) => sum + (c.keywords_count || 0), 0); + const readyClusters = clusters.filter(c => (c.ideas_count || 0) === 0).length; + const ideasPercent = totalClusters > 0 ? Math.round((withIdeas / totalClusters) * 100) : 0; + + return { + title: 'Page Progress', + metrics: [ + { label: 'Clusters', value: totalClusters }, + { label: 'With Ideas', value: withIdeas, percentage: `${ideasPercent}%` }, + { label: 'Keywords', value: totalKeywords }, + { label: 'Ready', value: readyClusters }, + ], + progress: { + value: ideasPercent, + label: `${ideasPercent}% Have Ideas`, + color: ideasPercent >= 70 ? 'green' : 'blue', + }, + hint: readyClusters > 0 ? `${readyClusters} clusters ready for idea generation` : 'All clusters have ideas!', + }; +} + +function buildIdeasPageProgress(data: PlannerPageData): PageProgressWidget { + const ideas = data.ideas || []; + const totalIdeas = data.totalIdeas || ideas.length; + const inTasks = ideas.filter(i => i.status === 'completed' || i.status === 'queued').length; + const pending = ideas.filter(i => i.status === 'new').length; + const convertedPercent = totalIdeas > 0 ? Math.round((inTasks / totalIdeas) * 100) : 0; + + return { + title: 'Page Progress', + metrics: [ + { label: 'Ideas', value: totalIdeas }, + { label: 'In Tasks', value: inTasks, percentage: `${convertedPercent}%` }, + { label: 'Pending', value: pending }, + { label: 'From Clusters', value: data.totalClusters || 0 }, + ], + progress: { + value: convertedPercent, + label: `${convertedPercent}% Converted`, + color: convertedPercent >= 60 ? 'green' : 'blue', + }, + hint: pending > 0 ? `${pending} ideas ready to become tasks` : 'All ideas converted!', + }; +} + +// ============================================================================ +// WRITER PAGE PROGRESS BUILDERS +// ============================================================================ + +function buildTasksPageProgress(data: WriterPageData): PageProgressWidget { + const tasks = data.tasks || []; + const total = data.totalTasks || tasks.length; + const completed = tasks.filter(t => t.status === 'completed').length; + const queue = tasks.filter(t => t.status === 'queued').length; + const processing = tasks.filter(t => t.status === 'in_progress').length; + const completedPercent = total > 0 ? Math.round((completed / total) * 100) : 0; + + return { + title: 'Page Progress', + metrics: [ + { label: 'Total', value: total }, + { label: 'Complete', value: completed, percentage: `${completedPercent}%` }, + { label: 'Queue', value: queue }, + { label: 'Processing', value: processing }, + ], + progress: { + value: completedPercent, + label: `${completedPercent}% Generated`, + color: completedPercent >= 60 ? 'green' : 'blue', + }, + hint: queue > 0 ? `${queue} tasks in queue for content generation` : 'All tasks processed!', + }; +} + +function buildContentPageProgress(data: WriterPageData): PageProgressWidget { + const content = data.content || []; + const drafts = content.filter(c => c.status === 'draft').length; + const hasImages = content.filter(c => c.has_generated_images).length; + const ready = content.filter(c => c.status === 'review' || c.status === 'published').length; + const imagesPercent = drafts > 0 ? Math.round((hasImages / drafts) * 100) : 0; + + return { + title: 'Page Progress', + metrics: [ + { label: 'Drafts', value: drafts }, + { label: 'Has Images', value: hasImages, percentage: `${imagesPercent}%` }, + { label: 'Total Words', value: '—' }, // Would need word count from API + { label: 'Ready', value: ready }, + ], + progress: { + value: imagesPercent, + label: `${imagesPercent}% Have Images`, + color: imagesPercent >= 70 ? 'green' : 'blue', + }, + hint: drafts - hasImages > 0 ? `${drafts - hasImages} drafts need images before review` : 'All drafts have images!', + }; +} + +// ============================================================================ +// MODULE STATS BUILDERS +// ============================================================================ + +function buildPlannerModuleStats(data: PlannerPageData): ModuleStatsWidget { + const keywords = data.keywords || []; + const clusters = data.clusters || []; + const ideas = data.ideas || []; + + const totalKeywords = data.totalKeywords || keywords.length; + const totalClusters = data.totalClusters || clusters.length; + const totalIdeas = data.totalIdeas || ideas.length; + const clusteredKeywords = keywords.filter(k => k.cluster_id).length; + const clustersWithIdeas = clusters.filter(c => (c.ideas_count || 0) > 0).length; + const ideasInTasks = ideas.filter(i => i.status === 'completed' || i.status === 'queued').length; + + return { + title: 'Planner Module', + pipeline: [ + { + fromLabel: 'Keywords', + fromValue: totalKeywords, + toLabel: 'Clusters', + toValue: totalClusters, + actionLabel: 'Auto Cluster', + progress: totalKeywords > 0 ? Math.round((clusteredKeywords / totalKeywords) * 100) : 0, + color: 'blue', + }, + { + fromLabel: 'Clusters', + fromValue: totalClusters, + toLabel: 'Ideas', + toValue: totalIdeas, + actionLabel: 'Generate Ideas', + progress: totalClusters > 0 ? Math.round((clustersWithIdeas / totalClusters) * 100) : 0, + color: 'green', + }, + { + fromLabel: 'Ideas', + fromValue: totalIdeas, + toLabel: 'Tasks', + toValue: ideasInTasks, + actionLabel: 'Create Tasks', + progress: totalIdeas > 0 ? Math.round((ideasInTasks / totalIdeas) * 100) : 0, + color: 'amber', + }, + ], + links: [ + { label: 'Keywords', href: '/planner/keywords' }, + { label: 'Clusters', href: '/planner/clusters' }, + { label: 'Ideas', href: '/planner/ideas' }, + ], + }; +} + +function buildWriterModuleStats(data: WriterPageData): ModuleStatsWidget { + const tasks = data.tasks || []; + const content = data.content || []; + + const totalTasks = data.totalTasks || tasks.length; + const completedTasks = tasks.filter(t => t.status === 'completed').length; + const drafts = content.filter(c => c.status === 'draft').length; + const withImages = content.filter(c => c.has_generated_images).length; + const ready = content.filter(c => c.status === 'review').length; + const published = data.totalPublished || content.filter(c => c.status === 'published').length; + + return { + title: 'Writer Module', + pipeline: [ + { + fromLabel: 'Tasks', + fromValue: totalTasks, + toLabel: 'Drafts', + toValue: drafts, + actionLabel: 'Generate Content', + progress: totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0, + color: 'blue', + }, + { + fromLabel: 'Drafts', + fromValue: drafts, + toLabel: 'Images', + toValue: withImages, + actionLabel: 'Generate Images', + progress: drafts > 0 ? Math.round((withImages / drafts) * 100) : 0, + color: 'purple', + }, + { + fromLabel: 'Ready', + fromValue: ready, + toLabel: 'Published', + toValue: published, + actionLabel: 'Review & Publish', + progress: ready > 0 ? Math.round((published / (ready + published)) * 100) : 0, + color: 'green', + }, + ], + links: [ + { label: 'Tasks', href: '/writer/tasks' }, + { label: 'Content', href: '/writer/content' }, + { label: 'Images', href: '/writer/images' }, + { label: 'Published', href: '/writer/published' }, + ], + }; +} + +// ============================================================================ +// COMPLETION STATS BUILDER +// ============================================================================ + +function buildCompletionStats(data: CompletionData): CompletionWidget { + return { + title: 'Workflow Completion', + plannerItems: [ + { label: 'Keywords Clustered', value: data.keywordsClustered || 0, color: 'blue' }, + { label: 'Clusters Created', value: data.clustersCreated || 0, color: 'green' }, + { label: 'Ideas Generated', value: data.ideasGenerated || 0, color: 'amber' }, + ], + writerItems: [ + { label: 'Content Generated', value: data.contentGenerated || 0, color: 'blue' }, + { label: 'Images Created', value: data.imagesCreated || 0, color: 'purple' }, + { label: 'Articles Published', value: data.articlesPublished || 0, color: 'green' }, + ], + creditsUsed: data.creditsUsed, + operationsCount: data.totalOperations, + analyticsHref: '/account/usage', + }; +} + +// ============================================================================ +// MAIN HOOK +// ============================================================================ + +export function useThreeWidgetFooter(options: UseThreeWidgetFooterOptions): ThreeWidgetFooterProps { + const { module, currentPage, plannerData = {}, writerData = {}, completionData = {} } = options; + + return useMemo(() => { + // Build page progress based on current page + let pageProgress: PageProgressWidget; + + if (module === 'planner') { + switch (currentPage) { + case 'keywords': + pageProgress = buildKeywordsPageProgress(plannerData); + break; + case 'clusters': + pageProgress = buildClustersPageProgress(plannerData); + break; + case 'ideas': + pageProgress = buildIdeasPageProgress(plannerData); + break; + default: + pageProgress = buildKeywordsPageProgress(plannerData); + } + } else { + switch (currentPage) { + case 'tasks': + pageProgress = buildTasksPageProgress(writerData); + break; + case 'content': + case 'images': + case 'review': + pageProgress = buildContentPageProgress(writerData); + break; + default: + pageProgress = buildTasksPageProgress(writerData); + } + } + + // Build module stats + const moduleStats = module === 'planner' + ? buildPlannerModuleStats(plannerData) + : buildWriterModuleStats(writerData); + + // Build completion stats + const completion = buildCompletionStats(completionData); + + // Determine submodule color based on current page + let submoduleColor: SubmoduleColor = 'blue'; + if (currentPage === 'clusters') submoduleColor = 'green'; + if (currentPage === 'ideas') submoduleColor = 'amber'; + if (currentPage === 'images') submoduleColor = 'purple'; + + return { + pageProgress, + moduleStats, + completion, + submoduleColor, + }; + }, [module, currentPage, plannerData, writerData, completionData]); +} + +export default useThreeWidgetFooter;