Add ThreeWidgetFooter component and hook for 3-column table footer layout

- ThreeWidgetFooter.tsx: 3-column layout matching Section 3 of audit report
  - Widget 1: Page Progress (current page metrics + progress bar + hint)
  - Widget 2: Module Stats (workflow pipeline with progress bars)
  - Widget 3: Completion (both Planner/Writer stats + credits)
- useThreeWidgetFooter.ts: Hook to build widget props from data
  - Builds page progress for Keywords, Clusters, Ideas, Tasks, Content
  - Builds Planner/Writer module pipelines
  - Calculates completion stats from data

Uses CSS tokens from styles/tokens.css for consistent styling
This commit is contained in:
IGNY8 VPS (Salman)
2025-12-27 17:51:46 +00:00
parent 24cdb4fdf9
commit a145e6742e
2 changed files with 775 additions and 0 deletions

View File

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