298 lines
12 KiB
TypeScript
298 lines
12 KiB
TypeScript
/**
|
|
* StandardThreeWidgetFooter - Enhanced 3-Column Layout with Standardized Widgets
|
|
*
|
|
* 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)
|
|
*
|
|
* Use this component for consistent data across all Planner and Writer module pages.
|
|
*/
|
|
|
|
import React from 'react';
|
|
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 (moved from ThreeWidgetFooter.tsx)
|
|
// ============================================================================
|
|
|
|
/** 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;
|
|
/** 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 */
|
|
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 }>;
|
|
}
|
|
|
|
// ============================================================================
|
|
// TYPE DEFINITIONS
|
|
// ============================================================================
|
|
|
|
export interface StandardThreeWidgetFooterProps {
|
|
pageProgress: PageProgressWidget;
|
|
/** @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;
|
|
}
|
|
|
|
// ============================================================================
|
|
// COLOR UTILITIES
|
|
// ============================================================================
|
|
|
|
const getProgressBarStyle = (color: SubmoduleColor = 'blue'): React.CSSProperties => {
|
|
const colorMap: Record<SubmoduleColor, string> = {
|
|
blue: 'var(--color-primary)',
|
|
green: 'var(--color-success)',
|
|
amber: 'var(--color-warning)',
|
|
purple: 'var(--color-info)',
|
|
};
|
|
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 (
|
|
<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}
|
|
</h3>
|
|
|
|
{/* 2x2 Metrics Grid */}
|
|
<div className="grid grid-cols-2 gap-x-6 gap-y-3 mb-5">
|
|
{widget.metrics.slice(0, 4).map((metric, idx) => (
|
|
<div key={idx} className="flex items-baseline justify-between">
|
|
<span className="text-sm text-[color:var(--color-text-dim)] dark:text-gray-400">{metric.label}</span>
|
|
<div className="flex items-baseline gap-1.5">
|
|
<span className="text-lg font-bold text-[color:var(--color-text)] dark:text-white tabular-nums">
|
|
{typeof metric.value === 'number' ? metric.value.toLocaleString() : metric.value}
|
|
</span>
|
|
{metric.percentage && (
|
|
<span className="text-xs text-[color:var(--color-text-dim)] dark:text-gray-400">({metric.percentage})</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Progress Bar */}
|
|
<div className="mb-4">
|
|
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full rounded-full transition-all duration-500"
|
|
style={{
|
|
...getProgressBarStyle(progressColor),
|
|
width: `${Math.min(100, Math.max(0, widget.progress.value))}%`
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="flex justify-between items-center mt-2">
|
|
<span className="text-sm font-medium text-[color:var(--color-text)] dark:text-gray-300">{widget.progress.label}</span>
|
|
<span className="text-sm font-bold text-[color:var(--color-text)] dark:text-white">{widget.progress.value}%</span>
|
|
</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 ${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>
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// WIDGET 2: MODULE STATS
|
|
// ============================================================================
|
|
|
|
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 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}
|
|
</h3>
|
|
|
|
{/* Pipeline Rows */}
|
|
<div className="space-y-4 mb-4">
|
|
{widget.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">
|
|
{row.fromHref ? (
|
|
<Link
|
|
to={row.fromHref}
|
|
className="text-sm font-medium hover:underline text-brand-500"
|
|
>
|
|
{row.fromLabel}
|
|
</Link>
|
|
) : (
|
|
<span className="text-sm font-medium text-[color:var(--color-text)] dark:text-gray-300">{row.fromLabel}</span>
|
|
)}
|
|
<span className="text-lg font-bold tabular-nums text-brand-500">
|
|
{row.fromValue}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Arrow icon */}
|
|
<ChevronRightIcon
|
|
className="w-6 h-6 flex-shrink-0 mx-2 text-brand-500"
|
|
/>
|
|
|
|
{/* To side */}
|
|
<div className="flex items-center gap-2">
|
|
{row.toHref ? (
|
|
<Link
|
|
to={row.toHref}
|
|
className="text-sm font-medium hover:underline text-brand-500"
|
|
>
|
|
{row.toLabel}
|
|
</Link>
|
|
) : (
|
|
<span className="text-sm font-medium text-[color:var(--color-text)] dark:text-gray-300">{row.toLabel}</span>
|
|
)}
|
|
<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={{
|
|
...getProgressBarStyle(row.color || 'blue'),
|
|
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">
|
|
{widget.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>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAIN COMPONENT
|
|
// ============================================================================
|
|
|
|
export default function StandardThreeWidgetFooter({
|
|
pageProgress,
|
|
moduleStats,
|
|
module,
|
|
submoduleColor = 'blue',
|
|
showCredits = true,
|
|
analyticsHref = '/account/usage',
|
|
className = '',
|
|
}: StandardThreeWidgetFooterProps) {
|
|
// Determine which module to show - prefer explicit module prop
|
|
const moduleType: ModuleType = module || (moduleStats?.title?.toLowerCase().includes('planner') ? 'planner' : 'writer');
|
|
|
|
return (
|
|
<div className={`pt-4 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} />
|
|
{/* Use standardized module widget for consistent data */}
|
|
<StandardizedModuleWidget module={moduleType} />
|
|
<WorkflowCompletionWidget
|
|
showCredits={showCredits}
|
|
analyticsHref={analyticsHref}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Export sub-components
|
|
export { PageProgressCard, ModuleStatsCard };
|