@koloseum/utils 0.2.28 → 0.3.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.
package/dist/client.js ADDED
@@ -0,0 +1,680 @@
1
+ import { Config } from "./platform.js";
2
+ import Bowser from "bowser";
3
+ /* BROWSER HELPERS */
4
+ export const Browser = {
5
+ /**
6
+ * Checks if a specific feature is supported by the browser.
7
+ * @param feature - The feature to check
8
+ * @param browserName - The name of the browser
9
+ * @param browserVersion - The version of the browser
10
+ * @returns `true` if the feature is supported, `false` otherwise
11
+ */
12
+ checkFeatureSupport: (feature, browserName, browserVersion) => {
13
+ // Feature support matrix based on browser versions
14
+ const featureSupport = {
15
+ optionalChaining: {
16
+ Chrome: 80,
17
+ Firefox: 74,
18
+ Safari: 13.4,
19
+ Edge: 80,
20
+ Opera: 80
21
+ },
22
+ nullishCoalescing: {
23
+ Chrome: 80,
24
+ Firefox: 72,
25
+ Safari: 13.4,
26
+ Edge: 80,
27
+ Opera: 80
28
+ },
29
+ fetch: {
30
+ Chrome: 42,
31
+ Firefox: 39,
32
+ Safari: 10.1,
33
+ Edge: 14,
34
+ Opera: 29
35
+ },
36
+ es6Modules: {
37
+ Chrome: 61,
38
+ Firefox: 60,
39
+ Safari: 10.1,
40
+ Edge: 16,
41
+ Opera: 48
42
+ },
43
+ asyncAwait: {
44
+ Chrome: 55,
45
+ Firefox: 52,
46
+ Safari: 10.1,
47
+ Edge: 14,
48
+ Opera: 42
49
+ }
50
+ };
51
+ const featureMatrix = featureSupport[feature];
52
+ if (!featureMatrix)
53
+ return true; // Unknown feature, assume supported
54
+ const minVersion = featureMatrix[browserName];
55
+ if (!minVersion)
56
+ return true; // Unknown browser, assume supported
57
+ return browserVersion >= minVersion;
58
+ },
59
+ /**
60
+ * Gets detailed browser information for debugging.
61
+ */
62
+ getDetailedInfo: () => {
63
+ if (typeof window === "undefined") {
64
+ return {
65
+ browser: "Server-side rendering",
66
+ os: "Unknown",
67
+ platform: "Unknown",
68
+ engine: "Unknown",
69
+ userAgent: "Unknown"
70
+ };
71
+ }
72
+ const browser = Bowser.getParser(window.navigator.userAgent);
73
+ return {
74
+ browser: `${browser.getBrowserName()} ${browser.getBrowserVersion()}`,
75
+ os: `${browser.getOSName()} ${browser.getOSVersion()}`,
76
+ platform: browser.getPlatformType(),
77
+ engine: browser.getEngineName(),
78
+ userAgent: window.navigator.userAgent
79
+ };
80
+ },
81
+ /**
82
+ * Gets comprehensive browser information using Bowser.
83
+ */
84
+ getInfo: () => {
85
+ if (typeof window === "undefined") {
86
+ return {
87
+ name: "server",
88
+ version: 0,
89
+ isOldSafari: false,
90
+ isIOS: false,
91
+ supportsOptionalChaining: true,
92
+ supportsNullishCoalescing: true,
93
+ supportsFetch: true
94
+ };
95
+ }
96
+ const browser = Bowser.getParser(window.navigator.userAgent);
97
+ const browserName = browser.getBrowserName();
98
+ const browserVersion = parseFloat(browser.getBrowserVersion() || "0");
99
+ const osName = browser.getOSName();
100
+ const isIOS = osName === "iOS";
101
+ const isSafari = browserName === "Safari";
102
+ const isOldSafari = isSafari && browserVersion < 13.4;
103
+ return {
104
+ name: browserName,
105
+ version: browserVersion,
106
+ isOldSafari,
107
+ isIOS,
108
+ supportsOptionalChaining: Browser.checkFeatureSupport("optionalChaining", browserName, browserVersion),
109
+ supportsNullishCoalescing: Browser.checkFeatureSupport("nullishCoalescing", browserName, browserVersion),
110
+ supportsFetch: Browser.checkFeatureSupport("fetch", browserName, browserVersion)
111
+ };
112
+ },
113
+ /**
114
+ * Gets specific browser recommendations based on the detected browser.
115
+ */
116
+ getRecommendations: () => {
117
+ if (typeof window === "undefined")
118
+ return {
119
+ title: "Browser compatibility issue",
120
+ message: "Browser detection is not available on theserver.",
121
+ recommendations: [],
122
+ critical: false
123
+ };
124
+ const browser = Bowser.getParser(window.navigator.userAgent);
125
+ const browserName = browser.getBrowserName();
126
+ const browserVersion = browser.getBrowserVersion();
127
+ // Internet Explorer
128
+ if (browserName === "Internet Explorer") {
129
+ return {
130
+ title: "Unsupported browser",
131
+ message: "Internet Explorer is not supported. Please use a modern browser.",
132
+ recommendations: [
133
+ "Download and install Microsoft Edge",
134
+ "Use an alternative such as Chrome or Firefox (or Safari on Mac)"
135
+ ],
136
+ critical: true
137
+ };
138
+ }
139
+ // Apple Safari
140
+ if (browserName === "Safari" && parseFloat(browserVersion || "0") < 13.4) {
141
+ return {
142
+ title: "Browser compatibility issue",
143
+ message: `You're using Safari ${browserVersion || "Unknown"}, which doesn't support modern web features.`,
144
+ recommendations: [
145
+ "Update to Safari 13.4 or later",
146
+ "Use Chrome or Firefox as an alternative",
147
+ "Update your iOS device to the latest version"
148
+ ],
149
+ critical: true
150
+ };
151
+ }
152
+ // Google Chrome
153
+ if (browserName === "Chrome" && parseFloat(browserVersion || "0") < 80) {
154
+ return {
155
+ title: "Outdated browser",
156
+ message: `You're using Chrome ${browserVersion || "Unknown"}, which may not support all features.`,
157
+ recommendations: ["Update Chrome to version 80 or later", "Enable automatic updates in Chrome settings"],
158
+ critical: false
159
+ };
160
+ }
161
+ // Mozilla Firefox
162
+ if (browserName === "Firefox" && parseFloat(browserVersion || "0") < 80) {
163
+ return {
164
+ title: "Outdated browser",
165
+ message: `You're using Firefox ${browserVersion || "Unknown"}, which may not support all features.`,
166
+ recommendations: ["Update Firefox to version 80 or later", "Enable automatic updates in Firefox settings"],
167
+ critical: false
168
+ };
169
+ }
170
+ // Microsoft Edge
171
+ if (browserName === "Edge" && parseFloat(browserVersion || "0") < 80) {
172
+ return {
173
+ title: "Outdated browser",
174
+ message: `You're using Edge ${browserVersion || "Unknown"}, which may not support all features.`,
175
+ recommendations: ["Update Edge to version 80 or later", "Enable automatic updates in Edge settings"],
176
+ critical: false
177
+ };
178
+ }
179
+ // For unknown or very old browsers
180
+ if (browserName === "Unknown" || parseFloat(browserVersion || "0") < 50) {
181
+ return {
182
+ title: "Unrecognised browser",
183
+ message: "Your browser may not be fully supported. For the best experience, please use a modern browser.",
184
+ recommendations: ["Use Chrome, Firefox or Edge (latest version)", "Use Safari 13.4+ (on Mac/iOS)"],
185
+ critical: true
186
+ };
187
+ }
188
+ // Default for modern browsers
189
+ return {
190
+ title: "Browser compatibility issue",
191
+ message: "Your browser may not fully support this application.",
192
+ recommendations: [
193
+ "Update your browser to the latest version",
194
+ "Clear your browser cache and cookies",
195
+ "Disable browser extensions temporarily",
196
+ "Try a different modern browser"
197
+ ],
198
+ critical: false
199
+ };
200
+ },
201
+ /**
202
+ * Checks if the current browser is compatible with the application.
203
+ */
204
+ isBrowserCompatible: () => {
205
+ if (typeof window === "undefined")
206
+ return true;
207
+ const browser = Bowser.getParser(window.navigator.userAgent);
208
+ const browserName = browser.getBrowserName();
209
+ const browserVersion = parseFloat(browser.getBrowserVersion() || "0");
210
+ // Define minimum supported versions
211
+ const minVersions = {
212
+ "Internet Explorer": 0, // Not supported at all
213
+ Safari: 13.4,
214
+ Chrome: 80,
215
+ Firefox: 80,
216
+ Edge: 80,
217
+ Opera: 80
218
+ };
219
+ // Internet Explorer is never compatible
220
+ if (browserName === "Internet Explorer") {
221
+ return false;
222
+ }
223
+ // Check if browser version meets minimum requirements
224
+ const minVersion = minVersions[browserName];
225
+ if (minVersion && browserVersion < minVersion) {
226
+ return false;
227
+ }
228
+ // Check for critical modern features
229
+ return (Browser.checkFeatureSupport("optionalChaining", browserName, browserVersion) &&
230
+ Browser.checkFeatureSupport("nullishCoalescing", browserName, browserVersion) &&
231
+ Browser.checkFeatureSupport("fetch", browserName, browserVersion) &&
232
+ Browser.checkFeatureSupport("es6Modules", browserName, browserVersion) &&
233
+ Browser.checkFeatureSupport("asyncAwait", browserName, browserVersion));
234
+ },
235
+ /**
236
+ * Checks if the browser is Internet Explorer.
237
+ */
238
+ isInternetExplorer: () => {
239
+ if (typeof window === "undefined")
240
+ return false;
241
+ const browser = Bowser.getParser(window.navigator.userAgent);
242
+ return browser.getBrowserName() === "Internet Explorer";
243
+ },
244
+ /**
245
+ * Checks if the browser is iOS Safari.
246
+ */
247
+ isIOSSafari: () => {
248
+ if (typeof window === "undefined")
249
+ return false;
250
+ const browser = Bowser.getParser(window.navigator.userAgent);
251
+ return browser.getOSName() === "iOS" && browser.getBrowserName() === "Safari";
252
+ },
253
+ /**
254
+ * Checks if the browser is a legacy version that needs updating.
255
+ */
256
+ isLegacyBrowser: () => {
257
+ if (typeof window === "undefined")
258
+ return false;
259
+ const browser = Bowser.getParser(window.navigator.userAgent);
260
+ const browserName = browser.getBrowserName();
261
+ const browserVersion = parseFloat(browser.getBrowserVersion() || "0");
262
+ // Legacy browser thresholds
263
+ const legacyThresholds = {
264
+ "Internet Explorer": 0, // All versions are legacy
265
+ Safari: 13.4,
266
+ Chrome: 80,
267
+ Firefox: 80,
268
+ Edge: 80,
269
+ Opera: 80
270
+ };
271
+ const threshold = legacyThresholds[browserName];
272
+ return threshold ? browserVersion < threshold : false;
273
+ },
274
+ /**
275
+ * Checks if the browser is an old version of Safari.
276
+ */
277
+ isOldSafari: () => {
278
+ if (typeof window === "undefined")
279
+ return false;
280
+ const browser = Bowser.getParser(window.navigator.userAgent);
281
+ const browserName = browser.getBrowserName();
282
+ const browserVersion = parseFloat(browser.getBrowserVersion() || "0");
283
+ return browserName === "Safari" && browserVersion < 13.4;
284
+ }
285
+ };
286
+ /* ACCESS HELPERS */
287
+ export const Access = {
288
+ /**
289
+ * Get the current role of the user.
290
+ * @param user - The user object
291
+ * @param type - The type of role to get, i.e. "backroom" or "lounges"
292
+ * @returns The current role of the user.
293
+ */
294
+ getCurrentRole: (user, type) => {
295
+ // Return null if no user is provided
296
+ if (!user)
297
+ return null;
298
+ // Get the role from the user's app metadata
299
+ const role = user.app_metadata.roles.find((role) => role.startsWith(type + "_")).split("_")[1];
300
+ // Check for Superuser
301
+ if (role === "superuser")
302
+ return {
303
+ name: "Superuser",
304
+ slug: "superuser",
305
+ superuser: true
306
+ };
307
+ // Define roles and slugs array
308
+ const roles = (type === "backroom" ? Config.microservices.backroom : Config.microservices.lounges)
309
+ .map(({ roles }) => roles)
310
+ .flat();
311
+ let slugs;
312
+ // Check if the user's role is a top-level microservice role
313
+ const topLevelRoles = roles
314
+ .filter((role) => role !== null && "root" in role && !("superuser" in role) && role.root === true)
315
+ .filter((role) => role !== null);
316
+ slugs = topLevelRoles.map((role) => role.slug);
317
+ if (slugs.includes(role))
318
+ return topLevelRoles.find(({ slug }) => slug === role);
319
+ // Check if the user's role is a sub-role of a microservice
320
+ const subRoles = roles.filter((role) => role !== null && !("root" in role)).filter((role) => role !== null);
321
+ slugs = subRoles.map((role) => role.slug);
322
+ if (slugs.includes(role))
323
+ return subRoles.find(({ slug }) => slug === role);
324
+ // Return null
325
+ return null;
326
+ },
327
+ /**
328
+ * Get a microservice feature by slug.
329
+ * @param featureSlug - The slug of the feature to get.
330
+ * @param type - The type of microservice to get the feature for, i.e. "backroom" or "lounges"
331
+ * @returns The feature object or `null` if the feature does not exist.
332
+ */
333
+ getMicroserviceFeature: (featureSlug, type) => (type === "backroom" ? Config.microservices.backroom : Config.microservices.lounges)
334
+ .flatMap(({ features }) => features)
335
+ .find(({ slug }) => slug === featureSlug) ?? null,
336
+ /**
337
+ * Get the microservices that a role has access to.
338
+ * @param role - The role to check.
339
+ * @param type - The type of microservice to get the features for, i.e. "backroom" or "lounges"
340
+ * @returns The microservices that the role has access to.
341
+ */
342
+ getMicroservicesForRole: (role, type) => (type === "backroom" ? Config.microservices.backroom : Config.microservices.lounges)
343
+ .filter(({ slug }) => slug !== "account")
344
+ .filter(({ features, roles }) => {
345
+ // Superusers have access to everything
346
+ if ("superuser" in role && role.superuser === true)
347
+ return true;
348
+ // Check if the user's role is a root role for this microservice
349
+ if (roles?.some((r) => "root" in r && r.root === true && r.slug === role.slug))
350
+ return true;
351
+ // Check if the user's role has access to any features in this microservice
352
+ return features.some((feature) => Access.roleHasAccessToFeature(role, feature, type));
353
+ }),
354
+ /**
355
+ * Renders the microservice features for the current user.
356
+ * @param user - The user to render the features for.
357
+ * @returns The microservice features for the current user, or `null` if no user is provided.
358
+ */
359
+ renderMicroserviceFeatures: (user) => {
360
+ // Return null if no user is provided
361
+ if (!user)
362
+ return null;
363
+ // Initialise variables
364
+ let features = Config.microservices.players.filter(({ slug }) => slug === "account").flatMap(({ features }) => features);
365
+ let loungesCount = 0;
366
+ let backroomCount = 0;
367
+ // Count number of Lounges and Backroom roles
368
+ for (const role of user.app_metadata.roles) {
369
+ if (role.startsWith("lounges_"))
370
+ loungesCount++;
371
+ if (role.startsWith("backroom_"))
372
+ backroomCount++;
373
+ }
374
+ // Filter features based on roles
375
+ if (loungesCount === 0)
376
+ features = features.filter(({ slug }) => slug !== "lounges");
377
+ if (backroomCount === 0)
378
+ features = features.filter(({ slug }) => slug !== "backroom");
379
+ // Return filtered features
380
+ return features;
381
+ },
382
+ /**
383
+ * Check if a role has access to a microservice's feature.
384
+ * @param role - The role to check.
385
+ * @param feature - The feature to check.
386
+ * @param type - The type of microservice to check the feature for, i.e. "backroom" or "lounges"
387
+ * @returns `true` if the role has access to the feature, `false` otherwise.
388
+ */
389
+ roleHasAccessToFeature: (role, feature, type) => {
390
+ // Return false if no role or feature is provided
391
+ if (!role || !feature)
392
+ return false;
393
+ // Return true if the role is superuser
394
+ if ("superuser" in role && role.superuser === true)
395
+ return true;
396
+ /* LOUNGES */
397
+ if (type === "lounges") {
398
+ // Branches
399
+ const { features: branchesFeatures, roles: branchesRoles } = Config.microservices.lounges.find(({ slug }) => slug === "branches");
400
+ if (branchesFeatures.some((f) => f.slug === feature.slug) && branchesRoles.some((r) => r.slug === role.slug)) {
401
+ if ("root" in role && role.root === true)
402
+ return true;
403
+ if ("featureSlugs" in role)
404
+ return role.featureSlugs.includes(feature.slug);
405
+ }
406
+ // Staff
407
+ const { features: staffFeatures, roles: staffRoles } = Config.microservices.lounges.find(({ slug }) => slug === "staff");
408
+ if (staffFeatures.some((f) => f.slug === feature.slug) && staffRoles.some((r) => r.slug === role.slug)) {
409
+ if ("root" in role && role.root === true)
410
+ return true;
411
+ if ("featureSlugs" in role)
412
+ return role.featureSlugs.includes(feature.slug);
413
+ }
414
+ }
415
+ /* BACKROOM */
416
+ if (type === "backroom") {
417
+ // Compliance
418
+ const { features: complianceFeatures, roles: complianceRoles } = Config.microservices.backroom.find(({ slug }) => slug === "compliance");
419
+ if (complianceFeatures.some((f) => f.slug === feature.slug) && complianceRoles.some((r) => r.slug === role.slug)) {
420
+ if ("root" in role && role.root === true)
421
+ return true;
422
+ if ("featureSlugs" in role)
423
+ return role.featureSlugs.includes(feature.slug);
424
+ }
425
+ // Commerce
426
+ const { features: commerceFeatures, roles: commerceRoles } = Config.microservices.backroom.find(({ slug }) => slug === "commerce");
427
+ if (commerceFeatures.some((f) => f.slug === feature.slug) && commerceRoles.some((r) => r.slug === role.slug)) {
428
+ if ("root" in role && role.root === true)
429
+ return true;
430
+ if ("featureSlugs" in role)
431
+ return role.featureSlugs.includes(feature.slug);
432
+ }
433
+ // Staff
434
+ const { features: staffFeatures, roles: staffRoles } = Config.microservices.backroom.find(({ slug }) => slug === "staff");
435
+ if (staffFeatures.some((f) => f.slug === feature.slug) && staffRoles.some((r) => r.slug === role.slug)) {
436
+ if ("root" in role && role.root === true)
437
+ return true;
438
+ if ("featureSlugs" in role)
439
+ return role.featureSlugs.includes(feature.slug);
440
+ }
441
+ }
442
+ // Return false
443
+ return false;
444
+ }
445
+ };
446
+ /* INTERFACE HELPERS */
447
+ export const Interface = {
448
+ /**
449
+ * Returns the URL for a menu item based on the slug.
450
+ * @param {string} base - The base URL
451
+ * @param {string} slug - The slug of the menu item
452
+ * @returns {string} The URL for the menu item
453
+ */
454
+ getMenuItemUrl: (base, slug) => {
455
+ // Validate base URL
456
+ if (typeof base !== "string")
457
+ return "";
458
+ // Format base URL
459
+ if (base === "/")
460
+ base = "";
461
+ if (base.charAt(base.length - 1) === "/")
462
+ base = base.slice(0, -1);
463
+ // Return URL
464
+ return `${base}${slug === "/" ? slug : typeof slug === "string" ? `/${slug}` : ""}`;
465
+ },
466
+ /**
467
+ * Generate a SuprSend notification inbox configuration object for a user.
468
+ * @param {string} userId - The user ID to generate the configuration for.
469
+ * @param {string} publicApiKey - The public API key to use for SuprSend.
470
+ * @returns The SuprSend notification inbox configuration object.
471
+ */
472
+ getSuprSendInboxConfig: (userId, publicApiKey) => ({
473
+ distinctId: userId,
474
+ publicApiKey,
475
+ inbox: {
476
+ stores: [
477
+ { storeId: "all", label: "Inbox", query: { archived: false } },
478
+ { storeId: "archived", label: "Archived", query: { archived: true } }
479
+ ],
480
+ theme: {
481
+ bell: {
482
+ color: "var(--color-accent)"
483
+ },
484
+ badge: {
485
+ backgroundColor: "var(--color-primary)",
486
+ color: "var(--color-primary-content)"
487
+ },
488
+ header: {
489
+ container: {
490
+ backgroundColor: "var(--color-base-100)",
491
+ borderColor: "var(--color-base-200)"
492
+ },
493
+ headerText: {
494
+ color: "var(--color-neutral)"
495
+ },
496
+ markAllReadText: {
497
+ color: "var(--color-accent)"
498
+ }
499
+ },
500
+ tabs: {
501
+ color: "var(--color-primary)",
502
+ unselectedColor: "var(--color-accent)",
503
+ bottomColor: "var(--color-primary)",
504
+ badgeColor: "var(--color-base-200)",
505
+ badgeText: "var(--color-accent)"
506
+ },
507
+ notificationsContainer: {
508
+ container: {
509
+ backgroundColor: "var(--color-base-100)",
510
+ borderColor: "var(--color-base-200)",
511
+ height: "75vh",
512
+ marginTop: "0.75rem"
513
+ },
514
+ noNotificationsText: {
515
+ color: "var(--color-warning)"
516
+ },
517
+ noNotificationsSubtext: {
518
+ color: "var(--color-neutral)"
519
+ },
520
+ loader: {
521
+ color: "var(--color-primary)"
522
+ }
523
+ },
524
+ notification: {
525
+ container: {
526
+ borderColor: "var(--color-base-200)",
527
+ readBackgroundColor: "var(--color-base-100)",
528
+ unreadBackgroundColor: "var(--color-base-150)",
529
+ hoverBackgroundColor: "var(--color-base-200)"
530
+ },
531
+ headerText: {
532
+ color: "var(--color-secondary)"
533
+ },
534
+ bodyText: {
535
+ color: "var(--color-neutral)",
536
+ linkColor: "var(--color-secondary)"
537
+ },
538
+ unseenDot: {
539
+ backgroundColor: "var(--color-warning)"
540
+ },
541
+ createdOnText: {
542
+ color: "var(--color-accent)"
543
+ },
544
+ subtext: {
545
+ color: "var(--color-neutral)"
546
+ },
547
+ expiresText: {
548
+ color: "var(--color-warning)",
549
+ expiringBackgroundColor: "var(--color-warning)",
550
+ expiringColor: "var(--color-neutral)"
551
+ },
552
+ actions: [
553
+ {
554
+ text: {
555
+ color: "var(--color-primary-content)"
556
+ },
557
+ container: {
558
+ backgroundColor: "var(--color-primary)",
559
+ hoverBackgroundColor: "var(--color-secondary)",
560
+ border: "unset"
561
+ }
562
+ },
563
+ {
564
+ text: {
565
+ color: "var(--color-primary-content)"
566
+ },
567
+ container: {
568
+ backgroundColor: "var(--color-primary)",
569
+ hoverBackgroundColor: "var(--color-secondary)",
570
+ border: "unset"
571
+ }
572
+ }
573
+ ],
574
+ actionsMenuIcon: {
575
+ hoverBackgroundColor: "var(--color-base-200)",
576
+ color: "var(--color-neutral)"
577
+ },
578
+ actionsMenu: {
579
+ backgroundColor: "var(--color-base-100)",
580
+ borderColor: "var(--color-base-200)"
581
+ },
582
+ actionsMenuItem: {
583
+ hoverBackgroundColor: "var(--color-base-200)"
584
+ },
585
+ actionsMenuItemIcon: {
586
+ color: "var(--color-accent)"
587
+ },
588
+ actionsMenuItemText: {
589
+ color: "var(--color-neutral)"
590
+ }
591
+ }
592
+ }
593
+ },
594
+ toast: {
595
+ theme: {
596
+ container: {
597
+ backgroundColor: "var(--color-base-100)",
598
+ borderColor: "var(--color-base-200)"
599
+ },
600
+ headerText: {
601
+ color: "var(--color-primary)"
602
+ },
603
+ bodyText: {
604
+ color: "var(--color-neutral)"
605
+ }
606
+ }
607
+ }
608
+ }),
609
+ /**
610
+ * Handles the click event for pronouns checkboxes.
611
+ * @param {MouseEvent} e - The click event
612
+ * @param {PronounsCheckboxes} checkboxes - The pronouns checkboxes
613
+ */
614
+ handlePronounsCheckboxClick: (e, checkboxes) => {
615
+ const target = e.target;
616
+ if (target.value === "none") {
617
+ for (let checkbox of Object.values(checkboxes))
618
+ if (checkbox && checkbox !== target && checkbox.checked) {
619
+ e.preventDefault();
620
+ return;
621
+ }
622
+ for (let checkbox of Object.values(checkboxes))
623
+ if (checkbox && checkbox !== target) {
624
+ checkbox.checked = false;
625
+ checkbox.disabled = !checkbox.disabled;
626
+ }
627
+ }
628
+ else {
629
+ if (checkboxes.none?.checked) {
630
+ e.preventDefault();
631
+ return;
632
+ }
633
+ if (target.value === "male" || target.value === "female") {
634
+ const oppositeIndex = target.value === "male" ? "female" : "male";
635
+ const oppositeCheckbox = checkboxes[oppositeIndex];
636
+ if (oppositeCheckbox?.checked) {
637
+ e.preventDefault();
638
+ return;
639
+ }
640
+ if (oppositeCheckbox) {
641
+ oppositeCheckbox.checked = false;
642
+ oppositeCheckbox.disabled = !oppositeCheckbox.disabled;
643
+ }
644
+ if (checkboxes.none) {
645
+ checkboxes.none.checked = false;
646
+ if (!checkboxes.neutral?.checked)
647
+ checkboxes.none.disabled = !checkboxes.none.disabled;
648
+ }
649
+ }
650
+ else {
651
+ if (checkboxes.none) {
652
+ checkboxes.none.checked = false;
653
+ if (checkboxes.neutral && !checkboxes.male?.checked && !checkboxes.female?.checked)
654
+ checkboxes.none.disabled = checkboxes.neutral.checked;
655
+ }
656
+ }
657
+ }
658
+ },
659
+ /**
660
+ * Check if a page is active based on the slug and microservice.
661
+ * @param page - The current page object.
662
+ * @param slug - The slug of the page to check.
663
+ * @param microservice - The microservice to check against.
664
+ * @param base - The base path of the application; defaults to an empty string.
665
+ * @returns `true` if the page is active, `false` otherwise.
666
+ */
667
+ pageIsActive: (page, slug, microservice, base = "") => {
668
+ // Return true if microservice is provided and matches slug
669
+ if (microservice)
670
+ return slug === microservice;
671
+ // Remove trailing slash from base for consistency
672
+ const cleanBase = base.replace(/\/$/, "");
673
+ // Match exactly the base path if slug is falsy (i.e. root microservice feature)
674
+ if (!slug)
675
+ return page.url.pathname === cleanBase || page.url.pathname === `${cleanBase}/`;
676
+ // Match exact or subpath otherwise
677
+ const target = `${cleanBase}/${slug}`;
678
+ return page.url.pathname === target || page.url.pathname.startsWith(`${target}/`);
679
+ }
680
+ };