keystone-design-bootstrap 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -0
- package/package.json +59 -0
- package/src/contexts/ThemeContext.tsx +34 -0
- package/src/contexts/index.ts +1 -0
- package/src/design_system/elements/IconComponent.tsx +98 -0
- package/src/design_system/elements/avatar/avatar-label-group.tsx +30 -0
- package/src/design_system/elements/avatar/avatar-profile-photo.tsx +125 -0
- package/src/design_system/elements/avatar/avatar.tsx +131 -0
- package/src/design_system/elements/avatar/base-components/avatar-add-button.tsx +34 -0
- package/src/design_system/elements/avatar/base-components/avatar-company-icon.tsx +26 -0
- package/src/design_system/elements/avatar/base-components/avatar-online-indicator.tsx +31 -0
- package/src/design_system/elements/avatar/base-components/index.tsx +4 -0
- package/src/design_system/elements/avatar/base-components/verified-tick.tsx +34 -0
- package/src/design_system/elements/avatar/utils.ts +12 -0
- package/src/design_system/elements/badges/avatar.tsx +132 -0
- package/src/design_system/elements/badges/badge-groups.tsx +176 -0
- package/src/design_system/elements/badges/badge-types.ts +266 -0
- package/src/design_system/elements/badges/badges.tsx +430 -0
- package/src/design_system/elements/breadcrumb/Breadcrumb.tsx +33 -0
- package/src/design_system/elements/button-group/button-group.tsx +106 -0
- package/src/design_system/elements/buttons/app-store-buttons-outline.tsx +378 -0
- package/src/design_system/elements/buttons/app-store-buttons.tsx +567 -0
- package/src/design_system/elements/buttons/button-utility.tsx +116 -0
- package/src/design_system/elements/buttons/button.aman.tsx +174 -0
- package/src/design_system/elements/buttons/button.tsx +271 -0
- package/src/design_system/elements/buttons/close-button.tsx +42 -0
- package/src/design_system/elements/buttons/round-button.tsx +29 -0
- package/src/design_system/elements/buttons/social-button.tsx +148 -0
- package/src/design_system/elements/buttons/social-logos.tsx +115 -0
- package/src/design_system/elements/carousel/carousel-base.tsx +308 -0
- package/src/design_system/elements/carousel/carousel.tsx +308 -0
- package/src/design_system/elements/checkbox/checkbox.tsx +120 -0
- package/src/design_system/elements/date-picker/calendar.tsx +101 -0
- package/src/design_system/elements/date-picker/cell.tsx +106 -0
- package/src/design_system/elements/date-picker/date-input.tsx +32 -0
- package/src/design_system/elements/date-picker/date-picker.tsx +86 -0
- package/src/design_system/elements/date-picker/date-range-picker.tsx +163 -0
- package/src/design_system/elements/date-picker/range-calendar.tsx +161 -0
- package/src/design_system/elements/date-picker/range-preset.tsx +28 -0
- package/src/design_system/elements/featured-icon/featured-icon.tsx +154 -0
- package/src/design_system/elements/form/form.tsx +10 -0
- package/src/design_system/elements/form/hook-form.tsx +75 -0
- package/src/design_system/elements/hint-text/hint-text.tsx +33 -0
- package/src/design_system/elements/index.tsx +158 -0
- package/src/design_system/elements/input/hint-text.tsx +33 -0
- package/src/design_system/elements/input/input-group.tsx +133 -0
- package/src/design_system/elements/input/input.aman.tsx +172 -0
- package/src/design_system/elements/input/input.tsx +271 -0
- package/src/design_system/elements/input/label.tsx +50 -0
- package/src/design_system/elements/label/label.tsx +50 -0
- package/src/design_system/elements/loading-indicator/loading-indicator.tsx +123 -0
- package/src/design_system/elements/map/GoogleMap.tsx +286 -0
- package/src/design_system/elements/markdown-renderer/MarkdownRenderer.tsx +155 -0
- package/src/design_system/elements/modals/modal.tsx +41 -0
- package/src/design_system/elements/pagination/pagination-base.tsx +378 -0
- package/src/design_system/elements/pagination/pagination-dot.tsx +54 -0
- package/src/design_system/elements/pagination/pagination-line.tsx +50 -0
- package/src/design_system/elements/pagination/pagination.tsx +330 -0
- package/src/design_system/elements/photo-fallback/photo-fallback.tsx +143 -0
- package/src/design_system/elements/progress-indicators/progress-circles.tsx +176 -0
- package/src/design_system/elements/progress-indicators/progress-indicators.tsx +123 -0
- package/src/design_system/elements/progress-indicators/simple-circle.tsx +29 -0
- package/src/design_system/elements/radio-buttons/radio-buttons.tsx +129 -0
- package/src/design_system/elements/rating/rating-badge.tsx +144 -0
- package/src/design_system/elements/rating/rating-stars.tsx +77 -0
- package/src/design_system/elements/select/combobox.tsx +152 -0
- package/src/design_system/elements/select/multi-select.tsx +363 -0
- package/src/design_system/elements/select/popover.tsx +34 -0
- package/src/design_system/elements/select/select-item.tsx +97 -0
- package/src/design_system/elements/select/select-native.tsx +69 -0
- package/src/design_system/elements/select/select.aman.tsx +75 -0
- package/src/design_system/elements/select/select.tsx +146 -0
- package/src/design_system/elements/shared-assets/credit-card/credit-card.tsx +237 -0
- package/src/design_system/elements/shared-assets/credit-card/icons.tsx +75 -0
- package/src/design_system/elements/shared-assets/iphone-mockup.tsx +172 -0
- package/src/design_system/elements/shared-assets/section-divider.tsx +12 -0
- package/src/design_system/elements/slideout-menus/slideout-menu.tsx +122 -0
- package/src/design_system/elements/tabs/tabs.tsx +225 -0
- package/src/design_system/elements/tags/base-components/tag-checkbox.tsx +45 -0
- package/src/design_system/elements/tags/base-components/tag-close-x.tsx +34 -0
- package/src/design_system/elements/tags/tags.tsx +176 -0
- package/src/design_system/elements/textarea/textarea.aman.tsx +52 -0
- package/src/design_system/elements/textarea/textarea.tsx +111 -0
- package/src/design_system/elements/toggle/toggle.tsx +140 -0
- package/src/design_system/elements/tooltip/tooltip.tsx +109 -0
- package/src/design_system/hooks/use-breakpoint.ts +37 -0
- package/src/design_system/hooks/use-resize-observer.ts +68 -0
- package/src/design_system/logo/keystone-logo-minimal.tsx +93 -0
- package/src/design_system/logo/keystone-logo.tsx +22 -0
- package/src/design_system/sections/about-home.aman.tsx +85 -0
- package/src/design_system/sections/about-home.tsx +115 -0
- package/src/design_system/sections/blog-cards.tsx +848 -0
- package/src/design_system/sections/blog-gallery.aman.tsx +77 -0
- package/src/design_system/sections/blog-gallery.tsx +204 -0
- package/src/design_system/sections/blog-home.aman.tsx +84 -0
- package/src/design_system/sections/blog-home.tsx +153 -0
- package/src/design_system/sections/blog-post.aman.tsx +74 -0
- package/src/design_system/sections/blog-post.tsx +301 -0
- package/src/design_system/sections/blog-section.aman.tsx +101 -0
- package/src/design_system/sections/blog-section.tsx +179 -0
- package/src/design_system/sections/contact-home.tsx +25 -0
- package/src/design_system/sections/contact-section.aman.tsx +173 -0
- package/src/design_system/sections/contact-section.tsx +143 -0
- package/src/design_system/sections/faq-grid.aman.tsx +79 -0
- package/src/design_system/sections/faq-grid.tsx +102 -0
- package/src/design_system/sections/faq-home.aman.tsx +92 -0
- package/src/design_system/sections/faq-home.tsx +134 -0
- package/src/design_system/sections/feature-tab.tsx +43 -0
- package/src/design_system/sections/feature-text.tsx +284 -0
- package/src/design_system/sections/footer-home.aman.tsx +62 -0
- package/src/design_system/sections/footer-home.tsx +259 -0
- package/src/design_system/sections/generic-header-component.tsx +103 -0
- package/src/design_system/sections/header-navigation.aman.tsx +360 -0
- package/src/design_system/sections/header-navigation.tsx +334 -0
- package/src/design_system/sections/hero-faq.aman.tsx +38 -0
- package/src/design_system/sections/hero-faq.tsx +55 -0
- package/src/design_system/sections/hero-generic-text.aman.tsx +49 -0
- package/src/design_system/sections/hero-generic-text.tsx +51 -0
- package/src/design_system/sections/hero-home.aman.tsx +84 -0
- package/src/design_system/sections/hero-home.tsx +246 -0
- package/src/design_system/sections/hero-location-detail.aman.tsx +33 -0
- package/src/design_system/sections/hero-location-detail.tsx +72 -0
- package/src/design_system/sections/hero-service-detail.aman.tsx +53 -0
- package/src/design_system/sections/hero-service-detail.tsx +51 -0
- package/src/design_system/sections/hero-social-media.aman.tsx +42 -0
- package/src/design_system/sections/hero-social-media.tsx +35 -0
- package/src/design_system/sections/hero-testimonials.aman.tsx +38 -0
- package/src/design_system/sections/hero-testimonials.tsx +55 -0
- package/src/design_system/sections/home-hero-component.tsx +228 -0
- package/src/design_system/sections/index.tsx +131 -0
- package/src/design_system/sections/job-gallery.aman.tsx +91 -0
- package/src/design_system/sections/job-gallery.tsx +183 -0
- package/src/design_system/sections/location-details-section.aman.tsx +179 -0
- package/src/design_system/sections/location-details-section.tsx +196 -0
- package/src/design_system/sections/location-grid.aman.tsx +76 -0
- package/src/design_system/sections/location-grid.tsx +123 -0
- package/src/design_system/sections/services-grid.aman.tsx +85 -0
- package/src/design_system/sections/services-grid.tsx +104 -0
- package/src/design_system/sections/services-home.aman.tsx +78 -0
- package/src/design_system/sections/services-home.tsx +131 -0
- package/src/design_system/sections/social-media-grid.aman.tsx +132 -0
- package/src/design_system/sections/social-media-grid.tsx +189 -0
- package/src/design_system/sections/statistics-section.aman.tsx +79 -0
- package/src/design_system/sections/statistics-section.tsx +97 -0
- package/src/design_system/sections/team-grid.aman.tsx +85 -0
- package/src/design_system/sections/team-grid.tsx +88 -0
- package/src/design_system/sections/testimonials-home.aman.tsx +113 -0
- package/src/design_system/sections/testimonials-home.tsx +90 -0
- package/src/design_system/sections/values-section.aman.tsx +73 -0
- package/src/design_system/sections/values-section.tsx +128 -0
- package/src/design_system/utils/icon-mapping.tsx +28 -0
- package/src/index.ts +7 -0
- package/src/lib/component-registry.ts +53 -0
- package/src/lib/hooks/index.ts +8 -0
- package/src/lib/hooks/use-breakpoint.ts +37 -0
- package/src/lib/hooks/use-clipboard.ts +79 -0
- package/src/lib/hooks/use-resize-observer.ts +68 -0
- package/src/lib/server-api.ts +115 -0
- package/src/styles/style-overrides.aman.css +101 -0
- package/src/styles/theme.css +224 -0
- package/src/styles/typography.css +430 -0
- package/src/themes/index.ts +23 -0
- package/src/types/api/blog-post.ts +53 -0
- package/src/types/api/company-information.ts +44 -0
- package/src/types/api/contact.ts +63 -0
- package/src/types/api/faq.ts +37 -0
- package/src/types/api/job-posting.ts +34 -0
- package/src/types/api/location.ts +36 -0
- package/src/types/api/photos.ts +28 -0
- package/src/types/api/service.ts +37 -0
- package/src/types/api/social-post.ts +28 -0
- package/src/types/api/team-member.ts +29 -0
- package/src/types/api/testimonial.ts +29 -0
- package/src/types/api/website-photos.ts +22 -0
- package/src/types/config.ts +21 -0
- package/src/types/index.ts +21 -0
- package/src/utils/countries.tsx +1351 -0
- package/src/utils/cx.ts +25 -0
- package/src/utils/gradient-placeholder.ts +59 -0
- package/src/utils/is-react-component.ts +33 -0
- package/src/utils/markdown-toc.ts +54 -0
- package/src/utils/photo-helpers.ts +94 -0
|
@@ -0,0 +1,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
|
+
};
|