spb/company-atlas
Public
Python 66.3%
TypeScript 22.7%
JavaScript 8.6%
HTML 1.4%
CSS 0.7%
1import Link from 'next/link';2import { Sparkline } from '@/components/charts/sparkline';3import { CompanyTierBadge, CountryChip } from '@/components/ui/badges';4import { LiveAgo } from '@/components/ui/live';5import { cn } from '@/lib/cn';6import { fmtInt, fmtPctSigned, fmtScore, num } from '@/lib/format';7import { descriptionOf, logoCandidates } from '@/lib/profile';8import { routes } from '@/lib/site';9import type { CompanyCard } from '@/lib/types';10import { CompanyLogo } from './company-logo';1112/**13 * Directory rows: dense table on ≥ md (logo · name + domain + one-line description · country · industry · activity ·14 * hiring 30d · AI · events · sensors · sparkline · last event) and stacked cards below. Missing metrics render as a dash,15 * never 0. Logos are lazy with a monogram fallback; the description is the attributed profile paragraph when present.16 */17export function CompanyTable({ items, className, rank = false, showSparkline = true, showDescription = true, valueLabel, formatValue, formatDelta }: { items: (CompanyCard & { rank?: number; value?: number; delta?: number | null })[]; className?: string; rank?: boolean; showSparkline?: boolean; showDescription?: boolean; valueLabel?: string; formatValue?: (c: CompanyCard & { value?: number }) => string; formatDelta?: (c: CompanyCard & { delta?: number | null }) => string | null }) {18 if (!items.length) return <p className="border border-dashed border-rule-strong px-4 py-8 text-center text-sm text-ink-3">No companies match these filters yet.</p>;19 return (20 <div className={className} data-company-table>21 {/* desktop */}22 <div className="table-scroll hidden md:block">23 <table className="data-table">24 <thead>25 <tr>26 {rank && <th className="num">#</th>}27 <th>Company</th>28 <th>Country</th>29 <th>Industry</th>30 {valueLabel && <th className="num">{valueLabel}</th>}31 {valueLabel && formatDelta && <th className="num">Δ</th>}32 <th className="num">Activity</th>33 <th className="num">Hiring 30d</th>34 <th className="num">AI</th>35 <th className="num">Events</th>36 <th className="num">Sensors</th>37 {showSparkline && <th>30 d</th>}38 <th className="num">Last event</th>39 </tr>40 </thead>41 <tbody>42 {items.map((c) => {43 const h = num(c.metrics.hiring_momentum_30d);44 const d = formatDelta?.(c) ?? null;45 const desc = showDescription ? descriptionOf(c)?.text : null;46 return (47 <tr key={c.id}>48 {rank && <td className="num tnum text-ink-3">{c.rank}</td>}49 <td className="primary">50 <Link href={routes.company(c.slug)} className="row-link flex items-center gap-2.5">51 <CompanyLogo name={c.display_name} candidates={logoCandidates(c)} size={28} rounded="sm" />52 <span className="min-w-0">53 <span className="inline-flex items-center gap-2">54 {c.display_name}55 <CompanyTierBadge tier={c.tier} />56 </span>57 <span className="flex max-w-[30rem] items-baseline gap-1.5 text-[11px] font-normal text-ink-3 lg:max-w-[38rem]">58 <span className="mono shrink-0">{c.canonical_domain}</span>59 {desc && (60 <span className="truncate" title={desc}>61 · {desc}62 </span>63 )}64 </span>65 </span>66 </Link>67 </td>68 <td>69 <CountryChip code={c.country} />70 </td>71 <td className="text-ink-2">{c.industry_primary ? <Link href={routes.industry(c.industry_primary)} className="hover:text-accent">{c.industry_primary.replace(/-/g, ' ')}</Link> : '—'}</td>72 {valueLabel && <td className="num tnum font-medium">{formatValue ? formatValue(c) : fmtScore(c.value)}</td>}73 {valueLabel && formatDelta && <td className={cn('num tnum text-xs', d?.startsWith('+') ? 'text-positive' : d?.startsWith('−') ? 'text-danger' : 'text-ink-3')}>{d ?? '—'}</td>}74 <td className="num tnum">{fmtScore(c.metrics.activity_score)}</td>75 <td className={cn('num tnum', h !== null && (h > 0 ? 'text-positive' : h < 0 ? 'text-danger' : ''))}>{h === null ? '—' : fmtPctSigned(h)}</td>76 <td className="num tnum">{fmtScore(c.metrics.ai_adoption)}</td>77 <td className="num tnum">{fmtInt(c.counts.events)}</td>78 <td className="num tnum">{fmtInt(c.counts.sensors)}</td>79 {showSparkline && (80 <td>81 <Sparkline values={c.sparkline} width={80} height={20} tone="accent" />82 </td>83 )}84 <td className="num text-xs text-ink-3">85 <LiveAgo at={c.last_event_at} tick={30000} absoluteFallback={false} />86 </td>87 </tr>88 );89 })}90 </tbody>91 </table>92 </div>93 {/* mobile cards */}94 <ul className="divide-y divide-rule border-y border-rule md:hidden">95 {items.map((c) => {96 const h = num(c.metrics.hiring_momentum_30d);97 return (98 <li key={c.id}>99 <Link href={routes.company(c.slug)} className="block py-3">100 <div className="flex items-start gap-3">101 {rank && <span className="tnum w-6 shrink-0 pt-0.5 text-sm text-ink-3">{c.rank}</span>}102 <CompanyLogo name={c.display_name} candidates={logoCandidates(c)} size={32} rounded="sm" className="mt-0.5" />103 <div className="min-w-0 flex-1">104 <p className="flex items-center gap-2 text-[15px] font-medium text-ink">105 <span className="truncate">{c.display_name}</span>106 <CountryChip code={c.country} link={false} />107 </p>108 <p className="mono truncate text-[11px] text-ink-3">109 {c.canonical_domain}110 {c.industry_primary ? ` · ${c.industry_primary.replace(/-/g, ' ')}` : ''}111 </p>112 </div>113 {showSparkline && <Sparkline values={c.sparkline} width={64} height={22} tone="accent" />}114 </div>115 <dl className="tnum mt-2 grid grid-cols-4 gap-2 text-center text-xs">116 {valueLabel ? (117 <div>118 <dt className="text-[10px] uppercase tracking-wider text-ink-3">{valueLabel}</dt>119 <dd className="font-medium text-ink">{formatValue ? formatValue(c) : fmtScore(c.value)}</dd>120 </div>121 ) : (122 <div>123 <dt className="text-[10px] uppercase tracking-wider text-ink-3">Activity</dt>124 <dd className="font-medium text-ink">{fmtScore(c.metrics.activity_score)}</dd>125 </div>126 )}127 <div>128 <dt className="text-[10px] uppercase tracking-wider text-ink-3">Hiring</dt>129 <dd className={cn('font-medium', h === null ? 'text-ink-3' : h > 0 ? 'text-positive' : h < 0 ? 'text-danger' : 'text-ink')}>{h === null ? '—' : fmtPctSigned(h, 0)}</dd>130 </div>131 <div>132 <dt className="text-[10px] uppercase tracking-wider text-ink-3">Events</dt>133 <dd className="font-medium text-ink">{fmtInt(c.counts.events)}</dd>134 </div>135 <div>136 <dt className="text-[10px] uppercase tracking-wider text-ink-3">Sensors</dt>137 <dd className="font-medium text-ink">{fmtInt(c.counts.sensors)}</dd>138 </div>139 </dl>140 </Link>141 </li>142 );143 })}144 </ul>145 </div>146 );147}148149/** Compact list of companies (logo · name · country · one metric) for homepage sections and side panels. */150export function CompanyMiniList({ items, metric = 'activity_score', label, format, className, sparkline = false }: { items: CompanyCard[]; metric?: keyof CompanyCard['metrics']; label?: string; format?: (v: number | null) => string; className?: string; sparkline?: boolean }) {151 if (!items.length) return <p className="border border-dashed border-rule-strong px-4 py-6 text-center text-xs text-ink-3">No monitored evidence available yet.</p>;152 const fmt = format ?? ((v: number | null) => (v === null ? '—' : metric.startsWith('hiring') ? fmtPctSigned(v) : fmtScore(v)));153 return (154 <ol className={cn('divide-y divide-rule border-y border-rule', className)}>155 {items.map((c, i) => {156 const v = num(c.metrics[metric]);157 return (158 <li key={c.id}>159 <Link href={routes.company(c.slug)} className="flex items-center gap-2.5 py-2 hover:text-accent">160 <span className="tnum w-4 shrink-0 text-xs text-ink-3">{i + 1}</span>161 <CompanyLogo name={c.display_name} candidates={logoCandidates(c)} size={24} rounded="sm" />162 <span className="min-w-0 flex-1">163 <span className="block truncate text-[14px] font-medium text-ink">{c.display_name}</span>164 <span className="mono block truncate text-[11px] text-ink-3">165 {c.canonical_domain}166 {c.country ? ` · ${c.country}` : ''}167 </span>168 </span>169 {sparkline && <Sparkline values={c.sparkline} width={56} height={18} tone={metric.startsWith('hiring') ? 'auto' : 'accent'} />}170 <span className="text-right">171 <span className={cn('tnum block text-[14px] font-medium', v !== null && metric.startsWith('hiring') && (v > 0 ? 'text-positive' : v < 0 ? 'text-danger' : ''))}>{fmt(v)}</span>172 {label && <span className="block text-[10px] uppercase tracking-wider text-ink-3">{label}</span>}173 </span>174 </Link>175 </li>176 );177 })}178 </ol>179 );180}181