@eeplatform/nuxt-layer-common 1.0.0

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 (108) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.editorconfig +12 -0
  4. package/.github/workflows/main.yml +17 -0
  5. package/.github/workflows/publish.yml +39 -0
  6. package/.nuxtrc +1 -0
  7. package/.playground/app.vue +37 -0
  8. package/.playground/nuxt.config.ts +20 -0
  9. package/CHANGELOG.md +7 -0
  10. package/README.md +73 -0
  11. package/app.vue +3 -0
  12. package/components/AddPaymentMethod.vue +585 -0
  13. package/components/BtnUploadFile.vue +139 -0
  14. package/components/ConfirmDialog.vue +66 -0
  15. package/components/Container/Standard.vue +33 -0
  16. package/components/Input/Date.vue +177 -0
  17. package/components/Input/ListGroupSelection.vue +93 -0
  18. package/components/Input/NewDate.vue +123 -0
  19. package/components/Input/Number.vue +124 -0
  20. package/components/Input/Password.vue +35 -0
  21. package/components/InputLabel.vue +18 -0
  22. package/components/InvitationMain.vue +195 -0
  23. package/components/Layout/Header.vue +285 -0
  24. package/components/Layout/NavigationDrawer.vue +52 -0
  25. package/components/LinkHome.vue +9 -0
  26. package/components/ListItem.vue +35 -0
  27. package/components/LocalPagination.vue +41 -0
  28. package/components/MemberMain.vue +452 -0
  29. package/components/NavigationItem.vue +73 -0
  30. package/components/PlaceholderComponent.vue +34 -0
  31. package/components/RolePermissionFormCreate.vue +179 -0
  32. package/components/RolePermissionFormPreviewUpdate.vue +184 -0
  33. package/components/RolePermissionMain.vue +376 -0
  34. package/components/Snackbar.vue +23 -0
  35. package/components/SpecificAttr.vue +57 -0
  36. package/components/Std/Pagination.vue +52 -0
  37. package/components/SwitchContext.vue +109 -0
  38. package/components/SwitchOrg.vue +159 -0
  39. package/components/TableList.vue +130 -0
  40. package/composables/useAddress.ts +144 -0
  41. package/composables/useChartOfAccount.ts +62 -0
  42. package/composables/useCommonPermission.ts +130 -0
  43. package/composables/useFile.ts +29 -0
  44. package/composables/useInvoice.ts +42 -0
  45. package/composables/useLocal.ts +63 -0
  46. package/composables/useLocalAuth.ts +157 -0
  47. package/composables/useLocalSetup.ts +46 -0
  48. package/composables/useMember.ts +107 -0
  49. package/composables/useOrder.ts +22 -0
  50. package/composables/useOrg.ts +106 -0
  51. package/composables/useOrgPermission.ts +27 -0
  52. package/composables/usePayment.ts +22 -0
  53. package/composables/usePaymentMethod.ts +347 -0
  54. package/composables/usePermission.ts +54 -0
  55. package/composables/usePrice.ts +15 -0
  56. package/composables/usePromoCode.ts +43 -0
  57. package/composables/useRecapPermission.ts +26 -0
  58. package/composables/useRole.ts +89 -0
  59. package/composables/useSchoolPermission.ts +13 -0
  60. package/composables/useSubscription.ts +264 -0
  61. package/composables/useUser.ts +102 -0
  62. package/composables/useUtils.ts +294 -0
  63. package/composables/useVerification.ts +19 -0
  64. package/error.vue +41 -0
  65. package/eslint.config.js +3 -0
  66. package/layouts/plain.vue +7 -0
  67. package/middleware/01.auth.ts +14 -0
  68. package/middleware/org.ts +16 -0
  69. package/nuxt.config.ts +48 -0
  70. package/package.json +35 -0
  71. package/pages/index.vue +3 -0
  72. package/pages/payment-method-cancel-link.vue +31 -0
  73. package/pages/payment-method-failed-link.vue +31 -0
  74. package/pages/payment-method-linked.vue +31 -0
  75. package/pages/require-organization-membership.vue +47 -0
  76. package/pages/unauthorized.vue +29 -0
  77. package/plugins/API.ts +58 -0
  78. package/plugins/iconify.client.ts +5 -0
  79. package/plugins/vuetify.ts +55 -0
  80. package/public/bdo-logo.svg +4 -0
  81. package/public/bpi-logo.svg +74 -0
  82. package/public/chinabank-logo.svg +120 -0
  83. package/public/gcash-logo.png +0 -0
  84. package/public/gcash-logo.svg +65 -0
  85. package/public/grabpay-logo.svg +99 -0
  86. package/public/paymaya-logo.jpg +0 -0
  87. package/public/paymaya-logo.png +0 -0
  88. package/public/paymaya-logo.svg +25 -0
  89. package/public/qrph-c567ff0f-ab6d-4662-86bf-24c6c731d8a8-logo.svg +20 -0
  90. package/public/rcbc-logo.svg +15 -0
  91. package/public/shopeepay-logo.svg +89 -0
  92. package/public/ubp-logo.svg +88 -0
  93. package/tsconfig.json +3 -0
  94. package/types/address.d.ts +13 -0
  95. package/types/invoice.d.ts +28 -0
  96. package/types/local.d.ts +25 -0
  97. package/types/member.d.ts +12 -0
  98. package/types/org.d.ts +13 -0
  99. package/types/payment-method.d.ts +11 -0
  100. package/types/payment.d.ts +18 -0
  101. package/types/permission.d.ts +14 -0
  102. package/types/price.d.ts +17 -0
  103. package/types/promo-code.d.ts +19 -0
  104. package/types/role.d.ts +13 -0
  105. package/types/subscription.d.ts +29 -0
  106. package/types/user.d.ts +21 -0
  107. package/types/verification.d.ts +15 -0
  108. package/types/xendit.d.ts +3 -0
@@ -0,0 +1,139 @@
1
+ <template>
2
+ <v-input
3
+ :label="label"
4
+ :error="!!errorMessage"
5
+ :error-messages="errorMessage"
6
+ :hide-details="readOnly ?? !!value"
7
+ class="btn-input-file"
8
+ >
9
+ <template #default>
10
+ <v-row no-gutters>
11
+ <v-col cols="12" v-if="value || readOnly">
12
+ <v-row no-gutters>
13
+ <v-card
14
+ variant="outlined"
15
+ border="thin"
16
+ class="px-4 text-caption font-weight-medium"
17
+ :rounded="readOnly ? '' : '0 s'"
18
+ @click="emit('click:view')"
19
+ height="36"
20
+ >
21
+ <v-row no-gutters class="fill-height" align="center">
22
+ {{ value ? "View attachment" : "No file attached" }}
23
+ </v-row>
24
+ </v-card>
25
+ <v-card
26
+ v-if="!readOnly"
27
+ variant="outlined"
28
+ border="s-0 thin"
29
+ rounded="0 e"
30
+ class="px-4 d-inline"
31
+ @click="emit('click:close')"
32
+ height="36"
33
+ >
34
+ <v-row no-gutters class="fill-height" align="center">
35
+ <v-icon size="small"> mdi-close </v-icon>
36
+ </v-row>
37
+ </v-card>
38
+ </v-row>
39
+ </v-col>
40
+
41
+ <v-col cols="12" v-else>
42
+ <v-btn
43
+ variant="outlined"
44
+ border="thin"
45
+ class="text-none"
46
+ :prepend-icon="value ? '' : 'mdi-tray-arrow-up'"
47
+ @click="selectAttachment"
48
+ :disabled="loading"
49
+ :loading="loading"
50
+ >
51
+ Add file
52
+ </v-btn>
53
+ </v-col>
54
+ </v-row>
55
+
56
+ <v-file-input
57
+ v-show="false"
58
+ v-model="attachment"
59
+ multiple
60
+ accept="image/*"
61
+ :class="`attachment-input-${inputClass}`"
62
+ @change="uploadAttachment"
63
+ :name="name + 'custom-input-file'"
64
+ ></v-file-input>
65
+ </template>
66
+ </v-input>
67
+ </template>
68
+
69
+ <script setup lang="ts">
70
+ const value = defineModel<string>({ required: true, default: "" });
71
+ const attachment = ref<File | null>(null);
72
+ const loading = ref(false);
73
+
74
+ const emit = defineEmits(["click:view", "click:close", "upload"]);
75
+
76
+ const { addFile } = useFile();
77
+ const inputClass = Date.now().toString();
78
+
79
+ function selectAttachment() {
80
+ const input = document.querySelector(
81
+ `.attachment-input-${inputClass} input`
82
+ ) as HTMLInputElement;
83
+ input.click();
84
+ }
85
+
86
+ async function uploadAttachment(e: Event) {
87
+ loading.value = true;
88
+ const files = (e.target as HTMLInputElement).files;
89
+
90
+ if (files && files.length > 0) {
91
+ const file = files[0];
92
+
93
+ try {
94
+ const res = await addFile(file);
95
+ value.value = res.id as string;
96
+ await emit("upload", res);
97
+ } catch (error) {
98
+ console.log(error);
99
+ }
100
+ } else {
101
+ console.log("No file selected");
102
+ }
103
+ loading.value = false;
104
+ }
105
+
106
+ // Other props (optional)
107
+ const { label, rules, name, readOnly } = defineProps({
108
+ label: {
109
+ type: String,
110
+ default: "Rating",
111
+ },
112
+ // Array of validation functions: (number) => true | string
113
+ rules: {
114
+ type: Array<Function>,
115
+ default: () => [],
116
+ },
117
+ name: {
118
+ type: String,
119
+ required: true,
120
+ default: "button-upload-file",
121
+ },
122
+ readOnly: {
123
+ type: Boolean,
124
+ default: false,
125
+ },
126
+ });
127
+
128
+ /**
129
+ * Compute the first failed validation rule, if any.
130
+ * Each rule returns true (if valid) or a string (if invalid).
131
+ */
132
+ const errorMessage = computed(() => {
133
+ for (const rule of rules) {
134
+ const result = rule(attachment.value || value.value);
135
+ return typeof result === "string" ? result : null;
136
+ }
137
+ return null;
138
+ });
139
+ </script>
@@ -0,0 +1,66 @@
1
+ <template>
2
+ <v-dialog v-model="dialog" max-width="400" persistent>
3
+ <v-card class="rounded-xl pa-6" elevation="2">
4
+ <!-- Close Button -->
5
+ <v-btn
6
+ icon
7
+ variant="text"
8
+ size="small"
9
+ class="position-absolute"
10
+ style="top: 8px; right: 8px"
11
+ @click="closeDialog"
12
+ >
13
+ <v-icon>mdi-close</v-icon>
14
+ </v-btn>
15
+
16
+ <v-card-text class="text-center">
17
+ <v-row v-if="imageSrc" justify="center" class="py-10">
18
+ <v-img height="120" :src="imageSrc" alt="Dialog Image" contain />
19
+ </v-row>
20
+
21
+ <!-- Title Slot -->
22
+ <div class="text-h6 font-weight-medium mb-2">
23
+ <slot name="title"></slot>
24
+ </div>
25
+
26
+ <!-- Description Slot -->
27
+ <div class="text-body-1 mb-4">
28
+ <slot name="description"></slot>
29
+ </div>
30
+ </v-card-text>
31
+
32
+ <!-- Footer Slot -->
33
+ <v-card-actions>
34
+ <slot name="footer"></slot>
35
+ </v-card-actions>
36
+ </v-card>
37
+ </v-dialog>
38
+ </template>
39
+
40
+ <script lang="ts" setup>
41
+ const props = defineProps({
42
+ modelValue: {
43
+ type: Boolean,
44
+ default: false,
45
+ },
46
+ imageSrc: {
47
+ type: String,
48
+ default: "",
49
+ },
50
+ loading: {
51
+ type: Boolean,
52
+ default: false,
53
+ },
54
+ });
55
+
56
+ const emit = defineEmits(["update:modelValue", "submit"]);
57
+
58
+ const dialog = computed({
59
+ get: () => props.modelValue,
60
+ set: (value) => emit("update:modelValue", value),
61
+ });
62
+
63
+ function closeDialog() {
64
+ dialog.value = false;
65
+ }
66
+ </script>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <v-col cols="12">
4
+ <span
5
+ class="font-weight-medium text-subtitle-1 text-decoration-underline cursor-pointer"
6
+ @click="emit('back')"
7
+ >
8
+ Back
9
+ </span>
10
+ </v-col>
11
+ <v-col cols="12">
12
+ <span class="text-h5 font-weight-medium">{{ props.title }}</span>
13
+ </v-col>
14
+
15
+ <v-col cols="12" class="mt-4">
16
+ <slot name="default"> </slot>
17
+ </v-col>
18
+
19
+ <v-col cols="12" class="mt-4">
20
+ <slot name="footer"> </slot>
21
+ </v-col>
22
+ </v-row>
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ const emit = defineEmits(["back"]);
27
+ const props = defineProps({
28
+ title: {
29
+ type: String,
30
+ default: "",
31
+ },
32
+ });
33
+ </script>
@@ -0,0 +1,177 @@
1
+ <template>
2
+ <v-input
3
+ :error="!!validationErrMsg"
4
+ :error-messages="validationErrMsg"
5
+ hide-details="auto"
6
+ :name="name + 'custom-input-date'"
7
+ :disabled="props.disabled"
8
+ >
9
+ <template #default>
10
+ <v-row>
11
+ <v-col cols="12" lg="6" md="6">
12
+ <v-select
13
+ v-model="month"
14
+ :items="props.months"
15
+ label="Month"
16
+ density="comfortable"
17
+ :name="props.name + 'custom-input-date-month'"
18
+ hide-details
19
+ :error="!!validationErrMsg"
20
+ :disabled="props.disabled"
21
+ />
22
+ </v-col>
23
+
24
+ <v-col cols="12" lg="3" md="3">
25
+ <v-text-field
26
+ v-model.number="day"
27
+ type="number"
28
+ label="Day"
29
+ hide-details
30
+ placeholder="DD"
31
+ density="comfortable"
32
+ :name="props.name + 'custom-input-date-day'"
33
+ :error="!!validationErrMsg"
34
+ :disabled="props.disabled"
35
+ />
36
+ </v-col>
37
+
38
+ <v-col cols="12" lg="3" md="3">
39
+ <v-text-field
40
+ v-model.number="year"
41
+ type="number"
42
+ label="Year"
43
+ hide-details
44
+ placeholder="YYYY"
45
+ density="comfortable"
46
+ :name="props.name + 'custom-input-date-year'"
47
+ :error="!!validationErrMsg"
48
+ :disabled="props.disabled"
49
+ />
50
+ </v-col>
51
+ </v-row>
52
+ </template>
53
+ </v-input>
54
+ </template>
55
+
56
+ <script setup lang="ts">
57
+ const month = defineModel("month", { default: "" });
58
+ const day = defineModel("day", { default: 0 });
59
+ const year = defineModel("year", { default: 0 });
60
+
61
+ const props = defineProps({
62
+ months: {
63
+ type: Array,
64
+ default: [
65
+ "January",
66
+ "February",
67
+ "March",
68
+ "April",
69
+ "May",
70
+ "June",
71
+ "July",
72
+ "August",
73
+ "September",
74
+ "October",
75
+ "November",
76
+ "December",
77
+ ],
78
+ },
79
+ name: {
80
+ type: String,
81
+ default: "input-date-combo",
82
+ },
83
+ disabled: {
84
+ type: Boolean,
85
+ default: false,
86
+ },
87
+ });
88
+
89
+ const isLeapYear = (year: number): boolean =>
90
+ (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
91
+
92
+ const monthMap: Record<string, number> = {
93
+ January: 1,
94
+ February: 2,
95
+ March: 3,
96
+ April: 4,
97
+ May: 5,
98
+ June: 6,
99
+ July: 7,
100
+ August: 8,
101
+ September: 9,
102
+ October: 10,
103
+ November: 11,
104
+ December: 12,
105
+ };
106
+
107
+ const validateBirthDate = (): string => {
108
+ const _month = monthMap[month.value];
109
+ const _day = day.value;
110
+ const _year = year.value;
111
+
112
+ const errorMessage = "Enter a valid date";
113
+
114
+ if (
115
+ !_month ||
116
+ !_day ||
117
+ !_year ||
118
+ _year < 1904 ||
119
+ _year > new Date().getFullYear()
120
+ ) {
121
+ return errorMessage;
122
+ }
123
+
124
+ const daysInMonth = [
125
+ 31,
126
+ isLeapYear(_year) ? 29 : 28,
127
+ 31,
128
+ 30,
129
+ 31,
130
+ 30,
131
+ 31,
132
+ 31,
133
+ 30,
134
+ 31,
135
+ 30,
136
+ 31,
137
+ ];
138
+
139
+ if (_day < 1 || _day > daysInMonth[_month - 1]) {
140
+ return errorMessage;
141
+ }
142
+
143
+ if (!_month || !_day || !_year) {
144
+ return errorMessage;
145
+ }
146
+
147
+ return "";
148
+ };
149
+
150
+ const validationErrMsg = ref("");
151
+
152
+ const date = computed(() => {
153
+ return {
154
+ day: day.value,
155
+ month: month.value,
156
+ year: year.value,
157
+ };
158
+ });
159
+
160
+ validationErrMsg.value = validateBirthDate();
161
+
162
+ const disabled = computed(() => props.disabled);
163
+
164
+ watch(disabled, (curr) => {
165
+ if (curr) {
166
+ validationErrMsg.value = validateBirthDate();
167
+ }
168
+ });
169
+
170
+ watch(
171
+ date,
172
+ () => {
173
+ validationErrMsg.value = validateBirthDate();
174
+ },
175
+ { deep: true }
176
+ );
177
+ </script>
@@ -0,0 +1,93 @@
1
+ <template>
2
+ <v-input v-bind="attrs">
3
+ <v-card width="100%" v-bind="attrs">
4
+ <v-list
5
+ v-model:selected="selected"
6
+ lines="two"
7
+ :select-strategy="attrs.readonly ? 'classic' : 'leaf'"
8
+ class="pa-0"
9
+ density="compact"
10
+ read-only
11
+ open-strategy="single"
12
+ >
13
+ <template
14
+ v-for="(permission, permissionKey, permissionIndex) in props.items"
15
+ :key="permissionKey"
16
+ >
17
+ <v-divider v-if="permissionIndex > 0"></v-divider>
18
+ <v-list-group :value="permissionKey" fluid>
19
+ <template v-slot:activator="{ props }">
20
+ <v-list-item v-bind="props" density="compact">
21
+ <span class="text-capitalize">
22
+ {{ String(permissionKey).replace(/-/g, " ") }}
23
+ </span>
24
+
25
+ <template #prepend>
26
+ <v-chip class="mr-2" small>
27
+ {{ selectedActionCount(String(permissionKey)) }}
28
+ </v-chip>
29
+ </template>
30
+ </v-list-item>
31
+ </template>
32
+
33
+ <template v-for="(item, itemKey) in permission" :key="itemKey">
34
+ <v-divider></v-divider>
35
+ <v-list-item v-if="attrs.readonly" density="compact">
36
+ <template #title class="pl-2">
37
+ <span class="text-subtitle-2 text-capitalize">
38
+ {{ String(itemKey).replace(/-/g, " ") }}
39
+ </span>
40
+ </template>
41
+
42
+ <template #subtitle class="pl-2">
43
+ <span class="text-subtitle-2">{{ item.description }}</span>
44
+ </template>
45
+ </v-list-item>
46
+
47
+ <v-list-item
48
+ v-else
49
+ :value="`${permissionKey}:${itemKey}`"
50
+ density="compact"
51
+ >
52
+ <template #title class="pl-2">
53
+ <span class="text-subtitle-2 text-capitalize">
54
+ {{ String(itemKey).replace(/-/g, " ") }}
55
+ </span>
56
+ </template>
57
+
58
+ <template #subtitle class="pl-2">
59
+ <span class="text-subtitle-2 text-capitalize">
60
+ {{ String(item.description).replace(/-/g, " ") }}
61
+ </span>
62
+ </template>
63
+
64
+ <template #prepend="{ isSelected }" class="pl-1">
65
+ <v-list-item-action start>
66
+ <v-checkbox-btn :model-value="isSelected"></v-checkbox-btn>
67
+ </v-list-item-action>
68
+ </template>
69
+ </v-list-item>
70
+ </template>
71
+ </v-list-group>
72
+ </template>
73
+ </v-list>
74
+ </v-card>
75
+ </v-input>
76
+ </template>
77
+
78
+ <script setup lang="ts">
79
+ const selected = defineModel<Array<string>>({ default: [] });
80
+ const attrs = useAttrs();
81
+ const props = defineProps({
82
+ items: {
83
+ type: Object,
84
+ required: true,
85
+ default: () => ({}),
86
+ },
87
+ });
88
+
89
+ const selectedActionCount = (resource: string) => {
90
+ return selected.value.filter((permission) => permission.startsWith(resource))
91
+ .length;
92
+ };
93
+ </script>
@@ -0,0 +1,123 @@
1
+ <script setup lang="ts">
2
+ import { defineModel, defineProps, computed, ref } from "vue";
3
+
4
+ const props = defineProps<{
5
+ rules?: ((value: string) => boolean | string)[]; // Accepts external validation rules
6
+ }>();
7
+
8
+ const dateValue = defineModel<string>({ default: "" });
9
+ const inputRef = ref<HTMLInputElement | null>(null);
10
+
11
+ const formatDate = (event: Event) => {
12
+ const input = event.target as HTMLInputElement;
13
+ let value = input.value.replace(/\D/g, ""); // Remove non-numeric characters
14
+
15
+ // Format as MM/DD/YYYY
16
+ let formattedValue = value
17
+ .slice(0, 8)
18
+ .replace(/(\d{2})(\d{0,2})?(\d{0,4})?/, (_, m, d, y) =>
19
+ [m, d, y].filter(Boolean).join("/")
20
+ );
21
+
22
+ // Preserve cursor position
23
+ const cursorPosition = input.selectionStart ?? 0;
24
+ const slashCountBefore = (dateValue.value.match(/\//g) || []).length;
25
+ const slashCountAfter = (formattedValue.match(/\//g) || []).length;
26
+ const cursorOffset = slashCountAfter - slashCountBefore;
27
+
28
+ // Only update if value changed to prevent unnecessary reactivity updates
29
+ if (dateValue.value !== formattedValue) {
30
+ dateValue.value = formattedValue;
31
+ setTimeout(() => {
32
+ input.setSelectionRange(
33
+ cursorPosition + cursorOffset,
34
+ cursorPosition + cursorOffset
35
+ );
36
+ });
37
+ }
38
+ };
39
+
40
+ // Compute combined validation rules
41
+ const computedRules = computed(() => {
42
+ return props.rules ? [...props.rules] : [];
43
+ });
44
+
45
+ // Handle arrow key increments with cursor preservation
46
+ const handleArrowKeys = (event: KeyboardEvent) => {
47
+ if (!dateValue.value) return;
48
+
49
+ const input = event.target as HTMLInputElement;
50
+ const cursorPosition = input.selectionStart ?? 0; // Store cursor position
51
+ dateValue.value.split("/").map(Number);
52
+
53
+ let updatedDate = dateValue.value;
54
+
55
+ // Determine which part to modify
56
+ if (cursorPosition <= 2) {
57
+ updatedDate = modifyDatePart(
58
+ dateValue.value,
59
+ "month",
60
+ event.key === "ArrowUp" ? 1 : -1
61
+ );
62
+ } else if (cursorPosition <= 5) {
63
+ updatedDate = modifyDatePart(
64
+ dateValue.value,
65
+ "day",
66
+ event.key === "ArrowUp" ? 1 : -1
67
+ );
68
+ } else {
69
+ updatedDate = modifyDatePart(
70
+ dateValue.value,
71
+ "year",
72
+ event.key === "ArrowUp" ? 1 : -1
73
+ );
74
+ }
75
+
76
+ if (dateValue.value !== updatedDate) {
77
+ dateValue.value = updatedDate;
78
+ setTimeout(() => {
79
+ input.setSelectionRange(cursorPosition, cursorPosition);
80
+ });
81
+ }
82
+
83
+ event.preventDefault();
84
+ };
85
+
86
+ const modifyDatePart = (
87
+ date: string,
88
+ part: "month" | "day" | "year",
89
+ change: number
90
+ ) => {
91
+ let [month, day, year] = date.split("/").map(Number);
92
+
93
+ if (part === "month") {
94
+ month = Math.max(1, Math.min(12, month + change));
95
+ const maxDays = new Date(year, month, 0).getDate();
96
+ day = Math.min(day, maxDays); // Adjust day to fit new month's max days
97
+ } else if (part === "day") {
98
+ const maxDays = new Date(year, month, 0).getDate();
99
+ day = Math.max(1, Math.min(maxDays, day + change));
100
+ } else if (part === "year") {
101
+ year += change;
102
+ const maxDays = new Date(year, month, 0).getDate();
103
+ day = Math.min(day, maxDays); // Adjust day to fit new year's month
104
+ }
105
+
106
+ return `${String(month).padStart(2, "0")}/${String(day).padStart(
107
+ 2,
108
+ "0"
109
+ )}/${year}`;
110
+ };
111
+ </script>
112
+
113
+ <template>
114
+ <v-text-field
115
+ ref="inputRef"
116
+ v-model="dateValue"
117
+ placeholder="MM/DD/YYYY"
118
+ @input="formatDate"
119
+ @keydown.up="handleArrowKeys"
120
+ @keydown.down="handleArrowKeys"
121
+ :rules="computedRules"
122
+ ></v-text-field>
123
+ </template>