@necrolab/dashboard 0.4.3

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.
Files changed (240) hide show
  1. package/.claude/settings.local.json +45 -0
  2. package/.eslintrc.js +24 -0
  3. package/.prettierignore +1 -0
  4. package/.prettierrc +10 -0
  5. package/.vscode/extensions.json +3 -0
  6. package/ICONS.md +21 -0
  7. package/README.md +65 -0
  8. package/backend/api.js +430 -0
  9. package/backend/auth.js +62 -0
  10. package/backend/batching.js +43 -0
  11. package/backend/endpoints.js +343 -0
  12. package/backend/index.js +23 -0
  13. package/backend/mock-data.js +66 -0
  14. package/backend/mock-src/classes/logger.js +112 -0
  15. package/backend/mock-src/classes/utils.js +42 -0
  16. package/backend/mock-src/ticketmaster.js +92 -0
  17. package/backend/validator.js +62 -0
  18. package/config/configs.json +20 -0
  19. package/config/filter.json +3 -0
  20. package/config/presale.csv +3 -0
  21. package/config/proxies.txt +6 -0
  22. package/config/used-codes.json +4 -0
  23. package/index.html +114 -0
  24. package/index.js +2 -0
  25. package/jsconfig.json +16 -0
  26. package/package.json +48 -0
  27. package/postcss.config.js +6 -0
  28. package/postinstall.js +9 -0
  29. package/public/android-chrome-192x192.png +0 -0
  30. package/public/android-chrome-512x512.png +0 -0
  31. package/public/apple-touch-icon.png +0 -0
  32. package/public/favicon-16x16.png +0 -0
  33. package/public/favicon-32x32.png +0 -0
  34. package/public/favicon.ico +0 -0
  35. package/public/flags/ae.svg +1 -0
  36. package/public/flags/at.svg +1 -0
  37. package/public/flags/au.svg +1 -0
  38. package/public/flags/be.svg +1 -0
  39. package/public/flags/ch.svg +1 -0
  40. package/public/flags/cz.svg +1 -0
  41. package/public/flags/de.svg +1 -0
  42. package/public/flags/dk.svg +1 -0
  43. package/public/flags/es.svg +1 -0
  44. package/public/flags/nl.svg +1 -0
  45. package/public/flags/no.svg +1 -0
  46. package/public/flags/nz.svg +1 -0
  47. package/public/flags/pl.svg +1 -0
  48. package/public/flags/se.svg +1 -0
  49. package/public/flags/uk.svg +1 -0
  50. package/public/flags/us.svg +1 -0
  51. package/public/img/award.svg +3 -0
  52. package/public/img/background.svg +14 -0
  53. package/public/img/bag_w.svg +12 -0
  54. package/public/img/banks/amex.svg +4 -0
  55. package/public/img/banks/mastercard.svg +4 -0
  56. package/public/img/banks/visa.svg +4 -0
  57. package/public/img/camera.svg +3 -0
  58. package/public/img/close.svg +3 -0
  59. package/public/img/controls/disable.svg +5 -0
  60. package/public/img/controls/enable.svg +5 -0
  61. package/public/img/groups.svg +3 -0
  62. package/public/img/hand.svg +3 -0
  63. package/public/img/key.svg +3 -0
  64. package/public/img/logo.png +0 -0
  65. package/public/img/logo_icon.png +0 -0
  66. package/public/img/logo_icon_2.png +0 -0
  67. package/public/img/logo_trans.png +0 -0
  68. package/public/img/loyalty.svg +3 -0
  69. package/public/img/mail.svg +3 -0
  70. package/public/img/pencil.svg +3 -0
  71. package/public/img/profile.svg +4 -0
  72. package/public/img/reload.svg +3 -0
  73. package/public/img/sandclock.svg +25 -0
  74. package/public/img/save.svg +5 -0
  75. package/public/img/savings.svg +3 -0
  76. package/public/img/scanner.svg +3 -0
  77. package/public/img/sell.svg +3 -0
  78. package/public/img/shield.svg +3 -0
  79. package/public/img/ski.svg +3 -0
  80. package/public/img/stadium.svg +8 -0
  81. package/public/img/stadium_w.svg +8 -0
  82. package/public/img/timer.svg +3 -0
  83. package/public/manifest.json +27 -0
  84. package/public/robots.txt +2 -0
  85. package/run +10 -0
  86. package/src/App.vue +307 -0
  87. package/src/assets/css/_input.scss +197 -0
  88. package/src/assets/css/main.scss +269 -0
  89. package/src/assets/css/tailwind.css +3 -0
  90. package/src/assets/img/award.svg +3 -0
  91. package/src/assets/img/background.svg +11 -0
  92. package/src/assets/img/camera.svg +3 -0
  93. package/src/assets/img/close.svg +3 -0
  94. package/src/assets/img/eyes/closed.svg +13 -0
  95. package/src/assets/img/eyes/open.svg +12 -0
  96. package/src/assets/img/groups.svg +3 -0
  97. package/src/assets/img/hand.svg +3 -0
  98. package/src/assets/img/key.svg +3 -0
  99. package/src/assets/img/logo.png +0 -0
  100. package/src/assets/img/logo_icon.png +0 -0
  101. package/src/assets/img/logo_icon_2.png +0 -0
  102. package/src/assets/img/logo_trans.png +0 -0
  103. package/src/assets/img/loyalty.svg +3 -0
  104. package/src/assets/img/mail.svg +3 -0
  105. package/src/assets/img/pencil.svg +3 -0
  106. package/src/assets/img/reload.svg +3 -0
  107. package/src/assets/img/savings.svg +3 -0
  108. package/src/assets/img/scanner.svg +3 -0
  109. package/src/assets/img/sell.svg +3 -0
  110. package/src/assets/img/shield.svg +3 -0
  111. package/src/assets/img/ski.svg +3 -0
  112. package/src/assets/img/square_check.svg +5 -0
  113. package/src/assets/img/square_uncheck.svg +5 -0
  114. package/src/assets/img/stadium.svg +8 -0
  115. package/src/assets/img/timer.svg +3 -0
  116. package/src/assets/img/wildcard.svg +7 -0
  117. package/src/components/Auth/LoginForm.vue +48 -0
  118. package/src/components/Editors/Account/Account.vue +119 -0
  119. package/src/components/Editors/Account/AccountCreator.vue +147 -0
  120. package/src/components/Editors/Account/AccountView.vue +87 -0
  121. package/src/components/Editors/Account/CreateAccount.vue +106 -0
  122. package/src/components/Editors/Profile/CreateProfile.vue +321 -0
  123. package/src/components/Editors/Profile/Profile.vue +142 -0
  124. package/src/components/Editors/Profile/ProfileCountryChooser.vue +75 -0
  125. package/src/components/Editors/Profile/ProfileView.vue +96 -0
  126. package/src/components/Editors/TagLabel.vue +16 -0
  127. package/src/components/Editors/TagToggle.vue +41 -0
  128. package/src/components/Filter/Filter.vue +409 -0
  129. package/src/components/Filter/FilterPreview.vue +236 -0
  130. package/src/components/Filter/PriceSortToggle.vue +105 -0
  131. package/src/components/Table/Header.vue +5 -0
  132. package/src/components/Table/Row.vue +5 -0
  133. package/src/components/Table/Table.vue +14 -0
  134. package/src/components/Table/index.js +4 -0
  135. package/src/components/Tasks/CheckStock.vue +62 -0
  136. package/src/components/Tasks/Controls/DesktopControls.vue +73 -0
  137. package/src/components/Tasks/Controls/MobileControls.vue +32 -0
  138. package/src/components/Tasks/Controls/index.js +3 -0
  139. package/src/components/Tasks/CreateTaskAXS.vue +339 -0
  140. package/src/components/Tasks/CreateTaskTM.vue +459 -0
  141. package/src/components/Tasks/MassEdit.vue +50 -0
  142. package/src/components/Tasks/QuickSettings.vue +167 -0
  143. package/src/components/Tasks/ScrapeVenue.vue +42 -0
  144. package/src/components/Tasks/Stats.vue +66 -0
  145. package/src/components/Tasks/Task.vue +296 -0
  146. package/src/components/Tasks/TaskLabel.vue +20 -0
  147. package/src/components/Tasks/TaskView.vue +126 -0
  148. package/src/components/Tasks/Utilities.vue +33 -0
  149. package/src/components/icons/Award.vue +8 -0
  150. package/src/components/icons/Bag.vue +8 -0
  151. package/src/components/icons/BagWhite.vue +8 -0
  152. package/src/components/icons/Box.vue +8 -0
  153. package/src/components/icons/Camera.vue +8 -0
  154. package/src/components/icons/Cart.vue +8 -0
  155. package/src/components/icons/Check.vue +5 -0
  156. package/src/components/icons/Checkmark.vue +11 -0
  157. package/src/components/icons/Click.vue +8 -0
  158. package/src/components/icons/Close.vue +21 -0
  159. package/src/components/icons/CloseX.vue +5 -0
  160. package/src/components/icons/Console.vue +13 -0
  161. package/src/components/icons/Down.vue +8 -0
  162. package/src/components/icons/Edit.vue +13 -0
  163. package/src/components/icons/Event.vue +8 -0
  164. package/src/components/icons/Expand.vue +8 -0
  165. package/src/components/icons/Filter.vue +13 -0
  166. package/src/components/icons/Gear.vue +8 -0
  167. package/src/components/icons/Group.vue +8 -0
  168. package/src/components/icons/Hand.vue +8 -0
  169. package/src/components/icons/Key.vue +21 -0
  170. package/src/components/icons/Logout.vue +13 -0
  171. package/src/components/icons/Loyalty.vue +8 -0
  172. package/src/components/icons/Mail.vue +8 -0
  173. package/src/components/icons/Menu.vue +8 -0
  174. package/src/components/icons/Pause.vue +5 -0
  175. package/src/components/icons/Pencil.vue +21 -0
  176. package/src/components/icons/Play.vue +8 -0
  177. package/src/components/icons/Plus.vue +8 -0
  178. package/src/components/icons/Profile.vue +18 -0
  179. package/src/components/icons/Reload.vue +7 -0
  180. package/src/components/icons/Sandclock.vue +33 -0
  181. package/src/components/icons/Savings.vue +8 -0
  182. package/src/components/icons/Scanner.vue +8 -0
  183. package/src/components/icons/Scrape.vue +8 -0
  184. package/src/components/icons/Sell.vue +21 -0
  185. package/src/components/icons/Shield.vue +8 -0
  186. package/src/components/icons/Shrink.vue +8 -0
  187. package/src/components/icons/Ski.vue +8 -0
  188. package/src/components/icons/Spinner.vue +42 -0
  189. package/src/components/icons/SquareCheck.vue +18 -0
  190. package/src/components/icons/SquareUncheck.vue +18 -0
  191. package/src/components/icons/Stadium.vue +13 -0
  192. package/src/components/icons/StadiumWhite.vue +13 -0
  193. package/src/components/icons/Status.vue +8 -0
  194. package/src/components/icons/Tag.vue +8 -0
  195. package/src/components/icons/Tasks.vue +13 -0
  196. package/src/components/icons/Ticket.vue +8 -0
  197. package/src/components/icons/Timer.vue +8 -0
  198. package/src/components/icons/Trash.vue +8 -0
  199. package/src/components/icons/Up.vue +10 -0
  200. package/src/components/icons/Wildcard.vue +18 -0
  201. package/src/components/icons/index.js +111 -0
  202. package/src/components/ui/Modal.vue +61 -0
  203. package/src/components/ui/Navbar.vue +207 -0
  204. package/src/components/ui/ReconnectIndicator.vue +90 -0
  205. package/src/components/ui/Splash.vue +24 -0
  206. package/src/components/ui/controls/CountryChooser.vue +87 -0
  207. package/src/components/ui/controls/EyeToggle.vue +11 -0
  208. package/src/components/ui/controls/atomic/Checkbox.vue +28 -0
  209. package/src/components/ui/controls/atomic/Dropdown.vue +138 -0
  210. package/src/components/ui/controls/atomic/LoadingButton.vue +45 -0
  211. package/src/components/ui/controls/atomic/MultiDropdown.vue +262 -0
  212. package/src/components/ui/controls/atomic/Switch.vue +84 -0
  213. package/src/libs/Filter.js +593 -0
  214. package/src/libs/ansii.js +565 -0
  215. package/src/libs/panzoom.js +1413 -0
  216. package/src/main.js +23 -0
  217. package/src/registerServiceWorker.js +32 -0
  218. package/src/router/index.js +65 -0
  219. package/src/stores/cities.json +1 -0
  220. package/src/stores/connection.js +399 -0
  221. package/src/stores/countries.js +88 -0
  222. package/src/stores/logger.js +103 -0
  223. package/src/stores/requests.js +88 -0
  224. package/src/stores/sampleData.js +1034 -0
  225. package/src/stores/ui.js +584 -0
  226. package/src/stores/utils.js +554 -0
  227. package/src/types/index.js +42 -0
  228. package/src/utils/debug.js +1 -0
  229. package/src/views/Accounts.vue +191 -0
  230. package/src/views/Console.vue +224 -0
  231. package/src/views/Editor.vue +785 -0
  232. package/src/views/FilterBuilder.vue +785 -0
  233. package/src/views/Login.vue +27 -0
  234. package/src/views/Profiles.vue +209 -0
  235. package/src/views/Tasks.vue +157 -0
  236. package/static/offline.html +184 -0
  237. package/tailwind.config.js +57 -0
  238. package/vite.config.js +63 -0
  239. package/vue.config.js +32 -0
  240. package/workbox-config.js +66 -0
@@ -0,0 +1,785 @@
1
+ <template>
2
+ <div class="filter-builder-container w-full min-h-0 flex flex-col overflow-hidden">
3
+ <!-- Heading -->
4
+ <div class="flex-between pt-5 pb-2 mx-4 mt-2">
5
+ <div class="flex-center gap-4">
6
+ <FilterIcon class="cursor-pointer smooth-hover text-white" />
7
+ <h4 class="text-heading">Filter creator</h4>
8
+ </div>
9
+ <div class="flex items-center">
10
+ <input
11
+ class="h-10 text-white text-sm p-2 bg-dark-500 w-48 flex items-center rounded-l relative border-2 border-dark-550"
12
+ placeholder="Event ID"
13
+ v-model="eventId"
14
+ />
15
+ <button
16
+ class="h-10 text-white text-sm px-3 bg-dark-400 flex items-center rounded-r relative font-medium border-2 border-dark-550 smooth-hover"
17
+ @click="updateShownVenue"
18
+ >
19
+ Load
20
+ </button>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="card-dark mx-4 mb-3 p-3 overflow-hidden">
25
+ <div class="w-full h-full">
26
+ <!-- Main -->
27
+ <div class="grid grid-cols-1 lg:grid-cols-5 gap-3 lg:gap-4 w-full h-full">
28
+ <!-- Map -->
29
+ <div
30
+ class="col-span-1 lg:col-span-3 w-full h-full rounded-lg relative flex flex-col lg:max-w-none lg:overflow-hidden"
31
+ >
32
+ <div v-if="svg" class="flex items-center mb-1">
33
+ <div class="flex items-center justify-between w-20 px-2 text-white font-black text-sm">
34
+ <span class="cursor-pointer" @click="handleZoom(true)">+</span>
35
+ <span class="cursor-pointer" @click="handleZoom(false)">-</span>
36
+ <ReloadIcon class="cursor-pointer w-4 h-4" @click="handleZoom('r')" />
37
+ </div>
38
+ </div>
39
+ <div class="overflow-hidden selecto-wrapper flex-1">
40
+ <div
41
+ v-if="svg"
42
+ class="h-full overflow-auto hidden-scrollbars p-2 rounded shadow border-2 border-dark-550 svg-container"
43
+ >
44
+ <div class="svg-wrapper" id="svg-wrapper" v-html="svg"></div>
45
+ </div>
46
+ <div
47
+ v-else
48
+ class="h-full flex items-center justify-center p-2 rounded shadow border-2 border-dark-550 svg-container"
49
+ >
50
+ <div class="text-center text-dark-400">
51
+ <StadiumIcon class="mx-auto mb-2 w-8 h-8 opacity-50" />
52
+ <p class="text-sm">Enter an event ID and click "Load" to display the venue map</p>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ <div class="col-span-1 lg:col-span-2 w-full flex flex-col h-full">
58
+ <div class="flex justify-between mb-2 items-center text-white">
59
+ <div class="rounded flex justify-between gap-2 items-center" v-if="hasLoaded">
60
+ <PriceSortToggle
61
+ :current="filterBuilder.globalFilter.priceSort"
62
+ class="smooth-hover"
63
+ @change="(e) => filterBuilder.updateGlobalFilter({ priceSort: e })"
64
+ />
65
+
66
+ <input
67
+ type="number"
68
+ :value="filterBuilder.globalFilter.maxPrice"
69
+ @input="
70
+ (e) =>
71
+ filterBuilder.updateGlobalFilter({
72
+ maxPrice: parseInt(e.target.value || 0)
73
+ })
74
+ "
75
+ class="w-14 bg-dark-500 border-2 border-dark-550 px-1 pl-2 h-8 rounded"
76
+ placeholder="max"
77
+ />
78
+ </div>
79
+ </div>
80
+ <Table class="border-2 border-dark-550 shadow-xl flex-1 flex flex-col">
81
+ <Header class="flex-shrink-0">
82
+ <div class="flex items-center font-bold gap-2 justify-between w-full">
83
+ <div class="flex items-center gap-2">
84
+ <span class="text-base">Filters</span>
85
+ <span class="text-base text-light-300">{{ filterBuilder.filters.length }}</span>
86
+ <PriceSortToggle
87
+ class="w-14 smooth-hover"
88
+ :options="['All', 'WL', 'BL']"
89
+ @change="(e) => (shownFilters = e)"
90
+ />
91
+ </div>
92
+ <div class="flex gap-2 items-center">
93
+ <button
94
+ class="header-btn save-btn"
95
+ @click="saveFilter"
96
+ >
97
+ <EditIcon class="w-4 h-4" />
98
+ <span class="lg:block hidden">Save</span>
99
+ </button>
100
+ <button
101
+ class="header-btn clear-btn"
102
+ @click="filterBuilder.reset(false)"
103
+ >
104
+ <TrashIcon class="w-4 h-4" />
105
+ <span class="lg:block hidden">Clear</span>
106
+ </button>
107
+ </div>
108
+ </div>
109
+ </Header>
110
+ <div class="overflow-auto hidden-scrollbars flex-1">
111
+ <div v-if="filterBuilder.filters.length" class="filters-container">
112
+ <draggable
113
+ :list="filterBuilder.filters"
114
+ handle=".handle"
115
+ item-key="id"
116
+ ghost-class="sortable-ghost"
117
+ chosen-class="sortable-chosen"
118
+ drag-class="sortable-drag"
119
+ :animation="200"
120
+ :easing="'cubic-bezier(0.4, 0, 0.2, 1)'"
121
+ :force-fallback="false"
122
+ @change="
123
+ () => {
124
+ filterBuilder.updateCss();
125
+ cssUpdateTrigger++;
126
+ }
127
+ "
128
+ >
129
+ <template #item="{ element: f, index: i }">
130
+ <Filter
131
+ v-if="doesFilterShow(f)"
132
+ :filter="f"
133
+ :index="i"
134
+ :filterBuilder="filterBuilder"
135
+ class="compact-filter"
136
+ />
137
+ </template>
138
+ </draggable>
139
+ </div>
140
+ <div v-else class="empty-state flex flex-col items-center justify-center py-8 text-center">
141
+ <FilterIcon class="w-12 h-12 text-dark-400 mb-3 opacity-50" />
142
+ <p class="text-dark-400 text-sm">No filters yet</p>
143
+ <p class="text-dark-500 text-xs mt-1">Click on the map to create filters</p>
144
+ </div>
145
+ </div>
146
+ </Table>
147
+ <div class="flex items-center justify-between text-white gap-2 mt-1 mb-4 md:mb-0 flex-shrink-0">
148
+ <button
149
+ @click="addWildcardFilter"
150
+ :disabled="hasWildcardFilter"
151
+ :class="[
152
+ 'border-2 rounded border-dark-550 px-2 h-7 text-xs bg-dark-500 overflow-hidden shadow transition-all duration-200',
153
+ hasWildcardFilter
154
+ ? 'text-gray opacity-50 cursor-not-allowed'
155
+ : 'text-gray smooth-hover hover:bg-dark-400 hover:border-light-300'
156
+ ]"
157
+ :title="hasWildcardFilter ? 'Wildcard filter already exists' : 'Add wildcard filter'"
158
+ >
159
+ * Wildcard
160
+ </button>
161
+ <button
162
+ @click="ui.toggleModal('preview-filter')"
163
+ class="border-2 gap-1 flex justify-between items-center rounded border-dark-550 px-2 h-7 text-gray text-xs bg-dark-500 overflow-hidden shadow smooth-hover"
164
+ >
165
+ <CameraIcon class="w-3 h-3" />
166
+ JSON
167
+ </button>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+
173
+ <transition-group name="fade" mode="out-in">
174
+ <FilterPreview v-if="activeModal === 'preview-filter'" :filter="filterBuilder" />
175
+ </transition-group>
176
+ </div>
177
+ </div>
178
+ </template>
179
+
180
+ <script setup>
181
+ import draggable from "vuedraggable";
182
+ import { ref, nextTick, watch, computed, onMounted, onUnmounted } from "vue";
183
+ import { onBeforeRouteLeave } from "vue-router";
184
+ import { Table, Header } from "@/components/Table";
185
+ import Filter from "@/components/Filter/Filter.vue";
186
+ import { FilterIcon } from "@/components/icons";
187
+ import DragSelect from "dragselect";
188
+ import { DEBUG } from "@/utils/debug";
189
+
190
+ import { ReloadIcon, TrashIcon, EditIcon, CameraIcon, StadiumIcon } from "@/components/icons";
191
+ import { sendSaveFilter } from "@/stores/requests";
192
+ import PriceSortToggle from "@/components/Filter/PriceSortToggle.vue";
193
+ import { useUIStore } from "@/stores/ui";
194
+ import FilterBuilder from "@/libs/Filter";
195
+ import panzoom from "@/libs/panzoom.js";
196
+ import FilterPreview from "@/components/Filter/FilterPreview.vue";
197
+ const activeModal = computed(() => ui.activeModal);
198
+
199
+ const ui = useUIStore();
200
+ const hasLoaded = ref(false);
201
+
202
+ // let isShiftPressed = false;
203
+ // window.onkeyup = (e) => {
204
+ // if (e.keyCode === 16) isShiftPressed = false;
205
+ // };
206
+ // window.onkeydown = (e) => {
207
+ // if (e.keyCode === 16) isShiftPressed = true;
208
+ // };
209
+
210
+ // const doDragSelect = () => {
211
+ // // https://dragselect.com/docs/API/Settings
212
+ // const ds = new DragSelect({
213
+ // selectables: [
214
+ // ...document.querySelectorAll("path[row]"),
215
+ // ...document.querySelectorAll("path[generaladmission]")
216
+ // ],
217
+ // area: document.getElementById("svg-wrapper"),
218
+ // selectionThreshold: 0.1,
219
+ // multiSelectKeys: []
220
+ // });
221
+
222
+ // ds.subscribe("DS:start:pre", () => {
223
+ // // ui.logger.Info("DS:start:pre - shift pressed:", isShiftPressed);
224
+ // if (!isShiftPressed) {
225
+ // ds.stop(false, false);
226
+ // ds.start();
227
+ // } else {
228
+ // panzoomInstance.pause();
229
+ // }
230
+ // });
231
+
232
+ // const parseSelected = (items) => {
233
+ // const parsed = items
234
+ // .map((e) => {
235
+ // return {
236
+ // section: e.attributes.sectionname?.nodeValue || e.attributes.name?.nodeValue,
237
+ // row: e.attributes.row?.nodeValue,
238
+ // GA: e.attributes.generaladmission?.nodeValue,
239
+ // name: e.attributes.name?.nodeValue
240
+ // };
241
+ // })
242
+ // .filter((e) => !filterBuilder.value.isUnselectable(e.section, e.row));
243
+
244
+ // const sectionMapping = {};
245
+ // parsed
246
+ // .filter((p) => !p.GA)
247
+ // .forEach((p) => {
248
+ // if (!sectionMapping[p.section]) sectionMapping[p.section] = [];
249
+ // sectionMapping[p.section].push(p);
250
+ // });
251
+ // parsed
252
+ // .filter((p) => p.GA)
253
+ // .forEach((p) => {
254
+ // sectionMapping[p.section] = {
255
+ // GA: true,
256
+ // section: p.section,
257
+ // name: p.name
258
+ // };
259
+ // });
260
+ // return sectionMapping;
261
+ // };
262
+
263
+ // ds.subscribe("DS:update:pre", (e) => {
264
+ // // ui.logger.Info("DS:update:pre - shift pressed:", isShiftPressed, e.items);
265
+ // filterBuilder.value.isDragging = true;
266
+ // if (!isShiftPressed) return;
267
+ // const sectionMapping = parseSelected(e.items);
268
+ // filterBuilder.value.clearHighlight();
269
+ // Object.entries(sectionMapping).forEach(([section, rows]) => {
270
+ // if (rows.GA) {
271
+ // const isSelected = filterBuilder.value.filters.find(
272
+ // (f) => filterBuilder.value.isForCurrentEvent(f) && f.section === section
273
+ // );
274
+ // if (isSelected) return;
275
+ // filterBuilder.value.highlight({ section: section }, true);
276
+ // } else
277
+ // rows.forEach((row) => {
278
+ // const isSelected = filterBuilder.value.filters.find(
279
+ // (f) =>
280
+ // (f.section === section && f.rows?.includes(row.row)) ||
281
+ // (!f.rows && f.section === section && filterBuilder.value.isForCurrentEvent(f))
282
+ // );
283
+ // if (isSelected) return;
284
+
285
+ // filterBuilder.value.highlight({ section, row: row.row });
286
+ // });
287
+ // });
288
+ // });
289
+
290
+ // ds.subscribe("DS:end", (e) => {
291
+ // filterBuilder.value.isDragging = false;
292
+ // panzoomInstance.resume();
293
+ // filterBuilder.value.clearHighlight();
294
+ // [...document.querySelectorAll("path")].map((f) => (f.style = ""));
295
+ // if (!isShiftPressed) return;
296
+ // const sectionMapping = parseSelected(e.items);
297
+ // const secNameMapping = filterBuilder.value.getSectionNameMapping();
298
+
299
+ // Object.entries(sectionMapping).forEach(([section, rows]) => {
300
+ // if (rows.GA) {
301
+ // const isSelected = filterBuilder.value.filters.find(
302
+ // (f) => filterBuilder.value.isForCurrentEvent(f) && f.section === secNameMapping[section]
303
+ // );
304
+ // if (isSelected) return;
305
+ // filterBuilder.value.addFilter({
306
+ // event: filterBuilder.value.currentEventId,
307
+ // section: secNameMapping[section]
308
+ // });
309
+ // } else {
310
+ // const unselectedRows = [];
311
+ // rows.forEach((row) => {
312
+ // const isSelected = filterBuilder.value.filters.find(
313
+ // (f) =>
314
+ // (f.section === section && f.rows?.includes(row.row)) ||
315
+ // (!f.rows && f.section === section && filterBuilder.value.isForCurrentEvent(f))
316
+ // );
317
+ // if (isSelected) return;
318
+ // unselectedRows.push(row.row);
319
+ // });
320
+ // if (unselectedRows.length === 0) return;
321
+ // filterBuilder.value.addFilter({
322
+ // event: filterBuilder.value.currentEventId,
323
+ // section,
324
+ // rows: unselectedRows
325
+ // });
326
+ // }
327
+ // });
328
+ // isShiftPressed = false;
329
+ // });
330
+ // };
331
+
332
+ const panzoomOptions = {
333
+ maxZoom: 8, // max zoom-in factor
334
+ minZoom: 0.8, // max zoom-out factor
335
+ panOnlyWhenZoomed: true,
336
+ bounds: true,
337
+ boundsPadding: 0.1, // Add padding to ensure it stays within bounds
338
+ transformOrigin: { x: 0.5, y: 0.5 }, // Center the zoom
339
+ contain: true // Force the image to stay within the parent container
340
+ };
341
+ let panzoomInstance;
342
+
343
+ const svg = ref("");
344
+ const eventId = ref("");
345
+ const renderSeats = ref(false);
346
+ let renderer;
347
+
348
+ const shownFilters = ref("All");
349
+ const filterBuilder = ref(new FilterBuilder());
350
+
351
+ const hasWildcardFilter = computed(() => {
352
+ return filterBuilder.value.filters.some(filter =>
353
+ filter.buyAny && filterBuilder.value.isForCurrentEvent(filter)
354
+ );
355
+ });
356
+
357
+ const addWildcardFilter = () => {
358
+ if (!hasWildcardFilter.value) {
359
+ filterBuilder.value.addFilter({ buyAny: true, eventId: filterBuilder.value.currentEventId });
360
+ }
361
+ };
362
+
363
+ // Initialize RendererFactory
364
+ let RendererFactory;
365
+ if (DEBUG) {
366
+ RendererFactory = class {
367
+ constructor() {}
368
+ };
369
+ } else RendererFactory = import("@necrolab/tm-renderer");
370
+
371
+ // Real-time CSS injection system
372
+ let styleElement = null;
373
+ const cssUpdateTrigger = ref(0);
374
+ const STYLE_ELEMENT_ID = "filter-builder-styles";
375
+
376
+ const injectStyles = () => {
377
+ // Only inject styles if we have an SVG to style
378
+ const svgWrapper = document.getElementById("svg-wrapper");
379
+ if (!svgWrapper || !svg.value) return;
380
+
381
+ if (!styleElement) {
382
+ // Remove any existing style element first
383
+ const existingStyle = document.getElementById(STYLE_ELEMENT_ID);
384
+ if (existingStyle) {
385
+ existingStyle.remove();
386
+ }
387
+
388
+ styleElement = document.createElement("style");
389
+ styleElement.id = STYLE_ELEMENT_ID;
390
+ document.head.appendChild(styleElement);
391
+ }
392
+
393
+ const combinedCSS = filterBuilder.value.cssClasses + filterBuilder.value.temporaryCSS;
394
+ if (styleElement.textContent !== combinedCSS) {
395
+ styleElement.textContent = combinedCSS;
396
+ }
397
+ };
398
+
399
+ // Setup real-time CSS updates
400
+ const setupCSSUpdates = () => {
401
+ // Override updateCss method to trigger immediate updates
402
+ const originalUpdateCss = filterBuilder.value.updateCss.bind(filterBuilder.value);
403
+ filterBuilder.value.updateCss = () => {
404
+ originalUpdateCss();
405
+ nextTick(() => {
406
+ injectStyles();
407
+ cssUpdateTrigger.value++;
408
+ });
409
+ };
410
+
411
+ // Override highlight method for immediate temporary CSS
412
+ const originalHighlight = filterBuilder.value.highlight.bind(filterBuilder.value);
413
+ filterBuilder.value.highlight = (...args) => {
414
+ originalHighlight(...args);
415
+ nextTick(() => {
416
+ injectStyles();
417
+ });
418
+ };
419
+
420
+ // Override clearHighlight method
421
+ const originalClearHighlight = filterBuilder.value.clearHighlight.bind(filterBuilder.value);
422
+ filterBuilder.value.clearHighlight = () => {
423
+ originalClearHighlight();
424
+ nextTick(() => {
425
+ injectStyles();
426
+ });
427
+ };
428
+ };
429
+
430
+ // Cleanup function
431
+ const cleanupStyles = () => {
432
+ // Remove by ID to ensure we get it even if reference is lost
433
+ const existingStyle = document.getElementById(STYLE_ELEMENT_ID);
434
+ if (existingStyle) {
435
+ existingStyle.remove();
436
+ }
437
+
438
+ if (styleElement) {
439
+ styleElement.remove();
440
+ styleElement = null;
441
+ }
442
+ };
443
+
444
+ // Basic cleanup monitoring
445
+ const debugCleanup = () => {
446
+ console.log("FilterBuilder cleanup completed");
447
+ };
448
+
449
+ // Clean up any leftover styles from previous instances
450
+ cleanupStyles();
451
+
452
+ // Initialize CSS system
453
+ setupCSSUpdates();
454
+
455
+ // Watch for reactive updates
456
+ watch(
457
+ cssUpdateTrigger,
458
+ () => {
459
+ injectStyles();
460
+ },
461
+ { immediate: true }
462
+ );
463
+
464
+ // Cleanup on unmount
465
+ onUnmounted(() => {
466
+ cleanupStyles();
467
+ });
468
+
469
+ // Also cleanup on route change using beforeRouteLeave
470
+ onBeforeRouteLeave(() => {
471
+ cleanupStyles();
472
+ });
473
+
474
+ const doesFilterShow = (filter) => {
475
+ if ((filter.event || filter.eventId) !== filterBuilder.value.currentEventId) return;
476
+ if (!["BL", "WL"].includes(shownFilters.value)) return true;
477
+ const filterState = filter.exclude ? "BL" : "WL";
478
+ return shownFilters.value === filterState;
479
+ };
480
+
481
+ let rendererFactory;
482
+ if (!DEBUG)
483
+ RendererFactory.then((r) => {
484
+ rendererFactory = new r.default();
485
+ rendererFactory.init();
486
+ }).catch((error) => {
487
+ console.error("Failed to initialize renderer:", error);
488
+ });
489
+
490
+ const updateShownVenue = async () => {
491
+ eventId.value = eventId.value.trim();
492
+ if (eventId.value.includes("/event/")) eventId.value = eventId.value.split("/event/")[1];
493
+
494
+ const country = eventId.value.length === 16 ? null : ui.currentCountry.id;
495
+
496
+ if (!country && eventId.value.length !== 16) {
497
+ ui.showError("Invalid eventId!");
498
+ return;
499
+ }
500
+
501
+ renderer = rendererFactory.createRenderer(eventId.value, "", country);
502
+ filterBuilder.value.highlightedSeatColor = renderer.c.highlightedSeatColor;
503
+ renderer.c.logFullError = true;
504
+ renderer.c.includeDetailedAttributes = true;
505
+ renderer.c.renderRowBlocks = true;
506
+ renderer.setUrlProxy("/api/cors?url=");
507
+
508
+ try {
509
+ await renderer.render();
510
+ svg.value = renderer.svg;
511
+ } catch (e) {
512
+ ui.logger.Error("COULD NOT RENDER SVG", e);
513
+ ui.showError(e.message);
514
+ }
515
+
516
+ if (!svg.value) {
517
+ ui.logger.Error("Could not render SVG");
518
+ ui.showError("Could not fetch event map");
519
+ await loadFilter();
520
+ filterBuilder.value.reload(eventId.value);
521
+ return;
522
+ }
523
+
524
+ await nextTick();
525
+ if (panzoomInstance?.dispose) panzoomInstance.dispose();
526
+ const elem = document.getElementById("svg-wrapper");
527
+ try {
528
+ // Ensure SVG has appropriate sizing before initializing panzoom
529
+ if (elem && elem.children[0]) {
530
+ const svg = elem.children[0];
531
+ const isDesktop = window.innerWidth >= 1024;
532
+
533
+ // Ensure SVG doesn't exceed container width
534
+ svg.style.maxWidth = "100%";
535
+ svg.style.height = "auto";
536
+ svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
537
+
538
+ // Additional desktop constraints to prevent expansion
539
+ if (isDesktop) {
540
+ svg.style.maxHeight = "100%";
541
+ svg.style.width = "100%";
542
+ svg.style.objectFit = "contain";
543
+ }
544
+
545
+ // Initialize panzoom with updated options
546
+ panzoomInstance = panzoom(svg, panzoomOptions);
547
+
548
+ // Reset to initial position
549
+ panzoomInstance.moveTo(0, 0);
550
+ panzoomInstance.zoomAbs(0, 0, 1);
551
+ }
552
+ } catch (e) {
553
+ ui.logger.Error("Couldnt use panzoom", e);
554
+ }
555
+ await nextTick();
556
+ await loadFilter();
557
+ filterBuilder.value.reload(eventId.value);
558
+
559
+ // Re-setup CSS system after reload
560
+ setupCSSUpdates();
561
+ filterBuilder.value.updateCss();
562
+ // doDragSelect();
563
+ };
564
+
565
+ const loadFilter = async () => {
566
+ try {
567
+ const res = await fetch(`/api/filter/load?eventId=${eventId.value}`);
568
+ const data = await res.json();
569
+ if (eventId.value) ui.showSuccess("Loaded filter data");
570
+ filterBuilder.value.reset(true);
571
+ filterBuilder.value.updateGlobalFilter(data.globalFilter || {});
572
+ data.filters?.forEach((f) => filterBuilder.value.addFilter(f));
573
+ hasLoaded.value = true;
574
+ } catch (e) {
575
+ ui.showError("Couldn't load filter data");
576
+ ui.logger.Error("Error loading filter data", e);
577
+ }
578
+ };
579
+
580
+ const saveFilter = async () => {
581
+ const data = filterBuilder.value.out();
582
+ try {
583
+ await sendSaveFilter(data);
584
+ ui.showSuccess("Saved filter data");
585
+ } catch (e) {
586
+ ui.showError("Couldn't save filter data");
587
+ ui.logger.Info("Error saving filter data", e);
588
+ }
589
+ };
590
+
591
+ const handleZoom = (zoomEvent) => {
592
+ // handle zoom reset
593
+ if (zoomEvent === "r") {
594
+ // Reset both zoom and position
595
+ panzoomInstance.moveTo(0, 0);
596
+ panzoomInstance.zoomAbs(0, 0, 1);
597
+ return;
598
+ }
599
+
600
+ // Get current transform
601
+ const { scale } = panzoomInstance.getTransform();
602
+
603
+ // Calculate new scale based on zoom direction
604
+ const newScale = zoomEvent ? scale + scale / 3 : scale - scale / 3;
605
+
606
+ // Apply zoom with bounds checking
607
+ if (newScale >= panzoomOptions.minZoom && newScale <= panzoomOptions.maxZoom) {
608
+ panzoomInstance.smoothZoom(0, 0, newScale / scale);
609
+ }
610
+ };
611
+
612
+ watch(renderSeats, async () => {
613
+ ui.logger.Info("Updated renderSeats", renderSeats.value);
614
+ await updateShownVenue();
615
+ });
616
+
617
+ (async () => {
618
+ await loadFilter();
619
+ })();
620
+ </script>
621
+
622
+ <style scoped>
623
+ .slider-dim {
624
+ width: 60px;
625
+ height: 30px;
626
+ }
627
+
628
+ .compact-filter {
629
+ font-size: 0.75rem;
630
+ padding: 0.25rem !important;
631
+ }
632
+
633
+ .compact-filter .handle {
634
+ transform: scale(0.8);
635
+ }
636
+
637
+ /* Enhanced table and filter styling */
638
+ .filters-container {
639
+ @apply space-y-1;
640
+ }
641
+
642
+ /* Dragging states for better UX */
643
+ .sortable-ghost {
644
+ @apply opacity-30;
645
+ border: 1px solid #4A4A61;
646
+ background-color: rgba(74, 74, 97, 0.1);
647
+ }
648
+
649
+ .sortable-chosen .drag-handle {
650
+ border: 1px solid #4A4A61;
651
+ background-color: rgba(74, 74, 97, 0.2);
652
+ }
653
+
654
+ .sortable-drag {
655
+ @apply shadow-xl transform rotate-1 scale-105 z-50;
656
+ }
657
+
658
+ /* Header button styling */
659
+ .header-btn {
660
+ @apply flex items-center gap-2 px-3 py-2 border rounded-md font-medium text-sm transition-all duration-200 hover:scale-105;
661
+ }
662
+
663
+ .save-btn {
664
+ background-color: rgba(34, 197, 94, 0.2);
665
+ border-color: rgba(74, 222, 128, 0.5);
666
+ }
667
+
668
+ .save-btn:hover {
669
+ background-color: rgba(34, 197, 94, 0.3);
670
+ }
671
+
672
+ .clear-btn {
673
+ background-color: rgba(239, 68, 68, 0.2);
674
+ border-color: rgba(248, 113, 113, 0.5);
675
+ }
676
+
677
+ .clear-btn:hover {
678
+ background-color: rgba(239, 68, 68, 0.3);
679
+ }
680
+
681
+ .filter-builder-container {
682
+ height: 90vh;
683
+ overflow: hidden;
684
+ }
685
+
686
+ .card-dark {
687
+ height: calc(100vh - 175px);
688
+ min-height: 500px;
689
+ overflow: hidden;
690
+ }
691
+
692
+ @media (max-width: 768px) {
693
+ .filter-builder-container {
694
+ overflow: auto;
695
+ }
696
+
697
+ .card-dark {
698
+ overflow: auto;
699
+ }
700
+ }
701
+
702
+ /* Desktop: enforce grid column constraints */
703
+ @media (min-width: 1024px) {
704
+ .lg\\:grid-cols-5 > .lg\\:col-span-3 {
705
+ max-width: 60%; /* 3/5 of grid */
706
+ flex-shrink: 0;
707
+ }
708
+ }
709
+
710
+ .svg-container {
711
+ min-height: 300px;
712
+ height: 100%;
713
+ width: 100%;
714
+ position: relative;
715
+ overflow: auto;
716
+ }
717
+
718
+ /* Desktop: constrain svg-container to prevent expansion */
719
+ @media (min-width: 1024px) {
720
+ .svg-container {
721
+ position: relative;
722
+ max-width: 100%;
723
+ max-height: 100%;
724
+ overflow: hidden;
725
+ }
726
+ }
727
+ </style>
728
+
729
+ <style>
730
+ /* Global styles needed for dynamically injected SVG content and Filter.js CSS injection */
731
+ .svg-wrapper {
732
+ position: relative;
733
+ transform-origin: 0 0;
734
+ min-width: 100%;
735
+ min-height: 100%;
736
+ }
737
+
738
+ /* Desktop constraints - prevent svg-wrapper from expanding beyond container */
739
+ @media (min-width: 1024px) {
740
+ .svg-wrapper {
741
+ position: absolute;
742
+ top: 0;
743
+ left: 0;
744
+ width: 100%;
745
+ height: 100%;
746
+ max-width: 100%;
747
+ max-height: 100%;
748
+ min-width: unset;
749
+ min-height: unset;
750
+ overflow: hidden;
751
+ }
752
+ }
753
+
754
+ .svg-wrapper > svg {
755
+ width: 100%;
756
+ height: auto;
757
+ background: center center no-repeat;
758
+ background-size: contain;
759
+ }
760
+
761
+ /* Desktop SVG constraints */
762
+ @media (min-width: 1024px) {
763
+ .svg-wrapper > svg {
764
+ max-width: 100%;
765
+ max-height: 100%;
766
+ }
767
+ }
768
+
769
+ /* Ensure Filter.js generated CSS works properly */
770
+ .svg-wrapper path {
771
+ pointer-events: auto;
772
+ cursor: pointer;
773
+ }
774
+
775
+ .svg-wrapper path[generaladmission] {
776
+ pointer-events: auto;
777
+ cursor: pointer;
778
+ }
779
+
780
+ /* Hover effects for general admission seats */
781
+ .svg-wrapper path[generaladmission]:hover {
782
+ pointer-events: all;
783
+ cursor: pointer;
784
+ }
785
+ </style>