@necrolab/dashboard 0.5.15 → 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 (121) 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 +12 -20
  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 +220 -0
  20. package/src/assets/css/main.scss +66 -77
  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 +45 -51
  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 -44
  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 +89 -56
  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 +103 -139
  83. package/src/components/ui/controls/atomic/MultiDropdown.vue +71 -119
  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 +3 -4
  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 +13 -24
  108. package/src/views/Console.vue +70 -172
  109. package/src/views/Editor.vue +211 -379
  110. package/src/views/FilterBuilder.vue +188 -371
  111. package/src/views/Login.vue +3 -28
  112. package/src/views/Profiles.vue +8 -15
  113. package/src/views/Tasks.vue +49 -36
  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 -2438
  117. package/exit +0 -209
  118. package/run +0 -177
  119. package/src/assets/css/base/color-fallbacks.scss +0 -10
  120. package/switch-branch.sh +0 -41
  121. /package/public/{reconnect-logo.png → img/reconnect-logo.png} +0 -0
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <div v-if="content" class="flex items-start gap-1 text-3xs leading-tight-sm min-h-2.75">
3
+ <svg class="icon-sm" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
4
+ <slot name="icon"></slot>
5
+ </svg>
6
+ <span :class="['text-light-500 text-3xs leading-tight-sm', truncate ? 'truncate' : '']">{{ content }}</span>
7
+ </div>
8
+ </template>
9
+
10
+ <script setup>
11
+ defineProps({
12
+ content: {
13
+ type: String,
14
+ required: true
15
+ },
16
+ truncate: {
17
+ type: Boolean,
18
+ default: false
19
+ }
20
+ });
21
+ </script>
@@ -2,8 +2,8 @@
2
2
  <Modal>
3
3
  <template #header> Mass Edit Presale Code <i class="fa fa-edit ml-3 mt-1"></i> </template>
4
4
  <!-- Event ID -->
5
- <div class="input-wrapper mt-7 mb-4">
6
- <label class="label-override mb-2">Event ID <StadiumIcon /></label>
5
+ <div class="mt-7 mb-4">
6
+ <label class="flex mb-2 label-override">Event ID <StadiumIcon /></label>
7
7
  <div class="input-default required">
8
8
  <input
9
9
  :placeholder="!isEU(ui.currentCountry.siteId) ? '102PDA9125510GYU' : '529171'"
@@ -12,33 +12,23 @@
12
12
  </div>
13
13
  </div>
14
14
  <!-- Presale Code -->
15
- <div class="input-wrapper mb-8">
16
- <label class="label-override mb-2">Presale Code <BagIcon /></label>
15
+ <div class="mb-8">
16
+ <label class="flex mb-2 label-override">Presale Code <BagIcon /></label>
17
17
  <div class="input-default required">
18
18
  <input placeholder="presale code" v-model="code" />
19
19
  </div>
20
20
  </div>
21
- <button
22
- class="button-default bg-dark-400 w-48 text-xs flex items-center justify-center gap-x-2 ml-auto border border-light-300 hover:border-light-400"
23
- @click="done()"
24
- >
21
+ <button class="btn-modal ml-auto" @click="done()">
25
22
  Edit <EditIcon />
26
23
  </button>
27
24
  </Modal>
28
25
  </template>
29
- <style lang="scss" scoped>
30
- .input-wrapper {
31
- label {
32
- @apply flex;
33
- }
34
- }
35
- </style>
36
26
  <script setup>
37
27
  import Modal from "@/components/ui/Modal.vue";
38
28
  import { StadiumIcon, BagIcon, EditIcon } from "@/components/icons";
39
29
  import { useUIStore } from "@/stores/ui";
40
30
  import { ref } from "vue";
41
- import { isEU } from "@/stores/utils";
31
+ import { isEU } from "@/libs/utils/eventUrl";
42
32
 
43
33
  const ui = useUIStore();
44
34
  const code = ref("");
@@ -9,23 +9,40 @@
9
9
  <div
10
10
  v-for="(value, keyName) in category"
11
11
  :key="keyName"
12
- class="input-container"
12
+ class="form-field-labeled-sm mb-3"
13
13
  >
14
14
  <span class="w-1/3 text-light-300 font-medium text-xs">{{ keyName }}</span>
15
- <div class="w-2/3 h-full flex items-center">
15
+ <div class="w-2/3 h-full relative">
16
16
  <input
17
17
  type="text"
18
- class="quick-settings-input"
18
+ class="api-key-input pr-16"
19
19
  :placeholder="`Enter ${keyName} key`"
20
20
  v-model="quickConfig.keys[categoryName][keyName]"
21
21
  />
22
+ <div class="balance-indicator">
23
+ <div v-if="balanceState[categoryName]?.[keyName]?.loading" class="flex items-center gap-1">
24
+ <svg class="animate-spin h-3 w-3 text-light-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
25
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
26
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
27
+ </svg>
28
+ </div>
29
+ <div v-else-if="balanceState[categoryName]?.[keyName]?.error" class="flex items-center gap-1" title="Failed to fetch balance">
30
+ <svg class="h-3 w-3 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
31
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
32
+ </svg>
33
+ </div>
34
+ <div v-else-if="balanceState[categoryName]?.[keyName]?.balance !== null && balanceState[categoryName]?.[keyName]?.balance !== undefined" class="flex items-center gap-1 text-green-400" :title="`Balance: ${balanceState[categoryName]?.[keyName]?.balance}`">
35
+ <img src="@/assets/img/sell.svg" width="9" class="opacity-60" />
36
+ <span class="text-2xs font-bold tabular-nums">{{ formatBalance(balanceState[categoryName]?.[keyName]?.balance) }}</span>
37
+ </div>
38
+ </div>
22
39
  </div>
23
40
  </div>
24
41
  </div>
25
42
 
26
43
  <div class="text-light-400 mb-2 mt-4">Proxy Lists</div>
27
44
 
28
- <div class="input-container relative-positioned z-tooltip">
45
+ <div class="form-field-labeled-sm mb-3 relative z-dropdown">
29
46
  <span class="w-1/3 text-light-300 font-medium text-xs">Checkout Proxies</span>
30
47
  <Dropdown
31
48
  class="!w-2/3"
@@ -38,7 +55,7 @@
38
55
  />
39
56
  </div>
40
57
 
41
- <div class="input-container relative-positioned z-dropdown">
58
+ <div class="form-field-labeled-sm mb-3 relative z-dropdown">
42
59
  <span class="w-1/3 text-light-300 font-medium text-xs">Queue Proxies</span>
43
60
  <Dropdown
44
61
  class="!w-2/3"
@@ -50,163 +67,56 @@
50
67
  :allowDefault="false"
51
68
  />
52
69
  </div>
53
- <div class="flex mt-5 justify-between flex-row items-center quicksettings-bottom">
54
- <div class="flex gap-3 items-center quicksettings-versions">
55
- <div class="version-badge">
56
- <span class="version-label">Dashboard</span>
57
- <span class="version-number">v{{ version }}</span>
70
+ <div class="flex mt-5 justify-between flex-row items-center gap-3 flex-col sm:flex-row max-xs:gap-3 max-xs:items-stretch">
71
+ <div class="flex gap-2 items-center max-xs:justify-center max-xs:flex-wrap max-xs:gap-2 max-xs:w-full">
72
+ <div class="flex items-center gap-x-1.5 px-2.5 py-1.5 rounded-lg border bg-dark-400 border-dark-550 max-xs:flex-1 max-xs:min-w-0 text-center text-2xs max-sm:gap-x-1">
73
+ <span class="font-medium text-light-500">Dashboard</span>
74
+ <span class="font-bold text-accent-green">v{{ version }}</span>
58
75
  </div>
59
- <div class="version-badge">
60
- <span class="version-label">Bot</span>
61
- <span class="version-number">v{{ ui.botVersion }}</span>
76
+ <div class="flex items-center gap-x-1.5 px-2.5 py-1.5 rounded-lg border bg-dark-400 border-dark-550 max-xs:flex-1 max-xs:min-w-0 text-center text-2xs max-sm:gap-x-1">
77
+ <span class="font-medium text-light-500">Bot</span>
78
+ <span class="font-bold text-accent-green">v{{ ui.botVersion }}</span>
62
79
  </div>
63
80
  </div>
64
81
 
65
- <div class="flex gap-3 quicksettings-buttons">
82
+ <div class="flex gap-2 max-xs:w-full max-xs:flex-col max-xs:gap-2">
66
83
  <button
67
- class="quicksettings-btn bg-dark-400 flex items-center justify-center gap-x-2"
84
+ class="btn-modal h-9 text-2xs px-2 max-xs:w-full"
68
85
  @click="checkBalances()"
69
86
  >
70
- Balances <img src="@/assets/img/sell.svg" width="12" />
87
+ Balances <img src="@/assets/img/sell.svg" width="10" class="ml-1" />
71
88
  </button>
72
89
  <button
73
- class="quicksettings-btn bg-dark-400 flex items-center justify-center gap-x-2"
90
+ class="btn-modal h-9 text-2xs px-2 max-xs:w-full"
74
91
  @click="done(true)"
75
92
  >
76
- Save <img src="/img/save.svg" width="12" class="invert" />
93
+ Save <img src="/img/save.svg" width="10" class="ml-1 invert" />
77
94
  </button>
78
95
  </div>
79
96
  </div>
80
97
  </Modal>
81
98
  </template>
82
99
  <style lang="scss">
83
- .input-wrapper {
84
- label {
85
- @apply flex;
86
- }
87
- }
88
-
89
- .transparent {
90
- background: transparent !important;
91
- }
92
-
93
- .input-container {
94
- @apply text-white bg-dark-500 mb-3 px-3 rounded-lg border-dark-550 border-2 flex items-center justify-between h-10;
95
- overflow: visible;
96
- transition: border-color 0.15s ease;
97
-
98
- &:focus-within,
99
- &:has(.dropdown.opened) {
100
- border-color: oklch(0.72 0.15 145) !important;
101
- outline: 1px solid oklch(0.72 0.15 145) !important;
102
- outline-offset: 0;
103
- }
104
- }
105
-
106
- .quick-settings-input {
107
- @apply w-full h-full text-sm text-white bg-transparent border-0 outline-none px-2 py-1 transition-colors duration-150;
108
-
109
- &:focus {
110
- @apply outline-none border-0 shadow-none bg-transparent;
111
- }
112
-
113
- &:hover:not(:focus) {
114
- background: transparent;
115
- }
116
-
117
- &::placeholder {
118
- color: oklch(0.50 0 0);
119
- }
120
- }
121
-
122
- .input-container:hover {
123
- border-color: oklch(0.30 0 0);
124
- }
125
-
126
- .input-container:focus-within {
127
- border-color: oklch(0.72 0.15 145) !important;
128
- outline: 1px solid oklch(0.72 0.15 145);
129
- outline-offset: 0;
130
- }
131
-
132
- /* iPhone portrait mode fixes */
133
- @media (max-width: 430px) and (orientation: portrait) {
134
- .quicksettings-bottom {
135
- flex-direction: column;
136
- gap: 1rem;
137
- align-items: stretch;
138
- }
139
-
140
- .quicksettings-versions {
141
- justify-content: center;
142
- flex-wrap: wrap;
143
- gap: 0.5rem;
144
- }
145
-
146
- .quicksettings-versions > div {
147
- flex: 1;
148
- min-width: 120px;
149
- text-align: center;
150
- }
151
-
152
- .quicksettings-buttons {
153
- justify-content: center;
154
- gap: 0.75rem;
155
- }
156
-
157
- .quicksettings-buttons button {
158
- width: 5rem;
159
- padding-left: 0.5rem;
160
- padding-right: 0.5rem;
161
- gap: 0.25rem;
162
- }
163
-
164
- .quicksettings-buttons button img {
165
- width: 12px;
166
- }
167
- }
168
-
169
- /* Fix modal scrolling on mobile */
100
+ /* Modal mobile scrolling uses :deep() due to teleported element */
170
101
  @media (max-width: 768px) {
171
- :deep(.modal-mask) {
172
- overflow-y: auto !important;
173
- overflow-x: hidden !important;
174
- touch-action: pan-y !important;
175
- -webkit-overflow-scrolling: touch !important;
176
- }
177
-
178
- :deep(.component-modal) {
179
- overflow-y: visible !important;
180
- overflow-x: hidden !important;
181
- touch-action: pan-y !important;
182
- max-height: none !important;
183
- }
102
+ :deep(.modal-mask) { overflow-y: auto !important; overflow-x: hidden !important; touch-action: pan-y !important; -webkit-overflow-scrolling: touch !important; }
103
+ :deep(.component-modal) { overflow-y: visible !important; overflow-x: hidden !important; touch-action: pan-y !important; max-height: none !important; }
184
104
  }
185
105
 
186
- /* Dropdowns are teleported to body, so modal can scroll normally */
187
-
188
- /* Dropdown styling fixes */
189
- :deep(.dropdown) {
106
+ /* Dropdown styling uses :deep() due to teleport to body */
107
+ :deep(.dropdown),
108
+ :deep(.dropdown.opened),
109
+ :deep(.dropdown:focus-within),
110
+ :deep(.dropdown:hover) {
190
111
  background: transparent !important;
191
112
  border: none !important;
192
113
  box-shadow: none !important;
193
- padding: 0 !important;
194
- height: 40px !important;
195
- }
196
-
197
- /* Keep dropdown border hidden when opened (container handles border) */
198
- :deep(.dropdown.opened),
199
- :deep(.dropdown:focus-within) {
200
- border: none !important;
201
114
  outline: none !important;
202
- box-shadow: none !important;
203
115
  }
204
116
 
205
- :deep(.dropdown:hover),
206
- :deep(.dropdown:focus-within) {
207
- border: none !important;
208
- background: transparent !important;
209
- box-shadow: none !important;
117
+ :deep(.dropdown) {
118
+ padding: 0 !important;
119
+ height: 40px !important;
210
120
  }
211
121
 
212
122
  :deep(.dropdown-display) {
@@ -219,6 +129,12 @@
219
129
  line-height: 1.25rem;
220
130
  }
221
131
 
132
+ :deep(.dropdown-item) {
133
+ padding: 0.625rem 1rem;
134
+ font-size: 0.875rem;
135
+ line-height: 1.25rem;
136
+ }
137
+
222
138
  :deep(.dropdown-menu-portal) {
223
139
  z-index: 26000 !important;
224
140
  max-height: 200px !important;
@@ -226,8 +142,8 @@
226
142
  overscroll-behavior: contain !important;
227
143
  touch-action: pan-y !important;
228
144
  -webkit-overflow-scrolling: touch !important;
229
- border: 1px solid oklch(0.26 0 0) !important;
230
- background: linear-gradient(135deg, oklch(0.18 0 0) 0%, oklch(0.20 0 0) 100%) !important;
145
+ @apply border border-dark-550 !important;
146
+ @apply bg-gradient-to-br from-dark-800 to-dark-750 !important;
231
147
  width: calc(100vw - 4rem) !important;
232
148
  max-width: 600px !important;
233
149
 
@@ -236,82 +152,21 @@
236
152
  }
237
153
  }
238
154
 
239
- :deep(.dropdown-item) {
240
- padding: 0.625rem 1rem;
241
- font-size: 0.875rem;
242
- line-height: 1.25rem;
243
- }
244
-
245
- /* Ensure proper z-index stacking */
246
- .z-tooltip {
247
- z-index: 100 !important;
248
- }
249
-
250
- .z-dropdown {
251
- z-index: 90 !important;
252
- }
253
-
254
- /* QuickSettings compact buttons */
255
- .quicksettings-btn {
256
- @apply rounded transition-all duration-150;
257
- background: oklch(0.2046 0 0);
258
- border: 2px solid oklch(0.2809 0 0);
259
- color: oklch(0.90 0 0);
260
- height: 2.5rem;
261
- width: 6.5rem;
262
- font-size: 0.75rem;
263
- font-weight: 500;
155
+ .api-key-input {
156
+ @apply w-full h-full text-sm text-white border-0 rounded-md;
157
+ @apply px-3 py-2;
158
+ @apply focus:outline-none;
159
+ background: oklch(26% 0 68deg);
264
160
 
265
- &:hover {
266
- border-color: oklch(0.72 0.15 145);
267
- }
268
-
269
- &:active, &:focus {
270
- border-color: oklch(0.72 0.15 145);
271
- outline: 1px solid oklch(0.72 0.15 145);
272
- outline-offset: 0;
273
- }
274
-
275
- @media (max-width: 640px) {
276
- height: 2.25rem;
277
- width: 5rem;
278
- font-size: 0.7rem;
279
- padding: 0 0.5rem;
280
-
281
- img {
282
- width: 10px !important;
283
- }
161
+ &::placeholder {
162
+ @apply text-light-500;
284
163
  }
285
164
  }
286
165
 
287
- /* Version badges */
288
- .version-badge {
289
- @apply flex items-center gap-x-2 px-3 py-2 rounded-lg border;
290
- background: oklch(0.2046 0 0);
291
- border-color: oklch(0.2809 0 0);
292
-
293
- .version-label {
294
- @apply text-xs font-medium;
295
- color: oklch(0.65 0 0);
296
- }
297
-
298
- .version-number {
299
- @apply text-xs font-bold;
300
- color: oklch(0.72 0.15 145);
301
- }
302
-
303
- @media (max-width: 640px) {
304
- padding: 0.375rem 0.625rem;
305
- gap: 0.375rem;
306
-
307
- .version-label {
308
- font-size: 0.625rem;
309
- }
310
-
311
- .version-number {
312
- font-size: 0.625rem;
313
- }
314
- }
166
+ .balance-indicator {
167
+ @apply absolute right-3 top-1/2 -translate-y-1/2;
168
+ @apply flex items-center justify-center;
169
+ @apply pointer-events-none;
315
170
  }
316
171
  </style>
317
172
  <script setup>
@@ -319,15 +174,14 @@ import Modal from "@/components/ui/Modal.vue";
319
174
  import { GearIcon } from "@/components/icons";
320
175
  import { useUIStore } from "@/stores/ui";
321
176
  import { ref } from "vue";
322
- import Switch from "@/components/ui/controls/atomic/Switch.vue";
323
177
  import Dropdown from "@/components/ui/controls/atomic/Dropdown.vue";
324
- import { sendQuickConfig, getQuickConfig } from "@/stores/requests";
325
- import { getBalances, sendProxyList, getProxyLists } from "../../stores/requests";
178
+ import { sendQuickConfig, getQuickConfig, getBalances, sendProxyList, getProxyLists } from "@/stores/requests";
326
179
 
327
180
  const version = __APP_VERSION__;
328
181
  const quickConfig = ref({});
329
182
  const ui = useUIStore();
330
183
  const proxyLists = ref(["loading..."]);
184
+ const balanceState = ref({});
331
185
 
332
186
  const loadCurrentQuickConfig = async () => {
333
187
  try {
@@ -367,12 +221,82 @@ const loadProxyLists = async () => {
367
221
  };
368
222
 
369
223
  const checkBalances = async () => {
224
+ // Initialize loading state for all keys
225
+ Object.entries(quickConfig.value.keys || {}).forEach(([categoryName, category]) => {
226
+ if (!balanceState.value[categoryName]) {
227
+ balanceState.value[categoryName] = {};
228
+ }
229
+ Object.keys(category).forEach((keyName) => {
230
+ balanceState.value[categoryName][keyName] = {
231
+ loading: true,
232
+ error: false,
233
+ balance: null
234
+ };
235
+ });
236
+ });
237
+
370
238
  const balances = await getBalances();
371
- if (typeof balances == "object")
239
+
240
+ if (typeof balances === "object") {
372
241
  Object.entries(balances).forEach(([service, balance]) => {
373
- ui.showSuccess(`${service} balance: ${balance}`);
242
+ // Find which category and key this service belongs to
243
+ Object.entries(quickConfig.value.keys || {}).forEach(([categoryName, category]) => {
244
+ Object.keys(category).forEach((keyName) => {
245
+ // Match service name (case-insensitive)
246
+ if (keyName.toLowerCase().includes(service.toLowerCase()) ||
247
+ service.toLowerCase().includes(keyName.toLowerCase())) {
248
+ balanceState.value[categoryName][keyName] = {
249
+ loading: false,
250
+ error: false,
251
+ balance: balance
252
+ };
253
+ }
254
+ });
255
+ });
256
+ });
257
+
258
+ // Mark any keys that didn't get a response as errors
259
+ Object.entries(balanceState.value).forEach(([categoryName, category]) => {
260
+ Object.entries(category).forEach(([keyName, state]) => {
261
+ if (state.loading) {
262
+ balanceState.value[categoryName][keyName] = {
263
+ loading: false,
264
+ error: true,
265
+ balance: null
266
+ };
267
+ }
268
+ });
269
+ });
270
+
271
+ ui.showSuccess("Balances updated");
272
+ } else {
273
+ // Mark all as error
274
+ Object.entries(balanceState.value).forEach(([categoryName, category]) => {
275
+ Object.keys(category).forEach((keyName) => {
276
+ balanceState.value[categoryName][keyName] = {
277
+ loading: false,
278
+ error: true,
279
+ balance: null
280
+ };
281
+ });
374
282
  });
375
- else ui.showError("Could not get balances");
283
+ ui.showError("Could not get balances");
284
+ }
285
+ };
286
+
287
+ const formatBalance = (balance) => {
288
+ if (typeof balance === 'number') {
289
+ // Format large numbers with K, M suffixes if needed
290
+ if (balance >= 1000000) {
291
+ return (balance / 1000000).toFixed(1) + 'M';
292
+ } else if (balance >= 100000) {
293
+ return (balance / 1000).toFixed(0) + 'K';
294
+ } else if (balance >= 10000) {
295
+ return (balance / 1000).toFixed(1) + 'K';
296
+ }
297
+ return balance.toLocaleString();
298
+ }
299
+ return String(balance);
376
300
  };
377
301
 
378
302
  loadProxyLists();
@@ -3,7 +3,7 @@
3
3
  <template #header> Scrape Venue <ScrapeIcon class="ml-4" /></template>
4
4
  <!-- Event ID -->
5
5
  <div class="input-wrapper mt-7 mb-4">
6
- <label class="label-override mb-2">Event ID or URL <StadiumIcon /></label>
6
+ <label class="label-override mb-2 flex">Event ID or URL <StadiumIcon /></label>
7
7
  <div class="input-default required">
8
8
  <input
9
9
  :placeholder="!isEU(ui.currentCountry.siteId) ? '102PDA9125510GYU' : '529171'"
@@ -11,27 +11,18 @@
11
11
  />
12
12
  </div>
13
13
  </div>
14
- <button
15
- class="button-default ml-auto mt-4 flex w-48 items-center justify-center gap-x-2 bg-dark-400 text-xs"
16
- @click="done()">
14
+ <button class="btn-modal ml-auto mt-4 w-48" @click="done()">
17
15
  Scrape Venue
18
- <ScrapeIcon />
16
+ <ScrapeIcon class="ml-2" />
19
17
  </button>
20
18
  </Modal>
21
19
  </template>
22
- <style lang="scss" scoped>
23
- .input-wrapper {
24
- label {
25
- @apply flex;
26
- }
27
- }
28
- </style>
29
20
  <script setup>
30
21
  import Modal from "@/components/ui/Modal.vue";
31
22
  import { StadiumIcon, ScrapeIcon } from "@/components/icons";
32
23
  import { useUIStore } from "@/stores/ui";
33
24
  import { ref } from "vue";
34
- import { isEU } from "@/stores/utils";
25
+ import { isEU } from "@/libs/utils/eventUrl";
35
26
 
36
27
  const ui = useUIStore();
37
28
  const eventId = ref("");