# Unidy Web SDK — Tickets

List, filter, paginate and export a user's tickets with <u-ticketable-list>. Templates are plain HTML — the SDK stamps one copy per ticket and fills in <ticketable-value> placeholders, so you keep full control over markup and styling. The same list also powers peer-to-peer ticket transfers.

## Setup

Every example below assumes the SDK is loaded and configured once per page. The
import map lets the headless examples use the bare `@unidy.io/sdk` specifier —
the same import a bundler/npm setup uses; drop the import map if you install the
package instead.

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@unidy.io/sdk@1.11.0/dist/sdk/sdk.css">
<script type="importmap">
  { "imports": { "@unidy.io/sdk": "https://cdn.jsdelivr.net/npm/@unidy.io/sdk@1.11.0/dist/sdk/index.esm.js" } }
</script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@unidy.io/sdk@1.11.0/dist/sdk/sdk.esm.js"></script>

<u-config
  base-url="https://demo.unidy.de"
  api-key="your-api-key"
  locale="en"
  check-signed-in="true"></u-config>
```

## Ticket cards

A card layout driven by a <template> element. ticketable-value reads any field from the ticket — including nested metadata with fallback defaults — while ticketable-conditional and unidy-attr let the markup react to the data.

- **<template> rendering** — one card stamped per ticket, your markup
- **Nested metadata** — name="metadata.category" with default values
- **Conditional content** — VIP badge and wallet export only when the data says so
- **Export** — PDF download and Apple Wallet (pkpass)

```html
<u-signed-in>
  <u-ticketable-list ticketable-type="ticket" limit="2" container-class="grid gap-4">
    <template>
      <div class="rounded-lg border border-border bg-white p-5 shadow-sm">
        <div class="flex items-start justify-between gap-4">
          <div>
            <h3 class="text-lg font-bold">
              <ticketable-value name="title"></ticketable-value>
            </h3>
            <p class="mt-1 text-sm text-text-light">
              <ticketable-value name="starts_at" date-format="dd.MM.yyyy HH:mm"></ticketable-value>
              –
              <ticketable-value name="ends_at" date-format="dd.MM.yyyy"></ticketable-value>
            </p>
            <p class="mt-2 font-semibold text-primary">
              <ticketable-value name="price" format="{{value}}" default="Free"></ticketable-value>
            </p>
            <!-- Nested metadata with a default value -->
            <p class="text-xs text-text-muted">
              Category: <ticketable-value name="metadata.category" default="General"
              ></ticketable-value>
            </p>
          </div>

          <!-- Conditional rendering based on ticket data -->
          <ticketable-conditional when="metadata.vip">
            <span class="rounded-full bg-warning-bg px-2 py-0.5 text-xs font-bold text-warning-text"
              >VIP</span
            >
          </ticketable-conditional>
        </div>

        <div class="mt-4 flex flex-wrap gap-2">
          <!-- Dynamic attributes from ticket data -->
          <a
            unidy-attr
            unidy-attr-href="{{button_cta_url}}"
            class="btn btn-primary !min-h-0 !px-3 !py-1.5 text-sm">
            View details
          </a>
          <u-ticketable-export
            format="pdf"
            class-name="px-3 py-1.5 rounded-lg text-sm font-medium bg-dark text-white hover:bg-dark-lighter transition-colors cursor-pointer disabled:opacity-50">
            Download PDF
          </u-ticketable-export>
          <!-- Wallet export only when the ticket supports it -->
          <ticketable-conditional when="exportable_to_wallet">
            <u-ticketable-export
              format="pkpass"
              class-name="px-3 py-1.5 rounded-lg text-sm font-medium bg-black text-white transition-colors cursor-pointer disabled:opacity-50">
              Add to Wallet
            </u-ticketable-export>
          </ticketable-conditional>
        </div>
      </div>
    </template>

    <!-- shown when the user has no tickets -->
    <p slot="empty" class="py-6 text-center text-sm text-text-light">No tickets yet.</p>
  </u-ticketable-list>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to see your tickets.
  </p>
</u-signed-in>
```

Live demo: /tickets#ticket-cards

## Table view with pagination and filtering

The target attribute renders rows into an existing element (here: a tbody), which makes tables easy. Pagination is provided by u-pagination-button/-page, and changing the filter attribute makes the list re-fetch — no custom data plumbing.

- **target rendering** — rows go into your own table structure
- **Pagination** — prev/next buttons and page indicator components
- **Live filtering** — set the filter attribute; the SDK re-fetches
- **Skeleton loading** — skeleton-all-text renders placeholders while loading

```html
<u-signed-in>
  <div class="mb-3 flex items-center justify-end">
    <select id="state-filter" class="rounded-lg border border-border bg-white px-3 py-2 text-sm">
      <option value="">All states</option>
      <option value="active">Active</option>
      <option value="inactive">Inactive</option>
    </select>
  </div>

  <!-- target renders rows into the tbody; skeleton rows show while loading -->
  <u-ticketable-list
    id="tickets-list"
    ticketable-type="ticket"
    target="#tickets-table-body"
    limit="5"
    skeleton-all-text="true">
    <div class="overflow-x-auto">
      <table class="min-w-full rounded-lg border border-border bg-white">
        <thead
          class="bg-background-light text-left text-xs uppercase tracking-wider text-text-muted">
          <tr>
            <th class="px-4 py-3">Title</th>
            <th class="px-4 py-3">Starts</th>
            <th class="px-4 py-3">Price</th>
            <th class="px-4 py-3">Actions</th>
          </tr>
        </thead>
        <tbody id="tickets-table-body" class="divide-y divide-border"></tbody>
      </table>
    </div>

    <div class="mt-4 flex items-center gap-2">
      <u-pagination-button
        direction="prev"
        class-name="px-3 py-2 rounded-lg border border-border bg-white text-sm cursor-pointer hover:bg-background-light disabled:opacity-50 disabled:cursor-not-allowed">
      </u-pagination-button>
      <u-pagination-page class-name="px-3 py-2 text-sm"></u-pagination-page>
      <u-pagination-button
        direction="next"
        class-name="px-3 py-2 rounded-lg border border-border bg-white text-sm cursor-pointer hover:bg-background-light disabled:opacity-50 disabled:cursor-not-allowed">
      </u-pagination-button>
    </div>

    <!-- shown when the user has no tickets -->
    <p slot="empty" class="py-6 text-center text-sm text-text-light">No tickets yet.</p>

    <template>
      <tr class="transition-colors hover:bg-background-light">
        <td class="whitespace-nowrap px-4 py-3 font-medium">
          <ticketable-value name="title"></ticketable-value>
        </td>
        <td class="whitespace-nowrap px-4 py-3 text-sm text-text-light">
          <ticketable-value name="starts_at" date-format="yyyy-MM-dd HH:mm"></ticketable-value>
        </td>
        <td class="whitespace-nowrap px-4 py-3 font-semibold text-primary">
          <ticketable-value name="price" default="—"></ticketable-value>
        </td>
        <td class="whitespace-nowrap px-4 py-3">
          <u-ticketable-export
            format="pdf"
            class-name="px-3 py-1.5 rounded-lg text-sm font-medium bg-dark text-white hover:bg-dark-lighter transition-colors cursor-pointer disabled:opacity-50">
            PDF
          </u-ticketable-export>
        </td>
      </tr>
    </template>
  </u-ticketable-list>

  <script is:inline>
    // Update the list's filter attribute; the SDK re-fetches automatically
    document.getElementById("state-filter").addEventListener("change", (event) => {
      const list = document.getElementById("tickets-list");
      list.setAttribute("filter", event.target.value ? `state=${event.target.value}` : "");
    });
  </script>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to see the table view.
  </p>
</u-signed-in>
```

Live demo: /tickets#ticket-table

## Transfer a ticket

Drop a u-ticket-transfer-form into a ticket <template> and u-ticketable-list stamps each ticket's id onto it — no wiring. The user types a recipient email and the SDK creates a pending transfer offer.

- **u-ticket-transfer-form** — email input + submit, styled via *-class-name
- **Auto ticket-id** — the ticket list stamps the id when nested
- **Inline feedback** — error-class-name / success-class-name outputs

```html
<u-signed-in>
  <!-- A u-ticket-transfer-form nested inside a ticket template needs no
       ticket-id: u-ticketable-list stamps each ticket's id onto it. -->
  <u-ticketable-list ticketable-type="ticket" limit="3" container-class="grid gap-4">
    <template>
      <div class="rounded-lg border border-border bg-white p-5 shadow-sm">
        <h3 class="text-lg font-bold">
          <ticketable-value name="title"></ticketable-value>
        </h3>
        <p class="mt-1 mb-4 text-sm text-text-light">
          <ticketable-value name="starts_at" date-format="dd.MM.yyyy HH:mm"></ticketable-value>
        </p>
        <u-ticket-transfer-form
          class-name="flex flex-wrap items-start gap-2"
          input-class-name="flex-1 min-w-48 rounded-lg border border-border bg-white px-3.5 py-2.5 text-base focus:border-primary focus:outline-none"
          button-class-name="btn btn-primary !min-h-0 !px-4 !py-2.5 text-sm"
          error-class-name="mt-1 w-full text-sm text-danger"
          success-class-name="mt-1 w-full text-sm text-success-text"></u-ticket-transfer-form>
      </div>
    </template>

    <p slot="empty" class="py-6 text-center text-sm text-text-light">No tickets to transfer.</p>
  </u-ticketable-list>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to transfer a ticket.
  </p>
</u-signed-in>
```

Live demo: /tickets#transfer-send

## Incoming & outgoing transfers

u-ticket-transfer-list renders pending offers per direction with its own <transfer-value> template. u-ticket-transfer-action stamps the transfer token automatically, so accept/decline/cancel buttons need no data plumbing.

- **direction="incoming|outgoing"** — one component, two views
- **<transfer-value>** — ticket.title, sender_email, expires_at, …
- **accept / decline / cancel** — u-ticket-transfer-action with auto token

```html
<u-signed-in>
  <div class="grid gap-8">
    <div>
      <h3 class="mb-3 text-sm font-semibold text-text-muted uppercase">Incoming</h3>
      <u-ticket-transfer-list direction="incoming" container-class="grid gap-3" skeleton-count="2">
        <template>
          <div
            class="flex flex-wrap items-start justify-between gap-3 rounded-lg border border-border bg-white p-4 shadow-sm">
            <div>
              <p class="font-bold">
                <transfer-value name="ticket.title"></transfer-value>
              </p>
              <p class="mt-1 text-sm text-text-light">
                From <transfer-value name="sender_email"></transfer-value> · expires
                <transfer-value name="expires_at" date-format="dd.MM.yyyy HH:mm"></transfer-value>
              </p>
            </div>
            <div class="flex gap-2">
              <u-ticket-transfer-action
                action="accept"
                class-name="btn btn-primary !min-h-0 !px-3 !py-1.5 text-sm">
                Accept
              </u-ticket-transfer-action>
              <u-ticket-transfer-action
                action="decline"
                class-name="btn btn-outline !min-h-0 !px-3 !py-1.5 text-sm">
                Decline
              </u-ticket-transfer-action>
            </div>
          </div>
        </template>
        <p slot="empty" class="text-sm text-text-light">No incoming transfers.</p>
      </u-ticket-transfer-list>
    </div>

    <div>
      <h3 class="mb-3 text-sm font-semibold text-text-muted uppercase">Outgoing</h3>
      <u-ticket-transfer-list direction="outgoing" container-class="grid gap-3" skeleton-count="2">
        <template>
          <div
            class="flex flex-wrap items-start justify-between gap-3 rounded-lg border border-border bg-white p-4 shadow-sm">
            <div>
              <p class="font-bold">
                <transfer-value name="ticket.title"></transfer-value>
              </p>
              <p class="mt-1 text-sm text-text-light">
                To <transfer-value name="recipient_email"></transfer-value> · expires
                <transfer-value name="expires_at" date-format="dd.MM.yyyy HH:mm"></transfer-value>
              </p>
            </div>
            <u-ticket-transfer-action
              action="cancel"
              class-name="btn btn-outline !min-h-0 !px-3 !py-1.5 text-sm">
              Cancel
            </u-ticket-transfer-action>
          </div>
        </template>
        <p slot="empty" class="text-sm text-text-light">No outgoing transfers.</p>
      </u-ticket-transfer-list>
    </div>
  </div>
</u-signed-in>

<u-signed-in not>
  <p class="text-text-light">
    Sign in on the <a href="/auth" class="text-primary underline">Auth page</a> to manage transfers.
  </p>
</u-signed-in>
```

Live demo: /tickets#transfer-manage

## Create test tickets through the Admin API

This optional demo-only control sends the signed-in user's short-lived SDK token to a same-origin server route. The route validates that token, derives the user id and creates the ticket with server-only Admin API credentials.

- **Authenticated user only** — the user id is derived from the SDK token
- **Server-side credentials** — client id and secret never reach the browser
- **Create and clean up** — add a test ticket and delete it again

```html
<u-signed-in>
  <div class="rounded-lg border border-amber-200 bg-amber-50/40 p-5 space-y-4">
    <div class="flex flex-wrap items-center gap-2">
      <span
        class="text-xs font-semibold uppercase tracking-wider text-amber-700 bg-amber-100 border border-amber-200 px-2 py-0.5 rounded-full"
        >Demo Controls</span
      >
      <p class="text-sm text-text-light">
        Create a ticket through the Admin API for the signed-in user. Admin credentials stay
        server-side.
      </p>
    </div>
    <div class="flex flex-wrap items-center gap-3">
      <button id="create-demo-ticket" class="btn btn-primary !min-h-0 !px-4 !py-2 text-sm"
        >+ Create Demo Ticket</button
      >
      <button
        id="delete-demo-ticket"
        class="!min-h-0 !px-4 !py-2 text-sm rounded-lg border border-red-200 text-red-600 bg-white hover:bg-red-50 transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
        disabled>Delete Last</button
      >
      <span id="demo-ticket-status" class="text-xs text-text-muted" role="status"></span>
    </div>
    <p class="text-xs text-text-muted">
      Reload after creating or deleting to refresh the SDK list above.
    </p>
  </div>
</u-signed-in>

<u-signed-in not>
  <p class="text-sm text-text-light italic">Sign in to use the Admin API demo controls.</p>
</u-signed-in>

<script type="module">
  import { Auth } from "@unidy.io/sdk";

  const createButton = document.getElementById("create-demo-ticket");
  const deleteButton = document.getElementById("delete-demo-ticket");
  const status = document.getElementById("demo-ticket-status");
  let lastId = null;

  function setStatus(message, failed = false) {
    status.textContent = message;
    status.className = `text-xs ${failed ? "text-red-500" : "text-green-600"}`;
  }

  async function authenticatedFetch(url, options = {}) {
    const auth = await Auth.getInstance();
    const token = await auth.getToken();
    if (typeof token !== "string") throw new Error("Please sign in again.");
    return fetch(url, {
      ...options,
      headers: { ...options.headers, Authorization: `Bearer ${token}` },
    });
  }

  createButton?.addEventListener("click", async () => {
    createButton.disabled = true;
    setStatus("Creating…");
    try {
      const response = await authenticatedFetch("/api/demo/tickets", { method: "POST" });
      const data = await response.json();
      if (!response.ok) throw new Error(data.error || "Create failed");
      lastId = data.id;
      deleteButton.disabled = !lastId;
      setStatus(`Created${data.title ? `: ${data.title}` : ""}.`);
    } catch (error) {
      setStatus(error instanceof Error ? error.message : "Create failed", true);
    } finally {
      createButton.disabled = false;
    }
  });

  deleteButton?.addEventListener("click", async () => {
    if (!lastId) return;
    deleteButton.disabled = true;
    setStatus("Deleting…");
    try {
      const response = await authenticatedFetch(
        `/api/demo/tickets?id=${encodeURIComponent(lastId)}`,
        { method: "DELETE" }
      );
      const data = await response.json();
      if (!response.ok) throw new Error(data.error || "Delete failed");
      lastId = null;
      setStatus("Deleted.");
    } catch (error) {
      setStatus(error instanceof Error ? error.message : "Delete failed", true);
      deleteButton.disabled = false;
    }
  });
</script>
```

Live demo: /tickets#ticket-demo-controls

## Further reading

- Full component reference: https://github.com/UnidyID/unidy-sdk/blob/HEAD/packages/sdk/readme.md
- Official quick-start examples: https://github.com/UnidyID/unidy-sdk/blob/HEAD/packages/sdk/quick-start-examples.md
- Package: https://www.npmjs.com/package/@unidy.io/sdk
- All guides: /llms.txt
