keystone-design-bootstrap 1.0.55 → 1.0.57
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/design_system/elements/index.js +8 -3
- package/dist/design_system/elements/index.js.map +1 -1
- package/dist/design_system/sections/index.js +203 -106
- package/dist/design_system/sections/index.js.map +1 -1
- package/dist/index.js +303 -247
- package/dist/index.js.map +1 -1
- package/dist/lib/hooks/index.js +72 -0
- package/dist/lib/hooks/index.js.map +1 -1
- package/dist/lib/server-api.js.map +1 -1
- package/dist/utils/phone-helpers.js +26 -0
- package/dist/utils/phone-helpers.js.map +1 -0
- package/package.json +5 -2
- package/src/design_system/components/ChatWidget.tsx +51 -34
- package/src/design_system/components/DynamicFormFields.tsx +1 -24
- package/src/design_system/elements/modal/modal.tsx +54 -35
- package/src/design_system/portal/LoginForm.tsx +358 -0
- package/src/design_system/portal/LoginModalController.tsx +63 -0
- package/src/design_system/portal/LogoutButton.tsx +22 -0
- package/src/design_system/portal/MessageComposer.tsx +92 -0
- package/src/design_system/portal/PortalPage.tsx +754 -0
- package/src/design_system/portal/RowThumbnail.tsx +76 -0
- package/src/design_system/portal/index.ts +5 -0
- package/src/design_system/sections/index.tsx +1 -1
- package/src/design_system/sections/service-menu-section.tsx +7 -108
- package/src/lib/actions.ts +51 -115
- package/src/lib/consumer-session.ts +74 -0
- package/src/lib/hooks/index.ts +2 -0
- package/src/lib/hooks/use-image-cycle.ts +105 -0
- package/src/lib/server-api.ts +7 -6
- package/src/next/routes/chat.ts +30 -58
- package/src/next/routes/consumer-auth.ts +180 -0
- package/src/types/api/consumer.ts +39 -0
- package/src/types/api/offer.ts +1 -1
- package/src/types/api/package.ts +20 -0
- package/src/types/api/service.ts +6 -24
- package/src/types/index.ts +2 -0
- package/src/utils/phone-helpers.ts +27 -0
- package/dist/blog-post-DGjaJ3wf.d.ts +0 -50
- package/dist/contexts/index.d.ts +0 -13
- package/dist/design_system/elements/index.d.ts +0 -372
- package/dist/design_system/logo/keystone-logo.d.ts +0 -6
- package/dist/design_system/sections/index.d.ts +0 -237
- package/dist/form-CpsCONG5.d.ts +0 -151
- package/dist/index.d.ts +0 -76
- package/dist/lib/component-registry.d.ts +0 -13
- package/dist/lib/hooks/index.d.ts +0 -64
- package/dist/lib/server-api.d.ts +0 -43
- package/dist/themes/index.d.ts +0 -16
- package/dist/types/index.d.ts +0 -264
- package/dist/utils/cx.d.ts +0 -15
- package/dist/utils/gradient-placeholder.d.ts +0 -8
- package/dist/utils/is-react-component.d.ts +0 -21
- package/dist/utils/markdown-toc.d.ts +0 -14
- package/dist/utils/photo-helpers.d.ts +0 -37
- package/dist/website-photos-Bm-CBK9g.d.ts +0 -47
package/src/next/routes/chat.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* // app/api/chat/route.ts
|
|
6
6
|
* export { GET, POST } from 'keystone-design-bootstrap/next/routes/chat';
|
|
7
7
|
*
|
|
8
|
+
* Supports both anonymous (session-based) and authenticated (contact-driven) flows.
|
|
9
|
+
* Both flows hit the same backend endpoint (`/public/messages`); the discriminator
|
|
10
|
+
* is whether `identifier` (session string) or `contact_id` (integer) is supplied.
|
|
11
|
+
*
|
|
8
12
|
* Env (server-side only):
|
|
9
13
|
* - API_URL (default: http://localhost:3000/api/v1)
|
|
10
14
|
* - API_KEY
|
|
@@ -26,105 +30,73 @@ export function createChatRouteHandlers(deps?: { NextResponse?: { json: JsonResp
|
|
|
26
30
|
const json: JsonResponder = deps?.NextResponse?.json ?? ((body, init) => Response.json(body, init));
|
|
27
31
|
|
|
28
32
|
return {
|
|
33
|
+
// POST /api/chat — send a message (session-based or contact-driven)
|
|
29
34
|
POST: async (request: Request): Promise<Response> => {
|
|
30
35
|
try {
|
|
31
36
|
const body = await request.json();
|
|
32
|
-
const { identifier, body: messageBody, display_name, page_url } = body;
|
|
37
|
+
const { identifier, contact_id, body: messageBody, display_name, page_url } = body;
|
|
33
38
|
|
|
34
|
-
if (!
|
|
39
|
+
if (!messageBody) {
|
|
40
|
+
return json({ success: false, error: 'Message body is required.' }, { status: 400 });
|
|
41
|
+
}
|
|
42
|
+
if (!identifier && !contact_id) {
|
|
35
43
|
return json(
|
|
36
|
-
{ success: false, error: '
|
|
44
|
+
{ success: false, error: 'identifier or contact_id is required.' },
|
|
37
45
|
{ status: 400 }
|
|
38
46
|
);
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
const response = await fetch(`${API_URL}/public/messages`, {
|
|
42
50
|
method: 'POST',
|
|
43
|
-
headers: {
|
|
44
|
-
|
|
45
|
-
'X-API-Key': API_KEY,
|
|
46
|
-
},
|
|
47
|
-
body: JSON.stringify({
|
|
48
|
-
identifier,
|
|
49
|
-
body: messageBody,
|
|
50
|
-
display_name,
|
|
51
|
-
page_url,
|
|
52
|
-
}),
|
|
51
|
+
headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
|
|
52
|
+
body: JSON.stringify({ identifier, contact_id, body: messageBody, display_name, page_url }),
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
const data = await response.json();
|
|
56
|
-
|
|
57
56
|
if (!response.ok) {
|
|
58
57
|
return json(
|
|
59
|
-
{
|
|
60
|
-
success: false,
|
|
61
|
-
error: data.error || 'Failed to send message. Please try again.',
|
|
62
|
-
},
|
|
58
|
+
{ success: false, error: data.error || 'Failed to send message. Please try again.' },
|
|
63
59
|
{ status: response.status }
|
|
64
60
|
);
|
|
65
61
|
}
|
|
66
|
-
|
|
67
|
-
return json({
|
|
68
|
-
success: true,
|
|
69
|
-
data: data.data,
|
|
70
|
-
});
|
|
62
|
+
return json({ success: true, data: data.data });
|
|
71
63
|
} catch (error) {
|
|
72
64
|
console.error('Chat message error:', error);
|
|
73
|
-
return json(
|
|
74
|
-
{
|
|
75
|
-
success: false,
|
|
76
|
-
error: 'Network error. Please try again later.',
|
|
77
|
-
},
|
|
78
|
-
{ status: 500 }
|
|
79
|
-
);
|
|
65
|
+
return json({ success: false, error: 'Network error. Please try again later.' }, { status: 500 });
|
|
80
66
|
}
|
|
81
67
|
},
|
|
82
68
|
|
|
69
|
+
// GET /api/chat?identifier=... or ?contact_id=... — load message history
|
|
83
70
|
GET: async (request: Request): Promise<Response> => {
|
|
84
71
|
try {
|
|
85
72
|
const { searchParams } = new URL(request.url);
|
|
86
73
|
const identifier = searchParams.get('identifier');
|
|
74
|
+
const contactId = searchParams.get('contact_id');
|
|
87
75
|
|
|
88
|
-
if (!identifier) {
|
|
89
|
-
return json({ success: false, error: '
|
|
76
|
+
if (!identifier && !contactId) {
|
|
77
|
+
return json({ success: false, error: 'identifier or contact_id is required.' }, { status: 400 });
|
|
90
78
|
}
|
|
91
79
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
{
|
|
95
|
-
headers: {
|
|
96
|
-
'X-API-Key': API_KEY,
|
|
97
|
-
},
|
|
98
|
-
}
|
|
99
|
-
);
|
|
80
|
+
const query = contactId
|
|
81
|
+
? `contact_id=${encodeURIComponent(contactId)}`
|
|
82
|
+
: `identifier=${encodeURIComponent(identifier!)}`;
|
|
100
83
|
|
|
101
|
-
const
|
|
84
|
+
const response = await fetch(`${API_URL}/public/messages?${query}`, {
|
|
85
|
+
headers: { 'X-API-Key': API_KEY },
|
|
86
|
+
});
|
|
102
87
|
|
|
88
|
+
const data = await response.json();
|
|
103
89
|
if (!response.ok) {
|
|
104
90
|
return json(
|
|
105
|
-
{
|
|
106
|
-
success: false,
|
|
107
|
-
error: data.error || 'Failed to load messages.',
|
|
108
|
-
},
|
|
91
|
+
{ success: false, error: data.error || 'Failed to load messages.' },
|
|
109
92
|
{ status: response.status }
|
|
110
93
|
);
|
|
111
94
|
}
|
|
112
|
-
|
|
113
|
-
return json({
|
|
114
|
-
success: true,
|
|
115
|
-
data: data.data || [],
|
|
116
|
-
});
|
|
95
|
+
return json({ success: true, data: data.data || [] });
|
|
117
96
|
} catch (error) {
|
|
118
97
|
console.error('Chat history error:', error);
|
|
119
|
-
return json(
|
|
120
|
-
{
|
|
121
|
-
success: false,
|
|
122
|
-
error: 'Network error. Please try again later.',
|
|
123
|
-
},
|
|
124
|
-
{ status: 500 }
|
|
125
|
-
);
|
|
98
|
+
return json({ success: false, error: 'Network error. Please try again later.' }, { status: 500 });
|
|
126
99
|
}
|
|
127
100
|
},
|
|
128
101
|
};
|
|
129
102
|
}
|
|
130
|
-
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consumer auth API route handlers for Next.js App Router.
|
|
3
|
+
*
|
|
4
|
+
* Usage in a customer site:
|
|
5
|
+
* // app/api/consumer/[action]/route.ts
|
|
6
|
+
* import { NextResponse } from 'next/server';
|
|
7
|
+
* import { createConsumerAuthHandlers } from 'keystone-design-bootstrap/next/routes/consumer-auth';
|
|
8
|
+
* export const { POST } = createConsumerAuthHandlers({ NextResponse });
|
|
9
|
+
*
|
|
10
|
+
* Handles: initiate, login, signup, logout
|
|
11
|
+
*
|
|
12
|
+
* Env (server-side only):
|
|
13
|
+
* - API_URL (default: http://localhost:3000/api/v1)
|
|
14
|
+
* - API_KEY
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { CONSUMER_TOKEN_COOKIE } from '../../lib/consumer-session';
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
type NextResponseLike = { json: (body: unknown, init?: ResponseInit) => any };
|
|
21
|
+
|
|
22
|
+
const COOKIE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days
|
|
23
|
+
|
|
24
|
+
function getApiUrl(): string {
|
|
25
|
+
return process.env.API_URL || 'http://localhost:3000/api/v1';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getApiKey(): string {
|
|
29
|
+
return process.env.API_KEY || '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function apiHeaders(): Record<string, string> {
|
|
33
|
+
const key = getApiKey();
|
|
34
|
+
return {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
...(key ? { 'X-API-Key': key } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// POST /api/consumer/initiate
|
|
41
|
+
// Looks up an existing user by email or phone, creating a Contact if needed.
|
|
42
|
+
// Returns { exists, firstName, hasPassword }.
|
|
43
|
+
async function handleInitiate(request: Request, NR: NextResponseLike): Promise<Response> {
|
|
44
|
+
const body = await request.json().catch(() => ({})) as Record<string, unknown>;
|
|
45
|
+
const { email, phone } = body;
|
|
46
|
+
|
|
47
|
+
if (!email && !phone) {
|
|
48
|
+
return NR.json({ exists: null });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(`${getApiUrl()}/consumer/auth/initiate`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: apiHeaders(),
|
|
55
|
+
body: JSON.stringify({ email: email || undefined, phone: phone || undefined }),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (res.status === 404) return NR.json({ exists: false });
|
|
59
|
+
if (!res.ok) return NR.json({ exists: null });
|
|
60
|
+
|
|
61
|
+
const json = await res.json().catch(() => ({}));
|
|
62
|
+
const data = json.data as Record<string, unknown> | undefined;
|
|
63
|
+
return NR.json({
|
|
64
|
+
exists: true,
|
|
65
|
+
firstName: data?.first_name ?? undefined,
|
|
66
|
+
hasPassword: data?.has_password != null ? Boolean(data.has_password) : true,
|
|
67
|
+
});
|
|
68
|
+
} catch {
|
|
69
|
+
return NR.json({ exists: null });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// POST /api/consumer/login
|
|
74
|
+
// Authenticates an existing user. Sets HttpOnly JWT cookie on success.
|
|
75
|
+
// Returns {} on success or { error, code } on failure.
|
|
76
|
+
async function handleLogin(request: Request, NR: NextResponseLike): Promise<Response> {
|
|
77
|
+
const body = await request.json().catch(() => ({})) as Record<string, string>;
|
|
78
|
+
const { email, phone, password } = body;
|
|
79
|
+
|
|
80
|
+
if (!email && !phone) return NR.json({ error: 'Email or phone is required.' }, { status: 422 });
|
|
81
|
+
if (!password) return NR.json({ error: 'Password is required.' }, { status: 422 });
|
|
82
|
+
|
|
83
|
+
const res = await fetch(`${getApiUrl()}/consumer/auth/login`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: apiHeaders(),
|
|
86
|
+
body: JSON.stringify({ email: email || undefined, phone: phone || undefined, password }),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const json = await res.json().catch(() => ({}));
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
return NR.json(
|
|
92
|
+
{ error: json.error || 'Login failed. Please try again.', code: json.code },
|
|
93
|
+
{ status: res.status }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const token = json.data?.token;
|
|
98
|
+
if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
|
|
99
|
+
|
|
100
|
+
const response = NR.json({});
|
|
101
|
+
response.cookies.set(CONSUMER_TOKEN_COOKIE, token, {
|
|
102
|
+
httpOnly: true,
|
|
103
|
+
secure: process.env.NODE_ENV === 'production',
|
|
104
|
+
sameSite: 'lax',
|
|
105
|
+
path: '/',
|
|
106
|
+
maxAge: COOKIE_MAX_AGE,
|
|
107
|
+
});
|
|
108
|
+
return response;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// POST /api/consumer/signup
|
|
112
|
+
// Creates a new account (or claims an existing contact). Sets HttpOnly JWT cookie on success.
|
|
113
|
+
// Returns {} on success or { error } on failure.
|
|
114
|
+
async function handleSignup(request: Request, NR: NextResponseLike): Promise<Response> {
|
|
115
|
+
const body = await request.json().catch(() => ({})) as Record<string, string>;
|
|
116
|
+
const { email, phone, password, password_confirmation, first_name, last_name } = body;
|
|
117
|
+
|
|
118
|
+
if (!email && !phone) return NR.json({ error: 'Email or phone is required.' }, { status: 422 });
|
|
119
|
+
|
|
120
|
+
const res = await fetch(`${getApiUrl()}/consumer/auth/signup`, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: apiHeaders(),
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
email: email || undefined,
|
|
125
|
+
phone: phone || undefined,
|
|
126
|
+
password,
|
|
127
|
+
password_confirmation,
|
|
128
|
+
first_name: first_name || undefined,
|
|
129
|
+
last_name: last_name || undefined,
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const json = await res.json().catch(() => ({}));
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
return NR.json({ error: json.error || 'Signup failed. Please try again.' }, { status: res.status });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const token = json.data?.token;
|
|
139
|
+
if (!token) return NR.json({ error: 'No token received.' }, { status: 500 });
|
|
140
|
+
|
|
141
|
+
const response = NR.json({ claimed: json.data?.claimed ?? false });
|
|
142
|
+
response.cookies.set(CONSUMER_TOKEN_COOKIE, token, {
|
|
143
|
+
httpOnly: true,
|
|
144
|
+
secure: process.env.NODE_ENV === 'production',
|
|
145
|
+
sameSite: 'lax',
|
|
146
|
+
path: '/',
|
|
147
|
+
maxAge: COOKIE_MAX_AGE,
|
|
148
|
+
});
|
|
149
|
+
return response;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// POST /api/consumer/logout
|
|
153
|
+
// Clears the JWT cookie.
|
|
154
|
+
async function handleLogout(_request: Request, NR: NextResponseLike): Promise<Response> {
|
|
155
|
+
const response = NR.json({ success: true });
|
|
156
|
+
response.cookies.delete(CONSUMER_TOKEN_COOKIE);
|
|
157
|
+
return response;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Creates a single POST handler that routes to the correct auth action based on
|
|
162
|
+
* the dynamic `[action]` path segment.
|
|
163
|
+
*
|
|
164
|
+
* Compatible with Next.js 14 (sync params) and 15 (async params).
|
|
165
|
+
*/
|
|
166
|
+
export function createConsumerAuthHandlers({ NextResponse }: { NextResponse: NextResponseLike }) {
|
|
167
|
+
return {
|
|
168
|
+
POST: async (
|
|
169
|
+
request: Request,
|
|
170
|
+
context: { params: Promise<{ action: string }> | { action: string } }
|
|
171
|
+
): Promise<Response> => {
|
|
172
|
+
const { action } = await Promise.resolve(context.params);
|
|
173
|
+
if (action === 'initiate') return handleInitiate(request, NextResponse);
|
|
174
|
+
if (action === 'login') return handleLogin(request, NextResponse);
|
|
175
|
+
if (action === 'signup') return handleSignup(request, NextResponse);
|
|
176
|
+
if (action === 'logout') return handleLogout(request, NextResponse);
|
|
177
|
+
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Consumer and messaging types (aligned with Rails ConsumerSerializer and conversation endpoints). */
|
|
2
|
+
|
|
3
|
+
export interface Consumer {
|
|
4
|
+
id: number;
|
|
5
|
+
email: string | null;
|
|
6
|
+
phone: string | null;
|
|
7
|
+
primary_identifier: string | null;
|
|
8
|
+
contacts?: ConsumerContact[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ConsumerContact {
|
|
12
|
+
id: number;
|
|
13
|
+
display_name: string;
|
|
14
|
+
account?: { id: number; name: string; slug?: string };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ConversationSummary {
|
|
18
|
+
contact_id: number;
|
|
19
|
+
business?: { id: number; name: string; company_name?: string };
|
|
20
|
+
last_message_at: string | null;
|
|
21
|
+
last_message_preview?: string | null;
|
|
22
|
+
message_count: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Message {
|
|
26
|
+
id: number;
|
|
27
|
+
body: string | null;
|
|
28
|
+
/** "outbound" = sent by the business; "inbound" = sent by the contact/member. */
|
|
29
|
+
direction: 'inbound' | 'outbound';
|
|
30
|
+
sender_type: 'contact' | 'agent' | 'human';
|
|
31
|
+
sender_display_name?: string;
|
|
32
|
+
created_at: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ContactSummary {
|
|
36
|
+
id: number;
|
|
37
|
+
display_name: string;
|
|
38
|
+
business?: { id: number; name: string; company_name?: string };
|
|
39
|
+
}
|
package/src/types/api/offer.ts
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PhotoAttachment } from './photos';
|
|
2
|
+
import type { OfferPublic } from './offer';
|
|
3
|
+
|
|
4
|
+
export interface PackageItem {
|
|
5
|
+
quantity: number;
|
|
6
|
+
service_item?: { id: number; name: string; slug: string; summary?: string | null };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Package {
|
|
10
|
+
id: number;
|
|
11
|
+
name: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
summary?: string | null;
|
|
14
|
+
description_markdown?: string | null;
|
|
15
|
+
pricing_info?: string | null;
|
|
16
|
+
price_cents?: number | null;
|
|
17
|
+
photo_attachments?: PhotoAttachment[];
|
|
18
|
+
package_items?: PackageItem[];
|
|
19
|
+
offers?: OfferPublic[];
|
|
20
|
+
}
|
package/src/types/api/service.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import type { PhotoAttachment } from './photos';
|
|
2
|
+
import type { OfferPublic } from './offer';
|
|
3
|
+
|
|
2
4
|
export interface Service {
|
|
3
5
|
id: number;
|
|
4
6
|
name: string;
|
|
@@ -9,27 +11,12 @@ export interface Service {
|
|
|
9
11
|
features_markdown?: string;
|
|
10
12
|
featured: boolean;
|
|
11
13
|
sort_order: number;
|
|
12
|
-
photo_attachments?:
|
|
13
|
-
id: number;
|
|
14
|
-
featured: boolean;
|
|
15
|
-
attachable_id?: number;
|
|
16
|
-
attachable_type?: string;
|
|
17
|
-
photo?: {
|
|
18
|
-
id: number;
|
|
19
|
-
title: string;
|
|
20
|
-
thumbnail_url?: string;
|
|
21
|
-
medium_url?: string;
|
|
22
|
-
large_url?: string;
|
|
23
|
-
original_url?: string;
|
|
24
|
-
};
|
|
25
|
-
}>;
|
|
26
|
-
/** Menu items under this service category (when loaded with associations). */
|
|
14
|
+
photo_attachments?: PhotoAttachment[];
|
|
27
15
|
service_items?: ServiceItem[];
|
|
28
16
|
created_at: string;
|
|
29
17
|
updated_at: string;
|
|
30
18
|
}
|
|
31
19
|
|
|
32
|
-
/** Single menu item under a service category. */
|
|
33
20
|
export interface ServiceItem {
|
|
34
21
|
id: number;
|
|
35
22
|
name: string;
|
|
@@ -41,13 +28,8 @@ export interface ServiceItem {
|
|
|
41
28
|
duration_minutes?: number | null;
|
|
42
29
|
sort_order: number;
|
|
43
30
|
service_id?: number;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
id: number;
|
|
47
|
-
photo?: { thumbnail_url?: string; medium_url?: string; large_url?: string; original_url?: string; title?: string; alt_text?: string };
|
|
48
|
-
}>;
|
|
49
|
-
/** Active offers that apply to this menu item (public API). */
|
|
50
|
-
offers?: import('./offer').OfferPublic[];
|
|
31
|
+
photo_attachments?: PhotoAttachment[];
|
|
32
|
+
offers?: OfferPublic[];
|
|
51
33
|
}
|
|
52
34
|
|
|
53
35
|
export interface ServiceParams {
|
package/src/types/index.ts
CHANGED
|
@@ -9,12 +9,14 @@ export * from './config';
|
|
|
9
9
|
// Re-export API types
|
|
10
10
|
export * from './api/blog-post';
|
|
11
11
|
export * from './api/company-information';
|
|
12
|
+
export * from './api/consumer';
|
|
12
13
|
export * from './api/contact';
|
|
13
14
|
export * from './api/form';
|
|
14
15
|
export * from './api/faq';
|
|
15
16
|
export * from './api/job-posting';
|
|
16
17
|
export * from './api/location';
|
|
17
18
|
export * from './api/offer';
|
|
19
|
+
export * from './api/package';
|
|
18
20
|
export * from './api/photos';
|
|
19
21
|
export * from './api/service';
|
|
20
22
|
export * from './api/social-post';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type countries from './countries';
|
|
2
|
+
|
|
3
|
+
type Country = (typeof countries)[0];
|
|
4
|
+
|
|
5
|
+
/** Get national-format mask from country by stripping the country code prefix (e.g. "+1 (###) ###-####" → "(###) ###-####"). */
|
|
6
|
+
export function getNationalMask(country: Country | undefined): string {
|
|
7
|
+
if (!country?.phoneMask) return '';
|
|
8
|
+
const code = country.phoneCode.startsWith('+') ? country.phoneCode : `+${country.phoneCode}`;
|
|
9
|
+
const escaped = code.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
10
|
+
return country.phoneMask.replace(new RegExp(`^\\s*${escaped}[\\s-]*`), '').trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Format a raw digit string into a mask pattern where '#' represents one digit. No trailing literals so backspace works naturally. */
|
|
14
|
+
export function formatDigitsToMask(digits: string, mask: string): string {
|
|
15
|
+
if (digits.length === 0) return '';
|
|
16
|
+
let i = 0;
|
|
17
|
+
let out = '';
|
|
18
|
+
for (const c of mask) {
|
|
19
|
+
if (c === '#') {
|
|
20
|
+
if (i < digits.length) out += digits[i++];
|
|
21
|
+
else break;
|
|
22
|
+
} else if (i < digits.length) {
|
|
23
|
+
out += c;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { P as PhotoAttachment } from './website-photos-Bm-CBK9g.js';
|
|
2
|
-
|
|
3
|
-
interface BlogPost {
|
|
4
|
-
id: number;
|
|
5
|
-
title: string;
|
|
6
|
-
slug: string;
|
|
7
|
-
status: string;
|
|
8
|
-
published_at?: string;
|
|
9
|
-
excerpt_markdown?: string;
|
|
10
|
-
content_markdown: string;
|
|
11
|
-
featured: boolean;
|
|
12
|
-
seo_title?: string;
|
|
13
|
-
seo_description?: string;
|
|
14
|
-
seo_keywords?: string;
|
|
15
|
-
created_at: string;
|
|
16
|
-
updated_at: string;
|
|
17
|
-
photo_attachments?: PhotoAttachment[];
|
|
18
|
-
blog_post_authors?: BlogPostAuthor[];
|
|
19
|
-
blog_post_tags?: BlogPostTag[];
|
|
20
|
-
}
|
|
21
|
-
interface BlogPostParams {
|
|
22
|
-
status?: string;
|
|
23
|
-
author_id?: number;
|
|
24
|
-
tag_id?: number;
|
|
25
|
-
q?: string;
|
|
26
|
-
page?: number;
|
|
27
|
-
per_page?: number;
|
|
28
|
-
featured?: boolean;
|
|
29
|
-
}
|
|
30
|
-
type BlogPostResponse = BlogPost[];
|
|
31
|
-
interface BlogPostAuthor {
|
|
32
|
-
id: number;
|
|
33
|
-
name: string;
|
|
34
|
-
slug: string;
|
|
35
|
-
bio_markdown?: string;
|
|
36
|
-
active: boolean;
|
|
37
|
-
created_at: string;
|
|
38
|
-
updated_at: string;
|
|
39
|
-
photo_attachments?: PhotoAttachment[];
|
|
40
|
-
}
|
|
41
|
-
interface BlogPostTag {
|
|
42
|
-
id: number;
|
|
43
|
-
name: string;
|
|
44
|
-
slug: string;
|
|
45
|
-
description?: string;
|
|
46
|
-
created_at: string;
|
|
47
|
-
updated_at: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type { BlogPost as B, BlogPostAuthor as a, BlogPostParams as b, BlogPostResponse as c, BlogPostTag as d };
|
package/dist/contexts/index.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import * as React$1 from 'react';
|
|
2
|
-
import { Theme } from '../themes/index.js';
|
|
3
|
-
|
|
4
|
-
interface ThemeContextValue {
|
|
5
|
-
theme: Theme;
|
|
6
|
-
}
|
|
7
|
-
declare function ThemeProvider({ theme, children }: {
|
|
8
|
-
theme: Theme;
|
|
9
|
-
children: React.ReactNode;
|
|
10
|
-
}): React$1.JSX.Element;
|
|
11
|
-
declare function useTheme(): ThemeContextValue;
|
|
12
|
-
|
|
13
|
-
export { ThemeProvider, useTheme };
|