keystone-design-bootstrap 1.0.4 → 1.0.6
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/package.json +1 -1
- package/src/design_system/elements/buttons/button.aman.tsx +1 -1
- package/src/design_system/elements/carousel/carousel-section-wrapper.tsx +59 -0
- package/src/design_system/elements/index.tsx +1 -0
- package/src/design_system/sections/blog-gallery.aman.tsx +92 -35
- package/src/design_system/sections/blog-gallery.tsx +4 -30
- package/src/design_system/sections/blog-section.aman.tsx +22 -36
- package/src/design_system/sections/contact-section.aman.tsx +7 -6
- package/src/design_system/sections/header-navigation.aman.tsx +56 -17
- package/src/design_system/sections/header-navigation.tsx +18 -1
- package/src/design_system/sections/location-details-section.aman.tsx +0 -1
- package/src/design_system/sections/testimonials-home.aman.tsx +23 -31
- package/src/styles/style-overrides.aman.css +14 -2
- package/src/types/api/company-information.ts +1 -0
package/package.json
CHANGED
|
@@ -34,7 +34,7 @@ export const styles = sortCx({
|
|
|
34
34
|
|
|
35
35
|
colors: {
|
|
36
36
|
primary: {
|
|
37
|
-
root: "bg-
|
|
37
|
+
root: "bg-brand-solid text-white hover:bg-brand-solid_hover transition-colors",
|
|
38
38
|
},
|
|
39
39
|
secondary: {
|
|
40
40
|
root: "bg-white text-fg-primary border-2 hover:opacity-90 transition-all",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { ReactNode } from 'react';
|
|
4
|
+
import { Carousel } from '../carousel/carousel';
|
|
5
|
+
import { ChevronLeft, ChevronRight } from '@untitledui/icons';
|
|
6
|
+
|
|
7
|
+
interface CarouselSectionWrapperProps {
|
|
8
|
+
title?: string;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
hasItems: boolean;
|
|
11
|
+
emptyMessage?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const CarouselSectionWrapper = ({
|
|
15
|
+
title = "",
|
|
16
|
+
children,
|
|
17
|
+
hasItems,
|
|
18
|
+
emptyMessage = "No items available"
|
|
19
|
+
}: CarouselSectionWrapperProps) => {
|
|
20
|
+
if (!hasItems) {
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
{title && (
|
|
24
|
+
<div className="mb-12">
|
|
25
|
+
<h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
|
|
26
|
+
{title}
|
|
27
|
+
</h2>
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
<div className="text-center py-12">
|
|
31
|
+
<p className="font-body text-base text-tertiary">{emptyMessage}</p>
|
|
32
|
+
</div>
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Carousel.Root opts={{ align: "start", loop: true }}>
|
|
39
|
+
<div className="flex items-center justify-between mb-12">
|
|
40
|
+
<h2 className="font-display text-4xl font-normal text-fg-primary md:text-5xl">
|
|
41
|
+
{title}
|
|
42
|
+
</h2>
|
|
43
|
+
<div className="flex gap-2">
|
|
44
|
+
<Carousel.PrevTrigger className="rounded-full p-2 border border-secondary hover:bg-primary_hover transition-colors disabled:opacity-30 disabled:cursor-not-allowed">
|
|
45
|
+
<ChevronLeft className="w-6 h-6 text-fg-primary" />
|
|
46
|
+
</Carousel.PrevTrigger>
|
|
47
|
+
<Carousel.NextTrigger className="rounded-full p-2 border border-secondary hover:bg-primary_hover transition-colors disabled:opacity-30 disabled:cursor-not-allowed">
|
|
48
|
+
<ChevronRight className="w-6 h-6 text-fg-primary" />
|
|
49
|
+
</Carousel.NextTrigger>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<Carousel.Content className="-ml-4">
|
|
54
|
+
{children}
|
|
55
|
+
</Carousel.Content>
|
|
56
|
+
</Carousel.Root>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
@@ -153,6 +153,7 @@ export { SlideoutMenu } from './slideout-menus/slideout-menu';
|
|
|
153
153
|
export * from './rating/rating-stars';
|
|
154
154
|
export * from './rating/rating-badge';
|
|
155
155
|
export * from './carousel/carousel';
|
|
156
|
+
export { CarouselSectionWrapper } from './carousel/carousel-section-wrapper';
|
|
156
157
|
|
|
157
158
|
// Re-export combobox (still exists in select directory but not wrapped with theming)
|
|
158
159
|
export { ComboBox } from './select/combobox';
|
|
@@ -1,60 +1,117 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { PhotoWithFallback, PaginationPageDefault } from '../elements';
|
|
4
5
|
import type { BlogPost } from '../../types/api/blog-post';
|
|
5
6
|
|
|
6
7
|
interface BlogGalleryProps {
|
|
7
8
|
blogPosts?: BlogPost[] | null;
|
|
9
|
+
postsPerPage?: number;
|
|
10
|
+
className?: string;
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
export const BlogGallery = ({
|
|
11
14
|
blogPosts: postsData,
|
|
15
|
+
postsPerPage = 12,
|
|
16
|
+
className = "",
|
|
12
17
|
}: BlogGalleryProps) => {
|
|
13
18
|
const posts = Array.isArray(postsData) ? postsData : [];
|
|
19
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
20
|
+
|
|
21
|
+
// Get featured post (first post with featured: true or first post)
|
|
22
|
+
const featuredPost = posts.find((post) => post.featured) || posts[0];
|
|
23
|
+
const nonFeaturedPosts = posts.filter((post) => post.id !== featuredPost?.id);
|
|
24
|
+
|
|
25
|
+
// Calculate pagination for non-featured posts
|
|
26
|
+
const totalPages = Math.ceil(nonFeaturedPosts.length / postsPerPage);
|
|
27
|
+
const startIndex = (currentPage - 1) * postsPerPage;
|
|
28
|
+
const endIndex = startIndex + postsPerPage;
|
|
29
|
+
const paginatedNonFeaturedPosts = nonFeaturedPosts.slice(startIndex, endIndex);
|
|
14
30
|
|
|
15
31
|
return (
|
|
16
|
-
<section>
|
|
32
|
+
<section className={className}>
|
|
17
33
|
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
18
34
|
{posts.length > 0 ? (
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
<>
|
|
36
|
+
{/* Featured Post - Full Width */}
|
|
37
|
+
{featuredPost && (
|
|
38
|
+
<a
|
|
39
|
+
href={`/blog/${featuredPost.slug}`}
|
|
40
|
+
className="block mb-16 group"
|
|
41
|
+
>
|
|
42
|
+
<div className="w-full h-96 md:h-[500px] mb-8 overflow-hidden">
|
|
43
|
+
<PhotoWithFallback
|
|
44
|
+
item={featuredPost}
|
|
45
|
+
fallbackId={featuredPost.id}
|
|
46
|
+
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div className="max-w-3xl">
|
|
51
|
+
<p className="text-xs font-body font-normal uppercase tracking-widest mb-4" style={{ color: 'var(--color-text-brand-secondary)' }}>
|
|
52
|
+
{featuredPost.published_at ? new Date(featuredPost.published_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) : 'Recent'}
|
|
35
53
|
</p>
|
|
36
54
|
|
|
37
|
-
<
|
|
38
|
-
{
|
|
39
|
-
</
|
|
55
|
+
<h2 className="font-display text-3xl md:text-4xl font-normal text-fg-primary mb-6 group-hover:underline">
|
|
56
|
+
{featuredPost.title}
|
|
57
|
+
</h2>
|
|
40
58
|
|
|
41
|
-
{
|
|
42
|
-
<p className="font-
|
|
43
|
-
{
|
|
59
|
+
{featuredPost.excerpt_markdown && (
|
|
60
|
+
<p className="font-display text-lg leading-relaxed text-tertiary">
|
|
61
|
+
{featuredPost.excerpt_markdown.replace(/[#*\[\]()]/g, '').trim()}
|
|
44
62
|
</p>
|
|
45
63
|
)}
|
|
46
|
-
|
|
47
|
-
<a
|
|
48
|
-
href={`/blog/${post.slug}`}
|
|
49
|
-
className="font-body text-base underline underline-offset-4 hover:no-underline transition-colors"
|
|
50
|
-
style={{ color: 'var(--color-text-brand-accent)' }}
|
|
51
|
-
>
|
|
52
|
-
Read more
|
|
53
|
-
</a>
|
|
54
64
|
</div>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
</a>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
{/* Posts Grid */}
|
|
69
|
+
{paginatedNonFeaturedPosts.length > 0 && (
|
|
70
|
+
<div className="grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-3 mb-16">
|
|
71
|
+
{paginatedNonFeaturedPosts.map((post: any) => {
|
|
72
|
+
const excerpt = post.excerpt_markdown || post.content_markdown?.replace(/[#*\[\]()]/g, '').trim().substring(0, 150) || '';
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<a key={post.id} href={`/blog/${post.slug}`} className="flex flex-col group">
|
|
76
|
+
<div className="w-full h-64 mb-6 overflow-hidden">
|
|
77
|
+
<PhotoWithFallback
|
|
78
|
+
item={post}
|
|
79
|
+
fallbackId={post.id}
|
|
80
|
+
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<p className="text-xs font-body font-normal uppercase tracking-widest mb-2" style={{ color: 'var(--color-text-brand-secondary)' }}>
|
|
85
|
+
{post.published_at ? new Date(post.published_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) : 'Recent'}
|
|
86
|
+
</p>
|
|
87
|
+
|
|
88
|
+
<h3 className="font-display text-2xl font-normal text-fg-primary mb-4 group-hover:underline">
|
|
89
|
+
{post.title}
|
|
90
|
+
</h3>
|
|
91
|
+
|
|
92
|
+
{excerpt && (
|
|
93
|
+
<p className="font-body text-base leading-relaxed text-tertiary mb-4 flex-grow line-clamp-3">
|
|
94
|
+
{excerpt}
|
|
95
|
+
</p>
|
|
96
|
+
)}
|
|
97
|
+
</a>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
{/* Pagination */}
|
|
104
|
+
{totalPages > 1 && (
|
|
105
|
+
<div className="flex justify-center">
|
|
106
|
+
<PaginationPageDefault
|
|
107
|
+
rounded
|
|
108
|
+
page={currentPage}
|
|
109
|
+
total={totalPages}
|
|
110
|
+
onPageChange={setCurrentPage}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</>
|
|
58
115
|
) : (
|
|
59
116
|
<div className="text-center py-12">
|
|
60
117
|
<p className="font-body text-base text-tertiary">No posts available</p>
|
|
@@ -11,11 +11,7 @@ import type { BlogPost } from '../../types/api/blog-post';
|
|
|
11
11
|
|
|
12
12
|
interface BlogGalleryProps {
|
|
13
13
|
blogPosts?: BlogPost[] | null;
|
|
14
|
-
label?: string;
|
|
15
|
-
title?: string;
|
|
16
|
-
subtitle?: string;
|
|
17
14
|
postsPerPage?: number;
|
|
18
|
-
backgroundColor?: string;
|
|
19
15
|
className?: string;
|
|
20
16
|
}
|
|
21
17
|
|
|
@@ -27,11 +23,7 @@ const defaultSortByOptions = [
|
|
|
27
23
|
|
|
28
24
|
export const BlogGallery = ({
|
|
29
25
|
blogPosts: postsData,
|
|
30
|
-
label = "",
|
|
31
|
-
title = "",
|
|
32
|
-
subtitle = "",
|
|
33
26
|
postsPerPage = 12,
|
|
34
|
-
backgroundColor = "bg-primary",
|
|
35
27
|
className = "",
|
|
36
28
|
}: BlogGalleryProps) => {
|
|
37
29
|
const sortByOptions = defaultSortByOptions;
|
|
@@ -68,26 +60,8 @@ export const BlogGallery = ({
|
|
|
68
60
|
}, [sortBy]);
|
|
69
61
|
|
|
70
62
|
return (
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
<section className={`${backgroundColor} py-16 md:py-24`}>
|
|
74
|
-
<div className="mx-auto w-full max-w-container px-4 md:px-8">
|
|
75
|
-
<div className="flex w-full max-w-3xl flex-col">
|
|
76
|
-
{label && (
|
|
77
|
-
<span className="text-sm font-semibold text-brand-secondary md:text-md">{label}</span>
|
|
78
|
-
)}
|
|
79
|
-
<h2 className="mt-3 text-display-md font-semibold text-primary md:text-display-lg">
|
|
80
|
-
{title}
|
|
81
|
-
</h2>
|
|
82
|
-
<p className="mt-4 text-lg text-tertiary md:mt-6 md:text-xl">
|
|
83
|
-
{subtitle}
|
|
84
|
-
</p>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
</section>
|
|
88
|
-
|
|
89
|
-
{/* Main Content */}
|
|
90
|
-
<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">
|
|
63
|
+
<section className={className}>
|
|
64
|
+
<div className="mx-auto flex w-full max-w-container flex-col gap-12 px-4 md:gap-16 md:px-8">
|
|
91
65
|
{posts.length > 0 ? (
|
|
92
66
|
<>
|
|
93
67
|
{/* Featured Post - Desktop */}
|
|
@@ -196,7 +170,7 @@ export const BlogGallery = ({
|
|
|
196
170
|
<p className="text-gray-500">No blog posts available</p>
|
|
197
171
|
</div>
|
|
198
172
|
)}
|
|
199
|
-
</
|
|
200
|
-
</
|
|
173
|
+
</div>
|
|
174
|
+
</section>
|
|
201
175
|
);
|
|
202
176
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import { PhotoWithFallback } from '../elements';
|
|
4
|
+
import { PhotoWithFallback, Carousel, CarouselSectionWrapper } from '../elements';
|
|
5
5
|
import type { BlogPost } from '../../types/api/blog-post';
|
|
6
6
|
|
|
7
7
|
interface BlogSectionProps {
|
|
@@ -13,10 +13,10 @@ interface BlogSectionProps {
|
|
|
13
13
|
export const BlogSection = ({
|
|
14
14
|
blogPosts: postsData,
|
|
15
15
|
title = "",
|
|
16
|
-
maxPosts
|
|
16
|
+
maxPosts,
|
|
17
17
|
}: BlogSectionProps) => {
|
|
18
18
|
|
|
19
|
-
const postsArray = Array.isArray(postsData) ? postsData.slice(0, maxPosts) : [];
|
|
19
|
+
const postsArray = Array.isArray(postsData) ? (maxPosts ? postsData.slice(0, maxPosts) : postsData) : [];
|
|
20
20
|
|
|
21
21
|
const formatDate = (dateString?: string) => {
|
|
22
22
|
if (!dateString) return 'Recent';
|
|
@@ -34,24 +34,22 @@ export const BlogSection = ({
|
|
|
34
34
|
return (
|
|
35
35
|
<section>
|
|
36
36
|
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<div key={post.id} className="flex flex-col">
|
|
37
|
+
<CarouselSectionWrapper
|
|
38
|
+
title={title}
|
|
39
|
+
hasItems={postsArray.length > 0}
|
|
40
|
+
emptyMessage="No posts available"
|
|
41
|
+
>
|
|
42
|
+
{postsArray.map((post: BlogPost) => {
|
|
43
|
+
const excerpt = (post as any).summary || post.content_markdown?.replace(/[#*\[\]()]/g, '').trim().substring(0, 150) || '';
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Carousel.Item key={post.id} className="pl-4 md:basis-1/2 lg:basis-1/3">
|
|
47
|
+
<a href={`/blog/${post.slug}`} className="flex flex-col h-full group">
|
|
50
48
|
<div className="w-full h-64 mb-6 overflow-hidden">
|
|
51
49
|
<PhotoWithFallback
|
|
52
50
|
item={post}
|
|
53
51
|
fallbackId={post.id}
|
|
54
|
-
className="w-full h-full object-cover"
|
|
52
|
+
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
55
53
|
/>
|
|
56
54
|
</div>
|
|
57
55
|
|
|
@@ -59,32 +57,20 @@ export const BlogSection = ({
|
|
|
59
57
|
{formatDate(post.published_at)}
|
|
60
58
|
</p>
|
|
61
59
|
|
|
62
|
-
<h3 className="font-display text-2xl font-normal text-fg-primary mb-4">
|
|
60
|
+
<h3 className="font-display text-2xl font-normal text-fg-primary mb-4 group-hover:underline">
|
|
63
61
|
{post.title}
|
|
64
62
|
</h3>
|
|
65
63
|
|
|
66
64
|
{excerpt && (
|
|
67
|
-
<p className="font-body text-base leading-relaxed text-tertiary mb-
|
|
65
|
+
<p className="font-body text-base leading-relaxed text-tertiary mb-4 flex-grow line-clamp-3">
|
|
68
66
|
{excerpt}
|
|
69
67
|
</p>
|
|
70
68
|
)}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
>
|
|
77
|
-
Read more
|
|
78
|
-
</a>
|
|
79
|
-
</div>
|
|
80
|
-
);
|
|
81
|
-
})}
|
|
82
|
-
</div>
|
|
83
|
-
) : (
|
|
84
|
-
<div className="text-center py-12">
|
|
85
|
-
<p className="font-body text-base text-tertiary">No posts available</p>
|
|
86
|
-
</div>
|
|
87
|
-
)}
|
|
69
|
+
</a>
|
|
70
|
+
</Carousel.Item>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
</CarouselSectionWrapper>
|
|
88
74
|
</div>
|
|
89
75
|
</section>
|
|
90
76
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import { Form, PhotoWithFallback } from '../elements';
|
|
4
|
+
import { Form, PhotoWithFallback, Button } from '../elements';
|
|
5
5
|
import type { WebsitePhotos } from '../../types/api/website-photos';
|
|
6
6
|
|
|
7
7
|
interface ContactSectionProps {
|
|
@@ -125,13 +125,14 @@ const ContactSection = ({
|
|
|
125
125
|
</label>
|
|
126
126
|
</div>
|
|
127
127
|
|
|
128
|
-
<
|
|
129
|
-
type="submit"
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
<Button
|
|
129
|
+
type="submit"
|
|
130
|
+
color="primary"
|
|
131
|
+
size="xl"
|
|
132
|
+
className="w-full font-body text-base uppercase tracking-wide rounded-sm"
|
|
132
133
|
>
|
|
133
134
|
Send message
|
|
134
|
-
</
|
|
135
|
+
</Button>
|
|
135
136
|
</Form>
|
|
136
137
|
</div>
|
|
137
138
|
|
|
@@ -145,14 +145,25 @@ export function HeaderNavigation({
|
|
|
145
145
|
{companyName}
|
|
146
146
|
</Link>
|
|
147
147
|
|
|
148
|
-
{/* Right:
|
|
149
|
-
<div className="flex items-center gap-
|
|
148
|
+
{/* Right: CTA Buttons */}
|
|
149
|
+
<div className="flex items-center gap-3">
|
|
150
|
+
{props?.cta_button?.secondary_label && props?.cta_button?.secondary_href && (
|
|
151
|
+
<Button
|
|
152
|
+
href={props.cta_button.secondary_href}
|
|
153
|
+
size="sm"
|
|
154
|
+
color="primary"
|
|
155
|
+
className="font-body text-sm uppercase tracking-wide px-6 py-2 rounded-sm"
|
|
156
|
+
>
|
|
157
|
+
{props.cta_button.secondary_label}
|
|
158
|
+
</Button>
|
|
159
|
+
)}
|
|
150
160
|
<Button
|
|
151
|
-
href="/contact"
|
|
161
|
+
href={props?.cta_button?.href || "/contact"}
|
|
152
162
|
size="sm"
|
|
153
|
-
|
|
163
|
+
color="primary"
|
|
164
|
+
className="font-body text-sm uppercase tracking-wide px-6 py-2 rounded-sm"
|
|
154
165
|
>
|
|
155
|
-
Contact
|
|
166
|
+
{props?.cta_button?.label || "Contact"}
|
|
156
167
|
</Button>
|
|
157
168
|
</div>
|
|
158
169
|
</div>
|
|
@@ -333,24 +344,52 @@ export function HeaderNavigation({
|
|
|
333
344
|
</nav>
|
|
334
345
|
|
|
335
346
|
{/* Mobile Menu Footer */}
|
|
336
|
-
<div className="border-t border-secondary px-4 py-
|
|
337
|
-
<
|
|
338
|
-
|
|
339
|
-
|
|
347
|
+
<div className="border-t border-secondary px-4 py-6">
|
|
348
|
+
<div className="flex flex-col gap-3">
|
|
349
|
+
{props?.cta_button?.secondary_label && props?.cta_button?.secondary_href && (
|
|
350
|
+
<Button
|
|
351
|
+
href={props.cta_button.secondary_href}
|
|
352
|
+
color="primary"
|
|
353
|
+
className="w-full font-body text-sm uppercase tracking-wide py-3 rounded-sm"
|
|
354
|
+
onClick={() => setIsMobileMenuOpen(false)}
|
|
355
|
+
>
|
|
356
|
+
{props.cta_button.secondary_label}
|
|
357
|
+
</Button>
|
|
358
|
+
)}
|
|
359
|
+
<Button
|
|
360
|
+
href={props?.cta_button?.href || "/contact"}
|
|
361
|
+
color="primary"
|
|
362
|
+
className="w-full font-body text-sm uppercase tracking-wide py-3 rounded-sm"
|
|
363
|
+
onClick={() => setIsMobileMenuOpen(false)}
|
|
364
|
+
>
|
|
365
|
+
{props?.cta_button?.label || "Contact"}
|
|
366
|
+
</Button>
|
|
367
|
+
</div>
|
|
340
368
|
</div>
|
|
341
369
|
</div>
|
|
342
370
|
</div>
|
|
343
371
|
)}
|
|
344
372
|
|
|
345
373
|
{/* Sticky Contact Button (Mobile) */}
|
|
346
|
-
<div className="fixed bottom-0 left-0 right-0 z-40 md:hidden"
|
|
347
|
-
<
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
374
|
+
<div className="fixed bottom-0 left-0 right-0 z-40 md:hidden bg-fg-primary">
|
|
375
|
+
<div className="flex gap-0">
|
|
376
|
+
{props?.cta_button?.secondary_label && props?.cta_button?.secondary_href && (
|
|
377
|
+
<Button
|
|
378
|
+
href={props.cta_button.secondary_href}
|
|
379
|
+
color="primary"
|
|
380
|
+
className="flex-1 font-body text-sm uppercase tracking-wide py-4 rounded-none border-r border-gray-700"
|
|
381
|
+
>
|
|
382
|
+
{props.cta_button.secondary_label}
|
|
383
|
+
</Button>
|
|
384
|
+
)}
|
|
385
|
+
<Button
|
|
386
|
+
href={props?.cta_button?.href || "/contact"}
|
|
387
|
+
color="primary"
|
|
388
|
+
className={`${props?.cta_button?.secondary_label ? 'flex-1' : 'w-full'} font-body text-sm uppercase tracking-wide py-4 rounded-none`}
|
|
389
|
+
>
|
|
390
|
+
{props?.cta_button?.label || "Contact"}
|
|
391
|
+
</Button>
|
|
392
|
+
</div>
|
|
354
393
|
</div>
|
|
355
394
|
</>
|
|
356
395
|
);
|
|
@@ -246,6 +246,15 @@ export function HeaderNavigation({
|
|
|
246
246
|
</div>
|
|
247
247
|
|
|
248
248
|
<div className="hidden items-center gap-3 md:flex">
|
|
249
|
+
{cta_button?.secondary_label && cta_button?.secondary_href && (
|
|
250
|
+
<Button
|
|
251
|
+
href={cta_button.secondary_href}
|
|
252
|
+
color="secondary"
|
|
253
|
+
size="lg"
|
|
254
|
+
>
|
|
255
|
+
{cta_button.secondary_label}
|
|
256
|
+
</Button>
|
|
257
|
+
)}
|
|
249
258
|
<Button
|
|
250
259
|
href={cta_button?.href || "/contact"}
|
|
251
260
|
color="primary"
|
|
@@ -254,7 +263,6 @@ export function HeaderNavigation({
|
|
|
254
263
|
{cta_button?.label || "Get Started"}
|
|
255
264
|
</Button>
|
|
256
265
|
</div>
|
|
257
|
-
|
|
258
266
|
<AriaDialogTrigger>
|
|
259
267
|
<AriaButton
|
|
260
268
|
aria-label="Toggle navigation menu"
|
|
@@ -315,6 +323,15 @@ export function HeaderNavigation({
|
|
|
315
323
|
</ul>
|
|
316
324
|
|
|
317
325
|
<div className="flex flex-col gap-3 border-t border-secondary px-4 py-6">
|
|
326
|
+
{cta_button?.secondary_label && cta_button?.secondary_href && (
|
|
327
|
+
<Button
|
|
328
|
+
href={cta_button.secondary_href}
|
|
329
|
+
color="secondary"
|
|
330
|
+
size="lg"
|
|
331
|
+
>
|
|
332
|
+
{cta_button.secondary_label}
|
|
333
|
+
</Button>
|
|
334
|
+
)}
|
|
318
335
|
<Button
|
|
319
336
|
href={cta_button?.href || "/contact"}
|
|
320
337
|
color="primary"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { PhotoWithFallback, Carousel, CarouselSectionWrapper } from '../elements';
|
|
4
5
|
import type { Testimonial } from '../../types/api/testimonial';
|
|
5
6
|
import { getAvatarUrl } from '../../utils/photo-helpers';
|
|
6
7
|
|
|
@@ -16,32 +17,27 @@ export const TestimonialsHome = ({
|
|
|
16
17
|
perPage,
|
|
17
18
|
}: TestimonialsHomeProps) => {
|
|
18
19
|
const testimonials = Array.isArray(testimonialsData) ? testimonialsData : [];
|
|
19
|
-
const displayTestimonials = perPage ? testimonials.slice(0, perPage) : testimonials
|
|
20
|
+
const displayTestimonials = perPage ? testimonials.slice(0, perPage) : testimonials;
|
|
20
21
|
|
|
21
22
|
return (
|
|
22
23
|
<section>
|
|
23
24
|
<div className="mx-auto max-w-container px-4 md:px-8">
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<div
|
|
42
|
-
key={testimonial.id || i}
|
|
43
|
-
className="bg-white p-8 flex flex-col"
|
|
44
|
-
>
|
|
25
|
+
<CarouselSectionWrapper
|
|
26
|
+
title={title}
|
|
27
|
+
hasItems={displayTestimonials.length > 0}
|
|
28
|
+
emptyMessage="No testimonials available"
|
|
29
|
+
>
|
|
30
|
+
{displayTestimonials.map((testimonial: any, i: number) => {
|
|
31
|
+
const quote = testimonial.content_markdown?.replace(/[#*\[\]()]/g, '').trim() || '';
|
|
32
|
+
const reviewerName = testimonial.reviewer_name || 'Customer';
|
|
33
|
+
const username = `@${reviewerName.toLowerCase().replace(/\s+/g, '')}`;
|
|
34
|
+
const avatarUrl = getAvatarUrl(testimonial.photo_attachments, testimonial.id, reviewerName);
|
|
35
|
+
const rating = testimonial.rating || 5;
|
|
36
|
+
const isVerified = testimonial.is_verified !== false;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Carousel.Item key={testimonial.id || i} className="pl-4 md:basis-1/2 lg:basis-1/3">
|
|
40
|
+
<div className="bg-white p-8 flex flex-col h-full">
|
|
45
41
|
<div className="flex gap-1 mb-6">
|
|
46
42
|
{Array.from({ length: 5 }).map((_, starIdx) => (
|
|
47
43
|
<svg
|
|
@@ -87,14 +83,10 @@ export const TestimonialsHome = ({
|
|
|
87
83
|
</div>
|
|
88
84
|
</div>
|
|
89
85
|
</div>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<div className="text-center py-12">
|
|
95
|
-
<p className="font-body text-base text-tertiary">No testimonials available</p>
|
|
96
|
-
</div>
|
|
97
|
-
)}
|
|
86
|
+
</Carousel.Item>
|
|
87
|
+
);
|
|
88
|
+
})}
|
|
89
|
+
</CarouselSectionWrapper>
|
|
98
90
|
</div>
|
|
99
91
|
</section>
|
|
100
92
|
);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
/* Aman color palette - warm, elegant tones */
|
|
10
10
|
--color-bg-primary: #F9F7F0; /* Light warm beige */
|
|
11
11
|
--color-bg-secondary: #F9F7F0;
|
|
12
|
+
--color-bg-primary_hover: #F0EDE4; /* Slightly darker beige on hover */
|
|
12
13
|
--background-color-primary: #F9F7F0;
|
|
13
14
|
--background-color-secondary: #F9F7F0;
|
|
14
15
|
|
|
@@ -22,9 +23,20 @@
|
|
|
22
23
|
--color-fg-secondary: #505050;
|
|
23
24
|
--color-fg-tertiary: #787878;
|
|
24
25
|
|
|
25
|
-
/* Brand
|
|
26
|
-
--color-
|
|
26
|
+
/* Brand colors - elegant gold/bronze for primary buttons and accents */
|
|
27
|
+
--color-brand-500: #B48C64; /* Elegant gold/bronze */
|
|
28
|
+
--color-brand-600: #8C6440; /* Darker bronze for primary button background */
|
|
29
|
+
--color-brand-700: #6D4E32; /* Even darker for hover */
|
|
30
|
+
--color-fg-brand-primary: #8C6440;
|
|
31
|
+
--color-fg-brand-secondary: #B48C64;
|
|
27
32
|
--color-text-brand-secondary: #8C6440;
|
|
33
|
+
--color-text-brand-accent: #B48C64; /* Used for links and accents in Aman components */
|
|
34
|
+
|
|
35
|
+
/* Button backgrounds - override semantic tokens */
|
|
36
|
+
--color-bg-brand-solid: #8C6440; /* Primary button (Contact, Send Message) */
|
|
37
|
+
--color-bg-brand-solid_hover: #6D4E32; /* Primary button hover */
|
|
38
|
+
--background-color-brand-solid: #8C6440;
|
|
39
|
+
--background-color-brand-solid_hover: #6D4E32;
|
|
28
40
|
|
|
29
41
|
/* Border colors */
|
|
30
42
|
--color-border-primary: #E5E2D9;
|