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,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PhotoWithFallback } from '../elements';
|
|
4
|
+
import type { SocialPost } from '../../types/api/social-post';
|
|
5
|
+
|
|
6
|
+
interface SocialMediaGridProps {
|
|
7
|
+
config: {
|
|
8
|
+
pages?: any[];
|
|
9
|
+
};
|
|
10
|
+
socialPosts?: SocialPost[] | null;
|
|
11
|
+
pageName?: string;
|
|
12
|
+
sectionKey?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const formatDate = (dateString?: string) => {
|
|
16
|
+
if (!dateString) return 'Recent';
|
|
17
|
+
try {
|
|
18
|
+
return new Date(dateString).toLocaleDateString('en-US', {
|
|
19
|
+
year: 'numeric',
|
|
20
|
+
month: 'short',
|
|
21
|
+
day: 'numeric'
|
|
22
|
+
});
|
|
23
|
+
} catch {
|
|
24
|
+
return 'Recent';
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const getPlatformColor = (platform?: string) => {
|
|
29
|
+
const platformLower = platform?.toLowerCase() || '';
|
|
30
|
+
const colors: Record<string, string> = {
|
|
31
|
+
facebook: '#1877F2',
|
|
32
|
+
instagram: '#E4405F',
|
|
33
|
+
twitter: '#1DA1F2',
|
|
34
|
+
linkedin: '#0A66C2',
|
|
35
|
+
youtube: '#FF0000',
|
|
36
|
+
};
|
|
37
|
+
return colors[platformLower] || '#6B625E';
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const SocialMediaGrid = ({
|
|
41
|
+
config,
|
|
42
|
+
socialPosts: postsData,
|
|
43
|
+
pageName = 'SocialMedia',
|
|
44
|
+
sectionKey = 'social_media_page_section_2_posts',
|
|
45
|
+
}: SocialMediaGridProps) => {
|
|
46
|
+
const posts = Array.isArray(postsData) ? postsData : [];
|
|
47
|
+
const title = 'Latest Posts';
|
|
48
|
+
const subtitle = '';
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<section>
|
|
52
|
+
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
53
|
+
<div className="mb-12 text-center">
|
|
54
|
+
<h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
|
|
55
|
+
{title}
|
|
56
|
+
</h2>
|
|
57
|
+
{subtitle && (
|
|
58
|
+
<p className="mt-4 font-display text-lg leading-relaxed text-tertiary md:text-xl max-w-3xl mx-auto">
|
|
59
|
+
{subtitle}
|
|
60
|
+
</p>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{posts.length > 0 ? (
|
|
65
|
+
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
66
|
+
{posts.map((post: any) => {
|
|
67
|
+
const images = post.media_urls?.images || [];
|
|
68
|
+
const firstImage = images[0] || post.image_url;
|
|
69
|
+
const content = post.content_markdown?.replace(/[#*\[\]()]/g, '').trim() || post.caption || '';
|
|
70
|
+
const platform = post.social_profile?.platform || 'social';
|
|
71
|
+
const platformColor = getPlatformColor(platform);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div key={post.id} className="flex flex-col bg-white overflow-hidden">
|
|
75
|
+
{firstImage && (
|
|
76
|
+
<div className="w-full h-64 overflow-hidden">
|
|
77
|
+
<PhotoWithFallback
|
|
78
|
+
photoUrl={firstImage}
|
|
79
|
+
photoAlt={content.substring(0, 50) || 'Social post'}
|
|
80
|
+
fallbackId={post.id}
|
|
81
|
+
className="w-full h-full object-cover"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
<div className="p-6 flex flex-col flex-grow">
|
|
87
|
+
<div className="flex items-center justify-between mb-4">
|
|
88
|
+
<span
|
|
89
|
+
className="text-xs font-body uppercase tracking-wide px-2 py-1 rounded text-white"
|
|
90
|
+
style={{ backgroundColor: platformColor }}
|
|
91
|
+
>
|
|
92
|
+
{platform}
|
|
93
|
+
</span>
|
|
94
|
+
<span className="text-xs font-body" style={{ color: 'var(--color-text-brand-secondary)' }}>
|
|
95
|
+
{formatDate(post.posted_at)}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{content && (
|
|
100
|
+
<p className="font-body text-sm text-tertiary mb-4 flex-grow line-clamp-4">
|
|
101
|
+
{content}
|
|
102
|
+
</p>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{post.external_url && (
|
|
106
|
+
<a
|
|
107
|
+
href={post.external_url}
|
|
108
|
+
target="_blank"
|
|
109
|
+
rel="noopener noreferrer"
|
|
110
|
+
className="font-body text-sm underline underline-offset-4 hover:no-underline mt-auto transition-colors"
|
|
111
|
+
style={{ color: 'var(--color-text-brand-accent)' }}
|
|
112
|
+
>
|
|
113
|
+
View on {platform.charAt(0).toUpperCase() + platform.slice(1)}
|
|
114
|
+
</a>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
})}
|
|
120
|
+
</div>
|
|
121
|
+
) : (
|
|
122
|
+
<div className="text-center py-12">
|
|
123
|
+
<p className="font-body text-base text-tertiary">No posts available</p>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</section>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
import { registerThemeVariant } from '../../lib/component-registry';
|
|
132
|
+
registerThemeVariant('social-media-grid', 'aman', SocialMediaGrid);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { ArrowLeft, ArrowRight } from '@untitledui/icons';
|
|
5
|
+
import { Carousel } from '../elements/carousel/carousel-base';
|
|
6
|
+
import { Button, PaginationPageMinimalCenter, PhotoWithFallback, RoundButton } from '../elements';
|
|
7
|
+
import { cx } from '../../utils/cx';
|
|
8
|
+
import type { SocialPost } from '../../types/api/social-post';
|
|
9
|
+
|
|
10
|
+
interface SocialMediaGridProps {
|
|
11
|
+
config: {
|
|
12
|
+
pages?: any[];
|
|
13
|
+
};
|
|
14
|
+
socialPosts?: SocialPost[] | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const getPlatformBadge = (platform?: string) => {
|
|
18
|
+
const platformLower = platform?.toLowerCase() || 'social';
|
|
19
|
+
const colors: Record<string, string> = {
|
|
20
|
+
facebook: 'bg-blue-600',
|
|
21
|
+
instagram: 'bg-pink-600',
|
|
22
|
+
twitter: 'bg-blue-400',
|
|
23
|
+
linkedin: 'bg-blue-700',
|
|
24
|
+
youtube: 'bg-red-600',
|
|
25
|
+
};
|
|
26
|
+
return colors[platformLower] || 'bg-gray-600';
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const formatDate = (dateString?: string) => {
|
|
30
|
+
if (!dateString) return 'Recent';
|
|
31
|
+
try {
|
|
32
|
+
return new Date(dateString).toLocaleDateString('en-US', {
|
|
33
|
+
year: 'numeric',
|
|
34
|
+
month: 'short',
|
|
35
|
+
day: 'numeric'
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
return 'Recent';
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
export const SocialMediaGrid = ({
|
|
44
|
+
config,
|
|
45
|
+
socialPosts: postsData,
|
|
46
|
+
}: SocialMediaGridProps) => {
|
|
47
|
+
const posts = Array.isArray(postsData) ? postsData : [];
|
|
48
|
+
|
|
49
|
+
const title = undefined;
|
|
50
|
+
const subtitle = undefined;
|
|
51
|
+
const postsPerPage = 8;
|
|
52
|
+
const backgroundColor = "bg-primary";
|
|
53
|
+
const className = "";
|
|
54
|
+
|
|
55
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
56
|
+
|
|
57
|
+
const totalPages = Math.ceil(posts.length / postsPerPage);
|
|
58
|
+
const startIndex = (currentPage - 1) * postsPerPage;
|
|
59
|
+
const endIndex = startIndex + postsPerPage;
|
|
60
|
+
const paginatedPosts = posts.slice(startIndex, endIndex);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<section className={`${backgroundColor} py-16 md:py-24 ${className}`}>
|
|
64
|
+
<div className="mx-auto w-full max-w-container px-4 md:px-8">
|
|
65
|
+
{(title || subtitle) && (
|
|
66
|
+
<div className="mx-auto mb-12 flex w-full max-w-3xl flex-col items-center text-center">
|
|
67
|
+
{title && (
|
|
68
|
+
<h2 className="text-display-sm font-semibold text-primary md:text-display-md">
|
|
69
|
+
{title}
|
|
70
|
+
</h2>
|
|
71
|
+
)}
|
|
72
|
+
{subtitle && (
|
|
73
|
+
<p className="mt-4 text-lg text-tertiary md:mt-5 md:text-xl">
|
|
74
|
+
{subtitle}
|
|
75
|
+
</p>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{paginatedPosts.length > 0 ? (
|
|
81
|
+
<>
|
|
82
|
+
<ul className="grid grid-cols-1 gap-x-8 gap-y-10 sm:grid-cols-2 md:gap-y-12 lg:grid-cols-3 xl:grid-cols-4">
|
|
83
|
+
{paginatedPosts.map((post, index) => {
|
|
84
|
+
const images = post.media_urls?.images || [];
|
|
85
|
+
const hasMultipleImages = images.length > 1;
|
|
86
|
+
const firstImage = images[0];
|
|
87
|
+
const content = post.content_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<li key={post.id || index} className="flex flex-col gap-5 md:gap-6">
|
|
91
|
+
<div className="relative h-78 w-full overflow-hidden md:h-74">
|
|
92
|
+
{firstImage ? (
|
|
93
|
+
hasMultipleImages ? (
|
|
94
|
+
<Carousel.Root
|
|
95
|
+
opts={{ align: "start" }}
|
|
96
|
+
className="h-full w-full"
|
|
97
|
+
>
|
|
98
|
+
<Carousel.Content className="h-full">
|
|
99
|
+
{images.map((imgUrl: string, imgIndex: number) => (
|
|
100
|
+
<Carousel.Item key={imgIndex} className="h-full">
|
|
101
|
+
<img
|
|
102
|
+
src={imgUrl}
|
|
103
|
+
alt={`${post.platform} post image ${imgIndex + 1}`}
|
|
104
|
+
className="size-full object-cover"
|
|
105
|
+
/>
|
|
106
|
+
</Carousel.Item>
|
|
107
|
+
))}
|
|
108
|
+
</Carousel.Content>
|
|
109
|
+
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 gap-2">
|
|
110
|
+
<Carousel.PrevTrigger asChild>
|
|
111
|
+
<RoundButton icon={ArrowLeft} />
|
|
112
|
+
</Carousel.PrevTrigger>
|
|
113
|
+
<Carousel.NextTrigger asChild>
|
|
114
|
+
<RoundButton icon={ArrowRight} />
|
|
115
|
+
</Carousel.NextTrigger>
|
|
116
|
+
</div>
|
|
117
|
+
</Carousel.Root>
|
|
118
|
+
) : (
|
|
119
|
+
<PhotoWithFallback
|
|
120
|
+
photoUrl={firstImage}
|
|
121
|
+
photoAlt={`${post.platform} post`}
|
|
122
|
+
fallbackId={`social-post-${post.id || index}`}
|
|
123
|
+
className="size-full object-cover"
|
|
124
|
+
/>
|
|
125
|
+
)
|
|
126
|
+
) : (
|
|
127
|
+
<PhotoWithFallback
|
|
128
|
+
photoUrl={undefined}
|
|
129
|
+
photoAlt={`${post.platform} post`}
|
|
130
|
+
fallbackId={`social-post-${post.id || index}`}
|
|
131
|
+
className="size-full object-cover"
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{post.platform && (
|
|
136
|
+
<div className={cx(
|
|
137
|
+
"absolute top-4 right-4 rounded-full px-3 py-1.5 text-sm font-semibold text-white",
|
|
138
|
+
getPlatformBadge(post.platform)
|
|
139
|
+
)}>
|
|
140
|
+
{post.platform.charAt(0).toUpperCase() + post.platform.slice(1)}
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className="flex flex-col gap-4">
|
|
146
|
+
<div className="flex items-center justify-between">
|
|
147
|
+
<span className="text-sm text-tertiary">{formatDate(post.posted_at)}</span>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{content && (
|
|
151
|
+
<p className="text-md text-tertiary line-clamp-4">
|
|
152
|
+
{content}
|
|
153
|
+
</p>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
<Button
|
|
157
|
+
href={`https://${post.platform.toLowerCase()}.com`}
|
|
158
|
+
target="_blank"
|
|
159
|
+
rel="noopener noreferrer"
|
|
160
|
+
color="link-color"
|
|
161
|
+
size="md"
|
|
162
|
+
>
|
|
163
|
+
View on {post.platform}
|
|
164
|
+
</Button>
|
|
165
|
+
</div>
|
|
166
|
+
</li>
|
|
167
|
+
);
|
|
168
|
+
})}
|
|
169
|
+
</ul>
|
|
170
|
+
|
|
171
|
+
{totalPages > 1 && (
|
|
172
|
+
<div className="mt-12">
|
|
173
|
+
<PaginationPageMinimalCenter
|
|
174
|
+
page={currentPage}
|
|
175
|
+
total={totalPages}
|
|
176
|
+
onPageChange={setCurrentPage}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</>
|
|
181
|
+
) : (
|
|
182
|
+
<div className="text-center py-12">
|
|
183
|
+
<p className="text-gray-500">No social media posts available</p>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
</section>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
interface StatisticsSectionProps {
|
|
4
|
+
config?: {
|
|
5
|
+
pages?: any[];
|
|
6
|
+
};
|
|
7
|
+
pageName?: string;
|
|
8
|
+
sectionKey?: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
subtitle?: string;
|
|
11
|
+
statistics?: Array<{
|
|
12
|
+
number?: string;
|
|
13
|
+
value?: string;
|
|
14
|
+
label: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const StatisticsSection = ({
|
|
19
|
+
config,
|
|
20
|
+
pageName = 'Testimonials',
|
|
21
|
+
sectionKey = 'testimonials_page_section_3_stats',
|
|
22
|
+
title: titleProp,
|
|
23
|
+
subtitle: subtitleProp,
|
|
24
|
+
statistics: statisticsProp,
|
|
25
|
+
}: StatisticsSectionProps) => {
|
|
26
|
+
// Get section config
|
|
27
|
+
const pageConfig = config?.pages?.find((p: any) =>
|
|
28
|
+
p.library_reference_name === pageName
|
|
29
|
+
);
|
|
30
|
+
const sectionConfig = pageConfig?.sections?.[sectionKey];
|
|
31
|
+
|
|
32
|
+
// Use props, then config, then defaults
|
|
33
|
+
const title = titleProp || sectionConfig?.title || '*TR* By the Numbers';
|
|
34
|
+
const subtitle = subtitleProp || sectionConfig?.subtitle || '*TR* Our track record speaks for itself';
|
|
35
|
+
const statistics = statisticsProp || sectionConfig?.statistics || [
|
|
36
|
+
{ number: '*TR* 500+', label: '*TR* Happy Clients' },
|
|
37
|
+
{ number: '*TR* 5.0', label: '*TR* Average Rating' },
|
|
38
|
+
{ number: '*TR* 98%', label: '*TR* Would Recommend' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<section>
|
|
43
|
+
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
44
|
+
{(title || subtitle) && (
|
|
45
|
+
<div className="mb-12 text-center">
|
|
46
|
+
{title && (
|
|
47
|
+
<h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
|
|
48
|
+
{title}
|
|
49
|
+
</h2>
|
|
50
|
+
)}
|
|
51
|
+
{subtitle && (
|
|
52
|
+
<p className="mt-4 font-body text-lg leading-relaxed text-tertiary md:text-xl max-w-3xl mx-auto">
|
|
53
|
+
{subtitle}
|
|
54
|
+
</p>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{statistics.length > 0 && (
|
|
60
|
+
<div className="grid grid-cols-1 gap-8 md:grid-cols-3 justify-items-center">
|
|
61
|
+
{statistics.map((stat: any, i: number) => (
|
|
62
|
+
<div key={i} className="text-center max-w-xs w-full">
|
|
63
|
+
<div className="font-display text-5xl font-normal text-fg-primary mb-2">
|
|
64
|
+
{stat.number || stat.value}
|
|
65
|
+
</div>
|
|
66
|
+
<p className="text-xs font-body uppercase tracking-widest" style={{ color: 'var(--color-text-brand-secondary)' }}>
|
|
67
|
+
{stat.label}
|
|
68
|
+
</p>
|
|
69
|
+
</div>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
</section>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
import { registerThemeVariant } from '../../lib/component-registry';
|
|
79
|
+
registerThemeVariant('statistics-section', 'aman', StatisticsSection);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { FC } from "react";
|
|
4
|
+
import { mapIcon } from '../utils/icon-mapping';
|
|
5
|
+
|
|
6
|
+
interface StatisticsSectionProps {
|
|
7
|
+
config?: {
|
|
8
|
+
pages?: any[];
|
|
9
|
+
};
|
|
10
|
+
pageName?: string;
|
|
11
|
+
sectionKey?: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
statistics?: Array<{
|
|
15
|
+
number?: string;
|
|
16
|
+
value?: string;
|
|
17
|
+
label: string;
|
|
18
|
+
icon?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const StatisticsSection = ({
|
|
24
|
+
config,
|
|
25
|
+
pageName = 'Testimonials',
|
|
26
|
+
sectionKey = 'testimonials_page_section_3_stats',
|
|
27
|
+
title: titleProp,
|
|
28
|
+
subtitle: subtitleProp,
|
|
29
|
+
statistics: statisticsProp,
|
|
30
|
+
}: StatisticsSectionProps) => {
|
|
31
|
+
// Get section config
|
|
32
|
+
const pageConfig = config?.pages?.find((p: any) =>
|
|
33
|
+
p.library_reference_name === pageName
|
|
34
|
+
);
|
|
35
|
+
const sectionConfig = pageConfig?.sections?.[sectionKey];
|
|
36
|
+
|
|
37
|
+
// Use props, then config, then defaults
|
|
38
|
+
const title = titleProp || sectionConfig?.title || '*TR* By the Numbers';
|
|
39
|
+
const subtitle = subtitleProp || sectionConfig?.subtitle || '*TR* Our track record speaks for itself';
|
|
40
|
+
const statsArray = statisticsProp || sectionConfig?.statistics || [
|
|
41
|
+
{ number: '*TR* 500+', label: '*TR* Happy Clients', icon: 'Users01' },
|
|
42
|
+
{ number: '*TR* 5.0', label: '*TR* Average Rating', icon: 'Star01' },
|
|
43
|
+
{ number: '*TR* 98%', label: '*TR* Would Recommend', icon: 'Heart' },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const stats = statsArray.map((stat: any) => ({
|
|
47
|
+
number: stat.number || stat.value || stat.title,
|
|
48
|
+
label: stat.label || stat.subtitle,
|
|
49
|
+
icon: stat.icon,
|
|
50
|
+
description: stat.description,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
const backgroundColor = "bg-secondary";
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<section className={`${backgroundColor} py-16 md:py-24`}>
|
|
57
|
+
<div className="mx-auto w-full max-w-container px-4 md:px-8">
|
|
58
|
+
{(title || subtitle) && (
|
|
59
|
+
<div className="mx-auto mb-12 flex w-full max-w-3xl flex-col items-center text-center">
|
|
60
|
+
{title && (
|
|
61
|
+
<h2 className="text-display-sm font-semibold text-primary md:text-display-md">
|
|
62
|
+
{title}
|
|
63
|
+
</h2>
|
|
64
|
+
)}
|
|
65
|
+
{subtitle && (
|
|
66
|
+
<p className="mt-4 text-lg text-tertiary md:mt-5 md:text-xl">
|
|
67
|
+
{subtitle}
|
|
68
|
+
</p>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
|
|
74
|
+
{stats.map((stat: { number: string | number; label: string; icon?: string | FC<{ className?: string }> }, index: number) => {
|
|
75
|
+
const IconComponent = mapIcon(stat.icon);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div key={index} className="text-center">
|
|
79
|
+
<div className="flex items-center justify-center gap-2">
|
|
80
|
+
{IconComponent && (
|
|
81
|
+
<div className="size-6 shrink-0">
|
|
82
|
+
<IconComponent className="w-full h-full text-brand-secondary" />
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
<div className="text-display-lg font-semibold text-primary md:text-display-xl">
|
|
86
|
+
{stat.number}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<div className="mt-2 text-lg text-tertiary">{stat.label}</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</section>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PhotoWithFallback } from '../elements';
|
|
4
|
+
import type { TeamMember } from '../../types/api/team-member';
|
|
5
|
+
|
|
6
|
+
interface TeamGridProps {
|
|
7
|
+
config: {
|
|
8
|
+
pages?: any[];
|
|
9
|
+
};
|
|
10
|
+
teamMembers?: TeamMember[] | null;
|
|
11
|
+
pageName?: string;
|
|
12
|
+
sectionKey?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const TeamGrid = ({
|
|
16
|
+
config,
|
|
17
|
+
teamMembers: membersData,
|
|
18
|
+
pageName = 'Team',
|
|
19
|
+
sectionKey = 'team_page_section_2_team',
|
|
20
|
+
}: TeamGridProps) => {
|
|
21
|
+
const members = Array.isArray(membersData) ? membersData : [];
|
|
22
|
+
|
|
23
|
+
const title = 'Our Team';
|
|
24
|
+
const subtitle = '';
|
|
25
|
+
const maxMembers = 8;
|
|
26
|
+
|
|
27
|
+
const displayMembers = members.slice(0, maxMembers);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<section>
|
|
31
|
+
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
32
|
+
{title && (
|
|
33
|
+
<div className="mb-12 text-center">
|
|
34
|
+
<h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
|
|
35
|
+
{title}
|
|
36
|
+
</h2>
|
|
37
|
+
{subtitle && (
|
|
38
|
+
<p className="mt-4 font-body text-lg text-tertiary">
|
|
39
|
+
{subtitle}
|
|
40
|
+
</p>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
{displayMembers.length > 0 ? (
|
|
46
|
+
<div className="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-3">
|
|
47
|
+
{displayMembers.map((member: any) => {
|
|
48
|
+
const bio = member.bio_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div key={member.id}>
|
|
52
|
+
<div className="aspect-[3/4] w-full mb-6">
|
|
53
|
+
<PhotoWithFallback
|
|
54
|
+
item={member}
|
|
55
|
+
fallbackId={member.id}
|
|
56
|
+
className="w-full h-full object-cover"
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
<h3 className="font-display text-xl font-normal text-fg-primary">
|
|
60
|
+
{member.name}
|
|
61
|
+
</h3>
|
|
62
|
+
<p className="mt-2 font-body text-sm" style={{ color: 'var(--color-text-brand-secondary)' }}>
|
|
63
|
+
{member.position}
|
|
64
|
+
</p>
|
|
65
|
+
{bio && (
|
|
66
|
+
<p className="mt-4 font-body text-sm leading-relaxed text-tertiary">
|
|
67
|
+
{bio}
|
|
68
|
+
</p>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
</div>
|
|
74
|
+
) : (
|
|
75
|
+
<div className="text-center py-12">
|
|
76
|
+
<p className="font-body text-base text-tertiary">No team members available</p>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
import { registerThemeVariant } from '../../lib/component-registry';
|
|
85
|
+
registerThemeVariant('team-grid', 'aman', TeamGrid);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { PhotoWithFallback } from '../elements';
|
|
5
|
+
import type { TeamMember } from '../../types/api/team-member';
|
|
6
|
+
|
|
7
|
+
interface TeamGridProps {
|
|
8
|
+
config: {
|
|
9
|
+
pages?: any[];
|
|
10
|
+
};
|
|
11
|
+
teamMembers?: TeamMember[] | null;
|
|
12
|
+
pageName?: string;
|
|
13
|
+
sectionKey?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const TeamGrid = ({
|
|
17
|
+
config,
|
|
18
|
+
teamMembers: membersData,
|
|
19
|
+
pageName = 'Team',
|
|
20
|
+
sectionKey = 'team_page_section_2_team',
|
|
21
|
+
}: TeamGridProps) => {
|
|
22
|
+
const members = Array.isArray(membersData) ? membersData : [];
|
|
23
|
+
|
|
24
|
+
const title = "Leadership";
|
|
25
|
+
const subtitle = "Our philosophy is simple—hire a team of diverse, passionate people and foster a culture of service.";
|
|
26
|
+
const maxMembers = 8;
|
|
27
|
+
const backgroundColor = "bg-primary";
|
|
28
|
+
const className = "";
|
|
29
|
+
|
|
30
|
+
const displayMembers = members.slice(0, maxMembers);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<section className={`${backgroundColor} py-16 md:py-24 ${className}`}>
|
|
34
|
+
<div className="mx-auto w-full max-w-container px-4 md:px-8">
|
|
35
|
+
{(title || subtitle) && (
|
|
36
|
+
<div className="mx-auto mb-12 flex w-full max-w-3xl flex-col items-center text-center">
|
|
37
|
+
{title && (
|
|
38
|
+
<h2 className="text-display-sm font-semibold text-primary md:text-display-md">
|
|
39
|
+
{title}
|
|
40
|
+
</h2>
|
|
41
|
+
)}
|
|
42
|
+
{subtitle && (
|
|
43
|
+
<p className="mt-4 text-lg text-tertiary md:mt-5 md:text-xl">
|
|
44
|
+
{subtitle}
|
|
45
|
+
</p>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
|
|
50
|
+
{displayMembers.length > 0 ? (
|
|
51
|
+
<div className="mt-12 md:mt-16">
|
|
52
|
+
<ul className="grid w-full grid-cols-1 gap-x-8 gap-y-10 sm:grid-cols-2 md:gap-y-12 lg:grid-cols-3 xl:grid-cols-4">
|
|
53
|
+
{displayMembers.map((member, index) => {
|
|
54
|
+
const bio = member.bio_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<li key={member.id || index} className="flex flex-col gap-5 md:gap-6">
|
|
58
|
+
<PhotoWithFallback
|
|
59
|
+
item={member}
|
|
60
|
+
fallbackId={member.id || index}
|
|
61
|
+
alt={member.name || 'Team member'}
|
|
62
|
+
className="h-78 w-full object-cover md:h-74 rounded-2xl"
|
|
63
|
+
/>
|
|
64
|
+
<div>
|
|
65
|
+
<h3 className="text-lg font-semibold text-primary md:text-xl">{member.name}</h3>
|
|
66
|
+
<p className="text-md text-brand-secondary md:mt-0.5 md:text-lg">
|
|
67
|
+
{member.position}
|
|
68
|
+
</p>
|
|
69
|
+
{bio && (
|
|
70
|
+
<p className="mt-4 text-md text-tertiary line-clamp-3">
|
|
71
|
+
{bio}
|
|
72
|
+
</p>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
</li>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</ul>
|
|
79
|
+
</div>
|
|
80
|
+
) : (
|
|
81
|
+
<div className="text-center mt-12">
|
|
82
|
+
<p className="text-gray-500">No team members available</p>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
</section>
|
|
87
|
+
);
|
|
88
|
+
};
|