final polish phase 1
This commit is contained in:
137
frontend/src/components/dashboard/RecentActivityWidget.tsx
Normal file
137
frontend/src/components/dashboard/RecentActivityWidget.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
const activityConfig = {
|
||||
clustering: { icon: GroupIcon, color: 'text-purple-600 dark:text-purple-400', bgColor: 'bg-purple-100 dark:bg-purple-900/40' },
|
||||
ideas: { icon: BoltIcon, color: 'text-orange-600 dark:text-orange-400', bgColor: 'bg-orange-100 dark:bg-orange-900/40' },
|
||||
content: { icon: FileTextIcon, color: 'text-green-600 dark:text-green-400', bgColor: 'bg-green-100 dark:bg-green-900/40' },
|
||||
images: { icon: FileIcon, color: 'text-pink-600 dark:text-pink-400', bgColor: 'bg-pink-100 dark:bg-pink-900/40' },
|
||||
published: { icon: PaperPlaneIcon, color: 'text-emerald-600 dark:text-emerald-400', bgColor: 'bg-emerald-100 dark:bg-emerald-900/40' },
|
||||
keywords: { icon: ListIcon, color: 'text-blue-600 dark:text-blue-400', bgColor: 'bg-blue-100 dark:bg-blue-900/40' },
|
||||
error: { icon: AlertIcon, color: 'text-red-600 dark:text-red-400', bgColor: 'bg-red-100 dark:bg-red-900/40' },
|
||||
sync: { icon: CheckCircleIcon, color: 'text-teal-600 dark:text-teal-400', bgColor: 'bg-teal-100 dark:bg-teal-900/40' },
|
||||
};
|
||||
|
||||
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];
|
||||
const Icon = config.icon;
|
||||
|
||||
const content = (
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`w-9 h-9 rounded-lg ${config.bgColor} flex items-center justify-center flex-shrink-0`}>
|
||||
<Icon className={`w-5 h-5 ${config.color}`} />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user