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%
10.3 KB · 181 lines tsx
Raw Blame History
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