JUNOHUB-test/src/app/admin/events/page.tsx
root 284fb12576 Set up JUNOHUB app and EC2 deployment
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 20:12:52 +05:00

253 lines
11 KiB
TypeScript

import { auth } from "@/auth"
import { redirect } from "next/navigation"
import { Activity, Link2, Radar, Shuffle } from "lucide-react"
type AdminEventType = "CONNECTION" | "PRODUCT_SYNC" | "SYNC_HEALTH"
interface AdminEvent {
id: string
type: AdminEventType
status: string
timestamp: string
storeName: string | null
supplierName: string | null
metadata?: Record<string, unknown>
}
async function getEvents(type?: AdminEventType): Promise<AdminEvent[]> {
const searchParams = new URLSearchParams()
searchParams.set("limit", "50")
if (type) {
searchParams.set("type", type)
}
const res = await fetch(
`${process.env.NEXTAUTH_URL}/api/admin/events?${searchParams.toString()}`,
{
cache: "no-store",
},
)
if (!res.ok) {
return []
}
const data = (await res.json()) as { events: AdminEvent[] }
return data.events
}
function typeBadge(eventType: AdminEventType) {
switch (eventType) {
case "CONNECTION":
return (
<span className="inline-flex items-center gap-1 rounded-full bg-blue-500/15 text-blue-200 border border-blue-500/40 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide">
<Link2 className="w-3 h-3" />
Connection
</span>
)
case "PRODUCT_SYNC":
return (
<span className="inline-flex items-center gap-1 rounded-full bg-purple-500/15 text-purple-200 border border-purple-500/40 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide">
<Shuffle className="w-3 h-3" />
Product Sync
</span>
)
case "SYNC_HEALTH":
return (
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/15 text-emerald-200 border border-emerald-500/40 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide">
<Radar className="w-3 h-3" />
Sync Health
</span>
)
default:
return null
}
}
function statusBadge(status: string) {
const normalized = status.toUpperCase()
let colorClasses =
"bg-slate-800/60 text-slate-200 border-slate-500/40"
if (normalized === "CONNECTED" || normalized === "ACCEPTED" || normalized === "SUCCESS") {
colorClasses = "bg-emerald-500/15 text-emerald-200 border-emerald-500/40"
} else if (normalized === "PENDING" || normalized === "IN_PROGRESS") {
colorClasses = "bg-amber-500/15 text-amber-200 border-amber-500/40"
} else if (normalized === "REJECTED" || normalized === "FAILED") {
colorClasses = "bg-rose-500/15 text-rose-200 border-rose-500/40"
}
return (
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide border ${colorClasses}`}
>
{normalized}
</span>
)
}
export default async function AdminEventsPage({
searchParams,
}: {
searchParams: Promise<{ type?: string }>
}) {
const session = await auth()
if (!session || session.user.role !== "ADMIN") {
redirect("/login")
}
const { type } = await searchParams
const filterType =
type && ["CONNECTION", "PRODUCT_SYNC", "SYNC_HEALTH"].includes(type)
? (type as AdminEventType)
: undefined
const events = await getEvents(filterType)
return (
<div className="flex flex-col gap-6 h-full">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-white tracking-tight">
Event Timeline
</h1>
<p className="text-sm text-slate-400 mt-1 max-w-2xl">
Chronological, read-only view of platform activity across connections,
product syncs, and sync health events.
</p>
</div>
</div>
<div className="rounded-2xl border border-white/10 bg-white/5 backdrop-blur-xl p-4 flex flex-wrap gap-2">
<span className="text-[11px] uppercase tracking-wide text-slate-400 font-semibold mr-2">
Filter by type:
</span>
<div className="flex flex-wrap gap-2">
<FilterChip label="All" href="/admin/events" active={!filterType} />
<FilterChip
label="Connections"
href="/admin/events?type=CONNECTION"
active={filterType === "CONNECTION"}
/>
<FilterChip
label="Product Syncs"
href="/admin/events?type=PRODUCT_SYNC"
active={filterType === "PRODUCT_SYNC"}
/>
<FilterChip
label="Sync Health"
href="/admin/events?type=SYNC_HEALTH"
active={filterType === "SYNC_HEALTH"}
/>
</div>
</div>
<div className="flex-1 rounded-2xl border border-white/10 bg-white/5 backdrop-blur-xl p-5 overflow-y-auto">
{events.length === 0 ? (
<div className="h-full flex items-center justify-center">
<p className="text-sm text-slate-500">
No events found for the current filter.
</p>
</div>
) : (
<div className="relative">
<div className="absolute left-4 top-0 bottom-0 w-px bg-white/10" />
<div className="space-y-4">
{events.map((event) => (
<div
key={event.id}
className="relative pl-10 pr-2 py-2 group"
>
<div className="absolute left-[14px] top-3 w-3 h-3 rounded-full bg-indigo-400 border-2 border-slate-900 shadow-md shadow-indigo-500/40" />
<div className="rounded-xl border border-white/10 bg-slate-900/60 px-4 py-3 flex flex-col gap-1">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 flex-wrap">
{typeBadge(event.type)}
{statusBadge(event.status)}
</div>
<div className="flex items-center gap-1 text-[11px] text-slate-400">
<Activity className="w-3 h-3" />
<span>
{new Date(
event.timestamp,
).toLocaleString("en-US", {
dateStyle: "short",
timeStyle: "short",
})}
</span>
</div>
</div>
<div className="mt-1 text-xs text-slate-200">
{event.storeName && (
<span className="font-semibold">
{event.storeName}
</span>
)}
{event.storeName && event.supplierName && (
<span className="mx-1 text-slate-500">↔</span>
)}
{event.supplierName && (
<span className="font-semibold">
{event.supplierName}
</span>
)}
{!event.storeName && !event.supplierName && (
<span className="text-slate-400">
Platform-level event
</span>
)}
</div>
{event.metadata && event.type === "PRODUCT_SYNC" && (
<p className="text-[11px] text-slate-400 mt-0.5">
{(event.metadata.productTitle as string) ||
"Product sync"}
</p>
)}
{event.metadata && event.type === "SYNC_HEALTH" && (
<p className="text-[11px] text-slate-400 mt-0.5">
{String(
(event.metadata.resourceType ||
"unknown") as string,
).toUpperCase()}{" "}
· Records:{" "}
{String(
(event.metadata.recordCount ||
0) as number,
)}
</p>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
)
}
function FilterChip({
label,
href,
active,
}: {
label: string
href: string
active: boolean
}) {
return (
<a
href={href}
className={`inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium transition-colors ${
active
? "border-white/20 bg-white/15 text-white"
: "border-white/10 bg-slate-900/40 text-slate-300 hover:bg-white/10"
}`}
>
{label}
</a>
)
}