@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,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export interface NavLink {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
url: string;
|
|
7
|
+
linkType?: 'internal' | 'external';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MinimalNavigationProps {
|
|
11
|
+
links: NavLink[];
|
|
12
|
+
style?: 'horizontal' | 'vertical';
|
|
13
|
+
colors: {
|
|
14
|
+
text: string;
|
|
15
|
+
};
|
|
16
|
+
enableInlineEditing?: boolean;
|
|
17
|
+
sectionId?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function MinimalNavigation({
|
|
21
|
+
links,
|
|
22
|
+
style = 'horizontal',
|
|
23
|
+
colors,
|
|
24
|
+
enableInlineEditing = false,
|
|
25
|
+
sectionId = ''
|
|
26
|
+
}: MinimalNavigationProps) {
|
|
27
|
+
const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, url: string) => {
|
|
28
|
+
if (url.startsWith('#')) {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
const sectionId = url.substring(1);
|
|
31
|
+
const element = document.querySelector(`[data-section-id="${sectionId}"]`);
|
|
32
|
+
if (element) {
|
|
33
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<nav className="container mx-auto px-4 py-4">
|
|
40
|
+
<div className={`flex ${style === 'vertical' ? 'flex-col space-y-2' : 'flex-row space-x-6 justify-center'}`}>
|
|
41
|
+
{links.map((link, index) => (
|
|
42
|
+
<a
|
|
43
|
+
key={link.id}
|
|
44
|
+
href={link.url}
|
|
45
|
+
onClick={(e) => handleNavClick(e, link.url)}
|
|
46
|
+
className="text-base font-medium transition-colors hover:opacity-70"
|
|
47
|
+
style={{ color: colors.text }}
|
|
48
|
+
{...(link.linkType === 'external' && !link.url.startsWith('#') ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
49
|
+
{...(enableInlineEditing && {
|
|
50
|
+
'data-editable': true,
|
|
51
|
+
'data-section-id': sectionId,
|
|
52
|
+
'data-field-path': `settings.links.${index}.label`
|
|
53
|
+
})}
|
|
54
|
+
>
|
|
55
|
+
{link.label}
|
|
56
|
+
</a>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
</nav>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default MinimalNavigation;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface NavbarLink {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
url: string;
|
|
9
|
+
linkType?: 'internal' | 'external';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface NavbarProps {
|
|
13
|
+
brandName?: string;
|
|
14
|
+
logoUrl?: string;
|
|
15
|
+
showLogo?: boolean;
|
|
16
|
+
showBrandName?: boolean;
|
|
17
|
+
links: NavbarLink[];
|
|
18
|
+
ctaText?: string;
|
|
19
|
+
ctaUrl?: string;
|
|
20
|
+
sticky?: boolean;
|
|
21
|
+
transparentOnTop?: boolean;
|
|
22
|
+
colors: {
|
|
23
|
+
primary: string;
|
|
24
|
+
text: string;
|
|
25
|
+
buttonText?: string;
|
|
26
|
+
};
|
|
27
|
+
typography?: {
|
|
28
|
+
headingFont?: string;
|
|
29
|
+
bodyFont?: string;
|
|
30
|
+
};
|
|
31
|
+
enableInlineEditing?: boolean;
|
|
32
|
+
sectionId?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function Navbar({
|
|
36
|
+
brandName = 'Your Brand',
|
|
37
|
+
logoUrl,
|
|
38
|
+
showLogo = true,
|
|
39
|
+
showBrandName = true,
|
|
40
|
+
links = [],
|
|
41
|
+
ctaText,
|
|
42
|
+
ctaUrl,
|
|
43
|
+
sticky = true,
|
|
44
|
+
transparentOnTop = false,
|
|
45
|
+
colors,
|
|
46
|
+
typography,
|
|
47
|
+
enableInlineEditing = false,
|
|
48
|
+
sectionId = ''
|
|
49
|
+
}: NavbarProps) {
|
|
50
|
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
51
|
+
const [scrolled, setScrolled] = useState(false);
|
|
52
|
+
const [activeSection, setActiveSection] = useState('');
|
|
53
|
+
|
|
54
|
+
// Handle scroll for sticky navbar effects
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const handleScroll = () => {
|
|
57
|
+
setScrolled(window.scrollY > 20);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
window.addEventListener('scroll', handleScroll);
|
|
61
|
+
return () => window.removeEventListener('scroll', handleScroll);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
// Track active section for navigation highlighting
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const handleScroll = () => {
|
|
67
|
+
const sections = links
|
|
68
|
+
.filter(link => link.url.startsWith('#'))
|
|
69
|
+
.map(link => document.querySelector(`[data-section-id="${link.url.substring(1)}"]`))
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
|
|
72
|
+
let currentSection = '';
|
|
73
|
+
sections.forEach((section) => {
|
|
74
|
+
if (section) {
|
|
75
|
+
const rect = section.getBoundingClientRect();
|
|
76
|
+
if (rect.top <= 100 && rect.bottom >= 100) {
|
|
77
|
+
currentSection = section.getAttribute('data-section-id') || '';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
setActiveSection(currentSection);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
window.addEventListener('scroll', handleScroll);
|
|
86
|
+
return () => window.removeEventListener('scroll', handleScroll);
|
|
87
|
+
}, [links]);
|
|
88
|
+
|
|
89
|
+
const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, url: string) => {
|
|
90
|
+
if (url.startsWith('#')) {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
const sectionId = url.substring(1);
|
|
93
|
+
const element = document.querySelector(`[data-section-id="${sectionId}"]`);
|
|
94
|
+
if (element) {
|
|
95
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
96
|
+
}
|
|
97
|
+
setMobileMenuOpen(false);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const isTransparent = transparentOnTop && !scrolled;
|
|
102
|
+
const navbarBg = isTransparent
|
|
103
|
+
? 'bg-transparent'
|
|
104
|
+
: sticky
|
|
105
|
+
? 'bg-white/80 backdrop-blur-md border-b'
|
|
106
|
+
: 'bg-white border-b';
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<nav
|
|
110
|
+
className={`${sticky ? 'sticky top-0 z-50' : ''} ${navbarBg} transition-all duration-300`}
|
|
111
|
+
style={{
|
|
112
|
+
borderColor: isTransparent ? 'transparent' : `${colors.primary}20`
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
<div className="container mx-auto px-4">
|
|
116
|
+
<div className="flex items-center justify-between h-16 md:h-20">
|
|
117
|
+
{/* Logo & Brand */}
|
|
118
|
+
<div className="flex items-center gap-3">
|
|
119
|
+
{showLogo && logoUrl && (
|
|
120
|
+
<img
|
|
121
|
+
src={logoUrl}
|
|
122
|
+
alt={brandName}
|
|
123
|
+
className="h-8 md:h-10 w-auto object-contain"
|
|
124
|
+
/>
|
|
125
|
+
)}
|
|
126
|
+
{showBrandName && (
|
|
127
|
+
<h1
|
|
128
|
+
className="text-xl md:text-2xl font-bold"
|
|
129
|
+
style={{
|
|
130
|
+
fontFamily: typography?.headingFont || 'inherit',
|
|
131
|
+
color: colors.text
|
|
132
|
+
}}
|
|
133
|
+
{...(enableInlineEditing && {
|
|
134
|
+
'data-editable': true,
|
|
135
|
+
'data-section-id': sectionId,
|
|
136
|
+
'data-field-path': 'settings.brandName'
|
|
137
|
+
})}
|
|
138
|
+
>
|
|
139
|
+
{brandName}
|
|
140
|
+
</h1>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Desktop Navigation */}
|
|
145
|
+
<div className="hidden md:flex items-center gap-8">
|
|
146
|
+
{links.map((link, index) => {
|
|
147
|
+
const isActive = link.url.startsWith('#') && link.url.substring(1) === activeSection;
|
|
148
|
+
return (
|
|
149
|
+
<a
|
|
150
|
+
key={link.id}
|
|
151
|
+
href={link.url}
|
|
152
|
+
onClick={(e) => handleNavClick(e, link.url)}
|
|
153
|
+
className="relative text-base font-medium transition-colors group"
|
|
154
|
+
style={{
|
|
155
|
+
color: isActive ? colors.primary : colors.text,
|
|
156
|
+
fontFamily: typography?.bodyFont || 'inherit'
|
|
157
|
+
}}
|
|
158
|
+
{...(link.linkType === 'external' && !link.url.startsWith('#') ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
159
|
+
{...(enableInlineEditing && {
|
|
160
|
+
'data-editable': true,
|
|
161
|
+
'data-section-id': sectionId,
|
|
162
|
+
'data-field-path': `settings.links.${index}.label`
|
|
163
|
+
})}
|
|
164
|
+
>
|
|
165
|
+
{link.label}
|
|
166
|
+
{/* Active indicator */}
|
|
167
|
+
<span
|
|
168
|
+
className={`absolute -bottom-1 left-0 h-0.5 transition-all ${isActive ? 'w-full' : 'w-0 group-hover:w-full'}`}
|
|
169
|
+
style={{ backgroundColor: colors.primary }}
|
|
170
|
+
/>
|
|
171
|
+
</a>
|
|
172
|
+
);
|
|
173
|
+
})}
|
|
174
|
+
|
|
175
|
+
{/* CTA Button */}
|
|
176
|
+
{ctaText && ctaUrl && (
|
|
177
|
+
<a
|
|
178
|
+
href={ctaUrl}
|
|
179
|
+
className="px-6 py-2.5 rounded-full font-semibold transition-all hover:opacity-90 hover:scale-105"
|
|
180
|
+
style={{
|
|
181
|
+
backgroundColor: colors.primary,
|
|
182
|
+
color: colors.buttonText || '#FFFFFF',
|
|
183
|
+
fontFamily: typography?.bodyFont || 'inherit'
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{ctaText}
|
|
187
|
+
</a>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{/* Mobile Menu Button */}
|
|
192
|
+
<button
|
|
193
|
+
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
194
|
+
className="md:hidden p-2 rounded-lg transition-colors hover:bg-black/5"
|
|
195
|
+
aria-label="Toggle menu"
|
|
196
|
+
>
|
|
197
|
+
<svg
|
|
198
|
+
className="w-6 h-6"
|
|
199
|
+
fill="none"
|
|
200
|
+
stroke={colors.text}
|
|
201
|
+
viewBox="0 0 24 24"
|
|
202
|
+
>
|
|
203
|
+
{mobileMenuOpen ? (
|
|
204
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
205
|
+
) : (
|
|
206
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
207
|
+
)}
|
|
208
|
+
</svg>
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{/* Mobile Menu */}
|
|
213
|
+
<div
|
|
214
|
+
className={`md:hidden overflow-hidden transition-all duration-300 ${mobileMenuOpen ? 'max-h-96 pb-4' : 'max-h-0'}`}
|
|
215
|
+
>
|
|
216
|
+
<div className="flex flex-col space-y-3 pt-4 border-t" style={{ borderColor: `${colors.primary}20` }}>
|
|
217
|
+
{links.map((link, index) => {
|
|
218
|
+
const isActive = link.url.startsWith('#') && link.url.substring(1) === activeSection;
|
|
219
|
+
return (
|
|
220
|
+
<a
|
|
221
|
+
key={link.id}
|
|
222
|
+
href={link.url}
|
|
223
|
+
onClick={(e) => handleNavClick(e, link.url)}
|
|
224
|
+
className="text-base font-medium py-2 px-4 rounded-lg transition-colors"
|
|
225
|
+
style={{
|
|
226
|
+
color: isActive ? colors.primary : colors.text,
|
|
227
|
+
backgroundColor: isActive ? `${colors.primary}10` : 'transparent',
|
|
228
|
+
fontFamily: typography?.bodyFont || 'inherit'
|
|
229
|
+
}}
|
|
230
|
+
{...(link.linkType === 'external' && !link.url.startsWith('#') ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
231
|
+
>
|
|
232
|
+
{link.label}
|
|
233
|
+
</a>
|
|
234
|
+
);
|
|
235
|
+
})}
|
|
236
|
+
|
|
237
|
+
{/* Mobile CTA Button */}
|
|
238
|
+
{ctaText && ctaUrl && (
|
|
239
|
+
<a
|
|
240
|
+
href={ctaUrl}
|
|
241
|
+
className="px-6 py-3 rounded-full font-semibold text-center transition-all hover:opacity-90"
|
|
242
|
+
style={{
|
|
243
|
+
backgroundColor: colors.primary,
|
|
244
|
+
color: colors.buttonText || '#FFFFFF',
|
|
245
|
+
fontFamily: typography?.bodyFont || 'inherit'
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
{ctaText}
|
|
249
|
+
</a>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</nav>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export default Navbar;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export interface PricingPlan {
|
|
4
|
+
id: string | number;
|
|
5
|
+
name: string;
|
|
6
|
+
price: number;
|
|
7
|
+
features: string[];
|
|
8
|
+
featured?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PricingTableProps {
|
|
12
|
+
title?: string;
|
|
13
|
+
plans?: PricingPlan[];
|
|
14
|
+
colors: {
|
|
15
|
+
primary: string;
|
|
16
|
+
text: string;
|
|
17
|
+
};
|
|
18
|
+
typography: {
|
|
19
|
+
headingFont?: string;
|
|
20
|
+
};
|
|
21
|
+
enableInlineEditing?: boolean;
|
|
22
|
+
sectionId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function PricingTable({
|
|
26
|
+
title,
|
|
27
|
+
plans = [],
|
|
28
|
+
colors,
|
|
29
|
+
typography,
|
|
30
|
+
enableInlineEditing = false,
|
|
31
|
+
sectionId = ''
|
|
32
|
+
}: PricingTableProps) {
|
|
33
|
+
const mockPlans = plans.length > 0 ? plans : [
|
|
34
|
+
{ id: 1, name: 'Basic', price: 50, features: ['Service 1', 'Service 2'] },
|
|
35
|
+
{ id: 2, name: 'Premium', price: 100, features: ['Service 1', 'Service 2', 'Service 3'], featured: true },
|
|
36
|
+
{ id: 3, name: 'Deluxe', price: 150, features: ['All Services', 'Priority Booking'] }
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<section className="py-16 px-6">
|
|
41
|
+
<div className="max-w-7xl mx-auto">
|
|
42
|
+
<h2
|
|
43
|
+
className="text-3xl md:text-4xl font-bold text-center mb-12"
|
|
44
|
+
style={{ fontFamily: typography.headingFont, color: colors.text }}
|
|
45
|
+
{...(enableInlineEditing && {
|
|
46
|
+
'data-editable': true,
|
|
47
|
+
'data-section-id': sectionId,
|
|
48
|
+
'data-field-path': 'settings.title'
|
|
49
|
+
})}
|
|
50
|
+
>
|
|
51
|
+
{title || 'Pricing'}
|
|
52
|
+
</h2>
|
|
53
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
54
|
+
{mockPlans.map((plan) => (
|
|
55
|
+
<div
|
|
56
|
+
key={plan.id}
|
|
57
|
+
className={`p-8 rounded-lg border-2 ${
|
|
58
|
+
plan.featured ? 'scale-105 shadow-xl' : ''
|
|
59
|
+
}`}
|
|
60
|
+
style={{
|
|
61
|
+
borderColor: plan.featured ? colors.primary : 'rgba(255,255,255,0.1)',
|
|
62
|
+
backgroundColor: plan.featured ? `${colors.primary}15` : 'rgba(255,255,255,0.05)'
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<h3
|
|
66
|
+
className="text-2xl font-bold mb-2 text-center"
|
|
67
|
+
style={{ color: colors.text }}
|
|
68
|
+
>
|
|
69
|
+
{plan.name}
|
|
70
|
+
</h3>
|
|
71
|
+
<div className="text-center mb-6">
|
|
72
|
+
<span
|
|
73
|
+
className="text-4xl font-bold"
|
|
74
|
+
style={{ color: colors.primary }}
|
|
75
|
+
>
|
|
76
|
+
${plan.price}
|
|
77
|
+
</span>
|
|
78
|
+
</div>
|
|
79
|
+
<ul className="space-y-3 mb-6">
|
|
80
|
+
{plan.features.map((feature, i) => (
|
|
81
|
+
<li key={i} className="flex items-center gap-2">
|
|
82
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" style={{ color: colors.primary }}>
|
|
83
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
84
|
+
</svg>
|
|
85
|
+
<span style={{ color: colors.text }}>{feature}</span>
|
|
86
|
+
</li>
|
|
87
|
+
))}
|
|
88
|
+
</ul>
|
|
89
|
+
<button
|
|
90
|
+
className="w-full px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
|
|
91
|
+
style={{
|
|
92
|
+
backgroundColor: plan.featured ? colors.primary : 'rgba(255,255,255,0.1)',
|
|
93
|
+
color: plan.featured ? '#FFFFFF' : colors.text
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
Choose Plan
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</section>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default PricingTable;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface ScrollingTextDividerProps {
|
|
6
|
+
text: string;
|
|
7
|
+
scrollDirection?: 'left' | 'right';
|
|
8
|
+
scrollSpeed?: 'slow' | 'medium' | 'fast' | number;
|
|
9
|
+
orientation?: 'horizontal' | 'diagonal-left' | 'diagonal-right';
|
|
10
|
+
dividerIcon?: 'bullet' | 'star' | 'diamond' | 'custom' | 'none';
|
|
11
|
+
customIcon?: string;
|
|
12
|
+
textSize?: 'small' | 'medium' | 'large' | 'xl';
|
|
13
|
+
textColor?: string;
|
|
14
|
+
fontWeight?: 'light' | 'normal' | 'bold';
|
|
15
|
+
backgroundColor?: string;
|
|
16
|
+
backgroundGradient?: string;
|
|
17
|
+
pauseOnHover?: boolean;
|
|
18
|
+
colors: {
|
|
19
|
+
primary: string;
|
|
20
|
+
text: string;
|
|
21
|
+
};
|
|
22
|
+
typography?: {
|
|
23
|
+
headingFont?: string;
|
|
24
|
+
};
|
|
25
|
+
enableInlineEditing?: boolean;
|
|
26
|
+
sectionId?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ScrollingTextDivider({
|
|
30
|
+
text = 'SCROLL TEXT',
|
|
31
|
+
scrollDirection = 'left',
|
|
32
|
+
scrollSpeed = 'medium',
|
|
33
|
+
orientation = 'horizontal',
|
|
34
|
+
dividerIcon = 'bullet',
|
|
35
|
+
customIcon,
|
|
36
|
+
textSize = 'medium',
|
|
37
|
+
textColor,
|
|
38
|
+
fontWeight = 'bold',
|
|
39
|
+
backgroundColor,
|
|
40
|
+
backgroundGradient,
|
|
41
|
+
pauseOnHover = true,
|
|
42
|
+
colors,
|
|
43
|
+
typography,
|
|
44
|
+
enableInlineEditing = false,
|
|
45
|
+
sectionId = ''
|
|
46
|
+
}: ScrollingTextDividerProps) {
|
|
47
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
48
|
+
|
|
49
|
+
// Icon mapping
|
|
50
|
+
const iconMap = {
|
|
51
|
+
bullet: '•',
|
|
52
|
+
star: '★',
|
|
53
|
+
diamond: '◆',
|
|
54
|
+
custom: customIcon || '•',
|
|
55
|
+
none: ''
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const icon = iconMap[dividerIcon];
|
|
59
|
+
|
|
60
|
+
// Speed mapping (duration in seconds)
|
|
61
|
+
const speedMap = {
|
|
62
|
+
slow: 60,
|
|
63
|
+
medium: 30,
|
|
64
|
+
fast: 15
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const duration = typeof scrollSpeed === 'number' ? scrollSpeed : speedMap[scrollSpeed];
|
|
68
|
+
|
|
69
|
+
// Text size mapping
|
|
70
|
+
const textSizeMap = {
|
|
71
|
+
small: 'text-xl md:text-2xl',
|
|
72
|
+
medium: 'text-2xl md:text-4xl',
|
|
73
|
+
large: 'text-3xl md:text-5xl',
|
|
74
|
+
xl: 'text-4xl md:text-6xl'
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Font weight mapping
|
|
78
|
+
const fontWeightMap = {
|
|
79
|
+
light: 'font-light',
|
|
80
|
+
normal: 'font-normal',
|
|
81
|
+
bold: 'font-bold'
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Orientation angle
|
|
85
|
+
const rotationMap = {
|
|
86
|
+
horizontal: '0deg',
|
|
87
|
+
'diagonal-left': '-5deg',
|
|
88
|
+
'diagonal-right': '5deg'
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Create repeating text with icons
|
|
92
|
+
const repeatingText = icon
|
|
93
|
+
? `${text} ${icon} `.repeat(20)
|
|
94
|
+
: `${text} `.repeat(20);
|
|
95
|
+
|
|
96
|
+
// Background style
|
|
97
|
+
const bgStyle = backgroundGradient
|
|
98
|
+
? { background: backgroundGradient }
|
|
99
|
+
: { backgroundColor: backgroundColor || 'transparent' };
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
className="relative overflow-hidden py-3"
|
|
104
|
+
style={{
|
|
105
|
+
...bgStyle,
|
|
106
|
+
transform: `rotate(${rotationMap[orientation]})`
|
|
107
|
+
}}
|
|
108
|
+
onMouseEnter={() => pauseOnHover && setIsPaused(true)}
|
|
109
|
+
onMouseLeave={() => pauseOnHover && setIsPaused(false)}
|
|
110
|
+
>
|
|
111
|
+
<style>{`
|
|
112
|
+
@keyframes scroll-${scrollDirection} {
|
|
113
|
+
0% {
|
|
114
|
+
transform: translateX(${scrollDirection === 'left' ? '0%' : '-50%'});
|
|
115
|
+
}
|
|
116
|
+
100% {
|
|
117
|
+
transform: translateX(${scrollDirection === 'left' ? '-50%' : '0%'});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
`}</style>
|
|
121
|
+
|
|
122
|
+
<div
|
|
123
|
+
className={`flex items-center whitespace-nowrap ${textSizeMap[textSize]} ${fontWeightMap[fontWeight]} uppercase tracking-wider`}
|
|
124
|
+
style={{
|
|
125
|
+
fontFamily: typography?.headingFont || 'inherit',
|
|
126
|
+
color: textColor || colors.text,
|
|
127
|
+
animation: `scroll-${scrollDirection} ${duration}s linear infinite`,
|
|
128
|
+
animationPlayState: isPaused ? 'paused' : 'running'
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<span className="inline-block">{repeatingText}</span>
|
|
132
|
+
<span className="inline-block" aria-hidden="true">{repeatingText}</span>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default ScrollingTextDivider;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface ScrollingTextDividerProps {
|
|
6
|
+
text: string;
|
|
7
|
+
scrollDirection?: 'left' | 'right';
|
|
8
|
+
scrollSpeed?: 'slow' | 'medium' | 'fast' | number;
|
|
9
|
+
orientation?: 'horizontal' | 'diagonal-left' | 'diagonal-right';
|
|
10
|
+
dividerIcon?: 'bullet' | 'star' | 'diamond' | 'custom' | 'none';
|
|
11
|
+
customIcon?: string;
|
|
12
|
+
textSize?: 'small' | 'medium' | 'large' | 'xl';
|
|
13
|
+
textColor?: string;
|
|
14
|
+
fontWeight?: 'light' | 'normal' | 'bold';
|
|
15
|
+
backgroundColor?: string;
|
|
16
|
+
backgroundGradient?: string;
|
|
17
|
+
pauseOnHover?: boolean;
|
|
18
|
+
colors: {
|
|
19
|
+
primary: string;
|
|
20
|
+
text: string;
|
|
21
|
+
};
|
|
22
|
+
typography?: {
|
|
23
|
+
headingFont?: string;
|
|
24
|
+
};
|
|
25
|
+
enableInlineEditing?: boolean;
|
|
26
|
+
sectionId?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ScrollingTextDivider({
|
|
30
|
+
text = 'SCROLL TEXT',
|
|
31
|
+
scrollDirection = 'left',
|
|
32
|
+
scrollSpeed = 'medium',
|
|
33
|
+
orientation = 'horizontal',
|
|
34
|
+
dividerIcon = 'bullet',
|
|
35
|
+
customIcon,
|
|
36
|
+
textSize = 'medium',
|
|
37
|
+
textColor,
|
|
38
|
+
fontWeight = 'bold',
|
|
39
|
+
backgroundColor,
|
|
40
|
+
backgroundGradient,
|
|
41
|
+
pauseOnHover = true,
|
|
42
|
+
colors,
|
|
43
|
+
typography,
|
|
44
|
+
enableInlineEditing = false,
|
|
45
|
+
sectionId = ''
|
|
46
|
+
}: ScrollingTextDividerProps) {
|
|
47
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
48
|
+
|
|
49
|
+
// Icon mapping
|
|
50
|
+
const iconMap = {
|
|
51
|
+
bullet: '"',
|
|
52
|
+
star: '',
|
|
53
|
+
diamond: '�',
|
|
54
|
+
custom: customIcon || '"',
|
|
55
|
+
none: ''
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const icon = iconMap[dividerIcon];
|
|
59
|
+
|
|
60
|
+
// Speed mapping (duration in seconds)
|
|
61
|
+
const speedMap = {
|
|
62
|
+
slow: 60,
|
|
63
|
+
medium: 30,
|
|
64
|
+
fast: 15
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const duration = typeof scrollSpeed === 'number' ? scrollSpeed : speedMap[scrollSpeed];
|
|
68
|
+
|
|
69
|
+
// Text size mapping
|
|
70
|
+
const textSizeMap = {
|
|
71
|
+
small: 'text-xl md:text-2xl',
|
|
72
|
+
medium: 'text-2xl md:text-4xl',
|
|
73
|
+
large: 'text-3xl md:text-5xl',
|
|
74
|
+
xl: 'text-4xl md:text-6xl'
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Font weight mapping
|
|
78
|
+
const fontWeightMap = {
|
|
79
|
+
light: 'font-light',
|
|
80
|
+
normal: 'font-normal',
|
|
81
|
+
bold: 'font-bold'
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Orientation angle
|
|
85
|
+
const rotationMap = {
|
|
86
|
+
horizontal: '0deg',
|
|
87
|
+
'diagonal-left': '-5deg',
|
|
88
|
+
'diagonal-right': '5deg'
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Create repeating text with icons
|
|
92
|
+
const repeatingText = icon
|
|
93
|
+
? `${text} ${icon} `.repeat(20)
|
|
94
|
+
: `${text} `.repeat(20);
|
|
95
|
+
|
|
96
|
+
// Background style
|
|
97
|
+
const bgStyle = backgroundGradient
|
|
98
|
+
? { background: backgroundGradient }
|
|
99
|
+
: { backgroundColor: backgroundColor || 'transparent' };
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
className="relative overflow-hidden py-3"
|
|
104
|
+
style={{
|
|
105
|
+
...bgStyle,
|
|
106
|
+
transform: `rotate(${rotationMap[orientation]})`
|
|
107
|
+
}}
|
|
108
|
+
onMouseEnter={() => pauseOnHover && setIsPaused(true)}
|
|
109
|
+
onMouseLeave={() => pauseOnHover && setIsPaused(false)}
|
|
110
|
+
>
|
|
111
|
+
<style>{`
|
|
112
|
+
@keyframes scroll-${scrollDirection} {
|
|
113
|
+
0% {
|
|
114
|
+
transform: translateX(${scrollDirection === 'left' ? '0%' : '-50%'});
|
|
115
|
+
}
|
|
116
|
+
100% {
|
|
117
|
+
transform: translateX(${scrollDirection === 'left' ? '-50%' : '0%'});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
`}</style>
|
|
121
|
+
|
|
122
|
+
<div
|
|
123
|
+
className={`flex items-center whitespace-nowrap ${textSizeMap[textSize]} ${fontWeightMap[fontWeight]} uppercase tracking-wider`}
|
|
124
|
+
style={{
|
|
125
|
+
fontFamily: typography?.headingFont || 'inherit',
|
|
126
|
+
color: textColor || colors.text,
|
|
127
|
+
animation: `scroll-${scrollDirection} ${duration}s linear infinite`,
|
|
128
|
+
animationPlayState: isPaused ? 'paused' : 'running'
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<span className="inline-block">{repeatingText}</span>
|
|
132
|
+
<span className="inline-block" aria-hidden="true">{repeatingText}</span>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default ScrollingTextDivider;
|