many changes for modules widgets and colors and styling
This commit is contained in:
267
frontend/src/components/dashboard/WorkflowCompletionWidget.tsx
Normal file
267
frontend/src/components/dashboard/WorkflowCompletionWidget.tsx
Normal 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 };
|
||||
Reference in New Issue
Block a user