@necrolab/dashboard 0.5.21 → 0.5.23
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/package.json +1 -3
- package/src/components/Filter/Filter.vue +26 -9
- package/src/libs/Filter.js +103 -23
- package/src/libs/tm-renderer/axs/renderer.js +97 -0
- package/src/libs/tm-renderer/axs/requests.js +37 -0
- package/src/libs/tm-renderer/base-renderer.js +171 -0
- package/src/libs/tm-renderer/dependencies/logger.js +186 -0
- package/src/libs/tm-renderer/dependencies/node.persist.js +100 -0
- package/src/libs/tm-renderer/dependencies/persist.js +23 -0
- package/src/libs/tm-renderer/dependencies/web.persist.js +64 -0
- package/src/libs/tm-renderer/factory.js +47 -0
- package/src/libs/tm-renderer/index.js +21 -0
- package/src/libs/tm-renderer/request-utils.js +101 -0
- package/src/libs/tm-renderer/tm/renderer.js +568 -0
- package/src/libs/tm-renderer/tm/requests.js +47 -0
- package/src/libs/tm-renderer/tm/tm-utils.js +40 -0
- package/src/libs/tm-renderer/utils.js +90 -0
- package/src/views/FilterBuilder.vue +269 -64
- package/vite.config.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@necrolab/dashboard",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "rm -rf dist && vite build && npx workbox-cli generateSW workbox-config.cjs",
|
|
@@ -8,13 +8,11 @@
|
|
|
8
8
|
"bot": "node dev-server.js",
|
|
9
9
|
"expose": "node dev-server.js",
|
|
10
10
|
"postinstall": "node postinstall.js",
|
|
11
|
-
"updaterenderer": "npm i @necrolab/tm-renderer@latest",
|
|
12
11
|
"preview": "vite preview",
|
|
13
12
|
"lint": "npx eslint src/ --fix"
|
|
14
13
|
},
|
|
15
14
|
"dependencies": {
|
|
16
15
|
"@msgpack/msgpack": "^3.0.0-beta2",
|
|
17
|
-
"@necrolab/tm-renderer": "^0.1.12",
|
|
18
16
|
"@vitejs/plugin-vue": "^5.2.1",
|
|
19
17
|
"@vueuse/core": "^11.3.0",
|
|
20
18
|
"autoprefixer": "^10.4.21",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
<div class="grid grid-cols-12 items-center py-3 px-2 sm:px-4 gap-2 sm:gap-3">
|
|
11
11
|
<div class="col-span-9 sm:col-span-10">
|
|
12
12
|
<div class="flex-gap-2 items-center sm:gap-3 cursor-pointer flex-1" @click="handleFilterClick(filter.id)">
|
|
13
|
+
<div
|
|
14
|
+
class="filter-color-dot flex-shrink-0"
|
|
15
|
+
:style="{ backgroundColor: color }">
|
|
16
|
+
</div>
|
|
13
17
|
<div class="filter-type-badge flex-shrink-0">
|
|
14
18
|
<component :is="getFilterIcon()" class="w-3 h-3 sm:w-4 sm:h-4" />
|
|
15
19
|
</div>
|
|
@@ -169,6 +173,10 @@ const props = defineProps({
|
|
|
169
173
|
filterBuilder: {
|
|
170
174
|
type: Object,
|
|
171
175
|
required: true
|
|
176
|
+
},
|
|
177
|
+
color: {
|
|
178
|
+
type: String,
|
|
179
|
+
default: "#7bc999"
|
|
172
180
|
}
|
|
173
181
|
});
|
|
174
182
|
|
|
@@ -292,15 +300,7 @@ props.filterBuilder.onUpdate(() => {
|
|
|
292
300
|
|
|
293
301
|
<style scoped>
|
|
294
302
|
.filter-card {
|
|
295
|
-
@apply bg-dark-500 border border-dark-625/30 relative mb-2 transition-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
.filter-card:hover {
|
|
299
|
-
transform: scale(1.03);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
.filter-card:active {
|
|
303
|
-
transform: scale(0.98);
|
|
303
|
+
@apply bg-dark-500 border border-dark-625/30 relative mb-2 transition-colors duration-200;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
.filter-card:hover:not(.expanded-filter) {
|
|
@@ -315,6 +315,11 @@ props.filterBuilder.onUpdate(() => {
|
|
|
315
315
|
@apply border-t border-t-dark-625/20;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
.filter-color-dot {
|
|
319
|
+
@apply w-2.5 h-2.5 rounded-full flex-shrink-0;
|
|
320
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
|
321
|
+
}
|
|
322
|
+
|
|
318
323
|
.filter-type-badge {
|
|
319
324
|
@apply w-6 h-6 sm:w-8 sm:h-8 rounded-full bg-dark-400 flex items-center justify-center flex-shrink-0;
|
|
320
325
|
}
|
|
@@ -353,17 +358,29 @@ props.filterBuilder.onUpdate(() => {
|
|
|
353
358
|
|
|
354
359
|
.drag-btn {
|
|
355
360
|
@apply text-light-500;
|
|
361
|
+
transition: all 0.2s ease;
|
|
356
362
|
|
|
357
363
|
&:hover {
|
|
358
364
|
@apply bg-dark-300 border-dark-550 text-yellow-400 shadow-md;
|
|
365
|
+
transform: scale(1.1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
&:active {
|
|
369
|
+
transform: scale(0.95);
|
|
359
370
|
}
|
|
360
371
|
}
|
|
361
372
|
|
|
362
373
|
.delete-btn {
|
|
363
374
|
@apply bg-error-500/30 border-error-400/50 text-error-300;
|
|
375
|
+
transition: all 0.2s ease;
|
|
364
376
|
|
|
365
377
|
&:hover {
|
|
366
378
|
@apply bg-error-400/80 border-error-500 text-white shadow-md;
|
|
379
|
+
transform: scale(1.1);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
&:active {
|
|
383
|
+
transform: scale(0.95);
|
|
367
384
|
}
|
|
368
385
|
}
|
|
369
386
|
|
package/src/libs/Filter.js
CHANGED
|
@@ -3,14 +3,55 @@ const log = (...args) => DEBUG && console.log("[filter]", ...args);
|
|
|
3
3
|
|
|
4
4
|
const colors = {
|
|
5
5
|
HIGHLIGHT: "#d3f8e2",
|
|
6
|
-
SELECTED: "#
|
|
7
|
-
SELECTED_EXPANDED: "#
|
|
6
|
+
SELECTED: "#7bc999",
|
|
7
|
+
SELECTED_EXPANDED: "#a0ddb5",
|
|
8
8
|
EXCLUDED: "#f694c1",
|
|
9
9
|
EXCLUDED_EXPANDED: "#f472b6",
|
|
10
10
|
UNSELECTABLE: "#311432"
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// Color palette for unique filter colors - distinct but professional
|
|
14
|
+
const filterColorPalette = [
|
|
15
|
+
"#7bc999", // Mint green
|
|
16
|
+
"#6ba8d6", // Steel blue
|
|
17
|
+
"#d4a876", // Tan/gold
|
|
18
|
+
"#a88bc9", // Soft purple
|
|
19
|
+
"#e89ba3", // Coral pink
|
|
20
|
+
"#7dc9c9", // Turquoise
|
|
21
|
+
"#c9a87b", // Warm tan
|
|
22
|
+
"#8bc99a", // Light green
|
|
23
|
+
"#9ba8c9", // Periwinkle
|
|
24
|
+
"#c9b88b" // Khaki
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const getFilterColor = (filterIndex, isExpanded, isExcluded) => {
|
|
28
|
+
if (isExcluded) {
|
|
29
|
+
return isExpanded ? colors.EXCLUDED_EXPANDED : colors.EXCLUDED;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Use palette colors for normal filters
|
|
33
|
+
const baseColor = filterColorPalette[filterIndex % filterColorPalette.length];
|
|
34
|
+
|
|
35
|
+
if (!isExpanded) {
|
|
36
|
+
return baseColor;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Make expanded filters brighter
|
|
40
|
+
const hex = baseColor.replace('#', '');
|
|
41
|
+
const r = parseInt(hex.substr(0, 2), 16);
|
|
42
|
+
const g = parseInt(hex.substr(2, 2), 16);
|
|
43
|
+
const b = parseInt(hex.substr(4, 2), 16);
|
|
44
|
+
|
|
45
|
+
// Lighten by 20%
|
|
46
|
+
const lighter = (c) => Math.min(255, Math.floor(c + (255 - c) * 0.3));
|
|
47
|
+
return `#${lighter(r).toString(16).padStart(2, '0')}${lighter(g).toString(16).padStart(2, '0')}${lighter(b).toString(16).padStart(2, '0')}`;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const BASE_CSS = `
|
|
51
|
+
.svg-wrapper path[generaladmission]:hover {fill: ${colors.HIGHLIGHT};pointer-events:all;cursor:pointer;}
|
|
52
|
+
.svg-wrapper path {pointer-events: all; cursor: pointer; touch-action: manipulation;}
|
|
53
|
+
.svg-wrapper {touch-action: manipulation; -webkit-user-select: none; user-select: none;}
|
|
54
|
+
`;
|
|
14
55
|
|
|
15
56
|
const getAttributeValue = (element, attributeName) => {
|
|
16
57
|
try {
|
|
@@ -211,9 +252,12 @@ export default class FilterBuilder {
|
|
|
211
252
|
}
|
|
212
253
|
|
|
213
254
|
addRowHandlers() {
|
|
255
|
+
let lastHighlightedSection = null;
|
|
256
|
+
|
|
214
257
|
for (let i = 0; i < this.rows.length; i++) {
|
|
215
258
|
const row = this.rows[i];
|
|
216
|
-
|
|
259
|
+
|
|
260
|
+
const clickHandler = () => {
|
|
217
261
|
const parsed = {
|
|
218
262
|
section: getAttributeValue(row, "section"),
|
|
219
263
|
row: getAttributeValue(row, "row"),
|
|
@@ -245,6 +289,13 @@ export default class FilterBuilder {
|
|
|
245
289
|
});
|
|
246
290
|
};
|
|
247
291
|
|
|
292
|
+
// Support both click and touch events
|
|
293
|
+
row.onclick = clickHandler;
|
|
294
|
+
row.ontouchend = (e) => {
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
clickHandler();
|
|
297
|
+
};
|
|
298
|
+
|
|
248
299
|
row.onmouseover = () => {
|
|
249
300
|
const parsed = {
|
|
250
301
|
section: getAttributeValue(row, "section"),
|
|
@@ -252,6 +303,18 @@ export default class FilterBuilder {
|
|
|
252
303
|
id: getAttributeValue(row, "id")
|
|
253
304
|
};
|
|
254
305
|
|
|
306
|
+
const sectionKey = `${parsed.section}`;
|
|
307
|
+
|
|
308
|
+
// Skip if already highlighting this section - prevents flickering
|
|
309
|
+
if (lastHighlightedSection === sectionKey) return;
|
|
310
|
+
|
|
311
|
+
// Only clear if switching sections
|
|
312
|
+
if (lastHighlightedSection && lastHighlightedSection !== sectionKey) {
|
|
313
|
+
this.clearHighlight();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
lastHighlightedSection = sectionKey;
|
|
317
|
+
|
|
255
318
|
const matchingFilter = this.filters.find(
|
|
256
319
|
(f) =>
|
|
257
320
|
this.isForCurrentEvent(f) &&
|
|
@@ -266,6 +329,7 @@ export default class FilterBuilder {
|
|
|
266
329
|
};
|
|
267
330
|
|
|
268
331
|
row.onmouseout = () => {
|
|
332
|
+
lastHighlightedSection = null;
|
|
269
333
|
if (!this.isDragging) this.clearHighlight();
|
|
270
334
|
};
|
|
271
335
|
}
|
|
@@ -360,37 +424,39 @@ export default class FilterBuilder {
|
|
|
360
424
|
this.cssClasses = BASE_CSS;
|
|
361
425
|
const gaSectionNameMapping = this.getSectionNameMapping();
|
|
362
426
|
|
|
363
|
-
this.filters.forEach((filter) => {
|
|
427
|
+
this.filters.forEach((filter, filterIndex) => {
|
|
364
428
|
if (!this.isForCurrentEvent(filter)) return;
|
|
365
429
|
|
|
366
|
-
|
|
367
|
-
|
|
430
|
+
// Try to find the short name (attribute name) from the real name (stored in filter)
|
|
431
|
+
const shortName = Object.entries(gaSectionNameMapping).find(
|
|
432
|
+
([, v]) => v === filter.section
|
|
368
433
|
)?.[0];
|
|
369
434
|
|
|
370
|
-
if
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
435
|
+
// Check if this is a GA section by looking for paths with this name attribute
|
|
436
|
+
const isGA = shortName && document.querySelectorAll(`path[name="${shortName}"][generaladmission="true"]`)?.length > 0;
|
|
437
|
+
|
|
438
|
+
if (shortName && isGA) {
|
|
439
|
+
log(`GA section detected: ${filter.section} -> short name: ${shortName}`);
|
|
374
440
|
}
|
|
375
441
|
|
|
376
442
|
const type = this.getFilterType(filter);
|
|
377
443
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
444
|
+
// Use unique color for each filter
|
|
445
|
+
const color = getFilterColor(
|
|
446
|
+
filterIndex,
|
|
447
|
+
this.expandedFilter === filter.id,
|
|
448
|
+
filter.exclude
|
|
449
|
+
);
|
|
384
450
|
|
|
385
451
|
switch (type) {
|
|
386
452
|
case this.filterTypes.NORMAL:
|
|
387
453
|
// If it has no 'rows' property
|
|
388
454
|
if (!Array.isArray(filter.rows)) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
this.cssClasses += `.svg-wrapper path[name="${tryFindRealName}"] {fill: ${color} !important;}\n`;
|
|
455
|
+
if (isGA && shortName) {
|
|
456
|
+
// GA sections use 'name' attribute and 'fill' for styling
|
|
457
|
+
this.cssClasses += `.svg-wrapper path[name="${shortName}"][generaladmission="true"] {fill: ${color} !important;}\n`;
|
|
393
458
|
} else {
|
|
459
|
+
// Regular sections use 'section' attribute and 'stroke' for styling
|
|
394
460
|
this.cssClasses += `.svg-wrapper path[section="${filter.section}"] {stroke: ${color} !important;}\n`;
|
|
395
461
|
}
|
|
396
462
|
} else {
|
|
@@ -440,7 +506,7 @@ export default class FilterBuilder {
|
|
|
440
506
|
const label = labels[i];
|
|
441
507
|
const section = getAttributeValue(label, "section");
|
|
442
508
|
|
|
443
|
-
|
|
509
|
+
const labelClickHandler = () => {
|
|
444
510
|
// filter exists already for section - return
|
|
445
511
|
const matchingFilter = this.filters.find((f) => {
|
|
446
512
|
if (!this.isForCurrentEvent(f)) return false;
|
|
@@ -488,6 +554,13 @@ export default class FilterBuilder {
|
|
|
488
554
|
log("Could not add labelHandler", section, e);
|
|
489
555
|
}
|
|
490
556
|
};
|
|
557
|
+
|
|
558
|
+
// Support both click and touch
|
|
559
|
+
label.onclick = labelClickHandler;
|
|
560
|
+
label.ontouchend = (e) => {
|
|
561
|
+
e.preventDefault();
|
|
562
|
+
labelClickHandler();
|
|
563
|
+
};
|
|
491
564
|
}
|
|
492
565
|
}
|
|
493
566
|
|
|
@@ -504,7 +577,7 @@ export default class FilterBuilder {
|
|
|
504
577
|
const section = getAttributeValue(path, "name");
|
|
505
578
|
const sectionName = gaSectionNameMapping[section];
|
|
506
579
|
|
|
507
|
-
|
|
580
|
+
const gaClickHandler = () => {
|
|
508
581
|
// filter exists already for section
|
|
509
582
|
const matchingFilter = this.filters.find((f) => f.section === sectionName && this.isForCurrentEvent(f));
|
|
510
583
|
if (matchingFilter) {
|
|
@@ -523,6 +596,13 @@ export default class FilterBuilder {
|
|
|
523
596
|
event: this.currentEventId
|
|
524
597
|
});
|
|
525
598
|
};
|
|
599
|
+
|
|
600
|
+
// Support both click and touch
|
|
601
|
+
path.onclick = gaClickHandler;
|
|
602
|
+
path.ontouchend = (e) => {
|
|
603
|
+
e.preventDefault();
|
|
604
|
+
gaClickHandler();
|
|
605
|
+
};
|
|
526
606
|
}
|
|
527
607
|
}
|
|
528
608
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import BaseRenderer from "../base-renderer.js";
|
|
2
|
+
import { getEventData, getEventDataUrl } from "./requests.js";
|
|
3
|
+
|
|
4
|
+
class AXSRenderer extends BaseRenderer {
|
|
5
|
+
constructor(eventId, options = {}) {
|
|
6
|
+
super(eventId, options);
|
|
7
|
+
this.config = this.getDefaultConfig();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async render() {
|
|
11
|
+
if (!this.storage) {
|
|
12
|
+
this.logger.Error("Cannot render without storage");
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const data = await this.getEventData();
|
|
18
|
+
this.logger.Debug("Event data loaded");
|
|
19
|
+
|
|
20
|
+
if (this.fs) {
|
|
21
|
+
this.fs.writeFileSync("out.json", JSON.stringify(data));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let svgBody = "";
|
|
25
|
+
|
|
26
|
+
data.n.forEach((subsection) => {
|
|
27
|
+
let sectionData = "";
|
|
28
|
+
switch (subsection.h.t) {
|
|
29
|
+
case "seat":
|
|
30
|
+
subsection.n.forEach((seat) => {
|
|
31
|
+
sectionData += this.createElement("circle", {
|
|
32
|
+
cx: seat.c[0],
|
|
33
|
+
cy: seat.c[1],
|
|
34
|
+
id: seat.i,
|
|
35
|
+
r: "0.5",
|
|
36
|
+
fill: "#fff",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
break;
|
|
40
|
+
case "section":
|
|
41
|
+
subsection.n.forEach((polygon) => {
|
|
42
|
+
sectionData += this.createElement(
|
|
43
|
+
"g",
|
|
44
|
+
{
|
|
45
|
+
id: polygon.i,
|
|
46
|
+
transform: `translate(${polygon.c[0]} ${polygon.c[1]})`,
|
|
47
|
+
},
|
|
48
|
+
this.createElement("path", {
|
|
49
|
+
d: polygon.pt,
|
|
50
|
+
fill: "rgba(100,100,100,0.5)",
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
svgBody += this.createElement("g", {}, sectionData);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this.svg = this.createElement(
|
|
62
|
+
"svg",
|
|
63
|
+
{
|
|
64
|
+
width: "1440",
|
|
65
|
+
height: "816",
|
|
66
|
+
viewBox: "0 0 1200 680",
|
|
67
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
68
|
+
version: "1.1",
|
|
69
|
+
},
|
|
70
|
+
svgBody,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (this.fs) {
|
|
74
|
+
this.fs.writeFileSync("test.svg", this.svg);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return true;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
this.logger.Error(`Couldn't render ${this.eventId} ${e.message}`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async getEventData() {
|
|
85
|
+
return await getEventData(this.eventId, this.options.proxy, this.logger);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async getEventDataUrl() {
|
|
89
|
+
return await getEventDataUrl(this.eventId, this.options.proxy, this.logger);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async fetchBackgroundImage() {
|
|
93
|
+
throw new Error("Background image fetching not implemented for AXS renderer");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default AXSRenderer;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getJsonDataWithHeaders } from "../request-utils.js";
|
|
2
|
+
|
|
3
|
+
function buildAXSEventDataUrl(eventId, data) {
|
|
4
|
+
const base = data.url.replace("*", "");
|
|
5
|
+
const policy = data.keys["CloudFront-Policy"];
|
|
6
|
+
const sig = data.keys["CloudFront-Signature"];
|
|
7
|
+
const keypair = data.keys["CloudFront-Key-Pair-Id"];
|
|
8
|
+
const version = "2.6.16";
|
|
9
|
+
return `${base}maps/main/master_full.json?Policy=${policy}_&Signature=${sig}&Key-Pair-Id=${keypair}&v=${version}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function getEventDataUrl(eventId, proxy, logger) {
|
|
13
|
+
const url = `https://contentdistribution.3ddvapis.com/api/v1/dvm/token/venue/${eventId}`;
|
|
14
|
+
const customHeaders = {
|
|
15
|
+
"referer": "https://tickets-de.axs.com/",
|
|
16
|
+
"user-agent":
|
|
17
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return await getJsonDataWithHeaders(url, customHeaders, proxy, 0, logger);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function getEventData(eventId, proxy, logger) {
|
|
24
|
+
const urlData = await getEventDataUrl(eventId, proxy, logger);
|
|
25
|
+
if (!urlData) {
|
|
26
|
+
throw new Error("Failed to get event data URL");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const url = buildAXSEventDataUrl(eventId, urlData);
|
|
30
|
+
return await getJsonDataWithHeaders(url, {}, proxy, 0, logger);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
getEventData,
|
|
35
|
+
getEventDataUrl,
|
|
36
|
+
buildAXSEventDataUrl,
|
|
37
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { createLogger } from "./dependencies/logger.js";
|
|
2
|
+
import * as utils from "./utils.js";
|
|
3
|
+
|
|
4
|
+
class BaseRenderer {
|
|
5
|
+
constructor(eventId, options = {}) {
|
|
6
|
+
this.eventId = eventId;
|
|
7
|
+
this.options = {
|
|
8
|
+
proxy: null,
|
|
9
|
+
country: null,
|
|
10
|
+
...options,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
this.logger = createLogger([this.constructor.name, this.eventId]);
|
|
14
|
+
this.storage = null;
|
|
15
|
+
this.sharp = null;
|
|
16
|
+
this.fs = null;
|
|
17
|
+
this.svg = null;
|
|
18
|
+
this.bgImg = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
init(dependencies) {
|
|
22
|
+
this.storage = dependencies.storage;
|
|
23
|
+
this.sharp = dependencies.sharp;
|
|
24
|
+
this.fs = dependencies.fs;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setHighlightedSeats(placeIds, color = null) {
|
|
29
|
+
if (!this.highlighted) {
|
|
30
|
+
this.highlighted = [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const seatsToAdd = (placeIds || []).map((id) => ({
|
|
34
|
+
id,
|
|
35
|
+
color: color || this.getDefaultConfig().highlightedSeatColor,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
this.highlighted.push(...seatsToAdd);
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setCustomConfig(config) {
|
|
43
|
+
this.config = {
|
|
44
|
+
...this.getDefaultConfig(),
|
|
45
|
+
...config,
|
|
46
|
+
};
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getDefaultConfig() {
|
|
51
|
+
return {
|
|
52
|
+
// Basic rendering options
|
|
53
|
+
outputWidth: 2048,
|
|
54
|
+
includeDetailedAttributes: false,
|
|
55
|
+
logFullError: true,
|
|
56
|
+
|
|
57
|
+
// Colors
|
|
58
|
+
seatColor: "#0557ae",
|
|
59
|
+
nonAvSeatColor: "#dadcde",
|
|
60
|
+
highlightedSeatColor: "#d0006f",
|
|
61
|
+
GAColor: "#61c6c2",
|
|
62
|
+
nonAvGAColor: "#b4babe",
|
|
63
|
+
grayedOutSection: "#a3a3a3",
|
|
64
|
+
|
|
65
|
+
// Text styling
|
|
66
|
+
labelColor: "#000",
|
|
67
|
+
labelColorOnGA: "#fff",
|
|
68
|
+
textOpacity: 0.8,
|
|
69
|
+
textOpacityOnGA: 1,
|
|
70
|
+
fontSize: "1em",
|
|
71
|
+
font: "sans-serif",
|
|
72
|
+
useBig: true,
|
|
73
|
+
|
|
74
|
+
// Rendering behavior
|
|
75
|
+
dontRenderUnusedSections: false,
|
|
76
|
+
dontRenderSeats: false,
|
|
77
|
+
seatsAsPins: false,
|
|
78
|
+
renderRowBlocks: false,
|
|
79
|
+
increaseHighlightedSeatSize: 6,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async render() {
|
|
84
|
+
throw new Error("render() method must be implemented by subclass");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async renderSeatmap(priceData) {
|
|
88
|
+
// Store price data for subclasses to use
|
|
89
|
+
this.priceData = priceData;
|
|
90
|
+
throw new Error("renderSeatmap() method must be implemented by subclass");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async outputToSVG(path) {
|
|
94
|
+
if (!this.svg || !this.fs) {
|
|
95
|
+
this.logger.Error("No SVG data available or fs not initialized");
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
this.fs.writeFileSync(path, this.svg);
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async outputToPNG(filePath) {
|
|
103
|
+
if (!this.svg || !this.sharp) {
|
|
104
|
+
this.logger.Error("No SVG data available or sharp not initialized");
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
const s = await this._png();
|
|
108
|
+
await s.toFile(filePath);
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async outputToPNGBuffer() {
|
|
113
|
+
if (!this.svg || !this.sharp) {
|
|
114
|
+
return Buffer.from([0x00]);
|
|
115
|
+
}
|
|
116
|
+
const s = await this._png();
|
|
117
|
+
return s.toBuffer();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async _png() {
|
|
121
|
+
let background = await this.storage.getItem(`renderer.background.${this.eventId}`);
|
|
122
|
+
if (!background) {
|
|
123
|
+
try {
|
|
124
|
+
background = await this._fetchSaveBgImage();
|
|
125
|
+
} catch (e) {
|
|
126
|
+
this.logger.Error(e.message);
|
|
127
|
+
}
|
|
128
|
+
} else if (background) {
|
|
129
|
+
if (typeof background === "string" && background.startsWith("{")) {
|
|
130
|
+
background = JSON.parse(background);
|
|
131
|
+
}
|
|
132
|
+
background = Buffer.from(background);
|
|
133
|
+
const bgData = await this.sharp(background).metadata();
|
|
134
|
+
if (bgData.width !== this.config.outputWidth) {
|
|
135
|
+
this.config.outputWidth = bgData.width;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const svgBuffer = Buffer.from(this.svg);
|
|
140
|
+
const png = await this.sharp(svgBuffer).png().resize(this.config.outputWidth).toBuffer();
|
|
141
|
+
|
|
142
|
+
if (background) {
|
|
143
|
+
const foreground = await this.sharp(png).toBuffer();
|
|
144
|
+
return this.sharp(background).composite([
|
|
145
|
+
{
|
|
146
|
+
input: foreground,
|
|
147
|
+
gravity: "southeast",
|
|
148
|
+
},
|
|
149
|
+
]);
|
|
150
|
+
} else {
|
|
151
|
+
return this.sharp(png);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async _fetchSaveBgImage() {
|
|
156
|
+
const bgImgData = await this.fetchBackgroundImage();
|
|
157
|
+
const background = await this.sharp(bgImgData).png().resize(this.config.outputWidth).toBuffer();
|
|
158
|
+
await this.storage.setItem(`renderer.background.${this.eventId}`, background);
|
|
159
|
+
return background;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
createElement(tag, attrs, body = "") {
|
|
163
|
+
return utils.createElement(tag, attrs, body);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async fetchBackgroundImage() {
|
|
167
|
+
throw new Error("fetchBackgroundImage() method must be implemented by subclass");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default BaseRenderer;
|