"use client"; import { ChevronDown, ChevronRight, FormIcon } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { stringify } from "yaml"; import SearchBox, { type Filters, type ParsedQuery } from "@/components/search"; import { SimpleTooltip } from "@/components/tooltip"; import { Button } from "@/components/ui/button"; import type { auditLog } from "@/lib/db/schema"; import { sse } from "@/lib/db/sse-endpoints"; type AuditLog = typeof auditLog.$inferSelect; export default function AuditLogViewer({ logs: initialLogs = [], users = [], }: { logs?: AuditLog[]; users?: { name: string; email: string }[]; }) { const [logs, updateLogs] = useState(initialLogs); const containerRef = useRef(null); const entryRefs = useRef(new Map()); useEffect(() => { return sse.log.sub("update", (res) => updateLogs((x) => [...x.slice(-999), res]), ).clean; }, []); const filters: Filters = { user: () => users.map((u) => u.email), has: () => ["artifact", "tierlist", "rubgram", "character", "file"], }; const [query, setQuery] = useState(); const filteredLogs = useMemo(() => { return logs.filter((log) => query ? (query.filters.user ? log.author?.toString() === users.find((u) => u.email === query.filters.user)?.name : true) && (query.filters.has ? log.text.toLowerCase().includes(query.filters.has) : true) && stringify(log).toLowerCase().includes(query.search.toLowerCase()) : true, ); }, [query, logs, users]); const [expandedIds, setExpandedIds] = useState>(new Set()); const focusedIds = useMemo(() => Array.from(expandedIds), [expandedIds]); // biome-ignore lint/correctness/useExhaustiveDependencies: need to update useEffect(() => { const container = containerRef.current; if (!container) return; // exactly one focused → scroll to that entry if (focusedIds.length === 1) { const el = entryRefs.current.get(focusedIds[0]); if (el) { el.scrollIntoView({ behavior: "smooth", block: "center", }); } return; } // more than one focused → do nothing if (focusedIds.length > 1) { return; } // none focused → always follow bottom container.scrollTo({ top: container.scrollHeight, behavior: "smooth", }); }, [logs, focusedIds, query]); const toggleExpand = (id: string) => { setExpandedIds((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }; const formatTime = (date: Date | string) => { return new Date(date).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: true, timeZone: "Asia/Bangkok", }); }; const formatDate = (date: Date | string) => { return new Date(date).toLocaleDateString("en-US", { month: "2-digit", day: "2-digit", year: "numeric", timeZone: "Asia/Bangkok", }); }; const shouldGroup = ( currentLog: AuditLog, previousLog: AuditLog | undefined, ): boolean => { if (!previousLog) return false; if (currentLog.author !== previousLog.author) return false; const timeDiff = Math.abs( new Date(currentLog.time).getTime() - new Date(previousLog.time).getTime(), ); return timeDiff < 5 * 60 * 1000; // Group if within 5 minutes }; return (
{filteredLogs.map((log, idx) => { const isGrouped = shouldGroup(log, logs[idx - 1]); const isExpanded = expandedIds.has(log.id); return (
{ if (el) entryRefs.current.set(log.id, el); else entryRefs.current.delete(log.id); }} className="group transition-colors hover:bg-[#2225]" >
{formatTime(log.time)}
{!isGrouped && (
{log.author || "System"} {formatDate(log.time)} {formatTime(log.time)}
)}
toggleExpand(log.id)} role="tree" > {log.text}{" "} {!isExpanded && log.details ? ( ) : ( "" )} {log.details ? ( ) : undefined}
{isExpanded && log.details ? (
                        {stringify(log.details)}
                      
) : undefined}
); })}
); }