keystone-design-bootstrap 1.0.86 → 1.0.87

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 (51) hide show
  1. package/dist/company-information-C1pP-SvU.d.ts +50 -0
  2. package/dist/config-C_XBZixg.d.ts +21 -0
  3. package/dist/consumer-BWjQawiO.d.ts +48 -0
  4. package/dist/design_system/portal/index.d.ts +52 -0
  5. package/dist/design_system/portal/index.js +3113 -0
  6. package/dist/design_system/portal/index.js.map +1 -0
  7. package/dist/design_system/sections/index.d.ts +2 -1
  8. package/dist/index.d.ts +5 -24
  9. package/dist/lib/consumer-session.d.ts +16 -0
  10. package/dist/lib/consumer-session.js +85 -0
  11. package/dist/lib/consumer-session.js.map +1 -0
  12. package/dist/lib/cta-urls.d.ts +34 -0
  13. package/dist/lib/cta-urls.js +33 -0
  14. package/dist/lib/cta-urls.js.map +1 -0
  15. package/dist/lib/server-api.d.ts +2 -1
  16. package/dist/lib/server-api.js +1 -1
  17. package/dist/lib/server-api.js.map +1 -1
  18. package/dist/next/contexts/form-definitions.d.ts +17 -0
  19. package/dist/next/contexts/form-definitions.js +21 -0
  20. package/dist/next/contexts/form-definitions.js.map +1 -0
  21. package/dist/next/gallery/design-gallery.d.ts +103 -0
  22. package/dist/next/gallery/design-gallery.js +19301 -0
  23. package/dist/next/gallery/design-gallery.js.map +1 -0
  24. package/dist/next/layouts/root-layout.d.ts +55 -0
  25. package/dist/next/layouts/root-layout.js +19713 -0
  26. package/dist/next/layouts/root-layout.js.map +1 -0
  27. package/dist/next/legal/privacy-policy.d.ts +7 -0
  28. package/dist/next/legal/privacy-policy.js +18949 -0
  29. package/dist/next/legal/privacy-policy.js.map +1 -0
  30. package/dist/next/legal/terms-of-service.d.ts +7 -0
  31. package/dist/next/legal/terms-of-service.js +18949 -0
  32. package/dist/next/legal/terms-of-service.js.map +1 -0
  33. package/dist/next/providers/ssr-provider.d.ts +12 -0
  34. package/dist/next/providers/ssr-provider.js +12 -0
  35. package/dist/next/providers/ssr-provider.js.map +1 -0
  36. package/dist/next/routes/chat.d.ts +26 -0
  37. package/dist/next/routes/chat.js +160 -0
  38. package/dist/next/routes/chat.js.map +1 -0
  39. package/dist/next/routes/consumer-auth.d.ts +33 -0
  40. package/dist/next/routes/consumer-auth.js +254 -0
  41. package/dist/next/routes/consumer-auth.js.map +1 -0
  42. package/dist/next/routes/form.d.ts +37 -0
  43. package/dist/next/routes/form.js +97 -0
  44. package/dist/next/routes/form.js.map +1 -0
  45. package/dist/package-IU_GpDA0.d.ts +74 -0
  46. package/dist/types/index.d.ts +6 -68
  47. package/package.json +28 -28
  48. package/src/design_system/portal/PortalPage.tsx +3 -2
  49. package/src/lib/server-api.ts +2 -1
  50. package/src/types/rails-actioncable.d.ts +16 -0
  51. package/dist/package-DeHKpQp7.d.ts +0 -121
@@ -0,0 +1,3113 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defProps = Object.defineProperties;
3
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __spreadValues = (a, b) => {
9
+ for (var prop in b || (b = {}))
10
+ if (__hasOwnProp.call(b, prop))
11
+ __defNormalProp(a, prop, b[prop]);
12
+ if (__getOwnPropSymbols)
13
+ for (var prop of __getOwnPropSymbols(b)) {
14
+ if (__propIsEnum.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ }
17
+ return a;
18
+ };
19
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
+ var __objRest = (source, exclude) => {
21
+ var target = {};
22
+ for (var prop in source)
23
+ if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
24
+ target[prop] = source[prop];
25
+ if (source != null && __getOwnPropSymbols)
26
+ for (var prop of __getOwnPropSymbols(source)) {
27
+ if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
28
+ target[prop] = source[prop];
29
+ }
30
+ return target;
31
+ };
32
+
33
+ // src/design_system/portal/PortalPage.tsx
34
+ import React8 from "react";
35
+ import Link from "next/link";
36
+ import { cookies } from "next/headers";
37
+
38
+ // src/utils/cx.ts
39
+ import { extendTailwindMerge } from "tailwind-merge";
40
+ var twMerge = extendTailwindMerge({
41
+ extend: {
42
+ theme: {
43
+ text: ["display-xs", "display-sm", "display-md", "display-lg", "display-xl", "display-2xl"]
44
+ }
45
+ }
46
+ });
47
+ var cx = twMerge;
48
+
49
+ // src/design_system/logo/keystone-logo-minimal.tsx
50
+ var KEYSTONE_APP_URL = "https://www.keystone.app";
51
+ var KeystoneLogoMinimal = (props) => {
52
+ return /* @__PURE__ */ React.createElement(
53
+ "svg",
54
+ __spreadProps(__spreadValues({
55
+ viewBox: "0 0 36 41",
56
+ fill: "none",
57
+ "aria-hidden": "true"
58
+ }, props), {
59
+ className: cx("text-[#6ECC8B]", props.className)
60
+ }),
61
+ /* @__PURE__ */ React.createElement(
62
+ "path",
63
+ {
64
+ d: "M36 13.6677L24.0031 20.4999L36 27.3321V40.9966L11.998 27.3288V13.671L36 0.0032959V13.6677Z",
65
+ fill: "currentColor"
66
+ }
67
+ ),
68
+ /* @__PURE__ */ React.createElement(
69
+ "path",
70
+ {
71
+ d: "M11.9971 27.3288V40.9958L0 34.1644V20.4973L11.9971 27.3288Z",
72
+ fill: "currentColor"
73
+ }
74
+ ),
75
+ /* @__PURE__ */ React.createElement(
76
+ "path",
77
+ {
78
+ d: "M11.9971 13.671L0.00476074 20.4999L0 20.4973V6.83551L11.9971 0.00402832V13.671Z",
79
+ fill: "currentColor"
80
+ }
81
+ )
82
+ );
83
+ };
84
+
85
+ // src/design_system/logo/keystone-wordmark.tsx
86
+ var KeystoneWordmark = (props) => {
87
+ return /* @__PURE__ */ React.createElement(
88
+ "svg",
89
+ __spreadProps(__spreadValues({
90
+ viewBox: "0 0 104 20",
91
+ fill: "none",
92
+ "aria-hidden": "true"
93
+ }, props), {
94
+ className: cx("text-[#6ECC8B]", props.className)
95
+ }),
96
+ /* @__PURE__ */ React.createElement(
97
+ "path",
98
+ {
99
+ d: "M3.14032 0L3.1612 8.23754H3.82871L9.47236 2.94575H13.0328V3.34299L6.45695 9.52024L13.2946 16.1684V16.5656H9.53903L3.8267 11.0811H3.15919V16.5584H0V0H3.14032Z",
100
+ fill: "currentColor"
101
+ }
102
+ ),
103
+ /* @__PURE__ */ React.createElement(
104
+ "path",
105
+ {
106
+ d: "M18.4299 2.62219C22.4643 2.62219 24.8242 5.10147 24.8242 9.20756V10.6049H14.6121C14.6635 12.8878 16.0848 14.486 18.4167 14.486C20.7485 14.486 21.6835 13.2718 21.8892 12.243H24.5535V12.6403C24.1957 14.422 22.5534 16.8876 18.4552 16.8876C14.357 16.8876 11.7031 14.4462 11.7031 9.7505C11.7031 5.05478 14.3675 2.62662 18.4295 2.62662L18.4299 2.62219ZM21.9647 8.28025C21.939 6.42119 20.7365 4.9932 18.3914 4.9932C16.0463 4.9932 14.8181 6.45903 14.6506 8.28025H21.9667H21.9647Z",
107
+ fill: "currentColor"
108
+ }
109
+ ),
110
+ /* @__PURE__ */ React.createElement(
111
+ "path",
112
+ {
113
+ d: "M27.5146 2.93809L31.2554 12.994H31.459L35.1865 2.93809H38.2369V3.33534L31.565 19.9996H28.5147V19.6024L29.7417 16.5942L24.3848 3.33292V2.93567H27.5142L27.5146 2.93809Z",
114
+ fill: "currentColor"
115
+ }
116
+ ),
117
+ /* @__PURE__ */ React.createElement(
118
+ "path",
119
+ {
120
+ d: "M44.1191 2.63538C47.759 2.63538 49.8832 4.19177 50.1017 6.73947V7.13671H47.3345C47.2445 5.53847 45.965 4.98667 44.1319 4.98667C42.2989 4.98667 41.3129 5.54088 41.3129 6.5982C41.3129 7.65551 42.1338 7.9956 43.3002 8.18115L45.8103 8.57839C48.4747 9.0022 50.2796 10.1214 50.2796 12.6027C50.2796 15.084 48.3591 16.8767 44.4898 16.8767C40.6205 16.8767 38.3782 15.082 38.1855 12.6667V12.2695H41.0045C41.133 13.8677 42.5692 14.5258 44.4898 14.5258C46.4104 14.5258 47.3859 13.8391 47.3859 12.7706C47.3859 11.702 46.5521 11.2782 45.1332 11.0552L42.623 10.658C40.1129 10.2607 38.4324 9.12818 38.4324 6.66219C38.4324 4.1962 40.4944 2.6378 44.1215 2.6378H44.1195L44.1191 2.63538Z",
121
+ fill: "currentColor"
122
+ }
123
+ ),
124
+ /* @__PURE__ */ React.createElement(
125
+ "path",
126
+ {
127
+ d: "M54.2257 2.93729V0H57.2632V2.93729H62.247V5.35257H57.2632V13.7724L57.4668 13.9821H62.1679V16.5673H57.8765C55.5828 16.5673 54.2237 15.353 54.2237 13.0327V5.35217H50.2773V2.93689L54.2257 2.93729Z",
128
+ fill: "currentColor"
129
+ }
130
+ ),
131
+ /* @__PURE__ */ React.createElement(
132
+ "path",
133
+ {
134
+ d: "M69.219 2.62219C73.3325 2.62219 76.2025 5.13004 76.2025 9.74608C76.2025 14.3621 73.3325 16.8832 69.219 16.8832C65.1056 16.8832 62.25 14.3754 62.25 9.74608C62.25 5.11676 65.1052 2.62219 69.219 2.62219ZM69.219 14.4442C71.6003 14.4442 73.1265 12.7687 73.1265 9.74648C73.1265 6.72426 71.6023 5.06202 69.219 5.06202C66.8358 5.06202 65.3241 6.73754 65.3241 9.74648C65.3241 12.7554 66.8482 14.4442 69.219 14.4442Z",
135
+ fill: "currentColor"
136
+ }
137
+ ),
138
+ /* @__PURE__ */ React.createElement(
139
+ "path",
140
+ {
141
+ d: "M80.2535 2.94007V4.45661H80.4568C81.3785 3.16304 82.8641 2.64868 84.6606 2.64868C87.7989 2.64868 89.7472 4.29763 89.7472 7.66236V16.5681H86.7097V7.93846C86.7097 6.03755 85.698 5.15451 83.8907 5.15451C81.9315 5.15451 80.3539 6.30238 80.3539 8.95392V16.566H77.3164V2.94007H80.2535Z",
142
+ fill: "currentColor"
143
+ }
144
+ ),
145
+ /* @__PURE__ */ React.createElement(
146
+ "path",
147
+ {
148
+ d: "M97.6065 2.62219C101.64 2.62219 104 5.10147 104 9.20756V10.6049H93.7886C93.838 12.8878 95.261 14.486 97.5933 14.486C99.9255 14.486 100.859 13.2718 101.065 12.243H103.729V12.6403C103.371 14.422 101.73 16.8876 97.6314 16.8876C93.5328 16.8876 90.8789 14.4462 90.8789 9.7505C90.8789 5.05478 93.5437 2.62662 97.6057 2.62662L97.6065 2.62219ZM101.141 8.28025C101.115 6.42119 99.9127 4.9932 97.568 4.9932C95.2233 4.9932 93.9943 6.45903 93.8272 8.28025H101.143H101.141Z",
149
+ fill: "currentColor"
150
+ }
151
+ )
152
+ );
153
+ };
154
+
155
+ // src/design_system/logo/keystone-brand-lockup.tsx
156
+ function KeystoneBrandLockup(_a) {
157
+ var _b = _a, {
158
+ className,
159
+ iconClassName,
160
+ wordmarkClassName
161
+ } = _b, props = __objRest(_b, [
162
+ "className",
163
+ "iconClassName",
164
+ "wordmarkClassName"
165
+ ]);
166
+ return /* @__PURE__ */ React.createElement("div", __spreadProps(__spreadValues({}, props), { className: cx("flex flex-col items-start gap-2", className) }), /* @__PURE__ */ React.createElement(KeystoneLogoMinimal, { className: cx("h-4 w-auto shrink-0", iconClassName) }), /* @__PURE__ */ React.createElement(KeystoneWordmark, { className: cx("h-4 w-auto shrink-0", wordmarkClassName) }));
167
+ }
168
+
169
+ // src/design_system/portal/LogoutButton.tsx
170
+ import { useRouter } from "next/navigation";
171
+ function LogoutButton() {
172
+ const router = useRouter();
173
+ const handleLogout = async () => {
174
+ await fetch("/api/consumer/logout", { method: "POST" });
175
+ router.refresh();
176
+ };
177
+ return /* @__PURE__ */ React.createElement(
178
+ "button",
179
+ {
180
+ type: "button",
181
+ onClick: handleLogout,
182
+ className: "text-xs text-tertiary hover:text-primary transition-colors"
183
+ },
184
+ "Sign out"
185
+ );
186
+ }
187
+
188
+ // src/design_system/portal/LoginModalController.tsx
189
+ import React4, { useState as useState2, useEffect as useEffect3, useTransition } from "react";
190
+ import { useRouter as useRouter3 } from "next/navigation";
191
+
192
+ // src/design_system/elements/modal/modal.tsx
193
+ import React2, { useEffect, useRef } from "react";
194
+ var MAX_WIDTH_CLASSES = {
195
+ sm: "max-w-sm",
196
+ md: "max-w-md",
197
+ lg: "max-w-lg",
198
+ xl: "max-w-xl",
199
+ "2xl": "max-w-2xl",
200
+ "3xl": "max-w-3xl",
201
+ "4xl": "max-w-4xl",
202
+ "5xl": "max-w-5xl"
203
+ };
204
+ function Modal({
205
+ isOpen,
206
+ onClose,
207
+ title,
208
+ titleId = "modal-title",
209
+ children,
210
+ footer,
211
+ hideHeader = false,
212
+ ariaLabel,
213
+ overlayClassName,
214
+ panelClassName,
215
+ maxWidth = "md"
216
+ }) {
217
+ const overlayRef = useRef(null);
218
+ const closeButtonRef = useRef(null);
219
+ useEffect(() => {
220
+ var _a;
221
+ if (!isOpen) return;
222
+ const previouslyFocused = document.activeElement;
223
+ (_a = closeButtonRef.current) == null ? void 0 : _a.focus();
224
+ const handleEscape = (e) => {
225
+ if (e.key === "Escape") onClose();
226
+ };
227
+ document.addEventListener("keydown", handleEscape);
228
+ document.body.style.overflow = "hidden";
229
+ return () => {
230
+ document.removeEventListener("keydown", handleEscape);
231
+ document.body.style.overflow = "";
232
+ previouslyFocused == null ? void 0 : previouslyFocused.focus();
233
+ };
234
+ }, [isOpen, onClose]);
235
+ if (!isOpen) return null;
236
+ const maxWidthClass = MAX_WIDTH_CLASSES[maxWidth];
237
+ return /* @__PURE__ */ React2.createElement(
238
+ "div",
239
+ {
240
+ ref: overlayRef,
241
+ className: overlayClassName != null ? overlayClassName : "fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/50",
242
+ role: "dialog",
243
+ "aria-modal": "true",
244
+ "aria-label": ariaLabel,
245
+ "aria-labelledby": !ariaLabel && title && !hideHeader ? titleId : void 0,
246
+ onClick: (e) => e.target === overlayRef.current && onClose()
247
+ },
248
+ /* @__PURE__ */ React2.createElement(
249
+ "div",
250
+ {
251
+ className: panelClassName != null ? panelClassName : `bg-primary border border-secondary rounded-lg shadow-xl w-full overflow-hidden ${maxWidthClass} max-h-[90vh] flex flex-col`,
252
+ onClick: (e) => e.stopPropagation()
253
+ },
254
+ !hideHeader && /* @__PURE__ */ React2.createElement("div", { className: "flex items-start justify-between gap-4 p-4 md:p-6 border-b border-secondary flex-shrink-0" }, title ? /* @__PURE__ */ React2.createElement(
255
+ "h2",
256
+ {
257
+ id: titleId,
258
+ className: "font-display text-lg font-normal text-fg-primary md:text-xl flex-1 min-w-0"
259
+ },
260
+ title
261
+ ) : /* @__PURE__ */ React2.createElement("span", { className: "flex-1", "aria-hidden": true }), /* @__PURE__ */ React2.createElement(
262
+ "button",
263
+ {
264
+ ref: closeButtonRef,
265
+ type: "button",
266
+ onClick: onClose,
267
+ className: "shrink-0 p-1 text-fg-primary hover:text-brand-accent rounded focus:outline-none focus:ring-2 focus:ring-brand-accent",
268
+ "aria-label": "Close"
269
+ },
270
+ /* @__PURE__ */ React2.createElement(
271
+ "svg",
272
+ {
273
+ className: "w-5 h-5",
274
+ fill: "none",
275
+ stroke: "currentColor",
276
+ viewBox: "0 0 24 24",
277
+ "aria-hidden": true
278
+ },
279
+ /* @__PURE__ */ React2.createElement(
280
+ "path",
281
+ {
282
+ strokeLinecap: "round",
283
+ strokeLinejoin: "round",
284
+ strokeWidth: 2,
285
+ d: "M6 18L18 6M6 6l12 12"
286
+ }
287
+ )
288
+ )
289
+ )),
290
+ /* @__PURE__ */ React2.createElement("div", { className: "overflow-y-auto flex-1 p-4 md:p-6" }, children),
291
+ footer && /* @__PURE__ */ React2.createElement("div", { className: "flex-shrink-0 border-t border-secondary" }, footer)
292
+ )
293
+ );
294
+ }
295
+
296
+ // src/design_system/portal/LoginForm.tsx
297
+ import React3, { useEffect as useEffect2, useMemo, useState } from "react";
298
+ import { useRouter as useRouter2 } from "next/navigation";
299
+
300
+ // src/utils/countries.tsx
301
+ import Image from "next/image";
302
+ var countries = [
303
+ {
304
+ name: "Afghanistan",
305
+ code: "AF",
306
+ flag: "https://www.untitledui.com/images/flags/AF.svg",
307
+ phoneCode: "93",
308
+ phoneMask: "+93-##-###-####"
309
+ },
310
+ {
311
+ name: "Albania",
312
+ code: "AL",
313
+ flag: "https://www.untitledui.com/images/flags/AL.svg",
314
+ phoneCode: "355",
315
+ phoneMask: "+355 (###) ###-###"
316
+ },
317
+ {
318
+ name: "Algeria",
319
+ code: "DZ",
320
+ flag: "https://www.untitledui.com/images/flags/DZ.svg",
321
+ phoneCode: "213",
322
+ phoneMask: "+213-##-###-####"
323
+ },
324
+ {
325
+ name: "Andorra",
326
+ code: "AD",
327
+ flag: "https://www.untitledui.com/images/flags/AD.svg",
328
+ phoneCode: "376",
329
+ phoneMask: "+376-###-###"
330
+ },
331
+ {
332
+ name: "Angola",
333
+ code: "AO",
334
+ flag: "https://www.untitledui.com/images/flags/AO.svg",
335
+ phoneCode: "244",
336
+ phoneMask: "+244 (###) ###-###"
337
+ },
338
+ {
339
+ name: "Antigua and Barbuda",
340
+ code: "AG",
341
+ flag: "https://www.untitledui.com/images/flags/AG.svg",
342
+ phoneCode: "+1-268",
343
+ phoneMask: "+1 (268) ###-####"
344
+ },
345
+ {
346
+ name: "Argentina",
347
+ code: "AR",
348
+ flag: "https://www.untitledui.com/images/flags/AR.svg",
349
+ phoneCode: "54",
350
+ phoneMask: "+54 (###) ###-####"
351
+ },
352
+ {
353
+ name: "Armenia",
354
+ code: "AM",
355
+ flag: "https://www.untitledui.com/images/flags/AM.svg",
356
+ phoneCode: "374",
357
+ phoneMask: "+374-##-###-###"
358
+ },
359
+ {
360
+ name: "Australia",
361
+ code: "AU",
362
+ flag: "https://www.untitledui.com/images/flags/AU.svg",
363
+ phoneCode: "61",
364
+ phoneMask: "+61-#-####-####"
365
+ },
366
+ {
367
+ name: "Austria",
368
+ code: "AT",
369
+ flag: "https://www.untitledui.com/images/flags/AT.svg",
370
+ phoneCode: "43",
371
+ phoneMask: "+43 (###) ###-####"
372
+ },
373
+ {
374
+ name: "Azerbaijan",
375
+ code: "AZ",
376
+ flag: "https://www.untitledui.com/images/flags/AZ.svg",
377
+ phoneCode: "994",
378
+ phoneMask: "+994-##-###-##-##"
379
+ },
380
+ {
381
+ name: "Bahamas",
382
+ code: "BS",
383
+ flag: "https://www.untitledui.com/images/flags/BS.svg",
384
+ phoneCode: "+1-242",
385
+ phoneMask: "+1 (242) ###-####"
386
+ },
387
+ {
388
+ name: "Bahrain",
389
+ code: "BH",
390
+ flag: "https://www.untitledui.com/images/flags/BH.svg",
391
+ phoneCode: "973",
392
+ phoneMask: "+973-####-####"
393
+ },
394
+ {
395
+ name: "Bangladesh",
396
+ code: "BD",
397
+ flag: "https://www.untitledui.com/images/flags/BD.svg",
398
+ phoneCode: "880",
399
+ phoneMask: "+880-##-###-###"
400
+ },
401
+ {
402
+ name: "Barbados",
403
+ code: "BB",
404
+ flag: "https://www.untitledui.com/images/flags/BB.svg",
405
+ phoneCode: "+1-246",
406
+ phoneMask: "+1 (246) ###-####"
407
+ },
408
+ {
409
+ name: "Belarus",
410
+ code: "BY",
411
+ flag: "https://www.untitledui.com/images/flags/BY.svg",
412
+ phoneCode: "375",
413
+ phoneMask: "+375 (##) ###-##-##"
414
+ },
415
+ {
416
+ name: "Belgium",
417
+ code: "BE",
418
+ flag: "https://www.untitledui.com/images/flags/BE.svg",
419
+ phoneCode: "32",
420
+ phoneMask: "+32 (###) ###-###"
421
+ },
422
+ {
423
+ name: "Belize",
424
+ code: "BZ",
425
+ flag: "https://www.untitledui.com/images/flags/BZ.svg",
426
+ phoneCode: "501",
427
+ phoneMask: "+501-###-####"
428
+ },
429
+ {
430
+ name: "Benin",
431
+ code: "BJ",
432
+ flag: "https://www.untitledui.com/images/flags/BJ.svg",
433
+ phoneCode: "229",
434
+ phoneMask: "+229-##-##-####"
435
+ },
436
+ {
437
+ name: "Bhutan",
438
+ code: "BT",
439
+ flag: "https://www.untitledui.com/images/flags/BT.svg",
440
+ phoneCode: "975",
441
+ phoneMask: "+975-#-###-###"
442
+ },
443
+ {
444
+ name: "Bolivia",
445
+ code: "BO",
446
+ flag: "https://www.untitledui.com/images/flags/BO.svg",
447
+ phoneCode: "591",
448
+ phoneMask: "+591-#-###-####"
449
+ },
450
+ {
451
+ name: "Bosnia and Herzegovina",
452
+ code: "BA",
453
+ flag: "https://www.untitledui.com/images/flags/BA.svg",
454
+ phoneCode: "387",
455
+ phoneMask: "+387-##-####"
456
+ },
457
+ {
458
+ name: "Botswana",
459
+ code: "BW",
460
+ flag: "https://www.untitledui.com/images/flags/BW.svg",
461
+ phoneCode: "267",
462
+ phoneMask: "+267-##-###-###"
463
+ },
464
+ {
465
+ name: "Brazil",
466
+ code: "BR",
467
+ flag: "https://www.untitledui.com/images/flags/BR.svg",
468
+ phoneCode: "55",
469
+ phoneMask: "+55 (##) 9####-####"
470
+ },
471
+ {
472
+ name: "Brunei",
473
+ code: "BN",
474
+ flag: "https://www.untitledui.com/images/flags/BN.svg",
475
+ phoneCode: "673",
476
+ phoneMask: "+673-###-####"
477
+ },
478
+ {
479
+ name: "Bulgaria",
480
+ code: "BG",
481
+ flag: "https://www.untitledui.com/images/flags/BG.svg",
482
+ phoneCode: "359",
483
+ phoneMask: "+359 (###) ###-###"
484
+ },
485
+ {
486
+ name: "Burkina Faso",
487
+ code: "BF",
488
+ flag: "https://www.untitledui.com/images/flags/BF.svg",
489
+ phoneCode: "226",
490
+ phoneMask: "+226-##-##-####"
491
+ },
492
+ {
493
+ name: "Burundi",
494
+ code: "BI",
495
+ flag: "https://www.untitledui.com/images/flags/BI.svg",
496
+ phoneCode: "257",
497
+ phoneMask: "+257-##-##-####"
498
+ },
499
+ {
500
+ name: "Cambodia",
501
+ code: "KH",
502
+ flag: "https://www.untitledui.com/images/flags/KH.svg",
503
+ phoneCode: "855",
504
+ phoneMask: "+855-##-###-###"
505
+ },
506
+ {
507
+ name: "Cameroon",
508
+ code: "CM",
509
+ flag: "https://www.untitledui.com/images/flags/CM.svg",
510
+ phoneCode: "237",
511
+ phoneMask: "+237-####-####"
512
+ },
513
+ {
514
+ name: "Canada",
515
+ code: "CA",
516
+ flag: "https://www.untitledui.com/images/flags/CA.svg",
517
+ phoneCode: "1",
518
+ phoneMask: "+1 (###) ###-####"
519
+ },
520
+ {
521
+ name: "Central African Republic",
522
+ code: "CF",
523
+ flag: "https://www.untitledui.com/images/flags/CF.svg",
524
+ phoneCode: "236",
525
+ phoneMask: "+236-##-##-####"
526
+ },
527
+ {
528
+ name: "Chad",
529
+ code: "TD",
530
+ flag: "https://www.untitledui.com/images/flags/TD.svg",
531
+ phoneCode: "235",
532
+ phoneMask: "+235-##-##-##-##"
533
+ },
534
+ {
535
+ name: "Chile",
536
+ code: "CL",
537
+ flag: "https://www.untitledui.com/images/flags/CL.svg",
538
+ phoneCode: "56",
539
+ phoneMask: "+56-#-####-####"
540
+ },
541
+ {
542
+ name: "China",
543
+ code: "CN",
544
+ flag: "https://www.untitledui.com/images/flags/CN.svg",
545
+ phoneCode: "86",
546
+ phoneMask: "+86-##-#####-#####"
547
+ },
548
+ {
549
+ name: "Colombia",
550
+ code: "CO",
551
+ flag: "https://www.untitledui.com/images/flags/CO.svg",
552
+ phoneCode: "57",
553
+ phoneMask: "+57 (###) ###-####"
554
+ },
555
+ {
556
+ name: "Comoros",
557
+ code: "KM",
558
+ flag: "https://www.untitledui.com/images/flags/KM.svg",
559
+ phoneCode: "269",
560
+ phoneMask: "+269-##-#####"
561
+ },
562
+ {
563
+ name: "Costa Rica",
564
+ code: "CR",
565
+ flag: "https://www.untitledui.com/images/flags/CR.svg",
566
+ phoneCode: "506",
567
+ phoneMask: "+506-####-####"
568
+ },
569
+ {
570
+ name: "Croatia",
571
+ code: "HR",
572
+ flag: "https://www.untitledui.com/images/flags/HR.svg",
573
+ phoneCode: "385",
574
+ phoneMask: "+385-##-###-###"
575
+ },
576
+ {
577
+ name: "Cuba",
578
+ code: "CU",
579
+ flag: "https://www.untitledui.com/images/flags/CU.svg",
580
+ phoneCode: "53",
581
+ phoneMask: "+53-#-###-####"
582
+ },
583
+ {
584
+ name: "Cyprus",
585
+ code: "CY",
586
+ flag: "https://www.untitledui.com/images/flags/CY.svg",
587
+ phoneCode: "357",
588
+ phoneMask: "+357-##-###-###"
589
+ },
590
+ {
591
+ name: "Czech Republic",
592
+ code: "CZ",
593
+ flag: "https://www.untitledui.com/images/flags/CZ.svg",
594
+ phoneCode: "420",
595
+ phoneMask: "+420 (###) ###-###"
596
+ },
597
+ {
598
+ name: "Democratic Republic of the Congo",
599
+ code: "CD",
600
+ flag: "https://www.untitledui.com/images/flags/CD.svg",
601
+ phoneCode: "243",
602
+ phoneMask: "+243 (###) ###-###"
603
+ },
604
+ {
605
+ name: "Denmark",
606
+ code: "DK",
607
+ flag: "https://www.untitledui.com/images/flags/DK.svg",
608
+ phoneCode: "45",
609
+ phoneMask: "+45-##-##-##-##"
610
+ },
611
+ {
612
+ name: "Djibouti",
613
+ code: "DJ",
614
+ flag: "https://www.untitledui.com/images/flags/DJ.svg",
615
+ phoneCode: "253",
616
+ phoneMask: "+253-##-##-##-##"
617
+ },
618
+ {
619
+ name: "Dominica",
620
+ code: "DM",
621
+ flag: "https://www.untitledui.com/images/flags/DM.svg",
622
+ phoneCode: "+1-767",
623
+ phoneMask: "+1 (767) ###-####"
624
+ },
625
+ {
626
+ name: "Dominican Republic",
627
+ code: "DO",
628
+ flag: "https://www.untitledui.com/images/flags/DO.svg",
629
+ phoneCode: "+1-809",
630
+ phoneMask: "+1 (809) ###-####"
631
+ },
632
+ {
633
+ name: "East Timor",
634
+ code: "TL",
635
+ flag: "https://www.untitledui.com/images/flags/TL.svg",
636
+ phoneCode: "670",
637
+ phoneMask: "+670-78#-#####"
638
+ },
639
+ {
640
+ name: "Ecuador",
641
+ code: "EC",
642
+ flag: "https://www.untitledui.com/images/flags/EC.svg",
643
+ phoneCode: "593",
644
+ phoneMask: "+593-#-###-####"
645
+ },
646
+ {
647
+ name: "Egypt",
648
+ code: "EG",
649
+ flag: "https://www.untitledui.com/images/flags/EG.svg",
650
+ phoneCode: "20",
651
+ phoneMask: "+20 (###) ###-####"
652
+ },
653
+ {
654
+ name: "El Salvador",
655
+ code: "SV",
656
+ flag: "https://www.untitledui.com/images/flags/SV.svg",
657
+ phoneCode: "503",
658
+ phoneMask: "+503-##-##-####"
659
+ },
660
+ {
661
+ name: "Equatorial Guinea",
662
+ code: "GQ",
663
+ flag: "https://www.untitledui.com/images/flags/GQ.svg",
664
+ phoneCode: "240",
665
+ phoneMask: "+240-##-###-####"
666
+ },
667
+ {
668
+ name: "Eritrea",
669
+ code: "ER",
670
+ flag: "https://www.untitledui.com/images/flags/ER.svg",
671
+ phoneCode: "291",
672
+ phoneMask: "+291-#-###-###"
673
+ },
674
+ {
675
+ name: "Estonia",
676
+ code: "EE",
677
+ flag: "https://www.untitledui.com/images/flags/EE.svg",
678
+ phoneCode: "372",
679
+ phoneMask: "+372-###-####"
680
+ },
681
+ {
682
+ name: "Eswatini",
683
+ code: "SZ",
684
+ flag: "https://www.untitledui.com/images/flags/SZ.svg",
685
+ phoneCode: "268",
686
+ phoneMask: "+268-##-##-####"
687
+ },
688
+ {
689
+ name: "Ethiopia",
690
+ code: "ET",
691
+ flag: "https://www.untitledui.com/images/flags/ET.svg",
692
+ phoneCode: "251",
693
+ phoneMask: "+251-##-###-####"
694
+ },
695
+ {
696
+ name: "Fiji",
697
+ code: "FJ",
698
+ flag: "https://www.untitledui.com/images/flags/FJ.svg",
699
+ phoneCode: "679",
700
+ phoneMask: "+679-##-#####"
701
+ },
702
+ {
703
+ name: "Finland",
704
+ code: "FI",
705
+ flag: "https://www.untitledui.com/images/flags/FI.svg",
706
+ phoneCode: "358",
707
+ phoneMask: "+358 (###) ###-##-##"
708
+ },
709
+ {
710
+ name: "France",
711
+ code: "FR",
712
+ flag: "https://www.untitledui.com/images/flags/FR.svg",
713
+ phoneCode: "33",
714
+ phoneMask: "+590 (###) ###-###"
715
+ },
716
+ {
717
+ name: "Gabon",
718
+ code: "GA",
719
+ flag: "https://www.untitledui.com/images/flags/GA.svg",
720
+ phoneCode: "241",
721
+ phoneMask: "+241-#-##-##-##"
722
+ },
723
+ {
724
+ name: "Gambia",
725
+ code: "GM",
726
+ flag: "https://www.untitledui.com/images/flags/GM.svg",
727
+ phoneCode: "220",
728
+ phoneMask: "+220 (###) ##-##"
729
+ },
730
+ {
731
+ name: "Georgia",
732
+ code: "GE",
733
+ flag: "https://www.untitledui.com/images/flags/GE.svg",
734
+ phoneCode: "995",
735
+ phoneMask: "+995 (###) ###-###"
736
+ },
737
+ {
738
+ name: "Germany",
739
+ code: "DE",
740
+ flag: "https://www.untitledui.com/images/flags/DE.svg",
741
+ phoneCode: "49",
742
+ phoneMask: "+49-###-###"
743
+ },
744
+ {
745
+ name: "Ghana",
746
+ code: "GH",
747
+ flag: "https://www.untitledui.com/images/flags/GH.svg",
748
+ phoneCode: "233",
749
+ phoneMask: "+233 (###) ###-###"
750
+ },
751
+ {
752
+ name: "Greece",
753
+ code: "GR",
754
+ flag: "https://www.untitledui.com/images/flags/GR.svg",
755
+ phoneCode: "30",
756
+ phoneMask: "+30 (###) ###-####"
757
+ },
758
+ {
759
+ name: "Grenada",
760
+ code: "GD",
761
+ flag: "https://www.untitledui.com/images/flags/GD.svg",
762
+ phoneCode: "+1-473",
763
+ phoneMask: "+1 (473) ###-####"
764
+ },
765
+ {
766
+ name: "Guatemala",
767
+ code: "GT",
768
+ flag: "https://www.untitledui.com/images/flags/GT.svg",
769
+ phoneCode: "502",
770
+ phoneMask: "+502-#-###-####"
771
+ },
772
+ {
773
+ name: "Guinea",
774
+ code: "GN",
775
+ flag: "https://www.untitledui.com/images/flags/GN.svg",
776
+ phoneCode: "224",
777
+ phoneMask: "+224-##-###-###"
778
+ },
779
+ {
780
+ name: "Guinea-Bissau",
781
+ code: "GW",
782
+ flag: "https://www.untitledui.com/images/flags/GW.svg",
783
+ phoneCode: "245",
784
+ phoneMask: "+245-#-######"
785
+ },
786
+ {
787
+ name: "Guyana",
788
+ code: "GY",
789
+ flag: "https://www.untitledui.com/images/flags/GY.svg",
790
+ phoneCode: "592",
791
+ phoneMask: "+592-###-####"
792
+ },
793
+ {
794
+ name: "Haiti",
795
+ code: "HT",
796
+ flag: "https://www.untitledui.com/images/flags/HT.svg",
797
+ phoneCode: "509",
798
+ phoneMask: "+509-##-##-####"
799
+ },
800
+ {
801
+ name: "Honduras",
802
+ code: "HN",
803
+ flag: "https://www.untitledui.com/images/flags/HN.svg",
804
+ phoneCode: "504",
805
+ phoneMask: "+504-####-####"
806
+ },
807
+ {
808
+ name: "Hungary",
809
+ code: "HU",
810
+ flag: "https://www.untitledui.com/images/flags/HU.svg",
811
+ phoneCode: "36",
812
+ phoneMask: "+36 (###) ###-###"
813
+ },
814
+ {
815
+ name: "Iceland",
816
+ code: "IS",
817
+ flag: "https://www.untitledui.com/images/flags/IS.svg",
818
+ phoneCode: "354",
819
+ phoneMask: "+354-###-####"
820
+ },
821
+ {
822
+ name: "India",
823
+ code: "IN",
824
+ flag: "https://www.untitledui.com/images/flags/IN.svg",
825
+ phoneCode: "91",
826
+ phoneMask: "+91 (####) ###-###"
827
+ },
828
+ {
829
+ name: "Indonesia",
830
+ code: "ID",
831
+ flag: "https://www.untitledui.com/images/flags/ID.svg",
832
+ phoneCode: "62",
833
+ phoneMask: "+62 (8##) ###-##-###"
834
+ },
835
+ {
836
+ name: "Iran",
837
+ code: "IR",
838
+ flag: "https://www.untitledui.com/images/flags/IR.svg",
839
+ phoneCode: "98",
840
+ phoneMask: "+98 (###) ###-####"
841
+ },
842
+ {
843
+ name: "Iraq",
844
+ code: "IQ",
845
+ flag: "https://www.untitledui.com/images/flags/IQ.svg",
846
+ phoneCode: "964",
847
+ phoneMask: "+964 (###) ###-####"
848
+ },
849
+ {
850
+ name: "Ireland",
851
+ code: "IE",
852
+ flag: "https://www.untitledui.com/images/flags/IE.svg",
853
+ phoneCode: "353",
854
+ phoneMask: "+353 (###) ###-###"
855
+ },
856
+ {
857
+ name: "Israel",
858
+ code: "IL",
859
+ flag: "https://www.untitledui.com/images/flags/IL.svg",
860
+ phoneCode: "972",
861
+ phoneMask: "+972-#-###-####"
862
+ },
863
+ {
864
+ name: "Italy",
865
+ code: "IT",
866
+ flag: "https://www.untitledui.com/images/flags/IT.svg",
867
+ phoneCode: "39",
868
+ phoneMask: "+39 (###) ####-###"
869
+ },
870
+ {
871
+ name: "Jamaica",
872
+ code: "JM",
873
+ flag: "https://www.untitledui.com/images/flags/JM.svg",
874
+ phoneCode: "+1-876",
875
+ phoneMask: "+1 (876) ###-####"
876
+ },
877
+ {
878
+ name: "Japan",
879
+ code: "JP",
880
+ flag: "https://www.untitledui.com/images/flags/JP.svg",
881
+ phoneCode: "81",
882
+ phoneMask: "+81 (###) ###-###"
883
+ },
884
+ {
885
+ name: "Jordan",
886
+ code: "JO",
887
+ flag: "https://www.untitledui.com/images/flags/JO.svg",
888
+ phoneCode: "962",
889
+ phoneMask: "+962-#-####-####"
890
+ },
891
+ {
892
+ name: "Kazakhstan",
893
+ code: "KZ",
894
+ flag: "https://www.untitledui.com/images/flags/KZ.svg",
895
+ phoneCode: "7",
896
+ phoneMask: "+7 (7##) ###-##-##"
897
+ },
898
+ {
899
+ name: "Kenya",
900
+ code: "KE",
901
+ flag: "https://www.untitledui.com/images/flags/KE.svg",
902
+ phoneCode: "254",
903
+ phoneMask: "+254-###-######"
904
+ },
905
+ {
906
+ name: "Kiribati",
907
+ code: "KI",
908
+ flag: "https://www.untitledui.com/images/flags/KI.svg",
909
+ phoneCode: "686",
910
+ phoneMask: "+686-##-###"
911
+ },
912
+ {
913
+ name: "Kuwait",
914
+ code: "KW",
915
+ flag: "https://www.untitledui.com/images/flags/KW.svg",
916
+ phoneCode: "965",
917
+ phoneMask: "+965-####-####"
918
+ },
919
+ {
920
+ name: "Kyrgyzstan",
921
+ code: "KG",
922
+ flag: "https://www.untitledui.com/images/flags/KG.svg",
923
+ phoneCode: "996",
924
+ phoneMask: "+996 (###) ###-###"
925
+ },
926
+ {
927
+ name: "Laos",
928
+ code: "LA",
929
+ flag: "https://www.untitledui.com/images/flags/LA.svg",
930
+ phoneCode: "856",
931
+ phoneMask: "+856-##-###-###"
932
+ },
933
+ {
934
+ name: "Latvia",
935
+ code: "LV",
936
+ flag: "https://www.untitledui.com/images/flags/LV.svg",
937
+ phoneCode: "371",
938
+ phoneMask: "+371-##-###-###"
939
+ },
940
+ {
941
+ name: "Lebanon",
942
+ code: "LB",
943
+ flag: "https://www.untitledui.com/images/flags/LB.svg",
944
+ phoneCode: "961",
945
+ phoneMask: "+961-#-###-###"
946
+ },
947
+ {
948
+ name: "Lesotho",
949
+ code: "LS",
950
+ flag: "https://www.untitledui.com/images/flags/LS.svg",
951
+ phoneCode: "266",
952
+ phoneMask: "+266-#-###-####"
953
+ },
954
+ {
955
+ name: "Liberia",
956
+ code: "LR",
957
+ flag: "https://www.untitledui.com/images/flags/LR.svg",
958
+ phoneCode: "231",
959
+ phoneMask: "+231-##-###-###"
960
+ },
961
+ {
962
+ name: "Libya",
963
+ code: "LY",
964
+ flag: "https://www.untitledui.com/images/flags/LY.svg",
965
+ phoneCode: "218",
966
+ phoneMask: "+218-21-###-####"
967
+ },
968
+ {
969
+ name: "Liechtenstein",
970
+ code: "LI",
971
+ flag: "https://www.untitledui.com/images/flags/LI.svg",
972
+ phoneCode: "423",
973
+ phoneMask: "+423 (###) ###-####"
974
+ },
975
+ {
976
+ name: "Lithuania",
977
+ code: "LT",
978
+ flag: "https://www.untitledui.com/images/flags/LT.svg",
979
+ phoneCode: "370",
980
+ phoneMask: "+370 (###) ##-###"
981
+ },
982
+ {
983
+ name: "Luxembourg",
984
+ code: "LU",
985
+ flag: "https://www.untitledui.com/images/flags/LU.svg",
986
+ phoneCode: "352",
987
+ phoneMask: "+352 (###) ###-###"
988
+ },
989
+ {
990
+ name: "Madagascar",
991
+ code: "MG",
992
+ flag: "https://www.untitledui.com/images/flags/MG.svg",
993
+ phoneCode: "261",
994
+ phoneMask: "+261-##-##-#####"
995
+ },
996
+ {
997
+ name: "Malawi",
998
+ code: "MW",
999
+ flag: "https://www.untitledui.com/images/flags/MW.svg",
1000
+ phoneCode: "265",
1001
+ phoneMask: "+265-#-####-####"
1002
+ },
1003
+ {
1004
+ name: "Malaysia",
1005
+ code: "MY",
1006
+ flag: "https://www.untitledui.com/images/flags/MY.svg",
1007
+ phoneCode: "60",
1008
+ phoneMask: "+60-#-###-###"
1009
+ },
1010
+ {
1011
+ name: "Maldives",
1012
+ code: "MV",
1013
+ flag: "https://www.untitledui.com/images/flags/MV.svg",
1014
+ phoneCode: "960",
1015
+ phoneMask: "+960-###-####"
1016
+ },
1017
+ {
1018
+ name: "Mali",
1019
+ code: "ML",
1020
+ flag: "https://www.untitledui.com/images/flags/ML.svg",
1021
+ phoneCode: "223",
1022
+ phoneMask: "+223-##-##-####"
1023
+ },
1024
+ {
1025
+ name: "Malta",
1026
+ code: "MT",
1027
+ flag: "https://www.untitledui.com/images/flags/MT.svg",
1028
+ phoneCode: "356",
1029
+ phoneMask: "+356-####-####"
1030
+ },
1031
+ {
1032
+ name: "Marshall Islands",
1033
+ code: "MH",
1034
+ flag: "https://www.untitledui.com/images/flags/MH.svg",
1035
+ phoneCode: "692",
1036
+ phoneMask: "+692-###-####"
1037
+ },
1038
+ {
1039
+ name: "Mauritania",
1040
+ code: "MR",
1041
+ flag: "https://www.untitledui.com/images/flags/MR.svg",
1042
+ phoneCode: "222",
1043
+ phoneMask: "+222-##-##-####"
1044
+ },
1045
+ {
1046
+ name: "Mauritius",
1047
+ code: "MU",
1048
+ flag: "https://www.untitledui.com/images/flags/MU.svg",
1049
+ phoneCode: "230",
1050
+ phoneMask: "+230-###-####"
1051
+ },
1052
+ {
1053
+ name: "Mexico",
1054
+ code: "MX",
1055
+ flag: "https://www.untitledui.com/images/flags/MX.svg",
1056
+ phoneCode: "52",
1057
+ phoneMask: "+52-##-##-####"
1058
+ },
1059
+ {
1060
+ name: "Micronesia",
1061
+ code: "FM",
1062
+ flag: "https://www.untitledui.com/images/flags/FM.svg",
1063
+ phoneCode: "691",
1064
+ phoneMask: "+691-###-####"
1065
+ },
1066
+ {
1067
+ name: "Moldova",
1068
+ code: "MD",
1069
+ flag: "https://www.untitledui.com/images/flags/MD.svg",
1070
+ phoneCode: "373",
1071
+ phoneMask: "+373-####-####"
1072
+ },
1073
+ {
1074
+ name: "Monaco",
1075
+ code: "MC",
1076
+ flag: "https://www.untitledui.com/images/flags/MC.svg",
1077
+ phoneCode: "377",
1078
+ phoneMask: "+377-##-###-###"
1079
+ },
1080
+ {
1081
+ name: "Mongolia",
1082
+ code: "MN",
1083
+ flag: "https://www.untitledui.com/images/flags/MN.svg",
1084
+ phoneCode: "976",
1085
+ phoneMask: "+976-##-##-####"
1086
+ },
1087
+ {
1088
+ name: "Montenegro",
1089
+ code: "ME",
1090
+ flag: "https://www.untitledui.com/images/flags/ME.svg",
1091
+ phoneCode: "382",
1092
+ phoneMask: "+382-##-###-###"
1093
+ },
1094
+ {
1095
+ name: "Morocco",
1096
+ code: "MA",
1097
+ flag: "https://www.untitledui.com/images/flags/MA.svg",
1098
+ phoneCode: "212",
1099
+ phoneMask: "+212-##-####-###"
1100
+ },
1101
+ {
1102
+ name: "Mozambique",
1103
+ code: "MZ",
1104
+ flag: "https://www.untitledui.com/images/flags/MZ.svg",
1105
+ phoneCode: "258",
1106
+ phoneMask: "+258-##-###-###"
1107
+ },
1108
+ {
1109
+ name: "Myanmar",
1110
+ code: "MM",
1111
+ flag: "https://www.untitledui.com/images/flags/MM.svg",
1112
+ phoneCode: "95",
1113
+ phoneMask: "+95-###-###"
1114
+ },
1115
+ {
1116
+ name: "Namibia",
1117
+ code: "NA",
1118
+ flag: "https://www.untitledui.com/images/flags/NA.svg",
1119
+ phoneCode: "264",
1120
+ phoneMask: "+264-##-###-####"
1121
+ },
1122
+ {
1123
+ name: "Nauru",
1124
+ code: "NR",
1125
+ flag: "https://www.untitledui.com/images/flags/NR.svg",
1126
+ phoneCode: "674",
1127
+ phoneMask: "+674-###-####"
1128
+ },
1129
+ {
1130
+ name: "Nepal",
1131
+ code: "NP",
1132
+ flag: "https://www.untitledui.com/images/flags/NP.svg",
1133
+ phoneCode: "977",
1134
+ phoneMask: "+977-##-###-###"
1135
+ },
1136
+ {
1137
+ name: "Netherlands",
1138
+ code: "NL",
1139
+ flag: "https://www.untitledui.com/images/flags/NL.svg",
1140
+ phoneCode: "31",
1141
+ phoneMask: "+31-##-###-####"
1142
+ },
1143
+ {
1144
+ name: "New Zealand",
1145
+ code: "NZ",
1146
+ flag: "https://www.untitledui.com/images/flags/NZ.svg",
1147
+ phoneCode: "64",
1148
+ phoneMask: "+64 (###) ###-####"
1149
+ },
1150
+ {
1151
+ name: "Nicaragua",
1152
+ code: "NI",
1153
+ flag: "https://www.untitledui.com/images/flags/NI.svg",
1154
+ phoneCode: "505",
1155
+ phoneMask: "+505-####-####"
1156
+ },
1157
+ {
1158
+ name: "Niger",
1159
+ code: "NE",
1160
+ flag: "https://www.untitledui.com/images/flags/NE.svg",
1161
+ phoneCode: "227",
1162
+ phoneMask: "+227-##-##-####"
1163
+ },
1164
+ {
1165
+ name: "Nigeria",
1166
+ code: "NG",
1167
+ flag: "https://www.untitledui.com/images/flags/NG.svg",
1168
+ phoneCode: "234",
1169
+ phoneMask: "+234 (###) ###-####"
1170
+ },
1171
+ {
1172
+ name: "North Korea",
1173
+ code: "KP",
1174
+ flag: "https://www.untitledui.com/images/flags/KP.svg",
1175
+ phoneCode: "850",
1176
+ phoneMask: "+850-####-#############"
1177
+ },
1178
+ {
1179
+ name: "North Macedonia",
1180
+ code: "MK",
1181
+ flag: "https://www.untitledui.com/images/flags/MK.svg",
1182
+ phoneCode: "389",
1183
+ phoneMask: "+389-##-###-###"
1184
+ },
1185
+ {
1186
+ name: "Norway",
1187
+ code: "NO",
1188
+ flag: "https://www.untitledui.com/images/flags/NO.svg",
1189
+ phoneCode: "47",
1190
+ phoneMask: "+47 (###) ##-###"
1191
+ },
1192
+ {
1193
+ name: "Oman",
1194
+ code: "OM",
1195
+ flag: "https://www.untitledui.com/images/flags/OM.svg",
1196
+ phoneCode: "968",
1197
+ phoneMask: "+968-##-###-###"
1198
+ },
1199
+ {
1200
+ name: "Pakistan",
1201
+ code: "PK",
1202
+ flag: "https://www.untitledui.com/images/flags/PK.svg",
1203
+ phoneCode: "92",
1204
+ phoneMask: "+92 (###) ###-####"
1205
+ },
1206
+ {
1207
+ name: "Palau",
1208
+ code: "PW",
1209
+ flag: "https://www.untitledui.com/images/flags/PW.svg",
1210
+ phoneCode: "680",
1211
+ phoneMask: "+680-###-####"
1212
+ },
1213
+ {
1214
+ name: "Panama",
1215
+ code: "PA",
1216
+ flag: "https://www.untitledui.com/images/flags/PA.svg",
1217
+ phoneCode: "507",
1218
+ phoneMask: "+507-###-####"
1219
+ },
1220
+ {
1221
+ name: "Papua New Guinea",
1222
+ code: "PG",
1223
+ flag: "https://www.untitledui.com/images/flags/PG.svg",
1224
+ phoneCode: "675",
1225
+ phoneMask: "+675 (###) ##-###"
1226
+ },
1227
+ {
1228
+ name: "Paraguay",
1229
+ code: "PY",
1230
+ flag: "https://www.untitledui.com/images/flags/PY.svg",
1231
+ phoneCode: "595",
1232
+ phoneMask: "+595 (###) ###-###"
1233
+ },
1234
+ {
1235
+ name: "Peru",
1236
+ code: "PE",
1237
+ flag: "https://www.untitledui.com/images/flags/PE.svg",
1238
+ phoneCode: "51",
1239
+ phoneMask: "+51 (###) ###-###"
1240
+ },
1241
+ {
1242
+ name: "Philippines",
1243
+ code: "PH",
1244
+ flag: "https://www.untitledui.com/images/flags/PH.svg",
1245
+ phoneCode: "63",
1246
+ phoneMask: "+63 (###) ###-####"
1247
+ },
1248
+ {
1249
+ name: "Poland",
1250
+ code: "PL",
1251
+ flag: "https://www.untitledui.com/images/flags/PL.svg",
1252
+ phoneCode: "48",
1253
+ phoneMask: "+48 (###) ###-###"
1254
+ },
1255
+ {
1256
+ name: "Portugal",
1257
+ code: "PT",
1258
+ flag: "https://www.untitledui.com/images/flags/PT.svg",
1259
+ phoneCode: "351",
1260
+ phoneMask: "+351-##-###-####"
1261
+ },
1262
+ {
1263
+ name: "Qatar",
1264
+ code: "QA",
1265
+ flag: "https://www.untitledui.com/images/flags/QA.svg",
1266
+ phoneCode: "974",
1267
+ phoneMask: "+974-####-####"
1268
+ },
1269
+ {
1270
+ name: "Romania",
1271
+ code: "RO",
1272
+ flag: "https://www.untitledui.com/images/flags/RO.svg",
1273
+ phoneCode: "40",
1274
+ phoneMask: "+40-##-###-####"
1275
+ },
1276
+ {
1277
+ name: "Russia",
1278
+ code: "RU",
1279
+ flag: "https://www.untitledui.com/images/flags/RU.svg",
1280
+ phoneCode: "7",
1281
+ phoneMask: "+7 (###) ###-##-##"
1282
+ },
1283
+ {
1284
+ name: "Rwanda",
1285
+ code: "RW",
1286
+ flag: "https://www.untitledui.com/images/flags/RW.svg",
1287
+ phoneCode: "250",
1288
+ phoneMask: "+250 (###) ###-###"
1289
+ },
1290
+ {
1291
+ name: "Saint Kitts and Nevis",
1292
+ code: "KN",
1293
+ flag: "https://www.untitledui.com/images/flags/KN.svg",
1294
+ phoneCode: "+1-869",
1295
+ phoneMask: "+1 (869) ###-####"
1296
+ },
1297
+ {
1298
+ name: "Saint Lucia",
1299
+ code: "LC",
1300
+ flag: "https://www.untitledui.com/images/flags/LC.svg",
1301
+ phoneCode: "+1-758",
1302
+ phoneMask: "+1 (758) ###-####"
1303
+ },
1304
+ {
1305
+ name: "Saint Vincent and the Grenadines",
1306
+ code: "VC",
1307
+ flag: "https://www.untitledui.com/images/flags/VC.svg",
1308
+ phoneCode: "+1-784",
1309
+ phoneMask: "+1 (784) ###-####"
1310
+ },
1311
+ {
1312
+ name: "Samoa",
1313
+ code: "WS",
1314
+ flag: "https://www.untitledui.com/images/flags/WS.svg",
1315
+ phoneCode: "685",
1316
+ phoneMask: "+685-##-####"
1317
+ },
1318
+ {
1319
+ name: "San Marino",
1320
+ code: "SM",
1321
+ flag: "https://www.untitledui.com/images/flags/SM.svg",
1322
+ phoneCode: "378",
1323
+ phoneMask: "+378-####-######"
1324
+ },
1325
+ {
1326
+ name: "Sao Tome and Principe",
1327
+ code: "ST",
1328
+ flag: "https://www.untitledui.com/images/flags/ST.svg",
1329
+ phoneCode: "239",
1330
+ phoneMask: "+239-##-#####"
1331
+ },
1332
+ {
1333
+ name: "Saudi Arabia",
1334
+ code: "SA",
1335
+ flag: "https://www.untitledui.com/images/flags/SA.svg",
1336
+ phoneCode: "966",
1337
+ phoneMask: "+966-#-###-####"
1338
+ },
1339
+ {
1340
+ name: "Senegal",
1341
+ code: "SN",
1342
+ flag: "https://www.untitledui.com/images/flags/SN.svg",
1343
+ phoneCode: "221",
1344
+ phoneMask: "+221-##-###-####"
1345
+ },
1346
+ {
1347
+ name: "Serbia",
1348
+ code: "RS",
1349
+ flag: "https://www.untitledui.com/images/flags/RS.svg",
1350
+ phoneCode: "381",
1351
+ phoneMask: "+381-##-###-####"
1352
+ },
1353
+ {
1354
+ name: "Seychelles",
1355
+ code: "SC",
1356
+ flag: "https://www.untitledui.com/images/flags/SC.svg",
1357
+ phoneCode: "248",
1358
+ phoneMask: "+248-#-###-###"
1359
+ },
1360
+ {
1361
+ name: "Sierra Leone",
1362
+ code: "SL",
1363
+ flag: "https://www.untitledui.com/images/flags/SL.svg",
1364
+ phoneCode: "232",
1365
+ phoneMask: "+232-##-######"
1366
+ },
1367
+ {
1368
+ name: "Singapore",
1369
+ code: "SG",
1370
+ flag: "https://www.untitledui.com/images/flags/SG.svg",
1371
+ phoneCode: "65",
1372
+ phoneMask: "+65-####-####"
1373
+ },
1374
+ {
1375
+ name: "Slovakia",
1376
+ code: "SK",
1377
+ flag: "https://www.untitledui.com/images/flags/SK.svg",
1378
+ phoneCode: "421",
1379
+ phoneMask: "+421 (###) ###-###"
1380
+ },
1381
+ {
1382
+ name: "Slovenia",
1383
+ code: "SI",
1384
+ flag: "https://www.untitledui.com/images/flags/SI.svg",
1385
+ phoneCode: "386",
1386
+ phoneMask: "+386-##-###-###"
1387
+ },
1388
+ {
1389
+ name: "Solomon Islands",
1390
+ code: "SB",
1391
+ flag: "https://www.untitledui.com/images/flags/SB.svg",
1392
+ phoneCode: "677",
1393
+ phoneMask: "+677-#####"
1394
+ },
1395
+ {
1396
+ name: "Somalia",
1397
+ code: "SO",
1398
+ flag: "https://www.untitledui.com/images/flags/SO.svg",
1399
+ phoneCode: "252",
1400
+ phoneMask: "+252-#-###-###"
1401
+ },
1402
+ {
1403
+ name: "South Africa",
1404
+ code: "ZA",
1405
+ flag: "https://www.untitledui.com/images/flags/ZA.svg",
1406
+ phoneCode: "27",
1407
+ phoneMask: "+27-##-###-####"
1408
+ },
1409
+ {
1410
+ name: "South Korea",
1411
+ code: "KR",
1412
+ flag: "https://www.untitledui.com/images/flags/KR.svg",
1413
+ phoneCode: "82",
1414
+ phoneMask: "+82-##-###-####"
1415
+ },
1416
+ {
1417
+ name: "South Sudan",
1418
+ code: "SS",
1419
+ flag: "https://www.untitledui.com/images/flags/SS.svg",
1420
+ phoneCode: "211",
1421
+ phoneMask: "+211-##-###-####"
1422
+ },
1423
+ {
1424
+ name: "Spain",
1425
+ code: "ES",
1426
+ flag: "https://www.untitledui.com/images/flags/ES.svg",
1427
+ phoneCode: "34",
1428
+ phoneMask: "+34 (###) ###-###"
1429
+ },
1430
+ {
1431
+ name: "Sri Lanka",
1432
+ code: "LK",
1433
+ flag: "https://www.untitledui.com/images/flags/LK.svg",
1434
+ phoneCode: "94",
1435
+ phoneMask: "+94-##-###-####"
1436
+ },
1437
+ {
1438
+ name: "Suriname",
1439
+ code: "SR",
1440
+ flag: "https://www.untitledui.com/images/flags/SR.svg",
1441
+ phoneCode: "597",
1442
+ phoneMask: "+597-###-###"
1443
+ },
1444
+ {
1445
+ name: "Sweden",
1446
+ code: "SE",
1447
+ flag: "https://www.untitledui.com/images/flags/SE.svg",
1448
+ phoneCode: "46",
1449
+ phoneMask: "+46-##-###-####"
1450
+ },
1451
+ {
1452
+ name: "Switzerland",
1453
+ code: "CH",
1454
+ flag: "https://www.untitledui.com/images/flags/CH.svg",
1455
+ phoneCode: "41",
1456
+ phoneMask: "+41-##-###-####"
1457
+ },
1458
+ {
1459
+ name: "Syria",
1460
+ code: "SY",
1461
+ flag: "https://www.untitledui.com/images/flags/SY.svg",
1462
+ phoneCode: "963",
1463
+ phoneMask: "+963-##-####-###"
1464
+ },
1465
+ {
1466
+ name: "Tajikistan",
1467
+ code: "TJ",
1468
+ flag: "https://www.untitledui.com/images/flags/TJ.svg",
1469
+ phoneCode: "992",
1470
+ phoneMask: "+992-##-###-####"
1471
+ },
1472
+ {
1473
+ name: "Tanzania",
1474
+ code: "TZ",
1475
+ flag: "https://www.untitledui.com/images/flags/TZ.svg",
1476
+ phoneCode: "255",
1477
+ phoneMask: "+255-##-###-####"
1478
+ },
1479
+ {
1480
+ name: "Thailand",
1481
+ code: "TH",
1482
+ flag: "https://www.untitledui.com/images/flags/TH.svg",
1483
+ phoneCode: "66",
1484
+ phoneMask: "+66-##-###-###"
1485
+ },
1486
+ {
1487
+ name: "Togo",
1488
+ code: "TG",
1489
+ flag: "https://www.untitledui.com/images/flags/TG.svg",
1490
+ phoneCode: "228",
1491
+ phoneMask: "+228-##-###-###"
1492
+ },
1493
+ {
1494
+ name: "Tonga",
1495
+ code: "TO",
1496
+ flag: "https://www.untitledui.com/images/flags/TO.svg",
1497
+ phoneCode: "676",
1498
+ phoneMask: "+676-#####"
1499
+ },
1500
+ {
1501
+ name: "Trinidad and Tobago",
1502
+ code: "TT",
1503
+ flag: "https://www.untitledui.com/images/flags/TT.svg",
1504
+ phoneCode: "+1-868",
1505
+ phoneMask: "+1 (868) ###-####"
1506
+ },
1507
+ {
1508
+ name: "Tunisia",
1509
+ code: "TN",
1510
+ flag: "https://www.untitledui.com/images/flags/TN.svg",
1511
+ phoneCode: "216",
1512
+ phoneMask: "+216-##-###-###"
1513
+ },
1514
+ {
1515
+ name: "Turkey",
1516
+ code: "TR",
1517
+ flag: "https://www.untitledui.com/images/flags/TR.svg",
1518
+ phoneCode: "90",
1519
+ phoneMask: "+90 (###) ###-####"
1520
+ },
1521
+ {
1522
+ name: "Turkmenistan",
1523
+ code: "TM",
1524
+ flag: "https://www.untitledui.com/images/flags/TM.svg",
1525
+ phoneCode: "993",
1526
+ phoneMask: "+993-#-###-####"
1527
+ },
1528
+ {
1529
+ name: "Tuvalu",
1530
+ code: "TV",
1531
+ flag: "https://www.untitledui.com/images/flags/TV.svg",
1532
+ phoneCode: "688",
1533
+ phoneMask: "+688-2####"
1534
+ },
1535
+ {
1536
+ name: "Uganda",
1537
+ code: "UG",
1538
+ flag: "https://www.untitledui.com/images/flags/UG.svg",
1539
+ phoneCode: "256",
1540
+ phoneMask: "+256 (###) ###-###"
1541
+ },
1542
+ {
1543
+ name: "Ukraine",
1544
+ code: "UA",
1545
+ flag: "https://www.untitledui.com/images/flags/UA.svg",
1546
+ phoneCode: "380",
1547
+ phoneMask: "+380 (##) ###-##-##"
1548
+ },
1549
+ {
1550
+ name: "United Arab Emirates",
1551
+ code: "AE",
1552
+ flag: "https://www.untitledui.com/images/flags/AE.svg",
1553
+ phoneCode: "971",
1554
+ phoneMask: "+971-#-###-####"
1555
+ },
1556
+ {
1557
+ name: "United Kingdom",
1558
+ code: "GB",
1559
+ flag: "https://www.untitledui.com/images/flags/GB.svg",
1560
+ phoneCode: "44"
1561
+ },
1562
+ {
1563
+ name: "United States",
1564
+ code: "US",
1565
+ flag: "https://www.untitledui.com/images/flags/US.svg",
1566
+ phoneCode: "1",
1567
+ phoneMask: "+1 (###) ###-####"
1568
+ },
1569
+ {
1570
+ name: "Uruguay",
1571
+ code: "UY",
1572
+ flag: "https://www.untitledui.com/images/flags/UY.svg",
1573
+ phoneCode: "598",
1574
+ phoneMask: "+598-#-###-##-##"
1575
+ },
1576
+ {
1577
+ name: "Uzbekistan",
1578
+ code: "UZ",
1579
+ flag: "https://www.untitledui.com/images/flags/UZ.svg",
1580
+ phoneCode: "998",
1581
+ phoneMask: "+998-##-###-####"
1582
+ },
1583
+ {
1584
+ name: "Vanuatu",
1585
+ code: "VU",
1586
+ flag: "https://www.untitledui.com/images/flags/VU.svg",
1587
+ phoneCode: "678",
1588
+ phoneMask: "+678-#####"
1589
+ },
1590
+ {
1591
+ name: "Venezuela",
1592
+ code: "VE",
1593
+ flag: "https://www.untitledui.com/images/flags/VE.svg",
1594
+ phoneCode: "58",
1595
+ phoneMask: "+58 (###) ###-####"
1596
+ },
1597
+ {
1598
+ name: "Vietnam",
1599
+ code: "VN",
1600
+ flag: "https://www.untitledui.com/images/flags/VN.svg",
1601
+ phoneCode: "84",
1602
+ phoneMask: "+84 (###) ####-###"
1603
+ },
1604
+ {
1605
+ name: "Yemen",
1606
+ code: "YE",
1607
+ flag: "https://www.untitledui.com/images/flags/YE.svg",
1608
+ phoneCode: "967",
1609
+ phoneMask: "+967-##-###-###"
1610
+ },
1611
+ {
1612
+ name: "Zambia",
1613
+ code: "ZM",
1614
+ flag: "https://www.untitledui.com/images/flags/ZM.svg",
1615
+ phoneCode: "260",
1616
+ phoneMask: "+260-##-###-####"
1617
+ },
1618
+ {
1619
+ name: "Zimbabwe",
1620
+ code: "ZW",
1621
+ flag: "https://www.untitledui.com/images/flags/ZW.svg",
1622
+ phoneCode: "263",
1623
+ phoneMask: "+263-#-######"
1624
+ }
1625
+ ];
1626
+ var phoneCodeOptions = countries.map((country) => ({
1627
+ id: country.code,
1628
+ label: country.code
1629
+ }));
1630
+ var countriesOptions = countries.map((country) => ({
1631
+ id: country.code,
1632
+ label: country.name,
1633
+ icon: (props) => {
1634
+ const restProps = __objRest(props, []);
1635
+ return /* @__PURE__ */ React.createElement(Image, __spreadProps(__spreadValues({}, restProps), { src: country.flag, alt: `${country.name} flag`, width: 20, height: 15 }));
1636
+ }
1637
+ }));
1638
+
1639
+ // src/utils/phone-helpers.ts
1640
+ function getNationalMask(country) {
1641
+ if (!(country == null ? void 0 : country.phoneMask)) return "";
1642
+ const code = country.phoneCode.startsWith("+") ? country.phoneCode : `+${country.phoneCode}`;
1643
+ const escaped = code.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1644
+ return country.phoneMask.replace(new RegExp(`^\\s*${escaped}[\\s-]*`), "").trim();
1645
+ }
1646
+ function formatDigitsToMask(digits, mask) {
1647
+ if (digits.length === 0) return "";
1648
+ let i = 0;
1649
+ let out = "";
1650
+ for (const c of mask) {
1651
+ if (c === "#") {
1652
+ if (i < digits.length) out += digits[i++];
1653
+ else break;
1654
+ } else if (i < digits.length) {
1655
+ out += c;
1656
+ }
1657
+ }
1658
+ return out;
1659
+ }
1660
+
1661
+ // src/tracking/firePixelEvent.ts
1662
+ var STORAGE_KEY = "ks_pud";
1663
+ async function sha256Hex(str) {
1664
+ const buffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(str));
1665
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
1666
+ }
1667
+ function getFbq() {
1668
+ if (typeof window === "undefined") return void 0;
1669
+ return window.fbq;
1670
+ }
1671
+ function getRegisteredPixelIds() {
1672
+ var _a;
1673
+ return (_a = window.__ks_pixel_ids) != null ? _a : [];
1674
+ }
1675
+ function applyStoredUserData(fbq) {
1676
+ try {
1677
+ const raw = sessionStorage.getItem(STORAGE_KEY);
1678
+ if (!raw) return;
1679
+ const hashed = JSON.parse(raw);
1680
+ if (Object.keys(hashed).length === 0) return;
1681
+ getRegisteredPixelIds().forEach((id) => fbq("init", id, hashed));
1682
+ } catch (e) {
1683
+ }
1684
+ }
1685
+ async function setPixelUserData(userData) {
1686
+ const hashed = {};
1687
+ if (userData.email) {
1688
+ hashed.em = await sha256Hex(userData.email.trim().toLowerCase());
1689
+ }
1690
+ if (userData.phone) {
1691
+ const digits = userData.phone.replace(/\D/g, "");
1692
+ if (digits) hashed.ph = await sha256Hex(digits);
1693
+ }
1694
+ if (Object.keys(hashed).length === 0) return;
1695
+ try {
1696
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(hashed));
1697
+ } catch (e) {
1698
+ }
1699
+ const fbq = getFbq();
1700
+ if (fbq) {
1701
+ getRegisteredPixelIds().forEach((id) => fbq("init", id, hashed));
1702
+ }
1703
+ }
1704
+ function firePixelEvent(event, params, eventId) {
1705
+ const fbq = getFbq();
1706
+ if (!fbq) {
1707
+ console.debug("[MetaPixel] skipped \u2014 fbq not loaded", { event });
1708
+ return;
1709
+ }
1710
+ applyStoredUserData(fbq);
1711
+ const normalized = {};
1712
+ if (params == null ? void 0 : params.contentName) normalized.content_name = params.contentName;
1713
+ if (params == null ? void 0 : params.contentCategory) normalized.content_category = params.contentCategory;
1714
+ const customData = Object.keys(normalized).length > 0 ? normalized : void 0;
1715
+ const eventData = eventId ? { eventID: eventId } : void 0;
1716
+ console.debug("[MetaPixel]", event, normalized, eventId ? { eventID: eventId } : "");
1717
+ fbq("track", event, customData, eventData);
1718
+ }
1719
+
1720
+ // src/tracking/captureEvent.ts
1721
+ import posthog from "posthog-js";
1722
+ function captureEvent(event, ...args) {
1723
+ posthog.capture(event, args[0]);
1724
+ }
1725
+
1726
+ // src/design_system/portal/LoginForm.tsx
1727
+ var inputClass = "block w-full rounded-input border border-primary bg-primary px-3 py-2.5 text-base text-primary placeholder-quaternary focus:border-brand focus:outline-none focus:ring-1 focus:ring-brand transition-colors";
1728
+ var labelClass = "block text-sm text-secondary mb-1";
1729
+ function LoginForm({ onSuccess, onClose }) {
1730
+ const router = useRouter2();
1731
+ const [step, setStep] = useState("identifier");
1732
+ const [phoneValue, setPhoneValue] = useState("");
1733
+ const [selectedCountry, setSelectedCountry] = useState("US");
1734
+ const [verificationCode, setVerificationCode] = useState("");
1735
+ const [verificationToken, setVerificationToken] = useState(null);
1736
+ const [firstName, setFirstName] = useState("");
1737
+ const [lastName, setLastName] = useState("");
1738
+ const [email, setEmail] = useState("");
1739
+ const [resendAvailableAt, setResendAvailableAt] = useState(null);
1740
+ const [secondsUntilResend, setSecondsUntilResend] = useState(0);
1741
+ const [error, setError] = useState(null);
1742
+ const [loading, setLoading] = useState(false);
1743
+ useEffect2(() => {
1744
+ captureEvent("portal_login_started");
1745
+ }, []);
1746
+ useEffect2(() => {
1747
+ const funnelStep = step === "identifier" ? "identifier" : step === "verify" ? "signin" : "signup";
1748
+ captureEvent("portal_login_step_viewed", { step: funnelStep });
1749
+ }, [step]);
1750
+ const country = useMemo(() => countries.find((c) => c.code === selectedCountry), [selectedCountry]);
1751
+ const phoneCode = country ? country.phoneCode.startsWith("+") ? country.phoneCode : `+${country.phoneCode}` : "+1";
1752
+ const nationalMask = useMemo(() => getNationalMask(country), [country]);
1753
+ const nationalPlaceholder = nationalMask ? nationalMask.replace(/#/g, "0") : "(555) 000-0000";
1754
+ const countryOptions = useMemo(
1755
+ () => countries.map((c) => ({ label: c.phoneCode.startsWith("+") ? c.phoneCode : `+${c.phoneCode}`, value: c.code })),
1756
+ []
1757
+ );
1758
+ const phoneDigits = phoneValue.replace(/\D/g, "");
1759
+ const fullPhone = phoneDigits.length > 0 ? `${phoneCode}${phoneDigits}` : null;
1760
+ useEffect2(() => {
1761
+ if (!resendAvailableAt) {
1762
+ setSecondsUntilResend(0);
1763
+ return;
1764
+ }
1765
+ const tick = () => {
1766
+ const remaining = Math.max(0, Math.ceil((resendAvailableAt - Date.now()) / 1e3));
1767
+ setSecondsUntilResend(remaining);
1768
+ };
1769
+ tick();
1770
+ const interval = window.setInterval(tick, 1e3);
1771
+ return () => window.clearInterval(interval);
1772
+ }, [resendAvailableAt]);
1773
+ const phoneInput = (value, onChange, required = false) => /* @__PURE__ */ React3.createElement("div", { className: "flex rounded-input border border-primary overflow-hidden focus-within:border-brand focus-within:ring-1 focus-within:ring-brand transition-colors" }, /* @__PURE__ */ React3.createElement(
1774
+ "select",
1775
+ {
1776
+ value: selectedCountry,
1777
+ onChange: (e) => {
1778
+ setSelectedCountry(e.target.value);
1779
+ onChange("");
1780
+ },
1781
+ className: "border-r border-secondary bg-secondary px-2 py-2.5 text-base text-secondary focus:outline-none",
1782
+ "aria-label": "Country code"
1783
+ },
1784
+ countryOptions.map((opt) => /* @__PURE__ */ React3.createElement("option", { key: opt.value, value: opt.value }, opt.label))
1785
+ ), /* @__PURE__ */ React3.createElement(
1786
+ "input",
1787
+ {
1788
+ type: "tel",
1789
+ value,
1790
+ onChange: (e) => {
1791
+ const digits = e.target.value.replace(/\D/g, "");
1792
+ onChange(nationalMask ? formatDigitsToMask(digits, nationalMask) : digits);
1793
+ },
1794
+ placeholder: nationalPlaceholder,
1795
+ className: "flex-1 px-3 py-2.5 text-base text-primary placeholder-quaternary bg-transparent focus:outline-none",
1796
+ autoComplete: "tel",
1797
+ required
1798
+ }
1799
+ ));
1800
+ const finishPasswordlessAuth = async ({
1801
+ token,
1802
+ firstNameValue,
1803
+ lastNameValue,
1804
+ emailValue
1805
+ }) => {
1806
+ const res = await fetch("/api/consumer/passwordless_auth", {
1807
+ method: "POST",
1808
+ headers: { "Content-Type": "application/json" },
1809
+ credentials: "include",
1810
+ body: JSON.stringify({
1811
+ phone: fullPhone,
1812
+ verification_token: token,
1813
+ first_name: firstNameValue.trim(),
1814
+ last_name: lastNameValue.trim(),
1815
+ email: emailValue.trim()
1816
+ })
1817
+ });
1818
+ const result = await res.json().catch(() => ({}));
1819
+ if (!res.ok) {
1820
+ setError(result.error || "Authentication failed. Please try again.");
1821
+ captureEvent("portal_login_failed", { step: "signup", reason: result.error || "passwordless_auth_failed" });
1822
+ return false;
1823
+ }
1824
+ firePixelEvent("Lead");
1825
+ captureEvent("portal_login_step_advanced", { from_step: "signup", to_step: "authenticated", reason: "passwordless_auth_success" });
1826
+ captureEvent("portal_login_completed", { flow: "signup" });
1827
+ setResendAvailableAt(null);
1828
+ setSecondsUntilResend(0);
1829
+ if (onSuccess) onSuccess();
1830
+ else router.refresh();
1831
+ return true;
1832
+ };
1833
+ const handleSendCode = async (e) => {
1834
+ e.preventDefault();
1835
+ if (!fullPhone) {
1836
+ setError("Enter your phone number to continue.");
1837
+ captureEvent("portal_login_failed", { step: "identifier", reason: "validation_missing_phone" });
1838
+ return;
1839
+ }
1840
+ setLoading(true);
1841
+ setError(null);
1842
+ captureEvent("portal_login_step_submitted", { step: "identifier" });
1843
+ try {
1844
+ const res = await fetch("/api/consumer/send_code", {
1845
+ method: "POST",
1846
+ headers: { "Content-Type": "application/json" },
1847
+ credentials: "include",
1848
+ body: JSON.stringify({ phone: fullPhone })
1849
+ });
1850
+ const result = await res.json().catch(() => ({}));
1851
+ if (!res.ok) {
1852
+ if (result.code === "cooldown_active" && typeof result.retry_in_seconds === "number") {
1853
+ setResendAvailableAt(Date.now() + result.retry_in_seconds * 1e3);
1854
+ }
1855
+ setError(result.error || "Could not send verification code.");
1856
+ return;
1857
+ }
1858
+ await setPixelUserData({ phone: fullPhone });
1859
+ firePixelEvent("Lead");
1860
+ setResendAvailableAt(result.resend_available_at ? new Date(result.resend_available_at).getTime() : Date.now() + 5 * 60 * 1e3);
1861
+ setStep("verify");
1862
+ captureEvent("portal_login_step_advanced", { from_step: "identifier", to_step: "signin", reason: "verification_code_sent" });
1863
+ } catch (e2) {
1864
+ setError("Could not send verification code.");
1865
+ captureEvent("portal_login_failed", { step: "identifier", reason: "network_error" });
1866
+ } finally {
1867
+ setLoading(false);
1868
+ }
1869
+ };
1870
+ const handleVerifyCode = async (e) => {
1871
+ e.preventDefault();
1872
+ const code = verificationCode.replace(/\D/g, "");
1873
+ if (!fullPhone || code.length !== 4) {
1874
+ setError("Enter the 4-digit code sent to your phone.");
1875
+ captureEvent("portal_login_failed", { step: "signin", reason: "validation_invalid_code" });
1876
+ return;
1877
+ }
1878
+ setLoading(true);
1879
+ setError(null);
1880
+ captureEvent("portal_login_step_submitted", { step: "signin" });
1881
+ try {
1882
+ const res = await fetch("/api/consumer/verify_code", {
1883
+ method: "POST",
1884
+ headers: { "Content-Type": "application/json" },
1885
+ credentials: "include",
1886
+ body: JSON.stringify({ phone: fullPhone, code })
1887
+ });
1888
+ const result = await res.json().catch(() => ({}));
1889
+ if (!res.ok || !result.verification_token) {
1890
+ setError(result.error || "That code is invalid or expired.");
1891
+ captureEvent("portal_login_failed", { step: "signin", reason: result.code || "verification_failed" });
1892
+ return;
1893
+ }
1894
+ setVerificationToken(result.verification_token);
1895
+ setFirstName(result.first_name || "");
1896
+ setLastName(result.last_name || "");
1897
+ setEmail(result.email || "");
1898
+ const hasCompleteExistingProfile = typeof result.first_name === "string" && result.first_name.trim().length > 0 && typeof result.last_name === "string" && result.last_name.trim().length > 0 && typeof result.email === "string" && result.email.trim().length > 0;
1899
+ if (hasCompleteExistingProfile) {
1900
+ captureEvent("portal_login_step_advanced", {
1901
+ from_step: "signin",
1902
+ to_step: "authenticated",
1903
+ reason: "verification_success_profile_complete"
1904
+ });
1905
+ await finishPasswordlessAuth({
1906
+ token: result.verification_token,
1907
+ firstNameValue: result.first_name,
1908
+ lastNameValue: result.last_name,
1909
+ emailValue: result.email
1910
+ });
1911
+ return;
1912
+ }
1913
+ setStep("profile");
1914
+ captureEvent("portal_login_step_advanced", { from_step: "signin", to_step: "signup", reason: "verification_success" });
1915
+ } catch (e2) {
1916
+ setError("That code is invalid or expired.");
1917
+ captureEvent("portal_login_failed", { step: "signin", reason: "network_error" });
1918
+ } finally {
1919
+ setLoading(false);
1920
+ }
1921
+ };
1922
+ const handleResendCode = async () => {
1923
+ if (!fullPhone || loading || secondsUntilResend > 0) return;
1924
+ setLoading(true);
1925
+ setError(null);
1926
+ try {
1927
+ const res = await fetch("/api/consumer/send_code", {
1928
+ method: "POST",
1929
+ headers: { "Content-Type": "application/json" },
1930
+ credentials: "include",
1931
+ body: JSON.stringify({ phone: fullPhone })
1932
+ });
1933
+ const result = await res.json().catch(() => ({}));
1934
+ if (!res.ok) {
1935
+ if (result.code === "cooldown_active" && typeof result.retry_in_seconds === "number") {
1936
+ setResendAvailableAt(Date.now() + result.retry_in_seconds * 1e3);
1937
+ }
1938
+ setError(result.error || "Could not request a new code yet.");
1939
+ return;
1940
+ }
1941
+ setVerificationCode("");
1942
+ setResendAvailableAt(result.resend_available_at ? new Date(result.resend_available_at).getTime() : Date.now() + 5 * 60 * 1e3);
1943
+ } catch (e) {
1944
+ setError("Could not request a new code yet.");
1945
+ } finally {
1946
+ setLoading(false);
1947
+ }
1948
+ };
1949
+ const handleProfileSubmit = async (e) => {
1950
+ e.preventDefault();
1951
+ if (!fullPhone || !verificationToken) {
1952
+ setError("Verification expired. Please request a new code.");
1953
+ setStep("identifier");
1954
+ return;
1955
+ }
1956
+ if (!firstName.trim() || !lastName.trim() || !email.trim()) {
1957
+ setError("Please enter your first name, last name, and email.");
1958
+ return;
1959
+ }
1960
+ setLoading(true);
1961
+ setError(null);
1962
+ captureEvent("portal_login_step_submitted", { step: "signup" });
1963
+ try {
1964
+ await finishPasswordlessAuth({
1965
+ token: verificationToken,
1966
+ firstNameValue: firstName,
1967
+ lastNameValue: lastName,
1968
+ emailValue: email
1969
+ });
1970
+ } catch (e2) {
1971
+ setError("Authentication failed. Please try again.");
1972
+ captureEvent("portal_login_failed", { step: "signup", reason: "network_error" });
1973
+ } finally {
1974
+ setLoading(false);
1975
+ }
1976
+ };
1977
+ const goBackToPhone = () => {
1978
+ setStep("identifier");
1979
+ setVerificationCode("");
1980
+ setVerificationToken(null);
1981
+ setFirstName("");
1982
+ setLastName("");
1983
+ setEmail("");
1984
+ setResendAvailableAt(null);
1985
+ setError(null);
1986
+ };
1987
+ const formatCountdown = (totalSeconds) => {
1988
+ const mins = Math.floor(totalSeconds / 60);
1989
+ const secs = totalSeconds % 60;
1990
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
1991
+ };
1992
+ const headerTitle = step === "identifier" ? "Welcome" : step === "verify" ? "Verify your phone" : "Last step";
1993
+ const headerSubtitle = step === "identifier" ? "Enter your phone number to get started." : step === "verify" ? "We sent a 4-digit verification code by text." : "Enter your first name, last name, and email.";
1994
+ return /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement("div", { className: "flex items-start justify-between gap-4 mb-5" }, /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement("h3", { className: "text-lg font-normal text-primary" }, headerTitle), /* @__PURE__ */ React3.createElement("p", { className: "mt-0.5 text-sm text-tertiary" }, headerSubtitle)), onClose && /* @__PURE__ */ React3.createElement(
1995
+ "button",
1996
+ {
1997
+ type: "button",
1998
+ onClick: onClose,
1999
+ className: "shrink-0 p-1 text-tertiary hover:text-primary transition-colors",
2000
+ "aria-label": "Close"
2001
+ },
2002
+ /* @__PURE__ */ React3.createElement("svg", { className: "size-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5 }, /* @__PURE__ */ React3.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }))
2003
+ )), error && /* @__PURE__ */ React3.createElement("div", { className: "mb-4 rounded-input border border-error bg-error-primary px-3 py-2 text-sm text-error-primary" }, error), /* @__PURE__ */ React3.createElement("div", { className: "relative overflow-hidden transition-all duration-300 ease-out" }, step === "identifier" && /* @__PURE__ */ React3.createElement("form", { onSubmit: handleSendCode, className: "space-y-3" }, /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement("label", { className: labelClass }, "Phone number"), phoneInput(phoneValue, setPhoneValue, true)), /* @__PURE__ */ React3.createElement("div", { className: "pt-1" }, /* @__PURE__ */ React3.createElement(
2004
+ "button",
2005
+ {
2006
+ type: "submit",
2007
+ disabled: loading,
2008
+ className: "w-full rounded-interactive bg-brand-solid px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-solid_hover transition-colors disabled:opacity-50"
2009
+ },
2010
+ loading ? "Sending code\u2026" : "Continue"
2011
+ ))), step === "verify" && /* @__PURE__ */ React3.createElement("form", { onSubmit: handleVerifyCode, className: "space-y-3" }, /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement("label", { className: labelClass }, "Verification code"), /* @__PURE__ */ React3.createElement(
2012
+ "input",
2013
+ {
2014
+ type: "text",
2015
+ value: verificationCode,
2016
+ onChange: (e) => setVerificationCode(e.target.value.replace(/\D/g, "").slice(0, 4)),
2017
+ placeholder: "0000",
2018
+ className: inputClass,
2019
+ autoComplete: "one-time-code",
2020
+ inputMode: "numeric",
2021
+ required: true
2022
+ }
2023
+ )), /* @__PURE__ */ React3.createElement("div", { className: "pt-1" }, /* @__PURE__ */ React3.createElement(
2024
+ "button",
2025
+ {
2026
+ type: "submit",
2027
+ disabled: loading,
2028
+ className: "w-full rounded-interactive bg-brand-solid px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-solid_hover transition-colors disabled:opacity-50"
2029
+ },
2030
+ loading ? "Verifying\u2026" : "Verify code"
2031
+ )), /* @__PURE__ */ React3.createElement(
2032
+ "button",
2033
+ {
2034
+ type: "button",
2035
+ onClick: handleResendCode,
2036
+ disabled: loading || secondsUntilResend > 0,
2037
+ className: "w-full text-center text-sm text-tertiary hover:text-primary transition-colors disabled:opacity-50"
2038
+ },
2039
+ secondsUntilResend > 0 ? `Request new code in ${formatCountdown(secondsUntilResend)}` : "Request new code"
2040
+ ), /* @__PURE__ */ React3.createElement(
2041
+ "button",
2042
+ {
2043
+ type: "button",
2044
+ onClick: goBackToPhone,
2045
+ className: "w-full text-center text-sm text-tertiary hover:text-primary transition-colors"
2046
+ },
2047
+ "\u2190 Use a different phone number"
2048
+ )), step === "profile" && /* @__PURE__ */ React3.createElement("form", { onSubmit: handleProfileSubmit, className: "space-y-3" }, /* @__PURE__ */ React3.createElement("div", { className: "grid grid-cols-2 gap-3" }, /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement("label", { className: labelClass }, "First name"), /* @__PURE__ */ React3.createElement(
2049
+ "input",
2050
+ {
2051
+ type: "text",
2052
+ value: firstName,
2053
+ onChange: (e) => setFirstName(e.target.value),
2054
+ placeholder: "Jane",
2055
+ className: inputClass,
2056
+ autoComplete: "given-name",
2057
+ required: true
2058
+ }
2059
+ )), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement("label", { className: labelClass }, "Last name"), /* @__PURE__ */ React3.createElement(
2060
+ "input",
2061
+ {
2062
+ type: "text",
2063
+ value: lastName,
2064
+ onChange: (e) => setLastName(e.target.value),
2065
+ placeholder: "Smith",
2066
+ className: inputClass,
2067
+ autoComplete: "family-name",
2068
+ required: true
2069
+ }
2070
+ ))), /* @__PURE__ */ React3.createElement("div", null, /* @__PURE__ */ React3.createElement("label", { className: labelClass }, "Email address"), /* @__PURE__ */ React3.createElement(
2071
+ "input",
2072
+ {
2073
+ type: "email",
2074
+ value: email,
2075
+ onChange: (e) => setEmail(e.target.value),
2076
+ placeholder: "you@example.com",
2077
+ className: inputClass,
2078
+ autoComplete: "email",
2079
+ required: true
2080
+ }
2081
+ )), /* @__PURE__ */ React3.createElement("div", { className: "pt-1" }, /* @__PURE__ */ React3.createElement(
2082
+ "button",
2083
+ {
2084
+ type: "submit",
2085
+ disabled: loading,
2086
+ className: "w-full rounded-interactive bg-brand-solid px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-solid_hover transition-colors disabled:opacity-50"
2087
+ },
2088
+ loading ? "Finishing\u2026" : "Continue"
2089
+ )), /* @__PURE__ */ React3.createElement(
2090
+ "button",
2091
+ {
2092
+ type: "button",
2093
+ onClick: goBackToPhone,
2094
+ className: "w-full text-center text-sm text-tertiary hover:text-primary transition-colors"
2095
+ },
2096
+ "\u2190 Use a different phone number"
2097
+ ))));
2098
+ }
2099
+
2100
+ // src/design_system/portal/LoginModalController.tsx
2101
+ var keystoneFooter = /* @__PURE__ */ React4.createElement("div", { className: "px-6 py-4 bg-secondary flex flex-col items-center gap-1" }, /* @__PURE__ */ React4.createElement("a", { href: KEYSTONE_APP_URL, target: "_blank", rel: "noopener noreferrer", "aria-label": "Visit Keystone website" }, /* @__PURE__ */ React4.createElement(KeystoneLogoMinimal, { className: "h-5 w-auto shrink-0" })), /* @__PURE__ */ React4.createElement(
2102
+ "a",
2103
+ {
2104
+ href: KEYSTONE_APP_URL,
2105
+ target: "_blank",
2106
+ rel: "noopener noreferrer",
2107
+ className: "text-xs text-quaternary hover:text-secondary transition-colors"
2108
+ },
2109
+ "Powered by Keystone Universal Login"
2110
+ ));
2111
+ function LoginModalController() {
2112
+ const [open, setOpen] = useState2(false);
2113
+ const [redirectUrl, setRedirectUrl] = useState2(null);
2114
+ const [refreshing, setRefreshing] = useState2(false);
2115
+ const [isPending, startTransition] = useTransition();
2116
+ const router = useRouter3();
2117
+ useEffect3(() => {
2118
+ const handleClick = (e) => {
2119
+ const trigger = e.target.closest("[data-open-login-modal]");
2120
+ if (trigger) {
2121
+ e.preventDefault();
2122
+ setRedirectUrl(trigger.getAttribute("data-login-redirect"));
2123
+ setOpen(true);
2124
+ }
2125
+ };
2126
+ document.addEventListener("click", handleClick);
2127
+ return () => document.removeEventListener("click", handleClick);
2128
+ }, []);
2129
+ const handleSuccess = () => {
2130
+ setRefreshing(true);
2131
+ startTransition(() => {
2132
+ if (redirectUrl) {
2133
+ router.push(redirectUrl);
2134
+ setRedirectUrl(null);
2135
+ } else {
2136
+ router.refresh();
2137
+ }
2138
+ });
2139
+ };
2140
+ useEffect3(() => {
2141
+ if (!(refreshing && !isPending)) return;
2142
+ const timeout = window.setTimeout(() => {
2143
+ setOpen(false);
2144
+ setRefreshing(false);
2145
+ }, 0);
2146
+ return () => window.clearTimeout(timeout);
2147
+ }, [refreshing, isPending]);
2148
+ return /* @__PURE__ */ React4.createElement(
2149
+ Modal,
2150
+ {
2151
+ isOpen: open,
2152
+ onClose: refreshing ? () => {
2153
+ } : () => setOpen(false),
2154
+ hideHeader: true,
2155
+ ariaLabel: "Sign in or create account",
2156
+ footer: refreshing ? void 0 : keystoneFooter
2157
+ },
2158
+ refreshing ? /* @__PURE__ */ React4.createElement("div", { className: "flex flex-col items-center justify-center py-12 gap-3" }, /* @__PURE__ */ React4.createElement("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-brand border-t-transparent" }), /* @__PURE__ */ React4.createElement("p", { className: "text-sm text-tertiary" }, "Signing you in\u2026")) : /* @__PURE__ */ React4.createElement(LoginForm, { onSuccess: handleSuccess, onClose: () => setOpen(false) })
2159
+ );
2160
+ }
2161
+
2162
+ // src/design_system/portal/MessageComposer.tsx
2163
+ import React5, { useState as useState3, useTransition as useTransition2, useRef as useRef3, useEffect as useEffect5, useCallback as useCallback2 } from "react";
2164
+ import { useRouter as useRouter4 } from "next/navigation";
2165
+
2166
+ // src/design_system/chat/useRealtimeReplyOrchestrator.ts
2167
+ import { useCallback, useEffect as useEffect4, useRef as useRef2 } from "react";
2168
+ import { createConsumer } from "@rails/actioncable";
2169
+ function useRealtimeReplyOrchestrator({
2170
+ debugLabel,
2171
+ fetchRealtimeData,
2172
+ loadLatestHasReply,
2173
+ onReplyResolved,
2174
+ onAgentThinking
2175
+ }) {
2176
+ const cableRef = useRef2(null);
2177
+ const subscriptionRef = useRef2(null);
2178
+ const subscribedContactRef = useRef2(null);
2179
+ const clearRealtime = useCallback(() => {
2180
+ if (subscriptionRef.current) {
2181
+ subscriptionRef.current.unsubscribe();
2182
+ subscriptionRef.current = null;
2183
+ }
2184
+ if (cableRef.current) {
2185
+ cableRef.current.disconnect();
2186
+ cableRef.current = null;
2187
+ }
2188
+ subscribedContactRef.current = null;
2189
+ }, []);
2190
+ const resolveReply = useCallback(() => {
2191
+ onReplyResolved();
2192
+ }, [onReplyResolved]);
2193
+ const subscribeRealtime = useCallback((realtime) => {
2194
+ const token = realtime.token;
2195
+ const contactIdForStream = realtime.contact_id;
2196
+ const cableUrl = realtime.cable_url;
2197
+ if (!token || !contactIdForStream || !cableUrl) return;
2198
+ if (subscribedContactRef.current === contactIdForStream && subscriptionRef.current && cableRef.current) {
2199
+ return;
2200
+ }
2201
+ clearRealtime();
2202
+ const cable = createConsumer(`${cableUrl}?token=${encodeURIComponent(token)}`);
2203
+ cableRef.current = cable;
2204
+ subscribedContactRef.current = contactIdForStream;
2205
+ subscriptionRef.current = cable.subscriptions.create(
2206
+ { channel: "ContactCommunicationsChannel", contact_id: String(contactIdForStream) },
2207
+ {
2208
+ connected: () => {
2209
+ console.info(`[${debugLabel}] realtime connected contact_id=${contactIdForStream}`);
2210
+ },
2211
+ disconnected: () => {
2212
+ console.warn(`[${debugLabel}] realtime disconnected contact_id=${contactIdForStream}`);
2213
+ },
2214
+ rejected: () => {
2215
+ console.warn(`[${debugLabel}] realtime rejected contact_id=${contactIdForStream}`);
2216
+ },
2217
+ received: async (data) => {
2218
+ var _a;
2219
+ console.info(`[${debugLabel}] realtime received type=${(_a = data == null ? void 0 : data.type) != null ? _a : "unknown"} contact_id=${contactIdForStream}`);
2220
+ if ((data == null ? void 0 : data.type) === "agent_thinking") {
2221
+ onAgentThinking == null ? void 0 : onAgentThinking();
2222
+ return;
2223
+ }
2224
+ if ((data == null ? void 0 : data.type) !== "new_communication") return;
2225
+ try {
2226
+ const hasReply = await loadLatestHasReply();
2227
+ if (hasReply) {
2228
+ resolveReply();
2229
+ }
2230
+ } catch (error) {
2231
+ console.error(`[${debugLabel}] realtime receive handling error`, error);
2232
+ }
2233
+ }
2234
+ }
2235
+ );
2236
+ }, [clearRealtime, debugLabel, loadLatestHasReply, onAgentThinking, resolveReply]);
2237
+ const ensureRealtimeSubscription = useCallback(async () => {
2238
+ try {
2239
+ const realtime = await fetchRealtimeData();
2240
+ if (!realtime) return;
2241
+ subscribeRealtime(realtime);
2242
+ } catch (e) {
2243
+ }
2244
+ }, [fetchRealtimeData, subscribeRealtime]);
2245
+ const beginReplyWait = useCallback((options) => {
2246
+ if (options == null ? void 0 : options.realtimeData) {
2247
+ subscribeRealtime(options.realtimeData);
2248
+ } else {
2249
+ void ensureRealtimeSubscription();
2250
+ }
2251
+ }, [ensureRealtimeSubscription, subscribeRealtime]);
2252
+ useEffect4(() => {
2253
+ return () => {
2254
+ clearRealtime();
2255
+ };
2256
+ }, [clearRealtime]);
2257
+ return {
2258
+ ensureRealtimeSubscription,
2259
+ subscribeRealtime,
2260
+ beginReplyWait
2261
+ };
2262
+ }
2263
+
2264
+ // src/design_system/portal/MessageComposer.tsx
2265
+ function MessageComposer({ contactId }) {
2266
+ const [body, setBody] = useState3("");
2267
+ const [error, setError] = useState3(null);
2268
+ const [isPending, startTransition] = useTransition2();
2269
+ const textareaRef = useRef3(null);
2270
+ const router = useRouter4();
2271
+ const hasAgentReplyWithBody = useCallback2(async () => {
2272
+ const response = await fetch(`/api/chat/?contact_id=${encodeURIComponent(contactId)}`);
2273
+ if (!response.ok) return false;
2274
+ const result = await response.json();
2275
+ const messages = (result == null ? void 0 : result.data) || [];
2276
+ const latest = messages[messages.length - 1];
2277
+ return (latest == null ? void 0 : latest.sender_type) === "agent" && (latest == null ? void 0 : latest.body) != null && String(latest.body).trim() !== "";
2278
+ }, [contactId]);
2279
+ const fetchRealtimeData = useCallback2(async () => {
2280
+ var _a, _b, _c;
2281
+ const response = await fetch(`/api/chat/?action=realtime_token&contact_id=${encodeURIComponent(contactId)}`);
2282
+ if (!response.ok) return null;
2283
+ const result = await response.json();
2284
+ const token = (_a = result == null ? void 0 : result.data) == null ? void 0 : _a.token;
2285
+ const streamContactId = (_b = result == null ? void 0 : result.data) == null ? void 0 : _b.contact_id;
2286
+ const cableUrl = (_c = result == null ? void 0 : result.data) == null ? void 0 : _c.cable_url;
2287
+ if (!token || !streamContactId || !cableUrl) return null;
2288
+ return { token, contact_id: streamContactId, cable_url: cableUrl };
2289
+ }, [contactId]);
2290
+ const {
2291
+ beginReplyWait,
2292
+ ensureRealtimeSubscription
2293
+ } = useRealtimeReplyOrchestrator({
2294
+ debugLabel: "MessageComposer",
2295
+ fetchRealtimeData,
2296
+ loadLatestHasReply: hasAgentReplyWithBody,
2297
+ onReplyResolved: () => {
2298
+ router.refresh();
2299
+ }
2300
+ });
2301
+ useEffect5(() => {
2302
+ const el = textareaRef.current;
2303
+ if (!el) return;
2304
+ el.style.height = "auto";
2305
+ el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
2306
+ }, [body]);
2307
+ useEffect5(() => {
2308
+ void ensureRealtimeSubscription();
2309
+ }, [ensureRealtimeSubscription]);
2310
+ function submit() {
2311
+ if (!body.trim() || isPending) return;
2312
+ setError(null);
2313
+ startTransition(async () => {
2314
+ var _a, _b, _c, _d;
2315
+ try {
2316
+ const res = await fetch("/api/chat", {
2317
+ method: "POST",
2318
+ headers: { "Content-Type": "application/json" },
2319
+ body: JSON.stringify({ contact_id: contactId, body: body.trim() })
2320
+ });
2321
+ const result = await res.json().catch(() => ({}));
2322
+ if (!res.ok) {
2323
+ setError(result.error || "Failed to send message.");
2324
+ } else {
2325
+ setBody("");
2326
+ router.refresh();
2327
+ const hasBackgroundReplyJob = Boolean((_a = result == null ? void 0 : result.data) == null ? void 0 : _a.job_id);
2328
+ if (hasBackgroundReplyJob) {
2329
+ const realtimeFromSend = ((_b = result == null ? void 0 : result.data) == null ? void 0 : _b.realtime_token) && ((_c = result == null ? void 0 : result.data) == null ? void 0 : _c.contact_id) && ((_d = result == null ? void 0 : result.data) == null ? void 0 : _d.cable_url) ? {
2330
+ token: result.data.realtime_token,
2331
+ contact_id: result.data.contact_id,
2332
+ cable_url: result.data.cable_url
2333
+ } : null;
2334
+ beginReplyWait({ realtimeData: realtimeFromSend });
2335
+ }
2336
+ }
2337
+ } catch (e) {
2338
+ setError("Failed to send message.");
2339
+ }
2340
+ });
2341
+ }
2342
+ function handleSubmit(e) {
2343
+ e.preventDefault();
2344
+ submit();
2345
+ }
2346
+ function handleKeyDown(e) {
2347
+ if (e.key === "Enter" && !e.shiftKey) {
2348
+ e.preventDefault();
2349
+ submit();
2350
+ }
2351
+ }
2352
+ return /* @__PURE__ */ React5.createElement("form", { onSubmit: handleSubmit, className: "border-t border-secondary px-4 py-3" }, error && /* @__PURE__ */ React5.createElement("p", { className: "mb-2 text-xs text-red-600" }, error), /* @__PURE__ */ React5.createElement("div", { className: "flex items-end gap-2" }, /* @__PURE__ */ React5.createElement(
2353
+ "textarea",
2354
+ {
2355
+ ref: textareaRef,
2356
+ value: body,
2357
+ onChange: (e) => setBody(e.target.value),
2358
+ onKeyDown: handleKeyDown,
2359
+ placeholder: "Type a message\u2026 (Enter to send)",
2360
+ rows: 1,
2361
+ disabled: isPending,
2362
+ className: "flex-1 resize-none rounded-input border border-primary bg-primary px-3 py-2 text-sm text-primary placeholder-quaternary focus:border-brand focus:outline-none focus:ring-1 focus:ring-brand transition-colors disabled:opacity-50"
2363
+ }
2364
+ ), /* @__PURE__ */ React5.createElement(
2365
+ "button",
2366
+ {
2367
+ type: "submit",
2368
+ disabled: !body.trim() || isPending,
2369
+ className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-interactive bg-brand-solid text-white transition-colors hover:bg-brand-solid_hover disabled:opacity-40 disabled:cursor-not-allowed",
2370
+ "aria-label": "Send message"
2371
+ },
2372
+ isPending ? /* @__PURE__ */ React5.createElement("svg", { className: "size-4 animate-spin", fill: "none", viewBox: "0 0 24 24" }, /* @__PURE__ */ React5.createElement("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), /* @__PURE__ */ React5.createElement("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" })) : /* @__PURE__ */ React5.createElement("svg", { className: "size-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2 }, /* @__PURE__ */ React5.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.269 20.876L5.999 12zm0 0h7.5" }))
2373
+ )));
2374
+ }
2375
+
2376
+ // src/design_system/portal/RowThumbnail.tsx
2377
+ import React6 from "react";
2378
+
2379
+ // src/lib/hooks/use-image-cycle.ts
2380
+ import { useState as useState4, useEffect as useEffect6, useMemo as useMemo2 } from "react";
2381
+ var CYCLE_INTERVAL_MIN_MS = 6e3;
2382
+ var CYCLE_INTERVAL_MAX_MS = 8e3;
2383
+ var CROSSFADE_DURATION_MS = 600;
2384
+ function seedToUnit(seed) {
2385
+ let h = 2166136261 >>> 0;
2386
+ for (let i = 0; i < seed.length; i++) {
2387
+ h ^= seed.charCodeAt(i);
2388
+ h = Math.imul(h, 16777619) >>> 0 >>> 0;
2389
+ }
2390
+ return (h >>> 0) / 4294967296;
2391
+ }
2392
+ function shuffleWithSeed(array, seed) {
2393
+ if (array.length <= 1) return array;
2394
+ const arr = [...array];
2395
+ let h = 2166136261 >>> 0;
2396
+ for (let i = 0; i < seed.length; i++) {
2397
+ h ^= seed.charCodeAt(i);
2398
+ h = Math.imul(h, 16777619) >>> 0 >>> 0;
2399
+ }
2400
+ const next = (step) => {
2401
+ h = Math.imul(1664525, h + step >>> 0) + 1013904223 >>> 0;
2402
+ return (h >>> 0) / 4294967296;
2403
+ };
2404
+ for (let i = arr.length - 1; i > 0; i--) {
2405
+ const j = Math.floor(next(i) * (i + 1));
2406
+ [arr[i], arr[j]] = [arr[j], arr[i]];
2407
+ }
2408
+ return arr;
2409
+ }
2410
+ function photoUrlFromAttachment(pa) {
2411
+ var _a, _b, _c;
2412
+ return ((_a = pa.photo) == null ? void 0 : _a.large_url) || ((_b = pa.photo) == null ? void 0 : _b.medium_url) || ((_c = pa.photo) == null ? void 0 : _c.thumbnail_url);
2413
+ }
2414
+ function photoAltFromAttachment(pa) {
2415
+ var _a, _b;
2416
+ return ((_a = pa.photo) == null ? void 0 : _a.alt_text) || ((_b = pa.photo) == null ? void 0 : _b.title) || "";
2417
+ }
2418
+ function useImageCycle(photoAttachments, seed) {
2419
+ const list = useMemo2(() => {
2420
+ const arr = Array.isArray(photoAttachments) && photoAttachments.length > 0 ? photoAttachments : [];
2421
+ if (arr.length === 0) return [];
2422
+ return shuffleWithSeed(arr, seed).map((pa) => {
2423
+ var _a;
2424
+ return { url: (_a = photoUrlFromAttachment(pa)) != null ? _a : "", alt: photoAltFromAttachment(pa) };
2425
+ }).filter((x) => x.url);
2426
+ }, [photoAttachments, seed]);
2427
+ const [currentIndex, setCurrentIndex] = useState4(0);
2428
+ const [transitioning, setTransitioning] = useState4(false);
2429
+ const intervalMs = useMemo2(
2430
+ () => CYCLE_INTERVAL_MIN_MS + Math.floor(seedToUnit(seed) * (CYCLE_INTERVAL_MAX_MS - CYCLE_INTERVAL_MIN_MS + 1)),
2431
+ [seed]
2432
+ );
2433
+ useEffect6(() => {
2434
+ if (list.length <= 1) return;
2435
+ const id = setInterval(() => setTransitioning(true), intervalMs);
2436
+ return () => clearInterval(id);
2437
+ }, [list.length, intervalMs]);
2438
+ useEffect6(() => {
2439
+ if (!transitioning || list.length <= 1) return;
2440
+ const t = setTimeout(() => {
2441
+ setCurrentIndex((i) => (i + 1) % list.length);
2442
+ setTransitioning(false);
2443
+ }, CROSSFADE_DURATION_MS);
2444
+ return () => clearTimeout(t);
2445
+ }, [transitioning, list.length]);
2446
+ const nextIndex = list.length > 1 ? (currentIndex + 1) % list.length : 0;
2447
+ return { list, currentIndex, nextIndex, transitioning };
2448
+ }
2449
+
2450
+ // src/design_system/portal/RowThumbnail.tsx
2451
+ var CROSSFADE_STYLE = { transitionDuration: `${CROSSFADE_DURATION_MS}ms` };
2452
+ function RowThumbnail({
2453
+ photoAttachments,
2454
+ seed,
2455
+ alt,
2456
+ sizeClassName = "w-14 h-14"
2457
+ }) {
2458
+ var _a, _b, _c, _d, _e;
2459
+ const { list, currentIndex, nextIndex, transitioning } = useImageCycle(photoAttachments, seed);
2460
+ const displayAlt = ((_a = list[currentIndex]) == null ? void 0 : _a.alt) || alt;
2461
+ if (list.length === 0) {
2462
+ return /* @__PURE__ */ React6.createElement("div", { className: `${sizeClassName} shrink-0 rounded-component bg-secondary border border-tertiary flex items-center justify-center` }, /* @__PURE__ */ React6.createElement("svg", { className: "size-5 text-quaternary", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5 }, /* @__PURE__ */ React6.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" })));
2463
+ }
2464
+ if (list.length === 1) {
2465
+ return /* @__PURE__ */ React6.createElement("div", { className: `${sizeClassName} shrink-0 rounded-component overflow-hidden` }, /* @__PURE__ */ React6.createElement(
2466
+ "img",
2467
+ {
2468
+ src: list[0].url,
2469
+ alt: list[0].alt || alt,
2470
+ className: "w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
2471
+ }
2472
+ ));
2473
+ }
2474
+ return /* @__PURE__ */ React6.createElement("div", { className: `${sizeClassName} shrink-0 rounded-component overflow-hidden relative` }, /* @__PURE__ */ React6.createElement(
2475
+ "div",
2476
+ {
2477
+ className: `absolute inset-0 ${transitioning ? "transition-opacity ease-in-out" : "transition-none"}`,
2478
+ style: __spreadValues({ opacity: transitioning ? 0 : 1 }, transitioning ? CROSSFADE_STYLE : {})
2479
+ },
2480
+ /* @__PURE__ */ React6.createElement(
2481
+ "img",
2482
+ {
2483
+ src: list[currentIndex].url,
2484
+ alt: (_c = (_b = list[currentIndex]) == null ? void 0 : _b.alt) != null ? _c : displayAlt,
2485
+ className: "w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
2486
+ }
2487
+ )
2488
+ ), /* @__PURE__ */ React6.createElement(
2489
+ "div",
2490
+ {
2491
+ className: `absolute inset-0 ${transitioning ? "transition-opacity ease-in-out" : "transition-none"}`,
2492
+ style: __spreadValues({ opacity: transitioning ? 1 : 0 }, transitioning ? CROSSFADE_STYLE : {})
2493
+ },
2494
+ /* @__PURE__ */ React6.createElement(
2495
+ "img",
2496
+ {
2497
+ src: list[nextIndex].url,
2498
+ alt: (_e = (_d = list[nextIndex]) == null ? void 0 : _d.alt) != null ? _e : displayAlt,
2499
+ className: "w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
2500
+ }
2501
+ )
2502
+ ));
2503
+ }
2504
+
2505
+ // src/design_system/portal/PortalTabTracker.tsx
2506
+ import { useEffect as useEffect7 } from "react";
2507
+ function PortalTabTracker({ event, params, tab }) {
2508
+ useEffect7(() => {
2509
+ firePixelEvent(event, params);
2510
+ captureEvent("portal_tab_viewed", { tab });
2511
+ }, []);
2512
+ return null;
2513
+ }
2514
+
2515
+ // src/design_system/portal/BookIframePanel.tsx
2516
+ import React7, { useState as useState5, useEffect as useEffect8, useRef as useRef4 } from "react";
2517
+ function BookIframePanel({ bookingHref, businessName }) {
2518
+ const [hasOpened, setHasOpened] = useState5(false);
2519
+ const [modalOpen, setModalOpen] = useState5(false);
2520
+ const [isVisible, setIsVisible] = useState5(false);
2521
+ const closeTimerRef = useRef4(null);
2522
+ const openModal = () => {
2523
+ setHasOpened(true);
2524
+ setModalOpen(true);
2525
+ };
2526
+ const closeModal = () => {
2527
+ setIsVisible(false);
2528
+ closeTimerRef.current = setTimeout(() => setModalOpen(false), 300);
2529
+ };
2530
+ useEffect8(() => {
2531
+ return () => {
2532
+ if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
2533
+ };
2534
+ }, []);
2535
+ useEffect8(() => {
2536
+ if (!modalOpen) return;
2537
+ let cancelled = false;
2538
+ const id = requestAnimationFrame(
2539
+ () => requestAnimationFrame(() => {
2540
+ if (!cancelled) setIsVisible(true);
2541
+ })
2542
+ );
2543
+ return () => {
2544
+ cancelled = true;
2545
+ cancelAnimationFrame(id);
2546
+ };
2547
+ }, [modalOpen]);
2548
+ useEffect8(() => {
2549
+ document.body.style.overflow = modalOpen ? "hidden" : "";
2550
+ return () => {
2551
+ document.body.style.overflow = "";
2552
+ };
2553
+ }, [modalOpen]);
2554
+ return /* @__PURE__ */ React7.createElement(React7.Fragment, null, /* @__PURE__ */ React7.createElement("div", { className: "relative rounded-component border border-secondary overflow-hidden", style: { height: "70vh" } }, /* @__PURE__ */ React7.createElement(
2555
+ "iframe",
2556
+ {
2557
+ src: bookingHref,
2558
+ className: "w-full h-full pointer-events-none select-none",
2559
+ title: "Booking preview",
2560
+ tabIndex: -1,
2561
+ "aria-hidden": "true"
2562
+ }
2563
+ ), /* @__PURE__ */ React7.createElement("div", { className: "absolute inset-0 flex flex-col items-center justify-center px-6 py-8 backdrop-blur-[3.9px] bg-primary/70" }, /* @__PURE__ */ React7.createElement("div", { className: "w-full max-w-sm rounded-component border border-secondary bg-primary px-6 py-6 text-center shadow-sm" }, /* @__PURE__ */ React7.createElement("p", { className: "text-xl font-bold text-primary" }, "Booking Instructions"), /* @__PURE__ */ React7.createElement("p", { className: "mt-5 text-sm text-secondary leading-relaxed" }, "We use a separate platform that may ask for an additional sign in or intake details to fully create your profile and collect your appointment information."), /* @__PURE__ */ React7.createElement("div", { className: "mt-5 flex flex-wrap items-center justify-center gap-3" }, /* @__PURE__ */ React7.createElement(
2564
+ "button",
2565
+ {
2566
+ onClick: openModal,
2567
+ className: "inline-flex items-center gap-2 rounded-interactive bg-brand-solid px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-solid_hover"
2568
+ },
2569
+ "Start Booking"
2570
+ ), /* @__PURE__ */ React7.createElement(
2571
+ "a",
2572
+ {
2573
+ href: bookingHref,
2574
+ target: "_blank",
2575
+ rel: "noopener noreferrer",
2576
+ className: "inline-flex items-center gap-1.5 rounded-interactive border border-secondary bg-secondary px-5 py-2.5 text-sm font-semibold text-primary transition-colors hover:bg-secondary_hover"
2577
+ },
2578
+ "Open in New Tab",
2579
+ /* @__PURE__ */ React7.createElement("svg", { className: "size-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2 }, /* @__PURE__ */ React7.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" }))
2580
+ ))))), hasOpened && /* @__PURE__ */ React7.createElement(
2581
+ "div",
2582
+ {
2583
+ className: `fixed inset-0 z-50 flex flex-col bg-primary transition-[opacity,transform] duration-300 ease-out ${modalOpen && isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-3 pointer-events-none"}`
2584
+ },
2585
+ /* @__PURE__ */ React7.createElement("div", { className: "flex shrink-0 items-center justify-between border-b border-secondary bg-primary px-4 py-3" }, /* @__PURE__ */ React7.createElement("span", { className: "text-sm font-semibold text-primary" }, businessName, " Booking"), /* @__PURE__ */ React7.createElement(
2586
+ "button",
2587
+ {
2588
+ onClick: closeModal,
2589
+ className: "inline-flex items-center gap-2 rounded-interactive bg-brand-solid px-4 py-1.5 text-sm font-semibold text-white transition-colors hover:bg-brand-solid_hover"
2590
+ },
2591
+ /* @__PURE__ */ React7.createElement("svg", { className: "size-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2.5 }, /* @__PURE__ */ React7.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" })),
2592
+ "Close"
2593
+ )),
2594
+ /* @__PURE__ */ React7.createElement(
2595
+ "iframe",
2596
+ {
2597
+ src: bookingHref,
2598
+ className: "min-h-0 flex-1 w-full",
2599
+ title: "Book appointment",
2600
+ allow: "payment"
2601
+ }
2602
+ )
2603
+ ));
2604
+ }
2605
+
2606
+ // src/lib/consumer-session.ts
2607
+ var CONSUMER_TOKEN_COOKIE = "ks_consumer_token";
2608
+ function getApiUrl() {
2609
+ return process.env.API_URL || "http://localhost:3000/api/v1";
2610
+ }
2611
+ async function consumerFetch(path, token) {
2612
+ var _a;
2613
+ try {
2614
+ const res = await fetch(`${getApiUrl()}${path}`, {
2615
+ headers: {
2616
+ "Content-Type": "application/json",
2617
+ Authorization: `Bearer ${token}`
2618
+ },
2619
+ cache: "no-store"
2620
+ });
2621
+ if (!res.ok) return null;
2622
+ const json = await res.json();
2623
+ return (_a = json.data) != null ? _a : json;
2624
+ } catch (e) {
2625
+ return null;
2626
+ }
2627
+ }
2628
+ async function fetchConsumerMe(token) {
2629
+ return consumerFetch("/consumer/me", token);
2630
+ }
2631
+ async function fetchConsumerConversations(token) {
2632
+ var _a;
2633
+ const apiKey = process.env.API_KEY;
2634
+ const headers = __spreadValues({
2635
+ "Content-Type": "application/json",
2636
+ Authorization: `Bearer ${token}`
2637
+ }, apiKey ? { "X-API-Key": apiKey } : {});
2638
+ try {
2639
+ const res = await fetch(`${getApiUrl()}/consumer/me/conversations`, { headers, cache: "no-store" });
2640
+ if (!res.ok) return [];
2641
+ const json = await res.json();
2642
+ return (_a = json.data) != null ? _a : [];
2643
+ } catch (e) {
2644
+ return [];
2645
+ }
2646
+ }
2647
+ async function fetchConsumerMessages(token, contactId) {
2648
+ var _a, _b;
2649
+ try {
2650
+ const res = await fetch(
2651
+ `${getApiUrl()}/consumer/me/contacts/${contactId}/messages?per_page=100`,
2652
+ {
2653
+ headers: { Authorization: `Bearer ${token}` },
2654
+ cache: "no-store"
2655
+ }
2656
+ );
2657
+ if (!res.ok) return { messages: [], contact: null };
2658
+ const json = await res.json();
2659
+ return {
2660
+ messages: (_a = json.data) != null ? _a : [],
2661
+ contact: (_b = json.contact) != null ? _b : null
2662
+ };
2663
+ } catch (e) {
2664
+ return { messages: [], contact: null };
2665
+ }
2666
+ }
2667
+
2668
+ // src/lib/server-api.ts
2669
+ var API_URL = process.env.API_URL || "http://localhost:3000/api/v1";
2670
+ var API_KEY = process.env.API_KEY || "";
2671
+ async function serverFetch(endpoint, options = {}) {
2672
+ var _a;
2673
+ const url = `${API_URL}${endpoint}`;
2674
+ try {
2675
+ const fetchOptions = {
2676
+ headers: {
2677
+ "X-API-Key": API_KEY,
2678
+ "Content-Type": "application/json"
2679
+ },
2680
+ cache: options.cache
2681
+ };
2682
+ if (options.revalidate) {
2683
+ fetchOptions.next = { revalidate: options.revalidate };
2684
+ }
2685
+ const response = await fetch(url, fetchOptions);
2686
+ if (!response.ok) {
2687
+ console.error(`[Server API] Error ${response.status} for ${endpoint}`);
2688
+ return null;
2689
+ }
2690
+ const json = await response.json();
2691
+ return (_a = json.data) != null ? _a : json;
2692
+ } catch (error) {
2693
+ console.error(`[Server API] Failed to fetch ${endpoint}:`, error);
2694
+ return null;
2695
+ }
2696
+ }
2697
+ var defaultOptions = { revalidate: 60 };
2698
+ async function getCompanyInformation() {
2699
+ return serverFetch("/public/company_information", defaultOptions);
2700
+ }
2701
+ async function getServices() {
2702
+ return serverFetch("/public/services", defaultOptions);
2703
+ }
2704
+ async function getPackages() {
2705
+ return serverFetch("/public/packages", defaultOptions);
2706
+ }
2707
+
2708
+ // src/design_system/portal/PortalPage.tsx
2709
+ var ALL_TABS = ["services", "packages", "specials", "messages", "book"];
2710
+ var DEFAULT_TABS = ["book", "services", "packages", "specials", "messages"];
2711
+ async function checkIframeAllowed(url) {
2712
+ var _a;
2713
+ try {
2714
+ const fetchOptions = {
2715
+ method: "HEAD",
2716
+ redirect: "follow",
2717
+ // Cache for 1 hour — booking platforms rarely change their embedding policy.
2718
+ next: { revalidate: 3600 }
2719
+ };
2720
+ const res = await fetch(url, fetchOptions);
2721
+ const xfo = (_a = res.headers.get("x-frame-options")) == null ? void 0 : _a.toUpperCase();
2722
+ if (xfo === "DENY" || xfo === "SAMEORIGIN") return false;
2723
+ const csp = res.headers.get("content-security-policy");
2724
+ if (csp) {
2725
+ const match = csp.match(/frame-ancestors\s+([^;]+)/i);
2726
+ if (match) {
2727
+ const policy = match[1].trim();
2728
+ if (policy === "'none'") return false;
2729
+ }
2730
+ }
2731
+ return true;
2732
+ } catch (e) {
2733
+ return false;
2734
+ }
2735
+ }
2736
+ function formatCents(cents) {
2737
+ return new Intl.NumberFormat("en-US", {
2738
+ style: "currency",
2739
+ currency: "USD",
2740
+ minimumFractionDigits: 0
2741
+ }).format(cents / 100);
2742
+ }
2743
+ function formatTime(iso) {
2744
+ return new Date(iso).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
2745
+ }
2746
+ function getInitials(str) {
2747
+ return str.split(" ").map((w) => w[0]).join("").slice(0, 2).toUpperCase();
2748
+ }
2749
+ function aggregateSpecials(services, packages) {
2750
+ var _a, _b, _c;
2751
+ const seen = /* @__PURE__ */ new Set();
2752
+ const specials = [];
2753
+ for (const service of services) {
2754
+ for (const item of (_a = service.service_items) != null ? _a : []) {
2755
+ for (const offer of (_b = item.offers) != null ? _b : []) {
2756
+ if (offer.active === false || offer.expired) continue;
2757
+ if (seen.has(offer.id)) continue;
2758
+ seen.add(offer.id);
2759
+ specials.push({
2760
+ id: offer.id,
2761
+ name: offer.name,
2762
+ value_terms: offer.value_terms,
2763
+ expires_at: offer.expires_at,
2764
+ parentName: item.name,
2765
+ parentType: "service",
2766
+ photoAttachments: item.photo_attachments
2767
+ });
2768
+ }
2769
+ }
2770
+ }
2771
+ for (const pkg of packages) {
2772
+ for (const offer of (_c = pkg.offers) != null ? _c : []) {
2773
+ if (offer.active === false || offer.expired) continue;
2774
+ if (seen.has(offer.id)) continue;
2775
+ seen.add(offer.id);
2776
+ specials.push({
2777
+ id: offer.id,
2778
+ name: offer.name,
2779
+ value_terms: offer.value_terms,
2780
+ expires_at: offer.expires_at,
2781
+ parentName: pkg.name,
2782
+ parentType: "package",
2783
+ photoAttachments: pkg.photo_attachments
2784
+ });
2785
+ }
2786
+ }
2787
+ return specials;
2788
+ }
2789
+ function LockButton({ label = "View price" }) {
2790
+ return /* @__PURE__ */ React8.createElement(
2791
+ "button",
2792
+ {
2793
+ "data-open-login-modal": true,
2794
+ className: "cursor-pointer rounded px-1.5 py-0.5 text-xs text-quaternary underline underline-offset-2 transition-colors hover:text-secondary"
2795
+ },
2796
+ label
2797
+ );
2798
+ }
2799
+ function LoginWall({ message, cta }) {
2800
+ return /* @__PURE__ */ React8.createElement("div", { className: "flex flex-col items-center justify-center py-20 text-center" }, /* @__PURE__ */ React8.createElement("div", { className: "mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-secondary" }, /* @__PURE__ */ React8.createElement("svg", { className: "size-5 text-quaternary", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5 }, /* @__PURE__ */ React8.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" }))), /* @__PURE__ */ React8.createElement("p", { className: "text-sm font-medium text-primary" }, message), /* @__PURE__ */ React8.createElement(
2801
+ "button",
2802
+ {
2803
+ "data-open-login-modal": true,
2804
+ className: "mt-4 cursor-pointer rounded-interactive bg-brand-solid px-5 py-2 text-sm font-semibold text-white transition-colors hover:bg-brand-solid_hover"
2805
+ },
2806
+ cta
2807
+ ));
2808
+ }
2809
+ function EmptyState({ message }) {
2810
+ return /* @__PURE__ */ React8.createElement("div", { className: "rounded-component border border-secondary bg-secondary py-16 text-center" }, /* @__PURE__ */ React8.createElement("p", { className: "text-sm text-tertiary" }, message));
2811
+ }
2812
+ function ServiceItemRow({
2813
+ item,
2814
+ isLoggedIn,
2815
+ specialsEnabled,
2816
+ specialsHref
2817
+ }) {
2818
+ var _a;
2819
+ const activeOffers = ((_a = item.offers) != null ? _a : []).filter((o) => o.active !== false && !o.expired);
2820
+ return /* @__PURE__ */ React8.createElement("div", { className: "group flex items-center gap-3 px-4 py-3" }, /* @__PURE__ */ React8.createElement(
2821
+ RowThumbnail,
2822
+ {
2823
+ photoAttachments: item.photo_attachments,
2824
+ seed: `item-${item.id}`,
2825
+ alt: item.name
2826
+ }
2827
+ ), /* @__PURE__ */ React8.createElement("div", { className: "flex-1 min-w-0 flex items-center justify-between gap-3" }, /* @__PURE__ */ React8.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React8.createElement("p", { className: "text-base font-medium text-primary" }, item.name), item.summary && /* @__PURE__ */ React8.createElement("p", { className: "mt-0.5 text-sm text-tertiary" }, item.summary), item.duration_minutes != null && item.duration_minutes > 0 && /* @__PURE__ */ React8.createElement("p", { className: "mt-0.5 text-xs text-quaternary" }, item.duration_minutes, " min"), specialsEnabled && activeOffers.length > 0 && /* @__PURE__ */ React8.createElement("div", { className: "mt-1.5 flex flex-wrap gap-1" }, activeOffers.map(
2828
+ (offer) => isLoggedIn ? /* @__PURE__ */ React8.createElement(
2829
+ Link,
2830
+ {
2831
+ key: offer.id,
2832
+ href: specialsHref,
2833
+ className: "inline-flex items-center rounded-badge bg-secondary border border-brand px-2 py-0.5 text-xs font-medium text-brand-secondary hover:bg-secondary_hover transition-colors"
2834
+ },
2835
+ "Special: ",
2836
+ offer.name
2837
+ ) : /* @__PURE__ */ React8.createElement(
2838
+ "button",
2839
+ {
2840
+ key: offer.id,
2841
+ "data-open-login-modal": true,
2842
+ "data-login-redirect": specialsHref,
2843
+ className: "inline-flex items-center rounded-badge bg-secondary border border-brand px-2 py-0.5 text-xs font-medium text-brand-secondary hover:bg-secondary_hover transition-colors cursor-pointer"
2844
+ },
2845
+ "Special: ",
2846
+ offer.name
2847
+ )
2848
+ ))), /* @__PURE__ */ React8.createElement("div", { className: "shrink-0 text-right" }, isLoggedIn ? item.price_cents != null ? /* @__PURE__ */ React8.createElement("span", { className: "text-sm font-semibold text-primary" }, formatCents(item.price_cents)) : item.pricing_info ? /* @__PURE__ */ React8.createElement("span", { className: "text-sm text-secondary" }, item.pricing_info) : null : item.price_cents != null || item.pricing_info ? /* @__PURE__ */ React8.createElement(LockButton, null) : null)));
2849
+ }
2850
+ function ServicesPanel({
2851
+ services,
2852
+ isLoggedIn,
2853
+ specialsEnabled,
2854
+ portalHref
2855
+ }) {
2856
+ const activeServices = services.filter((s) => {
2857
+ var _a, _b;
2858
+ return ((_b = (_a = s.service_items) == null ? void 0 : _a.length) != null ? _b : 0) > 0;
2859
+ });
2860
+ const specialsHref = `${portalHref}?tab=specials`;
2861
+ if (activeServices.length === 0) {
2862
+ return /* @__PURE__ */ React8.createElement(EmptyState, { message: "No services listed yet." });
2863
+ }
2864
+ return /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(PortalTabTracker, { event: "ViewContent", params: { contentName: "Services", contentCategory: "Services" }, tab: "services" }), /* @__PURE__ */ React8.createElement("div", { className: "divide-y divide-tertiary rounded-component border border-secondary bg-primary overflow-hidden" }, activeServices.map((service) => {
2865
+ var _a, _b;
2866
+ return /* @__PURE__ */ React8.createElement("details", { key: service.id, className: "group", open: true }, /* @__PURE__ */ React8.createElement("summary", { className: "flex cursor-pointer list-none items-center justify-between px-5 py-4 hover:bg-secondary transition-colors" }, /* @__PURE__ */ React8.createElement("div", null, /* @__PURE__ */ React8.createElement("span", { className: "font-medium text-primary" }, service.name), service.summary && /* @__PURE__ */ React8.createElement("p", { className: "mt-0.5 text-sm text-tertiary" }, service.summary)), /* @__PURE__ */ React8.createElement("div", { className: "ml-4 flex items-center gap-2 shrink-0" }, /* @__PURE__ */ React8.createElement("span", { className: "text-xs text-quaternary" }, (_b = (_a = service.service_items) == null ? void 0 : _a.length) != null ? _b : 0, " items"), /* @__PURE__ */ React8.createElement("svg", { className: "size-4 text-quaternary transition-transform group-open:rotate-180", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2 }, /* @__PURE__ */ React8.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" })))), service.service_items && service.service_items.length > 0 && /* @__PURE__ */ React8.createElement("div", { className: "border-t border-tertiary bg-secondary divide-y divide-tertiary" }, service.service_items.map((item) => /* @__PURE__ */ React8.createElement(
2867
+ ServiceItemRow,
2868
+ {
2869
+ key: item.id,
2870
+ item,
2871
+ isLoggedIn,
2872
+ specialsEnabled,
2873
+ specialsHref
2874
+ }
2875
+ ))));
2876
+ })));
2877
+ }
2878
+ function PackagesPanel({
2879
+ packages,
2880
+ isLoggedIn,
2881
+ specialsEnabled,
2882
+ portalHref
2883
+ }) {
2884
+ const specialsHref = `${portalHref}?tab=specials`;
2885
+ if (packages.length === 0) return /* @__PURE__ */ React8.createElement(EmptyState, { message: "No packages available yet." });
2886
+ return /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(PortalTabTracker, { event: "ViewContent", params: { contentName: "Packages", contentCategory: "Packages" }, tab: "packages" }), /* @__PURE__ */ React8.createElement("div", { className: "grid gap-4 sm:grid-cols-2" }, packages.map((pkg) => {
2887
+ var _a;
2888
+ const activeOffers = ((_a = pkg.offers) != null ? _a : []).filter((o) => o.active !== false && !o.expired);
2889
+ return /* @__PURE__ */ React8.createElement("div", { key: pkg.id, className: "group rounded-component border border-secondary bg-primary p-4 flex flex-col gap-3" }, /* @__PURE__ */ React8.createElement("div", { className: "flex items-start gap-3" }, /* @__PURE__ */ React8.createElement(
2890
+ RowThumbnail,
2891
+ {
2892
+ photoAttachments: pkg.photo_attachments,
2893
+ seed: `pkg-${pkg.id}`,
2894
+ alt: pkg.name,
2895
+ sizeClassName: "w-16 h-16"
2896
+ }
2897
+ ), /* @__PURE__ */ React8.createElement("div", { className: "flex-1 min-w-0 flex items-start justify-between gap-2" }, /* @__PURE__ */ React8.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React8.createElement("h4", { className: "font-semibold text-primary" }, pkg.name), pkg.summary && /* @__PURE__ */ React8.createElement("p", { className: "mt-0.5 text-sm text-tertiary" }, pkg.summary)), /* @__PURE__ */ React8.createElement("div", { className: "shrink-0 text-right" }, isLoggedIn ? pkg.price_cents != null ? /* @__PURE__ */ React8.createElement("span", { className: "text-xl font-bold text-primary" }, formatCents(pkg.price_cents)) : pkg.pricing_info ? /* @__PURE__ */ React8.createElement("span", { className: "text-sm font-medium text-secondary" }, pkg.pricing_info) : null : pkg.price_cents != null || pkg.pricing_info ? /* @__PURE__ */ React8.createElement(LockButton, null) : null))), pkg.package_items && pkg.package_items.length > 0 && /* @__PURE__ */ React8.createElement("ul", { className: "space-y-1.5" }, pkg.package_items.map((item, idx) => {
2898
+ var _a2, _b;
2899
+ return /* @__PURE__ */ React8.createElement("li", { key: idx, className: "flex items-center gap-2 text-sm text-secondary" }, /* @__PURE__ */ React8.createElement("span", { className: "size-1.5 rounded-full bg-quaternary shrink-0" }), item.quantity > 1 && /* @__PURE__ */ React8.createElement("span", { className: "font-medium text-primary" }, item.quantity, "\xD7"), (_b = (_a2 = item.service_item) == null ? void 0 : _a2.name) != null ? _b : "\u2014");
2900
+ })), specialsEnabled && activeOffers.length > 0 && /* @__PURE__ */ React8.createElement("div", { className: "flex flex-wrap gap-1.5" }, activeOffers.map(
2901
+ (offer) => isLoggedIn ? /* @__PURE__ */ React8.createElement(
2902
+ Link,
2903
+ {
2904
+ key: offer.id,
2905
+ href: specialsHref,
2906
+ className: "inline-flex items-center rounded-badge bg-secondary border border-brand px-2.5 py-0.5 text-xs font-medium text-brand-secondary hover:bg-secondary_hover transition-colors"
2907
+ },
2908
+ "Special: ",
2909
+ offer.name
2910
+ ) : /* @__PURE__ */ React8.createElement(
2911
+ "button",
2912
+ {
2913
+ key: offer.id,
2914
+ "data-open-login-modal": true,
2915
+ "data-login-redirect": specialsHref,
2916
+ className: "inline-flex items-center rounded-badge bg-secondary border border-brand px-2.5 py-0.5 text-xs font-medium text-brand-secondary hover:bg-secondary_hover transition-colors cursor-pointer"
2917
+ },
2918
+ "Special: ",
2919
+ offer.name
2920
+ )
2921
+ )));
2922
+ })));
2923
+ }
2924
+ function SpecialsPanel({ specials }) {
2925
+ if (specials.length === 0) {
2926
+ return /* @__PURE__ */ React8.createElement(EmptyState, { message: "No active specials right now. Check back soon." });
2927
+ }
2928
+ return /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(PortalTabTracker, { event: "ViewContent", params: { contentName: "Specials", contentCategory: "Specials" }, tab: "specials" }), /* @__PURE__ */ React8.createElement("div", { className: "space-y-3" }, specials.map((special) => /* @__PURE__ */ React8.createElement("div", { key: special.id, className: "group flex items-start gap-3 rounded-component border border-secondary bg-primary px-4 py-4" }, /* @__PURE__ */ React8.createElement(
2929
+ RowThumbnail,
2930
+ {
2931
+ photoAttachments: special.photoAttachments,
2932
+ seed: `special-${special.id}`,
2933
+ alt: special.parentName
2934
+ }
2935
+ ), /* @__PURE__ */ React8.createElement("div", { className: "flex-1 min-w-0" }, /* @__PURE__ */ React8.createElement("p", { className: "font-semibold text-primary" }, special.name), special.value_terms && /* @__PURE__ */ React8.createElement("p", { className: "mt-0.5 text-sm text-secondary" }, special.value_terms), /* @__PURE__ */ React8.createElement("p", { className: "mt-1 text-xs text-quaternary" }, special.parentType === "service" ? "Service" : "Package", ": ", special.parentName), special.expires_at && /* @__PURE__ */ React8.createElement("p", { className: "mt-0.5 text-xs text-quaternary" }, "Expires ", new Date(special.expires_at).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })))))));
2936
+ }
2937
+ function MessagesPanel({
2938
+ messages,
2939
+ contactSummary,
2940
+ contactId,
2941
+ businessName
2942
+ }) {
2943
+ var _a, _b;
2944
+ const threadBusiness = ((_a = contactSummary == null ? void 0 : contactSummary.business) == null ? void 0 : _a.company_name) || ((_b = contactSummary == null ? void 0 : contactSummary.business) == null ? void 0 : _b.name) || businessName;
2945
+ return /* @__PURE__ */ React8.createElement("div", { className: "flex flex-col rounded-component border border-secondary bg-primary overflow-hidden" }, /* @__PURE__ */ React8.createElement("div", { className: "flex items-center gap-2.5 border-b border-secondary px-4 py-3 shrink-0" }, /* @__PURE__ */ React8.createElement("div", { className: "flex h-7 w-7 items-center justify-center rounded-full bg-brand-solid text-xs font-semibold text-white shrink-0" }, getInitials(threadBusiness)), /* @__PURE__ */ React8.createElement("span", { className: "text-sm font-semibold text-primary" }, threadBusiness)), /* @__PURE__ */ React8.createElement("div", { className: "overflow-y-auto px-4 py-4 space-y-3", style: { height: "70vh" } }, messages.length === 0 ? /* @__PURE__ */ React8.createElement("div", { className: "flex flex-col items-center justify-center h-full py-16 text-center" }, /* @__PURE__ */ React8.createElement("div", { className: "mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-secondary" }, /* @__PURE__ */ React8.createElement("svg", { className: "size-5 text-quaternary", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5 }, /* @__PURE__ */ React8.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z" }))), /* @__PURE__ */ React8.createElement("p", { className: "text-sm font-medium text-primary" }, "No messages yet"), /* @__PURE__ */ React8.createElement("p", { className: "mt-1 text-xs text-tertiary" }, "Send a message to start the conversation.")) : messages.map((m) => {
2946
+ const isOutbound = m.direction === "outbound";
2947
+ return /* @__PURE__ */ React8.createElement("div", { key: m.id, className: `flex ${isOutbound ? "justify-start" : "justify-end"}` }, /* @__PURE__ */ React8.createElement("div", { className: `max-w-[80%] rounded-component px-4 py-2.5 text-sm ${isOutbound ? "bg-secondary text-primary" : "bg-brand-solid text-white"}` }, m.sender_display_name && /* @__PURE__ */ React8.createElement("p", { className: "mb-0.5 text-xs font-medium opacity-60" }, m.sender_display_name), /* @__PURE__ */ React8.createElement("p", { className: "whitespace-pre-wrap leading-relaxed" }, m.body || "\u2014"), /* @__PURE__ */ React8.createElement("p", { className: "mt-1 text-right text-[10px] opacity-50" }, formatTime(m.created_at))));
2948
+ })), contactId && /* @__PURE__ */ React8.createElement(MessageComposer, { contactId }));
2949
+ }
2950
+ function BookPanel({
2951
+ bookingHref,
2952
+ bookingLabel,
2953
+ bookingAllowsIframe,
2954
+ isLoggedIn,
2955
+ businessName
2956
+ }) {
2957
+ if (!isLoggedIn) {
2958
+ return /* @__PURE__ */ React8.createElement(LoginWall, { message: "Continue to view booking options.", cta: "View Booking Options" });
2959
+ }
2960
+ if (bookingAllowsIframe) {
2961
+ return /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(PortalTabTracker, { event: "InitiateCheckout", tab: "booking" }), /* @__PURE__ */ React8.createElement(BookIframePanel, { bookingHref, businessName }));
2962
+ }
2963
+ return /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(PortalTabTracker, { event: "InitiateCheckout", tab: "booking" }), /* @__PURE__ */ React8.createElement("div", { className: "flex flex-col items-center justify-center py-20 text-center" }, /* @__PURE__ */ React8.createElement("div", { className: "mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-secondary" }, /* @__PURE__ */ React8.createElement("svg", { className: "size-5 text-quaternary", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5 }, /* @__PURE__ */ React8.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 9v7.5" }))), /* @__PURE__ */ React8.createElement("p", { className: "text-sm font-medium text-primary" }, "Ready to book?"), /* @__PURE__ */ React8.createElement("p", { className: "mt-1 text-sm text-tertiary" }, "You'll be taken to our booking system."), /* @__PURE__ */ React8.createElement(
2964
+ "a",
2965
+ {
2966
+ href: bookingHref,
2967
+ target: "_blank",
2968
+ rel: "noopener noreferrer",
2969
+ className: "mt-5 inline-flex items-center gap-2 rounded-interactive bg-brand-solid px-6 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-solid_hover"
2970
+ },
2971
+ bookingLabel,
2972
+ /* @__PURE__ */ React8.createElement("svg", { className: "size-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2 }, /* @__PURE__ */ React8.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" }))
2973
+ )));
2974
+ }
2975
+ async function PortalPage({
2976
+ searchParams,
2977
+ portalHref = "/portal",
2978
+ bookingHref,
2979
+ bookingLabel = "Book Now",
2980
+ enabledTabs,
2981
+ tabLabels,
2982
+ forceExternalBooking = false,
2983
+ contactHref = "/contact"
2984
+ }) {
2985
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
2986
+ const params = (_a = await searchParams) != null ? _a : {};
2987
+ const contactIdParsed = params.contact ? parseInt(params.contact, 10) : NaN;
2988
+ const contactId = Number.isFinite(contactIdParsed) ? contactIdParsed : null;
2989
+ const cookieStore = await cookies();
2990
+ const token = (_c = (_b = cookieStore.get(CONSUMER_TOKEN_COOKIE)) == null ? void 0 : _b.value) != null ? _c : null;
2991
+ const [services, packages, companyInformation, consumer, conversations] = await Promise.all([
2992
+ getServices(),
2993
+ getPackages(),
2994
+ getCompanyInformation(),
2995
+ token ? fetchConsumerMe(token) : Promise.resolve(null),
2996
+ token ? fetchConsumerConversations(token) : Promise.resolve([])
2997
+ ]);
2998
+ const resolvedBookingHref = (_d = bookingHref != null ? bookingHref : companyInformation == null ? void 0 : companyInformation.external_management_url) != null ? _d : null;
2999
+ const hasCustomTabOrder = Array.isArray(enabledTabs) && enabledTabs.length > 0;
3000
+ const requestedTabs = (enabledTabs != null ? enabledTabs : DEFAULT_TABS).filter((tab2, index, arr) => ALL_TABS.includes(tab2) && arr.indexOf(tab2) === index);
3001
+ const normalizedTabs = requestedTabs.length > 0 ? requestedTabs : DEFAULT_TABS;
3002
+ const nonBookTabs = normalizedTabs.filter((tab2) => tab2 !== "book");
3003
+ const includesBookTab = normalizedTabs.includes("book");
3004
+ const labelForTab = (tab2) => {
3005
+ var _a2, _b2, _c2, _d2, _e2;
3006
+ if (tab2 === "book") return (_a2 = tabLabels == null ? void 0 : tabLabels.book) != null ? _a2 : bookingLabel;
3007
+ if (tab2 === "services") return (_b2 = tabLabels == null ? void 0 : tabLabels.services) != null ? _b2 : "Services";
3008
+ if (tab2 === "packages") return (_c2 = tabLabels == null ? void 0 : tabLabels.packages) != null ? _c2 : "Packages";
3009
+ if (tab2 === "specials") return (_d2 = tabLabels == null ? void 0 : tabLabels.specials) != null ? _d2 : "Specials";
3010
+ return (_e2 = tabLabels == null ? void 0 : tabLabels.messages) != null ? _e2 : "Messages";
3011
+ };
3012
+ const tabs = hasCustomTabOrder ? normalizedTabs.map((tab2) => ({ id: tab2, label: labelForTab(tab2) })) : [
3013
+ ...includesBookTab && resolvedBookingHref ? [{ id: "book", label: labelForTab("book") }] : [],
3014
+ ...nonBookTabs.map((tab2) => ({ id: tab2, label: labelForTab(tab2) })),
3015
+ ...includesBookTab && !resolvedBookingHref ? [{ id: "book", label: labelForTab("book") }] : []
3016
+ ];
3017
+ const selectableTabIds = tabs.map((tab2) => tab2.id);
3018
+ const tabParam = selectableTabIds.includes(params.tab) ? params.tab : null;
3019
+ const tab = tabParam != null ? tabParam : tabs[0].id;
3020
+ const bookingAllowsIframe = !forceExternalBooking && tab === "book" && resolvedBookingHref ? await checkIframeAllowed(resolvedBookingHref) : false;
3021
+ const businessName = (companyInformation == null ? void 0 : companyInformation.company_name) || "Member Portal";
3022
+ const isLoggedIn = !!consumer;
3023
+ const resolvedContactId = (_f = contactId != null ? contactId : (_e = conversations[0]) == null ? void 0 : _e.contact_id) != null ? _f : null;
3024
+ const businessContact = (_g = consumer == null ? void 0 : consumer.contacts) == null ? void 0 : _g.find((c) => c.id === resolvedContactId);
3025
+ const consumerDisplayName = (businessContact == null ? void 0 : businessContact.display_name) || ((_i = (_h = consumer == null ? void 0 : consumer.contacts) == null ? void 0 : _h[0]) == null ? void 0 : _i.display_name) || (consumer == null ? void 0 : consumer.primary_identifier) || (consumer == null ? void 0 : consumer.email) || (consumer == null ? void 0 : consumer.phone) || null;
3026
+ const messageData = token && tab === "messages" && resolvedContactId ? await fetchConsumerMessages(token, resolvedContactId) : { messages: [], contact: null };
3027
+ const serviceList = services != null ? services : [];
3028
+ const packageList = packages != null ? packages : [];
3029
+ const specials = aggregateSpecials(serviceList, packageList);
3030
+ const specialsEnabled = selectableTabIds.includes("specials");
3031
+ return /* @__PURE__ */ React8.createElement("div", { className: "min-h-screen bg-primary" }, /* @__PURE__ */ React8.createElement("div", { className: "border-b border-secondary bg-primary sticky top-0 z-40 shrink-0" }, /* @__PURE__ */ React8.createElement("div", { className: "mx-auto max-w-4xl px-4 py-4" }, /* @__PURE__ */ React8.createElement("div", { className: "flex items-center justify-between gap-4" }, /* @__PURE__ */ React8.createElement("div", null, /* @__PURE__ */ React8.createElement("p", { className: "text-[10px] font-semibold uppercase tracking-widest text-quaternary" }, "Member Portal"), /* @__PURE__ */ React8.createElement("h1", { className: "text-xl font-bold text-primary leading-tight" }, businessName)), /* @__PURE__ */ React8.createElement("div", { className: "flex items-center gap-3" }, isLoggedIn ? /* @__PURE__ */ React8.createElement("div", { className: "flex items-center gap-3" }, consumerDisplayName && /* @__PURE__ */ React8.createElement("div", { className: "hidden sm:flex items-center gap-2 rounded-full border border-secondary bg-secondary px-3 py-1.5" }, /* @__PURE__ */ React8.createElement("div", { className: "flex h-5 w-5 items-center justify-center rounded-full bg-brand-solid text-[9px] font-bold text-white shrink-0" }, getInitials(consumerDisplayName)), /* @__PURE__ */ React8.createElement("span", { className: "text-xs font-medium text-secondary max-w-[120px] truncate" }, consumerDisplayName)), /* @__PURE__ */ React8.createElement(LogoutButton, null)) : /* @__PURE__ */ React8.createElement(
3032
+ "button",
3033
+ {
3034
+ "data-open-login-modal": true,
3035
+ className: "cursor-pointer rounded-interactive border border-secondary px-3 py-1.5 text-xs font-medium text-secondary hover:bg-secondary transition-colors"
3036
+ },
3037
+ "Sign in"
3038
+ ), /* @__PURE__ */ React8.createElement("div", { className: "border-l border-tertiary pl-3" }, /* @__PURE__ */ React8.createElement(
3039
+ "a",
3040
+ {
3041
+ href: KEYSTONE_APP_URL,
3042
+ target: "_blank",
3043
+ rel: "noopener noreferrer",
3044
+ "aria-label": "Visit Keystone website"
3045
+ },
3046
+ /* @__PURE__ */ React8.createElement(
3047
+ KeystoneBrandLockup,
3048
+ {
3049
+ className: "gap-2",
3050
+ iconClassName: "h-3.5",
3051
+ wordmarkClassName: "hidden sm:block h-2.5"
3052
+ }
3053
+ )
3054
+ )))), /* @__PURE__ */ React8.createElement("div", { className: "relative mt-4" }, /* @__PURE__ */ React8.createElement("div", { className: "flex gap-1 overflow-x-auto pb-px scrollbar-none" }, tabs.map((t) => {
3055
+ const isActive = tab === t.id;
3056
+ const isGated = !isLoggedIn && (t.id === "specials" || t.id === "messages" || t.id === "book");
3057
+ const href = `${portalHref}?tab=${t.id}`;
3058
+ const className = `shrink-0 rounded-interactive px-4 py-2 text-sm font-medium transition-colors whitespace-nowrap ${isActive ? "bg-brand-solid text-white" : "text-secondary hover:bg-secondary hover:text-primary"}`;
3059
+ if (isGated) {
3060
+ return /* @__PURE__ */ React8.createElement(
3061
+ "button",
3062
+ {
3063
+ key: t.id,
3064
+ "data-open-login-modal": true,
3065
+ "data-login-redirect": href,
3066
+ className
3067
+ },
3068
+ t.label
3069
+ );
3070
+ }
3071
+ return /* @__PURE__ */ React8.createElement(Link, { key: t.id, href, className }, t.label);
3072
+ })), /* @__PURE__ */ React8.createElement("div", { className: "pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-primary to-transparent md:hidden" })))), /* @__PURE__ */ React8.createElement("div", { className: "mx-auto max-w-4xl px-4 py-8" }, tab === "services" && /* @__PURE__ */ React8.createElement(
3073
+ ServicesPanel,
3074
+ {
3075
+ services: serviceList,
3076
+ isLoggedIn,
3077
+ specialsEnabled,
3078
+ portalHref
3079
+ }
3080
+ ), tab === "packages" && /* @__PURE__ */ React8.createElement(
3081
+ PackagesPanel,
3082
+ {
3083
+ packages: packageList,
3084
+ isLoggedIn,
3085
+ specialsEnabled,
3086
+ portalHref
3087
+ }
3088
+ ), tab === "specials" && (isLoggedIn ? /* @__PURE__ */ React8.createElement(SpecialsPanel, { specials }) : /* @__PURE__ */ React8.createElement(LoginWall, { message: "Continue to view specials.", cta: "View Specials" })), tab === "messages" && (isLoggedIn ? /* @__PURE__ */ React8.createElement(
3089
+ MessagesPanel,
3090
+ {
3091
+ messages: messageData.messages,
3092
+ contactSummary: messageData.contact,
3093
+ contactId: resolvedContactId,
3094
+ businessName
3095
+ }
3096
+ ) : /* @__PURE__ */ React8.createElement(LoginWall, { message: "Continue to view your messages.", cta: "View Messages" })), tab === "book" && (resolvedBookingHref ? /* @__PURE__ */ React8.createElement(
3097
+ BookPanel,
3098
+ {
3099
+ bookingHref: resolvedBookingHref,
3100
+ bookingLabel,
3101
+ bookingAllowsIframe,
3102
+ isLoggedIn,
3103
+ businessName
3104
+ }
3105
+ ) : /* @__PURE__ */ React8.createElement("div", { className: "flex flex-col items-center justify-center py-20 text-center" }, /* @__PURE__ */ React8.createElement("div", { className: "mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-secondary" }, /* @__PURE__ */ React8.createElement("svg", { className: "size-5 text-quaternary", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5 }, /* @__PURE__ */ React8.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 9v7.5" }))), /* @__PURE__ */ React8.createElement("p", { className: "text-sm font-medium text-primary" }, "Ready to book?"), /* @__PURE__ */ React8.createElement("p", { className: "mt-1 text-sm text-tertiary max-w-xs" }, "Give us a call or submit a request on our", " ", /* @__PURE__ */ React8.createElement("a", { href: contactHref, className: "underline underline-offset-2 hover:text-primary transition-colors" }, "contact page"), " ", "and we'll get you scheduled.")))), /* @__PURE__ */ React8.createElement(LoginModalController, null));
3106
+ }
3107
+ export {
3108
+ LoginForm,
3109
+ LoginModalController,
3110
+ LogoutButton,
3111
+ PortalPage
3112
+ };
3113
+ //# sourceMappingURL=index.js.map