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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState, useTransition, useRef, useEffect } from 'react';
|
|
3
|
+
import React, { useState, useTransition, useRef, useEffect, useCallback } from 'react';
|
|
4
4
|
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { useRealtimeReplyOrchestrator, type RealtimeSubscriptionData } from '../chat/useRealtimeReplyOrchestrator';
|
|
5
6
|
|
|
6
7
|
export function MessageComposer({ contactId }: { contactId: number }) {
|
|
7
8
|
const [body, setBody] = useState('');
|
|
@@ -10,6 +11,42 @@ export function MessageComposer({ contactId }: { contactId: number }) {
|
|
|
10
11
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
11
12
|
const router = useRouter();
|
|
12
13
|
|
|
14
|
+
const hasAgentReplyWithBody = useCallback(async () => {
|
|
15
|
+
const response = await fetch(`/api/chat/?contact_id=${encodeURIComponent(contactId)}`);
|
|
16
|
+
if (!response.ok) return false;
|
|
17
|
+
const result = await response.json();
|
|
18
|
+
const messages = (result?.data || []) as Array<{ sender_type?: string; body?: string | null }>;
|
|
19
|
+
const latest = messages[messages.length - 1];
|
|
20
|
+
return (
|
|
21
|
+
latest?.sender_type === 'agent' &&
|
|
22
|
+
latest?.body != null &&
|
|
23
|
+
String(latest.body).trim() !== ''
|
|
24
|
+
);
|
|
25
|
+
}, [contactId]);
|
|
26
|
+
|
|
27
|
+
const fetchRealtimeData = useCallback(async (): Promise<RealtimeSubscriptionData | null> => {
|
|
28
|
+
const response = await fetch(`/api/chat/?action=realtime_token&contact_id=${encodeURIComponent(contactId)}`);
|
|
29
|
+
if (!response.ok) return null;
|
|
30
|
+
const result = await response.json();
|
|
31
|
+
const token = result?.data?.token as string | undefined;
|
|
32
|
+
const streamContactId = result?.data?.contact_id as number | undefined;
|
|
33
|
+
const cableUrl = result?.data?.cable_url as string | undefined;
|
|
34
|
+
if (!token || !streamContactId || !cableUrl) return null;
|
|
35
|
+
return { token, contact_id: streamContactId, cable_url: cableUrl };
|
|
36
|
+
}, [contactId]);
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
beginReplyWait,
|
|
40
|
+
ensureRealtimeSubscription,
|
|
41
|
+
} = useRealtimeReplyOrchestrator({
|
|
42
|
+
debugLabel: 'MessageComposer',
|
|
43
|
+
fetchRealtimeData,
|
|
44
|
+
loadLatestHasReply: hasAgentReplyWithBody,
|
|
45
|
+
onReplyResolved: () => {
|
|
46
|
+
router.refresh();
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
13
50
|
// Auto-resize textarea
|
|
14
51
|
useEffect(() => {
|
|
15
52
|
const el = textareaRef.current;
|
|
@@ -18,6 +55,10 @@ export function MessageComposer({ contactId }: { contactId: number }) {
|
|
|
18
55
|
el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
|
|
19
56
|
}, [body]);
|
|
20
57
|
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
void ensureRealtimeSubscription();
|
|
60
|
+
}, [ensureRealtimeSubscription]);
|
|
61
|
+
|
|
21
62
|
function submit() {
|
|
22
63
|
if (!body.trim() || isPending) return;
|
|
23
64
|
setError(null);
|
|
@@ -34,6 +75,17 @@ export function MessageComposer({ contactId }: { contactId: number }) {
|
|
|
34
75
|
} else {
|
|
35
76
|
setBody('');
|
|
36
77
|
router.refresh();
|
|
78
|
+
const hasBackgroundReplyJob = Boolean(result?.data?.job_id);
|
|
79
|
+
if (hasBackgroundReplyJob) {
|
|
80
|
+
const realtimeFromSend = result?.data?.realtime_token && result?.data?.contact_id && result?.data?.cable_url
|
|
81
|
+
? {
|
|
82
|
+
token: result.data.realtime_token,
|
|
83
|
+
contact_id: result.data.contact_id,
|
|
84
|
+
cable_url: result.data.cable_url,
|
|
85
|
+
}
|
|
86
|
+
: null;
|
|
87
|
+
beginReplyWait({ realtimeData: realtimeFromSend });
|
|
88
|
+
}
|
|
37
89
|
}
|
|
38
90
|
} catch {
|
|
39
91
|
setError('Failed to send message.');
|
|
@@ -72,12 +72,13 @@ export interface PortalPageProps {
|
|
|
72
72
|
/** HEAD-request the booking URL and inspect framing headers to decide whether to embed in an iframe. */
|
|
73
73
|
async function checkIframeAllowed(url: string): Promise<boolean> {
|
|
74
74
|
try {
|
|
75
|
-
const
|
|
75
|
+
const fetchOptions: RequestInit & { next?: { revalidate?: number } } = {
|
|
76
76
|
method: 'HEAD',
|
|
77
77
|
redirect: 'follow',
|
|
78
78
|
// Cache for 1 hour — booking platforms rarely change their embedding policy.
|
|
79
79
|
next: { revalidate: 3600 },
|
|
80
|
-
}
|
|
80
|
+
};
|
|
81
|
+
const res = await fetch(url, fetchOptions);
|
|
81
82
|
|
|
82
83
|
const xfo = res.headers.get('x-frame-options')?.toUpperCase();
|
|
83
84
|
if (xfo === 'DENY' || xfo === 'SAMEORIGIN') return false;
|
package/src/lib/server-api.ts
CHANGED
|
@@ -165,7 +165,8 @@ export async function getJobPosting(slug: string) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
export async function getSocialPosts() {
|
|
168
|
-
|
|
168
|
+
// Social payloads can exceed Next's data-cache item size; avoid noisy build warnings.
|
|
169
|
+
return serverFetch('/public/social_posts', { cache: 'no-store' });
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
/** Packages (bundles of service items). */
|
package/src/next/routes/chat.ts
CHANGED
|
@@ -25,9 +25,22 @@ import { clientContextHeaders } from './proxy-headers';
|
|
|
25
25
|
|
|
26
26
|
const API_URL = process.env.API_URL || 'http://localhost:3000/api/v1';
|
|
27
27
|
const API_KEY = process.env.API_KEY || '';
|
|
28
|
+
const CONSUMER_TOKEN_COOKIE = 'ks_consumer_token';
|
|
28
29
|
|
|
29
30
|
type JsonResponder = (body: unknown, init?: ResponseInit) => Response;
|
|
30
31
|
|
|
32
|
+
function cookieValue(cookieHeader: string | null, key: string): string | null {
|
|
33
|
+
if (!cookieHeader) return null;
|
|
34
|
+
const token = cookieHeader
|
|
35
|
+
.split(';')
|
|
36
|
+
.map((part) => part.trim())
|
|
37
|
+
.find((part) => part.startsWith(`${key}=`))
|
|
38
|
+
?.split('=')
|
|
39
|
+
.slice(1)
|
|
40
|
+
.join('=');
|
|
41
|
+
return token ? decodeURIComponent(token) : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
export function createChatRouteHandlers(deps?: { NextResponse?: { json: JsonResponder } }) {
|
|
32
45
|
const json: JsonResponder = deps?.NextResponse?.json ?? ((body, init) => Response.json(body, init));
|
|
33
46
|
|
|
@@ -65,7 +78,9 @@ export function createChatRouteHandlers(deps?: { NextResponse?: { json: JsonResp
|
|
|
65
78
|
{ status: response.status }
|
|
66
79
|
);
|
|
67
80
|
}
|
|
68
|
-
|
|
81
|
+
const responseData = data.data || {};
|
|
82
|
+
const cableUrl = API_URL.replace(/\/api\/v1\/?$/, '').replace(/^http/, 'ws') + '/cable';
|
|
83
|
+
return json({ success: true, data: { ...responseData, cable_url: cableUrl } });
|
|
69
84
|
} catch (error) {
|
|
70
85
|
console.error('Chat message error:', error);
|
|
71
86
|
return json({ success: false, error: 'Network error. Please try again later.' }, { status: 500 });
|
|
@@ -76,9 +91,50 @@ export function createChatRouteHandlers(deps?: { NextResponse?: { json: JsonResp
|
|
|
76
91
|
GET: async (request: Request): Promise<Response> => {
|
|
77
92
|
try {
|
|
78
93
|
const { searchParams } = new URL(request.url);
|
|
94
|
+
const action = searchParams.get('action');
|
|
79
95
|
const identifier = searchParams.get('identifier');
|
|
80
96
|
const contactId = searchParams.get('contact_id');
|
|
81
97
|
|
|
98
|
+
if (action === 'realtime_token') {
|
|
99
|
+
if (!identifier && !contactId) {
|
|
100
|
+
return json({ success: false, error: 'identifier or contact_id is required for realtime token.' }, { status: 400 });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const response = contactId
|
|
104
|
+
? await (async () => {
|
|
105
|
+
const consumerToken = cookieValue(request.headers.get('cookie'), CONSUMER_TOKEN_COOKIE);
|
|
106
|
+
if (!consumerToken) {
|
|
107
|
+
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
|
108
|
+
status: 401,
|
|
109
|
+
headers: { 'Content-Type': 'application/json' },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return fetch(
|
|
113
|
+
`${API_URL}/consumer/me/contacts/${encodeURIComponent(contactId)}/realtime_token`,
|
|
114
|
+
{
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: `Bearer ${consumerToken}`,
|
|
117
|
+
...(API_KEY ? { 'X-API-Key': API_KEY } : {}),
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
})()
|
|
122
|
+
: await fetch(
|
|
123
|
+
`${API_URL}/public/messages/realtime_token?identifier=${encodeURIComponent(identifier!)}`,
|
|
124
|
+
{ headers: { 'X-API-Key': API_KEY } }
|
|
125
|
+
);
|
|
126
|
+
const data = await response.json();
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
return json(
|
|
129
|
+
{ success: false, error: data.error || 'Failed to create realtime token.' },
|
|
130
|
+
{ status: response.status }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const cableUrl = API_URL.replace(/\/api\/v1\/?$/, '').replace(/^http/, 'ws') + '/cable';
|
|
135
|
+
return json({ success: true, data: { ...(data.data || {}), cable_url: cableUrl } });
|
|
136
|
+
}
|
|
137
|
+
|
|
82
138
|
if (!identifier && !contactId) {
|
|
83
139
|
return json({ success: false, error: 'identifier or contact_id is required.' }, { status: 400 });
|
|
84
140
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare module '@rails/actioncable' {
|
|
2
|
+
export function createConsumer(url?: string): {
|
|
3
|
+
subscriptions: {
|
|
4
|
+
create(
|
|
5
|
+
channel: Record<string, string>,
|
|
6
|
+
callbacks: {
|
|
7
|
+
connected?: () => void;
|
|
8
|
+
disconnected?: () => void;
|
|
9
|
+
rejected?: () => void;
|
|
10
|
+
received?: (data: { type?: string }) => void | Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
): { unsubscribe: () => void };
|
|
13
|
+
};
|
|
14
|
+
disconnect: () => void;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { P as PhotoAttachment, a as Photo } 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 CompanyInformation {
|
|
53
|
-
id: number;
|
|
54
|
-
company_name: string;
|
|
55
|
-
tagline: string;
|
|
56
|
-
mission_statement_markdown?: string;
|
|
57
|
-
about_text_markdown?: string;
|
|
58
|
-
description_markdown?: string;
|
|
59
|
-
values_markdown?: string;
|
|
60
|
-
stats_markdown?: string;
|
|
61
|
-
founded_year?: number;
|
|
62
|
-
logo_photo?: Photo;
|
|
63
|
-
favicon_url?: string;
|
|
64
|
-
facebook_url?: string;
|
|
65
|
-
instagram_url?: string;
|
|
66
|
-
tiktok_url?: string;
|
|
67
|
-
linkedin_url?: string;
|
|
68
|
-
twitter_url?: string;
|
|
69
|
-
youtube_url?: string;
|
|
70
|
-
pinterest_url?: string;
|
|
71
|
-
google_my_business_url?: string;
|
|
72
|
-
yelp_url?: string;
|
|
73
|
-
tripadvisor_url?: string;
|
|
74
|
-
google_reviews_url?: string;
|
|
75
|
-
primary_phone?: string;
|
|
76
|
-
primary_email?: string;
|
|
77
|
-
primary_address_line_1?: string;
|
|
78
|
-
primary_address_line_2?: string;
|
|
79
|
-
primary_city?: string;
|
|
80
|
-
primary_state?: string;
|
|
81
|
-
primary_zip_code?: string;
|
|
82
|
-
primary_country?: string;
|
|
83
|
-
support_email?: string;
|
|
84
|
-
sales_email?: string;
|
|
85
|
-
business_hours?: string;
|
|
86
|
-
external_management_url?: string;
|
|
87
|
-
/** Member portal URL for this account. When set, used as the primary CTA instead of external_management_url. */
|
|
88
|
-
portal_url?: string | null;
|
|
89
|
-
terms_of_service_markdown?: string;
|
|
90
|
-
privacy_policy_markdown?: string;
|
|
91
|
-
created_at: string;
|
|
92
|
-
updated_at: string;
|
|
93
|
-
photo_attachments?: PhotoAttachment[];
|
|
94
|
-
account_status?: string;
|
|
95
|
-
chat_enabled?: boolean;
|
|
96
|
-
}
|
|
97
|
-
type CompanyInformationResponse = CompanyInformation | null;
|
|
98
|
-
|
|
99
|
-
interface PackageItem {
|
|
100
|
-
quantity: number;
|
|
101
|
-
service_item?: {
|
|
102
|
-
id: number;
|
|
103
|
-
name: string;
|
|
104
|
-
slug: string;
|
|
105
|
-
summary?: string | null;
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
interface Package {
|
|
109
|
-
id: number;
|
|
110
|
-
name: string;
|
|
111
|
-
slug: string;
|
|
112
|
-
summary?: string | null;
|
|
113
|
-
description_markdown?: string | null;
|
|
114
|
-
pricing_info?: string | null;
|
|
115
|
-
price_cents?: number | null;
|
|
116
|
-
photo_attachments?: PhotoAttachment[];
|
|
117
|
-
package_items?: PackageItem[];
|
|
118
|
-
offers?: OfferPublic[];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export type { CompanyInformation as C, OfferPublic as O, Package as P, Service as S, CompanyInformationResponse as a, PackageItem as b, ServiceItem as c, ServiceParams as d, ServiceResponse as e };
|