Dashboard widgets: - WorkflowPipelineWidget: Sites now has transparent bg with colored icon - Tasks stage uses navy (gray-700/800), Content/Drafts use blue (brand) - AIOperationsWidget: Content now uses blue (brand) instead of green - RecentActivityWidget: Content activity now uses blue (brand) - QuickActionsWidget: Tasks step uses navy, Content uses blue Automation components: - AutomationPage STAGE_CONFIG: Tasks→Content now navy, Content→Prompts blue - GlobalProgressBar: Updated stage colors to match new scheme - CurrentProcessingCard: Stage colors match new module scheme Color scheme: - Planner Pipeline (Blue → Pink → Amber): Keywords, Clusters, Ideas - Writer Pipeline (Navy → Blue → Pink → Green): Tasks, Content, Images, Published
152 lines
5.5 KiB
TypeScript
152 lines
5.5 KiB
TypeScript
/**
|
|
* RecentActivityWidget - Shows last 5 significant operations
|
|
* Displays AI task completions, publishing events, etc.
|
|
*/
|
|
|
|
import { Link } from 'react-router-dom';
|
|
import {
|
|
GroupIcon,
|
|
BoltIcon,
|
|
FileTextIcon,
|
|
FileIcon,
|
|
PaperPlaneIcon,
|
|
ListIcon,
|
|
AlertIcon,
|
|
CheckCircleIcon,
|
|
} from '../../icons';
|
|
|
|
export interface ActivityItem {
|
|
id: string;
|
|
type: 'clustering' | 'ideas' | 'content' | 'images' | 'published' | 'keywords' | 'error' | 'sync';
|
|
title: string;
|
|
description: string;
|
|
timestamp: Date;
|
|
href?: string;
|
|
success?: boolean;
|
|
}
|
|
|
|
interface RecentActivityWidgetProps {
|
|
activities: ActivityItem[];
|
|
loading?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Activity config with solid colored backgrounds for visual distinction:
|
|
* - clustering: purple/pink (matches Clusters module)
|
|
* - ideas: warning/amber (matches Ideas module)
|
|
* - content: brand/primary (blue) - matches Writer content module
|
|
* - images: purple/pink (matches Images module)
|
|
* - published: success/green (matches Published module)
|
|
* - keywords: brand/primary (blue) - matches Keywords module
|
|
* - error: error/red
|
|
* - sync: success/green
|
|
*/
|
|
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-brand-500 to-brand-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, gradient: 'from-gray-500 to-gray-600' };
|
|
|
|
function formatRelativeTime(date: Date): string {
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMs / 3600000);
|
|
const diffDays = Math.floor(diffMs / 86400000);
|
|
|
|
if (diffMins < 1) return 'Just now';
|
|
if (diffMins < 60) return `${diffMins}m ago`;
|
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
if (diffDays === 1) return 'Yesterday';
|
|
if (diffDays < 7) return `${diffDays}d ago`;
|
|
|
|
return date.toLocaleDateString();
|
|
}
|
|
|
|
export default function RecentActivityWidget({ activities, loading }: RecentActivityWidgetProps) {
|
|
return (
|
|
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-5">
|
|
{/* Header */}
|
|
<h3 className="text-base font-semibold text-gray-800 dark:text-gray-200 uppercase tracking-wide mb-4">
|
|
Recent Activity
|
|
</h3>
|
|
|
|
{/* Activity List */}
|
|
<div className="space-y-3">
|
|
{loading ? (
|
|
// Loading skeleton
|
|
Array.from({ length: 5 }).map((_, i) => (
|
|
<div key={i} className="flex items-start gap-3 animate-pulse">
|
|
<div className="w-9 h-9 rounded-lg bg-gray-100 dark:bg-gray-800"></div>
|
|
<div className="flex-1">
|
|
<div className="h-4 w-3/4 bg-gray-100 dark:bg-gray-800 rounded mb-2"></div>
|
|
<div className="h-3 w-1/4 bg-gray-100 dark:bg-gray-800 rounded"></div>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : activities.length === 0 ? (
|
|
<div className="text-center py-8">
|
|
<p className="text-base text-gray-600 dark:text-gray-400">No recent activity</p>
|
|
<p className="text-sm text-gray-500 dark:text-gray-500 mt-1">
|
|
AI operations will appear here
|
|
</p>
|
|
</div>
|
|
) : (
|
|
activities.slice(0, 5).map((activity) => {
|
|
const config = activityConfig[activity.type] || defaultActivityConfig;
|
|
const Icon = config.icon;
|
|
|
|
const content = (
|
|
<div className="flex items-start gap-3">
|
|
<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">
|
|
{activity.title}
|
|
</p>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
{formatRelativeTime(activity.timestamp)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return activity.href ? (
|
|
<Link
|
|
key={activity.id}
|
|
to={activity.href}
|
|
className="block hover:bg-gray-50 dark:hover:bg-gray-800/50 rounded-lg p-1 -m-1 transition-colors"
|
|
>
|
|
{content}
|
|
</Link>
|
|
) : (
|
|
<div key={activity.id} className="p-1 -m-1">
|
|
{content}
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
|
|
{/* View All Link */}
|
|
{activities.length > 0 && (
|
|
<Link
|
|
to="/account/activity"
|
|
className="block mt-3 pt-3 border-t border-gray-100 dark:border-gray-800 text-xs font-medium text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300 text-center"
|
|
>
|
|
View All Activity →
|
|
</Link>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|