Skip to main content

Performing Arts

Example Finding events to attend and purchase tickets for.

Performaing Arts

Handlebars Template for tab List

Note first div has class carousel-card, javascipt buttons need it. The outer div is needed for Shadow DOM encapsulation. To go to a card need class card-button for javascript to find it. The prev-btn and next-btn classes are must used for the pagination buttons to listen the click events.

{{#each items as |item index|}}
<div class="carousel-card" data-index="{{index}}" data-hide-image-on-more="{{item.hideImageOnMore}}">
<div class="flex flex-col">
<div>
<img class="card-image" src="{{firstImage item.imageUrl item.images}}" alt="Image for {{item.title}}" />
</div>
<div class="event-buttons">
<button class="card-button-modern card-button" data-type="panel" onclick='carousel.selectByName("Details")'>Select</button>
</div>
</div>
<div class="card-right">
<div class="event-title">{{item.title}}</div>
{{#if item.display_date}}
<div class="event-subtitle">{{item.display_date}}</div>
{{/if}}
<div class="details-genres">{{genreBubbles item.genres}}</div>
<div class="short-description">{{truncateText (stripHtml item.description) 350}}</div>

</div>
</div>
{{/each}}
<div class="pagination-controls">
<button class="prev-btn list-btn" {{#unless hasPrev}}disabled{{/unless}}>Prev</button>
<span> Page {{getcurrentPage}} of {{getPageCount}} </span>
<button class="next-btn list-btn" {{#unless hasNext}}disabled{{/unless}}>Next</button>
</div>

Handlebars Template for tab Details

So that var item = select.closest('.details-panel').__itemData; is not undefined, we need to Use data-_ attributes Encode the item as JSON and add as a data-_ attribute: need a json helper for Handlebars that returns a JSON string of the object. The panel-button class must be used for the buttons in the details panel to listen the click events.

<div class="details-panel" data-index="{{@index}}" data-item="{{json item}}">
<div class="details-title">{{item.title}}</div>
<div class="details-venue">Venue: {{formatVenue item.venue}}</div>
{{#if item.performances.length}}
<label class="details-label" for="performance-select-{{@index}}"
>Select a Date:</label
>
<select
class="performance-select"
id="performance-select-{{@index}}"
onchange="onPerformanceChange(this)"
>
<option value="">-- Select a performance --</option>
{{#each item.performances as |perf|}}
<option value="{{perf.id}}">{{perf.date}}</option>
{{/each}}
</select>
<div class="performance-html"></div>
<div class="details-buttons">
<button class="card-button" onclick="buyAndSelectSeats(this)">
Buy and Seats
</button>
<button
class="panel-button card-button text-white"
data-type="panel"
onclick='carousel.selectByName("List")'
>
Back
</button>
</div>
{{else}}
<div class="details-label">No performances currently scheduled.</div>
<div class="details-buttons">
<button
class="panel-button card-button text-white"
data-type="panel"
onclick='carousel.selectByName("List")'
>
Back
</button>
</div>
{{/if}}
</div>

Javascript for a slider in the AI Handlebar

// --- Custom Handlebars Helpers ---
// Helper: firstImage - returns the first image URL or a placeholder
window.Handlebars.registerHelper("firstImage", function (images) {
if (Array.isArray(images) && images.length > 0) return images[0]
if (typeof images === "string" && images) return images
return "https://placehold.co/430x495?text=No+Image"
})
// Helper: stripHtml - strips HTML tags from a string
window.Handlebars.registerHelper("stripHtml", function (html) {
if (!html) return ""
var div = document.createElement("div")
div.innerHTML = html
return div.textContent || div.innerText || ""
})
// Helper: truncateText - truncates a string to maxLen chars, adds ... if needed
window.Handlebars.registerHelper("truncateText", function (text, maxLen) {
if (!text) return ""
if (text.length <= maxLen) return text
var truncated = text.slice(0, maxLen)
var lastSpace = truncated.lastIndexOf(" ")
if (lastSpace > 0) truncated = truncated.slice(0, lastSpace)
return truncated + "..."
})
// Helper: eq - strict equality
window.Handlebars.registerHelper("eq", function (a, b) {
return a === b
})
// Helper: genreBubbles - outputs genre bubbles
window.Handlebars.registerHelper("genreBubbles", function (genres) {
if (!Array.isArray(genres)) return ""
return new window.Handlebars.SafeString(
genres
.map(function (g) {
return '<span class="genre-bubble">' + g + "</span>"
})
.join(" ")
)
})
// Helper: formatVenue - returns venue string
window.Handlebars.registerHelper("formatVenue", function (venue) {
return venue ? venue : ""
})

// --- Details Panel JavaScript ---
// This code must be placed in the Script tab of the control (not in the template)
window.selectedPerformanceId = null
function onPerformanceChange(select) {
var id = select.value
window.selectedPerformanceId = id
var perfHtml = ""
var panel = select.closest(".details-panel")
var itemJson = panel.getAttribute("data-item")
var item = itemJson ? JSON.parse(itemJson) : null
if (item && Array.isArray(item.performances)) {
var perf = item.performances.find(function (p) {
return String(p.id) === String(id)
})
if (perf && perf.html) perfHtml = perf.html
}
var perfHtmlDiv = select
.closest(".details-panel")
.querySelector(".performance-html")
if (perfHtmlDiv) perfHtmlDiv.innerHTML = perfHtml
}
function buyAndSelectSeats(btn) {
var panel = btn.closest(".details-panel")
var select = panel.querySelector(".performance-select")
var id = select ? select.value : null
if (!id) {
alert("Please select a performance date.")
return
}
alert("Proceeding to buy and select seats for performance ID: " + id)
}
// On panel render, attach item data for performance lookup
function attachItemDataToDetailsPanels() {
var cards = carousel?.shadowRoot?.querySelectorAll(".details-panel") || []
var items = window.carouselDataItems || []
cards.forEach(function (card, idx) {
// Find the item index from data-index attribute
var itemIdx = card.getAttribute("data-index")
if (itemIdx && items[itemIdx]) {
card.__itemData = items[itemIdx]
}
})
}
// On render, call this after handlebars render
setTimeout(attachItemDataToDetailsPanels, 120)

Style for a slider in the AI Handlebar

/* --- Responsive Carousel Card Styles --- */
.carousel-card {
display: flex;
flex-direction: row;
align-items: stretch;
background: #fff;
border-radius: 1.5rem;
box-shadow: 0 8px 32px 0 rgba(52, 78, 134, 0.10), 0 1.5px 8px 0 rgba(40, 30, 70, 0.04);
margin-bottom: 2.2rem;
padding: 0;
border: 1.5px solid #e5e7eb;
min-width: 0;
width: 100%;
overflow: visible;
transition: box-shadow 0.18s, transform 0.12s;
}
.carousel-card:hover {
box-shadow: 0 12px 40px 0 rgba(52, 78, 134, 0.16), 0 2.5px 12px 0 rgba(40, 30, 70, 0.08);
transform: translateY(-2px) scale(1.01);
}
.card-image {
flex: 1 1 18%;
min-width: 170px;
max-width: 220px;
height: 240px;
object-fit: cover;
border-top-left-radius: 1.5rem;
border-bottom-left-radius: 1.5rem;
background: #f3f4f6;
margin: 0;
display: block;
}
.card-right {
flex: 1 1 70%;
padding: 2.2rem 2rem 2.2rem 1.2rem;
display: flex;
flex-direction: column;
justify-content: flex-start;
min-width: 0;
min-height: 0;
}
.event-title {
font-size: 1.4rem;
font-weight: 800;
color: #1e293b;
margin-bottom: 0.5rem;
font-family: 'Inter', Arial, sans-serif;
}
.event-subtitle {
color: #64748b;
font-size: 1.08rem;
margin-bottom: 0.7rem;
font-weight: 600;
}
.short-description {
color: #334155;
font-size: 1.13rem;
margin-bottom: 1.1rem;
line-height: 1.55;
font-family: 'Inter', Arial, sans-serif;
word-break: break-word;
}
.event-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.7rem;
margin-top: 1.2rem;
justify-content: center;
}
.card-button, .card-button-modern {
background: linear-gradient(135deg, #2d7be5 0%, #3b82f6 100%);
color: #fff;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
font-size: 1.01rem;
padding: 0.7em 1.5em;
border-radius: 1.2em;
border: none;
box-shadow: 0 2px 8px rgba(52, 123, 229, 0.10);
cursor: pointer;
transition: background 0.18s, box-shadow 0.14s, transform 0.10s;
min-width: 98px;
min-height: 44px;
display: flex;
align-items: left;
justify-content: center;
font-family: 'Inter', Arial, sans-serif;
line-height: 1.15;
}

.card-button:hover, .card-button:focus, .card-button-modern:hover, .card-button-modern:focus {
background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
box-shadow: 0 4px 16px rgba(52, 123, 229, 0.13);
transform: scale(1.04);
}
.list-btn {
background: #f3f4f6;
color: #2d7be5;
border: none;
border-radius: 1.2em;
padding: 0.6em 1.2em;
font-weight: 600;
font-size: 1.01rem;
cursor: pointer;
transition: background 0.15s, color 0.15s;
border-radius: 1.2em; !important
}
.list-btn[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 1.2rem;
margin: 1.2rem 0 0 0;
}
.prev-btn, .next-btn {
background: #f3f4f6;
color: #2d7be5;
border: none;
border-radius: 1.2em;
padding: 0.6em 1.2em;
font-weight: 600;
font-size: 1.01rem;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
@media (max-width: 700px) {
.carousel-card {
flex-direction: column;
border-radius: 1.1rem;
max-width: 99vw;
min-width: 0;
align-items: stretch;
}
.card-image {
border-radius: 1.1rem 1.1rem 0 0;
min-width: 0;
max-width: 100%;
width: 100%;
height: 180px;
object-fit: cover;
margin: 0;
display: block;
}
.card-right {
padding: 1.5rem 1.1rem 1.4rem 1.1rem;
width: 100%;
}
.event-buttons {
flex-direction: column;
gap: 0.7rem;
margin-top: 1.1rem;
}
.card-button, .card-button-modern {
width: 100%;
min-width: 0;
min-height: 54px;
font-size: 1.1rem;
padding: 1.2em 0;
border-radius: 1.2em;
margin-left: 0;
}
}
/* --- Details Panel Styles --- */
.details-panel {
background: #fff;
border-radius: 1.3rem;
box-shadow: 0 4px 18px 0 rgba(52, 78, 134, 0.10);
padding: 2rem 1.5rem 1.5rem 1.5rem;
max-width: 700px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.details-title {
font-size: 1.5rem;
font-weight: 800;
color: #1e293b;
margin-bottom: 0.5rem;
font-family: 'Inter', Arial, sans-serif;
}
.details-venue {
color: #64748b;
font-size: 1.08rem;
margin-bottom: 0.7rem;
font-weight: 600;
}
.details-genres {
margin-bottom: 1.1rem;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.genre-bubble {
background: linear-gradient(90deg, #e0e7ff 0%, #f3f4f6 100%);
color: #2563eb;
font-size: 0.98rem;
border-radius: 1.2em;
padding: 0.3em 1.1em;
font-weight: 700;
letter-spacing: 0.01em;
border: 1px solid #c7d2fe;
box-shadow: 0 1px 4px rgba(52, 123, 229, 0.07);
display: inline-block;
}
.details-description {
color: #334155;
font-size: 1.13rem;
margin-bottom: 1.2rem;
line-height: 1.55;
font-family: 'Inter', Arial, sans-serif;
word-break: break-word;
}
.details-label {
font-weight: 700;
color: #1e293b;
margin-bottom: 0.3rem;
font-size: 1.08rem;
}
.performance-select {
font-size: 1.1rem;
padding: 0.5em 1.2em 0.5em 1em;
border-radius: 1.2rem;
border: 1.4px solid #e2e8f0;
background: #f8fafc;
color: #22223b;
min-width: 180px;
outline: none;
box-shadow: 0 1.5px 7px rgba(100, 100, 150, 0.04);
appearance: none;
margin-bottom: 1.1rem;
margin-right: 1.2rem;
}
.performance-select:focus {
border-color: #2d7be5;
box-shadow: 0 0 0 2px #e0e7ff80;
}
.performance-html {
background: #f3f4f6;
border-radius: 1.1rem;
padding: 1.1rem 1.1rem 0.7rem 1.1rem;
margin-bottom: 1.2rem;
width: 100%;
font-size: 1.08rem;
color: #22223b;
box-shadow: 0 2px 8px rgba(52, 78, 134, 0.06);
}
.details-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.7rem;
margin-top: 1.2rem;
}
@media (max-width: 700px) {
.details-panel {
padding: 1.1rem 0.7rem 1rem 0.7rem;
border-radius: 1.1rem;
max-width: 99vw;
}
.performance-html {
padding: 0.7rem 0.5rem 0.5rem 0.5rem;
}
}

JSON

{
"items": [
{
"accesibility": "",
"accessibilityOptions": [],
"description": "You know the songs. You know the moves. It&rsquo;s the sensational ABBA tribute band performing some of the biggest hits...",
"display_date": "July 19",
"genres": ["Pop/Rock", "World Music", "Vocal"],
"imageUrl": "https://scfta-prod.imgix.net/scfta/events/2025/photo-2-the-concert_a-tribute-to-abba.jpg?format=auto&fit=crop&w=430&h=495&auto=format",
"panels": [],
"performances": [
{
"date": "July 19, 2025, 07:30 PM",
"html": "<ol><li>Orchestra (928 seats, $69.00 - $171.00)</li><li>Orchestra Terrace (601 seats, $49.00 - $79.00)</li><li>Loge (453 seats, $59.00 - $79.00)</li><li>Balcony (585 seats, $49.00 - $59.00)</li></ol>",
"id": 30496,
"priceHigh": 171,
"priceLow": 49,
"totalAvailableSeats": 2567
}
],
"startDate": "2025-07-20T02:30:00+00:00",
"title": "The Concert - A Tribute to ABBA",
"url": "https://www.scfta.org/events/2025/the-concert-a-tribute-to-abba",
"venue": "Segerstrom Hall"
}
]
}