ai_api / src /app /(dashboard) /dashboard /activity /ActivityFeedClient.tsx
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
4.19 kB
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { useTranslations } from "next-intl";
import type { AuditLogEntry } from "@/lib/compliance/index";
import ActivityFeed from "./components/ActivityFeed";
import EventTypeFilter, {
type EventCategory,
matchesCategory,
} from "./components/EventTypeFilter";
const FEED_LIMIT = 200;
export default function ActivityFeedClient() {
const t = useTranslations("activity");
const [allEntries, setAllEntries] = useState<AuditLogEntry[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [category, setCategory] = useState<EventCategory>("all");
const referenceNowMs = useRef<number>(Date.now());
const fetchEntries = useCallback(async () => {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams({
level: "high",
limit: String(FEED_LIMIT),
});
const res = await fetch(`/api/compliance/audit-log?${params.toString()}`);
if (!res.ok) {
throw new Error(t("description"));
}
const data = (await res.json()) as AuditLogEntry[];
// Reset reference time on fresh load so relative timestamps are stable
referenceNowMs.current = Date.now();
setAllEntries(Array.isArray(data) ? data : []);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : "Failed to fetch activity";
setError(msg);
} finally {
setLoading(false);
}
}, [t]);
useEffect(() => {
fetchEntries();
}, [fetchEntries]);
const filtered =
category === "all"
? allEntries
: allEntries.filter((e) => {
const action = typeof e.action === "string" ? e.action : "";
return matchesCategory(action, category);
});
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-start justify-between gap-4 flex-wrap">
<div>
<h1 className="text-xl font-bold text-[var(--color-text-main)]">{t("title")}</h1>
<p className="text-sm text-[var(--color-text-muted)] mt-1">{t("description")}</p>
</div>
<button
type="button"
onClick={() => fetchEntries()}
disabled={loading}
className="px-4 py-2 rounded-lg text-sm font-medium bg-[var(--color-surface)] border border-[var(--color-border)] text-[var(--color-text-main)] hover:bg-[var(--color-bg-alt)] transition-colors disabled:opacity-50"
aria-label="Refresh activity feed"
>
{loading ? (
<span className="flex items-center gap-2">
<span
className="material-symbols-outlined text-[16px] animate-spin"
aria-hidden="true"
>
progress_activity
</span>
Loading
</span>
) : (
<span className="flex items-center gap-2">
<span className="material-symbols-outlined text-[16px]" aria-hidden="true">
refresh
</span>
Refresh
</span>
)}
</button>
</div>
{/* Filter */}
<EventTypeFilter value={category} onChange={setCategory} />
{/* Error */}
{error && (
<div
className="p-4 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-sm"
role="alert"
>
{error}
</div>
)}
{/* Feed */}
<div className="rounded-xl border border-[var(--color-border)] overflow-hidden bg-[var(--color-surface)]">
{loading ? (
<div className="flex items-center justify-center py-20 text-[var(--color-text-muted)]">
<span
className="material-symbols-outlined text-[32px] animate-spin mr-3"
aria-hidden="true"
>
progress_activity
</span>
<span className="text-sm">Loading activity…</span>
</div>
) : (
<ActivityFeed entries={filtered} referenceNowMs={referenceNowMs.current} />
)}
</div>
</div>
);
}