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.
Files changed (182) hide show
  1. package/README.md +179 -0
  2. package/package.json +59 -0
  3. package/src/contexts/ThemeContext.tsx +34 -0
  4. package/src/contexts/index.ts +1 -0
  5. package/src/design_system/elements/IconComponent.tsx +98 -0
  6. package/src/design_system/elements/avatar/avatar-label-group.tsx +30 -0
  7. package/src/design_system/elements/avatar/avatar-profile-photo.tsx +125 -0
  8. package/src/design_system/elements/avatar/avatar.tsx +131 -0
  9. package/src/design_system/elements/avatar/base-components/avatar-add-button.tsx +34 -0
  10. package/src/design_system/elements/avatar/base-components/avatar-company-icon.tsx +26 -0
  11. package/src/design_system/elements/avatar/base-components/avatar-online-indicator.tsx +31 -0
  12. package/src/design_system/elements/avatar/base-components/index.tsx +4 -0
  13. package/src/design_system/elements/avatar/base-components/verified-tick.tsx +34 -0
  14. package/src/design_system/elements/avatar/utils.ts +12 -0
  15. package/src/design_system/elements/badges/avatar.tsx +132 -0
  16. package/src/design_system/elements/badges/badge-groups.tsx +176 -0
  17. package/src/design_system/elements/badges/badge-types.ts +266 -0
  18. package/src/design_system/elements/badges/badges.tsx +430 -0
  19. package/src/design_system/elements/breadcrumb/Breadcrumb.tsx +33 -0
  20. package/src/design_system/elements/button-group/button-group.tsx +106 -0
  21. package/src/design_system/elements/buttons/app-store-buttons-outline.tsx +378 -0
  22. package/src/design_system/elements/buttons/app-store-buttons.tsx +567 -0
  23. package/src/design_system/elements/buttons/button-utility.tsx +116 -0
  24. package/src/design_system/elements/buttons/button.aman.tsx +174 -0
  25. package/src/design_system/elements/buttons/button.tsx +271 -0
  26. package/src/design_system/elements/buttons/close-button.tsx +42 -0
  27. package/src/design_system/elements/buttons/round-button.tsx +29 -0
  28. package/src/design_system/elements/buttons/social-button.tsx +148 -0
  29. package/src/design_system/elements/buttons/social-logos.tsx +115 -0
  30. package/src/design_system/elements/carousel/carousel-base.tsx +308 -0
  31. package/src/design_system/elements/carousel/carousel.tsx +308 -0
  32. package/src/design_system/elements/checkbox/checkbox.tsx +120 -0
  33. package/src/design_system/elements/date-picker/calendar.tsx +101 -0
  34. package/src/design_system/elements/date-picker/cell.tsx +106 -0
  35. package/src/design_system/elements/date-picker/date-input.tsx +32 -0
  36. package/src/design_system/elements/date-picker/date-picker.tsx +86 -0
  37. package/src/design_system/elements/date-picker/date-range-picker.tsx +163 -0
  38. package/src/design_system/elements/date-picker/range-calendar.tsx +161 -0
  39. package/src/design_system/elements/date-picker/range-preset.tsx +28 -0
  40. package/src/design_system/elements/featured-icon/featured-icon.tsx +154 -0
  41. package/src/design_system/elements/form/form.tsx +10 -0
  42. package/src/design_system/elements/form/hook-form.tsx +75 -0
  43. package/src/design_system/elements/hint-text/hint-text.tsx +33 -0
  44. package/src/design_system/elements/index.tsx +158 -0
  45. package/src/design_system/elements/input/hint-text.tsx +33 -0
  46. package/src/design_system/elements/input/input-group.tsx +133 -0
  47. package/src/design_system/elements/input/input.aman.tsx +172 -0
  48. package/src/design_system/elements/input/input.tsx +271 -0
  49. package/src/design_system/elements/input/label.tsx +50 -0
  50. package/src/design_system/elements/label/label.tsx +50 -0
  51. package/src/design_system/elements/loading-indicator/loading-indicator.tsx +123 -0
  52. package/src/design_system/elements/map/GoogleMap.tsx +286 -0
  53. package/src/design_system/elements/markdown-renderer/MarkdownRenderer.tsx +155 -0
  54. package/src/design_system/elements/modals/modal.tsx +41 -0
  55. package/src/design_system/elements/pagination/pagination-base.tsx +378 -0
  56. package/src/design_system/elements/pagination/pagination-dot.tsx +54 -0
  57. package/src/design_system/elements/pagination/pagination-line.tsx +50 -0
  58. package/src/design_system/elements/pagination/pagination.tsx +330 -0
  59. package/src/design_system/elements/photo-fallback/photo-fallback.tsx +143 -0
  60. package/src/design_system/elements/progress-indicators/progress-circles.tsx +176 -0
  61. package/src/design_system/elements/progress-indicators/progress-indicators.tsx +123 -0
  62. package/src/design_system/elements/progress-indicators/simple-circle.tsx +29 -0
  63. package/src/design_system/elements/radio-buttons/radio-buttons.tsx +129 -0
  64. package/src/design_system/elements/rating/rating-badge.tsx +144 -0
  65. package/src/design_system/elements/rating/rating-stars.tsx +77 -0
  66. package/src/design_system/elements/select/combobox.tsx +152 -0
  67. package/src/design_system/elements/select/multi-select.tsx +363 -0
  68. package/src/design_system/elements/select/popover.tsx +34 -0
  69. package/src/design_system/elements/select/select-item.tsx +97 -0
  70. package/src/design_system/elements/select/select-native.tsx +69 -0
  71. package/src/design_system/elements/select/select.aman.tsx +75 -0
  72. package/src/design_system/elements/select/select.tsx +146 -0
  73. package/src/design_system/elements/shared-assets/credit-card/credit-card.tsx +237 -0
  74. package/src/design_system/elements/shared-assets/credit-card/icons.tsx +75 -0
  75. package/src/design_system/elements/shared-assets/iphone-mockup.tsx +172 -0
  76. package/src/design_system/elements/shared-assets/section-divider.tsx +12 -0
  77. package/src/design_system/elements/slideout-menus/slideout-menu.tsx +122 -0
  78. package/src/design_system/elements/tabs/tabs.tsx +225 -0
  79. package/src/design_system/elements/tags/base-components/tag-checkbox.tsx +45 -0
  80. package/src/design_system/elements/tags/base-components/tag-close-x.tsx +34 -0
  81. package/src/design_system/elements/tags/tags.tsx +176 -0
  82. package/src/design_system/elements/textarea/textarea.aman.tsx +52 -0
  83. package/src/design_system/elements/textarea/textarea.tsx +111 -0
  84. package/src/design_system/elements/toggle/toggle.tsx +140 -0
  85. package/src/design_system/elements/tooltip/tooltip.tsx +109 -0
  86. package/src/design_system/hooks/use-breakpoint.ts +37 -0
  87. package/src/design_system/hooks/use-resize-observer.ts +68 -0
  88. package/src/design_system/logo/keystone-logo-minimal.tsx +93 -0
  89. package/src/design_system/logo/keystone-logo.tsx +22 -0
  90. package/src/design_system/sections/about-home.aman.tsx +85 -0
  91. package/src/design_system/sections/about-home.tsx +115 -0
  92. package/src/design_system/sections/blog-cards.tsx +848 -0
  93. package/src/design_system/sections/blog-gallery.aman.tsx +77 -0
  94. package/src/design_system/sections/blog-gallery.tsx +204 -0
  95. package/src/design_system/sections/blog-home.aman.tsx +84 -0
  96. package/src/design_system/sections/blog-home.tsx +153 -0
  97. package/src/design_system/sections/blog-post.aman.tsx +74 -0
  98. package/src/design_system/sections/blog-post.tsx +301 -0
  99. package/src/design_system/sections/blog-section.aman.tsx +101 -0
  100. package/src/design_system/sections/blog-section.tsx +179 -0
  101. package/src/design_system/sections/contact-home.tsx +25 -0
  102. package/src/design_system/sections/contact-section.aman.tsx +173 -0
  103. package/src/design_system/sections/contact-section.tsx +143 -0
  104. package/src/design_system/sections/faq-grid.aman.tsx +79 -0
  105. package/src/design_system/sections/faq-grid.tsx +102 -0
  106. package/src/design_system/sections/faq-home.aman.tsx +92 -0
  107. package/src/design_system/sections/faq-home.tsx +134 -0
  108. package/src/design_system/sections/feature-tab.tsx +43 -0
  109. package/src/design_system/sections/feature-text.tsx +284 -0
  110. package/src/design_system/sections/footer-home.aman.tsx +62 -0
  111. package/src/design_system/sections/footer-home.tsx +259 -0
  112. package/src/design_system/sections/generic-header-component.tsx +103 -0
  113. package/src/design_system/sections/header-navigation.aman.tsx +360 -0
  114. package/src/design_system/sections/header-navigation.tsx +334 -0
  115. package/src/design_system/sections/hero-faq.aman.tsx +38 -0
  116. package/src/design_system/sections/hero-faq.tsx +55 -0
  117. package/src/design_system/sections/hero-generic-text.aman.tsx +49 -0
  118. package/src/design_system/sections/hero-generic-text.tsx +51 -0
  119. package/src/design_system/sections/hero-home.aman.tsx +84 -0
  120. package/src/design_system/sections/hero-home.tsx +246 -0
  121. package/src/design_system/sections/hero-location-detail.aman.tsx +33 -0
  122. package/src/design_system/sections/hero-location-detail.tsx +72 -0
  123. package/src/design_system/sections/hero-service-detail.aman.tsx +53 -0
  124. package/src/design_system/sections/hero-service-detail.tsx +51 -0
  125. package/src/design_system/sections/hero-social-media.aman.tsx +42 -0
  126. package/src/design_system/sections/hero-social-media.tsx +35 -0
  127. package/src/design_system/sections/hero-testimonials.aman.tsx +38 -0
  128. package/src/design_system/sections/hero-testimonials.tsx +55 -0
  129. package/src/design_system/sections/home-hero-component.tsx +228 -0
  130. package/src/design_system/sections/index.tsx +131 -0
  131. package/src/design_system/sections/job-gallery.aman.tsx +91 -0
  132. package/src/design_system/sections/job-gallery.tsx +183 -0
  133. package/src/design_system/sections/location-details-section.aman.tsx +179 -0
  134. package/src/design_system/sections/location-details-section.tsx +196 -0
  135. package/src/design_system/sections/location-grid.aman.tsx +76 -0
  136. package/src/design_system/sections/location-grid.tsx +123 -0
  137. package/src/design_system/sections/services-grid.aman.tsx +85 -0
  138. package/src/design_system/sections/services-grid.tsx +104 -0
  139. package/src/design_system/sections/services-home.aman.tsx +78 -0
  140. package/src/design_system/sections/services-home.tsx +131 -0
  141. package/src/design_system/sections/social-media-grid.aman.tsx +132 -0
  142. package/src/design_system/sections/social-media-grid.tsx +189 -0
  143. package/src/design_system/sections/statistics-section.aman.tsx +79 -0
  144. package/src/design_system/sections/statistics-section.tsx +97 -0
  145. package/src/design_system/sections/team-grid.aman.tsx +85 -0
  146. package/src/design_system/sections/team-grid.tsx +88 -0
  147. package/src/design_system/sections/testimonials-home.aman.tsx +113 -0
  148. package/src/design_system/sections/testimonials-home.tsx +90 -0
  149. package/src/design_system/sections/values-section.aman.tsx +73 -0
  150. package/src/design_system/sections/values-section.tsx +128 -0
  151. package/src/design_system/utils/icon-mapping.tsx +28 -0
  152. package/src/index.ts +7 -0
  153. package/src/lib/component-registry.ts +53 -0
  154. package/src/lib/hooks/index.ts +8 -0
  155. package/src/lib/hooks/use-breakpoint.ts +37 -0
  156. package/src/lib/hooks/use-clipboard.ts +79 -0
  157. package/src/lib/hooks/use-resize-observer.ts +68 -0
  158. package/src/lib/server-api.ts +115 -0
  159. package/src/styles/style-overrides.aman.css +101 -0
  160. package/src/styles/theme.css +224 -0
  161. package/src/styles/typography.css +430 -0
  162. package/src/themes/index.ts +23 -0
  163. package/src/types/api/blog-post.ts +53 -0
  164. package/src/types/api/company-information.ts +44 -0
  165. package/src/types/api/contact.ts +63 -0
  166. package/src/types/api/faq.ts +37 -0
  167. package/src/types/api/job-posting.ts +34 -0
  168. package/src/types/api/location.ts +36 -0
  169. package/src/types/api/photos.ts +28 -0
  170. package/src/types/api/service.ts +37 -0
  171. package/src/types/api/social-post.ts +28 -0
  172. package/src/types/api/team-member.ts +29 -0
  173. package/src/types/api/testimonial.ts +29 -0
  174. package/src/types/api/website-photos.ts +22 -0
  175. package/src/types/config.ts +21 -0
  176. package/src/types/index.ts +21 -0
  177. package/src/utils/countries.tsx +1351 -0
  178. package/src/utils/cx.ts +25 -0
  179. package/src/utils/gradient-placeholder.ts +59 -0
  180. package/src/utils/is-react-component.ts +33 -0
  181. package/src/utils/markdown-toc.ts +54 -0
  182. 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);