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,91 @@
1
+ "use client";
2
+
3
+ import type { JobPosting } from '../../types/api/job-posting';
4
+
5
+ interface JobGalleryProps {
6
+ config: {
7
+ pages?: any[];
8
+ };
9
+ jobs?: JobPosting[] | null;
10
+ pageName?: string;
11
+ sectionKey?: string;
12
+ }
13
+
14
+ export const JobGallery = ({
15
+ config,
16
+ jobs: jobsData,
17
+ pageName = 'Careers',
18
+ sectionKey = 'careers_page_section_2_jobs',
19
+ }: JobGalleryProps) => {
20
+ const jobs = Array.isArray(jobsData) ? jobsData : [];
21
+
22
+ const title = 'Open Positions';
23
+ const maxItems = undefined;
24
+ const displayJobs = maxItems ? jobs.slice(0, maxItems) : jobs;
25
+
26
+ return (
27
+ <section>
28
+ <div className="mx-auto max-w-container px-4 md:px-8">
29
+ <div className="mb-12 text-center">
30
+ <h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
31
+ {title}
32
+ </h2>
33
+ </div>
34
+
35
+ {displayJobs.length > 0 ? (
36
+ <div className="space-y-6">
37
+ {displayJobs.map((job: any) => {
38
+ const description = job.description_markdown?.replace(/[#*\[\]()]/g, '').trim().substring(0, 200) || '';
39
+
40
+ return (
41
+ <div key={job.id} className="border-b border-secondary pb-6">
42
+ <div className="flex flex-col md:flex-row md:items-center md:justify-between">
43
+ <div className="flex-grow">
44
+ <h3 className="font-display text-2xl font-normal text-fg-primary mb-2">
45
+ {job.title}
46
+ </h3>
47
+
48
+ <div className="flex gap-4 mb-4">
49
+ {job.department && (
50
+ <p className="text-xs font-body uppercase tracking-widest" style={{ color: 'var(--color-text-brand-secondary)' }}>
51
+ {job.department}
52
+ </p>
53
+ )}
54
+ {job.location && (
55
+ <p className="text-xs font-body uppercase tracking-widest" style={{ color: 'var(--color-text-brand-secondary)' }}>
56
+ {job.location}
57
+ </p>
58
+ )}
59
+ </div>
60
+
61
+ {description && (
62
+ <p className="font-body text-base text-tertiary mb-4">
63
+ {description}
64
+ </p>
65
+ )}
66
+ </div>
67
+
68
+ <a
69
+ href={`/careers/${job.slug}`}
70
+ className="font-body text-base underline underline-offset-4 hover:no-underline whitespace-nowrap md:ml-6 transition-colors"
71
+ style={{ color: 'var(--color-text-brand-accent)' }}
72
+ >
73
+ View details
74
+ </a>
75
+ </div>
76
+ </div>
77
+ );
78
+ })}
79
+ </div>
80
+ ) : (
81
+ <div className="text-center py-12">
82
+ <p className="font-body text-base text-tertiary">No open positions at this time</p>
83
+ </div>
84
+ )}
85
+ </div>
86
+ </section>
87
+ );
88
+ };
89
+
90
+ import { registerThemeVariant } from '../../lib/component-registry';
91
+ registerThemeVariant('job-gallery', 'aman', JobGallery);
@@ -0,0 +1,183 @@
1
+ "use client";
2
+
3
+ import React from 'react';
4
+ import { Clock, CurrencyDollarCircle } from '@untitledui/icons';
5
+ import { Badge, BadgeWithDot, Button, PhotoWithFallback } from '../elements';
6
+
7
+ import type { JobPosting } from '../../types/api/job-posting';
8
+ import type { WebsitePhotos } from '../../types/api/website-photos';
9
+
10
+ // Helper to get careers page section from config
11
+ const getCareersPageSection = (config: { pages?: any[] }, sectionKey: string) => {
12
+ if (!config?.pages) return null;
13
+ const careersPage = config.pages.find((p: any) => p.library_reference_name === 'Careers');
14
+ return (careersPage?.sections as any)?.[sectionKey];
15
+ };
16
+
17
+ interface JobGalleryProps {
18
+ // Config data - component extracts all values from it
19
+ config: {
20
+ pages?: any[];
21
+ };
22
+ jobs?: JobPosting[] | null;
23
+ websitePhotos?: WebsitePhotos | null;
24
+ }
25
+
26
+ export const JobGallery = ({
27
+ config,
28
+ jobs: jobsData,
29
+ websitePhotos,
30
+ }: JobGalleryProps) => {
31
+ // Get job postings from props
32
+ const jobs = Array.isArray(jobsData) ? jobsData : [];
33
+
34
+ // Get careers photo from website_photos
35
+ const careersPhoto = websitePhotos?.careers;
36
+
37
+ // Extract values from config
38
+ const label = "Careers";
39
+ const title = "Join Our Team";
40
+ const subtitle = "";
41
+ const backgroundColor = "bg-primary";
42
+ const className = "";
43
+
44
+ // Use careers photo from API, undefined if not available (triggers gradient fallback)
45
+ const finalHeaderImage = careersPhoto?.url;
46
+ const finalHeaderImageAlt = careersPhoto?.alt || "Team collaboration";
47
+
48
+ // Group jobs by employment_type
49
+ const groupedJobs = React.useMemo(() => {
50
+ if (!jobs.length) return [];
51
+
52
+ const groups: Record<string, JobPosting[]> = {};
53
+ jobs.forEach((job) => {
54
+ const category = job.employment_type || 'General';
55
+ if (!groups[category]) {
56
+ groups[category] = [];
57
+ }
58
+ groups[category].push(job);
59
+ });
60
+
61
+ return Object.entries(groups);
62
+ }, [jobs]);
63
+
64
+ return (
65
+ <div className={`${backgroundColor} ${className}`}>
66
+ {/* Hero Section */}
67
+ <section className={`${backgroundColor} py-16 md:py-24`}>
68
+ <div className="mx-auto w-full max-w-container px-4 md:px-8">
69
+ <div className="mx-auto flex w-full max-w-3xl flex-col items-center text-center">
70
+ {label && (
71
+ <span className="text-sm font-semibold text-brand-secondary md:text-md">{label}</span>
72
+ )}
73
+ <h1 className="mt-3 text-display-md font-semibold text-primary md:text-display-lg">
74
+ {title}
75
+ </h1>
76
+ <p className="mt-4 text-lg text-tertiary md:mt-6 md:text-xl">
77
+ {subtitle}
78
+ </p>
79
+ </div>
80
+ </div>
81
+ </section>
82
+
83
+ {/* Jobs Section */}
84
+ <section className={`${backgroundColor} py-16 md:py-24`}>
85
+ <div className="mx-auto w-full max-w-container px-4 md:px-8">
86
+ <div className="mx-auto flex w-full max-w-3xl flex-col items-center text-center">
87
+ <Badge className="hidden md:flex" size="lg" color="brand" type="pill-color">
88
+ Careers
89
+ </Badge>
90
+ <Badge className="md:hidden" size="md" color="brand" type="pill-color">
91
+ Careers
92
+ </Badge>
93
+
94
+ <h2 className="mt-4 text-display-sm font-semibold text-primary md:text-display-md">
95
+ {title}
96
+ </h2>
97
+ <p className="mt-4 text-lg text-tertiary md:mt-5 md:text-xl">
98
+ {subtitle}
99
+ </p>
100
+ </div>
101
+
102
+ {/* Header Image using PhotoWithFallback */}
103
+ <div className="mt-12 h-60 w-full md:mt-16 md:h-140 rounded-xl overflow-hidden">
104
+ <PhotoWithFallback
105
+ photoUrl={finalHeaderImage}
106
+ photoAlt={finalHeaderImageAlt}
107
+ fallbackId="careers-header"
108
+ className="size-full object-cover"
109
+ />
110
+ </div>
111
+
112
+ {groupedJobs.length > 0 ? (
113
+ <div className="mx-auto mt-12 max-w-3xl md:mt-16">
114
+ <ul className="flex flex-col gap-8 md:gap-16">
115
+ {groupedJobs.map(([category, categoryJobs]) => (
116
+ <li key={category}>
117
+ <h2 className="text-lg font-semibold text-primary md:text-xl">{category}</h2>
118
+ <ul className="mt-5 flex flex-col gap-4 md:mt-8 md:gap-6">
119
+ {categoryJobs.map((job, index) => {
120
+ const description = job.description_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
121
+
122
+ return (
123
+ <li key={job.id || index}>
124
+ <div
125
+ className="flex flex-col rounded-2xl bg-primary p-6 ring-1 ring-secondary ring-inset"
126
+ >
127
+ <div className="flex flex-col items-start gap-2 md:flex-row">
128
+ <h3 className="text-md font-semibold text-primary">{job.title}</h3>
129
+
130
+ <div className="flex flex-1 gap-2 md:flex-row-reverse md:justify-between">
131
+ {job.location && (
132
+ <Badge color="gray" size="md" type="modern">
133
+ {job.location}
134
+ </Badge>
135
+ )}
136
+
137
+ {job.employment_type && (
138
+ <BadgeWithDot color="brand" size="md" type="modern">
139
+ {job.employment_type}
140
+ </BadgeWithDot>
141
+ )}
142
+ </div>
143
+ </div>
144
+
145
+ <p className="mt-2 text-md text-tertiary line-clamp-2">
146
+ {description}
147
+ </p>
148
+
149
+ <div className="mt-5 flex gap-4">
150
+ {job.employment_type && (
151
+ <div className="flex items-center gap-1.5">
152
+ <Clock size={20} className="text-fg-quaternary" />
153
+ <span className="text-sm font-medium text-tertiary">{job.employment_type}</span>
154
+ </div>
155
+ )}
156
+ {job.salary_range && (
157
+ <div className="flex items-center gap-1.5">
158
+ <CurrencyDollarCircle size={20} className="text-fg-quaternary" />
159
+ <span className="text-sm font-medium text-tertiary">{job.salary_range}</span>
160
+ </div>
161
+ )}
162
+ </div>
163
+ </div>
164
+ </li>
165
+ );
166
+ })}
167
+ </ul>
168
+ </li>
169
+ ))}
170
+ </ul>
171
+ </div>
172
+ ) : (
173
+ <div className="text-center mt-12">
174
+ <p className="text-gray-500 mb-4">No open positions at the moment.</p>
175
+ <p className="text-gray-600 mb-6">We're always looking for talented individuals to join our team.</p>
176
+ <Button href="/contact">Send Us Your Resume</Button>
177
+ </div>
178
+ )}
179
+ </div>
180
+ </section>
181
+ </div>
182
+ );
183
+ };
@@ -0,0 +1,179 @@
1
+ "use client";
2
+
3
+ import { PhotoWithFallback, GoogleMap } from '../elements';
4
+ import type { Location } from '../../types/api/location';
5
+
6
+ interface LocationDetailsSectionProps {
7
+ config: {
8
+ pages?: any[];
9
+ };
10
+ location?: Location | null;
11
+ }
12
+
13
+ // Parse business hours - handle both string and object formats
14
+ const parseBusinessHours = (hours: string | Record<string, { open: string; close: string }> | undefined): Array<{ day: string; hours: string }> => {
15
+ if (!hours) return [];
16
+
17
+ if (typeof hours === 'object' && !Array.isArray(hours)) {
18
+ const dayNames: Record<string, string> = {
19
+ monday: 'Monday',
20
+ tuesday: 'Tuesday',
21
+ wednesday: 'Wednesday',
22
+ thursday: 'Thursday',
23
+ friday: 'Friday',
24
+ saturday: 'Saturday',
25
+ sunday: 'Sunday',
26
+ };
27
+
28
+ return Object.entries(hours)
29
+ .filter(([_, value]) => value && typeof value === 'object' && value.open && value.close)
30
+ .map(([day, value]) => ({
31
+ day: dayNames[day.toLowerCase()] || day.charAt(0).toUpperCase() + day.slice(1),
32
+ hours: `${value.open}-${value.close}`,
33
+ }));
34
+ }
35
+
36
+ if (typeof hours === 'string') {
37
+ const entries = hours.split(',').map(entry => {
38
+ const parts = entry.trim().split(':');
39
+ if (parts.length === 2) {
40
+ return { day: parts[0].trim(), hours: parts[1].trim() };
41
+ }
42
+ return null;
43
+ }).filter(Boolean) as Array<{ day: string; hours: string }>;
44
+
45
+ return entries.length > 0 ? entries : [{ day: 'Hours', hours }];
46
+ }
47
+
48
+ return [];
49
+ };
50
+
51
+ export const LocationDetailsSection = ({
52
+ config,
53
+ location,
54
+ }: LocationDetailsSectionProps) => {
55
+ if (!location) return null;
56
+
57
+ const description = location?.description_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
58
+ const fullAddress = `${location.address_line_1}${location.address_line_2 ? `, ${location.address_line_2}` : ''}, ${location.city}, ${location.state} ${location.zip_code}`.trim();
59
+ const businessHours = location.business_hours ? parseBusinessHours(location.business_hours) : [];
60
+
61
+ return (
62
+ <section>
63
+ <div className="mx-auto max-w-container px-4 md:px-8">
64
+ {/* Top Section: Description and Photo */}
65
+ <div className="grid grid-cols-1 gap-12 lg:grid-cols-2 lg:gap-16 mb-16">
66
+ <div className="flex flex-col justify-center">
67
+ <h2 className="font-display text-4xl font-normal leading-tight text-fg-primary md:text-5xl">
68
+ {location.name}
69
+ </h2>
70
+
71
+ {location.city && location.state && (
72
+ <p className="mt-4 text-xs font-body uppercase tracking-widest" style={{ color: 'var(--color-text-brand-secondary)' }}>
73
+ {location.city}, {location.state}
74
+ </p>
75
+ )}
76
+
77
+ {description && (
78
+ <p className="mt-6 font-display text-lg leading-relaxed text-tertiary">
79
+ {description}
80
+ </p>
81
+ )}
82
+ </div>
83
+
84
+ <div className="w-full h-64 md:h-96 overflow-hidden">
85
+ <PhotoWithFallback
86
+ item={location}
87
+ fallbackId={location.id}
88
+ className="w-full h-full object-cover"
89
+ />
90
+ </div>
91
+ </div>
92
+
93
+ {/* Bottom Section: Contact Info and Map */}
94
+ <div className="grid grid-cols-1 gap-12 lg:grid-cols-2 lg:gap-16">
95
+ {/* Contact Information */}
96
+ <div className="space-y-8">
97
+ {location.address_line_1 && (
98
+ <div>
99
+ <p className="text-xs font-body uppercase tracking-widest mb-2" style={{ color: 'var(--color-text-brand-secondary)' }}>
100
+ ADDRESS
101
+ </p>
102
+ <a
103
+ href={`https://maps.google.com/?q=${encodeURIComponent(fullAddress)}`}
104
+ className="font-body text-base underline underline-offset-4 hover:no-underline transition-colors"
105
+ style={{ color: 'var(--color-text-brand-accent)' }}
106
+ target="_blank"
107
+ rel="noopener noreferrer"
108
+ >
109
+ {location.address_line_1}
110
+ {location.address_line_2 && <><br />{location.address_line_2}</>}
111
+ <br />{location.city}, {location.state} {location.zip_code}
112
+ </a>
113
+ </div>
114
+ )}
115
+
116
+ {location.phone && (
117
+ <div>
118
+ <p className="text-xs font-body uppercase tracking-widest mb-2" style={{ color: 'var(--color-text-brand-secondary)' }}>
119
+ PHONE
120
+ </p>
121
+ <a
122
+ href={`tel:${location.phone}`}
123
+ className="font-body text-base underline underline-offset-4 hover:no-underline transition-colors"
124
+ style={{ color: 'var(--color-text-brand-accent)' }}
125
+ >
126
+ {location.phone}
127
+ </a>
128
+ </div>
129
+ )}
130
+
131
+ {location.email && (
132
+ <div>
133
+ <p className="text-xs font-body uppercase tracking-widest mb-2" style={{ color: 'var(--color-text-brand-secondary)' }}>
134
+ EMAIL
135
+ </p>
136
+ <a
137
+ href={`mailto:${location.email}`}
138
+ className="font-body text-base underline underline-offset-4 hover:no-underline transition-colors"
139
+ style={{ color: 'var(--color-text-brand-accent)' }}
140
+ >
141
+ {location.email}
142
+ </a>
143
+ </div>
144
+ )}
145
+
146
+ {businessHours.length > 0 && (
147
+ <div>
148
+ <p className="text-xs font-body uppercase tracking-widest mb-2" style={{ color: 'var(--color-text-brand-secondary)' }}>
149
+ HOURS
150
+ </p>
151
+ <div className="font-body text-base text-tertiary space-y-1">
152
+ {businessHours.map(({ day, hours }) => (
153
+ <div key={day} className="flex justify-between gap-4">
154
+ <span className="font-medium">{day}:</span>
155
+ <span>{hours}</span>
156
+ </div>
157
+ ))}
158
+ </div>
159
+ </div>
160
+ )}
161
+ </div>
162
+
163
+ {fullAddress && (
164
+ <div className="w-full h-96 overflow-hidden">
165
+ <GoogleMap
166
+ address={fullAddress}
167
+ locationName={location.name}
168
+ className="h-full w-full"
169
+ />
170
+ </div>
171
+ )}
172
+ </div>
173
+ </div>
174
+ </section>
175
+ );
176
+ };
177
+
178
+ import { registerThemeVariant } from '../../lib/component-registry';
179
+ registerThemeVariant('location-details-section', 'aman', LocationDetailsSection);
@@ -0,0 +1,196 @@
1
+ "use client";
2
+
3
+ import React from 'react';
4
+ import { Mail01, MarkerPin02, Phone, Clock } from '@untitledui/icons';
5
+ import { Button } from '../elements';
6
+ import { FeaturedIcon, GoogleMap } from '../elements';
7
+ import type { Location } from '../../types/api/location';
8
+
9
+ interface LocationDetailsSectionProps {
10
+ config: {
11
+ pages?: any[];
12
+ };
13
+ location?: Location | null;
14
+ }
15
+
16
+ // Parse business hours - handle both string and object formats
17
+ const parseBusinessHours = (hours: string | Record<string, { open: string; close: string }> | undefined): Array<{ day: string; hours: string }> => {
18
+ if (!hours) return [];
19
+
20
+ if (typeof hours === 'object' && !Array.isArray(hours)) {
21
+ const dayNames: Record<string, string> = {
22
+ monday: 'Monday',
23
+ tuesday: 'Tuesday',
24
+ wednesday: 'Wednesday',
25
+ thursday: 'Thursday',
26
+ friday: 'Friday',
27
+ saturday: 'Saturday',
28
+ sunday: 'Sunday',
29
+ };
30
+
31
+ return Object.entries(hours)
32
+ .filter(([_, value]) => value && typeof value === 'object' && value.open && value.close)
33
+ .map(([day, value]) => ({
34
+ day: dayNames[day.toLowerCase()] || day.charAt(0).toUpperCase() + day.slice(1),
35
+ hours: `${value.open}-${value.close}`,
36
+ }));
37
+ }
38
+
39
+ if (typeof hours === 'string') {
40
+ const entries = hours.split(',').map(entry => {
41
+ const parts = entry.trim().split(':');
42
+ if (parts.length === 2) {
43
+ return { day: parts[0].trim(), hours: parts[1].trim() };
44
+ }
45
+ return null;
46
+ }).filter(Boolean) as Array<{ day: string; hours: string }>;
47
+
48
+ return entries.length > 0 ? entries : [{ day: 'Hours', hours }];
49
+ }
50
+
51
+ return [];
52
+ };
53
+
54
+ export const LocationDetailsSection = ({
55
+ config,
56
+ location,
57
+ }: LocationDetailsSectionProps) => {
58
+ if (!location) return null;
59
+
60
+ const locationDetailPage = config.pages?.find((p: any) => p.library_reference_name === 'LocationDetail');
61
+ const locationSectionConfig = locationDetailPage?.sections?.location_detail_page_section_2_details;
62
+
63
+ const label = "Contact us";
64
+ const title = locationSectionConfig?.title || "Get in touch";
65
+ const subtitle = locationSectionConfig?.subtitle;
66
+ const emailLabel = locationSectionConfig?.email_label || "Email";
67
+ const emailDescription = locationSectionConfig?.email_description || "Our friendly team is here to help.";
68
+ const officeLabel = locationSectionConfig?.office_label || "Address";
69
+ const officeDescription = locationSectionConfig?.office_description || "Come say hello at our office HQ.";
70
+ const phoneLabel = locationSectionConfig?.phone_label || "Phone";
71
+ const phoneDescription = locationSectionConfig?.phone_description || "Reach us on the phone.";
72
+ const backgroundColor = "bg-primary";
73
+ const className = "";
74
+ const showMap = true;
75
+ const fullAddress = `${location.address_line_1}${location.address_line_2 ? `, ${location.address_line_2}` : ''}, ${location.city}, ${location.state} ${location.zip_code}`.trim();
76
+
77
+ const businessHours = location.business_hours ? parseBusinessHours(location.business_hours) : [];
78
+
79
+ return (
80
+ <section className={`${backgroundColor} py-16 md:py-24 ${className}`}>
81
+ <div className="mx-auto w-full max-w-container px-4 md:px-8">
82
+ <div className="flex w-full max-w-3xl flex-col">
83
+ {label && (
84
+ <span className="text-sm font-semibold text-brand-secondary md:text-md">
85
+ {label}
86
+ </span>
87
+ )}
88
+ {title && (
89
+ <h2 className="mt-3 text-display-sm font-semibold text-primary md:text-display-md">
90
+ {title}
91
+ </h2>
92
+ )}
93
+ {subtitle && (
94
+ <p className="mt-4 text-lg text-tertiary md:mt-5 md:text-xl">
95
+ {subtitle}
96
+ </p>
97
+ )}
98
+ </div>
99
+
100
+ <div className="mt-12 grid grid-cols-1 items-start gap-12 md:mt-16 md:gap-16 lg:grid-cols-3">
101
+ <ul className="col-span-1 grid w-full grid-cols-1 gap-x-8 gap-y-10 md:grid-cols-2 lg:grid-cols-1 lg:gap-y-12">
102
+ {location.address_line_1 && (
103
+ <li className="flex max-w-sm flex-col items-start gap-4 lg:flex-row">
104
+ <FeaturedIcon className="hidden md:flex" size="lg" icon={MarkerPin02} color="brand" theme="light" />
105
+ <FeaturedIcon className="md:hidden" size="md" icon={MarkerPin02} color="brand" theme="light" />
106
+ <div className="lg:pt-2.5">
107
+ <h3 className="text-lg font-semibold text-primary">
108
+ {officeLabel}
109
+ </h3>
110
+ <p className="mt-1 text-md text-tertiary">
111
+ {officeDescription}
112
+ </p>
113
+ <Button
114
+ href={`https://maps.google.com/?q=${encodeURIComponent(fullAddress)}`}
115
+ color="link-color"
116
+ size="lg"
117
+ className="mt-4 whitespace-pre lg:mt-5"
118
+ >
119
+ {location.address_line_1}{location.address_line_2 ? `\n${location.address_line_2}` : ''}
120
+ {`\n${location.city}, ${location.state} ${location.zip_code}`}
121
+ </Button>
122
+ </div>
123
+ </li>
124
+ )}
125
+
126
+ {location.phone && (
127
+ <li className="flex max-w-sm flex-col items-start gap-4 lg:flex-row">
128
+ <FeaturedIcon className="hidden md:flex" size="lg" icon={Phone} color="brand" theme="light" />
129
+ <FeaturedIcon className="md:hidden" size="md" icon={Phone} color="brand" theme="light" />
130
+ <div className="lg:pt-2.5">
131
+ <h3 className="text-lg font-semibold text-primary">
132
+ {phoneLabel}
133
+ </h3>
134
+ <p className="mt-1 text-md text-tertiary">
135
+ {phoneDescription}
136
+ </p>
137
+ <Button href={`tel:${location.phone}`} color="link-color" size="lg" className="mt-4 lg:mt-5">
138
+ {location.phone}
139
+ </Button>
140
+ </div>
141
+ </li>
142
+ )}
143
+
144
+ {location.email && (
145
+ <li className="flex max-w-sm flex-col items-start gap-4 lg:flex-row">
146
+ <FeaturedIcon className="hidden md:flex" size="lg" icon={Mail01} color="brand" theme="light" />
147
+ <FeaturedIcon className="md:hidden" size="md" icon={Mail01} color="brand" theme="light" />
148
+ <div className="lg:pt-2.5">
149
+ <h3 className="text-lg font-semibold text-primary">
150
+ {emailLabel}
151
+ </h3>
152
+ <p className="mt-1 text-md text-tertiary">
153
+ {emailDescription}
154
+ </p>
155
+ <Button href={`mailto:${location.email}`} color="link-color" size="lg" className="mt-4 lg:mt-5">
156
+ {location.email}
157
+ </Button>
158
+ </div>
159
+ </li>
160
+ )}
161
+
162
+ {businessHours.length > 0 && (
163
+ <li className="flex max-w-sm flex-col items-start gap-4 lg:flex-row">
164
+ <FeaturedIcon className="hidden md:flex" size="lg" icon={Clock} color="brand" theme="light" />
165
+ <FeaturedIcon className="md:hidden" size="md" icon={Clock} color="brand" theme="light" />
166
+ <div className="lg:pt-2.5">
167
+ <h3 className="text-lg font-semibold text-primary">
168
+ Business Hours
169
+ </h3>
170
+ <div className="mt-1 flex flex-col gap-2 text-md text-tertiary">
171
+ {businessHours.map(({ day, hours }) => (
172
+ <div key={day} className="flex justify-between gap-4">
173
+ <span className="font-medium capitalize">{day}:</span>
174
+ <span>{hours}</span>
175
+ </div>
176
+ ))}
177
+ </div>
178
+ </div>
179
+ </li>
180
+ )}
181
+ </ul>
182
+
183
+ {showMap && fullAddress && (
184
+ <div className="col-span-2 h-60 w-full border-none lg:h-full">
185
+ <GoogleMap
186
+ address={fullAddress}
187
+ locationName={location.name}
188
+ className="h-full"
189
+ />
190
+ </div>
191
+ )}
192
+ </div>
193
+ </div>
194
+ </section>
195
+ );
196
+ };