Skip to content

Commit ef6fb5e

Browse files
CopilotCopilot
andcommitted
feat(console): add ActivityFeed sidebar panel component (Phase 17 L1)
Add a lightweight ActivityFeed component that displays recent activity items (create, update, delete, comment) in a slide-out Sheet panel. - Shows activity items with type-specific icons and colors - Formats timestamps as relative time (e.g. '2m ago') - Validates timestamp input to handle invalid dates gracefully - Displays badge count on the bell trigger button - Shows empty state when no activities exist - Uses local state only (L1 - no server integration) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3d414ff commit ef6fb5e

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* ActivityFeed
3+
*
4+
* Sidebar panel that displays recent activity items (create, update, delete,
5+
* comment). Opens as a slide-out Sheet triggered by a bell icon button.
6+
* Phase 17 L1 – local state only, no server integration.
7+
* @module
8+
*/
9+
10+
import { useState } from 'react';
11+
import {
12+
Button,
13+
Sheet,
14+
SheetContent,
15+
SheetHeader,
16+
SheetTitle,
17+
SheetTrigger,
18+
} from '@object-ui/components';
19+
import { Bell, Plus, Pencil, Trash2, MessageSquare } from 'lucide-react';
20+
21+
export interface ActivityItem {
22+
id: string;
23+
type: 'create' | 'update' | 'delete' | 'comment';
24+
objectName: string;
25+
recordId?: string;
26+
user: string;
27+
description: string;
28+
timestamp: string;
29+
}
30+
31+
export interface ActivityFeedProps {
32+
activities?: ActivityItem[];
33+
className?: string;
34+
}
35+
36+
const typeConfig: Record<
37+
ActivityItem['type'],
38+
{ icon: React.ElementType; color: string }
39+
> = {
40+
create: { icon: Plus, color: 'text-green-500' },
41+
update: { icon: Pencil, color: 'text-blue-500' },
42+
delete: { icon: Trash2, color: 'text-red-500' },
43+
comment: { icon: MessageSquare, color: 'text-amber-500' },
44+
};
45+
46+
/** Format an ISO timestamp as a relative string (e.g. "2m ago"). */
47+
function formatRelativeTime(iso: string): string {
48+
const ms = new Date(iso).getTime();
49+
if (Number.isNaN(ms)) return '';
50+
const seconds = Math.floor((Date.now() - ms) / 1000);
51+
if (seconds < 60) return `${Math.max(seconds, 0)}s ago`;
52+
const minutes = Math.floor(seconds / 60);
53+
if (minutes < 60) return `${minutes}m ago`;
54+
const hours = Math.floor(minutes / 60);
55+
if (hours < 24) return `${hours}h ago`;
56+
const days = Math.floor(hours / 24);
57+
return `${days}d ago`;
58+
}
59+
60+
export function ActivityFeed({ activities = [], className }: ActivityFeedProps) {
61+
const [open, setOpen] = useState(false);
62+
63+
return (
64+
<Sheet open={open} onOpenChange={setOpen}>
65+
<SheetTrigger asChild>
66+
<Button
67+
variant="ghost"
68+
size="icon"
69+
className={className ?? 'h-8 w-8'}
70+
aria-label="Activity feed"
71+
>
72+
<Bell className="h-4 w-4" />
73+
{activities.length > 0 && (
74+
<span className="absolute -top-0.5 -right-0.5 h-3.5 w-3.5 rounded-full bg-primary text-[9px] font-bold text-primary-foreground flex items-center justify-center">
75+
{activities.length > 9 ? '9+' : activities.length}
76+
</span>
77+
)}
78+
</Button>
79+
</SheetTrigger>
80+
81+
<SheetContent side="right" className="w-80 sm:w-96">
82+
<SheetHeader>
83+
<SheetTitle>Recent Activity</SheetTitle>
84+
</SheetHeader>
85+
86+
{activities.length === 0 ? (
87+
<div className="flex flex-col items-center justify-center gap-2 py-16 text-muted-foreground">
88+
<Bell className="h-8 w-8 opacity-40" />
89+
<p className="text-sm">No recent activity</p>
90+
</div>
91+
) : (
92+
<ul className="mt-4 space-y-1 overflow-y-auto max-h-[calc(100vh-8rem)]">
93+
{activities.map((item) => {
94+
const { icon: Icon, color } = typeConfig[item.type];
95+
return (
96+
<li
97+
key={item.id}
98+
className="flex items-start gap-3 rounded-md px-2 py-2 hover:bg-muted/50 transition-colors"
99+
>
100+
<span className={`mt-0.5 shrink-0 ${color}`}>
101+
<Icon className="h-4 w-4" />
102+
</span>
103+
<div className="min-w-0 flex-1">
104+
<p className="text-sm leading-snug">{item.description}</p>
105+
<p className="mt-0.5 text-xs text-muted-foreground">
106+
{item.user} · {formatRelativeTime(item.timestamp)}
107+
</p>
108+
</div>
109+
</li>
110+
);
111+
})}
112+
</ul>
113+
)}
114+
</SheetContent>
115+
</Sheet>
116+
);
117+
}

0 commit comments

Comments
 (0)