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,38 @@
1
+ "use client";
2
+
3
+
4
+ interface FAQHeroProps {
5
+ config: {
6
+ pages?: any[];
7
+ };
8
+ pageName?: string;
9
+ sectionKey?: string;
10
+ }
11
+
12
+ export const FAQHero = ({
13
+ config,
14
+ pageName = 'FAQ',
15
+ sectionKey = 'faq_page_section_1_hero',
16
+ }: FAQHeroProps) => {
17
+ const title = 'Frequently Asked Questions';
18
+ const subtitle = '';
19
+
20
+ return (
21
+ <section className="py-24 md:py-32">
22
+ <div className="mx-auto max-w-4xl px-4 text-center md:px-8">
23
+ <h1 className="font-display text-5xl font-normal leading-tight text-fg-primary md:text-6xl lg:text-7xl">
24
+ {title}
25
+ </h1>
26
+
27
+ {subtitle && (
28
+ <p className="mt-6 font-display text-lg leading-relaxed text-tertiary md:text-xl max-w-3xl mx-auto">
29
+ {subtitle}
30
+ </p>
31
+ )}
32
+ </div>
33
+ </section>
34
+ );
35
+ };
36
+
37
+ import { registerThemeVariant } from '../../lib/component-registry';
38
+ registerThemeVariant('hero-faq', 'aman', FAQHero);
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import { SearchLg } from '@untitledui/icons';
4
+ import { Input } from '../elements';
5
+
6
+ interface FAQHeroProps {
7
+ // Config data - component extracts all values from it
8
+ config: {
9
+ pages?: any[];
10
+ };
11
+ // Optional search props
12
+ searchTerm?: string;
13
+ onSearchChange?: (value: string) => void;
14
+ }
15
+
16
+ export const FAQHero = ({
17
+ config,
18
+ searchTerm,
19
+ onSearchChange,
20
+ }: FAQHeroProps) => {
21
+ // Extract values from config
22
+ const label = "Support";
23
+ const headline = "Frequently Asked Questions";
24
+ const subhead = "";
25
+ return (
26
+ <section className="bg-primary py-16 md:py-24">
27
+ <div className="mx-auto max-w-container px-4 md:px-8">
28
+ <div className="mx-auto flex w-full max-w-3xl flex-col items-center text-center">
29
+ <span className="text-sm font-semibold text-brand-secondary md:text-md">{label}</span>
30
+ <h1 className="mt-3 text-display-md font-semibold text-primary md:text-display-lg">
31
+ {headline}
32
+ </h1>
33
+ <p className="mt-4 text-lg text-tertiary md:mt-6 md:text-xl">
34
+ {subhead}
35
+ </p>
36
+ {onSearchChange && (
37
+ <div className="mt-8 w-full sm:mt-12 sm:w-80">
38
+ <Input
39
+ size="md"
40
+ type="search"
41
+ placeholder="Search"
42
+ icon={SearchLg}
43
+ wrapperClassName="sm:py-0.5"
44
+ value={searchTerm || ''}
45
+ onChange={onSearchChange}
46
+ />
47
+ </div>
48
+ )}
49
+ </div>
50
+ </div>
51
+ </section>
52
+ );
53
+ };
54
+
55
+
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+
4
+ interface GenericTextHeroProps {
5
+ title?: string;
6
+ subtitle?: string;
7
+ config?: {
8
+ pages?: any[];
9
+ };
10
+ pageName?: string;
11
+ sectionKey?: string;
12
+ }
13
+
14
+ export const GenericTextHero = ({
15
+ title: titleProp,
16
+ subtitle: subtitleProp,
17
+ config,
18
+ pageName,
19
+ sectionKey,
20
+ }: GenericTextHeroProps) => {
21
+ // Try to get from config first, then fall back to props
22
+ let title = titleProp;
23
+ let subtitle = subtitleProp;
24
+
25
+ if (config && pageName && sectionKey) {
26
+ title = titleProp || pageName || '';
27
+ subtitle = subtitleProp || '';
28
+ } else if (!title && pageName) {
29
+ title = pageName;
30
+ }
31
+ return (
32
+ <section className="py-24 md:py-32">
33
+ <div className="mx-auto max-w-4xl px-4 text-center md:px-8">
34
+ <h1 className="font-display text-5xl font-normal leading-tight text-fg-primary md:text-6xl lg:text-7xl">
35
+ {title}
36
+ </h1>
37
+
38
+ {subtitle && (
39
+ <p className="mt-6 font-display text-lg leading-relaxed text-tertiary md:text-xl max-w-3xl mx-auto">
40
+ {subtitle}
41
+ </p>
42
+ )}
43
+ </div>
44
+ </section>
45
+ );
46
+ };
47
+
48
+ import { registerThemeVariant } from '../../lib/component-registry';
49
+ registerThemeVariant('hero-generic-text', 'aman', GenericTextHero);
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+
4
+ interface GenericTextHeroProps {
5
+ // Config data - component extracts all values from it
6
+ config: {
7
+ pages?: any[];
8
+ };
9
+ pageName: string; // e.g., 'Blog', 'FAQ', 'Testimonials'
10
+ sectionKey: string; // e.g., 'blog_page_section_1_hero'
11
+ }
12
+
13
+ export const GenericTextHero = ({
14
+ config,
15
+ pageName,
16
+ sectionKey,
17
+ }: GenericTextHeroProps) => {
18
+ // Extract values from config
19
+ const label = undefined;
20
+ const headline = "";
21
+ const subhead = "";
22
+
23
+ // Don't render if there's no content
24
+ if (!headline && !subhead && !label) {
25
+ return null;
26
+ }
27
+
28
+ return (
29
+ <section className="bg-primary py-16 md:py-24">
30
+ <div className="mx-auto max-w-container px-4 md:px-8">
31
+ <div className="mx-auto flex w-full max-w-3xl flex-col items-center text-center">
32
+ {label && (
33
+ <span className="text-sm font-semibold text-brand-secondary md:text-md">{label}</span>
34
+ )}
35
+ {headline && (
36
+ <h1 className={`${label ? 'mt-3' : ''} text-display-md font-semibold text-primary md:text-display-lg`}>
37
+ {headline}
38
+ </h1>
39
+ )}
40
+ {subhead && (
41
+ <p className="mt-4 text-lg text-tertiary md:mt-6 md:text-xl">
42
+ {subhead}
43
+ </p>
44
+ )}
45
+ </div>
46
+ </div>
47
+ </section>
48
+ );
49
+ };
50
+
51
+
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import { Fragment } from "react";
4
+ import React from "react";
5
+ import { PhotoWithFallback } from '../elements';
6
+ import type { WebsitePhotos } from '../../types/api/website-photos';
7
+
8
+ interface HeroHomeProps {
9
+ config?: any;
10
+ websitePhotos?: WebsitePhotos | null;
11
+ headline?: string;
12
+ subhead?: string;
13
+ ctaText?: string;
14
+ ctaHref?: string;
15
+ reviews?: { rating: number; count: number };
16
+ onEmailSubmit?: (email: string) => void;
17
+ }
18
+
19
+ export const HeroHome = ({
20
+ config,
21
+ websitePhotos,
22
+ headline,
23
+ subhead,
24
+ ctaText,
25
+ ctaHref = "/contact",
26
+ }: HeroHomeProps) => {
27
+ // Get section config for the hero
28
+ const homePageConfig = config?.pages?.find((p: any) =>
29
+ p.library_reference_name === 'Home' || p.slug === 'home'
30
+ );
31
+ const heroSection = homePageConfig?.sections?.home_page_section_1_hero;
32
+
33
+ // Use config values, then props, then defaults
34
+ const displayHeadline = headline || heroSection?.headline || '*TR* Your Slogan Here';
35
+ const displaySubhead = subhead || heroSection?.subhead || '*TR* A brief description of your business and what makes you special';
36
+ const displayCtaText = ctaText || heroSection?.cta_text || 'Discover more';
37
+
38
+ const heroImage = {
39
+ url: websitePhotos?.hero?.url || (websitePhotos as any)?.brand?.url || '',
40
+ alt: websitePhotos?.hero?.alt || (websitePhotos as any)?.brand?.alt || 'Hero image',
41
+ };
42
+
43
+ return (
44
+ <Fragment>
45
+ <section className="py-24 md:py-32">
46
+ <div className="mx-auto max-w-4xl px-4 text-center md:px-8">
47
+ <h1 className="font-display text-5xl font-normal leading-tight text-fg-primary md:text-6xl lg:text-7xl">
48
+ {displayHeadline}
49
+ </h1>
50
+
51
+ <p className="mt-6 font-body text-lg leading-relaxed text-tertiary md:text-xl max-w-3xl mx-auto">
52
+ {displaySubhead}
53
+ </p>
54
+
55
+ {displayCtaText && (
56
+ <a
57
+ href={ctaHref}
58
+ className="mt-8 inline-block font-body text-base underline underline-offset-4 hover:no-underline transition-colors"
59
+ style={{ color: 'var(--color-text-brand-accent)' }}
60
+ >
61
+ {displayCtaText}
62
+ </a>
63
+ )}
64
+ </div>
65
+ </section>
66
+
67
+ <section>
68
+ <div className="mx-auto max-w-container px-4 md:px-8">
69
+ <div className="w-full h-[400px] md:h-[500px] lg:h-[600px]">
70
+ <PhotoWithFallback
71
+ photoUrl={heroImage.url}
72
+ photoAlt={heroImage.alt}
73
+ fallbackId="hero-home-brand"
74
+ className="w-full h-full object-cover"
75
+ />
76
+ </div>
77
+ </div>
78
+ </section>
79
+ </Fragment>
80
+ );
81
+ };
82
+
83
+ import { registerThemeVariant } from '../../lib/component-registry';
84
+ registerThemeVariant('hero-home', 'aman', HeroHome);
@@ -0,0 +1,246 @@
1
+ "use client";
2
+
3
+ import { Fragment, useMemo } from "react";
4
+ import React from "react";
5
+ import { Button, Form, Input, PhotoWithFallback } from '../elements';
6
+ import { cx } from '../../utils/cx';
7
+ import { mapIcon } from '../utils/icon-mapping';
8
+ import type { FC } from "react";
9
+ import type { WebsitePhotos } from '../../types/api/website-photos';
10
+
11
+
12
+ interface HeroHomeProps {
13
+ config: {
14
+ pages?: any[];
15
+ };
16
+ websitePhotos?: WebsitePhotos | null;
17
+ reviews?: {
18
+ rating?: number;
19
+ count?: number;
20
+ };
21
+ onEmailSubmit?: (email: string) => void;
22
+ }
23
+
24
+ const AvatarsWithReview = ({
25
+ rating = 5.0,
26
+ count = 200,
27
+ stockPhotos = [],
28
+ className
29
+ }: {
30
+ rating?: number;
31
+ count?: number;
32
+ stockPhotos?: any[];
33
+ className?: string;
34
+ }) => {
35
+ // Get up to 5 stock photos for avatars, cycling if needed
36
+ const avatarPhotos = useMemo(() => {
37
+ if (stockPhotos.length === 0) {
38
+ return Array(5).fill(null).map((_, i) => ({
39
+ url: undefined,
40
+ alt: `Avatar ${i + 1}`,
41
+ fallbackId: `avatar-${i + 1}`
42
+ }));
43
+ }
44
+
45
+ return Array(5).fill(null).map((_, i) => {
46
+ const photo = stockPhotos[i % stockPhotos.length];
47
+ return {
48
+ url: photo?.url || photo?.thumbnail_url,
49
+ alt: photo?.alt || `Avatar ${i + 1}`,
50
+ fallbackId: photo?.id || `avatar-${i + 1}`
51
+ };
52
+ });
53
+ }, [stockPhotos]);
54
+
55
+ return (
56
+ <div className={cx("flex items-center gap-4", className)}>
57
+ <div className="inline-flex -space-x-3 overflow-hidden">
58
+ {avatarPhotos.map((avatar, index) => (
59
+ <div
60
+ key={index}
61
+ className="inline-block size-10 rounded-full ring-[1.5px] ring-bg-primary outline-1 -outline-offset-1 outline-avatar-contrast-border overflow-hidden"
62
+ >
63
+ <PhotoWithFallback
64
+ photoUrl={avatar.url}
65
+ photoAlt={avatar.alt}
66
+ fallbackId={avatar.fallbackId}
67
+ className="size-full object-cover"
68
+ />
69
+ </div>
70
+ ))}
71
+ </div>
72
+ <div className="flex flex-col gap-0.5">
73
+ <div className="flex items-center gap-2">
74
+ <div className="flex items-center gap-1">
75
+ {Array(5)
76
+ .fill(null)
77
+ .map((_, index) => {
78
+ const clipId0 = `clip0_star_${index}`;
79
+ const clipId1 = `clip1_star_${index}`;
80
+ return (
81
+ <svg
82
+ key={index}
83
+ width={20}
84
+ height={20}
85
+ viewBox="0 0 20 20"
86
+ fill="none"
87
+ className="relative size-5 shrink-0 grow-0"
88
+ preserveAspectRatio="none"
89
+ >
90
+ <g clipPath={`url(#${clipId0})`}>
91
+ <path
92
+ d="M9.53834 1.60996C9.70914 1.19932 10.2909 1.19932 10.4617 1.60996L12.5278 6.57744C12.5998 6.75056 12.7626 6.86885 12.9495 6.88383L18.3123 7.31376C18.7556 7.3493 18.9354 7.90256 18.5976 8.19189L14.5117 11.6919C14.3693 11.8139 14.3071 12.0053 14.3506 12.1876L15.5989 17.4208C15.7021 17.8534 15.2315 18.1954 14.8519 17.9635L10.2606 15.1592C10.1006 15.0615 9.89938 15.0615 9.73937 15.1592L5.14806 17.9635C4.76851 18.1954 4.29788 17.8534 4.40108 17.4208L5.64939 12.1876C5.69289 12.0053 5.6307 11.8139 5.48831 11.6919L1.40241 8.19189C1.06464 7.90256 1.24441 7.3493 1.68773 7.31376L7.05054 6.88383C7.23744 6.86885 7.40024 6.75056 7.47225 6.57744L9.53834 1.60996Z"
93
+ className="fill-bg-tertiary"
94
+ />
95
+ <g clipPath={`url(#${clipId1})`}>
96
+ <path
97
+ d="M9.53834 1.60996C9.70914 1.19932 10.2909 1.19932 10.4617 1.60996L12.5278 6.57744C12.5998 6.75056 12.7626 6.86885 12.9495 6.88383L18.3123 7.31376C18.7556 7.3493 18.9354 7.90256 18.5976 8.19189L14.5117 11.6919C14.3693 11.8139 14.3071 12.0053 14.3506 12.1876L15.5989 17.4208C15.7021 17.8534 15.2315 18.1954 14.8519 17.9635L10.2606 15.1592C10.1006 15.0615 9.89938 15.0615 9.73937 15.1592L5.14806 17.9635C4.76851 18.1954 4.29788 17.8534 4.40108 17.4208L5.64939 12.1876C5.69289 12.0053 5.6307 11.8139 5.48831 11.6919L1.40241 8.19189C1.06464 7.90256 1.24441 7.3493 1.68773 7.31376L7.05054 6.88383C7.23744 6.86885 7.40024 6.75056 7.47225 6.57744L9.53834 1.60996Z"
98
+ className="fill-warning-300"
99
+ />
100
+ </g>
101
+ </g>
102
+ <defs>
103
+ <clipPath id={clipId0}>
104
+ <rect width={20} height={20} fill="white" />
105
+ </clipPath>
106
+ <clipPath id={clipId1}>
107
+ <rect width={20} height={20} fill="white" />
108
+ </clipPath>
109
+ </defs>
110
+ </svg>
111
+ );
112
+ })}
113
+ </div>
114
+ </div>
115
+ <p className="text-md font-medium text-tertiary">from {count}+ reviews</p>
116
+ </div>
117
+ </div>
118
+ );
119
+ };
120
+
121
+ export const HeroHome = ({
122
+ config,
123
+ websitePhotos,
124
+ reviews,
125
+ onEmailSubmit,
126
+ }: HeroHomeProps) => {
127
+ // Extract values from config
128
+ const headline = '';
129
+ const subhead = '';
130
+ const email_signup = undefined ? {
131
+ placeholder: heroSection.email_signup.placeholder || "Enter your email",
132
+ button_text: heroSection.email_signup.button_text || "Get started",
133
+ privacy_policy_link: heroSection.email_signup.privacy_policy_link || "#",
134
+ } : undefined;
135
+ const statistics = (undefined || []).map((stat: any) => ({
136
+ number: stat.number,
137
+ label: stat.label,
138
+ icon: stat.icon,
139
+ color: stat.color,
140
+ }));
141
+
142
+ // Get hero image from props
143
+ const heroImage = {
144
+ url: websitePhotos?.hero?.url || '',
145
+ alt: websitePhotos?.hero?.alt || 'Hero image',
146
+ };
147
+
148
+ const stockPhotos = websitePhotos?.stock_photos || [];
149
+
150
+ return (
151
+ <Fragment>
152
+ <section className="bg-primary py-16 md:pb-24">
153
+ <div className="mx-auto grid max-w-container grid-cols-1 items-center gap-16 px-4 md:px-8 lg:grid-cols-2 lg:gap-8">
154
+ <div className="flex max-w-3xl flex-col items-start lg:pr-8">
155
+ <h1 className="text-display-md font-semibold text-primary md:text-display-lg lg:text-display-xl">
156
+ {headline || 'Welcome'}
157
+ </h1>
158
+ <p className="mt-4 max-w-lg text-lg text-balance text-tertiary md:mt-6 md:text-xl">
159
+ {subhead || ''}
160
+ </p>
161
+
162
+ {email_signup && (
163
+ <Form
164
+ onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
165
+ e.preventDefault();
166
+ const formData = new FormData(e.currentTarget);
167
+ const email = formData.get("email") as string;
168
+ if (onEmailSubmit) {
169
+ onEmailSubmit(email);
170
+ }
171
+ }}
172
+ className="mt-8 flex w-full flex-col items-stretch gap-4 md:mt-12 md:max-w-120 md:flex-row md:items-start"
173
+ >
174
+ <Input
175
+ isRequired
176
+ size="md"
177
+ name="email"
178
+ type="email"
179
+ placeholder={email_signup.placeholder}
180
+ wrapperClassName="py-0.5"
181
+ hint={
182
+ <span>
183
+ We care about your data in our{" "}
184
+ <a
185
+ href={email_signup.privacy_policy_link}
186
+ className="rounded-xs underline underline-offset-3 outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
187
+ >
188
+ privacy policy
189
+ </a>
190
+ .
191
+ </span>
192
+ }
193
+ />
194
+ <Button type="submit" color="primary" size="xl">
195
+ {email_signup.button_text}
196
+ </Button>
197
+ </Form>
198
+ )}
199
+
200
+ <AvatarsWithReview
201
+ rating={reviews?.rating}
202
+ count={reviews?.count}
203
+ stockPhotos={stockPhotos}
204
+ className="mt-8 md:mt-12"
205
+ />
206
+
207
+ {(() => {
208
+ const statsToShow = statistics && statistics.length > 0 ? statistics.slice(0, 4) : [];
209
+
210
+ return statsToShow.length > 0 && (
211
+ <dl className="mt-8 grid grid-cols-2 gap-x-6 gap-y-4 md:mt-12">
212
+ {statsToShow.map((stat: { number: string; label: string; icon?: string | FC<{ className?: string }> | React.ReactNode; color?: string }, index: number) => {
213
+ const IconComponent = mapIcon(stat.icon);
214
+
215
+ return (
216
+ <div key={index} className="flex flex-col gap-2">
217
+ <div className="flex items-center gap-2">
218
+ {IconComponent && (
219
+ <div className="size-5 shrink-0">
220
+ <IconComponent className="w-full h-full" />
221
+ </div>
222
+ )}
223
+ <dd className="text-lg font-semibold text-primary md:text-xl">{stat.number}</dd>
224
+ </div>
225
+ <dt className="text-sm font-medium text-tertiary md:text-md">{stat.label}</dt>
226
+ </div>
227
+ );
228
+ })}
229
+ </dl>
230
+ );
231
+ })()}
232
+ </div>
233
+
234
+ <div className="relative lg:h-full lg:min-h-160">
235
+ <PhotoWithFallback
236
+ photoUrl={heroImage.url}
237
+ photoAlt={heroImage.alt}
238
+ fallbackId="hero-home-image"
239
+ className="inset-0 h-70 w-full object-cover md:h-110 lg:absolute lg:h-full"
240
+ />
241
+ </div>
242
+ </div>
243
+ </section>
244
+ </Fragment>
245
+ );
246
+ };
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ interface LocationDetailHeroProps {
4
+ location?: any;
5
+ config?: {
6
+ pages?: any[];
7
+ };
8
+ }
9
+
10
+ export const LocationDetailHero = ({
11
+ location,
12
+ }: LocationDetailHeroProps) => {
13
+ if (!location) return null;
14
+
15
+ return (
16
+ <section className="py-24 md:py-32">
17
+ <div className="mx-auto max-w-4xl px-4 text-center md:px-8">
18
+ <h1 className="font-display text-5xl font-normal leading-tight text-fg-primary md:text-6xl lg:text-7xl">
19
+ {location.name}
20
+ </h1>
21
+
22
+ {location.city && location.state && (
23
+ <p className="mt-6 text-xs font-body uppercase tracking-widest" style={{ color: 'var(--color-text-brand-secondary)' }}>
24
+ {location.city}, {location.state}
25
+ </p>
26
+ )}
27
+ </div>
28
+ </section>
29
+ );
30
+ };
31
+
32
+ import { registerThemeVariant } from '../../lib/component-registry';
33
+ registerThemeVariant('hero-location-detail', 'aman', LocationDetailHero);
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ import { ChevronLeft } from "@untitledui/icons";
4
+ import { Button } from '../elements';
5
+ import type { Location } from '../../types/api/location';
6
+
7
+ interface LocationDetailHeroProps {
8
+ config: {
9
+ pages?: any[];
10
+ };
11
+ location?: Location | null;
12
+ }
13
+
14
+ export const LocationDetailHero = ({
15
+ config,
16
+ location,
17
+ }: LocationDetailHeroProps) => {
18
+ if (!location) return null;
19
+
20
+ const breadcrumb = {
21
+ backHref: "/locations",
22
+ backLabel: "Locations",
23
+ currentLabel: location.name || "Location",
24
+ };
25
+
26
+ const headline = location.name || "Location";
27
+ const subhead = location.description_markdown?.replace(/[#*\[\]()]/g, '').trim() || "";
28
+ const ctaText = "Contact Us";
29
+ const ctaHref = "/contact";
30
+
31
+ return (
32
+ <section className="bg-primary py-4 md:py-6">
33
+ <div className="mx-auto max-w-container px-4 md:px-8">
34
+ {breadcrumb && (
35
+ <div className="mb-8">
36
+ <nav aria-label="Breadcrumb">
37
+ <div className="flex items-center gap-2">
38
+ <Button
39
+ color="link-gray"
40
+ size="md"
41
+ href={breadcrumb.backHref}
42
+ iconLeading={ChevronLeft}
43
+ className="text-tertiary hover:text-primary"
44
+ >
45
+ {breadcrumb.backLabel}
46
+ </Button>
47
+ <span className="text-tertiary">/</span>
48
+ <span className="text-primary font-medium">{breadcrumb.currentLabel}</span>
49
+ </div>
50
+ </nav>
51
+ </div>
52
+ )}
53
+
54
+ <div className="mx-auto flex w-full max-w-3xl flex-col items-center text-center">
55
+ <h1 className="text-display-md font-semibold text-primary md:text-display-lg">
56
+ {headline}
57
+ </h1>
58
+
59
+ <p className="mt-4 text-lg text-tertiary md:mt-6 md:text-xl">
60
+ {subhead}
61
+ </p>
62
+
63
+ <div className="mt-8">
64
+ <Button size="xl" href={ctaHref}>
65
+ {ctaText}
66
+ </Button>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </section>
71
+ );
72
+ };