import { memo, useCallback, useMemo } from "react"; import { GitPullRequest, GitMerge, CheckCircle, XCircle, Plus, ArrowRightLeft, Settings, AlertTriangle, Folder } from "lucide-react"; import type { ActivityFeedEntry } from "../api"; export interface ActivityFeedProps { entries: ActivityFeedEntry[]; isLoading?: boolean; error?: string | null; projectNames?: Record; emptyMessage?: string; } const TYPE_CONFIG: Record = { "task:created": { label: "Created", icon: Plus, color: "var(--todo)" }, "task:moved": { label: "Moved", icon: ArrowRightLeft, color: "var(--in-progress)" }, "task:updated": { label: "Updated", icon: Settings, color: "var(--text-muted)" }, "task:deleted": { label: "Deleted", icon: XCircle, color: "var(--color-error)" }, "task:merged": { label: "Merged", icon: GitMerge, color: "var(--color-success)" }, "task:failed": { label: "Failed", icon: AlertTriangle, color: "var(--color-error)" }, "settings:updated": { label: "Settings", icon: Settings, color: "var(--text-muted)" }, }; function formatRelativeTime(timestamp: string): string { const date = new Date(timestamp); 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 < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } function formatFullTime(timestamp: string): string { const date = new Date(timestamp); return date.toLocaleString(); } interface ActivityFeedItemProps { entry: ActivityFeedEntry; projectName?: string; } function ActivityFeedItem({ entry, projectName }: ActivityFeedItemProps) { const config = TYPE_CONFIG[entry.type]; const Icon = config.icon; return (
{config.label} {projectName && ( {projectName} )}
{entry.taskId && ( {entry.taskId} )} {entry.taskTitle && ( {entry.taskTitle} )} {entry.details}
{formatRelativeTime(entry.timestamp)}
); } function areActivityFeedPropsEqual(previous: ActivityFeedProps, next: ActivityFeedProps): boolean { if (previous.isLoading !== next.isLoading) return false; if (previous.error !== next.error) return false; if (previous.entries.length !== next.entries.length) return false; for (let i = 0; i < previous.entries.length; i++) { const prev = previous.entries[i]; const curr = next.entries[i]; if (prev.id !== curr.id) return false; if (prev.timestamp !== curr.timestamp) return false; if (prev.type !== curr.type) return false; if (prev.details !== curr.details) return false; if (prev.taskId !== curr.taskId) return false; if (prev.taskTitle !== curr.taskTitle) return false; } return true; } function ActivityFeedInner({ entries, isLoading = false, error = null, projectNames = {}, emptyMessage = "No recent activity", }: ActivityFeedProps) { const getProjectName = useCallback((projectId: string): string => { return projectNames[projectId] || projectId; }, [projectNames]); const groupedEntries = useMemo(() => { const groups: { date: string; entries: ActivityFeedEntry[] }[] = []; let currentGroup: { date: string; entries: ActivityFeedEntry[] } | null = null; for (const entry of entries) { const date = new Date(entry.timestamp).toLocaleDateString(); if (!currentGroup || currentGroup.date !== date) { currentGroup = { date, entries: [] }; groups.push(currentGroup); } currentGroup.entries.push(entry); } return groups; }, [entries]); if (isLoading) { return (
{[1, 2, 3].map((i) => (
))}
); } if (error) { return (

{error}

); } if (entries.length === 0) { return (

{emptyMessage}

Activity will appear here when tasks are created, moved, or completed
); } return (
{groupedEntries.map((group) => (
{group.date} {group.entries.length} event{group.entries.length !== 1 ? "s" : ""}
{group.entries.map((entry) => ( ))}
))}
); } export const ActivityFeed = memo(ActivityFeedInner, areActivityFeedPropsEqual);