@necrolab/dashboard 0.5.22 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@necrolab/dashboard",
3
- "version": "0.5.22",
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
 
@@ -307,6 +315,11 @@ props.filterBuilder.onUpdate(() => {
307
315
  @apply border-t border-t-dark-625/20;
308
316
  }
309
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
+
310
323
  .filter-type-badge {
311
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;
312
325
  }
@@ -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 {
@@ -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
- row.onclick = () => {
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,7 +424,7 @@ 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
430
  // Try to find the short name (attribute name) from the real name (stored in filter)
@@ -377,12 +441,12 @@ export default class FilterBuilder {
377
441
 
378
442
  const type = this.getFilterType(filter);
379
443
 
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
- }
444
+ // Use unique color for each filter
445
+ const color = getFilterColor(
446
+ filterIndex,
447
+ this.expandedFilter === filter.id,
448
+ filter.exclude
449
+ );
386
450
 
387
451
  switch (type) {
388
452
  case this.filterTypes.NORMAL:
@@ -442,7 +506,7 @@ export default class FilterBuilder {
442
506
  const label = labels[i];
443
507
  const section = getAttributeValue(label, "section");
444
508
 
445
- label.onclick = () => {
509
+ const labelClickHandler = () => {
446
510
  // filter exists already for section - return
447
511
  const matchingFilter = this.filters.find((f) => {
448
512
  if (!this.isForCurrentEvent(f)) return false;
@@ -490,6 +554,13 @@ export default class FilterBuilder {
490
554
  log("Could not add labelHandler", section, e);
491
555
  }
492
556
  };
557
+
558
+ // Support both click and touch
559
+ label.onclick = labelClickHandler;
560
+ label.ontouchend = (e) => {
561
+ e.preventDefault();
562
+ labelClickHandler();
563
+ };
493
564
  }
494
565
  }
495
566
 
@@ -506,7 +577,7 @@ export default class FilterBuilder {
506
577
  const section = getAttributeValue(path, "name");
507
578
  const sectionName = gaSectionNameMapping[section];
508
579
 
509
- path.onclick = () => {
580
+ const gaClickHandler = () => {
510
581
  // filter exists already for section
511
582
  const matchingFilter = this.filters.find((f) => f.section === sectionName && this.isForCurrentEvent(f));
512
583
  if (matchingFilter) {
@@ -525,6 +596,13 @@ export default class FilterBuilder {
525
596
  event: this.currentEventId
526
597
  });
527
598
  };
599
+
600
+ // Support both click and touch
601
+ path.onclick = gaClickHandler;
602
+ path.ontouchend = (e) => {
603
+ e.preventDefault();
604
+ gaClickHandler();
605
+ };
528
606
  }
529
607
  }
530
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;