@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,81 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export interface Comparison {
|
|
4
|
+
id: string | number;
|
|
5
|
+
title?: string;
|
|
6
|
+
beforeImage?: string;
|
|
7
|
+
afterImage?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface BeforeAfterProps {
|
|
11
|
+
title?: string;
|
|
12
|
+
comparisons?: Comparison[];
|
|
13
|
+
colors: {
|
|
14
|
+
primary: string;
|
|
15
|
+
text: string;
|
|
16
|
+
};
|
|
17
|
+
typography: {
|
|
18
|
+
headingFont?: string;
|
|
19
|
+
};
|
|
20
|
+
enableInlineEditing?: boolean;
|
|
21
|
+
sectionId?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function BeforeAfter({
|
|
25
|
+
title,
|
|
26
|
+
comparisons = [],
|
|
27
|
+
colors,
|
|
28
|
+
typography,
|
|
29
|
+
enableInlineEditing = false,
|
|
30
|
+
sectionId = ''
|
|
31
|
+
}: BeforeAfterProps) {
|
|
32
|
+
const mockComparisons = comparisons.length > 0 ? comparisons : [
|
|
33
|
+
{ id: 1, title: 'Hair Transformation', beforeImage: '', afterImage: '' }
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<section className="py-16 px-6">
|
|
38
|
+
<div className="max-w-7xl mx-auto">
|
|
39
|
+
<h2
|
|
40
|
+
className="text-3xl md:text-4xl font-bold text-center mb-12"
|
|
41
|
+
style={{ fontFamily: typography.headingFont, color: colors.text }}
|
|
42
|
+
{...(enableInlineEditing && {
|
|
43
|
+
'data-editable': true,
|
|
44
|
+
'data-section-id': sectionId,
|
|
45
|
+
'data-field-path': 'settings.title'
|
|
46
|
+
})}
|
|
47
|
+
>
|
|
48
|
+
{title || 'Results'}
|
|
49
|
+
</h2>
|
|
50
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
51
|
+
{mockComparisons.map((comparison, index) => (
|
|
52
|
+
<div key={comparison.id || index} className="space-y-4">
|
|
53
|
+
{comparison.title && (
|
|
54
|
+
<h3
|
|
55
|
+
className="text-xl font-semibold text-center"
|
|
56
|
+
style={{ color: colors.text }}
|
|
57
|
+
>
|
|
58
|
+
{comparison.title}
|
|
59
|
+
</h3>
|
|
60
|
+
)}
|
|
61
|
+
<div className="relative aspect-[4/3] rounded-lg overflow-hidden">
|
|
62
|
+
<div
|
|
63
|
+
className="absolute inset-0"
|
|
64
|
+
style={{ backgroundColor: colors.primary, opacity: 0.1 }}
|
|
65
|
+
/>
|
|
66
|
+
<div className="absolute top-4 left-4 bg-black/50 text-white px-3 py-1 rounded text-sm">
|
|
67
|
+
Before
|
|
68
|
+
</div>
|
|
69
|
+
<div className="absolute top-4 right-4 bg-black/50 text-white px-3 py-1 rounded text-sm">
|
|
70
|
+
After
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</section>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default BeforeAfter;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export interface BookingConfig {
|
|
4
|
+
theme?: {
|
|
5
|
+
colors?: {
|
|
6
|
+
primary: string;
|
|
7
|
+
secondary: string;
|
|
8
|
+
text: string;
|
|
9
|
+
bookingText?: string;
|
|
10
|
+
};
|
|
11
|
+
typography?: {
|
|
12
|
+
headingFont?: string;
|
|
13
|
+
bodyFont?: string;
|
|
14
|
+
bodySize?: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
bookingSettings?: any; // Full booking flow configuration
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BookingSectionProps {
|
|
21
|
+
config: BookingConfig;
|
|
22
|
+
colors: {
|
|
23
|
+
primary: string;
|
|
24
|
+
secondary: string;
|
|
25
|
+
text: string;
|
|
26
|
+
bookingText?: string;
|
|
27
|
+
};
|
|
28
|
+
// Optional: If you want to pass a custom BookingFlow component
|
|
29
|
+
BookingFlowComponent?: React.ComponentType<any>;
|
|
30
|
+
enableInlineEditing?: boolean;
|
|
31
|
+
sectionId?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function BookingSection({
|
|
35
|
+
config,
|
|
36
|
+
colors,
|
|
37
|
+
BookingFlowComponent,
|
|
38
|
+
enableInlineEditing = false,
|
|
39
|
+
sectionId = ''
|
|
40
|
+
}: BookingSectionProps) {
|
|
41
|
+
// This is a wrapper component that can render any booking flow
|
|
42
|
+
// If a custom component is provided, use it; otherwise show a placeholder
|
|
43
|
+
|
|
44
|
+
if (BookingFlowComponent) {
|
|
45
|
+
return (
|
|
46
|
+
<div className="container mx-auto px-4 py-16">
|
|
47
|
+
<BookingFlowComponent config={config} colors={colors} />
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Placeholder when no BookingFlow component is provided
|
|
53
|
+
return (
|
|
54
|
+
<div className="container mx-auto px-4 py-16">
|
|
55
|
+
<div className="max-w-4xl mx-auto text-center">
|
|
56
|
+
<div
|
|
57
|
+
className="p-12 rounded-2xl border-2 border-dashed"
|
|
58
|
+
style={{ borderColor: colors.primary, opacity: 0.5 }}
|
|
59
|
+
>
|
|
60
|
+
<div className="text-6xl mb-4">📅</div>
|
|
61
|
+
<h3
|
|
62
|
+
className="text-2xl font-bold mb-3"
|
|
63
|
+
style={{ color: colors.text }}
|
|
64
|
+
>
|
|
65
|
+
Booking Flow
|
|
66
|
+
</h3>
|
|
67
|
+
<p className="text-base opacity-70" style={{ color: colors.text }}>
|
|
68
|
+
Pass a BookingFlowComponent prop to render your custom booking flow here.
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default BookingSection;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export interface ContactProps {
|
|
4
|
+
title?: string;
|
|
5
|
+
email?: string;
|
|
6
|
+
phone?: string;
|
|
7
|
+
address?: string;
|
|
8
|
+
showForm?: 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 Contact({
|
|
21
|
+
title,
|
|
22
|
+
email = 'contact@example.com',
|
|
23
|
+
phone = '(555) 123-4567',
|
|
24
|
+
address = '123 Main Street, City, State 12345',
|
|
25
|
+
showForm = true,
|
|
26
|
+
colors,
|
|
27
|
+
typography,
|
|
28
|
+
enableInlineEditing = false,
|
|
29
|
+
sectionId = ''
|
|
30
|
+
}: ContactProps) {
|
|
31
|
+
return (
|
|
32
|
+
<section className="py-16 px-6">
|
|
33
|
+
<div className="max-w-7xl mx-auto">
|
|
34
|
+
<h2
|
|
35
|
+
className="text-3xl md:text-4xl font-bold text-center mb-12"
|
|
36
|
+
style={{ fontFamily: typography.headingFont, color: colors.text }}
|
|
37
|
+
{...(enableInlineEditing && {
|
|
38
|
+
'data-editable': true,
|
|
39
|
+
'data-section-id': sectionId,
|
|
40
|
+
'data-field-path': 'settings.title'
|
|
41
|
+
})}
|
|
42
|
+
>
|
|
43
|
+
{title || 'Get In Touch'}
|
|
44
|
+
</h2>
|
|
45
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
|
46
|
+
<div className="space-y-6">
|
|
47
|
+
<div className="flex items-center gap-4">
|
|
48
|
+
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" style={{ color: colors.primary }}>
|
|
49
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
50
|
+
</svg>
|
|
51
|
+
<span style={{ color: colors.text }}>{email}</span>
|
|
52
|
+
</div>
|
|
53
|
+
<div className="flex items-center gap-4">
|
|
54
|
+
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" style={{ color: colors.primary }}>
|
|
55
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
|
56
|
+
</svg>
|
|
57
|
+
<span style={{ color: colors.text }}>{phone}</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="flex items-start gap-4">
|
|
60
|
+
<svg className="w-6 h-6 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" style={{ color: colors.primary }}>
|
|
61
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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" />
|
|
62
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
63
|
+
</svg>
|
|
64
|
+
<span style={{ color: colors.text }}>{address}</span>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{showForm && (
|
|
69
|
+
<form className="space-y-4">
|
|
70
|
+
<input
|
|
71
|
+
type="text"
|
|
72
|
+
placeholder="Name"
|
|
73
|
+
className="w-full px-4 py-3 rounded-lg bg-white/10 backdrop-blur-sm border border-white/20"
|
|
74
|
+
style={{ color: colors.text }}
|
|
75
|
+
/>
|
|
76
|
+
<input
|
|
77
|
+
type="email"
|
|
78
|
+
placeholder="Email"
|
|
79
|
+
className="w-full px-4 py-3 rounded-lg bg-white/10 backdrop-blur-sm border border-white/20"
|
|
80
|
+
style={{ color: colors.text }}
|
|
81
|
+
/>
|
|
82
|
+
<textarea
|
|
83
|
+
placeholder="Message"
|
|
84
|
+
rows={4}
|
|
85
|
+
className="w-full px-4 py-3 rounded-lg bg-white/10 backdrop-blur-sm border border-white/20"
|
|
86
|
+
style={{ color: colors.text }}
|
|
87
|
+
/>
|
|
88
|
+
<button
|
|
89
|
+
type="submit"
|
|
90
|
+
className="w-full px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
|
|
91
|
+
style={{ backgroundColor: colors.primary, color: '#FFFFFF' }}
|
|
92
|
+
>
|
|
93
|
+
Send Message
|
|
94
|
+
</button>
|
|
95
|
+
</form>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</section>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default Contact;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface FAQItem {
|
|
6
|
+
question: string;
|
|
7
|
+
answer: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FAQSectionProps {
|
|
11
|
+
title?: string;
|
|
12
|
+
faqs: FAQItem[];
|
|
13
|
+
layout?: 'accordion' | 'stacked';
|
|
14
|
+
maxWidth?: 'full' | 'large' | 'medium' | 'small';
|
|
15
|
+
colors: {
|
|
16
|
+
primary: string;
|
|
17
|
+
text: string;
|
|
18
|
+
};
|
|
19
|
+
typography?: {
|
|
20
|
+
headingFont?: string;
|
|
21
|
+
bodyFont?: string;
|
|
22
|
+
};
|
|
23
|
+
enableInlineEditing?: boolean;
|
|
24
|
+
sectionId?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function FAQAccordionItem({
|
|
28
|
+
faq,
|
|
29
|
+
index,
|
|
30
|
+
colors,
|
|
31
|
+
typography,
|
|
32
|
+
enableInlineEditing,
|
|
33
|
+
sectionId
|
|
34
|
+
}: {
|
|
35
|
+
faq: FAQItem;
|
|
36
|
+
index: number;
|
|
37
|
+
colors: any;
|
|
38
|
+
typography: any;
|
|
39
|
+
enableInlineEditing: boolean;
|
|
40
|
+
sectionId: string;
|
|
41
|
+
}) {
|
|
42
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className="border-b border-opacity-10"
|
|
47
|
+
style={{ borderColor: colors.text }}
|
|
48
|
+
>
|
|
49
|
+
<button
|
|
50
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
51
|
+
className="w-full py-4 px-6 flex items-center justify-between text-left hover:bg-opacity-5 transition-colors"
|
|
52
|
+
style={{
|
|
53
|
+
backgroundColor: isOpen ? `${colors.primary}10` : 'transparent'
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
<span
|
|
57
|
+
className="font-semibold text-lg pr-4"
|
|
58
|
+
style={{
|
|
59
|
+
color: colors.text,
|
|
60
|
+
fontFamily: typography?.headingFont || 'inherit'
|
|
61
|
+
}}
|
|
62
|
+
{...(enableInlineEditing && {
|
|
63
|
+
'data-editable': true,
|
|
64
|
+
'data-section-id': sectionId,
|
|
65
|
+
'data-field-path': `settings.faqs.${index}.question`
|
|
66
|
+
})}
|
|
67
|
+
>
|
|
68
|
+
{faq.question || 'Add question...'}
|
|
69
|
+
</span>
|
|
70
|
+
<svg
|
|
71
|
+
className="w-5 h-5 flex-shrink-0 transition-transform"
|
|
72
|
+
style={{
|
|
73
|
+
color: colors.primary,
|
|
74
|
+
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)'
|
|
75
|
+
}}
|
|
76
|
+
fill="none"
|
|
77
|
+
stroke="currentColor"
|
|
78
|
+
viewBox="0 0 24 24"
|
|
79
|
+
>
|
|
80
|
+
<path
|
|
81
|
+
strokeLinecap="round"
|
|
82
|
+
strokeLinejoin="round"
|
|
83
|
+
strokeWidth={2}
|
|
84
|
+
d="M19 9l-7 7-7-7"
|
|
85
|
+
/>
|
|
86
|
+
</svg>
|
|
87
|
+
</button>
|
|
88
|
+
{isOpen && (
|
|
89
|
+
<div
|
|
90
|
+
className="px-6 pb-4 pt-2"
|
|
91
|
+
style={{
|
|
92
|
+
color: colors.text,
|
|
93
|
+
opacity: 0.8,
|
|
94
|
+
fontFamily: typography?.bodyFont || 'inherit'
|
|
95
|
+
}}
|
|
96
|
+
{...(enableInlineEditing && {
|
|
97
|
+
'data-editable': true,
|
|
98
|
+
'data-section-id': sectionId,
|
|
99
|
+
'data-field-path': `settings.faqs.${index}.answer`
|
|
100
|
+
})}
|
|
101
|
+
>
|
|
102
|
+
{faq.answer || 'Add answer...'}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function FAQSection({
|
|
110
|
+
title = 'Frequently Asked Questions',
|
|
111
|
+
faqs = [],
|
|
112
|
+
layout = 'accordion',
|
|
113
|
+
maxWidth = 'large',
|
|
114
|
+
colors,
|
|
115
|
+
typography,
|
|
116
|
+
enableInlineEditing = false,
|
|
117
|
+
sectionId = ''
|
|
118
|
+
}: FAQSectionProps) {
|
|
119
|
+
// Max width classes
|
|
120
|
+
const maxWidthMap = {
|
|
121
|
+
full: 'max-w-full',
|
|
122
|
+
large: 'max-w-4xl',
|
|
123
|
+
medium: 'max-w-3xl',
|
|
124
|
+
small: 'max-w-2xl'
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className="container mx-auto px-4">
|
|
129
|
+
<div className={`${maxWidthMap[maxWidth]} mx-auto`}>
|
|
130
|
+
{/* Title */}
|
|
131
|
+
{title && (
|
|
132
|
+
<h2
|
|
133
|
+
className="text-3xl font-bold text-center mb-8"
|
|
134
|
+
style={{
|
|
135
|
+
color: colors.text,
|
|
136
|
+
fontFamily: typography?.headingFont || 'inherit'
|
|
137
|
+
}}
|
|
138
|
+
{...(enableInlineEditing && {
|
|
139
|
+
'data-editable': true,
|
|
140
|
+
'data-section-id': sectionId,
|
|
141
|
+
'data-field-path': 'settings.title'
|
|
142
|
+
})}
|
|
143
|
+
>
|
|
144
|
+
{title}
|
|
145
|
+
</h2>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{/* FAQ Items */}
|
|
149
|
+
{faqs.length > 0 ? (
|
|
150
|
+
<div
|
|
151
|
+
className="rounded-lg overflow-hidden"
|
|
152
|
+
style={{
|
|
153
|
+
border: `1px solid ${colors.text}20`,
|
|
154
|
+
backgroundColor: `${colors.primary}05`
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
{faqs.map((faq, index) => (
|
|
158
|
+
layout === 'accordion' ? (
|
|
159
|
+
<FAQAccordionItem
|
|
160
|
+
key={index}
|
|
161
|
+
faq={faq}
|
|
162
|
+
index={index}
|
|
163
|
+
colors={colors}
|
|
164
|
+
typography={typography}
|
|
165
|
+
enableInlineEditing={enableInlineEditing}
|
|
166
|
+
sectionId={sectionId}
|
|
167
|
+
/>
|
|
168
|
+
) : (
|
|
169
|
+
// Stacked layout (always expanded)
|
|
170
|
+
<div
|
|
171
|
+
key={index}
|
|
172
|
+
className="border-b border-opacity-10 last:border-b-0 p-6"
|
|
173
|
+
style={{ borderColor: colors.text }}
|
|
174
|
+
>
|
|
175
|
+
<h3
|
|
176
|
+
className="font-semibold text-lg mb-2"
|
|
177
|
+
style={{
|
|
178
|
+
color: colors.text,
|
|
179
|
+
fontFamily: typography?.headingFont || 'inherit'
|
|
180
|
+
}}
|
|
181
|
+
{...(enableInlineEditing && {
|
|
182
|
+
'data-editable': true,
|
|
183
|
+
'data-section-id': sectionId,
|
|
184
|
+
'data-field-path': `settings.faqs.${index}.question`
|
|
185
|
+
})}
|
|
186
|
+
>
|
|
187
|
+
{faq.question || 'Add question...'}
|
|
188
|
+
</h3>
|
|
189
|
+
<p
|
|
190
|
+
style={{
|
|
191
|
+
color: colors.text,
|
|
192
|
+
opacity: 0.8,
|
|
193
|
+
fontFamily: typography?.bodyFont || 'inherit'
|
|
194
|
+
}}
|
|
195
|
+
{...(enableInlineEditing && {
|
|
196
|
+
'data-editable': true,
|
|
197
|
+
'data-section-id': sectionId,
|
|
198
|
+
'data-field-path': `settings.faqs.${index}.answer`
|
|
199
|
+
})}
|
|
200
|
+
>
|
|
201
|
+
{faq.answer || 'Add answer...'}
|
|
202
|
+
</p>
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
) : (
|
|
208
|
+
// Empty state
|
|
209
|
+
<div
|
|
210
|
+
className="p-12 rounded-lg border-2 border-dashed text-center"
|
|
211
|
+
style={{
|
|
212
|
+
borderColor: `${colors.primary}30`,
|
|
213
|
+
backgroundColor: `${colors.primary}05`
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
<svg
|
|
217
|
+
className="w-12 h-12 mx-auto mb-4"
|
|
218
|
+
fill="none"
|
|
219
|
+
stroke={colors.primary}
|
|
220
|
+
viewBox="0 0 24 24"
|
|
221
|
+
>
|
|
222
|
+
<path
|
|
223
|
+
strokeLinecap="round"
|
|
224
|
+
strokeLinejoin="round"
|
|
225
|
+
strokeWidth={2}
|
|
226
|
+
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
227
|
+
/>
|
|
228
|
+
</svg>
|
|
229
|
+
<p style={{ color: colors.text, opacity: 0.6 }}>
|
|
230
|
+
Add FAQ items to get started
|
|
231
|
+
</p>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export default FAQSection;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export interface FeatureContentProps {
|
|
4
|
+
headline?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
image?: string;
|
|
7
|
+
buttonText?: string;
|
|
8
|
+
buttonUrl?: string;
|
|
9
|
+
layout?: 'image-left' | 'image-right' | 'image-top';
|
|
10
|
+
showButton?: boolean;
|
|
11
|
+
colors: {
|
|
12
|
+
primary: string;
|
|
13
|
+
text: string;
|
|
14
|
+
};
|
|
15
|
+
typography: {
|
|
16
|
+
headingFont?: string;
|
|
17
|
+
};
|
|
18
|
+
enableInlineEditing?: boolean;
|
|
19
|
+
sectionId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function FeatureContent({
|
|
23
|
+
headline,
|
|
24
|
+
description,
|
|
25
|
+
image,
|
|
26
|
+
buttonText,
|
|
27
|
+
buttonUrl,
|
|
28
|
+
layout = 'image-right',
|
|
29
|
+
showButton = true,
|
|
30
|
+
colors,
|
|
31
|
+
typography,
|
|
32
|
+
enableInlineEditing = false,
|
|
33
|
+
sectionId = ''
|
|
34
|
+
}: FeatureContentProps) {
|
|
35
|
+
const imageSection = (
|
|
36
|
+
<div className="w-full h-full aspect-video rounded-lg overflow-hidden bg-gray-100">
|
|
37
|
+
{image ? (
|
|
38
|
+
<img
|
|
39
|
+
src={image}
|
|
40
|
+
alt={headline || 'Feature image'}
|
|
41
|
+
className="w-full h-full object-cover"
|
|
42
|
+
/>
|
|
43
|
+
) : (
|
|
44
|
+
<div
|
|
45
|
+
className="w-full h-full flex items-center justify-center"
|
|
46
|
+
style={{ backgroundColor: colors.primary, opacity: 0.2 }}
|
|
47
|
+
>
|
|
48
|
+
<span className="text-sm opacity-50">Add image</span>
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const contentSection = (
|
|
55
|
+
<div className="space-y-4">
|
|
56
|
+
<h2
|
|
57
|
+
className="text-3xl font-bold"
|
|
58
|
+
style={{
|
|
59
|
+
fontFamily: typography.headingFont,
|
|
60
|
+
color: colors.text
|
|
61
|
+
}}
|
|
62
|
+
{...(enableInlineEditing && {
|
|
63
|
+
'data-editable': true,
|
|
64
|
+
'data-section-id': sectionId,
|
|
65
|
+
'data-field-path': 'settings.headline'
|
|
66
|
+
})}
|
|
67
|
+
>
|
|
68
|
+
{headline || 'Feature Headline'}
|
|
69
|
+
</h2>
|
|
70
|
+
<p
|
|
71
|
+
className="text-lg opacity-80"
|
|
72
|
+
style={{ color: colors.text }}
|
|
73
|
+
{...(enableInlineEditing && {
|
|
74
|
+
'data-editable': true,
|
|
75
|
+
'data-section-id': sectionId,
|
|
76
|
+
'data-field-path': 'settings.description'
|
|
77
|
+
})}
|
|
78
|
+
>
|
|
79
|
+
{description || 'Feature description goes here...'}
|
|
80
|
+
</p>
|
|
81
|
+
{showButton && (
|
|
82
|
+
<a
|
|
83
|
+
href={buttonUrl || '#'}
|
|
84
|
+
className="inline-block px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
|
|
85
|
+
style={{
|
|
86
|
+
backgroundColor: colors.primary,
|
|
87
|
+
color: '#FFFFFF'
|
|
88
|
+
}}
|
|
89
|
+
{...(enableInlineEditing && {
|
|
90
|
+
'data-editable': true,
|
|
91
|
+
'data-section-id': sectionId,
|
|
92
|
+
'data-field-path': 'settings.buttonText'
|
|
93
|
+
})}
|
|
94
|
+
>
|
|
95
|
+
{buttonText || 'Learn More'}
|
|
96
|
+
</a>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className="container mx-auto px-4 py-16">
|
|
103
|
+
<div className={`grid md:grid-cols-2 gap-12 items-center ${layout === 'image-top' ? 'grid-cols-1' : ''}`}>
|
|
104
|
+
{layout === 'image-left' && imageSection}
|
|
105
|
+
{contentSection}
|
|
106
|
+
{layout === 'image-right' && imageSection}
|
|
107
|
+
{layout === 'image-top' && imageSection}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default FeatureContent;
|