keystone-design-bootstrap 1.0.105 → 1.0.106
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/lib/server-api.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ declare function getLocations(): Promise<unknown>;
|
|
|
48
48
|
declare function getLocation(slug: string): Promise<unknown>;
|
|
49
49
|
declare function getReviews(): Promise<unknown>;
|
|
50
50
|
declare function getFAQs(): Promise<unknown>;
|
|
51
|
-
declare function getBlogPosts(): Promise<
|
|
51
|
+
declare function getBlogPosts(): Promise<{} | null>;
|
|
52
52
|
declare function getBlogPost(slug: string): Promise<unknown>;
|
|
53
53
|
declare function getTeamMembers(): Promise<unknown>;
|
|
54
54
|
declare function getWebsitePhotos(): Promise<WebsitePhotos | null>;
|
package/dist/lib/server-api.js
CHANGED
|
@@ -45,8 +45,7 @@ function serverError(event, error, detail) {
|
|
|
45
45
|
// src/lib/server-api.ts
|
|
46
46
|
var API_URL = process.env.API_URL || "http://localhost:3000/api/v1";
|
|
47
47
|
var API_KEY = process.env.API_KEY || "";
|
|
48
|
-
async function
|
|
49
|
-
var _a;
|
|
48
|
+
async function serverFetchJson(endpoint, options = {}) {
|
|
50
49
|
const url = `${API_URL}${endpoint}`;
|
|
51
50
|
try {
|
|
52
51
|
const fetchOptions = {
|
|
@@ -67,13 +66,18 @@ async function serverFetch(endpoint, options = {}) {
|
|
|
67
66
|
});
|
|
68
67
|
return null;
|
|
69
68
|
}
|
|
70
|
-
|
|
71
|
-
return (_a = json.data) != null ? _a : json;
|
|
69
|
+
return await response.json();
|
|
72
70
|
} catch (error) {
|
|
73
71
|
serverError("SERVER_API_FETCH_FAILED", error, { endpoint });
|
|
74
72
|
return null;
|
|
75
73
|
}
|
|
76
74
|
}
|
|
75
|
+
async function serverFetch(endpoint, options = {}) {
|
|
76
|
+
var _a;
|
|
77
|
+
const json = await serverFetchJson(endpoint, options);
|
|
78
|
+
if (json === null) return null;
|
|
79
|
+
return (_a = json.data) != null ? _a : json;
|
|
80
|
+
}
|
|
77
81
|
var serverApi = {
|
|
78
82
|
get: (endpoint, options) => {
|
|
79
83
|
return serverFetch(endpoint, options || { revalidate: 60 });
|
|
@@ -128,8 +132,30 @@ async function getReviews() {
|
|
|
128
132
|
async function getFAQs() {
|
|
129
133
|
return serverFetch("/public/faq_questions", defaultOptions);
|
|
130
134
|
}
|
|
135
|
+
var BLOG_POSTS_PER_PAGE = 100;
|
|
136
|
+
var BLOG_POSTS_MAX_PAGES = 50;
|
|
131
137
|
async function getBlogPosts() {
|
|
132
|
-
|
|
138
|
+
var _a, _b;
|
|
139
|
+
const first = await serverFetchJson(`/public/blog_posts?page=1&per_page=${BLOG_POSTS_PER_PAGE}`, defaultOptions);
|
|
140
|
+
if (first === null) return null;
|
|
141
|
+
const firstData = (_a = first.data) != null ? _a : first;
|
|
142
|
+
if (!Array.isArray(firstData)) return firstData;
|
|
143
|
+
const allPosts = [...firstData];
|
|
144
|
+
const totalCount = typeof ((_b = first.meta) == null ? void 0 : _b.total_count) === "number" ? first.meta.total_count : allPosts.length;
|
|
145
|
+
const effectivePerPage = firstData.length || BLOG_POSTS_PER_PAGE;
|
|
146
|
+
const lastPage = Math.min(Math.ceil(totalCount / effectivePerPage), BLOG_POSTS_MAX_PAGES);
|
|
147
|
+
if (lastPage > 1) {
|
|
148
|
+
const pageNumbers = Array.from({ length: lastPage - 1 }, (_, i) => i + 2);
|
|
149
|
+
const batches = await Promise.all(
|
|
150
|
+
pageNumbers.map(
|
|
151
|
+
(page) => serverFetch(`/public/blog_posts?page=${page}&per_page=${BLOG_POSTS_PER_PAGE}`, defaultOptions)
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
for (const batch of batches) {
|
|
155
|
+
if (Array.isArray(batch)) allPosts.push(...batch);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return allPosts;
|
|
133
159
|
}
|
|
134
160
|
async function getBlogPost(slug) {
|
|
135
161
|
return serverFetch(`/public/blog_posts/by_slug/${slug}`, defaultOptions);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/server-log.ts","../../src/lib/server-api.ts"],"sourcesContent":["type ServerLogLevel = 'INFO' | 'WARN' | 'ERROR';\n\nfunction normalizeError(error: unknown): Record<string, string> {\n if (error instanceof Error) {\n return {\n message: error.message,\n stack: error.stack || '',\n name: error.name,\n };\n }\n return { message: String(error) };\n}\n\nfunction writeServerLog(level: ServerLogLevel, event: string, detail?: Record<string, unknown>): void {\n const payload = {\n level,\n event,\n detail: detail ?? {},\n at: new Date().toISOString(),\n };\n const line = `[ks-server] ${JSON.stringify(payload)}\\n`;\n const stream = level === 'ERROR' ? process.stderr : process.stdout;\n stream.write(line);\n}\n\nexport function serverLog(event: string, detail?: Record<string, unknown>): void {\n writeServerLog('INFO', event, detail);\n}\n\nexport function serverWarn(event: string, detail?: Record<string, unknown>): void {\n writeServerLog('WARN', event, detail);\n}\n\nexport function serverError(event: string, error?: unknown, detail?: Record<string, unknown>): void {\n writeServerLog('ERROR', event, {\n ...(detail ?? {}),\n ...(error !== undefined ? { error: normalizeError(error) } : {}),\n });\n}\n","/**\n * Server-side API client for SSR\n * API key is stored securely on the server - never exposed to browser\n */\n\nimport type { CompanyInformation } from '../types/api/company-information';\nimport type { Service } from '../types/api/service';\nimport type { Package } from '../types/api/package';\nimport type { WebsitePhotos } from '../types/api/website-photos';\nimport { serverError } from './server-log';\n\nconst API_URL = process.env.API_URL || 'http://localhost:3000/api/v1';\nconst API_KEY = process.env.API_KEY || '';\n\ninterface FetchOptions {\n cache?: RequestCache;\n revalidate?: number;\n}\n\nasync function serverFetch<T>(endpoint: string, options: FetchOptions = {}): Promise<T | null> {\n const url = `${API_URL}${endpoint}`;\n \n try {\n const fetchOptions: RequestInit & { next?: { revalidate?: number } } = {\n headers: {\n 'X-API-Key': API_KEY,\n 'Content-Type': 'application/json',\n },\n cache: options.cache,\n };\n \n if (options.revalidate) {\n fetchOptions.next = { revalidate: options.revalidate };\n }\n \n const response = await fetch(url, fetchOptions);\n\n if (!response.ok) {\n serverError('SERVER_API_NON_OK_RESPONSE', undefined, {\n endpoint,\n status: response.status,\n });\n return null;\n }\n\n const json = await response.json();\n \n // Rails API returns { data: [...], meta: {...} }\n return json.data ?? json;\n } catch (error) {\n serverError('SERVER_API_FETCH_FAILED', error, { endpoint });\n return null;\n }\n}\n\n/**\n * Generic serverApi object for flexible endpoint access\n */\nexport const serverApi = {\n get: <T = unknown>(endpoint: string, options?: FetchOptions): Promise<T | null> => {\n return serverFetch<T>(endpoint, options || { revalidate: 60 });\n }\n};\n\n// Revalidate data every 60 seconds (ISR)\nconst defaultOptions: FetchOptions = { revalidate: 60 };\n\nexport async function getCompanyInformation(): Promise<CompanyInformation | null> {\n return serverFetch<CompanyInformation>('/public/company_information', defaultOptions);\n}\n\n/** Ads config (e.g. Meta Pixel). Returns { meta_pixel_id?: string } or {}. Only present when account has connected Meta Ads. */\nexport async function getAdsConfig(): Promise<{ meta_pixel_id?: string } | null> {\n const data = await serverFetch<{ meta_pixel_id?: string }>('/public/ads_config', defaultOptions);\n return data ?? null;\n}\n\n/** Extract Meta Pixel ID from ads config for use with <MetaPixel pixelId={...} />. Only returns a value when present and valid (numeric). */\nexport function getMetaPixelId(adsConfig: { meta_pixel_id?: string } | null | undefined): string | null {\n const id = adsConfig && typeof adsConfig === 'object' && 'meta_pixel_id' in adsConfig && adsConfig.meta_pixel_id;\n const str = id != null && id !== '' ? String(id).trim() : '';\n return str !== '' && str !== 'null' && /^\\d+$/.test(str) ? str : null;\n}\n\nexport type AnalyticsConfig = {\n /** PostHog project API key — platform-wide, from POSTHOG_API_KEY env var on the Rails server. */\n posthog_api_key?: string;\n /** Per-site GTM container ID (e.g. GTM-ABC123), provisioned by Keystone. */\n gtm_container_public_id?: string;\n /**\n * Environment identifier from KEYSTONE_ENV on the Rails server (e.g. \"production\", \"staging\",\n * \"development\"). Registered as a PostHog super property on every event so the sync job can\n * filter by environment and avoid cross-environment data contamination.\n */\n environment?: string;\n};\n\n/** Analytics config for customer sites. Returns platform-wide analytics keys (e.g. PostHog). */\nexport async function getAnalyticsConfig(): Promise<AnalyticsConfig | null> {\n const data = await serverFetch<AnalyticsConfig>('/public/analytics_config', defaultOptions);\n return data ?? null;\n}\n\n/** Extract PostHog API key from analytics config for use with <PostHogProvider apiKey={...} />. */\nexport function getPostHogApiKey(analyticsConfig: AnalyticsConfig | null | undefined): string | null {\n const key = analyticsConfig?.posthog_api_key;\n const str = key != null && key !== '' ? String(key).trim() : '';\n return str !== '' && str !== 'null' ? str : null;\n}\n\n/** Extract GTM container ID from analytics config for use with <GoogleTagManager containerId={...} />. */\nexport function getGtmContainerPublicId(analyticsConfig: AnalyticsConfig | null | undefined): string | null {\n const value = analyticsConfig?.gtm_container_public_id;\n const str = value != null && value !== '' ? String(value).trim() : '';\n return str !== '' && str !== 'null' && /^GTM-[A-Z0-9]+$/i.test(str) ? str : null;\n}\n\n/** Extract environment string from analytics config for use with <PostHogProvider environment={...} />. */\nexport function getKeystoneEnvironment(analyticsConfig: AnalyticsConfig | null | undefined): string {\n return analyticsConfig?.environment?.trim() || 'development';\n}\n\nexport async function getServices(): Promise<Service[] | null> {\n return serverFetch<Service[]>('/public/services', defaultOptions);\n}\n\nexport async function getService(slug: string): Promise<Service | null> {\n return serverFetch<Service>(`/public/services/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getLocations() {\n return serverFetch('/public/locations', defaultOptions);\n}\n\nexport async function getLocation(slug: string) {\n return serverFetch(`/public/locations/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getReviews() {\n return serverFetch('/public/reviews', defaultOptions);\n}\n\nexport async function getFAQs() {\n return serverFetch('/public/faq_questions', defaultOptions);\n}\n\nexport async function getBlogPosts() {\n return serverFetch('/public/blog_posts', defaultOptions);\n}\n\nexport async function getBlogPost(slug: string) {\n return serverFetch(`/public/blog_posts/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getTeamMembers() {\n return serverFetch('/public/team_members', defaultOptions);\n}\n\nexport async function getWebsitePhotos(): Promise<WebsitePhotos | null> {\n return serverFetch<WebsitePhotos>('/public/website_photos', defaultOptions);\n}\n\nexport async function getJobPostings() {\n return serverFetch('/public/job_postings', defaultOptions);\n}\n\nexport async function getJobPosting(slug: string) {\n return serverFetch(`/public/job_postings/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getSocialPosts() {\n return serverFetch('/public/social_posts', defaultOptions);\n}\n\n/** Packages (bundles of service items). */\nexport async function getPackages(): Promise<Package[] | null> {\n return serverFetch<Package[]>('/public/packages', defaultOptions);\n}\n\nexport async function getPackage(slug: string): Promise<Package | null> {\n return serverFetch<Package>(`/public/packages/by_slug/${encodeURIComponent(slug)}`, defaultOptions);\n}\n\n// Alias for testimonials (API uses \"reviews\")\nexport async function getTestimonials() {\n return getReviews();\n}\n\n/** Form definition for dynamic form rendering, fetched by form_type. */\nexport async function getForm(formType: string) {\n type FormDefinition = import('../types/api/form').FormDefinition;\n return serverFetch<FormDefinition>(`/public/forms/${encodeURIComponent(formType)}`, defaultOptions);\n}\n\n/** Form definition for dynamic form rendering, fetched by numeric ID. */\nexport async function getFormById(id: number | string) {\n type FormDefinition = import('../types/api/form').FormDefinition;\n return serverFetch<FormDefinition>(`/public/form_by_id/${encodeURIComponent(id)}`, defaultOptions);\n}\n\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/lib/server-log.ts","../../src/lib/server-api.ts"],"sourcesContent":["type ServerLogLevel = 'INFO' | 'WARN' | 'ERROR';\n\nfunction normalizeError(error: unknown): Record<string, string> {\n if (error instanceof Error) {\n return {\n message: error.message,\n stack: error.stack || '',\n name: error.name,\n };\n }\n return { message: String(error) };\n}\n\nfunction writeServerLog(level: ServerLogLevel, event: string, detail?: Record<string, unknown>): void {\n const payload = {\n level,\n event,\n detail: detail ?? {},\n at: new Date().toISOString(),\n };\n const line = `[ks-server] ${JSON.stringify(payload)}\\n`;\n const stream = level === 'ERROR' ? process.stderr : process.stdout;\n stream.write(line);\n}\n\nexport function serverLog(event: string, detail?: Record<string, unknown>): void {\n writeServerLog('INFO', event, detail);\n}\n\nexport function serverWarn(event: string, detail?: Record<string, unknown>): void {\n writeServerLog('WARN', event, detail);\n}\n\nexport function serverError(event: string, error?: unknown, detail?: Record<string, unknown>): void {\n writeServerLog('ERROR', event, {\n ...(detail ?? {}),\n ...(error !== undefined ? { error: normalizeError(error) } : {}),\n });\n}\n","/**\n * Server-side API client for SSR\n * API key is stored securely on the server - never exposed to browser\n */\n\nimport type { CompanyInformation } from '../types/api/company-information';\nimport type { Service } from '../types/api/service';\nimport type { Package } from '../types/api/package';\nimport type { WebsitePhotos } from '../types/api/website-photos';\nimport { serverError } from './server-log';\n\nconst API_URL = process.env.API_URL || 'http://localhost:3000/api/v1';\nconst API_KEY = process.env.API_KEY || '';\n\ninterface FetchOptions {\n cache?: RequestCache;\n revalidate?: number;\n}\n\n// Full response envelope — the API returns { data: [...], meta: {...} }\nasync function serverFetchJson(endpoint: string, options: FetchOptions = {}): Promise<{ data?: unknown; meta?: { total?: number; total_count?: number; page?: number; per_page?: number } } | null> {\n const url = `${API_URL}${endpoint}`;\n\n try {\n const fetchOptions: RequestInit & { next?: { revalidate?: number } } = {\n headers: {\n 'X-API-Key': API_KEY,\n 'Content-Type': 'application/json',\n },\n cache: options.cache,\n };\n\n if (options.revalidate) {\n fetchOptions.next = { revalidate: options.revalidate };\n }\n\n const response = await fetch(url, fetchOptions);\n\n if (!response.ok) {\n serverError('SERVER_API_NON_OK_RESPONSE', undefined, {\n endpoint,\n status: response.status,\n });\n return null;\n }\n\n return await response.json();\n } catch (error) {\n serverError('SERVER_API_FETCH_FAILED', error, { endpoint });\n return null;\n }\n}\n\nasync function serverFetch<T>(endpoint: string, options: FetchOptions = {}): Promise<T | null> {\n const json = await serverFetchJson(endpoint, options);\n if (json === null) return null;\n return (json.data ?? json) as T;\n}\n\n/**\n * Generic serverApi object for flexible endpoint access\n */\nexport const serverApi = {\n get: <T = unknown>(endpoint: string, options?: FetchOptions): Promise<T | null> => {\n return serverFetch<T>(endpoint, options || { revalidate: 60 });\n }\n};\n\n// Revalidate data every 60 seconds (ISR)\nconst defaultOptions: FetchOptions = { revalidate: 60 };\n\nexport async function getCompanyInformation(): Promise<CompanyInformation | null> {\n return serverFetch<CompanyInformation>('/public/company_information', defaultOptions);\n}\n\n/** Ads config (e.g. Meta Pixel). Returns { meta_pixel_id?: string } or {}. Only present when account has connected Meta Ads. */\nexport async function getAdsConfig(): Promise<{ meta_pixel_id?: string } | null> {\n const data = await serverFetch<{ meta_pixel_id?: string }>('/public/ads_config', defaultOptions);\n return data ?? null;\n}\n\n/** Extract Meta Pixel ID from ads config for use with <MetaPixel pixelId={...} />. Only returns a value when present and valid (numeric). */\nexport function getMetaPixelId(adsConfig: { meta_pixel_id?: string } | null | undefined): string | null {\n const id = adsConfig && typeof adsConfig === 'object' && 'meta_pixel_id' in adsConfig && adsConfig.meta_pixel_id;\n const str = id != null && id !== '' ? String(id).trim() : '';\n return str !== '' && str !== 'null' && /^\\d+$/.test(str) ? str : null;\n}\n\nexport type AnalyticsConfig = {\n /** PostHog project API key — platform-wide, from POSTHOG_API_KEY env var on the Rails server. */\n posthog_api_key?: string;\n /** Per-site GTM container ID (e.g. GTM-ABC123), provisioned by Keystone. */\n gtm_container_public_id?: string;\n /**\n * Environment identifier from KEYSTONE_ENV on the Rails server (e.g. \"production\", \"staging\",\n * \"development\"). Registered as a PostHog super property on every event so the sync job can\n * filter by environment and avoid cross-environment data contamination.\n */\n environment?: string;\n};\n\n/** Analytics config for customer sites. Returns platform-wide analytics keys (e.g. PostHog). */\nexport async function getAnalyticsConfig(): Promise<AnalyticsConfig | null> {\n const data = await serverFetch<AnalyticsConfig>('/public/analytics_config', defaultOptions);\n return data ?? null;\n}\n\n/** Extract PostHog API key from analytics config for use with <PostHogProvider apiKey={...} />. */\nexport function getPostHogApiKey(analyticsConfig: AnalyticsConfig | null | undefined): string | null {\n const key = analyticsConfig?.posthog_api_key;\n const str = key != null && key !== '' ? String(key).trim() : '';\n return str !== '' && str !== 'null' ? str : null;\n}\n\n/** Extract GTM container ID from analytics config for use with <GoogleTagManager containerId={...} />. */\nexport function getGtmContainerPublicId(analyticsConfig: AnalyticsConfig | null | undefined): string | null {\n const value = analyticsConfig?.gtm_container_public_id;\n const str = value != null && value !== '' ? String(value).trim() : '';\n return str !== '' && str !== 'null' && /^GTM-[A-Z0-9]+$/i.test(str) ? str : null;\n}\n\n/** Extract environment string from analytics config for use with <PostHogProvider environment={...} />. */\nexport function getKeystoneEnvironment(analyticsConfig: AnalyticsConfig | null | undefined): string {\n return analyticsConfig?.environment?.trim() || 'development';\n}\n\nexport async function getServices(): Promise<Service[] | null> {\n return serverFetch<Service[]>('/public/services', defaultOptions);\n}\n\nexport async function getService(slug: string): Promise<Service | null> {\n return serverFetch<Service>(`/public/services/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getLocations() {\n return serverFetch('/public/locations', defaultOptions);\n}\n\nexport async function getLocation(slug: string) {\n return serverFetch(`/public/locations/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getReviews() {\n return serverFetch('/public/reviews', defaultOptions);\n}\n\nexport async function getFAQs() {\n return serverFetch('/public/faq_questions', defaultOptions);\n}\n\n// The public blog_posts endpoint is paginated server-side (default per_page=10, max 100).\n// Blog gallery sections paginate client-side over the full set, so fetch every page,\n// using meta.total_count to know when we have everything (the server may clamp per_page).\nconst BLOG_POSTS_PER_PAGE = 100;\nconst BLOG_POSTS_MAX_PAGES = 50; // safety cap: 5000 posts\n\nexport async function getBlogPosts() {\n const first = await serverFetchJson(`/public/blog_posts?page=1&per_page=${BLOG_POSTS_PER_PAGE}`, defaultOptions);\n if (first === null) return null;\n const firstData = first.data ?? first;\n if (!Array.isArray(firstData)) return firstData; // unexpected shape — pass through unchanged\n\n const allPosts = [...firstData];\n // total_count lets us compute the remaining page count up front and fetch those\n // pages in parallel (page 1 is already in hand). Falls back to first-page-only when\n // meta is absent (older backends). Uses firstData.length as the effective per_page so\n // a server that clamps below BLOG_POSTS_PER_PAGE still gets every page covered.\n const totalCount = typeof first.meta?.total_count === 'number' ? first.meta.total_count : allPosts.length;\n const effectivePerPage = firstData.length || BLOG_POSTS_PER_PAGE;\n const lastPage = Math.min(Math.ceil(totalCount / effectivePerPage), BLOG_POSTS_MAX_PAGES);\n\n if (lastPage > 1) {\n const pageNumbers = Array.from({ length: lastPage - 1 }, (_, i) => i + 2);\n const batches = await Promise.all(\n pageNumbers.map((page) =>\n serverFetch<unknown[]>(`/public/blog_posts?page=${page}&per_page=${BLOG_POSTS_PER_PAGE}`, defaultOptions)\n )\n );\n // Promise.all preserves order, so posts stay in page order. Skip empty/failed pages.\n for (const batch of batches) {\n if (Array.isArray(batch)) allPosts.push(...batch);\n }\n }\n return allPosts;\n}\n\nexport async function getBlogPost(slug: string) {\n return serverFetch(`/public/blog_posts/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getTeamMembers() {\n return serverFetch('/public/team_members', defaultOptions);\n}\n\nexport async function getWebsitePhotos(): Promise<WebsitePhotos | null> {\n return serverFetch<WebsitePhotos>('/public/website_photos', defaultOptions);\n}\n\nexport async function getJobPostings() {\n return serverFetch('/public/job_postings', defaultOptions);\n}\n\nexport async function getJobPosting(slug: string) {\n return serverFetch(`/public/job_postings/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getSocialPosts() {\n return serverFetch('/public/social_posts', defaultOptions);\n}\n\n/** Packages (bundles of service items). */\nexport async function getPackages(): Promise<Package[] | null> {\n return serverFetch<Package[]>('/public/packages', defaultOptions);\n}\n\nexport async function getPackage(slug: string): Promise<Package | null> {\n return serverFetch<Package>(`/public/packages/by_slug/${encodeURIComponent(slug)}`, defaultOptions);\n}\n\n// Alias for testimonials (API uses \"reviews\")\nexport async function getTestimonials() {\n return getReviews();\n}\n\n/** Form definition for dynamic form rendering, fetched by form_type. */\nexport async function getForm(formType: string) {\n type FormDefinition = import('../types/api/form').FormDefinition;\n return serverFetch<FormDefinition>(`/public/forms/${encodeURIComponent(formType)}`, defaultOptions);\n}\n\n/** Form definition for dynamic form rendering, fetched by numeric ID. */\nexport async function getFormById(id: number | string) {\n type FormDefinition = import('../types/api/form').FormDefinition;\n return serverFetch<FormDefinition>(`/public/form_by_id/${encodeURIComponent(id)}`, defaultOptions);\n}\n\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
package/src/lib/server-api.ts
CHANGED
|
@@ -17,9 +17,10 @@ interface FetchOptions {
|
|
|
17
17
|
revalidate?: number;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// Full response envelope — the API returns { data: [...], meta: {...} }
|
|
21
|
+
async function serverFetchJson(endpoint: string, options: FetchOptions = {}): Promise<{ data?: unknown; meta?: { total?: number; total_count?: number; page?: number; per_page?: number } } | null> {
|
|
21
22
|
const url = `${API_URL}${endpoint}`;
|
|
22
|
-
|
|
23
|
+
|
|
23
24
|
try {
|
|
24
25
|
const fetchOptions: RequestInit & { next?: { revalidate?: number } } = {
|
|
25
26
|
headers: {
|
|
@@ -28,11 +29,11 @@ async function serverFetch<T>(endpoint: string, options: FetchOptions = {}): Pro
|
|
|
28
29
|
},
|
|
29
30
|
cache: options.cache,
|
|
30
31
|
};
|
|
31
|
-
|
|
32
|
+
|
|
32
33
|
if (options.revalidate) {
|
|
33
34
|
fetchOptions.next = { revalidate: options.revalidate };
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
+
|
|
36
37
|
const response = await fetch(url, fetchOptions);
|
|
37
38
|
|
|
38
39
|
if (!response.ok) {
|
|
@@ -43,16 +44,19 @@ async function serverFetch<T>(endpoint: string, options: FetchOptions = {}): Pro
|
|
|
43
44
|
return null;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// Rails API returns { data: [...], meta: {...} }
|
|
49
|
-
return json.data ?? json;
|
|
47
|
+
return await response.json();
|
|
50
48
|
} catch (error) {
|
|
51
49
|
serverError('SERVER_API_FETCH_FAILED', error, { endpoint });
|
|
52
50
|
return null;
|
|
53
51
|
}
|
|
54
52
|
}
|
|
55
53
|
|
|
54
|
+
async function serverFetch<T>(endpoint: string, options: FetchOptions = {}): Promise<T | null> {
|
|
55
|
+
const json = await serverFetchJson(endpoint, options);
|
|
56
|
+
if (json === null) return null;
|
|
57
|
+
return (json.data ?? json) as T;
|
|
58
|
+
}
|
|
59
|
+
|
|
56
60
|
/**
|
|
57
61
|
* Generic serverApi object for flexible endpoint access
|
|
58
62
|
*/
|
|
@@ -144,8 +148,40 @@ export async function getFAQs() {
|
|
|
144
148
|
return serverFetch('/public/faq_questions', defaultOptions);
|
|
145
149
|
}
|
|
146
150
|
|
|
151
|
+
// The public blog_posts endpoint is paginated server-side (default per_page=10, max 100).
|
|
152
|
+
// Blog gallery sections paginate client-side over the full set, so fetch every page,
|
|
153
|
+
// using meta.total_count to know when we have everything (the server may clamp per_page).
|
|
154
|
+
const BLOG_POSTS_PER_PAGE = 100;
|
|
155
|
+
const BLOG_POSTS_MAX_PAGES = 50; // safety cap: 5000 posts
|
|
156
|
+
|
|
147
157
|
export async function getBlogPosts() {
|
|
148
|
-
|
|
158
|
+
const first = await serverFetchJson(`/public/blog_posts?page=1&per_page=${BLOG_POSTS_PER_PAGE}`, defaultOptions);
|
|
159
|
+
if (first === null) return null;
|
|
160
|
+
const firstData = first.data ?? first;
|
|
161
|
+
if (!Array.isArray(firstData)) return firstData; // unexpected shape — pass through unchanged
|
|
162
|
+
|
|
163
|
+
const allPosts = [...firstData];
|
|
164
|
+
// total_count lets us compute the remaining page count up front and fetch those
|
|
165
|
+
// pages in parallel (page 1 is already in hand). Falls back to first-page-only when
|
|
166
|
+
// meta is absent (older backends). Uses firstData.length as the effective per_page so
|
|
167
|
+
// a server that clamps below BLOG_POSTS_PER_PAGE still gets every page covered.
|
|
168
|
+
const totalCount = typeof first.meta?.total_count === 'number' ? first.meta.total_count : allPosts.length;
|
|
169
|
+
const effectivePerPage = firstData.length || BLOG_POSTS_PER_PAGE;
|
|
170
|
+
const lastPage = Math.min(Math.ceil(totalCount / effectivePerPage), BLOG_POSTS_MAX_PAGES);
|
|
171
|
+
|
|
172
|
+
if (lastPage > 1) {
|
|
173
|
+
const pageNumbers = Array.from({ length: lastPage - 1 }, (_, i) => i + 2);
|
|
174
|
+
const batches = await Promise.all(
|
|
175
|
+
pageNumbers.map((page) =>
|
|
176
|
+
serverFetch<unknown[]>(`/public/blog_posts?page=${page}&per_page=${BLOG_POSTS_PER_PAGE}`, defaultOptions)
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
// Promise.all preserves order, so posts stay in page order. Skip empty/failed pages.
|
|
180
|
+
for (const batch of batches) {
|
|
181
|
+
if (Array.isArray(batch)) allPosts.push(...batch);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return allPosts;
|
|
149
185
|
}
|
|
150
186
|
|
|
151
187
|
export async function getBlogPost(slug: string) {
|