@goweekdays/layer-common 0.0.6 → 0.0.7

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @goweekdays/layer-common
2
2
 
3
+ ## 0.0.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 3b7164b: add promo code
8
+
3
9
  ## 0.0.6
4
10
 
5
11
  ### Patch Changes
@@ -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>
@@ -1,18 +1,29 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed, nextTick, useAttrs } from "vue";
3
3
 
4
+ const props = defineProps({
5
+ infinityEnabled: { type: Boolean, default: false }, // Enable ∞ display for 0
6
+ });
7
+
4
8
  const modelValue = defineModel<number>(); // Store actual number
5
- const inputRef = ref<HTMLInputElement | null>(null); // Reference input field
6
- const attrs = useAttrs(); // Get all passed attributes (rules, required, etc.)
9
+ const inputRef = ref<HTMLInputElement | null>(null);
10
+ const attrs = useAttrs();
7
11
 
8
- let cursorPosition = 0; // Track cursor position
9
- let forceCursorToEnd = false; // Track if cursor should be forced to end
12
+ let cursorPosition = 0;
13
+ let forceCursorToEnd = false;
10
14
 
11
- // Computed property to format value with commas
15
+ // Computed property to format value with commas or infinity sign
12
16
  const formattedValue = computed({
13
- get: () =>
14
- modelValue.value !== undefined ? modelValue.value.toLocaleString() : "",
17
+ get: () => {
18
+ if (props.infinityEnabled && modelValue.value === 0) return ""; // Show ∞ if enabled
19
+ return modelValue.value?.toLocaleString() ?? "";
20
+ },
15
21
  set: (val: string) => {
22
+ if (props.infinityEnabled && val === "∞") {
23
+ modelValue.value = 0; // Convert back to 0 internally
24
+ return;
25
+ }
26
+
16
27
  const rawValue = val.replace(/\D/g, ""); // Remove non-numeric characters
17
28
  const numericValue = rawValue ? Number(rawValue) : 0;
18
29
 
@@ -30,7 +41,6 @@ const handleKeyDown = (event: KeyboardEvent) => {
30
41
  const { selectionStart, selectionEnd, value } = inputRef.value;
31
42
  const isAllSelected = selectionStart === 0 && selectionEnd === value.length;
32
43
 
33
- // Allow only numbers (prevent non-numeric input)
34
44
  if (
35
45
  !/^\d$/.test(event.key) &&
36
46
  ![
@@ -46,7 +56,6 @@ const handleKeyDown = (event: KeyboardEvent) => {
46
56
  return;
47
57
  }
48
58
 
49
- // If all text is selected and a number is pressed, replace entire value
50
59
  if (isAllSelected && /^\d$/.test(event.key)) {
51
60
  event.preventDefault();
52
61
  modelValue.value = Number(event.key);
@@ -55,13 +64,11 @@ const handleKeyDown = (event: KeyboardEvent) => {
55
64
  return;
56
65
  }
57
66
 
58
- // Allow cursor movement with ArrowLeft & ArrowRight
59
67
  if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
60
68
  forceCursorToEnd = false;
61
69
  return;
62
70
  }
63
71
 
64
- // Force cursor to end when incrementing/decrementing
65
72
  if (event.key === "ArrowUp") {
66
73
  event.preventDefault();
67
74
  modelValue.value += 1;
@@ -71,21 +78,21 @@ const handleKeyDown = (event: KeyboardEvent) => {
71
78
  modelValue.value = Math.max(0, modelValue.value - 1);
72
79
  forceCursorToEnd = true;
73
80
  } else {
74
- cursorPosition = selectionStart || 0; // Store cursor position before input
81
+ cursorPosition = selectionStart || 0;
75
82
  }
76
83
 
77
84
  nextTick(() => restoreCursor());
78
85
  };
79
86
 
80
- // Restore cursor position correctly
87
+ // Restore cursor position
81
88
  const restoreCursor = () => {
82
89
  if (!inputRef.value) return;
83
90
  const length = formattedValue.value.length;
84
91
 
85
92
  if (forceCursorToEnd) {
86
- inputRef.value.setSelectionRange(length, length); // Keep cursor at end
93
+ inputRef.value.setSelectionRange(length, length);
87
94
  } else {
88
- inputRef.value.setSelectionRange(cursorPosition, cursorPosition); // Keep normal position
95
+ inputRef.value.setSelectionRange(cursorPosition, cursorPosition);
89
96
  }
90
97
  };
91
98
  </script>
@@ -96,7 +103,22 @@ const restoreCursor = () => {
96
103
  v-model="formattedValue"
97
104
  v-bind="attrs"
98
105
  type="text"
99
- placeholder="Enter a number"
100
106
  @keydown="handleKeyDown"
101
- ></v-text-field>
107
+ >
108
+ <template v-if="$slots.prepend" v-slot:prepend>
109
+ <slot name="prepend"></slot>
110
+ </template>
111
+
112
+ <template v-if="$slots.append" v-slot:append>
113
+ <slot name="append"></slot>
114
+ </template>
115
+
116
+ <template v-if="$slots['append-inner']" v-slot:append-inner>
117
+ <slot name="append-inner"></slot>
118
+ </template>
119
+
120
+ <template v-if="$slots['prepend-inner']" v-slot:prepend-inner>
121
+ <slot name="prepend-inner"></slot>
122
+ </template>
123
+ </v-text-field>
102
124
  </template>
@@ -0,0 +1,22 @@
1
+ export default function useOrder() {
2
+ function getOrders({
3
+ search = "",
4
+ id = "",
5
+ page = 1,
6
+ status = "succeeded",
7
+ } = {}) {
8
+ return useNuxtApp().$api<Record<string, any>>("/api/orders", {
9
+ method: "GET",
10
+ query: {
11
+ search,
12
+ id,
13
+ page,
14
+ status,
15
+ },
16
+ });
17
+ }
18
+
19
+ return {
20
+ getOrders,
21
+ };
22
+ }
@@ -0,0 +1,34 @@
1
+ export default function usePromoCode() {
2
+ function add(value: TPromoCode) {
3
+ return useNuxtApp().$api<Record<string, any>>("/api/promo-codes", {
4
+ method: "POST",
5
+ body: value,
6
+ });
7
+ }
8
+
9
+ function getPromoCodes({ search = "", page = 1, status = "active" } = {}) {
10
+ return useNuxtApp().$api<Record<string, any>>("/api/promo-codes", {
11
+ method: "GET",
12
+ query: {
13
+ search,
14
+ page,
15
+ status,
16
+ },
17
+ });
18
+ }
19
+
20
+ function getByCode(code: string) {
21
+ return useNuxtApp().$api<Record<string, any>>(
22
+ `/api/promo-codes/code/${code}`,
23
+ {
24
+ method: "GET",
25
+ }
26
+ );
27
+ }
28
+
29
+ return {
30
+ add,
31
+ getPromoCodes,
32
+ getByCode,
33
+ };
34
+ }
@@ -13,6 +13,12 @@ export default function useSubscription() {
13
13
  return useNuxtApp().$api<TSubscription>(`/api/subscriptions/id/${id}`);
14
14
  }
15
15
 
16
+ function getByAffiliateId(id: string) {
17
+ return useNuxtApp().$api<TSubscription>(
18
+ `/api/subscriptions/affiliate/${id}`
19
+ );
20
+ }
21
+
16
22
  function getByOrgId(id: string) {
17
23
  return useNuxtApp().$api<Record<string, any>>(
18
24
  `/api/subscriptions/org/${id}`
@@ -40,37 +46,35 @@ export default function useSubscription() {
40
46
  () => "inactive"
41
47
  );
42
48
 
43
- async function checkSubscription() {
49
+ async function affSubscriptionStatus() {
44
50
  const { currentUser } = useLocalAuth();
45
51
 
46
52
  if (currentUser.value && currentUser.value._id) {
47
53
  try {
48
- const result = await getSubscriptionStatusById(currentUser.value._id);
49
- affiliateSubscription.value = result.status;
54
+ const result = await getByAffiliateId(currentUser.value._id);
55
+ affiliateSubscription.value = result.status as string;
50
56
  } catch (error) {
51
57
  console.error("failed to get the subscription", error);
52
58
  }
53
59
  }
54
60
  }
55
61
 
56
- function initAffiliateSubscription(
57
- customer_id: string,
58
- amount: number,
59
- payment_method_id: string,
60
- currency?: string
61
- ) {
62
- return useNuxtApp().$api("/api/subscriptions/affiliate", {
63
- method: "POST",
64
- body: {
65
- customer_id,
66
- amount,
67
- payment_method_id,
68
- currency,
69
- },
70
- });
62
+ const orgSubscription = useState("orgSubscription", () => "inactive");
63
+
64
+ async function orgSubscriptionStatus() {
65
+ const { currentOrg } = useOrg();
66
+
67
+ if (currentOrg.value) {
68
+ try {
69
+ const result = await getByOrgId(currentOrg.value);
70
+ orgSubscription.value = result.status as string;
71
+ } catch (error) {
72
+ console.error("failed to get the subscription", error);
73
+ }
74
+ }
71
75
  }
72
76
 
73
- type TOrgSubscription = {
77
+ type TSub = {
74
78
  customer_id: string;
75
79
  amount: number;
76
80
  payment_method_id: string;
@@ -82,12 +86,12 @@ export default function useSubscription() {
82
86
  payment_method_cvv?: string;
83
87
  payment_method_cardholder_name?: string;
84
88
  currency?: string;
85
- seats: number;
86
- organization: TOrg;
89
+ seats?: number;
90
+ organization?: TOrg;
87
91
  billingAddress: TAddress;
88
92
  };
89
93
 
90
- function initOrgSubscription(value: TOrgSubscription) {
94
+ function initOrgSubscription(value: TSub) {
91
95
  return useNuxtApp().$api<Record<string, any>>(
92
96
  "/api/subscriptions/organization",
93
97
  {
@@ -97,16 +101,29 @@ export default function useSubscription() {
97
101
  );
98
102
  }
99
103
 
104
+ function initAffiliateSubscription(value: TSub) {
105
+ return useNuxtApp().$api<Record<string, any>>(
106
+ "/api/subscriptions/affiliate",
107
+ {
108
+ method: "POST",
109
+ body: value,
110
+ }
111
+ );
112
+ }
113
+
100
114
  return {
101
115
  add,
102
116
  getById,
103
117
  getByOrgId,
104
118
  getSubscriptions,
119
+ affSubscriptionStatus,
105
120
  affiliateSubscription,
106
- checkSubscription,
121
+ orgSubscriptionStatus,
122
+ orgSubscription,
107
123
  getSubscriptionStatusById,
108
124
  cancelSubscription,
109
125
  initAffiliateSubscription,
110
126
  initOrgSubscription,
127
+ getByAffiliateId,
111
128
  };
112
129
  }
@@ -6,6 +6,31 @@ export default function useUtils() {
6
6
  return regex.test(v) || "Please enter a valid email address";
7
7
  };
8
8
 
9
+ const validateDate = (value: string): boolean | string => {
10
+ const dateRegex = /^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$/;
11
+
12
+ if (!dateRegex.test(value)) return "Invalid date format (MM/DD/YYYY)";
13
+
14
+ return true;
15
+ };
16
+
17
+ const minOneMonthAdvance = (value: string): boolean | string => {
18
+ if (!/^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$/.test(value)) {
19
+ return "Invalid date format (MM/DD/YYYY)";
20
+ }
21
+
22
+ const [month, day, year] = value.split("/").map(Number);
23
+ const selectedDate = new Date(year, month - 1, day);
24
+
25
+ const currentDate = new Date();
26
+ const minDate = new Date();
27
+ minDate.setMonth(currentDate.getMonth() + 1); // 1 month in advance
28
+
29
+ return (
30
+ selectedDate >= minDate || "Date must be at least 1 month in advance"
31
+ );
32
+ };
33
+
9
34
  const passwordRule = (v: string) =>
10
35
  v.length >= 8 || "Password must be at least 8 characters long";
11
36
 
@@ -180,5 +205,7 @@ export default function useUtils() {
180
205
  getCountries,
181
206
  formatter,
182
207
  formatNumber,
208
+ validateDate,
209
+ minOneMonthAdvance,
183
210
  };
184
211
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@goweekdays/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "0.0.6",
5
+ "version": "0.0.7",
6
6
  "main": "./nuxt.config.ts",
7
7
  "publishConfig": {
8
8
  "access": "public"
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <h1>index</h1>
3
+ </template>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <v-row no-gutters class="fill-height" justify="center" align-content="center">
3
+ <v-card class="pa-4" elevation="4" color="success lighten-1">
4
+ <v-card-title class="text-white d-flex align-center">
5
+ <v-icon class="mr-2">mdi-check-circle</v-icon>
6
+ Successfully Linked Payment Method
7
+ </v-card-title>
8
+
9
+ <v-card-actions class="d-flex justify-center">
10
+ <v-btn
11
+ color="white"
12
+ variant="tonal"
13
+ rounded="xl"
14
+ size="large"
15
+ @click="closeWindow"
16
+ >Close</v-btn
17
+ >
18
+ </v-card-actions>
19
+ </v-card>
20
+ </v-row>
21
+ </template>
22
+
23
+ <script setup>
24
+ definePageMeta({
25
+ layout: "plain",
26
+ });
27
+
28
+ const closeWindow = () => {
29
+ window.close();
30
+ };
31
+ </script>
Binary file
@@ -0,0 +1,19 @@
1
+ declare type TPromoTier = {
2
+ min: number;
3
+ max: number;
4
+ price: number;
5
+ };
6
+
7
+ declare type TPromoCode = {
8
+ _id?: ObjectId;
9
+ code: string;
10
+ description?: string;
11
+ type: "tiered" | "fixed";
12
+ tiers?: TPromoTier[];
13
+ fixed_rate?: number;
14
+ appliesTo?: string; // "subscription" | "order"
15
+ createdAt?: string; // Date of creation
16
+ expiresAt?: string; // Optional expiry
17
+ assignedTo?: string | ObjectId; // Store the only ape who used it
18
+ status?: "active" | "expired" | "disabled";
19
+ };