spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
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