@notionhive/testimonials 0.1.0
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/bin/testimonials.js +16 -0
- package/category.config.json +7 -0
- package/package.json +24 -0
- package/registry/index.json +174 -0
- package/registry/testimonial-01.json +10 -0
- package/registry/testimonial-02.json +10 -0
- package/registry/testimonial-03.json +10 -0
- package/registry/testimonial-04.json +10 -0
- package/registry/testimonial-05.json +9 -0
- package/registry/testimonial-06.json +10 -0
- package/registry/testimonial-07.json +10 -0
- package/registry/testimonial-08.json +10 -0
- package/registry/testimonial-09.json +10 -0
- package/registry/testimonial-10.json +10 -0
- package/registry/testimonial-11.json +10 -0
- package/registry/testimonial-12.json +10 -0
- package/registry/testimonial-13.json +10 -0
- package/registry/testimonial-14.json +9 -0
- package/registry/testimonial-15.json +10 -0
- package/registry/testimonial-16.json +10 -0
- package/registry/testimonial-17.json +10 -0
- package/templates/components/atoms/SafeImage/SafeImage.jsx +101 -0
- package/templates/components/atoms/SafeImage/index.js +1 -0
- package/templates/components/hooks/useCarousel.js +73 -0
- package/templates/components/organisms/Testimonial01/Testimonial01.jsx +171 -0
- package/templates/components/organisms/Testimonial01/Testimonial01.propTypes.js +82 -0
- package/templates/components/organisms/Testimonial01/index.js +1 -0
- package/templates/components/organisms/Testimonial02/Testimonial02.jsx +200 -0
- package/templates/components/organisms/Testimonial02/Testimonial02.propTypes.js +75 -0
- package/templates/components/organisms/Testimonial02/index.js +1 -0
- package/templates/components/organisms/Testimonial03/Testimonial03.jsx +208 -0
- package/templates/components/organisms/Testimonial03/Testimonial03.propTypes.js +90 -0
- package/templates/components/organisms/Testimonial03/index.js +1 -0
- package/templates/components/organisms/Testimonial04/Testimonial04.jsx +242 -0
- package/templates/components/organisms/Testimonial04/Testimonial04.propTypes.js +81 -0
- package/templates/components/organisms/Testimonial04/index.js +1 -0
- package/templates/components/organisms/Testimonial05/Testimonial05.jsx +88 -0
- package/templates/components/organisms/Testimonial05/Testimonial05.propTypes.js +29 -0
- package/templates/components/organisms/Testimonial05/index.js +1 -0
- package/templates/components/organisms/Testimonial06/Testimonial06.jsx +203 -0
- package/templates/components/organisms/Testimonial06/Testimonial06.propTypes.js +106 -0
- package/templates/components/organisms/Testimonial06/index.js +1 -0
- package/templates/components/organisms/Testimonial07/Testimonial07.jsx +245 -0
- package/templates/components/organisms/Testimonial07/Testimonial07.propTypes.js +80 -0
- package/templates/components/organisms/Testimonial07/index.js +1 -0
- package/templates/components/organisms/Testimonial08/Testimonial08.jsx +188 -0
- package/templates/components/organisms/Testimonial08/Testimonial08.propTypes.js +69 -0
- package/templates/components/organisms/Testimonial08/index.js +1 -0
- package/templates/components/organisms/Testimonial09/Testimonial09.jsx +214 -0
- package/templates/components/organisms/Testimonial09/Testimonial09.propTypes.js +105 -0
- package/templates/components/organisms/Testimonial09/index.js +1 -0
- package/templates/components/organisms/Testimonial10/Testimonial10.jsx +218 -0
- package/templates/components/organisms/Testimonial10/Testimonial10.propTypes.js +65 -0
- package/templates/components/organisms/Testimonial10/index.js +1 -0
- package/templates/components/organisms/Testimonial11/Testimonial11.jsx +242 -0
- package/templates/components/organisms/Testimonial11/Testimonial11.propTypes.js +73 -0
- package/templates/components/organisms/Testimonial11/index.js +1 -0
- package/templates/components/organisms/Testimonial12/Testimonial12.jsx +271 -0
- package/templates/components/organisms/Testimonial12/Testimonial12.propTypes.js +81 -0
- package/templates/components/organisms/Testimonial12/index.js +1 -0
- package/templates/components/organisms/Testimonial13/Testimonial13.jsx +206 -0
- package/templates/components/organisms/Testimonial13/Testimonial13.propTypes.js +87 -0
- package/templates/components/organisms/Testimonial13/index.js +1 -0
- package/templates/components/organisms/Testimonial14/Testimonial14.jsx +89 -0
- package/templates/components/organisms/Testimonial14/Testimonial14.propTypes.js +87 -0
- package/templates/components/organisms/Testimonial14/index.js +1 -0
- package/templates/components/organisms/Testimonial15/Testimonial15.jsx +201 -0
- package/templates/components/organisms/Testimonial15/Testimonial15.propTypes.js +89 -0
- package/templates/components/organisms/Testimonial15/index.js +1 -0
- package/templates/components/organisms/Testimonial16/Testimonial16.jsx +193 -0
- package/templates/components/organisms/Testimonial16/Testimonial16.propTypes.js +90 -0
- package/templates/components/organisms/Testimonial16/index.js +1 -0
- package/templates/components/organisms/Testimonial17/Testimonial17.jsx +236 -0
- package/templates/components/organisms/Testimonial17/Testimonial17.propTypes.js +98 -0
- package/templates/components/organisms/Testimonial17/index.js +1 -0
- package/templates/public/testimonials/testimonial01/avatar-marcus.jpg +0 -0
- package/templates/public/testimonials/testimonial01/avatar-wilma.jpg +0 -0
- package/templates/public/testimonials/testimonial01/company-logo-2.png +0 -0
- package/templates/public/testimonials/testimonial01/company-logo-icon-raw.png +0 -0
- package/templates/public/testimonials/testimonial01/company-logo-icon.png +0 -0
- package/templates/public/testimonials/testimonial01/desco-logo-export.png +0 -0
- package/templates/public/testimonials/testimonial01/desco-logo-raw1.png +0 -0
- package/templates/public/testimonials/testimonial01/desco-logo-raw2.png +0 -0
- package/templates/public/testimonials/testimonial01/desco-logo.png +0 -0
- package/templates/public/testimonials/testimonial02/avatar-julia.jpg +0 -0
- package/templates/public/testimonials/testimonial02/avatar-marie.jpg +0 -0
- package/templates/public/testimonials/testimonial02/avatar-mark.jpg +0 -0
- package/templates/public/testimonials/testimonial02/bg-gradient.png +0 -0
- package/templates/public/testimonials/testimonial03/avatar-david.jpg +0 -0
- package/templates/public/testimonials/testimonial03/globe-bg.png +0 -0
- package/templates/public/testimonials/testimonial04/avatar-carlos.jpg +0 -0
- package/templates/public/testimonials/testimonial04/avatar-john.jpg +0 -0
- package/templates/public/testimonials/testimonial04/avatar-sabbir.jpg +0 -0
- package/templates/public/testimonials/testimonial05/portrait.jpg +0 -0
- package/templates/public/testimonials/testimonial06/avatar-ahmed.jpg +0 -0
- package/templates/public/testimonials/testimonial06/avatar-danzel.jpg +0 -0
- package/templates/public/testimonials/testimonial06/avatar-lisa.jpg +0 -0
- package/templates/public/testimonials/testimonial06/avatar-maria.jpg +0 -0
- package/templates/public/testimonials/testimonial06/avatar-sarah.jpg +0 -0
- package/templates/public/testimonials/testimonial07/photo-2.jpg +0 -0
- package/templates/public/testimonials/testimonial07/photo-3.jpg +0 -0
- package/templates/public/testimonials/testimonial07/photo-alt.png +0 -0
- package/templates/public/testimonials/testimonial07/photo.jpg +0 -0
- package/templates/public/testimonials/testimonial07/slide-2.png +0 -0
- package/templates/public/testimonials/testimonial08/student-1.jpg +0 -0
- package/templates/public/testimonials/testimonial08/student-2.jpg +0 -0
- package/templates/public/testimonials/testimonial08/student-3.jpg +0 -0
- package/templates/public/testimonials/testimonial09/avatar-1.jpg +0 -0
- package/templates/public/testimonials/testimonial09/avatar-2.jpg +0 -0
- package/templates/public/testimonials/testimonial09/avatar-3.jpg +0 -0
- package/templates/public/testimonials/testimonial09/avatar-4.jpg +0 -0
- package/templates/public/testimonials/testimonial09/avatar-5.jpg +0 -0
- package/templates/public/testimonials/testimonial09/avatar-6.jpg +0 -0
- package/templates/public/testimonials/testimonial10/card-1.jpg +0 -0
- package/templates/public/testimonials/testimonial10/card-2.jpg +0 -0
- package/templates/public/testimonials/testimonial10/card-3.jpg +0 -0
- package/templates/public/testimonials/testimonial11/avatar-1.jpg +0 -0
- package/templates/public/testimonials/testimonial11/avatar-2.jpg +0 -0
- package/templates/public/testimonials/testimonial11/avatar-3.jpg +0 -0
- package/templates/public/testimonials/testimonial12/story-1.jpg +0 -0
- package/templates/public/testimonials/testimonial12/story-2.jpg +0 -0
- package/templates/public/testimonials/testimonial12/story-3.jpg +0 -0
- package/templates/public/testimonials/testimonial12/story-4.jpg +0 -0
- package/templates/public/testimonials/testimonial13/slide-1.jpg +0 -0
- package/templates/public/testimonials/testimonial13/slide-2.jpg +0 -0
- package/templates/public/testimonials/testimonial13/slide-3.jpg +0 -0
- package/templates/public/testimonials/testimonial13/slide-4.jpg +0 -0
- package/templates/public/testimonials/testimonial14/avatar-1.jpg +0 -0
- package/templates/public/testimonials/testimonial14/avatar-2.jpg +0 -0
- package/templates/public/testimonials/testimonial14/avatar-3.jpg +0 -0
- package/templates/public/testimonials/testimonial14/avatar-4.jpg +0 -0
- package/templates/public/testimonials/testimonial15/slide-1.jpg +0 -0
- package/templates/public/testimonials/testimonial15/slide-2.jpg +0 -0
- package/templates/public/testimonials/testimonial15/slide-3.jpg +0 -0
- package/templates/public/testimonials/testimonial16/avatar.jpg +0 -0
- package/templates/public/testimonials/testimonial16/featured.jpg +0 -0
- package/templates/public/testimonials/testimonial17/avatar-1.jpg +0 -0
- package/templates/public/testimonials/testimonial17/avatar-2.jpg +0 -0
- package/templates/public/testimonials/testimonial17/avatar-3.jpg +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import SafeImage from "../../atoms/SafeImage";
|
|
6
|
+
import { useCarousel } from "../../hooks/useCarousel";
|
|
7
|
+
import {
|
|
8
|
+
testimonial03DefaultProps,
|
|
9
|
+
testimonial03PropTypes,
|
|
10
|
+
} from "./Testimonial03.propTypes";
|
|
11
|
+
|
|
12
|
+
function getCardsPerView() {
|
|
13
|
+
if (typeof window === "undefined") return 2;
|
|
14
|
+
if (window.matchMedia("(min-width: 1024px)").matches) return 2;
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ArrowIcon({ direction, className = "" }) {
|
|
19
|
+
return (
|
|
20
|
+
<svg
|
|
21
|
+
viewBox="0 0 20 20"
|
|
22
|
+
fill="none"
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
className={className}
|
|
25
|
+
>
|
|
26
|
+
<path
|
|
27
|
+
d={direction === "prev" ? "M12 5l-5 5 5 5" : "M8 5l5 5-5 5"}
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
strokeWidth="1.5"
|
|
30
|
+
strokeLinecap="round"
|
|
31
|
+
strokeLinejoin="round"
|
|
32
|
+
/>
|
|
33
|
+
</svg>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function NavButton({ direction, onClick, disabled, active }) {
|
|
38
|
+
return (
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
onClick={onClick}
|
|
42
|
+
disabled={disabled}
|
|
43
|
+
aria-label={direction === "prev" ? "Previous testimonial" : "Next testimonial"}
|
|
44
|
+
className={[
|
|
45
|
+
"flex size-10 items-center justify-center rounded-full border transition-colors duration-200 ease-out",
|
|
46
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
47
|
+
"disabled:cursor-not-allowed disabled:opacity-40",
|
|
48
|
+
active
|
|
49
|
+
? "border-white/20 bg-white/30 text-white hover:bg-white/40"
|
|
50
|
+
: "border-white/20 bg-transparent text-white hover:bg-white/10",
|
|
51
|
+
].join(" ")}
|
|
52
|
+
>
|
|
53
|
+
<ArrowIcon direction={direction} className="size-5" />
|
|
54
|
+
</button>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Testimonial03 — Axentec dark glassmorphism testimonial carousel with globe backdrop.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} props - See Testimonial03.propTypes.js.
|
|
62
|
+
*/
|
|
63
|
+
export function Testimonial03({
|
|
64
|
+
eyebrow = testimonial03DefaultProps.eyebrow,
|
|
65
|
+
headline = testimonial03DefaultProps.headline,
|
|
66
|
+
description = testimonial03DefaultProps.description,
|
|
67
|
+
backgroundImage = testimonial03DefaultProps.backgroundImage,
|
|
68
|
+
testimonials = testimonial03DefaultProps.testimonials,
|
|
69
|
+
activeSlide: activeSlideProp = testimonial03DefaultProps.activeSlide,
|
|
70
|
+
onSlideChange,
|
|
71
|
+
className = "",
|
|
72
|
+
}) {
|
|
73
|
+
const [perView, setPerView] = useState(2);
|
|
74
|
+
const maxIndex = Math.max(0, testimonials.length - perView);
|
|
75
|
+
const { activeSlide, goTo, next, prev } = useCarousel({
|
|
76
|
+
count: maxIndex + 1,
|
|
77
|
+
initialIndex: activeSlideProp,
|
|
78
|
+
onChange: onSlideChange,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const update = () => setPerView(getCardsPerView());
|
|
83
|
+
update();
|
|
84
|
+
window.addEventListener("resize", update);
|
|
85
|
+
return () => window.removeEventListener("resize", update);
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
goTo(Math.min(activeSlideProp, maxIndex));
|
|
90
|
+
}, [activeSlideProp, goTo, maxIndex]);
|
|
91
|
+
|
|
92
|
+
const stepPercent = 100 / perView;
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<section
|
|
96
|
+
className={[
|
|
97
|
+
"relative w-full overflow-hidden bg-[#221e1f]",
|
|
98
|
+
"px-4 py-16 sm:px-6 sm:py-20 md:px-10 lg:px-[70px] lg:py-24 xl:px-[70px]",
|
|
99
|
+
className,
|
|
100
|
+
]
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join(" ")}
|
|
103
|
+
data-testimonial="testimonial03"
|
|
104
|
+
>
|
|
105
|
+
{backgroundImage?.src ? (
|
|
106
|
+
<div
|
|
107
|
+
className="pointer-events-none absolute inset-0 overflow-hidden"
|
|
108
|
+
aria-hidden="true"
|
|
109
|
+
>
|
|
110
|
+
<SafeImage
|
|
111
|
+
src={backgroundImage.src}
|
|
112
|
+
alt=""
|
|
113
|
+
fill
|
|
114
|
+
className="object-cover object-center opacity-30 mix-blend-lighten"
|
|
115
|
+
sizes="100vw"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
) : null}
|
|
119
|
+
|
|
120
|
+
<div className="relative mx-auto flex w-full max-w-[1300px] flex-col gap-10 lg:gap-16">
|
|
121
|
+
<div className="flex flex-col gap-5 text-[#fffeff] lg:max-w-xl">
|
|
122
|
+
<div>
|
|
123
|
+
<p className="text-base font-medium tracking-[-0.02em] sm:text-lg">
|
|
124
|
+
{eyebrow}
|
|
125
|
+
</p>
|
|
126
|
+
<h2 className="text-3xl font-semibold leading-tight tracking-[-0.02em] sm:text-4xl lg:text-[56px] lg:leading-[70px]">
|
|
127
|
+
{headline}
|
|
128
|
+
</h2>
|
|
129
|
+
</div>
|
|
130
|
+
{description ? (
|
|
131
|
+
<p className="text-base leading-relaxed sm:text-lg lg:leading-[30px]">
|
|
132
|
+
{description}
|
|
133
|
+
</p>
|
|
134
|
+
) : null}
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div className="flex flex-col gap-6">
|
|
138
|
+
<div className="flex justify-end gap-1">
|
|
139
|
+
<NavButton
|
|
140
|
+
direction="prev"
|
|
141
|
+
onClick={prev}
|
|
142
|
+
disabled={activeSlide === 0}
|
|
143
|
+
active={false}
|
|
144
|
+
/>
|
|
145
|
+
<NavButton
|
|
146
|
+
direction="next"
|
|
147
|
+
onClick={next}
|
|
148
|
+
disabled={activeSlide >= maxIndex}
|
|
149
|
+
active
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div className="overflow-hidden">
|
|
154
|
+
<div
|
|
155
|
+
className="flex gap-5 transition-transform duration-500 ease-out motion-reduce:transition-none"
|
|
156
|
+
style={{ transform: `translateX(-${activeSlide * stepPercent}%)` }}
|
|
157
|
+
>
|
|
158
|
+
{testimonials.map((item) => (
|
|
159
|
+
<article
|
|
160
|
+
key={item.id ?? item.authorName}
|
|
161
|
+
className={[
|
|
162
|
+
"flex w-full shrink-0 flex-col justify-center rounded-[20px] border border-[#221e1f] p-5 backdrop-blur-sm sm:p-6",
|
|
163
|
+
"bg-gradient-to-br from-[rgba(34,30,31,0.2)] to-[rgba(93,93,97,0.2)]",
|
|
164
|
+
perView === 2 ? "lg:w-1/2" : "",
|
|
165
|
+
].join(" ")}
|
|
166
|
+
>
|
|
167
|
+
<div className="flex flex-col gap-10 lg:gap-[74px]">
|
|
168
|
+
<blockquote className="text-base leading-[30px] text-[#fffeff]/70 sm:text-lg">
|
|
169
|
+
{item.quote}
|
|
170
|
+
</blockquote>
|
|
171
|
+
|
|
172
|
+
<div className="flex items-center gap-4">
|
|
173
|
+
{item.avatar?.src ? (
|
|
174
|
+
<div className="relative size-11 shrink-0 overflow-hidden rounded-full sm:size-[46px]">
|
|
175
|
+
<SafeImage
|
|
176
|
+
src={item.avatar.src}
|
|
177
|
+
alt={item.avatar.alt ?? item.authorName}
|
|
178
|
+
fill
|
|
179
|
+
className="object-cover object-center"
|
|
180
|
+
sizes="46px"
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
) : null}
|
|
184
|
+
<div className="min-w-0">
|
|
185
|
+
<p className="text-base text-[#fffeff] sm:text-lg">
|
|
186
|
+
{item.authorName}
|
|
187
|
+
</p>
|
|
188
|
+
{item.authorRole ? (
|
|
189
|
+
<p className="text-sm text-white/60 sm:text-base">
|
|
190
|
+
{item.authorRole}
|
|
191
|
+
</p>
|
|
192
|
+
) : null}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</article>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</section>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
Testimonial03.propTypes = testimonial03PropTypes;
|
|
207
|
+
|
|
208
|
+
export default Testimonial03;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
|
|
3
|
+
const imageShape = PropTypes.shape({
|
|
4
|
+
src: PropTypes.string.isRequired,
|
|
5
|
+
alt: PropTypes.string,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const testimonialItemShape = PropTypes.shape({
|
|
9
|
+
id: PropTypes.string,
|
|
10
|
+
quote: PropTypes.string.isRequired,
|
|
11
|
+
authorName: PropTypes.string.isRequired,
|
|
12
|
+
authorRole: PropTypes.string,
|
|
13
|
+
avatar: imageShape,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const testimonial03PropTypes = {
|
|
17
|
+
/** Small label above the headline. */
|
|
18
|
+
eyebrow: PropTypes.string,
|
|
19
|
+
/** Section headline. */
|
|
20
|
+
headline: PropTypes.string,
|
|
21
|
+
/** Supporting description below the headline. */
|
|
22
|
+
description: PropTypes.string,
|
|
23
|
+
/** Decorative globe background image. */
|
|
24
|
+
backgroundImage: imageShape,
|
|
25
|
+
/** Testimonial cards for the carousel. */
|
|
26
|
+
testimonials: PropTypes.arrayOf(testimonialItemShape),
|
|
27
|
+
/** Controlled active slide index. */
|
|
28
|
+
activeSlide: PropTypes.number,
|
|
29
|
+
/** Called when the active slide changes. */
|
|
30
|
+
onSlideChange: PropTypes.func,
|
|
31
|
+
className: PropTypes.string,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const testimonial03DefaultProps = {
|
|
35
|
+
eyebrow: "Success Stories from",
|
|
36
|
+
headline: "Our Valued Clients",
|
|
37
|
+
description:
|
|
38
|
+
"Hear directly from our clients about the impact of our secure cloud, server management, and data security services.",
|
|
39
|
+
backgroundImage: {
|
|
40
|
+
src: "/testimonials/testimonial03/globe-bg.png",
|
|
41
|
+
alt: "",
|
|
42
|
+
},
|
|
43
|
+
testimonials: [
|
|
44
|
+
{
|
|
45
|
+
id: "david-lee-1",
|
|
46
|
+
quote:
|
|
47
|
+
"The custom solutions provided by Axentec have significantly improved our workflow efficiency and customer service capabilities. Their approach to problem-solving and dedication to excellence has greatly benefited our organization.",
|
|
48
|
+
authorName: "David Lee,",
|
|
49
|
+
authorRole: "Director of IT, Healthcare Trust",
|
|
50
|
+
avatar: {
|
|
51
|
+
src: "/testimonials/testimonial03/avatar-david.jpg",
|
|
52
|
+
alt: "David Lee",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "david-lee-2",
|
|
57
|
+
quote:
|
|
58
|
+
"The custom solutions provided by Axentec have significantly improved our workflow efficiency and customer service capabilities. Their approach to problem-solving and dedication to excellence has greatly benefited our organization.",
|
|
59
|
+
authorName: "David Lee,",
|
|
60
|
+
authorRole: "Director of IT, Healthcare Trust",
|
|
61
|
+
avatar: {
|
|
62
|
+
src: "/testimonials/testimonial03/avatar-david.jpg",
|
|
63
|
+
alt: "David Lee",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "david-lee-3",
|
|
68
|
+
quote:
|
|
69
|
+
"The custom solutions provided by Axentec have significantly improved our workflow efficiency and customer service capabilities. Their approach to problem-solving and dedication to excellence has greatly benefited our organization.",
|
|
70
|
+
authorName: "David Lee,",
|
|
71
|
+
authorRole: "Director of IT, Healthcare Trust",
|
|
72
|
+
avatar: {
|
|
73
|
+
src: "/testimonials/testimonial03/avatar-david.jpg",
|
|
74
|
+
alt: "David Lee",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "david-lee-4",
|
|
79
|
+
quote:
|
|
80
|
+
"The custom solutions provided by Axentec have significantly improved our workflow efficiency and customer service capabilities. Their approach to problem-solving and dedication to excellence has greatly benefited our organization.",
|
|
81
|
+
authorName: "David Lee,",
|
|
82
|
+
authorRole: "Director of IT, Healthcare Trust",
|
|
83
|
+
avatar: {
|
|
84
|
+
src: "/testimonials/testimonial03/avatar-david.jpg",
|
|
85
|
+
alt: "David Lee",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
activeSlide: 0,
|
|
90
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Testimonial03, default } from "./Testimonial03";
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import SafeImage from "../../atoms/SafeImage";
|
|
6
|
+
import { useCarousel } from "../../hooks/useCarousel";
|
|
7
|
+
import {
|
|
8
|
+
testimonial04DefaultProps,
|
|
9
|
+
testimonial04PropTypes,
|
|
10
|
+
} from "./Testimonial04.propTypes";
|
|
11
|
+
|
|
12
|
+
function StarIcon({ className = "" }) {
|
|
13
|
+
return (
|
|
14
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
|
|
15
|
+
<path d="M12 2l2.9 6.26L22 9.27l-5 4.87L18.2 22 12 18.27 5.8 22l1.2-7.86-5-4.87 7.1-1.01L12 2z" />
|
|
16
|
+
</svg>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ArrowIcon({ direction, className = "" }) {
|
|
21
|
+
return (
|
|
22
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
|
|
23
|
+
{direction === "prev" ? (
|
|
24
|
+
<>
|
|
25
|
+
<path
|
|
26
|
+
d="M15 6l-6 6 6 6"
|
|
27
|
+
stroke="currentColor"
|
|
28
|
+
strokeWidth="1.5"
|
|
29
|
+
strokeLinecap="round"
|
|
30
|
+
strokeLinejoin="round"
|
|
31
|
+
/>
|
|
32
|
+
<path
|
|
33
|
+
d="M9 12h10"
|
|
34
|
+
stroke="currentColor"
|
|
35
|
+
strokeWidth="1.5"
|
|
36
|
+
strokeLinecap="round"
|
|
37
|
+
/>
|
|
38
|
+
</>
|
|
39
|
+
) : (
|
|
40
|
+
<>
|
|
41
|
+
<path
|
|
42
|
+
d="M9 6l6 6-6 6"
|
|
43
|
+
stroke="currentColor"
|
|
44
|
+
strokeWidth="1.5"
|
|
45
|
+
strokeLinecap="round"
|
|
46
|
+
strokeLinejoin="round"
|
|
47
|
+
/>
|
|
48
|
+
<path
|
|
49
|
+
d="M5 12h10"
|
|
50
|
+
stroke="currentColor"
|
|
51
|
+
strokeWidth="1.5"
|
|
52
|
+
strokeLinecap="round"
|
|
53
|
+
/>
|
|
54
|
+
</>
|
|
55
|
+
)}
|
|
56
|
+
</svg>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function NavButton({ direction, onClick, disabled, active }) {
|
|
61
|
+
return (
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
onClick={onClick}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
aria-label={direction === "prev" ? "Previous review" : "Next review"}
|
|
67
|
+
className={[
|
|
68
|
+
"flex items-center justify-center rounded-full border p-3 transition-colors duration-200 ease-out",
|
|
69
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
70
|
+
"disabled:cursor-not-allowed disabled:opacity-40",
|
|
71
|
+
active
|
|
72
|
+
? "border-[#e31d4a] text-[#e31d4a] hover:bg-[#e31d4a]/5"
|
|
73
|
+
: "border-[#f5c2ce] text-[#f5c2ce] hover:bg-[#f5c2ce]/10",
|
|
74
|
+
].join(" ")}
|
|
75
|
+
>
|
|
76
|
+
<ArrowIcon direction={direction} className="size-6" />
|
|
77
|
+
</button>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function CardWave({ className = "" }) {
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
className={["pointer-events-none absolute rounded-full opacity-20", className].join(" ")}
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
style={{
|
|
87
|
+
background:
|
|
88
|
+
"radial-gradient(circle, rgba(255,255,255,0.35) 0%, rgba(255,255,255,0) 70%)",
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Testimonial04 — Travel review cards with star ratings and teal gradient panels.
|
|
96
|
+
*
|
|
97
|
+
* @param {object} props - See Testimonial04.propTypes.js.
|
|
98
|
+
*/
|
|
99
|
+
export function Testimonial04({
|
|
100
|
+
headline = testimonial04DefaultProps.headline,
|
|
101
|
+
testimonials = testimonial04DefaultProps.testimonials,
|
|
102
|
+
activeSlide: activeSlideProp = testimonial04DefaultProps.activeSlide,
|
|
103
|
+
onSlideChange,
|
|
104
|
+
className = "",
|
|
105
|
+
}) {
|
|
106
|
+
const trackRef = useRef(null);
|
|
107
|
+
const [slideOffsets, setSlideOffsets] = useState([0]);
|
|
108
|
+
const maxIndex = Math.max(0, testimonials.length - 1);
|
|
109
|
+
const { activeSlide, goTo, next, prev } = useCarousel({
|
|
110
|
+
count: testimonials.length,
|
|
111
|
+
initialIndex: activeSlideProp,
|
|
112
|
+
onChange: onSlideChange,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
goTo(Math.min(activeSlideProp, maxIndex));
|
|
117
|
+
}, [activeSlideProp, goTo, maxIndex]);
|
|
118
|
+
|
|
119
|
+
useLayoutEffect(() => {
|
|
120
|
+
const track = trackRef.current;
|
|
121
|
+
if (!track) return undefined;
|
|
122
|
+
|
|
123
|
+
const measure = () => {
|
|
124
|
+
const cards = track.querySelectorAll("[data-carousel-card]");
|
|
125
|
+
const offsets = Array.from(cards).map((card) => card.offsetLeft);
|
|
126
|
+
setSlideOffsets(offsets.length ? offsets : [0]);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
measure();
|
|
130
|
+
window.addEventListener("resize", measure);
|
|
131
|
+
return () => window.removeEventListener("resize", measure);
|
|
132
|
+
}, [testimonials.length]);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<section
|
|
136
|
+
className={[
|
|
137
|
+
"relative w-full overflow-hidden bg-[#fdf9f6]",
|
|
138
|
+
"px-4 py-12 sm:px-6 sm:py-16 md:px-10 lg:px-10 lg:py-20",
|
|
139
|
+
className,
|
|
140
|
+
]
|
|
141
|
+
.filter(Boolean)
|
|
142
|
+
.join(" ")}
|
|
143
|
+
data-testimonial="testimonial04"
|
|
144
|
+
>
|
|
145
|
+
<div className="mx-auto flex w-full max-w-[1440px] flex-col gap-8 lg:gap-10">
|
|
146
|
+
<div className="flex flex-col items-start justify-between gap-6 sm:flex-row sm:items-end">
|
|
147
|
+
<h2 className="text-2xl font-bold leading-tight text-[#18191a] sm:text-3xl lg:text-[34px] lg:leading-10">
|
|
148
|
+
{headline}
|
|
149
|
+
</h2>
|
|
150
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
151
|
+
<NavButton
|
|
152
|
+
direction="prev"
|
|
153
|
+
onClick={prev}
|
|
154
|
+
disabled={activeSlide === 0}
|
|
155
|
+
active={false}
|
|
156
|
+
/>
|
|
157
|
+
<NavButton
|
|
158
|
+
direction="next"
|
|
159
|
+
onClick={next}
|
|
160
|
+
disabled={activeSlide >= maxIndex}
|
|
161
|
+
active
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div className="overflow-hidden">
|
|
167
|
+
<div
|
|
168
|
+
ref={trackRef}
|
|
169
|
+
data-carousel-track
|
|
170
|
+
className="flex w-max gap-3 transition-transform duration-500 ease-out motion-reduce:transition-none"
|
|
171
|
+
style={{ transform: `translateX(-${slideOffsets[activeSlide] ?? 0}px)` }}
|
|
172
|
+
>
|
|
173
|
+
{testimonials.map((item) => (
|
|
174
|
+
<article
|
|
175
|
+
key={item.id ?? item.authorName}
|
|
176
|
+
data-carousel-card
|
|
177
|
+
className={[
|
|
178
|
+
"relative flex shrink-0 flex-col overflow-hidden rounded-[40px] bg-[#1a8b8d] text-white",
|
|
179
|
+
item.variant === "narrow"
|
|
180
|
+
? "h-[380px] w-[min(85vw,385px)] sm:h-[421px]"
|
|
181
|
+
: "h-[400px] w-[min(90vw,540px)] sm:h-[422px]",
|
|
182
|
+
].join(" ")}
|
|
183
|
+
>
|
|
184
|
+
<CardWave className="-right-16 -top-32 size-72 sm:size-96" />
|
|
185
|
+
<CardWave className="bottom-0 left-1/2 size-80 -translate-x-1/2 sm:size-[28rem]" />
|
|
186
|
+
|
|
187
|
+
<div className="relative flex flex-1 flex-col items-center justify-between gap-6 px-6 py-10 sm:px-8 sm:py-14 lg:px-14">
|
|
188
|
+
<div className="flex w-full flex-col items-center gap-4 text-center">
|
|
189
|
+
{item.rating ? (
|
|
190
|
+
<div className="flex items-center gap-1.5" aria-label={`${item.rating} out of 5 stars`}>
|
|
191
|
+
{Array.from({ length: item.rating }).map((_, i) => (
|
|
192
|
+
<StarIcon key={i} className="size-5 text-white sm:size-6" />
|
|
193
|
+
))}
|
|
194
|
+
</div>
|
|
195
|
+
) : null}
|
|
196
|
+
|
|
197
|
+
<div className="flex w-full flex-col gap-4">
|
|
198
|
+
{item.title ? (
|
|
199
|
+
<h3 className="text-xl font-bold leading-8 sm:text-2xl">
|
|
200
|
+
{item.title}
|
|
201
|
+
</h3>
|
|
202
|
+
) : null}
|
|
203
|
+
{item.quote ? (
|
|
204
|
+
<p className="text-sm leading-6 sm:text-base">
|
|
205
|
+
“{item.quote}”
|
|
206
|
+
</p>
|
|
207
|
+
) : null}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<hr className="w-full border-white/30" />
|
|
212
|
+
|
|
213
|
+
<div className="flex flex-col items-center gap-2 text-center">
|
|
214
|
+
{item.avatar?.src ? (
|
|
215
|
+
<div className="relative size-12 overflow-hidden rounded-full">
|
|
216
|
+
<SafeImage
|
|
217
|
+
src={item.avatar.src}
|
|
218
|
+
alt={item.avatar.alt ?? item.authorName}
|
|
219
|
+
fill
|
|
220
|
+
className="object-cover object-center"
|
|
221
|
+
sizes="48px"
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
) : null}
|
|
225
|
+
<p className="text-sm font-semibold leading-5">{item.authorName}</p>
|
|
226
|
+
{item.dateLabel ? (
|
|
227
|
+
<p className="text-sm leading-5 text-white/90">{item.dateLabel}</p>
|
|
228
|
+
) : null}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</article>
|
|
232
|
+
))}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</section>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
Testimonial04.propTypes = testimonial04PropTypes;
|
|
241
|
+
|
|
242
|
+
export default Testimonial04;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
|
|
3
|
+
const imageShape = PropTypes.shape({
|
|
4
|
+
src: PropTypes.string.isRequired,
|
|
5
|
+
alt: PropTypes.string,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const testimonialItemShape = PropTypes.shape({
|
|
9
|
+
id: PropTypes.string,
|
|
10
|
+
/** Short headline above the quote. */
|
|
11
|
+
title: PropTypes.string,
|
|
12
|
+
quote: PropTypes.string,
|
|
13
|
+
authorName: PropTypes.string.isRequired,
|
|
14
|
+
/** e.g. "6 Months 2 weeks ago". */
|
|
15
|
+
dateLabel: PropTypes.string,
|
|
16
|
+
/** Star rating 1–5. */
|
|
17
|
+
rating: PropTypes.number,
|
|
18
|
+
avatar: imageShape,
|
|
19
|
+
/** Card width variant — "wide" or "narrow". */
|
|
20
|
+
variant: PropTypes.oneOf(["wide", "narrow"]),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const testimonial04PropTypes = {
|
|
24
|
+
/** Section headline. */
|
|
25
|
+
headline: PropTypes.string,
|
|
26
|
+
/** Testimonial cards for the carousel. */
|
|
27
|
+
testimonials: PropTypes.arrayOf(testimonialItemShape),
|
|
28
|
+
/** Controlled active slide index. */
|
|
29
|
+
activeSlide: PropTypes.number,
|
|
30
|
+
/** Called when the active slide changes. */
|
|
31
|
+
onSlideChange: PropTypes.func,
|
|
32
|
+
className: PropTypes.string,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const testimonial04DefaultProps = {
|
|
36
|
+
headline: "What Our Travelers Say",
|
|
37
|
+
testimonials: [
|
|
38
|
+
{
|
|
39
|
+
id: "carlos-tokyo",
|
|
40
|
+
title: "The Best Way to See Tokyo",
|
|
41
|
+
quote:
|
|
42
|
+
"The Hokkaido nature tour was breathtaking. Our guide knew everything about the area and made the experience so special.",
|
|
43
|
+
authorName: "Carlos P. Traveled to Tokyo",
|
|
44
|
+
dateLabel: "6 Months 2 weeks ago",
|
|
45
|
+
rating: 5,
|
|
46
|
+
variant: "wide",
|
|
47
|
+
avatar: {
|
|
48
|
+
src: "/testimonials/testimonial04/avatar-carlos.jpg",
|
|
49
|
+
alt: "Carlos P.",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "sabbir-hiroshima",
|
|
54
|
+
title: "Amazing guide! We saw hidden spots we'd never find ourselves.",
|
|
55
|
+
quote: "",
|
|
56
|
+
authorName: "Sabbir H. Traveled to Hiroshima",
|
|
57
|
+
dateLabel: "2 weeks ago",
|
|
58
|
+
rating: 5,
|
|
59
|
+
variant: "narrow",
|
|
60
|
+
avatar: {
|
|
61
|
+
src: "/testimonials/testimonial04/avatar-sabbir.jpg",
|
|
62
|
+
alt: "Sabbir H.",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "john-tokyo",
|
|
67
|
+
title: 'A Truly Unforgettable Experience!"',
|
|
68
|
+
quote:
|
|
69
|
+
"Our guide took us to places we never would have found. From hidden street food spots to quiet temples Highly recommended!",
|
|
70
|
+
authorName: "John K. Traveled to Tokyo",
|
|
71
|
+
dateLabel: "6 Months 2 weeks ago",
|
|
72
|
+
rating: 5,
|
|
73
|
+
variant: "wide",
|
|
74
|
+
avatar: {
|
|
75
|
+
src: "/testimonials/testimonial04/avatar-john.jpg",
|
|
76
|
+
alt: "John K.",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
activeSlide: 0,
|
|
81
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Testimonial04, default } from "./Testimonial04";
|