feat: add activity notifications panel with session feed

- Bell icon shows dot indicator for recent activity
- Dropdown panel shows recent sessions with status icons
- Links to session detail and sessions list page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-02-15 05:09:33 -05:00
parent c8e67515b1
commit 336e37d018
2 changed files with 108 additions and 4 deletions

View File

@@ -0,0 +1,105 @@
import { useState, useEffect, useRef } from 'react'
import { Link } from 'react-router-dom'
import { Bell, CheckCircle, Clock } from 'lucide-react'
import { sessionsApi } from '@/api/sessions'
import type { Session } from '@/types/session'
function timeAgo(dateStr: string): string {
const diff = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000)
if (diff < 60) return 'just now'
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
return `${Math.floor(diff / 86400)}d ago`
}
export function NotificationsPanel() {
const [open, setOpen] = useState(false)
const [sessions, setSessions] = useState<Session[]>([])
const [hasNew, setHasNew] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
sessionsApi.list({ size: 8 })
.then(data => {
setSessions(data)
// Mark as "new" if any session was updated in the last hour
const oneHourAgo = Date.now() - 3600000
setHasNew(data.some(s => new Date(s.started_at).getTime() > oneHourAgo))
})
.catch(() => {})
}, [])
useEffect(() => {
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
}
if (open) document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [open])
return (
<div className="relative" ref={ref}>
<button
onClick={() => { setOpen(!open); setHasNew(false) }}
className="relative rounded-lg p-2 text-muted-foreground hover:bg-card hover:text-foreground transition-colors"
title="Notifications"
>
<Bell size={18} />
{hasNew && (
<span className="absolute right-1.5 top-1.5 h-2 w-2 rounded-full bg-primary" />
)}
</button>
{open && (
<div className="absolute right-0 z-50 mt-2 w-80 rounded-xl border border-border bg-card shadow-xl animate-scale-in">
<div className="flex items-center justify-between border-b border-border px-4 py-3">
<h3 className="text-sm font-heading font-semibold text-foreground">Activity</h3>
<Link
to="/sessions"
onClick={() => setOpen(false)}
className="text-[0.6875rem] text-muted-foreground hover:text-foreground"
>
View All
</Link>
</div>
{sessions.length === 0 ? (
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
No recent activity
</div>
) : (
<div className="max-h-72 overflow-y-auto divide-y divide-border">
{sessions.map(session => (
<Link
key={session.id}
to={`/sessions/${session.id}`}
onClick={() => setOpen(false)}
className="flex items-start gap-3 px-4 py-3 hover:bg-accent/50 transition-colors"
>
<div className="mt-0.5">
{session.completed_at ? (
<CheckCircle size={16} className="text-emerald-400" />
) : (
<Clock size={16} className="text-amber-400" />
)}
</div>
<div className="min-w-0 flex-1">
<p className="text-sm text-foreground truncate">
{session.tree_snapshot?.name || 'Session'}
</p>
<p className="text-[0.6875rem] text-muted-foreground">
{session.completed_at
? `Completed ${timeAgo(session.completed_at)}`
: `Started ${timeAgo(session.started_at)}`}
{session.client_name && ` · ${session.client_name}`}
</p>
</div>
</Link>
))}
</div>
)}
</div>
)}
</div>
)
}

View File

@@ -1,6 +1,6 @@
import { useState, useRef, useEffect, useCallback } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Search, Zap, Bell, LogOut, User, Shield, Settings } from 'lucide-react'
import { Search, Zap, LogOut, User, Shield, Settings } from 'lucide-react'
import { useAuthStore } from '@/store/authStore'
import { usePermissions } from '@/hooks/usePermissions'
import { useWorkspaceStore } from '@/store/workspaceStore'
@@ -8,6 +8,7 @@ import { getWorkspaceLabels } from '@/constants/workspaceLabels'
import { BrandLogo } from '@/components/common/BrandLogo'
import { CommandPalette } from './CommandPalette'
import { QuickLaunch } from './QuickLaunch'
import { NotificationsPanel } from './NotificationsPanel'
import { cn } from '@/lib/utils'
export function TopBar() {
@@ -103,9 +104,7 @@ export function TopBar() {
>
<Zap size={18} />
</button>
<button className="relative rounded-lg p-2 text-muted-foreground hover:bg-card hover:text-foreground transition-colors" title="Notifications">
<Bell size={18} />
</button>
<NotificationsPanel />
{/* User avatar & menu */}
<div className="relative ml-2" ref={menuRef}>