@koloseum/utils 0.1.3 → 0.1.5

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.
@@ -0,0 +1,145 @@
1
+ import type { Database } from "@koloseum/types/database";
2
+ import type { CustomError, PronounsCheckboxes, SocialMediaPlatform } from "@koloseum/types/public/auth";
3
+ import type { PostgrestError, SupabaseClient } from "@supabase/supabase-js";
4
+ import type { MobilePhoneLocale } from "validator";
5
+ export declare const Status: {
6
+ /**
7
+ * A generic error message.
8
+ */
9
+ ERROR: string;
10
+ /**
11
+ * A generic loading message.
12
+ */
13
+ LOADING: string;
14
+ /**
15
+ * A generic password reset request message.
16
+ */
17
+ PASSWORD_RESET_REQUESTED: string;
18
+ };
19
+ export declare const Utility: {
20
+ /**
21
+ * Calls an Edge function.
22
+ * @param {SupabaseClient} supabase - The Supabase client
23
+ * @param {string} path - The path to the Edge function
24
+ * @param {"GET" | "POST"} [method="GET"] - The HTTP method to use
25
+ * @returns The response from the Edge function
26
+ */
27
+ callEdgeFunction: (supabase: SupabaseClient<Database>, path: string, method?: "GET" | "POST") => Promise<{
28
+ data?: any;
29
+ error?: string;
30
+ }>;
31
+ /**
32
+ * Capitalises a given word.
33
+ * @param {string} word - The word to be capitalised
34
+ * @returns The capitalised word
35
+ */
36
+ capitalise: (word: string) => string;
37
+ /**
38
+ * Returns a custom error object.
39
+ * @param {number} code - The error code; defaults to `500` if not provided or invalid
40
+ * @param {string} message - The error message
41
+ * @returns An object with the error `code` and `message`
42
+ */
43
+ customError: (code: number | undefined, message: string) => CustomError;
44
+ /**
45
+ * A regular expression for E.164 phone numbers.
46
+ *
47
+ * - Source: https://www.twilio.com/docs/glossary/what-e164
48
+ */
49
+ e164Regex: RegExp;
50
+ /**
51
+ * Formats a date in the format DD/MM/YYYY (by default) or YYYY-MM-DD (for client).
52
+ * @param {string} date - Date to be formatted
53
+ * @param {boolean} forClient - Specify whether the data is for displaying on the client; defaults to `false`
54
+ * @returns The formatted date string, or `null` if invalid input or in case of an error
55
+ */
56
+ formatDate: (date: string, forClient?: boolean) => string | null;
57
+ /**
58
+ * Formats a social media platform for display.
59
+ * @param {SocialMediaPlatform} platform - The social media platform to be formatted
60
+ * @returns The formatted social media platform string
61
+ */
62
+ formatSocialMediaPlatform: (platform: SocialMediaPlatform) => string;
63
+ /**
64
+ * Returns an age in years given a birth date.
65
+ * @param {string} dateOfBirth - The date of birth as a string, e.g. `DD-MM-YYYY`
66
+ * @returns The age in years, or `NaN` if the input is invalid
67
+ */
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;
75
+ /**
76
+ * A regular expression for Koloseum Lounge Branch IDs. It covers the following rules:
77
+ * - 9 characters long
78
+ * - begins with "KLB" followed by 7 digits
79
+ */
80
+ loungeBranchIdRegex: RegExp;
81
+ /**
82
+ * A regular expression for Koloseum Lounge IDs. It covers the following rules:
83
+ * - 9 characters long
84
+ * - begins with "KL" followed by 7 digits
85
+ */
86
+ loungeIdRegex: RegExp;
87
+ /**
88
+ * The minimum birth date (from today's date) for a user who is a minor, i.e. `YYYY-MM-DD`
89
+ */
90
+ minimumMinorBirthDate: string;
91
+ /**
92
+ * Parses a `CustomError` object within a page/layout server load function and returns an error to be thrown.
93
+ * @param {CustomError} error - The error object
94
+ * @returns A new `Error` object if the error is unexpected, or a Svelte error otherwise
95
+ */
96
+ parseLoadError: (error: CustomError) => Error;
97
+ /**
98
+ * Parses a `PostgrestError` object and returns a custom error object if any has occurred.
99
+ * @param {PostgrestError | null} postgrestError - The `PostgrestError` object, or `null` if no error occurred
100
+ * @returns An object with an `error` if any has occurred
101
+ */
102
+ parsePostgrestError: (postgrestError: PostgrestError | null) => {
103
+ error?: CustomError;
104
+ };
105
+ /**
106
+ * A regular expression for user passwords to match during authentication. It covers the following rules:
107
+ * - at least 8 characters long
108
+ * - at least one digit
109
+ * - at least one lowercase letter
110
+ * - at least one uppercase letter
111
+ * - at least one symbol
112
+ */
113
+ passwordRegex: RegExp;
114
+ /**
115
+ * A regular expression for Koloseum Player IDs. It covers the following rules:
116
+ * - 9 characters long
117
+ * - begins with "KP" followed by 7 digits
118
+ */
119
+ playerIdRegex: RegExp;
120
+ /**
121
+ * Sanitises any potential HTML injected into user input.
122
+ * @param {string} input - The input to be sanitised
123
+ * @returns A sanitised string, or an empty string if the input is invalid
124
+ */
125
+ sanitiseHtml: (input: string) => string;
126
+ /**
127
+ * A regular expression for social media handles, without a leading slash or @ character.
128
+ *
129
+ * - Source: https://stackoverflow.com/a/74579651
130
+ */
131
+ socialMediaHandleRegex: RegExp;
132
+ /**
133
+ * Validates an E.164 phone number.
134
+ * @param {string} phoneNumber - The phone number to be validated
135
+ * @param {"any" | MobilePhoneLocale | MobilePhoneLocale[]} [locale="any"] - The locale(s) to validate the phone number against
136
+ * @returns `true` if the phone number is valid; `false` otherwise
137
+ */
138
+ validateE164: (phoneNumber: string, locale?: "any" | MobilePhoneLocale | MobilePhoneLocale[]) => boolean;
139
+ /**
140
+ * Validates a social media handle.
141
+ * @param {string} handle - The social media handle to be validated
142
+ * @returns `true` if the handle is valid; `false` otherwise
143
+ */
144
+ validateSocialMediaHandle: (handle: string) => boolean;
145
+ };
package/dist/utils.js ADDED
@@ -0,0 +1,302 @@
1
+ import { FunctionsFetchError, FunctionsHttpError, FunctionsRelayError } from "@supabase/supabase-js";
2
+ import { error as svelteError } from "@sveltejs/kit";
3
+ import parser from "any-date-parser";
4
+ import sanitize from "sanitize-html";
5
+ import validator from "validator";
6
+ /* Helper functions */
7
+ const { isMobilePhone, isURL } = validator;
8
+ /* Status messages */
9
+ export const Status = {
10
+ /**
11
+ * A generic error message.
12
+ */
13
+ ERROR: "An unexpected error occurred. Kindly try again.",
14
+ /**
15
+ * A generic loading message.
16
+ */
17
+ LOADING: "Please wait…",
18
+ /**
19
+ * A generic password reset request message.
20
+ */
21
+ PASSWORD_RESET_REQUESTED: "If the provided email address is registered, you will receive a password reset link shortly."
22
+ };
23
+ /* Utility functions */
24
+ export const Utility = {
25
+ /**
26
+ * Calls an Edge function.
27
+ * @param {SupabaseClient} supabase - The Supabase client
28
+ * @param {string} path - The path to the Edge function
29
+ * @param {"GET" | "POST"} [method="GET"] - The HTTP method to use
30
+ * @returns The response from the Edge function
31
+ */
32
+ callEdgeFunction: async (supabase, path, method = "GET") => {
33
+ // Fetch response
34
+ const { data, error } = await supabase.functions.invoke(path, { method });
35
+ // Return error if any
36
+ if (error instanceof FunctionsHttpError)
37
+ return { error: await error.context.json() };
38
+ else if (error instanceof FunctionsRelayError)
39
+ return { error: error.message };
40
+ else if (error instanceof FunctionsFetchError)
41
+ return { error: error.message };
42
+ // Return response
43
+ return { data };
44
+ },
45
+ /**
46
+ * Capitalises a given word.
47
+ * @param {string} word - The word to be capitalised
48
+ * @returns The capitalised word
49
+ */
50
+ capitalise: (word) => word.charAt(0).toUpperCase() +
51
+ word
52
+ .slice(1)
53
+ .split("")
54
+ .map((c) => c.toLowerCase())
55
+ .join(""),
56
+ /**
57
+ * Returns a custom error object.
58
+ * @param {number} code - The error code; defaults to `500` if not provided or invalid
59
+ * @param {string} message - The error message
60
+ * @returns An object with the error `code` and `message`
61
+ */
62
+ customError: (code, message) => !code || code < 400 || code > 599 ? { code: 500, message: Status.ERROR } : { code, message },
63
+ /**
64
+ * A regular expression for E.164 phone numbers.
65
+ *
66
+ * - Source: https://www.twilio.com/docs/glossary/what-e164
67
+ */
68
+ e164Regex: /^\+?[1-9]\d{1,14}$/,
69
+ /**
70
+ * Formats a date in the format DD/MM/YYYY (by default) or YYYY-MM-DD (for client).
71
+ * @param {string} date - Date to be formatted
72
+ * @param {boolean} forClient - Specify whether the data is for displaying on the client; defaults to `false`
73
+ * @returns The formatted date string, or `null` if invalid input or in case of an error
74
+ */
75
+ formatDate: (date, forClient = false) => {
76
+ if (date === "")
77
+ return null;
78
+ try {
79
+ const formattedDate = new Intl.DateTimeFormat(forClient ? "fr-CA" : "en-KE", {
80
+ day: "2-digit",
81
+ month: "2-digit",
82
+ year: "numeric"
83
+ // @ts-ignore: .fromString method exists but is not typed in `any-date-parser` package
84
+ }).format(parser.fromString(date));
85
+ return formattedDate;
86
+ }
87
+ catch (error) {
88
+ return null;
89
+ }
90
+ },
91
+ /**
92
+ * Formats a social media platform for display.
93
+ * @param {SocialMediaPlatform} platform - The social media platform to be formatted
94
+ * @returns The formatted social media platform string
95
+ */
96
+ formatSocialMediaPlatform: (platform) => {
97
+ const platforms = ["facebook", "instagram", "kick", "tiktok", "twitch", "twitter", "youtube"];
98
+ if (!platforms.includes(platform))
99
+ return "";
100
+ if (platform === "tiktok")
101
+ return "TikTok";
102
+ if (platform === "twitter")
103
+ return "X (formerly Twitter)";
104
+ if (platform === "youtube")
105
+ return "YouTube";
106
+ return Utility.capitalise(platform);
107
+ },
108
+ /**
109
+ * Returns an age in years given a birth date.
110
+ * @param {string} dateOfBirth - The date of birth as a string, e.g. `DD-MM-YYYY`
111
+ * @returns The age in years, or `NaN` if the input is invalid
112
+ */
113
+ getAge: (dateOfBirth) => {
114
+ if (dateOfBirth === "" || typeof dateOfBirth !== "string")
115
+ return NaN;
116
+ dateOfBirth = Utility.formatDate(dateOfBirth, true);
117
+ const birthDate = new Date(dateOfBirth);
118
+ const currentDate = new Date();
119
+ let age = currentDate.getFullYear() - birthDate.getFullYear();
120
+ const monthDiff = currentDate.getMonth() - birthDate.getMonth();
121
+ return monthDiff < 0 || (monthDiff === 0 && currentDate.getDate() < birthDate.getDate()) ? --age : age;
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
+ },
173
+ /**
174
+ * A regular expression for Koloseum Lounge Branch IDs. It covers the following rules:
175
+ * - 9 characters long
176
+ * - begins with "KLB" followed by 7 digits
177
+ */
178
+ loungeBranchIdRegex: /^KLB\d{7}$/,
179
+ /**
180
+ * A regular expression for Koloseum Lounge IDs. It covers the following rules:
181
+ * - 9 characters long
182
+ * - begins with "KL" followed by 7 digits
183
+ */
184
+ loungeIdRegex: /^KL\d{7}$/,
185
+ /**
186
+ * The minimum birth date (from today's date) for a user who is a minor, i.e. `YYYY-MM-DD`
187
+ */
188
+ minimumMinorBirthDate: (() => {
189
+ const today = new Date();
190
+ const tomorrow = new Date();
191
+ tomorrow.setUTCDate(today.getDate() + 1);
192
+ const tomorrow18YearsAgo = tomorrow.getFullYear() - 18;
193
+ tomorrow.setFullYear(tomorrow18YearsAgo);
194
+ return tomorrow.toISOString().split("T")[0];
195
+ })(),
196
+ /**
197
+ * Parses a `CustomError` object within a page/layout server load function and returns an error to be thrown.
198
+ * @param {CustomError} error - The error object
199
+ * @returns A new `Error` object if the error is unexpected, or a Svelte error otherwise
200
+ */
201
+ parseLoadError: (error) => error.code === 500 ? new Error(error.message) : svelteError(error.code, { message: error.message }),
202
+ /**
203
+ * Parses a `PostgrestError` object and returns a custom error object if any has occurred.
204
+ * @param {PostgrestError | null} postgrestError - The `PostgrestError` object, or `null` if no error occurred
205
+ * @returns An object with an `error` if any has occurred
206
+ */
207
+ parsePostgrestError: (postgrestError) => {
208
+ // Return undefined if no error occurred
209
+ let error;
210
+ if (!postgrestError)
211
+ return { error };
212
+ // Return custom error if hint is a number between 400 and 599
213
+ const customErrorCode = Number(postgrestError.hint);
214
+ if (!isNaN(customErrorCode) && customErrorCode >= 400 && customErrorCode <= 599) {
215
+ error = Utility.customError(customErrorCode, postgrestError.message);
216
+ return { error };
217
+ }
218
+ // Map Postgrest error codes to custom error codes
219
+ const errorMap = [
220
+ { code: "08", status: 503 },
221
+ { code: "09", status: 500 },
222
+ { code: "0L", status: 403 },
223
+ { code: "0P", status: 403 },
224
+ { code: "23503", status: 409 },
225
+ { code: "23505", status: 409 },
226
+ { code: "25006", status: 405 },
227
+ { code: "25", status: 500 },
228
+ { code: "28", status: 403 },
229
+ { code: "2D", status: 500 },
230
+ { code: "38", status: 500 },
231
+ { code: "39", status: 500 },
232
+ { code: "3B", status: 500 },
233
+ { code: "40", status: 500 },
234
+ { code: "53400", status: 500 },
235
+ { code: "53", status: 503 },
236
+ { code: "54", status: 500 },
237
+ { code: "55", status: 500 },
238
+ { code: "57", status: 500 },
239
+ { code: "58", status: 500 },
240
+ { code: "F0", status: 500 },
241
+ { code: "HV", status: 500 },
242
+ { code: "P0001", status: 400 },
243
+ { code: "P0", status: 500 },
244
+ { code: "XX", status: 500 },
245
+ { code: "42883", status: 404 },
246
+ { code: "42P01", status: 404 },
247
+ { code: "42P17", status: 500 },
248
+ { code: "42501", status: 403 }
249
+ ];
250
+ // Return custom error if Postgrest error code is found
251
+ for (const { code, status } of errorMap)
252
+ if (postgrestError.code === code || postgrestError.code.startsWith(code)) {
253
+ error = Utility.customError(status, Status.ERROR);
254
+ return { error };
255
+ }
256
+ // Return generic error
257
+ error = Utility.customError(500, Status.ERROR);
258
+ return { error };
259
+ },
260
+ /**
261
+ * A regular expression for user passwords to match during authentication. It covers the following rules:
262
+ * - at least 8 characters long
263
+ * - at least one digit
264
+ * - at least one lowercase letter
265
+ * - at least one uppercase letter
266
+ * - at least one symbol
267
+ */
268
+ passwordRegex: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()-_+=|{}[\]:;'<>,.?/~]).{8,}$/,
269
+ /**
270
+ * A regular expression for Koloseum Player IDs. It covers the following rules:
271
+ * - 9 characters long
272
+ * - begins with "KP" followed by 7 digits
273
+ */
274
+ playerIdRegex: /^KP\d{7}$/,
275
+ /**
276
+ * Sanitises any potential HTML injected into user input.
277
+ * @param {string} input - The input to be sanitised
278
+ * @returns A sanitised string, or an empty string if the input is invalid
279
+ */
280
+ sanitiseHtml: (input) => typeof input !== "string" ? "" : sanitize(input, { allowedTags: [], allowedAttributes: {} }),
281
+ /**
282
+ * A regular expression for social media handles, without a leading slash or @ character.
283
+ *
284
+ * - Source: https://stackoverflow.com/a/74579651
285
+ */
286
+ socialMediaHandleRegex: /^([A-Za-z0-9_.]{3,25})$/gm,
287
+ /**
288
+ * Validates an E.164 phone number.
289
+ * @param {string} phoneNumber - The phone number to be validated
290
+ * @param {"any" | MobilePhoneLocale | MobilePhoneLocale[]} [locale="any"] - The locale(s) to validate the phone number against
291
+ * @returns `true` if the phone number is valid; `false` otherwise
292
+ */
293
+ validateE164: (phoneNumber, locale = "en-KE") => isMobilePhone(phoneNumber, locale) && Boolean(phoneNumber.match(Utility.e164Regex)),
294
+ /**
295
+ * Validates a social media handle.
296
+ * @param {string} handle - The social media handle to be validated
297
+ * @returns `true` if the handle is valid; `false` otherwise
298
+ */
299
+ validateSocialMediaHandle: (handle) => !isURL(handle, { require_protocol: false }) &&
300
+ !handle.startsWith("@") &&
301
+ Boolean(handle.match(Utility.socialMediaHandleRegex))
302
+ };
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@koloseum/utils",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "author": "Koloseum Technologies Limited",
5
+ "type": "module",
5
6
  "description": "Utility logic for use across Koloseum web apps (TypeScript)",
6
7
  "keywords": [
7
8
  "Koloseum"
@@ -14,14 +15,18 @@
14
15
  "type": "git",
15
16
  "url": "git+https://github.com/koloseum-technologies/npm-utils.git"
16
17
  },
17
- "main": "./src/utils.ts",
18
+ "main": "./dist/utils.js",
18
19
  "exports": {
19
- "./src": "./src/utils.ts"
20
+ "./src": "./dist/utils.js"
20
21
  },
21
22
  "files": [
22
- "./src/**/*.ts"
23
+ "./src/**/*.ts",
24
+ "./dist/**/*"
23
25
  ],
24
26
  "scripts": {
27
+ "build": "rm -rf dist && npx tsc",
28
+ "check": "npx tsc --noEmit && prettier --check .",
29
+ "format": "prettier --write .",
25
30
  "test": "vitest --run"
26
31
  },
27
32
  "dependencies": {
@@ -36,6 +41,7 @@
36
41
  "@playwright/test": "^1.50.1",
37
42
  "@types/sanitize-html": "^2.13.0",
38
43
  "@types/validator": "^13.12.2",
44
+ "prettier": "^3.5.1",
39
45
  "typescript": "^5.0.0",
40
46
  "vitest": "^3.0.5"
41
47
  }
package/src/utils.test.ts CHANGED
@@ -99,8 +99,8 @@ describe("Utility helper", () => {
99
99
  expect(parsePostgrestError(null)).toEqual({ error: undefined });
100
100
  expect(
101
101
  parsePostgrestError({
102
- code: "42501",
103
- name: "insufficient_privilege",
102
+ code: "42501",
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
106
  hint: ""
@@ -108,8 +108,8 @@ describe("Utility helper", () => {
108
108
  ).toEqual({ error: { code: 403, message: Status.ERROR } });
109
109
  expect(
110
110
  parsePostgrestError({
111
- code: "42883",
112
- name: "undefined_function",
111
+ code: "42883",
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
115
  hint: "No function matches the given name and argument types. You might need to add explicit type casts."
@@ -117,8 +117,8 @@ describe("Utility helper", () => {
117
117
  ).toEqual({ error: { code: 404, message: Status.ERROR } });
118
118
  expect(
119
119
  parsePostgrestError({
120
- code: "25006",
121
- name: "read_only_sql_transaction",
120
+ code: "25006",
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
124
  hint: ""
@@ -126,8 +126,8 @@ describe("Utility helper", () => {
126
126
  ).toEqual({ error: { code: 405, message: Status.ERROR } });
127
127
  expect(
128
128
  parsePostgrestError({
129
- code: "23505",
130
- name: "unique_violation",
129
+ code: "23505",
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
133
  hint: ""
@@ -135,8 +135,8 @@ describe("Utility helper", () => {
135
135
  ).toEqual({ error: { code: 409, message: Status.ERROR } });
136
136
  expect(
137
137
  parsePostgrestError({
138
- code: "53400",
139
- name: "configuration_file_error",
138
+ code: "53400",
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
142
  hint: ""
@@ -144,8 +144,8 @@ describe("Utility helper", () => {
144
144
  ).toEqual({ error: { code: 500, message: Status.ERROR } });
145
145
  expect(
146
146
  parsePostgrestError({
147
- code: "08003",
148
- name: "connection_does_not_exist",
147
+ code: "08003",
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
151
  hint: ""
@@ -153,8 +153,8 @@ describe("Utility helper", () => {
153
153
  ).toEqual({ error: { code: 503, message: Status.ERROR } });
154
154
  expect(
155
155
  parsePostgrestError({
156
- code: "P0001",
157
- name: "raise_exception",
156
+ code: "P0001",
157
+ name: "raise_exception",
158
158
  message: "I'm a teapot!",
159
159
  details: "Pretty simple",
160
160
  hint: "418"
package/src/utils.ts CHANGED
@@ -1,11 +1,9 @@
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
- import type { Page, Locator } from "@playwright/test";
5
4
  import type { PostgrestError, SupabaseClient } from "@supabase/supabase-js";
6
5
  import type { MobilePhoneLocale } from "validator";
7
6
 
8
- import { expect } from "@playwright/test";
9
7
  import { FunctionsFetchError, FunctionsHttpError, FunctionsRelayError } from "@supabase/supabase-js";
10
8
  import { error as svelteError } from "@sveltejs/kit";
11
9
 
@@ -29,7 +27,7 @@ export const Status = {
29
27
  /**
30
28
  * A generic password reset request message.
31
29
  */
32
- 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."
33
31
  };
34
32
 
35
33
  /* Utility functions */
@@ -41,7 +39,11 @@ export const Utility = {
41
39
  * @param {"GET" | "POST"} [method="GET"] - The HTTP method to use
42
40
  * @returns The response from the Edge function
43
41
  */
44
- 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 }> => {
45
47
  // Fetch response
46
48
  const { data, error } = await supabase.functions.invoke(path, { method });
47
49
 
@@ -71,7 +73,8 @@ export const Utility = {
71
73
  * @param {string} message - The error message
72
74
  * @returns An object with the error `code` and `message`
73
75
  */
74
- 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 },
75
78
  /**
76
79
  * A regular expression for E.164 phone numbers.
77
80
  *
@@ -90,9 +93,9 @@ export const Utility = {
90
93
  const formattedDate = new Intl.DateTimeFormat(forClient ? "fr-CA" : "en-KE", {
91
94
  day: "2-digit",
92
95
  month: "2-digit",
93
- year: "numeric",
94
- // @ts-ignore: .fromString method exists but is not typed in `any-date-parser` package
95
- }).format(parser.fromString(date));
96
+ year: "numeric"
97
+ // @ts-ignore: .fromString method exists but is not typed in `any-date-parser` package
98
+ }).format(parser.fromString(date));
96
99
 
97
100
  return formattedDate;
98
101
  } catch (error) {
@@ -131,6 +134,59 @@ export const Utility = {
131
134
 
132
135
  return monthDiff < 0 || (monthDiff === 0 && currentDate.getDate() < birthDate.getDate()) ? --age : age;
133
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
+ },
134
190
  /**
135
191
  * A regular expression for Koloseum Lounge Branch IDs. It covers the following rules:
136
192
  * - 9 characters long
@@ -161,7 +217,8 @@ export const Utility = {
161
217
  * @param {CustomError} error - The error object
162
218
  * @returns A new `Error` object if the error is unexpected, or a Svelte error otherwise
163
219
  */
164
- 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 }),
165
222
  /**
166
223
  * Parses a `PostgrestError` object and returns a custom error object if any has occurred.
167
224
  * @param {PostgrestError | null} postgrestError - The `PostgrestError` object, or `null` if no error occurred
@@ -209,7 +266,7 @@ export const Utility = {
209
266
  { code: "42883", status: 404 },
210
267
  { code: "42P01", status: 404 },
211
268
  { code: "42P17", status: 500 },
212
- { code: "42501", status: 403 },
269
+ { code: "42501", status: 403 }
213
270
  ];
214
271
 
215
272
  // Return custom error if Postgrest error code is found
@@ -243,7 +300,8 @@ export const Utility = {
243
300
  * @param {string} input - The input to be sanitised
244
301
  * @returns A sanitised string, or an empty string if the input is invalid
245
302
  */
246
- sanitiseHtml: (input: string) => (typeof input !== "string" ? "" : sanitize(input, { allowedTags: [], allowedAttributes: {} })),
303
+ sanitiseHtml: (input: string) =>
304
+ typeof input !== "string" ? "" : sanitize(input, { allowedTags: [], allowedAttributes: {} }),
247
305
  /**
248
306
  * A regular expression for social media handles, without a leading slash or @ character.
249
307
  *
@@ -256,40 +314,15 @@ export const Utility = {
256
314
  * @param {"any" | MobilePhoneLocale | MobilePhoneLocale[]} [locale="any"] - The locale(s) to validate the phone number against
257
315
  * @returns `true` if the phone number is valid; `false` otherwise
258
316
  */
259
- 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)),
260
319
  /**
261
320
  * Validates a social media handle.
262
321
  * @param {string} handle - The social media handle to be validated
263
322
  * @returns `true` if the handle is valid; `false` otherwise
264
323
  */
265
- 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))
266
328
  };
267
-
268
- /* Testing functions */
269
- export const Testing = {
270
- /**
271
- * Asserts the absence of a field on the page.
272
- * @param {Locator[]} fields - The fields to check for absence
273
- * @returns A promise that resolves when the check is complete
274
- */
275
- assertFieldAbsence: async (...fields: Locator[]) => {
276
- for (const field of fields) await expect(field).not.toBeVisible();
277
- },
278
- /**
279
- * Clicks a locator and waits for a response.
280
- * @param {Page} page - The page on which to click the locator
281
- * @param {Locator} locator - The locator to click
282
- * @param {string} endpoint - The endpoint to wait for
283
- * @returns A promise that resolves when the click and response are complete
284
- */
285
- clickAndWait: async (page: Page, locator: Locator, endpoint: string) => {
286
- // Ensure the locator is visible
287
- await expect(locator).toBeVisible();
288
-
289
- // Click with force option to ensure the click happens
290
- await locator.click({ force: true });
291
-
292
- // Wait for the API response
293
- await page.waitForResponse(endpoint);
294
- },
295
- };