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,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PhotoWithFallback } from '../elements';
|
|
4
|
+
import type { BlogPost } from '../../types/api/blog-post';
|
|
5
|
+
|
|
6
|
+
interface BlogGalleryProps {
|
|
7
|
+
config: {
|
|
8
|
+
pages?: any[];
|
|
9
|
+
};
|
|
10
|
+
blogPosts?: BlogPost[] | null;
|
|
11
|
+
pageName?: string;
|
|
12
|
+
sectionKey?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const BlogGallery = ({
|
|
16
|
+
config,
|
|
17
|
+
blogPosts: postsData,
|
|
18
|
+
pageName = 'Blog',
|
|
19
|
+
sectionKey = 'blog_page_section_2_gallery',
|
|
20
|
+
}: BlogGalleryProps) => {
|
|
21
|
+
const posts = Array.isArray(postsData) ? postsData : [];
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<section>
|
|
25
|
+
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
26
|
+
{posts.length > 0 ? (
|
|
27
|
+
<div className="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-3">
|
|
28
|
+
{posts.map((post: any) => {
|
|
29
|
+
const excerpt = post.summary || post.content_markdown?.replace(/[#*\[\]()]/g, '').trim().substring(0, 150) || '';
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div key={post.id} className="flex flex-col">
|
|
33
|
+
<div className="w-full h-64 mb-6 overflow-hidden">
|
|
34
|
+
<PhotoWithFallback
|
|
35
|
+
item={post}
|
|
36
|
+
fallbackId={post.id}
|
|
37
|
+
className="w-full h-full object-cover"
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<p className="text-xs font-body font-normal uppercase tracking-widest mb-2" style={{ color: 'var(--color-text-brand-secondary)' }}>
|
|
42
|
+
{new Date(post.published_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<h3 className="font-display text-2xl font-normal text-fg-primary mb-4">
|
|
46
|
+
{post.title}
|
|
47
|
+
</h3>
|
|
48
|
+
|
|
49
|
+
{excerpt && (
|
|
50
|
+
<p className="font-body text-base leading-relaxed text-tertiary mb-6 flex-grow">
|
|
51
|
+
{excerpt}
|
|
52
|
+
</p>
|
|
53
|
+
)}
|
|
54
|
+
|
|
55
|
+
<a
|
|
56
|
+
href={`/blog/${post.slug}`}
|
|
57
|
+
className="font-body text-base underline underline-offset-4 hover:no-underline transition-colors"
|
|
58
|
+
style={{ color: 'var(--color-text-brand-accent)' }}
|
|
59
|
+
>
|
|
60
|
+
Read more
|
|
61
|
+
</a>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</div>
|
|
66
|
+
) : (
|
|
67
|
+
<div className="text-center py-12">
|
|
68
|
+
<p className="font-body text-base text-tertiary">No posts available</p>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</section>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
import { registerThemeVariant } from '../../lib/component-registry';
|
|
77
|
+
registerThemeVariant('blog-gallery', 'aman', BlogGallery);
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { ArrowUpRight } from '@untitledui/icons';
|
|
5
|
+
import { Avatar, Select, SelectItem, PaginationPageDefault, PhotoWithFallback } from '../elements';
|
|
6
|
+
import { BlogCardVertical } from './blog-cards';
|
|
7
|
+
import { useBreakpoint } from '../../lib/hooks/use-breakpoint';
|
|
8
|
+
import { cx } from '../../utils/cx';
|
|
9
|
+
import { getAvatarUrl } from '../../utils/photo-helpers';
|
|
10
|
+
import type { BlogPost } from '../../types/api/blog-post';
|
|
11
|
+
|
|
12
|
+
interface BlogGalleryProps {
|
|
13
|
+
config: {
|
|
14
|
+
pages?: any[];
|
|
15
|
+
};
|
|
16
|
+
blogPosts?: BlogPost[] | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const defaultSortByOptions = [
|
|
20
|
+
{ id: "recent", label: "Most recent" },
|
|
21
|
+
{ id: "popular", label: "Most popular" },
|
|
22
|
+
{ id: "viewed", label: "Most viewed" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const BlogGallery = ({
|
|
26
|
+
config,
|
|
27
|
+
blogPosts: postsData,
|
|
28
|
+
}: BlogGalleryProps) => {
|
|
29
|
+
const blogPage = config.pages?.find((p: any) => p.library_reference_name === 'Blog');
|
|
30
|
+
const blogPageSection = blogPage?.sections?.blog_page_section_2_blog;
|
|
31
|
+
const heroSection = blogPage?.sections?.blog_page_section_1_hero;
|
|
32
|
+
|
|
33
|
+
const label = undefined;
|
|
34
|
+
const title = 'Latest Articles';
|
|
35
|
+
const subtitle = 'Browse our collection of articles below.';
|
|
36
|
+
const postsPerPage = 12;
|
|
37
|
+
const sortByOptions = defaultSortByOptions;
|
|
38
|
+
const backgroundColor = "bg-primary";
|
|
39
|
+
const className = "";
|
|
40
|
+
|
|
41
|
+
const posts = Array.isArray(postsData) ? postsData : [];
|
|
42
|
+
const isDesktop = useBreakpoint("lg");
|
|
43
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
44
|
+
const [sortBy, setSortBy] = useState(sortByOptions[0].id);
|
|
45
|
+
|
|
46
|
+
// Sort posts based on sortBy
|
|
47
|
+
let sortedPosts = [...posts];
|
|
48
|
+
if (sortBy === 'recent') {
|
|
49
|
+
sortedPosts.sort((a, b) => {
|
|
50
|
+
const dateA = a.published_at ? new Date(a.published_at).getTime() : 0;
|
|
51
|
+
const dateB = b.published_at ? new Date(b.published_at).getTime() : 0;
|
|
52
|
+
return dateB - dateA;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// TODO: Add popular and viewed sorting when those fields are available
|
|
56
|
+
|
|
57
|
+
// Get featured post (first post with featured: true or first post)
|
|
58
|
+
const featuredPost = sortedPosts.find((post) => post.featured) || sortedPosts[0];
|
|
59
|
+
const nonFeaturedPosts = sortedPosts.filter((post) => post.id !== featuredPost?.id);
|
|
60
|
+
|
|
61
|
+
// Calculate pagination for non-featured posts
|
|
62
|
+
const totalPages = Math.ceil(nonFeaturedPosts.length / postsPerPage);
|
|
63
|
+
const startIndex = (currentPage - 1) * postsPerPage;
|
|
64
|
+
const endIndex = startIndex + postsPerPage;
|
|
65
|
+
const paginatedNonFeaturedPosts = nonFeaturedPosts.slice(startIndex, endIndex);
|
|
66
|
+
|
|
67
|
+
// Reset to page 1 when sort changes
|
|
68
|
+
React.useEffect(() => {
|
|
69
|
+
setCurrentPage(1);
|
|
70
|
+
}, [sortBy]);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className={`${backgroundColor} ${className}`}>
|
|
74
|
+
{/* Hero Section */}
|
|
75
|
+
<section className={`${backgroundColor} py-16 md:py-24`}>
|
|
76
|
+
<div className="mx-auto w-full max-w-container px-4 md:px-8">
|
|
77
|
+
<div className="flex w-full max-w-3xl flex-col">
|
|
78
|
+
{label && (
|
|
79
|
+
<span className="text-sm font-semibold text-brand-secondary md:text-md">{label}</span>
|
|
80
|
+
)}
|
|
81
|
+
<h2 className="mt-3 text-display-md font-semibold text-primary md:text-display-lg">
|
|
82
|
+
{title}
|
|
83
|
+
</h2>
|
|
84
|
+
<p className="mt-4 text-lg text-tertiary md:mt-6 md:text-xl">
|
|
85
|
+
{subtitle}
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
|
|
91
|
+
{/* Main Content */}
|
|
92
|
+
<main className="mx-auto flex w-full max-w-container flex-col gap-12 px-4 pb-16 md:gap-16 md:px-8 md:pb-24">
|
|
93
|
+
{posts.length > 0 ? (
|
|
94
|
+
<>
|
|
95
|
+
{/* Featured Post - Desktop */}
|
|
96
|
+
{featuredPost && (
|
|
97
|
+
<a
|
|
98
|
+
href={`/blog/${featuredPost.slug}`}
|
|
99
|
+
className="relative hidden w-full overflow-hidden rounded-2xl outline-focus-ring select-none focus:outline-2 focus:outline-offset-4 md:block md:h-145 lg:h-180"
|
|
100
|
+
>
|
|
101
|
+
<PhotoWithFallback
|
|
102
|
+
item={featuredPost}
|
|
103
|
+
fallbackId={featuredPost.id}
|
|
104
|
+
alt={featuredPost.title || 'Featured post'}
|
|
105
|
+
className="absolute inset-0 size-full object-cover"
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
{featuredPost && (() => {
|
|
109
|
+
const author = Array.isArray(featuredPost.blog_post_authors) && featuredPost.blog_post_authors.length > 0
|
|
110
|
+
? featuredPost.blog_post_authors[0]
|
|
111
|
+
: null;
|
|
112
|
+
const authorName = author?.name || 'Author';
|
|
113
|
+
const authorAvatarUrl = getAvatarUrl(author?.photo_attachments, author?.id, authorName);
|
|
114
|
+
const publishedAt = featuredPost.published_at
|
|
115
|
+
? new Date(featuredPost.published_at).toLocaleDateString('en-US', { day: 'numeric', month: 'short', year: 'numeric' })
|
|
116
|
+
: 'Recent';
|
|
117
|
+
const summary = featuredPost.excerpt_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="absolute inset-x-0 bottom-0 w-full bg-linear-to-t from-black/40 to-transparent pt-24">
|
|
121
|
+
<div className="flex w-full flex-col gap-6 p-8">
|
|
122
|
+
<div className="flex flex-col gap-2">
|
|
123
|
+
<div className="flex gap-4">
|
|
124
|
+
<p className="flex-1 text-display-xs font-semibold text-white">{featuredPost.title || 'Untitled Post'}</p>
|
|
125
|
+
<ArrowUpRight className="size-6 shrink-0 text-fg-white" />
|
|
126
|
+
</div>
|
|
127
|
+
<p className="line-clamp-2 text-md text-white">{summary}</p>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="flex gap-6">
|
|
130
|
+
<div className="flex flex-1 gap-8">
|
|
131
|
+
<div className="flex flex-col gap-2">
|
|
132
|
+
<p className="text-sm font-semibold text-white">Written by</p>
|
|
133
|
+
<div className="flex items-center gap-2">
|
|
134
|
+
<Avatar focusable size="md" src={authorAvatarUrl} alt={authorName} />
|
|
135
|
+
<p className="text-sm font-semibold text-white">{authorName}</p>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div className="flex flex-col gap-2">
|
|
139
|
+
<p className="text-sm font-semibold text-white">Published on</p>
|
|
140
|
+
<div className="flex h-10 items-center">
|
|
141
|
+
<p className="text-md font-semibold text-white">{publishedAt}</p>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
})()}
|
|
150
|
+
</a>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
{/* Featured Post - Mobile */}
|
|
154
|
+
{featuredPost && (
|
|
155
|
+
<div className="md:hidden">
|
|
156
|
+
<BlogCardVertical article={featuredPost} />
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{/* Sort Controls */}
|
|
161
|
+
<div className="flex flex-col items-end gap-8 md:flex-row">
|
|
162
|
+
<div className="relative w-full md:max-w-44">
|
|
163
|
+
<Select
|
|
164
|
+
aria-label="Sort by"
|
|
165
|
+
size="md"
|
|
166
|
+
selectedKey={sortBy}
|
|
167
|
+
onSelectionChange={(value: any) => setSortBy(value as string)}
|
|
168
|
+
items={sortByOptions}
|
|
169
|
+
>
|
|
170
|
+
{(item: any) => <SelectItem id={item.id}>{item.label}</SelectItem>}
|
|
171
|
+
</Select>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{/* Posts Grid */}
|
|
176
|
+
{paginatedNonFeaturedPosts.length > 0 && (
|
|
177
|
+
<ul className="grid grid-cols-1 gap-x-8 gap-y-12 md:grid-cols-2 md:gap-y-12 lg:grid-cols-3">
|
|
178
|
+
{paginatedNonFeaturedPosts.map((post) => (
|
|
179
|
+
<li key={post.id} className={cx(!isDesktop && "nth-[n+7]:hidden")}>
|
|
180
|
+
<BlogCardVertical article={post} />
|
|
181
|
+
</li>
|
|
182
|
+
))}
|
|
183
|
+
</ul>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{/* Pagination */}
|
|
187
|
+
{totalPages > 1 && (
|
|
188
|
+
<PaginationPageDefault
|
|
189
|
+
rounded
|
|
190
|
+
page={currentPage}
|
|
191
|
+
total={totalPages}
|
|
192
|
+
onPageChange={setCurrentPage}
|
|
193
|
+
/>
|
|
194
|
+
)}
|
|
195
|
+
</>
|
|
196
|
+
) : (
|
|
197
|
+
<div className="text-center py-12">
|
|
198
|
+
<p className="text-gray-500">No blog posts available</p>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
</main>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PhotoWithFallback } from '../elements';
|
|
4
|
+
import type { BlogPost } from '../../types/api/blog-post';
|
|
5
|
+
|
|
6
|
+
interface BlogHomeProps {
|
|
7
|
+
config: {
|
|
8
|
+
pages?: any[];
|
|
9
|
+
};
|
|
10
|
+
blogPosts?: BlogPost[] | null;
|
|
11
|
+
pageName?: string;
|
|
12
|
+
sectionKey?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const BlogHome = ({
|
|
16
|
+
config,
|
|
17
|
+
blogPosts: postsData,
|
|
18
|
+
pageName = 'Blog',
|
|
19
|
+
sectionKey = 'blog_page_section_1_posts',
|
|
20
|
+
}: BlogHomeProps) => {
|
|
21
|
+
const posts = Array.isArray(postsData) ? postsData : [];
|
|
22
|
+
const title = 'Latest Posts';
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<section>
|
|
26
|
+
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
27
|
+
<div className="mb-12 text-center">
|
|
28
|
+
<h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
|
|
29
|
+
{title}
|
|
30
|
+
</h2>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
{posts.length > 0 ? (
|
|
34
|
+
<div className="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-3">
|
|
35
|
+
{posts.map((post: any) => {
|
|
36
|
+
const excerpt = post.summary || post.content_markdown?.replace(/[#*\[\]()]/g, '').trim().substring(0, 150) || '';
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div key={post.id} className="flex flex-col">
|
|
40
|
+
<div className="w-full h-64 mb-6 overflow-hidden">
|
|
41
|
+
<PhotoWithFallback
|
|
42
|
+
item={post}
|
|
43
|
+
fallbackId={post.id}
|
|
44
|
+
className="w-full h-full object-cover"
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<p className="text-xs font-body font-normal uppercase tracking-widest mb-2" style={{ color: 'var(--color-text-brand-secondary)' }}>
|
|
49
|
+
{new Date(post.published_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<h3 className="font-display text-2xl font-normal text-fg-primary mb-4">
|
|
53
|
+
{post.title}
|
|
54
|
+
</h3>
|
|
55
|
+
|
|
56
|
+
{excerpt && (
|
|
57
|
+
<p className="font-body text-base leading-relaxed text-tertiary mb-6 flex-grow">
|
|
58
|
+
{excerpt}
|
|
59
|
+
</p>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
<a
|
|
63
|
+
href={`/blog/${post.slug}`}
|
|
64
|
+
className="font-body text-base underline underline-offset-4 hover:no-underline transition-colors"
|
|
65
|
+
style={{ color: 'var(--color-text-brand-accent)' }}
|
|
66
|
+
>
|
|
67
|
+
Read more
|
|
68
|
+
</a>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</div>
|
|
73
|
+
) : (
|
|
74
|
+
<div className="text-center py-12">
|
|
75
|
+
<p className="font-body text-base text-tertiary">No posts available</p>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
</section>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
import { registerThemeVariant } from '../../lib/component-registry';
|
|
84
|
+
registerThemeVariant('blog-home', 'aman', BlogHome);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowUpRight, ArrowLeft, ArrowRight } from "@untitledui/icons";
|
|
4
|
+
import { Button, PhotoWithFallback, RoundButton } from '../elements';
|
|
5
|
+
import { Carousel } from '../elements/carousel/carousel-base';
|
|
6
|
+
import { cx } from '../../utils/cx';
|
|
7
|
+
import type { BlogPost } from '../../types/api/blog-post';
|
|
8
|
+
|
|
9
|
+
interface BlogHomeProps {
|
|
10
|
+
config: {
|
|
11
|
+
pages?: any[];
|
|
12
|
+
};
|
|
13
|
+
blogPosts?: BlogPost[] | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const BlogHome = ({
|
|
17
|
+
config,
|
|
18
|
+
blogPosts: postsData,
|
|
19
|
+
}: BlogHomeProps) => {
|
|
20
|
+
const label = "Latest posts";
|
|
21
|
+
const title = "Latest posts";
|
|
22
|
+
const subtitle = "";
|
|
23
|
+
const maxPosts = 6;
|
|
24
|
+
const viewAllHref = "/blog";
|
|
25
|
+
const viewAllText = "View all posts";
|
|
26
|
+
|
|
27
|
+
const posts = Array.isArray(postsData) ? postsData.slice(0, maxPosts) : [];
|
|
28
|
+
|
|
29
|
+
const formatDate = (dateString?: string) => {
|
|
30
|
+
if (!dateString) return 'Recent';
|
|
31
|
+
try {
|
|
32
|
+
return new Date(dateString).toLocaleDateString('en-US', {
|
|
33
|
+
day: 'numeric',
|
|
34
|
+
month: 'short',
|
|
35
|
+
year: 'numeric'
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
return 'Recent';
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<section className="overflow-hidden bg-primary py-16 md:py-24">
|
|
44
|
+
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
45
|
+
<div className="flex flex-col items-start justify-between lg:flex-row">
|
|
46
|
+
<div className="max-w-3xl">
|
|
47
|
+
<p className="text-sm font-semibold text-brand-secondary md:text-md">{label}</p>
|
|
48
|
+
<h2 className="mt-3 text-display-sm font-semibold text-primary md:text-display-md">
|
|
49
|
+
{title}
|
|
50
|
+
</h2>
|
|
51
|
+
<p className="mt-4 text-lg text-tertiary md:mt-5 md:text-xl">
|
|
52
|
+
{subtitle}
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{viewAllHref && (
|
|
57
|
+
<div className="hidden gap-3 lg:flex">
|
|
58
|
+
<Button size="xl" href={viewAllHref}>{viewAllText}</Button>
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{posts && posts.length > 0 ? (
|
|
64
|
+
<>
|
|
65
|
+
<Carousel.Root
|
|
66
|
+
className="mt-12 md:mt-16"
|
|
67
|
+
opts={{
|
|
68
|
+
align: "start",
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<Carousel.Content overflowHidden={false} className="gap-6 pr-4 md:gap-8 md:pr-8">
|
|
72
|
+
{posts.map((post, index) => {
|
|
73
|
+
const author = post.blog_post_authors?.[0];
|
|
74
|
+
const excerpt = post.excerpt_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Carousel.Item key={post.id || index} className="max-w-xs md:max-w-96">
|
|
78
|
+
<article className={cx("flex flex-col gap-4")}>
|
|
79
|
+
<div className="relative">
|
|
80
|
+
<a href={`/blog/${post.slug || post.id}`} className="w-full" tabIndex={-1}>
|
|
81
|
+
<PhotoWithFallback
|
|
82
|
+
item={post}
|
|
83
|
+
fallbackId={post.id || index}
|
|
84
|
+
alt={post.title || 'Blog post'}
|
|
85
|
+
className={cx("aspect-[1.5] w-full object-cover")}
|
|
86
|
+
/>
|
|
87
|
+
</a>
|
|
88
|
+
<div className="absolute inset-x-0 bottom-0 overflow-hidden bg-linear-to-b from-transparent to-black/40">
|
|
89
|
+
<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">
|
|
90
|
+
<div>
|
|
91
|
+
<a
|
|
92
|
+
href={author?.slug ? `/blog/author/${author.slug}` : '/blog'}
|
|
93
|
+
className="block rounded-xs text-sm font-semibold text-white outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
94
|
+
>
|
|
95
|
+
{author?.name || 'Author'}
|
|
96
|
+
</a>
|
|
97
|
+
<time className="block text-sm text-white">
|
|
98
|
+
{formatDate(post.published_at)}
|
|
99
|
+
</time>
|
|
100
|
+
</div>
|
|
101
|
+
<a
|
|
102
|
+
href="/blog"
|
|
103
|
+
className="rounded-xs text-sm font-semibold text-white outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
104
|
+
>
|
|
105
|
+
Blog
|
|
106
|
+
</a>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="flex flex-col gap-2">
|
|
111
|
+
<a
|
|
112
|
+
href={`/blog/${post.slug || post.id}`}
|
|
113
|
+
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"
|
|
114
|
+
>
|
|
115
|
+
{post.title || 'Untitled Post'}
|
|
116
|
+
<ArrowUpRight
|
|
117
|
+
className="mt-0.5 size-6 shrink-0 text-fg-quaternary transition duration-100 ease-linear group-hover/title:text-fg-quaternary_hover"
|
|
118
|
+
aria-hidden="true"
|
|
119
|
+
/>
|
|
120
|
+
</a>
|
|
121
|
+
{excerpt && (
|
|
122
|
+
<p className="line-clamp-2 text-md text-tertiary">{excerpt}</p>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</article>
|
|
126
|
+
</Carousel.Item>
|
|
127
|
+
);
|
|
128
|
+
})}
|
|
129
|
+
</Carousel.Content>
|
|
130
|
+
<div className="mt-8 flex gap-4 md:gap-8">
|
|
131
|
+
<Carousel.PrevTrigger asChild>
|
|
132
|
+
<RoundButton icon={ArrowLeft} />
|
|
133
|
+
</Carousel.PrevTrigger>
|
|
134
|
+
<Carousel.NextTrigger asChild>
|
|
135
|
+
<RoundButton icon={ArrowRight} />
|
|
136
|
+
</Carousel.NextTrigger>
|
|
137
|
+
</div>
|
|
138
|
+
</Carousel.Root>
|
|
139
|
+
{viewAllHref && (
|
|
140
|
+
<div className="mt-12 flex flex-col gap-3 lg:hidden">
|
|
141
|
+
<Button size="xl" href={viewAllHref}>{viewAllText}</Button>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</>
|
|
145
|
+
) : (
|
|
146
|
+
<div className="mt-12 text-center md:mt-16">
|
|
147
|
+
<p className="text-gray-500">No blog posts available</p>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
</section>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PhotoWithFallback, Button, Breadcrumb } from '../elements';
|
|
4
|
+
import MarkdownRenderer from '../elements/markdown-renderer/MarkdownRenderer';
|
|
5
|
+
import type { BlogPost } from '../../types/api/blog-post';
|
|
6
|
+
|
|
7
|
+
interface BlogPostSectionProps {
|
|
8
|
+
config: {
|
|
9
|
+
pages?: any[];
|
|
10
|
+
};
|
|
11
|
+
blogPost?: BlogPost | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const BlogPostSection = ({
|
|
15
|
+
config,
|
|
16
|
+
blogPost,
|
|
17
|
+
}: BlogPostSectionProps) => {
|
|
18
|
+
if (!blogPost) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="bg-primary text-center py-12">
|
|
21
|
+
<div className="text-6xl mb-4">📝</div>
|
|
22
|
+
<h3 className="font-display text-xl font-normal text-fg-primary mb-2">Blog Post Not Found</h3>
|
|
23
|
+
<p className="font-body text-tertiary mb-4">The blog post you're looking for doesn't exist.</p>
|
|
24
|
+
<Button href="/blog">View All Blog Posts</Button>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<section className="py-2 md:py-2">
|
|
32
|
+
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
33
|
+
<Breadcrumb
|
|
34
|
+
backHref="/blog"
|
|
35
|
+
backLabel="Blog"
|
|
36
|
+
currentLabel={blogPost.title}
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
40
|
+
|
|
41
|
+
<section>
|
|
42
|
+
<div className="mx-auto max-w-3xl px-4 md:px-8">
|
|
43
|
+
<div className="mb-8">
|
|
44
|
+
{blogPost.published_at && (
|
|
45
|
+
<p className="text-xs font-body font-normal uppercase tracking-widest text-secondary mb-4">
|
|
46
|
+
{new Date(blogPost.published_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
|
|
47
|
+
</p>
|
|
48
|
+
)}
|
|
49
|
+
|
|
50
|
+
<h1 className="font-display text-5xl font-normal leading-tight text-fg-primary md:text-6xl">
|
|
51
|
+
{blogPost.title}
|
|
52
|
+
</h1>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="aspect-video w-full mb-12">
|
|
56
|
+
<PhotoWithFallback
|
|
57
|
+
item={blogPost}
|
|
58
|
+
fallbackId={blogPost.id}
|
|
59
|
+
alt={blogPost.title}
|
|
60
|
+
className="w-full h-full object-cover"
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="prose prose-lg max-w-none font-display text-tertiary">
|
|
65
|
+
<MarkdownRenderer content={blogPost.content_markdown || ''} />
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
import { registerThemeVariant } from '../../lib/component-registry';
|
|
74
|
+
registerThemeVariant('blog-post', 'aman', BlogPostSection);
|