@ticketlayer/elements 0.3.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cjs/{index-BpfoKd9Q.js → index-DanPUX0s.js} +1 -1
- package/dist/cjs/index.cjs.js +2 -1
- package/dist/cjs/loader.cjs.js +2 -2
- package/dist/cjs/ticket-selector-BYVL13k_.js +3623 -0
- package/dist/cjs/ticketlayer-elements.cjs.js +2 -2
- package/dist/cjs/tl-buy-modal.cjs.entry.js +4 -4
- package/dist/cjs/tl-buy-tickets-button.cjs.entry.js +89 -71
- package/dist/cjs/tl-buy-tickets-wrapper.cjs.entry.js +62 -31
- package/dist/cjs/tl-cart-badge.cjs.entry.js +75 -62
- package/dist/cjs/tl-cart-drawer.cjs.entry.js +52 -24
- package/dist/cjs/tl-cart.cjs.entry.js +72 -98
- package/dist/cjs/tl-checkout-wrapper.cjs.entry.js +52 -30
- package/dist/cjs/tl-event-card.cjs.entry.js +44 -105
- package/dist/cjs/tl-event-list.cjs.entry.js +73 -36
- package/dist/cjs/tl-login.cjs.entry.js +75 -141
- package/dist/cjs/tl-my-orders.cjs.entry.js +53 -92
- package/dist/cjs/tl-occurrence-selector_2.cjs.entry.js +205 -641
- package/dist/cjs/tl-order-confirmation.cjs.entry.js +72 -105
- package/dist/cjs/tl-order-tickets_2.cjs.entry.js +50 -76
- package/dist/cjs/tl-promo-code.cjs.entry.js +51 -47
- package/dist/cjs/tl-seat-map.cjs.entry.js +4 -4
- package/dist/cjs/web-query-Ce73jB4k.js +16 -0
- package/dist/collection/components/tl-buy-modal/tl-buy-modal.css +12 -19
- package/dist/collection/components/tl-buy-modal/tl-buy-modal.js +2 -2
- package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.css +41 -4
- package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.js +105 -76
- package/dist/collection/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.css +2 -2
- package/dist/collection/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.js +73 -30
- package/dist/collection/components/tl-cart/tl-cart.css +52 -51
- package/dist/collection/components/tl-cart/tl-cart.js +82 -101
- package/dist/collection/components/tl-cart-badge/tl-cart-badge.css +11 -11
- package/dist/collection/components/tl-cart-badge/tl-cart-badge.js +62 -62
- package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.css +6 -6
- package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.js +56 -24
- package/dist/collection/components/tl-checkout-wrapper/tl-checkout-wrapper.css +2 -2
- package/dist/collection/components/tl-checkout-wrapper/tl-checkout-wrapper.js +61 -29
- package/dist/collection/components/tl-event-card/event-card-adapter.js +11 -23
- package/dist/collection/components/tl-event-card/tl-event-card.css +22 -22
- package/dist/collection/components/tl-event-card/tl-event-card.js +51 -84
- package/dist/collection/components/tl-event-list/tl-event-list.css +17 -17
- package/dist/collection/components/tl-event-list/tl-event-list.js +82 -38
- package/dist/collection/components/tl-login/tl-login.css +26 -26
- package/dist/collection/components/tl-login/tl-login.js +83 -147
- package/dist/collection/components/tl-my-orders/tl-my-orders.css +40 -40
- package/dist/collection/components/tl-my-orders/tl-my-orders.js +58 -96
- package/dist/collection/components/tl-occurrence-selector/tl-occurrence-selector.css +54 -65
- package/dist/collection/components/tl-occurrence-selector/tl-occurrence-selector.js +137 -516
- package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.css +38 -38
- package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.js +81 -113
- package/dist/collection/components/tl-order-tickets/tl-order-tickets.css +9 -9
- package/dist/collection/components/tl-order-tickets/tl-order-tickets.js +57 -92
- package/dist/collection/components/tl-promo-code/tl-promo-code.css +25 -25
- package/dist/collection/components/tl-promo-code/tl-promo-code.js +55 -50
- package/dist/collection/components/tl-seat-map/tl-seat-map.css +10 -10
- package/dist/collection/components/tl-seat-map/tl-seat-map.js +2 -2
- package/dist/collection/components/tl-ticket/tl-ticket.css +13 -13
- package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.css +74 -49
- package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.js +95 -135
- package/dist/collection/utils/cart-totals.js +9 -0
- package/dist/collection/utils/order-summary.js +15 -99
- package/dist/collection/utils/testing/deep-text.js +29 -0
- package/dist/collection/utils/testing/fixtures.js +290 -0
- package/dist/collection/utils/testing/mock-sdk.js +13 -2
- package/dist/collection/utils/web-query.js +12 -0
- package/dist/collection/utils/web-storage.js +22 -0
- package/dist/components/ticket-selector.js +1 -0
- package/dist/components/tl-buy-modal.js +1 -1
- package/dist/components/tl-buy-tickets-button.js +1 -1
- package/dist/components/tl-buy-tickets-wrapper2.js +1 -1
- package/dist/components/tl-cart-badge.js +1 -1
- package/dist/components/tl-cart-drawer.js +1 -1
- package/dist/components/tl-cart2.js +1 -1
- package/dist/components/tl-checkout-wrapper2.js +1 -1
- package/dist/components/tl-event-card2.js +1 -1
- package/dist/components/tl-event-list.js +1 -1
- package/dist/components/tl-login2.js +1 -1
- package/dist/components/tl-my-orders.js +1 -1
- package/dist/components/tl-occurrence-selector2.js +1 -1
- package/dist/components/tl-order-confirmation.js +1 -1
- package/dist/components/tl-order-tickets2.js +1 -1
- package/dist/components/tl-promo-code.js +1 -1
- package/dist/components/tl-seat-map.js +1 -1
- package/dist/components/tl-ticket-selector2.js +1 -1
- package/dist/components/tl-ticket2.js +1 -1
- package/dist/components/web-query.js +1 -0
- package/dist/elements.docs.json +36 -36
- package/dist/esm/{index-CNX8hZa8.js → index-imfxp765.js} +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/loader.js +3 -3
- package/dist/esm/ticket-selector-BwChnHVC.js +3606 -0
- package/dist/esm/ticketlayer-elements.js +3 -3
- package/dist/esm/tl-buy-modal.entry.js +4 -4
- package/dist/esm/tl-buy-tickets-button.entry.js +90 -72
- package/dist/esm/tl-buy-tickets-wrapper.entry.js +63 -32
- package/dist/esm/tl-cart-badge.entry.js +76 -63
- package/dist/esm/tl-cart-drawer.entry.js +53 -25
- package/dist/esm/tl-cart.entry.js +73 -99
- package/dist/esm/tl-checkout-wrapper.entry.js +53 -31
- package/dist/esm/tl-event-card.entry.js +45 -106
- package/dist/esm/tl-event-list.entry.js +74 -37
- package/dist/esm/tl-login.entry.js +76 -142
- package/dist/esm/tl-my-orders.entry.js +54 -93
- package/dist/esm/tl-occurrence-selector_2.entry.js +206 -642
- package/dist/esm/tl-order-confirmation.entry.js +73 -106
- package/dist/esm/tl-order-tickets_2.entry.js +51 -77
- package/dist/esm/tl-promo-code.entry.js +52 -48
- package/dist/esm/tl-seat-map.entry.js +4 -4
- package/dist/esm/web-query-Bdbb2lk5.js +14 -0
- package/dist/ticketlayer-elements/index.esm.js +1 -1
- package/dist/ticketlayer-elements/p-45923d2c.entry.js +1 -0
- package/dist/ticketlayer-elements/p-492adc77.entry.js +1 -0
- package/dist/ticketlayer-elements/p-4cfc9e53.entry.js +1 -0
- package/dist/ticketlayer-elements/p-5135630e.entry.js +1 -0
- package/dist/ticketlayer-elements/p-5bbce3b5.entry.js +1 -0
- package/dist/ticketlayer-elements/p-7d547721.entry.js +1 -0
- package/dist/ticketlayer-elements/p-7fe7b6ba.entry.js +1 -0
- package/dist/ticketlayer-elements/p-8122f0c1.entry.js +1 -0
- package/dist/ticketlayer-elements/p-98d40869.entry.js +1 -0
- package/dist/ticketlayer-elements/p-Bdbb2lk5.js +1 -0
- package/dist/ticketlayer-elements/p-BwChnHVC.js +1 -0
- package/dist/ticketlayer-elements/p-abec290a.entry.js +1 -0
- package/dist/ticketlayer-elements/p-ac79a3f6.entry.js +1 -0
- package/dist/ticketlayer-elements/p-d01f4429.entry.js +1 -0
- package/dist/ticketlayer-elements/p-d7ffaf46.entry.js +1 -0
- package/dist/ticketlayer-elements/p-e7170f3c.entry.js +1 -0
- package/dist/ticketlayer-elements/p-e787e862.entry.js +1 -0
- package/dist/ticketlayer-elements/p-f1ff931f.entry.js +1 -0
- package/dist/ticketlayer-elements/p-imfxp765.js +3 -0
- package/dist/ticketlayer-elements/ticketlayer-elements.css +1 -1
- package/dist/ticketlayer-elements/ticketlayer-elements.esm.js +1 -1
- package/dist/types/components/tl-buy-tickets-button/tl-buy-tickets-button.d.ts +40 -8
- package/dist/types/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.d.ts +20 -1
- package/dist/types/components/tl-cart/tl-cart.d.ts +40 -32
- package/dist/types/components/tl-cart-badge/tl-cart-badge.d.ts +22 -7
- package/dist/types/components/tl-cart-drawer/tl-cart-drawer.d.ts +28 -5
- package/dist/types/components/tl-checkout-wrapper/tl-checkout-wrapper.d.ts +18 -1
- package/dist/types/components/tl-event-card/event-card-adapter.d.ts +9 -20
- package/dist/types/components/tl-event-card/tl-event-card.d.ts +25 -28
- package/dist/types/components/tl-event-list/tl-event-list.d.ts +46 -6
- package/dist/types/components/tl-login/tl-login.d.ts +68 -22
- package/dist/types/components/tl-my-orders/tl-my-orders.d.ts +30 -12
- package/dist/types/components/tl-occurrence-selector/tl-occurrence-selector.d.ts +73 -161
- package/dist/types/components/tl-order-confirmation/tl-order-confirmation.d.ts +48 -15
- package/dist/types/components/tl-order-tickets/tl-order-tickets.d.ts +29 -31
- package/dist/types/components/tl-promo-code/tl-promo-code.d.ts +28 -10
- package/dist/types/components/tl-ticket/tl-ticket.d.ts +2 -11
- package/dist/types/components/tl-ticket-selector/tl-ticket-selector.d.ts +61 -32
- package/dist/types/components.d.ts +302 -62
- package/dist/types/utils/cart-totals.d.ts +10 -0
- package/dist/types/utils/order-summary.d.ts +16 -65
- package/dist/types/utils/testing/deep-text.d.ts +13 -0
- package/dist/types/utils/testing/fixtures.d.ts +225 -0
- package/dist/types/utils/testing/mock-sdk.d.ts +14 -1
- package/dist/types/utils/web-query.d.ts +18 -0
- package/dist/types/utils/web-storage.d.ts +16 -0
- package/elements.catalogue.json +676 -55
- package/package.json +14 -6
- package/dist/cjs/order-summary-ZMkJ5bez.js +0 -116
- package/dist/components/order-summary.js +0 -1
- package/dist/esm/order-summary-BGekdjN9.js +0 -103
- package/dist/ticketlayer-elements/p-0b1d16f8.entry.js +0 -1
- package/dist/ticketlayer-elements/p-1471f6d8.entry.js +0 -1
- package/dist/ticketlayer-elements/p-1c51cbc1.entry.js +0 -1
- package/dist/ticketlayer-elements/p-5169f68b.entry.js +0 -1
- package/dist/ticketlayer-elements/p-895ba1f3.entry.js +0 -1
- package/dist/ticketlayer-elements/p-8d5722e1.entry.js +0 -1
- package/dist/ticketlayer-elements/p-917d8e09.entry.js +0 -1
- package/dist/ticketlayer-elements/p-9629029a.entry.js +0 -1
- package/dist/ticketlayer-elements/p-BGekdjN9.js +0 -1
- package/dist/ticketlayer-elements/p-CNX8hZa8.js +0 -3
- package/dist/ticketlayer-elements/p-a22714d0.entry.js +0 -1
- package/dist/ticketlayer-elements/p-bf73044b.entry.js +0 -1
- package/dist/ticketlayer-elements/p-e02079fa.entry.js +0 -1
- package/dist/ticketlayer-elements/p-e04ace15.entry.js +0 -1
- package/dist/ticketlayer-elements/p-e6fab1d5.entry.js +0 -1
- package/dist/ticketlayer-elements/p-f826b753.entry.js +0 -1
- package/dist/ticketlayer-elements/p-fa639d88.entry.js +0 -1
- package/dist/ticketlayer-elements/p-fbaca948.entry.js +0 -1
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
import { r as registerInstance, c as createEvent, h } from './index-
|
|
2
|
-
import { w as whenSDK, g as getSDK
|
|
1
|
+
import { r as registerInstance, c as createEvent, h } from './index-imfxp765.js';
|
|
2
|
+
import { w as whenSDK, g as getSDK } from './sdk-connector-CqhvfyKV.js';
|
|
3
|
+
import { c as createElement, h as occurrenceSelectorCore, t as ticketSelectorCore } from './ticket-selector-BwChnHVC.js';
|
|
3
4
|
|
|
4
|
-
const tlOccurrenceSelectorCss = () => `:host{
|
|
5
|
+
const tlOccurrenceSelectorCss = () => `:host{display:block;font-family:var(--tl-font-family);color:var(--tl-foreground)}*{box-sizing:border-box;margin:0;padding:0}.occurrence-selector{width:100%}.no-occurrences{text-align:center;padding:3rem 1rem;color:var(--tl-muted-foreground)}.list-view{display:flex;flex-direction:column;gap:0.75rem}.occurrence-item{display:flex;justify-content:space-between;align-items:flex-start;width:100%;padding:1rem 1.25rem;background:var(--tl-background);border:1px solid var(--tl-border);border-radius:var(--tl-radius);text-align:left;cursor:pointer;transition:all 0.15s ease;font-family:inherit}.occurrence-item:hover:not(:disabled){border-color:var(--tl-primary);background:color-mix(in srgb, var(--tl-primary) 5%, var(--tl-background))}.occurrence-item:focus-visible{outline:2px solid var(--tl-primary);outline-offset:2px}.occurrence-item:disabled{cursor:not-allowed;opacity:0.6;background:var(--tl-muted)}.occurrence-item.sold-out{opacity:0.6;background:var(--tl-muted)}.occurrence-info{flex:1}.occurrence-date{font-weight:600;font-size:0.9375rem;color:var(--tl-foreground);margin-bottom:0.25rem}.occurrence-time{font-size:0.875rem;color:var(--tl-muted-foreground)}.occurrence-status{flex-shrink:0;text-align:right}.status-sold-out{font-size:0.875rem;font-weight:500;color:var(--tl-muted-foreground)}.status-available{font-size:0.875rem;color:var(--tl-muted-foreground)}.status-arrow{font-size:1.25rem;color:var(--tl-primary)}.slots-view{display:flex;flex-direction:column;gap:1rem}.slots-date-header{font-size:1.125rem;font-weight:600;color:var(--tl-foreground);padding-bottom:0.5rem;border-bottom:1px solid var(--tl-border)}.time-slots-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(100px, 1fr));gap:0.5rem}.time-slot{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0.75rem 0.5rem;background:var(--tl-background);border:1px solid var(--tl-border);border-radius:var(--tl-radius);cursor:pointer;transition:all 0.15s ease;font-family:inherit;min-height:3.5rem}.time-slot:hover:not(:disabled){border-color:var(--tl-primary);background:color-mix(in srgb, var(--tl-primary) 5%, var(--tl-background))}.time-slot:focus-visible{outline:2px solid var(--tl-primary);outline-offset:2px}.time-slot:disabled{cursor:not-allowed;opacity:0.6;background:var(--tl-muted)}.time-slot.sold-out{opacity:0.6;background:var(--tl-muted)}.slot-time{font-size:0.9375rem;font-weight:600;color:var(--tl-foreground)}.slot-sold-out{font-size:0.6875rem;color:var(--tl-muted-foreground);margin-top:0.125rem}.calendar-view{display:flex;flex-direction:column;gap:1.5rem}.calendar-view.horizontal{flex-direction:row;gap:1.5rem;align-items:flex-start}.calendar-view.horizontal .calendar-panel{flex-shrink:0;width:340px}.calendar-view.horizontal .slots-panel{flex:1;min-width:0}.calendar-view.vertical .calendar-panel{width:100%}.calendar-view.vertical .slots-panel{width:100%}.calendar-panel{border:1px solid var(--tl-border);border-radius:var(--tl-radius);padding:1rem}.calendar-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem}.month-label{font-size:1rem;font-weight:600;color:var(--tl-foreground);flex:1}.calendar-nav-buttons{display:flex;gap:0.375rem}.nav-button{display:flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;background:var(--tl-background);border:1px solid var(--tl-border);border-radius:var(--tl-radius);cursor:pointer;transition:all 0.15s ease;color:var(--tl-foreground)}.nav-button:hover:not(:disabled){background:var(--tl-muted)}.nav-button:disabled{cursor:not-allowed;opacity:0.4}.nav-button svg{width:0.875rem;height:0.875rem}.calendar-grid{display:grid;grid-template-columns:repeat(7, 1fr);gap:0.1875rem}.weekday-header{text-align:center;font-size:0.6875rem;font-weight:600;color:var(--tl-muted-foreground);padding:0.375rem 0;text-transform:uppercase;letter-spacing:0.05em}.calendar-day{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;aspect-ratio:1;padding:0.1875rem;background:var(--tl-background);border:1px solid transparent;border-radius:var(--tl-radius);cursor:default;transition:all 0.15s ease;font-family:inherit;font-size:0.9375rem;font-weight:500;color:var(--tl-foreground)}.calendar-day.other-month{color:var(--tl-muted-foreground);opacity:0.5}.calendar-day.has-occurrences{cursor:pointer;background:color-mix(in srgb, var(--tl-primary) 10%, var(--tl-background));border-color:transparent;font-weight:600;color:var(--tl-primary)}.calendar-day.has-occurrences:hover{background:color-mix(in srgb, var(--tl-primary) 18%, var(--tl-background));border-color:transparent}.calendar-day.has-occurrences.sold-out{background:var(--tl-muted);border-color:transparent}.calendar-day.has-occurrences.sold-out:hover{background:color-mix(in srgb, var(--tl-muted) 80%, var(--tl-background));border-color:transparent}.calendar-day.selected{background:var(--tl-primary);border-color:var(--tl-primary);color:var(--tl-primary-foreground)}.calendar-day.selected:hover{background:color-mix(in srgb, var(--tl-primary) 90%, black)}.calendar-day.selected .occurrence-dot{background:var(--tl-primary-foreground)}.calendar-day.today .day-number{font-weight:700;text-decoration:underline;text-underline-offset:2px}.calendar-day.past:not(.has-occurrences){opacity:0.4}.day-number{line-height:1}.occurrence-dot{display:none}.slots-panel{min-height:200px}.selected-date-slots{height:100%}.selected-date-slots.empty{display:flex;align-items:center;justify-content:center;color:var(--tl-muted-foreground);background:var(--tl-muted);border-radius:var(--tl-radius);padding:2rem 1rem;text-align:center;min-height:200px;font-size:0.875rem}.selected-date-header{font-size:0.9375rem;font-weight:600;color:var(--tl-foreground);padding-bottom:0.625rem;margin-bottom:0.625rem}.time-slots-list{display:flex;flex-direction:column;gap:0.5rem}.time-slot-item{display:flex;align-items:center;justify-content:space-between;width:100%;padding:0.625rem 0.875rem;background:var(--tl-background);border:1px solid var(--tl-border);border-radius:var(--tl-radius);text-align:left;cursor:pointer;transition:all 0.15s ease;font-family:inherit}.time-slot-item:hover:not(:disabled){border-color:var(--tl-primary);background:color-mix(in srgb, var(--tl-primary) 5%, var(--tl-background))}.time-slot-item:focus-visible{outline:2px solid var(--tl-primary);outline-offset:2px}.time-slot-item:disabled{cursor:not-allowed;opacity:0.6;background:var(--tl-muted)}.time-slot-item.sold-out{opacity:0.6;background:var(--tl-muted)}.time-slot-item .slot-time{font-size:0.9375rem;font-weight:600}.slot-status{flex-shrink:0}.slot-status .status-sold-out{font-size:0.75rem}.slot-status .status-available{font-size:0.75rem}.slot-status .status-arrow{font-size:1rem}@media (max-width: 640px){.event-name{font-size:1.25rem}.occurrence-date{font-size:0.875rem}.time-slots-grid{grid-template-columns:repeat(auto-fill, minmax(80px, 1fr))}.time-slot{padding:0.625rem 0.375rem;min-height:3rem}.slot-time{font-size:0.875rem}.calendar-day{font-size:0.75rem}.weekday-header{font-size:0.625rem}.calendar-view.horizontal .calendar-panel{width:100%}}@media (max-width: 767px){.calendar-view.horizontal{flex-direction:column}}.loading-container{padding:1rem 0;min-height:400px}@keyframes skeleton-pulse{0%,100%{opacity:1}50%{opacity:0.5}}.skeleton{background:var(--tl-muted);border-radius:var(--tl-radius);animation:skeleton-pulse 1.5s ease-in-out infinite}.skeleton-list{display:flex;flex-direction:column;gap:0.75rem}.skeleton-list-item{height:4.5rem;border-radius:var(--tl-radius)}.skeleton-calendar{border:1px solid var(--tl-border);border-radius:var(--tl-radius);padding:1rem;min-height:350px}.skeleton-calendar-header{height:1.75rem;width:50%;margin-bottom:0.75rem;border-radius:var(--tl-radius)}.skeleton-calendar-grid{display:grid;grid-template-columns:repeat(7, 1fr);gap:0.1875rem;margin-top:0.75rem}.skeleton-calendar-day{aspect-ratio:1;border-radius:var(--tl-radius)}.skeleton-slots{display:flex;flex-direction:column;gap:1rem}.skeleton-slots-header{height:1.75rem;width:60%;border-radius:var(--tl-radius);margin-bottom:0.5rem}.skeleton-slots-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(100px, 1fr));gap:0.5rem}.skeleton-slot-item{height:3.5rem;border-radius:var(--tl-radius)}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:3rem 1rem;gap:1rem;text-align:center}.error-icon{width:3rem;height:3rem;color:var(--tl-muted-foreground);stroke-width:1.5}.error-message{color:var(--tl-foreground);font-size:0.9375rem;max-width:400px}.retry-button{margin-top:0.5rem;padding:0.625rem 1.25rem;background:var(--tl-primary);color:var(--tl-primary-foreground);border:none;border-radius:var(--tl-radius);font-family:inherit;font-size:0.875rem;font-weight:500;cursor:pointer;transition:all 0.15s ease}.retry-button:hover{background:color-mix(in srgb, var(--tl-primary) 90%, black)}.retry-button:focus-visible{outline:2px solid var(--tl-primary);outline-offset:2px}`;
|
|
5
6
|
|
|
7
|
+
/** Theme field -> the custom property the stylesheet reads it through. */
|
|
8
|
+
const THEME_PROPERTIES = [
|
|
9
|
+
['primaryColor', '--tl-primary'],
|
|
10
|
+
['primaryForeground', '--tl-primary-foreground'],
|
|
11
|
+
['backgroundColor', '--tl-background'],
|
|
12
|
+
['foregroundColor', '--tl-foreground'],
|
|
13
|
+
['mutedColor', '--tl-muted'],
|
|
14
|
+
['mutedForeground', '--tl-muted-foreground'],
|
|
15
|
+
['borderColor', '--tl-border'],
|
|
16
|
+
['borderRadius', '--tl-radius'],
|
|
17
|
+
['fontFamily', '--tl-font-family'],
|
|
18
|
+
];
|
|
6
19
|
const TlOccurrenceSelector = class {
|
|
7
20
|
constructor(hostRef) {
|
|
8
21
|
registerInstance(this, hostRef);
|
|
@@ -11,565 +24,161 @@ const TlOccurrenceSelector = class {
|
|
|
11
24
|
* Layout orientation (auto = horizontal on desktop, vertical on mobile)
|
|
12
25
|
*/
|
|
13
26
|
this.orientation = 'auto';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* Fetched occurrences
|
|
24
|
-
*/
|
|
25
|
-
this.occurrences = [];
|
|
26
|
-
/**
|
|
27
|
-
* Current month being displayed in calendar view
|
|
28
|
-
*/
|
|
29
|
-
this.currentMonth = new Date();
|
|
30
|
-
/**
|
|
31
|
-
* Currently selected date in calendar view
|
|
32
|
-
*/
|
|
33
|
-
this.selectedDate = null;
|
|
34
|
-
/**
|
|
35
|
-
* Computed display mode based on occurrences
|
|
36
|
-
*/
|
|
37
|
-
this.computedDisplayMode = 'list';
|
|
38
|
-
/**
|
|
39
|
-
* Container width for responsive layout
|
|
40
|
-
*/
|
|
41
|
-
this.containerWidth = 0;
|
|
42
|
-
}
|
|
43
|
-
async componentWillLoad() {
|
|
44
|
-
await this.waitForSDKAndFetch();
|
|
27
|
+
this.element = createElement(occurrenceSelectorCore, {
|
|
28
|
+
client: () => getSDK(),
|
|
29
|
+
// What "the client is up" means on the web. The core waits on it inside its
|
|
30
|
+
// own mount and reads there; the harness decides none of it (TKT-197).
|
|
31
|
+
clientReady: () => whenSDK(),
|
|
32
|
+
emit: (event, detail) => this.forward(event, detail),
|
|
33
|
+
});
|
|
34
|
+
/** The core's whole view. Assigning it is what makes Stencil re-render. */
|
|
35
|
+
this.view = this.element.view;
|
|
45
36
|
}
|
|
46
37
|
/**
|
|
47
|
-
*
|
|
38
|
+
* A new event is a new subject: the element is torn down and mounted again,
|
|
39
|
+
* so whatever the core set up for the old one is undone before anything is
|
|
40
|
+
* set up for the new one.
|
|
48
41
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.loading = false;
|
|
62
|
-
}
|
|
42
|
+
watchSubject() {
|
|
43
|
+
this.syncProps();
|
|
44
|
+
this.teardown?.();
|
|
45
|
+
this.teardown = this.element.mount();
|
|
46
|
+
}
|
|
47
|
+
syncProps() {
|
|
48
|
+
this.element.setProps({
|
|
49
|
+
eventId: this.eventId,
|
|
50
|
+
displayMode: this.displayMode,
|
|
51
|
+
orientation: this.orientation,
|
|
52
|
+
theme: this.theme,
|
|
53
|
+
});
|
|
63
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* On screen: subscribe to the view, then put the core to work.
|
|
57
|
+
*
|
|
58
|
+
* `connectedCallback` and not `componentWillLoad` because this runs again
|
|
59
|
+
* when the element is moved in the DOM, and that is exactly when a mount and
|
|
60
|
+
* its teardown have to pair up.
|
|
61
|
+
*/
|
|
62
|
+
connectedCallback() {
|
|
63
|
+
this.syncProps();
|
|
64
|
+
this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
|
|
65
|
+
this.view = view;
|
|
66
|
+
}));
|
|
67
|
+
this.view = this.element.view;
|
|
68
|
+
this.teardown = this.element.mount();
|
|
69
|
+
}
|
|
70
|
+
/** Measuring the element is the runtime's job; what a width means is not. */
|
|
64
71
|
componentDidLoad() {
|
|
65
|
-
// Set up resize observer for responsive layout
|
|
66
72
|
if (this.containerRef && typeof ResizeObserver !== 'undefined') {
|
|
67
73
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
68
|
-
for (const entry of entries)
|
|
69
|
-
this.
|
|
70
|
-
}
|
|
74
|
+
for (const entry of entries)
|
|
75
|
+
this.element.actions.resized(entry.contentRect.width);
|
|
71
76
|
});
|
|
72
77
|
this.resizeObserver.observe(this.containerRef);
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
|
-
disconnectedCallback() {
|
|
76
|
-
if (this.resizeObserver) {
|
|
77
|
-
this.resizeObserver.disconnect();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
async onEventIdChange() {
|
|
81
|
-
await this.fetchOccurrences();
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Fetch occurrences from the SDK
|
|
85
|
-
*/
|
|
86
|
-
async fetchOccurrences() {
|
|
87
|
-
this.loading = true;
|
|
88
|
-
this.error = null;
|
|
89
|
-
try {
|
|
90
|
-
const sdk = getSDK();
|
|
91
|
-
const eventsManager = sdk.getEventsManager();
|
|
92
|
-
const event = await eventsManager.get(this.eventId);
|
|
93
|
-
if (!event) {
|
|
94
|
-
throw new Error('Event not found');
|
|
95
|
-
}
|
|
96
|
-
// Map EventOccurrence to TlOccurrence
|
|
97
|
-
this.occurrences = (event.occurrences || []).map((occ) => ({
|
|
98
|
-
id: occ.id,
|
|
99
|
-
startsAt: occ.startsAt,
|
|
100
|
-
endsAt: occ.endsAt,
|
|
101
|
-
status: occ.availabilityStatus,
|
|
102
|
-
availableCapacity: occ.availableCapacity,
|
|
103
|
-
soldOut: occ.availabilityStatus === 'sold_out',
|
|
104
|
-
}));
|
|
105
|
-
this.computeDisplayMode();
|
|
106
|
-
this.computeInitialSelection();
|
|
107
|
-
}
|
|
108
|
-
catch (err) {
|
|
109
|
-
console.error('[tl-occurrence-selector] Failed to fetch occurrences:', err);
|
|
110
|
-
this.error = err instanceof Error ? err.message : 'Failed to load occurrences';
|
|
111
|
-
this.occurrences = [];
|
|
112
|
-
}
|
|
113
|
-
finally {
|
|
114
|
-
this.loading = false;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Compute the display mode based on occurrence count and date distribution
|
|
119
|
-
*/
|
|
120
|
-
computeDisplayMode() {
|
|
121
|
-
if (this.displayMode) {
|
|
122
|
-
this.computedDisplayMode = this.displayMode;
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
const count = this.occurrences.length;
|
|
126
|
-
if (count <= 5) {
|
|
127
|
-
this.computedDisplayMode = 'list';
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
// Check if all occurrences are on the same calendar date
|
|
131
|
-
const uniqueDates = new Set();
|
|
132
|
-
for (const occ of this.occurrences) {
|
|
133
|
-
const date = new Date(occ.startsAt);
|
|
134
|
-
uniqueDates.add(date.toDateString());
|
|
135
|
-
}
|
|
136
|
-
if (uniqueDates.size === 1) {
|
|
137
|
-
this.computedDisplayMode = 'slots';
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
this.computedDisplayMode = 'calendar';
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Compute the initial date selection for calendar/slots views
|
|
145
|
-
*/
|
|
146
|
-
computeInitialSelection() {
|
|
147
|
-
if (this.occurrences.length === 0)
|
|
148
|
-
return;
|
|
149
|
-
const today = new Date();
|
|
150
|
-
today.setHours(0, 0, 0, 0);
|
|
151
|
-
// Get all dates with occurrences
|
|
152
|
-
const datesWithOccurrences = this.getDatesWithOccurrences();
|
|
153
|
-
// Find the first date that is today or in the future
|
|
154
|
-
let selectedDate = null;
|
|
155
|
-
for (const dateStr of Array.from(datesWithOccurrences.keys()).sort()) {
|
|
156
|
-
const date = new Date(dateStr);
|
|
157
|
-
if (date >= today) {
|
|
158
|
-
selectedDate = date;
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// If no future dates, select the last available date
|
|
163
|
-
if (!selectedDate && datesWithOccurrences.size > 0) {
|
|
164
|
-
const sortedDates = Array.from(datesWithOccurrences.keys()).sort();
|
|
165
|
-
selectedDate = new Date(sortedDates[sortedDates.length - 1]);
|
|
166
|
-
}
|
|
167
|
-
if (selectedDate) {
|
|
168
|
-
this.selectedDate = selectedDate;
|
|
169
|
-
this.currentMonth = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Get a map of dates to occurrences
|
|
174
|
-
*/
|
|
175
|
-
getDatesWithOccurrences() {
|
|
176
|
-
const map = new Map();
|
|
177
|
-
for (const occ of this.occurrences) {
|
|
178
|
-
const date = new Date(occ.startsAt);
|
|
179
|
-
date.setHours(0, 0, 0, 0);
|
|
180
|
-
const dateStr = date.toDateString();
|
|
181
|
-
if (!map.has(dateStr)) {
|
|
182
|
-
map.set(dateStr, []);
|
|
183
|
-
}
|
|
184
|
-
map.get(dateStr).push(occ);
|
|
185
|
-
}
|
|
186
|
-
return map;
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* Get the first and last occurrence dates
|
|
190
|
-
*/
|
|
191
|
-
getDateRange() {
|
|
192
|
-
if (this.occurrences.length === 0)
|
|
193
|
-
return null;
|
|
194
|
-
let first = new Date(this.occurrences[0].startsAt);
|
|
195
|
-
let last = new Date(this.occurrences[0].startsAt);
|
|
196
|
-
for (const occ of this.occurrences) {
|
|
197
|
-
const date = new Date(occ.startsAt);
|
|
198
|
-
if (date < first)
|
|
199
|
-
first = date;
|
|
200
|
-
if (date > last)
|
|
201
|
-
last = date;
|
|
202
|
-
}
|
|
203
|
-
return { first, last };
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Check if all occurrences fit in one week (Monday-Sunday)
|
|
207
|
-
*/
|
|
208
|
-
isSingleWeek() {
|
|
209
|
-
const dateRange = this.getDateRange();
|
|
210
|
-
if (!dateRange)
|
|
211
|
-
return false;
|
|
212
|
-
// Get Monday of the week containing the first date
|
|
213
|
-
const firstMonday = new Date(dateRange.first);
|
|
214
|
-
const dayOfWeek = firstMonday.getDay();
|
|
215
|
-
const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Monday = 0 days
|
|
216
|
-
firstMonday.setDate(firstMonday.getDate() - daysToSubtract);
|
|
217
|
-
firstMonday.setHours(0, 0, 0, 0);
|
|
218
|
-
// Get Sunday of that week
|
|
219
|
-
const sunday = new Date(firstMonday);
|
|
220
|
-
sunday.setDate(sunday.getDate() + 6);
|
|
221
|
-
sunday.setHours(23, 59, 59, 999);
|
|
222
|
-
// Check if last date is within this week
|
|
223
|
-
return dateRange.last <= sunday;
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Check if all occurrences are in the same month
|
|
227
|
-
*/
|
|
228
|
-
isSingleMonth() {
|
|
229
|
-
const dateRange = this.getDateRange();
|
|
230
|
-
if (!dateRange)
|
|
231
|
-
return false;
|
|
232
|
-
return (dateRange.first.getMonth() === dateRange.last.getMonth() &&
|
|
233
|
-
dateRange.first.getFullYear() === dateRange.last.getFullYear());
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Check if we should use horizontal layout
|
|
237
|
-
*/
|
|
238
|
-
isHorizontalLayout() {
|
|
239
|
-
if (this.orientation === 'horizontal')
|
|
240
|
-
return true;
|
|
241
|
-
if (this.orientation === 'vertical')
|
|
242
|
-
return false;
|
|
243
|
-
// Auto: use horizontal if container is wide enough (768px)
|
|
244
|
-
return this.containerWidth >= 768;
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Format a date for display
|
|
248
|
-
*/
|
|
249
|
-
formatDate(date) {
|
|
250
|
-
return date.toLocaleDateString('en-GB', {
|
|
251
|
-
weekday: 'long',
|
|
252
|
-
day: 'numeric',
|
|
253
|
-
month: 'long',
|
|
254
|
-
year: 'numeric',
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Format a time for display
|
|
259
|
-
*/
|
|
260
|
-
formatTime(dateString) {
|
|
261
|
-
const date = new Date(dateString);
|
|
262
|
-
return date.toLocaleTimeString('en-GB', {
|
|
263
|
-
hour: '2-digit',
|
|
264
|
-
minute: '2-digit',
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Format a full date and time for list view
|
|
269
|
-
*/
|
|
270
|
-
formatDateTime(dateString) {
|
|
271
|
-
const date = new Date(dateString);
|
|
272
|
-
return {
|
|
273
|
-
date: date.toLocaleDateString('en-GB', {
|
|
274
|
-
weekday: 'long',
|
|
275
|
-
day: 'numeric',
|
|
276
|
-
month: 'long',
|
|
277
|
-
year: 'numeric',
|
|
278
|
-
}),
|
|
279
|
-
time: date.toLocaleTimeString('en-GB', {
|
|
280
|
-
hour: '2-digit',
|
|
281
|
-
minute: '2-digit',
|
|
282
|
-
}),
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Handle occurrence selection
|
|
287
|
-
*/
|
|
288
|
-
handleSelect(occurrence) {
|
|
289
|
-
if (occurrence.soldOut || occurrence.availableCapacity === 0)
|
|
290
|
-
return;
|
|
291
|
-
console.log('[tl-occurrence-selector] Selected:', occurrence.id);
|
|
292
|
-
this.tlSelect.emit({ occurrence });
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Handle date selection in calendar view
|
|
296
|
-
*/
|
|
297
|
-
handleDateSelect(date) {
|
|
298
|
-
this.selectedDate = date;
|
|
299
|
-
// If date is in a different month, navigate to that month
|
|
300
|
-
if (date.getMonth() !== this.currentMonth.getMonth() ||
|
|
301
|
-
date.getFullYear() !== this.currentMonth.getFullYear()) {
|
|
302
|
-
this.currentMonth = new Date(date.getFullYear(), date.getMonth(), 1);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
80
|
/**
|
|
306
|
-
*
|
|
81
|
+
* Off screen: run the mount's teardown, and nothing more. Not `destroy()`:
|
|
82
|
+
* Stencil calls this for a DOM move as well as a removal, and a destroyed
|
|
83
|
+
* core never mounts again.
|
|
307
84
|
*/
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const firstMonth = new Date(dateRange.first.getFullYear(), dateRange.first.getMonth(), 1);
|
|
316
|
-
if (newMonth >= firstMonth) {
|
|
317
|
-
this.currentMonth = newMonth;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Navigate to next month
|
|
322
|
-
*/
|
|
323
|
-
handleNextMonth() {
|
|
324
|
-
const dateRange = this.getDateRange();
|
|
325
|
-
if (!dateRange)
|
|
326
|
-
return;
|
|
327
|
-
const newMonth = new Date(this.currentMonth);
|
|
328
|
-
newMonth.setMonth(newMonth.getMonth() + 1);
|
|
329
|
-
// Don't go past the last occurrence month
|
|
330
|
-
const lastMonth = new Date(dateRange.last.getFullYear(), dateRange.last.getMonth(), 1);
|
|
331
|
-
if (newMonth <= lastMonth) {
|
|
332
|
-
this.currentMonth = newMonth;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Check if prev month button should be disabled
|
|
337
|
-
*/
|
|
338
|
-
isPrevMonthDisabled() {
|
|
339
|
-
const dateRange = this.getDateRange();
|
|
340
|
-
if (!dateRange)
|
|
341
|
-
return true;
|
|
342
|
-
const firstMonth = new Date(dateRange.first.getFullYear(), dateRange.first.getMonth(), 1);
|
|
343
|
-
return this.currentMonth <= firstMonth;
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Check if next month button should be disabled
|
|
347
|
-
*/
|
|
348
|
-
isNextMonthDisabled() {
|
|
349
|
-
const dateRange = this.getDateRange();
|
|
350
|
-
if (!dateRange)
|
|
351
|
-
return true;
|
|
352
|
-
const lastMonth = new Date(dateRange.last.getFullYear(), dateRange.last.getMonth(), 1);
|
|
353
|
-
return this.currentMonth >= lastMonth;
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Get calendar grid days for the current month
|
|
357
|
-
*/
|
|
358
|
-
getCalendarDays() {
|
|
359
|
-
// If all occurrences fit in one week, only show that week
|
|
360
|
-
if (this.isSingleWeek()) {
|
|
361
|
-
const dateRange = this.getDateRange();
|
|
362
|
-
if (!dateRange)
|
|
363
|
-
return [];
|
|
364
|
-
// Get Monday of the week containing the first date
|
|
365
|
-
const firstMonday = new Date(dateRange.first);
|
|
366
|
-
const dayOfWeek = firstMonday.getDay();
|
|
367
|
-
const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
|
368
|
-
firstMonday.setDate(firstMonday.getDate() - daysToSubtract);
|
|
369
|
-
firstMonday.setHours(0, 0, 0, 0);
|
|
370
|
-
// Generate 7 days (Monday to Sunday)
|
|
371
|
-
const days = [];
|
|
372
|
-
const current = new Date(firstMonday);
|
|
373
|
-
for (let i = 0; i < 7; i++) {
|
|
374
|
-
days.push(new Date(current));
|
|
375
|
-
current.setDate(current.getDate() + 1);
|
|
376
|
-
}
|
|
377
|
-
return days;
|
|
378
|
-
}
|
|
379
|
-
// Otherwise show full month grid
|
|
380
|
-
const year = this.currentMonth.getFullYear();
|
|
381
|
-
const month = this.currentMonth.getMonth();
|
|
382
|
-
// Start of month
|
|
383
|
-
const monthStart = new Date(year, month, 1);
|
|
384
|
-
// End of month
|
|
385
|
-
const monthEnd = new Date(year, month + 1, 0);
|
|
386
|
-
// Get the start of the week containing the first day (Monday = 1)
|
|
387
|
-
const startDay = monthStart.getDay();
|
|
388
|
-
const daysToSubtract = startDay === 0 ? 6 : startDay - 1; // Monday = 0 days to subtract
|
|
389
|
-
const calendarStart = new Date(monthStart);
|
|
390
|
-
calendarStart.setDate(calendarStart.getDate() - daysToSubtract);
|
|
391
|
-
// Get the end of the week containing the last day
|
|
392
|
-
const endDay = monthEnd.getDay();
|
|
393
|
-
const daysToAdd = endDay === 0 ? 0 : 7 - endDay;
|
|
394
|
-
const calendarEnd = new Date(monthEnd);
|
|
395
|
-
calendarEnd.setDate(calendarEnd.getDate() + daysToAdd);
|
|
396
|
-
// Generate all days
|
|
397
|
-
const days = [];
|
|
398
|
-
const current = new Date(calendarStart);
|
|
399
|
-
while (current <= calendarEnd) {
|
|
400
|
-
days.push(new Date(current));
|
|
401
|
-
current.setDate(current.getDate() + 1);
|
|
402
|
-
}
|
|
403
|
-
return days;
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Check if a date has occurrences
|
|
407
|
-
*/
|
|
408
|
-
hasOccurrencesOnDate(date) {
|
|
409
|
-
const dateStr = date.toDateString();
|
|
410
|
-
return this.getDatesWithOccurrences().has(dateStr);
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Check if all occurrences on a date are sold out
|
|
414
|
-
*/
|
|
415
|
-
isDateSoldOut(date) {
|
|
416
|
-
const dateStr = date.toDateString();
|
|
417
|
-
const occurrences = this.getDatesWithOccurrences().get(dateStr);
|
|
418
|
-
if (!occurrences)
|
|
419
|
-
return false;
|
|
420
|
-
return occurrences.every(occ => occ.soldOut || occ.availableCapacity === 0);
|
|
85
|
+
disconnectedCallback() {
|
|
86
|
+
this.resizeObserver?.disconnect();
|
|
87
|
+
this.resizeObserver = undefined;
|
|
88
|
+
this.teardown?.();
|
|
89
|
+
this.teardown = undefined;
|
|
90
|
+
this.unsubscribe?.();
|
|
91
|
+
this.unsubscribe = undefined;
|
|
421
92
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
getSelectedDateOccurrences() {
|
|
426
|
-
if (!this.selectedDate)
|
|
427
|
-
return [];
|
|
428
|
-
const dateStr = this.selectedDate.toDateString();
|
|
429
|
-
const occurrences = this.getDatesWithOccurrences().get(dateStr) || [];
|
|
430
|
-
// Sort by start time
|
|
431
|
-
return occurrences.sort((a, b) => new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime());
|
|
93
|
+
forward(event, detail) {
|
|
94
|
+
if (event === 'tlSelect')
|
|
95
|
+
this.tlSelect.emit(detail);
|
|
432
96
|
}
|
|
433
97
|
/**
|
|
434
|
-
*
|
|
98
|
+
* The host's theme, as custom properties over the ones the theme package
|
|
99
|
+
* ships. Web only: a custom property is not a fact every runtime has, so the
|
|
100
|
+
* core declares the prop and the harness is what acts on it.
|
|
435
101
|
*/
|
|
436
|
-
|
|
437
|
-
if (!this.theme)
|
|
438
|
-
return {};
|
|
102
|
+
themeStyles() {
|
|
439
103
|
const styles = {};
|
|
440
|
-
if (this.theme
|
|
441
|
-
styles
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (this.theme.backgroundColor) {
|
|
447
|
-
styles['--tl-background'] = this.theme.backgroundColor;
|
|
448
|
-
}
|
|
449
|
-
if (this.theme.foregroundColor) {
|
|
450
|
-
styles['--tl-foreground'] = this.theme.foregroundColor;
|
|
451
|
-
}
|
|
452
|
-
if (this.theme.mutedColor) {
|
|
453
|
-
styles['--tl-muted'] = this.theme.mutedColor;
|
|
454
|
-
}
|
|
455
|
-
if (this.theme.mutedForeground) {
|
|
456
|
-
styles['--tl-muted-foreground'] = this.theme.mutedForeground;
|
|
457
|
-
}
|
|
458
|
-
if (this.theme.borderColor) {
|
|
459
|
-
styles['--tl-border'] = this.theme.borderColor;
|
|
460
|
-
}
|
|
461
|
-
if (this.theme.borderRadius) {
|
|
462
|
-
styles['--tl-radius'] = this.theme.borderRadius;
|
|
463
|
-
}
|
|
464
|
-
if (this.theme.fontFamily) {
|
|
465
|
-
styles['--tl-font-family'] = this.theme.fontFamily;
|
|
104
|
+
if (!this.theme)
|
|
105
|
+
return styles;
|
|
106
|
+
for (const [field, property] of THEME_PROPERTIES) {
|
|
107
|
+
const value = this.theme[field];
|
|
108
|
+
if (value)
|
|
109
|
+
styles[property] = value;
|
|
466
110
|
}
|
|
467
111
|
return styles;
|
|
468
112
|
}
|
|
469
113
|
// =========================================================================
|
|
470
114
|
// RENDER METHODS
|
|
471
115
|
// =========================================================================
|
|
472
|
-
/**
|
|
473
|
-
|
|
474
|
-
|
|
116
|
+
/** The availability mark a row carries, in the list and the slots panel. */
|
|
117
|
+
renderRowStatus(row) {
|
|
118
|
+
const { strings } = this.view;
|
|
119
|
+
if (row.statusKind === 'soldOut')
|
|
120
|
+
return h("span", { class: "status-sold-out" }, strings.soldOut);
|
|
121
|
+
if (row.statusKind === 'available')
|
|
122
|
+
return h("span", { class: "status-available" }, row.capacityLabel);
|
|
123
|
+
return h("span", { class: "status-arrow" }, "\u2192");
|
|
124
|
+
}
|
|
125
|
+
/** The list view (five occurrences or fewer). */
|
|
475
126
|
renderListView() {
|
|
476
|
-
return (h("div", { class: "list-view" }, this.
|
|
477
|
-
const isSoldOut = occurrence.soldOut || occurrence.availableCapacity === 0;
|
|
478
|
-
const { date, time } = this.formatDateTime(occurrence.startsAt);
|
|
479
|
-
return (h("button", { key: occurrence.id, class: `occurrence-item ${isSoldOut ? 'sold-out' : ''}`, onClick: () => this.handleSelect(occurrence), disabled: isSoldOut }, h("div", { class: "occurrence-info" }, h("div", { class: "occurrence-date" }, date), h("div", { class: "occurrence-time" }, time, occurrence.endsAt && ` - ${this.formatTime(occurrence.endsAt)}`)), h("div", { class: "occurrence-status" }, isSoldOut ? (h("span", { class: "status-sold-out" }, "Sold Out")) : occurrence.availableCapacity ? (h("span", { class: "status-available" }, occurrence.availableCapacity, " available")) : (h("span", { class: "status-arrow" }, "\u2192")))));
|
|
480
|
-
})));
|
|
127
|
+
return (h("div", { class: "list-view" }, this.view.derived.rows.map((row) => (h("button", { key: row.id, class: `occurrence-item ${row.soldOut ? 'sold-out' : ''}`, onClick: () => this.element.actions.select(row.id), disabled: row.soldOut }, h("div", { class: "occurrence-info" }, h("div", { class: "occurrence-date" }, row.dateLabel), h("div", { class: "occurrence-time" }, row.timeLabel)), h("div", { class: "occurrence-status" }, this.renderRowStatus(row)))))));
|
|
481
128
|
}
|
|
482
|
-
/**
|
|
483
|
-
* Render the time slots view (>5 occurrences, same day)
|
|
484
|
-
*/
|
|
129
|
+
/** The time slots view (more than five, all on one date). */
|
|
485
130
|
renderSlotsView() {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
// Get the date from the first occurrence
|
|
489
|
-
const dateLabel = sortedOccurrences.length > 0
|
|
490
|
-
? this.formatDate(new Date(sortedOccurrences[0].startsAt))
|
|
491
|
-
: '';
|
|
492
|
-
return (h("div", { class: "slots-view" }, dateLabel && h("div", { class: "slots-date-header" }, dateLabel), h("div", { class: "time-slots-grid" }, sortedOccurrences.map((occurrence) => {
|
|
493
|
-
const isSoldOut = occurrence.soldOut || occurrence.availableCapacity === 0;
|
|
494
|
-
return (h("button", { key: occurrence.id, class: `time-slot ${isSoldOut ? 'sold-out' : ''}`, onClick: () => this.handleSelect(occurrence), disabled: isSoldOut }, h("span", { class: "slot-time" }, this.formatTime(occurrence.startsAt)), isSoldOut && h("span", { class: "slot-sold-out" }, "Sold Out")));
|
|
495
|
-
}))));
|
|
131
|
+
const { slots, slotsDateLabel } = this.view.derived;
|
|
132
|
+
return (h("div", { class: "slots-view" }, slotsDateLabel && h("div", { class: "slots-date-header" }, slotsDateLabel), h("div", { class: "time-slots-grid" }, slots.map((row) => (h("button", { key: row.id, class: `time-slot ${row.soldOut ? 'sold-out' : ''}`, onClick: () => this.element.actions.select(row.id), disabled: row.soldOut }, h("span", { class: "slot-time" }, row.startLabel), row.soldOut && h("span", { class: "slot-sold-out" }, this.view.strings.soldOut)))))));
|
|
496
133
|
}
|
|
497
|
-
/**
|
|
498
|
-
* Render the calendar header
|
|
499
|
-
*/
|
|
500
134
|
renderCalendarHeader() {
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
return (h("div", { class: "calendar-header" }, h("span", { class: "month-label" }, monthLabel), showNavigation && (h("div", { class: "calendar-nav-buttons" }, h("button", { class: "nav-button", onClick: () => this.handlePrevMonth(), disabled: this.isPrevMonthDisabled(), "aria-label": "Previous month" }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M15 18l-6-6 6-6" }))), h("button", { class: "nav-button", onClick: () => this.handleNextMonth(), disabled: this.isNextMonthDisabled(), "aria-label": "Next month" }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M9 18l6-6-6-6" })))))));
|
|
135
|
+
const { strings, derived } = this.view;
|
|
136
|
+
return (h("div", { class: "calendar-header" }, h("span", { class: "month-label" }, derived.monthLabel), derived.showMonthNav && (h("div", { class: "calendar-nav-buttons" }, h("button", { class: "nav-button", onClick: () => this.element.actions.previousMonth(), disabled: !derived.canGoBack, "aria-label": strings.previousMonth }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M15 18l-6-6 6-6" }))), h("button", { class: "nav-button", onClick: () => this.element.actions.nextMonth(), disabled: !derived.canGoForward, "aria-label": strings.nextMonth }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M9 18l6-6-6-6" })))))));
|
|
137
|
+
}
|
|
138
|
+
renderCalendarDay(day) {
|
|
139
|
+
return (h("button", { key: day.key, class: day.className, onClick: () => this.element.actions.selectDate(day.key), disabled: !day.selectable }, h("span", { class: "day-number" }, day.dayNumber), day.hasOccurrences && h("span", { class: "occurrence-dot" })));
|
|
507
140
|
}
|
|
508
|
-
/**
|
|
509
|
-
* Render the calendar grid
|
|
510
|
-
*/
|
|
511
141
|
renderCalendarGrid() {
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
const today = new Date();
|
|
515
|
-
today.setHours(0, 0, 0, 0);
|
|
516
|
-
return (h("div", { class: "calendar-grid" }, weekDays.map((day) => (h("div", { class: "weekday-header", key: day }, day))), days.map((date) => {
|
|
517
|
-
const isCurrentMonth = date.getMonth() === this.currentMonth.getMonth();
|
|
518
|
-
const hasOccurrences = this.hasOccurrencesOnDate(date);
|
|
519
|
-
const isSoldOut = this.isDateSoldOut(date);
|
|
520
|
-
const isSelected = this.selectedDate && date.toDateString() === this.selectedDate.toDateString();
|
|
521
|
-
const isToday = date.toDateString() === today.toDateString();
|
|
522
|
-
const isPast = date < today;
|
|
523
|
-
return (h("button", { key: date.toISOString(), class: `calendar-day
|
|
524
|
-
${!isCurrentMonth ? 'other-month' : ''}
|
|
525
|
-
${hasOccurrences ? 'has-occurrences' : ''}
|
|
526
|
-
${isSoldOut ? 'sold-out' : ''}
|
|
527
|
-
${isSelected ? 'selected' : ''}
|
|
528
|
-
${isToday ? 'today' : ''}
|
|
529
|
-
${isPast ? 'past' : ''}`, onClick: () => hasOccurrences && this.handleDateSelect(date), disabled: !hasOccurrences }, h("span", { class: "day-number" }, date.getDate()), hasOccurrences && h("span", { class: "occurrence-dot" })));
|
|
530
|
-
})));
|
|
142
|
+
const { derived } = this.view;
|
|
143
|
+
return (h("div", { class: "calendar-grid" }, derived.weekdayLabels.map((day) => (h("div", { class: "weekday-header", key: day }, day))), derived.days.map((day) => this.renderCalendarDay(day))));
|
|
531
144
|
}
|
|
532
|
-
/**
|
|
533
|
-
* Render the selected date's time slots
|
|
534
|
-
*/
|
|
145
|
+
/** The picked date's times, beside or below the calendar. */
|
|
535
146
|
renderSelectedDateSlots() {
|
|
536
|
-
const
|
|
537
|
-
if (!
|
|
538
|
-
return (h("div", { class: "selected-date-slots empty" }, h("p", null,
|
|
147
|
+
const { strings, derived } = this.view;
|
|
148
|
+
if (!derived.selectedDateLabel) {
|
|
149
|
+
return (h("div", { class: "selected-date-slots empty" }, h("p", null, strings.selectADate)));
|
|
539
150
|
}
|
|
540
|
-
|
|
541
|
-
return (h("div", { class: "selected-date-slots" }, h("div", { class: "selected-date-header" }, dateLabel), h("div", { class: "time-slots-list" }, occurrences.map((occurrence) => {
|
|
542
|
-
const isSoldOut = occurrence.soldOut || occurrence.availableCapacity === 0;
|
|
543
|
-
return (h("button", { key: occurrence.id, class: `time-slot-item ${isSoldOut ? 'sold-out' : ''}`, onClick: () => this.handleSelect(occurrence), disabled: isSoldOut }, h("span", { class: "slot-time" }, this.formatTime(occurrence.startsAt)), h("div", { class: "slot-status" }, isSoldOut ? (h("span", { class: "status-sold-out" }, "Sold Out")) : occurrence.availableCapacity ? (h("span", { class: "status-available" }, occurrence.availableCapacity, " available")) : (h("span", { class: "status-arrow" }, "\u2192")))));
|
|
544
|
-
}))));
|
|
151
|
+
return (h("div", { class: "selected-date-slots" }, h("div", { class: "selected-date-header" }, derived.selectedDateLabel), h("div", { class: "time-slots-list" }, derived.selectedSlots.map((row) => (h("button", { key: row.id, class: `time-slot-item ${row.soldOut ? 'sold-out' : ''}`, onClick: () => this.element.actions.select(row.id), disabled: row.soldOut }, h("span", { class: "slot-time" }, row.startLabel), h("div", { class: "slot-status" }, this.renderRowStatus(row))))))));
|
|
545
152
|
}
|
|
546
|
-
/**
|
|
547
|
-
* Render the calendar view with slots panel
|
|
548
|
-
*/
|
|
549
153
|
renderCalendarView() {
|
|
550
|
-
|
|
551
|
-
return (h("div", { class: `calendar-view ${isHorizontal ? 'horizontal' : 'vertical'}` }, h("div", { class: "calendar-panel" }, this.renderCalendarHeader(), this.renderCalendarGrid()), h("div", { class: "slots-panel" }, this.renderSelectedDateSlots())));
|
|
154
|
+
return (h("div", { class: this.view.derived.viewClass }, h("div", { class: "calendar-panel" }, this.renderCalendarHeader(), this.renderCalendarGrid()), h("div", { class: "slots-panel" }, this.renderSelectedDateSlots())));
|
|
552
155
|
}
|
|
553
|
-
/**
|
|
554
|
-
* Render skeleton loading state
|
|
555
|
-
*/
|
|
156
|
+
/** The skeleton, which is a calendar because that is the common shape. */
|
|
556
157
|
renderSkeleton() {
|
|
557
|
-
// Default to calendar skeleton as it's the most common
|
|
558
158
|
return (h("div", { class: "loading-container" }, h("div", { class: "skeleton-calendar" }, h("div", { class: "skeleton skeleton-calendar-header" }), h("div", { class: "skeleton-calendar-grid" }, Array.from({ length: 35 }, (_, i) => (h("div", { key: i, class: "skeleton skeleton-calendar-day" })))))));
|
|
559
159
|
}
|
|
560
160
|
render() {
|
|
561
|
-
const
|
|
562
|
-
return (h("div", { key: '
|
|
161
|
+
const { status, strings, state, derived } = this.view;
|
|
162
|
+
return (h("div", { key: '84951fb5171ed6c79604861a171f49f43c8b809c', class: "occurrence-selector", ref: (el) => (this.containerRef = el), style: this.themeStyles() }, status === 'loading' && this.renderSkeleton(), status === 'error' && (h("div", { key: '2850712eae277f32f3538426c8a5d36b73ca918e', class: "error-container" }, h("svg", { key: '89192cd7cce29781b87c83c8dee71d59f6a685ed', class: "error-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("circle", { key: '27a52ecb1228c541202752be84323abe687c1655', cx: "12", cy: "12", r: "10" }), h("path", { key: 'f51c2b49de89b9c66cde01e11a2de4c56d4dbad9', d: "M12 8v4m0 4h.01" })), h("p", { key: '3c85d762de430f477d2c9ad4341b23765c93e2e3', class: "error-message" }, state.error), h("button", { key: '22fa271a8f0a15e18141cf6d66a25a2bf9299c72', class: "retry-button", onClick: () => void this.element.actions.load() }, strings.retry))), status === 'empty' && (h("div", { key: '285403135ccff6cd6c8de6a9c62c9186c67c102c', class: "no-occurrences" }, h("p", { key: '98373f0c5793cb1aba1328558fac3210e7c6aeeb' }, strings.empty))), status === 'ready' && (h("div", { key: 'a61d2c02538f3339e54770e53a4f807e72182357', class: "selector-content" }, derived.mode === 'list' && this.renderListView(), derived.mode === 'slots' && this.renderSlotsView(), derived.mode === 'calendar' && this.renderCalendarView()))));
|
|
563
163
|
}
|
|
564
164
|
static get watchers() { return {
|
|
565
165
|
"eventId": [{
|
|
566
|
-
"
|
|
166
|
+
"watchSubject": 0
|
|
167
|
+
}],
|
|
168
|
+
"displayMode": [{
|
|
169
|
+
"syncProps": 0
|
|
170
|
+
}],
|
|
171
|
+
"orientation": [{
|
|
172
|
+
"syncProps": 0
|
|
173
|
+
}],
|
|
174
|
+
"theme": [{
|
|
175
|
+
"syncProps": 0
|
|
567
176
|
}]
|
|
568
177
|
}; }
|
|
569
178
|
};
|
|
570
179
|
TlOccurrenceSelector.style = tlOccurrenceSelectorCss();
|
|
571
180
|
|
|
572
|
-
const tlTicketSelectorCss = () => `:host{--tl-color-primary:var(--tl-primary
|
|
181
|
+
const tlTicketSelectorCss = () => `:host{--tl-color-primary:var(--tl-primary);--tl-color-primary-foreground:var(--tl-primary-foreground);--tl-color-background:var(--tl-background);--tl-color-foreground:var(--tl-foreground);--tl-color-muted:var(--tl-muted);--tl-color-muted-foreground:var(--tl-muted-foreground);--tl-color-border:var(--tl-border);--tl-color-destructive:var(--tl-error);display:block;font-family:var(--tl-font-family)}.ticket-selector{background:var(--tl-color-background)}.skeleton-container{padding:var(--tl-spacing-4)}.skeleton-row{height:60px;background:var(--tl-color-muted);border-radius:var(--tl-radius-md);margin-bottom:var(--tl-spacing-3);animation:pulse 1.5s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}.error-state{padding:var(--tl-spacing-4);color:var(--tl-color-destructive)}.error-message{margin:0;font-size:14px}.ticket-list{margin-bottom:var(--tl-spacing-4)}.ticket-row{display:flex;align-items:center;justify-content:space-between;padding:var(--tl-spacing-3) 0;border-bottom:1px solid var(--tl-color-border)}.ticket-row.sold-out{opacity:0.5}.ticket-info{flex:1;min-width:0}.ticket-header{display:flex;align-items:center;gap:var(--tl-spacing-2);flex-wrap:wrap}.ticket-name{font-weight:600;color:var(--tl-color-foreground);font-size:15px}.status-badge{font-size:12px;font-weight:500;padding:2px 8px;border-radius:4px}.status-badge.limited{background-color:var(--tl-color-warning-bg);color:var(--tl-color-warning)}.status-badge.sold-out{background-color:var(--tl-color-destructive-bg);color:var(--tl-color-destructive)}.ticket-description{margin:var(--tl-spacing-1) 0 0;font-size:14px;color:var(--tl-color-muted-foreground)}.ticket-price{margin:var(--tl-spacing-1) 0 0;font-size:14px;font-weight:600;color:var(--tl-color-foreground)}.quantity-controls{display:flex;align-items:center;gap:var(--tl-spacing-2);margin-left:var(--tl-spacing-4)}.qty-btn{width:32px;height:32px;border-radius:var(--tl-radius-sm);border:1px solid var(--tl-color-border);background:var(--tl-color-background);color:var(--tl-color-foreground);font-size:18px;font-weight:500;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background-color 0.15s, border-color 0.15s}.qty-btn:hover:not(:disabled){background-color:var(--tl-color-muted);border-color:var(--tl-color-border-hover)}.qty-btn:disabled{cursor:not-allowed;opacity:0.5}.qty-value{width:32px;text-align:center;font-weight:600;font-size:16px;color:var(--tl-color-foreground)}.success-message{display:flex;align-items:center;justify-content:center;gap:var(--tl-spacing-2);padding:var(--tl-spacing-3);background-color:var(--tl-color-success);color:#ffffff;border-radius:var(--tl-radius-md);margin-bottom:var(--tl-spacing-3);font-weight:500;font-size:14px}.success-icon{width:18px;height:18px}.add-to-cart-btn{width:100%;padding:var(--tl-spacing-3) var(--tl-spacing-4);border-radius:var(--tl-radius-md);border:none;background-color:var(--tl-color-primary);color:var(--tl-color-primary-foreground);font-weight:600;font-size:16px;cursor:pointer;transition:opacity 0.15s, transform 0.1s}.add-to-cart-btn:hover:not(:disabled){opacity:0.9}.add-to-cart-btn:active:not(:disabled){transform:scale(0.98)}.add-to-cart-btn.disabled,.add-to-cart-btn:disabled{background-color:var(--tl-color-muted);color:var(--tl-color-muted-foreground);cursor:not-allowed}.selection-totals{padding:var(--tl-spacing-3) 0;border-top:1px solid var(--tl-color-border)}.selection-totals .total-row{display:flex;justify-content:space-between;align-items:center;padding:var(--tl-spacing-1) 0;font-size:14px;color:var(--tl-color-muted-foreground)}.selection-totals .total-row.total{font-size:16px;font-weight:700;color:var(--tl-color-foreground);padding-top:var(--tl-spacing-2);border-top:1px solid var(--tl-color-border);margin-top:var(--tl-spacing-2)}`;
|
|
573
182
|
|
|
574
183
|
const TlTicketSelector = class {
|
|
575
184
|
constructor(hostRef) {
|
|
@@ -585,157 +194,106 @@ const TlTicketSelector = class {
|
|
|
585
194
|
* Button label
|
|
586
195
|
*/
|
|
587
196
|
this.buttonLabel = 'Add to Cart';
|
|
588
|
-
this.
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
197
|
+
this.element = createElement(ticketSelectorCore, {
|
|
198
|
+
client: () => getSDK(),
|
|
199
|
+
// What "the client is up" means on the web. The core waits on it in its
|
|
200
|
+
// own mount; the harness no longer decides when to load (TKT-197).
|
|
201
|
+
clientReady: () => whenSDK(),
|
|
202
|
+
emit: (event, detail) => this.forward(event, detail),
|
|
203
|
+
});
|
|
204
|
+
/** The core's whole view. Assigning it is what makes Stencil re-render. */
|
|
205
|
+
this.view = this.element.view;
|
|
594
206
|
}
|
|
595
|
-
|
|
596
|
-
|
|
207
|
+
/**
|
|
208
|
+
* The fee the surface has quoted for this selection. Core state, exposed here
|
|
209
|
+
* because it is set from outside the element and not from an attribute: there
|
|
210
|
+
* is no quote operation on the buyer-facing surface yet, so it stays null and
|
|
211
|
+
* no fee row is rendered.
|
|
212
|
+
*/
|
|
213
|
+
get feeQuote() {
|
|
214
|
+
return this.element.state.feeQuote;
|
|
597
215
|
}
|
|
598
|
-
|
|
599
|
-
|
|
216
|
+
set feeQuote(quote) {
|
|
217
|
+
this.element.actions.setFeeQuote(quote);
|
|
600
218
|
}
|
|
601
219
|
/**
|
|
602
|
-
*
|
|
220
|
+
* A new event or occurrence is a new subject: the element is torn down and
|
|
221
|
+
* mounted again, so whatever the core set up for the old one is undone
|
|
222
|
+
* before anything is set up for the new one.
|
|
603
223
|
*/
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
async loadTicketTypes() {
|
|
618
|
-
try {
|
|
619
|
-
console.log('[tl-ticket-selector] Loading ticket types...', {
|
|
620
|
-
eventId: this.eventId,
|
|
621
|
-
occurrenceId: this.occurrenceId
|
|
622
|
-
});
|
|
623
|
-
this.loading = true;
|
|
624
|
-
this.error = null;
|
|
625
|
-
const sdk = getSDK();
|
|
626
|
-
const eventsManager = sdk.getEventsManager();
|
|
627
|
-
const types = await eventsManager.getTicketTypes(this.eventId, this.occurrenceId);
|
|
628
|
-
console.log('[tl-ticket-selector] Ticket types loaded:', types.length);
|
|
629
|
-
this.ticketTypes = types;
|
|
630
|
-
// Initialise quantities to 0
|
|
631
|
-
const initialQuantities = {};
|
|
632
|
-
types.forEach((t) => {
|
|
633
|
-
initialQuantities[t.id] = 0;
|
|
634
|
-
});
|
|
635
|
-
this.quantities = initialQuantities;
|
|
636
|
-
}
|
|
637
|
-
catch (err) {
|
|
638
|
-
console.error('[tl-ticket-selector] Failed to load ticket types:', err);
|
|
639
|
-
this.error = err instanceof Error ? err.message : 'Failed to load tickets';
|
|
640
|
-
this.tlError.emit({ message: this.error });
|
|
641
|
-
}
|
|
642
|
-
finally {
|
|
643
|
-
this.loading = false;
|
|
644
|
-
}
|
|
224
|
+
watchPropsChange() {
|
|
225
|
+
this.syncProps();
|
|
226
|
+
this.remount();
|
|
227
|
+
}
|
|
228
|
+
syncProps() {
|
|
229
|
+
this.element.setProps({
|
|
230
|
+
eventId: this.eventId,
|
|
231
|
+
occurrenceId: this.occurrenceId,
|
|
232
|
+
showButton: this.showButton,
|
|
233
|
+
buttonLabel: this.buttonLabel,
|
|
234
|
+
});
|
|
645
235
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
this.
|
|
236
|
+
/**
|
|
237
|
+
* On screen: render what the core says and put the core to work. What "to
|
|
238
|
+
* work" means is the core's own business - for this element, waiting for the
|
|
239
|
+
* client and reading the ticket types.
|
|
240
|
+
*
|
|
241
|
+
* `connectedCallback` and not `componentWillLoad` because this runs again
|
|
242
|
+
* when the element is moved in the DOM, and that is exactly when a mount and
|
|
243
|
+
* its teardown have to pair up.
|
|
244
|
+
*/
|
|
245
|
+
connectedCallback() {
|
|
246
|
+
this.syncProps();
|
|
247
|
+
this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
|
|
248
|
+
this.view = view;
|
|
249
|
+
}));
|
|
250
|
+
this.view = this.element.view;
|
|
251
|
+
this.teardown = this.element.mount();
|
|
658
252
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Off screen: run the mount's teardown, and nothing more. `destroy()` is
|
|
255
|
+
* terminal and Stencil calls this for a move as well as a removal, so
|
|
256
|
+
* destroying here would leave a re-inserted element frozen. The instance
|
|
257
|
+
* holds no listener and no subscription once these two have run.
|
|
258
|
+
*/
|
|
259
|
+
disconnectedCallback() {
|
|
260
|
+
this.teardown?.();
|
|
261
|
+
this.teardown = undefined;
|
|
262
|
+
this.unsubscribe?.();
|
|
263
|
+
this.unsubscribe = undefined;
|
|
663
264
|
}
|
|
664
|
-
|
|
665
|
-
|
|
265
|
+
remount() {
|
|
266
|
+
this.teardown?.();
|
|
267
|
+
this.teardown = this.element.mount();
|
|
666
268
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
.
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
return;
|
|
673
|
-
this.adding = true;
|
|
674
|
-
this.addSuccess = false;
|
|
675
|
-
try {
|
|
676
|
-
if (hasSDK()) {
|
|
677
|
-
const sdk = getSDK();
|
|
678
|
-
const cartManager = sdk.getCartManager?.();
|
|
679
|
-
if (cartManager) {
|
|
680
|
-
for (const item of itemsToAdd) {
|
|
681
|
-
await cartManager.addItem(item.ticketTypeId, item.quantity, this.occurrenceId);
|
|
682
|
-
}
|
|
683
|
-
console.log('[tl-ticket-selector] Items added to cart');
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
// Reset quantities after adding
|
|
687
|
-
const resetQuantities = {};
|
|
688
|
-
this.ticketTypes.forEach(t => {
|
|
689
|
-
resetQuantities[t.id] = 0;
|
|
690
|
-
});
|
|
691
|
-
this.quantities = resetQuantities;
|
|
692
|
-
this.addSuccess = true;
|
|
693
|
-
// Clear success message after 3 seconds
|
|
694
|
-
setTimeout(() => {
|
|
695
|
-
this.addSuccess = false;
|
|
696
|
-
}, 3000);
|
|
697
|
-
this.tlAddToCart.emit({ items: itemsToAdd });
|
|
269
|
+
forward(event, detail) {
|
|
270
|
+
if (event === 'tlAddToCart')
|
|
271
|
+
this.tlAddToCart.emit(detail);
|
|
272
|
+
else if (event === 'tlQuantityChange') {
|
|
273
|
+
this.tlQuantityChange.emit(detail);
|
|
698
274
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
this.error = err instanceof Error ? err.message : 'Failed to add to cart';
|
|
702
|
-
this.tlError.emit({ message: this.error });
|
|
703
|
-
}
|
|
704
|
-
finally {
|
|
705
|
-
this.adding = false;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
formatPrice(priceInPence, currency = 'GBP') {
|
|
709
|
-
return new Intl.NumberFormat('en-GB', {
|
|
710
|
-
style: 'currency',
|
|
711
|
-
currency,
|
|
712
|
-
}).format(priceInPence / 100);
|
|
275
|
+
else if (event === 'tlError')
|
|
276
|
+
this.tlError.emit(detail);
|
|
713
277
|
}
|
|
714
278
|
renderSkeleton() {
|
|
715
279
|
return (h("div", { class: "skeleton-container" }, h("div", { class: "skeleton-row" }), h("div", { class: "skeleton-row" }), h("div", { class: "skeleton-row" })));
|
|
716
280
|
}
|
|
717
281
|
renderError() {
|
|
718
|
-
return (h("div", { class: "error-state" }, h("p", { class: "error-message" }, this.error)));
|
|
282
|
+
return (h("div", { class: "error-state" }, h("p", { class: "error-message" }, this.view.state.error)));
|
|
719
283
|
}
|
|
720
|
-
renderTicketRow(
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
return (h("div", { class: `ticket-row ${isSoldOut ? 'sold-out' : ''}`, key: tt.id }, h("div", { class: "ticket-info" }, h("div", { class: "ticket-header" }, h("span", { class: "ticket-name" }, tt.name), tt.availabilityStatus === 'limited' && (h("span", { class: "status-badge limited" }, "Limited")), isSoldOut && (h("span", { class: "status-badge sold-out" }, "Sold Out"))), tt.description && (h("p", { class: "ticket-description" }, tt.description)), h("p", { class: "ticket-price" }, this.formatPrice(tt.price, tt.currency))), h("div", { class: "quantity-controls" }, h("button", { class: "qty-btn", onClick: () => this.handleQuantityChange(tt.id, -1), disabled: isSoldOut || qty === 0, "aria-label": "Decrease quantity" }, "\u2212"), h("span", { class: "qty-value" }, qty), h("button", { class: "qty-btn", onClick: () => this.handleQuantityChange(tt.id, 1), disabled: isSoldOut || qty >= tt.maxPerOrder, "aria-label": "Increase quantity" }, "+"))));
|
|
284
|
+
renderTicketRow(row) {
|
|
285
|
+
const strings = this.view.strings;
|
|
286
|
+
return (h("div", { class: `ticket-row ${row.soldOut ? 'sold-out' : ''}`, key: row.id }, h("div", { class: "ticket-info" }, h("div", { class: "ticket-header" }, h("span", { class: "ticket-name" }, row.name), row.limited && h("span", { class: "status-badge limited" }, strings.limited), row.soldOut && h("span", { class: "status-badge sold-out" }, strings.soldOut)), row.description && h("p", { class: "ticket-description" }, row.description), h("p", { class: "ticket-price" }, row.priceLabel)), h("div", { class: "quantity-controls" }, h("button", { class: "qty-btn", onClick: () => this.element.actions.changeQuantity(row.id, -1), disabled: !row.canDecrease, "aria-label": strings.decrease }, "\u2212"), h("span", { class: "qty-value" }, row.quantity), h("button", { class: "qty-btn", onClick: () => this.element.actions.changeQuantity(row.id, 1), disabled: !row.canIncrease, "aria-label": strings.increase }, "+"))));
|
|
724
287
|
}
|
|
725
288
|
render() {
|
|
726
|
-
|
|
289
|
+
const { status, state, strings, derived } = this.view;
|
|
290
|
+
if (status === 'loading') {
|
|
727
291
|
return this.renderSkeleton();
|
|
728
292
|
}
|
|
729
|
-
if (
|
|
293
|
+
if (status === 'error') {
|
|
730
294
|
return this.renderError();
|
|
731
295
|
}
|
|
732
|
-
|
|
733
|
-
const totalPrice = this.calculateTotal();
|
|
734
|
-
return (h("div", { class: "ticket-selector" }, h("div", { class: "ticket-list" }, this.ticketTypes.map(tt => this.renderTicketRow(tt))), this.addSuccess && (h("div", { class: "success-message" }, h("svg", { class: "success-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 13l4 4L19 7" })), "Added to cart!")), this.showButton && (h("button", { class: `add-to-cart-btn ${totalItems === 0 ? 'disabled' : ''}`, onClick: () => this.handleAddToCart(), disabled: totalItems === 0 || this.adding }, this.adding
|
|
735
|
-
? 'Adding...'
|
|
736
|
-
: totalItems === 0
|
|
737
|
-
? 'Select Tickets'
|
|
738
|
-
: `${this.buttonLabel} - ${this.formatPrice(totalPrice)}`))));
|
|
296
|
+
return (h("div", { class: "ticket-selector" }, h("div", { class: "ticket-list" }, derived.rows.map((row) => this.renderTicketRow(row))), derived.showTotals && (h("div", { class: "selection-totals" }, h("div", { class: "total-row" }, h("span", null, strings.ticketsRow), h("span", null, derived.subtotalLabel)), h("div", { class: "total-row fee" }, h("span", null, derived.totals.feeLabel), h("span", null, derived.feeLabel)), h("div", { class: "total-row total" }, h("span", null, strings.totalRow), h("span", null, derived.totalLabel)))), state.addSuccess && (h("div", { class: "success-message" }, h("svg", { class: "success-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 13l4 4L19 7" })), strings.added)), this.showButton && (h("button", { class: `add-to-cart-btn ${derived.totalItems === 0 ? 'disabled' : ''}`, onClick: () => this.element.actions.addToCart(), disabled: derived.actionDisabled }, derived.actionLabel))));
|
|
739
297
|
}
|
|
740
298
|
static get watchers() { return {
|
|
741
299
|
"eventId": [{
|
|
@@ -743,6 +301,12 @@ const TlTicketSelector = class {
|
|
|
743
301
|
}],
|
|
744
302
|
"occurrenceId": [{
|
|
745
303
|
"watchPropsChange": 0
|
|
304
|
+
}],
|
|
305
|
+
"showButton": [{
|
|
306
|
+
"syncProps": 0
|
|
307
|
+
}],
|
|
308
|
+
"buttonLabel": [{
|
|
309
|
+
"syncProps": 0
|
|
746
310
|
}]
|
|
747
311
|
}; }
|
|
748
312
|
};
|