keystone-design-bootstrap 1.0.49 → 1.0.51
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/{blog-post-D7HFCDp1.d.ts → blog-post-DGjaJ3wf.d.ts} +2 -2
- package/dist/design_system/sections/index.d.ts +7 -25
- package/dist/design_system/sections/index.js +257 -405
- package/dist/design_system/sections/index.js.map +1 -1
- package/dist/{form-BLZuTGkr.d.ts → form-CpsCONG5.d.ts} +16 -2
- package/dist/index.d.ts +4 -5
- package/dist/index.js +275 -423
- package/dist/index.js.map +1 -1
- package/dist/lib/server-api.d.ts +3 -46
- package/dist/lib/server-api.js +0 -9
- package/dist/lib/server-api.js.map +1 -1
- package/dist/types/index.d.ts +4 -4
- package/dist/utils/photo-helpers.d.ts +1 -1
- package/package.json +1 -1
- package/src/design_system/sections/index.tsx +2 -17
- package/src/design_system/sections/service-menu-section.tsx +58 -146
- package/src/lib/server-api.ts +0 -54
- package/src/types/api/offer.ts +13 -0
- package/src/types/api/service.ts +2 -0
- package/src/types/index.ts +1 -0
- package/src/design_system/sections/offer-detail.tsx +0 -46
- package/src/design_system/sections/offers-gallery.tsx +0 -40
- package/src/design_system/sections/offers-grid.tsx +0 -108
- package/src/design_system/sections/offers-section.tsx +0 -90
- package/dist/{photos-8jMeetqV.d.ts → website-photos-Bm-CBK9g.d.ts} +20 -20
package/dist/lib/server-api.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CompanyInformation,
|
|
2
|
-
import {
|
|
1
|
+
import { C as CompanyInformation, F as FormDefinition, S as Service } from '../form-CpsCONG5.js';
|
|
2
|
+
import { W as WebsitePhotos } from '../website-photos-Bm-CBK9g.js';
|
|
3
3
|
|
|
4
4
|
interface FetchOptions {
|
|
5
5
|
cache?: RequestCache;
|
|
@@ -39,48 +39,5 @@ declare function getPackage(slug: string): Promise<unknown>;
|
|
|
39
39
|
declare function getTestimonials(): Promise<unknown>;
|
|
40
40
|
/** Form definition for dynamic form rendering (fields may include optional placeholder). */
|
|
41
41
|
declare function getForm(formType: string): Promise<FormDefinition | null>;
|
|
42
|
-
/** Minimal service for offer detail (from GET /public/offers/:id). */
|
|
43
|
-
interface OfferServiceSummary {
|
|
44
|
-
id: number;
|
|
45
|
-
name: string;
|
|
46
|
-
slug: string;
|
|
47
|
-
summary?: string | null;
|
|
48
|
-
}
|
|
49
|
-
/** Package summary (nested on each offer in list and single-offer responses). */
|
|
50
|
-
interface OfferPackageSummary {
|
|
51
|
-
id: number;
|
|
52
|
-
name: string;
|
|
53
|
-
slug: string;
|
|
54
|
-
summary?: string | null;
|
|
55
|
-
description_markdown?: string | null;
|
|
56
|
-
}
|
|
57
|
-
/** Offer from public API. */
|
|
58
|
-
interface OfferPublic {
|
|
59
|
-
id: number;
|
|
60
|
-
name: string;
|
|
61
|
-
description: string | null;
|
|
62
|
-
value_terms: string | null;
|
|
63
|
-
/** Optional; when absent or null, offer has no expiration. */
|
|
64
|
-
expires_at?: string | null;
|
|
65
|
-
expired?: boolean;
|
|
66
|
-
/** Service IDs (used for category_names fallback when not provided by API). */
|
|
67
|
-
service_ids?: number[];
|
|
68
|
-
/** Package IDs (included in list response; nested services/packages are the source of truth). */
|
|
69
|
-
package_ids?: number[];
|
|
70
|
-
/** Category names from API (services + service_items + packages). */
|
|
71
|
-
category_names?: string[];
|
|
72
|
-
/** Photo attachments from related services (same shape as Service.photo_attachments). */
|
|
73
|
-
photo_attachments?: PhotoAttachment[];
|
|
74
|
-
/** Present when fetching single offer (GET /public/offers/:id). */
|
|
75
|
-
services?: OfferServiceSummary[];
|
|
76
|
-
/** Specific menu items this offer applies to (single offer only). */
|
|
77
|
-
service_items?: OfferServiceSummary[];
|
|
78
|
-
/** Packages this offer applies to (single offer only). */
|
|
79
|
-
packages?: OfferPackageSummary[];
|
|
80
|
-
}
|
|
81
|
-
/** List offers for the current account (API key scoped). */
|
|
82
|
-
declare function getOffers(): Promise<OfferPublic[] | null>;
|
|
83
|
-
/** Single offer with services for detail page. */
|
|
84
|
-
declare function getOffer(id: number | string): Promise<OfferPublic | null>;
|
|
85
42
|
|
|
86
|
-
export {
|
|
43
|
+
export { getAdsConfig, getBlogPost, getBlogPosts, getCompanyInformation, getFAQs, getForm, getJobPosting, getJobPostings, getLocation, getLocations, getMetaPixelId, getPackage, getPackages, getReviews, getService, getServices, getSocialPosts, getTeamMembers, getTestimonials, getWebsitePhotos, serverApi };
|
package/dist/lib/server-api.js
CHANGED
|
@@ -96,13 +96,6 @@ async function getTestimonials() {
|
|
|
96
96
|
async function getForm(formType) {
|
|
97
97
|
return serverFetch(`/public/forms/${encodeURIComponent(formType)}`, defaultOptions);
|
|
98
98
|
}
|
|
99
|
-
async function getOffers() {
|
|
100
|
-
const data = await serverFetch("/public/offers", defaultOptions);
|
|
101
|
-
return Array.isArray(data) ? data : null;
|
|
102
|
-
}
|
|
103
|
-
async function getOffer(id) {
|
|
104
|
-
return serverFetch(`/public/offers/${id}`, defaultOptions);
|
|
105
|
-
}
|
|
106
99
|
export {
|
|
107
100
|
getAdsConfig,
|
|
108
101
|
getBlogPost,
|
|
@@ -115,8 +108,6 @@ export {
|
|
|
115
108
|
getLocation,
|
|
116
109
|
getLocations,
|
|
117
110
|
getMetaPixelId,
|
|
118
|
-
getOffer,
|
|
119
|
-
getOffers,
|
|
120
111
|
getPackage,
|
|
121
112
|
getPackages,
|
|
122
113
|
getReviews,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/server-api.ts"],"sourcesContent":["/**\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 {
|
|
1
|
+
{"version":3,"sources":["../../src/lib/server-api.ts"],"sourcesContent":["/**\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 { WebsitePhotos } from '../types/api/website-photos';\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 console.error(`[Server API] Error ${response.status} for ${endpoint}`);\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 console.error(`[Server API] Failed to fetch ${endpoint}:`, error);\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 async function getServices() {\n return serverFetch('/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() {\n return serverFetch('/public/packages', defaultOptions);\n}\n\nexport async function getPackage(slug: string) {\n return serverFetch(`/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 (fields may include optional placeholder). */\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"],"mappings":"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","names":[]}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Theme } from '../themes/index.js';
|
|
2
|
-
export { B as BlogPost,
|
|
3
|
-
export { C as CompanyInformation, a as CompanyInformationResponse, F as FormDefinition, b as FormFieldDefinition, c as FormFieldItem, d as FormType, S as Service, e as ServiceItem, f as ServiceParams, g as ServiceResponse } from '../form-
|
|
4
|
-
import { P as PhotoAttachment } from '../photos-
|
|
5
|
-
export { a as Photo, b as WebsitePhoto, W as WebsitePhotos, c as WebsitePhotosResponse } from '../photos-
|
|
2
|
+
export { B as BlogPost, a as BlogPostAuthor, b as BlogPostParams, c as BlogPostResponse, d as BlogPostTag } from '../blog-post-DGjaJ3wf.js';
|
|
3
|
+
export { C as CompanyInformation, a as CompanyInformationResponse, F as FormDefinition, b as FormFieldDefinition, c as FormFieldItem, d as FormType, O as OfferPublic, S as Service, e as ServiceItem, f as ServiceParams, g as ServiceResponse } from '../form-CpsCONG5.js';
|
|
4
|
+
import { P as PhotoAttachment } from '../website-photos-Bm-CBK9g.js';
|
|
5
|
+
export { a as Photo, b as WebsitePhoto, W as WebsitePhotos, c as WebsitePhotosResponse } from '../website-photos-Bm-CBK9g.js';
|
|
6
6
|
|
|
7
7
|
interface NavItem {
|
|
8
8
|
label: string;
|
package/package.json
CHANGED
|
@@ -46,9 +46,6 @@ import { HomeHeroComponent as BaseHomeHeroComponent } from './home-hero-componen
|
|
|
46
46
|
import { JobDetailHero as BaseJobDetailHero } from './hero-job-detail';
|
|
47
47
|
import { JobDetailSection as BaseJobDetailSection } from './job-detail-section';
|
|
48
48
|
import { PolicyDocumentSection as BasePolicyDocumentSection } from './policy-document-section';
|
|
49
|
-
import { OffersSection as BaseOffersSection } from './offers-section';
|
|
50
|
-
import { OffersGallery as BaseOffersGallery } from './offers-gallery';
|
|
51
|
-
import { OfferDetailSection as BaseOfferDetailSection } from './offer-detail';
|
|
52
49
|
// Import variant files to trigger their registration
|
|
53
50
|
// Aman theme variants
|
|
54
51
|
import './hero-home.aman';
|
|
@@ -94,7 +91,7 @@ import './social-media-grid.barelux';
|
|
|
94
91
|
import './contact-section.barelux';
|
|
95
92
|
import './footer-home.barelux';
|
|
96
93
|
|
|
97
|
-
// Balance theme variants
|
|
94
|
+
// Balance theme variants
|
|
98
95
|
import './hero-home.balance';
|
|
99
96
|
import './header-navigation.balance';
|
|
100
97
|
import './footer-home.balance';
|
|
@@ -181,19 +178,7 @@ export const HomeHeroComponent = createThemedExport('home-hero-component', BaseH
|
|
|
181
178
|
// Re-export application form (client component, no theme variants)
|
|
182
179
|
export { JobApplicationForm } from './job-application-form';
|
|
183
180
|
|
|
184
|
-
//
|
|
185
|
-
export const OffersSection = createThemedExport('offers-section', BaseOffersSection as unknown as React.ComponentType<Record<string, unknown>>);
|
|
186
|
-
|
|
187
|
-
// Full offers page gallery (themed; use on /offers page, Aman = blog-style)
|
|
188
|
-
export const OffersGallery = createThemedExport('offers-gallery', BaseOffersGallery as unknown as React.ComponentType<Record<string, unknown>>);
|
|
189
|
-
|
|
190
|
-
// Single offer detail (themed; use on /offers/[id] page)
|
|
191
|
-
export const OfferDetailSection = createThemedExport('offer-detail', BaseOfferDetailSection as unknown as React.ComponentType<Record<string, unknown>>);
|
|
192
|
-
|
|
193
|
-
// Full offers page grid (no theme variant; fallback when gallery not used)
|
|
194
|
-
export { OffersGrid } from './offers-grid';
|
|
195
|
-
|
|
196
|
-
// Service Menu: up to 3 carousel rows (Offers, Packages, Services) with smaller cards
|
|
181
|
+
// Service Menu: packages + treatments; nested specials as badges / modal callouts
|
|
197
182
|
export { ServiceMenuSection } from './service-menu-section';
|
|
198
183
|
export type { ServiceMenuSectionProps, PackagePublic } from './service-menu-section';
|
|
199
184
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { PhotoWithFallback, Carousel, Modal, MarkdownRenderer, Button } from '../elements';
|
|
6
|
-
import type { OfferPublic } from '../../
|
|
6
|
+
import type { OfferPublic } from '../../types/api/offer';
|
|
7
7
|
import type { Service, ServiceItem } from '../../types/api/service';
|
|
8
8
|
import type { WebsitePhotos } from '../../types/api/website-photos';
|
|
9
9
|
import type { CompanyInformation } from '../../types/api/company-information';
|
|
@@ -34,6 +34,7 @@ export interface PackagePublic {
|
|
|
34
34
|
price_cents?: number | null;
|
|
35
35
|
photo_attachments?: PhotoAttachment[];
|
|
36
36
|
package_items?: Array<{ quantity: number; service_item?: { id: number; name: string; slug: string; summary?: string | null } }>;
|
|
37
|
+
offers?: OfferPublic[];
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/** Package with category names and first-service description fallback (derived from services). */
|
|
@@ -42,16 +43,9 @@ export interface PackageForMenu extends PackagePublic {
|
|
|
42
43
|
first_service_description_markdown?: string | null;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
/** Offer with category names and first-service description fallback (derived from services). */
|
|
46
|
-
export interface OfferForMenu extends OfferPublic {
|
|
47
|
-
category_names?: string[];
|
|
48
|
-
first_service_description_markdown?: string | null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
46
|
export interface ServiceMenuSectionProps {
|
|
52
47
|
title?: string;
|
|
53
48
|
subtitle?: string;
|
|
54
|
-
offers?: OfferPublic[] | null;
|
|
55
49
|
packages?: PackagePublic[] | null;
|
|
56
50
|
/** Services (used to derive service items for the third row). */
|
|
57
51
|
services?: Service[] | null;
|
|
@@ -74,10 +68,14 @@ export interface ServiceItemWithService extends ServiceItem {
|
|
|
74
68
|
}
|
|
75
69
|
|
|
76
70
|
type DetailItem =
|
|
77
|
-
| { type: 'offer'; item: OfferForMenu }
|
|
78
71
|
| { type: 'package'; item: PackageForMenu }
|
|
79
72
|
| { type: 'service_item'; item: ServiceItemWithService };
|
|
80
73
|
|
|
74
|
+
function getActivePublicOffers(offers: OfferPublic[] | undefined | null): OfferPublic[] {
|
|
75
|
+
if (!Array.isArray(offers) || offers.length === 0) return [];
|
|
76
|
+
return offers.filter((o) => o.active !== false && o.expired !== true);
|
|
77
|
+
}
|
|
78
|
+
|
|
81
79
|
function photoAttachmentDisplayUrl(pa: PhotoAttachment): string | undefined {
|
|
82
80
|
return pa.photo?.large_url || pa.photo?.medium_url;
|
|
83
81
|
}
|
|
@@ -138,6 +136,7 @@ function GridCardWithImage({
|
|
|
138
136
|
websitePhotos,
|
|
139
137
|
companyInformation,
|
|
140
138
|
cycleSeed,
|
|
139
|
+
hasSpecial,
|
|
141
140
|
}: {
|
|
142
141
|
photoAttachments?: PhotoAttachment[];
|
|
143
142
|
fallbackId: string | number;
|
|
@@ -149,6 +148,8 @@ function GridCardWithImage({
|
|
|
149
148
|
websitePhotos?: WebsitePhotos | null;
|
|
150
149
|
companyInformation?: CompanyInformation | null;
|
|
151
150
|
cycleSeed?: string;
|
|
151
|
+
/** Show a small “Special” badge when this package or treatment has active offers. */
|
|
152
|
+
hasSpecial?: boolean;
|
|
152
153
|
}) {
|
|
153
154
|
const seed = cycleSeed ?? String(fallbackId);
|
|
154
155
|
const list = useCycledPhotoList(photoAttachments, seed);
|
|
@@ -192,6 +193,14 @@ function GridCardWithImage({
|
|
|
192
193
|
className="flex flex-col h-full w-full text-left group block rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-brand-accent focus-visible:ring-offset-2"
|
|
193
194
|
>
|
|
194
195
|
<div className="w-full h-36 overflow-hidden rounded-lg mb-3 relative">
|
|
196
|
+
{hasSpecial ? (
|
|
197
|
+
<span
|
|
198
|
+
className="absolute top-2 right-2 z-10 rounded-full border border-brand-accent/40 bg-secondary/95 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-brand-accent shadow-sm backdrop-blur-sm"
|
|
199
|
+
aria-label="Has special offer"
|
|
200
|
+
>
|
|
201
|
+
Special
|
|
202
|
+
</span>
|
|
203
|
+
) : null}
|
|
195
204
|
{list.length === 0 ? (
|
|
196
205
|
<PhotoWithFallback
|
|
197
206
|
item={undefined}
|
|
@@ -350,7 +359,37 @@ function ModalSection({ title, children }: { title: string; children: React.Reac
|
|
|
350
359
|
);
|
|
351
360
|
}
|
|
352
361
|
|
|
353
|
-
|
|
362
|
+
function ActiveOffersCallout({ offers }: { offers: OfferPublic[] }) {
|
|
363
|
+
if (!offers.length) return null;
|
|
364
|
+
return (
|
|
365
|
+
<div className="rounded-xl border border-secondary bg-secondary/25 p-4 md:p-5 space-y-3 ring-1 ring-brand-accent/25">
|
|
366
|
+
<p className="text-xs font-semibold uppercase tracking-wide text-brand-accent">Current specials</p>
|
|
367
|
+
<ul className="space-y-3 list-none m-0 p-0">
|
|
368
|
+
{offers.map((o) => (
|
|
369
|
+
<li
|
|
370
|
+
key={o.id}
|
|
371
|
+
className="rounded-lg border border-secondary border-l-[3px] border-l-brand-accent bg-primary_hover/30 p-4"
|
|
372
|
+
>
|
|
373
|
+
<p className="font-display text-base font-medium text-fg-primary">{o.name}</p>
|
|
374
|
+
{o.value_terms ? (
|
|
375
|
+
<p className="font-body text-sm text-brand-accent mt-1 font-medium">{o.value_terms}</p>
|
|
376
|
+
) : null}
|
|
377
|
+
{o.description ? (
|
|
378
|
+
<p className="font-body text-sm text-fg-secondary mt-2 leading-relaxed">{o.description}</p>
|
|
379
|
+
) : null}
|
|
380
|
+
{o.expires_at ? (
|
|
381
|
+
<p className="text-xs text-tertiary mt-2">
|
|
382
|
+
Ends {new Date(o.expires_at).toLocaleDateString(undefined, { dateStyle: 'medium' })}
|
|
383
|
+
</p>
|
|
384
|
+
) : null}
|
|
385
|
+
</li>
|
|
386
|
+
))}
|
|
387
|
+
</ul>
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Modal body for package or service item (specials shown inline when present). */
|
|
354
393
|
function DetailModalContent({
|
|
355
394
|
detail,
|
|
356
395
|
serviceItems = [],
|
|
@@ -360,90 +399,18 @@ function DetailModalContent({
|
|
|
360
399
|
packages?: PackageForMenu[];
|
|
361
400
|
serviceItems?: ServiceItemWithService[];
|
|
362
401
|
}) {
|
|
363
|
-
if (detail.type === 'offer') {
|
|
364
|
-
const o = detail.item;
|
|
365
|
-
const categoryLine = o.category_names?.length ? o.category_names.join(' | ') : null;
|
|
366
|
-
const descriptionContent = o.description || o.first_service_description_markdown;
|
|
367
|
-
// Backend includes nested services, service_items, and packages on every offer (list and single).
|
|
368
|
-
const relatedServices = o.services?.length ? o.services : (o.service_items ?? []);
|
|
369
|
-
const relatedPackages = o.packages ?? [];
|
|
370
|
-
|
|
371
|
-
return (
|
|
372
|
-
<div className="space-y-6">
|
|
373
|
-
{categoryLine && (
|
|
374
|
-
<p className="text-sm font-medium text-fg-secondary uppercase tracking-wide">{categoryLine}</p>
|
|
375
|
-
)}
|
|
376
|
-
{o.value_terms && !categoryLine && (
|
|
377
|
-
<p className="text-sm font-medium text-fg-secondary uppercase tracking-wide">{o.value_terms}</p>
|
|
378
|
-
)}
|
|
379
|
-
{descriptionContent && (
|
|
380
|
-
<ModalSection title="Offer details">
|
|
381
|
-
<div className="prose prose-sm font-body text-fg-primary max-w-none">
|
|
382
|
-
{o.description ? <p className="text-fg-primary">{o.description}</p> : <MarkdownRenderer content={o.first_service_description_markdown!} />}
|
|
383
|
-
</div>
|
|
384
|
-
</ModalSection>
|
|
385
|
-
)}
|
|
386
|
-
{o.expires_at && (() => {
|
|
387
|
-
const d = new Date(o.expires_at);
|
|
388
|
-
if (Number.isNaN(d.getTime())) return null;
|
|
389
|
-
return (
|
|
390
|
-
<p className="text-sm text-tertiary">
|
|
391
|
-
Expires {d.toLocaleDateString(undefined, { dateStyle: 'medium' })}
|
|
392
|
-
</p>
|
|
393
|
-
);
|
|
394
|
-
})()}
|
|
395
|
-
|
|
396
|
-
{relatedServices.length > 0 && (
|
|
397
|
-
<ModalSection title="Related services">
|
|
398
|
-
<ul className="space-y-4">
|
|
399
|
-
{relatedServices.map((s: Service | { id: number; name: string; summary?: string | null }) => (
|
|
400
|
-
<li key={s.id} className="border border-secondary rounded-lg p-4 bg-secondary/20">
|
|
401
|
-
<p className="font-display font-medium text-fg-primary">{s.name}</p>
|
|
402
|
-
{'description_markdown' in s && s.description_markdown ? (
|
|
403
|
-
<div className="mt-2 prose prose-sm font-body text-fg-secondary max-w-none">
|
|
404
|
-
<MarkdownRenderer content={s.description_markdown} />
|
|
405
|
-
</div>
|
|
406
|
-
) : s.summary ? (
|
|
407
|
-
<p className="mt-2 font-body text-sm text-fg-secondary">{s.summary}</p>
|
|
408
|
-
) : null}
|
|
409
|
-
</li>
|
|
410
|
-
))}
|
|
411
|
-
</ul>
|
|
412
|
-
</ModalSection>
|
|
413
|
-
)}
|
|
414
|
-
|
|
415
|
-
{relatedPackages.length > 0 && (
|
|
416
|
-
<ModalSection title="Related packages">
|
|
417
|
-
<ul className="space-y-4">
|
|
418
|
-
{relatedPackages.map((pkg) => (
|
|
419
|
-
<li key={pkg.id} className="border border-secondary rounded-lg p-4 bg-secondary/20">
|
|
420
|
-
<p className="font-display font-medium text-fg-primary">{pkg.name}</p>
|
|
421
|
-
{'description_markdown' in pkg && pkg.description_markdown ? (
|
|
422
|
-
<div className="mt-2 prose prose-sm font-body text-fg-secondary max-w-none">
|
|
423
|
-
<MarkdownRenderer content={pkg.description_markdown} />
|
|
424
|
-
</div>
|
|
425
|
-
) : pkg.summary ? (
|
|
426
|
-
<p className="mt-2 font-body text-sm text-fg-secondary">{pkg.summary}</p>
|
|
427
|
-
) : null}
|
|
428
|
-
</li>
|
|
429
|
-
))}
|
|
430
|
-
</ul>
|
|
431
|
-
</ModalSection>
|
|
432
|
-
)}
|
|
433
|
-
</div>
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
402
|
if (detail.type === 'package') {
|
|
438
403
|
const p = detail.item;
|
|
439
404
|
const categoryLine = p.category_names?.length ? p.category_names.join(' | ') : null;
|
|
440
405
|
const descriptionMarkdown = p.description_markdown || p.summary || p.first_service_description_markdown;
|
|
406
|
+
const packageSpecials = getActivePublicOffers(p.offers);
|
|
441
407
|
|
|
442
408
|
return (
|
|
443
409
|
<div className="space-y-6">
|
|
444
410
|
{categoryLine && (
|
|
445
411
|
<p className="text-sm font-medium text-fg-secondary uppercase tracking-wide">{categoryLine}</p>
|
|
446
412
|
)}
|
|
413
|
+
<ActiveOffersCallout offers={packageSpecials} />
|
|
447
414
|
{descriptionMarkdown && (
|
|
448
415
|
<ModalSection title="Package details">
|
|
449
416
|
<div className="prose prose-sm font-body text-fg-primary max-w-none">
|
|
@@ -497,12 +464,14 @@ function DetailModalContent({
|
|
|
497
464
|
const priceStr = formatPriceCents(si.price_cents);
|
|
498
465
|
const summary = si.summary || si.service_summary;
|
|
499
466
|
const descriptionMarkdown = si.description_markdown || si.service_description_markdown;
|
|
467
|
+
const itemSpecials = getActivePublicOffers(si.offers);
|
|
500
468
|
|
|
501
469
|
return (
|
|
502
470
|
<div className="space-y-6">
|
|
503
471
|
{si.service_name && (
|
|
504
472
|
<p className="text-sm font-medium text-fg-secondary uppercase tracking-wide">{si.service_name}</p>
|
|
505
473
|
)}
|
|
474
|
+
<ActiveOffersCallout offers={itemSpecials} />
|
|
506
475
|
|
|
507
476
|
{priceStr && (
|
|
508
477
|
<ModalSection title="Price">
|
|
@@ -541,14 +510,11 @@ function DetailModalContent({
|
|
|
541
510
|
}
|
|
542
511
|
|
|
543
512
|
/**
|
|
544
|
-
* Service Menu
|
|
545
|
-
* Service items are derived from services (each service’s service_items with service name).
|
|
546
|
-
* Cards cycle through photos; clicking opens a detail modal.
|
|
513
|
+
* Service Menu: packages and treatments (service items). Specials appear as card badges and in modals.
|
|
547
514
|
*/
|
|
548
515
|
export function ServiceMenuSection({
|
|
549
516
|
title = 'Service Menu',
|
|
550
517
|
subtitle,
|
|
551
|
-
offers = null,
|
|
552
518
|
packages = null,
|
|
553
519
|
services = null,
|
|
554
520
|
websitePhotos,
|
|
@@ -558,10 +524,6 @@ export function ServiceMenuSection({
|
|
|
558
524
|
servicesRowTitle = 'Treatments',
|
|
559
525
|
variant = 'section',
|
|
560
526
|
}: ServiceMenuSectionProps) {
|
|
561
|
-
const offerList = React.useMemo(
|
|
562
|
-
() => (Array.isArray(offers) ? offers.filter((o) => !o.expired) : []),
|
|
563
|
-
[offers]
|
|
564
|
-
);
|
|
565
527
|
const packageList = React.useMemo(
|
|
566
528
|
() => (Array.isArray(packages) ? packages : []),
|
|
567
529
|
[packages]
|
|
@@ -596,23 +558,6 @@ export function ServiceMenuSection({
|
|
|
596
558
|
[packageList, serviceItemIdToService]
|
|
597
559
|
);
|
|
598
560
|
|
|
599
|
-
const offersForMenu: OfferForMenu[] = React.useMemo(
|
|
600
|
-
() =>
|
|
601
|
-
offerList.map((offer) => {
|
|
602
|
-
const category_names =
|
|
603
|
-
(offer.category_names?.length ? offer.category_names : null) ??
|
|
604
|
-
((offer.service_ids || [])
|
|
605
|
-
.map((id) => serviceList.find((s) => s.id === id)?.name)
|
|
606
|
-
.filter(Boolean) as string[]);
|
|
607
|
-
const firstServiceId = offer.service_ids?.[0];
|
|
608
|
-
const firstService = firstServiceId ? serviceList.find((s) => s.id === firstServiceId) : undefined;
|
|
609
|
-
const first_service_description_markdown =
|
|
610
|
-
firstService?.description_markdown || firstService?.summary || null;
|
|
611
|
-
return { ...offer, category_names: category_names ?? [], first_service_description_markdown };
|
|
612
|
-
}),
|
|
613
|
-
[offerList, serviceList]
|
|
614
|
-
);
|
|
615
|
-
|
|
616
561
|
const serviceItemsForMenu: ServiceItemWithService[] = serviceList.flatMap((s) =>
|
|
617
562
|
(s.service_items || []).map((si) => ({
|
|
618
563
|
...si,
|
|
@@ -621,12 +566,11 @@ export function ServiceMenuSection({
|
|
|
621
566
|
service_description_markdown: s.description_markdown,
|
|
622
567
|
}))
|
|
623
568
|
);
|
|
624
|
-
const hasAny =
|
|
569
|
+
const hasAny = packagesForMenu.length > 0 || serviceItemsForMenu.length > 0;
|
|
625
570
|
|
|
626
571
|
const CAROUSEL_MIN = 3;
|
|
627
572
|
// Page variant: always grid (full list, no carousel). Section variant: carousel when >=3 items else grid.
|
|
628
573
|
const isPage = variant === 'page';
|
|
629
|
-
const offersLayout: 'carousel' | 'grid' = isPage ? 'grid' : (offersForMenu.length >= CAROUSEL_MIN ? 'carousel' : 'grid');
|
|
630
574
|
const packagesLayout: 'carousel' | 'grid' = isPage ? 'grid' : (packagesForMenu.length >= CAROUSEL_MIN ? 'carousel' : 'grid');
|
|
631
575
|
const serviceItemsLayout: 'carousel' | 'grid' = isPage ? 'grid' : (serviceItemsForMenu.length >= CAROUSEL_MIN ? 'carousel' : 'grid');
|
|
632
576
|
|
|
@@ -648,40 +592,13 @@ export function ServiceMenuSection({
|
|
|
648
592
|
|
|
649
593
|
const closeModal = useCallback(() => setDetailItem(null), []);
|
|
650
594
|
|
|
651
|
-
const renderOfferCard = useCallback(
|
|
652
|
-
(offer: OfferForMenu, index: number) => {
|
|
653
|
-
const categorySubtitle =
|
|
654
|
-
offer.category_names?.length ? offer.category_names.join(' | ') : (offer.value_terms ?? null);
|
|
655
|
-
const cardDesc = offer.description || offer.first_service_description_markdown;
|
|
656
|
-
const plainDesc = cardDesc ? (typeof cardDesc === 'string' ? cardDesc : '').replace(/[#*`\[\]]/g, '').replace(/\n+/g, ' ').trim() : '';
|
|
657
|
-
const descText = plainDesc.length > 120 ? plainDesc.slice(0, 120) + '…' : plainDesc;
|
|
658
|
-
return (
|
|
659
|
-
<GridCardWithImage
|
|
660
|
-
photoAttachments={offer.photo_attachments}
|
|
661
|
-
fallbackId={offer.id}
|
|
662
|
-
fallbackAlt={offer.name}
|
|
663
|
-
title={offer.name}
|
|
664
|
-
subtitle={categorySubtitle}
|
|
665
|
-
onClick={() => setDetailItem({ type: 'offer', item: offer })}
|
|
666
|
-
websitePhotos={websitePhotos}
|
|
667
|
-
companyInformation={companyInformation}
|
|
668
|
-
cycleSeed={`offer-${offer.id}-${index}`}
|
|
669
|
-
>
|
|
670
|
-
{descText && (
|
|
671
|
-
<p className="font-body text-sm text-tertiary mt-1 line-clamp-2">{descText}</p>
|
|
672
|
-
)}
|
|
673
|
-
</GridCardWithImage>
|
|
674
|
-
);
|
|
675
|
-
},
|
|
676
|
-
[websitePhotos, companyInformation]
|
|
677
|
-
);
|
|
678
|
-
|
|
679
595
|
const renderPackageCard = useCallback(
|
|
680
596
|
(pkg: PackageForMenu, index: number) => {
|
|
681
597
|
const categorySubtitle = pkg.category_names?.length ? pkg.category_names.join(' | ') : null;
|
|
682
598
|
const cardDesc = pkg.description_markdown || pkg.summary || pkg.first_service_description_markdown;
|
|
683
599
|
const plainDesc = cardDesc ? cardDesc.replace(/[#*`\[\]]/g, '').replace(/\n+/g, ' ').trim() : '';
|
|
684
600
|
const descText = plainDesc.length > 120 ? plainDesc.slice(0, 120) + '…' : plainDesc;
|
|
601
|
+
const hasSpecial = getActivePublicOffers(pkg.offers).length > 0;
|
|
685
602
|
return (
|
|
686
603
|
<GridCardWithImage
|
|
687
604
|
photoAttachments={pkg.photo_attachments}
|
|
@@ -693,6 +610,7 @@ export function ServiceMenuSection({
|
|
|
693
610
|
websitePhotos={websitePhotos}
|
|
694
611
|
companyInformation={companyInformation}
|
|
695
612
|
cycleSeed={`pkg-${pkg.id}-${index}`}
|
|
613
|
+
hasSpecial={hasSpecial}
|
|
696
614
|
>
|
|
697
615
|
{descText && (
|
|
698
616
|
<p className="font-body text-sm text-tertiary mt-1 line-clamp-2">{descText}</p>
|
|
@@ -715,6 +633,7 @@ export function ServiceMenuSection({
|
|
|
715
633
|
websitePhotos={websitePhotos}
|
|
716
634
|
companyInformation={companyInformation}
|
|
717
635
|
cycleSeed={`service-item-${si.id}-${index}`}
|
|
636
|
+
hasSpecial={getActivePublicOffers(si.offers).length > 0}
|
|
718
637
|
>
|
|
719
638
|
{formatPriceCents(si.price_cents) && (
|
|
720
639
|
<p className="font-body text-sm font-medium text-fg-primary mt-1">
|
|
@@ -760,13 +679,6 @@ export function ServiceMenuSection({
|
|
|
760
679
|
</div>
|
|
761
680
|
)}
|
|
762
681
|
|
|
763
|
-
<MenuBlock
|
|
764
|
-
rowTitle="Offers"
|
|
765
|
-
items={offersForMenu}
|
|
766
|
-
layout={offersLayout}
|
|
767
|
-
renderItem={renderOfferCard}
|
|
768
|
-
/>
|
|
769
|
-
|
|
770
682
|
<MenuBlock
|
|
771
683
|
rowTitle="Packages"
|
|
772
684
|
items={packagesForMenu}
|
package/src/lib/server-api.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { CompanyInformation } from '../types/api/company-information';
|
|
7
|
-
import type { PhotoAttachment } from '../types/api/photos';
|
|
8
7
|
import type { Service } from '../types/api/service';
|
|
9
8
|
import type { WebsitePhotos } from '../types/api/website-photos';
|
|
10
9
|
|
|
@@ -150,56 +149,3 @@ export async function getForm(formType: string) {
|
|
|
150
149
|
return serverFetch<FormDefinition>(`/public/forms/${encodeURIComponent(formType)}`, defaultOptions);
|
|
151
150
|
}
|
|
152
151
|
|
|
153
|
-
/** Minimal service for offer detail (from GET /public/offers/:id). */
|
|
154
|
-
export interface OfferServiceSummary {
|
|
155
|
-
id: number;
|
|
156
|
-
name: string;
|
|
157
|
-
slug: string;
|
|
158
|
-
summary?: string | null;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Package summary (nested on each offer in list and single-offer responses). */
|
|
162
|
-
export interface OfferPackageSummary {
|
|
163
|
-
id: number;
|
|
164
|
-
name: string;
|
|
165
|
-
slug: string;
|
|
166
|
-
summary?: string | null;
|
|
167
|
-
description_markdown?: string | null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Offer from public API. */
|
|
171
|
-
export interface OfferPublic {
|
|
172
|
-
id: number;
|
|
173
|
-
name: string;
|
|
174
|
-
description: string | null;
|
|
175
|
-
value_terms: string | null;
|
|
176
|
-
/** Optional; when absent or null, offer has no expiration. */
|
|
177
|
-
expires_at?: string | null;
|
|
178
|
-
expired?: boolean;
|
|
179
|
-
/** Service IDs (used for category_names fallback when not provided by API). */
|
|
180
|
-
service_ids?: number[];
|
|
181
|
-
/** Package IDs (included in list response; nested services/packages are the source of truth). */
|
|
182
|
-
package_ids?: number[];
|
|
183
|
-
/** Category names from API (services + service_items + packages). */
|
|
184
|
-
category_names?: string[];
|
|
185
|
-
/** Photo attachments from related services (same shape as Service.photo_attachments). */
|
|
186
|
-
photo_attachments?: PhotoAttachment[];
|
|
187
|
-
/** Present when fetching single offer (GET /public/offers/:id). */
|
|
188
|
-
services?: OfferServiceSummary[];
|
|
189
|
-
/** Specific menu items this offer applies to (single offer only). */
|
|
190
|
-
service_items?: OfferServiceSummary[];
|
|
191
|
-
/** Packages this offer applies to (single offer only). */
|
|
192
|
-
packages?: OfferPackageSummary[];
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** List offers for the current account (API key scoped). */
|
|
196
|
-
export async function getOffers(): Promise<OfferPublic[] | null> {
|
|
197
|
-
const data = await serverFetch<OfferPublic[]>('/public/offers', defaultOptions);
|
|
198
|
-
return Array.isArray(data) ? data : null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** Single offer with services for detail page. */
|
|
202
|
-
export async function getOffer(id: number | string): Promise<OfferPublic | null> {
|
|
203
|
-
return serverFetch<OfferPublic>(`/public/offers/${id}`, defaultOptions);
|
|
204
|
-
}
|
|
205
|
-
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PhotoAttachment } from './photos';
|
|
2
|
+
|
|
3
|
+
/** Nested under `service_items[].offers` and `packages[].offers` in public API. */
|
|
4
|
+
export 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
|
+
}
|
package/src/types/api/service.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface ServiceItem {
|
|
|
46
46
|
id: number;
|
|
47
47
|
photo?: { thumbnail_url?: string; medium_url?: string; large_url?: string; original_url?: string; title?: string; alt_text?: string };
|
|
48
48
|
}>;
|
|
49
|
+
/** Active offers that apply to this menu item (public API). */
|
|
50
|
+
offers?: import('./offer').OfferPublic[];
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export interface ServiceParams {
|
package/src/types/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './api/form';
|
|
|
14
14
|
export * from './api/faq';
|
|
15
15
|
export * from './api/job-posting';
|
|
16
16
|
export * from './api/location';
|
|
17
|
+
export * from './api/offer';
|
|
17
18
|
export * from './api/photos';
|
|
18
19
|
export * from './api/service';
|
|
19
20
|
export * from './api/social-post';
|