@necrolab/dashboard 0.5.14 → 0.5.16

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 (120) 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 +140 -170
  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 +58 -15
  13. package/src/assets/css/components/forms.scss +31 -32
  14. package/src/assets/css/components/headers.scss +119 -0
  15. package/src/assets/css/components/modals.scss +2 -2
  16. package/src/assets/css/components/search-groups.scss +28 -19
  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 +220 -0
  20. package/src/assets/css/main.scss +72 -75
  21. package/src/components/Auth/LoginForm.vue +5 -84
  22. package/src/components/Editors/Account/Account.vue +8 -10
  23. package/src/components/Editors/Account/AccountCreator.vue +28 -59
  24. package/src/components/Editors/Account/AccountView.vue +38 -86
  25. package/src/components/Editors/Account/CreateAccount.vue +8 -50
  26. package/src/components/Editors/Profile/CreateProfile.vue +74 -131
  27. package/src/components/Editors/Profile/Profile.vue +15 -17
  28. package/src/components/Editors/Profile/ProfileCountryChooser.vue +16 -60
  29. package/src/components/Editors/Profile/ProfileView.vue +46 -96
  30. package/src/components/Editors/TagLabel.vue +16 -55
  31. package/src/components/Editors/TagToggle.vue +20 -8
  32. package/src/components/Filter/Filter.vue +62 -75
  33. package/src/components/Filter/FilterPreview.vue +161 -135
  34. package/src/components/Filter/PriceSortToggle.vue +36 -43
  35. package/src/components/Table/Header.vue +1 -1
  36. package/src/components/Table/Table.vue +61 -12
  37. package/src/components/Tasks/CheckStock.vue +7 -16
  38. package/src/components/Tasks/Controls/DesktopControls.vue +15 -60
  39. package/src/components/Tasks/Controls/MobileControls.vue +5 -20
  40. package/src/components/Tasks/CreateTaskAXS.vue +20 -118
  41. package/src/components/Tasks/CreateTaskTM.vue +33 -189
  42. package/src/components/Tasks/EventDetailRow.vue +21 -0
  43. package/src/components/Tasks/MassEdit.vue +6 -16
  44. package/src/components/Tasks/QuickSettings.vue +140 -216
  45. package/src/components/Tasks/ScrapeVenue.vue +4 -13
  46. package/src/components/Tasks/Stats.vue +19 -38
  47. package/src/components/Tasks/Task.vue +65 -268
  48. package/src/components/Tasks/TaskLabel.vue +9 -3
  49. package/src/components/Tasks/TaskView.vue +43 -63
  50. package/src/components/Tasks/Utilities.vue +10 -42
  51. package/src/components/Tasks/ViewTask.vue +23 -107
  52. package/src/components/icons/Close.vue +2 -8
  53. package/src/components/icons/Gear.vue +8 -8
  54. package/src/components/icons/Hash.vue +5 -0
  55. package/src/components/icons/Key.vue +2 -8
  56. package/src/components/icons/Pencil.vue +2 -8
  57. package/src/components/icons/Profile.vue +2 -8
  58. package/src/components/icons/Sell.vue +2 -8
  59. package/src/components/icons/Spinner.vue +4 -7
  60. package/src/components/icons/SquareCheck.vue +2 -8
  61. package/src/components/icons/SquareUncheck.vue +2 -8
  62. package/src/components/icons/Wildcard.vue +2 -8
  63. package/src/components/icons/index.js +3 -1
  64. package/src/components/ui/ActionButtonGroup.vue +113 -52
  65. package/src/components/ui/BalanceIndicator.vue +60 -0
  66. package/src/components/ui/EmptyState.vue +24 -0
  67. package/src/components/ui/EnableDisableToggle.vue +23 -0
  68. package/src/components/ui/FormField.vue +48 -48
  69. package/src/components/ui/IconLabel.vue +23 -0
  70. package/src/components/ui/InfoRow.vue +21 -54
  71. package/src/components/ui/Modal.vue +78 -37
  72. package/src/components/ui/Navbar.vue +60 -41
  73. package/src/components/ui/ReadonlyFieldsSection.vue +31 -0
  74. package/src/components/ui/ReconnectIndicator.vue +111 -124
  75. package/src/components/ui/SectionCard.vue +6 -14
  76. package/src/components/ui/Splash.vue +2 -10
  77. package/src/components/ui/StatusBadge.vue +26 -28
  78. package/src/components/ui/TaskToggle.vue +54 -0
  79. package/src/components/ui/controls/CountryChooser.vue +27 -64
  80. package/src/components/ui/controls/EyeToggle.vue +1 -1
  81. package/src/components/ui/controls/atomic/Checkbox.vue +40 -121
  82. package/src/components/ui/controls/atomic/Dropdown.vue +102 -95
  83. package/src/components/ui/controls/atomic/MultiDropdown.vue +72 -94
  84. package/src/components/ui/controls/atomic/Switch.vue +21 -84
  85. package/src/composables/useColorMapping.js +15 -0
  86. package/src/composables/useCopyToClipboard.js +1 -1
  87. package/src/composables/useDateFormatting.js +21 -0
  88. package/src/composables/useDeviceDetection.js +14 -0
  89. package/src/composables/useDropdownPosition.js +5 -6
  90. package/src/composables/useDynamicTableHeight.js +31 -0
  91. package/src/composables/useRowSelection.js +0 -3
  92. package/src/composables/useTicketPricing.js +16 -0
  93. package/src/composables/useWindowDimensions.js +21 -0
  94. package/src/libs/Filter.js +14 -20
  95. package/src/libs/panzoom.js +1 -5
  96. package/src/libs/utils/array.js +60 -0
  97. package/src/{stores/utils.js → libs/utils/dataGeneration.js} +2 -250
  98. package/src/libs/utils/eventUrl.js +40 -0
  99. package/src/libs/utils/string.js +28 -0
  100. package/src/libs/utils/time.js +20 -0
  101. package/src/libs/utils/validation.js +88 -0
  102. package/src/main.js +0 -2
  103. package/src/stores/connection.js +1 -4
  104. package/src/stores/logger.js +6 -12
  105. package/src/stores/sampleData.js +1 -2
  106. package/src/stores/ui.js +59 -36
  107. package/src/views/Accounts.vue +17 -31
  108. package/src/views/Console.vue +76 -176
  109. package/src/views/Editor.vue +217 -383
  110. package/src/views/FilterBuilder.vue +190 -373
  111. package/src/views/Login.vue +3 -28
  112. package/src/views/Profiles.vue +12 -22
  113. package/src/views/Tasks.vue +51 -38
  114. package/tailwind.config.js +82 -71
  115. package/workbox-config.cjs +47 -5
  116. package/docs/plans/2026-02-08-tailwind-consolidation.md +0 -2416
  117. package/exit +0 -209
  118. package/run +0 -177
  119. package/switch-branch.sh +0 -41
  120. /package/public/{reconnect-logo.png → img/reconnect-logo.png} +0 -0
@@ -1,196 +1,176 @@
1
1
  <template>
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>
2
+ <div class="flex w-full flex-col">
3
+ <div class="page-header">
4
+ <div class="page-header-card flex-shrink-0">
5
+ <FilterIcon />
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-all duration-150 hover:bg-dark-450"
16
+ @click="updateShownVenue"
17
+ aria-label="Load venue">
18
+ <ReloadIcon class="h-4 w-4" />
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 items-center gap-2">
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="h-4 w-4 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 items-center gap-2">
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 items-center gap-2">
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, nextTick, watch, computed, defineAsyncComponent, onUnmounted } from "vue";
181
162
  import { onBeforeRouteLeave } from "vue-router";
182
- import { Table, Header } from "@/components/Table";
183
163
  import Filter from "@/components/Filter/Filter.vue";
184
164
  import { FilterIcon } from "@/components/icons";
185
- import { DEBUG } from "@/utils/debug";
186
-
187
- import { ReloadIcon, TrashIcon, EditIcon, CameraIcon, StadiumIcon, SavingsIcon } from "@/components/icons";
165
+ import { ReloadIcon, TrashIcon, EditIcon, CameraIcon } from "@/components/icons";
188
166
  import { sendSaveFilter } from "@/stores/requests";
189
167
  import PriceSortToggle from "@/components/Filter/PriceSortToggle.vue";
190
168
  import { useUIStore } from "@/stores/ui";
191
169
  import FilterBuilder from "@/libs/Filter";
192
170
  import panzoom from "@/libs/panzoom.js";
193
- import FilterPreview from "@/components/Filter/FilterPreview.vue";
171
+
172
+ // Lazy-loaded modal component
173
+ const FilterPreview = defineAsyncComponent(() => import("@/components/Filter/FilterPreview.vue"));
194
174
  const activeModal = computed(() => ui.activeModal);
195
175
 
196
176
  const ui = useUIStore();
@@ -214,6 +194,12 @@ let renderer;
214
194
 
215
195
  const shownFilters = ref("All");
216
196
  const filterBuilder = ref(new FilterBuilder());
197
+ const draggableFilters = computed({
198
+ get: () => filterBuilder.value.filters,
199
+ set: (value) => {
200
+ filterBuilder.value.filters = value;
201
+ }
202
+ });
217
203
 
218
204
  const hasWildcardFilter = computed(() => {
219
205
  return filterBuilder.value.filters.some((filter) => filter.buyAny && filterBuilder.value.isForCurrentEvent(filter));
@@ -221,28 +207,23 @@ const hasWildcardFilter = computed(() => {
221
207
 
222
208
  const addWildcardFilter = () => {
223
209
  if (!hasWildcardFilter.value) {
224
- // Close any expanded filter first
225
210
  filterBuilder.value.setExpandedFilter(null);
226
211
  filterBuilder.value.addFilter({ buyAny: true, eventId: filterBuilder.value.currentEventId });
227
212
  filterBuilder.value.updateCss();
228
213
  }
229
214
  };
230
215
 
231
- // Initialize RendererFactory
232
216
  let RendererFactory = import("@necrolab/tm-renderer");
233
217
 
234
- // Real-time CSS injection system
235
218
  let styleElement = null;
236
219
  const cssUpdateTrigger = ref(0);
237
220
  const STYLE_ELEMENT_ID = "filter-builder-styles";
238
221
 
239
222
  const injectStyles = () => {
240
- // Only inject styles if we have an SVG to style
241
223
  const svgWrapper = document.getElementById("svg-wrapper");
242
224
  if (!svgWrapper || !svg.value) return;
243
225
 
244
226
  if (!styleElement) {
245
- // Remove any existing style element first
246
227
  const existingStyle = document.getElementById(STYLE_ELEMENT_ID);
247
228
  if (existingStyle) {
248
229
  existingStyle.remove();
@@ -259,9 +240,7 @@ const injectStyles = () => {
259
240
  }
260
241
  };
261
242
 
262
- // Setup real-time CSS updates
263
243
  const setupCSSUpdates = () => {
264
- // Override updateCss method to trigger immediate updates
265
244
  const originalUpdateCss = filterBuilder.value.updateCss.bind(filterBuilder.value);
266
245
  filterBuilder.value.updateCss = () => {
267
246
  originalUpdateCss();
@@ -271,7 +250,6 @@ const setupCSSUpdates = () => {
271
250
  });
272
251
  };
273
252
 
274
- // Override highlight method for immediate temporary CSS
275
253
  const originalHighlight = filterBuilder.value.highlight.bind(filterBuilder.value);
276
254
  filterBuilder.value.highlight = (...args) => {
277
255
  originalHighlight(...args);
@@ -280,7 +258,6 @@ const setupCSSUpdates = () => {
280
258
  });
281
259
  };
282
260
 
283
- // Override clearHighlight method
284
261
  const originalClearHighlight = filterBuilder.value.clearHighlight.bind(filterBuilder.value);
285
262
  filterBuilder.value.clearHighlight = () => {
286
263
  originalClearHighlight();
@@ -304,11 +281,6 @@ const cleanupStyles = () => {
304
281
  }
305
282
  };
306
283
 
307
- // Basic cleanup monitoring
308
- const debugCleanup = () => {
309
- console.log("FilterBuilder cleanup completed");
310
- };
311
-
312
284
  // Clean up any leftover styles from previous instances
313
285
  cleanupStyles();
314
286
 
@@ -346,42 +318,63 @@ RendererFactory.then((r) => {
346
318
  rendererFactory = new r.default();
347
319
  rendererFactory.init();
348
320
  }).catch((error) => {
349
- console.error("Failed to initialize renderer:", error);
321
+ ui.logger.Error("Failed to initialize renderer:", error);
350
322
  });
351
323
 
352
324
  const updateShownVenue = async () => {
353
325
  eventId.value = eventId.value.trim();
354
326
  if (eventId.value.includes("/event/")) eventId.value = eventId.value.split("/event/")[1];
355
327
 
356
- const country = eventId.value.length === 16 ? null : ui.currentCountry.id;
357
-
358
- if (!country && eventId.value.length !== 16) {
359
- ui.showError("Invalid eventId!");
328
+ // Determine country based on eventId length and current module
329
+ let country = null;
330
+ if (eventId.value.length === 16) {
331
+ // 16-char eventIds are global, but try current country first as fallback
332
+ country = ui.currentCountry?.id || null;
333
+ } else if (eventId.value.length > 0) {
334
+ // Shorter eventIds need country specification
335
+ country = ui.currentCountry?.id;
336
+ if (!country) {
337
+ ui.showError("Invalid eventId or missing country!");
338
+ return;
339
+ }
340
+ } else {
341
+ ui.showError("Event ID is required!");
360
342
  return;
361
343
  }
362
344
 
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
345
  try {
346
+ if (!rendererFactory) {
347
+ ui.showError("Renderer not initialized yet. Please wait and try again.");
348
+ return;
349
+ }
350
+
351
+ renderer = rendererFactory.createRenderer(eventId.value, {
352
+ proxy: "",
353
+ country: country,
354
+ seatColor: "#0557ae",
355
+ nonAvSeatColor: "#dadcde",
356
+ highlightedSeatColor: "#d0006f"
357
+ });
358
+
359
+ filterBuilder.value.highlightedSeatColor = renderer.config.highlightedSeatColor;
360
+ renderer.setCustomConfig({
361
+ logFullError: true,
362
+ includeDetailedAttributes: true,
363
+ renderRowBlocks: true
364
+ });
365
+
375
366
  await renderer.render();
376
367
  svg.value = renderer.svg;
368
+
369
+ if (!svg.value) {
370
+ throw new Error("Renderer returned empty SVG");
371
+ }
377
372
  } catch (e) {
378
373
  ui.logger.Error("COULD NOT RENDER SVG", e);
379
- ui.showError(e.message);
380
- }
374
+ const errorMsg = e?.message || "Unknown rendering error";
375
+ ui.showError(`Failed to render venue: ${errorMsg}`);
381
376
 
382
- if (!svg.value) {
383
- ui.logger.Error("Could not render SVG");
384
- ui.showError("Could not fetch event map");
377
+ // Try to load existing filter even if rendering fails
385
378
  await loadFilter();
386
379
  filterBuilder.value.reload(eventId.value);
387
380
  return;
@@ -485,203 +478,27 @@ watch(renderSeats, async () => {
485
478
  })();
486
479
  </script>
487
480
 
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
481
  <style>
646
482
  /* Global styles needed for dynamically injected SVG content and Filter.js CSS injection */
647
483
  .svg-wrapper {
648
484
  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
- }
485
+ transform-origin: center center;
486
+ width: 100%;
487
+ height: 100%;
488
+ display: flex;
489
+ align-items: center;
490
+ justify-content: center;
668
491
  }
669
492
 
670
493
  .svg-wrapper > svg {
671
494
  width: 100%;
672
495
  height: auto;
496
+ max-height: 100%;
497
+ object-fit: contain;
673
498
  background: center center no-repeat;
674
499
  background-size: contain;
675
500
  }
676
501
 
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
502
  /* Ensure Filter.js generated CSS works properly */
686
503
  .svg-wrapper path {
687
504
  pointer-events: auto;