@koloseum/utils 0.2.8 → 0.2.9

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,6 +1,6 @@
1
1
  import type { Database } from "@koloseum/types/database";
2
2
  import type { CustomError, Microservice, MicroserviceGroup, MicroserviceObject, UserWithCustomMetadata } from "@koloseum/types/general";
3
- import type { IdentityType, PronounsCheckboxes, PronounsItem, Sex, SocialMediaPlatform } from "@koloseum/types/public-auth";
3
+ import type { BranchAddressObject, County, IdentityType, PronounsCheckboxes, PronounsItem, Sex, SocialMediaPlatform } from "@koloseum/types/public-auth";
4
4
  import type { Page } from "@sveltejs/kit";
5
5
  import type { SupabaseClient, FunctionInvokeOptions, PostgrestError } from "@supabase/supabase-js";
6
6
  import type { IOptions } from "@suprsend/web-components/dist/types/interface.d.ts";
@@ -103,6 +103,12 @@ export declare const Utility: {
103
103
  * @returns The formatted identity type string
104
104
  */
105
105
  getIdentityTypeString: (identityType: IdentityType) => string;
106
+ /**
107
+ * Returns a list of all counties in Kenya.
108
+ * @param {"name" | "code"} sortBy - The field to sort the counties by, i.e. `name` or `code`; defaults to `name`
109
+ * @returns {Promise<County[]>} A list of objects with the county `name`, `code`, and a list of `subCounties`
110
+ */
111
+ getKenyaCounties: (sortBy?: "name" | "code") => Promise<County[]>;
106
112
  /**
107
113
  * Returns the URL for a menu item based on the slug.
108
114
  * @param {string} base - The base URL
@@ -183,6 +189,18 @@ export declare const Utility: {
183
189
  * @returns `true` if the page is active, `false` otherwise.
184
190
  */
185
191
  pageIsActive: (page: Page, slug: string | undefined, microservice?: Microservice<MicroserviceGroup>, base?: string) => boolean;
192
+ /**
193
+ * Paginates an array.
194
+ * @template T - The type of array items; defaults to `any`
195
+ * @param {T[]} array - The array to paginate
196
+ * @param {number} page - The page number
197
+ * @param {number} pageSize - The number of items per page; defaults to 10
198
+ * @returns {Object} The paginated array and total number of pages
199
+ */
200
+ paginateArray: <T = any>(array: T[], page: number, pageSize?: number) => {
201
+ paginatedItems: T[];
202
+ totalPages: number;
203
+ };
186
204
  /**
187
205
  * Parses a `CustomError` object within a page/layout server load function and returns an error to be thrown.
188
206
  * @param {CustomError} error - The error object
@@ -224,6 +242,15 @@ export declare const Utility: {
224
242
  * - Source: https://stackoverflow.com/a/74579651
225
243
  */
226
244
  socialMediaHandleRegex: RegExp;
245
+ /**
246
+ * Validates address data submitted in a form and returns the validated data.
247
+ * @param {FormData} formData - The submitted form data
248
+ * @returns An object with the validated `address`, or an `error` if any has occurred
249
+ */
250
+ validateAddress: (formData: FormData) => Promise<{
251
+ address?: BranchAddressObject;
252
+ error?: CustomError;
253
+ }>;
227
254
  /**
228
255
  * Validates an E.164 phone number.
229
256
  * @param {string} phoneNumber - The phone number to be validated
package/dist/utils.js CHANGED
@@ -2,10 +2,12 @@ import { v4 as uuidv4 } from "uuid";
2
2
  import { error as svelteError } from "@sveltejs/kit";
3
3
  import { FunctionsFetchError, FunctionsHttpError, FunctionsRelayError } from "@supabase/supabase-js";
4
4
  import validator from "validator";
5
- /* Helper functions */
5
+ /* HELPERS */
6
6
  const sanitize = (await import("sanitize-html")).default;
7
- const { isMobilePhone, isURL } = validator;
8
- /* Dummy data */
7
+ const { trim, escape, isMobilePhone, isURL } = validator;
8
+ const { KenyaAdministrativeDivisions } = (await import("kenya-administrative-divisions"));
9
+ const kenyaAdmin = new KenyaAdministrativeDivisions();
10
+ /* DUMMY DATA */
9
11
  export const Data = {
10
12
  /**
11
13
  * A generic authenticated user.
@@ -68,7 +70,7 @@ export const Data = {
68
70
  };
69
71
  }
70
72
  };
71
- /* Status messages */
73
+ /* STATUS MESSAGES */
72
74
  export const Status = {
73
75
  /**
74
76
  * A generic error message.
@@ -83,7 +85,7 @@ export const Status = {
83
85
  */
84
86
  PASSWORD_RESET_REQUESTED: "If the provided email address is registered, you will receive a password reset link shortly."
85
87
  };
86
- /* Utility functions */
88
+ /* UTILITY FUNCTIONS */
87
89
  export const Utility = {
88
90
  /**
89
91
  * Calls a Supabase Edge function.
@@ -328,6 +330,32 @@ export const Utility = {
328
330
  return "Driver's licence";
329
331
  return "";
330
332
  },
333
+ /**
334
+ * Returns a list of all counties in Kenya.
335
+ * @param {"name" | "code"} sortBy - The field to sort the counties by, i.e. `name` or `code`; defaults to `name`
336
+ * @returns {Promise<County[]>} A list of objects with the county `name`, `code`, and a list of `subCounties`
337
+ */
338
+ getKenyaCounties: async (sortBy = "name") => {
339
+ // Get all counties
340
+ const countiesData = await kenyaAdmin.getAll();
341
+ // Format counties
342
+ const counties = countiesData.map((county) => {
343
+ // Get list of sub-counties
344
+ const subCounties = [];
345
+ for (const subCounty of county.constituencies)
346
+ subCounties.push(subCounty.constituency_name);
347
+ subCounties.sort();
348
+ // Format county name and add to list
349
+ const countyName = county.county_name.split(" ");
350
+ let name = county.county_name;
351
+ if (countyName.length > 1)
352
+ name = countyName.map((word) => Utility.capitalise(word)).join(" ");
353
+ // Return county
354
+ return { name, code: county.county_code, subCounties };
355
+ });
356
+ // Return sorted counties
357
+ return counties.sort((a, b) => (sortBy === "name" ? a.name.localeCompare(b.name) : a.code - b.code));
358
+ },
331
359
  /**
332
360
  * Returns the URL for a menu item based on the slug.
333
361
  * @param {string} base - The base URL
@@ -981,7 +1009,7 @@ export const Utility = {
981
1009
  {
982
1010
  name: "Rankings",
983
1011
  description: "Manage rankings for any Savanna Circuit season.",
984
- slug: "rankings"
1012
+ root: true
985
1013
  },
986
1014
  {
987
1015
  name: "Tournaments",
@@ -989,20 +1017,15 @@ export const Utility = {
989
1017
  slug: "tournaments",
990
1018
  tabs: [
991
1019
  {
992
- name: "Players",
993
- description: "Manage Players for any Savanna Circuit tournament.",
994
- slug: "players"
1020
+ name: "Events",
1021
+ description: "Manage events for any Savanna Circuit tournament.",
1022
+ root: true
995
1023
  },
996
1024
  {
997
1025
  name: "Tickets",
998
1026
  description: "Manage tickets for any Savanna Circuit tournament.",
999
1027
  slug: "tickets"
1000
1028
  },
1001
- {
1002
- name: "Events",
1003
- description: "Manage events for any Savanna Circuit tournament.",
1004
- slug: "events"
1005
- },
1006
1029
  {
1007
1030
  name: "Settings",
1008
1031
  description: "Manage settings for any Savanna Circuit tournament.",
@@ -1178,6 +1201,24 @@ export const Utility = {
1178
1201
  const target = `${cleanBase}/${slug}`;
1179
1202
  return page.url.pathname === target || page.url.pathname.startsWith(`${target}/`);
1180
1203
  },
1204
+ /**
1205
+ * Paginates an array.
1206
+ * @template T - The type of array items; defaults to `any`
1207
+ * @param {T[]} array - The array to paginate
1208
+ * @param {number} page - The page number
1209
+ * @param {number} pageSize - The number of items per page; defaults to 10
1210
+ * @returns {Object} The paginated array and total number of pages
1211
+ */
1212
+ paginateArray: (array, page, pageSize = 10) => {
1213
+ // Define variables
1214
+ const startIndex = (page - 1) * pageSize;
1215
+ const endIndex = startIndex + pageSize;
1216
+ const totalPages = Math.ceil(array.length / pageSize);
1217
+ // Paginate items
1218
+ const paginatedItems = array.slice(startIndex, endIndex);
1219
+ // Return data
1220
+ return { paginatedItems, totalPages };
1221
+ },
1181
1222
  /**
1182
1223
  * Parses a `CustomError` object within a page/layout server load function and returns an error to be thrown.
1183
1224
  * @param {CustomError} error - The error object
@@ -1277,6 +1318,102 @@ export const Utility = {
1277
1318
  * - Source: https://stackoverflow.com/a/74579651
1278
1319
  */
1279
1320
  socialMediaHandleRegex: /^([A-Za-z0-9_.]{3,25})$/gm,
1321
+ /**
1322
+ * Validates address data submitted in a form and returns the validated data.
1323
+ * @param {FormData} formData - The submitted form data
1324
+ * @returns An object with the validated `address`, or an `error` if any has occurred
1325
+ */
1326
+ validateAddress: async (formData) => {
1327
+ // Building name
1328
+ let buildingName = formData.get("building-name");
1329
+ if (!buildingName)
1330
+ return { error: Utility.customError(400, "Building name is required.") };
1331
+ buildingName = Utility.sanitiseHtml(trim(escape(buildingName)));
1332
+ // Unit number
1333
+ let unitNumber = formData.get("unit-number") || undefined;
1334
+ if (unitNumber) {
1335
+ unitNumber = Utility.sanitiseHtml(trim(escape(unitNumber)));
1336
+ if (unitNumber === "")
1337
+ unitNumber = undefined;
1338
+ }
1339
+ // Street name
1340
+ let streetName = formData.get("street-name");
1341
+ if (!streetName)
1342
+ return { error: Utility.customError(400, "Street name is required.") };
1343
+ streetName = Utility.sanitiseHtml(trim(escape(streetName)));
1344
+ // P.O. box and postal code
1345
+ let boxNumber = formData.get("box-number") || undefined;
1346
+ let postalCode = formData.get("postal-code") || undefined;
1347
+ if (boxNumber) {
1348
+ boxNumber = Utility.sanitiseHtml(trim(escape(boxNumber)));
1349
+ if (boxNumber === "")
1350
+ boxNumber = undefined;
1351
+ if (boxNumber && !postalCode)
1352
+ return { error: Utility.customError(400, "Postal code is required.") };
1353
+ }
1354
+ if (postalCode) {
1355
+ postalCode = Utility.sanitiseHtml(trim(escape(postalCode)));
1356
+ if (postalCode === "")
1357
+ postalCode = undefined;
1358
+ if (postalCode && !boxNumber)
1359
+ return { error: Utility.customError(400, "P.O. box is required.") };
1360
+ }
1361
+ // Town
1362
+ let town = formData.get("town");
1363
+ if (!town)
1364
+ return { error: Utility.customError(400, "City/town is required.") };
1365
+ town = Utility.sanitiseHtml(trim(escape(town)));
1366
+ // County
1367
+ let county = undefined;
1368
+ const countyCode = Number(formData.get("county-code"));
1369
+ if (isNaN(countyCode))
1370
+ return { error: Utility.customError(400, "County is invalid.") };
1371
+ if (!countyCode)
1372
+ return { error: Utility.customError(400, "County is required.") };
1373
+ const counties = await kenyaAdmin.getAll();
1374
+ if (!counties || counties.length === 0)
1375
+ return { error: Utility.customError(400, "County is invalid.") };
1376
+ const countyData = counties.find(({ county_code }) => county_code === countyCode);
1377
+ if (!countyData)
1378
+ return { error: Utility.customError(400, "County is invalid.") };
1379
+ const { county_name, constituencies } = countyData;
1380
+ county = county_name;
1381
+ // Sub-county
1382
+ let subCounty = formData.get("sub-county") || undefined;
1383
+ if (subCounty) {
1384
+ subCounty = Utility.sanitiseHtml(trim(escape(subCounty)));
1385
+ if (subCounty === "")
1386
+ subCounty = undefined;
1387
+ else {
1388
+ // Check if constituencies exists and is an array before mapping
1389
+ if (!constituencies || !Array.isArray(constituencies))
1390
+ return { error: Utility.customError(400, "Sub-county data is unavailable.") };
1391
+ // Get list of sub-counties
1392
+ const subCounties = constituencies.map(({ constituency_name: name }) => name);
1393
+ if (!subCounties.includes(subCounty))
1394
+ return { error: Utility.customError(400, "Sub-county is invalid.") };
1395
+ }
1396
+ }
1397
+ // Additional information
1398
+ let additionalInfo = formData.get("address-additional-info") || undefined;
1399
+ if (additionalInfo) {
1400
+ additionalInfo = Utility.sanitiseHtml(trim(escape(additionalInfo)));
1401
+ if (additionalInfo === "")
1402
+ additionalInfo = undefined;
1403
+ }
1404
+ // Create address object
1405
+ const address = {
1406
+ buildingName,
1407
+ unitNumber,
1408
+ streetName,
1409
+ town,
1410
+ subCounty,
1411
+ county,
1412
+ additionalInfo
1413
+ };
1414
+ // Return data
1415
+ return { address };
1416
+ },
1280
1417
  /**
1281
1418
  * Validates an E.164 phone number.
1282
1419
  * @param {string} phoneNumber - The phone number to be validated
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koloseum/utils",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "author": "Koloseum Technologies Limited",
5
5
  "type": "module",
6
6
  "description": "Utility logic for use across Koloseum web apps (TypeScript)",
@@ -31,17 +31,19 @@
31
31
  "dependencies": {
32
32
  "@supabase/supabase-js": "^2.50.1",
33
33
  "@sveltejs/kit": "^2.22.0",
34
+ "kenya-administrative-divisions": "^0.0.18",
34
35
  "sanitize-html": "^2.17.0",
35
36
  "uuid": "^11.1.0",
36
37
  "validator": "^13.15.15"
37
38
  },
38
39
  "devDependencies": {
39
- "@koloseum/types": "^0.2.1",
40
+ "@koloseum/types": "^0.2.4",
40
41
  "@playwright/test": "^1.53.1",
41
42
  "@suprsend/web-components": "^0.2.1",
42
43
  "@types/sanitize-html": "^2.16.0",
43
44
  "@types/uuid": "^10.0.0",
44
45
  "@types/validator": "^13.15.2",
46
+ "postgres-interval": "^4.0.2",
45
47
  "prettier": "^3.6.0",
46
48
  "typescript": "^5.8.3",
47
49
  "vitest": "^3.2.4"