The Event detail page is the heaviest in Lookout: it fires roughly nine GETs in parallel on mount and composes a full event picture. The same component tree powers the Wave campaign detail (which adds a setup wizard on top).

What you’ll build

  • Hero (title, date, venue, status pills, action menu).
  • Event stats bar (Revenue / Purchases / Spent / ROAS / Active / Campaigns).
  • Sales overview chart (benchmark vs prediction vs actuals).
  • Daily revenue chart (with range and metric toggles).
  • Taste clusters panel.
  • Ticket categories chart + ticket type chart.
  • Side actions: edit, flag, archive, add sales, clear sales, edit series.

Prerequisites

  • An eid (from the Events list row click or a deep link).
  • Tenant-level partnerId set in your client.

The call chain

All in parallel on mount unless noted:
#MethodPathComponentPurpose
1GET/events/{eid}HeroTitle, date, venue, status, category, currency. 404 → redirect to 404 page.
2GET/integrations/facebook/events/{eid}/campaign_resultsEventStatsProviderBranch the layout between “has Meta stats” and “no stats”.
3GET/integrations/facebook/events/{eid}/fb_insightsEventStatsProviderImpressions / clicks / ad spend.
4GET/events/{eid}/statisticsEventStatsBarPer-event KPIs: revenue, purchases, spent, roas, currency, campaigns, active.
5GET/events/{eid}/benchmarkSalesOverview{ today/now, prediction, state }. Auto-detect fraction vs percent.
6GET/events/{eid}/sales-graphSalesOverview{ sales: [{ mapped_date, norm_tickets_sold_current, norm_tickets_sold_avg }] }.
7GET/daily_revenue_summary/?event_id=&currency=&start_date=1900-01-01&end_date=2100-01-01DailyRevenueChartDaily revenue + manual adjustments.
8GET/events/{eid}/ticketsTicketCategories + TicketTypeChartFired twice — once per chart. Easy dedup win.
9GET/events/{eid}/clustersTasteClustersPanelAffinity clusters for this event.
10GET/events/{eid}/campaignsTasteClustersPanelUsed only for suggested_creative keyword chips.
11GET/events/{eid}/attribution_modelEventAttributionModelContextProviderCurrent attribution model.
The reference webapp fires /integrations/facebook/... even for non-Facebook partners. Treat 404 / empty payloads as “no Meta stats” silently.

Payload — /events/{eid}/benchmark

{
  "today":      { "value": 0.42, "benchmark": 0.50 },
  "prediction": { "value": 0.78, "benchmark": 0.65 },
  "state":      "above"
}
Scale auto-detection. Some deployments return fractions (0.78), some return percentages (78). The reference webapp infers per response:
const peak = Math.max(today.value, prediction.value);
const factor = peak < 1.5 ? 100 : 1;     // fraction → ×100, percent → ×1

Payload — /events/{eid}/sales-graph

{
  "sales": [
    { "mapped_date": "2026-05-01", "norm_tickets_sold_current": 0.12, "norm_tickets_sold_avg": 0.10 },
    { "mapped_date": "2026-05-02", "norm_tickets_sold_current": 0.18, "norm_tickets_sold_avg": 0.13 }
  ]
}
mapped_date is a normalised x-axis so multiple events can be compared on the same time-to-event scale.

Payload — /daily_revenue_summary/

{
  "summaries": [
    {
      "date": "2026-05-15",
      "revenue": { "total": 4250, "parts": { "primary": 4250, "secondary": 0 } },
      "number_of_tickets": { "total": 95, "parts": { "primary": 95, "secondary": 0 } }
    }
  ],
  "manual_adjustments": [
    { "id": "ma_xyz", "date": "2026-05-14", "revenue": 150, "tickets": 3, "note": "..." }
  ]
}
The chart range toggle (14d / 8w / all) filters this client-side; the metric toggle (Tickets / Revenue) just flips which series renders.

Reference implementation

export function EventDetail({ eid }: { eid: string }) {
  const event   = useFd<Event>(`/events/${eid}`);
  const stats   = useFd<EventStats>(`/events/${eid}/statistics`);
  const bench   = useFd<Benchmark>(`/events/${eid}/benchmark`);
  const sales   = useFd<SalesGraph>(`/events/${eid}/sales-graph`);
  const daily   = useFd<DailyRevenue>(`/daily_revenue_summary/`, {
    event_id: eid,
    currency: event.data?.currency,
    start_date: "1900-01-01",
    end_date:   "2100-01-01",
  });
  const tickets = useFd<Tickets>(`/events/${eid}/tickets`);
  const clusters = useFd<Clusters>(`/events/${eid}/clusters`);
  const attrModel = useFd<AttributionModel>(`/events/${eid}/attribution_model`);

  if (!event.data) return <Skeleton />;
  if (event.error?.status === 404) return <NotFound />;

  return (
    <>
      <Hero event={event.data} />
      <EventStatsBar stats={stats.data} />
      <SalesOverview benchmark={bench.data} sales={sales.data} />
      <DailyRevenueChart data={daily.data} currency={event.data.currency} />
      <TasteClustersPanel clusters={clusters.data} eid={eid} />
      <TicketCategoriesChart tickets={tickets.data} />
      <TicketTypeChart tickets={tickets.data} />
    </>
  );
}
Pass the same tickets.data to both ticket charts to dedupe the call — the reference webapp doesn’t do this and pays for it.

Status updates (flag / archive) — the two-step dance

export async function setFrontendStatus(
  eid: string,
  status: "FLAGGED" | "DEFAULT" | "HIDDEN",
) {
  // Step 1 — main API, 204 No Content
  await fd.put(`/events/${eid}/setup_frontend_status`, { frontend_status: status });

  // Step 2 — CR API on a different host
  const crStatus = status === "HIDDEN" ? "CANCELLED" : "RUNNING";
  await fdCr.put(`/events/last_updated_on_fe/${eid}`, { status: crStatus });
}
If step 2 fails, surface an error and offer “Retry sync” — don’t silently proceed.

Other interactions

InteractionAPI calls
Open in Wavenavigate to /campaigns/{eid} (no API)
Edit (event)navigate to /events/edit/{eid} — see Event editor
Edit event series (modal)GET /events/{eid}/series?limit=1000; GET /events/{eid}/available_events_for_series?limit=1000; save via POST /events/{eid}/series?event_series=...&event_series=...; remove via DELETE /events/{eid}/series?event_series=...
Add sales (manual)navigate to /events/{eid}/update-sales/salesSummary — see Sales uploads
Add sales (file)navigate to /events/{eid}/update-sales/documentUpload — see Sales uploads
Clear salesDELETE /daily_revenue_summary?eid={eid}
Toggle flaggedsetFrontendStatus(eid, "FLAGGED" | "DEFAULT")
ArchivesetFrontendStatus(eid, "HIDDEN") then redirect to /events
Range / metric togglesnone (client-side only)

Gotchas

Fire them all in parallel — sequencing them is the easiest way to make this page slow. HTTP/2 keeps the cost reasonable, but if your client serialises requests by accident (an await in the wrong place), you’ll see it immediately.
Once per chart. Pass the data through context or dedupe via your request cache. This is the single easiest perf win over the reference webapp.
Apply the scale heuristic on every response; don’t cache the inferred factor across events.
A GET /events/{eid} 404 means the user typed a bad URL or the event was hard-deleted. Redirect to a 404 page rather than showing an inline error — the reference webapp does <Redirect to="/404">.
Even for non-Meta partners. Don’t try to suppress them — handle the empty/404 response gracefully.