@goweekdays/layer-core 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 (60) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/main.yml +19 -0
  4. package/.github/workflows/publish.yml +39 -0
  5. package/CHANGELOG.md +7 -0
  6. package/README.md +75 -0
  7. package/app/app.vue +8 -0
  8. package/app/assets/fonts/ProductSans-Black.ttf +0 -0
  9. package/app/assets/fonts/ProductSans-BlackItalic.ttf +0 -0
  10. package/app/assets/fonts/ProductSans-Bold.ttf +0 -0
  11. package/app/assets/fonts/ProductSans-BoldItalic.ttf +0 -0
  12. package/app/assets/fonts/ProductSans-Italic.ttf +0 -0
  13. package/app/assets/fonts/ProductSans-Light.ttf +0 -0
  14. package/app/assets/fonts/ProductSans-LightItalic.ttf +0 -0
  15. package/app/assets/fonts/ProductSans-Medium.ttf +0 -0
  16. package/app/assets/fonts/ProductSans-MediumItalic.ttf +0 -0
  17. package/app/assets/fonts/ProductSans-Regular.ttf +0 -0
  18. package/app/assets/fonts/ProductSans-Thin.ttf +0 -0
  19. package/app/assets/fonts/ProductSans-ThinItalic.ttf +0 -0
  20. package/app/assets/main.css +10 -0
  21. package/app/assets/settings.scss +89 -0
  22. package/app/components/Input/ListGroupSelection.vue +107 -0
  23. package/app/components/Input/Password.vue +35 -0
  24. package/app/composables/useLocalAuth.ts +131 -0
  25. package/app/composables/useLocalSetup.ts +34 -0
  26. package/app/composables/useMember.ts +100 -0
  27. package/app/composables/useRole.ts +87 -0
  28. package/app/composables/useUtils.ts +308 -0
  29. package/app/layouts/plain.vue +7 -0
  30. package/app/middleware/01.auth.ts +14 -0
  31. package/app/middleware/org.ts +13 -0
  32. package/app/pages/forgot-password.vue +76 -0
  33. package/app/pages/index.vue +3 -0
  34. package/app/pages/login.vue +134 -0
  35. package/app/pages/logout.vue +18 -0
  36. package/app/pages/privacy-policy.vue +23 -0
  37. package/app/pages/refund-policy.vue +23 -0
  38. package/app/pages/sign-up.vue +141 -0
  39. package/app/pages/terms-and-conditions.vue +23 -0
  40. package/app/plugins/member.client.ts +66 -0
  41. package/app/plugins/secure.client.ts +34 -0
  42. package/app/plugins/vuetify.ts +54 -0
  43. package/app/public/background.png +0 -0
  44. package/app/public/favicon.svg +1 -0
  45. package/app/public/logo.png +0 -0
  46. package/app/public/pwa-192x192.png +0 -0
  47. package/app/public/pwa-512x512.png +0 -0
  48. package/app/public/robots.txt +1 -0
  49. package/app/types/local.d.ts +113 -0
  50. package/app/types/member.d.ts +13 -0
  51. package/app/types/role.d.ts +12 -0
  52. package/content/privacy-policy.md +151 -0
  53. package/content/refund-policy.md +65 -0
  54. package/content/terms-and-conditions.md +137 -0
  55. package/content.config.ts +10 -0
  56. package/nuxt.config.ts +45 -0
  57. package/package.json +32 -0
  58. package/public/favicon.ico +0 -0
  59. package/public/robots.txt +2 -0
  60. package/tsconfig.json +18 -0
@@ -0,0 +1,100 @@
1
+ export default function useMember() {
2
+ const members = useState<Array<TMember>>("members", () => []);
3
+ const page = useState("page", () => 1);
4
+ const pages = useState("pages", () => 0);
5
+ const pageRange = useState("pageRange", () => "-- - -- of --");
6
+
7
+ const member = useState<TMember>("member", () => ({
8
+ _id: "",
9
+ org: "",
10
+ orgName: "",
11
+ name: "",
12
+ user: "",
13
+ role: "",
14
+ roleName: "",
15
+ status: "",
16
+ createdAt: "",
17
+ updatedAt: "",
18
+ deletedAt: "",
19
+ }));
20
+
21
+ function getByUserId(user: string) {
22
+ return $fetch<TMember>(`/api/members/user/${user}`);
23
+ }
24
+
25
+ function getByUserType(user: string, type: string, org?: string) {
26
+ return $fetch<TMember>(`/api/members/user/${user}/app/${type}`, {
27
+ method: "GET",
28
+ query: { org },
29
+ });
30
+ }
31
+
32
+ async function getAll({
33
+ page = 1,
34
+ search = "",
35
+ limit = 10,
36
+ user = "",
37
+ org = "",
38
+ type = "",
39
+ status = "active",
40
+ } = {}) {
41
+ return $fetch<Record<string, any>>("/api/members", {
42
+ method: "GET",
43
+ query: { page, limit, search, user, org, type, status },
44
+ });
45
+ }
46
+
47
+ function createUserByVerification(
48
+ verificationId: string,
49
+ payload: Record<string, any>
50
+ ) {
51
+ return $fetch<Record<string, any>>(`/api/users/invite/${verificationId}`, {
52
+ method: "POST",
53
+ body: payload,
54
+ });
55
+ }
56
+
57
+ function createMemberInvite(verificatonId: string) {
58
+ return $fetch<TMember>(`/api/members/verification/${verificatonId}`, {
59
+ method: "POST",
60
+ });
61
+ }
62
+
63
+ function updateMemberStatus(id: string, status: string) {
64
+ return $fetch<Record<string, any>>(
65
+ `/api/members/status/${status}/id/${id}`,
66
+ {
67
+ method: "PUT",
68
+ }
69
+ );
70
+ }
71
+
72
+ function updateMemberRole(
73
+ id: string,
74
+ role: string,
75
+ type: string,
76
+ org: string
77
+ ) {
78
+ return $fetch<Record<string, any>>(
79
+ `/api/members/id/${id}/role/${role}/type/${type}/org/${org}`,
80
+ {
81
+ method: "PUT",
82
+ }
83
+ );
84
+ }
85
+ return {
86
+ members,
87
+ member,
88
+ page,
89
+ pages,
90
+ pageRange,
91
+
92
+ getAll,
93
+ getByUserId,
94
+ createUserByVerification,
95
+ createMemberInvite,
96
+ getByUserType,
97
+ updateMemberStatus,
98
+ updateMemberRole,
99
+ };
100
+ }
@@ -0,0 +1,87 @@
1
+ export default function useRole() {
2
+ function createRole(
3
+ { name, permissions, type, org } = {} as {
4
+ name: string;
5
+ permissions: Array<string>;
6
+ type: string;
7
+ org: string;
8
+ }
9
+ ) {
10
+ return $fetch("/api/roles", {
11
+ method: "POST",
12
+ body: { name, permissions, type, org },
13
+ });
14
+ }
15
+
16
+ function getRoles({
17
+ search = "",
18
+ page = 1,
19
+ limit = 20,
20
+ type = "",
21
+ org = "",
22
+ } = {}) {
23
+ return $fetch<Record<string, any>>("/api/roles", {
24
+ method: "GET",
25
+ query: { search, page, limit, type, org },
26
+ });
27
+ }
28
+
29
+ function getRoleById(id: string) {
30
+ return $fetch<Record<string, any>>(`/api/roles/id/${id}`, {
31
+ method: "GET",
32
+ });
33
+ }
34
+
35
+ function updateRoleById(
36
+ _id: string,
37
+ name?: string,
38
+ permissions?: Array<string>
39
+ ) {
40
+ return $fetch(`/api/roles/id/${_id}`, {
41
+ method: "PATCH",
42
+ body: { name, permissions },
43
+ });
44
+ }
45
+
46
+ function updatePermissionById(_id: string, permissions?: Array<string>) {
47
+ return $fetch(`/api/roles/permissions/id/${_id}`, {
48
+ method: "PATCH",
49
+ body: { permissions },
50
+ });
51
+ }
52
+
53
+ function updateRoleFieldById(_id: string, field: string, value: string) {
54
+ return $fetch(`/api/roles/${_id}`, {
55
+ method: "PATCH",
56
+ body: { field, value },
57
+ });
58
+ }
59
+
60
+ function deleteRole(_id: string) {
61
+ return $fetch<Record<string, any>>(`/api/roles/${_id}`, {
62
+ method: "DELETE",
63
+ });
64
+ }
65
+
66
+ const role = ref({
67
+ _id: "",
68
+ name: "",
69
+ org: "",
70
+ permissions: [],
71
+ createdAt: "",
72
+ updatedAt: "",
73
+ deletedAt: "",
74
+ default: false,
75
+ });
76
+
77
+ return {
78
+ role,
79
+ createRole,
80
+ getRoles,
81
+ getRoleById,
82
+ updateRoleById,
83
+ updateRoleFieldById,
84
+ deleteRole,
85
+ updatePermissionById,
86
+ };
87
+ }
@@ -0,0 +1,308 @@
1
+ export default function useUtils() {
2
+ const requiredRule = (v: string) => !!v || "Required";
3
+
4
+ const emailRule = (v: string): true | string => {
5
+ const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
6
+ if (!v) return true;
7
+ return regex.test(v) || "Please enter a valid email address";
8
+ };
9
+
10
+ const validateDate = (value: string): boolean | string => {
11
+ const dateRegex = /^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$/;
12
+
13
+ if (!dateRegex.test(value)) return "Invalid date format (MM/DD/YYYY)";
14
+
15
+ return true;
16
+ };
17
+
18
+ function requireListRule(value: string[]) {
19
+ return value.length ? "" : "Required";
20
+ }
21
+
22
+ const minOneMonthAdvance = (value: string): boolean | string => {
23
+ if (!/^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$/.test(value)) {
24
+ return "Invalid date format (MM/DD/YYYY)";
25
+ }
26
+
27
+ const [month, day, year] = value.split("/").map(Number);
28
+ const selectedDate = new Date(year ?? 0, (month ?? 0) - 1, day);
29
+
30
+ const currentDate = new Date();
31
+ const minDate = new Date();
32
+ minDate.setMonth(currentDate.getMonth() + 1); // 1 month in advance
33
+
34
+ return (
35
+ selectedDate >= minDate || "Date must be at least 1 month in advance"
36
+ );
37
+ };
38
+
39
+ const passwordRule = (v: string) =>
40
+ v.length >= 8 || "Password must be at least 8 characters long";
41
+
42
+ const confirmPasswordRule =
43
+ (newPassword: Ref<string>, confirmPassword: Ref<string>) => () =>
44
+ confirmPassword.value === newPassword.value || "Passwords must match";
45
+
46
+ const validateEmail = (email: Ref<string>, emailErrors: Ref<string[]>) => {
47
+ emailErrors.value = [];
48
+
49
+ if (!email.value) {
50
+ emailErrors.value.push("Please input your email");
51
+ } else {
52
+ const result = emailRule(email.value);
53
+ if (result !== true) {
54
+ emailErrors.value.push(result);
55
+ }
56
+ }
57
+ };
58
+
59
+ const validateWord = (value: string): boolean | string => {
60
+ if (value.trim().length < 3) return "Must be at least 3 characters";
61
+ return true;
62
+ };
63
+
64
+ const validatePassword = (
65
+ password: Ref<string>,
66
+ passwordErrors: Ref<string[]>
67
+ ) => {
68
+ passwordErrors.value = [];
69
+
70
+ if (!password.value) {
71
+ passwordErrors.value.push("Please input your password");
72
+ } else if (!passwordRule(password.value)) {
73
+ passwordErrors.value.push("Password must be at least 8 characters long");
74
+ }
75
+ };
76
+
77
+ function validateKey(value: string) {
78
+ const pattern = /^[a-z._-]+$/;
79
+ return (
80
+ pattern.test(value) || "Key must be lowercase and can include ., _, -"
81
+ );
82
+ }
83
+
84
+ function back() {
85
+ useRouter().back();
86
+ }
87
+
88
+ function debounce<T extends (...args: any[]) => void>(
89
+ func: T,
90
+ wait: number
91
+ ): (...args: Parameters<T>) => void {
92
+ let timeoutId: ReturnType<typeof setTimeout>;
93
+
94
+ return function (...args: Parameters<T>) {
95
+ clearTimeout(timeoutId);
96
+ timeoutId = setTimeout(() => func(...args), wait);
97
+ };
98
+ }
99
+
100
+ function insertBetween(arr: any, elementToInsert: any) {
101
+ return arr.flatMap((item: any, index: number, array: any) =>
102
+ index < array.length - 1 ? [item, elementToInsert] : [item]
103
+ );
104
+ }
105
+
106
+ function getNameInitials(name: string) {
107
+ if (typeof name !== "string" || name.trim() === "") {
108
+ return ""; // Default fallback for invalid input
109
+ }
110
+
111
+ const words = name.trim().split(/\s+/); // Split by spaces
112
+
113
+ if (words.length === 1) {
114
+ return words[0]?.slice(0, 2).toUpperCase(); // Take first two letters of the single word
115
+ }
116
+
117
+ return words
118
+ .map((word) => word[0]?.toUpperCase()) // Take the first letter of each word and capitalize
119
+ .join("")
120
+ .slice(0, 2); // Limit to 2 characters
121
+ }
122
+
123
+ function getDimensions(
124
+ file: File
125
+ ): Promise<{ width: number; height: number }> {
126
+ const img = new Image();
127
+ img.src = URL.createObjectURL(file);
128
+ return new Promise((resolve, reject) => {
129
+ img.onload = () => {
130
+ const width = img.width;
131
+ const height = img.height;
132
+ resolve({ width, height });
133
+ };
134
+
135
+ img.onerror = () => {
136
+ reject(new Error("Failed to read image dimensions"));
137
+ };
138
+ });
139
+ }
140
+
141
+ async function getCountries() {
142
+ try {
143
+ const countries = await $fetch<Array<Record<string, any>>>(
144
+ "https://restcountries.com/v3.1/all?fields=name,currencies,idd",
145
+ { method: "GET" }
146
+ );
147
+
148
+ const uniqueCountries: Map<
149
+ string,
150
+ { index: string; name: string; code: string }
151
+ > = new Map();
152
+
153
+ countries.forEach((country) => {
154
+ let suffixes = country.idd?.suffixes?.[0] || "";
155
+ let code = `${country.idd?.root || ""}${suffixes}`;
156
+ let name = country.name?.common || "";
157
+
158
+ if (name && code && !uniqueCountries.has(code)) {
159
+ uniqueCountries.set(code, { index: `${name}-${code}`, name, code });
160
+ }
161
+ });
162
+
163
+ return Array.from(uniqueCountries.values()).sort((a, b) =>
164
+ a.name.localeCompare(b.name)
165
+ );
166
+ } catch (error) {
167
+ console.error(error);
168
+ throw new Error("Failed to fetch countries.");
169
+ }
170
+ }
171
+
172
+ const formatter = new Intl.NumberFormat("en-US", {
173
+ minimumFractionDigits: 2,
174
+ maximumFractionDigits: 2,
175
+ });
176
+
177
+ function formatNumber(
178
+ amount: number,
179
+ options: {
180
+ currency?: string;
181
+ locale?: string;
182
+ useSymbol?: boolean;
183
+ decimalPlaces?: number;
184
+ } = {}
185
+ ): string {
186
+ const {
187
+ currency,
188
+ locale = "en-US",
189
+ useSymbol = false,
190
+ decimalPlaces = 2,
191
+ } = options;
192
+
193
+ return new Intl.NumberFormat(locale, {
194
+ style: useSymbol && currency ? "currency" : "decimal",
195
+ currency: currency || "USD", // Default currency (ignored if `useSymbol` is false)
196
+ minimumFractionDigits: decimalPlaces,
197
+ maximumFractionDigits: decimalPlaces,
198
+ }).format(amount);
199
+ }
200
+
201
+ // 🔹 Examples:
202
+ // console.log(formatNumber(1234.56)); // "1,234.56" (comma separator, 2 decimals)
203
+ // console.log(formatNumber(1234.56, { decimalPlaces: 0 })); // "1,234" (no decimals)
204
+ // console.log(formatNumber(1234.56, { useSymbol: true, currency: "USD" })); // "$1,234.56"
205
+ // console.log(formatNumber(1234.56, { useSymbol: true, currency: "PHP", decimalPlaces: 0 })); // "₱1,234"
206
+ // console.log(formatNumber(1234.56, { useSymbol: false, decimalPlaces: 0 })); // "1,234"
207
+ // console.log(formatNumber(1234.56, { useSymbol: true, currency: "EUR", locale: "de-DE" })); // "1.234,56 €"
208
+ // console.log(formatNumber(1234.56, { useSymbol: true, currency: "EUR", locale: "de-DE", decimalPlaces: 0 })); // "1.234 €"
209
+
210
+ function computeTieredCost(
211
+ seats: number,
212
+ tiers: { min: number; max: number; price: number }[],
213
+ remainingDays?: number,
214
+ totalDaysInMonth?: number
215
+ ) {
216
+ let totalCost = 0;
217
+
218
+ for (let i = 1; i <= seats; i++) {
219
+ for (const { min, max, price } of tiers) {
220
+ const effectiveMax = max === 0 ? Infinity : max;
221
+
222
+ if (i >= min && i <= effectiveMax) {
223
+ // Prorate the cost for this seat based on its tier price
224
+ if (remainingDays && totalDaysInMonth) {
225
+ totalCost += price * (remainingDays / totalDaysInMonth);
226
+ break;
227
+ }
228
+
229
+ totalCost += price;
230
+ break; // Stop checking once we apply the correct tier pricing
231
+ }
232
+ }
233
+ }
234
+
235
+ return totalCost; // This now returns the total prorated cost
236
+ }
237
+
238
+ function convertPermissionsToArray(permissions: TPermissions) {
239
+ return Object.entries(permissions).flatMap(([resource, actions]) =>
240
+ Object.keys(actions).map((action) => `${resource}:${action}`)
241
+ );
242
+ }
243
+
244
+ function extractMonthYear(expiry: string) {
245
+ const [month, year] = expiry.split("/");
246
+ return {
247
+ month: month?.padStart(2, "0"),
248
+ year: 2000 + parseInt(year ?? "", 10),
249
+ };
250
+ }
251
+
252
+ function getRouteParam(field = "") {
253
+ if (!field) return "";
254
+
255
+ return (useRoute().params[field] as string) ?? "";
256
+ }
257
+
258
+ function replaceMatch(str: string, match: string, replace: string) {
259
+ return str.replace(new RegExp(match, "g"), replace);
260
+ }
261
+
262
+ function setRouteParams(...args: Array<Record<string, string>>) {
263
+ const params: Record<string, string> = {};
264
+
265
+ args.forEach((arg) => {
266
+ Object.entries(arg).forEach(([key, value]) => {
267
+ if (value !== undefined && value !== null && value !== "") {
268
+ params[key] = value;
269
+ }
270
+ });
271
+ });
272
+
273
+ return params;
274
+ }
275
+
276
+ function positiveNumberRule(v: number) {
277
+ return (v && v >= 1) || "Value must be greater or equal to 1";
278
+ }
279
+
280
+ return {
281
+ requiredRule,
282
+ emailRule,
283
+ passwordRule,
284
+ confirmPasswordRule,
285
+ validateEmail,
286
+ validatePassword,
287
+ back,
288
+ debounce,
289
+ insertBetween,
290
+ getNameInitials,
291
+ getDimensions,
292
+ validateWord,
293
+ getCountries,
294
+ formatter,
295
+ formatNumber,
296
+ validateDate,
297
+ minOneMonthAdvance,
298
+ computeTieredCost,
299
+ requireListRule,
300
+ convertPermissionsToArray,
301
+ extractMonthYear,
302
+ getRouteParam,
303
+ replaceMatch,
304
+ setRouteParams,
305
+ positiveNumberRule,
306
+ validateKey,
307
+ };
308
+ }
@@ -0,0 +1,7 @@
1
+ <template>
2
+ <v-app>
3
+ <v-main>
4
+ <slot />
5
+ </v-main>
6
+ </v-app>
7
+ </template>
@@ -0,0 +1,14 @@
1
+ export default defineNuxtRouteMiddleware(async () => {
2
+ // Ensure middleware runs only on the client side
3
+ if (import.meta.server) return;
4
+
5
+ const { cookieConfig } = useLocalSetup();
6
+
7
+ // Get access token from cookies
8
+ const sid = useCookie("sid", cookieConfig).value;
9
+
10
+ if (!sid) {
11
+ // Redirect to login page if no access token
12
+ return navigateTo({ name: "index" });
13
+ }
14
+ });
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+
3
+ const hexSchema = z
4
+ .string()
5
+ .regex(/^[0-9a-fA-F]{24}$/, "Invalid organization ID");
6
+
7
+ export default defineNuxtRouteMiddleware((to) => {
8
+ const org = (to.params.org as string) ?? "";
9
+
10
+ if (org && !hexSchema.safeParse(org).success) {
11
+ return navigateTo({ name: "require-organization" }, { replace: true });
12
+ }
13
+ });
@@ -0,0 +1,76 @@
1
+ <template>
2
+ <v-row no-gutters class="fill-height" justify="center" align-content="center">
3
+ <v-col cols="12" class="mb-6">
4
+ <v-row no-gutters justify="center">
5
+ <nuxt-link
6
+ class="text-h2 font-weight-bold text-decoration-none"
7
+ style="color: unset"
8
+ :to="{ name: 'index' }"
9
+ >
10
+ GoWeekdays
11
+ </nuxt-link>
12
+ </v-row>
13
+ </v-col>
14
+
15
+ <v-col cols="12" lg="3" md="4" sm="6">
16
+ <v-form v-model="isValid" @submit.prevent="submit(email)">
17
+ <v-row no-gutters>
18
+ <v-col cols="12">
19
+ <v-row no-gutters>
20
+ <v-col cols="12" class="text-h6 font-weight-bold"> Email </v-col>
21
+ <v-col cols="12">
22
+ <v-text-field
23
+ v-model="email"
24
+ :rules="[requiredRule, emailRule]"
25
+ ></v-text-field>
26
+ </v-col>
27
+ </v-row>
28
+ </v-col>
29
+
30
+ <v-col v-if="message" cols="12" class="my-2 text-center font-italic">
31
+ {{ message }}
32
+ </v-col>
33
+
34
+ <v-col cols="12">
35
+ <v-row no-gutters justify="center">
36
+ <v-btn
37
+ type="submit"
38
+ variant="tonal"
39
+ :disabled="!isValid"
40
+ rounded="xl"
41
+ size="large"
42
+ >
43
+ submit
44
+ </v-btn>
45
+ </v-row>
46
+ </v-col>
47
+
48
+ <v-col cols="12" class="mt-2 text-center">
49
+ All good? <nuxt-link :to="{ name: 'login' }">Login</nuxt-link>
50
+ </v-col>
51
+ </v-row>
52
+ </v-form>
53
+ </v-col>
54
+ </v-row>
55
+ </template>
56
+
57
+ <script setup lang="ts">
58
+ definePageMeta({
59
+ layout: "plain",
60
+ });
61
+ const { requiredRule, emailRule } = useUtils();
62
+ const email = ref("");
63
+ const isValid = ref(false);
64
+
65
+ const { forgotPassword } = useLocalAuth();
66
+ const message = ref("");
67
+
68
+ async function submit(email = "") {
69
+ try {
70
+ const result: any = await forgotPassword(email);
71
+ message.value = result.message;
72
+ } catch (error: any) {
73
+ message.value = error.message;
74
+ }
75
+ }
76
+ </script>
@@ -0,0 +1,3 @@
1
+ <template>
2
+ <h1>Index</h1>
3
+ </template>