@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,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
+ &ldquo;{item.quote}&rdquo;
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
+ &ldquo;{item.quote}&rdquo;
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";