SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
4.3 KB · 69 lines tsx
Raw Blame History
1'use client';2import Link from 'next/link';3import { ConfidenceBadge, CountryChip, EventTypeBadge, ImportanceMeter } from '@/components/ui/badges';4import { LiveAgo } from '@/components/ui/live';5import { cn } from '@/lib/cn';6import { eventStyle } from '@/lib/event-styles';7import { fmtTime, truncate } from '@/lib/format';8import { routes } from '@/lib/site';9import type { Event } from '@/lib/types';10import { useEventDrawer } from './event-drawer-context';1112/**13 * Dense feed row: hue bar · type chip · title (opens the evidence drawer) · company · importance · confidence · time.14 * `variant="feed"` is the live feed (fresh rows animate); `"table"` is the events list; `"timeline"` hides the company.15 */16export function EventRow({ event, fresh = false, selected = false, variant = 'feed', showCompany = true, className, id }: { event: Event; fresh?: boolean; selected?: boolean; variant?: 'feed' | 'table' | 'timeline'; showCompany?: boolean; className?: string; id?: string }) {17  const { open } = useEventDrawer();18  const s = eventStyle(event.event_type);19  const retracted = event.status === 'retracted';20  return (21    <li id={id} data-event-id={event.id} data-selected={selected || undefined} className={cn('group relative grid grid-cols-[3px_minmax(0,1fr)] gap-x-3 border-b border-rule', fresh && 'feed-new', selected && 'bg-surface-2', className)}>22      <span className="my-2 rounded-full" style={{ background: s.color }} aria-hidden />23      <div className={cn('min-w-0 py-2.5', variant === 'timeline' ? 'pr-1' : 'pr-0')}>24        <div className="flex flex-wrap items-center gap-x-2 gap-y-1">25          <EventTypeBadge type={event.event_type} subtype={variant !== 'feed' ? event.event_subtype : undefined} small />26          {showCompany && (27            <Link href={routes.company(event.company.slug)} className="max-w-[55%] truncate text-[13px] font-medium text-ink hover:text-accent">28              {event.company.display_name}29            </Link>30          )}31          {showCompany && <CountryChip code={event.company.country} />}32          <span className="ml-auto inline-flex items-center gap-2 text-[11px] text-ink-3">33            <ImportanceMeter importance={event.importance} />34            {variant === 'timeline' ? <span className="tnum">{fmtTime(event.detected_at)} UTC</span> : <LiveAgo at={event.detected_at} tick={variant === 'feed' ? 1000 : 30000} />}35          </span>36        </div>37        <button type="button" onClick={() => open(event)} className={cn('mt-1 block w-full text-left text-[14px] leading-snug text-ink hover:text-accent', retracted && 'line-through decoration-danger/60')} data-open-event>38          {event.title}39        </button>40        {event.summary && variant !== 'feed' && <p className="mt-0.5 text-[13px] leading-snug text-ink-2">{truncate(event.summary, 200)}</p>}41        <div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-ink-3">42          <ConfidenceBadge label={event.confidence_label} />43          {event.old_value && event.new_value && (44            <span className="min-w-0 truncate">45              <span className="text-ink-3 line-through">{truncate(event.old_value, 28)}</span> → <span className="text-ink-2">{truncate(event.new_value, 28)}</span>46            </span>47          )}48          {event.origin !== 'deterministic' && <span className="uppercase tracking-wider">{event.origin}</span>}49          {retracted && <span className="text-danger">retracted</span>}50          <Link href={routes.event(event.id)} className="ml-auto opacity-0 transition-opacity group-hover:opacity-100 focus:opacity-100 hover:text-accent">51            Details →52          </Link>53        </div>54      </div>55    </li>56  );57}5859export function EventList({ events, className, variant = 'table', showCompany = true, emptyLabel = 'No monitored evidence available yet.' }: { events: Event[]; className?: string; variant?: 'feed' | 'table' | 'timeline'; showCompany?: boolean; emptyLabel?: string }) {60  if (!events.length) return <p className={cn('border border-dashed border-rule-strong px-4 py-8 text-center text-sm text-ink-3', className)}>{emptyLabel}</p>;61  return (62    <ul className={cn('border-t border-rule', className)}>63      {events.map((e) => (64        <EventRow key={e.id} event={e} variant={variant} showCompany={showCompany} />65      ))}66    </ul>67  );68}69