@necrolab/dashboard 0.4.38 → 0.4.40

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 (65) hide show
  1. package/.claude/settings.local.json +7 -1
  2. package/.prettierrc +14 -1
  3. package/backend/api.js +25 -16
  4. package/backend/auth.js +2 -2
  5. package/backend/batching.js +1 -1
  6. package/backend/endpoints.js +5 -5
  7. package/backend/index.js +2 -2
  8. package/backend/mock-data.js +27 -28
  9. package/backend/mock-src/classes/logger.js +5 -7
  10. package/backend/mock-src/classes/utils.js +3 -2
  11. package/backend/mock-src/ticketmaster.js +2 -2
  12. package/backend/validator.js +2 -2
  13. package/dev-server.js +136 -0
  14. package/index.html +1 -1
  15. package/index.js +1 -1
  16. package/package.json +8 -6
  17. package/postcss.config.js +1 -1
  18. package/postinstall.js +30 -16
  19. package/public/android-chrome-192x192.png +0 -0
  20. package/public/android-chrome-512x512.png +0 -0
  21. package/public/apple-touch-icon.png +0 -0
  22. package/public/favicon-16x16.png +0 -0
  23. package/public/favicon-32x32.png +0 -0
  24. package/public/favicon.ico +0 -0
  25. package/public/manifest.json +4 -4
  26. package/src/App.vue +471 -49
  27. package/src/assets/css/_input.scss +37 -37
  28. package/src/assets/css/main.scss +177 -30
  29. package/src/assets/img/logo_icon-old.png +0 -0
  30. package/src/assets/img/logo_icon.png +0 -0
  31. package/src/components/Auth/LoginForm.vue +12 -5
  32. package/src/components/Editors/Account/Account.vue +19 -19
  33. package/src/components/Editors/Account/AccountCreator.vue +53 -24
  34. package/src/components/Editors/Account/AccountView.vue +79 -17
  35. package/src/components/Editors/Account/CreateAccount.vue +47 -28
  36. package/src/components/Editors/Profile/Profile.vue +24 -24
  37. package/src/components/Editors/Profile/ProfileView.vue +67 -16
  38. package/src/components/Editors/TagLabel.vue +6 -7
  39. package/src/components/Filter/FilterPreview.vue +0 -4
  40. package/src/components/Table/Table.vue +15 -0
  41. package/src/components/Tasks/Controls/DesktopControls.vue +1 -1
  42. package/src/components/Tasks/CreateTaskAXS.vue +15 -15
  43. package/src/components/Tasks/CreateTaskTM.vue +5 -4
  44. package/src/components/Tasks/Stats.vue +22 -16
  45. package/src/components/Tasks/Task.vue +100 -81
  46. package/src/components/Tasks/TaskView.vue +25 -23
  47. package/src/components/Tasks/Utilities.vue +1 -1
  48. package/src/components/icons/Mail.vue +2 -2
  49. package/src/components/ui/Modal.vue +84 -15
  50. package/src/components/ui/Navbar.vue +118 -39
  51. package/src/components/ui/controls/atomic/Dropdown.vue +23 -3
  52. package/src/components/ui/controls/atomic/MultiDropdown.vue +43 -23
  53. package/src/stores/sampleData.js +89 -64
  54. package/src/stores/ui.js +30 -4
  55. package/src/views/Accounts.vue +2 -2
  56. package/src/views/Console.vue +276 -41
  57. package/src/views/Editor.vue +175 -28
  58. package/src/views/FilterBuilder.vue +45 -49
  59. package/src/views/Login.vue +134 -12
  60. package/src/views/Profiles.vue +8 -8
  61. package/src/views/Tasks.vue +51 -2
  62. package/tailwind.config.js +2 -2
  63. package/vite.config.js +34 -1
  64. package/vue.config.js +1 -1
  65. package/{workbox-config.js → workbox-config.cjs} +1 -4
@@ -1,9 +1,5 @@
1
1
  <template>
2
- <Row
3
- class="relative text-white grid-cols-12 gap-2"
4
- @click="ui.setOpenContextMenu('')"
5
- @click.right.prevent="ui.setOpenContextMenu('')"
6
- >
2
+ <Row class="relative text-white grid-cols-10 gap-2 ipadlg:grid-cols-12" @click="ui.setOpenContextMenu('')">
7
3
  <div class="block md:hidden absolute left-1 top-1">
8
4
  <h4 class="text-xs task-id text-white font-bold">
9
5
  {{ props.task.taskId }}
@@ -13,18 +9,13 @@
13
9
  <Checkbox
14
10
  class="ml-2 mr-4 flex-shrink-0"
15
11
  :toggled="props.task.selected"
16
- @valueUpdate="ui.toggleTaskSelected(props.task.taskId)"
17
- />
12
+ @valueUpdate="ui.toggleTaskSelected(props.task.taskId)" />
18
13
  <h4
19
14
  class="task-event-id mx-auto hidden lg:block text-white cursor-pointer hover:text-light-300"
20
- @click="copy(props.task.eventId)"
21
- >
15
+ @click="copy(props.task.eventId)">
22
16
  {{ props.task.eventId }}
23
17
  </h4>
24
18
  </div>
25
- <div class="col-span-2 hidden lg:block">
26
- <h4 class="text-white">{{ props.task.quantity }}</h4>
27
- </div>
28
19
  <div class="col-span-2">
29
20
  <h4 class="text-white">
30
21
  <span v-if="!props.task.reservedTicketsList">-</span>
@@ -38,13 +29,12 @@
38
29
  :class="{
39
30
  'text-red-400':
40
31
  props.task._timeLeftString === '00:00' || props.task._timeLeftString === 'No Cartholds'
41
- }"
42
- >
32
+ }">
43
33
  {{ props.task._timeLeftString !== "00:00" ? props.task._timeLeftString : "Expired" }}
44
34
  </span>
45
35
  </h4>
46
36
  </div>
47
- <div class="col-span-6 md:col-span-4 lg:col-span-3 text-center justify-center">
37
+ <div class="col-span-5 md:col-span-4 lg:col-span-5 ipadlg:col-span-6 text-center justify-center">
48
38
  <div class="status-container">
49
39
  <div
50
40
  class="status-indicator"
@@ -54,12 +44,11 @@
54
44
  ? props.task.statusColor
55
45
  : 'red'
56
46
  )
57
- "
58
- ></div>
59
- <span class="status-text">{{ truncate(props.task.status, statusTruncateLength) }}</span>
47
+ "></div>
48
+ <span class="status-text">{{ props.task.status }}</span>
60
49
  </div>
61
50
  </div>
62
- <div class="col-span-2 flex">
51
+ <div class="col-span-2 ipadlg:col-span-3 flex">
63
52
  <ul class="task-buttons">
64
53
  <li>
65
54
  <button v-if="task.active" @click="ui.stopTask(task.taskId)">
@@ -84,7 +73,7 @@
84
73
  <TrashIcon />
85
74
  </button>
86
75
  </li>
87
- <li @click.right.prevent="window.setTimeout(() => ui.setOpenContextMenu(task.taskId), 10)">
76
+ <li @contextmenu.prevent="handleRightClick">
88
77
  <button @click="props.task.isExpanded = !props.task.isExpanded">
89
78
  <span>{{ props.task.isExpanded ? "−" : "+" }}</span>
90
79
  </button>
@@ -98,30 +87,26 @@
98
87
  </div>
99
88
  <transition name="fade">
100
89
  <div
101
- class="col-span-12 flex flex-wrap gap-x-4 gap-y-4 lg:gap-x-10 pt-8 pb-2 xl:justify-around will-change-auto"
102
- v-if="props.task.isExpanded"
103
- >
90
+ class="col-span-10 ipadlg:col-span-12 flex flex-wrap gap-x-4 gap-y-4 lg:gap-x-10 pt-8 pb-2 xl:justify-around will-change-auto"
91
+ v-if="props.task.isExpanded">
104
92
  <!-- Details -->
105
93
  <TaskLabel
106
94
  class="md:hidden"
107
95
  image="stadium_w"
108
96
  :text="props.task.eventId"
109
- @click="copy(props.task.eventId)"
110
- />
97
+ @click="copy(props.task.eventId)" />
111
98
 
112
99
  <TaskLabel class="md:hidden" image="bag_w" :text="props.task.quantity" />
113
100
  <TaskLabel
114
101
  v-if="props.task.email"
115
102
  image="mail"
116
103
  :text="props.task.email"
117
- @click="copy(props.task.email)"
118
- />
104
+ @click="copy(props.task.email)" />
119
105
  <TaskLabel
120
106
  v-if="props.task.password"
121
107
  image="key"
122
108
  :text="props.task.password"
123
- @click="copy(props.task.password)"
124
- />
109
+ @click="copy(props.task.password)" />
125
110
  <TaskLabel v-if="!props.task.email && !props.task.password" image="mail" text="No account chosen yet" />
126
111
  <TaskLabel v-if="props.task.profileName" image="profile" :text="props.task.profileName" />
127
112
  <TaskLabel image="timer" :text="props.task.smartTimer ? 'On' : 'Off'" />
@@ -136,40 +121,37 @@
136
121
  v-if="props.task.presaleCode"
137
122
  @click="copy(props.task.presaleCode)"
138
123
  image="pencil"
139
- :text="props.task.presaleCode"
140
- />
124
+ :text="props.task.presaleCode" />
141
125
 
142
126
  <TaskLabel v-if="props.task.eventName" image="stadium_w" :text="props.task.eventName" />
143
127
  <TaskLabel v-if="props.task.eventVenue" image="stadium_w" :text="props.task.eventVenue" />
144
128
  <TaskLabel
145
129
  v-if="props.task.eventLocalDate"
146
130
  image="stadium_w"
147
- :text="formatDate(props.task.eventLocalDate)"
148
- />
131
+ :text="formatDate(props.task.eventLocalDate)" />
149
132
  <TaskLabel image="sandclock" :text="props.task.agedAccount ? 'On' : 'Off'" />
150
133
  </div>
151
134
  </transition>
152
135
 
153
136
  <!-- Context menu -->
154
-
155
- <div class="absolute -bottom-1.5 right-5">
156
- <transition name="fade">
157
- <div
158
- v-if="ui.openContextMenu === task.taskId"
159
- class="bg-dark-500 text-white w-42 grid grid-cols-1 p-1 gap-1 z-50 rounded-lg shadow-xl relative"
160
- >
161
- <button class="btn-primary" @click="openInNewTab(`${ui.currentCountry.url}/event/${task.eventId}`)">
162
- Open Event
163
- </button>
164
- <button v-if="task.openerLink" class="btn-primary" @click="openInBrowser(false)">
165
- Open in browser (proxy)
166
- </button>
167
- <button v-if="task.openerLink" class="btn-primary" @click="openInBrowser(true)">
168
- Open in browser (debug)
169
- </button>
170
- </div>
171
- </transition>
172
- </div>
137
+ <transition name="fade">
138
+ <div
139
+ v-if="ui.openContextMenu === task.taskId"
140
+ ref="contextMenuRef"
141
+ class="fixed bg-dark-500 text-white w-42 grid grid-cols-1 p-1 gap-1 rounded-lg shadow-xl border border-dark-650"
142
+ style="z-index: 9999"
143
+ :style="contextMenuPosition">
144
+ <button class="btn-primary" @click="openInNewTab(`${ui.currentCountry.url}/event/${task.eventId}`)">
145
+ Open Event
146
+ </button>
147
+ <button v-if="task.openerLink" class="btn-primary" @click="openInBrowser(false)">
148
+ Open in browser (proxy)
149
+ </button>
150
+ <button v-if="task.openerLink" class="btn-primary" @click="openInBrowser(true)">
151
+ Open in browser (debug)
152
+ </button>
153
+ </div>
154
+ </transition>
173
155
  </Row>
174
156
  </template>
175
157
  <style lang="scss" scoped>
@@ -183,6 +165,7 @@ h4 {
183
165
  padding: 6px 12px;
184
166
  gap: 6px;
185
167
  transition: all 0.15s ease;
168
+ max-width: 100%;
186
169
 
187
170
  &:hover {
188
171
  @apply bg-dark-550 border-dark-700;
@@ -300,19 +283,23 @@ h4 {
300
283
  gap: 0;
301
284
  border-radius: 7px;
302
285
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.08);
286
+ max-width: calc(100vw - 260px); /* Prevent overflow on small screens */
287
+ overflow: hidden;
303
288
 
304
289
  button {
305
- width: 22px;
306
- height: 22px;
290
+ width: 20px;
291
+ height: 20px;
307
292
  border-radius: 5px;
293
+ min-width: 20px;
294
+ flex-shrink: 0;
308
295
  }
309
296
  svg,
310
297
  img {
311
- width: 11px;
312
- height: 11px;
298
+ width: 10px;
299
+ height: 10px;
313
300
  }
314
301
  span {
315
- font-size: 10px;
302
+ font-size: 9px;
316
303
  }
317
304
  }
318
305
  }
@@ -340,12 +327,13 @@ h4 {
340
327
  .task-buttons {
341
328
  padding: 1px;
342
329
  gap: 0;
343
- border-radius: 6px;
330
+ border-radius: 5px;
344
331
 
345
332
  button {
346
- width: 18px;
347
- height: 18px;
348
- border-radius: 4px;
333
+ width: 15px;
334
+ height: 15px;
335
+ border-radius: 2px;
336
+ min-width: 15px;
349
337
 
350
338
  &:hover {
351
339
  transform: scale(1.1);
@@ -354,11 +342,11 @@ h4 {
354
342
 
355
343
  svg,
356
344
  img {
357
- width: 9px;
358
- height: 9px;
345
+ width: 8px;
346
+ height: 8px;
359
347
  }
360
348
  span {
361
- font-size: 9px;
349
+ font-size: 8px;
362
350
  }
363
351
  }
364
352
  }
@@ -372,7 +360,7 @@ import { PlayIcon, TrashIcon, BagWhiteIcon, PauseIcon, EditIcon } from "@/compon
372
360
  import Checkbox from "@/components/ui/controls/atomic/Checkbox.vue";
373
361
  import { useUIStore } from "@/stores/ui";
374
362
  import TaskLabel from "@/components/Tasks/TaskLabel.vue";
375
- import { ref } from "vue";
363
+ import { computed, ref, onMounted, onUnmounted, nextTick } from "vue";
376
364
 
377
365
  const ui = useUIStore();
378
366
 
@@ -381,6 +369,53 @@ const props = defineProps({
381
369
  task: { type: Object }
382
370
  });
383
371
 
372
+ // Context menu positioning
373
+ const contextMenuPosition = ref({});
374
+ const contextMenuRef = ref(null);
375
+
376
+ // Handle right-click to position context menu
377
+ const handleRightClick = (event) => {
378
+ const menuWidth = 168; // w-42 = 10.5rem = 168px
379
+ const menuHeight = 200; // Approximate height
380
+
381
+ let x = event.clientX;
382
+ let y = event.clientY - 55;
383
+
384
+ // Prevent menu from going off screen
385
+ if (x + menuWidth > window.innerWidth) {
386
+ x = event.clientX - menuWidth; // Show to the left instead
387
+ }
388
+ if (y + menuHeight > window.innerHeight) {
389
+ y = event.clientY - menuHeight; // Show above instead
390
+ }
391
+
392
+ contextMenuPosition.value = {
393
+ left: `${x}px`,
394
+ top: `${y}px`
395
+ };
396
+
397
+ // Open the context menu for this task
398
+ ui.setOpenContextMenu(props.task.taskId);
399
+
400
+ // Add click outside listener after menu opens
401
+ nextTick(() => {
402
+ document.addEventListener("click", handleClickOutside);
403
+ });
404
+ };
405
+
406
+ // Handle clicking outside the context menu
407
+ const handleClickOutside = (event) => {
408
+ if (contextMenuRef.value && !contextMenuRef.value.contains(event.target)) {
409
+ ui.setOpenContextMenu("");
410
+ document.removeEventListener("click", handleClickOutside);
411
+ }
412
+ };
413
+
414
+ // Cleanup on unmount
415
+ onUnmounted(() => {
416
+ document.removeEventListener("click", handleClickOutside);
417
+ });
418
+
384
419
  const copy = (txt) => {
385
420
  if (!txt) return;
386
421
  navigator.clipboard.writeText(txt);
@@ -396,11 +431,6 @@ const colorToClass = (color) => {
396
431
  return colorMapping.get(color) || "bg-white";
397
432
  };
398
433
 
399
- const truncate = (text, after) => {
400
- if (text?.length <= after || after === -1) return text;
401
- return text?.substring(0, after) + "...";
402
- };
403
-
404
434
  const openInBrowser = (debug) => {
405
435
  if (!props.task.openerLink) return;
406
436
  ui.showSuccess(`Opening in browser ${debug ? "(debug)" : ""}`);
@@ -433,15 +463,4 @@ const openInNewTab = (href) => {
433
463
  href: href
434
464
  }).click();
435
465
  };
436
-
437
- const getMaxStatusLength = (width) => {
438
- if (width > 1279) return -1;
439
- if (width > 767) return 25;
440
- if (width > 639) return 30;
441
- if (width > 540) return 18;
442
- return 13;
443
- };
444
-
445
- let statusTruncateLength = ref(getMaxStatusLength(window.innerWidth));
446
- window.addEventListener("resize", () => (statusTruncateLength.value = getMaxStatusLength(window.innerWidth)));
447
466
  </script>
@@ -1,13 +1,12 @@
1
1
  <template>
2
2
  <Table>
3
- <Header class="text-center grid-cols-12 gap-2">
3
+ <Header class="text-center grid-cols-10 gap-2 ipadlg:grid-cols-12">
4
4
  <div class="col-span-1 lg:col-span-2 flex items-center justify-start">
5
5
  <Checkbox
6
6
  class="ml-2 mr-4"
7
7
  :toggled="ui.mainCheckbox.tasks"
8
8
  @valueUpdate="ui.toggleMainCheckbox('tasks')"
9
- :isHeader="true"
10
- />
9
+ :isHeader="true" />
11
10
  <div class="mx-auto hidden lg:flex items-center" @click="ui.toggleSort('eventId')">
12
11
  <EventIcon class="ipadlg:mr-3" />
13
12
  <h4 class="hidden ipadlg:flex">Event</h4>
@@ -15,22 +14,19 @@
15
14
  <UpIcon v-if="ui.sortData.sortBy === 'eventId' && ui.sortData.reversed" class="ml-1" />
16
15
  </div>
17
16
  </div>
18
- <div class="col-span-2 items-center justify-center hidden lg:flex" v-once>
19
- <CartIcon class="mr-0 ipadlg:mr-3" />
20
-
21
- <h4 class="hidden ipadlg:flex">Quantity</h4>
22
- </div>
23
17
  <div class="col-span-2 flex-center" v-once>
24
18
  <TicketIcon class="mr-0 ipadlg:mr-3" />
25
19
  <h4 class="hidden ipadlg:flex">Tickets</h4>
26
20
  </div>
27
- <div class="col-span-6 md:col-span-4 lg:col-span-3 flex-center" @click="ui.toggleSort('status')">
21
+ <div
22
+ class="col-span-5 md:col-span-4 lg:col-span-5 ipadlg:col-span-6 flex-center"
23
+ @click="ui.toggleSort('status')">
28
24
  <StatusIcon class="mr-0 ipadlg:mr-3" />
29
25
  <h4 class="hidden ipadlg:flex">Status</h4>
30
26
  <DownIcon v-if="ui.sortData.sortBy === 'status' && !ui.sortData.reversed" class="ml-1" />
31
27
  <UpIcon v-if="ui.sortData.sortBy === 'status' && ui.sortData.reversed" class="ml-1" />
32
28
  </div>
33
- <div class="col-span-2 flex-center" v-once>
29
+ <div class="col-span-2 ipadlg:col-span-3 flex-center" v-once>
34
30
  <ClickIcon class="mr-0 ipadlg:mr-3" />
35
31
  <h4 class="hidden ipadlg:flex">Actions</h4>
36
32
  </div>
@@ -42,18 +38,25 @@
42
38
  </Header>
43
39
  <div
44
40
  class="flex flex-col divide-y divide-dark-650 overflow-y-auto hidden-scrollbars overflow-x-hidden stop-pan"
45
- :style="{ maxHeight: dynamicTableHeight }"
46
- >
47
- <div
48
- v-for="(task, i) in getTasksInOrder()"
49
- :key="task.taskId"
50
- class="task-row-container"
51
- >
41
+ :style="{ maxHeight: dynamicTableHeight }">
42
+ <div v-for="(task, i) in getTasksInOrder()" :key="task.taskId" class="task-row-container">
52
43
  <Task :task="task" :style="{ backgroundColor: i % 2 == 1 ? '#1a1b1e' : '#242529' }" />
53
44
  </div>
54
- <div v-if="getTasksInOrder().length === 0" class="flex justify-center py-8 empty-state">
55
- <span v-if="ui.queueStats.total === 0"> No tasks yet</span>
56
- <span v-else>{{ ui.queueStats.total }} hidden task{{ ui.queueStats.total === 1 ? "" : "s" }}</span>
45
+ <div
46
+ v-if="getTasksInOrder().length === 0"
47
+ class="flex flex-col items-center justify-center py-8 empty-state text-center">
48
+ <div v-if="ui.queueStats.total === 0">
49
+ <TasksIcon class="w-12 h-12 text-dark-400 mb-3 opacity-50 mx-auto" />
50
+ <p class="text-light-400 text-sm">No tasks yet</p>
51
+ <p class="text-light-500 text-xs mt-1">Create tasks to get started</p>
52
+ </div>
53
+ <div v-else>
54
+ <TasksIcon class="w-12 h-12 text-dark-400 mb-3 opacity-50 mx-auto" />
55
+ <p class="text-light-400 text-sm">
56
+ {{ ui.queueStats.total }} hidden task{{ ui.queueStats.total === 1 ? "" : "s" }}
57
+ </p>
58
+ <p class="text-light-500 text-xs mt-1">Adjust filters to see tasks</p>
59
+ </div>
57
60
  </div>
58
61
  </div>
59
62
  </Table>
@@ -81,7 +84,6 @@ h4 {
81
84
  }
82
85
  }
83
86
 
84
-
85
87
  .empty-state {
86
88
  @apply bg-dark-400;
87
89
  color: #969696;
@@ -92,7 +94,7 @@ h4 {
92
94
  <script setup>
93
95
  import { computed, ref, onMounted, onUnmounted } from "vue";
94
96
  import { Table, Header } from "@/components/Table";
95
- import { EventIcon, CartIcon, TicketIcon, StatusIcon, ClickIcon, DownIcon, UpIcon } from "@/components/icons";
97
+ import { EventIcon, TicketIcon, StatusIcon, ClickIcon, DownIcon, UpIcon, TasksIcon } from "@/components/icons";
96
98
  import Task from "./Task.vue";
97
99
  import Checkbox from "@/components/ui/controls/atomic/Checkbox.vue";
98
100
  import { useUIStore } from "@/stores/ui";
@@ -173,7 +175,7 @@ const dynamicTableHeight = computed(() => {
173
175
  const minHeight = minRowsToShow * rowHeight;
174
176
 
175
177
  // Calculate how many complete rows can fit
176
- const maxCompleteRows = Math.floor(Math.max(availableHeight, minHeight) / rowHeight);
178
+ const maxCompleteRows = Math.floor(Math.max(availableHeight, minHeight) / rowHeight) + 1;
177
179
  const exactHeight = maxCompleteRows * rowHeight;
178
180
 
179
181
  return exactHeight + "px";
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="grid grid-cols-1 gap-3 lg:grid-cols-1" v-once v-if="ui.currentModule == 'TM'">
2
+ <div class="grid grid-cols-1 gap-3 lg:grid-cols-1" v-if="ui.currentModule == 'TM'">
3
3
  <div class="lg:justify-self-end">
4
4
  <h4 class="hidden lg:block text-white opacity-40 uppercase font-medium">Utils</h4>
5
5
  <div class="flex gap-3 justify-between lg:justify-start">
@@ -1,7 +1,7 @@
1
1
  <template>
2
- <svg width="19" height="19" viewBox="0 0 19 11" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
3
3
  <path
4
- d="M1.54165 10.7083C1.19217 10.7083 0.893098 10.584 0.644438 10.3353C0.395355 10.0863 0.270813 9.78698 0.270813 9.4375V1.8125C0.270813 1.46302 0.395355 1.16395 0.644438 0.915289C0.893098 0.666206 1.19217 0.541664 1.54165 0.541664H11.7083C12.0578 0.541664 12.3571 0.666206 12.6062 0.915289C12.8548 1.16395 12.9791 1.46302 12.9791 1.8125V9.4375C12.9791 9.78698 12.8548 10.0863 12.6062 10.3353C12.3571 10.584 12.0578 10.7083 11.7083 10.7083H1.54165ZM6.62498 6.26041L1.54165 3.08333V9.4375H11.7083V3.08333L6.62498 6.26041ZM6.62498 4.98958L11.7083 1.8125H1.54165L6.62498 4.98958ZM1.54165 3.08333V1.8125V9.4375V3.08333Z"
4
+ d="M3.54165 15.2083C3.19217 15.2083 2.893098 15.084 2.644438 14.8353C2.395355 14.5863 2.270813 14.287 2.270813 13.9375V6.3125C2.270813 5.96302 2.395355 5.66395 2.644438 5.41529C2.893098 5.16621 3.19217 5.04166 3.54165 5.04166H16.7083C17.0578 5.04166 17.3571 5.16621 17.6062 5.41529C17.8548 5.66395 17.9791 5.96302 17.9791 6.3125V13.9375C17.9791 14.287 17.8548 14.5863 17.6062 14.8353C17.3571 15.084 17.0578 15.2083 16.7083 15.2083H3.54165ZM10.125 10.7604L3.54165 7.58333V13.9375H16.7083V7.58333L10.125 10.7604ZM10.125 9.48958L16.7083 6.3125H3.54165L10.125 9.48958ZM3.54165 7.58333V6.3125V13.9375V7.58333Z"
5
5
  fill="white"
6
6
  />
7
7
  </svg>
@@ -1,6 +1,6 @@
1
1
  <template>
2
- <div class="modal-mask pt-14 ipadlg:py-1 overflow-y-scroll scrollable">
3
- <div class="component-modal" ref="target">
2
+ <div class="modal-mask pt-14 ipadlg:py-1 overflow-y-scroll scrollable" @touchmove.prevent>
3
+ <div class="component-modal" ref="target" @touchmove.stop>
4
4
  <div class="modal-header">
5
5
  <slot name="header" />
6
6
  <button @click="ui.toggleModal()" class="btn-icon border-none hover:bg-dark-400">
@@ -16,12 +16,40 @@
16
16
  <script setup>
17
17
  import { useUIStore } from "@/stores/ui";
18
18
  import { onClickOutside } from "@vueuse/core";
19
- import { ref } from "vue";
19
+ import { ref, onMounted, onUnmounted } from "vue";
20
20
  import { CloseIcon } from "@/components/icons";
21
21
 
22
22
  const ui = useUIStore();
23
23
  const target = ref(null);
24
24
 
25
+ // Store original body styles
26
+ let originalOverflow = "";
27
+ let originalPosition = "";
28
+ let originalTop = "";
29
+ let scrollY = 0;
30
+
31
+ onMounted(() => {
32
+ // Lock body scroll
33
+ scrollY = window.scrollY;
34
+ originalOverflow = document.body.style.overflow;
35
+ originalPosition = document.body.style.position;
36
+ originalTop = document.body.style.top;
37
+
38
+ document.body.style.overflow = "hidden";
39
+ document.body.style.position = "fixed";
40
+ document.body.style.top = `-${scrollY}px`;
41
+ document.body.style.width = "100%";
42
+ });
43
+
44
+ onUnmounted(() => {
45
+ // Restore body scroll
46
+ document.body.style.overflow = originalOverflow;
47
+ document.body.style.position = originalPosition;
48
+ document.body.style.top = originalTop;
49
+ document.body.style.width = "";
50
+ window.scrollTo(0, scrollY);
51
+ });
52
+
25
53
  onClickOutside(target, (event) => {
26
54
  if (event.target.classList.contains("modal-mask")) ui.toggleModal();
27
55
  });
@@ -32,21 +60,24 @@ onClickOutside(target, (event) => {
32
60
  z-index: 9998;
33
61
  background-color: rgba(17, 17, 17, 0.85);
34
62
  backdrop-filter: blur(4px);
63
+ align-items: center; // Keep centered on desktop
64
+ justify-content: center;
65
+ padding: 2rem;
35
66
  }
36
67
 
37
68
  .component-modal {
38
- margin: auto;
39
69
  width: 640px;
40
- @apply bg-dark-300 px-5 py-5 rounded-lg m-auto flex flex-col;
41
-
70
+ // Remove max-height on desktop to show full modal
71
+ @apply bg-dark-300 px-5 py-5 rounded-lg flex flex-col;
72
+
42
73
  .modal-header {
43
74
  @apply flex text-white font-bold;
44
-
75
+
45
76
  button {
46
77
  @apply ml-auto;
47
78
  }
48
79
  }
49
-
80
+
50
81
  .modal-body {
51
82
  @apply flex flex-col;
52
83
  }
@@ -54,19 +85,57 @@ onClickOutside(target, (event) => {
54
85
 
55
86
  @media (max-width: 810px) {
56
87
  .modal-mask {
57
- @apply items-start pt-4;
58
- padding-bottom: 2rem;
88
+ align-items: flex-start; // Start from top on mobile
89
+ justify-content: center;
90
+ padding: 1rem;
91
+ padding-top: 2rem;
59
92
  }
60
-
93
+
61
94
  .component-modal {
62
95
  width: calc(100vw - 2rem);
63
- max-height: calc(100vh - 6rem);
64
- overflow-y: auto;
65
- margin: 1rem;
96
+ max-height: calc(100vh - 8rem); // Only limit height on mobile
97
+ overflow-y: auto; // Only add scrolling on mobile
98
+ }
99
+
100
+ .modal-body {
101
+ padding-bottom: 3rem; // Increased padding for create button accessibility
102
+ overflow-y: auto; // Allow scrolling on mobile
103
+ flex: 1;
104
+ min-height: 0;
105
+ }
106
+ }
107
+
108
+ // iPhone landscape mode specific adjustments
109
+ @media only screen and (max-device-width: 430px) and (orientation: landscape) {
110
+ .modal-mask {
111
+ top: 0;
112
+ left: 0;
113
+ right: 0;
114
+ bottom: 0;
115
+ width: 100vw;
116
+ height: 100vh;
117
+ padding: 0;
118
+ padding-top: calc(env(safe-area-inset-top, 0px) + 4rem); // Navbar + safe area
119
+ padding-bottom: env(safe-area-inset-bottom, 0.5rem);
120
+ padding-left: env(safe-area-inset-left, 0.5rem);
121
+ padding-right: env(safe-area-inset-right, 0.5rem);
122
+ align-items: flex-start;
123
+ justify-content: center;
124
+ }
125
+
126
+ .component-modal {
127
+ width: 100%;
128
+ max-width: none;
129
+ max-height: calc(100vh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 5rem);
130
+ min-height: auto;
131
+ margin: 0;
66
132
  }
67
133
 
68
134
  .modal-body {
69
- padding-bottom: 1rem;
135
+ padding-bottom: 4rem; // Ensure create button is accessible
136
+ max-height: calc(100vh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 8rem);
137
+ overflow-y: auto;
138
+ -webkit-overflow-scrolling: touch;
70
139
  }
71
140
  }
72
141
  </style>