@seatlayer/js 0.7.3 → 0.9.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/dist/index.cjs +474 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -3
- package/dist/index.d.ts +104 -3
- package/dist/index.js +477 -8
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -86,6 +86,14 @@ var PubApi = class {
|
|
|
86
86
|
body: { labels, holdId }
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
|
+
/** P4 "need more time?": push an active hold's expiry out. Throws ApiError 409
|
|
90
|
+
* (reason: expired | extend_limit | not_found | not_active) if it can't. */
|
|
91
|
+
extend(key, holdId, ttlMs) {
|
|
92
|
+
return request(this.base, `/pub/events/${encodeURIComponent(key)}/extend`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
body: { holdId, ...ttlMs ? { ttlMs } : {} }
|
|
95
|
+
});
|
|
96
|
+
}
|
|
89
97
|
socketUrl(key) {
|
|
90
98
|
const wsBase = this.base.replace(/^http/, "ws");
|
|
91
99
|
return `${wsBase}/pub/events/${encodeURIComponent(key)}/subscribe`;
|
|
@@ -407,6 +415,7 @@ var EmbeddedDesigner = class {
|
|
|
407
415
|
var import_core2 = require("@seatlayer/core");
|
|
408
416
|
var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
|
|
409
417
|
var DEFAULT_MAX_SELECTION2 = 10;
|
|
418
|
+
var EXTEND_PROMPT_MS = 6e4;
|
|
410
419
|
function resolveContainer3(container) {
|
|
411
420
|
if (typeof container === "string") {
|
|
412
421
|
const el = document.querySelector(container);
|
|
@@ -520,6 +529,30 @@ var CSS = `
|
|
|
520
529
|
.sl-boot-retry{margin-top:4px;padding:9px 20px;border-radius:var(--sl-r-sm);background:var(--sl-accent);
|
|
521
530
|
color:var(--sl-accent-ink);font-weight:700;font-size:13px}
|
|
522
531
|
|
|
532
|
+
/* "Need more time?" extend prompt (bottom-center over the map, above the toast) */
|
|
533
|
+
.sl-extend{position:absolute;left:50%;bottom:16px;transform:translateX(-50%) translateY(6px);z-index:9;
|
|
534
|
+
display:none;align-items:center;gap:12px;max-width:92%;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
535
|
+
color:var(--sl-text);border-radius:14px;padding:10px 12px 10px 16px;box-shadow:0 18px 50px -18px rgba(0,0,0,.6);
|
|
536
|
+
opacity:0;transition:opacity .2s,transform .2s}
|
|
537
|
+
.sl-extend.on{display:flex;opacity:1;transform:translateX(-50%) translateY(0)}
|
|
538
|
+
.sl-extend-txt{font-size:12.5px;font-weight:600;line-height:1.35}
|
|
539
|
+
.sl-extend-txt b{font-variant-numeric:tabular-nums}
|
|
540
|
+
.sl-extend-btn{flex:none;padding:8px 14px;border-radius:999px;font-weight:800;font-size:12.5px;
|
|
541
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);transition:filter .15s,opacity .15s}
|
|
542
|
+
.sl-extend-btn:hover{filter:brightness(1.08)}
|
|
543
|
+
.sl-extend-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
544
|
+
|
|
545
|
+
/* booked confirmation overlay (covers the widget once the held seats are sold) */
|
|
546
|
+
.sl-booked{position:absolute;inset:0;z-index:11;display:none;flex-direction:column;align-items:center;
|
|
547
|
+
justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg)}
|
|
548
|
+
.sl-booked.on{display:flex}
|
|
549
|
+
.sl-booked-badge{width:60px;height:60px;border-radius:999px;display:flex;align-items:center;justify-content:center;
|
|
550
|
+
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
551
|
+
.sl-booked-badge svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
552
|
+
.sl-booked-title{font-weight:800;font-size:19px;color:var(--sl-text)}
|
|
553
|
+
.sl-booked-sub{font-size:13px;color:var(--sl-muted);line-height:1.5;max-width:320px}
|
|
554
|
+
.sl-booked-seats{font-weight:700;color:var(--sl-text)}
|
|
555
|
+
|
|
523
556
|
/* a11y filter chips (over the map, top-left) */
|
|
524
557
|
.sl-chips{position:absolute;top:12px;left:12px;z-index:5;display:flex;gap:6px;flex-wrap:wrap;max-width:70%}
|
|
525
558
|
.sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
|
|
@@ -553,6 +586,79 @@ var CSS = `
|
|
|
553
586
|
/* screen-reader live region */
|
|
554
587
|
.sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
555
588
|
|
|
589
|
+
/* per-seat ticket-tier select + view-from-seat button in tray chips */
|
|
590
|
+
.sl-chip .tier{background:var(--sl-bg);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:7px;
|
|
591
|
+
font:inherit;font-size:11px;padding:3px 5px;max-width:130px;cursor:pointer}
|
|
592
|
+
.sl-chip .view{width:24px;height:24px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
593
|
+
color:var(--sl-muted);transition:color .15s}
|
|
594
|
+
.sl-chip .view:hover{color:var(--sl-text)}
|
|
595
|
+
.sl-chip .view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
596
|
+
|
|
597
|
+
/* arena: LOD rung pills (top-center over the map) */
|
|
598
|
+
.sl-rungs{position:absolute;top:12px;left:50%;transform:translateX(-50%);z-index:5;display:none;
|
|
599
|
+
background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
|
|
600
|
+
.sl-rungs.on{display:inline-flex;gap:2px}
|
|
601
|
+
.sl-rungs button{padding:6px 13px;border-radius:999px;font-size:10.5px;font-weight:800;letter-spacing:.07em;
|
|
602
|
+
color:var(--sl-muted);white-space:nowrap;transition:color .15s}
|
|
603
|
+
.sl-rungs button:hover{color:var(--sl-text)}
|
|
604
|
+
.sl-rungs button.on{background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
605
|
+
|
|
606
|
+
/* multi-floor switcher (center-left rail over the map) */
|
|
607
|
+
.sl-floors{position:absolute;top:50%;left:12px;transform:translateY(-50%);z-index:5;display:none;
|
|
608
|
+
flex-direction:column;gap:6px;max-width:42%}
|
|
609
|
+
.sl-floors.on{display:flex}
|
|
610
|
+
.sl-floors button{padding:7px 13px;border-radius:999px;font-size:12px;font-weight:700;background:var(--sl-surface);
|
|
611
|
+
border:1px solid var(--sl-line);color:var(--sl-muted);white-space:nowrap;max-width:100%;overflow:hidden;
|
|
612
|
+
text-overflow:ellipsis;transition:color .15s,border-color .15s}
|
|
613
|
+
.sl-floors button:hover{color:var(--sl-text)}
|
|
614
|
+
.sl-floors button.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
|
|
615
|
+
|
|
616
|
+
/* tapped-section summary card (top-center, under the rung pills) */
|
|
617
|
+
.sl-seccard{position:absolute;top:54px;left:50%;transform:translateX(-50%);z-index:6;width:250px;
|
|
618
|
+
max-width:calc(100% - 24px);background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:12px;
|
|
619
|
+
padding:12px 14px;box-shadow:0 18px 50px -18px rgba(0,0,0,.6);display:none}
|
|
620
|
+
.sl-seccard.on{display:block}
|
|
621
|
+
.sl-seccard-head{display:flex;align-items:center;gap:8px}
|
|
622
|
+
.sl-seccard-dot{width:10px;height:10px;border-radius:50%;flex:none}
|
|
623
|
+
.sl-seccard-name{font-weight:800;font-size:14px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
624
|
+
.sl-seccard-price{font-weight:800;font-size:12.5px;font-variant-numeric:tabular-nums}
|
|
625
|
+
.sl-seccard-x{width:22px;height:22px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
626
|
+
color:var(--sl-muted);font-size:12px}
|
|
627
|
+
.sl-seccard-x:hover{color:var(--sl-text)}
|
|
628
|
+
.sl-seccard-zone{font-size:11.5px;color:var(--sl-muted);margin-top:6px}
|
|
629
|
+
.sl-seccard-left{color:var(--sl-text);font-weight:700}
|
|
630
|
+
.sl-seccard-mix{display:flex;flex-wrap:wrap;gap:6px 10px;margin-top:8px}
|
|
631
|
+
.sl-seccard-mix-item{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--sl-muted)}
|
|
632
|
+
.sl-seccard-mix-dot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
633
|
+
.sl-seccard-mix-price{font-weight:700;color:var(--sl-text)}
|
|
634
|
+
.sl-seccard-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px}
|
|
635
|
+
.sl-seccard-overview{font-size:12px;font-weight:800;color:var(--sl-accent)}
|
|
636
|
+
.sl-seccard-hint{font-size:10.5px;color:var(--sl-muted)}
|
|
637
|
+
|
|
638
|
+
/* view-from-seat button on the confirm popover */
|
|
639
|
+
.sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
640
|
+
color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
|
|
641
|
+
.sl-confirm-view:hover{border-color:var(--sl-muted)}
|
|
642
|
+
.sl-confirm-view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
643
|
+
|
|
644
|
+
/* 360\xB0 seat-view modal (fills the widget; drag-to-look-around equirectangular) */
|
|
645
|
+
.sl-view{position:absolute;inset:0;z-index:12;display:flex;flex-direction:column;background:var(--sl-bg)}
|
|
646
|
+
.sl-view-head{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--sl-line);flex:none}
|
|
647
|
+
.sl-view-title{font-weight:800;font-size:15px}
|
|
648
|
+
.sl-view-cap{font-size:11px;color:var(--sl-muted)}
|
|
649
|
+
.sl-view-x{margin-left:auto;width:32px;height:32px;border-radius:999px;border:1px solid var(--sl-line);color:var(--sl-muted);
|
|
650
|
+
flex:none;display:flex;align-items:center;justify-content:center;transition:color .15s,border-color .15s}
|
|
651
|
+
.sl-view-x:hover{color:var(--sl-text);border-color:var(--sl-muted)}
|
|
652
|
+
.sl-view-x svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round}
|
|
653
|
+
.sl-view-pano{position:relative;flex:1;min-height:0;overflow:hidden;cursor:grab;background-color:#05070c;
|
|
654
|
+
background-repeat:repeat-x;touch-action:none;user-select:none}
|
|
655
|
+
.sl-view-pano.drag{cursor:grabbing}
|
|
656
|
+
.sl-view-badge{position:absolute;top:12px;left:12px;padding:5px 11px;border-radius:999px;font-size:10px;font-weight:800;
|
|
657
|
+
letter-spacing:.08em;background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted)}
|
|
658
|
+
.sl-view-hint{position:absolute;left:50%;bottom:12px;transform:translateX(-50%);padding:6px 14px;border-radius:999px;
|
|
659
|
+
font-size:11.5px;font-weight:600;background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted);
|
|
660
|
+
white-space:nowrap;pointer-events:none;max-width:90%;overflow:hidden;text-overflow:ellipsis}
|
|
661
|
+
|
|
556
662
|
/* modal host */
|
|
557
663
|
.sl-modal-scrim{position:fixed;inset:0;z-index:2147483000;background:rgba(5,7,12,.66);display:flex;align-items:center;justify-content:center;padding:18px}
|
|
558
664
|
.sl-modal-frame{width:min(1200px,100%);height:min(820px,100%);border-radius:16px;overflow:hidden;box-shadow:0 40px 120px -30px rgba(0,0,0,.8)}
|
|
@@ -594,6 +700,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
594
700
|
// state
|
|
595
701
|
this.currency = "USD";
|
|
596
702
|
this.hold = null;
|
|
703
|
+
/** Latest server expiry for the open hold (moves on extend). */
|
|
704
|
+
this.holdExpiresAt = 0;
|
|
705
|
+
/** True once we handed off to checkout — arms booked-confirmation detection. */
|
|
706
|
+
this.handedOff = false;
|
|
707
|
+
/** Guards single onBooked + single success overlay per hold. */
|
|
708
|
+
this.bookedShown = false;
|
|
709
|
+
this.extendEl = null;
|
|
710
|
+
this.bookedEl = null;
|
|
597
711
|
this.gaQty = /* @__PURE__ */ new Map();
|
|
598
712
|
this.tipEl = null;
|
|
599
713
|
this.tipPos = { x: 0, y: 0 };
|
|
@@ -603,6 +717,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
603
717
|
this.a11yFilter = "all";
|
|
604
718
|
this.baQty = 2;
|
|
605
719
|
this.baCat = "";
|
|
720
|
+
// arena / multi-floor / seat-view chrome
|
|
721
|
+
this.rungsEl = null;
|
|
722
|
+
this.floorsEl = null;
|
|
723
|
+
this.secCardEl = null;
|
|
724
|
+
this.viewEl = null;
|
|
725
|
+
this.viewCleanup = null;
|
|
726
|
+
this.allSeatsCache = null;
|
|
606
727
|
// modal plumbing (set by open())
|
|
607
728
|
this.modalScrim = null;
|
|
608
729
|
this.prevFocus = null;
|
|
@@ -625,9 +746,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
625
746
|
onStatusChange: () => {
|
|
626
747
|
this.syncPrices();
|
|
627
748
|
this.evictTakenSelections();
|
|
749
|
+
this.detectBooked();
|
|
628
750
|
},
|
|
629
751
|
onHoldExpired: () => {
|
|
630
752
|
this.hold = null;
|
|
753
|
+
this.handedOff = false;
|
|
754
|
+
this.bookedShown = false;
|
|
631
755
|
this.stopHoldTimer();
|
|
632
756
|
this.gaQty.clear();
|
|
633
757
|
this.toast((0, import_core2.t)("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.");
|
|
@@ -638,7 +762,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
638
762
|
onSelect: (seat) => {
|
|
639
763
|
if (this.opts.confirmSelection) this.showConfirm(seat);
|
|
640
764
|
},
|
|
641
|
-
onViewChange: () =>
|
|
765
|
+
onViewChange: () => {
|
|
766
|
+
this.reanchorConfirm();
|
|
767
|
+
this.syncRung();
|
|
768
|
+
},
|
|
769
|
+
// Tapped-section glide-in → surface (or clear) the section-summary card.
|
|
770
|
+
onSectionFocus: (summary) => this.showSectionCard(summary),
|
|
642
771
|
onFocusSeat: (seat) => this.announceSeat(seat),
|
|
643
772
|
onSeatHover: (d) => this.updateTooltip(d),
|
|
644
773
|
onHint: (m) => {
|
|
@@ -830,10 +959,130 @@ var SeatPicker = class _SeatPicker {
|
|
|
830
959
|
this.srEl.className = "sl-sr";
|
|
831
960
|
this.srEl.setAttribute("aria-live", "polite");
|
|
832
961
|
root.appendChild(this.srEl);
|
|
962
|
+
this.buildArenaChrome();
|
|
963
|
+
this.buildExtendPrompt();
|
|
964
|
+
this.buildBookedOverlay();
|
|
833
965
|
this.syncPrices();
|
|
834
966
|
this.syncTray();
|
|
835
967
|
return this;
|
|
836
968
|
}
|
|
969
|
+
/** The "Need more time?" prompt shown in the hold's final EXTEND_PROMPT_MS. */
|
|
970
|
+
buildExtendPrompt() {
|
|
971
|
+
const el = document.createElement("div");
|
|
972
|
+
el.className = "sl-extend";
|
|
973
|
+
el.setAttribute("role", "status");
|
|
974
|
+
el.innerHTML = `<span class="sl-extend-txt" data-ref="extendTxt"></span><button type="button" class="sl-extend-btn" data-ref="extendBtn"></button>`;
|
|
975
|
+
this.els.map.appendChild(el);
|
|
976
|
+
this.extendEl = el;
|
|
977
|
+
this.els.extendTxt = el.querySelector('[data-ref="extendTxt"]');
|
|
978
|
+
this.els.extendBtn = el.querySelector('[data-ref="extendBtn"]');
|
|
979
|
+
this.els.extendBtn.textContent = "Add time";
|
|
980
|
+
this.els.extendBtn.addEventListener("click", () => void this.handleExtend());
|
|
981
|
+
}
|
|
982
|
+
/** Success overlay + onBooked fire when the held seats settle to booked. */
|
|
983
|
+
buildBookedOverlay() {
|
|
984
|
+
const el = document.createElement("div");
|
|
985
|
+
el.className = "sl-booked";
|
|
986
|
+
el.setAttribute("role", "status");
|
|
987
|
+
el.setAttribute("aria-live", "polite");
|
|
988
|
+
el.innerHTML = `<div class="sl-booked-badge"><svg viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg></div><div class="sl-booked-title">You're all set</div><div class="sl-booked-sub" data-ref="bookedSub"></div>`;
|
|
989
|
+
this.root.appendChild(el);
|
|
990
|
+
this.bookedEl = el;
|
|
991
|
+
this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
|
|
992
|
+
}
|
|
993
|
+
// ---- arena / multi-floor chrome -------------------------------------------
|
|
994
|
+
/** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
|
|
995
|
+
buildArenaChrome() {
|
|
996
|
+
const doc = this.controller.doc;
|
|
997
|
+
if (!doc || !this.els.map) return;
|
|
998
|
+
const hasSections = doc.objects.some((o) => o.type === "section") || (doc.floors ?? []).some((f) => f.objects.some((o) => o.type === "section"));
|
|
999
|
+
if (hasSections) {
|
|
1000
|
+
const RUNGS = ["zones", "sections", "seats"];
|
|
1001
|
+
const pills = document.createElement("div");
|
|
1002
|
+
pills.className = "sl-rungs on";
|
|
1003
|
+
pills.setAttribute("role", "group");
|
|
1004
|
+
pills.setAttribute("aria-label", (0, import_core2.t)("picker.zoomLevel"));
|
|
1005
|
+
const LABEL = {
|
|
1006
|
+
zones: (0, import_core2.t)("picker.rungLabel.zones"),
|
|
1007
|
+
sections: (0, import_core2.t)("picker.rungLabel.sections"),
|
|
1008
|
+
seats: (0, import_core2.t)("picker.rungLabel.seats")
|
|
1009
|
+
};
|
|
1010
|
+
const TIP = {
|
|
1011
|
+
zones: (0, import_core2.t)("picker.rungTip.zones"),
|
|
1012
|
+
sections: (0, import_core2.t)("picker.rungTip.sections"),
|
|
1013
|
+
seats: (0, import_core2.t)("picker.rungTip.seats")
|
|
1014
|
+
};
|
|
1015
|
+
pills.innerHTML = RUNGS.map(
|
|
1016
|
+
(r) => `<button type="button" data-rung="${r}" title="${TIP[r]}" aria-pressed="false">${LABEL[r]}</button>`
|
|
1017
|
+
).join("");
|
|
1018
|
+
pills.querySelectorAll("button").forEach((btn) => {
|
|
1019
|
+
btn.addEventListener("click", () => this.controller.setRung(btn.dataset.rung));
|
|
1020
|
+
});
|
|
1021
|
+
this.els.map.appendChild(pills);
|
|
1022
|
+
this.rungsEl = pills;
|
|
1023
|
+
this.syncRung();
|
|
1024
|
+
}
|
|
1025
|
+
if (this.controller.isMultiFloor()) {
|
|
1026
|
+
const floors = this.controller.getFloors();
|
|
1027
|
+
const rail = document.createElement("div");
|
|
1028
|
+
rail.className = "sl-floors on";
|
|
1029
|
+
rail.setAttribute("role", "group");
|
|
1030
|
+
rail.setAttribute("aria-label", (0, import_core2.t)("picker.floor"));
|
|
1031
|
+
rail.innerHTML = floors.map((f) => `<button type="button" data-floor="${f.id}">${f.name}</button>`).join("");
|
|
1032
|
+
rail.querySelectorAll("button").forEach((btn) => {
|
|
1033
|
+
btn.addEventListener("click", () => {
|
|
1034
|
+
this.controller.setFloor(btn.dataset.floor);
|
|
1035
|
+
this.showSectionCard(null);
|
|
1036
|
+
this.syncFloors();
|
|
1037
|
+
this.syncRung();
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
this.els.map.appendChild(rail);
|
|
1041
|
+
this.floorsEl = rail;
|
|
1042
|
+
this.syncFloors();
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/** Reflect the engine's current LOD rung onto the pill group. */
|
|
1046
|
+
syncRung() {
|
|
1047
|
+
if (!this.rungsEl) return;
|
|
1048
|
+
const active = this.controller.getRung();
|
|
1049
|
+
this.rungsEl.querySelectorAll("button").forEach((btn) => {
|
|
1050
|
+
const on = btn.dataset.rung === active;
|
|
1051
|
+
btn.classList.toggle("on", on);
|
|
1052
|
+
btn.setAttribute("aria-pressed", String(on));
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
/** Reflect the active floor onto the switcher rail. */
|
|
1056
|
+
syncFloors() {
|
|
1057
|
+
if (!this.floorsEl) return;
|
|
1058
|
+
const active = this.controller.getActiveFloorId();
|
|
1059
|
+
this.floorsEl.querySelectorAll("button").forEach((btn) => {
|
|
1060
|
+
btn.classList.toggle("on", btn.dataset.floor === active);
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
/** Show (or clear, on null) the tapped-section summary card. */
|
|
1064
|
+
showSectionCard(summary) {
|
|
1065
|
+
if (!summary) {
|
|
1066
|
+
this.secCardEl?.remove();
|
|
1067
|
+
this.secCardEl = null;
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
if (!this.els.map) return;
|
|
1071
|
+
this.secCardEl?.remove();
|
|
1072
|
+
const priceLabel = summary.priceMin === summary.priceMax ? this.money(summary.priceMin) : `${this.money(summary.priceMin)}\u2013${this.money(summary.priceMax)}`;
|
|
1073
|
+
const card = document.createElement("div");
|
|
1074
|
+
card.className = "sl-seccard on";
|
|
1075
|
+
card.setAttribute("role", "dialog");
|
|
1076
|
+
card.setAttribute("aria-label", (0, import_core2.t)("picker.sectionSummaryAria", { label: summary.label }));
|
|
1077
|
+
const mix = summary.categories.map(
|
|
1078
|
+
(c) => `<span class="sl-seccard-mix-item"><span class="sl-seccard-mix-dot" style="background:${c.color}"></span>${c.label} <span class="sl-seccard-mix-price">${this.money(c.price)}</span></span>`
|
|
1079
|
+
).join("");
|
|
1080
|
+
card.innerHTML = `<div class="sl-seccard-head"><span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + `<button type="button" class="sl-seccard-x" aria-label="${(0, import_core2.t)("picker.closeSectionSummary")}">\u2715</button></div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${(0, import_core2.tCount)("picker.seatsLeftInSection", summary.seatsLeft)}</span></div>` + (mix ? `<div class="sl-seccard-mix">${mix}</div>` : "") + `<div class="sl-seccard-foot"><button type="button" class="sl-seccard-overview">\u2190 ${(0, import_core2.t)("picker.overview")}</button><span class="sl-seccard-hint">${(0, import_core2.t)("picker.tapSeatHint")}</span></div>`;
|
|
1081
|
+
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
1082
|
+
card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
|
|
1083
|
+
this.els.map.appendChild(card);
|
|
1084
|
+
this.secCardEl = card;
|
|
1085
|
+
}
|
|
837
1086
|
/** aria-live readout when keyboard focus lands on a seat. */
|
|
838
1087
|
announceSeat(seat) {
|
|
839
1088
|
if (!this.srEl) return;
|
|
@@ -855,11 +1104,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
855
1104
|
const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
|
|
856
1105
|
const el = document.createElement("div");
|
|
857
1106
|
el.className = "sl-confirm";
|
|
858
|
-
el.innerHTML = `<div class="sl-confirm-label">${seat.label}</div><div class="sl-confirm-meta"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span>${cat?.label ?? seat.categoryKey}${price != null ? `<b>${this.money(price)}</b>` : ""}</div><div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add">Add seat</button></div>`;
|
|
1107
|
+
el.innerHTML = `<div class="sl-confirm-label">${seat.label}</div><div class="sl-confirm-meta"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span>${cat?.label ?? seat.categoryKey}${price != null ? `<b>${this.money(price)}</b>` : ""}</div>` + (this.seatViewEnabled() ? `<button type="button" class="sl-confirm-view"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>${(0, import_core2.t)("picker.open360")}</button>` : "") + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add">Add seat</button></div>`;
|
|
859
1108
|
this.els.map.appendChild(el);
|
|
860
1109
|
this.confirmEl = el;
|
|
861
1110
|
this.confirmSeat = seat;
|
|
862
1111
|
this.reanchorConfirm();
|
|
1112
|
+
el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
|
|
863
1113
|
el.querySelector(".sl-confirm-add").addEventListener("click", () => this.closeConfirm());
|
|
864
1114
|
el.querySelector(".sl-confirm-cancel").addEventListener("click", () => {
|
|
865
1115
|
this.controller.deselect([seat.id]);
|
|
@@ -877,6 +1127,122 @@ var SeatPicker = class _SeatPicker {
|
|
|
877
1127
|
this.confirmEl = null;
|
|
878
1128
|
this.confirmSeat = null;
|
|
879
1129
|
}
|
|
1130
|
+
// ---- 360° view-from-seat modal --------------------------------------------
|
|
1131
|
+
seatViewEnabled() {
|
|
1132
|
+
return this.opts.seatView !== false;
|
|
1133
|
+
}
|
|
1134
|
+
/** Every bookable seat (cached) — neighbor heads for the generated panorama. */
|
|
1135
|
+
allSeats() {
|
|
1136
|
+
if (!this.allSeatsCache) {
|
|
1137
|
+
const doc = this.controller.doc;
|
|
1138
|
+
this.allSeatsCache = doc ? (0, import_core2.expandChart)(doc) : [];
|
|
1139
|
+
}
|
|
1140
|
+
return this.allSeatsCache;
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Open the drag-to-look-around 360° preview for a seat. Uses the organizer's
|
|
1144
|
+
* uploaded photo (seat.viewUrl) when present, else a panorama generated from
|
|
1145
|
+
* the chart geometry — the stage placed at this seat's true bearing + size.
|
|
1146
|
+
* Zero extra dependencies: an equirectangular image panned with `repeat-x`.
|
|
1147
|
+
*/
|
|
1148
|
+
openSeatView(seat) {
|
|
1149
|
+
if (!this.root || !this.seatViewEnabled()) return;
|
|
1150
|
+
this.closeSeatView();
|
|
1151
|
+
this.closeConfirm();
|
|
1152
|
+
const doc = this.controller.doc;
|
|
1153
|
+
const activeId = this.controller.getActiveFloorId();
|
|
1154
|
+
const focal = doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
|
|
1155
|
+
let panoUrl;
|
|
1156
|
+
let caption;
|
|
1157
|
+
let real = false;
|
|
1158
|
+
if (seat.viewUrl) {
|
|
1159
|
+
panoUrl = seat.viewUrl;
|
|
1160
|
+
caption = (0, import_core2.t)("picker.panorama360");
|
|
1161
|
+
real = true;
|
|
1162
|
+
} else {
|
|
1163
|
+
const pano2 = (0, import_core2.generateSeatPanorama)(seat, focal, this.allSeats());
|
|
1164
|
+
panoUrl = pano2.url;
|
|
1165
|
+
caption = (0, import_core2.t)("picker.illustrationCaption", { m: pano2.distanceM });
|
|
1166
|
+
}
|
|
1167
|
+
const el = document.createElement("div");
|
|
1168
|
+
el.className = "sl-view";
|
|
1169
|
+
el.setAttribute("role", "dialog");
|
|
1170
|
+
el.setAttribute("aria-label", (0, import_core2.t)("picker.viewFromSeat", { label: seat.label }));
|
|
1171
|
+
el.innerHTML = `<div class="sl-view-head"><span class="sl-view-title">${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}</span><span class="sl-view-cap">${caption}</span><button type="button" class="sl-view-x" aria-label="Close"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div><div class="sl-view-pano"><span class="sl-view-badge">${real ? (0, import_core2.t)("picker.real360") : (0, import_core2.t)("picker.preview")}</span><span class="sl-view-hint">Drag to look around \xB7 scroll to zoom</span></div>`;
|
|
1172
|
+
this.root.appendChild(el);
|
|
1173
|
+
this.viewEl = el;
|
|
1174
|
+
const pano = el.querySelector(".sl-view-pano");
|
|
1175
|
+
pano.style.backgroundImage = `url("${panoUrl}")`;
|
|
1176
|
+
let zoom = 1.2;
|
|
1177
|
+
let posX = 0;
|
|
1178
|
+
let posY = 0;
|
|
1179
|
+
const apply = () => {
|
|
1180
|
+
const h = pano.clientHeight || 1;
|
|
1181
|
+
const bgH = h * zoom;
|
|
1182
|
+
const overV = Math.max(0, bgH - h);
|
|
1183
|
+
posY = Math.min(overV / 2, Math.max(-overV / 2, posY));
|
|
1184
|
+
pano.style.backgroundSize = `auto ${bgH}px`;
|
|
1185
|
+
pano.style.backgroundPosition = `${posX}px ${posY + overV / 2}px`;
|
|
1186
|
+
};
|
|
1187
|
+
apply();
|
|
1188
|
+
let dragging = false;
|
|
1189
|
+
let lastX = 0;
|
|
1190
|
+
let lastY = 0;
|
|
1191
|
+
const onDown = (e) => {
|
|
1192
|
+
dragging = true;
|
|
1193
|
+
lastX = e.clientX;
|
|
1194
|
+
lastY = e.clientY;
|
|
1195
|
+
pano.classList.add("drag");
|
|
1196
|
+
pano.setPointerCapture?.(e.pointerId);
|
|
1197
|
+
};
|
|
1198
|
+
const onMove = (e) => {
|
|
1199
|
+
if (!dragging) return;
|
|
1200
|
+
posX += e.clientX - lastX;
|
|
1201
|
+
posY += e.clientY - lastY;
|
|
1202
|
+
lastX = e.clientX;
|
|
1203
|
+
lastY = e.clientY;
|
|
1204
|
+
apply();
|
|
1205
|
+
};
|
|
1206
|
+
const onUp = (e) => {
|
|
1207
|
+
dragging = false;
|
|
1208
|
+
pano.classList.remove("drag");
|
|
1209
|
+
pano.releasePointerCapture?.(e.pointerId);
|
|
1210
|
+
};
|
|
1211
|
+
const onWheel = (e) => {
|
|
1212
|
+
e.preventDefault();
|
|
1213
|
+
zoom = Math.min(2.4, Math.max(1, zoom + (e.deltaY < 0 ? 0.12 : -0.12)));
|
|
1214
|
+
apply();
|
|
1215
|
+
};
|
|
1216
|
+
pano.addEventListener("pointerdown", onDown);
|
|
1217
|
+
pano.addEventListener("pointermove", onMove);
|
|
1218
|
+
pano.addEventListener("pointerup", onUp);
|
|
1219
|
+
pano.addEventListener("pointercancel", onUp);
|
|
1220
|
+
pano.addEventListener("wheel", onWheel, { passive: false });
|
|
1221
|
+
const closeBtn = el.querySelector(".sl-view-x");
|
|
1222
|
+
closeBtn.addEventListener("click", () => this.closeSeatView());
|
|
1223
|
+
const onKey = (e) => {
|
|
1224
|
+
if (e.key === "Escape") {
|
|
1225
|
+
e.stopPropagation();
|
|
1226
|
+
this.closeSeatView();
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
el.addEventListener("keydown", onKey);
|
|
1230
|
+
closeBtn.focus();
|
|
1231
|
+
this.viewCleanup = () => {
|
|
1232
|
+
pano.removeEventListener("pointerdown", onDown);
|
|
1233
|
+
pano.removeEventListener("pointermove", onMove);
|
|
1234
|
+
pano.removeEventListener("pointerup", onUp);
|
|
1235
|
+
pano.removeEventListener("pointercancel", onUp);
|
|
1236
|
+
pano.removeEventListener("wheel", onWheel);
|
|
1237
|
+
el.removeEventListener("keydown", onKey);
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
closeSeatView() {
|
|
1241
|
+
this.viewCleanup?.();
|
|
1242
|
+
this.viewCleanup = null;
|
|
1243
|
+
this.viewEl?.remove();
|
|
1244
|
+
this.viewEl = null;
|
|
1245
|
+
}
|
|
880
1246
|
// ---- chrome sync ----------------------------------------------------------
|
|
881
1247
|
money(n) {
|
|
882
1248
|
try {
|
|
@@ -919,15 +1285,20 @@ var SeatPicker = class _SeatPicker {
|
|
|
919
1285
|
}
|
|
920
1286
|
for (const item of heldItems) {
|
|
921
1287
|
const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
|
|
1288
|
+
const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
|
|
1289
|
+
const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
|
|
922
1290
|
parts.push(
|
|
923
|
-
`<div class="sl-chip"><b>${item.label}</b><span class="cat">${cat?.label ?? item.categoryKey}</span><span class="amt">${this.money(item.unitPrice * (item.quantity ?? 1))}</span></div>`
|
|
1291
|
+
`<div class="sl-chip"><b>${item.label}</b><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span><span class="amt">${this.money(item.unitPrice * (item.quantity ?? 1))}</span>` + (canView2 ? `<button type="button" class="view" data-view-label="${item.label}" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: item.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "") + `</div>`
|
|
924
1292
|
);
|
|
925
1293
|
}
|
|
926
1294
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
1295
|
+
const canView = this.seatViewEnabled();
|
|
927
1296
|
for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
|
|
928
1297
|
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
1298
|
+
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${(0, import_core2.t)("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(ti.price)}</option>`).join("") + `</select>` : "";
|
|
1299
|
+
const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: s.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "";
|
|
929
1300
|
parts.push(
|
|
930
|
-
`<div class="sl-chip" data-seat="${s.id}"><b>${s.label}</b><span class="cat">${cat?.label ?? s.categoryKey}</span
|
|
1301
|
+
`<div class="sl-chip" data-seat="${s.id}"><b>${s.label}</b><span class="cat">${cat?.label ?? s.categoryKey}</span>` + tierSelect + `<span class="amt">${this.money(s.price)}</span>` + viewBtn + `<button type="button" class="rm" aria-label="Remove ${s.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
|
|
931
1302
|
);
|
|
932
1303
|
}
|
|
933
1304
|
for (const area of gaAreas) {
|
|
@@ -961,6 +1332,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
961
1332
|
this.controller.deselect([id]);
|
|
962
1333
|
});
|
|
963
1334
|
});
|
|
1335
|
+
this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
|
|
1336
|
+
sel.addEventListener("change", () => this.controller.setSeatTier(sel.dataset.tier, sel.value || null));
|
|
1337
|
+
});
|
|
1338
|
+
this.els.tray.querySelectorAll(".sl-chip .view[data-view-label]").forEach((btn) => {
|
|
1339
|
+
btn.addEventListener("click", () => {
|
|
1340
|
+
const seat = this.controller.seatByLabel(btn.dataset.viewLabel);
|
|
1341
|
+
if (seat) this.openSeatView(seat);
|
|
1342
|
+
});
|
|
1343
|
+
});
|
|
964
1344
|
this.els.tray.querySelectorAll(".sl-ga button").forEach((btn) => {
|
|
965
1345
|
btn.addEventListener("click", () => {
|
|
966
1346
|
const areaEl = btn.closest(".sl-ga");
|
|
@@ -988,7 +1368,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
988
1368
|
async handleCta() {
|
|
989
1369
|
const cta = this.els.cta;
|
|
990
1370
|
if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
|
|
991
|
-
|
|
1371
|
+
const seats = this.hold.seats ?? this.controller.getSelection();
|
|
1372
|
+
this.handedOff = true;
|
|
1373
|
+
this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
|
|
992
1374
|
return;
|
|
993
1375
|
}
|
|
994
1376
|
cta.disabled = true;
|
|
@@ -1011,8 +1393,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1011
1393
|
return;
|
|
1012
1394
|
}
|
|
1013
1395
|
this.hold = hold;
|
|
1396
|
+
this.handedOff = true;
|
|
1014
1397
|
this.startHoldTimer(hold.expiresAt);
|
|
1015
|
-
this.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? []);
|
|
1398
|
+
this.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? [], this.buildHandoff(hold));
|
|
1016
1399
|
} catch (err) {
|
|
1017
1400
|
this.opts.onError?.(err);
|
|
1018
1401
|
this.toast("One or more seats were just taken. Please pick again.");
|
|
@@ -1022,13 +1405,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
1022
1405
|
}
|
|
1023
1406
|
startHoldTimer(expiresAt) {
|
|
1024
1407
|
this.stopHoldTimer();
|
|
1408
|
+
this.holdExpiresAt = expiresAt;
|
|
1025
1409
|
const pill = this.els.hold;
|
|
1026
1410
|
const tick = () => {
|
|
1027
|
-
const ms = Math.max(0,
|
|
1411
|
+
const ms = Math.max(0, this.holdExpiresAt - Date.now());
|
|
1028
1412
|
const m = Math.floor(ms / 6e4);
|
|
1029
1413
|
const s = String(Math.floor(ms % 6e4 / 1e3)).padStart(2, "0");
|
|
1030
1414
|
pill.textContent = `Held ${m}:${s}`;
|
|
1031
1415
|
pill.classList.add("on");
|
|
1416
|
+
this.setExtendPrompt(ms > 0 && ms <= EXTEND_PROMPT_MS, ms);
|
|
1032
1417
|
if (ms <= 0) this.stopHoldTimer();
|
|
1033
1418
|
};
|
|
1034
1419
|
tick();
|
|
@@ -1038,6 +1423,83 @@ var SeatPicker = class _SeatPicker {
|
|
|
1038
1423
|
if (this.holdTimer) clearInterval(this.holdTimer);
|
|
1039
1424
|
this.holdTimer = null;
|
|
1040
1425
|
this.els.hold?.classList.remove("on");
|
|
1426
|
+
this.setExtendPrompt(false, 0);
|
|
1427
|
+
}
|
|
1428
|
+
/** Show/refresh (or hide) the "Need more time?" prompt with the live seconds left. */
|
|
1429
|
+
setExtendPrompt(show, ms) {
|
|
1430
|
+
if (!this.extendEl) return;
|
|
1431
|
+
if (show && this.controller.currentHold() && !this.bookedShown) {
|
|
1432
|
+
const secs = Math.ceil(ms / 1e3);
|
|
1433
|
+
this.els.extendTxt.innerHTML = `Your seats are held for <b>0:${String(secs).padStart(2, "0")}</b>. Need more time?`;
|
|
1434
|
+
this.extendEl.classList.add("on");
|
|
1435
|
+
} else {
|
|
1436
|
+
this.extendEl.classList.remove("on");
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
async handleExtend() {
|
|
1440
|
+
const btn = this.els.extendBtn;
|
|
1441
|
+
btn.disabled = true;
|
|
1442
|
+
const prev = btn.textContent;
|
|
1443
|
+
btn.textContent = "Adding\u2026";
|
|
1444
|
+
try {
|
|
1445
|
+
const h = await this.controller.extendHold(this.opts.holdTtlMs);
|
|
1446
|
+
if (h) {
|
|
1447
|
+
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
1448
|
+
this.holdExpiresAt = h.expiresAt;
|
|
1449
|
+
this.extendEl?.classList.remove("on");
|
|
1450
|
+
this.toast("More time added \u2014 your seats are still held.");
|
|
1451
|
+
} else {
|
|
1452
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
1453
|
+
}
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
this.opts.onError?.(err);
|
|
1456
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
1457
|
+
} finally {
|
|
1458
|
+
btn.disabled = false;
|
|
1459
|
+
btn.textContent = prev;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Fire the booked-confirmation state once the buyer's held seats settle to
|
|
1464
|
+
* booked. The controller clears its own hold the moment every held label reads
|
|
1465
|
+
* 'booked' over the realtime channel (clearBookedHoldIfSettled), and this runs
|
|
1466
|
+
* on the same onStatusChange — so `currentHold() === null` while we still hold
|
|
1467
|
+
* a checkout handoff means "sold", not expired (expiry clears via onHoldExpired
|
|
1468
|
+
* on a different path, which nulls this.hold first).
|
|
1469
|
+
*/
|
|
1470
|
+
detectBooked() {
|
|
1471
|
+
if (this.bookedShown || !this.handedOff || !this.hold) return;
|
|
1472
|
+
if (this.controller.currentHold() !== null) return;
|
|
1473
|
+
this.showBooked();
|
|
1474
|
+
}
|
|
1475
|
+
showBooked() {
|
|
1476
|
+
if (this.bookedShown || !this.hold) return;
|
|
1477
|
+
this.bookedShown = true;
|
|
1478
|
+
const handoff = this.buildHandoff(this.hold);
|
|
1479
|
+
this.stopHoldTimer();
|
|
1480
|
+
const n = handoff.lineItems.reduce((sum, i) => sum + i.quantity, 0);
|
|
1481
|
+
if (this.els.bookedSub) {
|
|
1482
|
+
this.els.bookedSub.innerHTML = `<span class="sl-booked-seats">${n} ${n === 1 ? "ticket" : "tickets"}</span> confirmed. A confirmation is on its way.`;
|
|
1483
|
+
}
|
|
1484
|
+
this.bookedEl?.classList.add("on");
|
|
1485
|
+
this.opts.onBooked?.(handoff);
|
|
1486
|
+
}
|
|
1487
|
+
/** Assemble the stable {@link CheckoutHandoff} from a hold's server line items. */
|
|
1488
|
+
buildHandoff(hold) {
|
|
1489
|
+
const items = hold.items ?? [];
|
|
1490
|
+
const lineItems = items.map((it) => ({
|
|
1491
|
+
label: it.label,
|
|
1492
|
+
objectId: it.objectId,
|
|
1493
|
+
objectType: it.objectType,
|
|
1494
|
+
categoryKey: it.categoryKey,
|
|
1495
|
+
tierId: it.tierId,
|
|
1496
|
+
unitPrice: it.unitPrice,
|
|
1497
|
+
currency: it.currency ?? this.currency,
|
|
1498
|
+
quantity: it.quantity ?? 1
|
|
1499
|
+
}));
|
|
1500
|
+
const currency = lineItems[0]?.currency ?? this.currency;
|
|
1501
|
+
const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
|
|
1502
|
+
return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
|
|
1041
1503
|
}
|
|
1042
1504
|
toast(msg) {
|
|
1043
1505
|
const el = this.els.toast;
|
|
@@ -1079,6 +1541,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1079
1541
|
const h = await this.controller.bestAvailable(qty, categoryKey);
|
|
1080
1542
|
if (h) {
|
|
1081
1543
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
1544
|
+
this.handedOff = false;
|
|
1545
|
+
this.bookedShown = false;
|
|
1082
1546
|
this.startHoldTimer(h.expiresAt);
|
|
1083
1547
|
this.syncTray();
|
|
1084
1548
|
return this.hold;
|
|
@@ -1092,6 +1556,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1092
1556
|
async release() {
|
|
1093
1557
|
await this.controller.release();
|
|
1094
1558
|
this.hold = null;
|
|
1559
|
+
this.handedOff = false;
|
|
1560
|
+
this.bookedShown = false;
|
|
1095
1561
|
this.stopHoldTimer();
|
|
1096
1562
|
this.gaQty.clear();
|
|
1097
1563
|
this.syncTray();
|
|
@@ -1099,6 +1565,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1099
1565
|
destroy() {
|
|
1100
1566
|
this.destroyed = true;
|
|
1101
1567
|
this.closeConfirm();
|
|
1568
|
+
this.closeSeatView();
|
|
1102
1569
|
this.stopHoldTimer();
|
|
1103
1570
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
1104
1571
|
this.ro?.disconnect();
|