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,76 @@
1
+ "use client";
2
+
3
+ import { PhotoWithFallback } from '../elements';
4
+ import type { Location } from '../../types/api/location';
5
+
6
+ interface LocationGridProps {
7
+ config: {
8
+ pages?: any[];
9
+ };
10
+ locations?: Location[] | null;
11
+ pageName?: string;
12
+ sectionKey?: string;
13
+ }
14
+
15
+ export const LocationGrid = ({
16
+ config,
17
+ locations: locationsData,
18
+ pageName = 'Locations',
19
+ sectionKey = 'locations_page_section_2_locations',
20
+ }: LocationGridProps) => {
21
+ const locations = Array.isArray(locationsData) ? locationsData : [];
22
+ const title = 'Our Locations';
23
+
24
+ return (
25
+ <section>
26
+ <div className="mx-auto max-w-container px-4 md:px-8">
27
+ <div className="mb-12 text-center">
28
+ <h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
29
+ {title}
30
+ </h2>
31
+ </div>
32
+
33
+ {locations.length > 0 ? (
34
+ <div className="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-3">
35
+ {locations.map((location: any) => (
36
+ <div key={location.id} className="flex flex-col">
37
+ <div className="w-full h-64 mb-6 overflow-hidden">
38
+ <PhotoWithFallback
39
+ item={location}
40
+ fallbackId={location.id}
41
+ className="w-full h-full object-cover"
42
+ />
43
+ </div>
44
+
45
+ <h3 className="font-display text-2xl font-normal text-fg-primary mb-2">
46
+ {location.name}
47
+ </h3>
48
+
49
+ {location.city && location.state && (
50
+ <p className="font-body text-base text-tertiary mb-4">
51
+ {location.city}, {location.state}
52
+ </p>
53
+ )}
54
+
55
+ <a
56
+ href={`/locations/${location.slug}`}
57
+ className="font-body text-base underline underline-offset-4 hover:no-underline transition-colors"
58
+ style={{ color: 'var(--color-text-brand-accent)' }}
59
+ >
60
+ Discover more
61
+ </a>
62
+ </div>
63
+ ))}
64
+ </div>
65
+ ) : (
66
+ <div className="text-center py-12">
67
+ <p className="font-body text-base text-tertiary">No locations available</p>
68
+ </div>
69
+ )}
70
+ </div>
71
+ </section>
72
+ );
73
+ };
74
+
75
+ import { registerThemeVariant } from '../../lib/component-registry';
76
+ registerThemeVariant('location-grid', 'aman', LocationGrid);
@@ -0,0 +1,123 @@
1
+ "use client";
2
+
3
+ import React from 'react';
4
+ import { ArrowRight } from '@untitledui/icons';
5
+ import { Button } from '../elements';
6
+ import type { Location } from '../../types/api/location';
7
+
8
+ // Helper to get locations page section from config
9
+ const getLocationsPageSection = (config: { pages?: any[] }, sectionKey: string) => {
10
+ if (!config?.pages) return null;
11
+ const locationsPage = config.pages.find((p: any) => p.library_reference_name === 'Locations');
12
+ return (locationsPage?.sections as any)?.[sectionKey];
13
+ };
14
+
15
+ interface LocationGridProps {
16
+ config: {
17
+ pages?: any[];
18
+ };
19
+ locations?: Location[] | null;
20
+ }
21
+
22
+ // Simple icon components
23
+ const LocationIcon = ({ className }: { className?: string }) => (
24
+ <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
25
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
26
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
27
+ </svg>
28
+ );
29
+
30
+ const PhoneIcon = ({ className }: { className?: string }) => (
31
+ <svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
32
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
33
+ </svg>
34
+ );
35
+
36
+ export const LocationGrid = ({
37
+ config,
38
+ locations: locationsData,
39
+ }: LocationGridProps) => {
40
+ const locations = Array.isArray(locationsData) ? locationsData : [];
41
+
42
+ const title = "Our Locations";
43
+ const subtitle = "Browse our service locations below. Click any location to learn more.";
44
+ const backgroundColor = "bg-primary";
45
+ const className = "";
46
+
47
+ return (
48
+ <section className={`${backgroundColor} py-16 md:py-24 ${className}`}>
49
+ <div className="mx-auto w-full max-w-container px-4 md:px-8">
50
+ {(title || subtitle) && (
51
+ <div className="mx-auto mb-12 flex w-full max-w-3xl flex-col items-center text-center">
52
+ {title && (
53
+ <h2 className="text-display-sm font-semibold text-primary md:text-display-md">
54
+ {title}
55
+ </h2>
56
+ )}
57
+ {subtitle && (
58
+ <p className="mt-4 text-lg text-tertiary md:mt-5 md:text-xl">
59
+ {subtitle}
60
+ </p>
61
+ )}
62
+ </div>
63
+ )}
64
+
65
+ {locations.length > 0 ? (
66
+ <ul className="grid grid-cols-1 gap-x-8 gap-y-10 sm:grid-cols-2 md:gap-y-12 lg:grid-cols-3">
67
+ {locations.map((location, index) => {
68
+ const fullAddress = `${location.address_line_1}${location.address_line_2 ? `, ${location.address_line_2}` : ''}, ${location.city}, ${location.state} ${location.zip_code}`.trim();
69
+
70
+ return (
71
+ <li key={location.id || index} className="flex flex-col gap-6 rounded-2xl bg-secondary p-6 ring-1 ring-secondary_alt md:p-8">
72
+ <div className="flex flex-col gap-4">
73
+ <h3 className="text-lg font-semibold text-primary md:text-xl">{location.name}</h3>
74
+
75
+ {fullAddress && (
76
+ <div className="flex items-start gap-3">
77
+ <div className="size-5 shrink-0 mt-0.5 text-fg-quaternary">
78
+ <LocationIcon className="w-full h-full" />
79
+ </div>
80
+ <span className="text-md text-tertiary">{fullAddress}</span>
81
+ </div>
82
+ )}
83
+
84
+ {location.phone && (
85
+ <div className="flex items-center gap-3">
86
+ <div className="size-5 shrink-0 text-fg-quaternary">
87
+ <PhoneIcon className="w-full h-full" />
88
+ </div>
89
+ <a
90
+ href={`tel:${location.phone}`}
91
+ className="text-md text-tertiary hover:text-primary transition-colors"
92
+ >
93
+ {location.phone}
94
+ </a>
95
+ </div>
96
+ )}
97
+ </div>
98
+
99
+ <div className="flex flex-col gap-3 mt-auto">
100
+ <Button
101
+ size="lg"
102
+ href={`/locations/${location.slug}`}
103
+ iconTrailing={ArrowRight}
104
+ >
105
+ View details
106
+ </Button>
107
+ </div>
108
+ </li>
109
+ );
110
+ })}
111
+ </ul>
112
+ ) : (
113
+ <div className="text-center py-12">
114
+ <div className="text-gray-400 text-6xl mb-4">📍</div>
115
+ <h3 className="text-xl font-semibold text-primary mb-2">No Locations Available</h3>
116
+ <p className="text-gray-600 mb-4">We're working on adding our service locations.</p>
117
+ <Button href="/contact">Contact Us</Button>
118
+ </div>
119
+ )}
120
+ </div>
121
+ </section>
122
+ );
123
+ };
@@ -0,0 +1,85 @@
1
+ "use client";
2
+
3
+ import { PhotoWithFallback } from '../elements';
4
+ import type { Service } from '../../types/api/service';
5
+
6
+ interface ServicesGridProps {
7
+ config: {
8
+ pages?: any[];
9
+ };
10
+ services?: Service[] | null;
11
+ pageName?: string;
12
+ sectionKey?: string;
13
+ }
14
+
15
+ export const ServicesGrid = ({
16
+ config,
17
+ services: servicesData,
18
+ pageName = 'Services',
19
+ sectionKey = 'services_page_section_2_services',
20
+ }: ServicesGridProps) => {
21
+ const services = Array.isArray(servicesData) ? servicesData : [];
22
+ const title = 'Our Services';
23
+
24
+ return (
25
+ <section>
26
+ <div className="mx-auto max-w-container px-4 md:px-8">
27
+ <div className="mb-12 text-center">
28
+ <h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
29
+ {title}
30
+ </h2>
31
+ </div>
32
+
33
+ {services.length > 0 ? (
34
+ <div className="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-3">
35
+ {services.map((service: Service) => {
36
+ const description = service.summary ||
37
+ (service.description_markdown ? service.description_markdown.replace(/[#*\[\]()]/g, '').trim().substring(0, 150) : '');
38
+
39
+ return (
40
+ <div key={service.id} className="flex flex-col">
41
+ <div className="w-full h-64 mb-6 overflow-hidden">
42
+ <PhotoWithFallback
43
+ item={service}
44
+ fallbackId={service.id}
45
+ className="w-full h-full object-cover"
46
+ />
47
+ </div>
48
+
49
+ <p className="text-xs font-body font-normal uppercase tracking-widest mb-2" style={{ color: 'var(--color-text-brand-secondary)' }}>
50
+ SERVICE
51
+ </p>
52
+
53
+ <h3 className="font-display text-2xl font-normal text-fg-primary mb-4">
54
+ {service.name}
55
+ </h3>
56
+
57
+ {description && (
58
+ <p className="font-body text-base leading-relaxed text-tertiary mb-6 flex-grow">
59
+ {description}
60
+ </p>
61
+ )}
62
+
63
+ <a
64
+ href={`/services/${service.slug}`}
65
+ className="font-body text-base underline underline-offset-4 hover:no-underline transition-colors"
66
+ style={{ color: 'var(--color-text-brand-accent)' }}
67
+ >
68
+ Discover more
69
+ </a>
70
+ </div>
71
+ );
72
+ })}
73
+ </div>
74
+ ) : (
75
+ <div className="text-center py-12">
76
+ <p className="font-body text-base text-tertiary">No services available</p>
77
+ </div>
78
+ )}
79
+ </div>
80
+ </section>
81
+ );
82
+ };
83
+
84
+ import { registerThemeVariant } from '../../lib/component-registry';
85
+ registerThemeVariant('services-grid', 'aman', ServicesGrid);
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import React from 'react';
4
+ import { ArrowRight } from '@untitledui/icons';
5
+ import { Button } from '../elements';
6
+ import { PhotoWithFallback } from '../elements';
7
+ import type { Service } from '../../types/api/service';
8
+
9
+ interface ServicesGridProps {
10
+ // Config data - component extracts all values from it
11
+ config: {
12
+ pages?: any[];
13
+ };
14
+ services?: Service[] | null;
15
+ }
16
+
17
+ export const ServicesGrid = ({
18
+ config,
19
+ services: servicesData,
20
+ }: ServicesGridProps) => {
21
+ // Get services from props
22
+ const services = Array.isArray(servicesData) ? servicesData : [];
23
+
24
+ // Extract values from config
25
+ const title = "Available Services";
26
+ const subtitle = "All of our core services are available in this location. Each can be customized to meet your specific needs.";
27
+ const backgroundColor = "bg-secondary";
28
+ const className = "";
29
+
30
+ return (
31
+ <section className={`${backgroundColor} py-16 md:py-24 ${className}`}>
32
+ <div className="mx-auto w-full max-w-container px-4 md:px-8">
33
+ {(title || subtitle) && (
34
+ <div className="mx-auto mb-12 flex w-full max-w-3xl flex-col items-center text-center">
35
+ {title && (
36
+ <h2 className="text-display-sm font-semibold text-primary md:text-display-md">
37
+ {title}
38
+ </h2>
39
+ )}
40
+ {subtitle && (
41
+ <p className="mt-4 text-lg text-tertiary md:mt-5 md:text-xl">
42
+ {subtitle}
43
+ </p>
44
+ )}
45
+ </div>
46
+ )}
47
+
48
+ {services.length > 0 ? (
49
+ <ul className="grid grid-cols-1 gap-x-8 gap-y-10 sm:grid-cols-2 md:gap-y-12 lg:grid-cols-3">
50
+ {services.map((service, index) => {
51
+ const description = service.summary ||
52
+ (service.description_markdown ? service.description_markdown.replace(/[#*\[\]()]/g, '').trim().substring(0, 120) + '...' : '');
53
+
54
+ // Get service image from photo_attachments
55
+ const photo = service.photo_attachments?.find((pa) => pa.featured)?.photo ||
56
+ service.photo_attachments?.[0]?.photo;
57
+ const imageAlt = photo?.title || service.name;
58
+
59
+ return (
60
+ <li key={service.id || index}>
61
+ <div className="flex flex-col gap-12 bg-secondary p-5 md:inline-flex md:gap-16 md:p-6">
62
+ {/* Service Image using PhotoWithFallback */}
63
+ <div className="h-48 w-full overflow-hidden rounded-lg md:h-64">
64
+ <PhotoWithFallback
65
+ item={service}
66
+ fallbackId={service.id || index}
67
+ alt={imageAlt || "Service image"}
68
+ className="size-full object-cover"
69
+ />
70
+ </div>
71
+
72
+ <div className="flex flex-col gap-4">
73
+ <div>
74
+ <h3 className="text-lg font-semibold text-primary">{service.name}</h3>
75
+ {description && (
76
+ <p className="mt-1 text-md text-tertiary">
77
+ {description}
78
+ </p>
79
+ )}
80
+ </div>
81
+
82
+ <Button
83
+ color="link-color"
84
+ size="lg"
85
+ href={`/services/${service.slug}`}
86
+ iconTrailing={ArrowRight}
87
+ >
88
+ Learn more
89
+ </Button>
90
+ </div>
91
+ </div>
92
+ </li>
93
+ );
94
+ })}
95
+ </ul>
96
+ ) : (
97
+ <div className="text-center py-12">
98
+ <p className="text-gray-500">No services available</p>
99
+ </div>
100
+ )}
101
+ </div>
102
+ </section>
103
+ );
104
+ };
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import { PhotoWithFallback } from '../elements';
4
+ import type { Service } from '../../types/api/service';
5
+
6
+ interface ServicesHomeProps {
7
+ services?: Service[] | null;
8
+ title?: string;
9
+ }
10
+
11
+ export const ServicesHome = ({
12
+ services: servicesData,
13
+ title = 'Our Services',
14
+ }: ServicesHomeProps) => {
15
+ const services = Array.isArray(servicesData) ? servicesData.slice(0, 6) : [];
16
+
17
+ return (
18
+ <section>
19
+ <div className="mx-auto max-w-container px-4 md:px-8">
20
+ <div className="mb-12 text-center">
21
+ <h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
22
+ {title}
23
+ </h2>
24
+ </div>
25
+
26
+ {services.length > 0 ? (
27
+ <div className="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-3">
28
+ {services.map((service: Service) => {
29
+ const description = service.summary ||
30
+ (service.description_markdown ? service.description_markdown.replace(/[#*\[\]()]/g, '').trim().substring(0, 150) : '');
31
+
32
+ return (
33
+ <div key={service.id} className="flex flex-col">
34
+ <div className="w-full h-64 mb-6 overflow-hidden">
35
+ <PhotoWithFallback
36
+ item={service}
37
+ fallbackId={service.id}
38
+ className="w-full h-full object-cover"
39
+ />
40
+ </div>
41
+
42
+ <p className="text-xs font-body font-normal uppercase tracking-widest mb-2" style={{ color: 'var(--color-text-brand-secondary)' }}>
43
+ SERVICE
44
+ </p>
45
+
46
+ <h3 className="font-display text-2xl font-normal text-fg-primary mb-4">
47
+ {service.name}
48
+ </h3>
49
+
50
+ {description && (
51
+ <p className="font-body text-base leading-relaxed text-tertiary mb-6 flex-grow">
52
+ {description}
53
+ </p>
54
+ )}
55
+
56
+ <a
57
+ href={`/services/${service.slug}`}
58
+ className="font-body text-base underline underline-offset-4 hover:no-underline transition-colors"
59
+ style={{ color: 'var(--color-text-brand-accent)' }}
60
+ >
61
+ Discover more
62
+ </a>
63
+ </div>
64
+ );
65
+ })}
66
+ </div>
67
+ ) : (
68
+ <div className="text-center py-12">
69
+ <p className="font-body text-base text-tertiary">No services available</p>
70
+ </div>
71
+ )}
72
+ </div>
73
+ </section>
74
+ );
75
+ };
76
+
77
+ import { registerThemeVariant } from '../../lib/component-registry';
78
+ registerThemeVariant('services-home', 'aman', ServicesHome);
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { ArrowRight } from "@untitledui/icons";
5
+ import { Button, PhotoWithFallback } from '../elements';
6
+ import { cx } from '../../utils/cx';
7
+ import type { Service } from '../../types/api/service';
8
+
9
+
10
+ interface ServicesHomeProps {
11
+ services: Service[];
12
+ label?: string;
13
+ title?: string;
14
+ subtitle?: string;
15
+ viewAllText?: string;
16
+ }
17
+
18
+ export const ServicesHome = ({
19
+ services: servicesData,
20
+ label = "Services",
21
+ title = "Our Services",
22
+ subtitle = "",
23
+ viewAllText = "View all services",
24
+ }: ServicesHomeProps) => {
25
+ const services = Array.isArray(servicesData) ? servicesData.slice(0, 6) : [];
26
+ const viewAllHref = "/services";
27
+
28
+ const [internalSelectedId, setInternalSelectedId] = useState<string | undefined>(
29
+ services.length > 0 ? services[0].id.toString() : undefined
30
+ );
31
+
32
+ const handleServiceClick = (serviceId: string) => {
33
+ setInternalSelectedId(serviceId);
34
+ };
35
+
36
+ const currentSelectedId = internalSelectedId || (services.length > 0 ? services[0].id.toString() : undefined);
37
+ const selectedService = services.find(s => s.id.toString() === currentSelectedId) || services[0];
38
+
39
+ const getServiceDescription = (service: Service | undefined): string => {
40
+ if (!service) return '';
41
+ return (service as any)?.summary || service.description_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
42
+ };
43
+
44
+ return (
45
+ <section className="overflow-hidden bg-primary py-16 md:py-24">
46
+ <div className="mx-auto w-full max-w-container px-4 md:px-8">
47
+ <div className="flex w-full flex-col lg:max-w-3xl">
48
+ <span className="text-sm font-semibold text-brand-secondary md:text-md">{label}</span>
49
+ <h2 className="mt-3 text-display-sm font-semibold text-primary md:text-display-md">
50
+ {title}
51
+ </h2>
52
+ {subtitle && (
53
+ <p className="mt-4 text-lg text-tertiary md:mt-5 md:text-xl">
54
+ {subtitle}
55
+ </p>
56
+ )}
57
+ </div>
58
+
59
+ <div className="mt-12 grid grid-cols-1 gap-12 md:mt-16 md:gap-16 lg:grid-cols-5 lg:items-start">
60
+ {/* Left side - Service list */}
61
+ <div className="lg:col-span-2">
62
+ <ul className="flex flex-col">
63
+ {services.map((service) => {
64
+ const serviceId = service.id.toString();
65
+ const isSelected = serviceId === currentSelectedId;
66
+ return (
67
+ <li key={service.id}>
68
+ <div className="relative">
69
+ {isSelected && (
70
+ <div className="absolute left-0 top-0 bottom-0 w-1 bg-brand-secondary rounded-r" />
71
+ )}
72
+ <div
73
+ className={cx(
74
+ "flex items-center py-4 pl-6 pr-4",
75
+ isSelected && "pl-5",
76
+ "hover:bg-primary_hover transition-colors cursor-pointer"
77
+ )}
78
+ onClick={() => handleServiceClick(serviceId)}
79
+ >
80
+ <h3 className={cx(
81
+ "text-lg font-semibold",
82
+ isSelected ? "text-brand-secondary" : "text-primary"
83
+ )}>
84
+ {service.name}
85
+ </h3>
86
+ </div>
87
+ </div>
88
+ </li>
89
+ );
90
+ })}
91
+ </ul>
92
+
93
+ <div className="mt-4">
94
+ <Button
95
+ color="link-color"
96
+ size="lg"
97
+ href={viewAllHref}
98
+ iconTrailing={ArrowRight}
99
+ >
100
+ {viewAllText}
101
+ </Button>
102
+ </div>
103
+ </div>
104
+
105
+ {/* Right side - Featured service image */}
106
+ {selectedService && (
107
+ <div className="lg:col-span-3">
108
+ <div className="relative flex w-full items-center justify-center h-108 md:h-128">
109
+ <PhotoWithFallback
110
+ item={selectedService}
111
+ fallbackId={selectedService.id}
112
+ alt={selectedService.name || "Service image"}
113
+ className="absolute top-0 left-0 z-0 size-full object-cover"
114
+ />
115
+
116
+ <div className="z-10 flex w-full items-center justify-center p-3 md:p-4">
117
+ <div className="w-[70%] rounded-xl bg-primary/30 px-4 pt-5 pb-6 ring-1 ring-alpha-white/30 backdrop-blur-[10px] ring-inset">
118
+ <h3 className="text-xl font-semibold text-white">{selectedService.name}</h3>
119
+ {getServiceDescription(selectedService) && (
120
+ <p className="mt-2 text-md text-white/90">{getServiceDescription(selectedService)}</p>
121
+ )}
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ )}
127
+ </div>
128
+ </div>
129
+ </section>
130
+ );
131
+ };