keystone-design-bootstrap 1.0.85 → 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.
- package/dist/company-information-C1pP-SvU.d.ts +50 -0
- package/dist/config-C_XBZixg.d.ts +21 -0
- package/dist/consumer-BWjQawiO.d.ts +48 -0
- package/dist/design_system/portal/index.d.ts +52 -0
- package/dist/design_system/portal/index.js +3113 -0
- package/dist/design_system/portal/index.js.map +1 -0
- package/dist/design_system/sections/index.d.ts +2 -1
- package/dist/index.d.ts +5 -24
- package/dist/index.js +156 -37
- package/dist/index.js.map +1 -1
- package/dist/lib/consumer-session.d.ts +16 -0
- package/dist/lib/consumer-session.js +85 -0
- package/dist/lib/consumer-session.js.map +1 -0
- package/dist/lib/cta-urls.d.ts +34 -0
- package/dist/lib/cta-urls.js +33 -0
- package/dist/lib/cta-urls.js.map +1 -0
- package/dist/lib/server-api.d.ts +2 -1
- package/dist/lib/server-api.js +1 -1
- package/dist/lib/server-api.js.map +1 -1
- package/dist/next/contexts/form-definitions.d.ts +17 -0
- package/dist/next/contexts/form-definitions.js +21 -0
- package/dist/next/contexts/form-definitions.js.map +1 -0
- package/dist/next/gallery/design-gallery.d.ts +103 -0
- package/dist/next/gallery/design-gallery.js +19301 -0
- package/dist/next/gallery/design-gallery.js.map +1 -0
- package/dist/next/layouts/root-layout.d.ts +55 -0
- package/dist/next/layouts/root-layout.js +19713 -0
- package/dist/next/layouts/root-layout.js.map +1 -0
- package/dist/next/legal/privacy-policy.d.ts +7 -0
- package/dist/next/legal/privacy-policy.js +18949 -0
- package/dist/next/legal/privacy-policy.js.map +1 -0
- package/dist/next/legal/terms-of-service.d.ts +7 -0
- package/dist/next/legal/terms-of-service.js +18949 -0
- package/dist/next/legal/terms-of-service.js.map +1 -0
- package/dist/next/providers/ssr-provider.d.ts +12 -0
- package/dist/next/providers/ssr-provider.js +12 -0
- package/dist/next/providers/ssr-provider.js.map +1 -0
- package/dist/next/routes/chat.d.ts +26 -0
- package/dist/next/routes/chat.js +160 -0
- package/dist/next/routes/chat.js.map +1 -0
- package/dist/next/routes/consumer-auth.d.ts +33 -0
- package/dist/next/routes/consumer-auth.js +254 -0
- package/dist/next/routes/consumer-auth.js.map +1 -0
- package/dist/next/routes/form.d.ts +37 -0
- package/dist/next/routes/form.js +97 -0
- package/dist/next/routes/form.js.map +1 -0
- package/dist/package-IU_GpDA0.d.ts +74 -0
- package/dist/types/index.d.ts +6 -68
- package/package.json +30 -28
- package/src/design_system/chat/useRealtimeReplyOrchestrator.ts +127 -0
- package/src/design_system/components/ChatWidget.tsx +58 -37
- package/src/design_system/portal/MessageComposer.tsx +53 -1
- package/src/design_system/portal/PortalPage.tsx +3 -2
- package/src/lib/server-api.ts +2 -1
- package/src/next/routes/chat.ts +57 -1
- package/src/types/rails-actioncable.d.ts +16 -0
- package/dist/package-DeHKpQp7.d.ts +0 -121
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
3
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
4
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
5
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
6
|
+
var __spreadValues = (a, b) => {
|
|
7
|
+
for (var prop in b || (b = {}))
|
|
8
|
+
if (__hasOwnProp.call(b, prop))
|
|
9
|
+
__defNormalProp(a, prop, b[prop]);
|
|
10
|
+
if (__getOwnPropSymbols)
|
|
11
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
12
|
+
if (__propIsEnum.call(b, prop))
|
|
13
|
+
__defNormalProp(a, prop, b[prop]);
|
|
14
|
+
}
|
|
15
|
+
return a;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/next/routes/proxy-headers.ts
|
|
19
|
+
function clientContextHeaders(request) {
|
|
20
|
+
var _a, _b;
|
|
21
|
+
const ip = request.headers.get("x-real-ip") || ((_b = (_a = request.headers.get("x-forwarded-for")) == null ? void 0 : _a.split(",")[0]) == null ? void 0 : _b.trim());
|
|
22
|
+
const ua = request.headers.get("user-agent");
|
|
23
|
+
const cookieHeader = request.headers.get("cookie") || "";
|
|
24
|
+
const cookies = Object.fromEntries(
|
|
25
|
+
cookieHeader.split(";").map((c) => {
|
|
26
|
+
const [k, ...v] = c.trim().split("=");
|
|
27
|
+
return [k, v.join("=")];
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
const fbp = cookies["_fbp"];
|
|
31
|
+
const fbc = cookies["_fbc"];
|
|
32
|
+
const headers = {};
|
|
33
|
+
if (ip) headers["X-Real-Client-IP"] = ip;
|
|
34
|
+
if (ua) headers["X-Real-Client-UA"] = ua;
|
|
35
|
+
if (fbp) headers["X-Meta-FBP"] = fbp;
|
|
36
|
+
if (fbc) headers["X-Meta-FBC"] = fbc;
|
|
37
|
+
return headers;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/next/routes/form.ts
|
|
41
|
+
var API_URL = process.env.API_URL || "http://localhost:3000/api/v1";
|
|
42
|
+
var API_KEY = process.env.API_KEY || "";
|
|
43
|
+
function createFormRouteHandlers(deps) {
|
|
44
|
+
var _a, _b;
|
|
45
|
+
const json = (_b = (_a = deps == null ? void 0 : deps.NextResponse) == null ? void 0 : _a.json) != null ? _b : ((body, init) => Response.json(body, init));
|
|
46
|
+
return {
|
|
47
|
+
POST: async (request) => {
|
|
48
|
+
var _a2;
|
|
49
|
+
try {
|
|
50
|
+
const body = await request.json();
|
|
51
|
+
const formType = body == null ? void 0 : body.formType;
|
|
52
|
+
if (!formType) {
|
|
53
|
+
return json({ success: false, error: "Form type is required." }, { status: 400 });
|
|
54
|
+
}
|
|
55
|
+
const response = await fetch(`${API_URL}/public/form_submissions`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: __spreadValues({
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
"X-API-Key": API_KEY
|
|
60
|
+
}, clientContextHeaders(request)),
|
|
61
|
+
body: JSON.stringify(body)
|
|
62
|
+
});
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
return json(
|
|
66
|
+
{
|
|
67
|
+
success: false,
|
|
68
|
+
error: data.error || "Failed to submit form. Please try again."
|
|
69
|
+
},
|
|
70
|
+
{ status: response.status }
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const payload = {
|
|
74
|
+
success: true,
|
|
75
|
+
message: data.message || "Form submitted successfully."
|
|
76
|
+
};
|
|
77
|
+
if (formType === "lead" && ((_a2 = data.data) == null ? void 0 : _a2.event_id)) {
|
|
78
|
+
payload.eventId = data.data.event_id;
|
|
79
|
+
}
|
|
80
|
+
return json(payload);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error("Form submission error:", error);
|
|
83
|
+
return json(
|
|
84
|
+
{
|
|
85
|
+
success: false,
|
|
86
|
+
error: "Network error. Please try again later."
|
|
87
|
+
},
|
|
88
|
+
{ status: 500 }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
createFormRouteHandlers
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/next/routes/proxy-headers.ts","../../../src/next/routes/form.ts"],"sourcesContent":["/**\n * Extracts the real client IP, user-agent, and Meta browser cookies (_fbp / _fbc)\n * from an incoming Next.js route request and returns them as headers to forward\n * to the upstream Rails API.\n *\n * Cloudflare and other load-balancers set x-real-ip (or x-forwarded-for) on\n * inbound requests before they reach the Next.js function. Without this, the\n * Rails API sees the Next.js server IP instead of the real browser IP, which\n * produces inaccurate server-side CAPI signals.\n *\n * _fbp (Meta Browser ID) and _fbc (Meta Click ID) are first-party cookies set by\n * the Meta Pixel. Forwarding them allows the API to store them on the Contact\n * record so they can be included in all subsequent CAPI events — even those fired\n * from background jobs that have no live HTTP request.\n *\n * Convention:\n * X-Real-Client-IP — real browser IP\n * X-Real-Client-UA — real browser user-agent\n * X-Meta-FBP — value of the _fbp cookie\n * X-Meta-FBC — value of the _fbc cookie (or built from fbclid param)\n */\nexport function clientContextHeaders(request: Request): Record<string, string> {\n const ip =\n request.headers.get('x-real-ip') ||\n request.headers.get('x-forwarded-for')?.split(',')[0]?.trim();\n const ua = request.headers.get('user-agent');\n\n const cookieHeader = request.headers.get('cookie') || '';\n const cookies = Object.fromEntries(\n cookieHeader.split(';').map((c) => {\n const [k, ...v] = c.trim().split('=');\n return [k, v.join('=')];\n })\n );\n const fbp = cookies['_fbp'];\n const fbc = cookies['_fbc'];\n\n const headers: Record<string, string> = {};\n if (ip) headers['X-Real-Client-IP'] = ip;\n if (ua) headers['X-Real-Client-UA'] = ua;\n if (fbp) headers['X-Meta-FBP'] = fbp;\n if (fbc) headers['X-Meta-FBC'] = fbc;\n return headers;\n}\n","/**\n * Form submission proxy route handler for Next.js App Router.\n *\n * Usage in a site/template:\n * // app/api/form/route.ts\n * export { POST } from 'keystone-design-bootstrap/next/routes/form';\n *\n * Env (server-side only):\n * - API_URL (default: http://localhost:3000/api/v1)\n * - API_KEY\n *\n * ## Meta Pixel + CAPI tracking for custom forms\n *\n * POST body must include `formType: 'lead'` for conversion tracking to fire:\n * - Server-side: the API automatically fires a CAPI Lead event (no extra work needed).\n * - Client-side: the response includes `eventId` for browser/server deduplication.\n * After a successful response, call these two functions from 'keystone-design-bootstrap/tracking':\n *\n * const result = await response.json();\n * if (result.success) {\n * await setPixelUserData({ email, phone }); // hash + store identity for the session\n * firePixelEvent('Lead', undefined, result.eventId); // fire fbq('track', 'Lead') with server event ID for dedup\n * }\n *\n * Both tracking calls are silent no-ops when no Meta Pixel is configured for the site.\n * Non-lead form types (e.g. 'job_application') do not fire any tracking events.\n */\n\n// IMPORTANT:\n// Do NOT import NextRequest/NextResponse here.\n// Export a factory so the consuming app can pass its own `NextResponse` (from its own\n// `next/server`) to avoid type identity conflicts when used as a local file dependency.\n\nimport { clientContextHeaders } from './proxy-headers';\n\nconst API_URL = process.env.API_URL || 'http://localhost:3000/api/v1';\nconst API_KEY = process.env.API_KEY || '';\n\ntype JsonResponder = (body: unknown, init?: ResponseInit) => Response;\n\nexport function createFormRouteHandlers(deps?: { NextResponse?: { json: JsonResponder } }) {\n const json: JsonResponder = deps?.NextResponse?.json ?? ((body, init) => Response.json(body, init));\n\n return {\n POST: async (request: Request): Promise<Response> => {\n try {\n const body = await request.json();\n const formType = body?.formType;\n\n if (!formType) {\n return json({ success: false, error: 'Form type is required.' }, { status: 400 });\n }\n\n const response = await fetch(`${API_URL}/public/form_submissions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': API_KEY,\n ...clientContextHeaders(request),\n },\n body: JSON.stringify(body),\n });\n\n const data = await response.json();\n\n if (!response.ok) {\n return json(\n {\n success: false,\n error: data.error || 'Failed to submit form. Please try again.',\n },\n { status: response.status }\n );\n }\n\n const payload: { success: true; message?: string; eventId?: string } = {\n success: true,\n message: data.message || 'Form submitted successfully.',\n };\n\n if (formType === 'lead' && data.data?.event_id) {\n payload.eventId = data.data.event_id;\n }\n\n return json(payload);\n } catch (error) {\n console.error('Form submission error:', error);\n return json(\n {\n success: false,\n error: 'Network error. Please try again later.',\n },\n { status: 500 }\n );\n }\n },\n };\n}\n\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBO,SAAS,qBAAqB,SAA0C;AArB/E;AAsBE,QAAM,KACJ,QAAQ,QAAQ,IAAI,WAAW,OAC/B,mBAAQ,QAAQ,IAAI,iBAAiB,MAArC,mBAAwC,MAAM,KAAK,OAAnD,mBAAuD;AACzD,QAAM,KAAK,QAAQ,QAAQ,IAAI,YAAY;AAE3C,QAAM,eAAe,QAAQ,QAAQ,IAAI,QAAQ,KAAK;AACtD,QAAM,UAAU,OAAO;AAAA,IACrB,aAAa,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM;AACjC,YAAM,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG;AACpC,aAAO,CAAC,GAAG,EAAE,KAAK,GAAG,CAAC;AAAA,IACxB,CAAC;AAAA,EACH;AACA,QAAM,MAAM,QAAQ,MAAM;AAC1B,QAAM,MAAM,QAAQ,MAAM;AAE1B,QAAM,UAAkC,CAAC;AACzC,MAAI,GAAI,SAAQ,kBAAkB,IAAI;AACtC,MAAI,GAAI,SAAQ,kBAAkB,IAAI;AACtC,MAAI,IAAK,SAAQ,YAAY,IAAI;AACjC,MAAI,IAAK,SAAQ,YAAY,IAAI;AACjC,SAAO;AACT;;;ACRA,IAAM,UAAU,QAAQ,IAAI,WAAW;AACvC,IAAM,UAAU,QAAQ,IAAI,WAAW;AAIhC,SAAS,wBAAwB,MAAmD;AAxC3F;AAyCE,QAAM,QAAsB,wCAAM,iBAAN,mBAAoB,SAApB,aAA6B,CAAC,MAAM,SAAS,SAAS,KAAK,MAAM,IAAI;AAEjG,SAAO;AAAA,IACL,MAAM,OAAO,YAAwC;AA5CzD,UAAAA;AA6CM,UAAI;AACF,cAAM,OAAO,MAAM,QAAQ,KAAK;AAChC,cAAM,WAAW,6BAAM;AAEvB,YAAI,CAAC,UAAU;AACb,iBAAO,KAAK,EAAE,SAAS,OAAO,OAAO,yBAAyB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,QAClF;AAEA,cAAM,WAAW,MAAM,MAAM,GAAG,OAAO,4BAA4B;AAAA,UACjE,QAAQ;AAAA,UACR,SAAS;AAAA,YACP,gBAAgB;AAAA,YAChB,aAAa;AAAA,aACV,qBAAqB,OAAO;AAAA,UAEjC,MAAM,KAAK,UAAU,IAAI;AAAA,QAC3B,CAAC;AAED,cAAM,OAAO,MAAM,SAAS,KAAK;AAEjC,YAAI,CAAC,SAAS,IAAI;AAChB,iBAAO;AAAA,YACL;AAAA,cACE,SAAS;AAAA,cACT,OAAO,KAAK,SAAS;AAAA,YACvB;AAAA,YACA,EAAE,QAAQ,SAAS,OAAO;AAAA,UAC5B;AAAA,QACF;AAEA,cAAM,UAAiE;AAAA,UACrE,SAAS;AAAA,UACT,SAAS,KAAK,WAAW;AAAA,QAC3B;AAEA,YAAI,aAAa,YAAUA,MAAA,KAAK,SAAL,gBAAAA,IAAW,WAAU;AAC9C,kBAAQ,UAAU,KAAK,KAAK;AAAA,QAC9B;AAEA,eAAO,KAAK,OAAO;AAAA,MACrB,SAAS,OAAO;AACd,gBAAQ,MAAM,0BAA0B,KAAK;AAC7C,eAAO;AAAA,UACL;AAAA,YACE,SAAS;AAAA,YACT,OAAO;AAAA,UACT;AAAA,UACA,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":["_a"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { P as PhotoAttachment } from './photos-CmBdWiuZ.js';
|
|
2
|
+
|
|
3
|
+
/** Nested under `service_items[].offers` and `packages[].offers` in public API. */
|
|
4
|
+
interface OfferPublic {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
description: string | null;
|
|
8
|
+
value_terms: string | null;
|
|
9
|
+
active?: boolean;
|
|
10
|
+
expires_at?: string | null;
|
|
11
|
+
expired?: boolean;
|
|
12
|
+
photo_attachments?: PhotoAttachment[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Service {
|
|
16
|
+
id: number;
|
|
17
|
+
name: string;
|
|
18
|
+
slug: string;
|
|
19
|
+
description_markdown: string;
|
|
20
|
+
summary?: string;
|
|
21
|
+
pricing_info?: string;
|
|
22
|
+
features_markdown?: string;
|
|
23
|
+
featured: boolean;
|
|
24
|
+
sort_order: number;
|
|
25
|
+
photo_attachments?: PhotoAttachment[];
|
|
26
|
+
service_items?: ServiceItem[];
|
|
27
|
+
created_at: string;
|
|
28
|
+
updated_at: string;
|
|
29
|
+
}
|
|
30
|
+
interface ServiceItem {
|
|
31
|
+
id: number;
|
|
32
|
+
name: string;
|
|
33
|
+
slug: string;
|
|
34
|
+
summary?: string | null;
|
|
35
|
+
description_markdown?: string | null;
|
|
36
|
+
pricing_info?: string | null;
|
|
37
|
+
price_cents?: number | null;
|
|
38
|
+
duration_minutes?: number | null;
|
|
39
|
+
sort_order: number;
|
|
40
|
+
service_id?: number;
|
|
41
|
+
photo_attachments?: PhotoAttachment[];
|
|
42
|
+
offers?: OfferPublic[];
|
|
43
|
+
}
|
|
44
|
+
interface ServiceParams {
|
|
45
|
+
featured?: boolean;
|
|
46
|
+
q?: string;
|
|
47
|
+
page?: number;
|
|
48
|
+
per_page?: number;
|
|
49
|
+
}
|
|
50
|
+
type ServiceResponse = Service[];
|
|
51
|
+
|
|
52
|
+
interface PackageItem {
|
|
53
|
+
quantity: number;
|
|
54
|
+
service_item?: {
|
|
55
|
+
id: number;
|
|
56
|
+
name: string;
|
|
57
|
+
slug: string;
|
|
58
|
+
summary?: string | null;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
interface Package {
|
|
62
|
+
id: number;
|
|
63
|
+
name: string;
|
|
64
|
+
slug: string;
|
|
65
|
+
summary?: string | null;
|
|
66
|
+
description_markdown?: string | null;
|
|
67
|
+
pricing_info?: string | null;
|
|
68
|
+
price_cents?: number | null;
|
|
69
|
+
photo_attachments?: PhotoAttachment[];
|
|
70
|
+
package_items?: PackageItem[];
|
|
71
|
+
offers?: OfferPublic[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type { OfferPublic as O, Package as P, Service as S, PackageItem as a, ServiceItem as b, ServiceParams as c, ServiceResponse as d };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,29 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
export { Theme } from '../themes/index.js';
|
|
2
|
+
export { N as NavItem, S as SiteConfig } from '../config-C_XBZixg.js';
|
|
2
3
|
export { B as BlogPost, a as BlogPostAuthor, b as BlogPostParams, c as BlogPostResponse, d as BlogPostTag } from '../blog-post-vWzW8yFb.js';
|
|
3
|
-
export { C as CompanyInformation, a as CompanyInformationResponse
|
|
4
|
+
export { C as CompanyInformation, a as CompanyInformationResponse } from '../company-information-C1pP-SvU.js';
|
|
5
|
+
export { a as Consumer, c as ConsumerContact, b as ContactSummary, C as ConversationSummary, M as Message } from '../consumer-BWjQawiO.js';
|
|
4
6
|
import { P as PhotoAttachment } from '../photos-CmBdWiuZ.js';
|
|
5
7
|
export { a as Photo } from '../photos-CmBdWiuZ.js';
|
|
6
8
|
export { F as FormDefinition, a as FormFieldDefinition, b as FormFieldItem, c as FormFieldOption, d as FormType } from '../form-C94A_PX_.js';
|
|
9
|
+
export { O as OfferPublic, P as Package, a as PackageItem, S as Service, b as ServiceItem, c as ServiceParams, d as ServiceResponse } from '../package-IU_GpDA0.js';
|
|
7
10
|
export { a as WebsitePhoto, W as WebsitePhotos, b as WebsitePhotosResponse } from '../website-photos-Cl1YqAno.js';
|
|
8
11
|
|
|
9
|
-
interface NavItem {
|
|
10
|
-
label: string;
|
|
11
|
-
href: string;
|
|
12
|
-
subtitle?: string;
|
|
13
|
-
children?: NavItem[];
|
|
14
|
-
}
|
|
15
|
-
interface SiteConfig {
|
|
16
|
-
site: {
|
|
17
|
-
title: string;
|
|
18
|
-
description: string;
|
|
19
|
-
theme: Theme;
|
|
20
|
-
};
|
|
21
|
-
navigation: {
|
|
22
|
-
header: NavItem[];
|
|
23
|
-
footer: NavItem[][];
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
12
|
interface Testimonial {
|
|
28
13
|
id: number;
|
|
29
14
|
reviewer_name: string;
|
|
@@ -203,53 +188,6 @@ interface JobPostingParams {
|
|
|
203
188
|
}
|
|
204
189
|
type JobPostingResponse = JobPosting[];
|
|
205
190
|
|
|
206
|
-
/** Consumer and messaging types (aligned with Rails ConsumerSerializer and conversation endpoints). */
|
|
207
|
-
interface Consumer {
|
|
208
|
-
id: number;
|
|
209
|
-
email: string | null;
|
|
210
|
-
phone: string | null;
|
|
211
|
-
primary_identifier: string | null;
|
|
212
|
-
contacts?: ConsumerContact[];
|
|
213
|
-
}
|
|
214
|
-
interface ConsumerContact {
|
|
215
|
-
id: number;
|
|
216
|
-
display_name: string;
|
|
217
|
-
account?: {
|
|
218
|
-
id: number;
|
|
219
|
-
name: string;
|
|
220
|
-
slug?: string;
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
interface ConversationSummary {
|
|
224
|
-
contact_id: number;
|
|
225
|
-
business?: {
|
|
226
|
-
id: number;
|
|
227
|
-
name: string;
|
|
228
|
-
company_name?: string;
|
|
229
|
-
};
|
|
230
|
-
last_message_at: string | null;
|
|
231
|
-
last_message_preview?: string | null;
|
|
232
|
-
message_count: number;
|
|
233
|
-
}
|
|
234
|
-
interface Message {
|
|
235
|
-
id: number;
|
|
236
|
-
body: string | null;
|
|
237
|
-
/** "outbound" = sent by the business; "inbound" = sent by the contact/member. */
|
|
238
|
-
direction: 'inbound' | 'outbound';
|
|
239
|
-
sender_type: 'contact' | 'agent' | 'human';
|
|
240
|
-
sender_display_name?: string;
|
|
241
|
-
created_at: string;
|
|
242
|
-
}
|
|
243
|
-
interface ContactSummary {
|
|
244
|
-
id: number;
|
|
245
|
-
display_name: string;
|
|
246
|
-
business?: {
|
|
247
|
-
id: number;
|
|
248
|
-
name: string;
|
|
249
|
-
company_name?: string;
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
191
|
interface ContactInfo {
|
|
254
192
|
id?: number;
|
|
255
193
|
about_text_markdown?: string;
|
|
@@ -310,4 +248,4 @@ interface ContactFormResponse {
|
|
|
310
248
|
data: ContactFormSubmission;
|
|
311
249
|
}
|
|
312
250
|
|
|
313
|
-
export { type
|
|
251
|
+
export { type ContactFormData, type ContactFormResponse, type ContactFormSubmission, type ContactInfo, type ContactResponse, type FaqCategory, type FaqCategoryResponse, type FaqParams, type FaqQuestion, type FaqResponse, type JobPosting, type JobPostingParams, type JobPostingResponse, type Location, type LocationParams, type LocationResponse, PhotoAttachment, type SocialPost, type SocialPostParams, type SocialPostResponse, type TeamMember, type TeamMemberParams, type TeamMemberResponse, type Testimonial, type TestimonialParams, type TestimonialResponse };
|
package/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keystone-design-bootstrap",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.87",
|
|
4
4
|
"description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./
|
|
7
|
-
"types": "./
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./
|
|
10
|
-
"./sections": "./
|
|
11
|
-
"./elements": "./
|
|
12
|
-
"./logo": "./
|
|
13
|
-
"./hooks": "./
|
|
14
|
-
"./contexts": "./
|
|
15
|
-
"./tracking": "./
|
|
16
|
-
"./components/DynamicFormFields": "./
|
|
17
|
-
"./lib/server-api": "./
|
|
18
|
-
"./lib/cta-urls": "./
|
|
19
|
-
"./lib/component-registry": "./
|
|
20
|
-
"./next/routes/chat": "./
|
|
21
|
-
"./next/routes/form": "./
|
|
22
|
-
"./next/contexts/form-definitions": "./
|
|
23
|
-
"./next/layouts/root-layout": "./
|
|
24
|
-
"./next/providers/ssr-provider": "./
|
|
25
|
-
"./next/gallery/design-gallery": "./
|
|
26
|
-
"./next/legal/privacy-policy": "./
|
|
27
|
-
"./next/legal/terms-of-service": "./
|
|
9
|
+
".": "./dist/index.js",
|
|
10
|
+
"./sections": "./dist/design_system/sections/index.js",
|
|
11
|
+
"./elements": "./dist/design_system/elements/index.js",
|
|
12
|
+
"./logo": "./dist/design_system/logo/keystone-logo.js",
|
|
13
|
+
"./hooks": "./dist/lib/hooks/index.js",
|
|
14
|
+
"./contexts": "./dist/contexts/index.js",
|
|
15
|
+
"./tracking": "./dist/tracking/index.js",
|
|
16
|
+
"./components/DynamicFormFields": "./dist/design_system/components/DynamicFormFields.js",
|
|
17
|
+
"./lib/server-api": "./dist/lib/server-api.js",
|
|
18
|
+
"./lib/cta-urls": "./dist/lib/cta-urls.js",
|
|
19
|
+
"./lib/component-registry": "./dist/lib/component-registry.js",
|
|
20
|
+
"./next/routes/chat": "./dist/next/routes/chat.js",
|
|
21
|
+
"./next/routes/form": "./dist/next/routes/form.js",
|
|
22
|
+
"./next/contexts/form-definitions": "./dist/next/contexts/form-definitions.js",
|
|
23
|
+
"./next/layouts/root-layout": "./dist/next/layouts/root-layout.js",
|
|
24
|
+
"./next/providers/ssr-provider": "./dist/next/providers/ssr-provider.js",
|
|
25
|
+
"./next/gallery/design-gallery": "./dist/next/gallery/design-gallery.js",
|
|
26
|
+
"./next/legal/privacy-policy": "./dist/next/legal/privacy-policy.js",
|
|
27
|
+
"./next/legal/terms-of-service": "./dist/next/legal/terms-of-service.js",
|
|
28
28
|
"./styles/*": "./src/styles/*",
|
|
29
|
-
"./types": "./
|
|
30
|
-
"./themes": "./
|
|
31
|
-
"./utils/*": "./
|
|
32
|
-
"./portal": "./
|
|
33
|
-
"./lib/consumer-session": "./
|
|
34
|
-
"./next/routes/consumer-auth": "./
|
|
29
|
+
"./types": "./dist/types/index.js",
|
|
30
|
+
"./themes": "./dist/themes/index.js",
|
|
31
|
+
"./utils/*": "./dist/utils/*",
|
|
32
|
+
"./portal": "./dist/design_system/portal/index.js",
|
|
33
|
+
"./lib/consumer-session": "./dist/lib/consumer-session.js",
|
|
34
|
+
"./next/routes/consumer-auth": "./dist/next/routes/consumer-auth.js"
|
|
35
35
|
},
|
|
36
36
|
"repository": {
|
|
37
37
|
"type": "git",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"@fontsource/montserrat": "^5.2.8",
|
|
65
65
|
"@fontsource/playfair-display": "^5.2.8",
|
|
66
66
|
"@fontsource/poppins": "^5.2.7",
|
|
67
|
+
"@rails/actioncable": "^8.1.300",
|
|
67
68
|
"@untitledui/file-icons": "^0.0.9",
|
|
68
69
|
"@untitledui/icons": "^0.0.20",
|
|
69
70
|
"clsx": "^2.1.1",
|
|
@@ -79,6 +80,7 @@
|
|
|
79
80
|
},
|
|
80
81
|
"devDependencies": {
|
|
81
82
|
"@types/node": "^20",
|
|
83
|
+
"@types/rails__actioncable": "^8.0.3",
|
|
82
84
|
"@types/react": "^19",
|
|
83
85
|
"@types/react-dom": "^19",
|
|
84
86
|
"eslint": "^9",
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import { createConsumer } from '@rails/actioncable';
|
|
5
|
+
|
|
6
|
+
export interface RealtimeSubscriptionData {
|
|
7
|
+
token: string;
|
|
8
|
+
contact_id: number;
|
|
9
|
+
cable_url: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface UseRealtimeReplyOrchestratorOptions {
|
|
13
|
+
debugLabel: string;
|
|
14
|
+
fetchRealtimeData: () => Promise<RealtimeSubscriptionData | null>;
|
|
15
|
+
loadLatestHasReply: () => Promise<boolean>;
|
|
16
|
+
onReplyResolved: () => void;
|
|
17
|
+
onAgentThinking?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useRealtimeReplyOrchestrator({
|
|
21
|
+
debugLabel,
|
|
22
|
+
fetchRealtimeData,
|
|
23
|
+
loadLatestHasReply,
|
|
24
|
+
onReplyResolved,
|
|
25
|
+
onAgentThinking,
|
|
26
|
+
}: UseRealtimeReplyOrchestratorOptions) {
|
|
27
|
+
const cableRef = useRef<ReturnType<typeof createConsumer> | null>(null);
|
|
28
|
+
const subscriptionRef = useRef<{ unsubscribe: () => void } | null>(null);
|
|
29
|
+
const subscribedContactRef = useRef<number | null>(null);
|
|
30
|
+
|
|
31
|
+
const clearRealtime = useCallback(() => {
|
|
32
|
+
if (subscriptionRef.current) {
|
|
33
|
+
subscriptionRef.current.unsubscribe();
|
|
34
|
+
subscriptionRef.current = null;
|
|
35
|
+
}
|
|
36
|
+
if (cableRef.current) {
|
|
37
|
+
cableRef.current.disconnect();
|
|
38
|
+
cableRef.current = null;
|
|
39
|
+
}
|
|
40
|
+
subscribedContactRef.current = null;
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const resolveReply = useCallback(() => {
|
|
44
|
+
onReplyResolved();
|
|
45
|
+
}, [onReplyResolved]);
|
|
46
|
+
|
|
47
|
+
const subscribeRealtime = useCallback((realtime: RealtimeSubscriptionData) => {
|
|
48
|
+
const token = realtime.token;
|
|
49
|
+
const contactIdForStream = realtime.contact_id;
|
|
50
|
+
const cableUrl = realtime.cable_url;
|
|
51
|
+
if (!token || !contactIdForStream || !cableUrl) return;
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
subscribedContactRef.current === contactIdForStream &&
|
|
55
|
+
subscriptionRef.current &&
|
|
56
|
+
cableRef.current
|
|
57
|
+
) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
clearRealtime();
|
|
62
|
+
|
|
63
|
+
const cable = createConsumer(`${cableUrl}?token=${encodeURIComponent(token)}`);
|
|
64
|
+
cableRef.current = cable;
|
|
65
|
+
subscribedContactRef.current = contactIdForStream;
|
|
66
|
+
subscriptionRef.current = cable.subscriptions.create(
|
|
67
|
+
{ channel: 'ContactCommunicationsChannel', contact_id: String(contactIdForStream) },
|
|
68
|
+
{
|
|
69
|
+
connected: () => {
|
|
70
|
+
console.info(`[${debugLabel}] realtime connected contact_id=${contactIdForStream}`);
|
|
71
|
+
},
|
|
72
|
+
disconnected: () => {
|
|
73
|
+
console.warn(`[${debugLabel}] realtime disconnected contact_id=${contactIdForStream}`);
|
|
74
|
+
},
|
|
75
|
+
rejected: () => {
|
|
76
|
+
console.warn(`[${debugLabel}] realtime rejected contact_id=${contactIdForStream}`);
|
|
77
|
+
},
|
|
78
|
+
received: async (data: { type?: string }) => {
|
|
79
|
+
console.info(`[${debugLabel}] realtime received type=${data?.type ?? 'unknown'} contact_id=${contactIdForStream}`);
|
|
80
|
+
if (data?.type === 'agent_thinking') {
|
|
81
|
+
onAgentThinking?.();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (data?.type !== 'new_communication') return;
|
|
85
|
+
try {
|
|
86
|
+
const hasReply = await loadLatestHasReply();
|
|
87
|
+
if (hasReply) {
|
|
88
|
+
resolveReply();
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error(`[${debugLabel}] realtime receive handling error`, error);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
}, [clearRealtime, debugLabel, loadLatestHasReply, onAgentThinking, resolveReply]);
|
|
97
|
+
|
|
98
|
+
const ensureRealtimeSubscription = useCallback(async () => {
|
|
99
|
+
try {
|
|
100
|
+
const realtime = await fetchRealtimeData();
|
|
101
|
+
if (!realtime) return;
|
|
102
|
+
subscribeRealtime(realtime);
|
|
103
|
+
} catch {
|
|
104
|
+
// Realtime is best-effort; UI remains in waiting state until connection succeeds.
|
|
105
|
+
}
|
|
106
|
+
}, [fetchRealtimeData, subscribeRealtime]);
|
|
107
|
+
|
|
108
|
+
const beginReplyWait = useCallback((options?: { realtimeData?: RealtimeSubscriptionData | null }) => {
|
|
109
|
+
if (options?.realtimeData) {
|
|
110
|
+
subscribeRealtime(options.realtimeData);
|
|
111
|
+
} else {
|
|
112
|
+
void ensureRealtimeSubscription();
|
|
113
|
+
}
|
|
114
|
+
}, [ensureRealtimeSubscription, subscribeRealtime]);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
return () => {
|
|
118
|
+
clearRealtime();
|
|
119
|
+
};
|
|
120
|
+
}, [clearRealtime]);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
ensureRealtimeSubscription,
|
|
124
|
+
subscribeRealtime,
|
|
125
|
+
beginReplyWait,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -5,6 +5,7 @@ import { X, MessageChatSquare } from '@untitledui/icons';
|
|
|
5
5
|
import { Avatar } from '../elements/avatar/avatar';
|
|
6
6
|
import { cx } from '../../utils/cx';
|
|
7
7
|
import { captureEvent } from '../../tracking/captureEvent';
|
|
8
|
+
import { useRealtimeReplyOrchestrator, type RealtimeSubscriptionData } from '../chat/useRealtimeReplyOrchestrator';
|
|
8
9
|
|
|
9
10
|
interface Message {
|
|
10
11
|
id: string;
|
|
@@ -126,47 +127,55 @@ export function ChatWidget({
|
|
|
126
127
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
127
128
|
}, [messages]);
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
clearInterval(pollIntervalRef.current);
|
|
137
|
-
}
|
|
138
|
-
};
|
|
130
|
+
const hasAgentReplyWithBody = useCallback((list: Message[]) => {
|
|
131
|
+
const latest = list[list.length - 1];
|
|
132
|
+
return (
|
|
133
|
+
latest?.sender_type === 'agent' &&
|
|
134
|
+
latest?.body != null &&
|
|
135
|
+
String(latest.body).trim() !== ''
|
|
136
|
+
);
|
|
139
137
|
}, []);
|
|
140
138
|
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
139
|
+
const fetchRealtimeData = useCallback(async (): Promise<RealtimeSubscriptionData | null> => {
|
|
140
|
+
if (!contactId && !sessionId) return null;
|
|
141
|
+
const query = contactId
|
|
142
|
+
? `contact_id=${encodeURIComponent(contactId)}`
|
|
143
|
+
: `identifier=${encodeURIComponent(sessionId)}`;
|
|
144
|
+
const response = await fetch(`/api/chat/?action=realtime_token&${query}`);
|
|
145
|
+
if (!response.ok) return null;
|
|
146
|
+
const result = await response.json();
|
|
147
|
+
const token = result?.data?.token as string | undefined;
|
|
148
|
+
const contactIdForStream = result?.data?.contact_id as number | undefined;
|
|
149
|
+
const cableUrl = result?.data?.cable_url as string | undefined;
|
|
150
|
+
if (!token || !contactIdForStream || !cableUrl) return null;
|
|
151
|
+
return { token, contact_id: contactIdForStream, cable_url: cableUrl };
|
|
152
|
+
}, [contactId, sessionId]);
|
|
149
153
|
|
|
150
|
-
|
|
151
|
-
|
|
154
|
+
const {
|
|
155
|
+
beginReplyWait,
|
|
156
|
+
ensureRealtimeSubscription,
|
|
157
|
+
} = useRealtimeReplyOrchestrator({
|
|
158
|
+
debugLabel: 'ChatWidget',
|
|
159
|
+
fetchRealtimeData,
|
|
160
|
+
loadLatestHasReply: async () => hasAgentReplyWithBody(await loadMessages()),
|
|
161
|
+
onReplyResolved: () => {
|
|
162
|
+
setWaitingForReply(false);
|
|
163
|
+
setIsLoading(false);
|
|
164
|
+
},
|
|
165
|
+
onAgentThinking: () => {
|
|
166
|
+
setWaitingForReply(true);
|
|
167
|
+
},
|
|
168
|
+
});
|
|
152
169
|
|
|
153
|
-
|
|
154
|
-
const hasAgentReplyWithBody =
|
|
155
|
-
latest?.sender_type === 'agent' &&
|
|
156
|
-
latest?.body != null &&
|
|
157
|
-
String(latest.body).trim() !== '';
|
|
170
|
+
const hasPersistedMessages = messages.some((message) => !String(message.id).startsWith('temp_'));
|
|
158
171
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
console.error('[ChatWidget] Error polling for messages:', error);
|
|
167
|
-
}
|
|
168
|
-
}, 1000);
|
|
169
|
-
};
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!isOpen || (!contactId && !sessionId)) return;
|
|
174
|
+
// Anonymous sessions do not have a contact row until the first message is sent.
|
|
175
|
+
// Ignore optimistic temp rows to avoid pre-contact token probes (404 noise).
|
|
176
|
+
if (!contactId && !hasPersistedMessages) return;
|
|
177
|
+
ensureRealtimeSubscription();
|
|
178
|
+
}, [contactId, ensureRealtimeSubscription, hasPersistedMessages, isOpen, sessionId]);
|
|
170
179
|
|
|
171
180
|
const sendMessage = async () => {
|
|
172
181
|
if (!inputValue.trim() || (!contactId && !sessionId)) return;
|
|
@@ -201,24 +210,36 @@ export function ChatWidget({
|
|
|
201
210
|
captureEvent('chat_message_sent', { is_authenticated: Boolean(contactId) });
|
|
202
211
|
|
|
203
212
|
if (result.data?.job_id) {
|
|
204
|
-
|
|
213
|
+
setWaitingForReply(true);
|
|
214
|
+
const realtimeFromSend = result.data?.realtime_token && result.data?.contact_id && result.data?.cable_url
|
|
215
|
+
? {
|
|
216
|
+
token: result.data.realtime_token,
|
|
217
|
+
contact_id: result.data.contact_id,
|
|
218
|
+
cable_url: result.data.cable_url,
|
|
219
|
+
}
|
|
220
|
+
: null;
|
|
221
|
+
beginReplyWait({ realtimeData: realtimeFromSend });
|
|
205
222
|
} else if (result.data?.status === 'agent_unavailable' || result.data?.status === 'no_auto_reply') {
|
|
206
223
|
setIsLoading(false);
|
|
224
|
+
setWaitingForReply(false);
|
|
207
225
|
} else {
|
|
208
226
|
await loadMessages();
|
|
209
227
|
setIsLoading(false);
|
|
228
|
+
setWaitingForReply(false);
|
|
210
229
|
}
|
|
211
230
|
} else {
|
|
212
231
|
setMessages(prev => prev.filter(m => m.id !== tempMessage.id));
|
|
213
232
|
captureEvent('chat_message_failed', { error: 'send_failed' });
|
|
214
233
|
console.error('Failed to send message');
|
|
215
234
|
setIsLoading(false);
|
|
235
|
+
setWaitingForReply(false);
|
|
216
236
|
}
|
|
217
237
|
} catch (error) {
|
|
218
238
|
setMessages(prev => prev.filter(m => m.id !== tempMessage.id));
|
|
219
239
|
captureEvent('chat_message_failed', { error: 'network_error' });
|
|
220
240
|
console.error('Failed to send message:', error);
|
|
221
241
|
setIsLoading(false);
|
|
242
|
+
setWaitingForReply(false);
|
|
222
243
|
}
|
|
223
244
|
};
|
|
224
245
|
|