@bailierich/booking-components 2.0.0
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/README.md +319 -0
- package/TENANT_DATA_INTEGRATION.md +402 -0
- package/TENANT_SETUP.md +316 -0
- package/components/BookingFlow/BookingFlow.tsx +790 -0
- package/components/BookingFlow/index.ts +5 -0
- package/components/BookingFlow/steps/AddonsSelection.tsx +118 -0
- package/components/BookingFlow/steps/Confirmation.tsx +185 -0
- package/components/BookingFlow/steps/ContactForm.tsx +292 -0
- package/components/BookingFlow/steps/CycleAwareDateSelection.tsx +277 -0
- package/components/BookingFlow/steps/DateSelection.tsx +473 -0
- package/components/BookingFlow/steps/ServiceSelection.tsx +315 -0
- package/components/BookingFlow/steps/TimeSelection.tsx +230 -0
- package/components/BookingFlow/steps/index.ts +10 -0
- package/components/BottomSheet/index.tsx +120 -0
- package/components/Forms/FormBlock.tsx +283 -0
- package/components/Forms/FormField.tsx +385 -0
- package/components/Forms/FormRenderer.tsx +216 -0
- package/components/Forms/FormValidation.ts +122 -0
- package/components/Forms/index.ts +4 -0
- package/components/HoldTimer/HoldTimer.tsx +266 -0
- package/components/HoldTimer/index.ts +2 -0
- package/components/SectionRenderer.tsx +558 -0
- package/components/Sections/About.tsx +145 -0
- package/components/Sections/BeforeAfter.tsx +81 -0
- package/components/Sections/BookingSection.tsx +76 -0
- package/components/Sections/Contact.tsx +103 -0
- package/components/Sections/FAQSection.tsx +239 -0
- package/components/Sections/FeatureContent.tsx +113 -0
- package/components/Sections/FeaturedLink.tsx +103 -0
- package/components/Sections/FixedInfoCard.tsx +189 -0
- package/components/Sections/Gallery.tsx +83 -0
- package/components/Sections/Header.tsx +78 -0
- package/components/Sections/Hero.tsx +178 -0
- package/components/Sections/ImageSection.tsx +147 -0
- package/components/Sections/InstagramFeed.tsx +38 -0
- package/components/Sections/LinkList.tsx +76 -0
- package/components/Sections/LocationMap.tsx +202 -0
- package/components/Sections/Logo.tsx +61 -0
- package/components/Sections/MinimalFooter.tsx +78 -0
- package/components/Sections/MinimalHeader.tsx +81 -0
- package/components/Sections/MinimalNavigation.tsx +63 -0
- package/components/Sections/Navbar.tsx +258 -0
- package/components/Sections/PricingTable.tsx +106 -0
- package/components/Sections/ScrollingTextDivider.tsx +138 -0
- package/components/Sections/ScrollingTextDivider.tsx.bak +138 -0
- package/components/Sections/ServicesPreview.tsx +129 -0
- package/components/Sections/SocialBar.tsx +177 -0
- package/components/Sections/Team.tsx +80 -0
- package/components/Sections/Testimonials.tsx +92 -0
- package/components/Sections/TextSection.tsx +116 -0
- package/components/Sections/VideoSection.tsx +178 -0
- package/components/Sections/index.ts +57 -0
- package/components/index.ts +21 -0
- package/dist/index-DAai7Glf.d.mts +474 -0
- package/dist/index-DAai7Glf.d.ts +474 -0
- package/dist/index.d.mts +1075 -0
- package/dist/index.d.ts +1075 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +22 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles/index.d.mts +1 -0
- package/dist/styles/index.d.ts +1 -0
- package/dist/styles/index.js +2 -0
- package/dist/styles/index.js.map +1 -0
- package/dist/styles/index.mjs +2 -0
- package/dist/styles/index.mjs.map +1 -0
- package/docs/API.md +849 -0
- package/docs/CALLBACKS.md +760 -0
- package/docs/COMPLETE_SESSION_SUMMARY.md +404 -0
- package/docs/DATA_SHAPES.md +684 -0
- package/docs/MIGRATION.md +662 -0
- package/docs/PAYMENT_INTEGRATION.md +766 -0
- package/docs/SESSION_SUMMARY.md +185 -0
- package/docs/STYLING.md +735 -0
- package/index.ts +4 -0
- package/lib/storage.ts +239 -0
- package/package.json +59 -0
- package/styles/animations.ts +210 -0
- package/styles/index.ts +1 -0
- package/tsconfig.json +32 -0
- package/tsup.config.ts +13 -0
- package/types/index.ts +369 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export interface ImageSectionProps {
|
|
4
|
+
imageUrl: string;
|
|
5
|
+
altText?: string;
|
|
6
|
+
caption?: string;
|
|
7
|
+
maxWidth?: 'full' | 'large' | 'medium' | 'small';
|
|
8
|
+
alignment?: 'left' | 'center' | 'right';
|
|
9
|
+
aspectRatio?: 'auto' | '16:9' | '4:3' | '1:1' | '3:2';
|
|
10
|
+
borderRadius?: number;
|
|
11
|
+
linkUrl?: string;
|
|
12
|
+
openInNewTab?: boolean;
|
|
13
|
+
colors: {
|
|
14
|
+
primary: string;
|
|
15
|
+
text: string;
|
|
16
|
+
};
|
|
17
|
+
typography?: {
|
|
18
|
+
bodyFont?: string;
|
|
19
|
+
};
|
|
20
|
+
enableInlineEditing?: boolean;
|
|
21
|
+
sectionId?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ImageSection({
|
|
25
|
+
imageUrl,
|
|
26
|
+
altText = '',
|
|
27
|
+
caption,
|
|
28
|
+
maxWidth = 'large',
|
|
29
|
+
alignment = 'center',
|
|
30
|
+
aspectRatio = 'auto',
|
|
31
|
+
borderRadius = 12,
|
|
32
|
+
linkUrl,
|
|
33
|
+
openInNewTab = true,
|
|
34
|
+
colors,
|
|
35
|
+
typography,
|
|
36
|
+
enableInlineEditing = false,
|
|
37
|
+
sectionId = ''
|
|
38
|
+
}: ImageSectionProps) {
|
|
39
|
+
// Max width classes
|
|
40
|
+
const maxWidthMap = {
|
|
41
|
+
full: 'max-w-full',
|
|
42
|
+
large: 'max-w-6xl',
|
|
43
|
+
medium: 'max-w-4xl',
|
|
44
|
+
small: 'max-w-2xl'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Aspect ratio padding hack
|
|
48
|
+
const aspectRatioMap = {
|
|
49
|
+
auto: null,
|
|
50
|
+
'16:9': '56.25%',
|
|
51
|
+
'4:3': '75%',
|
|
52
|
+
'1:1': '100%',
|
|
53
|
+
'3:2': '66.67%'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const alignmentClass = alignment === 'center' ? 'mx-auto' : alignment === 'right' ? 'ml-auto' : 'mr-auto';
|
|
57
|
+
|
|
58
|
+
const imageContent = (
|
|
59
|
+
<div className={`${maxWidthMap[maxWidth]} ${alignmentClass}`}>
|
|
60
|
+
<div
|
|
61
|
+
className="relative w-full overflow-hidden"
|
|
62
|
+
style={{
|
|
63
|
+
borderRadius: `${borderRadius}px`,
|
|
64
|
+
...(aspectRatioMap[aspectRatio] && { paddingBottom: aspectRatioMap[aspectRatio] })
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{imageUrl ? (
|
|
68
|
+
<img
|
|
69
|
+
src={imageUrl}
|
|
70
|
+
alt={altText || 'Image'}
|
|
71
|
+
className={aspectRatio === 'auto' ? 'w-full h-auto' : 'absolute top-0 left-0 w-full h-full object-cover'}
|
|
72
|
+
style={{
|
|
73
|
+
borderRadius: `${borderRadius}px`
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
) : (
|
|
77
|
+
// Placeholder when no image
|
|
78
|
+
<div
|
|
79
|
+
className="flex items-center justify-center bg-gray-100"
|
|
80
|
+
style={{
|
|
81
|
+
backgroundColor: `${colors.primary}10`,
|
|
82
|
+
borderRadius: `${borderRadius}px`,
|
|
83
|
+
minHeight: aspectRatio === 'auto' ? '300px' : undefined
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<div className="text-center px-4">
|
|
87
|
+
<svg
|
|
88
|
+
className="w-16 h-16 mx-auto mb-4"
|
|
89
|
+
fill="none"
|
|
90
|
+
stroke={colors.primary}
|
|
91
|
+
viewBox="0 0 24 24"
|
|
92
|
+
>
|
|
93
|
+
<path
|
|
94
|
+
strokeLinecap="round"
|
|
95
|
+
strokeLinejoin="round"
|
|
96
|
+
strokeWidth={2}
|
|
97
|
+
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
98
|
+
/>
|
|
99
|
+
</svg>
|
|
100
|
+
<p style={{ color: colors.text, opacity: 0.6 }}>
|
|
101
|
+
Add an image URL to display
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{/* Caption */}
|
|
109
|
+
{caption && imageUrl && (
|
|
110
|
+
<p
|
|
111
|
+
className="mt-3 text-sm text-center"
|
|
112
|
+
style={{
|
|
113
|
+
color: colors.text,
|
|
114
|
+
opacity: 0.7,
|
|
115
|
+
fontFamily: typography?.bodyFont || 'inherit'
|
|
116
|
+
}}
|
|
117
|
+
{...(enableInlineEditing && {
|
|
118
|
+
'data-editable': true,
|
|
119
|
+
'data-section-id': sectionId,
|
|
120
|
+
'data-field-path': 'settings.caption'
|
|
121
|
+
})}
|
|
122
|
+
>
|
|
123
|
+
{caption}
|
|
124
|
+
</p>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className={maxWidth === 'full' ? 'w-full' : 'container mx-auto px-4'}>
|
|
131
|
+
{linkUrl && imageUrl ? (
|
|
132
|
+
<a
|
|
133
|
+
href={linkUrl}
|
|
134
|
+
target={openInNewTab ? '_blank' : '_self'}
|
|
135
|
+
rel={openInNewTab ? 'noopener noreferrer' : undefined}
|
|
136
|
+
className="block"
|
|
137
|
+
>
|
|
138
|
+
{imageContent}
|
|
139
|
+
</a>
|
|
140
|
+
) : (
|
|
141
|
+
imageContent
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default ImageSection;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export interface InstagramFeedProps {
|
|
4
|
+
username?: string;
|
|
5
|
+
colors: {
|
|
6
|
+
primary: string;
|
|
7
|
+
};
|
|
8
|
+
enableInlineEditing?: boolean;
|
|
9
|
+
sectionId?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function InstagramFeed({
|
|
13
|
+
username,
|
|
14
|
+
colors,
|
|
15
|
+
enableInlineEditing = false,
|
|
16
|
+
sectionId = ''
|
|
17
|
+
}: InstagramFeedProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="container mx-auto px-4 py-8">
|
|
20
|
+
<div className="max-w-4xl mx-auto">
|
|
21
|
+
<p className="text-center text-sm opacity-60 mb-4">
|
|
22
|
+
Instagram Feed: {username || '@handle'}
|
|
23
|
+
</p>
|
|
24
|
+
<div className="grid grid-cols-3 gap-1">
|
|
25
|
+
{[1, 2, 3, 4, 5, 6].map((i) => (
|
|
26
|
+
<div
|
|
27
|
+
key={i}
|
|
28
|
+
className="aspect-square rounded"
|
|
29
|
+
style={{ backgroundColor: colors.primary, opacity: 0.2 }}
|
|
30
|
+
/>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default InstagramFeed;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export interface Link {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
url: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LinkListProps {
|
|
10
|
+
links: Link[];
|
|
11
|
+
style?: 'pill' | 'square' | 'outline';
|
|
12
|
+
colors: {
|
|
13
|
+
primary: string;
|
|
14
|
+
linkBackground?: string;
|
|
15
|
+
linkText?: string;
|
|
16
|
+
};
|
|
17
|
+
enableInlineEditing?: boolean;
|
|
18
|
+
sectionId?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function LinkList({ links, style = 'pill', colors, enableInlineEditing = false, sectionId = '' }: LinkListProps) {
|
|
22
|
+
const getLinkClassName = () => {
|
|
23
|
+
const baseClasses = "block w-full p-4 text-center font-medium transition-all hover:scale-105";
|
|
24
|
+
|
|
25
|
+
switch (style) {
|
|
26
|
+
case 'pill':
|
|
27
|
+
return `${baseClasses} rounded-full`;
|
|
28
|
+
case 'square':
|
|
29
|
+
return `${baseClasses} rounded-md`;
|
|
30
|
+
case 'outline':
|
|
31
|
+
return `${baseClasses} rounded-full border-2`;
|
|
32
|
+
default:
|
|
33
|
+
return `${baseClasses} rounded-lg`;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getLinkStyle = (isOutline: boolean) => {
|
|
38
|
+
if (isOutline) {
|
|
39
|
+
return {
|
|
40
|
+
backgroundColor: 'transparent',
|
|
41
|
+
borderColor: colors.primary,
|
|
42
|
+
color: colors.primary
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
backgroundColor: colors.linkBackground || colors.primary,
|
|
47
|
+
color: colors.linkText || '#FFFFFF'
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="container mx-auto px-4">
|
|
53
|
+
<div className="max-w-2xl mx-auto space-y-3">
|
|
54
|
+
{links.map((link, index) => (
|
|
55
|
+
<a
|
|
56
|
+
key={link.id}
|
|
57
|
+
href={link.url}
|
|
58
|
+
target="_blank"
|
|
59
|
+
rel="noopener noreferrer"
|
|
60
|
+
className={getLinkClassName()}
|
|
61
|
+
style={getLinkStyle(style === 'outline')}
|
|
62
|
+
{...(enableInlineEditing && {
|
|
63
|
+
'data-editable': true,
|
|
64
|
+
'data-section-id': sectionId,
|
|
65
|
+
'data-field-path': `settings.links.${index}.label`
|
|
66
|
+
})}
|
|
67
|
+
>
|
|
68
|
+
{link.label}
|
|
69
|
+
</a>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default LinkList;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export interface LocationMapProps {
|
|
4
|
+
address: string;
|
|
5
|
+
displayType?: 'map-only' | 'address-only' | 'both';
|
|
6
|
+
mapHeight?: 'small' | 'medium' | 'large';
|
|
7
|
+
mapStyle?: 'roadmap' | 'satellite';
|
|
8
|
+
zoom?: number;
|
|
9
|
+
showMarker?: boolean;
|
|
10
|
+
borderRadius?: number;
|
|
11
|
+
directionsButtonText?: string;
|
|
12
|
+
colors: {
|
|
13
|
+
primary: string;
|
|
14
|
+
text: string;
|
|
15
|
+
};
|
|
16
|
+
typography?: {
|
|
17
|
+
headingFont?: string;
|
|
18
|
+
bodyFont?: string;
|
|
19
|
+
};
|
|
20
|
+
enableInlineEditing?: boolean;
|
|
21
|
+
sectionId?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function LocationMap({
|
|
25
|
+
address,
|
|
26
|
+
displayType = 'both',
|
|
27
|
+
mapHeight = 'medium',
|
|
28
|
+
mapStyle = 'roadmap',
|
|
29
|
+
zoom = 15,
|
|
30
|
+
showMarker = true,
|
|
31
|
+
borderRadius = 12,
|
|
32
|
+
directionsButtonText = 'Get Directions',
|
|
33
|
+
colors,
|
|
34
|
+
typography,
|
|
35
|
+
enableInlineEditing = false,
|
|
36
|
+
sectionId = ''
|
|
37
|
+
}: LocationMapProps) {
|
|
38
|
+
// Map height mapping
|
|
39
|
+
const heightMap = {
|
|
40
|
+
small: 300,
|
|
41
|
+
medium: 400,
|
|
42
|
+
large: 500
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const height = heightMap[mapHeight];
|
|
46
|
+
|
|
47
|
+
// Encode address for Google Maps
|
|
48
|
+
const encodedAddress = encodeURIComponent(address);
|
|
49
|
+
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${encodedAddress}`;
|
|
50
|
+
|
|
51
|
+
// Google Maps embed URL
|
|
52
|
+
// Support both Next.js (process.env.NEXT_PUBLIC_*) and Vite (import.meta.env.VITE_*)
|
|
53
|
+
const apiKey = (import.meta as any).env?.VITE_GOOGLE_MAPS_API_KEY ||
|
|
54
|
+
(typeof globalThis !== 'undefined' && (globalThis as any).process?.env?.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY) ||
|
|
55
|
+
'';
|
|
56
|
+
const mapEmbedUrl = `https://www.google.com/maps/embed/v1/place?key=${apiKey}&q=${encodedAddress}&zoom=${zoom}&maptype=${mapStyle}`;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="container mx-auto px-4">
|
|
60
|
+
<div className="max-w-4xl mx-auto">
|
|
61
|
+
{/* Map */}
|
|
62
|
+
{(displayType === 'map-only' || displayType === 'both') && address && (
|
|
63
|
+
<div
|
|
64
|
+
className="w-full overflow-hidden mb-6"
|
|
65
|
+
style={{
|
|
66
|
+
height: `${height}px`,
|
|
67
|
+
borderRadius: `${borderRadius}px`
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<iframe
|
|
71
|
+
src={mapEmbedUrl}
|
|
72
|
+
className="w-full h-full"
|
|
73
|
+
style={{ border: 0 }}
|
|
74
|
+
allowFullScreen
|
|
75
|
+
loading="lazy"
|
|
76
|
+
referrerPolicy="no-referrer-when-downgrade"
|
|
77
|
+
title="Location Map"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
{/* Address & Directions */}
|
|
83
|
+
{(displayType === 'address-only' || displayType === 'both') && (
|
|
84
|
+
<div
|
|
85
|
+
className="p-6 rounded-lg border"
|
|
86
|
+
style={{
|
|
87
|
+
backgroundColor: `${colors.primary}10`,
|
|
88
|
+
borderColor: `${colors.primary}30`,
|
|
89
|
+
borderRadius: `${borderRadius}px`
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
93
|
+
{/* Address */}
|
|
94
|
+
<div className="flex-1">
|
|
95
|
+
<div className="flex items-start gap-3">
|
|
96
|
+
<svg
|
|
97
|
+
className="w-5 h-5 mt-0.5 flex-shrink-0"
|
|
98
|
+
fill="none"
|
|
99
|
+
stroke={colors.primary}
|
|
100
|
+
viewBox="0 0 24 24"
|
|
101
|
+
>
|
|
102
|
+
<path
|
|
103
|
+
strokeLinecap="round"
|
|
104
|
+
strokeLinejoin="round"
|
|
105
|
+
strokeWidth={2}
|
|
106
|
+
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
|
107
|
+
/>
|
|
108
|
+
<path
|
|
109
|
+
strokeLinecap="round"
|
|
110
|
+
strokeLinejoin="round"
|
|
111
|
+
strokeWidth={2}
|
|
112
|
+
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
|
113
|
+
/>
|
|
114
|
+
</svg>
|
|
115
|
+
<div>
|
|
116
|
+
<p
|
|
117
|
+
className="font-medium mb-1"
|
|
118
|
+
style={{
|
|
119
|
+
color: colors.text,
|
|
120
|
+
fontFamily: typography?.bodyFont || 'inherit'
|
|
121
|
+
}}
|
|
122
|
+
{...(enableInlineEditing && {
|
|
123
|
+
'data-editable': true,
|
|
124
|
+
'data-section-id': sectionId,
|
|
125
|
+
'data-field-path': 'settings.address'
|
|
126
|
+
})}
|
|
127
|
+
>
|
|
128
|
+
{address || 'Add your address'}
|
|
129
|
+
</p>
|
|
130
|
+
{address && (
|
|
131
|
+
<a
|
|
132
|
+
href={directionsUrl}
|
|
133
|
+
target="_blank"
|
|
134
|
+
rel="noopener noreferrer"
|
|
135
|
+
className="text-sm underline hover:no-underline"
|
|
136
|
+
style={{ color: colors.primary }}
|
|
137
|
+
>
|
|
138
|
+
View in Google Maps �
|
|
139
|
+
</a>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Get Directions Button */}
|
|
146
|
+
{address && (
|
|
147
|
+
<a
|
|
148
|
+
href={directionsUrl}
|
|
149
|
+
target="_blank"
|
|
150
|
+
rel="noopener noreferrer"
|
|
151
|
+
className="px-6 py-3 rounded-lg font-semibold transition-all hover:opacity-90 text-center"
|
|
152
|
+
style={{
|
|
153
|
+
backgroundColor: colors.primary,
|
|
154
|
+
color: '#FFFFFF'
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
{directionsButtonText}
|
|
158
|
+
</a>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{/* Fallback when no address */}
|
|
165
|
+
{!address && (
|
|
166
|
+
<div
|
|
167
|
+
className="p-12 rounded-lg border-2 border-dashed text-center"
|
|
168
|
+
style={{
|
|
169
|
+
borderColor: `${colors.primary}30`,
|
|
170
|
+
borderRadius: `${borderRadius}px`
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<svg
|
|
174
|
+
className="w-12 h-12 mx-auto mb-4"
|
|
175
|
+
fill="none"
|
|
176
|
+
stroke={colors.primary}
|
|
177
|
+
viewBox="0 0 24 24"
|
|
178
|
+
>
|
|
179
|
+
<path
|
|
180
|
+
strokeLinecap="round"
|
|
181
|
+
strokeLinejoin="round"
|
|
182
|
+
strokeWidth={2}
|
|
183
|
+
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
|
184
|
+
/>
|
|
185
|
+
<path
|
|
186
|
+
strokeLinecap="round"
|
|
187
|
+
strokeLinejoin="round"
|
|
188
|
+
strokeWidth={2}
|
|
189
|
+
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
|
190
|
+
/>
|
|
191
|
+
</svg>
|
|
192
|
+
<p style={{ color: colors.text, opacity: 0.6 }}>
|
|
193
|
+
Add your business address to display a map
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export default LocationMap;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export interface LogoProps {
|
|
4
|
+
logoUrl?: string;
|
|
5
|
+
fallbackText?: string;
|
|
6
|
+
size?: 'small' | 'medium' | 'large';
|
|
7
|
+
alignment?: 'left' | 'center' | 'right';
|
|
8
|
+
colors: {
|
|
9
|
+
primary: string;
|
|
10
|
+
};
|
|
11
|
+
enableInlineEditing?: boolean;
|
|
12
|
+
sectionId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Logo({ logoUrl, fallbackText, size = 'medium', alignment = 'center', colors, enableInlineEditing = false, sectionId = '' }: LogoProps) {
|
|
16
|
+
const logoSizeMap = {
|
|
17
|
+
small: 80,
|
|
18
|
+
medium: 120,
|
|
19
|
+
large: 160
|
|
20
|
+
};
|
|
21
|
+
const logoSize = logoSizeMap[size];
|
|
22
|
+
const alignmentClass = alignment === 'center' ? 'justify-center' : alignment === 'right' ? 'justify-end' : 'justify-start';
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className={`container mx-auto px-4 flex ${alignmentClass}`}>
|
|
26
|
+
{logoUrl ? (
|
|
27
|
+
<img
|
|
28
|
+
src={logoUrl}
|
|
29
|
+
alt={fallbackText || 'Logo'}
|
|
30
|
+
className="object-contain"
|
|
31
|
+
style={{ height: `${logoSize}px`, width: 'auto' }}
|
|
32
|
+
/>
|
|
33
|
+
) : fallbackText ? (
|
|
34
|
+
<div
|
|
35
|
+
className="flex items-center justify-center rounded-full font-bold"
|
|
36
|
+
style={{
|
|
37
|
+
width: `${logoSize}px`,
|
|
38
|
+
height: `${logoSize}px`,
|
|
39
|
+
backgroundColor: colors.primary,
|
|
40
|
+
color: '#FFFFFF',
|
|
41
|
+
fontSize: `${logoSize * 0.4}px`
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
{fallbackText.slice(0, 2).toUpperCase()}
|
|
45
|
+
</div>
|
|
46
|
+
) : (
|
|
47
|
+
<div
|
|
48
|
+
className="rounded-full"
|
|
49
|
+
style={{
|
|
50
|
+
width: `${logoSize}px`,
|
|
51
|
+
height: `${logoSize}px`,
|
|
52
|
+
backgroundColor: colors.primary,
|
|
53
|
+
opacity: 0.2
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default Logo;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export interface MinimalFooterProps {
|
|
4
|
+
footerText?: string;
|
|
5
|
+
alignment?: 'left' | 'center' | 'right';
|
|
6
|
+
showSocial?: boolean;
|
|
7
|
+
showContact?: boolean;
|
|
8
|
+
showLinks?: boolean;
|
|
9
|
+
colors: {
|
|
10
|
+
primary: string;
|
|
11
|
+
text: string;
|
|
12
|
+
};
|
|
13
|
+
enableInlineEditing?: boolean;
|
|
14
|
+
sectionId?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function MinimalFooter({
|
|
18
|
+
footerText,
|
|
19
|
+
alignment = 'center',
|
|
20
|
+
showSocial = true,
|
|
21
|
+
showContact = true,
|
|
22
|
+
showLinks = true,
|
|
23
|
+
colors,
|
|
24
|
+
enableInlineEditing = false,
|
|
25
|
+
sectionId = ''
|
|
26
|
+
}: MinimalFooterProps) {
|
|
27
|
+
const alignClass = alignment === 'center' ? 'text-center' : alignment === 'right' ? 'text-right' : 'text-left';
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<footer
|
|
31
|
+
className={`border-t py-8`}
|
|
32
|
+
style={{ borderColor: colors.primary }}
|
|
33
|
+
>
|
|
34
|
+
<div className={`container mx-auto px-4 ${alignClass}`}>
|
|
35
|
+
<div className="space-y-4">
|
|
36
|
+
{showSocial && (
|
|
37
|
+
<div className={`flex gap-4 ${alignment === 'center' ? 'justify-center' : alignment === 'right' ? 'justify-end' : 'justify-start'}`}>
|
|
38
|
+
{['instagram', 'facebook', 'twitter'].map((platform) => (
|
|
39
|
+
<a
|
|
40
|
+
key={platform}
|
|
41
|
+
href={`https://${platform}.com/`}
|
|
42
|
+
className="w-8 h-8 rounded-full flex items-center justify-center transition-transform hover:scale-110"
|
|
43
|
+
style={{ backgroundColor: colors.primary, color: '#FFFFFF' }}
|
|
44
|
+
>
|
|
45
|
+
{platform[0].toUpperCase()}
|
|
46
|
+
</a>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
{showContact && (
|
|
51
|
+
<div className="text-sm opacity-70" style={{ color: colors.text }}>
|
|
52
|
+
<p>contact@example.com | (555) 123-4567</p>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
{showLinks && (
|
|
56
|
+
<div className={`flex gap-4 text-sm ${alignment === 'center' ? 'justify-center' : alignment === 'right' ? 'justify-end' : 'justify-start'}`}>
|
|
57
|
+
<a href="/privacy" className="hover:opacity-70" style={{ color: colors.text }}>Privacy</a>
|
|
58
|
+
<a href="/terms" className="hover:opacity-70" style={{ color: colors.text }}>Terms</a>
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
<p
|
|
62
|
+
className="text-sm opacity-60"
|
|
63
|
+
style={{ color: colors.text }}
|
|
64
|
+
{...(enableInlineEditing && {
|
|
65
|
+
'data-editable': true,
|
|
66
|
+
'data-section-id': sectionId,
|
|
67
|
+
'data-field-path': 'settings.footerText'
|
|
68
|
+
})}
|
|
69
|
+
>
|
|
70
|
+
{footerText || '© 2025 Your Business'}
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</footer>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default MinimalFooter;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export interface MinimalHeaderProps {
|
|
4
|
+
brandName?: string;
|
|
5
|
+
logoUrl?: string;
|
|
6
|
+
style?: 'logo' | 'text' | 'both';
|
|
7
|
+
alignment?: 'left' | 'center' | 'right';
|
|
8
|
+
sticky?: boolean;
|
|
9
|
+
colors: {
|
|
10
|
+
primary: string;
|
|
11
|
+
text: string;
|
|
12
|
+
};
|
|
13
|
+
typography: {
|
|
14
|
+
headingFont?: string;
|
|
15
|
+
};
|
|
16
|
+
enableInlineEditing?: boolean;
|
|
17
|
+
sectionId?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function MinimalHeader({
|
|
21
|
+
brandName,
|
|
22
|
+
logoUrl,
|
|
23
|
+
style = 'both',
|
|
24
|
+
alignment = 'left',
|
|
25
|
+
sticky = true,
|
|
26
|
+
colors,
|
|
27
|
+
typography,
|
|
28
|
+
enableInlineEditing = false,
|
|
29
|
+
sectionId = ''
|
|
30
|
+
}: MinimalHeaderProps) {
|
|
31
|
+
const alignmentClass = alignment === 'center' ? 'justify-center' : alignment === 'right' ? 'justify-end' : 'justify-start';
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<header
|
|
35
|
+
className={`${sticky ? 'sticky top-0 z-50 backdrop-blur-sm' : ''} border-b`}
|
|
36
|
+
style={{
|
|
37
|
+
backgroundColor: sticky ? 'rgba(255, 255, 255, 0.8)' : 'transparent',
|
|
38
|
+
borderColor: colors.primary
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<div className={`container mx-auto px-4 py-4 flex items-center ${alignmentClass}`}>
|
|
42
|
+
<div className="flex items-center gap-3">
|
|
43
|
+
{(style === 'logo' || style === 'both') && (
|
|
44
|
+
<>
|
|
45
|
+
{logoUrl ? (
|
|
46
|
+
<img
|
|
47
|
+
src={logoUrl}
|
|
48
|
+
alt={brandName || 'Logo'}
|
|
49
|
+
className="h-10 w-auto object-contain"
|
|
50
|
+
/>
|
|
51
|
+
) : (
|
|
52
|
+
<div
|
|
53
|
+
className="w-10 h-10 rounded-full"
|
|
54
|
+
style={{ backgroundColor: colors.primary, opacity: 0.2 }}
|
|
55
|
+
/>
|
|
56
|
+
)}
|
|
57
|
+
</>
|
|
58
|
+
)}
|
|
59
|
+
{(style === 'text' || style === 'both' || !style) && (
|
|
60
|
+
<h1
|
|
61
|
+
className="text-2xl font-bold"
|
|
62
|
+
style={{
|
|
63
|
+
fontFamily: typography.headingFont,
|
|
64
|
+
color: colors.text
|
|
65
|
+
}}
|
|
66
|
+
{...(enableInlineEditing && {
|
|
67
|
+
'data-editable': true,
|
|
68
|
+
'data-section-id': sectionId,
|
|
69
|
+
'data-field-path': 'settings.brandName'
|
|
70
|
+
})}
|
|
71
|
+
>
|
|
72
|
+
{brandName || 'Your Brand'}
|
|
73
|
+
</h1>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</header>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default MinimalHeader;
|