keystone-design-bootstrap 1.0.55 → 1.0.56

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 (56) hide show
  1. package/dist/design_system/elements/index.js +8 -3
  2. package/dist/design_system/elements/index.js.map +1 -1
  3. package/dist/design_system/sections/index.js +203 -106
  4. package/dist/design_system/sections/index.js.map +1 -1
  5. package/dist/index.js +303 -247
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/hooks/index.js +72 -0
  8. package/dist/lib/hooks/index.js.map +1 -1
  9. package/dist/lib/server-api.js.map +1 -1
  10. package/dist/utils/phone-helpers.js +26 -0
  11. package/dist/utils/phone-helpers.js.map +1 -0
  12. package/package.json +5 -2
  13. package/src/design_system/components/ChatWidget.tsx +51 -34
  14. package/src/design_system/components/DynamicFormFields.tsx +1 -24
  15. package/src/design_system/elements/modal/modal.tsx +54 -35
  16. package/src/design_system/portal/LoginForm.tsx +339 -0
  17. package/src/design_system/portal/LoginModalController.tsx +63 -0
  18. package/src/design_system/portal/LogoutButton.tsx +23 -0
  19. package/src/design_system/portal/MessageComposer.tsx +84 -0
  20. package/src/design_system/portal/PortalPage.tsx +754 -0
  21. package/src/design_system/portal/RowThumbnail.tsx +76 -0
  22. package/src/design_system/portal/actions.ts +160 -0
  23. package/src/design_system/portal/index.ts +5 -0
  24. package/src/design_system/sections/index.tsx +1 -1
  25. package/src/design_system/sections/service-menu-section.tsx +7 -108
  26. package/src/lib/actions.ts +51 -115
  27. package/src/lib/consumer-session.ts +74 -0
  28. package/src/lib/hooks/index.ts +2 -0
  29. package/src/lib/hooks/use-image-cycle.ts +105 -0
  30. package/src/lib/server-api.ts +7 -6
  31. package/src/next/routes/chat.ts +30 -58
  32. package/src/next/routes/consumer-auth.ts +113 -0
  33. package/src/types/api/consumer.ts +39 -0
  34. package/src/types/api/offer.ts +1 -1
  35. package/src/types/api/package.ts +20 -0
  36. package/src/types/api/service.ts +6 -24
  37. package/src/types/index.ts +2 -0
  38. package/src/utils/phone-helpers.ts +27 -0
  39. package/dist/blog-post-DGjaJ3wf.d.ts +0 -50
  40. package/dist/contexts/index.d.ts +0 -13
  41. package/dist/design_system/elements/index.d.ts +0 -372
  42. package/dist/design_system/logo/keystone-logo.d.ts +0 -6
  43. package/dist/design_system/sections/index.d.ts +0 -237
  44. package/dist/form-CpsCONG5.d.ts +0 -151
  45. package/dist/index.d.ts +0 -76
  46. package/dist/lib/component-registry.d.ts +0 -13
  47. package/dist/lib/hooks/index.d.ts +0 -64
  48. package/dist/lib/server-api.d.ts +0 -43
  49. package/dist/themes/index.d.ts +0 -16
  50. package/dist/types/index.d.ts +0 -264
  51. package/dist/utils/cx.d.ts +0 -15
  52. package/dist/utils/gradient-placeholder.d.ts +0 -8
  53. package/dist/utils/is-react-component.d.ts +0 -21
  54. package/dist/utils/markdown-toc.d.ts +0 -14
  55. package/dist/utils/photo-helpers.d.ts +0 -37
  56. package/dist/website-photos-Bm-CBK9g.d.ts +0 -47
@@ -0,0 +1,105 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useMemo } from 'react';
4
+ import type { PhotoAttachment } from '../../types/api/photos';
5
+
6
+ const CYCLE_INTERVAL_MIN_MS = 6000;
7
+ const CYCLE_INTERVAL_MAX_MS = 8000;
8
+ export const CROSSFADE_DURATION_MS = 600;
9
+
10
+ export interface CycledImage {
11
+ url: string;
12
+ alt: string;
13
+ }
14
+
15
+ export interface UseImageCycleResult {
16
+ list: CycledImage[];
17
+ currentIndex: number;
18
+ nextIndex: number;
19
+ transitioning: boolean;
20
+ }
21
+
22
+ /** Stable value in [0, 1) derived from seed string (FNV-1a hash). Same seed always produces same value. */
23
+ function seedToUnit(seed: string): number {
24
+ let h = 2166136261 >>> 0;
25
+ for (let i = 0; i < seed.length; i++) {
26
+ h ^= seed.charCodeAt(i);
27
+ h = (Math.imul(h, 16777619) >>> 0) >>> 0;
28
+ }
29
+ return (h >>> 0) / 4294967296;
30
+ }
31
+
32
+ /** Seeded Fisher-Yates shuffle. Same seed produces same order (SSR-safe). */
33
+ function shuffleWithSeed<T>(array: T[], seed: string): T[] {
34
+ if (array.length <= 1) return array;
35
+ const arr = [...array];
36
+ let h = 2166136261 >>> 0;
37
+ for (let i = 0; i < seed.length; i++) {
38
+ h ^= seed.charCodeAt(i);
39
+ h = (Math.imul(h, 16777619) >>> 0) >>> 0;
40
+ }
41
+ const next = (step: number) => {
42
+ h = (Math.imul(1664525, (h + step) >>> 0) + 1013904223) >>> 0;
43
+ return (h >>> 0) / 4294967296;
44
+ };
45
+ for (let i = arr.length - 1; i > 0; i--) {
46
+ const j = Math.floor(next(i) * (i + 1));
47
+ [arr[i], arr[j]] = [arr[j], arr[i]];
48
+ }
49
+ return arr;
50
+ }
51
+
52
+ function photoUrlFromAttachment(pa: PhotoAttachment): string | undefined {
53
+ return pa.photo?.large_url || pa.photo?.medium_url || pa.photo?.thumbnail_url;
54
+ }
55
+
56
+ function photoAltFromAttachment(pa: PhotoAttachment): string {
57
+ return pa.photo?.alt_text || pa.photo?.title || '';
58
+ }
59
+
60
+ /**
61
+ * Cycles through a list of photo attachments with a seeded shuffle and crossfade transitions.
62
+ *
63
+ * Each card uses a unique seed so intervals are staggered — cards don't all transition in sync.
64
+ * The shuffle is deterministic (SSR-safe) and the timer only activates when there are 2+ images.
65
+ */
66
+ export function useImageCycle(
67
+ photoAttachments: PhotoAttachment[] | undefined,
68
+ seed: string
69
+ ): UseImageCycleResult {
70
+ const list = useMemo<CycledImage[]>(() => {
71
+ const arr = Array.isArray(photoAttachments) && photoAttachments.length > 0 ? photoAttachments : [];
72
+ if (arr.length === 0) return [];
73
+ return shuffleWithSeed(arr, seed)
74
+ .map((pa) => ({ url: photoUrlFromAttachment(pa) ?? '', alt: photoAltFromAttachment(pa) }))
75
+ .filter((x) => x.url);
76
+ }, [photoAttachments, seed]);
77
+
78
+ const [currentIndex, setCurrentIndex] = useState(0);
79
+ const [transitioning, setTransitioning] = useState(false);
80
+
81
+ // Per-card random interval (6–8 s) so cards don't all transition in sync
82
+ const intervalMs = useMemo(
83
+ () => CYCLE_INTERVAL_MIN_MS + Math.floor(seedToUnit(seed) * (CYCLE_INTERVAL_MAX_MS - CYCLE_INTERVAL_MIN_MS + 1)),
84
+ [seed]
85
+ );
86
+
87
+ useEffect(() => {
88
+ if (list.length <= 1) return;
89
+ const id = setInterval(() => setTransitioning(true), intervalMs);
90
+ return () => clearInterval(id);
91
+ }, [list.length, intervalMs]);
92
+
93
+ useEffect(() => {
94
+ if (!transitioning || list.length <= 1) return;
95
+ const t = setTimeout(() => {
96
+ setCurrentIndex((i) => (i + 1) % list.length);
97
+ setTransitioning(false);
98
+ }, CROSSFADE_DURATION_MS);
99
+ return () => clearTimeout(t);
100
+ }, [transitioning, list.length]);
101
+
102
+ const nextIndex = list.length > 1 ? (currentIndex + 1) % list.length : 0;
103
+
104
+ return { list, currentIndex, nextIndex, transitioning };
105
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { CompanyInformation } from '../types/api/company-information';
7
7
  import type { Service } from '../types/api/service';
8
+ import type { Package } from '../types/api/package';
8
9
  import type { WebsitePhotos } from '../types/api/website-photos';
9
10
 
10
11
  const API_URL = process.env.API_URL || 'http://localhost:3000/api/v1';
@@ -77,8 +78,8 @@ export function getMetaPixelId(adsConfig: { meta_pixel_id?: string } | null | un
77
78
  return str !== '' && str !== 'null' && /^\d+$/.test(str) ? str : null;
78
79
  }
79
80
 
80
- export async function getServices() {
81
- return serverFetch('/public/services', defaultOptions);
81
+ export async function getServices(): Promise<Service[] | null> {
82
+ return serverFetch<Service[]>('/public/services', defaultOptions);
82
83
  }
83
84
 
84
85
  export async function getService(slug: string): Promise<Service | null> {
@@ -130,12 +131,12 @@ export async function getSocialPosts() {
130
131
  }
131
132
 
132
133
  /** Packages (bundles of service items). */
133
- export async function getPackages() {
134
- return serverFetch('/public/packages', defaultOptions);
134
+ export async function getPackages(): Promise<Package[] | null> {
135
+ return serverFetch<Package[]>('/public/packages', defaultOptions);
135
136
  }
136
137
 
137
- export async function getPackage(slug: string) {
138
- return serverFetch(`/public/packages/by_slug/${encodeURIComponent(slug)}`, defaultOptions);
138
+ export async function getPackage(slug: string): Promise<Package | null> {
139
+ return serverFetch<Package>(`/public/packages/by_slug/${encodeURIComponent(slug)}`, defaultOptions);
139
140
  }
140
141
 
141
142
  // Alias for testimonials (API uses "reviews")
@@ -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 (!identifier || !messageBody) {
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: 'Identifier and message body are required.' },
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
- 'Content-Type': 'application/json',
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: 'Identifier is required.' }, { status: 400 });
76
+ if (!identifier && !contactId) {
77
+ return json({ success: false, error: 'identifier or contact_id is required.' }, { status: 400 });
90
78
  }
91
79
 
92
- const response = await fetch(
93
- `${API_URL}/public/messages?identifier=${encodeURIComponent(identifier)}`,
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 data = await response.json();
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,113 @@
1
+ /**
2
+ * Factory functions for consumer auth Next.js API route handlers.
3
+ *
4
+ * Usage in customer site:
5
+ * // app/api/consumer/login/route.ts
6
+ * import { NextResponse } from 'next/server';
7
+ * import { createConsumerLoginHandler } from 'keystone-design-bootstrap/next/routes/consumer-auth';
8
+ * export const { POST } = createConsumerLoginHandler({ NextResponse });
9
+ */
10
+
11
+ import { CONSUMER_TOKEN_COOKIE } from '../../lib/consumer-session';
12
+
13
+ interface NextResponseLike {
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ json: (body: unknown, init?: ResponseInit) => any;
16
+ }
17
+
18
+ const COOKIE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days
19
+
20
+ function getApiUrl(): string {
21
+ return process.env.API_URL || 'http://localhost:3000/api/v1';
22
+ }
23
+
24
+ function setCookieOnResponse(response: Response, token: string): void {
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ (response as any).cookies.set(CONSUMER_TOKEN_COOKIE, token, {
27
+ httpOnly: true,
28
+ secure: process.env.NODE_ENV === 'production',
29
+ sameSite: 'lax',
30
+ path: '/',
31
+ maxAge: COOKIE_MAX_AGE,
32
+ });
33
+ }
34
+
35
+ export function createConsumerLoginHandler({ NextResponse }: { NextResponse: NextResponseLike }) {
36
+ return {
37
+ async POST(request: Request) {
38
+ const body = await request.json().catch(() => ({})) as Record<string, string>;
39
+ const { email, phone, password } = body;
40
+
41
+ if (!email && !phone) {
42
+ return NextResponse.json({ error: 'Email or phone is required' }, { status: 422 });
43
+ }
44
+ if (!password) {
45
+ return NextResponse.json({ error: 'Password is required' }, { status: 422 });
46
+ }
47
+
48
+ const apiRes = await fetch(`${getApiUrl()}/consumer/auth/login`, {
49
+ method: 'POST',
50
+ headers: { 'Content-Type': 'application/json' },
51
+ body: JSON.stringify({ email: email || undefined, phone: phone || undefined, password }),
52
+ });
53
+
54
+ const json = await apiRes.json().catch(() => ({}));
55
+ if (!apiRes.ok) {
56
+ return NextResponse.json({ error: json.error || 'Login failed' }, { status: apiRes.status });
57
+ }
58
+
59
+ const token = json.data?.token;
60
+ if (!token) {
61
+ return NextResponse.json({ error: 'No token received' }, { status: 500 });
62
+ }
63
+
64
+ const response = NextResponse.json({ success: true });
65
+ setCookieOnResponse(response, token);
66
+ return response;
67
+ },
68
+ };
69
+ }
70
+
71
+ export function createConsumerSignupHandler({ NextResponse }: { NextResponse: NextResponseLike }) {
72
+ return {
73
+ async POST(request: Request) {
74
+ const body = await request.json().catch(() => ({})) as Record<string, string>;
75
+ const { email, phone, password, password_confirmation } = body;
76
+
77
+ if (!email && !phone) {
78
+ return NextResponse.json({ error: 'Email or phone is required' }, { status: 422 });
79
+ }
80
+
81
+ const apiRes = await fetch(`${getApiUrl()}/consumer/auth/signup`, {
82
+ method: 'POST',
83
+ headers: { 'Content-Type': 'application/json' },
84
+ body: JSON.stringify({ email: email || undefined, phone: phone || undefined, password, password_confirmation }),
85
+ });
86
+
87
+ const json = await apiRes.json().catch(() => ({}));
88
+ if (!apiRes.ok) {
89
+ return NextResponse.json({ error: json.error || 'Signup failed' }, { status: apiRes.status });
90
+ }
91
+
92
+ const token = json.data?.token;
93
+ if (!token) {
94
+ return NextResponse.json({ error: 'No token received' }, { status: 500 });
95
+ }
96
+
97
+ const response = NextResponse.json({ success: true, claimed: json.data?.claimed ?? false });
98
+ setCookieOnResponse(response, token);
99
+ return response;
100
+ },
101
+ };
102
+ }
103
+
104
+ export function createConsumerLogoutHandler({ NextResponse }: { NextResponse: NextResponseLike }) {
105
+ return {
106
+ async POST() {
107
+ const response = NextResponse.json({ success: true });
108
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
+ (response as any).cookies.delete(CONSUMER_TOKEN_COOKIE);
110
+ return response;
111
+ },
112
+ };
113
+ }
@@ -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
+ }
@@ -6,7 +6,7 @@ export interface OfferPublic {
6
6
  name: string;
7
7
  description: string | null;
8
8
  value_terms: string | null;
9
- active: boolean;
9
+ active?: boolean;
10
10
  expires_at?: string | null;
11
11
  expired?: boolean;
12
12
  photo_attachments?: PhotoAttachment[];
@@ -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
+ }
@@ -1,4 +1,6 @@
1
- // Service type definitions (aligned with Rails ServiceSerializer)
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?: Array<{
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
- /** Present when loaded for public/menu (from related service’s photos). */
45
- photo_attachments?: Array<{
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 {
@@ -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 };
@@ -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 };