@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
@@ -2,63 +2,56 @@
2
2
  <div class="flex items-center gap-1 font-bold text-white lg:gap-3" v-if="ui.queueStats.show" :key="key">
3
3
  <div
4
4
  v-if="ui.queueStats.total"
5
- class="stats-card flex h-10 min-w-0 items-center justify-between gap-2 rounded-lg px-3 text-sm">
6
- <h2 class="flex items-center gap-1 whitespace-nowrap text-sm font-bold">
5
+ class="stat-badge">
6
+ <h2 class="stat-header">
7
7
  <img width="14px" src="@/assets/img/wildcard.svg" />
8
8
  <span class="hidden md:inline">Total</span>
9
9
  </h2>
10
- <span class="flex items-center justify-center whitespace-nowrap text-xs font-black text-light-400">
10
+ <span class="stat-value">
11
11
  {{ ui.queueStats.total }}
12
12
  </span>
13
13
  </div>
14
14
  <div
15
15
  v-if="ui.queueStats.queued"
16
- class="stats-card flex h-10 min-w-0 items-center justify-between gap-2 rounded-lg px-3 text-sm">
17
- <h2 class="flex items-center gap-1 whitespace-nowrap text-sm font-bold">
16
+ class="stat-badge">
17
+ <h2 class="stat-header">
18
18
  <SkiIcon />
19
19
  <span class="hidden md:inline">Queued</span>
20
20
  </h2>
21
- <span class="flex items-center justify-center whitespace-nowrap text-xs font-black text-light-400">
21
+ <span class="stat-value">
22
22
  {{ ui.queueStats.queued }}
23
23
  </span>
24
24
  </div>
25
25
  <div
26
26
  v-if="ui.queueStats.sleeping"
27
- class="stats-card flex h-10 min-w-0 items-center justify-between gap-2 rounded-lg px-3 text-sm">
28
- <h2 class="flex items-center gap-1 whitespace-nowrap text-sm font-bold">
27
+ class="stat-badge">
28
+ <h2 class="stat-header">
29
29
  <TimerIcon />
30
30
  <span class="hidden md:inline">Sleeping</span>
31
31
  </h2>
32
- <span class="flex items-center justify-center whitespace-nowrap text-xs font-black text-light-400">
32
+ <span class="stat-value">
33
33
  {{ ui.queueStats.sleeping }}
34
34
  </span>
35
35
  </div>
36
36
  <div
37
37
  v-if="ui.queueStats.nextQueuePasses.length > 0"
38
- class="stats-card mb-2 flex h-8 min-w-0 items-center justify-between gap-2 rounded-xl p-2 text-sm lg:mb-5 lg:gap-3 lg:p-3">
39
- <h2 class="flex items-center gap-1 whitespace-nowrap text-sm font-bold">
38
+ class="mb-2 flex h-8 min-w-0 items-center justify-between gap-2 rounded-xl p-2 text-sm lg:mb-5 lg:gap-3 lg:p-3 bg-dark-400 border-2 border-dark-550">
39
+ <h2 class="stat-header">
40
40
  <CartIcon />
41
41
  <span class="hidden sm:block">Next Passes</span>
42
42
  <span class="block sm:hidden">Pass</span>
43
43
  </h2>
44
- <span class="flex items-center truncate text-right text-xs font-black text-light-400">
44
+ <span class="flex items-center truncate text-right text-xs font-bold text-light-300">
45
45
  {{ ui.queueStats.nextQueuePasses.slice(0, queuePassAmount).join(", ") }}
46
46
  </span>
47
47
  </div>
48
- <!-- <div
49
- v-if="ui.queueStats.carts"
50
- class="bg-dark-500 mb-2 text-sm rounded-lg lg:p-2 p-1 lg:gap-3 gap-2 flex justify-between"
51
- >
52
- <h2 class="font-bold text-sm flex items-center gap-1"><CartIcon />Carts</h2>
53
- <span class="text-light-400 text-sm font-black flex justify-center">{{ ui.queueStats.sleeping }} </span>
54
- </div> -->
55
48
  </div>
56
49
  </template>
57
50
 
58
51
  <script setup>
59
52
  import { SkiIcon, TimerIcon, CartIcon } from "@/components/icons";
60
53
  import { useUIStore } from "@/stores/ui";
61
- import { ref } from "vue";
54
+ import { ref, onUnmounted } from "vue";
62
55
 
63
56
  const ui = useUIStore();
64
57
 
@@ -71,22 +64,10 @@ const getQueuePassAmount = (width) => {
71
64
  let key = ref(0);
72
65
  let queuePassAmount = ref(getQueuePassAmount(window.innerWidth));
73
66
 
74
- window.addEventListener("resize", () => (queuePassAmount.value = getQueuePassAmount(window.innerWidth)));
75
- </script>
76
- <style lang="scss" scoped>
77
- .stats-card {
78
- @apply rounded;
79
- background: oklch(0.2046 0 0);
80
- border: 2px solid oklch(0.2809 0 0);
81
-
82
- h2 {
83
- font-weight: 600;
84
- color: oklch(0.90 0 0);
85
- }
67
+ const handleResize = () => (queuePassAmount.value = getQueuePassAmount(window.innerWidth));
68
+ window.addEventListener("resize", handleResize);
86
69
 
87
- span {
88
- font-weight: 700;
89
- color: oklch(0.90 0 0);
90
- }
91
- }
92
- </style>
70
+ onUnmounted(() => {
71
+ window.removeEventListener("resize", handleResize);
72
+ });
73
+ </script>
@@ -5,68 +5,65 @@
5
5
  @dblclick="handleDoubleClick"
6
6
  @touchstart="handleTouchStart"
7
7
  @touchend="handleTouchEnd">
8
- <div class="col-span-1 flex items-start justify-start lg:col-span-2 py-2">
8
+ <div class="col-span-1 flex items-center justify-start lg:col-span-2 py-2">
9
9
  <Checkbox
10
10
  class="ml-2 mr-4 flex-shrink-0"
11
11
  :toggled="props.task.selected"
12
12
  @valueUpdate="ui.toggleTaskSelected(props.task.taskId)" />
13
13
  <div
14
14
  v-if="props.preferEventName && props.task.eventName"
15
- class="event-details hidden lg:flex flex-col gap-0.5 justify-center cursor-pointer"
15
+ class="hidden lg:flex flex-col gap-0.5 justify-center cursor-pointer max-w-full min-w-0"
16
16
  @click="copy(props.task.eventId, 'Copied event ID')"
17
17
  :title="`Event ID: ${props.task.eventId}`">
18
- <div class="event-name text-white text-[11px] font-semibold leading-tight">
18
+ <div class="max-w-45 truncate text-xs+ font-semibold leading-tight text-white">
19
19
  {{ props.task.eventName }}
20
20
  </div>
21
- <div v-if="props.task.venueName || props.task.eventCity" class="event-venue flex items-start gap-1 text-[9px] leading-tight">
22
- <svg class="event-icon mt-[1px]" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
21
+ <EventDetailRow :content="[props.task.venueName, props.task.eventCity].filter(Boolean).join(', ')" truncate>
22
+ <template #icon>
23
23
  <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
24
24
  <circle cx="12" cy="10" r="3"></circle>
25
- </svg>
26
- <span class="truncate text-light-500">{{ [props.task.venueName, props.task.eventCity].filter(Boolean).join(', ') }}</span>
27
- </div>
28
- <div v-if="props.task.eventLocalDate" class="event-date flex items-start gap-1 text-[9px] leading-tight">
29
- <svg class="event-icon mt-[1px]" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
25
+ </template>
26
+ </EventDetailRow>
27
+ <EventDetailRow :content="formatEventDate(props.task.eventLocalDate)">
28
+ <template #icon>
30
29
  <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
31
30
  <line x1="16" y1="2" x2="16" y2="6"></line>
32
31
  <line x1="8" y1="2" x2="8" y2="6"></line>
33
32
  <line x1="3" y1="10" x2="21" y2="10"></line>
34
- </svg>
35
- <span class="text-light-500">{{ formatEventDate(props.task.eventLocalDate) }}</span>
36
- </div>
37
- <div v-if="props.task.email" class="event-email flex items-start gap-1 text-[9px] leading-tight">
38
- <svg class="event-icon mt-[1px]" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
33
+ </template>
34
+ </EventDetailRow>
35
+ <div v-if="props.task.email" class="flex items-start gap-1 text-3xs leading-tight-sm min-h-2.75">
36
+ <svg class="icon-sm" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
39
37
  <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
40
38
  <polyline points="22,6 12,13 2,6"></polyline>
41
39
  </svg>
42
- <span class="truncate text-light-500">{{ props.task.email }}</span>
40
+ <span class="truncate text-light-500 text-3xs leading-tight-sm">{{ props.task.email }}</span>
43
41
  </div>
44
42
  </div>
45
43
  <div
46
44
  v-else
47
- class="event-id-details hidden lg:flex flex-col gap-0.5 justify-center cursor-pointer"
48
- @click="copy(props.task.eventId, 'Copied event ID')">
49
- <div class="event-id-row flex items-center gap-1 text-[11px] text-white font-semibold">
50
- <svg class="event-icon" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
45
+ class="hidden lg:flex flex-col gap-0.5 justify-center cursor-pointer max-w-full min-w-0"
46
+ @click="copy(props.task.eventId || props.task.email, props.task.eventId ? 'Copied event ID' : 'Copied email')">
47
+ <div class="flex items-center gap-1 text-xs+ text-white font-semibold max-w-45 truncate leading-tight-md">
48
+ <svg class="icon-sm" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
51
49
  <path d="M9 11l3 3L22 4"></path>
52
50
  <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
53
51
  </svg>
54
- <span>{{ props.task.eventId }}</span>
52
+ <span>{{ props.task.eventId || props.task.email }}</span>
55
53
  </div>
56
- <div v-if="props.task.email" class="event-email flex items-start gap-1 text-[9px] leading-tight">
57
- <svg class="event-icon mt-[1px]" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
54
+ <EventDetailRow v-if="props.task.eventId && props.task.email && props.task.eventId !== props.task.email" :content="props.task.email" truncate>
55
+ <template #icon>
58
56
  <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
59
57
  <polyline points="22,6 12,13 2,6"></polyline>
60
- </svg>
61
- <span class="truncate text-light-500">{{ props.task.email }}</span>
62
- </div>
58
+ </template>
59
+ </EventDetailRow>
63
60
  </div>
64
61
  </div>
65
- <div class="col-span-2 overflow-hidden">
66
- <h4 class="text-white text-xs leading-tight">
62
+ <div class="col-span-2 overflow-hidden lg:col-span-3 xl:col-span-2">
63
+ <h4 class="text-center text-light-300 text-xs leading-tight lg:text-2xs">
67
64
  <span v-if="!props.task.reservedTicketsList">-</span>
68
65
  <div v-else class="overflow-hidden">
69
- <div v-for="(l, index) in props.task.reservedTicketsList.split('\n')" :key="l" class="truncate">
66
+ <div v-for="(l, index) in props.task.reservedTicketsList.split('\n')" :key="`ticket-${index}`" class="truncate">
70
67
  <span
71
68
  v-if="!!l.trim()"
72
69
  class="text-xs"
@@ -84,10 +81,10 @@
84
81
  </span>
85
82
  </h4>
86
83
  </div>
87
- <div class="col-span-5 justify-center text-center md:col-span-4 lg:col-span-5">
88
- <div class="status-container">
84
+ <div class="col-span-6 justify-center text-center md:col-span-5 lg:col-span-4 xl:col-span-5">
85
+ <div class="task-status-badge">
89
86
  <div
90
- class="status-indicator"
87
+ class="w-2 h-2 rounded-full flex-shrink-0"
91
88
  :class="
92
89
  colorToClass(
93
90
  props.task.active || props.task.status.toLowerCase() === 'idle'
@@ -95,43 +92,43 @@
95
92
  : 'red'
96
93
  )
97
94
  "></div>
98
- <span class="status-text">{{ props.task.status }}</span>
95
+ <span class="truncate text-sm font-medium uppercase text-light-300 tracking-wide max-md:text-xs max-md:max-w-full max-sm:tracking-normal">{{ props.task.status }}</span>
99
96
  </div>
100
97
  </div>
101
- <div class="col-span-2 flex lg:col-span-3 overflow-visible">
102
- <ActionButtonGroup class="overflow-visible">
98
+ <div class="col-span-1 flex md:col-span-2 lg:col-span-3 overflow-visible max-sm:min-w-0 max-sm:flex-shrink-0 max-sm:items-center max-sm:justify-end max-sm:px-0.5">
99
+ <ActionButtonGroup class="overflow-visible" :allowWrap="true">
103
100
  <li>
104
- <button v-if="task.active" @click="ui.stopTask(task.taskId)">
101
+ <button v-if="task.active" @click="ui.stopTask(task.taskId)" aria-label="Stop task">
105
102
  <PauseIcon />
106
103
  </button>
107
- <button v-else @click="ui.startTask(task.taskId)">
104
+ <button v-else @click="ui.startTask(task.taskId)" aria-label="Start task">
108
105
  <PlayIcon />
109
106
  </button>
110
107
  </li>
111
108
  <li v-if="task.status?.toLowerCase() == 'waiting' && props.task._timeLeftString !== '00:00'">
112
- <button @click="ui.continueTask(task.taskId, 'autocheckout')">
109
+ <button @click="ui.continueTask(task.taskId, 'autocheckout')" aria-label="Auto checkout">
113
110
  <BagWhiteIcon />
114
111
  </button>
115
112
  </li>
116
113
  <li v-if="task.status?.toLowerCase() == 'waiting'">
117
- <button @click="ui.continueTask(task.taskId, 'change_reservation')">
114
+ <button @click="ui.continueTask(task.taskId, 'change_reservation')" aria-label="Change reservation">
118
115
  <EditIcon />
119
116
  </button>
120
117
  </li>
121
118
  <li>
122
- <button @click="ui.deleteTask(task.taskId)">
119
+ <button @click="ui.deleteTask(task.taskId)" aria-label="Delete task">
123
120
  <TrashIcon />
124
121
  </button>
125
122
  </li>
126
123
  <li @contextmenu.prevent="handleRightClick">
127
- <button @click="openViewTaskModal">
124
+ <button @click="openViewTaskModal" aria-label="View task details">
128
125
  <EyeIcon />
129
126
  </button>
130
127
  </li>
131
128
  </ActionButtonGroup>
132
129
  </div>
133
- <div class="absolute right-5 top-4 col-span-1 hidden items-center justify-center md:block lg:flex">
134
- <h4 class="task-id text-center text-xs text-white">
130
+ <div class="absolute right-5 top-4 col-span-1 hidden items-center justify-center xl:flex max-sm:left-16 max-sm:top-1 max-sm:z-10">
131
+ <h4 class="text-center text-xs text-light-500 bg-dark-475/40 border border-dark-500 font-semibold tracking-wide m-0 px-1.5 py-0.5 rounded text-2xs lg:text-2xs">
135
132
  {{ props.task.taskId }}
136
133
  </h4>
137
134
  </div>
@@ -155,202 +152,40 @@
155
152
  </transition>
156
153
  </Row>
157
154
  </template>
158
- <style lang="scss" scoped>
159
- h4 {
160
- @apply text-center;
161
- color: oklch(0.90 0 0);
162
- }
163
-
164
- .event-details,
165
- .event-id-details {
166
- max-width: 100%;
167
- min-width: 0;
168
- gap: 2px;
169
-
170
- .event-name,
171
- .event-id-row {
172
- max-width: 180px;
173
- overflow: hidden;
174
- text-overflow: ellipsis;
175
- white-space: nowrap;
176
- font-size: 11px;
177
- line-height: 1.3;
178
- }
179
-
180
- .event-icon {
181
- @apply min-w-[10px] flex-shrink-0;
182
- min-height: 10px;
183
- color: oklch(0.60 0 0);
184
- stroke: oklch(0.60 0 0);
185
- fill: none;
186
-
187
- path,
188
- rect,
189
- line,
190
- circle,
191
- polyline {
192
- stroke: oklch(0.60 0 0);
193
- fill: none;
194
- }
195
- }
196
-
197
- .event-venue,
198
- .event-date,
199
- .event-email {
200
- line-height: 1.2;
201
- min-height: 11px;
202
-
203
- span {
204
- color: oklch(0.60 0 0);
205
- font-size: 9px;
206
- line-height: 1.2;
207
- }
208
- }
209
- }
210
-
211
- .status-container {
212
- @apply mx-auto flex w-fit items-center justify-center rounded-lg border border-dark-650 bg-dark-500;
213
- padding: 6px 12px;
214
- gap: 6px;
215
- max-width: 100%;
216
- pointer-events: none;
217
-
218
- @media (max-width: 768px) {
219
- padding: 4px 8px;
220
- gap: 4px;
221
- }
222
- }
223
-
224
- .status-text {
225
- @apply truncate text-sm font-medium uppercase;
226
- color: oklch(0.90 0 0);
227
- letter-spacing: 0.025em;
228
-
229
- @media (max-width: 768px) {
230
- font-size: 0.65rem;
231
- max-width: 140px;
232
- }
233
- }
234
-
235
- /* Mobile specific styling - changed from 480px to 640px for earlier transition */
236
- @media (max-width: 640px) {
237
- /* Position adjustment for mobile taskId */
238
- .block.md\\:hidden {
239
- @apply left-16 top-1 z-10;
240
- }
241
-
242
- /* Make the actions column more flexible */
243
- .col-span-2.lg\\:col-span-3 {
244
- min-width: 0;
245
- flex-shrink: 0;
246
- display: flex;
247
- align-items: center;
248
- justify-content: center;
249
- padding: 0 2px;
250
- }
251
-
252
- /* Compact status container */
253
- .status-container {
254
- @apply border-0 shadow-sm;
255
- padding: 2px 4px;
256
- font-size: 0.7rem;
257
- max-width: 100%;
258
- }
259
-
260
- .status-text {
261
- font-size: 0.65rem;
262
- letter-spacing: 0;
263
- }
264
- }
265
-
266
- .task-id {
267
- font-size: 10px;
268
- font-weight: 600;
269
- letter-spacing: 0.5px;
270
- margin: 0;
271
- color: oklch(0.65 0 0);
272
- background: rgba(46, 47, 52, 0.4);
273
- padding: 2px 6px;
274
- border-radius: 4px;
275
- border: 1px solid oklch(0.26 0 0);
276
- }
277
-
278
- .task-event-id {
279
- font-size: 11px;
280
- font-weight: 500;
281
- text-align: center;
282
- color: oklch(0.82 0 0);
283
-
284
- &:hover {
285
- color: #ffffff;
286
- }
287
-
288
- @media (max-width: 1024px) {
289
- font-size: 10px;
290
- }
291
-
292
- @media (max-width: 768px) {
293
- font-size: 9px;
294
- }
295
- }
296
-
297
- /* Responsive task styling */
298
- @screen lg {
299
- h4 {
300
- font-size: 10px;
301
- }
302
-
303
- .task-id {
304
- font-size: 8px;
305
- }
306
-
307
- .task-event-id {
308
- font-size: 10px;
309
- }
310
- }
311
- </style>
312
155
 
313
156
  <script setup>
314
157
  /// <reference path="@/types/index.js" />
315
158
 
316
159
  import { Row } from "@/components/Table";
317
- import { PlayIcon, TrashIcon, BagWhiteIcon, PauseIcon, EditIcon, EyeIcon, StadiumIcon, TimerIcon } from "@/components/icons";
160
+ import { PlayIcon, TrashIcon, BagWhiteIcon, PauseIcon, EditIcon, EyeIcon } from "@/components/icons";
318
161
  import Checkbox from "@/components/ui/controls/atomic/Checkbox.vue";
319
162
  import ActionButtonGroup from "@/components/ui/ActionButtonGroup.vue";
163
+ import EventDetailRow from "@/components/Tasks/EventDetailRow.vue";
320
164
  import { useUIStore } from "@/stores/ui";
321
- import TaskLabel from "@/components/Tasks/TaskLabel.vue";
322
- import ViewTask from "@/components/Tasks/ViewTask.vue";
323
- import { computed, ref, onMounted, onUnmounted, nextTick } from "vue";
165
+ import { ref, onUnmounted, nextTick } from "vue";
324
166
  import { useRowSelection } from "@/composables/useRowSelection";
325
167
  import { useCopyToClipboard } from "@/composables/useCopyToClipboard";
168
+ import { useColorMapping } from "@/composables/useColorMapping";
169
+ import { useDateFormatting } from "@/composables/useDateFormatting";
170
+ import { useTicketPricing } from "@/composables/useTicketPricing";
326
171
 
327
172
  const ui = useUIStore();
328
173
  const { copy } = useCopyToClipboard();
174
+ const { colorToClass } = useColorMapping();
175
+ const { formatEventDate } = useDateFormatting();
176
+ const { isTotalPrice } = useTicketPricing();
329
177
 
330
178
  /** @type {{ task: Task }} */
331
179
  const props = defineProps({
332
- task: { type: Object },
333
- preferEventName: { type: Boolean, default: false }
334
- });
335
-
336
- // Format event date for display
337
- const formatEventDate = (dateString) => {
338
- if (!dateString) return '';
339
- try {
340
- const date = new Date(dateString);
341
- const options = {
342
- month: 'short',
343
- day: 'numeric',
344
- year: 'numeric',
345
- hour: 'numeric',
346
- minute: '2-digit',
347
- hour12: true
348
- };
349
- return date.toLocaleString('en-US', options).replace(',', '');
350
- } catch {
351
- return dateString;
180
+ task: {
181
+ type: Object,
182
+ required: true
183
+ },
184
+ preferEventName: {
185
+ type: Boolean,
186
+ default: false
352
187
  }
353
- };
188
+ });
354
189
 
355
190
  // Context menu positioning
356
191
  const contextMenuPosition = ref({});
@@ -361,20 +196,21 @@ const { handleDoubleClick, handleTouchStart, handleTouchEnd } = useRowSelection(
361
196
  ui.toggleTaskSelected(props.task.taskId);
362
197
  });
363
198
 
199
+ // Context menu dimensions
200
+ const MENU_WIDTH = 168; // w-42 = 10.5rem = 168px
201
+ const MENU_HEIGHT = 200; // Approximate height
202
+
364
203
  // Handle right-click to position context menu
365
204
  const handleRightClick = (event) => {
366
- const menuWidth = 168; // w-42 = 10.5rem = 168px
367
- const menuHeight = 200; // Approximate height
368
-
369
205
  let x = event.clientX;
370
206
  let y = event.clientY - 55;
371
207
 
372
208
  // Prevent menu from going off screen
373
- if (x + menuWidth > window.innerWidth) {
374
- x = event.clientX - menuWidth; // Show to the left instead
209
+ if (x + MENU_WIDTH > window.innerWidth) {
210
+ x = event.clientX - MENU_WIDTH; // Show to the left instead
375
211
  }
376
- if (y + menuHeight > window.innerHeight) {
377
- y = event.clientY - menuHeight; // Show above instead
212
+ if (y + MENU_HEIGHT > window.innerHeight) {
213
+ y = event.clientY - MENU_HEIGHT; // Show above instead
378
214
  }
379
215
 
380
216
  contextMenuPosition.value = {
@@ -410,32 +246,6 @@ const openViewTaskModal = () => {
410
246
  ui.toggleModal('view-task');
411
247
  };
412
248
 
413
- const isTotalPrice = (line, index, lines) => {
414
- const trimmed = line.trim();
415
- if (!trimmed) return false;
416
-
417
- // Check if this is the last non-empty line
418
- const nonEmptyLines = lines.filter(l => l.trim());
419
- const isLastLine = index === lines.lastIndexOf(nonEmptyLines[nonEmptyLines.length - 1]);
420
-
421
- if (!isLastLine) return false;
422
-
423
- // Check if line is a standalone price (not in parentheses)
424
- // Matches: $345.88, €345.88, USD 345.88, EUR 345.88, etc.
425
- // Does NOT match: ($86.47) or 2× 301/E ($86.47)
426
- const totalPricePattern = /^([$€£¥₹₽¢]|[A-Z]{3})\s*[\d,]+\.?\d*$/;
427
- return totalPricePattern.test(trimmed) && !trimmed.includes('(') && !trimmed.includes(')');
428
- };
429
-
430
- const colorMapping = new Map();
431
- colorMapping.set("green", "bg-green-400");
432
- colorMapping.set("red", "bg-red-400");
433
- colorMapping.set("error", "bg-red-400");
434
- colorMapping.set("success", "bg-green-400");
435
- const colorToClass = (color) => {
436
- return colorMapping.get(color) || "bg-white";
437
- };
438
-
439
249
  const openInBrowser = (debug) => {
440
250
  if (!props.task.openerLink) return;
441
251
  ui.showSuccess(`Opening in browser ${debug ? "(debug)" : ""}`);
@@ -446,19 +256,6 @@ const openInBrowser = (debug) => {
446
256
  openInNewTab(out);
447
257
  };
448
258
 
449
- const formatDate = (date) => {
450
- if (!date) return "TBA";
451
- const d = new Date(date);
452
- const iso = d.toISOString();
453
- const [year, month, day] = iso.substring(0, 10).split("-");
454
- const time = iso.substring(11, 16);
455
- const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
456
- const dayName = dayNames[d.getUTCDay()];
457
- const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
458
- const monthName = monthNames[parseInt(month) - 1];
459
- return `${dayName} ${monthName} ${parseInt(day)} ${year} ${time}`;
460
- };
461
-
462
259
  const openInNewTab = (href) => {
463
260
  if (!href) return;
464
261
  ui.logger.Info("Opening", href);
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="flex rounded-2xl gap-x-2 max-w-full shadow-3xl mx-auto items-center justify-center bg-dark-600">
2
+ <div class="flex rounded-2xl gap-x-2 max-w-full shadow-3xl mx-auto items-center justify-center bg-dark-550">
3
3
  <img class="ml-3 flex-shrink-0" v-if="logo" :src="logo" />
4
4
  <div v-else class="ml-3"></div>
5
5
  <span class="font-bold p-2 truncate mr-3">{{ text }}</span>
@@ -10,8 +10,14 @@
10
10
  import { ref } from "vue";
11
11
 
12
12
  const props = defineProps({
13
- image: { type: String },
14
- text: { type: String }
13
+ image: {
14
+ type: String,
15
+ default: ''
16
+ },
17
+ text: {
18
+ type: String,
19
+ required: true
20
+ }
15
21
  });
16
22
 
17
23
  const logo = ref(`/img/${props.image}.svg`);