"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Item, ItemContent, ItemHeader, ItemTitle, ItemDescription } from "@/components/ui/item"; import { BookOpenTextIcon, ScrollTextIcon } from "lucide-react"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; interface LogEntry { id: number; timestamp: string; type: string; message: string; system?: boolean; actor?: { id: number; username: string } | number | null; data?: Record; } function formatType(type: string) { return type .replace(":", " ") .split(" ") .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(" "); } export function EventLedger({ structureId, refreshKey, }: { structureId: number; refreshKey?: number; }) { const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const [selectedTypes, setSelectedTypes] = useState>(new Set()); const [currentUserId, setCurrentUserId] = useState(null); const initialDone = useRef(false); const eventTypes = useMemo(() => { const set = new Set(entries.map((e) => e.type)); return Array.from(set).sort(); }, [entries]); const filtered = useMemo( () => (selectedTypes.size === 0 ? entries : entries.filter((e) => selectedTypes.has(e.type))), [entries, selectedTypes], ); const toggleType = (type: string) => { setSelectedTypes((prev) => { const next = new Set(prev); if (next.has(type)) next.delete(type); else next.add(type); return next; }); }; const dropdownLabel = selectedTypes.size === 0 ? "All" : `${selectedTypes.size} selected`; const fetchEntries = useCallback(async () => { if (!initialDone.current) setLoading(true); const params = new URLSearchParams({ "where[structure.id][equals]": String(structureId), sort: "-timestamp", limit: "50", depth: "1", }); try { const r = await fetch(`/api/game-event-logs?${params}`); const body = await r.json(); setEntries(body.docs ?? []); } catch { setEntries([]); } finally { initialDone.current = true; setLoading(false); } }, [structureId]); useEffect(() => { fetch("/api/users/me") .then((r) => r.json()) .then((body) => setCurrentUserId(body?.user?.id ?? null)) .catch(() => setCurrentUserId(null)); }, []); useEffect(() => { fetchEntries(); }, [fetchEntries]); useEffect(() => { if (initialDone.current) fetchEntries(); }, [refreshKey, fetchEntries]); useEffect(() => { const interval = setInterval(() => { if (initialDone.current) fetchEntries(); }, 30_000); return () => clearInterval(interval); }, [fetchEntries]); if (loading) { return ( Event Log

Loading...

); } return ( Event Log Recent activity for this structure. {eventTypes.length > 1 && ( Filter event logs... setSelectedTypes(new Set())} > All {eventTypes.map((t) => ( toggleType(t)} > {formatType(t)} ))} )} {filtered.length === 0 ? (

No events recorded yet.

) : (
{filtered.map((entry, index) => { const time = new Date(entry.timestamp); const actorId = typeof entry.actor === "object" && entry.actor ? entry.actor.id : entry.actor; const actorName = typeof entry.actor === "object" && entry.actor ? entry.actor.username : null; const isYou = actorId != null && actorId === currentUserId; const actorLabel = isYou ? "You" : actorName; const displayMessage = actorLabel ? entry.message.charAt(0).toLowerCase() + entry.message.slice(1) : entry.message; const isNarrative = entry.system === false; return (
{isNarrative && ( )} {time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} {actorLabel && ( {actorLabel} )}{" "} {displayMessage} {formatType(entry.type)}
{index < filtered.length - 1 &&
}
); })}
)} ); }