@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,171 @@
|
|
|
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
|
+
testimonial01DefaultProps,
|
|
9
|
+
testimonial01PropTypes,
|
|
10
|
+
} from "./Testimonial01.propTypes";
|
|
11
|
+
|
|
12
|
+
function getCardsPerView() {
|
|
13
|
+
if (typeof window === "undefined") return 3;
|
|
14
|
+
if (window.matchMedia("(min-width: 1024px)").matches) return 3;
|
|
15
|
+
if (window.matchMedia("(min-width: 640px)").matches) return 2;
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Testimonial01 — PASHA Group client quotes with company logos and progress bar.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} props - See Testimonial01.propTypes.js.
|
|
23
|
+
*/
|
|
24
|
+
export function Testimonial01({
|
|
25
|
+
eyebrow = testimonial01DefaultProps.eyebrow,
|
|
26
|
+
headline = testimonial01DefaultProps.headline,
|
|
27
|
+
testimonials = testimonial01DefaultProps.testimonials,
|
|
28
|
+
activeSlide: activeSlideProp = testimonial01DefaultProps.activeSlide,
|
|
29
|
+
onSlideChange,
|
|
30
|
+
className = "",
|
|
31
|
+
}) {
|
|
32
|
+
const [perView, setPerView] = useState(3);
|
|
33
|
+
const maxIndex = Math.max(0, testimonials.length - perView);
|
|
34
|
+
const { activeSlide, goTo } = useCarousel({
|
|
35
|
+
count: maxIndex + 1,
|
|
36
|
+
initialIndex: activeSlideProp,
|
|
37
|
+
onChange: onSlideChange,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const update = () => setPerView(getCardsPerView());
|
|
42
|
+
update();
|
|
43
|
+
window.addEventListener("resize", update);
|
|
44
|
+
return () => window.removeEventListener("resize", update);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
goTo(Math.min(activeSlideProp, maxIndex));
|
|
49
|
+
}, [activeSlideProp, goTo, maxIndex]);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (activeSlide > maxIndex) goTo(maxIndex);
|
|
53
|
+
}, [activeSlide, goTo, maxIndex]);
|
|
54
|
+
|
|
55
|
+
const stepPercent = 100 / perView;
|
|
56
|
+
const pageCount = maxIndex + 1;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<section
|
|
60
|
+
className={[
|
|
61
|
+
"relative w-full overflow-hidden bg-[#f9f9f9]",
|
|
62
|
+
"px-4 py-12 sm:px-6 sm:py-16 md:px-10 lg:px-20 lg:py-[120px] xl:px-[80px]",
|
|
63
|
+
className,
|
|
64
|
+
]
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.join(" ")}
|
|
67
|
+
data-testimonial="testimonial01"
|
|
68
|
+
>
|
|
69
|
+
<div className="mx-auto flex w-full max-w-7xl flex-col gap-10 sm:gap-14 lg:gap-[60px]">
|
|
70
|
+
<header className="flex flex-col gap-2.5 uppercase">
|
|
71
|
+
<p className="font-mono text-sm font-semibold leading-6 text-black sm:text-base">
|
|
72
|
+
{eyebrow}
|
|
73
|
+
</p>
|
|
74
|
+
<h2 className="text-3xl font-medium leading-tight tracking-[-0.02em] text-black sm:text-4xl lg:text-[48px] lg:leading-[54px]">
|
|
75
|
+
{headline}
|
|
76
|
+
</h2>
|
|
77
|
+
</header>
|
|
78
|
+
|
|
79
|
+
<div className="overflow-hidden">
|
|
80
|
+
<div
|
|
81
|
+
className="flex gap-1 transition-transform duration-500 ease-out motion-reduce:transition-none"
|
|
82
|
+
style={{ transform: `translateX(-${activeSlide * stepPercent}%)` }}
|
|
83
|
+
>
|
|
84
|
+
{testimonials.map((item) => (
|
|
85
|
+
<article
|
|
86
|
+
key={item.id ?? item.authorName}
|
|
87
|
+
className="flex w-full shrink-0 flex-col justify-between bg-white p-6 sm:w-1/2 sm:p-8 lg:min-h-[610px] lg:w-1/3 lg:p-10"
|
|
88
|
+
>
|
|
89
|
+
<div className="flex flex-col gap-10 sm:gap-16 lg:gap-20">
|
|
90
|
+
{item.companyLogo?.src ? (
|
|
91
|
+
<div className="relative h-16 w-[178px] shrink-0">
|
|
92
|
+
<SafeImage
|
|
93
|
+
src={item.companyLogo.src}
|
|
94
|
+
alt={item.companyLogo.alt ?? ""}
|
|
95
|
+
fill
|
|
96
|
+
className="object-contain object-left object-bottom"
|
|
97
|
+
sizes="178px"
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
) : null}
|
|
101
|
+
<blockquote className="text-xl leading-[1.4] text-black sm:text-2xl lg:text-[28px]">
|
|
102
|
+
“{item.quote}”
|
|
103
|
+
</blockquote>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div className="mt-8 flex items-center gap-[15px]">
|
|
107
|
+
{item.avatar?.src ? (
|
|
108
|
+
<div className="relative size-14 shrink-0 overflow-hidden rounded-full border border-black/10">
|
|
109
|
+
<SafeImage
|
|
110
|
+
src={item.avatar.src}
|
|
111
|
+
alt={item.avatar.alt ?? item.authorName}
|
|
112
|
+
fill
|
|
113
|
+
className="object-cover object-center"
|
|
114
|
+
sizes="56px"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
) : null}
|
|
118
|
+
<div className="min-w-0">
|
|
119
|
+
<p className="text-lg leading-tight text-black sm:text-2xl">
|
|
120
|
+
{item.authorName}
|
|
121
|
+
</p>
|
|
122
|
+
{item.authorCompany ? (
|
|
123
|
+
<p className="mt-1 text-sm leading-snug text-black/60">
|
|
124
|
+
{item.authorCompany}
|
|
125
|
+
</p>
|
|
126
|
+
) : null}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</article>
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{pageCount > 1 ? (
|
|
135
|
+
<div
|
|
136
|
+
className="relative h-px w-full bg-black/10"
|
|
137
|
+
role="tablist"
|
|
138
|
+
aria-label="Testimonial slides"
|
|
139
|
+
>
|
|
140
|
+
<div
|
|
141
|
+
className="absolute left-0 top-0 h-full bg-black transition-all duration-500 ease-out motion-reduce:transition-none"
|
|
142
|
+
style={{
|
|
143
|
+
width: `${100 / pageCount}%`,
|
|
144
|
+
transform: `translateX(${activeSlide * 100}%)`,
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
{Array.from({ length: pageCount }).map((_, index) => (
|
|
148
|
+
<button
|
|
149
|
+
key={index}
|
|
150
|
+
type="button"
|
|
151
|
+
role="tab"
|
|
152
|
+
aria-selected={index === activeSlide}
|
|
153
|
+
aria-label={`Go to slide ${index + 1}`}
|
|
154
|
+
onClick={() => goTo(index)}
|
|
155
|
+
className="absolute top-0 h-full transition-opacity duration-200 hover:opacity-70 focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
156
|
+
style={{
|
|
157
|
+
left: `${(index / pageCount) * 100}%`,
|
|
158
|
+
width: `${100 / pageCount}%`,
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
))}
|
|
162
|
+
</div>
|
|
163
|
+
) : null}
|
|
164
|
+
</div>
|
|
165
|
+
</section>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
Testimonial01.propTypes = testimonial01PropTypes;
|
|
170
|
+
|
|
171
|
+
export default Testimonial01;
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
authorCompany: PropTypes.string,
|
|
13
|
+
avatar: imageShape,
|
|
14
|
+
companyLogo: imageShape,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const testimonial01PropTypes = {
|
|
18
|
+
/** Small uppercase label above the headline. */
|
|
19
|
+
eyebrow: PropTypes.string,
|
|
20
|
+
/** Section headline. */
|
|
21
|
+
headline: PropTypes.string,
|
|
22
|
+
/** Testimonial cards for the carousel. */
|
|
23
|
+
testimonials: PropTypes.arrayOf(testimonialItemShape),
|
|
24
|
+
/** Controlled active slide index. */
|
|
25
|
+
activeSlide: PropTypes.number,
|
|
26
|
+
/** Called when the active slide changes. */
|
|
27
|
+
onSlideChange: PropTypes.func,
|
|
28
|
+
className: PropTypes.string,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const testimonial01DefaultProps = {
|
|
32
|
+
eyebrow: "Testimonial",
|
|
33
|
+
headline: "What Our Clients Say",
|
|
34
|
+
testimonials: [
|
|
35
|
+
{
|
|
36
|
+
id: "marcus-boyer",
|
|
37
|
+
quote:
|
|
38
|
+
"PASHA Group's electrical solutions deliver exactly what they promise — durable, reliable, and built to industrial standards. Their team has always been responsive and supportive from inquiry to delivery.",
|
|
39
|
+
authorName: "Marcus Boyer",
|
|
40
|
+
authorCompany: "Dhaka Electric Supply Company Limited (DESCO)",
|
|
41
|
+
avatar: {
|
|
42
|
+
src: "/testimonials/testimonial01/avatar-marcus.jpg",
|
|
43
|
+
alt: "Marcus Boyer",
|
|
44
|
+
},
|
|
45
|
+
companyLogo: {
|
|
46
|
+
src: "/testimonials/testimonial01/desco-logo.png",
|
|
47
|
+
alt: "DESCO logo",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "wilma-parker",
|
|
52
|
+
quote:
|
|
53
|
+
"We have partnered with PASHA Group on several infrastructure projects. Their products and service quality consistently exceed our expectations and help us meet project deadlines.",
|
|
54
|
+
authorName: "Wilma Parker",
|
|
55
|
+
authorCompany: "Dhaka Electric Supply Company Limited (DESCO)",
|
|
56
|
+
avatar: {
|
|
57
|
+
src: "/testimonials/testimonial01/avatar-wilma.jpg",
|
|
58
|
+
alt: "Wilma Parker",
|
|
59
|
+
},
|
|
60
|
+
companyLogo: {
|
|
61
|
+
src: "/testimonials/testimonial01/company-logo-icon.png",
|
|
62
|
+
alt: "DESCO icon",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "brooke-schmidt",
|
|
67
|
+
quote:
|
|
68
|
+
"PASHA Group's electrical solutions deliver exactly what they promise — durable, reliable, and built to industrial standards. Their team has always been responsive and supportive from inquiry to delivery.",
|
|
69
|
+
authorName: "Ms. Brooke Schmidt",
|
|
70
|
+
authorCompany: "Dhaka Electric Supply Company Limited (DESCO)",
|
|
71
|
+
avatar: {
|
|
72
|
+
src: "/testimonials/testimonial01/avatar-marcus.jpg",
|
|
73
|
+
alt: "Ms. Brooke Schmidt",
|
|
74
|
+
},
|
|
75
|
+
companyLogo: {
|
|
76
|
+
src: "/testimonials/testimonial01/desco-logo.png",
|
|
77
|
+
alt: "DESCO logo",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
activeSlide: 0,
|
|
82
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Testimonial01, default } from "./Testimonial01";
|
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
testimonial02DefaultProps,
|
|
9
|
+
testimonial02PropTypes,
|
|
10
|
+
} from "./Testimonial02.propTypes";
|
|
11
|
+
|
|
12
|
+
function getCardsPerView() {
|
|
13
|
+
if (typeof window === "undefined") return 3;
|
|
14
|
+
if (window.matchMedia("(min-width: 1024px)").matches) return 3;
|
|
15
|
+
if (window.matchMedia("(min-width: 640px)").matches) return 2;
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ChevronIcon({ className = "" }) {
|
|
20
|
+
return (
|
|
21
|
+
<svg
|
|
22
|
+
viewBox="0 0 24 24"
|
|
23
|
+
fill="none"
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
className={className}
|
|
26
|
+
>
|
|
27
|
+
<path
|
|
28
|
+
d="M9 6l6 6-6 6"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
strokeWidth="1.5"
|
|
31
|
+
strokeLinecap="round"
|
|
32
|
+
strokeLinejoin="round"
|
|
33
|
+
/>
|
|
34
|
+
</svg>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function NavButton({ direction, onClick, disabled }) {
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={onClick}
|
|
43
|
+
disabled={disabled}
|
|
44
|
+
aria-label={direction === "prev" ? "Previous testimonial" : "Next testimonial"}
|
|
45
|
+
className={[
|
|
46
|
+
"flex items-center justify-center rounded-full border border-[#343744] px-8 py-3",
|
|
47
|
+
"transition-opacity duration-200 ease-out hover:opacity-80",
|
|
48
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
49
|
+
"disabled:cursor-not-allowed disabled:opacity-40",
|
|
50
|
+
direction === "prev" ? "opacity-70" : "",
|
|
51
|
+
].join(" ")}
|
|
52
|
+
>
|
|
53
|
+
<ChevronIcon
|
|
54
|
+
className={[
|
|
55
|
+
"size-6 text-white",
|
|
56
|
+
direction === "prev" ? "rotate-180" : "",
|
|
57
|
+
].join(" ")}
|
|
58
|
+
/>
|
|
59
|
+
</button>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Testimonial02 — Platform One dark hero with light quote cards and arrow navigation.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} props - See Testimonial02.propTypes.js.
|
|
67
|
+
*/
|
|
68
|
+
export function Testimonial02({
|
|
69
|
+
eyebrow = testimonial02DefaultProps.eyebrow,
|
|
70
|
+
headline = testimonial02DefaultProps.headline,
|
|
71
|
+
backgroundImage = testimonial02DefaultProps.backgroundImage,
|
|
72
|
+
testimonials = testimonial02DefaultProps.testimonials,
|
|
73
|
+
activeSlide: activeSlideProp = testimonial02DefaultProps.activeSlide,
|
|
74
|
+
onSlideChange,
|
|
75
|
+
className = "",
|
|
76
|
+
}) {
|
|
77
|
+
const [perView, setPerView] = useState(3);
|
|
78
|
+
const maxIndex = Math.max(0, testimonials.length - perView);
|
|
79
|
+
const { activeSlide, goTo, next, prev } = useCarousel({
|
|
80
|
+
count: maxIndex + 1,
|
|
81
|
+
initialIndex: activeSlideProp,
|
|
82
|
+
onChange: onSlideChange,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const update = () => setPerView(getCardsPerView());
|
|
87
|
+
update();
|
|
88
|
+
window.addEventListener("resize", update);
|
|
89
|
+
return () => window.removeEventListener("resize", update);
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
goTo(Math.min(activeSlideProp, maxIndex));
|
|
94
|
+
}, [activeSlideProp, goTo, maxIndex]);
|
|
95
|
+
|
|
96
|
+
const stepPercent = 100 / perView;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<section
|
|
100
|
+
className={[
|
|
101
|
+
"relative w-full overflow-hidden bg-[#020617]",
|
|
102
|
+
"px-4 py-16 sm:px-6 sm:py-20 md:px-10 lg:px-20 lg:py-[160px] xl:px-[80px]",
|
|
103
|
+
className,
|
|
104
|
+
]
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.join(" ")}
|
|
107
|
+
data-testimonial="testimonial02"
|
|
108
|
+
>
|
|
109
|
+
{backgroundImage?.src ? (
|
|
110
|
+
<div
|
|
111
|
+
className="pointer-events-none absolute inset-0 overflow-hidden"
|
|
112
|
+
aria-hidden="true"
|
|
113
|
+
>
|
|
114
|
+
<SafeImage
|
|
115
|
+
src={backgroundImage.src}
|
|
116
|
+
alt=""
|
|
117
|
+
fill
|
|
118
|
+
className="object-cover object-center opacity-80"
|
|
119
|
+
sizes="100vw"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
) : null}
|
|
123
|
+
|
|
124
|
+
<div className="relative mx-auto flex w-full max-w-[1600px] flex-col gap-12 lg:gap-[120px]">
|
|
125
|
+
<header className="max-w-3xl">
|
|
126
|
+
<p className="text-lg leading-[1.3] text-white sm:text-xl">
|
|
127
|
+
{eyebrow}
|
|
128
|
+
</p>
|
|
129
|
+
<h2 className="mt-2.5 text-3xl leading-[1.1] tracking-[-0.02em] text-white sm:text-4xl md:text-5xl lg:text-[70px]">
|
|
130
|
+
{headline}
|
|
131
|
+
</h2>
|
|
132
|
+
</header>
|
|
133
|
+
|
|
134
|
+
<div className="flex flex-col gap-10 lg:gap-[90px]">
|
|
135
|
+
<div className="overflow-hidden">
|
|
136
|
+
<div
|
|
137
|
+
className="flex gap-4 transition-transform duration-500 ease-out motion-reduce:transition-none"
|
|
138
|
+
style={{ transform: `translateX(-${activeSlide * stepPercent}%)` }}
|
|
139
|
+
>
|
|
140
|
+
{testimonials.map((item) => (
|
|
141
|
+
<article
|
|
142
|
+
key={item.id ?? item.authorName}
|
|
143
|
+
className={[
|
|
144
|
+
"flex w-full shrink-0 flex-col justify-between rounded-2xl bg-[#f9fcff] p-6 sm:w-[calc(50%-8px)] sm:p-8",
|
|
145
|
+
"lg:h-[436px] lg:w-[523px] lg:max-w-[523px] lg:p-10",
|
|
146
|
+
].join(" ")}
|
|
147
|
+
>
|
|
148
|
+
<blockquote className="text-lg leading-[1.2] text-[#020617] sm:text-xl lg:text-2xl">
|
|
149
|
+
“{item.quote}”
|
|
150
|
+
</blockquote>
|
|
151
|
+
|
|
152
|
+
<div className="mt-8 flex items-center gap-5">
|
|
153
|
+
{item.avatar?.src ? (
|
|
154
|
+
<div className="relative size-24 shrink-0 overflow-hidden rounded-full sm:size-32 lg:size-[160px]">
|
|
155
|
+
<SafeImage
|
|
156
|
+
src={item.avatar.src}
|
|
157
|
+
alt={item.avatar.alt ?? item.authorName}
|
|
158
|
+
fill
|
|
159
|
+
className="object-cover object-center transition-transform duration-300 ease-out motion-reduce:transition-none"
|
|
160
|
+
sizes="160px"
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
) : null}
|
|
164
|
+
<div className="min-w-0">
|
|
165
|
+
<p className="text-lg font-medium leading-[1.2] tracking-[-0.02em] text-[#020617] sm:text-xl lg:text-2xl">
|
|
166
|
+
{item.authorName}
|
|
167
|
+
</p>
|
|
168
|
+
{item.authorRole ? (
|
|
169
|
+
<p className="mt-2 text-sm leading-[1.2] tracking-wide text-[#343744] sm:text-base">
|
|
170
|
+
{item.authorRole}
|
|
171
|
+
</p>
|
|
172
|
+
) : null}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</article>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div className="flex items-center justify-end gap-2">
|
|
181
|
+
<NavButton
|
|
182
|
+
direction="prev"
|
|
183
|
+
onClick={prev}
|
|
184
|
+
disabled={activeSlide === 0}
|
|
185
|
+
/>
|
|
186
|
+
<NavButton
|
|
187
|
+
direction="next"
|
|
188
|
+
onClick={next}
|
|
189
|
+
disabled={activeSlide >= maxIndex}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</section>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
Testimonial02.propTypes = testimonial02PropTypes;
|
|
199
|
+
|
|
200
|
+
export default Testimonial02;
|
|
@@ -0,0 +1,75 @@
|
|
|
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 testimonial02PropTypes = {
|
|
17
|
+
/** Small label above the headline. */
|
|
18
|
+
eyebrow: PropTypes.string,
|
|
19
|
+
/** Section headline. */
|
|
20
|
+
headline: PropTypes.string,
|
|
21
|
+
/** Decorative background image behind the header area. */
|
|
22
|
+
backgroundImage: imageShape,
|
|
23
|
+
/** Testimonial cards for the carousel. */
|
|
24
|
+
testimonials: PropTypes.arrayOf(testimonialItemShape),
|
|
25
|
+
/** Controlled active slide index. */
|
|
26
|
+
activeSlide: PropTypes.number,
|
|
27
|
+
/** Called when the active slide changes. */
|
|
28
|
+
onSlideChange: PropTypes.func,
|
|
29
|
+
className: PropTypes.string,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const testimonial02DefaultProps = {
|
|
33
|
+
eyebrow: "Testimonials",
|
|
34
|
+
headline: "What Experience-Led Growth Looks Like",
|
|
35
|
+
backgroundImage: {
|
|
36
|
+
src: "/testimonials/testimonial02/bg-gradient.png",
|
|
37
|
+
alt: "",
|
|
38
|
+
},
|
|
39
|
+
testimonials: [
|
|
40
|
+
{
|
|
41
|
+
id: "marie-towne",
|
|
42
|
+
quote:
|
|
43
|
+
"What stood out was the partnership. The Platform One team didn't just sell us software—they co-designed our program and stayed with us throughout.",
|
|
44
|
+
authorName: "Marie Towne",
|
|
45
|
+
authorRole: "CX Lead, EcoPower",
|
|
46
|
+
avatar: {
|
|
47
|
+
src: "/testimonials/testimonial02/avatar-marie.jpg",
|
|
48
|
+
alt: "Marie Towne",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "julia-tan",
|
|
53
|
+
quote:
|
|
54
|
+
"Platform One helped us build an always-on CX program that actually works. We went from scattered feedback to real-time visibility—across every touchpoint.",
|
|
55
|
+
authorName: "Julia Tan",
|
|
56
|
+
authorRole: "Head of Customer Strategy, RetailCo",
|
|
57
|
+
avatar: {
|
|
58
|
+
src: "/testimonials/testimonial02/avatar-julia.jpg",
|
|
59
|
+
alt: "Julia Tan",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "mark-evans",
|
|
64
|
+
quote:
|
|
65
|
+
"The ability to connect employee and customer experience on one platform gave us clarity we didn't know we needed.",
|
|
66
|
+
authorName: "Mark Evans",
|
|
67
|
+
authorRole: "Director of Experience, Axis Finance",
|
|
68
|
+
avatar: {
|
|
69
|
+
src: "/testimonials/testimonial02/avatar-mark.jpg",
|
|
70
|
+
alt: "Mark Evans",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
activeSlide: 0,
|
|
75
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Testimonial02, default } from "./Testimonial02";
|