1import { ExternalLink } from 'lucide-react';2import Link from 'next/link';3import { EntityBadge, ImportanceMark } from '@/components/ui/badges';4import { LiveAgo } from '@/components/ui/live';5import { cn } from '@/lib/cn';6import { fmtDate, fmtDateTime, fmtValue } from '@/lib/format';7import { eventLabel, eventTone, propertyLabel, routes } from '@/lib/site';8import type { ChangeEvent } from '@/lib/types';910const TONE: Record<ReturnType<typeof eventTone>, string> = {11 new: 'text-positive',12 price: 'text-accent-2',13 warn: 'text-warning',14 danger: 'text-danger',15 bench: 'text-type-benchmark',16 neutral: 'text-ink-2',17};1819function host(url: string): string {20 try {21 return new URL(url).hostname.replace(/^www\./, '');22 } catch {23 return 'source';24 }25}2627/** Old → new rendering for property changes and prices. */28export function Delta({ e }: { e: ChangeEvent }) {29 if (e.old_value === undefined || e.old_value === null || e.new_value === undefined || e.new_value === null) return null;30 const key = e.property ?? undefined;31 return (32 <span className="tnum inline-flex flex-wrap items-center gap-1 text-xs">33 <span className="text-ink-3 line-through decoration-ink-3/60">{fmtValue(e.old_value, key)}</span>34 <span className="text-ink-3">→</span>35 <span className="font-medium text-ink">{fmtValue(e.new_value, key)}</span>36 </span>37 );38}3940/**41 * One event in a feed: importance meter · time · entity badge + name · summary · property delta · source.42 * `dense` (homepage) hides the delta and connector; `showDate` prints the date instead of relative time.43 */44export function ChangeRow({ e, dense = false, showDate = false, className, live = true }: { e: ChangeEvent; dense?: boolean; showDate?: boolean; className?: string; live?: boolean }) {45 const tone = eventTone(e.event_type);46 const when = e.effective_at ?? e.observed_at;47 return (48 <li className={cn('grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 border-b border-rule py-2.5 md:grid-cols-[5.5rem_auto_minmax(0,1fr)_auto] md:items-baseline', className)}>49 <div className="col-span-2 flex items-center gap-2 text-xs text-ink-3 md:col-span-1 md:block">50 {showDate ? <time dateTime={when} title={fmtDateTime(e.observed_at)}>{fmtDate(when)}</time> : live ? <LiveAgo at={e.observed_at} /> : <time dateTime={e.observed_at} title={fmtDateTime(e.observed_at)}>{fmtDate(e.observed_at)}</time>}51 <ImportanceMark importance={e.importance} className="md:hidden" />52 </div>53 <div className="hidden md:flex md:items-center md:self-center">54 <ImportanceMark importance={e.importance} />55 </div>56 <div className="col-span-2 min-w-0 md:col-span-1">57 <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">58 <span className={cn('text-[11px] font-medium uppercase tracking-wide', TONE[tone])}>{eventLabel(e.event_type)}</span>59 {e.entity && (60 <>61 <EntityBadge type={e.entity.entity_type} small />62 <Link href={routes.entity(e.entity)} className="truncate text-[15px] font-medium text-ink hover:text-accent hover:underline">63 {e.entity.name}64 </Link>65 {e.entity.organization && <span className="truncate text-xs text-ink-3">{e.entity.organization.name}</span>}66 </>67 )}68 </div>69 <p className={cn('mt-0.5 text-sm text-ink-2', dense && 'line-clamp-2')}>{e.summary}</p>70 {!dense && (71 <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5">72 {e.property && <span className="text-xs text-ink-3">{propertyLabel(e.property)}</span>}73 <Delta e={e} />74 {e.connector_name && <span className="mono text-[11px] text-ink-3">{e.connector_name}</span>}75 </div>76 )}77 </div>78 <div className="col-span-2 mt-1 text-xs md:col-span-1 md:mt-0 md:text-right">79 {e.source_url ? (80 <a href={e.source_url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-ink-3 hover:text-accent">81 {host(e.source_url)} <ExternalLink className="size-3" aria-hidden />82 </a>83 ) : (84 <span className="text-ink-3">—</span>85 )}86 </div>87 </li>88 );89}9091/** Group events by UTC day (YYYY-MM-DD of observed_at). */92export function groupByDay(events: ChangeEvent[]): { day: string; items: ChangeEvent[] }[] {93 const map = new Map<string, ChangeEvent[]>();94 for (const e of events) {95 const day = e.observed_at.slice(0, 10);96 const arr = map.get(day);97 if (arr) arr.push(e);98 else map.set(day, [e]);99 }100 return [...map.entries()].map(([day, items]) => ({ day, items }));101}102