@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,271 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import SafeImage from "../../atoms/SafeImage";
|
|
5
|
+
import { useCarousel } from "../../hooks/useCarousel";
|
|
6
|
+
import {
|
|
7
|
+
testimonial12DefaultProps,
|
|
8
|
+
testimonial12PropTypes,
|
|
9
|
+
} from "./Testimonial12.propTypes";
|
|
10
|
+
|
|
11
|
+
function ChevronIcon({ direction = "right", className = "" }) {
|
|
12
|
+
return (
|
|
13
|
+
<svg
|
|
14
|
+
viewBox="0 0 24 24"
|
|
15
|
+
fill="none"
|
|
16
|
+
aria-hidden="true"
|
|
17
|
+
className={className}
|
|
18
|
+
>
|
|
19
|
+
<path
|
|
20
|
+
d={direction === "right" ? "M9 6l6 6-6 6" : "M15 6l-6 6 6 6"}
|
|
21
|
+
stroke="currentColor"
|
|
22
|
+
strokeWidth="1.5"
|
|
23
|
+
strokeLinecap="round"
|
|
24
|
+
strokeLinejoin="round"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function PlayIcon({ active = false, className = "" }) {
|
|
31
|
+
return (
|
|
32
|
+
<svg
|
|
33
|
+
viewBox="0 0 62 62"
|
|
34
|
+
fill="none"
|
|
35
|
+
aria-hidden="true"
|
|
36
|
+
className={className}
|
|
37
|
+
>
|
|
38
|
+
<circle
|
|
39
|
+
cx="31"
|
|
40
|
+
cy="31"
|
|
41
|
+
r="31"
|
|
42
|
+
fill={active ? "#0860A8" : "rgba(255,255,255,0.35)"}
|
|
43
|
+
className="transition-colors duration-200 ease-out"
|
|
44
|
+
/>
|
|
45
|
+
<path
|
|
46
|
+
d="M26 20l18 11-18 11V20z"
|
|
47
|
+
fill="white"
|
|
48
|
+
/>
|
|
49
|
+
</svg>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Testimonial12 — Dark video-story carousel with play buttons and CTA.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} props - See Testimonial12.propTypes.js.
|
|
57
|
+
*/
|
|
58
|
+
export function Testimonial12({
|
|
59
|
+
headline = testimonial12DefaultProps.headline,
|
|
60
|
+
subtitle = testimonial12DefaultProps.subtitle,
|
|
61
|
+
stories = testimonial12DefaultProps.stories,
|
|
62
|
+
activeSlide: controlledSlide,
|
|
63
|
+
onSlideChange,
|
|
64
|
+
ctaText = testimonial12DefaultProps.ctaText,
|
|
65
|
+
ctaHref = testimonial12DefaultProps.ctaHref,
|
|
66
|
+
onCtaClick,
|
|
67
|
+
onStoryPlay,
|
|
68
|
+
className = "",
|
|
69
|
+
}) {
|
|
70
|
+
const isControlled = controlledSlide !== undefined;
|
|
71
|
+
const [hoveredStory, setHoveredStory] = useState(null);
|
|
72
|
+
const [slidesPerView, setSlidesPerView] = useState(1);
|
|
73
|
+
|
|
74
|
+
const handleChange = useCallback(
|
|
75
|
+
(index) => {
|
|
76
|
+
onSlideChange?.(index);
|
|
77
|
+
},
|
|
78
|
+
[onSlideChange]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const slideCount = Math.max(1, stories.length - slidesPerView + 1);
|
|
82
|
+
const maxSlide = slideCount - 1;
|
|
83
|
+
|
|
84
|
+
const {
|
|
85
|
+
activeSlide: carouselSlide,
|
|
86
|
+
next,
|
|
87
|
+
prev,
|
|
88
|
+
pause,
|
|
89
|
+
resume,
|
|
90
|
+
goTo,
|
|
91
|
+
} = useCarousel({
|
|
92
|
+
count: slideCount,
|
|
93
|
+
initialIndex: isControlled ? controlledSlide : 0,
|
|
94
|
+
loop: false,
|
|
95
|
+
onChange: handleChange,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const activeSlide = isControlled ? controlledSlide : carouselSlide;
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const updateSlidesPerView = () => {
|
|
102
|
+
const width = window.innerWidth;
|
|
103
|
+
if (width >= 1280) setSlidesPerView(4);
|
|
104
|
+
else if (width >= 1024) setSlidesPerView(3);
|
|
105
|
+
else if (width >= 640) setSlidesPerView(2);
|
|
106
|
+
else setSlidesPerView(1);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
updateSlidesPerView();
|
|
110
|
+
window.addEventListener("resize", updateSlidesPerView);
|
|
111
|
+
return () => window.removeEventListener("resize", updateSlidesPerView);
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (isControlled) goTo(controlledSlide);
|
|
116
|
+
}, [controlledSlide, goTo, isControlled]);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (carouselSlide > maxSlide) goTo(maxSlide);
|
|
120
|
+
}, [carouselSlide, goTo, maxSlide]);
|
|
121
|
+
|
|
122
|
+
const CtaTag = ctaHref ? "a" : "button";
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<section
|
|
126
|
+
className={[
|
|
127
|
+
"relative w-full overflow-hidden bg-black",
|
|
128
|
+
"px-4 py-16 sm:px-6 sm:py-20 md:px-10 lg:px-20 lg:py-24 xl:px-[80px]",
|
|
129
|
+
className,
|
|
130
|
+
]
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join(" ")}
|
|
133
|
+
data-testimonial="testimonial12"
|
|
134
|
+
onMouseEnter={pause}
|
|
135
|
+
onMouseLeave={resume}
|
|
136
|
+
>
|
|
137
|
+
<div className="mx-auto flex w-full max-w-7xl flex-col gap-10 lg:gap-12">
|
|
138
|
+
<header className="mx-auto flex max-w-2xl flex-col items-center gap-4 text-center text-white sm:gap-5">
|
|
139
|
+
<h2 className="text-2xl font-medium leading-tight sm:text-3xl md:text-4xl lg:text-[40px] lg:leading-[48px]">
|
|
140
|
+
{headline}
|
|
141
|
+
</h2>
|
|
142
|
+
{subtitle ? (
|
|
143
|
+
<p className="text-sm leading-relaxed sm:text-base lg:leading-[26px]">
|
|
144
|
+
{subtitle}
|
|
145
|
+
</p>
|
|
146
|
+
) : null}
|
|
147
|
+
</header>
|
|
148
|
+
|
|
149
|
+
<div className="relative">
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
aria-label="Previous story"
|
|
153
|
+
onClick={prev}
|
|
154
|
+
disabled={activeSlide === 0}
|
|
155
|
+
className="absolute left-0 top-1/2 z-10 hidden -translate-y-1/2 text-white/20 transition-colors duration-200 ease-out hover:text-white/60 focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-20 sm:flex"
|
|
156
|
+
>
|
|
157
|
+
<ChevronIcon direction="left" className="size-10" />
|
|
158
|
+
</button>
|
|
159
|
+
|
|
160
|
+
<div className="overflow-hidden px-0 sm:px-10 lg:px-12">
|
|
161
|
+
<div
|
|
162
|
+
className="flex gap-6 transition-transform duration-500 ease-out motion-reduce:transition-none lg:gap-[30px]"
|
|
163
|
+
style={{
|
|
164
|
+
transform: `translateX(-${activeSlide * (100 / slidesPerView)}%)`,
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
{stories.map((story, index) => {
|
|
168
|
+
const isActive =
|
|
169
|
+
hoveredStory === story.id ||
|
|
170
|
+
(hoveredStory === null && index === activeSlide);
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<article
|
|
174
|
+
key={story.id ?? story.description}
|
|
175
|
+
className="flex shrink-0 flex-col gap-5 sm:gap-6"
|
|
176
|
+
style={{ width: `calc(${100 / slidesPerView}% - ${slidesPerView > 1 ? "18px" : "0px"})` }}
|
|
177
|
+
onMouseEnter={() => setHoveredStory(story.id ?? String(index))}
|
|
178
|
+
onMouseLeave={() => setHoveredStory(null)}
|
|
179
|
+
onFocus={() => setHoveredStory(story.id ?? String(index))}
|
|
180
|
+
onBlur={() => setHoveredStory(null)}
|
|
181
|
+
>
|
|
182
|
+
<div className="group relative aspect-[300/350] w-full overflow-hidden">
|
|
183
|
+
{story.thumbnail ? (
|
|
184
|
+
<SafeImage
|
|
185
|
+
src={story.thumbnail.src}
|
|
186
|
+
alt={story.thumbnail.alt ?? ""}
|
|
187
|
+
fill
|
|
188
|
+
className="object-cover object-center transition-transform duration-300 ease-out motion-safe:group-hover:scale-[1.02] motion-reduce:transition-none"
|
|
189
|
+
sizes="(max-width:640px) 85vw, 300px"
|
|
190
|
+
/>
|
|
191
|
+
) : null}
|
|
192
|
+
|
|
193
|
+
<button
|
|
194
|
+
type="button"
|
|
195
|
+
aria-label={`Play story ${index + 1}`}
|
|
196
|
+
onClick={() => onStoryPlay?.(story, index)}
|
|
197
|
+
className="absolute bottom-5 left-5 transition-transform duration-200 ease-out hover:scale-105 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white motion-reduce:transition-none"
|
|
198
|
+
>
|
|
199
|
+
<PlayIcon
|
|
200
|
+
active={isActive}
|
|
201
|
+
className="size-12 sm:size-14 lg:size-[62px]"
|
|
202
|
+
/>
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<p
|
|
207
|
+
className={[
|
|
208
|
+
"text-base font-medium leading-relaxed text-white/80 sm:text-lg lg:text-xl lg:leading-[26px]",
|
|
209
|
+
"transition-colors duration-200 ease-out",
|
|
210
|
+
isActive ? "text-white underline decoration-solid underline-offset-4" : "",
|
|
211
|
+
].join(" ")}
|
|
212
|
+
>
|
|
213
|
+
{story.description}
|
|
214
|
+
</p>
|
|
215
|
+
</article>
|
|
216
|
+
);
|
|
217
|
+
})}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
aria-label="Next story"
|
|
224
|
+
onClick={next}
|
|
225
|
+
disabled={activeSlide >= maxSlide}
|
|
226
|
+
className="absolute right-0 top-1/2 z-10 hidden -translate-y-1/2 text-white transition-colors duration-200 ease-out hover:text-white/60 focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-20 sm:flex"
|
|
227
|
+
>
|
|
228
|
+
<ChevronIcon direction="right" className="size-10" />
|
|
229
|
+
</button>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div className="flex flex-col items-center gap-6">
|
|
233
|
+
{slideCount > 1 ? (
|
|
234
|
+
<div className="flex gap-2 sm:hidden">
|
|
235
|
+
{Array.from({ length: slideCount }).map((_, index) => (
|
|
236
|
+
<button
|
|
237
|
+
key={index}
|
|
238
|
+
type="button"
|
|
239
|
+
aria-label={`Go to slide ${index + 1}`}
|
|
240
|
+
aria-current={activeSlide === index ? "true" : undefined}
|
|
241
|
+
onClick={() => goTo(index)}
|
|
242
|
+
className={[
|
|
243
|
+
"h-1.5 rounded-full transition-all duration-300 ease-out motion-reduce:transition-none",
|
|
244
|
+
activeSlide === index
|
|
245
|
+
? "w-6 bg-white"
|
|
246
|
+
: "w-1.5 bg-white/30 hover:bg-white/50",
|
|
247
|
+
].join(" ")}
|
|
248
|
+
/>
|
|
249
|
+
))}
|
|
250
|
+
</div>
|
|
251
|
+
) : null}
|
|
252
|
+
|
|
253
|
+
{ctaText ? (
|
|
254
|
+
<CtaTag
|
|
255
|
+
{...(ctaHref
|
|
256
|
+
? { href: ctaHref }
|
|
257
|
+
: { type: "button", onClick: onCtaClick })}
|
|
258
|
+
className="inline-flex items-center justify-center border border-white px-6 py-3 text-sm font-semibold text-white shadow-[0_4px_10px_rgba(8,96,168,0.06)] transition-colors duration-200 ease-out hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-offset-2 sm:px-8 sm:py-[13px] sm:text-base"
|
|
259
|
+
>
|
|
260
|
+
{ctaText}
|
|
261
|
+
</CtaTag>
|
|
262
|
+
) : null}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</section>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
Testimonial12.propTypes = testimonial12PropTypes;
|
|
270
|
+
|
|
271
|
+
export default Testimonial12;
|
|
@@ -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 storyItemShape = PropTypes.shape({
|
|
9
|
+
id: PropTypes.string,
|
|
10
|
+
description: PropTypes.string.isRequired,
|
|
11
|
+
thumbnail: imageShape,
|
|
12
|
+
videoUrl: PropTypes.string,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const testimonial12PropTypes = {
|
|
16
|
+
/** Main section headline. */
|
|
17
|
+
headline: PropTypes.string,
|
|
18
|
+
/** Subtitle below the headline. */
|
|
19
|
+
subtitle: PropTypes.string,
|
|
20
|
+
/** Video story cards. */
|
|
21
|
+
stories: PropTypes.arrayOf(storyItemShape),
|
|
22
|
+
/** Controlled active slide index. */
|
|
23
|
+
activeSlide: PropTypes.number,
|
|
24
|
+
/** Called when the active slide changes. */
|
|
25
|
+
onSlideChange: PropTypes.func,
|
|
26
|
+
/** CTA button label. */
|
|
27
|
+
ctaText: PropTypes.string,
|
|
28
|
+
/** CTA href (renders an anchor when set). */
|
|
29
|
+
ctaHref: PropTypes.string,
|
|
30
|
+
/** CTA click handler (button mode when no href). */
|
|
31
|
+
onCtaClick: PropTypes.func,
|
|
32
|
+
/** Called when a story play button is clicked. */
|
|
33
|
+
onStoryPlay: PropTypes.func,
|
|
34
|
+
className: PropTypes.string,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const testimonial12DefaultProps = {
|
|
38
|
+
headline: "Story of the new beginnings",
|
|
39
|
+
subtitle: "Yes, we can!",
|
|
40
|
+
stories: [
|
|
41
|
+
{
|
|
42
|
+
id: "story-1",
|
|
43
|
+
description:
|
|
44
|
+
"Auctor quam dictumst nibh a sagis. Eu in enim risus augue non enim in tincidunt.",
|
|
45
|
+
thumbnail: {
|
|
46
|
+
src: "/testimonials/testimonial12/story-1.jpg",
|
|
47
|
+
alt: "Woman in wheelchair against red background",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "story-2",
|
|
52
|
+
description:
|
|
53
|
+
"Auctor quam dictumst nibh a sagis. Eu in enim risus augue non enim in tincidunt.",
|
|
54
|
+
thumbnail: {
|
|
55
|
+
src: "/testimonials/testimonial12/story-2.jpg",
|
|
56
|
+
alt: "Woman with glasses in purple shirt",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "story-3",
|
|
61
|
+
description:
|
|
62
|
+
"Auctor quam dictumst nibh a sagis. Eu in enim risus augue non enim in tincidunt.",
|
|
63
|
+
thumbnail: {
|
|
64
|
+
src: "/testimonials/testimonial12/story-3.jpg",
|
|
65
|
+
alt: "Woman in headscarf holding a mug",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "story-4",
|
|
70
|
+
description:
|
|
71
|
+
"Auctor quam dictumst nibh a sagis. Eu in enim risus augue non enim in tincidunt.",
|
|
72
|
+
thumbnail: {
|
|
73
|
+
src: "/testimonials/testimonial12/story-4.jpg",
|
|
74
|
+
alt: "Woman in blue gown holding a card",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
activeSlide: 0,
|
|
79
|
+
ctaText: "Read More Story",
|
|
80
|
+
ctaHref: "#",
|
|
81
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Testimonial12, default } from "./Testimonial12";
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
import SafeImage from "../../atoms/SafeImage";
|
|
6
|
+
import { useCarousel } from "../../hooks/useCarousel";
|
|
7
|
+
import {
|
|
8
|
+
testimonial13DefaultProps,
|
|
9
|
+
testimonial13PropTypes,
|
|
10
|
+
} from "./Testimonial13.propTypes";
|
|
11
|
+
|
|
12
|
+
function PlayIcon({ className = "" }) {
|
|
13
|
+
return (
|
|
14
|
+
<svg
|
|
15
|
+
viewBox="0 0 16 16"
|
|
16
|
+
fill="none"
|
|
17
|
+
aria-hidden="true"
|
|
18
|
+
className={className}
|
|
19
|
+
>
|
|
20
|
+
<path d="M4 2.5v11l9-5.5-9-5.5z" fill="currentColor" />
|
|
21
|
+
</svg>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Testimonial13 — Featured story carousel with preview cards and video play CTA.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} props - See Testimonial13.propTypes.js.
|
|
29
|
+
*/
|
|
30
|
+
export function Testimonial13({
|
|
31
|
+
headline = testimonial13DefaultProps.headline,
|
|
32
|
+
subtitle = testimonial13DefaultProps.subtitle,
|
|
33
|
+
slides = testimonial13DefaultProps.slides,
|
|
34
|
+
activeSlide: activeSlideProp = testimonial13DefaultProps.activeSlide,
|
|
35
|
+
onSlideChange,
|
|
36
|
+
ctaText = testimonial13DefaultProps.ctaText,
|
|
37
|
+
ctaHref = testimonial13DefaultProps.ctaHref,
|
|
38
|
+
onCtaClick,
|
|
39
|
+
onPlayClick,
|
|
40
|
+
className = "",
|
|
41
|
+
}) {
|
|
42
|
+
const isControlled = typeof activeSlideProp === "number";
|
|
43
|
+
const { activeSlide, goTo } = useCarousel({
|
|
44
|
+
count: slides.length,
|
|
45
|
+
initialIndex: activeSlideProp,
|
|
46
|
+
onChange: onSlideChange,
|
|
47
|
+
});
|
|
48
|
+
const current = isControlled ? activeSlideProp : activeSlide;
|
|
49
|
+
|
|
50
|
+
const setSlide = (index) => {
|
|
51
|
+
if (isControlled) {
|
|
52
|
+
onSlideChange?.(index);
|
|
53
|
+
} else {
|
|
54
|
+
goTo(index);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!isControlled) {
|
|
60
|
+
goTo(activeSlideProp);
|
|
61
|
+
}
|
|
62
|
+
}, [activeSlideProp, goTo, isControlled]);
|
|
63
|
+
|
|
64
|
+
const active = slides[current] ?? slides[0];
|
|
65
|
+
const previewSlides = slides
|
|
66
|
+
.map((slide, index) => ({ slide, index }))
|
|
67
|
+
.filter(({ index }) => index !== current)
|
|
68
|
+
.slice(0, 2);
|
|
69
|
+
|
|
70
|
+
const CtaTag = ctaHref ? "a" : "button";
|
|
71
|
+
const progress = slides.length > 0 ? ((current + 1) / slides.length) * 100 : 0;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<section
|
|
75
|
+
className={[
|
|
76
|
+
"relative w-full overflow-hidden bg-[#003035]",
|
|
77
|
+
"px-4 py-16 sm:px-6 sm:py-20 md:px-10 lg:px-20 lg:py-[140px] xl:px-[80px]",
|
|
78
|
+
className,
|
|
79
|
+
]
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.join(" ")}
|
|
82
|
+
data-testimonial="testimonial13"
|
|
83
|
+
>
|
|
84
|
+
<div className="mx-auto flex w-full max-w-7xl flex-col gap-12 sm:gap-16 lg:gap-20">
|
|
85
|
+
<header className="flex flex-col items-center gap-5 text-center text-white">
|
|
86
|
+
<h2 className="text-3xl leading-tight tracking-[-0.04em] sm:text-4xl md:text-5xl lg:text-[56px]">
|
|
87
|
+
{headline}
|
|
88
|
+
</h2>
|
|
89
|
+
{subtitle ? (
|
|
90
|
+
<p className="max-w-2xl text-base leading-relaxed sm:text-lg lg:text-xl">
|
|
91
|
+
{subtitle}
|
|
92
|
+
</p>
|
|
93
|
+
) : null}
|
|
94
|
+
</header>
|
|
95
|
+
|
|
96
|
+
<div className="flex flex-col gap-6 lg:flex-row lg:items-stretch lg:gap-2.5">
|
|
97
|
+
<article className="flex min-h-[420px] flex-1 flex-col overflow-hidden rounded-xl bg-[#003035] sm:min-h-[480px] lg:min-h-[535px] lg:flex-row">
|
|
98
|
+
<div className="relative aspect-[4/5] w-full shrink-0 sm:aspect-[3/4] lg:aspect-auto lg:h-auto lg:w-[35%]">
|
|
99
|
+
<SafeImage
|
|
100
|
+
src={active.image.src}
|
|
101
|
+
alt={active.image.alt ?? active.authorName}
|
|
102
|
+
fill
|
|
103
|
+
className="object-cover object-center transition-opacity duration-500 ease-out motion-reduce:transition-none"
|
|
104
|
+
sizes="(max-width:1024px) 100vw, 430px"
|
|
105
|
+
/>
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
aria-label="Play video testimonial"
|
|
109
|
+
onClick={() => onPlayClick?.(active, current)}
|
|
110
|
+
className="absolute bottom-4 right-4 flex size-12 items-center justify-center rounded-full bg-[#ff6900] text-white transition-colors duration-200 ease-out hover:bg-[#e55e00] focus-visible:outline-2 focus-visible:outline-offset-2 sm:bottom-6 sm:right-6 sm:size-16"
|
|
111
|
+
>
|
|
112
|
+
<PlayIcon className="size-4 sm:size-5" />
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div className="flex flex-1 flex-col justify-between gap-8 px-6 py-8 sm:px-8 sm:py-10 lg:px-10 lg:py-[30px]">
|
|
117
|
+
<blockquote className="text-2xl leading-snug text-white sm:text-3xl lg:text-[40px] lg:leading-[1.4]">
|
|
118
|
+
{active.quote}
|
|
119
|
+
</blockquote>
|
|
120
|
+
<div className="flex flex-col gap-1 text-center lg:items-start lg:text-left">
|
|
121
|
+
<p className="text-xl text-white sm:text-2xl">{active.authorName}</p>
|
|
122
|
+
{active.authorLocation ? (
|
|
123
|
+
<p className="text-sm text-white/60">{active.authorLocation}</p>
|
|
124
|
+
) : null}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</article>
|
|
128
|
+
|
|
129
|
+
<div className="hidden shrink-0 gap-2.5 lg:flex">
|
|
130
|
+
{previewSlides.map(({ slide, index }) => (
|
|
131
|
+
<button
|
|
132
|
+
key={slide.id ?? index}
|
|
133
|
+
type="button"
|
|
134
|
+
aria-label={`View testimonial from ${slide.authorName}`}
|
|
135
|
+
onClick={() => setSlide(index)}
|
|
136
|
+
className="group relative h-[535px] w-[250px] overflow-hidden rounded-xl transition-transform duration-300 ease-out hover:scale-[1.02] focus-visible:outline-2 focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:hover:scale-100"
|
|
137
|
+
>
|
|
138
|
+
<SafeImage
|
|
139
|
+
src={slide.image.src}
|
|
140
|
+
alt={slide.image.alt ?? slide.authorName}
|
|
141
|
+
fill
|
|
142
|
+
className="object-cover object-center"
|
|
143
|
+
sizes="250px"
|
|
144
|
+
/>
|
|
145
|
+
{slide.overlayColor ? (
|
|
146
|
+
<span
|
|
147
|
+
className="absolute inset-0 rounded-xl"
|
|
148
|
+
style={{ backgroundColor: slide.overlayColor }}
|
|
149
|
+
aria-hidden="true"
|
|
150
|
+
/>
|
|
151
|
+
) : null}
|
|
152
|
+
</button>
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div className="flex flex-col items-center gap-10 sm:gap-14 lg:gap-[60px]">
|
|
158
|
+
<div
|
|
159
|
+
className="relative h-px w-full max-w-[1670px] bg-white/20"
|
|
160
|
+
role="progressbar"
|
|
161
|
+
aria-valuemin={0}
|
|
162
|
+
aria-valuemax={100}
|
|
163
|
+
aria-valuenow={Math.round(progress)}
|
|
164
|
+
aria-label="Testimonial progress"
|
|
165
|
+
>
|
|
166
|
+
<div
|
|
167
|
+
className="absolute left-0 top-0 h-full bg-white transition-all duration-500 ease-out motion-reduce:transition-none"
|
|
168
|
+
style={{ width: `${progress}%` }}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{ctaText ? (
|
|
173
|
+
<CtaTag
|
|
174
|
+
{...(ctaHref
|
|
175
|
+
? { href: ctaHref }
|
|
176
|
+
: { type: "button", onClick: onCtaClick })}
|
|
177
|
+
className="inline-flex h-12 items-center border border-white px-5 text-base font-semibold text-white backdrop-blur-[17px] transition-colors duration-200 ease-out hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
178
|
+
>
|
|
179
|
+
{ctaText}
|
|
180
|
+
</CtaTag>
|
|
181
|
+
) : null}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div className="flex justify-center gap-2 lg:hidden">
|
|
185
|
+
{slides.map((slide, index) => (
|
|
186
|
+
<button
|
|
187
|
+
key={slide.id ?? index}
|
|
188
|
+
type="button"
|
|
189
|
+
aria-label={`Go to slide ${index + 1}`}
|
|
190
|
+
aria-current={index === current ? "true" : undefined}
|
|
191
|
+
onClick={() => setSlide(index)}
|
|
192
|
+
className={[
|
|
193
|
+
"h-2 rounded-full transition-all duration-300 motion-reduce:transition-none",
|
|
194
|
+
index === current ? "w-6 bg-white" : "w-2 bg-white/40",
|
|
195
|
+
].join(" ")}
|
|
196
|
+
/>
|
|
197
|
+
))}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</section>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
Testimonial13.propTypes = testimonial13PropTypes;
|
|
205
|
+
|
|
206
|
+
export default Testimonial13;
|
|
@@ -0,0 +1,87 @@
|
|
|
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 slideShape = PropTypes.shape({
|
|
9
|
+
id: PropTypes.string,
|
|
10
|
+
quote: PropTypes.string.isRequired,
|
|
11
|
+
authorName: PropTypes.string.isRequired,
|
|
12
|
+
authorLocation: PropTypes.string,
|
|
13
|
+
image: imageShape.isRequired,
|
|
14
|
+
/** Preview card tint — e.g. `rgba(255,105,0,0.7)` or `rgba(0,189,214,0.7)`. */
|
|
15
|
+
overlayColor: PropTypes.string,
|
|
16
|
+
videoUrl: PropTypes.string,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const testimonial13PropTypes = {
|
|
20
|
+
headline: PropTypes.string,
|
|
21
|
+
subtitle: PropTypes.string,
|
|
22
|
+
slides: PropTypes.arrayOf(slideShape),
|
|
23
|
+
activeSlide: PropTypes.number,
|
|
24
|
+
onSlideChange: PropTypes.func,
|
|
25
|
+
ctaText: PropTypes.string,
|
|
26
|
+
ctaHref: PropTypes.string,
|
|
27
|
+
onCtaClick: PropTypes.func,
|
|
28
|
+
onPlayClick: PropTypes.func,
|
|
29
|
+
className: PropTypes.string,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const testimonial13DefaultProps = {
|
|
33
|
+
headline: "Real Stories, Real Change",
|
|
34
|
+
subtitle: "Behind every program is a real human story.",
|
|
35
|
+
slides: [
|
|
36
|
+
{
|
|
37
|
+
id: "marcus",
|
|
38
|
+
quote:
|
|
39
|
+
"When everything felt confusing and overwhelming, Refugee 613 helped me find clear information and people who truly listened. I didn't feel alone anymore.",
|
|
40
|
+
authorName: "Marcus Boyer",
|
|
41
|
+
authorLocation: "From Myanmar",
|
|
42
|
+
image: {
|
|
43
|
+
src: "/testimonials/testimonial13/slide-1.jpg",
|
|
44
|
+
alt: "Woman smiling while on a phone call",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "amina",
|
|
49
|
+
quote:
|
|
50
|
+
"The support I received gave me confidence to rebuild my life. Every step felt guided and compassionate.",
|
|
51
|
+
authorName: "Amina Hassan",
|
|
52
|
+
authorLocation: "From Syria",
|
|
53
|
+
image: {
|
|
54
|
+
src: "/testimonials/testimonial13/slide-2.jpg",
|
|
55
|
+
alt: "Portrait of a newcomer",
|
|
56
|
+
},
|
|
57
|
+
overlayColor: "rgba(255,105,0,0.7)",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "david",
|
|
61
|
+
quote:
|
|
62
|
+
"I finally understood my rights and options. The team made a difficult transition feel possible.",
|
|
63
|
+
authorName: "David Chen",
|
|
64
|
+
authorLocation: "From Vietnam",
|
|
65
|
+
image: {
|
|
66
|
+
src: "/testimonials/testimonial13/slide-3.jpg",
|
|
67
|
+
alt: "Community member portrait",
|
|
68
|
+
},
|
|
69
|
+
overlayColor: "rgba(0,189,214,0.7)",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "leila",
|
|
73
|
+
quote:
|
|
74
|
+
"Having someone who spoke my language and understood my culture changed everything for my family.",
|
|
75
|
+
authorName: "Leila Farah",
|
|
76
|
+
authorLocation: "From Afghanistan",
|
|
77
|
+
image: {
|
|
78
|
+
src: "/testimonials/testimonial13/slide-4.jpg",
|
|
79
|
+
alt: "Family receiving support",
|
|
80
|
+
},
|
|
81
|
+
overlayColor: "rgba(0,189,214,0.7)",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
activeSlide: 0,
|
|
85
|
+
ctaText: "Read More Stories",
|
|
86
|
+
ctaHref: "#",
|
|
87
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Testimonial13, default } from "./Testimonial13";
|