@seatlayer/js 0.47.2 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/dist/{channelsMode-33ZLORVF.js → channelsMode-NCH5GYHN.js} +3 -3
- package/dist/{chunk-X2R4AQSZ.js → chunk-H4OJF6LE.js} +176 -7
- package/dist/chunk-H4OJF6LE.js.map +1 -0
- package/dist/{chunk-DMEFXZIL.js → chunk-KNMZQZXR.js} +2 -2
- package/dist/{chunk-CF5MS7UR.js → chunk-Q5QT3YLA.js} +186 -59
- package/dist/chunk-Q5QT3YLA.js.map +1 -0
- package/dist/index.cjs +555 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -4
- package/dist/index.d.ts +17 -4
- package/dist/index.js +200 -32
- package/dist/index.js.map +1 -1
- package/dist/{manager-CAIPkUYl.d.cts → manager-DEjbKiqJ.d.cts} +200 -17
- package/dist/{manager-CAIPkUYl.d.ts → manager-DEjbKiqJ.d.ts} +200 -17
- package/dist/manager.cjs +358 -62
- package/dist/manager.cjs.map +1 -1
- package/dist/manager.d.cts +1 -1
- package/dist/manager.d.ts +1 -1
- package/dist/manager.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-CF5MS7UR.js.map +0 -1
- package/dist/chunk-X2R4AQSZ.js.map +0 -1
- /package/dist/{channelsMode-33ZLORVF.js.map → channelsMode-NCH5GYHN.js.map} +0 -0
- /package/dist/{chunk-DMEFXZIL.js.map → chunk-KNMZQZXR.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -431,6 +431,99 @@ async function parse(res) {
|
|
|
431
431
|
}
|
|
432
432
|
return data;
|
|
433
433
|
}
|
|
434
|
+
function record(value) {
|
|
435
|
+
return value && typeof value === "object" ? value : {};
|
|
436
|
+
}
|
|
437
|
+
function finite(primary, legacy, fallback = 0) {
|
|
438
|
+
if (typeof primary === "number" && Number.isFinite(primary)) return primary;
|
|
439
|
+
if (typeof legacy === "number" && Number.isFinite(legacy)) return legacy;
|
|
440
|
+
return fallback;
|
|
441
|
+
}
|
|
442
|
+
function nullableFinite(primary, legacy) {
|
|
443
|
+
if (primary === null) return null;
|
|
444
|
+
if (typeof primary === "number" && Number.isFinite(primary)) return primary;
|
|
445
|
+
if (primary !== void 0) return null;
|
|
446
|
+
if (typeof legacy === "number" && Number.isFinite(legacy)) return legacy;
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
function normalizeSection(value) {
|
|
450
|
+
const row = record(value);
|
|
451
|
+
const bookedValue = finite(row.bookedValue, row.bookedRevenue);
|
|
452
|
+
return { ...row, bookedValue, bookedRevenue: bookedValue };
|
|
453
|
+
}
|
|
454
|
+
function normalizeReportResult(value) {
|
|
455
|
+
const source = record(value);
|
|
456
|
+
const report = record(source.report);
|
|
457
|
+
const byCategory = Array.isArray(report.byCategory) ? report.byCategory.map((value2) => {
|
|
458
|
+
const row = record(value2);
|
|
459
|
+
const bookedValue = finite(row.bookedValue, row.bookedRevenue);
|
|
460
|
+
return { ...row, bookedValue, bookedRevenue: bookedValue };
|
|
461
|
+
}) : [];
|
|
462
|
+
const bySection = Array.isArray(report.bySection) ? report.bySection.map(normalizeSection) : void 0;
|
|
463
|
+
return {
|
|
464
|
+
...source,
|
|
465
|
+
report: { ...report, byCategory, ...bySection ? { bySection } : {} }
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function normalizeControlRoomSnapshot(value) {
|
|
469
|
+
const source = record(value);
|
|
470
|
+
const canonical = record(source.bookedValue);
|
|
471
|
+
const legacy = record(source.revenue);
|
|
472
|
+
const selected = Object.keys(canonical).length ? canonical : legacy;
|
|
473
|
+
const bySectionSource = Array.isArray(canonical.bySection) ? canonical.bySection : Array.isArray(legacy.bySection) ? legacy.bySection : [];
|
|
474
|
+
const bookedValue = {
|
|
475
|
+
...selected,
|
|
476
|
+
gross: finite(canonical.gross, legacy.gross),
|
|
477
|
+
bySection: bySectionSource.map(normalizeSection)
|
|
478
|
+
};
|
|
479
|
+
const velocity = record(source.velocity);
|
|
480
|
+
const velocityRows = Array.isArray(velocity.bySection) ? velocity.bySection.map((value2) => {
|
|
481
|
+
const row = record(value2);
|
|
482
|
+
const rowValue = finite(row.bookedValue, row.grossRevenue);
|
|
483
|
+
return { ...row, bookedValue: rowValue, grossRevenue: rowValue };
|
|
484
|
+
}) : [];
|
|
485
|
+
return {
|
|
486
|
+
...source,
|
|
487
|
+
bookedValue,
|
|
488
|
+
revenue: bookedValue,
|
|
489
|
+
velocity: { ...velocity, bySection: velocityRows }
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function normalizeChannelReportResult(value) {
|
|
493
|
+
const source = record(value);
|
|
494
|
+
const report = record(source.report);
|
|
495
|
+
const includesBookedValue = typeof report.includesBookedValue === "boolean" ? report.includesBookedValue : report.includesRevenue === true;
|
|
496
|
+
const rows = Array.isArray(report.rows) ? report.rows.map((value2) => {
|
|
497
|
+
const row = record(value2);
|
|
498
|
+
const attribution = record(row.attribution);
|
|
499
|
+
const bookedValue = nullableFinite(attribution.bookedValue, attribution.revenue);
|
|
500
|
+
return {
|
|
501
|
+
...row,
|
|
502
|
+
attribution: { ...attribution, bookedValue, revenue: bookedValue }
|
|
503
|
+
};
|
|
504
|
+
}) : [];
|
|
505
|
+
const totals = record(report.totals);
|
|
506
|
+
const totalBookedValue = nullableFinite(totals.bookedValue, totals.revenue);
|
|
507
|
+
return {
|
|
508
|
+
...source,
|
|
509
|
+
report: {
|
|
510
|
+
...report,
|
|
511
|
+
includesBookedValue,
|
|
512
|
+
includesRevenue: includesBookedValue,
|
|
513
|
+
rows,
|
|
514
|
+
totals: { ...totals, bookedValue: totalBookedValue, revenue: totalBookedValue }
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function normalizeChannelReportLink(value) {
|
|
519
|
+
const link = record(value);
|
|
520
|
+
const includesBookedValue = typeof link.includesBookedValue === "boolean" ? link.includesBookedValue : link.includesRevenue === true;
|
|
521
|
+
return {
|
|
522
|
+
...link,
|
|
523
|
+
includesBookedValue,
|
|
524
|
+
includesRevenue: includesBookedValue
|
|
525
|
+
};
|
|
526
|
+
}
|
|
434
527
|
var ManageApiError, ManageApi;
|
|
435
528
|
var init_manageApi = __esm({
|
|
436
529
|
"src/manageApi.ts"() {
|
|
@@ -465,13 +558,28 @@ var init_manageApi = __esm({
|
|
|
465
558
|
}
|
|
466
559
|
return fetch(`${this.base}${path}`, { method, headers, body, credentials: "omit" }).then((r) => parse(r));
|
|
467
560
|
}
|
|
468
|
-
|
|
469
|
-
|
|
561
|
+
async authBlob(path) {
|
|
562
|
+
const res = await fetch(`${this.base}${path}`, {
|
|
563
|
+
method: "GET",
|
|
564
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
565
|
+
credentials: "omit"
|
|
566
|
+
});
|
|
567
|
+
if (!res.ok) await parse(res);
|
|
568
|
+
return res.blob();
|
|
470
569
|
}
|
|
471
570
|
// ---- realtime read ----
|
|
472
|
-
/**
|
|
571
|
+
/** Event-pinned organizer geometry. A manage token is never sent to `/pub`. */
|
|
473
572
|
chart(key) {
|
|
474
|
-
return this.
|
|
573
|
+
return this.auth(`/v1/events/${encodeURIComponent(key)}/chart`);
|
|
574
|
+
}
|
|
575
|
+
/** Authenticated bytes for an Event-scoped organizer chart asset. */
|
|
576
|
+
asset(key, asset) {
|
|
577
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(asset)) {
|
|
578
|
+
return Promise.reject(new ManageApiError(404, "not_found", "not_found"));
|
|
579
|
+
}
|
|
580
|
+
return this.authBlob(
|
|
581
|
+
`/v1/events/${encodeURIComponent(key)}/assets/${encodeURIComponent(asset)}`
|
|
582
|
+
);
|
|
475
583
|
}
|
|
476
584
|
/**
|
|
477
585
|
* The ORGANIZER's seat map: physical state, token-authed.
|
|
@@ -531,6 +639,31 @@ var init_manageApi = __esm({
|
|
|
531
639
|
setHoldTtl(key, holdTtlMs) {
|
|
532
640
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/hold-ttl`, { method: "POST", body: { holdTtlMs } });
|
|
533
641
|
}
|
|
642
|
+
// ---- Platform inventory booking history (token) ----
|
|
643
|
+
/** Inventory lifecycle by stable integrator bookingRef. Absent on Managed. */
|
|
644
|
+
bookings(key, query = {}) {
|
|
645
|
+
const params = new URLSearchParams();
|
|
646
|
+
if (query.q) params.set("q", query.q);
|
|
647
|
+
if (query.state) params.set("state", query.state);
|
|
648
|
+
if (query.cursor) params.set("cursor", query.cursor);
|
|
649
|
+
if (query.limit != null) params.set("limit", String(query.limit));
|
|
650
|
+
const qs = params.toString();
|
|
651
|
+
return this.auth(`/v1/events/${encodeURIComponent(key)}/bookings${qs ? `?${qs}` : ""}`);
|
|
652
|
+
}
|
|
653
|
+
/** Exact configured-value snapshot plus book/replay/cancellation audit. */
|
|
654
|
+
booking(key, bookingRef) {
|
|
655
|
+
return this.auth(
|
|
656
|
+
`/v1/events/${encodeURIComponent(key)}/bookings/${encodeURIComponent(bookingRef)}`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
/** Alias matching the server SDK vocabulary. */
|
|
660
|
+
listBookings(key, query = {}) {
|
|
661
|
+
return this.bookings(key, query);
|
|
662
|
+
}
|
|
663
|
+
/** Alias matching the server SDK vocabulary. */
|
|
664
|
+
retrieveBooking(key, bookingRef) {
|
|
665
|
+
return this.booking(key, bookingRef);
|
|
666
|
+
}
|
|
534
667
|
// ---- availability windows (token) ----
|
|
535
668
|
/** The organizer's current per section/zone availability windows (needs
|
|
536
669
|
* `event:view`). Ids absent from `rules` are open / on sale. */
|
|
@@ -712,10 +845,46 @@ var init_manageApi = __esm({
|
|
|
712
845
|
}
|
|
713
846
|
// ---- reports (token) ----
|
|
714
847
|
report(key) {
|
|
715
|
-
return this.auth(`/v1/events/${encodeURIComponent(key)}/report`);
|
|
848
|
+
return this.auth(`/v1/events/${encodeURIComponent(key)}/report`).then(normalizeReportResult);
|
|
716
849
|
}
|
|
717
850
|
controlRoom(key, windowMinutes = 15) {
|
|
718
|
-
return this.auth(
|
|
851
|
+
return this.auth(
|
|
852
|
+
`/v1/events/${encodeURIComponent(key)}/control-room?window=${windowMinutes}`
|
|
853
|
+
).then(normalizeControlRoomSnapshot);
|
|
854
|
+
}
|
|
855
|
+
/** Allocation beside immutable booking-time channel attribution. */
|
|
856
|
+
channelReport(key) {
|
|
857
|
+
return this.auth(
|
|
858
|
+
`/v1/events/${encodeURIComponent(key)}/channels/report`
|
|
859
|
+
).then(normalizeChannelReportResult);
|
|
860
|
+
}
|
|
861
|
+
createChannelReportLink(key, channelId, input = {}) {
|
|
862
|
+
return this.auth(
|
|
863
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/report-links`,
|
|
864
|
+
{ method: "POST", body: input }
|
|
865
|
+
).then((value) => {
|
|
866
|
+
const reveal = record(value);
|
|
867
|
+
return { ...reveal, link: normalizeChannelReportLink(reveal.link) };
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
channelReportLinks(key, channelId) {
|
|
871
|
+
return this.auth(
|
|
872
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/report-links`
|
|
873
|
+
).then((value) => {
|
|
874
|
+
const result = record(value);
|
|
875
|
+
return {
|
|
876
|
+
links: Array.isArray(result.links) ? result.links.map(normalizeChannelReportLink) : []
|
|
877
|
+
};
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
revokeChannelReportLink(key, channelId, linkId) {
|
|
881
|
+
return this.auth(
|
|
882
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/report-links/${encodeURIComponent(linkId)}`,
|
|
883
|
+
{ method: "DELETE" }
|
|
884
|
+
).then((value) => {
|
|
885
|
+
const result = record(value);
|
|
886
|
+
return { ...result, link: normalizeChannelReportLink(result.link) };
|
|
887
|
+
});
|
|
719
888
|
}
|
|
720
889
|
log(key, opts = {}) {
|
|
721
890
|
const params = new URLSearchParams();
|
|
@@ -4446,9 +4615,54 @@ var PubApi = class {
|
|
|
4446
4615
|
}
|
|
4447
4616
|
return data;
|
|
4448
4617
|
}
|
|
4618
|
+
/**
|
|
4619
|
+
* Binary counterpart to `request`. Buyer media needs the same in-memory
|
|
4620
|
+
* bearer/refresh rules as JSON, but returns bytes that the picker turns into
|
|
4621
|
+
* a blob URL. The bearer stays in the Authorization header and is never
|
|
4622
|
+
* appended to `path`.
|
|
4623
|
+
*/
|
|
4624
|
+
async requestBlob(path, retried = {}) {
|
|
4625
|
+
const headers = {};
|
|
4626
|
+
const authorization = await this.access?.authorization(retried.auth ? "unauthorized" : "initial");
|
|
4627
|
+
if (authorization) headers.Authorization = authorization;
|
|
4628
|
+
const res = await fetch(`${this.base}${path}`, { method: "GET", headers, credentials: "omit" });
|
|
4629
|
+
if (res.ok) return res.blob();
|
|
4630
|
+
const isJson = (res.headers.get("content-type") ?? "").includes("application/json");
|
|
4631
|
+
const data = isJson ? await res.json().catch(() => null) : null;
|
|
4632
|
+
const code = data?.code ?? data?.error;
|
|
4633
|
+
if (this.access?.configured && (res.status === 401 || res.status === 403 || res.status === 422)) {
|
|
4634
|
+
const refreshed = await this.access.handleFailure(res.status, code);
|
|
4635
|
+
if (refreshed && !retried.auth) return this.requestBlob(path, { ...retried, auth: true });
|
|
4636
|
+
}
|
|
4637
|
+
let retryAfterS;
|
|
4638
|
+
if (res.status === 429) {
|
|
4639
|
+
retryAfterS = parseRetryAfter(res.headers.get("Retry-After"), data?.retryAfterSeconds) ?? DEFAULT_RATE_LIMIT_WAIT_S;
|
|
4640
|
+
if (!retried.rateLimit && retryAfterS <= MAX_RATE_LIMIT_WAIT_S) {
|
|
4641
|
+
await new Promise((resolve) => setTimeout(resolve, retryAfterS * 1e3));
|
|
4642
|
+
return this.requestBlob(path, { ...retried, rateLimit: true });
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
throw new ApiError(
|
|
4646
|
+
res.status,
|
|
4647
|
+
data?.error ?? `request_failed_${res.status}`,
|
|
4648
|
+
code,
|
|
4649
|
+
void 0,
|
|
4650
|
+
void 0,
|
|
4651
|
+
retryAfterS
|
|
4652
|
+
);
|
|
4653
|
+
}
|
|
4449
4654
|
chart(key) {
|
|
4450
4655
|
return this.request(`/pub/events/${encodeURIComponent(key)}/chart`);
|
|
4451
4656
|
}
|
|
4657
|
+
/** Authenticated bytes for an Event-scoped authored view image. */
|
|
4658
|
+
asset(key, asset) {
|
|
4659
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(asset)) {
|
|
4660
|
+
return Promise.reject(new ApiError(404, "not_found", "not_found"));
|
|
4661
|
+
}
|
|
4662
|
+
return this.requestBlob(
|
|
4663
|
+
`/pub/events/${encodeURIComponent(key)}/assets/${encodeURIComponent(asset)}`
|
|
4664
|
+
);
|
|
4665
|
+
}
|
|
4452
4666
|
objects(key) {
|
|
4453
4667
|
return this.request(`/pub/events/${encodeURIComponent(key)}/objects?compact=1`);
|
|
4454
4668
|
}
|
|
@@ -4551,12 +4765,11 @@ var PubApi = class {
|
|
|
4551
4765
|
/**
|
|
4552
4766
|
* What PickerController opens its own socket with.
|
|
4553
4767
|
*
|
|
4554
|
-
* Empty for an access-scoped client: a
|
|
4768
|
+
* Empty for an access-scoped client: a scoped audience authenticates with a
|
|
4555
4769
|
* subprotocol ticket, which a URL-only constructor cannot carry, so the SDK's
|
|
4556
4770
|
* BuyerRealtimeClient owns that socket instead and the controller skips its
|
|
4557
4771
|
* own (an empty URL is its documented "no live feed" contract). A tokenless
|
|
4558
|
-
* public client returns exactly the URL it always has
|
|
4559
|
-
* public picker's realtime path changes.
|
|
4772
|
+
* Managed public client returns exactly the URL it always has.
|
|
4560
4773
|
*/
|
|
4561
4774
|
socketUrl(key) {
|
|
4562
4775
|
return this.accessScoped ? "" : this.subscribeUrl(key);
|
|
@@ -5902,6 +6115,81 @@ var import_core2 = require("@seatlayer/core");
|
|
|
5902
6115
|
var import_panorama = require("@seatlayer/core/view3d/crossfade/panorama");
|
|
5903
6116
|
var import_panoramaDelivery = require("@seatlayer/core/view/panoramaDelivery");
|
|
5904
6117
|
|
|
6118
|
+
// src/buyerAssets.ts
|
|
6119
|
+
var SAFE_ASSET = /^[a-zA-Z0-9._-]+$/;
|
|
6120
|
+
function buyerEventAssetReference(value) {
|
|
6121
|
+
let url;
|
|
6122
|
+
try {
|
|
6123
|
+
url = new URL(value, "https://seatlayer.invalid");
|
|
6124
|
+
} catch {
|
|
6125
|
+
return null;
|
|
6126
|
+
}
|
|
6127
|
+
if (url.search || url.hash) return null;
|
|
6128
|
+
const match = /^\/pub\/events\/([^/]+)\/assets\/([^/]+)$/.exec(url.pathname);
|
|
6129
|
+
if (!match) return null;
|
|
6130
|
+
try {
|
|
6131
|
+
const eventKey = decodeURIComponent(match[1]);
|
|
6132
|
+
const asset = decodeURIComponent(match[2]);
|
|
6133
|
+
if (!eventKey || !SAFE_ASSET.test(asset)) return null;
|
|
6134
|
+
return { eventKey, asset };
|
|
6135
|
+
} catch {
|
|
6136
|
+
return null;
|
|
6137
|
+
}
|
|
6138
|
+
}
|
|
6139
|
+
function looksLikeBuyerAsset(value) {
|
|
6140
|
+
try {
|
|
6141
|
+
return /^\/pub\/events\/[^/]+\/assets(?:\/|$)/.test(
|
|
6142
|
+
new URL(value, "https://seatlayer.invalid").pathname
|
|
6143
|
+
);
|
|
6144
|
+
} catch {
|
|
6145
|
+
return false;
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
6148
|
+
var BuyerAssetObjectUrls = class {
|
|
6149
|
+
constructor(eventKey, load) {
|
|
6150
|
+
this.eventKey = eventKey;
|
|
6151
|
+
this.load = load;
|
|
6152
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
6153
|
+
this.created = /* @__PURE__ */ new Set();
|
|
6154
|
+
this.disposed = false;
|
|
6155
|
+
}
|
|
6156
|
+
/**
|
|
6157
|
+
* External organizer/CDN URLs pass through unchanged. SeatLayer event assets
|
|
6158
|
+
* never do: they require the transport, and a reference for another Event is
|
|
6159
|
+
* refused instead of being loaded anonymously.
|
|
6160
|
+
*/
|
|
6161
|
+
resolve(reference) {
|
|
6162
|
+
const parsed = buyerEventAssetReference(reference);
|
|
6163
|
+
if (!parsed) {
|
|
6164
|
+
return Promise.resolve(looksLikeBuyerAsset(reference) ? null : reference);
|
|
6165
|
+
}
|
|
6166
|
+
if (parsed.eventKey !== this.eventKey || !this.load || this.disposed) return Promise.resolve(null);
|
|
6167
|
+
const existing = this.pending.get(reference);
|
|
6168
|
+
if (existing) return existing;
|
|
6169
|
+
const task = this.load(this.eventKey, parsed.asset).then((blob) => {
|
|
6170
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
6171
|
+
if (this.disposed) {
|
|
6172
|
+
URL.revokeObjectURL(objectUrl);
|
|
6173
|
+
return null;
|
|
6174
|
+
}
|
|
6175
|
+
this.created.add(objectUrl);
|
|
6176
|
+
return objectUrl;
|
|
6177
|
+
}).catch((error) => {
|
|
6178
|
+
this.pending.delete(reference);
|
|
6179
|
+
throw error;
|
|
6180
|
+
});
|
|
6181
|
+
this.pending.set(reference, task);
|
|
6182
|
+
return task;
|
|
6183
|
+
}
|
|
6184
|
+
dispose() {
|
|
6185
|
+
if (this.disposed) return;
|
|
6186
|
+
this.disposed = true;
|
|
6187
|
+
for (const url of this.created) URL.revokeObjectURL(url);
|
|
6188
|
+
this.created.clear();
|
|
6189
|
+
this.pending.clear();
|
|
6190
|
+
}
|
|
6191
|
+
};
|
|
6192
|
+
|
|
5905
6193
|
// src/offerAvailability.ts
|
|
5906
6194
|
var SALE_STATES = [
|
|
5907
6195
|
"on-sale",
|
|
@@ -7066,6 +7354,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
7066
7354
|
this.secCardEl = null;
|
|
7067
7355
|
this.viewEl = null;
|
|
7068
7356
|
this.viewCleanup = null;
|
|
7357
|
+
/** Supersedes an older authored-view byte request when another seat is opened. */
|
|
7358
|
+
this.seatViewGen = 0;
|
|
7069
7359
|
this.allSeatsCache = null;
|
|
7070
7360
|
// F3 minimap
|
|
7071
7361
|
this.miniCanvas = null;
|
|
@@ -7138,6 +7428,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
7138
7428
|
onObjectUnavailable: (event) => this.opts.onSelectedObjectUnavailable?.(event)
|
|
7139
7429
|
});
|
|
7140
7430
|
this.api = options.transport ?? this.pubApi;
|
|
7431
|
+
this.buyerAssetUrls = new BuyerAssetObjectUrls(
|
|
7432
|
+
options.event,
|
|
7433
|
+
this.api.asset ? (key, asset) => this.api.asset(key, asset) : void 0
|
|
7434
|
+
);
|
|
7141
7435
|
if (options.checkout === "hosted" && !this.pubApi) {
|
|
7142
7436
|
console.warn(
|
|
7143
7437
|
'seatlayer: checkout: "hosted" needs the widget\'s own transport \u2014 a custom `transport` owns its backend, so the picker is staying on onCheckout for this mount.'
|
|
@@ -7266,7 +7560,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
7266
7560
|
}
|
|
7267
7561
|
}
|
|
7268
7562
|
const sightHtml = hasStage && distance != null ? `<div class="sl-confirm-sight">${(0, import_core2.t)("picker.sightline", { m: distance })}</div>` : "";
|
|
7269
|
-
const viewBtn = realPhoto ? `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}"><img class="sl-confirm-thumb"
|
|
7563
|
+
const viewBtn = realPhoto ? `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}"><img class="sl-confirm-thumb" alt="" /><span class="sl-confirm-thumb-badge">\u{1F52D} ${this.tf("picker.viewFromHere", "View from here")}</span></button>` : `<button type="button" class="sl-confirm-view sl-confirm-viewbtn" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}"><span aria-hidden="true">\u{1F52D}</span><span>${this.tf("picker.viewFromHere", "View from here")}</span></button>`;
|
|
7270
7564
|
return viewBtn + sightHtml;
|
|
7271
7565
|
}
|
|
7272
7566
|
/** "See it in 3D" (2D) / "View from this seat" (already in 3D) action for the
|
|
@@ -7717,7 +8011,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
7717
8011
|
}
|
|
7718
8012
|
this.els.boot.remove();
|
|
7719
8013
|
this.startRealtime();
|
|
7720
|
-
this.salesClosed = !!info.salesClosed;
|
|
8014
|
+
this.salesClosed = !!info.salesClosed || !!this.opts.readOnly;
|
|
7721
8015
|
this.controller.setViewMode(this.normalizeInitialView(this.opts.initialView));
|
|
7722
8016
|
this.buildRegions();
|
|
7723
8017
|
this.regions["bottom-right"].appendChild(this.els.zoom);
|
|
@@ -7964,8 +8258,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
7964
8258
|
* idempotent DOM apply used at load and on transition.
|
|
7965
8259
|
*/
|
|
7966
8260
|
setSalesClosed(closed) {
|
|
7967
|
-
|
|
7968
|
-
this.salesClosed
|
|
8261
|
+
const next = closed || !!this.opts.readOnly;
|
|
8262
|
+
if (this.salesClosed === next) return;
|
|
8263
|
+
this.salesClosed = next;
|
|
7969
8264
|
this.applySalesClosed();
|
|
7970
8265
|
}
|
|
7971
8266
|
applySalesClosed() {
|
|
@@ -8849,6 +9144,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
8849
9144
|
el2.innerHTML = `<div class="sl-confirm-grid">` + identityFields + `</div><div class="sl-confirm-cat"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span><span class="sl-confirm-cat-name">${safe(details?.categoryLabel ?? cat?.label ?? seat.categoryKey)}</span>` + (price != null ? `<span class="sl-confirm-price">${this.money(price)}</span>` : "") + `</div><div class="sl-confirm-body">` + this.wheelchairConfirmHtml(details?.wheelchairSpaceType) + this.commercialConfirmHtml(seat.commercial) + (this.seatViewEnabled() ? this.confirmThumbHtml(seat) : "") + this.see3dConfirmHtml() + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4 4L19 7"/></svg>Select</button></div></div>`;
|
|
8850
9145
|
this.els.map.appendChild(el2);
|
|
8851
9146
|
this.confirmEl = el2;
|
|
9147
|
+
const thumb = el2.querySelector(".sl-confirm-thumb");
|
|
9148
|
+
if (thumb && seat.viewUrl) {
|
|
9149
|
+
const thumbReference = seat.viewMeta?.previewUrl ?? seat.viewUrl;
|
|
9150
|
+
void this.buyerAssetUrls.resolve(thumbReference).then((url) => {
|
|
9151
|
+
if (url && el2.isConnected && this.confirmEl === el2) thumb.src = url;
|
|
9152
|
+
}).catch((error) => this.opts.onError?.(error));
|
|
9153
|
+
}
|
|
8852
9154
|
this.reanchorConfirm();
|
|
8853
9155
|
el2.querySelector(".sl-confirm-view")?.addEventListener("click", () => void this.openSeatView(seat));
|
|
8854
9156
|
el2.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
|
|
@@ -8929,6 +9231,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
8929
9231
|
*/
|
|
8930
9232
|
async openSeatView(seat) {
|
|
8931
9233
|
if (!this.root || !this.seatViewEnabled()) return;
|
|
9234
|
+
const generation = ++this.seatViewGen;
|
|
8932
9235
|
const doc = this.controller.doc;
|
|
8933
9236
|
const activeId = this.controller.getActiveFloorId();
|
|
8934
9237
|
const focal = seat.focalPoint ?? doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
|
|
@@ -8936,9 +9239,24 @@ var SeatPicker = class _SeatPicker {
|
|
|
8936
9239
|
let caption;
|
|
8937
9240
|
let real = false;
|
|
8938
9241
|
if (seat.viewUrl) {
|
|
9242
|
+
let resolvedUrl;
|
|
9243
|
+
let resolvedPreviewUrl = null;
|
|
9244
|
+
try {
|
|
9245
|
+
const previewReference = seat.viewMeta?.previewUrl;
|
|
9246
|
+
if (previewReference && previewReference !== seat.viewUrl) {
|
|
9247
|
+
resolvedPreviewUrl = await this.buyerAssetUrls.resolve(previewReference);
|
|
9248
|
+
resolvedUrl = seat.viewUrl;
|
|
9249
|
+
} else {
|
|
9250
|
+
resolvedUrl = await this.buyerAssetUrls.resolve(seat.viewUrl);
|
|
9251
|
+
}
|
|
9252
|
+
} catch (error) {
|
|
9253
|
+
if (generation === this.seatViewGen) this.opts.onError?.(error);
|
|
9254
|
+
return;
|
|
9255
|
+
}
|
|
9256
|
+
if (generation !== this.seatViewGen || !resolvedUrl || seat.viewMeta?.previewUrl && seat.viewMeta.previewUrl !== seat.viewUrl && !resolvedPreviewUrl || !this.root || !this.seatViewEnabled()) return;
|
|
8939
9257
|
const view = {
|
|
8940
|
-
url:
|
|
8941
|
-
...
|
|
9258
|
+
url: resolvedUrl,
|
|
9259
|
+
...resolvedPreviewUrl ? { previewUrl: resolvedPreviewUrl } : {},
|
|
8942
9260
|
...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
|
|
8943
9261
|
...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
|
|
8944
9262
|
...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
|
|
@@ -8956,14 +9274,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
8956
9274
|
const { generateSeatPanorama } = await loadPanorama();
|
|
8957
9275
|
pano2 = generateSeatPanorama(seat, focal, this.allSeats());
|
|
8958
9276
|
} catch (err) {
|
|
8959
|
-
this.opts.onError?.(err);
|
|
9277
|
+
if (generation === this.seatViewGen) this.opts.onError?.(err);
|
|
8960
9278
|
return;
|
|
8961
9279
|
}
|
|
8962
|
-
if (!this.root || !this.seatViewEnabled()) return;
|
|
9280
|
+
if (generation !== this.seatViewGen || !this.root || !this.seatViewEnabled()) return;
|
|
8963
9281
|
panoSource = { url: pano2.url, generated: true };
|
|
8964
9282
|
caption = (0, import_core2.t)("picker.illustrationCaption", { m: pano2.distanceM });
|
|
8965
9283
|
}
|
|
8966
|
-
this.closeSeatView();
|
|
9284
|
+
this.closeSeatView(false);
|
|
8967
9285
|
const el2 = document.createElement("div");
|
|
8968
9286
|
el2.className = "sl-view";
|
|
8969
9287
|
el2.setAttribute("role", "dialog");
|
|
@@ -8979,9 +9297,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
8979
9297
|
};
|
|
8980
9298
|
if (delivery.upgradeUrl) {
|
|
8981
9299
|
cancelUpgrade = (0, import_panoramaDelivery.schedulePanoramaUpgrade)(() => {
|
|
8982
|
-
void
|
|
8983
|
-
if (!
|
|
8984
|
-
|
|
9300
|
+
void this.buyerAssetUrls.resolve(delivery.upgradeUrl).then((url) => {
|
|
9301
|
+
if (!url || loadAbort.signal.aborted) return null;
|
|
9302
|
+
return (0, import_panoramaDelivery.loadPanoramaImage)(url, loadAbort.signal).then(() => url);
|
|
9303
|
+
}).then((url) => {
|
|
9304
|
+
if (!url || !el2.isConnected || loadAbort.signal.aborted) return;
|
|
9305
|
+
pano.style.backgroundImage = `url("${url}")`;
|
|
8985
9306
|
}).catch(() => {
|
|
8986
9307
|
});
|
|
8987
9308
|
});
|
|
@@ -9057,7 +9378,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
9057
9378
|
el2.removeEventListener("keydown", onKey);
|
|
9058
9379
|
};
|
|
9059
9380
|
}
|
|
9060
|
-
closeSeatView() {
|
|
9381
|
+
closeSeatView(cancelPending = true) {
|
|
9382
|
+
if (cancelPending) this.seatViewGen += 1;
|
|
9061
9383
|
this.viewCleanup?.();
|
|
9062
9384
|
this.viewCleanup = null;
|
|
9063
9385
|
this.viewEl?.remove();
|
|
@@ -10140,19 +10462,33 @@ var SeatPicker = class _SeatPicker {
|
|
|
10140
10462
|
async seatViewFor3d(seatId) {
|
|
10141
10463
|
const seat = this.allSeats().find((s) => s.id === seatId);
|
|
10142
10464
|
if (!seat) return null;
|
|
10143
|
-
if (seat.viewUrl)
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10465
|
+
if (seat.viewUrl) {
|
|
10466
|
+
try {
|
|
10467
|
+
const previewReference = seat.viewMeta?.previewUrl;
|
|
10468
|
+
const progressive = !!previewReference && previewReference !== seat.viewUrl;
|
|
10469
|
+
const previewUrl = progressive ? await this.buyerAssetUrls.resolve(previewReference) : null;
|
|
10470
|
+
const url = progressive ? seat.viewUrl : await this.buyerAssetUrls.resolve(seat.viewUrl);
|
|
10471
|
+
if (!url) return null;
|
|
10472
|
+
if (progressive && !previewUrl) return null;
|
|
10473
|
+
return {
|
|
10474
|
+
url,
|
|
10475
|
+
...previewUrl ? { previewUrl } : {},
|
|
10476
|
+
...progressive ? { resolveUrl: (reference) => this.buyerAssetUrls.resolve(reference) } : {},
|
|
10477
|
+
...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
|
|
10478
|
+
...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
|
|
10479
|
+
...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
|
|
10480
|
+
...seat.viewMeta?.previewHeight !== void 0 ? { previewHeight: seat.viewMeta.previewHeight } : {},
|
|
10481
|
+
...seat.viewMeta?.initialBearingDeg !== void 0 ? { initialBearingDeg: seat.viewMeta.initialBearingDeg } : {},
|
|
10482
|
+
...seat.viewMeta?.initialPitchDeg !== void 0 ? { initialPitchDeg: seat.viewMeta.initialPitchDeg } : {},
|
|
10483
|
+
...seat.viewMeta?.coverage ? { coverage: seat.viewMeta.coverage } : {},
|
|
10484
|
+
...seat.viewMeta?.capturedAt ? { capturedAt: seat.viewMeta.capturedAt } : {},
|
|
10485
|
+
...seat.viewMeta?.sourceLabel ? { sourceLabel: seat.viewMeta.sourceLabel } : {}
|
|
10486
|
+
};
|
|
10487
|
+
} catch (error) {
|
|
10488
|
+
this.opts.onError?.(error);
|
|
10489
|
+
return null;
|
|
10490
|
+
}
|
|
10491
|
+
}
|
|
10156
10492
|
const doc = this.controller.doc;
|
|
10157
10493
|
if (!doc) return null;
|
|
10158
10494
|
const activeId = this.controller.getActiveFloorId();
|
|
@@ -10735,6 +11071,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
10735
11071
|
this.closeSeatView();
|
|
10736
11072
|
this.closeCheckoutPanel();
|
|
10737
11073
|
this.exit3d();
|
|
11074
|
+
this.buyerAssetUrls.dispose();
|
|
10738
11075
|
this.stopHoldTimer();
|
|
10739
11076
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
10740
11077
|
if (this.liveTimer) clearTimeout(this.liveTimer);
|
|
@@ -10855,6 +11192,108 @@ function attachPickerFrame(iframe, opts = {}) {
|
|
|
10855
11192
|
// src/SeatManager.ts
|
|
10856
11193
|
var import_core3 = require("@seatlayer/core");
|
|
10857
11194
|
init_manageApi();
|
|
11195
|
+
|
|
11196
|
+
// src/manageAssets.ts
|
|
11197
|
+
var SAFE_ASSET2 = /^[a-zA-Z0-9._-]+$/;
|
|
11198
|
+
function organizerEventAssetReference(value) {
|
|
11199
|
+
let url;
|
|
11200
|
+
try {
|
|
11201
|
+
url = new URL(value, "https://seatlayer.invalid");
|
|
11202
|
+
} catch {
|
|
11203
|
+
return null;
|
|
11204
|
+
}
|
|
11205
|
+
if (url.search || url.hash) return null;
|
|
11206
|
+
const match = /^\/v1\/events\/([^/]+)\/assets\/([^/]+)$/.exec(url.pathname);
|
|
11207
|
+
if (!match) return null;
|
|
11208
|
+
try {
|
|
11209
|
+
const eventKey = decodeURIComponent(match[1]);
|
|
11210
|
+
const asset = decodeURIComponent(match[2]);
|
|
11211
|
+
if (!eventKey || !SAFE_ASSET2.test(asset)) return null;
|
|
11212
|
+
return { eventKey, asset };
|
|
11213
|
+
} catch {
|
|
11214
|
+
return null;
|
|
11215
|
+
}
|
|
11216
|
+
}
|
|
11217
|
+
function looksLikeOrganizerAsset(value) {
|
|
11218
|
+
try {
|
|
11219
|
+
return /^\/v1\/events\/[^/]+\/assets(?:\/|$)/.test(
|
|
11220
|
+
new URL(value, "https://seatlayer.invalid").pathname
|
|
11221
|
+
);
|
|
11222
|
+
} catch {
|
|
11223
|
+
return false;
|
|
11224
|
+
}
|
|
11225
|
+
}
|
|
11226
|
+
var OrganizerAssetObjectUrls = class {
|
|
11227
|
+
constructor(eventKey, load) {
|
|
11228
|
+
this.eventKey = eventKey;
|
|
11229
|
+
this.load = load;
|
|
11230
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
11231
|
+
this.created = /* @__PURE__ */ new Set();
|
|
11232
|
+
this.disposed = false;
|
|
11233
|
+
}
|
|
11234
|
+
resolve(reference) {
|
|
11235
|
+
const parsed = organizerEventAssetReference(reference);
|
|
11236
|
+
if (!parsed) {
|
|
11237
|
+
return Promise.resolve(looksLikeOrganizerAsset(reference) ? null : reference);
|
|
11238
|
+
}
|
|
11239
|
+
if (parsed.eventKey !== this.eventKey || this.disposed) return Promise.resolve(null);
|
|
11240
|
+
const cacheKey = `${parsed.eventKey}/${parsed.asset}`;
|
|
11241
|
+
const existing = this.pending.get(cacheKey);
|
|
11242
|
+
if (existing) return existing;
|
|
11243
|
+
const task = this.load(parsed.eventKey, parsed.asset).then((blob) => {
|
|
11244
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
11245
|
+
if (this.disposed) {
|
|
11246
|
+
URL.revokeObjectURL(objectUrl);
|
|
11247
|
+
return null;
|
|
11248
|
+
}
|
|
11249
|
+
this.created.add(objectUrl);
|
|
11250
|
+
return objectUrl;
|
|
11251
|
+
}).catch((error) => {
|
|
11252
|
+
this.pending.delete(cacheKey);
|
|
11253
|
+
throw error;
|
|
11254
|
+
});
|
|
11255
|
+
this.pending.set(cacheKey, task);
|
|
11256
|
+
return task;
|
|
11257
|
+
}
|
|
11258
|
+
/**
|
|
11259
|
+
* Resolve the image fields the synchronous map renderer loads immediately.
|
|
11260
|
+
* View-from-seat media stays lazy: SeatManager does not open that buyer
|
|
11261
|
+
* surface, and eagerly downloading every row panorama would be unbounded.
|
|
11262
|
+
*/
|
|
11263
|
+
async prepareRendererChart(doc) {
|
|
11264
|
+
const prepareBackground = async (background) => {
|
|
11265
|
+
if (!background?.url) return;
|
|
11266
|
+
const resolved = await this.resolve(background.url);
|
|
11267
|
+
if (!resolved) throw new Error("organizer_event_asset_scope_mismatch");
|
|
11268
|
+
background.url = resolved;
|
|
11269
|
+
};
|
|
11270
|
+
const prepareObjects = async (objects) => {
|
|
11271
|
+
for (const object of objects) {
|
|
11272
|
+
if (object.type !== "decorImage") continue;
|
|
11273
|
+
const image = object;
|
|
11274
|
+
const resolved = await this.resolve(image.href);
|
|
11275
|
+
if (!resolved) throw new Error("organizer_event_asset_scope_mismatch");
|
|
11276
|
+
image.href = resolved;
|
|
11277
|
+
}
|
|
11278
|
+
};
|
|
11279
|
+
const prepareOwner = async (owner) => {
|
|
11280
|
+
await prepareBackground(owner.backgroundImage);
|
|
11281
|
+
await prepareObjects(owner.objects);
|
|
11282
|
+
};
|
|
11283
|
+
await prepareOwner(doc);
|
|
11284
|
+
for (const floor of doc.floors ?? []) await prepareOwner(floor);
|
|
11285
|
+
return doc;
|
|
11286
|
+
}
|
|
11287
|
+
dispose() {
|
|
11288
|
+
if (this.disposed) return;
|
|
11289
|
+
this.disposed = true;
|
|
11290
|
+
for (const url of this.created) URL.revokeObjectURL(url);
|
|
11291
|
+
this.created.clear();
|
|
11292
|
+
this.pending.clear();
|
|
11293
|
+
}
|
|
11294
|
+
};
|
|
11295
|
+
|
|
11296
|
+
// src/SeatManager.ts
|
|
10858
11297
|
function availabilityModeOf(rule) {
|
|
10859
11298
|
return rule ? rule.mode : "open";
|
|
10860
11299
|
}
|
|
@@ -11133,8 +11572,8 @@ var MANAGER_CSS = (
|
|
|
11133
11572
|
.slm.compact .slm-modes{min-width:0}.slm.compact .slm-mode{padding-inline:11px}
|
|
11134
11573
|
.slm.compact .slm-live{justify-self:end}.slm.compact .slm-bar-actions{grid-column:1/-1;justify-self:stretch}
|
|
11135
11574
|
.slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
|
11136
|
-
.slm.compact .slm-kpi[data-kpi="
|
|
11137
|
-
.slm.compact .slm-kpi[data-kpi="
|
|
11575
|
+
.slm.compact .slm-kpi[data-kpi="viewing-map"],.slm.compact .slm-kpi[data-kpi="active-holds"],
|
|
11576
|
+
.slm.compact .slm-kpi[data-kpi="booked-pct"],.slm.compact .slm-kpi[data-kpi="booked-value"]{display:none}
|
|
11138
11577
|
/* Reduced motion, as a BLANKET over the cockpit subtree rather than a list of
|
|
11139
11578
|
selectors. The list this replaces named four animations and two transitions,
|
|
11140
11579
|
and had silently fallen behind the stylesheet: the zoom hint, the toast and
|
|
@@ -11344,6 +11783,10 @@ var SeatManager = class {
|
|
|
11344
11783
|
this.currency = options.currency ?? "USD";
|
|
11345
11784
|
this.tokenExpiresAt = options.tokenExpiresAt ?? null;
|
|
11346
11785
|
this.api = new ManageApi(options.apiBase ?? DEFAULT_API_BASE3, options.token);
|
|
11786
|
+
this.organizerAssetUrls = new OrganizerAssetObjectUrls(
|
|
11787
|
+
this.key,
|
|
11788
|
+
(key, asset) => this.withAuthRetry(() => this.api.asset(key, asset))
|
|
11789
|
+
);
|
|
11347
11790
|
this.host = resolveContainer4(options.container);
|
|
11348
11791
|
}
|
|
11349
11792
|
/** Build the DOM, load the chart, subscribe to realtime, mount the board. */
|
|
@@ -11351,10 +11794,10 @@ var SeatManager = class {
|
|
|
11351
11794
|
injectStyle();
|
|
11352
11795
|
this.buildChrome();
|
|
11353
11796
|
try {
|
|
11354
|
-
const res = await this.api.chart(this.key);
|
|
11355
|
-
this.doc = res.doc;
|
|
11797
|
+
const res = await this.withAuthRetry(() => this.api.chart(this.key));
|
|
11798
|
+
this.doc = await this.organizerAssetUrls.prepareRendererChart(res.doc);
|
|
11356
11799
|
this.currency = res.event.currency ?? this.opts.currency ?? this.currency;
|
|
11357
|
-
this.buildUnitUniverse(
|
|
11800
|
+
this.buildUnitUniverse(this.doc);
|
|
11358
11801
|
this.buildRenderer();
|
|
11359
11802
|
this.buildSectionOptions();
|
|
11360
11803
|
const [, controlRoom] = await Promise.all([
|
|
@@ -11701,10 +12144,10 @@ var SeatManager = class {
|
|
|
11701
12144
|
try {
|
|
11702
12145
|
await this.api.unbook(this.key, targets, bookingRef);
|
|
11703
12146
|
this.clearSelection();
|
|
11704
|
-
this.done("cancelBooking", targets, `
|
|
12147
|
+
this.done("cancelBooking", targets, `Released ${targets.length} booked unit${targets.length === 1 ? "" : "s"}.`);
|
|
11705
12148
|
} catch (err) {
|
|
11706
12149
|
this.setSeatsLocal(targets, "booked");
|
|
11707
|
-
this.toastErr("Couldn't
|
|
12150
|
+
this.toastErr("Couldn't release that booked inventory. Check the booking reference.");
|
|
11708
12151
|
this.opts.onError?.(err);
|
|
11709
12152
|
}
|
|
11710
12153
|
}
|
|
@@ -11757,9 +12200,9 @@ var SeatManager = class {
|
|
|
11757
12200
|
async setHoldTtl(ms) {
|
|
11758
12201
|
try {
|
|
11759
12202
|
await this.api.setHoldTtl(this.key, ms);
|
|
11760
|
-
this.done("setHoldTtl", [], ms ? `
|
|
12203
|
+
this.done("setHoldTtl", [], ms ? `Hold window set to ${Math.round(ms / 6e4)} min.` : "Hold window reset.");
|
|
11761
12204
|
} catch (err) {
|
|
11762
|
-
this.toastErr("Couldn't update the
|
|
12205
|
+
this.toastErr("Couldn't update the hold window.");
|
|
11763
12206
|
this.opts.onError?.(err);
|
|
11764
12207
|
}
|
|
11765
12208
|
}
|
|
@@ -11804,6 +12247,7 @@ var SeatManager = class {
|
|
|
11804
12247
|
}
|
|
11805
12248
|
this.renderer?.destroy();
|
|
11806
12249
|
this.renderer = null;
|
|
12250
|
+
this.organizerAssetUrls.dispose();
|
|
11807
12251
|
if (this.root && this.root.parentNode === this.host) this.host.removeChild(this.root);
|
|
11808
12252
|
}
|
|
11809
12253
|
// ---- renderer lifecycle ---------------------------------------------------
|
|
@@ -11924,24 +12368,28 @@ var SeatManager = class {
|
|
|
11924
12368
|
* projects its deltas, so any change inside a private channel allocation is
|
|
11925
12369
|
* structurally suppressed and the map silently drifts.
|
|
11926
12370
|
*
|
|
11927
|
-
* If the mint fails
|
|
11928
|
-
*
|
|
11929
|
-
*
|
|
11930
|
-
* the authenticated HTTP read.
|
|
12371
|
+
* If the mint fails, remain reconnecting. An unticketed socket is a buyer
|
|
12372
|
+
* projection, so applying it to organizer state would be worse than staying
|
|
12373
|
+
* visibly offline while the host refreshes authority or upgrades the API.
|
|
11931
12374
|
*/
|
|
11932
12375
|
async connect() {
|
|
11933
12376
|
if (this.closed) return;
|
|
11934
12377
|
let protocols;
|
|
11935
12378
|
try {
|
|
11936
|
-
protocols = (await this.api.subscribeTicket(this.key)).protocols;
|
|
11937
|
-
|
|
11938
|
-
|
|
12379
|
+
protocols = (await this.withAuthRetry(() => this.api.subscribeTicket(this.key))).protocols;
|
|
12380
|
+
if (!protocols.length) throw new Error("manage_subscribe_ticket_missing");
|
|
12381
|
+
} catch (err) {
|
|
12382
|
+
this.setLive(false);
|
|
12383
|
+
this.opts.onError?.(err);
|
|
12384
|
+
this.scheduleReconnect();
|
|
12385
|
+
return;
|
|
11939
12386
|
}
|
|
11940
12387
|
if (this.closed) return;
|
|
11941
12388
|
let ws;
|
|
11942
12389
|
try {
|
|
11943
|
-
ws =
|
|
11944
|
-
} catch {
|
|
12390
|
+
ws = new WebSocket(this.api.socketUrl(this.key), protocols);
|
|
12391
|
+
} catch (err) {
|
|
12392
|
+
this.opts.onError?.(err);
|
|
11945
12393
|
this.scheduleReconnect();
|
|
11946
12394
|
return;
|
|
11947
12395
|
}
|
|
@@ -12032,8 +12480,9 @@ var SeatManager = class {
|
|
|
12032
12480
|
this.lastSyncedAt = Date.now();
|
|
12033
12481
|
this.afterPaint();
|
|
12034
12482
|
}
|
|
12035
|
-
|
|
12036
|
-
|
|
12483
|
+
const liveBookedValue = typeof m.bookedValue?.gross === "number" ? m.bookedValue.gross : m.revenue?.gross;
|
|
12484
|
+
if (typeof liveBookedValue === "number" && Number.isFinite(liveBookedValue)) {
|
|
12485
|
+
this.applyLiveGross(liveBookedValue);
|
|
12037
12486
|
}
|
|
12038
12487
|
this.recomputeTallies();
|
|
12039
12488
|
}
|
|
@@ -12050,9 +12499,12 @@ var SeatManager = class {
|
|
|
12050
12499
|
this.authoritativeGrossRevenue = gross;
|
|
12051
12500
|
this.revenueStatus = "current";
|
|
12052
12501
|
if (this.controlRoomSnapshot) {
|
|
12502
|
+
const current = this.controlRoomSnapshot.bookedValue ?? this.controlRoomSnapshot.revenue;
|
|
12503
|
+
const bookedValue = { ...current, gross };
|
|
12053
12504
|
this.controlRoomSnapshot = {
|
|
12054
12505
|
...this.controlRoomSnapshot,
|
|
12055
|
-
|
|
12506
|
+
bookedValue,
|
|
12507
|
+
revenue: bookedValue
|
|
12056
12508
|
};
|
|
12057
12509
|
this.opts.onControlRoom?.(this.controlRoomSnapshot);
|
|
12058
12510
|
}
|
|
@@ -12230,7 +12682,10 @@ var SeatManager = class {
|
|
|
12230
12682
|
// ---- tallies + feed -------------------------------------------------------
|
|
12231
12683
|
applyReportRevenue(report) {
|
|
12232
12684
|
this.authoritativeGrossRevenue = report.report.byCategory.reduce(
|
|
12233
|
-
(sum, row) =>
|
|
12685
|
+
(sum, row) => {
|
|
12686
|
+
const value = Number.isFinite(row.bookedValue) ? row.bookedValue : row.bookedRevenue;
|
|
12687
|
+
return sum + (Number.isFinite(value) ? value : 0);
|
|
12688
|
+
},
|
|
12234
12689
|
0
|
|
12235
12690
|
);
|
|
12236
12691
|
this.revenueStatus = "current";
|
|
@@ -12249,7 +12704,13 @@ var SeatManager = class {
|
|
|
12249
12704
|
const requestedAt = Date.now();
|
|
12250
12705
|
try {
|
|
12251
12706
|
const fetched = await this.api.controlRoom(this.key, this.trendWindowMinutes);
|
|
12252
|
-
|
|
12707
|
+
const incoming = fetched.bookedValue ?? fetched.revenue ?? { gross: 0, bySection: [] };
|
|
12708
|
+
const normalizedSections = (incoming.bySection ?? []).map((row) => {
|
|
12709
|
+
const value = Number.isFinite(row.bookedValue) ? row.bookedValue : row.bookedRevenue;
|
|
12710
|
+
return { ...row, bookedValue: value ?? 0, bookedRevenue: value ?? 0 };
|
|
12711
|
+
});
|
|
12712
|
+
const canonical = { ...incoming, bySection: normalizedSections };
|
|
12713
|
+
let snapshot = { ...fetched, bookedValue: canonical, revenue: canonical };
|
|
12253
12714
|
if (request === this.revenueRequest) {
|
|
12254
12715
|
if (this.livePresence && this.livePresence.at >= requestedAt) {
|
|
12255
12716
|
snapshot = { ...snapshot, presence: this.livePresence.value };
|
|
@@ -12257,14 +12718,15 @@ var SeatManager = class {
|
|
|
12257
12718
|
this.livePresence = null;
|
|
12258
12719
|
}
|
|
12259
12720
|
if (this.liveGross && this.liveGross.at >= requestedAt) {
|
|
12260
|
-
|
|
12721
|
+
const bookedValue = { ...snapshot.bookedValue, gross: this.liveGross.value };
|
|
12722
|
+
snapshot = { ...snapshot, bookedValue, revenue: bookedValue };
|
|
12261
12723
|
} else {
|
|
12262
12724
|
this.liveGross = null;
|
|
12263
12725
|
}
|
|
12264
12726
|
this.controlRoomSnapshot = snapshot;
|
|
12265
12727
|
this.rebaseServerTotals(snapshot);
|
|
12266
12728
|
this.lastSyncedAt = Date.now();
|
|
12267
|
-
this.authoritativeGrossRevenue = snapshot.
|
|
12729
|
+
this.authoritativeGrossRevenue = snapshot.bookedValue.gross;
|
|
12268
12730
|
this.currency = snapshot.currency;
|
|
12269
12731
|
this.revenueStatus = "current";
|
|
12270
12732
|
this.recomputeTallies();
|
|
@@ -12322,7 +12784,9 @@ var SeatManager = class {
|
|
|
12322
12784
|
total: Number.isFinite(seatTotal) ? seatTotal : this.unitTotal(),
|
|
12323
12785
|
capacityPct: 0,
|
|
12324
12786
|
sellThroughPct: 0,
|
|
12787
|
+
bookedValue: this.authoritativeGrossRevenue,
|
|
12325
12788
|
grossRevenue: this.authoritativeGrossRevenue,
|
|
12789
|
+
bookedValueStatus: this.revenueStatus,
|
|
12326
12790
|
revenueStatus: this.revenueStatus,
|
|
12327
12791
|
currency: this.currency
|
|
12328
12792
|
};
|
|
@@ -12477,8 +12941,8 @@ var SeatManager = class {
|
|
|
12477
12941
|
<button class="slm-barbtn follow" data-ref="follow" aria-pressed="false"
|
|
12478
12942
|
title="Stay on the current map view unless enabled">Follow live</button>
|
|
12479
12943
|
<button class="slm-barbtn" data-ref="heat" aria-pressed="false"
|
|
12480
|
-
aria-label="
|
|
12481
|
-
title="Highlight sections
|
|
12944
|
+
aria-label="Booking momentum overlay off"
|
|
12945
|
+
title="Highlight sections booking fastest in the selected time window">Booking momentum</button>
|
|
12482
12946
|
<button class="slm-barbtn" data-ref="fullscreen" title="Full screen (F)" aria-keyshortcuts="F">Full screen</button>
|
|
12483
12947
|
</div>
|
|
12484
12948
|
<div class="slm-kpis" data-ref="kpis"></div>
|
|
@@ -12581,16 +13045,16 @@ var SeatManager = class {
|
|
|
12581
13045
|
if (!button) return;
|
|
12582
13046
|
button.classList.toggle("on", this.followLive);
|
|
12583
13047
|
button.setAttribute("aria-pressed", String(this.followLive));
|
|
12584
|
-
button.setAttribute("title", this.followLive ? "Following new
|
|
13048
|
+
button.setAttribute("title", this.followLive ? "Following new holds and bookings. Turn off to keep the current view." : "Stay on the current map view. Enable to follow new holds and bookings.");
|
|
12585
13049
|
}
|
|
12586
13050
|
paintHeatButton() {
|
|
12587
13051
|
const button = this.els.heat;
|
|
12588
13052
|
if (!button) return;
|
|
12589
13053
|
button.classList.toggle("on", this.heatEnabled);
|
|
12590
13054
|
button.setAttribute("aria-pressed", String(this.heatEnabled));
|
|
12591
|
-
button.setAttribute("aria-label", `
|
|
12592
|
-
button.setAttribute("title", `${this.heatEnabled ? "Hide" : "Highlight"} sections
|
|
12593
|
-
button.textContent = "
|
|
13055
|
+
button.setAttribute("aria-label", `Booking momentum overlay ${this.heatEnabled ? "on" : "off"}`);
|
|
13056
|
+
button.setAttribute("title", `${this.heatEnabled ? "Hide" : "Highlight"} sections booking fastest in the selected time window`);
|
|
13057
|
+
button.textContent = "Booking momentum";
|
|
12594
13058
|
this.paintMomentumHelp();
|
|
12595
13059
|
}
|
|
12596
13060
|
paintMomentumHelp() {
|
|
@@ -12634,23 +13098,23 @@ var SeatManager = class {
|
|
|
12634
13098
|
formatKpiDelta(key, delta, currency) {
|
|
12635
13099
|
const sign = delta > 0 ? "+" : "\u2212";
|
|
12636
13100
|
const absolute = Math.abs(delta);
|
|
12637
|
-
if (key === "
|
|
12638
|
-
if (key === "
|
|
13101
|
+
if (key === "booked-value") return `${sign}${fmtMoney(absolute, currency)}`;
|
|
13102
|
+
if (key === "booked-pct") return `${sign}${absolute.toLocaleString()}pt`;
|
|
12639
13103
|
return `${sign}${absolute.toLocaleString()}`;
|
|
12640
13104
|
}
|
|
12641
13105
|
paintKpis(t3) {
|
|
12642
13106
|
if (!this.els.kpis) return;
|
|
12643
|
-
const
|
|
13107
|
+
const bookedValue = t3.bookedValueStatus === "current" ? fmtMoney(t3.bookedValue, t3.currency) : "\u2014";
|
|
12644
13108
|
const presence = this.presenceCounts();
|
|
12645
13109
|
const items = [
|
|
12646
|
-
{ key: "
|
|
12647
|
-
{ key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held
|
|
12648
|
-
{ key: "free-seats", raw: t3.free, n: t3.free.toLocaleString(), l: "
|
|
12649
|
-
{ key: "blocked", raw: t3.blocked, n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac", title: "
|
|
12650
|
-
{ key: "
|
|
12651
|
-
{ key: "
|
|
12652
|
-
{ key: "
|
|
12653
|
-
{ key: "
|
|
13110
|
+
{ key: "booked-inventory", raw: t3.booked, n: t3.booked.toLocaleString(), l: "Booked inventory", dot: "#22a06b", title: "Inventory units booked" },
|
|
13111
|
+
{ key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held inventory", dot: "#f4b740", title: "Inventory currently held" },
|
|
13112
|
+
{ key: "free-seats", raw: t3.free, n: t3.free.toLocaleString(), l: "Available", dot: "#6e7bff", title: "Inventory available to book" },
|
|
13113
|
+
{ key: "blocked", raw: t3.blocked, n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac", title: "Inventory withheld from booking" },
|
|
13114
|
+
{ key: "viewing-map", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Viewing map", title: "Active map sessions right now" },
|
|
13115
|
+
{ key: "active-holds", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds", title: "Sessions currently holding inventory" },
|
|
13116
|
+
{ key: "booked-pct", raw: t3.capacityPct, n: `${t3.capacityPct}%`, l: "Booked", title: "Booked inventory as a share of the whole event" },
|
|
13117
|
+
{ key: "booked-value", raw: t3.bookedValueStatus === "current" ? t3.bookedValue : null, n: bookedValue, l: "Booked value", title: "Configured value attached to booked inventory" }
|
|
12654
13118
|
];
|
|
12655
13119
|
let hasChanges = false;
|
|
12656
13120
|
this.els.kpis.innerHTML = items.map((item) => {
|
|
@@ -12699,12 +13163,12 @@ var SeatManager = class {
|
|
|
12699
13163
|
renderViewRail() {
|
|
12700
13164
|
this.els.rail.innerHTML = `
|
|
12701
13165
|
<p class="slm-eyebrow">Monitor</p>
|
|
12702
|
-
<p class="slm-hint">Read-only. Inventory,
|
|
13166
|
+
<p class="slm-hint">Read-only. Inventory, map activity and booking movement update on the same live board.</p>
|
|
12703
13167
|
<div class="slm-health" data-ref="presence"></div>
|
|
12704
13168
|
<div class="slm-legend" data-ref="legend"></div>
|
|
12705
13169
|
<div class="slm-sectionhead">
|
|
12706
|
-
<div><p class="slm-eyebrow">Section
|
|
12707
|
-
<div class="slm-windows" aria-label="
|
|
13170
|
+
<div><p class="slm-eyebrow">Section inventory</p><p class="slm-note">Configured booked value \xB7 booking momentum</p></div>
|
|
13171
|
+
<div class="slm-windows" aria-label="Booking momentum window">
|
|
12708
13172
|
${[5, 15, 30, 60].map((window2) => `<button class="slm-window" data-window="${window2}">${window2}m</button>`).join("")}
|
|
12709
13173
|
</div>
|
|
12710
13174
|
</div>
|
|
@@ -12743,8 +13207,8 @@ var SeatManager = class {
|
|
|
12743
13207
|
const sync = this.lastSyncedAt ? relTime(this.lastSyncedAt, Date.now()) : "waiting";
|
|
12744
13208
|
const presence = this.presenceCounts();
|
|
12745
13209
|
this.els.presence.innerHTML = `
|
|
12746
|
-
<div class="slm-healthitem" title="
|
|
12747
|
-
<div class="slm-healthitem" title="
|
|
13210
|
+
<div class="slm-healthitem" title="Active map sessions right now"><b>${presence ? presence.shoppingSessions.toLocaleString() : "\u2014"}</b><span>Viewing map</span></div>
|
|
13211
|
+
<div class="slm-healthitem" title="Sessions currently holding inventory"><b>${presence ? presence.activeHolds.toLocaleString() : "\u2014"}</b><span>Active holds</span></div>
|
|
12748
13212
|
<div class="slm-healthitem"><b>${connected ? "Healthy" : "Reconnecting"}</b><span>Live connection</span></div>
|
|
12749
13213
|
<div class="slm-healthitem"><b>${sync}</b><span>Last sync</span></div>`;
|
|
12750
13214
|
}
|
|
@@ -12754,10 +13218,10 @@ var SeatManager = class {
|
|
|
12754
13218
|
return;
|
|
12755
13219
|
}
|
|
12756
13220
|
const velocity = new Map(snapshot.velocity.bySection.map((row) => [row.sectionId, row]));
|
|
12757
|
-
const rows = [...snapshot.
|
|
13221
|
+
const rows = [...snapshot.bookedValue.bySection].sort((a, b) => {
|
|
12758
13222
|
const av = velocity.get(a.sectionId)?.netBooked ?? 0;
|
|
12759
13223
|
const bv = velocity.get(b.sectionId)?.netBooked ?? 0;
|
|
12760
|
-
return bv - av || b.
|
|
13224
|
+
return bv - av || b.bookedValue - a.bookedValue;
|
|
12761
13225
|
});
|
|
12762
13226
|
this.els.sections.innerHTML = rows.length ? rows.map((row) => {
|
|
12763
13227
|
const speed = velocity.get(row.sectionId);
|
|
@@ -12765,8 +13229,8 @@ var SeatManager = class {
|
|
|
12765
13229
|
const netLabel = `${net > 0 ? "+" : ""}${net}`;
|
|
12766
13230
|
const trend = speed?.trend === "rising" || speed?.trend === "cooling" ? speed.trend : "steady";
|
|
12767
13231
|
return `<button type="button" class="slm-sectionrow" data-section-focus="${esc2(row.sectionId)}" title="Focus ${esc2(row.sectionLabel)} on the map">
|
|
12768
|
-
<span class="slm-sectiontop"><span>${esc2(row.sectionLabel)}</span><span>${fmtMoney(row.
|
|
12769
|
-
<span class="slm-sectionmeta"><span>${row.booked.toLocaleString()}/${row.total.toLocaleString()}
|
|
13232
|
+
<span class="slm-sectiontop"><span>${esc2(row.sectionLabel)}</span><span>${fmtMoney(row.bookedValue, snapshot.currency)}</span></span>
|
|
13233
|
+
<span class="slm-sectionmeta"><span>${row.booked.toLocaleString()}/${row.total.toLocaleString()} booked \xB7 ${netLabel} in ${snapshot.velocity.windowMinutes}m</span><span class="slm-trend ${trend}">${trend}</span><span class="slm-sectionlocate">Locate</span></span>
|
|
12770
13234
|
</button>`;
|
|
12771
13235
|
}).join("") : '<div class="slm-empty">No section metrics are available for this chart.</div>';
|
|
12772
13236
|
this.paintTrendWindow();
|
|
@@ -12778,7 +13242,7 @@ var SeatManager = class {
|
|
|
12778
13242
|
this.renderer?.setSectionHeat(null);
|
|
12779
13243
|
return;
|
|
12780
13244
|
}
|
|
12781
|
-
const capacity = new Map(snapshot.
|
|
13245
|
+
const capacity = new Map(snapshot.bookedValue.bySection.map((row) => [row.sectionId, Math.max(1, row.total)]));
|
|
12782
13246
|
const rates = snapshot.velocity.bySection.map((row) => ({
|
|
12783
13247
|
sectionId: row.sectionId,
|
|
12784
13248
|
rate: Math.max(0, row.netBooked) / (capacity.get(row.sectionId) ?? 1) / snapshot.velocity.windowMinutes
|
|
@@ -12793,7 +13257,7 @@ var SeatManager = class {
|
|
|
12793
13257
|
if (!seat) {
|
|
12794
13258
|
this.els.rail.innerHTML = `
|
|
12795
13259
|
<p class="slm-eyebrow">Inspect seats</p>
|
|
12796
|
-
<p class="slm-hint">Select a seat to see its availability and
|
|
13260
|
+
<p class="slm-hint">Select a seat to see its availability and booking context. Nothing changes in this view.</p>
|
|
12797
13261
|
<div class="slm-empty">Select a seat on the map.</div>`;
|
|
12798
13262
|
return;
|
|
12799
13263
|
}
|
|
@@ -12802,7 +13266,7 @@ var SeatManager = class {
|
|
|
12802
13266
|
const sectionId = this.sectionByObject.get(seat.rowId) ?? import_core3.UNGROUPED_ID;
|
|
12803
13267
|
const sectionLabel = this.sectionLabelById.get(sectionId) ?? "Other seats";
|
|
12804
13268
|
const category = this.doc?.categories.find((item) => item.key === seat.categoryKey);
|
|
12805
|
-
const sectionMetric = this.controlRoomSnapshot?.
|
|
13269
|
+
const sectionMetric = this.controlRoomSnapshot?.bookedValue.bySection.find((row) => row.sectionId === sectionId);
|
|
12806
13270
|
const object = this.doc?.objects.find((item) => item.id === seat.rowId);
|
|
12807
13271
|
const location2 = object?.type === "row" ? { label: "Row", value: object.label } : object?.type === "table" ? { label: "Table", value: object.label } : seat.kind === "booth" ? { label: "Type", value: "Booth" } : null;
|
|
12808
13272
|
const itemKind = seat.kind === "booth" ? "Booth" : "Seat";
|
|
@@ -12816,8 +13280,8 @@ var SeatManager = class {
|
|
|
12816
13280
|
<div><span>Section</span><b>${esc2(sectionLabel)}</b></div>
|
|
12817
13281
|
${location2 ? `<div><span>${location2.label}</span><b>${esc2(location2.value)}</b></div>` : ""}
|
|
12818
13282
|
<div><span>Category</span><b>${esc2(category?.label ?? seat.categoryKey)}</b></div>
|
|
12819
|
-
<div><span>
|
|
12820
|
-
<div><span>Section
|
|
13283
|
+
<div><span>Booked in section</span><b>${sectionMetric ? `${sectionMetric.booked} of ${sectionMetric.total}` : "\u2014"}</b></div>
|
|
13284
|
+
<div><span>Section booked value</span><b>${sectionMetric && this.controlRoomSnapshot ? fmtMoney(sectionMetric.bookedValue, this.controlRoomSnapshot.currency) : "\u2014"}</b></div>
|
|
12821
13285
|
</div>
|
|
12822
13286
|
</div>`;
|
|
12823
13287
|
}
|