@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.
Files changed (139) hide show
  1. package/bin/testimonials.js +16 -0
  2. package/category.config.json +7 -0
  3. package/package.json +24 -0
  4. package/registry/index.json +174 -0
  5. package/registry/testimonial-01.json +10 -0
  6. package/registry/testimonial-02.json +10 -0
  7. package/registry/testimonial-03.json +10 -0
  8. package/registry/testimonial-04.json +10 -0
  9. package/registry/testimonial-05.json +9 -0
  10. package/registry/testimonial-06.json +10 -0
  11. package/registry/testimonial-07.json +10 -0
  12. package/registry/testimonial-08.json +10 -0
  13. package/registry/testimonial-09.json +10 -0
  14. package/registry/testimonial-10.json +10 -0
  15. package/registry/testimonial-11.json +10 -0
  16. package/registry/testimonial-12.json +10 -0
  17. package/registry/testimonial-13.json +10 -0
  18. package/registry/testimonial-14.json +9 -0
  19. package/registry/testimonial-15.json +10 -0
  20. package/registry/testimonial-16.json +10 -0
  21. package/registry/testimonial-17.json +10 -0
  22. package/templates/components/atoms/SafeImage/SafeImage.jsx +101 -0
  23. package/templates/components/atoms/SafeImage/index.js +1 -0
  24. package/templates/components/hooks/useCarousel.js +73 -0
  25. package/templates/components/organisms/Testimonial01/Testimonial01.jsx +171 -0
  26. package/templates/components/organisms/Testimonial01/Testimonial01.propTypes.js +82 -0
  27. package/templates/components/organisms/Testimonial01/index.js +1 -0
  28. package/templates/components/organisms/Testimonial02/Testimonial02.jsx +200 -0
  29. package/templates/components/organisms/Testimonial02/Testimonial02.propTypes.js +75 -0
  30. package/templates/components/organisms/Testimonial02/index.js +1 -0
  31. package/templates/components/organisms/Testimonial03/Testimonial03.jsx +208 -0
  32. package/templates/components/organisms/Testimonial03/Testimonial03.propTypes.js +90 -0
  33. package/templates/components/organisms/Testimonial03/index.js +1 -0
  34. package/templates/components/organisms/Testimonial04/Testimonial04.jsx +242 -0
  35. package/templates/components/organisms/Testimonial04/Testimonial04.propTypes.js +81 -0
  36. package/templates/components/organisms/Testimonial04/index.js +1 -0
  37. package/templates/components/organisms/Testimonial05/Testimonial05.jsx +88 -0
  38. package/templates/components/organisms/Testimonial05/Testimonial05.propTypes.js +29 -0
  39. package/templates/components/organisms/Testimonial05/index.js +1 -0
  40. package/templates/components/organisms/Testimonial06/Testimonial06.jsx +203 -0
  41. package/templates/components/organisms/Testimonial06/Testimonial06.propTypes.js +106 -0
  42. package/templates/components/organisms/Testimonial06/index.js +1 -0
  43. package/templates/components/organisms/Testimonial07/Testimonial07.jsx +245 -0
  44. package/templates/components/organisms/Testimonial07/Testimonial07.propTypes.js +80 -0
  45. package/templates/components/organisms/Testimonial07/index.js +1 -0
  46. package/templates/components/organisms/Testimonial08/Testimonial08.jsx +188 -0
  47. package/templates/components/organisms/Testimonial08/Testimonial08.propTypes.js +69 -0
  48. package/templates/components/organisms/Testimonial08/index.js +1 -0
  49. package/templates/components/organisms/Testimonial09/Testimonial09.jsx +214 -0
  50. package/templates/components/organisms/Testimonial09/Testimonial09.propTypes.js +105 -0
  51. package/templates/components/organisms/Testimonial09/index.js +1 -0
  52. package/templates/components/organisms/Testimonial10/Testimonial10.jsx +218 -0
  53. package/templates/components/organisms/Testimonial10/Testimonial10.propTypes.js +65 -0
  54. package/templates/components/organisms/Testimonial10/index.js +1 -0
  55. package/templates/components/organisms/Testimonial11/Testimonial11.jsx +242 -0
  56. package/templates/components/organisms/Testimonial11/Testimonial11.propTypes.js +73 -0
  57. package/templates/components/organisms/Testimonial11/index.js +1 -0
  58. package/templates/components/organisms/Testimonial12/Testimonial12.jsx +271 -0
  59. package/templates/components/organisms/Testimonial12/Testimonial12.propTypes.js +81 -0
  60. package/templates/components/organisms/Testimonial12/index.js +1 -0
  61. package/templates/components/organisms/Testimonial13/Testimonial13.jsx +206 -0
  62. package/templates/components/organisms/Testimonial13/Testimonial13.propTypes.js +87 -0
  63. package/templates/components/organisms/Testimonial13/index.js +1 -0
  64. package/templates/components/organisms/Testimonial14/Testimonial14.jsx +89 -0
  65. package/templates/components/organisms/Testimonial14/Testimonial14.propTypes.js +87 -0
  66. package/templates/components/organisms/Testimonial14/index.js +1 -0
  67. package/templates/components/organisms/Testimonial15/Testimonial15.jsx +201 -0
  68. package/templates/components/organisms/Testimonial15/Testimonial15.propTypes.js +89 -0
  69. package/templates/components/organisms/Testimonial15/index.js +1 -0
  70. package/templates/components/organisms/Testimonial16/Testimonial16.jsx +193 -0
  71. package/templates/components/organisms/Testimonial16/Testimonial16.propTypes.js +90 -0
  72. package/templates/components/organisms/Testimonial16/index.js +1 -0
  73. package/templates/components/organisms/Testimonial17/Testimonial17.jsx +236 -0
  74. package/templates/components/organisms/Testimonial17/Testimonial17.propTypes.js +98 -0
  75. package/templates/components/organisms/Testimonial17/index.js +1 -0
  76. package/templates/public/testimonials/testimonial01/avatar-marcus.jpg +0 -0
  77. package/templates/public/testimonials/testimonial01/avatar-wilma.jpg +0 -0
  78. package/templates/public/testimonials/testimonial01/company-logo-2.png +0 -0
  79. package/templates/public/testimonials/testimonial01/company-logo-icon-raw.png +0 -0
  80. package/templates/public/testimonials/testimonial01/company-logo-icon.png +0 -0
  81. package/templates/public/testimonials/testimonial01/desco-logo-export.png +0 -0
  82. package/templates/public/testimonials/testimonial01/desco-logo-raw1.png +0 -0
  83. package/templates/public/testimonials/testimonial01/desco-logo-raw2.png +0 -0
  84. package/templates/public/testimonials/testimonial01/desco-logo.png +0 -0
  85. package/templates/public/testimonials/testimonial02/avatar-julia.jpg +0 -0
  86. package/templates/public/testimonials/testimonial02/avatar-marie.jpg +0 -0
  87. package/templates/public/testimonials/testimonial02/avatar-mark.jpg +0 -0
  88. package/templates/public/testimonials/testimonial02/bg-gradient.png +0 -0
  89. package/templates/public/testimonials/testimonial03/avatar-david.jpg +0 -0
  90. package/templates/public/testimonials/testimonial03/globe-bg.png +0 -0
  91. package/templates/public/testimonials/testimonial04/avatar-carlos.jpg +0 -0
  92. package/templates/public/testimonials/testimonial04/avatar-john.jpg +0 -0
  93. package/templates/public/testimonials/testimonial04/avatar-sabbir.jpg +0 -0
  94. package/templates/public/testimonials/testimonial05/portrait.jpg +0 -0
  95. package/templates/public/testimonials/testimonial06/avatar-ahmed.jpg +0 -0
  96. package/templates/public/testimonials/testimonial06/avatar-danzel.jpg +0 -0
  97. package/templates/public/testimonials/testimonial06/avatar-lisa.jpg +0 -0
  98. package/templates/public/testimonials/testimonial06/avatar-maria.jpg +0 -0
  99. package/templates/public/testimonials/testimonial06/avatar-sarah.jpg +0 -0
  100. package/templates/public/testimonials/testimonial07/photo-2.jpg +0 -0
  101. package/templates/public/testimonials/testimonial07/photo-3.jpg +0 -0
  102. package/templates/public/testimonials/testimonial07/photo-alt.png +0 -0
  103. package/templates/public/testimonials/testimonial07/photo.jpg +0 -0
  104. package/templates/public/testimonials/testimonial07/slide-2.png +0 -0
  105. package/templates/public/testimonials/testimonial08/student-1.jpg +0 -0
  106. package/templates/public/testimonials/testimonial08/student-2.jpg +0 -0
  107. package/templates/public/testimonials/testimonial08/student-3.jpg +0 -0
  108. package/templates/public/testimonials/testimonial09/avatar-1.jpg +0 -0
  109. package/templates/public/testimonials/testimonial09/avatar-2.jpg +0 -0
  110. package/templates/public/testimonials/testimonial09/avatar-3.jpg +0 -0
  111. package/templates/public/testimonials/testimonial09/avatar-4.jpg +0 -0
  112. package/templates/public/testimonials/testimonial09/avatar-5.jpg +0 -0
  113. package/templates/public/testimonials/testimonial09/avatar-6.jpg +0 -0
  114. package/templates/public/testimonials/testimonial10/card-1.jpg +0 -0
  115. package/templates/public/testimonials/testimonial10/card-2.jpg +0 -0
  116. package/templates/public/testimonials/testimonial10/card-3.jpg +0 -0
  117. package/templates/public/testimonials/testimonial11/avatar-1.jpg +0 -0
  118. package/templates/public/testimonials/testimonial11/avatar-2.jpg +0 -0
  119. package/templates/public/testimonials/testimonial11/avatar-3.jpg +0 -0
  120. package/templates/public/testimonials/testimonial12/story-1.jpg +0 -0
  121. package/templates/public/testimonials/testimonial12/story-2.jpg +0 -0
  122. package/templates/public/testimonials/testimonial12/story-3.jpg +0 -0
  123. package/templates/public/testimonials/testimonial12/story-4.jpg +0 -0
  124. package/templates/public/testimonials/testimonial13/slide-1.jpg +0 -0
  125. package/templates/public/testimonials/testimonial13/slide-2.jpg +0 -0
  126. package/templates/public/testimonials/testimonial13/slide-3.jpg +0 -0
  127. package/templates/public/testimonials/testimonial13/slide-4.jpg +0 -0
  128. package/templates/public/testimonials/testimonial14/avatar-1.jpg +0 -0
  129. package/templates/public/testimonials/testimonial14/avatar-2.jpg +0 -0
  130. package/templates/public/testimonials/testimonial14/avatar-3.jpg +0 -0
  131. package/templates/public/testimonials/testimonial14/avatar-4.jpg +0 -0
  132. package/templates/public/testimonials/testimonial15/slide-1.jpg +0 -0
  133. package/templates/public/testimonials/testimonial15/slide-2.jpg +0 -0
  134. package/templates/public/testimonials/testimonial15/slide-3.jpg +0 -0
  135. package/templates/public/testimonials/testimonial16/avatar.jpg +0 -0
  136. package/templates/public/testimonials/testimonial16/featured.jpg +0 -0
  137. package/templates/public/testimonials/testimonial17/avatar-1.jpg +0 -0
  138. package/templates/public/testimonials/testimonial17/avatar-2.jpg +0 -0
  139. 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";