@necrolab/dashboard 0.5.22 → 0.5.24

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.
@@ -10,7 +10,48 @@ const colors = {
10
10
  UNSELECTABLE: "#311432"
11
11
  };
12
12
 
13
- const BASE_CSS = `.svg-wrapper path[generaladmission]:hover {fill: ${colors.HIGHLIGHT};pointer-events:all;cursor:pointer;}`;
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 {
@@ -158,6 +199,8 @@ export default class FilterBuilder {
158
199
  };
159
200
  this.cssClasses = "";
160
201
  this.temporaryCSS = "";
202
+ this._highlightedElements = null;
203
+ this._currentlyHighlightedSection = null;
161
204
 
162
205
  this.filterTypes = {
163
206
  INVALID: -1,
@@ -211,9 +254,16 @@ export default class FilterBuilder {
211
254
  }
212
255
 
213
256
  addRowHandlers() {
257
+ // Track the currently highlighted section across all handlers
258
+ // This prevents flicker when moving mouse between rows
259
+ if (!this.lastHighlightedSection) {
260
+ this.lastHighlightedSection = null;
261
+ }
262
+
214
263
  for (let i = 0; i < this.rows.length; i++) {
215
264
  const row = this.rows[i];
216
- row.onclick = () => {
265
+
266
+ const clickHandler = () => {
217
267
  const parsed = {
218
268
  section: getAttributeValue(row, "section"),
219
269
  row: getAttributeValue(row, "row"),
@@ -245,6 +295,13 @@ export default class FilterBuilder {
245
295
  });
246
296
  };
247
297
 
298
+ // Support both click and touch events
299
+ row.onclick = clickHandler;
300
+ row.ontouchend = (e) => {
301
+ e.preventDefault();
302
+ clickHandler();
303
+ };
304
+
248
305
  row.onmouseover = () => {
249
306
  const parsed = {
250
307
  section: getAttributeValue(row, "section"),
@@ -252,6 +309,18 @@ export default class FilterBuilder {
252
309
  id: getAttributeValue(row, "id")
253
310
  };
254
311
 
312
+ const sectionKey = `${parsed.section}`;
313
+
314
+ // Skip if already highlighting this section - prevents flickering
315
+ if (this.lastHighlightedSection === sectionKey) return;
316
+
317
+ // Only clear if switching sections
318
+ if (this.lastHighlightedSection && this.lastHighlightedSection !== sectionKey) {
319
+ this.clearHighlight();
320
+ }
321
+
322
+ this.lastHighlightedSection = sectionKey;
323
+
255
324
  const matchingFilter = this.filters.find(
256
325
  (f) =>
257
326
  this.isForCurrentEvent(f) &&
@@ -265,21 +334,101 @@ export default class FilterBuilder {
265
334
  else this.highlight({ section: matchingFilter.section });
266
335
  };
267
336
 
268
- row.onmouseout = () => {
269
- if (!this.isDragging) this.clearHighlight();
270
- };
337
+ // Don't use onmouseout - it causes flicker when moving between rows in the same section
338
+ // clearHighlight is already handled when switching to a different section (line 312-314)
271
339
  }
272
340
  }
273
341
 
274
342
  reload(eventId) {
275
343
  const paths = document.querySelectorAll("path");
276
- this.rows = [...paths].filter((r) => r.id.startsWith("s_"));
344
+ // Include ALL paths that have a section attribute, not just rows
345
+ // This includes section containers, backgrounds, and any other paths
346
+ this.rows = [...paths].filter((r) => {
347
+ const hasSection = r.attributes['section'];
348
+ const isNotGA = !r.attributes['generaladmission'];
349
+ return hasSection && isNotGA;
350
+ });
277
351
 
278
352
  this.addLabelHandlers();
279
353
  this.addGAHandlers();
280
354
  this.addRowHandlers();
281
355
  this.currentEventId = eventId;
282
356
 
357
+ // Handle mouse events on the SVG wrapper
358
+ const svgWrapper = document.querySelector('.svg-wrapper');
359
+ if (svgWrapper) {
360
+ // Clear highlight when mouse leaves the entire SVG area
361
+ svgWrapper.onmouseleave = () => {
362
+ this.lastHighlightedSection = null;
363
+ if (!this.isDragging) this.clearHighlight();
364
+ };
365
+
366
+ // When hovering on SVG background (between rows), maintain current highlight
367
+ svgWrapper.onmouseover = (e) => {
368
+ const target = e.target;
369
+ if (target === svgWrapper || (target.tagName && target.tagName.toLowerCase() === 'svg')) {
370
+ e.stopPropagation();
371
+ return;
372
+ }
373
+ };
374
+
375
+ // CRITICAL: Handle clicks on SVG background (empty space)
376
+ // When user clicks empty space, create filter for currently highlighted section
377
+ svgWrapper.onclick = (e) => {
378
+ const target = e.target;
379
+ // Only handle clicks on background (not on path elements)
380
+ if (target === svgWrapper || (target.tagName && target.tagName.toLowerCase() === 'svg')) {
381
+ // If we have a highlighted section, create filter for it
382
+ if (this._currentlyHighlightedSection) {
383
+ const matchingFilter = this.filters.find(
384
+ (f) => this.isForCurrentEvent(f) && f.section === this._currentlyHighlightedSection
385
+ );
386
+
387
+ if (matchingFilter) {
388
+ // Toggle expanded state
389
+ if (this.expandedFilter === matchingFilter.id) {
390
+ this.setExpandedFilter(null);
391
+ } else {
392
+ this.setExpandedFilter(matchingFilter.id);
393
+ }
394
+ } else {
395
+ // Create new filter
396
+ this.addFilter({
397
+ section: this._currentlyHighlightedSection,
398
+ event: this.currentEventId
399
+ });
400
+ }
401
+ }
402
+ }
403
+ };
404
+
405
+ // Support touch events on background too
406
+ svgWrapper.ontouchend = (e) => {
407
+ const target = e.target;
408
+ if (target === svgWrapper || (target.tagName && target.tagName.toLowerCase() === 'svg')) {
409
+ e.preventDefault();
410
+ if (this._currentlyHighlightedSection) {
411
+ const matchingFilter = this.filters.find(
412
+ (f) => this.isForCurrentEvent(f) && f.section === this._currentlyHighlightedSection
413
+ );
414
+
415
+ if (matchingFilter) {
416
+ if (this.expandedFilter === matchingFilter.id) {
417
+ this.setExpandedFilter(null);
418
+ } else {
419
+ this.setExpandedFilter(matchingFilter.id);
420
+ }
421
+ } else {
422
+ this.addFilter({
423
+ section: this._currentlyHighlightedSection,
424
+ event: this.currentEventId
425
+ });
426
+ }
427
+ }
428
+ }
429
+ };
430
+ }
431
+
283
432
  this.updateCss();
284
433
  }
285
434
 
@@ -360,7 +509,7 @@ export default class FilterBuilder {
360
509
  this.cssClasses = BASE_CSS;
361
510
  const gaSectionNameMapping = this.getSectionNameMapping();
362
511
 
363
- this.filters.forEach((filter) => {
512
+ this.filters.forEach((filter, filterIndex) => {
364
513
  if (!this.isForCurrentEvent(filter)) return;
365
514
 
366
515
  // Try to find the short name (attribute name) from the real name (stored in filter)
@@ -377,12 +526,12 @@ export default class FilterBuilder {
377
526
 
378
527
  const type = this.getFilterType(filter);
379
528
 
380
- let color;
381
- if (filter.exclude) {
382
- color = this.expandedFilter === filter.id ? colors.EXCLUDED_EXPANDED : colors.EXCLUDED;
383
- } else {
384
- color = this.expandedFilter === filter.id ? colors.SELECTED_EXPANDED : colors.SELECTED;
385
- }
529
+ // Use unique color for each filter
530
+ const color = getFilterColor(
531
+ filterIndex,
532
+ this.expandedFilter === filter.id,
533
+ filter.exclude
534
+ );
386
535
 
387
536
  switch (type) {
388
537
  case this.filterTypes.NORMAL:
@@ -430,6 +579,11 @@ export default class FilterBuilder {
430
579
  this.cssClasses += `.svg-wrapper path[section="${section}"][row="${row}"] {stroke: ${color} !important;}\n`;
431
580
  });
432
581
 
582
+ // CRITICAL: Add hover styles AFTER filter styles so they win specificity battle
583
+ // Use BOTH stroke and fill to cover entire section area, not just row lines
584
+ this.cssClasses += `.svg-wrapper path.hover-highlight-stroke {stroke: ${colors.HIGHLIGHT} !important; fill: ${colors.HIGHLIGHT} !important; fill-opacity: 0.3 !important;}\n`;
585
+ this.cssClasses += `.svg-wrapper path.hover-highlight-fill {fill: ${colors.HIGHLIGHT} !important;}\n`;
586
+
433
587
  log("Generated CSS:", this.cssClasses);
434
588
  }
435
589
 
@@ -442,7 +596,7 @@ export default class FilterBuilder {
442
596
  const label = labels[i];
443
597
  const section = getAttributeValue(label, "section");
444
598
 
445
- label.onclick = () => {
599
+ const labelClickHandler = () => {
446
600
  // filter exists already for section - return
447
601
  const matchingFilter = this.filters.find((f) => {
448
602
  if (!this.isForCurrentEvent(f)) return false;
@@ -490,6 +644,43 @@ export default class FilterBuilder {
490
644
  log("Could not add labelHandler", section, e);
491
645
  }
492
646
  };
647
+
648
+ // Support both click and touch
649
+ label.onclick = labelClickHandler;
650
+ label.ontouchend = (e) => {
651
+ e.preventDefault();
652
+ labelClickHandler();
653
+ };
654
+
655
+ // Add hover handler to highlight section when hovering over label
656
+ label.onmouseover = () => {
657
+ const matchingFilter = this.filters.find((f) => {
658
+ if (!this.isForCurrentEvent(f)) return false;
659
+ if (f.section !== section && f.section !== gaSectionNameMapping[section]) return false;
660
+ return true;
661
+ });
662
+
663
+ if (!matchingFilter) return;
664
+
665
+ const sectionKey = matchingFilter.section;
666
+
667
+ // Skip if already highlighting this section
668
+ if (this.lastHighlightedSection === sectionKey) return;
669
+
670
+ // Clear if switching sections
671
+ if (this.lastHighlightedSection && this.lastHighlightedSection !== sectionKey) {
672
+ this.clearHighlight();
673
+ }
674
+
675
+ this.lastHighlightedSection = sectionKey;
676
+
677
+ // Highlight the section
678
+ if (Array.isArray(matchingFilter.rows)) {
679
+ matchingFilter.rows.forEach((row) => this.highlight({ section: matchingFilter.section, row: row }));
680
+ } else {
681
+ this.highlight({ section: matchingFilter.section });
682
+ }
683
+ };
493
684
  }
494
685
  }
495
686
 
@@ -506,7 +697,7 @@ export default class FilterBuilder {
506
697
  const section = getAttributeValue(path, "name");
507
698
  const sectionName = gaSectionNameMapping[section];
508
699
 
509
- path.onclick = () => {
700
+ const gaClickHandler = () => {
510
701
  // filter exists already for section
511
702
  const matchingFilter = this.filters.find((f) => f.section === sectionName && this.isForCurrentEvent(f));
512
703
  if (matchingFilter) {
@@ -525,6 +716,13 @@ export default class FilterBuilder {
525
716
  event: this.currentEventId
526
717
  });
527
718
  };
719
+
720
+ // Support both click and touch
721
+ path.onclick = gaClickHandler;
722
+ path.ontouchend = (e) => {
723
+ e.preventDefault();
724
+ gaClickHandler();
725
+ };
528
726
  }
529
727
  }
530
728
 
@@ -566,21 +764,43 @@ export default class FilterBuilder {
566
764
  highlight(filter, isGA = false) {
567
765
  if (filter.row && this.isUnselectable(filter.sec, filter.row)) return;
568
766
 
569
- let newCSS = "";
767
+ // Check if we're already highlighting this exact section
768
+ const sectionKey = filter.section || filter.name;
769
+ if (this._currentlyHighlightedSection === sectionKey) {
770
+ return; // Already highlighting this section
771
+ }
570
772
 
773
+ // Clear previous highlight first
774
+ this.clearHighlight();
775
+ this._currentlyHighlightedSection = sectionKey;
776
+
777
+ // Use direct DOM manipulation instead of CSS injection
778
+ // This is MUCH faster and prevents flicker
571
779
  if (isGA) {
572
780
  if (this.filters.find((f) => f.section === filter.name)) return;
573
- newCSS += `.svg-wrapper path[name="${filter.section || filter.name}"] {fill: ${colors.HIGHLIGHT} !important}`;
781
+ const paths = document.querySelectorAll(`path[name="${filter.section || filter.name}"]`);
782
+ paths.forEach(p => p.classList.add('hover-highlight-fill'));
783
+ this._highlightedElements = Array.from(paths);
574
784
  } else if (filter.row) {
575
- newCSS += `.svg-wrapper path[section="${filter.section}"][row="${filter.row}"] {stroke: ${colors.HIGHLIGHT} !important}`;
785
+ const paths = document.querySelectorAll(`path[section="${filter.section}"][row="${filter.row}"]`);
786
+ paths.forEach(p => p.classList.add('hover-highlight-stroke'));
787
+ this._highlightedElements = Array.from(paths);
576
788
  } else {
577
- newCSS += `.svg-wrapper path[section="${filter.section}"] {stroke: ${colors.HIGHLIGHT} !important}`;
789
+ const paths = document.querySelectorAll(`path[section="${filter.section}"]`);
790
+ paths.forEach(p => p.classList.add('hover-highlight-stroke'));
791
+ this._highlightedElements = Array.from(paths);
578
792
  }
579
- this.temporaryCSS += newCSS;
580
793
  }
581
794
 
582
795
  clearHighlight() {
583
- this.temporaryCSS = "";
796
+ // Remove classes from previously highlighted elements
797
+ if (this._highlightedElements) {
798
+ this._highlightedElements.forEach(el => {
799
+ el.classList.remove('hover-highlight-stroke', 'hover-highlight-fill');
800
+ });
801
+ this._highlightedElements = null;
802
+ }
803
+ this._currentlyHighlightedSection = null;
584
804
  }
585
805
 
586
806
  isUnselectable(section, row) {
@@ -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.logger);
86
+ }
87
+
88
+ async getEventDataUrl() {
89
+ return await getEventDataUrl(this.eventId, 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, 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, 0, logger);
21
+ }
22
+
23
+ async function getEventData(eventId, logger) {
24
+ const urlData = await getEventDataUrl(eventId, 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, {}, 0, logger);
31
+ }
32
+
33
+ export {
34
+ getEventData,
35
+ getEventDataUrl,
36
+ buildAXSEventDataUrl,
37
+ };
@@ -0,0 +1,93 @@
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.svg = null;
16
+ }
17
+
18
+ init(dependencies) {
19
+ this.storage = dependencies.storage;
20
+ return this;
21
+ }
22
+
23
+ setHighlightedSeats(placeIds, color = null) {
24
+ if (!this.highlighted) {
25
+ this.highlighted = [];
26
+ }
27
+
28
+ const seatsToAdd = (placeIds || []).map((id) => ({
29
+ id,
30
+ color: color || this.getDefaultConfig().highlightedSeatColor,
31
+ }));
32
+
33
+ this.highlighted.push(...seatsToAdd);
34
+ return this;
35
+ }
36
+
37
+ setCustomConfig(config) {
38
+ this.config = {
39
+ ...this.getDefaultConfig(),
40
+ ...config,
41
+ };
42
+ return this;
43
+ }
44
+
45
+ getDefaultConfig() {
46
+ return {
47
+ // Basic rendering options
48
+ outputWidth: 2048,
49
+ includeDetailedAttributes: false,
50
+ logFullError: true,
51
+
52
+ // Colors
53
+ seatColor: "#0557ae",
54
+ nonAvSeatColor: "#dadcde",
55
+ highlightedSeatColor: "#d0006f",
56
+ GAColor: "#61c6c2",
57
+ nonAvGAColor: "#b4babe",
58
+ grayedOutSection: "#a3a3a3",
59
+
60
+ // Text styling
61
+ labelColor: "#000",
62
+ labelColorOnGA: "#fff",
63
+ textOpacity: 0.8,
64
+ textOpacityOnGA: 1,
65
+ fontSize: "1em",
66
+ font: "sans-serif",
67
+ useBig: true,
68
+
69
+ // Rendering behavior
70
+ dontRenderUnusedSections: false,
71
+ dontRenderSeats: false,
72
+ seatsAsPins: false,
73
+ renderRowBlocks: false,
74
+ increaseHighlightedSeatSize: 6,
75
+ };
76
+ }
77
+
78
+ async render() {
79
+ throw new Error("render() method must be implemented by subclass");
80
+ }
81
+
82
+ async renderSeatmap(priceData) {
83
+ // Store price data for subclasses to use
84
+ this.priceData = priceData;
85
+ throw new Error("renderSeatmap() method must be implemented by subclass");
86
+ }
87
+
88
+ createElement(tag, attrs, body = "") {
89
+ return utils.createElement(tag, attrs, body);
90
+ }
91
+ }
92
+
93
+ export default BaseRenderer;