SPB Git forge
3commits 1branches 0releases
417.0 KBsize
maindefault branch
10 days agolast push
TypeScript 66.5% Python 30.9% JavaScript 1.4% CSS 0.7%
3.9 KB · 91 lines tsx
Raw Blame History
1import Link from 'next/link';2import { fmtDate, fmtDateTime } from '@/lib/format';3import type { EventRow } from '@/lib/types';4import { Confidence, EntityLinks, EventTypeBadge, eventColor } from './event-badge';56function dayKey(iso: string): string {7  return iso.slice(0, 10);8}9function dayHeading(key: string, today: string): string {10  if (key === today) return `Today · ${fmtDate(key)}`;11  const y = new Date(`${today}T00:00:00Z`);12  y.setUTCDate(y.getUTCDate() - 1);13  if (key === y.toISOString().slice(0, 10)) return `Yesterday · ${fmtDate(key)}`;14  return fmtDate(key);15}16function timeOnly(iso: string): string {17  const d = new Date(iso);18  if (Number.isNaN(d.getTime())) return '—';19  return `${d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', timeZone: 'UTC' })} UTC`;20}2122/** One event row: time · type · title/summary/entities · source + confidence. Desktop = 4-column terminal grid; mobile = stacked. */23export function EventItem({ e, dense = false }: { e: EventRow; dense?: boolean }) {24  const href = `/events/${encodeURIComponent(e.id)}`;25  return (26    <li className="relative border-b border-rule py-3 md:grid md:grid-cols-[92px_150px_minmax(0,1fr)_180px] md:items-start md:gap-4 md:py-3.5">27      <span className="absolute -left-4 top-[18px] hidden size-1.5 rounded-full md:block" style={{ background: eventColor(e.type) }} aria-hidden />28      <div className="flex items-center justify-between gap-2 md:block">29        <time dateTime={e.event_time} className="mono text-xs text-ink-2" title={fmtDateTime(e.event_time)}>30          {timeOnly(e.event_time)}31        </time>32        <div className="md:hidden">33          <EventTypeBadge type={e.type} />34        </div>35      </div>36      <div className="hidden md:block">37        <EventTypeBadge type={e.type} />38      </div>39      <div className="mt-1.5 min-w-0 md:mt-0">40        <Link href={href} className="link text-sm font-medium leading-snug">41          {e.title}42        </Link>43        {e.summary && !dense && <p className="mt-0.5 line-clamp-2 text-xs leading-relaxed text-ink-3">{e.summary}</p>}44        <EntityLinks entities={e.entities} className="mt-1.5" />45      </div>46      <div className="mt-2 flex items-center justify-between gap-3 text-xs md:mt-0 md:block md:text-right">47        <span className="truncate text-ink-3" title={e.source_id ?? undefined}>48          {e.source_url ? (49            <a href={e.source_url} target="_blank" rel="noopener noreferrer" className="hover:text-accent">{e.source_name ?? e.source_id ?? 'Source'}</a>50          ) : (51            e.source_name ?? e.source_id ?? 'Source unavailable'52          )}53        </span>54        <span className="md:mt-0.5 md:block">55          <Confidence value={e.confidence} />56        </span>57      </div>58    </li>59  );60}6162/** Events grouped visually by UTC day. */63export function EventTimeline({ events, dense = false }: { events: EventRow[]; dense?: boolean }) {64  const today = new Date().toISOString().slice(0, 10);65  const groups: { day: string; items: EventRow[] }[] = [];66  for (const e of events) {67    const k = dayKey(e.event_time);68    const last = groups[groups.length - 1];69    if (last && last.day === k) last.items.push(e);70    else groups.push({ day: k, items: [e] });71  }72  return (73    <div className="space-y-8">74      {groups.map((g) => (75        <section key={g.day} aria-label={fmtDate(g.day)}>76          <div className="mb-1 flex items-center gap-3">77            <h3 className="mono text-xs font-semibold uppercase tracking-wider text-ink-2">{dayHeading(g.day, today)}</h3>78            <span className="h-px flex-1 bg-rule" aria-hidden />79            <span className="mono text-[11px] text-ink-3">{g.items.length} event{g.items.length > 1 ? 's' : ''}</span>80          </div>81          <ol className="md:ml-4 md:border-l md:border-rule md:pl-4">82            {g.items.map((e) => (83              <EventItem key={e.id} e={e} dense={dense} />84            ))}85          </ol>86        </section>87      ))}88    </div>89  );90}91