keystone-design-bootstrap 1.0.3
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 +179 -0
- package/package.json +59 -0
- package/src/contexts/ThemeContext.tsx +34 -0
- package/src/contexts/index.ts +1 -0
- package/src/design_system/elements/IconComponent.tsx +98 -0
- package/src/design_system/elements/avatar/avatar-label-group.tsx +30 -0
- package/src/design_system/elements/avatar/avatar-profile-photo.tsx +125 -0
- package/src/design_system/elements/avatar/avatar.tsx +131 -0
- package/src/design_system/elements/avatar/base-components/avatar-add-button.tsx +34 -0
- package/src/design_system/elements/avatar/base-components/avatar-company-icon.tsx +26 -0
- package/src/design_system/elements/avatar/base-components/avatar-online-indicator.tsx +31 -0
- package/src/design_system/elements/avatar/base-components/index.tsx +4 -0
- package/src/design_system/elements/avatar/base-components/verified-tick.tsx +34 -0
- package/src/design_system/elements/avatar/utils.ts +12 -0
- package/src/design_system/elements/badges/avatar.tsx +132 -0
- package/src/design_system/elements/badges/badge-groups.tsx +176 -0
- package/src/design_system/elements/badges/badge-types.ts +266 -0
- package/src/design_system/elements/badges/badges.tsx +430 -0
- package/src/design_system/elements/breadcrumb/Breadcrumb.tsx +33 -0
- package/src/design_system/elements/button-group/button-group.tsx +106 -0
- package/src/design_system/elements/buttons/app-store-buttons-outline.tsx +378 -0
- package/src/design_system/elements/buttons/app-store-buttons.tsx +567 -0
- package/src/design_system/elements/buttons/button-utility.tsx +116 -0
- package/src/design_system/elements/buttons/button.aman.tsx +174 -0
- package/src/design_system/elements/buttons/button.tsx +271 -0
- package/src/design_system/elements/buttons/close-button.tsx +42 -0
- package/src/design_system/elements/buttons/round-button.tsx +29 -0
- package/src/design_system/elements/buttons/social-button.tsx +148 -0
- package/src/design_system/elements/buttons/social-logos.tsx +115 -0
- package/src/design_system/elements/carousel/carousel-base.tsx +308 -0
- package/src/design_system/elements/carousel/carousel.tsx +308 -0
- package/src/design_system/elements/checkbox/checkbox.tsx +120 -0
- package/src/design_system/elements/date-picker/calendar.tsx +101 -0
- package/src/design_system/elements/date-picker/cell.tsx +106 -0
- package/src/design_system/elements/date-picker/date-input.tsx +32 -0
- package/src/design_system/elements/date-picker/date-picker.tsx +86 -0
- package/src/design_system/elements/date-picker/date-range-picker.tsx +163 -0
- package/src/design_system/elements/date-picker/range-calendar.tsx +161 -0
- package/src/design_system/elements/date-picker/range-preset.tsx +28 -0
- package/src/design_system/elements/featured-icon/featured-icon.tsx +154 -0
- package/src/design_system/elements/form/form.tsx +10 -0
- package/src/design_system/elements/form/hook-form.tsx +75 -0
- package/src/design_system/elements/hint-text/hint-text.tsx +33 -0
- package/src/design_system/elements/index.tsx +158 -0
- package/src/design_system/elements/input/hint-text.tsx +33 -0
- package/src/design_system/elements/input/input-group.tsx +133 -0
- package/src/design_system/elements/input/input.aman.tsx +172 -0
- package/src/design_system/elements/input/input.tsx +271 -0
- package/src/design_system/elements/input/label.tsx +50 -0
- package/src/design_system/elements/label/label.tsx +50 -0
- package/src/design_system/elements/loading-indicator/loading-indicator.tsx +123 -0
- package/src/design_system/elements/map/GoogleMap.tsx +286 -0
- package/src/design_system/elements/markdown-renderer/MarkdownRenderer.tsx +155 -0
- package/src/design_system/elements/modals/modal.tsx +41 -0
- package/src/design_system/elements/pagination/pagination-base.tsx +378 -0
- package/src/design_system/elements/pagination/pagination-dot.tsx +54 -0
- package/src/design_system/elements/pagination/pagination-line.tsx +50 -0
- package/src/design_system/elements/pagination/pagination.tsx +330 -0
- package/src/design_system/elements/photo-fallback/photo-fallback.tsx +143 -0
- package/src/design_system/elements/progress-indicators/progress-circles.tsx +176 -0
- package/src/design_system/elements/progress-indicators/progress-indicators.tsx +123 -0
- package/src/design_system/elements/progress-indicators/simple-circle.tsx +29 -0
- package/src/design_system/elements/radio-buttons/radio-buttons.tsx +129 -0
- package/src/design_system/elements/rating/rating-badge.tsx +144 -0
- package/src/design_system/elements/rating/rating-stars.tsx +77 -0
- package/src/design_system/elements/select/combobox.tsx +152 -0
- package/src/design_system/elements/select/multi-select.tsx +363 -0
- package/src/design_system/elements/select/popover.tsx +34 -0
- package/src/design_system/elements/select/select-item.tsx +97 -0
- package/src/design_system/elements/select/select-native.tsx +69 -0
- package/src/design_system/elements/select/select.aman.tsx +75 -0
- package/src/design_system/elements/select/select.tsx +146 -0
- package/src/design_system/elements/shared-assets/credit-card/credit-card.tsx +237 -0
- package/src/design_system/elements/shared-assets/credit-card/icons.tsx +75 -0
- package/src/design_system/elements/shared-assets/iphone-mockup.tsx +172 -0
- package/src/design_system/elements/shared-assets/section-divider.tsx +12 -0
- package/src/design_system/elements/slideout-menus/slideout-menu.tsx +122 -0
- package/src/design_system/elements/tabs/tabs.tsx +225 -0
- package/src/design_system/elements/tags/base-components/tag-checkbox.tsx +45 -0
- package/src/design_system/elements/tags/base-components/tag-close-x.tsx +34 -0
- package/src/design_system/elements/tags/tags.tsx +176 -0
- package/src/design_system/elements/textarea/textarea.aman.tsx +52 -0
- package/src/design_system/elements/textarea/textarea.tsx +111 -0
- package/src/design_system/elements/toggle/toggle.tsx +140 -0
- package/src/design_system/elements/tooltip/tooltip.tsx +109 -0
- package/src/design_system/hooks/use-breakpoint.ts +37 -0
- package/src/design_system/hooks/use-resize-observer.ts +68 -0
- package/src/design_system/logo/keystone-logo-minimal.tsx +93 -0
- package/src/design_system/logo/keystone-logo.tsx +22 -0
- package/src/design_system/sections/about-home.aman.tsx +85 -0
- package/src/design_system/sections/about-home.tsx +115 -0
- package/src/design_system/sections/blog-cards.tsx +848 -0
- package/src/design_system/sections/blog-gallery.aman.tsx +77 -0
- package/src/design_system/sections/blog-gallery.tsx +204 -0
- package/src/design_system/sections/blog-home.aman.tsx +84 -0
- package/src/design_system/sections/blog-home.tsx +153 -0
- package/src/design_system/sections/blog-post.aman.tsx +74 -0
- package/src/design_system/sections/blog-post.tsx +301 -0
- package/src/design_system/sections/blog-section.aman.tsx +101 -0
- package/src/design_system/sections/blog-section.tsx +179 -0
- package/src/design_system/sections/contact-home.tsx +25 -0
- package/src/design_system/sections/contact-section.aman.tsx +173 -0
- package/src/design_system/sections/contact-section.tsx +143 -0
- package/src/design_system/sections/faq-grid.aman.tsx +79 -0
- package/src/design_system/sections/faq-grid.tsx +102 -0
- package/src/design_system/sections/faq-home.aman.tsx +92 -0
- package/src/design_system/sections/faq-home.tsx +134 -0
- package/src/design_system/sections/feature-tab.tsx +43 -0
- package/src/design_system/sections/feature-text.tsx +284 -0
- package/src/design_system/sections/footer-home.aman.tsx +62 -0
- package/src/design_system/sections/footer-home.tsx +259 -0
- package/src/design_system/sections/generic-header-component.tsx +103 -0
- package/src/design_system/sections/header-navigation.aman.tsx +360 -0
- package/src/design_system/sections/header-navigation.tsx +334 -0
- package/src/design_system/sections/hero-faq.aman.tsx +38 -0
- package/src/design_system/sections/hero-faq.tsx +55 -0
- package/src/design_system/sections/hero-generic-text.aman.tsx +49 -0
- package/src/design_system/sections/hero-generic-text.tsx +51 -0
- package/src/design_system/sections/hero-home.aman.tsx +84 -0
- package/src/design_system/sections/hero-home.tsx +246 -0
- package/src/design_system/sections/hero-location-detail.aman.tsx +33 -0
- package/src/design_system/sections/hero-location-detail.tsx +72 -0
- package/src/design_system/sections/hero-service-detail.aman.tsx +53 -0
- package/src/design_system/sections/hero-service-detail.tsx +51 -0
- package/src/design_system/sections/hero-social-media.aman.tsx +42 -0
- package/src/design_system/sections/hero-social-media.tsx +35 -0
- package/src/design_system/sections/hero-testimonials.aman.tsx +38 -0
- package/src/design_system/sections/hero-testimonials.tsx +55 -0
- package/src/design_system/sections/home-hero-component.tsx +228 -0
- package/src/design_system/sections/index.tsx +131 -0
- package/src/design_system/sections/job-gallery.aman.tsx +91 -0
- package/src/design_system/sections/job-gallery.tsx +183 -0
- package/src/design_system/sections/location-details-section.aman.tsx +179 -0
- package/src/design_system/sections/location-details-section.tsx +196 -0
- package/src/design_system/sections/location-grid.aman.tsx +76 -0
- package/src/design_system/sections/location-grid.tsx +123 -0
- package/src/design_system/sections/services-grid.aman.tsx +85 -0
- package/src/design_system/sections/services-grid.tsx +104 -0
- package/src/design_system/sections/services-home.aman.tsx +78 -0
- package/src/design_system/sections/services-home.tsx +131 -0
- package/src/design_system/sections/social-media-grid.aman.tsx +132 -0
- package/src/design_system/sections/social-media-grid.tsx +189 -0
- package/src/design_system/sections/statistics-section.aman.tsx +79 -0
- package/src/design_system/sections/statistics-section.tsx +97 -0
- package/src/design_system/sections/team-grid.aman.tsx +85 -0
- package/src/design_system/sections/team-grid.tsx +88 -0
- package/src/design_system/sections/testimonials-home.aman.tsx +113 -0
- package/src/design_system/sections/testimonials-home.tsx +90 -0
- package/src/design_system/sections/values-section.aman.tsx +73 -0
- package/src/design_system/sections/values-section.tsx +128 -0
- package/src/design_system/utils/icon-mapping.tsx +28 -0
- package/src/index.ts +7 -0
- package/src/lib/component-registry.ts +53 -0
- package/src/lib/hooks/index.ts +8 -0
- package/src/lib/hooks/use-breakpoint.ts +37 -0
- package/src/lib/hooks/use-clipboard.ts +79 -0
- package/src/lib/hooks/use-resize-observer.ts +68 -0
- package/src/lib/server-api.ts +115 -0
- package/src/styles/style-overrides.aman.css +101 -0
- package/src/styles/theme.css +224 -0
- package/src/styles/typography.css +430 -0
- package/src/themes/index.ts +23 -0
- package/src/types/api/blog-post.ts +53 -0
- package/src/types/api/company-information.ts +44 -0
- package/src/types/api/contact.ts +63 -0
- package/src/types/api/faq.ts +37 -0
- package/src/types/api/job-posting.ts +34 -0
- package/src/types/api/location.ts +36 -0
- package/src/types/api/photos.ts +28 -0
- package/src/types/api/service.ts +37 -0
- package/src/types/api/social-post.ts +28 -0
- package/src/types/api/team-member.ts +29 -0
- package/src/types/api/testimonial.ts +29 -0
- package/src/types/api/website-photos.ts +22 -0
- package/src/types/config.ts +21 -0
- package/src/types/index.ts +21 -0
- package/src/utils/countries.tsx +1351 -0
- package/src/utils/cx.ts +25 -0
- package/src/utils/gradient-placeholder.ts +59 -0
- package/src/utils/is-react-component.ts +33 -0
- package/src/utils/markdown-toc.ts +54 -0
- package/src/utils/photo-helpers.ts +94 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Form, PhotoWithFallback } from '../elements';
|
|
5
|
+
import type { WebsitePhotos } from '../../types/api/website-photos';
|
|
6
|
+
|
|
7
|
+
interface ContactSectionProps {
|
|
8
|
+
config?: any;
|
|
9
|
+
pageName?: string;
|
|
10
|
+
sectionKey?: string;
|
|
11
|
+
websitePhotos?: WebsitePhotos | null;
|
|
12
|
+
title?: string;
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ContactSection = ({
|
|
17
|
+
config,
|
|
18
|
+
pageName,
|
|
19
|
+
sectionKey,
|
|
20
|
+
websitePhotos,
|
|
21
|
+
title,
|
|
22
|
+
subtitle,
|
|
23
|
+
}: ContactSectionProps) => {
|
|
24
|
+
const [selectedCountryPhone, setSelectedCountryPhone] = useState("US");
|
|
25
|
+
|
|
26
|
+
// Get section config for custom title/subtitle
|
|
27
|
+
const pageConfig = config?.pages?.find((p: any) =>
|
|
28
|
+
p.library_reference_name === pageName ||
|
|
29
|
+
p.library_reference_name === 'Home' ||
|
|
30
|
+
p.slug === 'home'
|
|
31
|
+
);
|
|
32
|
+
const contactSection = sectionKey
|
|
33
|
+
? pageConfig?.sections?.[sectionKey]
|
|
34
|
+
: pageConfig?.sections?.home_page_section_7_contact || pageConfig?.sections?.home_page_section_6_contact;
|
|
35
|
+
|
|
36
|
+
// Use config values, then props, then defaults
|
|
37
|
+
const displayTitle = title || contactSection?.title || '*TR* Get in Touch';
|
|
38
|
+
const displaySubtitle = subtitle || contactSection?.subtitle || '*TR* We would love to hear from you';
|
|
39
|
+
|
|
40
|
+
const contactPhoto = websitePhotos?.contact;
|
|
41
|
+
const contactImageUrl = contactPhoto?.url;
|
|
42
|
+
const finalContactImage = contactImageUrl && contactImageUrl.trim() !== "" ? contactImageUrl : undefined;
|
|
43
|
+
const finalContactImageAlt = contactPhoto?.alt || "Contact image";
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<section>
|
|
47
|
+
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
48
|
+
<div className="grid grid-cols-1 gap-12 md:gap-16 lg:grid-cols-2">
|
|
49
|
+
{/* Form Side */}
|
|
50
|
+
<div className="flex w-full flex-col">
|
|
51
|
+
<div className="mb-8">
|
|
52
|
+
<h2 className="font-display text-4xl font-normal leading-tight text-fg-primary md:text-5xl">
|
|
53
|
+
{displayTitle}
|
|
54
|
+
</h2>
|
|
55
|
+
<p className="mt-4 font-body text-lg leading-relaxed text-tertiary">
|
|
56
|
+
{displaySubtitle}
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<Form
|
|
61
|
+
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
const data = Object.fromEntries(new FormData(e.currentTarget));
|
|
64
|
+
console.log("Form data:", data);
|
|
65
|
+
}}
|
|
66
|
+
className="flex flex-col gap-6"
|
|
67
|
+
>
|
|
68
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
69
|
+
<div>
|
|
70
|
+
<label className="block text-xs font-body uppercase tracking-widest text-secondary mb-2">
|
|
71
|
+
First name *
|
|
72
|
+
</label>
|
|
73
|
+
<input
|
|
74
|
+
required
|
|
75
|
+
type="text"
|
|
76
|
+
name="firstName"
|
|
77
|
+
placeholder="First name"
|
|
78
|
+
className="w-full px-4 py-3 bg-white border border-secondary rounded-sm font-body text-base text-fg-primary placeholder:text-secondary focus:outline-none focus:ring-2 focus:ring-focus-ring"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
<div>
|
|
82
|
+
<label className="block text-xs font-body uppercase tracking-widest text-secondary mb-2">
|
|
83
|
+
Last name *
|
|
84
|
+
</label>
|
|
85
|
+
<input
|
|
86
|
+
required
|
|
87
|
+
type="text"
|
|
88
|
+
name="lastName"
|
|
89
|
+
placeholder="Last name"
|
|
90
|
+
className="w-full px-4 py-3 bg-white border border-secondary rounded-sm font-body text-base text-fg-primary placeholder:text-secondary focus:outline-none focus:ring-2 focus:ring-focus-ring"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div>
|
|
96
|
+
<label className="block text-xs font-body uppercase tracking-widest text-secondary mb-2">
|
|
97
|
+
Email *
|
|
98
|
+
</label>
|
|
99
|
+
<input
|
|
100
|
+
required
|
|
101
|
+
type="email"
|
|
102
|
+
name="email"
|
|
103
|
+
placeholder="you@company.com"
|
|
104
|
+
className="w-full px-4 py-3 bg-white border border-secondary rounded-sm font-body text-base text-fg-primary placeholder:text-secondary focus:outline-none focus:ring-2 focus:ring-focus-ring"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div>
|
|
109
|
+
<label className="block text-xs font-body uppercase tracking-widest text-secondary mb-2">
|
|
110
|
+
Phone number
|
|
111
|
+
</label>
|
|
112
|
+
<input
|
|
113
|
+
type="tel"
|
|
114
|
+
name="phone"
|
|
115
|
+
placeholder="(555) 000-0000"
|
|
116
|
+
className="w-full px-4 py-3 bg-white border border-secondary rounded-sm font-body text-base text-fg-primary placeholder:text-secondary focus:outline-none focus:ring-2 focus:ring-focus-ring"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div>
|
|
121
|
+
<label className="block text-xs font-body uppercase tracking-widest text-secondary mb-2">
|
|
122
|
+
Message *
|
|
123
|
+
</label>
|
|
124
|
+
<textarea
|
|
125
|
+
required
|
|
126
|
+
name="message"
|
|
127
|
+
rows={4}
|
|
128
|
+
placeholder="Leave us a message..."
|
|
129
|
+
className="w-full px-4 py-3 bg-white border border-secondary rounded-sm font-body text-base text-fg-primary placeholder:text-secondary focus:outline-none focus:ring-2 focus:ring-focus-ring resize-none"
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div className="flex items-start gap-3">
|
|
134
|
+
<input
|
|
135
|
+
type="checkbox"
|
|
136
|
+
name="privacy"
|
|
137
|
+
required
|
|
138
|
+
className="mt-1 w-4 h-4 border-secondary rounded focus:ring-focus-ring"
|
|
139
|
+
/>
|
|
140
|
+
<label className="font-body text-sm text-tertiary">
|
|
141
|
+
You agree to our friendly privacy policy. *
|
|
142
|
+
</label>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<button
|
|
146
|
+
type="submit"
|
|
147
|
+
className="w-full font-body text-base uppercase tracking-wide py-3 px-6 rounded-sm hover:opacity-90 transition-opacity"
|
|
148
|
+
style={{ backgroundColor: '#1E1E1E', color: '#FFFFFF' }}
|
|
149
|
+
>
|
|
150
|
+
Send message
|
|
151
|
+
</button>
|
|
152
|
+
</Form>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Image Side */}
|
|
156
|
+
<div className="w-full h-96 md:h-full overflow-hidden">
|
|
157
|
+
<PhotoWithFallback
|
|
158
|
+
photoUrl={finalContactImage || ''}
|
|
159
|
+
photoAlt={finalContactImageAlt}
|
|
160
|
+
fallbackId="contact-image"
|
|
161
|
+
className="w-full h-full object-cover min-h-[300px]"
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</section>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export default ContactSection;
|
|
171
|
+
|
|
172
|
+
import { registerThemeVariant } from '../../lib/component-registry';
|
|
173
|
+
registerThemeVariant('contact-section', 'aman', ContactSection);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { Checkbox, Form, Input, InputGroup, NativeSelect, Textarea, PhotoWithFallback, Button } from '../elements';
|
|
5
|
+
import countries, { phoneCodeOptions } from '../../utils/countries';
|
|
6
|
+
import type { WebsitePhotos } from '../../types/api/website-photos';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
interface ContactSectionProps {
|
|
10
|
+
websitePhotos?: WebsitePhotos | null;
|
|
11
|
+
title?: string;
|
|
12
|
+
subtitle?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ContactSection = ({
|
|
16
|
+
websitePhotos,
|
|
17
|
+
title = "Contact us",
|
|
18
|
+
subtitle = "Our friendly team would love to hear from you.",
|
|
19
|
+
}: ContactSectionProps) => {
|
|
20
|
+
const [selectedCountryPhone, setSelectedCountryPhone] = useState("US");
|
|
21
|
+
|
|
22
|
+
// Get contact photo from props
|
|
23
|
+
const contactPhoto = websitePhotos?.contact;
|
|
24
|
+
const contactImageUrl = contactPhoto?.url;
|
|
25
|
+
const finalContactImage = contactImageUrl && contactImageUrl.trim() !== "" ? contactImageUrl : undefined;
|
|
26
|
+
const finalContactImageAlt = contactPhoto?.alt || "Contact image";
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<section className="bg-primary py-16 md:pt-16 md:pb-24">
|
|
30
|
+
<div className="mx-auto w-full max-w-container px-4 md:px-8">
|
|
31
|
+
<div className="grid grid-cols-1 gap-12 md:gap-16 lg:grid-cols-2">
|
|
32
|
+
<div className="flex w-full flex-col gap-12">
|
|
33
|
+
<div className="flex flex-col">
|
|
34
|
+
<h2 className="text-display-md font-semibold text-primary">
|
|
35
|
+
{title}
|
|
36
|
+
</h2>
|
|
37
|
+
{subtitle && (
|
|
38
|
+
<p className="mt-4 text-lg text-tertiary md:mt-5 md:text-xl">
|
|
39
|
+
{subtitle}
|
|
40
|
+
</p>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
<Form
|
|
44
|
+
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
const data = Object.fromEntries(new FormData(e.currentTarget));
|
|
47
|
+
console.log("Form data:", data);
|
|
48
|
+
}}
|
|
49
|
+
className="flex flex-col gap-8"
|
|
50
|
+
>
|
|
51
|
+
<div className="flex flex-col gap-6">
|
|
52
|
+
<div className="flex flex-col gap-x-8 gap-y-6 md:flex-row">
|
|
53
|
+
<Input
|
|
54
|
+
isRequired
|
|
55
|
+
size="md"
|
|
56
|
+
name="firstName"
|
|
57
|
+
label="First name"
|
|
58
|
+
placeholder="First name"
|
|
59
|
+
wrapperClassName="flex-1"
|
|
60
|
+
/>
|
|
61
|
+
<Input
|
|
62
|
+
isRequired
|
|
63
|
+
size="md"
|
|
64
|
+
name="lastName"
|
|
65
|
+
label="Last name"
|
|
66
|
+
placeholder="Last name"
|
|
67
|
+
wrapperClassName="flex-1"
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
<Input
|
|
71
|
+
isRequired
|
|
72
|
+
size="md"
|
|
73
|
+
name="email"
|
|
74
|
+
label="Email"
|
|
75
|
+
type="email"
|
|
76
|
+
placeholder="you@company.com"
|
|
77
|
+
/>
|
|
78
|
+
<InputGroup
|
|
79
|
+
size="md"
|
|
80
|
+
name="phone"
|
|
81
|
+
label="Phone number"
|
|
82
|
+
leadingAddon={
|
|
83
|
+
<NativeSelect
|
|
84
|
+
aria-label="Country code"
|
|
85
|
+
value={selectedCountryPhone}
|
|
86
|
+
onChange={(value: any) => setSelectedCountryPhone(value.currentTarget.value)}
|
|
87
|
+
options={phoneCodeOptions.map((item: any) => ({
|
|
88
|
+
label: item.label as string,
|
|
89
|
+
value: item.id as string,
|
|
90
|
+
}))}
|
|
91
|
+
/>
|
|
92
|
+
}
|
|
93
|
+
>
|
|
94
|
+
<Input
|
|
95
|
+
type="tel"
|
|
96
|
+
placeholder={countries.find((country: any) => country.code === selectedCountryPhone)?.phoneMask?.replaceAll("#", "0")}
|
|
97
|
+
/>
|
|
98
|
+
</InputGroup>
|
|
99
|
+
<Textarea
|
|
100
|
+
isRequired
|
|
101
|
+
name="message"
|
|
102
|
+
label="Message"
|
|
103
|
+
placeholder="Leave us a message..."
|
|
104
|
+
rows={4}
|
|
105
|
+
/>
|
|
106
|
+
<Checkbox
|
|
107
|
+
name="privacy"
|
|
108
|
+
size="md"
|
|
109
|
+
hint={
|
|
110
|
+
<>
|
|
111
|
+
You agree to our friendly{" "}
|
|
112
|
+
<a
|
|
113
|
+
href="#"
|
|
114
|
+
className="rounded-xs underline underline-offset-3 outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
115
|
+
>
|
|
116
|
+
privacy policy.
|
|
117
|
+
</a>
|
|
118
|
+
</>
|
|
119
|
+
}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<Button type="submit" size="xl">
|
|
124
|
+
Send message
|
|
125
|
+
</Button>
|
|
126
|
+
</Form>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div className="max-lg:hidden lg:h-200">
|
|
130
|
+
<PhotoWithFallback
|
|
131
|
+
photoUrl={finalContactImage}
|
|
132
|
+
photoAlt={finalContactImageAlt}
|
|
133
|
+
fallbackId="contact-section-image"
|
|
134
|
+
className="size-full object-cover"
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</section>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export default ContactSection;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import type { FaqQuestion } from '../../types/api/faq';
|
|
5
|
+
|
|
6
|
+
interface FAQGridProps {
|
|
7
|
+
config: {
|
|
8
|
+
pages?: any[];
|
|
9
|
+
};
|
|
10
|
+
faqs: FaqQuestion[];
|
|
11
|
+
pageName?: string;
|
|
12
|
+
sectionKey?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const FAQGrid = ({
|
|
16
|
+
config,
|
|
17
|
+
faqs: faqsData,
|
|
18
|
+
pageName = 'FAQ',
|
|
19
|
+
sectionKey = 'faq_page_section_2_faqs',
|
|
20
|
+
}: FAQGridProps) => {
|
|
21
|
+
const faqs = Array.isArray(faqsData) ? faqsData : [];
|
|
22
|
+
|
|
23
|
+
const title = 'Frequently Asked Questions';
|
|
24
|
+
const maxItems = undefined;
|
|
25
|
+
const displayFaqs = maxItems ? faqs.slice(0, maxItems) : faqs;
|
|
26
|
+
|
|
27
|
+
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<section>
|
|
31
|
+
<div className="mx-auto max-w-3xl px-4 md:px-8">
|
|
32
|
+
<div className="mb-12 text-center">
|
|
33
|
+
<h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
|
|
34
|
+
{title}
|
|
35
|
+
</h2>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{displayFaqs.length > 0 ? (
|
|
39
|
+
<div className="space-y-4">
|
|
40
|
+
{displayFaqs.map((faq: any, i: number) => {
|
|
41
|
+
const isOpen = openIndex === i;
|
|
42
|
+
const answer = faq.answer_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div key={faq.id} className="border-b border-secondary">
|
|
46
|
+
<button
|
|
47
|
+
onClick={() => setOpenIndex(isOpen ? null : i)}
|
|
48
|
+
className="flex w-full items-center justify-between py-4 text-left"
|
|
49
|
+
>
|
|
50
|
+
<h3 className="font-display text-xl font-normal text-fg-primary">
|
|
51
|
+
{faq.question}
|
|
52
|
+
</h3>
|
|
53
|
+
<span className="ml-4 text-fg-primary">
|
|
54
|
+
{isOpen ? '−' : '+'}
|
|
55
|
+
</span>
|
|
56
|
+
</button>
|
|
57
|
+
{isOpen && (
|
|
58
|
+
<div className="pb-4">
|
|
59
|
+
<p className="font-body text-base leading-relaxed text-tertiary">
|
|
60
|
+
{answer}
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
</div>
|
|
68
|
+
) : (
|
|
69
|
+
<div className="text-center py-12">
|
|
70
|
+
<p className="font-body text-base text-tertiary">No FAQs available</p>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
</section>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
import { registerThemeVariant } from '../../lib/component-registry';
|
|
79
|
+
registerThemeVariant('faq-grid', 'aman', FAQGrid);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Button } from '../elements';
|
|
5
|
+
import type { FaqQuestion } from '../../types/api/faq';
|
|
6
|
+
|
|
7
|
+
interface FAQGridProps {
|
|
8
|
+
config: {
|
|
9
|
+
pages?: any[];
|
|
10
|
+
};
|
|
11
|
+
faqs?: FaqQuestion[] | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Simple markdown renderer - converts basic markdown to HTML
|
|
15
|
+
const renderMarkdown = (content: string) => {
|
|
16
|
+
let html = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="text-brand-secondary underline">$1</a>');
|
|
17
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
18
|
+
html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
|
19
|
+
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
20
|
+
html = html.replace(/_([^_]+)_/g, '<em>$1</em>');
|
|
21
|
+
html = html.replace(/\n/g, '<br />');
|
|
22
|
+
return html;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const FAQGrid = ({
|
|
26
|
+
config,
|
|
27
|
+
faqs: faqsData,
|
|
28
|
+
}: FAQGridProps) => {
|
|
29
|
+
const faqs = Array.isArray(faqsData) ? faqsData : [];
|
|
30
|
+
|
|
31
|
+
const title = "Frequently asked questions";
|
|
32
|
+
const subtitle = "";
|
|
33
|
+
const ctaTitle = "Still have questions?";
|
|
34
|
+
const ctaSubtitle = "Can't find the answer you're looking for? Please chat to our friendly team.";
|
|
35
|
+
const ctaButtonText = "Get in touch";
|
|
36
|
+
const ctaButtonHref = "/contact";
|
|
37
|
+
const backgroundColor = "bg-primary";
|
|
38
|
+
const className = "";
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<section className={`${backgroundColor} py-16 md:py-24 ${className}`} id="faq">
|
|
42
|
+
<div className="mx-auto w-full max-w-container px-4 md:px-8">
|
|
43
|
+
<div className="flex w-full max-w-3xl flex-col">
|
|
44
|
+
<h2 className="text-display-sm font-semibold text-primary md:text-display-md">
|
|
45
|
+
{title}
|
|
46
|
+
</h2>
|
|
47
|
+
{subtitle && (
|
|
48
|
+
<p className="mt-4 text-lg text-tertiary md:mt-5 md:text-xl">
|
|
49
|
+
{subtitle}
|
|
50
|
+
</p>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{faqs.length > 0 && (
|
|
55
|
+
<div className="mt-12 md:mt-16">
|
|
56
|
+
<dl className="grid w-full grid-cols-1 gap-x-8 gap-y-10 sm:grid-cols-2 md:gap-y-16 lg:grid-cols-3">
|
|
57
|
+
{faqs.map((faq: FaqQuestion, index: number) => {
|
|
58
|
+
const answerContent = faq.answer_markdown || '';
|
|
59
|
+
const answerHtml = renderMarkdown(answerContent);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div key={faq.id || index}>
|
|
63
|
+
<div className="flex max-w-sm flex-col">
|
|
64
|
+
<dt className="text-md font-semibold text-primary">{faq.question}</dt>
|
|
65
|
+
<dd
|
|
66
|
+
className="mt-1 text-md text-tertiary"
|
|
67
|
+
dangerouslySetInnerHTML={{ __html: answerHtml }}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
</dl>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{(ctaTitle || ctaSubtitle || ctaButtonText) && (
|
|
78
|
+
<div className="mt-12 flex flex-col items-start justify-between gap-6 rounded-2xl bg-secondary px-5 py-8 md:mt-16 md:flex-row md:gap-8 md:p-8">
|
|
79
|
+
<div className="w-full max-w-3xl">
|
|
80
|
+
{ctaTitle && (
|
|
81
|
+
<h4 className="text-xl font-semibold text-primary">{ctaTitle}</h4>
|
|
82
|
+
)}
|
|
83
|
+
{ctaSubtitle && (
|
|
84
|
+
<p className="mt-2 text-md text-tertiary md:text-lg">
|
|
85
|
+
{ctaSubtitle}
|
|
86
|
+
</p>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
{ctaButtonText && (
|
|
90
|
+
<Button
|
|
91
|
+
size="xl"
|
|
92
|
+
href={ctaButtonHref}
|
|
93
|
+
>
|
|
94
|
+
{ctaButtonText}
|
|
95
|
+
</Button>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
</section>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { motion } from 'framer-motion';
|
|
5
|
+
import type { FaqQuestion } from '../../types/api/faq';
|
|
6
|
+
|
|
7
|
+
interface FAQHomeProps {
|
|
8
|
+
config: {
|
|
9
|
+
pages?: any[];
|
|
10
|
+
};
|
|
11
|
+
faqs: FaqQuestion[];
|
|
12
|
+
pageName?: string;
|
|
13
|
+
sectionKey?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const FAQHome = ({
|
|
17
|
+
config,
|
|
18
|
+
faqs: faqsData,
|
|
19
|
+
pageName = 'Home',
|
|
20
|
+
sectionKey = 'home_page_section_4_faq',
|
|
21
|
+
}: FAQHomeProps) => {
|
|
22
|
+
const faqs = Array.isArray(faqsData) ? faqsData : [];
|
|
23
|
+
|
|
24
|
+
const faqSection = pageName === 'Home'
|
|
25
|
+
const title = 'Frequently Asked Questions';
|
|
26
|
+
const maxItems = undefined;
|
|
27
|
+
const displayFaqs = maxItems ? faqs.slice(0, maxItems) : faqs;
|
|
28
|
+
|
|
29
|
+
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<section>
|
|
33
|
+
<div className="mx-auto max-w-3xl px-4 md:px-8">
|
|
34
|
+
<div className="mb-12 text-center">
|
|
35
|
+
<h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
|
|
36
|
+
{title}
|
|
37
|
+
</h2>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{displayFaqs.length > 0 ? (
|
|
41
|
+
<div className="space-y-4">
|
|
42
|
+
{displayFaqs.map((faq: any, i: number) => {
|
|
43
|
+
const isOpen = openIndex === i;
|
|
44
|
+
const answer = faq.answer_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div key={faq.id} className="border-b border-secondary">
|
|
48
|
+
<button
|
|
49
|
+
onClick={() => setOpenIndex(isOpen ? null : i)}
|
|
50
|
+
className="flex w-full items-center justify-between py-4 text-left"
|
|
51
|
+
>
|
|
52
|
+
<h3 className="font-display text-xl font-normal text-fg-primary">
|
|
53
|
+
{faq.question}
|
|
54
|
+
</h3>
|
|
55
|
+
<span className="ml-4 text-fg-primary">
|
|
56
|
+
{isOpen ? '−' : '+'}
|
|
57
|
+
</span>
|
|
58
|
+
</button>
|
|
59
|
+
<motion.div
|
|
60
|
+
initial={false}
|
|
61
|
+
animate={{
|
|
62
|
+
height: isOpen ? 'auto' : 0,
|
|
63
|
+
opacity: isOpen ? 1 : 0,
|
|
64
|
+
}}
|
|
65
|
+
transition={{
|
|
66
|
+
duration: 0.3,
|
|
67
|
+
ease: [0.4, 0.0, 0.2, 1],
|
|
68
|
+
}}
|
|
69
|
+
style={{ overflow: 'hidden' }}
|
|
70
|
+
>
|
|
71
|
+
<div className="pb-4">
|
|
72
|
+
<p className="font-body text-base leading-relaxed text-tertiary">
|
|
73
|
+
{answer}
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
</motion.div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
</div>
|
|
81
|
+
) : (
|
|
82
|
+
<div className="text-center py-12">
|
|
83
|
+
<p className="font-body text-base text-tertiary">No FAQs available</p>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
</section>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
import { registerThemeVariant } from '../../lib/component-registry';
|
|
92
|
+
registerThemeVariant('faq-home', 'aman', FAQHome);
|