@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.js
CHANGED
|
@@ -57,6 +57,14 @@ var PubApi = class {
|
|
|
57
57
|
body: { labels, holdId }
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
|
+
/** P4 "need more time?": push an active hold's expiry out. Throws ApiError 409
|
|
61
|
+
* (reason: expired | extend_limit | not_found | not_active) if it can't. */
|
|
62
|
+
extend(key, holdId, ttlMs) {
|
|
63
|
+
return request(this.base, `/pub/events/${encodeURIComponent(key)}/extend`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
body: { holdId, ...ttlMs ? { ttlMs } : {} }
|
|
66
|
+
});
|
|
67
|
+
}
|
|
60
68
|
socketUrl(key) {
|
|
61
69
|
const wsBase = this.base.replace(/^http/, "ws");
|
|
62
70
|
return `${wsBase}/pub/events/${encodeURIComponent(key)}/subscribe`;
|
|
@@ -378,12 +386,15 @@ var EmbeddedDesigner = class {
|
|
|
378
386
|
import {
|
|
379
387
|
PickerController as PickerController2,
|
|
380
388
|
expandChart,
|
|
389
|
+
generateSeatPanorama,
|
|
381
390
|
loadLocale as loadLocale2,
|
|
382
391
|
setStringOverrides as setStringOverrides2,
|
|
383
|
-
t as t2
|
|
392
|
+
t as t2,
|
|
393
|
+
tCount
|
|
384
394
|
} from "@seatlayer/core";
|
|
385
395
|
var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
|
|
386
396
|
var DEFAULT_MAX_SELECTION2 = 10;
|
|
397
|
+
var EXTEND_PROMPT_MS = 6e4;
|
|
387
398
|
function resolveContainer3(container) {
|
|
388
399
|
if (typeof container === "string") {
|
|
389
400
|
const el = document.querySelector(container);
|
|
@@ -497,6 +508,30 @@ var CSS = `
|
|
|
497
508
|
.sl-boot-retry{margin-top:4px;padding:9px 20px;border-radius:var(--sl-r-sm);background:var(--sl-accent);
|
|
498
509
|
color:var(--sl-accent-ink);font-weight:700;font-size:13px}
|
|
499
510
|
|
|
511
|
+
/* "Need more time?" extend prompt (bottom-center over the map, above the toast) */
|
|
512
|
+
.sl-extend{position:absolute;left:50%;bottom:16px;transform:translateX(-50%) translateY(6px);z-index:9;
|
|
513
|
+
display:none;align-items:center;gap:12px;max-width:92%;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
514
|
+
color:var(--sl-text);border-radius:14px;padding:10px 12px 10px 16px;box-shadow:0 18px 50px -18px rgba(0,0,0,.6);
|
|
515
|
+
opacity:0;transition:opacity .2s,transform .2s}
|
|
516
|
+
.sl-extend.on{display:flex;opacity:1;transform:translateX(-50%) translateY(0)}
|
|
517
|
+
.sl-extend-txt{font-size:12.5px;font-weight:600;line-height:1.35}
|
|
518
|
+
.sl-extend-txt b{font-variant-numeric:tabular-nums}
|
|
519
|
+
.sl-extend-btn{flex:none;padding:8px 14px;border-radius:999px;font-weight:800;font-size:12.5px;
|
|
520
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);transition:filter .15s,opacity .15s}
|
|
521
|
+
.sl-extend-btn:hover{filter:brightness(1.08)}
|
|
522
|
+
.sl-extend-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
523
|
+
|
|
524
|
+
/* booked confirmation overlay (covers the widget once the held seats are sold) */
|
|
525
|
+
.sl-booked{position:absolute;inset:0;z-index:11;display:none;flex-direction:column;align-items:center;
|
|
526
|
+
justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg)}
|
|
527
|
+
.sl-booked.on{display:flex}
|
|
528
|
+
.sl-booked-badge{width:60px;height:60px;border-radius:999px;display:flex;align-items:center;justify-content:center;
|
|
529
|
+
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
530
|
+
.sl-booked-badge svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
531
|
+
.sl-booked-title{font-weight:800;font-size:19px;color:var(--sl-text)}
|
|
532
|
+
.sl-booked-sub{font-size:13px;color:var(--sl-muted);line-height:1.5;max-width:320px}
|
|
533
|
+
.sl-booked-seats{font-weight:700;color:var(--sl-text)}
|
|
534
|
+
|
|
500
535
|
/* a11y filter chips (over the map, top-left) */
|
|
501
536
|
.sl-chips{position:absolute;top:12px;left:12px;z-index:5;display:flex;gap:6px;flex-wrap:wrap;max-width:70%}
|
|
502
537
|
.sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
|
|
@@ -530,6 +565,79 @@ var CSS = `
|
|
|
530
565
|
/* screen-reader live region */
|
|
531
566
|
.sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
532
567
|
|
|
568
|
+
/* per-seat ticket-tier select + view-from-seat button in tray chips */
|
|
569
|
+
.sl-chip .tier{background:var(--sl-bg);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:7px;
|
|
570
|
+
font:inherit;font-size:11px;padding:3px 5px;max-width:130px;cursor:pointer}
|
|
571
|
+
.sl-chip .view{width:24px;height:24px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
572
|
+
color:var(--sl-muted);transition:color .15s}
|
|
573
|
+
.sl-chip .view:hover{color:var(--sl-text)}
|
|
574
|
+
.sl-chip .view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
575
|
+
|
|
576
|
+
/* arena: LOD rung pills (top-center over the map) */
|
|
577
|
+
.sl-rungs{position:absolute;top:12px;left:50%;transform:translateX(-50%);z-index:5;display:none;
|
|
578
|
+
background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
|
|
579
|
+
.sl-rungs.on{display:inline-flex;gap:2px}
|
|
580
|
+
.sl-rungs button{padding:6px 13px;border-radius:999px;font-size:10.5px;font-weight:800;letter-spacing:.07em;
|
|
581
|
+
color:var(--sl-muted);white-space:nowrap;transition:color .15s}
|
|
582
|
+
.sl-rungs button:hover{color:var(--sl-text)}
|
|
583
|
+
.sl-rungs button.on{background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
584
|
+
|
|
585
|
+
/* multi-floor switcher (center-left rail over the map) */
|
|
586
|
+
.sl-floors{position:absolute;top:50%;left:12px;transform:translateY(-50%);z-index:5;display:none;
|
|
587
|
+
flex-direction:column;gap:6px;max-width:42%}
|
|
588
|
+
.sl-floors.on{display:flex}
|
|
589
|
+
.sl-floors button{padding:7px 13px;border-radius:999px;font-size:12px;font-weight:700;background:var(--sl-surface);
|
|
590
|
+
border:1px solid var(--sl-line);color:var(--sl-muted);white-space:nowrap;max-width:100%;overflow:hidden;
|
|
591
|
+
text-overflow:ellipsis;transition:color .15s,border-color .15s}
|
|
592
|
+
.sl-floors button:hover{color:var(--sl-text)}
|
|
593
|
+
.sl-floors button.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
|
|
594
|
+
|
|
595
|
+
/* tapped-section summary card (top-center, under the rung pills) */
|
|
596
|
+
.sl-seccard{position:absolute;top:54px;left:50%;transform:translateX(-50%);z-index:6;width:250px;
|
|
597
|
+
max-width:calc(100% - 24px);background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:12px;
|
|
598
|
+
padding:12px 14px;box-shadow:0 18px 50px -18px rgba(0,0,0,.6);display:none}
|
|
599
|
+
.sl-seccard.on{display:block}
|
|
600
|
+
.sl-seccard-head{display:flex;align-items:center;gap:8px}
|
|
601
|
+
.sl-seccard-dot{width:10px;height:10px;border-radius:50%;flex:none}
|
|
602
|
+
.sl-seccard-name{font-weight:800;font-size:14px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
603
|
+
.sl-seccard-price{font-weight:800;font-size:12.5px;font-variant-numeric:tabular-nums}
|
|
604
|
+
.sl-seccard-x{width:22px;height:22px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
605
|
+
color:var(--sl-muted);font-size:12px}
|
|
606
|
+
.sl-seccard-x:hover{color:var(--sl-text)}
|
|
607
|
+
.sl-seccard-zone{font-size:11.5px;color:var(--sl-muted);margin-top:6px}
|
|
608
|
+
.sl-seccard-left{color:var(--sl-text);font-weight:700}
|
|
609
|
+
.sl-seccard-mix{display:flex;flex-wrap:wrap;gap:6px 10px;margin-top:8px}
|
|
610
|
+
.sl-seccard-mix-item{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--sl-muted)}
|
|
611
|
+
.sl-seccard-mix-dot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
612
|
+
.sl-seccard-mix-price{font-weight:700;color:var(--sl-text)}
|
|
613
|
+
.sl-seccard-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px}
|
|
614
|
+
.sl-seccard-overview{font-size:12px;font-weight:800;color:var(--sl-accent)}
|
|
615
|
+
.sl-seccard-hint{font-size:10.5px;color:var(--sl-muted)}
|
|
616
|
+
|
|
617
|
+
/* view-from-seat button on the confirm popover */
|
|
618
|
+
.sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
619
|
+
color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
|
|
620
|
+
.sl-confirm-view:hover{border-color:var(--sl-muted)}
|
|
621
|
+
.sl-confirm-view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
622
|
+
|
|
623
|
+
/* 360\xB0 seat-view modal (fills the widget; drag-to-look-around equirectangular) */
|
|
624
|
+
.sl-view{position:absolute;inset:0;z-index:12;display:flex;flex-direction:column;background:var(--sl-bg)}
|
|
625
|
+
.sl-view-head{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--sl-line);flex:none}
|
|
626
|
+
.sl-view-title{font-weight:800;font-size:15px}
|
|
627
|
+
.sl-view-cap{font-size:11px;color:var(--sl-muted)}
|
|
628
|
+
.sl-view-x{margin-left:auto;width:32px;height:32px;border-radius:999px;border:1px solid var(--sl-line);color:var(--sl-muted);
|
|
629
|
+
flex:none;display:flex;align-items:center;justify-content:center;transition:color .15s,border-color .15s}
|
|
630
|
+
.sl-view-x:hover{color:var(--sl-text);border-color:var(--sl-muted)}
|
|
631
|
+
.sl-view-x svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round}
|
|
632
|
+
.sl-view-pano{position:relative;flex:1;min-height:0;overflow:hidden;cursor:grab;background-color:#05070c;
|
|
633
|
+
background-repeat:repeat-x;touch-action:none;user-select:none}
|
|
634
|
+
.sl-view-pano.drag{cursor:grabbing}
|
|
635
|
+
.sl-view-badge{position:absolute;top:12px;left:12px;padding:5px 11px;border-radius:999px;font-size:10px;font-weight:800;
|
|
636
|
+
letter-spacing:.08em;background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted)}
|
|
637
|
+
.sl-view-hint{position:absolute;left:50%;bottom:12px;transform:translateX(-50%);padding:6px 14px;border-radius:999px;
|
|
638
|
+
font-size:11.5px;font-weight:600;background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted);
|
|
639
|
+
white-space:nowrap;pointer-events:none;max-width:90%;overflow:hidden;text-overflow:ellipsis}
|
|
640
|
+
|
|
533
641
|
/* modal host */
|
|
534
642
|
.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}
|
|
535
643
|
.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)}
|
|
@@ -571,6 +679,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
571
679
|
// state
|
|
572
680
|
this.currency = "USD";
|
|
573
681
|
this.hold = null;
|
|
682
|
+
/** Latest server expiry for the open hold (moves on extend). */
|
|
683
|
+
this.holdExpiresAt = 0;
|
|
684
|
+
/** True once we handed off to checkout — arms booked-confirmation detection. */
|
|
685
|
+
this.handedOff = false;
|
|
686
|
+
/** Guards single onBooked + single success overlay per hold. */
|
|
687
|
+
this.bookedShown = false;
|
|
688
|
+
this.extendEl = null;
|
|
689
|
+
this.bookedEl = null;
|
|
574
690
|
this.gaQty = /* @__PURE__ */ new Map();
|
|
575
691
|
this.tipEl = null;
|
|
576
692
|
this.tipPos = { x: 0, y: 0 };
|
|
@@ -580,6 +696,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
580
696
|
this.a11yFilter = "all";
|
|
581
697
|
this.baQty = 2;
|
|
582
698
|
this.baCat = "";
|
|
699
|
+
// arena / multi-floor / seat-view chrome
|
|
700
|
+
this.rungsEl = null;
|
|
701
|
+
this.floorsEl = null;
|
|
702
|
+
this.secCardEl = null;
|
|
703
|
+
this.viewEl = null;
|
|
704
|
+
this.viewCleanup = null;
|
|
705
|
+
this.allSeatsCache = null;
|
|
583
706
|
// modal plumbing (set by open())
|
|
584
707
|
this.modalScrim = null;
|
|
585
708
|
this.prevFocus = null;
|
|
@@ -602,9 +725,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
602
725
|
onStatusChange: () => {
|
|
603
726
|
this.syncPrices();
|
|
604
727
|
this.evictTakenSelections();
|
|
728
|
+
this.detectBooked();
|
|
605
729
|
},
|
|
606
730
|
onHoldExpired: () => {
|
|
607
731
|
this.hold = null;
|
|
732
|
+
this.handedOff = false;
|
|
733
|
+
this.bookedShown = false;
|
|
608
734
|
this.stopHoldTimer();
|
|
609
735
|
this.gaQty.clear();
|
|
610
736
|
this.toast(t2("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.");
|
|
@@ -615,7 +741,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
615
741
|
onSelect: (seat) => {
|
|
616
742
|
if (this.opts.confirmSelection) this.showConfirm(seat);
|
|
617
743
|
},
|
|
618
|
-
onViewChange: () =>
|
|
744
|
+
onViewChange: () => {
|
|
745
|
+
this.reanchorConfirm();
|
|
746
|
+
this.syncRung();
|
|
747
|
+
},
|
|
748
|
+
// Tapped-section glide-in → surface (or clear) the section-summary card.
|
|
749
|
+
onSectionFocus: (summary) => this.showSectionCard(summary),
|
|
619
750
|
onFocusSeat: (seat) => this.announceSeat(seat),
|
|
620
751
|
onSeatHover: (d) => this.updateTooltip(d),
|
|
621
752
|
onHint: (m) => {
|
|
@@ -807,10 +938,130 @@ var SeatPicker = class _SeatPicker {
|
|
|
807
938
|
this.srEl.className = "sl-sr";
|
|
808
939
|
this.srEl.setAttribute("aria-live", "polite");
|
|
809
940
|
root.appendChild(this.srEl);
|
|
941
|
+
this.buildArenaChrome();
|
|
942
|
+
this.buildExtendPrompt();
|
|
943
|
+
this.buildBookedOverlay();
|
|
810
944
|
this.syncPrices();
|
|
811
945
|
this.syncTray();
|
|
812
946
|
return this;
|
|
813
947
|
}
|
|
948
|
+
/** The "Need more time?" prompt shown in the hold's final EXTEND_PROMPT_MS. */
|
|
949
|
+
buildExtendPrompt() {
|
|
950
|
+
const el = document.createElement("div");
|
|
951
|
+
el.className = "sl-extend";
|
|
952
|
+
el.setAttribute("role", "status");
|
|
953
|
+
el.innerHTML = `<span class="sl-extend-txt" data-ref="extendTxt"></span><button type="button" class="sl-extend-btn" data-ref="extendBtn"></button>`;
|
|
954
|
+
this.els.map.appendChild(el);
|
|
955
|
+
this.extendEl = el;
|
|
956
|
+
this.els.extendTxt = el.querySelector('[data-ref="extendTxt"]');
|
|
957
|
+
this.els.extendBtn = el.querySelector('[data-ref="extendBtn"]');
|
|
958
|
+
this.els.extendBtn.textContent = "Add time";
|
|
959
|
+
this.els.extendBtn.addEventListener("click", () => void this.handleExtend());
|
|
960
|
+
}
|
|
961
|
+
/** Success overlay + onBooked fire when the held seats settle to booked. */
|
|
962
|
+
buildBookedOverlay() {
|
|
963
|
+
const el = document.createElement("div");
|
|
964
|
+
el.className = "sl-booked";
|
|
965
|
+
el.setAttribute("role", "status");
|
|
966
|
+
el.setAttribute("aria-live", "polite");
|
|
967
|
+
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>`;
|
|
968
|
+
this.root.appendChild(el);
|
|
969
|
+
this.bookedEl = el;
|
|
970
|
+
this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
|
|
971
|
+
}
|
|
972
|
+
// ---- arena / multi-floor chrome -------------------------------------------
|
|
973
|
+
/** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
|
|
974
|
+
buildArenaChrome() {
|
|
975
|
+
const doc = this.controller.doc;
|
|
976
|
+
if (!doc || !this.els.map) return;
|
|
977
|
+
const hasSections = doc.objects.some((o) => o.type === "section") || (doc.floors ?? []).some((f) => f.objects.some((o) => o.type === "section"));
|
|
978
|
+
if (hasSections) {
|
|
979
|
+
const RUNGS = ["zones", "sections", "seats"];
|
|
980
|
+
const pills = document.createElement("div");
|
|
981
|
+
pills.className = "sl-rungs on";
|
|
982
|
+
pills.setAttribute("role", "group");
|
|
983
|
+
pills.setAttribute("aria-label", t2("picker.zoomLevel"));
|
|
984
|
+
const LABEL = {
|
|
985
|
+
zones: t2("picker.rungLabel.zones"),
|
|
986
|
+
sections: t2("picker.rungLabel.sections"),
|
|
987
|
+
seats: t2("picker.rungLabel.seats")
|
|
988
|
+
};
|
|
989
|
+
const TIP = {
|
|
990
|
+
zones: t2("picker.rungTip.zones"),
|
|
991
|
+
sections: t2("picker.rungTip.sections"),
|
|
992
|
+
seats: t2("picker.rungTip.seats")
|
|
993
|
+
};
|
|
994
|
+
pills.innerHTML = RUNGS.map(
|
|
995
|
+
(r) => `<button type="button" data-rung="${r}" title="${TIP[r]}" aria-pressed="false">${LABEL[r]}</button>`
|
|
996
|
+
).join("");
|
|
997
|
+
pills.querySelectorAll("button").forEach((btn) => {
|
|
998
|
+
btn.addEventListener("click", () => this.controller.setRung(btn.dataset.rung));
|
|
999
|
+
});
|
|
1000
|
+
this.els.map.appendChild(pills);
|
|
1001
|
+
this.rungsEl = pills;
|
|
1002
|
+
this.syncRung();
|
|
1003
|
+
}
|
|
1004
|
+
if (this.controller.isMultiFloor()) {
|
|
1005
|
+
const floors = this.controller.getFloors();
|
|
1006
|
+
const rail = document.createElement("div");
|
|
1007
|
+
rail.className = "sl-floors on";
|
|
1008
|
+
rail.setAttribute("role", "group");
|
|
1009
|
+
rail.setAttribute("aria-label", t2("picker.floor"));
|
|
1010
|
+
rail.innerHTML = floors.map((f) => `<button type="button" data-floor="${f.id}">${f.name}</button>`).join("");
|
|
1011
|
+
rail.querySelectorAll("button").forEach((btn) => {
|
|
1012
|
+
btn.addEventListener("click", () => {
|
|
1013
|
+
this.controller.setFloor(btn.dataset.floor);
|
|
1014
|
+
this.showSectionCard(null);
|
|
1015
|
+
this.syncFloors();
|
|
1016
|
+
this.syncRung();
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
this.els.map.appendChild(rail);
|
|
1020
|
+
this.floorsEl = rail;
|
|
1021
|
+
this.syncFloors();
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
/** Reflect the engine's current LOD rung onto the pill group. */
|
|
1025
|
+
syncRung() {
|
|
1026
|
+
if (!this.rungsEl) return;
|
|
1027
|
+
const active = this.controller.getRung();
|
|
1028
|
+
this.rungsEl.querySelectorAll("button").forEach((btn) => {
|
|
1029
|
+
const on = btn.dataset.rung === active;
|
|
1030
|
+
btn.classList.toggle("on", on);
|
|
1031
|
+
btn.setAttribute("aria-pressed", String(on));
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
/** Reflect the active floor onto the switcher rail. */
|
|
1035
|
+
syncFloors() {
|
|
1036
|
+
if (!this.floorsEl) return;
|
|
1037
|
+
const active = this.controller.getActiveFloorId();
|
|
1038
|
+
this.floorsEl.querySelectorAll("button").forEach((btn) => {
|
|
1039
|
+
btn.classList.toggle("on", btn.dataset.floor === active);
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
/** Show (or clear, on null) the tapped-section summary card. */
|
|
1043
|
+
showSectionCard(summary) {
|
|
1044
|
+
if (!summary) {
|
|
1045
|
+
this.secCardEl?.remove();
|
|
1046
|
+
this.secCardEl = null;
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
if (!this.els.map) return;
|
|
1050
|
+
this.secCardEl?.remove();
|
|
1051
|
+
const priceLabel = summary.priceMin === summary.priceMax ? this.money(summary.priceMin) : `${this.money(summary.priceMin)}\u2013${this.money(summary.priceMax)}`;
|
|
1052
|
+
const card = document.createElement("div");
|
|
1053
|
+
card.className = "sl-seccard on";
|
|
1054
|
+
card.setAttribute("role", "dialog");
|
|
1055
|
+
card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
|
|
1056
|
+
const mix = summary.categories.map(
|
|
1057
|
+
(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>`
|
|
1058
|
+
).join("");
|
|
1059
|
+
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="${t2("picker.closeSectionSummary")}">\u2715</button></div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${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 ${t2("picker.overview")}</button><span class="sl-seccard-hint">${t2("picker.tapSeatHint")}</span></div>`;
|
|
1060
|
+
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
1061
|
+
card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
|
|
1062
|
+
this.els.map.appendChild(card);
|
|
1063
|
+
this.secCardEl = card;
|
|
1064
|
+
}
|
|
814
1065
|
/** aria-live readout when keyboard focus lands on a seat. */
|
|
815
1066
|
announceSeat(seat) {
|
|
816
1067
|
if (!this.srEl) return;
|
|
@@ -832,11 +1083,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
832
1083
|
const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
|
|
833
1084
|
const el = document.createElement("div");
|
|
834
1085
|
el.className = "sl-confirm";
|
|
835
|
-
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>`;
|
|
1086
|
+
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>${t2("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>`;
|
|
836
1087
|
this.els.map.appendChild(el);
|
|
837
1088
|
this.confirmEl = el;
|
|
838
1089
|
this.confirmSeat = seat;
|
|
839
1090
|
this.reanchorConfirm();
|
|
1091
|
+
el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
|
|
840
1092
|
el.querySelector(".sl-confirm-add").addEventListener("click", () => this.closeConfirm());
|
|
841
1093
|
el.querySelector(".sl-confirm-cancel").addEventListener("click", () => {
|
|
842
1094
|
this.controller.deselect([seat.id]);
|
|
@@ -854,6 +1106,122 @@ var SeatPicker = class _SeatPicker {
|
|
|
854
1106
|
this.confirmEl = null;
|
|
855
1107
|
this.confirmSeat = null;
|
|
856
1108
|
}
|
|
1109
|
+
// ---- 360° view-from-seat modal --------------------------------------------
|
|
1110
|
+
seatViewEnabled() {
|
|
1111
|
+
return this.opts.seatView !== false;
|
|
1112
|
+
}
|
|
1113
|
+
/** Every bookable seat (cached) — neighbor heads for the generated panorama. */
|
|
1114
|
+
allSeats() {
|
|
1115
|
+
if (!this.allSeatsCache) {
|
|
1116
|
+
const doc = this.controller.doc;
|
|
1117
|
+
this.allSeatsCache = doc ? expandChart(doc) : [];
|
|
1118
|
+
}
|
|
1119
|
+
return this.allSeatsCache;
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Open the drag-to-look-around 360° preview for a seat. Uses the organizer's
|
|
1123
|
+
* uploaded photo (seat.viewUrl) when present, else a panorama generated from
|
|
1124
|
+
* the chart geometry — the stage placed at this seat's true bearing + size.
|
|
1125
|
+
* Zero extra dependencies: an equirectangular image panned with `repeat-x`.
|
|
1126
|
+
*/
|
|
1127
|
+
openSeatView(seat) {
|
|
1128
|
+
if (!this.root || !this.seatViewEnabled()) return;
|
|
1129
|
+
this.closeSeatView();
|
|
1130
|
+
this.closeConfirm();
|
|
1131
|
+
const doc = this.controller.doc;
|
|
1132
|
+
const activeId = this.controller.getActiveFloorId();
|
|
1133
|
+
const focal = doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
|
|
1134
|
+
let panoUrl;
|
|
1135
|
+
let caption;
|
|
1136
|
+
let real = false;
|
|
1137
|
+
if (seat.viewUrl) {
|
|
1138
|
+
panoUrl = seat.viewUrl;
|
|
1139
|
+
caption = t2("picker.panorama360");
|
|
1140
|
+
real = true;
|
|
1141
|
+
} else {
|
|
1142
|
+
const pano2 = generateSeatPanorama(seat, focal, this.allSeats());
|
|
1143
|
+
panoUrl = pano2.url;
|
|
1144
|
+
caption = t2("picker.illustrationCaption", { m: pano2.distanceM });
|
|
1145
|
+
}
|
|
1146
|
+
const el = document.createElement("div");
|
|
1147
|
+
el.className = "sl-view";
|
|
1148
|
+
el.setAttribute("role", "dialog");
|
|
1149
|
+
el.setAttribute("aria-label", t2("picker.viewFromSeat", { label: seat.label }));
|
|
1150
|
+
el.innerHTML = `<div class="sl-view-head"><span class="sl-view-title">${t2("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 ? t2("picker.real360") : t2("picker.preview")}</span><span class="sl-view-hint">Drag to look around \xB7 scroll to zoom</span></div>`;
|
|
1151
|
+
this.root.appendChild(el);
|
|
1152
|
+
this.viewEl = el;
|
|
1153
|
+
const pano = el.querySelector(".sl-view-pano");
|
|
1154
|
+
pano.style.backgroundImage = `url("${panoUrl}")`;
|
|
1155
|
+
let zoom = 1.2;
|
|
1156
|
+
let posX = 0;
|
|
1157
|
+
let posY = 0;
|
|
1158
|
+
const apply = () => {
|
|
1159
|
+
const h = pano.clientHeight || 1;
|
|
1160
|
+
const bgH = h * zoom;
|
|
1161
|
+
const overV = Math.max(0, bgH - h);
|
|
1162
|
+
posY = Math.min(overV / 2, Math.max(-overV / 2, posY));
|
|
1163
|
+
pano.style.backgroundSize = `auto ${bgH}px`;
|
|
1164
|
+
pano.style.backgroundPosition = `${posX}px ${posY + overV / 2}px`;
|
|
1165
|
+
};
|
|
1166
|
+
apply();
|
|
1167
|
+
let dragging = false;
|
|
1168
|
+
let lastX = 0;
|
|
1169
|
+
let lastY = 0;
|
|
1170
|
+
const onDown = (e) => {
|
|
1171
|
+
dragging = true;
|
|
1172
|
+
lastX = e.clientX;
|
|
1173
|
+
lastY = e.clientY;
|
|
1174
|
+
pano.classList.add("drag");
|
|
1175
|
+
pano.setPointerCapture?.(e.pointerId);
|
|
1176
|
+
};
|
|
1177
|
+
const onMove = (e) => {
|
|
1178
|
+
if (!dragging) return;
|
|
1179
|
+
posX += e.clientX - lastX;
|
|
1180
|
+
posY += e.clientY - lastY;
|
|
1181
|
+
lastX = e.clientX;
|
|
1182
|
+
lastY = e.clientY;
|
|
1183
|
+
apply();
|
|
1184
|
+
};
|
|
1185
|
+
const onUp = (e) => {
|
|
1186
|
+
dragging = false;
|
|
1187
|
+
pano.classList.remove("drag");
|
|
1188
|
+
pano.releasePointerCapture?.(e.pointerId);
|
|
1189
|
+
};
|
|
1190
|
+
const onWheel = (e) => {
|
|
1191
|
+
e.preventDefault();
|
|
1192
|
+
zoom = Math.min(2.4, Math.max(1, zoom + (e.deltaY < 0 ? 0.12 : -0.12)));
|
|
1193
|
+
apply();
|
|
1194
|
+
};
|
|
1195
|
+
pano.addEventListener("pointerdown", onDown);
|
|
1196
|
+
pano.addEventListener("pointermove", onMove);
|
|
1197
|
+
pano.addEventListener("pointerup", onUp);
|
|
1198
|
+
pano.addEventListener("pointercancel", onUp);
|
|
1199
|
+
pano.addEventListener("wheel", onWheel, { passive: false });
|
|
1200
|
+
const closeBtn = el.querySelector(".sl-view-x");
|
|
1201
|
+
closeBtn.addEventListener("click", () => this.closeSeatView());
|
|
1202
|
+
const onKey = (e) => {
|
|
1203
|
+
if (e.key === "Escape") {
|
|
1204
|
+
e.stopPropagation();
|
|
1205
|
+
this.closeSeatView();
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
el.addEventListener("keydown", onKey);
|
|
1209
|
+
closeBtn.focus();
|
|
1210
|
+
this.viewCleanup = () => {
|
|
1211
|
+
pano.removeEventListener("pointerdown", onDown);
|
|
1212
|
+
pano.removeEventListener("pointermove", onMove);
|
|
1213
|
+
pano.removeEventListener("pointerup", onUp);
|
|
1214
|
+
pano.removeEventListener("pointercancel", onUp);
|
|
1215
|
+
pano.removeEventListener("wheel", onWheel);
|
|
1216
|
+
el.removeEventListener("keydown", onKey);
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
closeSeatView() {
|
|
1220
|
+
this.viewCleanup?.();
|
|
1221
|
+
this.viewCleanup = null;
|
|
1222
|
+
this.viewEl?.remove();
|
|
1223
|
+
this.viewEl = null;
|
|
1224
|
+
}
|
|
857
1225
|
// ---- chrome sync ----------------------------------------------------------
|
|
858
1226
|
money(n) {
|
|
859
1227
|
try {
|
|
@@ -896,15 +1264,20 @@ var SeatPicker = class _SeatPicker {
|
|
|
896
1264
|
}
|
|
897
1265
|
for (const item of heldItems) {
|
|
898
1266
|
const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
|
|
1267
|
+
const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
|
|
1268
|
+
const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
|
|
899
1269
|
parts.push(
|
|
900
|
-
`<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>`
|
|
1270
|
+
`<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="${t2("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>`
|
|
901
1271
|
);
|
|
902
1272
|
}
|
|
903
1273
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
1274
|
+
const canView = this.seatViewEnabled();
|
|
904
1275
|
for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
|
|
905
1276
|
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
1277
|
+
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("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>` : "";
|
|
1278
|
+
const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${t2("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>` : "";
|
|
906
1279
|
parts.push(
|
|
907
|
-
`<div class="sl-chip" data-seat="${s.id}"><b>${s.label}</b><span class="cat">${cat?.label ?? s.categoryKey}</span
|
|
1280
|
+
`<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>`
|
|
908
1281
|
);
|
|
909
1282
|
}
|
|
910
1283
|
for (const area of gaAreas) {
|
|
@@ -938,6 +1311,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
938
1311
|
this.controller.deselect([id]);
|
|
939
1312
|
});
|
|
940
1313
|
});
|
|
1314
|
+
this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
|
|
1315
|
+
sel.addEventListener("change", () => this.controller.setSeatTier(sel.dataset.tier, sel.value || null));
|
|
1316
|
+
});
|
|
1317
|
+
this.els.tray.querySelectorAll(".sl-chip .view[data-view-label]").forEach((btn) => {
|
|
1318
|
+
btn.addEventListener("click", () => {
|
|
1319
|
+
const seat = this.controller.seatByLabel(btn.dataset.viewLabel);
|
|
1320
|
+
if (seat) this.openSeatView(seat);
|
|
1321
|
+
});
|
|
1322
|
+
});
|
|
941
1323
|
this.els.tray.querySelectorAll(".sl-ga button").forEach((btn) => {
|
|
942
1324
|
btn.addEventListener("click", () => {
|
|
943
1325
|
const areaEl = btn.closest(".sl-ga");
|
|
@@ -965,7 +1347,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
965
1347
|
async handleCta() {
|
|
966
1348
|
const cta = this.els.cta;
|
|
967
1349
|
if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
|
|
968
|
-
|
|
1350
|
+
const seats = this.hold.seats ?? this.controller.getSelection();
|
|
1351
|
+
this.handedOff = true;
|
|
1352
|
+
this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
|
|
969
1353
|
return;
|
|
970
1354
|
}
|
|
971
1355
|
cta.disabled = true;
|
|
@@ -988,8 +1372,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
988
1372
|
return;
|
|
989
1373
|
}
|
|
990
1374
|
this.hold = hold;
|
|
1375
|
+
this.handedOff = true;
|
|
991
1376
|
this.startHoldTimer(hold.expiresAt);
|
|
992
|
-
this.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? []);
|
|
1377
|
+
this.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? [], this.buildHandoff(hold));
|
|
993
1378
|
} catch (err) {
|
|
994
1379
|
this.opts.onError?.(err);
|
|
995
1380
|
this.toast("One or more seats were just taken. Please pick again.");
|
|
@@ -999,13 +1384,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
999
1384
|
}
|
|
1000
1385
|
startHoldTimer(expiresAt) {
|
|
1001
1386
|
this.stopHoldTimer();
|
|
1387
|
+
this.holdExpiresAt = expiresAt;
|
|
1002
1388
|
const pill = this.els.hold;
|
|
1003
1389
|
const tick = () => {
|
|
1004
|
-
const ms = Math.max(0,
|
|
1390
|
+
const ms = Math.max(0, this.holdExpiresAt - Date.now());
|
|
1005
1391
|
const m = Math.floor(ms / 6e4);
|
|
1006
1392
|
const s = String(Math.floor(ms % 6e4 / 1e3)).padStart(2, "0");
|
|
1007
1393
|
pill.textContent = `Held ${m}:${s}`;
|
|
1008
1394
|
pill.classList.add("on");
|
|
1395
|
+
this.setExtendPrompt(ms > 0 && ms <= EXTEND_PROMPT_MS, ms);
|
|
1009
1396
|
if (ms <= 0) this.stopHoldTimer();
|
|
1010
1397
|
};
|
|
1011
1398
|
tick();
|
|
@@ -1015,6 +1402,83 @@ var SeatPicker = class _SeatPicker {
|
|
|
1015
1402
|
if (this.holdTimer) clearInterval(this.holdTimer);
|
|
1016
1403
|
this.holdTimer = null;
|
|
1017
1404
|
this.els.hold?.classList.remove("on");
|
|
1405
|
+
this.setExtendPrompt(false, 0);
|
|
1406
|
+
}
|
|
1407
|
+
/** Show/refresh (or hide) the "Need more time?" prompt with the live seconds left. */
|
|
1408
|
+
setExtendPrompt(show, ms) {
|
|
1409
|
+
if (!this.extendEl) return;
|
|
1410
|
+
if (show && this.controller.currentHold() && !this.bookedShown) {
|
|
1411
|
+
const secs = Math.ceil(ms / 1e3);
|
|
1412
|
+
this.els.extendTxt.innerHTML = `Your seats are held for <b>0:${String(secs).padStart(2, "0")}</b>. Need more time?`;
|
|
1413
|
+
this.extendEl.classList.add("on");
|
|
1414
|
+
} else {
|
|
1415
|
+
this.extendEl.classList.remove("on");
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
async handleExtend() {
|
|
1419
|
+
const btn = this.els.extendBtn;
|
|
1420
|
+
btn.disabled = true;
|
|
1421
|
+
const prev = btn.textContent;
|
|
1422
|
+
btn.textContent = "Adding\u2026";
|
|
1423
|
+
try {
|
|
1424
|
+
const h = await this.controller.extendHold(this.opts.holdTtlMs);
|
|
1425
|
+
if (h) {
|
|
1426
|
+
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
1427
|
+
this.holdExpiresAt = h.expiresAt;
|
|
1428
|
+
this.extendEl?.classList.remove("on");
|
|
1429
|
+
this.toast("More time added \u2014 your seats are still held.");
|
|
1430
|
+
} else {
|
|
1431
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
1432
|
+
}
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
this.opts.onError?.(err);
|
|
1435
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
1436
|
+
} finally {
|
|
1437
|
+
btn.disabled = false;
|
|
1438
|
+
btn.textContent = prev;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Fire the booked-confirmation state once the buyer's held seats settle to
|
|
1443
|
+
* booked. The controller clears its own hold the moment every held label reads
|
|
1444
|
+
* 'booked' over the realtime channel (clearBookedHoldIfSettled), and this runs
|
|
1445
|
+
* on the same onStatusChange — so `currentHold() === null` while we still hold
|
|
1446
|
+
* a checkout handoff means "sold", not expired (expiry clears via onHoldExpired
|
|
1447
|
+
* on a different path, which nulls this.hold first).
|
|
1448
|
+
*/
|
|
1449
|
+
detectBooked() {
|
|
1450
|
+
if (this.bookedShown || !this.handedOff || !this.hold) return;
|
|
1451
|
+
if (this.controller.currentHold() !== null) return;
|
|
1452
|
+
this.showBooked();
|
|
1453
|
+
}
|
|
1454
|
+
showBooked() {
|
|
1455
|
+
if (this.bookedShown || !this.hold) return;
|
|
1456
|
+
this.bookedShown = true;
|
|
1457
|
+
const handoff = this.buildHandoff(this.hold);
|
|
1458
|
+
this.stopHoldTimer();
|
|
1459
|
+
const n = handoff.lineItems.reduce((sum, i) => sum + i.quantity, 0);
|
|
1460
|
+
if (this.els.bookedSub) {
|
|
1461
|
+
this.els.bookedSub.innerHTML = `<span class="sl-booked-seats">${n} ${n === 1 ? "ticket" : "tickets"}</span> confirmed. A confirmation is on its way.`;
|
|
1462
|
+
}
|
|
1463
|
+
this.bookedEl?.classList.add("on");
|
|
1464
|
+
this.opts.onBooked?.(handoff);
|
|
1465
|
+
}
|
|
1466
|
+
/** Assemble the stable {@link CheckoutHandoff} from a hold's server line items. */
|
|
1467
|
+
buildHandoff(hold) {
|
|
1468
|
+
const items = hold.items ?? [];
|
|
1469
|
+
const lineItems = items.map((it) => ({
|
|
1470
|
+
label: it.label,
|
|
1471
|
+
objectId: it.objectId,
|
|
1472
|
+
objectType: it.objectType,
|
|
1473
|
+
categoryKey: it.categoryKey,
|
|
1474
|
+
tierId: it.tierId,
|
|
1475
|
+
unitPrice: it.unitPrice,
|
|
1476
|
+
currency: it.currency ?? this.currency,
|
|
1477
|
+
quantity: it.quantity ?? 1
|
|
1478
|
+
}));
|
|
1479
|
+
const currency = lineItems[0]?.currency ?? this.currency;
|
|
1480
|
+
const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
|
|
1481
|
+
return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
|
|
1018
1482
|
}
|
|
1019
1483
|
toast(msg) {
|
|
1020
1484
|
const el = this.els.toast;
|
|
@@ -1056,6 +1520,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1056
1520
|
const h = await this.controller.bestAvailable(qty, categoryKey);
|
|
1057
1521
|
if (h) {
|
|
1058
1522
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
1523
|
+
this.handedOff = false;
|
|
1524
|
+
this.bookedShown = false;
|
|
1059
1525
|
this.startHoldTimer(h.expiresAt);
|
|
1060
1526
|
this.syncTray();
|
|
1061
1527
|
return this.hold;
|
|
@@ -1069,6 +1535,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1069
1535
|
async release() {
|
|
1070
1536
|
await this.controller.release();
|
|
1071
1537
|
this.hold = null;
|
|
1538
|
+
this.handedOff = false;
|
|
1539
|
+
this.bookedShown = false;
|
|
1072
1540
|
this.stopHoldTimer();
|
|
1073
1541
|
this.gaQty.clear();
|
|
1074
1542
|
this.syncTray();
|
|
@@ -1076,6 +1544,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1076
1544
|
destroy() {
|
|
1077
1545
|
this.destroyed = true;
|
|
1078
1546
|
this.closeConfirm();
|
|
1547
|
+
this.closeSeatView();
|
|
1079
1548
|
this.stopHoldTimer();
|
|
1080
1549
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
1081
1550
|
this.ro?.disconnect();
|