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%
4.1 KB · 64 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import { Suspense } from 'react';3import { EventFilters } from '@/components/events/event-filters';4import { EventList } from '@/components/events/event-row';5import { EventTypeBadge } from '@/components/ui/badges';6import { Pagination, withParams } from '@/components/ui/pagination';7import { Container, Note, PageHeader, Unavailable } from '@/components/ui/section';8import { api, safe } from '@/lib/api';9import { fmtInt, fmtPctSigned } from '@/lib/format';10import { flat, int, str, type SP } from '@/lib/params';1112export const metadata: Metadata = { title: 'Events', description: 'Structured corporate events detected across monitored public surfaces, with filters by type, importance, confidence, country and industry.' };13export const revalidate = 60;1415export default async function EventsPage({ searchParams }: { searchParams: Promise<SP> }) {16  const sp = await searchParams;17  const cur = flat(sp);18  const page = int(sp.page, 1);19  const query = { event_type: str(sp.event_type), event_subtype: str(sp.event_subtype), country: str(sp.country), industry: str(sp.industry), min_importance: str(sp.min_importance), min_confidence: str(sp.min_confidence), q: str(sp.q), origin: str(sp.origin), surface: str(sp.surface), company: str(sp.company), sort: str(sp.sort), since: str(sp.since), until: str(sp.until), page, per_page: 50 };20  const [data, summary, countries, industries] = await Promise.all([safe(api.events(query)), safe(api.eventSummary(7, 'type')), safe(api.countries()), safe(api.industries())]);21  return (22    <Container wide>23      <PageHeader eyebrow="Events" title="Structured corporate events" lede="Each event is an interpreted change on a monitored public page, with the source URL, detection time, before/after values and a confidence label. Wording is deliberately careful: a listing that disappears is “no longer listed”, never more." />24      {summary?.items.length ? (25        <div className="no-scrollbar -mx-4 mb-4 flex gap-4 overflow-x-auto px-4 pb-1 md:mx-0 md:px-0">26          {summary.items.slice(0, 12).map((s) => (27            <div key={s.key} className="shrink-0">28              <EventTypeBadge type={s.key} small />29              <p className="tnum mt-0.5 text-sm font-medium">30                {fmtInt(s.count)} <span className={`text-[11px] font-normal ${(s.delta_pct ?? 0) > 0 ? 'text-positive' : (s.delta_pct ?? 0) < 0 ? 'text-danger' : 'text-ink-3'}`}>{s.delta_pct === null ? '' : fmtPctSigned(s.delta_pct, 0)}</span>31              </p>32            </div>33          ))}34          <p className="shrink-0 self-end text-[10px] uppercase tracking-wider text-ink-3">last 7 days vs previous 7</p>35        </div>36      ) : null}37      <Suspense>38        <EventFilters countries={(countries?.items ?? []).map((c) => ({ value: c.code, label: c.name }))} industries={(industries?.items ?? []).map((i) => ({ value: i.slug, label: i.name }))} className="mb-4" />39      </Suspense>40      <form method="get" action="/events" className="mb-3 flex max-w-lg gap-2">41        {Object.entries(cur)42          .filter(([k, v]) => k !== 'q' && k !== 'page' && v)43          .map(([k, v]) => (44            <input key={k} type="hidden" name={k} value={v} />45          ))}46        <input type="search" name="q" defaultValue={str(sp.q)} placeholder="Search event titles and summaries…" className="field flex-1" aria-label="Search events" />47        <button type="submit" className="btn">48          Search49        </button>50      </form>51      {!data ? (52        <Unavailable what="Events" />53      ) : (54        <>55          <p className="tnum mb-1 text-xs text-ink-3">{fmtInt(data.total)} events</p>56          <EventList events={data.items} variant="table" />57          <Pagination total={data.total} page={data.page} pages={data.pages} perPage={data.per_page} makeHref={(p) => withParams('/events', cur, { page: p > 1 ? p : undefined })} className="mt-4" />58        </>59      )}60      <Note className="mt-6">Retracted events remain visible with a strike-through and are excluded from metrics. Duplicate detections are merged into one canonical event whose sources list every surface that corroborated it.</Note>61    </Container>62  );63}64