@necrolab/dashboard 0.5.15 → 0.5.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/backend/api.js +2 -3
  2. package/eslint.config.js +46 -0
  3. package/index.html +2 -1
  4. package/package.json +5 -2
  5. package/src/App.vue +70 -566
  6. package/src/assets/css/base/mixins.scss +72 -0
  7. package/src/assets/css/base/reset.scss +0 -2
  8. package/src/assets/css/base/scroll.scss +43 -36
  9. package/src/assets/css/base/typography.scss +9 -10
  10. package/src/assets/css/base/variables.scss +43 -0
  11. package/src/assets/css/components/accessibility.scss +37 -0
  12. package/src/assets/css/components/buttons.scss +61 -74
  13. package/src/assets/css/components/forms.scss +31 -32
  14. package/src/assets/css/components/headers.scss +13 -21
  15. package/src/assets/css/components/modals.scss +2 -2
  16. package/src/assets/css/components/search-groups.scss +28 -22
  17. package/src/assets/css/components/tables.scss +5 -7
  18. package/src/assets/css/components/toasts.scss +7 -7
  19. package/src/assets/css/components/utilities.scss +295 -0
  20. package/src/assets/css/main.scss +55 -139
  21. package/src/components/Auth/LoginForm.vue +7 -86
  22. package/src/components/Console/ConsoleToolbar.vue +123 -0
  23. package/src/components/Editors/Account/Account.vue +12 -12
  24. package/src/components/Editors/Account/AccountView.vue +38 -111
  25. package/src/components/Editors/Account/CreateAccount.vue +11 -61
  26. package/src/components/Editors/Account/{AccountCreator.vue → CreateAccountBatch.vue} +28 -59
  27. package/src/components/Editors/AdminFileEditor.vue +179 -0
  28. package/src/components/Editors/Profile/CreateProfile.vue +77 -150
  29. package/src/components/Editors/Profile/Profile.vue +20 -21
  30. package/src/components/Editors/Profile/ProfileCountryChooser.vue +16 -60
  31. package/src/components/Editors/Profile/ProfileView.vue +41 -116
  32. package/src/components/Editors/ProxyFileEditor.vue +86 -0
  33. package/src/components/Editors/TagLabel.vue +16 -55
  34. package/src/components/Editors/TagToggle.vue +20 -8
  35. package/src/components/Filter/Filter.vue +66 -79
  36. package/src/components/Filter/FilterPreview.vue +153 -135
  37. package/src/components/Filter/PriceSortToggle.vue +36 -43
  38. package/src/components/Table/Header.vue +1 -1
  39. package/src/components/Table/Table.vue +45 -51
  40. package/src/components/Tasks/CheckStock.vue +7 -16
  41. package/src/components/Tasks/Controls/DesktopControls.vue +15 -60
  42. package/src/components/Tasks/Controls/MobileControls.vue +5 -20
  43. package/src/components/Tasks/CreateTaskAXS.vue +20 -118
  44. package/src/components/Tasks/CreateTaskTM.vue +33 -189
  45. package/src/components/Tasks/EventDetailRow.vue +21 -0
  46. package/src/components/Tasks/MassEdit.vue +6 -16
  47. package/src/components/Tasks/QuickSettings.vue +140 -216
  48. package/src/components/Tasks/ScrapeVenue.vue +4 -13
  49. package/src/components/Tasks/Stats.vue +20 -39
  50. package/src/components/Tasks/Task.vue +64 -270
  51. package/src/components/Tasks/TaskLabel.vue +9 -3
  52. package/src/components/Tasks/TaskView.vue +45 -64
  53. package/src/components/Tasks/Utilities.vue +10 -44
  54. package/src/components/Tasks/ViewTask.vue +23 -107
  55. package/src/components/icons/Close.vue +2 -8
  56. package/src/components/icons/Gear.vue +8 -8
  57. package/src/components/icons/Hash.vue +5 -0
  58. package/src/components/icons/Key.vue +2 -8
  59. package/src/components/icons/Pencil.vue +2 -8
  60. package/src/components/icons/Profile.vue +2 -8
  61. package/src/components/icons/Sell.vue +2 -8
  62. package/src/components/icons/Spinner.vue +4 -7
  63. package/src/components/icons/Wildcard.vue +2 -8
  64. package/src/components/icons/index.js +3 -5
  65. package/src/components/ui/ActionButtonGroup.vue +113 -52
  66. package/src/components/ui/BalanceIndicator.vue +60 -0
  67. package/src/components/ui/EmptyState.vue +24 -0
  68. package/src/components/ui/EnableDisableToggle.vue +23 -0
  69. package/src/components/ui/FormField.vue +49 -49
  70. package/src/components/ui/IconLabel.vue +23 -0
  71. package/src/components/ui/InfoRow.vue +21 -54
  72. package/src/components/ui/Modal.vue +161 -54
  73. package/src/components/ui/Navbar.vue +63 -44
  74. package/src/components/ui/ReadonlyFieldsSection.vue +31 -0
  75. package/src/components/ui/ReconnectIndicator.vue +111 -124
  76. package/src/components/ui/SectionCard.vue +6 -14
  77. package/src/components/ui/Splash.vue +2 -10
  78. package/src/components/ui/StatusBadge.vue +26 -28
  79. package/src/components/ui/TaskToggle.vue +54 -0
  80. package/src/components/ui/controls/CountryChooser.vue +29 -66
  81. package/src/components/ui/controls/EyeToggle.vue +1 -1
  82. package/src/components/ui/controls/atomic/Checkbox.vue +40 -121
  83. package/src/components/ui/controls/atomic/Dropdown.vue +103 -139
  84. package/src/components/ui/controls/atomic/MultiDropdown.vue +72 -120
  85. package/src/components/ui/controls/atomic/Switch.vue +21 -84
  86. package/src/composables/useCodeEditor.js +117 -0
  87. package/src/composables/useColorMapping.js +15 -0
  88. package/src/composables/useCopyToClipboard.js +1 -1
  89. package/src/composables/useDateFormatting.js +21 -0
  90. package/src/composables/useDeviceDetection.js +14 -0
  91. package/src/composables/useDropdownPosition.js +1 -4
  92. package/src/composables/useDynamicTableHeight.js +31 -0
  93. package/src/composables/useEnableDisable.js +6 -0
  94. package/src/composables/useFilterCSS.js +71 -0
  95. package/src/composables/useFormValidation.js +92 -0
  96. package/src/composables/useGetAllTags.js +9 -0
  97. package/src/composables/useIOSViewportHandling.js +76 -0
  98. package/src/composables/useNotchHandling.js +306 -0
  99. package/src/composables/useRowSelection.js +0 -3
  100. package/src/composables/useTableRender.js +23 -0
  101. package/src/composables/useTicketPricing.js +16 -0
  102. package/src/composables/useWindowDimensions.js +21 -0
  103. package/src/composables/useZoomPrevention.js +96 -0
  104. package/src/constants/tableLayout.js +14 -0
  105. package/src/libs/Filter.js +14 -20
  106. package/src/libs/panzoom.js +1 -5
  107. package/src/libs/utils/array.js +58 -0
  108. package/src/{stores/utils.js → libs/utils/dataGeneration.js} +2 -250
  109. package/src/libs/utils/eventUrl.js +40 -0
  110. package/src/libs/utils/string.js +3 -0
  111. package/src/libs/utils/time.js +20 -0
  112. package/src/libs/utils/validation.js +64 -0
  113. package/src/main.js +0 -2
  114. package/src/stores/connection.js +1 -29
  115. package/src/stores/logger.js +6 -12
  116. package/src/stores/sampleData.js +1 -2
  117. package/src/stores/ui.js +80 -71
  118. package/src/utils/tableHelpers.js +1 -0
  119. package/src/views/Accounts.vue +19 -38
  120. package/src/views/Console.vue +74 -253
  121. package/src/views/Editor.vue +47 -1114
  122. package/src/views/FilterBuilder.vue +190 -461
  123. package/src/views/Login.vue +3 -28
  124. package/src/views/Profiles.vue +17 -32
  125. package/src/views/Tasks.vue +51 -38
  126. package/tailwind.config.js +82 -71
  127. package/workbox-config.cjs +47 -5
  128. package/docs/plans/2026-02-08-tailwind-consolidation.md +0 -2438
  129. package/exit +0 -209
  130. package/run +0 -177
  131. package/src/assets/css/base/color-fallbacks.scss +0 -10
  132. package/src/assets/img/background.svg.backup +0 -11
  133. package/src/components/icons/SquareCheck.vue +0 -18
  134. package/src/components/icons/SquareUncheck.vue +0 -18
  135. package/src/components/ui/controls/atomic/LoadingButton.vue +0 -45
  136. package/switch-branch.sh +0 -41
  137. /package/public/{reconnect-logo.png → img/reconnect-logo.png} +0 -0
@@ -1,96 +1,33 @@
1
1
  <template>
2
- <label class="switch" :class="{ 'disabled': disabled }">
3
- <input type="checkbox" v-model="value" :disabled="disabled" />
4
- <span class="slider round"></span>
2
+ <label
3
+ class="relative inline-block w-[51px] h-[31px] max-modal:w-11 max-modal:min-w-11 max-modal:h-6.5"
4
+ :class="disabled ? 'pointer-events-none' : ''"
5
+ >
6
+ <input
7
+ type="checkbox"
8
+ v-model="value"
9
+ :disabled="disabled"
10
+ class="opacity-0 w-0 h-0 peer"
11
+ role="switch"
12
+ :aria-checked="value"
13
+ />
14
+ <span
15
+ class="absolute inset-0 cursor-pointer border-2 rounded-[31px] transition-all duration-300 ease-out
16
+ before:absolute before:content-[''] before:size-[23px] before:left-0.5 before:bottom-0.5 before:rounded-full before:shadow-switch before:transition-all before:duration-300 before:ease-out
17
+ bg-dark-550 border-dark-550 before:bg-dark-200
18
+ peer-checked:bg-accent-green peer-checked:border-accent-green peer-checked:before:bg-white peer-checked:before:translate-x-5
19
+ peer-disabled:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:before:opacity-70
20
+ max-modal:before:size-[22px] max-modal:peer-checked:before:translate-x-[18px]"
21
+ ></span>
5
22
  </label>
6
23
  </template>
7
24
 
8
25
  <script setup>
9
26
  const value = defineModel();
10
- const props = defineProps({
27
+ defineProps({
11
28
  disabled: {
12
29
  type: Boolean,
13
30
  default: false
14
31
  }
15
32
  });
16
33
  </script>
17
-
18
- <style lang="scss" scoped>
19
- /* iOS-style switch */
20
- .switch {
21
- @apply relative inline-block;
22
- width: 51px;
23
- height: 31px;
24
- }
25
-
26
- /* Hide default HTML checkbox */
27
- .switch input {
28
- @apply opacity-0 w-0 h-0;
29
- }
30
-
31
- /* The slider */
32
- .slider {
33
- @apply absolute cursor-pointer inset-0;
34
- @apply border-2;
35
- background-color: oklch(0.26 0 0);
36
- border-color: oklch(0.35 0 0);
37
- transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
38
- }
39
-
40
- .slider:before {
41
- @apply absolute;
42
- @apply w-[23px] h-[23px] left-0.5 bottom-0.5;
43
- content: "";
44
- background-color: oklch(0.50 0 0);
45
- transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
46
- box-shadow: 0 1px 3px oklch(0 0 0 / 0.3);
47
- }
48
-
49
- input:checked + .slider {
50
- background-color: oklch(0.72 0.15 145);
51
- border-color: oklch(0.72 0.15 145);
52
- }
53
-
54
- input:checked + .slider:before {
55
- background-color: oklch(1 0 0);
56
- }
57
-
58
- input:checked + .slider:before {
59
- transform: translateX(20px);
60
- }
61
-
62
- .switch.disabled {
63
- @apply pointer-events-none;
64
- }
65
-
66
- input:disabled + .slider {
67
- @apply opacity-50 cursor-not-allowed;
68
- }
69
-
70
- input:disabled + .slider:before {
71
- @apply opacity-70;
72
- }
73
-
74
- /* Rounded sliders */
75
- .slider.round {
76
- border-radius: 31px;
77
- }
78
-
79
- .slider.round:before {
80
- border-radius: 50%;
81
- }
82
-
83
- @media (max-width: 810px) {
84
- .switch {
85
- @apply w-11 min-w-11 h-[26px];
86
-
87
- .slider:before {
88
- @apply w-[22px] h-[22px] left-0.5 bottom-0.5;
89
- }
90
- }
91
-
92
- input:checked + .slider:before {
93
- @apply translate-x-[18px];
94
- }
95
- }
96
- </style>
@@ -0,0 +1,117 @@
1
+ import { nextTick } from 'vue';
2
+
3
+ export function useCodeEditor(logger) {
4
+ let highlightTimer = null;
5
+
6
+ const highlightCode = (codeDisplay, codeEditor, currentContent, currentFile) => {
7
+ if (!codeDisplay || !codeEditor) return;
8
+
9
+ let language = "javascript";
10
+
11
+ if (currentFile) {
12
+ if (currentFile.endsWith(".json")) {
13
+ language = "json";
14
+ } else if (currentFile.endsWith(".js")) {
15
+ language = "javascript";
16
+ } else if (currentFile.endsWith(".txt") || currentFile.endsWith(".csv")) {
17
+ language = "text";
18
+ if (codeDisplay) {
19
+ codeDisplay.textContent = currentContent || "";
20
+ codeDisplay.className = "language-text code-highlight";
21
+ }
22
+ syncScroll(codeDisplay, codeEditor);
23
+ return;
24
+ }
25
+ }
26
+
27
+ if (highlightTimer) clearTimeout(highlightTimer);
28
+
29
+ highlightTimer = setTimeout(() => {
30
+ requestAnimationFrame(() => {
31
+ try {
32
+ if (typeof window.Prism === "undefined" || !window.Prism.languages?.[language]) {
33
+ if (codeDisplay) {
34
+ codeDisplay.textContent = currentContent || "";
35
+ codeDisplay.className = `language-${language} code-highlight`;
36
+ }
37
+ syncScroll(codeDisplay, codeEditor);
38
+ return;
39
+ }
40
+
41
+ const highlighted = window.Prism.highlight(
42
+ currentContent || "",
43
+ window.Prism.languages[language],
44
+ language
45
+ );
46
+
47
+ if (codeDisplay) {
48
+ codeDisplay.innerHTML = highlighted;
49
+ codeDisplay.className = `language-${language} code-highlight`;
50
+ }
51
+ } catch (e) {
52
+ if (logger) logger.Error("Highlight error:", e);
53
+ if (codeDisplay) {
54
+ codeDisplay.textContent = currentContent || "";
55
+ }
56
+ } finally {
57
+ syncScroll(codeDisplay, codeEditor);
58
+ }
59
+ });
60
+ }, 50);
61
+ };
62
+
63
+ const syncScroll = (codeDisplay, codeEditor) => {
64
+ if (!codeDisplay || !codeEditor) return;
65
+
66
+ codeDisplay.scrollTop = codeEditor.scrollTop;
67
+ codeDisplay.scrollLeft = codeEditor.scrollLeft;
68
+ };
69
+
70
+ const handleTab = (textarea, contentRef, onContentUpdate) => {
71
+ const start = textarea.selectionStart;
72
+ const end = textarea.selectionEnd;
73
+
74
+ const spaces = " ";
75
+ contentRef.value = contentRef.value.substring(0, start) + spaces + contentRef.value.substring(end);
76
+
77
+ nextTick(() => {
78
+ textarea.selectionStart = textarea.selectionEnd = start + spaces.length;
79
+ if (onContentUpdate) onContentUpdate();
80
+ });
81
+ };
82
+
83
+ const formatJSON = (contentRef, currentFile, onContentUpdate) => {
84
+ try {
85
+ if (!currentFile.endsWith(".json")) return;
86
+
87
+ const formatted = JSON.stringify(JSON.parse(contentRef.value), null, 4);
88
+ contentRef.value = formatted;
89
+
90
+ nextTick(() => {
91
+ if (onContentUpdate) onContentUpdate();
92
+ });
93
+ } catch (e) {
94
+ if (logger) logger.Error("Could not format JSON", e);
95
+ }
96
+ };
97
+
98
+ const setLanguageClass = (codeDisplay, fileName) => {
99
+ if (!codeDisplay) return;
100
+
101
+ if (fileName.endsWith(".json")) {
102
+ codeDisplay.className = "language-json";
103
+ } else if (fileName.endsWith(".js")) {
104
+ codeDisplay.className = "language-javascript";
105
+ } else {
106
+ codeDisplay.className = "language-text";
107
+ }
108
+ };
109
+
110
+ return {
111
+ highlightCode,
112
+ syncScroll,
113
+ handleTab,
114
+ formatJSON,
115
+ setLanguageClass
116
+ };
117
+ }
@@ -0,0 +1,15 @@
1
+ export function useColorMapping() {
2
+ const colorToClass = (color) => {
3
+ const colorMap = {
4
+ green: "bg-green-400",
5
+ red: "bg-red-400",
6
+ yellow: "bg-yellow-400",
7
+ blue: "bg-blue-400",
8
+ error: "bg-red-400",
9
+ success: "bg-green-400"
10
+ };
11
+ return colorMap[color?.toLowerCase()] || "bg-white";
12
+ };
13
+
14
+ return { colorToClass };
15
+ }
@@ -17,7 +17,7 @@ export function useCopyToClipboard() {
17
17
  })
18
18
  .catch((err) => {
19
19
  ui.showError('Failed to copy')
20
- console.error('Copy failed:', err)
20
+ ui.logger.Error('Copy failed:', err)
21
21
  })
22
22
  }
23
23
 
@@ -0,0 +1,21 @@
1
+ export function useDateFormatting() {
2
+ const formatEventDate = (dateString) => {
3
+ if (!dateString) return '';
4
+ try {
5
+ const date = new Date(dateString);
6
+ const options = {
7
+ month: 'short',
8
+ day: 'numeric',
9
+ year: 'numeric',
10
+ hour: 'numeric',
11
+ minute: '2-digit',
12
+ hour12: true
13
+ };
14
+ return date.toLocaleString('en-US', options).replace(',', '');
15
+ } catch {
16
+ return dateString;
17
+ }
18
+ };
19
+
20
+ return { formatEventDate };
21
+ }
@@ -0,0 +1,14 @@
1
+ export function useDeviceDetection() {
2
+ const isIOS = () => {
3
+ if (/iPad|iPhone|iPod/.test(navigator.platform)) {
4
+ return true;
5
+ }
6
+ return navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform);
7
+ };
8
+
9
+ const isIpadOS = () => {
10
+ return navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform);
11
+ };
12
+
13
+ return { isIOS, isIpadOS };
14
+ }
@@ -8,8 +8,7 @@ export function useDropdownPosition(dropdownRef, options = {}) {
8
8
  minWidth = null,
9
9
  maxHeight = window.innerHeight * 0.8, // Use 80% of viewport height
10
10
  estimateHeight = null,
11
- includeAdjacentButtons = false,
12
- containerSelector = null
11
+ includeAdjacentButtons = false
13
12
  } = options;
14
13
 
15
14
  const calculateMenuPosition = () => {
@@ -109,8 +108,6 @@ export function useDropdownPosition(dropdownRef, options = {}) {
109
108
  maxHeight: `${maxHeight}px`
110
109
  };
111
110
  } catch (error) {
112
- console.warn("Error calculating dropdown position:", error);
113
- // Fallback to basic positioning
114
111
  menuStyle.value = {
115
112
  position: "fixed",
116
113
  top: "0px",
@@ -0,0 +1,31 @@
1
+ import { computed } from "vue";
2
+ import { useWindowDimensions } from "./useWindowDimensions";
3
+
4
+ export function useDynamicTableHeight(options = {}) {
5
+ const { windowHeight, windowWidth } = useWindowDimensions();
6
+
7
+ const {
8
+ topReservedSpace = 243,
9
+ bottomBuffer = 16,
10
+ rowHeight = 64,
11
+ minRowsToShow = 2
12
+ } = options;
13
+
14
+ const dynamicTableHeight = computed(() => {
15
+ // Detect PWA mode and small screens
16
+ const isPWA = window.matchMedia('(display-mode: standalone)').matches;
17
+ const isMobile = windowWidth.value <= 768;
18
+
19
+ // Extra buffer for iPhone PWA to prevent overflow
20
+ const extraBuffer = isPWA && isMobile ? 60 : 0;
21
+
22
+ const availableHeight = windowHeight.value - topReservedSpace - bottomBuffer - extraBuffer;
23
+ const minHeight = minRowsToShow * rowHeight;
24
+ const maxCompleteRows = Math.floor(Math.max(availableHeight, minHeight) / rowHeight);
25
+ const exactHeight = maxCompleteRows * rowHeight;
26
+
27
+ return exactHeight + "px";
28
+ });
29
+
30
+ return { dynamicTableHeight };
31
+ }
@@ -0,0 +1,6 @@
1
+ export function useEnableDisable(item, updateFn) {
2
+ const enable = async () => await updateFn({ ...item.value, enabled: true });
3
+ const disable = async () => await updateFn({ ...item.value, enabled: false });
4
+
5
+ return { enable, disable };
6
+ }
@@ -0,0 +1,71 @@
1
+ import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue';
2
+ import { onBeforeRouteLeave } from 'vue-router';
3
+
4
+ export function useFilterCSS(filterBuilder, svg) {
5
+ const STYLE_ELEMENT_ID = "filter-builder-styles";
6
+ let styleElement = null;
7
+ const cssUpdateTrigger = ref(0);
8
+
9
+ const injectStyles = () => {
10
+ const svgWrapper = document.getElementById("svg-wrapper");
11
+ if (!svgWrapper || !svg.value) return;
12
+
13
+ if (!styleElement) {
14
+ const existingStyle = document.getElementById(STYLE_ELEMENT_ID);
15
+ if (existingStyle) existingStyle.remove();
16
+
17
+ styleElement = document.createElement("style");
18
+ styleElement.id = STYLE_ELEMENT_ID;
19
+ document.head.appendChild(styleElement);
20
+ }
21
+
22
+ const combinedCSS = filterBuilder.value.cssClasses + filterBuilder.value.temporaryCSS;
23
+ if (styleElement.textContent !== combinedCSS) {
24
+ styleElement.textContent = combinedCSS;
25
+ }
26
+ };
27
+
28
+ const setupCSSUpdates = () => {
29
+ const originalUpdateCss = filterBuilder.value.updateCss.bind(filterBuilder.value);
30
+ filterBuilder.value.updateCss = () => {
31
+ originalUpdateCss();
32
+ nextTick(() => {
33
+ injectStyles();
34
+ cssUpdateTrigger.value++;
35
+ });
36
+ };
37
+
38
+ const originalHighlight = filterBuilder.value.highlight.bind(filterBuilder.value);
39
+ filterBuilder.value.highlight = (...args) => {
40
+ originalHighlight(...args);
41
+ nextTick(() => injectStyles());
42
+ };
43
+
44
+ const originalClearHighlight = filterBuilder.value.clearHighlight.bind(filterBuilder.value);
45
+ filterBuilder.value.clearHighlight = () => {
46
+ originalClearHighlight();
47
+ nextTick(() => injectStyles());
48
+ };
49
+ };
50
+
51
+ const cleanupStyles = () => {
52
+ const existingStyle = document.getElementById(STYLE_ELEMENT_ID);
53
+ if (existingStyle) existingStyle.remove();
54
+
55
+ if (styleElement) {
56
+ styleElement.remove();
57
+ styleElement = null;
58
+ }
59
+ };
60
+
61
+ onMounted(() => {
62
+ cleanupStyles();
63
+ setupCSSUpdates();
64
+ watch(cssUpdateTrigger, () => injectStyles(), { immediate: true });
65
+ });
66
+
67
+ onUnmounted(() => cleanupStyles());
68
+ onBeforeRouteLeave(() => cleanupStyles());
69
+
70
+ return { injectStyles, cssUpdateTrigger };
71
+ }
@@ -0,0 +1,92 @@
1
+ import { ref } from 'vue';
2
+
3
+ export function useFormValidation() {
4
+ const errors = ref([]);
5
+
6
+ const clearErrors = () => {
7
+ errors.value = [];
8
+ };
9
+
10
+ const addError = (fieldName) => {
11
+ if (!errors.value.includes(fieldName)) {
12
+ errors.value.push(fieldName);
13
+ }
14
+ };
15
+
16
+ const hasError = (fieldName) => {
17
+ return errors.value.includes(fieldName);
18
+ };
19
+
20
+ const isValidEmail = (email) => {
21
+ return email && email.includes("@");
22
+ };
23
+
24
+ const isValidPassword = (password, minLength = 5) => {
25
+ return password && password.length >= minLength;
26
+ };
27
+
28
+ const isRequired = (value) => {
29
+ if (typeof value === 'string') return value.trim().length > 0;
30
+ if (typeof value === 'number') return true;
31
+ return value != null && value !== undefined;
32
+ };
33
+
34
+ const isValidCVV = (cvv) => {
35
+ return /^\d{3,4}$/.test(String(cvv));
36
+ };
37
+
38
+ const isValidCardNumber = (cardNumber) => {
39
+ const clean = String(cardNumber).replace(/\s+/g, "");
40
+ return (
41
+ clean.match(/^4\d{15}$/) || // Visa (16 digits)
42
+ clean.match(/^5[1-5]\d{14}$/) || // Mastercard (16 digits)
43
+ clean.match(/^3[47]\d{13}$/) // AMEX (15 digits)
44
+ );
45
+ };
46
+
47
+ const isValidState = (state, country) => {
48
+ // State required only for US
49
+ if (country === "US") {
50
+ return state && state.length === 2;
51
+ }
52
+ return true; // Not required for other countries
53
+ };
54
+
55
+ const isValidZipCode = (zipCode) => {
56
+ return zipCode && zipCode.trim().length > 0;
57
+ };
58
+
59
+ const validateAccount = (account) => {
60
+ clearErrors();
61
+
62
+ if (!isValidEmail(account.email)) addError("email");
63
+ if (!isValidPassword(account.password)) addError("password");
64
+
65
+ return errors.value.length === 0;
66
+ };
67
+
68
+ const validateProfile = (profile) => {
69
+ clearErrors();
70
+
71
+ // Address validation
72
+ if (!isRequired(profile.zipCode)) addError("zipCode");
73
+ if (!isRequired(profile.address)) addError("address");
74
+ if (!isValidState(profile.state, profile.country)) addError("state");
75
+ if (!isRequired(profile.city)) addError("city");
76
+ if (!isRequired(profile.country)) addError("country");
77
+
78
+ // Card validation
79
+ if (!isValidCVV(profile.cvv)) addError("cvv");
80
+ if (!isValidCardNumber(profile.cardNumber)) addError("cardNumber");
81
+ if (!isRequired(profile.expYear)) addError("expYear");
82
+ if (!isRequired(profile.expMonth)) addError("expMonth");
83
+
84
+ return errors.value.length === 0;
85
+ };
86
+
87
+ return {
88
+ errors,
89
+ validateAccount,
90
+ validateProfile
91
+ };
92
+ }
@@ -0,0 +1,9 @@
1
+ export function useGetAllTags(results) {
2
+ let tags = ["Any"];
3
+ results.forEach((p) =>
4
+ p.tags.forEach((tag) => {
5
+ if (!tags.includes(tag)) tags.push(tag);
6
+ })
7
+ );
8
+ return tags;
9
+ }
@@ -0,0 +1,76 @@
1
+ import { useDeviceDetection } from './useDeviceDetection';
2
+
3
+ export function useIOSViewportHandling() {
4
+ const { isIpadOS } = useDeviceDetection();
5
+
6
+ const isIOSDevice =
7
+ /iPad|iPhone|iPod/.test(navigator.platform) ||
8
+ (navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform));
9
+
10
+ if (!isIOSDevice) {
11
+ return { isIOSDevice: false };
12
+ }
13
+
14
+ const handleViewportResize = () => {
15
+ if (isIpadOS()) {
16
+ // For iPad, allow natural viewport behavior
17
+ return;
18
+ }
19
+
20
+ // For iPhone, maintain viewport stability
21
+ const vh = window.innerHeight * 0.01;
22
+ document.documentElement.style.setProperty("--vh", `${vh}px`);
23
+ };
24
+
25
+ // Initial setup
26
+ handleViewportResize();
27
+
28
+ // Listen for viewport changes
29
+ window.addEventListener("resize", handleViewportResize);
30
+ if (window.visualViewport) {
31
+ window.visualViewport.addEventListener("resize", handleViewportResize);
32
+ }
33
+
34
+ // Precise scroll control - only allow scrolling within specific scrollable elements
35
+ window.addEventListener(
36
+ "touchmove",
37
+ function (event) {
38
+ const isScrollableTextarea =
39
+ event.target.tagName === "TEXTAREA" &&
40
+ (event.target.classList.contains("code-editor") || event.target.classList.contains("proxy-editor"));
41
+
42
+ const isInScrollableContainer =
43
+ event.target.closest(".stop-pan") ||
44
+ event.target.closest(".overflow-y-auto") ||
45
+ event.target.closest(".vue-recycle-scroller") ||
46
+ event.target.closest(".scroller") ||
47
+ event.target.closest(".scrollable");
48
+
49
+ const isInTable = event.target.closest(".table-component");
50
+ const isScrollableTableContent =
51
+ isInTable &&
52
+ (event.target.closest(".grid") ||
53
+ event.target.closest(".table-row") ||
54
+ isInScrollableContainer ||
55
+ (event.target.closest(".table-component") && !event.target.closest(".table-header")));
56
+
57
+ const isInNavbar = event.target.closest(".navbar") || event.target.closest(".mobile-menu");
58
+ const isInModal = event.target.closest('[role="dialog"]');
59
+
60
+ if (isScrollableTextarea || isScrollableTableContent || isInScrollableContainer || isInNavbar || isInModal) {
61
+ if (isScrollableTableContent || isScrollableTextarea || isInScrollableContainer) {
62
+ event.stopPropagation();
63
+ }
64
+ return;
65
+ }
66
+
67
+ event.preventDefault();
68
+ },
69
+ { passive: false }
70
+ );
71
+
72
+ return {
73
+ isIOSDevice: true,
74
+ handleViewportResize
75
+ };
76
+ }