Entwicklerdokumentation

Events

Die Events einer Location mit renderEventsWidget rendern – Karten, Detail, Anmeldung, Tickets – oder eigene Eventseiten auf den Event-Aufrufen des Clients.

Dieselbe SDK-Datei, die das Buchungswidget rendert, rendert auch die Events einer Location: veröffentlichte Events als Karten, das Detail eines Events, ein Anmeldeformular für kostenlose Events und eine Bestätigung mit den Ticketcodes. Zwei Wege hinein, beide auf derselben Events-API:

  • renderEventsWidget – ein Aufruf, ein vollständiger Ablauf in einem Element Ihrer Wahl, stilisoliert wie das Buchungswidget. Für eine Location-Website ohne eigene Eventseiten.
  • Der Headless-Clientclient.events(), client.event(), client.registerForEvent(), client.ticket() und client.cancelEventRegistration() neben den Buchungsaufrufen aus Eigene Formulare. Für eine Website, die schon einen Eventbereich hat und nur die Daten und den Anmeldeaufruf braucht.

Beides lässt sich kombinieren: Eine Website kann die Liste mit eigenen Karten rendern und ein gewähltes Event dem Widget übergeben – oder umgekehrt.

Installation

Laden Sie das SDK wie für das Buchungswidget – über den Alias der Hauptversionslinie oder eine exakte Version mit Integritäts-Hash (CDN) – und fügen Sie ein Element für die Events ein:

<div id="bookdineplay-events"></div>
<script src="https://cdn.bookdineplay.com/sdk/v0/bookdineplay.js"></script>

Events-Widget rendern

<script>
  window.BookDinePlay.renderEventsWidget({
    container: '#bookdineplay-events',
    venueSlug: 'your-venue',
    apiBaseUrl: 'https://api.bookdineplay.com',
    publishableKey: 'bdp_pk_your_publishable_key',
    layout: 'grid',
    language: 'de',
    theme: 'auto'
  });
</script>

Der Aufruf gibt die Widget-Instanz zurück oder null, wenn er das Rendern verweigert hat (Wenn es nicht rendert). Eine Seite kann ein Events-Widget und ein Buchungswidget nebeneinander tragen; sie teilen sich Script und Schlüssel.

Optionen

Option Pflicht Bedeutung
container ja Ein CSS-Selektor oder ein DOM-Element. Das Widget rendert darin.
venueSlug ja Der Slug Ihrer Location.
apiBaseUrl ja https://api.bookdineplay.com. Nur bei einer privaten Installation anders.
publishableKey ja Der bdp_pk_…-Schlüssel der Location. Nie ein geheimer Schlüssel.
layout nein grid (Standard) – Karten in Spalten – oder list, eine Karte pro Zeile.
eventSlug nein Rendert direkt das Detail dieses einen Events, ohne Liste und ohne Zurück-Button – für eine Seite, die die Seite des Events ist (Ein Event pro Seite).
language nein en (Standard) oder de – die eigenen Texte des Widgets (Buttons, Zustände, Datumsangaben) und das Accept-Language, das es sendet, damit Problemmeldungen in derselben Sprache zurückkommen. Jedes de-*-Tag zählt als de. Eventtitel und -beschreibungen kommen immer in der Sprache, in der die Location sie geschrieben hat.
onNavigate nein function (slug, summary). Wenn gesetzt, ruft die Wahl einer Karte diese Funktion auf, statt das Detail im Widget zu öffnen – die Website leitet auf ihre eigene Eventseite (Ein Event pro Seite).
theme nein auto (folgt der Systemeinstellung des Besuchers, Standard), light oder dark.

Was der Gast sieht

  1. Die Liste – jedes anstehende veröffentlichte Event als Karte: Bild (mit Alt-Text), ein Datums-Badge (oder „Laufend“ für ein Event ohne Datum), ein Chip – Kostenlos oder Tickets –, Titel, Teaser und eine Zeile mit Wochentag, Datum, Uhrzeiten und dem Zeitlabel der Location. „Ausverkauft“ erscheint erst im Detail, sobald das Anmeldefenster bekannt ist. Eine Location ohne anstehende Events zeigt einen leeren Zustand; eine Location, deren Tarif keine Events enthält, sieht genauso aus, weil die API eine leere Liste liefert.
  2. Das Detail – Bild, Titel, Teaser, die Beschreibung, wie die Location sie geschrieben hat, und die Fakten: wann, wo, Preis, verbleibende Tickets. Darunter entweder ein Anmelden-Button oder der Grund, warum es keinen gibt (Tabelle der Zustände unten).
  3. Das Formular – Name und E-Mail (Pflicht), Telefon, die Ticketanzahl (1 bis zum kleineren Wert von maxPerRegistration und remaining), ein Teilnehmername pro Ticket (der erste spiegelt den Käufer, bis er bearbeitet wird), Anmerkungen. Absenden bleibt deaktiviert, bis das Formular gültig ist; ein unsichtbares Honeypot-Feld verwirft Bot-Eingaben stillschweigend. Eine abgelehnte Anmeldung zeigt den Grund der API über dem Formular: den Satz des Fensters bei registration-closed, den „bald verfügbar“-Text bei ticket-sales-not-enabled, die Validierungsmeldungen bei einer 400.
  4. Die Bestätigung – die Referenz, jedes Ticket mit Code, Teilnehmer und einem Link Ticket anzeigen zur Ticketseite sowie ein Link Anmeldung stornieren. Dasselbe kommt per E-Mail (Was der Gast erhält).

Die Widget-Wurzel meldet Ansichtswechsel an Screenreader (aria-live="polite"); Ladefehler sind role="alert" mit Wiederholen, Ladezustände role="status".

Der Zustand des Details ergibt sich aus status und registration des Events – BookDinePlay.helpers.registrationState(detail) liefert denselben Wert für eine Seite mit eigenem Markup:

Zustand Wann Text (Deutsch)
open Ein kostenloses Event mit offenem Fenster Der Anmelden-Button
none Anmeldemodus None „Keine Anmeldung nötig – kommen Sie einfach vorbei.“
closed registration.reason ist closed „Die Anmeldung für diese Veranstaltung ist geschlossen.“
sold-out registration.reason ist sold-out „Diese Veranstaltung ist ausverkauft.“
past registration.reason ist past „Diese Veranstaltung hat bereits stattgefunden.“
not-open registration.reason ist not-open „Die Anmeldung für diese Veranstaltung ist nicht geöffnet.“
cancelled status ist Cancelled „Diese Veranstaltung wurde abgesagt.“
paid Ein bezahltes Event mit offenem Fenster „Tickets bald verfügbar“ – der Online-Ticketverkauf ist noch nicht freigeschaltet

Ein Event pro Seite

Eine Website, die jedem Event eine eigene URL gibt – /events/darts-league-night –, nutzt die beiden Optionen zusammen. Auf der Eventübersicht wird geroutet, statt das Detail im Widget zu öffnen:

<script>
  window.BookDinePlay.renderEventsWidget({
    container: '#bookdineplay-events',
    venueSlug: 'your-venue',
    apiBaseUrl: 'https://api.bookdineplay.com',
    publishableKey: 'bdp_pk_your_publishable_key',
    onNavigate: function (slug) { window.location.href = '/events/' + slug + '/'; }
  });
</script>

Auf der Eventseite wird dieses Event direkt gerendert. Das Widget zeigt Detail, Formular und Bestätigung, aber keine Liste und keinen Zurück-Button – das übernimmt die Navigation der Seite:

<script>
  window.BookDinePlay.renderEventsWidget({
    container: '#bookdineplay-event',
    venueSlug: 'your-venue',
    apiBaseUrl: 'https://api.bookdineplay.com',
    publishableKey: 'bdp_pk_your_publishable_key',
    eventSlug: 'darts-league-night'
  });
</script>

onNavigate erhält als zweites Argument die summary der Karte – den Listeneintrag aus GET /api/venues/{venueSlug}/events – für einen Router, der mehr als den Slug will. Ein eventSlug, den die API nicht kennt, rendert „Diese Veranstaltung ist nicht mehr verfügbar“ mit Wiederholen.

Theming

Das Events-Widget rendert in derselben isolierten Wurzel wie das Buchungswidget und liest dieselben neun --bdp-*-Custom-Properties von der Seite – --bdp-accent für Chips, Badges und den Anmelden-Button, --bdp-surface für die Karten, --bdp-radius für deren Ecken. Einmal gesetzt, folgen beide Widgets; Theming listet jede Property mit ihrem Standard. Eine Events-spezifische Property gibt es nicht.

Wenn es nicht rendert

renderEventsWidget gibt null zurück und schreibt eine Zeile in die Browserkonsole, statt eine Ausnahme in Ihre Seite zu werfen – dieselben drei Ablehnungen wie renderBookingWidget (Wenn es nicht rendert): Der Container wurde nicht gefunden, venueSlug oder apiBaseUrl fehlt, oder publishableKey fehlt bzw. ist kein bdp_pk_…-Schlüssel.

Der Headless-Client

BookDinePlay.createClient (Eigene Formulare) hat fünf Event-Aufrufe neben den vier Buchungsaufrufen. Jeder gibt ein Promise auf den geparsten JSON-Body zurück, nimmt { signal } als optionales letztes Argument und lehnt mit einem BookDinePlayError ab (Fehler):

Methode Endpunkt Liefert
client.events({ upcoming }) GET /api/venues/{venueSlug}/events Das events-Array der Zusammenfassungen (nicht die Hülle); upcoming ist standardmäßig true und wird nur als ?upcoming=false gesendet
client.event(eventSlug) GET /api/venues/{venueSlug}/events/{eventSlug} Das Detail mit seinem registration-Fenster; lehnt bei unbekanntem oder unveröffentlichtem Event mit status: 404 ab
client.registerForEvent(eventSlug, input) POST …/events/{eventSlug}/registrations Die Anmeldung – reference, tickets[], cancelUrl
client.ticket(code) GET /api/tickets/{code} Das Ticket mit seinem qrSvg; es wird kein Schlüssel gesendet
client.cancelEventRegistration(cancelToken) POST /api/event-registrations/{cancelToken}/cancel Die stornierte Anmeldung; es wird kein Schlüssel gesendet

registerForEvent(eventSlug, input) nimmt buyerName, email, phone, notes, quantity und attendees und sendet sie so geformt wie das Widget: Zeichenketten getrimmt, phone und notes null, wenn leer, quantity als Zahl, attendees ein getrimmter Name pro Eintrag. Ein fehlender eventSlug, code oder cancelToken wirft synchron einen TypeError.

Die beiden schlüssellosen Aufrufe brauchen keinen venueSlug: Eine Seite, die nur ein Ticket oder einen Storno-Link zeigt, kann ihren Client allein mit apiBaseUrl und publishableKey erstellen; nur die an die Location gebundenen Aufrufe werfen ohne Slug.

const client = window.BookDinePlay.createClient({
  apiBaseUrl: 'https://api.bookdineplay.com',
  publishableKey: 'bdp_pk_your_publishable_key',
  venueSlug: 'your-venue',
  language: 'de'
});

Eigenes Markup bauen

Der ganze Ablauf im eigenen HTML der Seite – die Reihenfolge, die das Widget durchläuft, mit Ihren Vorlagen. Zuerst die Liste: eine Karte pro Event, verlinkt auf die Seite des Events. BookDinePlay.helpers enthält die reinen Formatierungsfunktionen des Widgets (formatEventDate, dateBadge, eventChip, registrationState, maxQuantity), damit Datumsangaben und Zustände genauso lesen wie im Widget:

const helpers = window.BookDinePlay.helpers;
const list = document.querySelector('#events');

async function renderList() {
  const events = await client.events();
  if (!events.length) {
    list.textContent = 'Keine anstehenden Veranstaltungen.';
    return;
  }
  for (const event of events) {
    const card = document.createElement('a');
    card.href = '/events/' + event.slug + '/';

    const title = document.createElement('strong');
    title.textContent = event.title;

    const when = document.createElement('span');
    when.textContent = helpers.formatEventDate('de', event.date, event.start, event.end, event.timeLabel);

    const chip = document.createElement('span');
    chip.textContent = event.registrationMode === 'Free' ? 'Kostenlos' : event.registrationMode === 'Paid' ? 'Tickets' : '';

    const excerpt = document.createElement('p');
    excerpt.textContent = event.excerpt || '';

    card.append(title, when, chip, excerpt);
    list.append(card);
  }
}
renderList();

Dann die Eventseite: das Detail lesen, die Beschreibung zeigen (descriptionHtml ist serverseitig bereinigt) und aus registrationState ableiten, ob das Formular erscheint:

const slug = location.pathname.split('/').filter(Boolean).pop();
const page = document.querySelector('#event');
const form = document.querySelector('#register');

async function renderEvent() {
  let detail;
  try {
    detail = await client.event(slug);
  } catch (err) {
    page.textContent = err.status === 404 ? 'Diese Veranstaltung ist nicht mehr verfügbar.' : err.message;
    throw err;
  }

  page.querySelector('h1').textContent = detail.title;
  page.querySelector('.description').innerHTML = detail.descriptionHtml;
  page.querySelector('.when').textContent =
    helpers.formatEventDate('de', detail.date, detail.start, detail.end, detail.timeLabel);
  page.querySelector('.where').textContent = detail.locationLabel;

  const state = helpers.registrationState(detail);   // 'open' | 'none' | 'closed' | 'sold-out' | 'past' | 'not-open' | 'cancelled' | 'paid'
  if (state === 'open') {
    form.hidden = false;
    form.elements.quantity.max = helpers.maxQuantity(detail.registration);
  } else {
    page.querySelector('.state').textContent = stateText(state); // Ihre eigene Formulierung je Zustand
  }
}
renderEvent();

Und die Anmeldung: ein Teilnehmerfeld pro Ticket, das erste fällt auf den Käufer zurück, dann der Aufruf. Bei Erfolg Referenz und Tickets zeigen; bei einer Ablehnung, was der Fehler trägt:

form.addEventListener('submit', async (event) => {
  event.preventDefault();
  const data = new FormData(form);
  const quantity = Number(data.get('quantity'));
  const attendees = [];
  for (let i = 0; i < quantity; i++) attendees.push(data.get('attendee' + i) || '');

  try {
    const registration = await client.registerForEvent(slug, {
      buyerName: data.get('name'),
      email: data.get('email'),
      phone: data.get('phone'),
      notes: data.get('notes'),
      quantity,
      attendees
    });
    showConfirmation(registration.reference, registration.tickets, registration.cancelUrl);
  } catch (err) {
    if (err.name === 'AbortError') return;
    if (err.reason) {                 // 409 registration-closed: 'closed' | 'sold-out' | 'past' | 'not-open'
      showError(stateText(err.reason));
      if (err.reason === 'sold-out') form.hidden = true;
      return;
    }
    if (err.status === 400) {         // Validierung – Feldname (oder 'registration') → Meldungen
      showError(err.messages().join(' '));
      return;
    }
    showError(err.message);           // ticket-sales-not-enabled, 403, Netzwerk – der Satz der API
  }
});

showConfirmation, showError und stateText sind Ihre. Jedes Ticket hat eine ticketUrl zum Verlinken und einen code zum Anzeigen; cancelUrl ist die Storno-Seite des Gastes. Der Gast bekommt dasselbe per E-Mail, eine Website kann also eine kurze Bestätigung zeigen und den Rest dem Posteingang überlassen.

Ticket- und Storno-Seiten auf Ihrer Website

Beide Seiten gibt es auf app.bookdineplay.com, und die E-Mails verlinken dorthin (Was der Gast erhält). Eine Website, die sie unter eigener Domain haben will, rendert sie aus den beiden schlüssellosen Aufrufen – ein Client ohne venueSlug reicht hier:

const client = window.BookDinePlay.createClient({
  apiBaseUrl: 'https://api.bookdineplay.com',
  publishableKey: 'bdp_pk_your_publishable_key'
});

const code = location.pathname.split('/').pop();
const ticket = await client.ticket(code);             // sendet keinen Schlüssel; lehnt bei unbekanntem Code mit 404 ab
document.querySelector('#qr').innerHTML = ticket.qrSvg;
document.querySelector('#attendee').textContent = ticket.attendeeName;
document.querySelector('#status').textContent = ticket.status;  // 'Valid' | 'CheckedIn' | 'Cancelled'

document.querySelector('#cancel').addEventListener('click', async () => {
  const token = new URLSearchParams(location.search).get('token');
  const result = await client.cancelEventRegistration(token); // sendet keinen Schlüssel; 409, sobald ein Ticket eingecheckt wurde
  document.querySelector('#status').textContent = result.status; // 'Cancelled'
});

ticketUrl und cancelUrl in der Anmeldeantwort zeigen weiterhin auf die BookDinePlay-Seiten; um Gäste auf Ihre zu schicken, bauen Sie die Links aus ticket.code und dem letzten Segment von cancelUrl.

Fehler

Jeder Client-Aufruf lehnt mit einem BookDinePlayError ab, wenn die Anfrage nicht erfolgreich war; ein von Ihnen ausgelöster AbortError geht unverändert durch. Zusätzlich zu den Feldern aus Eigene Formularename, status, problem, message – werden die Event-Probleme auf den Fehler gehoben, damit Sie die RFC-7807-Form nie kennen müssen:

Feld Wert
type Die type-URL des Problems – endet auf registration-closed, ticket-sales-not-enabled, feature-not-in-plan oder einen Schlüsselgrund; die allgemeine RFC-9110-URL bei einer 400/404/einfachen 409; null, wenn die Antwort keinen Problem-Body hatte
reason Bei registration-closed: 'closed', 'sold-out', 'past' oder 'not-open'; sonst null
remaining 0 bei sold-out; sonst null
errors Bei einer 400: das Validierungs-Wörterbuch, Feldname (oder registration bei einer verletzten Regel) → Array von Meldungen; sonst null
messages() Jede Meldung aus errors als ein flaches Array – [], wenn es keine gibt
try {
  await client.registerForEvent(slug, input);
} catch (err) {
  if (err.type && err.type.endsWith('/ticket-sales-not-enabled')) showError('Tickets bald verfügbar');
  else if (err.reason === 'sold-out') showError('Ausverkauft');
  else if (err.reason) showError('Die Anmeldung ist nicht geöffnet');
  else if (err.status === 400) showError(err.messages().join(' '));
  else showError(err.message);
}

Die Meldungen in errors kommen in der language des Clients. Eine 403 bei der Anmeldung ist feature-not-in-plan – der Tarif der Location enthält keine Events – oder ein Schlüsselproblem; ein status von 0 bedeutet, dass gar keine Antwort ankam.

Nächste Schritte

  • Events-API – jedes Feld von Liste, Detail, Anmeldung und Ticket.
  • Fehlerregistration-closed und ticket-sales-not-enabled im Detail.
  • Eigene Formulare – die Buchungsaufrufe desselben Clients.
  • WordPress-Plugin – dasselbe Widget als [bookdineplay_events] und [bookdineplay_event].