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,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
|
+
};
|