"use client"; import { useState, useMemo, useEffect } from "react"; import type { ActivityEvent, ActivityFilterCategory } from "@/lib/activity"; import { getEventCategory, ACTIVITY_PAGE_SIZE } from "@/lib/activity"; import { ActivityItem } from "./ActivityItem"; interface Props { events: ActivityEvent[]; isOpen: boolean; onClose: () => void; onNodeClick: (nodeId: string) => void; onHandleClick?: (handle: string) => void; } const FILTER_CATEGORIES: { key: ActivityFilterCategory; label: string; icon: string; }[] = [ { key: "issues", label: "Issues", icon: "circle" }, { key: "deps", label: "Deps", icon: "link" }, { key: "comments", label: "Comments", icon: "chat" }, { key: "claims", label: "Claims", icon: "user" }, { key: "likes", label: "Likes", icon: "heart" }, ]; // ============================================================================ // Time grouping helpers // ============================================================================ function getTimeGroup(time: number): string { const now = Date.now(); const diff = now - time; const hours = diff / (1000 * 60 * 60); const days = diff / (1000 * 60 * 60 * 24); if (hours < 1) return "Just now"; if (hours < 24) return "Today"; if (days < 2) return "Yesterday"; if (days < 7) return "This week"; if (days < 30) return "This month"; return "Older"; } function groupEventsByTime(events: ActivityEvent[]): { label: string; events: ActivityEvent[] }[] { const groups: Map = new Map(); const order: string[] = []; for (const event of events) { const label = getTimeGroup(event.time); if (!groups.has(label)) { groups.set(label, []); order.push(label); } groups.get(label)!.push(event); } return order.map((label) => ({ label, events: groups.get(label)! })); } /** * Full slide-in sidebar panel for the activity feed. * Includes search bar, filter chips, and time-grouped events. * Same pattern as AllCommentsPanel / NodeDetail. */ export function ActivityPanel({ events, isOpen, onClose, onNodeClick, onHandleClick, }: Props) { const [search, setSearch] = useState(""); const [activeFilters, setActiveFilters] = useState>( new Set(FILTER_CATEGORIES.map((c) => c.key)) ); const [visibleCount, setVisibleCount] = useState(ACTIVITY_PAGE_SIZE); useEffect(() => { setVisibleCount(ACTIVITY_PAGE_SIZE); }, [events, activeFilters, search]); const toggleFilter = (category: ActivityFilterCategory) => { setActiveFilters((prev) => { const next = new Set(prev); if (next.has(category)) { // Don't allow deactivating all filters if (next.size > 1) next.delete(category); } else { next.add(category); } return next; }); }; const filteredEvents = useMemo(() => { let filtered = events; // Apply category filters filtered = filtered.filter((e) => activeFilters.has(getEventCategory(e.type)) ); // Apply text search if (search.trim()) { const q = search.trim().toLowerCase(); filtered = filtered.filter( (e) => e.nodeId.toLowerCase().includes(q) || (e.nodeTitle && e.nodeTitle.toLowerCase().includes(q)) || (e.actor?.handle && e.actor.handle.toLowerCase().includes(q)) || (e.detail && e.detail.toLowerCase().includes(q)) ); } return filtered; }, [events, activeFilters, search]); const groupedEvents = useMemo( () => groupEventsByTime(filteredEvents), [filteredEvents] ); const visibleEvents = useMemo( () => filteredEvents.slice(0, visibleCount), [filteredEvents, visibleCount] ); const hasMore = filteredEvents.length > visibleCount; const visibleGroupedEvents = useMemo( () => groupEventsByTime(visibleEvents), [visibleEvents] ); const allActive = activeFilters.size === FILTER_CATEGORIES.length; return ( <> {/* Desktop sidebar */} {/* Mobile drawer */}
{/* Drag handle */}
{/* Header */}

Activity

{events.length > 0 && ( {events.length} )}
{/* Filter chips */}
{FILTER_CATEGORIES.map((cat) => ( ))}
{/* Event list */}
{filteredEvents.length === 0 ? (
No activity matching filters
) : ( <> {visibleGroupedEvents.map((group) => (
{group.label}
{group.events.map((event) => ( ))}
))} {hasMore && (
)} )}
); }