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%
15.4 KB · 255 lines tsx
Raw Blame History
1import { ArrowRight, Code2 } from 'lucide-react';2import Link from 'next/link';3import { Suspense } from 'react';4import { Sparkline } from '@/components/charts/sparkline';5import { WorldMap } from '@/components/charts/world-map';6import { CompanyMiniList } from '@/components/company/company-table';7import { EventList } from '@/components/events/event-row';8import { LiveCounters } from '@/components/live/counters';9import { LiveFeed } from '@/components/live/live-feed';10import { Chip, CountryChip } from '@/components/ui/badges';11import { Container, Empty, Note, Section, Stat, StatGrid, Unavailable } from '@/components/ui/section';12import { api, liveItems, safe } from '@/lib/api';13import { countryName } from '@/lib/countries';14import { fmtBytes, fmtDate, fmtDays, fmtInt, fmtPctSigned, fmtScore } from '@/lib/format';15import { ALT_TAGLINE, DESCRIPTION, PUBLIC_API_BASE, routes, TAGLINE } from '@/lib/site';16import type { Event, Pulse } from '@/lib/types';1718export const revalidate = 60;1920export default async function HomePage() {21  const [pulse, live, index, signals] = await Promise.all([safe(api.pulse()), safe(api.live({ limit: 40 })), safe(api.index()), safe(api.signals({ limit: 8 }))]);22  const initialLive: Event[] = liveItems(live).length ? liveItems(live) : (pulse?.live ?? []);23  const stats = pulse?.stats ?? (await safe(api.stats()));24  const p: Partial<Pulse> = pulse ?? {};2526  return (27    <>28      {/* Hero */}29      <div className="relative overflow-hidden border-b border-rule">30        <div className="grid-bg pointer-events-none absolute inset-0" aria-hidden />31        <Container className="relative pb-8 pt-10 md:pb-12 md:pt-16">32          <p className="eyebrow flex items-center gap-2">33            <span className="dot pulse" aria-hidden /> Continuous corporate observation network34          </p>35          <h1 className="display mt-3 max-w-4xl text-[34px] md:text-[60px]">{TAGLINE}</h1>36          <p className="mt-4 max-w-2xl text-[16px] leading-relaxed text-ink-2 md:text-[18px]">{DESCRIPTION}</p>37          <p className="mt-2 max-w-2xl text-sm text-ink-3">{ALT_TAGLINE}</p>38          <div className="mt-6 flex flex-wrap gap-2">39            <Link href={routes.live()} className="btn btn-primary">40              Open the live feed <ArrowRight className="size-4" aria-hidden />41            </Link>42            <Link href={routes.companies()} className="btn">43              Browse companies44            </Link>45            <Link href={routes.api()} className="btn">46              <Code2 className="size-4" aria-hidden /> API & data47            </Link>48          </div>49          <LiveCounters stats={stats} className="mt-8 md:mt-10" />50        </Container>51      </div>5253      <Container>54        {/* Live feed + movers */}55        <div className="grid gap-8 lg:grid-cols-12">56          <Section eyebrow="Live activity feed" title="What changed in the last minutes" action={{ href: routes.live(), label: 'Full feed' }} className="lg:col-span-7" hairline={false}>57            {live || pulse ? (58              <Suspense fallback={<EventList events={initialLive.slice(0, 18)} variant="feed" />}>59                <LiveFeed initial={initialLive} limit={18} compact showControls={false} />60              </Suspense>61            ) : (62              <Unavailable what="The live feed" />63            )}64          </Section>65          <Section eyebrow="Companies moving fastest" title="Highest Activity Score (7 days)" action={{ href: routes.rankings('most_active', '7d'), label: 'Rankings' }} className="lg:col-span-5" hairline={false}>66            {p.movers ? <CompanyMiniList items={p.movers} metric="activity_score" label="Activity" sparkline /> : <Unavailable what="Movers" compact />}67            <Note className="mt-3">Activity Score = decayed weight of meaningful changes and structured events over 30 days, normalised per active sensor (0–100, see methodology).</Note>68          </Section>69        </div>7071        {/* Map */}72        <Section eyebrow="Global activity map" title="Where monitored companies are changing" lede="Bubbles are sized by structured events detected in the last 30 days, clustered by headquarters city. Hover for detail, click to open the country atlas." action={{ href: routes.countries(), label: 'Country atlas' }}>73          {p.map && p.map.length ? (74            <div className="grid gap-6 lg:grid-cols-[minmax(0,2.2fr)_minmax(0,1fr)]">75              <WorldMap buckets={p.map} />76              <div>77                <p className="eyebrow mb-1">Most active countries (30 d)</p>78                <ol className="divide-y divide-rule border-y border-rule text-sm">79                  {(p.countries ?? []).slice(0, 8).map((c) => (80                    <li key={c.code}>81                      <Link href={routes.country(c.code)} className="flex items-center gap-2 py-1.5 hover:text-accent">82                        <CountryChip code={c.code} link={false} />83                        <span className="min-w-0 flex-1 truncate">{c.name}</span>84                        <span className="tnum text-xs text-ink-3">{fmtInt(c.companies)} co.</span>85                        <span className="tnum w-12 text-right font-medium">{fmtInt(c.events_30d)}</span>86                      </Link>87                    </li>88                  ))}89                </ol>90              </div>91            </div>92          ) : (93            <Unavailable what="The activity map" />94          )}95        </Section>9697        {/* Hiring / launches / pricing */}98        <div className="grid gap-8 md:grid-cols-3">99          <Section eyebrow="Hiring momentum" title="Open listings, 30-day change" action={{ href: routes.rankings('hiring_growth', '30d'), label: 'All' }} className="md:border-t">100            {p.hiring ? <CompanyMiniList items={p.hiring} metric="hiring_momentum_30d" /> : <Unavailable what="Hiring momentum" compact />}101          </Section>102          <Section eyebrow="Product launches" title="New products detected" action={{ href: routes.events({ event_type: 'PRODUCT' }), label: 'All' }}>103            {p.launches ? <EventList events={p.launches.slice(0, 6)} variant="feed" emptyLabel="No product launches detected in the current window." /> : <Unavailable what="Launches" compact />}104          </Section>105          <Section eyebrow="Pricing changes" title="Public pricing pages that changed" action={{ href: routes.events({ event_type: 'PRICING' }), label: 'All' }}>106            {p.pricing ? <EventList events={p.pricing.slice(0, 6)} variant="feed" emptyLabel="No pricing changes detected in the current window." /> : <Unavailable what="Pricing changes" compact />}107          </Section>108        </div>109110        {/* AI / industries / countries */}111        <div className="grid gap-8 md:grid-cols-3">112          <Section eyebrow="AI adoption" title="Observable public AI signals" action={{ href: routes.rankings('ai_active', '30d'), label: 'All' }}>113            {p.ai ? <CompanyMiniList items={p.ai} metric="ai_adoption" label="score" /> : <Unavailable what="AI adoption" compact />}114            <Note className="mt-2">Scored from public evidence only (AI listings, products, documentation, communications). Never a claim about internal use.</Note>115          </Section>116          <Section eyebrow="Industries" title="Industry atlas" action={{ href: routes.industries(), label: 'All industries' }}>117            {p.industries ? (118              <ol className="divide-y divide-rule border-y border-rule text-sm">119                {p.industries.slice(0, 10).map((i) => (120                  <li key={i.slug}>121                    <Link href={routes.industry(i.slug)} className="flex items-center gap-2 py-1.5 hover:text-accent">122                      <span className="min-w-0 flex-1 truncate">{i.name}</span>123                      <span className="tnum text-xs text-ink-3">{fmtInt(i.companies)} co.</span>124                      <span className="tnum w-14 text-right text-xs">{fmtScore(i.activity_score)}</span>125                      <span className="tnum w-12 text-right font-medium">{fmtInt(i.events_30d)}</span>126                    </Link>127                  </li>128                ))}129              </ol>130            ) : (131              <Unavailable what="Industries" compact />132            )}133            <p className="mt-1 text-right text-[10px] uppercase tracking-wider text-ink-3">activity · events 30 d</p>134          </Section>135          <Section eyebrow="Countries" title="Country atlas" action={{ href: routes.countries(), label: 'All countries' }}>136            {p.countries ? (137              <ol className="divide-y divide-rule border-y border-rule text-sm">138                {p.countries.slice(0, 10).map((c) => (139                  <li key={c.code}>140                    <Link href={routes.country(c.code)} className="flex items-center gap-2 py-1.5 hover:text-accent">141                      <CountryChip code={c.code} link={false} />142                      <span className="min-w-0 flex-1 truncate">{c.name}</span>143                      <span className={`tnum w-16 text-right text-xs ${(c.hiring_momentum_30d ?? 0) > 0 ? 'text-positive' : (c.hiring_momentum_30d ?? 0) < 0 ? 'text-danger' : 'text-ink-3'}`}>{fmtPctSigned(c.hiring_momentum_30d, 0)}</span>144                      <span className="tnum w-12 text-right font-medium">{fmtInt(c.events_30d)}</span>145                    </Link>146                  </li>147                ))}148              </ol>149            ) : (150              <Unavailable what="Countries" compact />151            )}152            <p className="mt-1 text-right text-[10px] uppercase tracking-wider text-ink-3">hiring 30 d · events 30 d</p>153          </Section>154        </div>155156        {/* Trending + index */}157        <div className="grid gap-8 lg:grid-cols-12">158          <Section eyebrow="Trending signals" title="Terms and patterns gaining momentum" action={{ href: routes.events(), label: 'Events' }} className="lg:col-span-7">159            <div className="grid gap-6 md:grid-cols-2">160              <div>161                <p className="eyebrow mb-1">Trending terms (7 d)</p>162                {p.trending?.length ? (163                  <ul className="divide-y divide-rule border-y border-rule text-sm">164                    {p.trending.slice(0, 8).map((t) => (165                      <li key={t.term} className="flex items-center gap-2 py-1.5">166                        <Link href={routes.search(t.term)} className="min-w-0 flex-1 truncate hover:text-accent">167                          {t.term}168                        </Link>169                        <Sparkline values={t.series} width={56} height={16} />170                        <span className="tnum text-xs text-ink-3">{fmtInt(t.companies)} co.</span>171                        <span className={`tnum w-14 text-right text-xs font-medium ${(t.momentum ?? 0) > 0 ? 'text-positive' : 'text-ink-3'}`}>{t.momentum === null ? '—' : fmtPctSigned(t.momentum, 0)}</span>172                      </li>173                    ))}174                  </ul>175                ) : (176                  <Empty compact />177                )}178              </div>179              <div>180                <p className="eyebrow mb-1">Cross-company signals</p>181                {signals?.items.length ? (182                  <ul className="divide-y divide-rule border-y border-rule text-sm">183                    {signals.items.slice(0, 6).map((s) => (184                      <li key={s.id} className="py-1.5">185                        <div className="flex items-center gap-2">186                          <Chip tone="accent">signal</Chip>187                          <span className="min-w-0 flex-1 truncate text-ink">{s.title}</span>188                          <span className="tnum text-xs text-ink-3">{Math.round(s.confidence * 100)} %</span>189                        </div>190                        {s.scope_key && <p className="text-[11px] text-ink-3">{s.scope} · {s.scope === 'country' ? countryName(s.scope_key) : s.scope_key}</p>}191                      </li>192                    ))}193                  </ul>194                ) : (195                  <Empty compact title="No cross-company signals detected yet." />196                )}197              </div>198            </div>199          </Section>200          <Section eyebrow="Global Corporate Activity Index" title="Baseline 100 = normalised historical activity" action={{ href: routes.methodology(), label: 'Methodology' }} className="lg:col-span-5">201            {p.activity_index ? (202              <div>203                <div className="flex items-end gap-4">204                  <p className="tnum text-[44px] font-semibold leading-none tracking-tight">{p.activity_index.value === null ? '—' : p.activity_index.value.toFixed(1)}</p>205                  <div className="pb-1 text-xs text-ink-3">206                    <p className={`tnum font-medium ${(p.activity_index.delta_7d ?? 0) > 0 ? 'text-positive' : (p.activity_index.delta_7d ?? 0) < 0 ? 'text-danger' : ''}`}>{fmtPctSigned(p.activity_index.delta_7d)} 7 d</p>207                    {index && <p className={`tnum ${(index.delta_30d ?? 0) > 0 ? 'text-positive' : (index.delta_30d ?? 0) < 0 ? 'text-danger' : ''}`}>{fmtPctSigned(index.delta_30d)} 30 d</p>}208                  </div>209                </div>210                <Sparkline values={(index?.series ?? p.activity_index.series).slice(-90).map((x) => x.value)} width={420} height={72} tone="accent" baseline={100} className="mt-3 h-auto w-full" />211                <p className="mt-1 text-[11px] text-ink-3">Last 90 days · coverage-normalised · {index?.formula_version ?? 'v1'}</p>212              </div>213            ) : (214              <Unavailable what="The index" compact />215            )}216          </Section>217        </div>218219        {/* Platform statistics + API CTA */}220        <Section eyebrow="Platform statistics" title="The dataset gets harder to reproduce every day">221          {stats ? (222            <StatGrid cols={6}>223              <Stat label="Dataset age" value={fmtDays(stats.dataset_age_days)} hint={stats.dataset_started_at ? `since ${fmtDate(stats.dataset_started_at)}` : undefined} size="sm" />224              <Stat label="Oldest continuous history" value={fmtDays(stats.oldest_history_days)} size="sm" />225              <Stat label="Observations today" value={fmtInt(stats.observations_today)} size="sm" />226              <Stat label="Changes today" value={fmtInt(stats.changes_today)} hint={`${fmtInt(stats.events_today)} events`} size="sm" />227              <Stat label="Countries · industries" value={`${fmtInt(stats.countries)} · ${fmtInt(stats.industries)}`} size="sm" />228              <Stat label="Archive" value={fmtBytes(stats.archive?.bytes)} hint={`${fmtInt(stats.archive?.objects)} content objects`} size="sm" />229            </StatGrid>230          ) : (231            <Unavailable what="Platform statistics" />232          )}233          <div className="mt-6 grid gap-4 border border-rule p-4 md:grid-cols-[minmax(0,1fr)_auto] md:items-center md:p-6">234            <div>235              <p className="eyebrow">API & data</p>236              <p className="mt-1 text-lg font-semibold tracking-tight">Every event, metric and history version is available through a public JSON API.</p>237              <p className="mt-1 text-sm text-ink-2">238                Companies, events with filters, timelines, rankings, industries, countries, search, streaming (SSE) and exports (JSON, NDJSON, CSV). <span className="mono text-xs text-ink-3">{PUBLIC_API_BASE}</span>239              </p>240            </div>241            <div className="flex flex-wrap gap-2">242              <Link href={routes.api()} className="btn btn-primary">243                Read the API docs244              </Link>245              <Link href={routes.methodology()} className="btn">246                Methodology247              </Link>248            </div>249          </div>250        </Section>251      </Container>252    </>253  );254}255