Skip to content

Full Page Example

This page demonstrates a complete, production-ready integration of all three Seva components. It includes dark-mode toggling, live status badges, an event log panel, and URL-based configuration overrides.

The page reads configuration from URL query parameters with sensible defaults:

ParameterDefaultDescription
tenantmy-clubYour tenant slug
eventsample-eventThe event to display
apiCurrent originYour Seva API base URL

Example: your-page.html?tenant=rotary-west&event=annual-gala&api=https://api.seva.tools

Copy this file, replace the defaults and YOUR_COMPONENTS_URL, and open it in a browser.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event Registration</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0; padding: 1.5rem;
font-family: system-ui, -apple-system, sans-serif;
background: #f4f4f5; color: #18181b;
}
body.dark { background: #18181b; color: #e4e4e7; }
.header {
display: flex; justify-content: space-between; align-items: center;
flex-wrap: wrap; gap: 0.75rem; margin-bottom: 1.5rem;
max-width: 1100px; margin-left: auto; margin-right: auto;
}
.header h1 { margin: 0; font-size: 1.25rem; }
.status-bar {
display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;
}
.badge {
display: inline-flex; align-items: center; gap: 0.35rem;
padding: 0.2rem 0.6rem; border-radius: 999px;
font-size: 0.75rem; font-weight: 600;
}
.badge.auth-yes { background: #dcfce7; color: #166534; }
.badge.auth-no { background: #fef3c7; color: #92400e; }
.badge.cart { background: #dbeafe; color: #1e40af; }
body.dark .badge.auth-yes { background: #14532d; color: #86efac; }
body.dark .badge.auth-no { background: #78350f; color: #fde68a; }
body.dark .badge.cart { background: #1e3a5f; color: #93c5fd; }
.controls { display: flex; gap: 0.35rem; }
.controls button {
padding: 0.45rem 0.75rem; border-radius: 0.375rem;
border: 1px solid #d4d4d8; background: #fff; color: #18181b;
font-size: 0.75rem; cursor: pointer;
}
body.dark .controls button { background: #27272a; color: #e4e4e7; border-color: #3f3f46; }
.layout {
display: grid;
grid-template-columns: minmax(0, 420px) minmax(0, 1fr);
gap: 1.5rem;
max-width: 1100px; margin: 0 auto;
}
@media (max-width: 800px) { .layout { grid-template-columns: 1fr; } }
.left-col { display: flex; flex-direction: column; gap: 1.5rem; }
.right-col { display: flex; flex-direction: column; gap: 1rem; }
.section-label {
font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em;
color: #71717a; margin-bottom: 0.35rem; font-weight: 600;
}
#event-log {
background: #18181b; color: #a1a1aa; border-radius: 0.5rem;
padding: 1rem; font-family: ui-monospace, monospace; font-size: 0.7rem;
max-height: 500px; overflow: auto; white-space: pre-wrap;
word-break: break-all; flex: 1; min-height: 250px;
}
</style>
</head>
<body>
<div class="header">
<h1>Event Registration</h1>
<div class="status-bar">
<span id="auth-badge" class="badge auth-no">Not Signed In</span>
<span id="cart-badge" class="badge cart">Cart: 0 items</span>
<div class="controls">
<button onclick="toggleTheme()">Toggle Dark</button>
<button onclick="doSignOut()">Sign Out</button>
</div>
</div>
</div>
<div class="layout">
<div class="left-col">
<div>
<div class="section-label">1. Authenticate</div>
<seva-auth id="auth-el"></seva-auth>
</div>
<div>
<div class="section-label">2. Register for Event</div>
<seva-event-register id="register-el"></seva-event-register>
</div>
<div>
<div class="section-label">3. Cart &amp; Checkout</div>
<seva-cart id="cart-el"></seva-cart>
</div>
</div>
<div class="right-col">
<div id="event-log">Listening for seva:* events...</div>
</div>
</div>
<!--
IMPORTANT: This non-module script runs synchronously BEFORE
the module scripts below (modules are deferred by spec).
This ensures connectedCallback sees correct attribute values.
-->
<script>
const params = new URLSearchParams(location.search);
const tenantSlug = params.get('tenant') || 'my-club';
const eventSlug = params.get('event') || 'sample-event';
const apiUrl = params.get('api') || location.origin;
const authEl = document.getElementById('auth-el');
const registerEl = document.getElementById('register-el');
const cartEl = document.getElementById('cart-el');
authEl.setAttribute('tenant-slug', tenantSlug);
authEl.setAttribute('api-url', apiUrl);
registerEl.setAttribute('tenant-slug', tenantSlug);
registerEl.setAttribute('event-slug', eventSlug);
registerEl.setAttribute('api-url', apiUrl);
cartEl.setAttribute('tenant-slug', tenantSlug);
cartEl.setAttribute('api-url', apiUrl);
</script>
<!-- Load component bundles -->
<script type="module" src="YOUR_COMPONENTS_URL/seva-auth.js"></script>
<script type="module" src="YOUR_COMPONENTS_URL/seva-event-register.js"></script>
<script type="module" src="YOUR_COMPONENTS_URL/seva-cart.js"></script>
<script>
const _authEl = document.getElementById('auth-el');
const _registerEl = document.getElementById('register-el');
const _cartEl = document.getElementById('cart-el');
const logEl = document.getElementById('event-log');
const authBadge = document.getElementById('auth-badge');
const cartBadge = document.getElementById('cart-badge');
// --- Theme ---
let dark = false;
function toggleTheme() {
dark = !dark;
document.body.classList.toggle('dark', dark);
_authEl.theme = dark ? 'dark' : 'light';
_registerEl.theme = dark ? 'dark' : 'light';
_cartEl.theme = dark ? 'dark' : 'light';
}
// --- Sign out ---
function doSignOut() {
_authEl.signOut();
propagateAuth();
}
// --- Auth propagation ---
function propagateAuth() {
const token = _authEl.getToken();
if (token) {
_registerEl.setAttribute('auth-token', token);
_cartEl.setAttribute('auth-token', token);
} else {
_registerEl.removeAttribute('auth-token');
_cartEl.removeAttribute('auth-token');
}
updateAuthBadge();
}
function updateAuthBadge() {
const user = _authEl.getUser();
if (user) {
authBadge.className = 'badge auth-yes';
authBadge.textContent = 'Signed In: ' + user.name;
} else {
authBadge.className = 'badge auth-no';
authBadge.textContent = 'Not Signed In';
}
}
// --- Event logging ---
function logEvent(name, detail) {
const ts = new Date().toLocaleTimeString();
logEl.textContent += '\n[' + ts + '] ' + name + '\n' + JSON.stringify(detail, null, 2) + '\n';
logEl.scrollTop = logEl.scrollHeight;
}
// --- Listeners ---
_authEl.addEventListener('seva:authenticated', () => propagateAuth());
_authEl.addEventListener('seva:signed-out', () => propagateAuth());
document.addEventListener('seva:cart-updated', (e) => {
const { itemCount, totalCents } = e.detail;
const dollars = (totalCents / 100).toFixed(2);
cartBadge.textContent = 'Cart: ' + itemCount + ' item' + (itemCount !== 1 ? 's' : '') + ' ($' + dollars + ')';
});
// Log all seva events
['seva:authenticated','seva:signed-out','seva:error','seva:view-changed',
'seva:event-loaded','seva:registration-error','seva:add-to-cart',
'seva:registration-declined','seva:cart-updated','seva:checkout-complete',
'seva:checkout-error'
].forEach(name => {
document.addEventListener(name, (e) => logEvent(name, e.detail));
});
</script>
</body>
</html>

The non-module <script> block runs synchronously before the module scripts load. It reads query parameters and sets tenant-slug, event-slug, and api-url on each element. When the custom element classes upgrade those elements, connectedCallback sees the correct values immediately.

The propagateAuth function reads the token from <seva-auth> and sets or removes auth-token on the registration and cart components. It also updates the status badge in the header.

<seva-cart> listens on document for seva:add-to-cart. When the registration component dispatches that event, the cart picks it up with no extra glue code.

All three components accept a theme property. The toggleTheme function sets 'dark' or 'light' on each component and toggles a dark class on <body> for the page styles.

The right-hand panel logs every seva:* event with a timestamp and JSON payload. This is useful during development but can be removed in production.