@koloseum/utils 0.1.4 → 0.1.6

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/dist/utils.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Database } from "@koloseum/types/database";
2
- import type { CustomError, SocialMediaPlatform } from "@koloseum/types/public/auth";
2
+ import type { CustomError, PronounsCheckboxes, SocialMediaPlatform } from "@koloseum/types/public/auth";
3
3
  import type { PostgrestError, SupabaseClient } from "@supabase/supabase-js";
4
4
  import type { MobilePhoneLocale } from "validator";
5
5
  export declare const Status: {
@@ -66,6 +66,12 @@ export declare const Utility: {
66
66
  * @returns The age in years, or `NaN` if the input is invalid
67
67
  */
68
68
  getAge: (dateOfBirth: string) => number;
69
+ /**
70
+ * Handles the click event for pronouns checkboxes.
71
+ * @param {MouseEvent} e - The click event
72
+ * @param {PronounsCheckboxes} checkboxes - The pronouns checkboxes
73
+ */
74
+ handlePronounsCheckboxClick: (e: MouseEvent, checkboxes: PronounsCheckboxes) => void;
69
75
  /**
70
76
  * A regular expression for Koloseum Lounge Branch IDs. It covers the following rules:
71
77
  * - 9 characters long
package/dist/utils.js CHANGED
@@ -18,7 +18,7 @@ export const Status = {
18
18
  /**
19
19
  * A generic password reset request message.
20
20
  */
21
- PASSWORD_RESET_REQUESTED: "If the provided email address is registered, you will receive a password reset link shortly.",
21
+ PASSWORD_RESET_REQUESTED: "If the provided email address is registered, you will receive a password reset link shortly."
22
22
  };
23
23
  /* Utility functions */
24
24
  export const Utility = {
@@ -59,7 +59,7 @@ export const Utility = {
59
59
  * @param {string} message - The error message
60
60
  * @returns An object with the error `code` and `message`
61
61
  */
62
- customError: (code, message) => (!code || code < 400 || code > 599 ? { code: 500, message: Status.ERROR } : { code, message }),
62
+ customError: (code, message) => !code || code < 400 || code > 599 ? { code: 500, message: Status.ERROR } : { code, message },
63
63
  /**
64
64
  * A regular expression for E.164 phone numbers.
65
65
  *
@@ -79,7 +79,7 @@ export const Utility = {
79
79
  const formattedDate = new Intl.DateTimeFormat(forClient ? "fr-CA" : "en-KE", {
80
80
  day: "2-digit",
81
81
  month: "2-digit",
82
- year: "numeric",
82
+ year: "numeric"
83
83
  // @ts-ignore: .fromString method exists but is not typed in `any-date-parser` package
84
84
  }).format(parser.fromString(date));
85
85
  return formattedDate;
@@ -120,6 +120,56 @@ export const Utility = {
120
120
  const monthDiff = currentDate.getMonth() - birthDate.getMonth();
121
121
  return monthDiff < 0 || (monthDiff === 0 && currentDate.getDate() < birthDate.getDate()) ? --age : age;
122
122
  },
123
+ /**
124
+ * Handles the click event for pronouns checkboxes.
125
+ * @param {MouseEvent} e - The click event
126
+ * @param {PronounsCheckboxes} checkboxes - The pronouns checkboxes
127
+ */
128
+ handlePronounsCheckboxClick: (e, checkboxes) => {
129
+ const target = e.target;
130
+ if (target.value === "none") {
131
+ for (let checkbox of Object.values(checkboxes))
132
+ if (checkbox && checkbox !== target && checkbox.checked) {
133
+ e.preventDefault();
134
+ return;
135
+ }
136
+ for (let checkbox of Object.values(checkboxes))
137
+ if (checkbox && checkbox !== target) {
138
+ checkbox.checked = false;
139
+ checkbox.disabled = !checkbox.disabled;
140
+ }
141
+ }
142
+ else {
143
+ if (checkboxes.none?.checked) {
144
+ e.preventDefault();
145
+ return;
146
+ }
147
+ if (target.value === "male" || target.value === "female") {
148
+ const oppositeIndex = target.value === "male" ? "female" : "male";
149
+ const oppositeCheckbox = checkboxes[oppositeIndex];
150
+ if (oppositeCheckbox?.checked) {
151
+ e.preventDefault();
152
+ return;
153
+ }
154
+ if (oppositeCheckbox) {
155
+ oppositeCheckbox.checked = false;
156
+ oppositeCheckbox.disabled = !oppositeCheckbox.disabled;
157
+ }
158
+ if (checkboxes.none) {
159
+ checkboxes.none.checked = false;
160
+ if (!checkboxes.neutral?.checked)
161
+ checkboxes.none.disabled = !checkboxes.none.disabled;
162
+ }
163
+ }
164
+ else {
165
+ if (checkboxes.none) {
166
+ checkboxes.none.checked = false;
167
+ if (checkboxes.neutral && !checkboxes.male?.checked && !checkboxes.female?.checked)
168
+ checkboxes.none.disabled = checkboxes.neutral.checked;
169
+ }
170
+ }
171
+ }
172
+ },
123
173
  /**
124
174
  * A regular expression for Koloseum Lounge Branch IDs. It covers the following rules:
125
175
  * - 9 characters long
@@ -148,7 +198,7 @@ export const Utility = {
148
198
  * @param {CustomError} error - The error object
149
199
  * @returns A new `Error` object if the error is unexpected, or a Svelte error otherwise
150
200
  */
151
- parseLoadError: (error) => (error.code === 500 ? new Error(error.message) : svelteError(error.code, { message: error.message })),
201
+ parseLoadError: (error) => error.code === 500 ? new Error(error.message) : svelteError(error.code, { message: error.message }),
152
202
  /**
153
203
  * Parses a `PostgrestError` object and returns a custom error object if any has occurred.
154
204
  * @param {PostgrestError | null} postgrestError - The `PostgrestError` object, or `null` if no error occurred
@@ -195,7 +245,7 @@ export const Utility = {
195
245
  { code: "42883", status: 404 },
196
246
  { code: "42P01", status: 404 },
197
247
  { code: "42P17", status: 500 },
198
- { code: "42501", status: 403 },
248
+ { code: "42501", status: 403 }
199
249
  ];
200
250
  // Return custom error if Postgrest error code is found
201
251
  for (const { code, status } of errorMap)
@@ -227,7 +277,7 @@ export const Utility = {
227
277
  * @param {string} input - The input to be sanitised
228
278
  * @returns A sanitised string, or an empty string if the input is invalid
229
279
  */
230
- sanitiseHtml: (input) => (typeof input !== "string" ? "" : sanitize(input, { allowedTags: [], allowedAttributes: {} })),
280
+ sanitiseHtml: (input) => typeof input !== "string" ? "" : sanitize(input, { allowedTags: [], allowedAttributes: {} }),
231
281
  /**
232
282
  * A regular expression for social media handles, without a leading slash or @ character.
233
283
  *
@@ -246,5 +296,7 @@ export const Utility = {
246
296
  * @param {string} handle - The social media handle to be validated
247
297
  * @returns `true` if the handle is valid; `false` otherwise
248
298
  */
249
- validateSocialMediaHandle: (handle) => !isURL(handle, { require_protocol: false }) && !handle.startsWith("@") && Boolean(handle.match(Utility.socialMediaHandleRegex)),
299
+ validateSocialMediaHandle: (handle) => !isURL(handle, { require_protocol: false }) &&
300
+ !handle.startsWith("@") &&
301
+ Boolean(handle.match(Utility.socialMediaHandleRegex))
250
302
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koloseum/utils",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "author": "Koloseum Technologies Limited",
5
5
  "type": "module",
6
6
  "description": "Utility logic for use across Koloseum web apps (TypeScript)",
@@ -24,7 +24,9 @@
24
24
  "./dist/**/*"
25
25
  ],
26
26
  "scripts": {
27
- "build": "npx tsc",
27
+ "build": "rm -rf dist && npx tsc",
28
+ "check": "npx tsc --noEmit && prettier --check .",
29
+ "format": "prettier --write .",
28
30
  "test": "vitest --run"
29
31
  },
30
32
  "dependencies": {
@@ -39,6 +41,7 @@
39
41
  "@playwright/test": "^1.50.1",
40
42
  "@types/sanitize-html": "^2.13.0",
41
43
  "@types/validator": "^13.12.2",
44
+ "prettier": "^3.5.1",
42
45
  "typescript": "^5.0.0",
43
46
  "vitest": "^3.0.5"
44
47
  }
package/src/utils.test.ts CHANGED
@@ -28,7 +28,7 @@ describe("Utility helper", () => {
28
28
  parsePostgrestError,
29
29
  sanitiseHtml,
30
30
  validateE164,
31
- validateSocialMediaHandle,
31
+ validateSocialMediaHandle
32
32
  } = Utility;
33
33
 
34
34
  // Test each helper
@@ -103,7 +103,7 @@ describe("Utility helper", () => {
103
103
  name: "insufficient_privilege",
104
104
  message: 'Insufficient privileges: permission denied for relation "restricted_table"',
105
105
  details: 'User does not have the necessary permissions to access the relation "restricted_table".',
106
- hint: "",
106
+ hint: ""
107
107
  })
108
108
  ).toEqual({ error: { code: 403, message: Status.ERROR } });
109
109
  expect(
@@ -112,7 +112,7 @@ describe("Utility helper", () => {
112
112
  name: "undefined_function",
113
113
  message: "Undefined function: function non_existent_function() does not exist",
114
114
  details: "Function non_existent_function() does not exist.",
115
- hint: "No function matches the given name and argument types. You might need to add explicit type casts.",
115
+ hint: "No function matches the given name and argument types. You might need to add explicit type casts."
116
116
  })
117
117
  ).toEqual({ error: { code: 404, message: Status.ERROR } });
118
118
  expect(
@@ -121,7 +121,7 @@ describe("Utility helper", () => {
121
121
  name: "read_only_sql_transaction",
122
122
  message: "Read only SQL transaction: cannot execute INSERT in a read-only transaction",
123
123
  details: "Cannot execute INSERT in a read-only transaction.",
124
- hint: "",
124
+ hint: ""
125
125
  })
126
126
  ).toEqual({ error: { code: 405, message: Status.ERROR } });
127
127
  expect(
@@ -130,7 +130,7 @@ describe("Utility helper", () => {
130
130
  name: "unique_violation",
131
131
  message: 'Uniqueness violation: duplicate key value violates unique constraint "users_username_key"',
132
132
  details: "Key (username)=(john_doe) already exists.",
133
- hint: "",
133
+ hint: ""
134
134
  })
135
135
  ).toEqual({ error: { code: 409, message: Status.ERROR } });
136
136
  expect(
@@ -139,7 +139,7 @@ describe("Utility helper", () => {
139
139
  name: "configuration_file_error",
140
140
  message: "invalid page header in block 0 of relation base/12345/67890",
141
141
  details: "The page header is corrupted. This may indicate hardware or file system issues.",
142
- hint: "",
142
+ hint: ""
143
143
  })
144
144
  ).toEqual({ error: { code: 500, message: Status.ERROR } });
145
145
  expect(
@@ -148,7 +148,7 @@ describe("Utility helper", () => {
148
148
  name: "connection_does_not_exist",
149
149
  message: "connection does not exist",
150
150
  details: "The connection to the database was lost or never established.",
151
- hint: "",
151
+ hint: ""
152
152
  })
153
153
  ).toEqual({ error: { code: 503, message: Status.ERROR } });
154
154
  expect(
@@ -157,7 +157,7 @@ describe("Utility helper", () => {
157
157
  name: "raise_exception",
158
158
  message: "I'm a teapot!",
159
159
  details: "Pretty simple",
160
- hint: "418",
160
+ hint: "418"
161
161
  })
162
162
  ).toEqual({ error: { code: 418, message: "I'm a teapot!" } });
163
163
  });
package/src/utils.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Database } from "@koloseum/types/database";
2
- import type { CustomError, SocialMediaPlatform } from "@koloseum/types/public/auth";
2
+ import type { CustomError, PronounsCheckboxes, SocialMediaPlatform } from "@koloseum/types/public/auth";
3
3
 
4
4
  import type { PostgrestError, SupabaseClient } from "@supabase/supabase-js";
5
5
  import type { MobilePhoneLocale } from "validator";
@@ -27,7 +27,7 @@ export const Status = {
27
27
  /**
28
28
  * A generic password reset request message.
29
29
  */
30
- PASSWORD_RESET_REQUESTED: "If the provided email address is registered, you will receive a password reset link shortly.",
30
+ PASSWORD_RESET_REQUESTED: "If the provided email address is registered, you will receive a password reset link shortly."
31
31
  };
32
32
 
33
33
  /* Utility functions */
@@ -39,12 +39,16 @@ export const Utility = {
39
39
  * @param {"GET" | "POST"} [method="GET"] - The HTTP method to use
40
40
  * @returns The response from the Edge function
41
41
  */
42
- callEdgeFunction: async (supabase: SupabaseClient<Database>, path: string, method: "GET" | "POST" = "GET"): Promise<{ data?: any; error?: string }> => {
42
+ callEdgeFunction: async (
43
+ supabase: SupabaseClient<Database>,
44
+ path: string,
45
+ method: "GET" | "POST" = "GET"
46
+ ): Promise<{ data?: any; error?: string }> => {
43
47
  // Fetch response
44
48
  const { data, error } = await supabase.functions.invoke(path, { method });
45
49
 
46
50
  // Return error if any
47
- if (error instanceof FunctionsHttpError) return { error: await error.context.json() };
51
+ if (error instanceof FunctionsHttpError) return { error: (await error.context.json()).message };
48
52
  else if (error instanceof FunctionsRelayError) return { error: error.message };
49
53
  else if (error instanceof FunctionsFetchError) return { error: error.message };
50
54
 
@@ -69,7 +73,8 @@ export const Utility = {
69
73
  * @param {string} message - The error message
70
74
  * @returns An object with the error `code` and `message`
71
75
  */
72
- customError: (code: number | undefined, message: string): CustomError => (!code || code < 400 || code > 599 ? { code: 500, message: Status.ERROR } : { code, message }),
76
+ customError: (code: number | undefined, message: string): CustomError =>
77
+ !code || code < 400 || code > 599 ? { code: 500, message: Status.ERROR } : { code, message },
73
78
  /**
74
79
  * A regular expression for E.164 phone numbers.
75
80
  *
@@ -88,7 +93,7 @@ export const Utility = {
88
93
  const formattedDate = new Intl.DateTimeFormat(forClient ? "fr-CA" : "en-KE", {
89
94
  day: "2-digit",
90
95
  month: "2-digit",
91
- year: "numeric",
96
+ year: "numeric"
92
97
  // @ts-ignore: .fromString method exists but is not typed in `any-date-parser` package
93
98
  }).format(parser.fromString(date));
94
99
 
@@ -129,6 +134,59 @@ export const Utility = {
129
134
 
130
135
  return monthDiff < 0 || (monthDiff === 0 && currentDate.getDate() < birthDate.getDate()) ? --age : age;
131
136
  },
137
+ /**
138
+ * Handles the click event for pronouns checkboxes.
139
+ * @param {MouseEvent} e - The click event
140
+ * @param {PronounsCheckboxes} checkboxes - The pronouns checkboxes
141
+ */
142
+ handlePronounsCheckboxClick: (e: MouseEvent, checkboxes: PronounsCheckboxes) => {
143
+ const target = e.target as EventTarget & HTMLInputElement;
144
+
145
+ if (target.value === "none") {
146
+ for (let checkbox of Object.values(checkboxes))
147
+ if (checkbox && checkbox !== target && checkbox.checked) {
148
+ e.preventDefault();
149
+ return;
150
+ }
151
+
152
+ for (let checkbox of Object.values(checkboxes))
153
+ if (checkbox && checkbox !== target) {
154
+ checkbox.checked = false;
155
+ checkbox.disabled = !checkbox.disabled;
156
+ }
157
+ } else {
158
+ if (checkboxes.none?.checked) {
159
+ e.preventDefault();
160
+ return;
161
+ }
162
+
163
+ if (target.value === "male" || target.value === "female") {
164
+ const oppositeIndex = target.value === "male" ? "female" : "male";
165
+ const oppositeCheckbox = checkboxes[oppositeIndex];
166
+
167
+ if (oppositeCheckbox?.checked) {
168
+ e.preventDefault();
169
+ return;
170
+ }
171
+
172
+ if (oppositeCheckbox) {
173
+ oppositeCheckbox.checked = false;
174
+ oppositeCheckbox.disabled = !oppositeCheckbox.disabled;
175
+ }
176
+
177
+ if (checkboxes.none) {
178
+ checkboxes.none.checked = false;
179
+ if (!checkboxes.neutral?.checked) checkboxes.none.disabled = !checkboxes.none.disabled;
180
+ }
181
+ } else {
182
+ if (checkboxes.none) {
183
+ checkboxes.none.checked = false;
184
+ if (checkboxes.neutral && !checkboxes.male?.checked && !checkboxes.female?.checked)
185
+ checkboxes.none.disabled = checkboxes.neutral.checked;
186
+ }
187
+ }
188
+ }
189
+ },
132
190
  /**
133
191
  * A regular expression for Koloseum Lounge Branch IDs. It covers the following rules:
134
192
  * - 9 characters long
@@ -159,7 +217,8 @@ export const Utility = {
159
217
  * @param {CustomError} error - The error object
160
218
  * @returns A new `Error` object if the error is unexpected, or a Svelte error otherwise
161
219
  */
162
- parseLoadError: (error: CustomError) => (error.code === 500 ? new Error(error.message) : svelteError(error.code, { message: error.message })),
220
+ parseLoadError: (error: CustomError) =>
221
+ error.code === 500 ? new Error(error.message) : svelteError(error.code, { message: error.message }),
163
222
  /**
164
223
  * Parses a `PostgrestError` object and returns a custom error object if any has occurred.
165
224
  * @param {PostgrestError | null} postgrestError - The `PostgrestError` object, or `null` if no error occurred
@@ -207,7 +266,7 @@ export const Utility = {
207
266
  { code: "42883", status: 404 },
208
267
  { code: "42P01", status: 404 },
209
268
  { code: "42P17", status: 500 },
210
- { code: "42501", status: 403 },
269
+ { code: "42501", status: 403 }
211
270
  ];
212
271
 
213
272
  // Return custom error if Postgrest error code is found
@@ -241,7 +300,8 @@ export const Utility = {
241
300
  * @param {string} input - The input to be sanitised
242
301
  * @returns A sanitised string, or an empty string if the input is invalid
243
302
  */
244
- sanitiseHtml: (input: string) => (typeof input !== "string" ? "" : sanitize(input, { allowedTags: [], allowedAttributes: {} })),
303
+ sanitiseHtml: (input: string) =>
304
+ typeof input !== "string" ? "" : sanitize(input, { allowedTags: [], allowedAttributes: {} }),
245
305
  /**
246
306
  * A regular expression for social media handles, without a leading slash or @ character.
247
307
  *
@@ -254,11 +314,15 @@ export const Utility = {
254
314
  * @param {"any" | MobilePhoneLocale | MobilePhoneLocale[]} [locale="any"] - The locale(s) to validate the phone number against
255
315
  * @returns `true` if the phone number is valid; `false` otherwise
256
316
  */
257
- validateE164: (phoneNumber: string, locale: "any" | MobilePhoneLocale | MobilePhoneLocale[] = "en-KE") => isMobilePhone(phoneNumber, locale) && Boolean(phoneNumber.match(Utility.e164Regex)),
317
+ validateE164: (phoneNumber: string, locale: "any" | MobilePhoneLocale | MobilePhoneLocale[] = "en-KE") =>
318
+ isMobilePhone(phoneNumber, locale) && Boolean(phoneNumber.match(Utility.e164Regex)),
258
319
  /**
259
320
  * Validates a social media handle.
260
321
  * @param {string} handle - The social media handle to be validated
261
322
  * @returns `true` if the handle is valid; `false` otherwise
262
323
  */
263
- validateSocialMediaHandle: (handle: string) => !isURL(handle, { require_protocol: false }) && !handle.startsWith("@") && Boolean(handle.match(Utility.socialMediaHandleRegex)),
324
+ validateSocialMediaHandle: (handle: string) =>
325
+ !isURL(handle, { require_protocol: false }) &&
326
+ !handle.startsWith("@") &&
327
+ Boolean(handle.match(Utility.socialMediaHandleRegex))
264
328
  };