keystone-design-bootstrap 1.0.50 → 1.0.53
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/email-signup-section.tsx +115 -0
- package/src/design_system/sections/header-navigation.aman.tsx +8 -3
- package/src/design_system/sections/header-navigation.balance.tsx +2 -0
- package/src/design_system/sections/header-navigation.barelux.tsx +4 -1
- package/src/design_system/sections/index.tsx +5 -16
- package/src/design_system/sections/service-menu-section.tsx +58 -146
- package/src/lib/server-api.ts +0 -54
- package/src/next/contexts/form-definitions.tsx +5 -2
- package/src/next/layouts/root-layout.tsx +3 -0
- package/src/types/api/form.ts +1 -1
- 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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useRef, useState } from 'react';
|
|
4
|
+
import { Form, Button } from '../elements';
|
|
5
|
+
import { DynamicFormFields } from '../components/DynamicFormFields';
|
|
6
|
+
import type { FormDefinition } from '../../types/api/form';
|
|
7
|
+
import { useFormDefinitions } from '../../next/contexts/form-definitions';
|
|
8
|
+
|
|
9
|
+
export interface EmailSignupSectionProps {
|
|
10
|
+
title?: string;
|
|
11
|
+
subtitle?: string;
|
|
12
|
+
buttonText?: string;
|
|
13
|
+
successMessage?: string;
|
|
14
|
+
/** Override the form definition (falls back to context). */
|
|
15
|
+
formDefinition?: FormDefinition | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const EmailSignupSection = ({
|
|
19
|
+
title = "Stay in the loop",
|
|
20
|
+
subtitle = "Subscribe to our newsletter for updates, tips, and exclusive offers.",
|
|
21
|
+
buttonText = "Subscribe",
|
|
22
|
+
successMessage = "You're subscribed! Thank you.",
|
|
23
|
+
formDefinition,
|
|
24
|
+
}: EmailSignupSectionProps) => {
|
|
25
|
+
const { marketingListSignupFormDefinition } = useFormDefinitions();
|
|
26
|
+
const resolvedFormDefinition = formDefinition ?? marketingListSignupFormDefinition;
|
|
27
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
28
|
+
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
|
29
|
+
const [statusMessage, setStatusMessage] = useState('');
|
|
30
|
+
const formRef = useRef<HTMLFormElement>(null);
|
|
31
|
+
|
|
32
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
setIsSubmitting(true);
|
|
35
|
+
setSubmitStatus('idle');
|
|
36
|
+
setStatusMessage('');
|
|
37
|
+
|
|
38
|
+
const formData = new FormData(e.currentTarget);
|
|
39
|
+
const data: Record<string, string> = { formType: 'marketing_list_signup' };
|
|
40
|
+
formData.forEach((value, key) => {
|
|
41
|
+
if (key.endsWith('_prefix')) return;
|
|
42
|
+
if (typeof value === 'string') data[key] = value;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch('/api/form/', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify(data),
|
|
50
|
+
});
|
|
51
|
+
const result = await response.json();
|
|
52
|
+
|
|
53
|
+
if (result.success) {
|
|
54
|
+
setSubmitStatus('success');
|
|
55
|
+
setStatusMessage(result.message || successMessage);
|
|
56
|
+
formRef.current?.reset();
|
|
57
|
+
setTimeout(() => setSubmitStatus('idle'), 6000);
|
|
58
|
+
} else {
|
|
59
|
+
setSubmitStatus('error');
|
|
60
|
+
setStatusMessage(result.error || 'Something went wrong. Please try again.');
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
setSubmitStatus('error');
|
|
64
|
+
setStatusMessage('Network error. Please try again later.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setIsSubmitting(false);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (!resolvedFormDefinition) return null;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<section className="bg-secondary py-16 md:py-20">
|
|
74
|
+
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
75
|
+
<div className="mx-auto max-w-xl text-center">
|
|
76
|
+
<h2 className="font-display text-display-sm font-semibold text-primary md:text-display-md">
|
|
77
|
+
{title}
|
|
78
|
+
</h2>
|
|
79
|
+
{subtitle && (
|
|
80
|
+
<p className="mt-4 font-body text-lg text-tertiary">
|
|
81
|
+
{subtitle}
|
|
82
|
+
</p>
|
|
83
|
+
)}
|
|
84
|
+
<Form
|
|
85
|
+
ref={formRef}
|
|
86
|
+
onSubmit={handleSubmit}
|
|
87
|
+
className="mt-8 flex flex-col gap-6 text-left"
|
|
88
|
+
>
|
|
89
|
+
<DynamicFormFields form={resolvedFormDefinition} />
|
|
90
|
+
{submitStatus === 'success' && (
|
|
91
|
+
<div className="rounded-lg bg-success-50 p-4 text-success-700">
|
|
92
|
+
{statusMessage || successMessage}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
{submitStatus === 'error' && (
|
|
96
|
+
<div className="rounded-lg bg-error-50 p-4 text-error-700">
|
|
97
|
+
{statusMessage}
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
<Button
|
|
101
|
+
type="submit"
|
|
102
|
+
color="primary"
|
|
103
|
+
size="xl"
|
|
104
|
+
isDisabled={isSubmitting}
|
|
105
|
+
isLoading={isSubmitting}
|
|
106
|
+
className="w-full"
|
|
107
|
+
>
|
|
108
|
+
{isSubmitting ? 'Subscribing...' : buttonText}
|
|
109
|
+
</Button>
|
|
110
|
+
</Form>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</section>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
@@ -30,13 +30,15 @@ export function HeaderNavigation({
|
|
|
30
30
|
// Timeout ref for delayed dropdown closing
|
|
31
31
|
const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
32
32
|
|
|
33
|
-
// Track scroll position for header animation
|
|
33
|
+
// Track scroll position for header animation; close dropdown on scroll to
|
|
34
|
+
// avoid a stale fixed `top` value creating a gap between the nav and dropdown.
|
|
34
35
|
React.useEffect(() => {
|
|
35
36
|
const handleScroll = () => {
|
|
36
37
|
setIsScrolled(window.scrollY > 10);
|
|
38
|
+
setActiveDropdown(null);
|
|
37
39
|
};
|
|
38
40
|
|
|
39
|
-
window.addEventListener('scroll', handleScroll);
|
|
41
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
40
42
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
41
43
|
}, []);
|
|
42
44
|
|
|
@@ -67,7 +69,8 @@ export function HeaderNavigation({
|
|
|
67
69
|
}
|
|
68
70
|
}, []);
|
|
69
71
|
|
|
70
|
-
// Open dropdown immediately
|
|
72
|
+
// Open dropdown immediately; clear it when hovering a childless item so the
|
|
73
|
+
// previous dropdown doesn't linger while a different top-level item is active.
|
|
71
74
|
const handleMouseEnter = useCallback((item: NavItem, e: React.MouseEvent<HTMLDivElement>) => {
|
|
72
75
|
cancelCloseTimeout();
|
|
73
76
|
if (item.children && item.children.length > 0) {
|
|
@@ -77,6 +80,8 @@ export function HeaderNavigation({
|
|
|
77
80
|
setDropdownTop(rect.bottom);
|
|
78
81
|
}
|
|
79
82
|
setActiveDropdown(item.label);
|
|
83
|
+
} else {
|
|
84
|
+
setActiveDropdown(null);
|
|
80
85
|
}
|
|
81
86
|
}, [cancelCloseTimeout]);
|
|
82
87
|
|
|
@@ -54,11 +54,14 @@ export function HeaderNavigation({
|
|
|
54
54
|
}
|
|
55
55
|
}, []);
|
|
56
56
|
|
|
57
|
-
// Open dropdown immediately
|
|
57
|
+
// Open dropdown immediately; clear it when hovering a childless item so the
|
|
58
|
+
// previous dropdown doesn't linger while a different top-level item is active.
|
|
58
59
|
const handleMouseEnter = useCallback((item: NavItem) => {
|
|
59
60
|
cancelCloseTimeout();
|
|
60
61
|
if (item.children && item.children.length > 0) {
|
|
61
62
|
setActiveDropdown(item.label);
|
|
63
|
+
} else {
|
|
64
|
+
setActiveDropdown(null);
|
|
62
65
|
}
|
|
63
66
|
}, [cancelCloseTimeout]);
|
|
64
67
|
|
|
@@ -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,11 @@ 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
|
|
181
|
+
// Email / newsletter signup section (client component, no theme variants needed)
|
|
182
|
+
export { EmailSignupSection } from './email-signup-section';
|
|
183
|
+
export type { EmailSignupSectionProps } from './email-signup-section';
|
|
186
184
|
|
|
187
|
-
//
|
|
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
|
|
185
|
+
// Service Menu: packages + treatments; nested specials as badges / modal callouts
|
|
197
186
|
export { ServiceMenuSection } from './service-menu-section';
|
|
198
187
|
export type { ServiceMenuSectionProps, PackagePublic } from './service-menu-section';
|
|
199
188
|
|