@seatlayer/js 0.8.0 → 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 +159 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -3
- package/dist/index.d.ts +73 -3
- package/dist/index.js +159 -3
- 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;
|
|
@@ -667,6 +700,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
667
700
|
// state
|
|
668
701
|
this.currency = "USD";
|
|
669
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;
|
|
670
711
|
this.gaQty = /* @__PURE__ */ new Map();
|
|
671
712
|
this.tipEl = null;
|
|
672
713
|
this.tipPos = { x: 0, y: 0 };
|
|
@@ -705,9 +746,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
705
746
|
onStatusChange: () => {
|
|
706
747
|
this.syncPrices();
|
|
707
748
|
this.evictTakenSelections();
|
|
749
|
+
this.detectBooked();
|
|
708
750
|
},
|
|
709
751
|
onHoldExpired: () => {
|
|
710
752
|
this.hold = null;
|
|
753
|
+
this.handedOff = false;
|
|
754
|
+
this.bookedShown = false;
|
|
711
755
|
this.stopHoldTimer();
|
|
712
756
|
this.gaQty.clear();
|
|
713
757
|
this.toast((0, import_core2.t)("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.");
|
|
@@ -916,10 +960,36 @@ var SeatPicker = class _SeatPicker {
|
|
|
916
960
|
this.srEl.setAttribute("aria-live", "polite");
|
|
917
961
|
root.appendChild(this.srEl);
|
|
918
962
|
this.buildArenaChrome();
|
|
963
|
+
this.buildExtendPrompt();
|
|
964
|
+
this.buildBookedOverlay();
|
|
919
965
|
this.syncPrices();
|
|
920
966
|
this.syncTray();
|
|
921
967
|
return this;
|
|
922
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
|
+
}
|
|
923
993
|
// ---- arena / multi-floor chrome -------------------------------------------
|
|
924
994
|
/** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
|
|
925
995
|
buildArenaChrome() {
|
|
@@ -1298,7 +1368,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1298
1368
|
async handleCta() {
|
|
1299
1369
|
const cta = this.els.cta;
|
|
1300
1370
|
if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
|
|
1301
|
-
|
|
1371
|
+
const seats = this.hold.seats ?? this.controller.getSelection();
|
|
1372
|
+
this.handedOff = true;
|
|
1373
|
+
this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
|
|
1302
1374
|
return;
|
|
1303
1375
|
}
|
|
1304
1376
|
cta.disabled = true;
|
|
@@ -1321,8 +1393,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1321
1393
|
return;
|
|
1322
1394
|
}
|
|
1323
1395
|
this.hold = hold;
|
|
1396
|
+
this.handedOff = true;
|
|
1324
1397
|
this.startHoldTimer(hold.expiresAt);
|
|
1325
|
-
this.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? []);
|
|
1398
|
+
this.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? [], this.buildHandoff(hold));
|
|
1326
1399
|
} catch (err) {
|
|
1327
1400
|
this.opts.onError?.(err);
|
|
1328
1401
|
this.toast("One or more seats were just taken. Please pick again.");
|
|
@@ -1332,13 +1405,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
1332
1405
|
}
|
|
1333
1406
|
startHoldTimer(expiresAt) {
|
|
1334
1407
|
this.stopHoldTimer();
|
|
1408
|
+
this.holdExpiresAt = expiresAt;
|
|
1335
1409
|
const pill = this.els.hold;
|
|
1336
1410
|
const tick = () => {
|
|
1337
|
-
const ms = Math.max(0,
|
|
1411
|
+
const ms = Math.max(0, this.holdExpiresAt - Date.now());
|
|
1338
1412
|
const m = Math.floor(ms / 6e4);
|
|
1339
1413
|
const s = String(Math.floor(ms % 6e4 / 1e3)).padStart(2, "0");
|
|
1340
1414
|
pill.textContent = `Held ${m}:${s}`;
|
|
1341
1415
|
pill.classList.add("on");
|
|
1416
|
+
this.setExtendPrompt(ms > 0 && ms <= EXTEND_PROMPT_MS, ms);
|
|
1342
1417
|
if (ms <= 0) this.stopHoldTimer();
|
|
1343
1418
|
};
|
|
1344
1419
|
tick();
|
|
@@ -1348,6 +1423,83 @@ var SeatPicker = class _SeatPicker {
|
|
|
1348
1423
|
if (this.holdTimer) clearInterval(this.holdTimer);
|
|
1349
1424
|
this.holdTimer = null;
|
|
1350
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 };
|
|
1351
1503
|
}
|
|
1352
1504
|
toast(msg) {
|
|
1353
1505
|
const el = this.els.toast;
|
|
@@ -1389,6 +1541,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1389
1541
|
const h = await this.controller.bestAvailable(qty, categoryKey);
|
|
1390
1542
|
if (h) {
|
|
1391
1543
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
1544
|
+
this.handedOff = false;
|
|
1545
|
+
this.bookedShown = false;
|
|
1392
1546
|
this.startHoldTimer(h.expiresAt);
|
|
1393
1547
|
this.syncTray();
|
|
1394
1548
|
return this.hold;
|
|
@@ -1402,6 +1556,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1402
1556
|
async release() {
|
|
1403
1557
|
await this.controller.release();
|
|
1404
1558
|
this.hold = null;
|
|
1559
|
+
this.handedOff = false;
|
|
1560
|
+
this.bookedShown = false;
|
|
1405
1561
|
this.stopHoldTimer();
|
|
1406
1562
|
this.gaQty.clear();
|
|
1407
1563
|
this.syncTray();
|