The portal timeline is now available

Webhook Dashboards, Ready to Paste

The Repost portal's surfaces as blocks: delivery history, endpoint management, the dead-letter queue and the event catalog. Wired to your data, owned by you. Open Source.

Files
components/event-catalog.tsx
"use client";

import { RepostPortalProvider, type PortalClient, useEndpoints, useEventTypes, useSendSampleEvent } from "@repost/portal-react";
import { useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { EventCatalogBrowser, type CatalogEventType } from "@/components/event-catalog-browser";

export type EventCatalogProps = {
  /** Return a portal access token minted by your backend. */
  getToken?: () => Promise<string>;
  /**
   * A pre-built client, instead of `getToken` — used by previews and tests to
   * run the block against fixtures.
   */
  client?: PortalClient;
};

/**
 * The event catalog, wired: browse the environment's event types with
 * schema-derived samples and send signed test events to the customer's
 * first endpoint. Hooks at the top, props down.
 */
export function EventCatalog({ getToken, client }: EventCatalogProps) {
  return (
    <RepostPortalProvider {...(client ? { client } : { getToken: getToken! })}>
      <EventCatalogContent />
    </RepostPortalProvider>
  );
}

function EventCatalogContent() {
  const [sendingEventTypeId, setSendingEventTypeId] = useState<string | null>(null);
  const [lastOutcome, setLastOutcome] = useState<string | null>(null);

  const eventTypes = useEventTypes();
  const endpoints = useEndpoints();
  const sendSample = useSendSampleEvent();

  const targetEndpoint = useMemo(() => (endpoints.data ?? []).find((endpoint) => endpoint.status === "ENABLED"), [endpoints.data]);

  const handleSendSample = (eventType: CatalogEventType, samplePayload: unknown) => {
    if (!targetEndpoint) {
      setLastOutcome("No enabled endpoint to send to — create one first.");
      return;
    }
    setSendingEventTypeId(eventType.id);
    sendSample
      .mutateAsync({
        eventTypeId: eventType.id,
        destination: { kind: "endpoint", endpointId: targetEndpoint.id },
        data: samplePayload,
      })
      .then((result) =>
        setLastOutcome(result.delivered ? `Delivered to ${targetEndpoint.url} (${result.status})` : `Failed: ${result.error ?? result.status}`),
      )
      .catch((error: Error) => setLastOutcome(`Failed: ${error.message}`))
      .finally(() => setSendingEventTypeId(null));
  };

  return (
    <div className="flex flex-col gap-3">
      <header className="flex items-center gap-2">
        <h2 className="text-lg font-semibold">Event catalog</h2>
        {lastOutcome && (
          <Badge variant={lastOutcome.startsWith("Delivered") ? "success" : "warning"} size="sm">
            {lastOutcome}
          </Badge>
        )}
      </header>
      <EventCatalogBrowser
        eventTypes={eventTypes.data ?? []}
        isLoading={eventTypes.isLoading}
        onSendSample={handleSendSample}
        sendingEventTypeId={sendingEventTypeId}
      />
    </div>
  );
}
Event types with sample payloads and signed test sends.
event-catalog
event-catalog