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,301 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { Link01, Copy01 } from '@untitledui/icons';
|
|
5
|
+
import { Badge, BadgeGroup, Button, Form, Input, Avatar, Breadcrumb, MarkdownRenderer, PhotoWithFallback } from '../elements';
|
|
6
|
+
import { extractTableOfContents } from '../../utils/markdown-toc';
|
|
7
|
+
import type { BlogPost } from '../../types/api/blog-post';
|
|
8
|
+
|
|
9
|
+
interface BlogPostSectionProps {
|
|
10
|
+
// Config data - component extracts all values from it
|
|
11
|
+
config: {
|
|
12
|
+
pages?: any[];
|
|
13
|
+
};
|
|
14
|
+
// Blog post data from SSR
|
|
15
|
+
blogPost?: BlogPost | null;
|
|
16
|
+
// Related posts
|
|
17
|
+
relatedPosts?: BlogPost[] | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const BlogPostSection = ({
|
|
21
|
+
config,
|
|
22
|
+
blogPost,
|
|
23
|
+
relatedPosts: relatedPostsData,
|
|
24
|
+
}: BlogPostSectionProps) => {
|
|
25
|
+
const relatedPosts = Array.isArray(relatedPostsData) ? relatedPostsData.slice(0, 3) : [];
|
|
26
|
+
|
|
27
|
+
const formatDate = (dateString?: string) => {
|
|
28
|
+
if (!dateString) return 'Recent';
|
|
29
|
+
try {
|
|
30
|
+
return new Date(dateString).toLocaleDateString('en-US', {
|
|
31
|
+
day: 'numeric',
|
|
32
|
+
month: 'short',
|
|
33
|
+
year: 'numeric'
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
return 'Recent';
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Extract table of contents from markdown
|
|
41
|
+
const tableOfContents = blogPost?.content_markdown
|
|
42
|
+
? extractTableOfContents(blogPost.content_markdown)
|
|
43
|
+
: [];
|
|
44
|
+
|
|
45
|
+
const [copied, setCopied] = useState(false);
|
|
46
|
+
|
|
47
|
+
const handleCopyLink = async () => {
|
|
48
|
+
try {
|
|
49
|
+
await navigator.clipboard.writeText(window.location.href);
|
|
50
|
+
setCopied(true);
|
|
51
|
+
setTimeout(() => setCopied(false), 2000);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error('Failed to copy:', err);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleShareLink = () => {
|
|
58
|
+
if (navigator.share) {
|
|
59
|
+
navigator.share({
|
|
60
|
+
title: blogPost?.title,
|
|
61
|
+
url: window.location.href,
|
|
62
|
+
}).catch((err) => {
|
|
63
|
+
console.error('Error sharing:', err);
|
|
64
|
+
});
|
|
65
|
+
} else {
|
|
66
|
+
// Fallback to copy if share is not available
|
|
67
|
+
handleCopyLink();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (!blogPost) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="text-center py-12">
|
|
74
|
+
<div className="text-gray-600 text-6xl mb-4">📝</div>
|
|
75
|
+
<h3 className="text-xl font-semibold text-gray-900 mb-2">Blog Post Not Found</h3>
|
|
76
|
+
<p className="text-gray-600 mb-4">The blog post you're looking for doesn't exist.</p>
|
|
77
|
+
<Button href="/blog">View All Blog Posts</Button>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
{/* Breadcrumb - top left aligned */}
|
|
85
|
+
<section className="bg-primary py-2 md:py-2">
|
|
86
|
+
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
87
|
+
<Breadcrumb
|
|
88
|
+
backHref="/blog"
|
|
89
|
+
backLabel="Blog"
|
|
90
|
+
currentLabel={blogPost.title}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</section>
|
|
94
|
+
|
|
95
|
+
{/* Blog Post Header with Image */}
|
|
96
|
+
<section className="bg-primary py-4 md:py-6">
|
|
97
|
+
<div className="relative mx-auto max-w-container gap-16 px-4 pb-16 md:gap-8 md:px-8 md:pt-16 md:pb-24 grid grid-cols-1 items-center md:grid-cols-2">
|
|
98
|
+
<div className="flex max-w-180 flex-col items-start">
|
|
99
|
+
<BadgeGroup
|
|
100
|
+
size="md"
|
|
101
|
+
addonText={(blogPost as any).category?.name || "Blog"}
|
|
102
|
+
color="brand"
|
|
103
|
+
theme="modern"
|
|
104
|
+
className="pr-3 mt-4"
|
|
105
|
+
iconTrailing={null}
|
|
106
|
+
>
|
|
107
|
+
{(blogPost as any).reading_time_minutes
|
|
108
|
+
? `${(blogPost as any).reading_time_minutes} min read`
|
|
109
|
+
: (blogPost.published_at ? formatDate(blogPost.published_at) : 'Recent')}
|
|
110
|
+
</BadgeGroup>
|
|
111
|
+
|
|
112
|
+
<h1 className="mt-4 text-display-md font-semibold text-primary md:text-display-lg">
|
|
113
|
+
{blogPost.title}
|
|
114
|
+
</h1>
|
|
115
|
+
{(blogPost as any).excerpt && (
|
|
116
|
+
<p className="mt-4 text-lg text-tertiary md:mt-6 md:max-w-120 md:text-xl">
|
|
117
|
+
{((blogPost as any).excerpt || (blogPost as any).excerpt_markdown?.replace(/[#*\[\]()]/g, '').trim() || '').replace(/[#*\[\]()]/g, '').trim()}
|
|
118
|
+
</p>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{/* Hero Image - Always show with fallback support */}
|
|
123
|
+
<PhotoWithFallback
|
|
124
|
+
item={blogPost}
|
|
125
|
+
fallbackId={blogPost.id}
|
|
126
|
+
alt={blogPost.title}
|
|
127
|
+
className="order-first -ml-4 h-36 w-screen max-w-none object-cover md:order-1 md:ml-0 md:h-96 md:w-full md:max-w-full rounded-2xl"
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Content Section with Sidebar */}
|
|
132
|
+
{blogPost.content_markdown && (
|
|
133
|
+
<div className="mx-auto max-w-container px-4 pb-16 md:px-8 md:pb-24">
|
|
134
|
+
<div className="mx-auto flex justify-center gap-16">
|
|
135
|
+
{/* Left sidebar - hidden on mobile */}
|
|
136
|
+
<div className="hidden w-70 flex-col gap-8 lg:flex">
|
|
137
|
+
{tableOfContents.length > 0 && (
|
|
138
|
+
<>
|
|
139
|
+
<div className="w-full border-t border-secondary" />
|
|
140
|
+
<div className="flex flex-col gap-4">
|
|
141
|
+
<p className="text-md font-semibold text-brand-secondary">Table of contents</p>
|
|
142
|
+
<ul className="flex flex-col gap-3">
|
|
143
|
+
{tableOfContents.map((item: any) => (
|
|
144
|
+
<li key={item.id}>
|
|
145
|
+
<Button
|
|
146
|
+
size="lg"
|
|
147
|
+
color="link-gray"
|
|
148
|
+
href={`#${item.id}`}
|
|
149
|
+
className={`${item.level === 3 ? 'pl-4' : ''} w-full text-left whitespace-normal justify-start`}
|
|
150
|
+
>
|
|
151
|
+
<span className="break-words block w-full">{item.title}</span>
|
|
152
|
+
</Button>
|
|
153
|
+
</li>
|
|
154
|
+
))}
|
|
155
|
+
</ul>
|
|
156
|
+
</div>
|
|
157
|
+
<div className="w-full border-t border-secondary" />
|
|
158
|
+
</>
|
|
159
|
+
)}
|
|
160
|
+
{(blogPost as any).blog_post_authors && Array.isArray((blogPost as any).blog_post_authors) && (blogPost as any).blog_post_authors.length > 0 && (
|
|
161
|
+
<div className="flex flex-col gap-6">
|
|
162
|
+
<p className="text-md font-semibold text-brand-secondary">Author</p>
|
|
163
|
+
<div className="flex items-center gap-3">
|
|
164
|
+
{(() => {
|
|
165
|
+
const author = (blogPost as any).blog_post_authors[0];
|
|
166
|
+
return (
|
|
167
|
+
<>
|
|
168
|
+
<Avatar
|
|
169
|
+
src={author?.photo_attachments?.find((pa: any) => pa.featured)?.photo?.thumbnail_url ||
|
|
170
|
+
author?.photo_attachments?.[0]?.photo?.thumbnail_url ||
|
|
171
|
+
`https://ui-avatars.com/api/?name=${encodeURIComponent(author?.name || 'Author')}&background=random`}
|
|
172
|
+
alt={author?.name || 'Author'}
|
|
173
|
+
size="md"
|
|
174
|
+
/>
|
|
175
|
+
<div>
|
|
176
|
+
<p className="text-md font-semibold text-primary">{author?.name || 'Author'}</p>
|
|
177
|
+
{author?.bio_markdown && (
|
|
178
|
+
<p className="text-md text-tertiary">{author.bio_markdown.replace(/[#*\[\]()]/g, '').trim().substring(0, 100)}</p>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
</>
|
|
182
|
+
);
|
|
183
|
+
})()}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
<div className="w-full border-t border-secondary" />
|
|
188
|
+
<Form
|
|
189
|
+
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
const formData = new FormData(e.currentTarget);
|
|
192
|
+
console.log("Form data:", formData);
|
|
193
|
+
}}
|
|
194
|
+
className="flex flex-col gap-4"
|
|
195
|
+
>
|
|
196
|
+
<label htmlFor="blog-email-input" className="text-md font-semibold text-brand-secondary">
|
|
197
|
+
Subscribe to our newsletter
|
|
198
|
+
</label>
|
|
199
|
+
<Input isRequired id="blog-email-input" name="email" type="email" placeholder="Enter your email" size="md" />
|
|
200
|
+
<Button type="submit" size="xl">
|
|
201
|
+
Subscribe
|
|
202
|
+
</Button>
|
|
203
|
+
</Form>
|
|
204
|
+
<div className="w-full border-t border-secondary" />
|
|
205
|
+
<div className="flex gap-3">
|
|
206
|
+
<button
|
|
207
|
+
onClick={handleShareLink}
|
|
208
|
+
title="Share link"
|
|
209
|
+
className="inline-flex h-max cursor-pointer items-center justify-center gap-1.5 rounded-lg px-3.5 py-2.5 text-sm font-semibold outline-brand transition duration-100 ease-linear focus-visible:outline-2 focus-visible:outline-offset-2 bg-secondary text-primary hover:bg-secondary_hover"
|
|
210
|
+
>
|
|
211
|
+
<Link01 className="size-5 shrink-0" />
|
|
212
|
+
</button>
|
|
213
|
+
<button
|
|
214
|
+
onClick={handleCopyLink}
|
|
215
|
+
title={copied ? "Copied!" : "Copy link"}
|
|
216
|
+
className="inline-flex h-max cursor-pointer items-center justify-center gap-1.5 rounded-lg px-3.5 py-2.5 text-sm font-semibold outline-brand transition duration-100 ease-linear focus-visible:outline-2 focus-visible:outline-offset-2 bg-secondary text-fg-quaternary hover:bg-secondary_hover"
|
|
217
|
+
>
|
|
218
|
+
<Copy01 className="size-5 shrink-0" />
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{/* Main content */}
|
|
224
|
+
<div className="max-w-prose lg:max-w-180">
|
|
225
|
+
<div className="prose-centered-quote mx-auto prose md:prose-lg" id="content">
|
|
226
|
+
<MarkdownRenderer content={blogPost.content_markdown || ''} />
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Mobile-only contributors and social */}
|
|
230
|
+
<div className="flex flex-col items-start justify-between gap-y-8 lg:hidden lg:flex-row mt-8">
|
|
231
|
+
{(blogPost as any).blog_post_authors && Array.isArray((blogPost as any).blog_post_authors) && (blogPost as any).blog_post_authors.length > 0 && (
|
|
232
|
+
<div className="flex flex-col gap-6">
|
|
233
|
+
<p className="text-md font-semibold text-brand-secondary">Author</p>
|
|
234
|
+
<div className="flex items-center gap-3">
|
|
235
|
+
{(() => {
|
|
236
|
+
const author = (blogPost as any).blog_post_authors[0];
|
|
237
|
+
return (
|
|
238
|
+
<>
|
|
239
|
+
<Avatar
|
|
240
|
+
src={author?.photo_attachments?.find((pa: any) => pa.featured)?.photo?.thumbnail_url ||
|
|
241
|
+
author?.photo_attachments?.[0]?.photo?.thumbnail_url ||
|
|
242
|
+
`https://ui-avatars.com/api/?name=${encodeURIComponent(author?.name || 'Author')}&background=random`}
|
|
243
|
+
alt={author?.name || 'Author'}
|
|
244
|
+
size="md"
|
|
245
|
+
/>
|
|
246
|
+
<div>
|
|
247
|
+
<p className="text-md font-semibold text-primary">{author?.name || 'Author'}</p>
|
|
248
|
+
{author?.bio_markdown && (
|
|
249
|
+
<p className="text-md text-tertiary">{author.bio_markdown.replace(/[#*\[\]()]/g, '').trim().substring(0, 100)}</p>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
</>
|
|
253
|
+
);
|
|
254
|
+
})()}
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
<div className="flex gap-3">
|
|
259
|
+
<button
|
|
260
|
+
onClick={handleShareLink}
|
|
261
|
+
title="Share link"
|
|
262
|
+
className="inline-flex h-max cursor-pointer items-center justify-center gap-1.5 rounded-lg px-3.5 py-2.5 text-sm font-semibold outline-brand transition duration-100 ease-linear focus-visible:outline-2 focus-visible:outline-offset-2 bg-secondary text-primary hover:bg-secondary_hover"
|
|
263
|
+
>
|
|
264
|
+
<Link01 className="size-5 shrink-0" />
|
|
265
|
+
</button>
|
|
266
|
+
<button
|
|
267
|
+
onClick={handleCopyLink}
|
|
268
|
+
title={copied ? "Copied!" : "Copy link"}
|
|
269
|
+
className="inline-flex h-max cursor-pointer items-center justify-center gap-1.5 rounded-lg px-3.5 py-2.5 text-sm font-semibold outline-brand transition duration-100 ease-linear focus-visible:outline-2 focus-visible:outline-offset-2 bg-secondary text-fg-quaternary hover:bg-secondary_hover"
|
|
270
|
+
>
|
|
271
|
+
<Copy01 className="size-5 shrink-0" />
|
|
272
|
+
</button>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
</section>
|
|
280
|
+
|
|
281
|
+
{/* Navigation */}
|
|
282
|
+
<section className="bg-primary pb-16 md:pb-24">
|
|
283
|
+
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
284
|
+
<div className="mx-auto max-w-prose md:max-w-180">
|
|
285
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-end border-t border-secondary pt-8">
|
|
286
|
+
{(blogPost as any).tags && Array.isArray((blogPost as any).tags) && (blogPost as any).tags.length > 0 && (
|
|
287
|
+
<div className="flex flex-wrap gap-2">
|
|
288
|
+
{(blogPost as any).tags.map((tag: any, index: number) => (
|
|
289
|
+
<Badge key={index} color="gray" size="md" type="modern">
|
|
290
|
+
{tag.name || tag}
|
|
291
|
+
</Badge>
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</section>
|
|
299
|
+
</>
|
|
300
|
+
);
|
|
301
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { PhotoWithFallback } from '../elements';
|
|
5
|
+
import type { BlogPost } from '../../types/api/blog-post';
|
|
6
|
+
|
|
7
|
+
interface BlogSectionProps {
|
|
8
|
+
config: {
|
|
9
|
+
pages?: any[];
|
|
10
|
+
};
|
|
11
|
+
blogPosts?: BlogPost[] | null;
|
|
12
|
+
pageName?: string;
|
|
13
|
+
sectionKey?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const BlogSection = ({
|
|
17
|
+
config,
|
|
18
|
+
blogPosts: postsData,
|
|
19
|
+
pageName = 'Home',
|
|
20
|
+
sectionKey = 'home_page_section_5_blog',
|
|
21
|
+
}: BlogSectionProps) => {
|
|
22
|
+
const blogSection = pageName === 'Home'
|
|
23
|
+
const title = "Latest posts";
|
|
24
|
+
const maxPosts = 3;
|
|
25
|
+
|
|
26
|
+
const postsArray = Array.isArray(postsData) ? postsData.slice(0, maxPosts) : [];
|
|
27
|
+
|
|
28
|
+
const formatDate = (dateString?: string) => {
|
|
29
|
+
if (!dateString) return 'Recent';
|
|
30
|
+
try {
|
|
31
|
+
return new Date(dateString).toLocaleDateString('en-US', {
|
|
32
|
+
day: 'numeric',
|
|
33
|
+
month: 'long',
|
|
34
|
+
year: 'numeric'
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
return 'Recent';
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<section>
|
|
43
|
+
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
44
|
+
<div className="mb-12">
|
|
45
|
+
<h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
|
|
46
|
+
{title}
|
|
47
|
+
</h2>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{postsArray.length > 0 ? (
|
|
51
|
+
<div className="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-3">
|
|
52
|
+
{postsArray.map((post: BlogPost) => {
|
|
53
|
+
const excerpt = (post as any).summary || post.content_markdown?.replace(/[#*\[\]()]/g, '').trim().substring(0, 150) || '';
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div key={post.id} className="flex flex-col">
|
|
57
|
+
<div className="w-full h-64 mb-6 overflow-hidden">
|
|
58
|
+
<PhotoWithFallback
|
|
59
|
+
item={post}
|
|
60
|
+
fallbackId={post.id}
|
|
61
|
+
className="w-full h-full object-cover"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<p className="text-xs font-body font-normal uppercase tracking-widest mb-2" style={{ color: 'var(--color-text-brand-secondary)' }}>
|
|
66
|
+
{formatDate(post.published_at)}
|
|
67
|
+
</p>
|
|
68
|
+
|
|
69
|
+
<h3 className="font-display text-2xl font-normal text-fg-primary mb-4">
|
|
70
|
+
{post.title}
|
|
71
|
+
</h3>
|
|
72
|
+
|
|
73
|
+
{excerpt && (
|
|
74
|
+
<p className="font-body text-base leading-relaxed text-tertiary mb-6 flex-grow">
|
|
75
|
+
{excerpt}
|
|
76
|
+
</p>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
<a
|
|
80
|
+
href={`/blog/${post.slug}`}
|
|
81
|
+
className="font-body text-base underline underline-offset-4 hover:no-underline transition-colors"
|
|
82
|
+
style={{ color: 'var(--color-text-brand-accent)' }}
|
|
83
|
+
>
|
|
84
|
+
Read more
|
|
85
|
+
</a>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
})}
|
|
89
|
+
</div>
|
|
90
|
+
) : (
|
|
91
|
+
<div className="text-center py-12">
|
|
92
|
+
<p className="font-body text-base text-tertiary">No posts available</p>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
import { registerThemeVariant } from '../../lib/component-registry';
|
|
101
|
+
registerThemeVariant('blog-section', 'aman', BlogSection);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { ArrowLeft, ArrowRight, ArrowUpRight } from "@untitledui/icons";
|
|
5
|
+
import { Carousel } from '../elements/carousel/carousel-base';
|
|
6
|
+
import { Button, PhotoWithFallback, RoundButton } from '../elements';
|
|
7
|
+
import { cx } from '../../utils/cx';
|
|
8
|
+
import { getAvatarUrl } from '../../utils/photo-helpers';
|
|
9
|
+
import type { BlogPost } from '../../types/api/blog-post';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
interface BlogSectionProps {
|
|
13
|
+
config: {
|
|
14
|
+
pages?: any[];
|
|
15
|
+
};
|
|
16
|
+
blogPosts: BlogPost[];
|
|
17
|
+
pageName?: string;
|
|
18
|
+
sectionKey?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const BlogSection = ({
|
|
22
|
+
config,
|
|
23
|
+
blogPosts: postsData,
|
|
24
|
+
pageName = 'Home',
|
|
25
|
+
sectionKey = 'home_page_section_5_blog',
|
|
26
|
+
}: BlogSectionProps) => {
|
|
27
|
+
// Extract values from config
|
|
28
|
+
const blogSection = pageName === 'Home'
|
|
29
|
+
const title = "Latest posts";
|
|
30
|
+
const subtitle = "Interviews, tips, guides, industry best practices, and news.";
|
|
31
|
+
const maxPosts = 8;
|
|
32
|
+
const backgroundColor = "bg-primary";
|
|
33
|
+
const showViewAll = undefined !== false;
|
|
34
|
+
|
|
35
|
+
let postsArray = Array.isArray(postsData) ? postsData : [];
|
|
36
|
+
|
|
37
|
+
// If this is a BlogPost page and use_related is true, filter to related posts
|
|
38
|
+
if (pageName === 'BlogPost' && undefined) {
|
|
39
|
+
const currentPath = typeof window !== 'undefined' ? window.location.pathname : '';
|
|
40
|
+
const currentSlug = currentPath.split('/').pop();
|
|
41
|
+
|
|
42
|
+
postsArray = postsArray
|
|
43
|
+
.filter((post: BlogPost) => post.slug !== currentSlug)
|
|
44
|
+
.slice(0, maxPosts);
|
|
45
|
+
} else {
|
|
46
|
+
postsArray = postsArray.slice(0, maxPosts);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const formatDate = (dateString: string) => {
|
|
50
|
+
if (!dateString) return 'Recent';
|
|
51
|
+
try {
|
|
52
|
+
return new Date(dateString).toLocaleDateString('en-US', {
|
|
53
|
+
day: 'numeric',
|
|
54
|
+
month: 'short',
|
|
55
|
+
year: 'numeric'
|
|
56
|
+
});
|
|
57
|
+
} catch {
|
|
58
|
+
return 'Recent';
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<section className={`overflow-hidden ${backgroundColor} py-16 md:py-24`}>
|
|
64
|
+
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
65
|
+
<div className="flex flex-col items-start justify-between lg:flex-row">
|
|
66
|
+
<div className="max-w-3xl">
|
|
67
|
+
<p className="text-sm font-semibold text-brand-secondary md:text-md">Latest posts</p>
|
|
68
|
+
<h2 className="mt-3 text-display-sm font-semibold text-primary md:text-display-md">
|
|
69
|
+
{title}
|
|
70
|
+
</h2>
|
|
71
|
+
<p className="mt-4 text-lg text-tertiary md:mt-5 md:text-xl">
|
|
72
|
+
{subtitle}
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{showViewAll && (
|
|
77
|
+
<div className="hidden gap-3 lg:flex">
|
|
78
|
+
<Button size="xl" href="/blog">View all posts</Button>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{postsArray.length > 0 ? (
|
|
84
|
+
<>
|
|
85
|
+
<Carousel.Root
|
|
86
|
+
className="mt-12 md:mt-16"
|
|
87
|
+
opts={{
|
|
88
|
+
align: "start",
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<Carousel.Content overflowHidden={false} className="gap-6 pr-4 md:gap-8 md:pr-8">
|
|
92
|
+
{postsArray.map((post: BlogPost, index: number) => {
|
|
93
|
+
const author = Array.isArray(post.blog_post_authors) && post.blog_post_authors.length > 0
|
|
94
|
+
? post.blog_post_authors[0]
|
|
95
|
+
: null;
|
|
96
|
+
|
|
97
|
+
const authorName = author?.name || 'Author';
|
|
98
|
+
const authorAvatarUrl = getAvatarUrl(author?.photo_attachments, author?.id, authorName);
|
|
99
|
+
|
|
100
|
+
const excerpt = post.excerpt_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Carousel.Item key={post.id || index} className="max-w-xs md:max-w-96">
|
|
104
|
+
<article className={cx("flex flex-col gap-4")}>
|
|
105
|
+
<div className="relative">
|
|
106
|
+
<a href={`/blog/${post.slug || post.id}`} className="w-full" tabIndex={-1}>
|
|
107
|
+
<PhotoWithFallback
|
|
108
|
+
item={post}
|
|
109
|
+
fallbackId={post.id || index}
|
|
110
|
+
alt={post.title || 'Blog post'}
|
|
111
|
+
className={cx("aspect-[1.5] w-full object-cover")}
|
|
112
|
+
/>
|
|
113
|
+
</a>
|
|
114
|
+
<div className="absolute inset-x-0 bottom-0 overflow-hidden bg-linear-to-b from-transparent to-black/40">
|
|
115
|
+
<div className="relative flex items-start justify-between bg-alpha-white/30 p-4 backdrop-blur-md before:absolute before:inset-x-0 before:top-0 before:h-px before:bg-alpha-white/30 md:p-5">
|
|
116
|
+
<div>
|
|
117
|
+
<a
|
|
118
|
+
href={author?.slug ? `/blog/author/${author.slug}` : '/blog'}
|
|
119
|
+
className="block rounded-xs text-sm font-semibold text-white outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
120
|
+
>
|
|
121
|
+
{author?.name || 'Author'}
|
|
122
|
+
</a>
|
|
123
|
+
<time className="block text-sm text-white">
|
|
124
|
+
{formatDate(post.published_at || '')}
|
|
125
|
+
</time>
|
|
126
|
+
</div>
|
|
127
|
+
<a
|
|
128
|
+
href="/blog"
|
|
129
|
+
className="rounded-xs text-sm font-semibold text-white outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
130
|
+
>
|
|
131
|
+
Blog
|
|
132
|
+
</a>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div className="flex flex-col gap-2">
|
|
137
|
+
<a
|
|
138
|
+
href={`/blog/${post.slug || post.id}`}
|
|
139
|
+
className="group/title flex justify-between gap-x-4 rounded-md text-lg font-semibold text-primary outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
140
|
+
>
|
|
141
|
+
{post.title || 'Untitled Post'}
|
|
142
|
+
<ArrowUpRight
|
|
143
|
+
className="mt-0.5 size-6 shrink-0 text-fg-quaternary transition duration-100 ease-linear group-hover/title:text-fg-quaternary_hover"
|
|
144
|
+
aria-hidden="true"
|
|
145
|
+
/>
|
|
146
|
+
</a>
|
|
147
|
+
{excerpt && (
|
|
148
|
+
<p className="line-clamp-2 text-md text-tertiary">{excerpt}</p>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
</article>
|
|
152
|
+
</Carousel.Item>
|
|
153
|
+
);
|
|
154
|
+
})}
|
|
155
|
+
</Carousel.Content>
|
|
156
|
+
<div className="mt-8 flex gap-4 md:gap-8">
|
|
157
|
+
<Carousel.PrevTrigger asChild>
|
|
158
|
+
<RoundButton icon={ArrowLeft} />
|
|
159
|
+
</Carousel.PrevTrigger>
|
|
160
|
+
<Carousel.NextTrigger asChild>
|
|
161
|
+
<RoundButton icon={ArrowRight} />
|
|
162
|
+
</Carousel.NextTrigger>
|
|
163
|
+
</div>
|
|
164
|
+
</Carousel.Root>
|
|
165
|
+
{showViewAll && (
|
|
166
|
+
<div className="mt-12 flex flex-col gap-3 lg:hidden">
|
|
167
|
+
<Button size="xl" href="/blog">View all posts</Button>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</>
|
|
171
|
+
) : (
|
|
172
|
+
<div className="mt-12 text-center md:mt-16">
|
|
173
|
+
<p className="text-gray-500">No blog posts available</p>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
</section>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import ContactSection from "./contact-section";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
interface ContactHomeProps {
|
|
7
|
+
// Config data - component extracts all values from it
|
|
8
|
+
config: {
|
|
9
|
+
pages?: any[];
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ContactHome = ({
|
|
14
|
+
config,
|
|
15
|
+
}: ContactHomeProps) => {
|
|
16
|
+
// Extract values from config
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<ContactSection
|
|
20
|
+
config={config}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
|