TypeScript 55.4%
Python 43.2%
SQL 1.2%
1"use client";23import Link from "next/link";4import type { MouseEvent } from "react";5import type { EventItem } from "@/lib/api";6import { relTime, utcTime, utcDate, utcDateTime, localDateTime } from "@/lib/format";7import { useEventDrawer } from "./event-drawer";8import { FieldChangeInline } from "./field-changes";9import { usePrefs } from "./prefs";10import { Badge, Chip, Flag, Score, TypeChip } from "./ui";1112const CHANNEL_GROUPS: Record<string, string[]> = { finance: ["finance", "payments", "crypto", "commerce"], health: ["health", "pharma"], government: ["government", "statistics"], science: ["science", "space"], products: ["consumer-tech", "automotive", "semiconductors", "enterprise"], infrastructure: ["cloud", "developer", "internet", "standards"], news: ["news", "media"] };13function sameChannel(a: string, b: string): boolean {14 return CHANNEL_GROUPS[a]?.includes(b) ?? false;15}1617/**18 * Feed row (spec §31): TIME · ENTITY/SOURCE · SIGNAL · TYPE · TITLE · STATUS · TAGS, with badges19 * BREAKING / DEVELOPING / SILENT / FIRST PARTY / CONFIRMED / ANOMALOUS. Density comes from CSS20 * variables (spec §32). Clicking the title opens the intelligence drawer (spec §33); the link keeps21 * a real href so middle-click / crawlers / no-JS still reach the permanent page.22 */23export function EventRow({ ev, flash = false, showDate = false, now, fresh = false, replayed = false }: { ev: EventItem; flash?: boolean; showDate?: boolean; now?: number; fresh?: boolean; replayed?: boolean }) {24 const { open } = useEventDrawer();25 const { exactTime, density, mounted } = usePrefs();26 const iso = (() => {27 const d = new Date(ev.detected_at);28 return Number.isNaN(d.getTime()) ? ev.detected_at : d.toISOString();29 })();30 const cats = (ev.categories ?? []).filter((c) => !Object.keys(CHANNEL_GROUPS).includes(c) || !ev.categories.some((x) => x !== c && sameChannel(c, x))).slice(0, density === "compact" ? 1 : 2);31 const signal = ev.signal_score ?? ev.importance;32 const state = ev.cluster?.state;33 const isBreaking = state === "breaking" || (signal >= 80 && !state);34 const isDeveloping = state === "developing";35 const anomalous = (ev.anomaly_score ?? 0) >= 60;36 const clusterHref = ev.cluster?.slug ? `/cluster/${ev.cluster.slug}` : ev.cluster_id ? `/cluster/${ev.cluster_id}` : null;37 const onClick = (e: MouseEvent<HTMLAnchorElement>): void => {38 if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;39 e.preventDefault();40 open(ev.slug, ev);41 };42 return (43 <article className={`row-dense offscreen-ok grid grid-cols-[auto_1fr] gap-x-3 px-3 hairline sm:grid-cols-[6.25rem_1fr_auto] ${flash || fresh ? "animate-row-in" : ""} ${isBreaking ? "border-l-2 border-l-hot/70" : isDeveloping ? "border-l-2 border-l-high/60" : ev.silent_change ? "border-l-2 border-l-silent/60" : "border-l-2 border-l-transparent"}`}>44 <div className="flex flex-col font-mono meta leading-4 text-fg-subtle tabular">45 <time dateTime={iso} title={mounted ? localDateTime(ev.detected_at) : utcDateTime(ev.detected_at)} suppressHydrationWarning className="text-fg-muted">{utcTime(ev.detected_at)}</time>46 <span suppressHydrationWarning>{showDate ? utcDate(ev.detected_at) : exactTime ? "UTC" : relTime(ev.detected_at, now)}</span>47 </div>48 <div className="min-w-0 sm:col-start-2">49 <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">50 <Link href={`/source/${ev.source?.id ?? ev.source_id}`} className="font-mono meta font-semibold uppercase tracking-wide text-fg-muted hover:text-fg">51 {ev.source?.name ?? ev.source_id}52 </Link>53 {ev.country && <Flag code={ev.country} className="text-[11px]" />}54 <span className="sm:hidden">55 <Score value={signal} size="sm" kind="signal" />56 </span>57 {isBreaking && <Badge kind="breaking" compact={density === "compact"} />}58 {isDeveloping && <Badge kind="developing" compact={density === "compact"} />}59 {ev.silent_change && <Badge kind="silent" compact={density === "compact"} />}60 {ev.first_party === false && <Badge kind="external" compact />}61 {ev.evidence_label === "CONFIRMED" && <Badge kind="confirmed" compact={density === "compact"} />}62 {anomalous && <Badge kind="anomalous" compact />}63 {replayed && <Badge kind="replayed" compact />}64 </div>65 <Link href={`/event/${ev.slug}`} onClick={onClick} className={`mt-0.5 block font-medium leading-snug text-fg hover:underline [overflow-wrap:anywhere] ${density === "compact" ? "truncate" : ""}`}>66 {ev.title}67 </Link>68 {density !== "compact" && ev.field_changes && ev.field_changes.length > 0 && (69 <div className="mt-0.5">70 <FieldChangeInline items={ev.field_changes} max={density === "comfortable" ? 3 : 2} />71 </div>72 )}73 {density === "comfortable" && ev.summary && <p className="mt-0.5 line-clamp-2 text-[12.5px] text-fg-muted">{ev.summary}</p>}74 <div className="mt-1 flex flex-wrap items-center gap-1">75 <TypeChip type={ev.event_type} href={`/live?event_type=${ev.event_type}`} />76 {cats.map((c) => (77 <Chip key={c} href={`/category/${c}`}>{c}</Chip>78 ))}79 {ev.entities?.filter((x) => x.role === "subject").slice(0, 1).map((x) => (80 <Chip key={x.id} href={`/entity/${x.id}`} tone="default" className="!text-fg-muted">{x.name}</Chip>81 ))}82 {ev.cluster_size && ev.cluster_size > 1 && clusterHref && (83 <Chip tone="info" href={clusterHref} title="Signals in this event cluster">84 {ev.cluster_size} signals{ev.cluster?.source_count && ev.cluster.source_count > 1 ? ` · ${ev.cluster.source_count} sources` : ""}85 </Chip>86 )}87 </div>88 </div>89 <div className="hidden items-start pt-0.5 sm:flex">90 <Score value={signal} kind="signal" title={`Signal ${Math.round(signal)} · importance ${Math.round(ev.importance)} · confidence ${Math.round(ev.confidence)}`} />91 </div>92 </article>93 );94}95