@necrolab/dashboard 0.4.220 → 0.5.1

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 (140) hide show
  1. package/.prettierrc +27 -1
  2. package/.vscode/extensions.json +1 -1
  3. package/README.md +64 -2
  4. package/artwork/image.png +0 -0
  5. package/backend/api.js +26 -24
  6. package/backend/auth.js +2 -2
  7. package/backend/batching.js +1 -1
  8. package/backend/endpoints.js +8 -11
  9. package/backend/index.js +2 -2
  10. package/backend/mock-data.js +27 -36
  11. package/backend/mock-src/classes/logger.js +5 -7
  12. package/backend/mock-src/classes/utils.js +3 -2
  13. package/backend/mock-src/ticketmaster.js +4 -4
  14. package/backend/validator.js +2 -2
  15. package/config/configs.json +0 -1
  16. package/dev-server.js +134 -0
  17. package/exit +209 -0
  18. package/index.html +78 -8
  19. package/index.js +1 -1
  20. package/jsconfig.json +16 -0
  21. package/package.json +39 -25
  22. package/postcss.config.js +1 -1
  23. package/postinstall.js +124 -20
  24. package/public/android-chrome-192x192.png +0 -0
  25. package/public/android-chrome-512x512.png +0 -0
  26. package/public/apple-touch-icon.png +0 -0
  27. package/public/favicon-16x16.png +0 -0
  28. package/public/favicon-32x32.png +0 -0
  29. package/public/favicon.ico +0 -0
  30. package/public/img/logo_trans.png +0 -0
  31. package/public/img/necro_logo.png +0 -0
  32. package/public/manifest.json +16 -10
  33. package/run +176 -9
  34. package/src/App.vue +498 -85
  35. package/src/assets/css/base/reset.scss +43 -0
  36. package/src/assets/css/base/scroll.scss +114 -0
  37. package/src/assets/css/base/typography.scss +37 -0
  38. package/src/assets/css/components/buttons.scss +216 -0
  39. package/src/assets/css/components/forms.scss +221 -0
  40. package/src/assets/css/components/modals.scss +13 -0
  41. package/src/assets/css/components/tables.scss +27 -0
  42. package/src/assets/css/components/toasts.scss +100 -0
  43. package/src/assets/css/main.scss +201 -122
  44. package/src/assets/img/background.svg +2 -2
  45. package/src/assets/img/background.svg.backup +11 -0
  46. package/src/assets/img/logo_trans.png +0 -0
  47. package/src/components/Auth/LoginForm.vue +62 -11
  48. package/src/components/Editors/Account/Account.vue +116 -40
  49. package/src/components/Editors/Account/AccountCreator.vue +88 -39
  50. package/src/components/Editors/Account/AccountView.vue +102 -34
  51. package/src/components/Editors/Account/CreateAccount.vue +80 -32
  52. package/src/components/Editors/Profile/CreateProfile.vue +269 -83
  53. package/src/components/Editors/Profile/Profile.vue +132 -47
  54. package/src/components/Editors/Profile/ProfileCountryChooser.vue +82 -20
  55. package/src/components/Editors/Profile/ProfileView.vue +89 -32
  56. package/src/components/Editors/TagLabel.vue +67 -6
  57. package/src/components/Editors/TagToggle.vue +7 -2
  58. package/src/components/Filter/Filter.vue +288 -71
  59. package/src/components/Filter/FilterPreview.vue +202 -31
  60. package/src/components/Filter/PriceSortToggle.vue +76 -6
  61. package/src/components/Table/Header.vue +1 -1
  62. package/src/components/Table/Row.vue +1 -1
  63. package/src/components/Table/Table.vue +19 -2
  64. package/src/components/Tasks/CheckStock.vue +6 -8
  65. package/src/components/Tasks/Controls/DesktopControls.vue +27 -17
  66. package/src/components/Tasks/Controls/MobileControls.vue +8 -45
  67. package/src/components/Tasks/CreateTaskAXS.vue +80 -72
  68. package/src/components/Tasks/CreateTaskTM.vue +95 -141
  69. package/src/components/Tasks/MassEdit.vue +4 -6
  70. package/src/components/Tasks/QuickSettings.vue +199 -30
  71. package/src/components/Tasks/ScrapeVenue.vue +5 -6
  72. package/src/components/Tasks/Stats.vue +50 -24
  73. package/src/components/Tasks/Task.vue +384 -179
  74. package/src/components/Tasks/TaskLabel.vue +2 -2
  75. package/src/components/Tasks/TaskView.vue +136 -48
  76. package/src/components/Tasks/Utilities.vue +25 -10
  77. package/src/components/Tasks/ViewTask.vue +321 -0
  78. package/src/components/icons/Bag.vue +1 -1
  79. package/src/components/icons/Check.vue +5 -0
  80. package/src/components/icons/Close.vue +21 -0
  81. package/src/components/icons/CloseX.vue +5 -0
  82. package/src/components/icons/Eye.vue +6 -0
  83. package/src/components/icons/Key.vue +21 -0
  84. package/src/components/icons/Loyalty.vue +1 -1
  85. package/src/components/icons/Mail.vue +2 -2
  86. package/src/components/icons/Pencil.vue +21 -0
  87. package/src/components/icons/Play.vue +2 -2
  88. package/src/components/icons/Profile.vue +18 -0
  89. package/src/components/icons/Reload.vue +4 -5
  90. package/src/components/icons/Sandclock.vue +2 -2
  91. package/src/components/icons/Sell.vue +21 -0
  92. package/src/components/icons/Spinner.vue +42 -0
  93. package/src/components/icons/SquareCheck.vue +18 -0
  94. package/src/components/icons/SquareUncheck.vue +18 -0
  95. package/src/components/icons/Stadium.vue +1 -1
  96. package/src/components/icons/Wildcard.vue +18 -0
  97. package/src/components/icons/index.js +26 -1
  98. package/src/components/ui/Modal.vue +107 -13
  99. package/src/components/ui/Navbar.vue +175 -40
  100. package/src/components/ui/ReconnectIndicator.vue +351 -55
  101. package/src/components/ui/Splash.vue +5 -35
  102. package/src/components/ui/controls/CountryChooser.vue +200 -62
  103. package/src/components/ui/controls/atomic/Checkbox.vue +119 -10
  104. package/src/components/ui/controls/atomic/Dropdown.vue +216 -39
  105. package/src/components/ui/controls/atomic/LoadingButton.vue +45 -0
  106. package/src/components/ui/controls/atomic/MultiDropdown.vue +300 -37
  107. package/src/components/ui/controls/atomic/Switch.vue +53 -25
  108. package/src/composables/useClickOutside.js +21 -0
  109. package/src/composables/useDropdownPosition.js +174 -0
  110. package/src/libs/Filter.js +60 -24
  111. package/src/registerServiceWorker.js +1 -1
  112. package/src/stores/connection.js +4 -4
  113. package/src/stores/sampleData.js +172 -199
  114. package/src/stores/ui.js +55 -20
  115. package/src/stores/utils.js +30 -4
  116. package/src/types/index.js +41 -0
  117. package/src/utils/debug.js +1 -0
  118. package/src/views/Accounts.vue +116 -50
  119. package/src/views/Console.vue +394 -79
  120. package/src/views/Editor.vue +1176 -123
  121. package/src/views/FilterBuilder.vue +528 -250
  122. package/src/views/Login.vue +76 -14
  123. package/src/views/Profiles.vue +119 -34
  124. package/src/views/Tasks.vue +266 -98
  125. package/static/offline.html +192 -50
  126. package/switch-branch.sh +41 -0
  127. package/tailwind.config.js +119 -27
  128. package/vite.config.js +73 -16
  129. package/workbox-config.cjs +63 -0
  130. package/ICONS.md +0 -21
  131. package/public/img/background.svg +0 -14
  132. package/public/img/logo.png +0 -0
  133. package/public/img/logo_icon.png +0 -0
  134. package/public/img/logo_icon_2.png +0 -0
  135. package/src/assets/css/_input.scss +0 -143
  136. package/src/assets/img/logo.png +0 -0
  137. package/src/assets/img/logo_icon.png +0 -0
  138. package/src/assets/img/logo_icon_2.png +0 -0
  139. package/vue.config.js +0 -32
  140. package/workbox-config.js +0 -7
@@ -1,60 +1,76 @@
1
1
  <template>
2
- <div v-html="`<style>${filterBuilder.cssClasses}</style>`" />
3
- <div v-html="`<style>${filterBuilder.temporaryCSS}</style>`" />
4
- <div class="ios-wrapper mt-3">
5
- <div class="bg-dark-500 mt-6 m-4 p-2 rounded shadow border-2 border-dark-550">
6
- <div class="lg:px-3 px-0 mx-3 pb-8 mt-5">
2
+ <div class="filter-builder-container flex min-h-0 w-full flex-col overflow-hidden">
3
+ <!-- Heading -->
4
+ <div class="mt-3 flex items-center justify-between pb-1 pt-4">
5
+ <div class="flex items-center justify-center gap-4">
6
+ <FilterIcon class="smooth-hover cursor-pointer text-white" />
7
+ <h4 class="text-base font-semibold text-light-300">Filter creator</h4>
8
+ </div>
9
+ <div class="unified-load-group flex items-center h-9">
10
+ <input
11
+ class="relative flex h-full w-40 items-center bg-transparent px-2 text-sm text-white"
12
+ placeholder="Event ID"
13
+ v-model="eventId" />
14
+ <button
15
+ class="relative flex h-full items-center px-3 text-sm font-medium text-white bg-transparent"
16
+ @click="updateShownVenue">
17
+ Load
18
+ </button>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="mb-2 overflow-hidden rounded border border-dark-650 bg-dark-400 p-3 shadow-sm">
23
+ <div class="h-full w-full">
7
24
  <!-- Main -->
8
- <div class="grid grid-cols-1 md:grid-cols-5 gap-0 md:gap-8">
25
+ <div class="grid h-full w-full grid-cols-1 gap-3 lg:grid-cols-5 lg:gap-4">
9
26
  <!-- Map -->
10
- <div class="col-span-3 w-auto h-auto rounded-lg relative">
11
- <!-- Heading -->
12
- <div class="text-white">
13
- <h4 class="text-2xl font-bold" @click="filterBuilder.log()">Filter creator</h4>
14
- <div class="flex items-center">
15
- <input
16
- class="h-6 mt-1 text-sm p-2 bg-dark-500 w-40 flex items-center rounded-l relative border-2 border-dark-550"
17
- placeholder="Event"
18
- v-model="eventId"
19
- />
20
- <button
21
- class="h-6 mt-1 text-sm p-2 bg-dark-550 flex items-center rounded-r relative font-bold border-2 border-dark-550 smooth-hover"
22
- @click="updateShownVenue"
23
- >
24
- Load
25
- </button>
26
- </div>
27
- </div>
28
- <div v-if="svg" class="flex items-center">
29
- <div class="flex items-center justify-between w-20 px-2 text-white font-black text-xl">
27
+ <div
28
+ class="relative col-span-1 flex h-full w-full flex-col rounded-lg lg:col-span-3 lg:max-w-none lg:overflow-hidden">
29
+ <div v-if="svg" class="mb-1 flex items-center">
30
+ <div class="flex w-20 items-center justify-between px-2 text-sm font-black text-white">
30
31
  <span class="cursor-pointer" @click="handleZoom(true)">+</span>
31
32
  <span class="cursor-pointer" @click="handleZoom(false)">-</span>
32
- <ReloadIcon class="cursor-pointer" @click="handleZoom('r')" />
33
-
34
- <!-- <h3 class="ml-10 text-sm text-white">Auto</h3>
35
- <Switch class="scale-75 slider-dim" v-model="renderSeats" /> -->
33
+ <ReloadIcon class="h-4 w-4 cursor-pointer" @click="handleZoom('r')" />
36
34
  </div>
37
35
  </div>
38
- <div class="overflow-hidden selecto-wrapper">
36
+ <div class="selecto-wrapper flex-1 overflow-hidden">
39
37
  <div
40
38
  v-if="svg"
41
- class="mt-2 overflow-scroll hidden-scrollbars p-2 rounded shadow border-2 border-dark-550"
42
- >
43
- <!-- <drag-select v-model="selection"> -->
39
+ class="hidden-scrollbars svg-container h-full overflow-auto rounded border-2 border-dark-550 p-2 shadow">
44
40
  <div class="svg-wrapper" id="svg-wrapper" v-html="svg"></div>
45
- <!-- </drag-select> -->
41
+ </div>
42
+ <div
43
+ v-else
44
+ class="svg-container flex h-full items-center justify-center rounded border-2 border-dark-550 p-2 shadow">
45
+ <div class="text-center">
46
+ <svg
47
+ class="mx-auto mb-3 h-12 w-12 opacity-50"
48
+ viewBox="0 0 19 19"
49
+ fill="none"
50
+ xmlns="http://www.w3.org/2000/svg">
51
+ <path
52
+ d="M2.37499 5.54165V2.37498L5.54166 3.95831L2.37499 5.54165ZM14.25 5.54165V2.37498L17.4167 3.95831L14.25 5.54165ZM8.70833 4.74998V1.58331L11.875 3.16665L8.70833 4.74998ZM8.70833 17.4166C7.70555 17.3903 6.77218 17.3079 5.9082 17.1696C5.0437 17.0308 4.29162 16.8559 3.65195 16.6448C3.01176 16.4337 2.50694 16.1896 2.13749 15.9125C1.76805 15.6354 1.58333 15.3451 1.58333 15.0416V7.91665C1.58333 7.58678 1.79127 7.27988 2.20716 6.99594C2.62252 6.71252 3.18645 6.46183 3.89895 6.24385C4.61145 6.02641 5.4493 5.85488 6.4125 5.72927C7.37569 5.60419 8.40486 5.54165 9.49999 5.54165C10.5951 5.54165 11.6243 5.60419 12.5875 5.72927C13.5507 5.85488 14.3885 6.02641 15.101 6.24385C15.8135 6.46183 16.3775 6.71252 16.7928 6.99594C17.2087 7.27988 17.4167 7.58678 17.4167 7.91665V15.0416C17.4167 15.3451 17.2319 15.6354 16.8625 15.9125C16.493 16.1896 15.9885 16.4337 15.3488 16.6448C14.7086 16.8559 13.9565 17.0308 13.0926 17.1696C12.2281 17.3079 11.2944 17.3903 10.2917 17.4166V14.25H8.70833V17.4166ZM9.49999 8.70831C10.7799 8.70831 11.885 8.63231 12.8155 8.48031C13.7454 8.32884 14.4875 8.15415 15.0417 7.95623C15.0417 7.89026 14.5403 7.73509 13.5375 7.49073C12.5347 7.2469 11.1889 7.12498 9.49999 7.12498C7.81111 7.12498 6.46527 7.2469 5.46249 7.49073C4.45972 7.73509 3.95833 7.89026 3.95833 7.95623C4.51249 8.15415 5.25481 8.32884 6.18529 8.48031C7.11523 8.63231 8.22013 8.70831 9.49999 8.70831ZM7.12499 15.7146V12.6666H11.875V15.7146C12.9305 15.609 13.7948 15.4538 14.4677 15.2491C15.1406 15.0448 15.5958 14.8635 15.8333 14.7052V9.34165C15.1076 9.63192 14.1972 9.86283 13.1021 10.0344C12.0069 10.2059 10.8062 10.2916 9.49999 10.2916C8.19374 10.2916 6.99305 10.2059 5.89791 10.0344C4.80277 9.86283 3.89236 9.63192 3.16666 9.34165V14.7052C3.40416 14.8635 3.85937 15.0448 4.53229 15.2491C5.2052 15.4538 6.06944 15.609 7.12499 15.7146Z"
53
+ fill="#F5F5F5" />
54
+ </svg>
55
+ <p class="text-sm text-light-400">No Map</p>
56
+ <p class="text-xs text-light-500">
57
+ Enter an event ID and click "Load" to display the venue map
58
+ </p>
59
+ </div>
46
60
  </div>
47
61
  </div>
48
62
  </div>
49
- <div class="col-span-2 mt-10">
50
- <div class="flex justify-between mb-2 items-center text-white">
51
- <div class="rounded flex justify-between gap-2 items-center" v-if="hasLoaded">
63
+ <div class="col-span-1 flex h-full w-full flex-col lg:col-span-2">
64
+ <div class="mb-2 flex items-center justify-between text-white">
65
+ <div class="flex items-center gap-2 rounded" v-if="hasLoaded">
52
66
  <PriceSortToggle
53
67
  :current="filterBuilder.globalFilter.priceSort"
54
68
  class="smooth-hover"
55
- @change="(e) => filterBuilder.updateGlobalFilter({ priceSort: e })"
56
- />
57
-
69
+ @change="(e) => filterBuilder.updateGlobalFilter({ priceSort: e })" />
70
+ </div>
71
+ <div class="flex items-center gap-1" v-if="hasLoaded">
72
+ <SavingsIcon class="h-4 w-4 text-light-400" />
73
+ <label class="text-sm text-light-400">Max Price:</label>
58
74
  <input
59
75
  type="number"
60
76
  :value="filterBuilder.globalFilter.maxPrice"
@@ -64,72 +80,87 @@
64
80
  maxPrice: parseInt(e.target.value || 0)
65
81
  })
66
82
  "
67
- class="w-14 bg-dark-500 border-2 border-dark-550 px-1 pl-2 h-8 rounded"
68
- placeholder="max"
69
- />
83
+ class="h-8 w-14 rounded border-2 border-dark-550 bg-dark-500 px-1 pl-2"
84
+ placeholder="max" />
70
85
  </div>
71
86
  </div>
72
- <Table class="border-2 border-dark-550 shadow-xl">
73
- <Header>
74
- <div class="flex items-center font-bold gap-2">
75
- <span class="text-xl lg:block hidden">Filters</span>
76
- <span class="text-xl text-light-300">{{ filterBuilder.filters.length }}</span>
77
- <PriceSortToggle
78
- class="w-14 smooth-hover"
79
- :options="['All', 'WL', 'BL']"
80
- @change="(e) => (shownFilters = e)"
81
- />
82
- <div
83
- class="flex gap-1 h-8 items-center justify-self-end border-2 border-dark-550 p-2 rounded smooth-hover"
84
- @click="saveFilter"
85
- >
86
- <EditIcon />
87
- <button class="lg:block hidden">Save</button>
87
+ <Table class="flex flex-1 flex-col border-2 border-dark-550 shadow-xl">
88
+ <Header class="flex-shrink-0">
89
+ <div class="flex w-full items-center justify-between gap-2 font-bold">
90
+ <div class="flex items-center gap-2">
91
+ <span class="text-base">Filters</span>
92
+ <span class="text-base text-light-300">{{ filterBuilder.filters.length }}</span>
88
93
  </div>
89
- <div
90
- class="flex gap-1 h-8 items-center absolute right-5 justify-self-end border-2 border-dark-550 p-2 rounded smooth-hover"
91
- @click="filterBuilder.reset(false)"
92
- >
93
- <TrashIcon />
94
- <button class="lg:block hidden">Clear</button>
94
+ <div class="flex items-center gap-2">
95
+ <PriceSortToggle
96
+ class="smooth-hover w-14"
97
+ :options="['All', 'WL', 'BL']"
98
+ @change="(e) => (shownFilters = e)" />
99
+ <button class="header-btn save-btn" @click="saveFilter">
100
+ <EditIcon class="h-4 w-4" />
101
+ <span class="hidden lg:block">Save</span>
102
+ </button>
103
+ <button class="header-btn clear-btn" @click="filterBuilder.reset(false)">
104
+ <TrashIcon class="h-4 w-4" />
105
+ <span class="hidden lg:block">Clear</span>
106
+ </button>
95
107
  </div>
96
108
  </div>
97
109
  </Header>
98
- <div class="overflow-scroll hidden-scrollbars h-96">
99
- <div v-if="filterBuilder.filters.length">
110
+ <div class="hidden-scrollbars flex-1 overflow-auto bg-dark-400">
111
+ <div v-if="filterBuilder.filters.length" class="filters-container">
100
112
  <draggable
101
113
  :list="filterBuilder.filters"
102
114
  handle=".handle"
103
115
  item-key="id"
104
- @change="filterBuilder.updateCss()"
105
- >
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
+ ">
106
128
  <template #item="{ element: f, index: i }">
107
129
  <Filter
108
130
  v-if="doesFilterShow(f)"
109
131
  :filter="f"
110
132
  :index="i"
111
133
  :filterBuilder="filterBuilder"
112
- />
134
+ class="compact-filter" />
113
135
  </template>
114
136
  </draggable>
115
137
  </div>
116
- <p v-else class="text-dark-400 m-2">No filters yet...</p>
138
+ <div
139
+ v-else
140
+ class="empty-state flex flex-col items-center justify-center py-8 text-center">
141
+ <FilterIcon class="mb-3 h-12 w-12 text-dark-400 opacity-50" />
142
+ <p class="text-sm text-light-400">No filters yet</p>
143
+ <p class="mt-1 text-xs text-light-500">Click on the map to create filters</p>
144
+ </div>
117
145
  </div>
118
146
  </Table>
119
- <div class="flex items-center justify-between font-bold text-white gap-2 my-2">
147
+ <div class="mb-2 mt-1 flex flex-shrink-0 items-center justify-between gap-2 text-white md:mb-0">
120
148
  <button
121
- @click="
122
- filterBuilder.addFilter({ buyAny: true, eventId: filterBuilder.currentEventId })
123
- "
124
- class="border-2 rounded border-dark-550 px-2 h-8 text-gray bg-dark-500 overflow-hidden shadow smooth-hover"
125
- >
149
+ @click="addWildcardFilter"
150
+ :disabled="hasWildcardFilter"
151
+ :class="[
152
+ 'h-7 overflow-hidden rounded border-2 border-dark-550 bg-dark-500 px-2 text-xs shadow transition-all duration-200',
153
+ hasWildcardFilter
154
+ ? 'text-gray cursor-not-allowed opacity-50'
155
+ : 'text-gray smooth-hover hover:border-light-300 hover:bg-dark-400'
156
+ ]"
157
+ :title="hasWildcardFilter ? 'Wildcard filter already exists' : 'Add wildcard filter'">
126
158
  * Wildcard
127
159
  </button>
128
160
  <button
129
161
  @click="ui.toggleModal('preview-filter')"
130
- class="border-2 gap-2 flex justify-between items-center rounded border-dark-550 px-2 h-8 text-gray bg-dark-500 overflow-hidden shadow smooth-hover"
131
- >
132
- <CameraIcon />
162
+ class="text-gray smooth-hover flex h-7 items-center justify-between gap-1 overflow-hidden rounded border-2 border-dark-550 bg-dark-500 px-2 text-xs shadow">
163
+ <CameraIcon class="h-3 w-3" />
133
164
  JSON
134
165
  </button>
135
166
  </div>
@@ -137,7 +168,7 @@
137
168
  </div>
138
169
  </div>
139
170
 
140
- <transition-group name="fade" mode="out-in">
171
+ <transition-group name="fade">
141
172
  <FilterPreview v-if="activeModal === 'preview-filter'" :filter="filterBuilder" />
142
173
  </transition-group>
143
174
  </div>
@@ -146,20 +177,14 @@
146
177
 
147
178
  <script setup>
148
179
  import draggable from "vuedraggable";
149
- import { ref, nextTick, watch, computed } from "vue";
180
+ import { ref, nextTick, watch, computed, onMounted, onUnmounted } from "vue";
181
+ import { onBeforeRouteLeave } from "vue-router";
150
182
  import { Table, Header } from "@/components/Table";
151
183
  import Filter from "@/components/Filter/Filter.vue";
152
- import DragSelect from "dragselect";
184
+ import { FilterIcon } from "@/components/icons";
185
+ import { DEBUG } from "@/utils/debug";
153
186
 
154
- const DEBUG = window.location.href.startsWith("http://localhost:5173");
155
- var RendererFactory;
156
- if (DEBUG) {
157
- RendererFactory = class {
158
- constructor() {}
159
- };
160
- } else RendererFactory = import("@necrolab/tm-renderer");
161
-
162
- import { ReloadIcon, TrashIcon, EditIcon, CameraIcon } from "@/components/icons";
187
+ import { ReloadIcon, TrashIcon, EditIcon, CameraIcon, StadiumIcon, SavingsIcon } from "@/components/icons";
163
188
  import { sendSaveFilter } from "@/stores/requests";
164
189
  import PriceSortToggle from "@/components/Filter/PriceSortToggle.vue";
165
190
  import { useUIStore } from "@/stores/ui";
@@ -171,141 +196,14 @@ const activeModal = computed(() => ui.activeModal);
171
196
  const ui = useUIStore();
172
197
  const hasLoaded = ref(false);
173
198
 
174
- // let isShiftPressed = false;
175
- // window.onkeyup = (e) => {
176
- // if (e.keyCode === 16) isShiftPressed = false;
177
- // };
178
- // window.onkeydown = (e) => {
179
- // if (e.keyCode === 16) isShiftPressed = true;
180
- // };
181
-
182
- // const doDragSelect = () => {
183
- // // https://dragselect.com/docs/API/Settings
184
- // const ds = new DragSelect({
185
- // selectables: [
186
- // ...document.querySelectorAll("path[row]"),
187
- // ...document.querySelectorAll("path[generaladmission]")
188
- // ],
189
- // area: document.getElementById("svg-wrapper"),
190
- // selectionThreshold: 0.1,
191
- // multiSelectKeys: []
192
- // });
193
-
194
- // ds.subscribe("DS:start:pre", () => {
195
- // // ui.logger.Info("DS:start:pre - shift pressed:", isShiftPressed);
196
- // if (!isShiftPressed) {
197
- // ds.stop(false, false);
198
- // ds.start();
199
- // } else {
200
- // panzoomInstance.pause();
201
- // }
202
- // });
203
-
204
- // const parseSelected = (items) => {
205
- // const parsed = items
206
- // .map((e) => {
207
- // return {
208
- // section: e.attributes.sectionname?.nodeValue || e.attributes.name?.nodeValue,
209
- // row: e.attributes.row?.nodeValue,
210
- // GA: e.attributes.generaladmission?.nodeValue,
211
- // name: e.attributes.name?.nodeValue
212
- // };
213
- // })
214
- // .filter((e) => !filterBuilder.value.isUnselectable(e.section, e.row));
215
-
216
- // const sectionMapping = {};
217
- // parsed
218
- // .filter((p) => !p.GA)
219
- // .forEach((p) => {
220
- // if (!sectionMapping[p.section]) sectionMapping[p.section] = [];
221
- // sectionMapping[p.section].push(p);
222
- // });
223
- // parsed
224
- // .filter((p) => p.GA)
225
- // .forEach((p) => {
226
- // sectionMapping[p.section] = {
227
- // GA: true,
228
- // section: p.section,
229
- // name: p.name
230
- // };
231
- // });
232
- // return sectionMapping;
233
- // };
234
-
235
- // ds.subscribe("DS:update:pre", (e) => {
236
- // // ui.logger.Info("DS:update:pre - shift pressed:", isShiftPressed, e.items);
237
- // filterBuilder.value.isDragging = true;
238
- // if (!isShiftPressed) return;
239
- // const sectionMapping = parseSelected(e.items);
240
- // filterBuilder.value.clearHighlight();
241
- // Object.entries(sectionMapping).forEach(([section, rows]) => {
242
- // if (rows.GA) {
243
- // const isSelected = filterBuilder.value.filters.find(
244
- // (f) => filterBuilder.value.isForCurrentEvent(f) && f.section === section
245
- // );
246
- // if (isSelected) return;
247
- // filterBuilder.value.highlight({ section: section }, true);
248
- // } else
249
- // rows.forEach((row) => {
250
- // const isSelected = filterBuilder.value.filters.find(
251
- // (f) =>
252
- // (f.section === section && f.rows?.includes(row.row)) ||
253
- // (!f.rows && f.section === section && filterBuilder.value.isForCurrentEvent(f))
254
- // );
255
- // if (isSelected) return;
256
-
257
- // filterBuilder.value.highlight({ section, row: row.row });
258
- // });
259
- // });
260
- // });
261
-
262
- // ds.subscribe("DS:end", (e) => {
263
- // filterBuilder.value.isDragging = false;
264
- // panzoomInstance.resume();
265
- // filterBuilder.value.clearHighlight();
266
- // [...document.querySelectorAll("path")].map((f) => (f.style = ""));
267
- // if (!isShiftPressed) return;
268
- // const sectionMapping = parseSelected(e.items);
269
- // const secNameMapping = filterBuilder.value.getSectionNameMapping();
270
-
271
- // Object.entries(sectionMapping).forEach(([section, rows]) => {
272
- // if (rows.GA) {
273
- // const isSelected = filterBuilder.value.filters.find(
274
- // (f) => filterBuilder.value.isForCurrentEvent(f) && f.section === secNameMapping[section]
275
- // );
276
- // if (isSelected) return;
277
- // filterBuilder.value.addFilter({
278
- // event: filterBuilder.value.currentEventId,
279
- // section: secNameMapping[section]
280
- // });
281
- // } else {
282
- // const unselectedRows = [];
283
- // rows.forEach((row) => {
284
- // const isSelected = filterBuilder.value.filters.find(
285
- // (f) =>
286
- // (f.section === section && f.rows?.includes(row.row)) ||
287
- // (!f.rows && f.section === section && filterBuilder.value.isForCurrentEvent(f))
288
- // );
289
- // if (isSelected) return;
290
- // unselectedRows.push(row.row);
291
- // });
292
- // if (unselectedRows.length === 0) return;
293
- // filterBuilder.value.addFilter({
294
- // event: filterBuilder.value.currentEventId,
295
- // section,
296
- // rows: unselectedRows
297
- // });
298
- // }
299
- // });
300
- // isShiftPressed = false;
301
- // });
302
- // };
303
-
304
199
  const panzoomOptions = {
305
- maxZoom: 8, // max zoom-in facor
200
+ maxZoom: 8, // max zoom-in factor
306
201
  minZoom: 0.8, // max zoom-out factor
307
202
  panOnlyWhenZoomed: true,
308
- bounds: true
203
+ bounds: true,
204
+ boundsPadding: 0.1, // Add padding to ensure it stays within bounds
205
+ transformOrigin: { x: 0.5, y: 0.5 }, // Center the zoom
206
+ contain: true // Force the image to stay within the parent container
309
207
  };
310
208
  let panzoomInstance;
311
209
 
@@ -317,6 +215,125 @@ let renderer;
317
215
  const shownFilters = ref("All");
318
216
  const filterBuilder = ref(new FilterBuilder());
319
217
 
218
+ const hasWildcardFilter = computed(() => {
219
+ return filterBuilder.value.filters.some((filter) => filter.buyAny && filterBuilder.value.isForCurrentEvent(filter));
220
+ });
221
+
222
+ const addWildcardFilter = () => {
223
+ if (!hasWildcardFilter.value) {
224
+ // Close any expanded filter first
225
+ filterBuilder.value.setExpandedFilter(null);
226
+ filterBuilder.value.addFilter({ buyAny: true, eventId: filterBuilder.value.currentEventId });
227
+ filterBuilder.value.updateCss();
228
+ }
229
+ };
230
+
231
+ // Initialize RendererFactory
232
+ let RendererFactory = import("@necrolab/tm-renderer");
233
+
234
+ // Real-time CSS injection system
235
+ let styleElement = null;
236
+ const cssUpdateTrigger = ref(0);
237
+ const STYLE_ELEMENT_ID = "filter-builder-styles";
238
+
239
+ const injectStyles = () => {
240
+ // Only inject styles if we have an SVG to style
241
+ const svgWrapper = document.getElementById("svg-wrapper");
242
+ if (!svgWrapper || !svg.value) return;
243
+
244
+ if (!styleElement) {
245
+ // Remove any existing style element first
246
+ const existingStyle = document.getElementById(STYLE_ELEMENT_ID);
247
+ if (existingStyle) {
248
+ existingStyle.remove();
249
+ }
250
+
251
+ styleElement = document.createElement("style");
252
+ styleElement.id = STYLE_ELEMENT_ID;
253
+ document.head.appendChild(styleElement);
254
+ }
255
+
256
+ const combinedCSS = filterBuilder.value.cssClasses + filterBuilder.value.temporaryCSS;
257
+ if (styleElement.textContent !== combinedCSS) {
258
+ styleElement.textContent = combinedCSS;
259
+ }
260
+ };
261
+
262
+ // Setup real-time CSS updates
263
+ const setupCSSUpdates = () => {
264
+ // Override updateCss method to trigger immediate updates
265
+ const originalUpdateCss = filterBuilder.value.updateCss.bind(filterBuilder.value);
266
+ filterBuilder.value.updateCss = () => {
267
+ originalUpdateCss();
268
+ nextTick(() => {
269
+ injectStyles();
270
+ cssUpdateTrigger.value++;
271
+ });
272
+ };
273
+
274
+ // Override highlight method for immediate temporary CSS
275
+ const originalHighlight = filterBuilder.value.highlight.bind(filterBuilder.value);
276
+ filterBuilder.value.highlight = (...args) => {
277
+ originalHighlight(...args);
278
+ nextTick(() => {
279
+ injectStyles();
280
+ });
281
+ };
282
+
283
+ // Override clearHighlight method
284
+ const originalClearHighlight = filterBuilder.value.clearHighlight.bind(filterBuilder.value);
285
+ filterBuilder.value.clearHighlight = () => {
286
+ originalClearHighlight();
287
+ nextTick(() => {
288
+ injectStyles();
289
+ });
290
+ };
291
+ };
292
+
293
+ // Cleanup function
294
+ const cleanupStyles = () => {
295
+ // Remove by ID to ensure we get it even if reference is lost
296
+ const existingStyle = document.getElementById(STYLE_ELEMENT_ID);
297
+ if (existingStyle) {
298
+ existingStyle.remove();
299
+ }
300
+
301
+ if (styleElement) {
302
+ styleElement.remove();
303
+ styleElement = null;
304
+ }
305
+ };
306
+
307
+ // Basic cleanup monitoring
308
+ const debugCleanup = () => {
309
+ console.log("FilterBuilder cleanup completed");
310
+ };
311
+
312
+ // Clean up any leftover styles from previous instances
313
+ cleanupStyles();
314
+
315
+ // Initialize CSS system
316
+ setupCSSUpdates();
317
+
318
+ // Watch for reactive updates
319
+ watch(
320
+ cssUpdateTrigger,
321
+ () => {
322
+ injectStyles();
323
+ },
324
+ { immediate: true }
325
+ );
326
+
327
+ // Cleanup on unmount
328
+ onUnmounted(() => {
329
+ cleanupStyles();
330
+ });
331
+
332
+ // Also cleanup on route change using beforeRouteLeave
333
+ onBeforeRouteLeave(() => {
334
+ cleanupStyles();
335
+ });
336
+
320
337
  const doesFilterShow = (filter) => {
321
338
  if ((filter.event || filter.eventId) !== filterBuilder.value.currentEventId) return;
322
339
  if (!["BL", "WL"].includes(shownFilters.value)) return true;
@@ -325,13 +342,12 @@ const doesFilterShow = (filter) => {
325
342
  };
326
343
 
327
344
  let rendererFactory;
328
- if (!DEBUG)
329
- RendererFactory.then((r) => {
330
- rendererFactory = new r.default();
331
- rendererFactory.init();
332
- }).catch((error) => {
333
- console.error("Failed to initialize renderer:", error);
334
- });
345
+ RendererFactory.then((r) => {
346
+ rendererFactory = new r.default();
347
+ rendererFactory.init();
348
+ }).catch((error) => {
349
+ console.error("Failed to initialize renderer:", error);
350
+ });
335
351
 
336
352
  const updateShownVenue = async () => {
337
353
  eventId.value = eventId.value.trim();
@@ -344,12 +360,16 @@ const updateShownVenue = async () => {
344
360
  return;
345
361
  }
346
362
 
347
- renderer = rendererFactory.createRenderer(eventId.value, "", country);
348
- filterBuilder.value.highlightedSeatColor = renderer.c.highlightedSeatColor;
349
- renderer.c.logFullError = true;
350
- renderer.c.includeDetailedAttributes = true;
351
- renderer.c.renderRowBlocks = true;
352
- renderer.setUrlProxy("/api/cors?url=");
363
+ renderer = rendererFactory.createRenderer(eventId.value, {
364
+ proxy: "",
365
+ country: country
366
+ });
367
+ filterBuilder.value.highlightedSeatColor = renderer.config.highlightedSeatColor;
368
+ renderer.setCustomConfig({
369
+ logFullError: true,
370
+ includeDetailedAttributes: true,
371
+ renderRowBlocks: true
372
+ });
353
373
 
354
374
  try {
355
375
  await renderer.render();
@@ -371,18 +391,45 @@ const updateShownVenue = async () => {
371
391
  if (panzoomInstance?.dispose) panzoomInstance.dispose();
372
392
  const elem = document.getElementById("svg-wrapper");
373
393
  try {
374
- panzoomInstance = panzoom(elem.children[0], panzoomOptions);
394
+ // Ensure SVG has appropriate sizing before initializing panzoom
395
+ if (elem && elem.children[0]) {
396
+ const svg = elem.children[0];
397
+ const isDesktop = window.innerWidth >= 1024;
398
+
399
+ // Ensure SVG doesn't exceed container width
400
+ svg.style.maxWidth = "100%";
401
+ svg.style.height = "auto";
402
+ svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
403
+
404
+ // Additional desktop constraints to prevent expansion
405
+ if (isDesktop) {
406
+ svg.style.maxHeight = "100%";
407
+ svg.style.width = "100%";
408
+ svg.style.objectFit = "contain";
409
+ }
410
+
411
+ // Initialize panzoom with updated options
412
+ panzoomInstance = panzoom(svg, panzoomOptions);
413
+
414
+ // Reset to initial position
415
+ panzoomInstance.moveTo(0, 0);
416
+ panzoomInstance.zoomAbs(0, 0, 1);
417
+ }
375
418
  } catch (e) {
376
419
  ui.logger.Error("Couldnt use panzoom", e);
377
420
  }
378
421
  await nextTick();
379
422
  await loadFilter();
380
423
  filterBuilder.value.reload(eventId.value);
381
- // doDragSelect();
424
+
425
+ // Re-setup CSS system after reload
426
+ setupCSSUpdates();
427
+ filterBuilder.value.updateCss();
382
428
  };
383
429
 
384
430
  const loadFilter = async () => {
385
431
  try {
432
+ if (!eventId.value) return;
386
433
  const res = await fetch(`/api/filter/load?eventId=${eventId.value}`);
387
434
  const data = await res.json();
388
435
  if (eventId.value) ui.showSuccess("Loaded filter data");
@@ -410,16 +457,22 @@ const saveFilter = async () => {
410
457
  const handleZoom = (zoomEvent) => {
411
458
  // handle zoom reset
412
459
  if (zoomEvent === "r") {
413
- // reset zoom
414
- panzoomInstance.smoothZoomAbs(0, 0, 1);
415
- // reset location
460
+ // Reset both zoom and position
461
+ panzoomInstance.moveTo(0, 0);
462
+ panzoomInstance.zoomAbs(0, 0, 1);
416
463
  return;
417
464
  }
418
465
 
419
- // handle zoom in / out
466
+ // Get current transform
420
467
  const { scale } = panzoomInstance.getTransform();
421
- if (zoomEvent) return panzoomInstance.smoothZoomAbs(0, 0, scale + scale / 3);
422
- else return panzoomInstance.smoothZoomAbs(0, 0, scale - scale / 3);
468
+
469
+ // Calculate new scale based on zoom direction
470
+ const newScale = zoomEvent ? scale + scale / 3 : scale - scale / 3;
471
+
472
+ // Apply zoom with bounds checking
473
+ if (newScale >= panzoomOptions.minZoom && newScale <= panzoomOptions.maxZoom) {
474
+ panzoomInstance.smoothZoom(0, 0, newScale / scale);
475
+ }
423
476
  };
424
477
 
425
478
  watch(renderSeats, async () => {
@@ -432,15 +485,240 @@ watch(renderSeats, async () => {
432
485
  })();
433
486
  </script>
434
487
 
488
+ <style scoped>
489
+ .slider-dim {
490
+ width: 60px;
491
+ height: 30px;
492
+ }
493
+
494
+ .compact-filter {
495
+ font-size: 0.75rem;
496
+ padding: 0.25rem !important;
497
+ }
498
+
499
+ .compact-filter .handle {
500
+ transform: scale(0.8);
501
+ }
502
+
503
+ /* Enhanced table and filter styling */
504
+ .filters-container {
505
+ @apply space-y-0;
506
+ padding: 4px;
507
+ }
508
+
509
+ /* Dragging states for better UX */
510
+ .sortable-ghost {
511
+ @apply opacity-30;
512
+ border: 1px solid oklch(0.28 0 0);
513
+ background-color: rgba(68, 69, 75, 0.1);
514
+ }
515
+
516
+ .sortable-chosen .drag-handle {
517
+ border: 1px solid oklch(0.28 0 0);
518
+ background-color: rgba(68, 69, 75, 0.2);
519
+ }
520
+
521
+ .sortable-drag {
522
+ @apply z-50 rotate-1 scale-105 transform shadow-xl;
523
+ }
524
+
525
+ /* Unified Event ID + Load button group */
526
+ .unified-load-group {
527
+ border: 2px solid oklch(0.2809 0 0);
528
+ border-radius: 0.5rem;
529
+ overflow: hidden;
530
+ background: oklch(0.2603 0 0);
531
+ transition: all 0.15s ease;
532
+
533
+ input, button {
534
+ border: none !important;
535
+ border-radius: 0 !important;
536
+ outline: none !important;
537
+ }
538
+
539
+ input {
540
+ flex: 1;
541
+
542
+ &::placeholder {
543
+ color: oklch(0.50 0 0);
544
+ }
545
+ }
546
+
547
+ button {
548
+ border-left: 1px solid oklch(0.2809 0 0) !important;
549
+ padding: 0 1rem;
550
+ font-weight: 500;
551
+ transition: all 0.15s ease;
552
+
553
+ &:hover {
554
+ background: oklch(0.72 0.15 145 / 0.1);
555
+ }
556
+ }
557
+
558
+ &:hover {
559
+ border-color: oklch(0.30 0 0);
560
+ }
561
+
562
+ &:focus-within {
563
+ border-color: oklch(0.72 0.15 145);
564
+ outline: 1px solid oklch(0.72 0.15 145);
565
+ outline-offset: 0;
566
+ }
567
+ }
568
+
569
+ /* Header button styling */
570
+ .header-btn {
571
+ @apply flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium transition-all duration-150;
572
+ }
573
+
574
+ .save-btn {
575
+ background-color: oklch(0.72 0.15 145 / 0.15);
576
+ border-color: oklch(0.72 0.15 145 / 0.5);
577
+ color: oklch(0.90 0 0);
578
+ }
579
+
580
+ .save-btn:hover {
581
+ background-color: oklch(0.72 0.15 145 / 0.25);
582
+ border-color: oklch(0.72 0.15 145);
583
+ }
584
+
585
+ .clear-btn {
586
+ background-color: oklch(0.60 0.20 25 / 0.15);
587
+ border-color: oklch(0.60 0.20 25 / 0.5);
588
+ color: oklch(0.90 0 0);
589
+ }
590
+
591
+ .clear-btn:hover {
592
+ background-color: oklch(0.60 0.20 25 / 0.25);
593
+ border-color: oklch(0.60 0.20 25);
594
+ }
595
+
596
+ .filter-builder-container {
597
+ height: 90vh;
598
+ overflow: hidden;
599
+ }
600
+
601
+ /* Component-scoped definition (not global utility) */
602
+ .card-dark {
603
+ height: calc(100vh - 175px);
604
+ min-height: 500px;
605
+ overflow: hidden;
606
+ }
607
+
608
+ @media (max-width: 768px) {
609
+ .filter-builder-container {
610
+ overflow: auto;
611
+ }
612
+
613
+ .card-dark {
614
+ height: auto;
615
+ overflow: auto;
616
+ }
617
+ }
618
+
619
+ /* Mobile portrait header spacing fixes */
620
+ @media (max-width: 640px) {
621
+ .filter-builder-container .flex.items-center.justify-center {
622
+ gap: 0.5rem;
623
+ }
624
+ }
625
+
626
+ /* iPhone landscape and tablet portrait mode fixes */
627
+ @media (max-width: 1023px) and (max-height: 768px) {
628
+ .filter-builder-container {
629
+ height: auto;
630
+ min-height: 100vh;
631
+ overflow: auto;
632
+ }
633
+
634
+ .card-dark {
635
+ height: auto;
636
+ min-height: 500px;
637
+ overflow: auto;
638
+ }
639
+ }
640
+
641
+ /* Desktop: enforce grid column constraints */
642
+ @media (min-width: 1024px) {
643
+ .lg\\:grid-cols-5 > .lg\\:col-span-3 {
644
+ max-width: 60%; /* 3/5 of grid */
645
+ flex-shrink: 0;
646
+ }
647
+ }
648
+
649
+ .svg-container {
650
+ min-height: 300px;
651
+ height: 100%;
652
+ width: 100%;
653
+ position: relative;
654
+ overflow: auto;
655
+ }
656
+
657
+ /* Desktop: constrain svg-container to prevent expansion */
658
+ @media (min-width: 1024px) {
659
+ .svg-container {
660
+ position: relative;
661
+ max-width: 100%;
662
+ max-height: 100%;
663
+ overflow: hidden;
664
+ }
665
+ }
666
+ </style>
667
+
435
668
  <style>
669
+ /* Global styles needed for dynamically injected SVG content and Filter.js CSS injection */
670
+ .svg-wrapper {
671
+ position: relative;
672
+ transform-origin: 0 0;
673
+ min-width: 100%;
674
+ min-height: 100%;
675
+ }
676
+
677
+ /* Desktop constraints - prevent svg-wrapper from expanding beyond container */
678
+ @media (min-width: 1024px) {
679
+ .svg-wrapper {
680
+ position: absolute;
681
+ top: 0;
682
+ left: 0;
683
+ width: 100%;
684
+ height: 100%;
685
+ max-width: 100%;
686
+ max-height: 100%;
687
+ min-width: unset;
688
+ min-height: unset;
689
+ overflow: hidden;
690
+ }
691
+ }
692
+
436
693
  .svg-wrapper > svg {
694
+ width: 100%;
695
+ height: auto;
437
696
  background: center center no-repeat;
438
- overflow: hidden;
439
697
  background-size: contain;
440
698
  }
441
699
 
442
- .slider-dim {
443
- width: 60px;
444
- height: 30px;
700
+ /* Desktop SVG constraints */
701
+ @media (min-width: 1024px) {
702
+ .svg-wrapper > svg {
703
+ max-width: 100%;
704
+ max-height: 100%;
705
+ }
706
+ }
707
+
708
+ /* Ensure Filter.js generated CSS works properly */
709
+ .svg-wrapper path {
710
+ pointer-events: auto;
711
+ cursor: pointer;
712
+ }
713
+
714
+ .svg-wrapper path[generaladmission] {
715
+ pointer-events: auto;
716
+ cursor: pointer;
717
+ }
718
+
719
+ /* Hover effects for general admission seats */
720
+ .svg-wrapper path[generaladmission]:hover {
721
+ pointer-events: all;
722
+ cursor: pointer;
445
723
  }
446
724
  </style>