@necrolab/dashboard 0.5.15 → 0.5.17

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 (137) hide show
  1. package/backend/api.js +2 -3
  2. package/eslint.config.js +46 -0
  3. package/index.html +2 -1
  4. package/package.json +5 -2
  5. package/src/App.vue +70 -566
  6. package/src/assets/css/base/mixins.scss +72 -0
  7. package/src/assets/css/base/reset.scss +0 -2
  8. package/src/assets/css/base/scroll.scss +43 -36
  9. package/src/assets/css/base/typography.scss +9 -10
  10. package/src/assets/css/base/variables.scss +43 -0
  11. package/src/assets/css/components/accessibility.scss +37 -0
  12. package/src/assets/css/components/buttons.scss +61 -74
  13. package/src/assets/css/components/forms.scss +31 -32
  14. package/src/assets/css/components/headers.scss +13 -21
  15. package/src/assets/css/components/modals.scss +2 -2
  16. package/src/assets/css/components/search-groups.scss +28 -22
  17. package/src/assets/css/components/tables.scss +5 -7
  18. package/src/assets/css/components/toasts.scss +7 -7
  19. package/src/assets/css/components/utilities.scss +295 -0
  20. package/src/assets/css/main.scss +55 -139
  21. package/src/components/Auth/LoginForm.vue +7 -86
  22. package/src/components/Console/ConsoleToolbar.vue +123 -0
  23. package/src/components/Editors/Account/Account.vue +12 -12
  24. package/src/components/Editors/Account/AccountView.vue +38 -111
  25. package/src/components/Editors/Account/CreateAccount.vue +11 -61
  26. package/src/components/Editors/Account/{AccountCreator.vue → CreateAccountBatch.vue} +28 -59
  27. package/src/components/Editors/AdminFileEditor.vue +179 -0
  28. package/src/components/Editors/Profile/CreateProfile.vue +77 -150
  29. package/src/components/Editors/Profile/Profile.vue +20 -21
  30. package/src/components/Editors/Profile/ProfileCountryChooser.vue +16 -60
  31. package/src/components/Editors/Profile/ProfileView.vue +41 -116
  32. package/src/components/Editors/ProxyFileEditor.vue +86 -0
  33. package/src/components/Editors/TagLabel.vue +16 -55
  34. package/src/components/Editors/TagToggle.vue +20 -8
  35. package/src/components/Filter/Filter.vue +66 -79
  36. package/src/components/Filter/FilterPreview.vue +153 -135
  37. package/src/components/Filter/PriceSortToggle.vue +36 -43
  38. package/src/components/Table/Header.vue +1 -1
  39. package/src/components/Table/Table.vue +45 -51
  40. package/src/components/Tasks/CheckStock.vue +7 -16
  41. package/src/components/Tasks/Controls/DesktopControls.vue +15 -60
  42. package/src/components/Tasks/Controls/MobileControls.vue +5 -20
  43. package/src/components/Tasks/CreateTaskAXS.vue +20 -118
  44. package/src/components/Tasks/CreateTaskTM.vue +33 -189
  45. package/src/components/Tasks/EventDetailRow.vue +21 -0
  46. package/src/components/Tasks/MassEdit.vue +6 -16
  47. package/src/components/Tasks/QuickSettings.vue +140 -216
  48. package/src/components/Tasks/ScrapeVenue.vue +4 -13
  49. package/src/components/Tasks/Stats.vue +20 -39
  50. package/src/components/Tasks/Task.vue +64 -270
  51. package/src/components/Tasks/TaskLabel.vue +9 -3
  52. package/src/components/Tasks/TaskView.vue +45 -64
  53. package/src/components/Tasks/Utilities.vue +10 -44
  54. package/src/components/Tasks/ViewTask.vue +23 -107
  55. package/src/components/icons/Close.vue +2 -8
  56. package/src/components/icons/Gear.vue +8 -8
  57. package/src/components/icons/Hash.vue +5 -0
  58. package/src/components/icons/Key.vue +2 -8
  59. package/src/components/icons/Pencil.vue +2 -8
  60. package/src/components/icons/Profile.vue +2 -8
  61. package/src/components/icons/Sell.vue +2 -8
  62. package/src/components/icons/Spinner.vue +4 -7
  63. package/src/components/icons/Wildcard.vue +2 -8
  64. package/src/components/icons/index.js +3 -5
  65. package/src/components/ui/ActionButtonGroup.vue +113 -52
  66. package/src/components/ui/BalanceIndicator.vue +60 -0
  67. package/src/components/ui/EmptyState.vue +24 -0
  68. package/src/components/ui/EnableDisableToggle.vue +23 -0
  69. package/src/components/ui/FormField.vue +49 -49
  70. package/src/components/ui/IconLabel.vue +23 -0
  71. package/src/components/ui/InfoRow.vue +21 -54
  72. package/src/components/ui/Modal.vue +161 -54
  73. package/src/components/ui/Navbar.vue +63 -44
  74. package/src/components/ui/ReadonlyFieldsSection.vue +31 -0
  75. package/src/components/ui/ReconnectIndicator.vue +111 -124
  76. package/src/components/ui/SectionCard.vue +6 -14
  77. package/src/components/ui/Splash.vue +2 -10
  78. package/src/components/ui/StatusBadge.vue +26 -28
  79. package/src/components/ui/TaskToggle.vue +54 -0
  80. package/src/components/ui/controls/CountryChooser.vue +29 -66
  81. package/src/components/ui/controls/EyeToggle.vue +1 -1
  82. package/src/components/ui/controls/atomic/Checkbox.vue +40 -121
  83. package/src/components/ui/controls/atomic/Dropdown.vue +103 -139
  84. package/src/components/ui/controls/atomic/MultiDropdown.vue +72 -120
  85. package/src/components/ui/controls/atomic/Switch.vue +21 -84
  86. package/src/composables/useCodeEditor.js +117 -0
  87. package/src/composables/useColorMapping.js +15 -0
  88. package/src/composables/useCopyToClipboard.js +1 -1
  89. package/src/composables/useDateFormatting.js +21 -0
  90. package/src/composables/useDeviceDetection.js +14 -0
  91. package/src/composables/useDropdownPosition.js +1 -4
  92. package/src/composables/useDynamicTableHeight.js +31 -0
  93. package/src/composables/useEnableDisable.js +6 -0
  94. package/src/composables/useFilterCSS.js +71 -0
  95. package/src/composables/useFormValidation.js +92 -0
  96. package/src/composables/useGetAllTags.js +9 -0
  97. package/src/composables/useIOSViewportHandling.js +76 -0
  98. package/src/composables/useNotchHandling.js +306 -0
  99. package/src/composables/useRowSelection.js +0 -3
  100. package/src/composables/useTableRender.js +23 -0
  101. package/src/composables/useTicketPricing.js +16 -0
  102. package/src/composables/useWindowDimensions.js +21 -0
  103. package/src/composables/useZoomPrevention.js +96 -0
  104. package/src/constants/tableLayout.js +14 -0
  105. package/src/libs/Filter.js +14 -20
  106. package/src/libs/panzoom.js +1 -5
  107. package/src/libs/utils/array.js +58 -0
  108. package/src/{stores/utils.js → libs/utils/dataGeneration.js} +2 -250
  109. package/src/libs/utils/eventUrl.js +40 -0
  110. package/src/libs/utils/string.js +3 -0
  111. package/src/libs/utils/time.js +20 -0
  112. package/src/libs/utils/validation.js +64 -0
  113. package/src/main.js +0 -2
  114. package/src/stores/connection.js +1 -29
  115. package/src/stores/logger.js +6 -12
  116. package/src/stores/sampleData.js +1 -2
  117. package/src/stores/ui.js +80 -71
  118. package/src/utils/tableHelpers.js +1 -0
  119. package/src/views/Accounts.vue +19 -38
  120. package/src/views/Console.vue +74 -253
  121. package/src/views/Editor.vue +47 -1114
  122. package/src/views/FilterBuilder.vue +190 -461
  123. package/src/views/Login.vue +3 -28
  124. package/src/views/Profiles.vue +17 -32
  125. package/src/views/Tasks.vue +51 -38
  126. package/tailwind.config.js +82 -71
  127. package/workbox-config.cjs +47 -5
  128. package/docs/plans/2026-02-08-tailwind-consolidation.md +0 -2438
  129. package/exit +0 -209
  130. package/run +0 -177
  131. package/src/assets/css/base/color-fallbacks.scss +0 -10
  132. package/src/assets/img/background.svg.backup +0 -11
  133. package/src/components/icons/SquareCheck.vue +0 -18
  134. package/src/components/icons/SquareUncheck.vue +0 -18
  135. package/src/components/ui/controls/atomic/LoadingButton.vue +0 -45
  136. package/switch-branch.sh +0 -41
  137. /package/public/{reconnect-logo.png → img/reconnect-logo.png} +0 -0
@@ -1,196 +1,177 @@
1
1
  <template>
2
- <div class="filter-builder-container flex min-h-0 w-full flex-col overflow-hidden">
3
- <!-- Heading -->
4
- <div class="page-header" style="margin-top: 0.75rem; padding-bottom: 0.25rem;">
5
- <div class="page-header-card">
2
+ <div class="flex w-full flex-col">
3
+ <div class="page-header">
4
+ <div class="page-header-card flex-shrink-0">
6
5
  <FilterIcon />
7
6
  <h4>Filter creator</h4>
8
7
  </div>
9
- <div class="unified-load-group flex items-center h-9">
8
+ <div class="unified-search-group flex w-auto items-center">
10
9
  <input
11
- class="relative flex h-full w-40 items-center bg-transparent px-2 text-sm text-white"
10
+ class="h-10 w-32 flex-1 px-3 text-sm text-white placeholder-light-500 sm:w-48 md:w-64"
12
11
  placeholder="Event ID"
13
- v-model="eventId" />
12
+ v-model="eventId"
13
+ aria-label="Event ID" />
14
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
15
+ class="flex h-10 w-9 flex-shrink-0 items-center justify-center bg-dark-400 text-white transition-standard hover:bg-dark-450"
16
+ @click="updateShownVenue"
17
+ aria-label="Load venue">
18
+ <ReloadIcon class="icon-md" />
18
19
  </button>
19
20
  </div>
20
21
  </div>
21
22
 
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">
24
- <!-- Main -->
25
- <div class="grid h-full w-full grid-cols-1 gap-3 lg:grid-cols-5 lg:gap-4">
26
- <!-- Map -->
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">
31
- <span class="cursor-pointer" @click="handleZoom(true)">+</span>
32
- <span class="cursor-pointer" @click="handleZoom(false)">-</span>
33
- <ReloadIcon class="h-4 w-4 cursor-pointer" @click="handleZoom('r')" />
34
- </div>
23
+ <div class="mb-3 flex flex-1 flex-col overflow-hidden rounded border border-dark-650 bg-dark-400 p-3 shadow-sm md:mb-4">
24
+ <div class="flex h-full w-full flex-col gap-3 lg:flex-row lg:gap-4">
25
+ <div class="relative flex min-h-75 min-w-0 w-full flex-col overflow-hidden rounded-lg lg:min-h-125 lg:w-3/5">
26
+ <div v-if="svg" class="mb-2 flex-gap-2 items-center">
27
+ <button @click="handleZoom(true)" class="btn-icon-small" aria-label="Zoom in">
28
+ +
29
+ </button>
30
+ <button @click="handleZoom(false)" class="btn-icon-small" aria-label="Zoom out">
31
+ -
32
+ </button>
33
+ <button @click="handleZoom('r')" class="btn-icon-small" aria-label="Reset zoom">
34
+ <ReloadIcon class="icon-md text-white" />
35
+ </button>
36
+ </div>
37
+ <div class="selecto-wrapper flex-1 overflow-hidden">
38
+ <div
39
+ v-if="svg"
40
+ class="hidden-scrollbars relative h-full min-h-87.5 w-full overflow-auto rounded border border-dark-550 bg-dark-500 p-2 shadow">
41
+ <div class="svg-wrapper" id="svg-wrapper" v-html="svg"></div>
35
42
  </div>
36
- <div class="selecto-wrapper flex-1 overflow-hidden">
37
- <div
38
- v-if="svg"
39
- class="hidden-scrollbars svg-container h-full overflow-auto rounded border-2 border-dark-550 p-2 shadow">
40
- <div class="svg-wrapper" id="svg-wrapper" v-html="svg"></div>
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>
43
+ <div
44
+ v-else
45
+ class="relative flex h-full min-h-87.5 w-full items-center justify-center rounded border border-dark-550 bg-dark-500 p-2 shadow">
46
+ <div class="text-center">
47
+ <FilterIcon class="mx-auto empty-state-icon" />
48
+ <p class="text-sm text-light-400">No Map</p>
49
+ <p class="mt-1 text-xs text-light-500">
50
+ Enter an event ID and click "Load" to display the venue map
51
+ </p>
60
52
  </div>
61
53
  </div>
62
54
  </div>
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">
66
- <PriceSortToggle
67
- :current="filterBuilder.globalFilter.priceSort"
68
- class="smooth-hover"
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>
74
- <input
75
- type="number"
76
- :value="filterBuilder.globalFilter.maxPrice"
77
- @input="
78
- (e) =>
79
- filterBuilder.updateGlobalFilter({
80
- maxPrice: parseInt(e.target.value || 0)
81
- })
82
- "
83
- class="h-8 w-14 rounded border-2 border-dark-550 bg-dark-500 px-1 pl-2"
84
- placeholder="max" />
85
- </div>
86
- </div>
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>
93
- </div>
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>
107
- </div>
55
+ </div>
56
+ <div class="flex min-h-75 min-w-0 w-full flex-col lg:min-h-125 lg:w-2/5">
57
+ <div class="mb-2 flex flex-shrink-0 flex-wrap items-center gap-2 text-white" v-if="hasLoaded">
58
+ <PriceSortToggle
59
+ :current="filterBuilder.globalFilter.priceSort"
60
+ class="smooth-hover h-8"
61
+ @change="(e) => filterBuilder.updateGlobalFilter({ priceSort: e })" />
62
+ <label class="text-xs text-light-400">Max:</label>
63
+ <input
64
+ type="number"
65
+ :value="filterBuilder.globalFilter.maxPrice"
66
+ @input="
67
+ (e) =>
68
+ filterBuilder.updateGlobalFilter({
69
+ maxPrice: parseInt(e.target.value || 0)
70
+ })
71
+ "
72
+ class="input-default h-8 w-16 px-2 text-sm"
73
+ placeholder="999" />
74
+ </div>
75
+ <div class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border border-dark-550 bg-dark-500 shadow-sm">
76
+ <div class="flex-shrink-0 border-b border-dark-550 bg-dark-300 px-4 py-3 text-xs text-white">
77
+ <div class="flex w-full items-center justify-between gap-2">
78
+ <div class="flex-gap-2 items-center">
79
+ <span class="text-sm font-medium text-white">Filters</span>
80
+ <span class="text-sm text-light-400">{{ filterBuilder.filters.length }}</span>
108
81
  </div>
109
- </Header>
110
- <div class="hidden-scrollbars flex-1 overflow-auto bg-dark-400">
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
- <template #item="{ element: f, index: i }">
129
- <Filter
130
- v-if="doesFilterShow(f)"
131
- :filter="f"
132
- :index="i"
133
- :filterBuilder="filterBuilder"
134
- class="compact-filter" />
135
- </template>
136
- </draggable>
82
+ <div class="flex-gap-2 items-center">
83
+ <PriceSortToggle
84
+ class="h-8 w-14 flex-shrink-0 text-xs"
85
+ :options="['All', 'WL', 'BL']"
86
+ :current="shownFilters"
87
+ @change="(e) => (shownFilters = e)" />
88
+ <button
89
+ class="filter-action-btn"
90
+ @click="saveFilter"
91
+ title="Save filter">
92
+ <EditIcon class="h-3 w-3 flex-shrink-0" />
93
+ <span>Save</span>
94
+ </button>
95
+ <button
96
+ class="filter-action-btn"
97
+ @click="filterBuilder.reset(false)"
98
+ title="Clear filters">
99
+ <TrashIcon class="h-3 w-3 flex-shrink-0" />
100
+ <span>Clear</span>
101
+ </button>
137
102
  </div>
103
+ </div>
104
+ </div>
105
+ <div class="hidden-scrollbars flex-1 overflow-auto bg-dark-400">
106
+ <draggable
107
+ v-if="filterBuilder.filters.length"
108
+ v-model="draggableFilters"
109
+ handle=".handle"
110
+ item-key="id"
111
+ tag="div"
112
+ class="space-y-0 p-1"
113
+ ghost-class="opacity-30 border border-dark-550 bg-dark-550/10"
114
+ drag-class="z-50 shadow-xl"
115
+ :animation="200">
116
+ <template #item="{ element: f, index: i }">
117
+ <Filter
118
+ v-show="doesFilterShow(f)"
119
+ :filter="f"
120
+ :index="i"
121
+ :filterBuilder="filterBuilder"
122
+ class="!p-1 !text-xs" />
123
+ </template>
124
+ </draggable>
138
125
  <div
139
126
  v-else
140
127
  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" />
128
+ <FilterIcon class="empty-state-icon" />
142
129
  <p class="text-sm text-light-400">No filters yet</p>
143
130
  <p class="mt-1 text-xs text-light-500">Click on the map to create filters</p>
144
131
  </div>
145
132
  </div>
146
- </Table>
147
- <div class="mb-2 mt-1 flex flex-shrink-0 items-center justify-between gap-2 text-white md:mb-0">
148
- <button
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'">
158
- * Wildcard
159
- </button>
160
- <button
161
- @click="ui.toggleModal('preview-filter')"
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" />
164
- JSON
165
- </button>
166
- </div>
133
+ </div>
134
+ <div class="mt-2 flex flex-shrink-0 items-center justify-between gap-2">
135
+ <button
136
+ @click="addWildcardFilter"
137
+ :disabled="hasWildcardFilter"
138
+ class="filter-wildcard-btn"
139
+ :title="hasWildcardFilter ? 'Wildcard filter already exists' : 'Add wildcard filter'">
140
+ * Wildcard
141
+ </button>
142
+ <button
143
+ @click="ui.toggleModal('preview-filter')"
144
+ class="filter-action-btn">
145
+ <CameraIcon class="h-3 w-3" />
146
+ <span>JSON</span>
147
+ </button>
167
148
  </div>
168
149
  </div>
169
150
  </div>
170
-
171
- <transition-group name="fade">
172
- <FilterPreview v-if="activeModal === 'preview-filter'" :filter="filterBuilder" />
173
- </transition-group>
174
151
  </div>
152
+
153
+ <transition-group name="fade">
154
+ <FilterPreview v-if="activeModal === 'preview-filter'" :filter="filterBuilder" />
155
+ </transition-group>
175
156
  </div>
176
157
  </template>
177
158
 
178
159
  <script setup>
179
160
  import draggable from "vuedraggable";
180
- import { ref, nextTick, watch, computed, onMounted, onUnmounted } from "vue";
161
+ import { ref, computed, defineAsyncComponent, watch, onMounted, onUnmounted, nextTick } from "vue";
181
162
  import { onBeforeRouteLeave } from "vue-router";
182
- import { Table, Header } from "@/components/Table";
163
+ import { useFilterCSS } from "@/composables/useFilterCSS";
183
164
  import Filter from "@/components/Filter/Filter.vue";
184
165
  import { FilterIcon } from "@/components/icons";
185
- import { DEBUG } from "@/utils/debug";
186
-
187
- import { ReloadIcon, TrashIcon, EditIcon, CameraIcon, StadiumIcon, SavingsIcon } from "@/components/icons";
166
+ import { ReloadIcon, TrashIcon, EditIcon, CameraIcon } from "@/components/icons";
188
167
  import { sendSaveFilter } from "@/stores/requests";
189
168
  import PriceSortToggle from "@/components/Filter/PriceSortToggle.vue";
190
169
  import { useUIStore } from "@/stores/ui";
191
170
  import FilterBuilder from "@/libs/Filter";
192
171
  import panzoom from "@/libs/panzoom.js";
193
- import FilterPreview from "@/components/Filter/FilterPreview.vue";
172
+
173
+ // Lazy-loaded modal component
174
+ const FilterPreview = defineAsyncComponent(() => import("@/components/Filter/FilterPreview.vue"));
194
175
  const activeModal = computed(() => ui.activeModal);
195
176
 
196
177
  const ui = useUIStore();
@@ -214,6 +195,12 @@ let renderer;
214
195
 
215
196
  const shownFilters = ref("All");
216
197
  const filterBuilder = ref(new FilterBuilder());
198
+ const draggableFilters = computed({
199
+ get: () => filterBuilder.value.filters,
200
+ set: (value) => {
201
+ filterBuilder.value.filters = value;
202
+ }
203
+ });
217
204
 
218
205
  const hasWildcardFilter = computed(() => {
219
206
  return filterBuilder.value.filters.some((filter) => filter.buyAny && filterBuilder.value.isForCurrentEvent(filter));
@@ -221,118 +208,15 @@ const hasWildcardFilter = computed(() => {
221
208
 
222
209
  const addWildcardFilter = () => {
223
210
  if (!hasWildcardFilter.value) {
224
- // Close any expanded filter first
225
211
  filterBuilder.value.setExpandedFilter(null);
226
212
  filterBuilder.value.addFilter({ buyAny: true, eventId: filterBuilder.value.currentEventId });
227
213
  filterBuilder.value.updateCss();
228
214
  }
229
215
  };
230
216
 
231
- // Initialize RendererFactory
232
217
  let RendererFactory = import("@necrolab/tm-renderer");
233
218
 
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
- });
219
+ const { injectStyles, cssUpdateTrigger } = useFilterCSS(filterBuilder, svg);
336
220
 
337
221
  const doesFilterShow = (filter) => {
338
222
  if ((filter.event || filter.eventId) !== filterBuilder.value.currentEventId) return;
@@ -346,42 +230,63 @@ RendererFactory.then((r) => {
346
230
  rendererFactory = new r.default();
347
231
  rendererFactory.init();
348
232
  }).catch((error) => {
349
- console.error("Failed to initialize renderer:", error);
233
+ ui.logger.Error("Failed to initialize renderer:", error);
350
234
  });
351
235
 
352
236
  const updateShownVenue = async () => {
353
237
  eventId.value = eventId.value.trim();
354
238
  if (eventId.value.includes("/event/")) eventId.value = eventId.value.split("/event/")[1];
355
239
 
356
- const country = eventId.value.length === 16 ? null : ui.currentCountry.id;
357
-
358
- if (!country && eventId.value.length !== 16) {
359
- ui.showError("Invalid eventId!");
240
+ // Determine country based on eventId length and current module
241
+ let country = null;
242
+ if (eventId.value.length === 16) {
243
+ // 16-char eventIds are global, but try current country first as fallback
244
+ country = ui.currentCountry?.id || null;
245
+ } else if (eventId.value.length > 0) {
246
+ // Shorter eventIds need country specification
247
+ country = ui.currentCountry?.id;
248
+ if (!country) {
249
+ ui.showError("Invalid eventId or missing country!");
250
+ return;
251
+ }
252
+ } else {
253
+ ui.showError("Event ID is required!");
360
254
  return;
361
255
  }
362
256
 
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
- });
373
-
374
257
  try {
258
+ if (!rendererFactory) {
259
+ ui.showError("Renderer not initialized yet. Please wait and try again.");
260
+ return;
261
+ }
262
+
263
+ renderer = rendererFactory.createRenderer(eventId.value, {
264
+ proxy: "",
265
+ country: country,
266
+ seatColor: "#0557ae",
267
+ nonAvSeatColor: "#dadcde",
268
+ highlightedSeatColor: "#d0006f"
269
+ });
270
+
271
+ filterBuilder.value.highlightedSeatColor = renderer.config.highlightedSeatColor;
272
+ renderer.setCustomConfig({
273
+ logFullError: true,
274
+ includeDetailedAttributes: true,
275
+ renderRowBlocks: true
276
+ });
277
+
375
278
  await renderer.render();
376
279
  svg.value = renderer.svg;
280
+
281
+ if (!svg.value) {
282
+ throw new Error("Renderer returned empty SVG");
283
+ }
377
284
  } catch (e) {
378
285
  ui.logger.Error("COULD NOT RENDER SVG", e);
379
- ui.showError(e.message);
380
- }
286
+ const errorMsg = e?.message || "Unknown rendering error";
287
+ ui.showError(`Failed to render venue: ${errorMsg}`);
381
288
 
382
- if (!svg.value) {
383
- ui.logger.Error("Could not render SVG");
384
- ui.showError("Could not fetch event map");
289
+ // Try to load existing filter even if rendering fails
385
290
  await loadFilter();
386
291
  filterBuilder.value.reload(eventId.value);
387
292
  return;
@@ -485,203 +390,27 @@ watch(renderSeats, async () => {
485
390
  })();
486
391
  </script>
487
392
 
488
- <style scoped>
489
- .compact-filter {
490
- font-size: 0.75rem;
491
- padding: 0.25rem !important;
492
- }
493
-
494
- .compact-filter .handle {
495
- transform: scale(0.8);
496
- }
497
-
498
- /* Enhanced table and filter styling */
499
- .filters-container {
500
- @apply space-y-0;
501
- padding: 4px;
502
- }
503
-
504
- /* Dragging states for better UX */
505
- .sortable-ghost {
506
- @apply opacity-30;
507
- border: 1px solid oklch(0.28 0 0);
508
- background-color: rgba(68, 69, 75, 0.1);
509
- }
510
-
511
- .sortable-chosen .drag-handle {
512
- border: 1px solid oklch(0.28 0 0);
513
- background-color: rgba(68, 69, 75, 0.2);
514
- }
515
-
516
- .sortable-drag {
517
- @apply z-50 rotate-1 scale-105 transform shadow-xl;
518
- }
519
-
520
- /* Unified Event ID + Load button group */
521
- .unified-load-group {
522
- border: 2px solid oklch(0.2809 0 0);
523
- border-radius: 0.5rem;
524
- overflow: hidden;
525
- background: oklch(0.2603 0 0);
526
- transition: all 0.15s ease;
527
-
528
- input, button {
529
- border: none !important;
530
- border-radius: 0 !important;
531
- outline: none !important;
532
- }
533
-
534
- input {
535
- flex: 1;
536
-
537
- &::placeholder {
538
- color: oklch(0.50 0 0);
539
- }
540
- }
541
-
542
- button {
543
- border-left: 1px solid oklch(0.2809 0 0) !important;
544
- padding: 0 1rem;
545
- font-weight: 500;
546
- transition: all 0.15s ease;
547
-
548
- &:hover {
549
- background: oklch(0.72 0.15 145 / 0.1);
550
- }
551
- }
552
-
553
- &:hover {
554
- border-color: oklch(0.30 0 0);
555
- }
556
-
557
- &:focus-within {
558
- border-color: oklch(0.72 0.15 145);
559
- outline: 1px solid oklch(0.72 0.15 145);
560
- outline-offset: 0;
561
- }
562
- }
563
-
564
- /* Header button styling */
565
- .header-btn {
566
- @apply flex items-center gap-2 rounded-md border px-3 py-2 text-sm font-medium transition-all duration-150;
567
- }
568
-
569
- .save-btn {
570
- background-color: oklch(0.72 0.15 145 / 0.15);
571
- border-color: oklch(0.72 0.15 145 / 0.5);
572
- color: oklch(0.90 0 0);
573
- }
574
-
575
- .save-btn:hover {
576
- background-color: oklch(0.72 0.15 145 / 0.25);
577
- border-color: oklch(0.72 0.15 145);
578
- }
579
-
580
- .clear-btn {
581
- background-color: oklch(0.60 0.20 25 / 0.15);
582
- border-color: oklch(0.60 0.20 25 / 0.5);
583
- color: oklch(0.90 0 0);
584
- }
585
-
586
- .clear-btn:hover {
587
- background-color: oklch(0.60 0.20 25 / 0.25);
588
- border-color: oklch(0.60 0.20 25);
589
- }
590
-
591
- .filter-builder-container {
592
- height: 90vh;
593
- overflow: hidden;
594
- }
595
-
596
- @media (max-width: 768px) {
597
- .filter-builder-container {
598
- overflow: auto;
599
- }
600
- }
601
-
602
- /* Mobile portrait header spacing fixes */
603
- @media (max-width: 640px) {
604
- .filter-builder-container .flex.items-center.justify-center {
605
- gap: 0.5rem;
606
- }
607
- }
608
-
609
- /* iPhone landscape and tablet portrait mode fixes */
610
- @media (max-width: 1023px) and (max-height: 768px) {
611
- .filter-builder-container {
612
- height: auto;
613
- min-height: 100vh;
614
- overflow: auto;
615
- }
616
- }
617
-
618
- /* Desktop: enforce grid column constraints */
619
- @media (min-width: 1024px) {
620
- .lg\\:grid-cols-5 > .lg\\:col-span-3 {
621
- max-width: 60%; /* 3/5 of grid */
622
- flex-shrink: 0;
623
- }
624
- }
625
-
626
- .svg-container {
627
- min-height: 300px;
628
- height: 100%;
629
- width: 100%;
630
- position: relative;
631
- overflow: auto;
632
- }
633
-
634
- /* Desktop: constrain svg-container to prevent expansion */
635
- @media (min-width: 1024px) {
636
- .svg-container {
637
- position: relative;
638
- max-width: 100%;
639
- max-height: 100%;
640
- overflow: hidden;
641
- }
642
- }
643
- </style>
644
-
645
393
  <style>
646
394
  /* Global styles needed for dynamically injected SVG content and Filter.js CSS injection */
647
395
  .svg-wrapper {
648
396
  position: relative;
649
- transform-origin: 0 0;
650
- min-width: 100%;
651
- min-height: 100%;
652
- }
653
-
654
- /* Desktop constraints - prevent svg-wrapper from expanding beyond container */
655
- @media (min-width: 1024px) {
656
- .svg-wrapper {
657
- position: absolute;
658
- top: 0;
659
- left: 0;
660
- width: 100%;
661
- height: 100%;
662
- max-width: 100%;
663
- max-height: 100%;
664
- min-width: unset;
665
- min-height: unset;
666
- overflow: hidden;
667
- }
397
+ transform-origin: center center;
398
+ width: 100%;
399
+ height: 100%;
400
+ display: flex;
401
+ align-items: center;
402
+ justify-content: center;
668
403
  }
669
404
 
670
405
  .svg-wrapper > svg {
671
406
  width: 100%;
672
407
  height: auto;
408
+ max-height: 100%;
409
+ object-fit: contain;
673
410
  background: center center no-repeat;
674
411
  background-size: contain;
675
412
  }
676
413
 
677
- /* Desktop SVG constraints */
678
- @media (min-width: 1024px) {
679
- .svg-wrapper > svg {
680
- max-width: 100%;
681
- max-height: 100%;
682
- }
683
- }
684
-
685
414
  /* Ensure Filter.js generated CSS works properly */
686
415
  .svg-wrapper path {
687
416
  pointer-events: auto;