@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,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import SafeImage from "../../atoms/SafeImage";
|
|
4
|
+
import {
|
|
5
|
+
testimonial14DefaultProps,
|
|
6
|
+
testimonial14PropTypes,
|
|
7
|
+
} from "./Testimonial14.propTypes";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Testimonial14 — Staggered impact cards with tilted sticky-note styling.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} props - See Testimonial14.propTypes.js.
|
|
13
|
+
*/
|
|
14
|
+
export function Testimonial14({
|
|
15
|
+
eyebrow = testimonial14DefaultProps.eyebrow,
|
|
16
|
+
introPrimary = testimonial14DefaultProps.introPrimary,
|
|
17
|
+
introSecondary = testimonial14DefaultProps.introSecondary,
|
|
18
|
+
cards = testimonial14DefaultProps.cards,
|
|
19
|
+
className = "",
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<section
|
|
23
|
+
className={[
|
|
24
|
+
"relative w-full overflow-hidden",
|
|
25
|
+
"bg-gradient-to-b from-[#f0f7fa]/40 via-white to-white",
|
|
26
|
+
"px-4 py-16 sm:px-6 sm:py-20 md:px-10 lg:px-20 lg:py-[160px] xl:px-[80px] 2xl:px-[240px]",
|
|
27
|
+
className,
|
|
28
|
+
]
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join(" ")}
|
|
31
|
+
data-testimonial="testimonial14"
|
|
32
|
+
>
|
|
33
|
+
<div className="mx-auto flex w-full max-w-7xl flex-col gap-12 sm:gap-16 lg:gap-20">
|
|
34
|
+
<header className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between lg:gap-16">
|
|
35
|
+
<p className="shrink-0 font-mono text-sm font-medium uppercase tracking-wide text-[#524b44] sm:text-base">
|
|
36
|
+
{eyebrow}
|
|
37
|
+
</p>
|
|
38
|
+
<p className="max-w-3xl text-2xl leading-snug tracking-[-0.025em] text-[#0d0a08] sm:text-3xl lg:text-[32px] lg:leading-[42px]">
|
|
39
|
+
<span>{introPrimary}</span>
|
|
40
|
+
<span className="text-[#0d0a08]/50">{introSecondary}</span>
|
|
41
|
+
</p>
|
|
42
|
+
</header>
|
|
43
|
+
|
|
44
|
+
<div className="flex gap-4 overflow-x-auto pb-4 [-ms-overflow-style:none] [scrollbar-width:none] sm:gap-6 lg:overflow-visible lg:pb-0 [&::-webkit-scrollbar]:hidden">
|
|
45
|
+
{cards.map((card) => (
|
|
46
|
+
<article
|
|
47
|
+
key={card.id ?? card.authorName}
|
|
48
|
+
className={[
|
|
49
|
+
"group flex w-[min(85vw,320px)] shrink-0 flex-col justify-between rounded-lg p-8 transition-transform duration-300 ease-out hover:rotate-0 hover:shadow-lg focus-within:shadow-lg motion-reduce:transition-none sm:w-[min(70vw,360px] lg:min-h-[360px] lg:flex-1 lg:p-10",
|
|
50
|
+
card.rotation ?? "",
|
|
51
|
+
].join(" ")}
|
|
52
|
+
style={{ backgroundColor: card.bgColor ?? "#ffffff" }}
|
|
53
|
+
>
|
|
54
|
+
<blockquote className="text-xl leading-[1.4] tracking-[0.005em] text-[#0d0a08] sm:text-2xl lg:text-[24px] lg:leading-[34px]">
|
|
55
|
+
“{card.quote}”
|
|
56
|
+
</blockquote>
|
|
57
|
+
|
|
58
|
+
<div className="mt-10 flex items-center gap-3">
|
|
59
|
+
{card.avatar?.src ? (
|
|
60
|
+
<div className="relative size-12 shrink-0 overflow-hidden rounded-full bg-white">
|
|
61
|
+
<SafeImage
|
|
62
|
+
src={card.avatar.src}
|
|
63
|
+
alt={card.avatar.alt ?? card.authorName}
|
|
64
|
+
fill
|
|
65
|
+
className="object-cover object-center"
|
|
66
|
+
sizes="48px"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
) : null}
|
|
70
|
+
<div className="min-w-0">
|
|
71
|
+
<p className="text-lg leading-tight text-[#0d0a08] sm:text-xl">
|
|
72
|
+
{card.authorName}
|
|
73
|
+
</p>
|
|
74
|
+
{card.authorRole ? (
|
|
75
|
+
<p className="text-sm text-black/70">{card.authorRole}</p>
|
|
76
|
+
) : null}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</article>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</section>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
Testimonial14.propTypes = testimonial14PropTypes;
|
|
88
|
+
|
|
89
|
+
export default Testimonial14;
|
|
@@ -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 cardShape = PropTypes.shape({
|
|
9
|
+
id: PropTypes.string,
|
|
10
|
+
quote: PropTypes.string.isRequired,
|
|
11
|
+
authorName: PropTypes.string.isRequired,
|
|
12
|
+
authorRole: PropTypes.string,
|
|
13
|
+
avatar: imageShape,
|
|
14
|
+
bgColor: PropTypes.string,
|
|
15
|
+
/** Tailwind rotate class, e.g. `-rotate-3`. */
|
|
16
|
+
rotation: PropTypes.string,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const testimonial14PropTypes = {
|
|
20
|
+
eyebrow: PropTypes.string,
|
|
21
|
+
introPrimary: PropTypes.string,
|
|
22
|
+
introSecondary: PropTypes.string,
|
|
23
|
+
cards: PropTypes.arrayOf(cardShape),
|
|
24
|
+
className: PropTypes.string,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const testimonial14DefaultProps = {
|
|
28
|
+
eyebrow: "Impacts",
|
|
29
|
+
introPrimary:
|
|
30
|
+
"Through personalized guidance and trusted connections, the Connectors Program ",
|
|
31
|
+
introSecondary:
|
|
32
|
+
"helps newcomers overcome challenges, access essential services, and build confidence, belonging, and long-term support in their new communities.",
|
|
33
|
+
cards: [
|
|
34
|
+
{
|
|
35
|
+
id: "nasim",
|
|
36
|
+
quote:
|
|
37
|
+
"My Connector helped me understand my options and made everything feel manageable.",
|
|
38
|
+
authorName: "Nasim Khan",
|
|
39
|
+
authorRole: "Refugee Claimant",
|
|
40
|
+
avatar: {
|
|
41
|
+
src: "/testimonials/testimonial14/avatar-1.jpg",
|
|
42
|
+
alt: "Nasim Khan",
|
|
43
|
+
},
|
|
44
|
+
bgColor: "#e5ffe9",
|
|
45
|
+
rotation: "-rotate-3",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "rashed",
|
|
49
|
+
quote:
|
|
50
|
+
"Having someone listen and guide me made settling into a new community much easier.",
|
|
51
|
+
authorName: "Rashed Ali",
|
|
52
|
+
authorRole: "Newcomer and Student",
|
|
53
|
+
avatar: {
|
|
54
|
+
src: "/testimonials/testimonial14/avatar-2.jpg",
|
|
55
|
+
alt: "Rashed Ali",
|
|
56
|
+
},
|
|
57
|
+
bgColor: "#ffebfd",
|
|
58
|
+
rotation: "rotate-2",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "rupa",
|
|
62
|
+
quote:
|
|
63
|
+
"I felt supported every step of the way. The program is caring and reliable.",
|
|
64
|
+
authorName: "Rupa Khaleda",
|
|
65
|
+
authorRole: "Single Parent and Newcomer",
|
|
66
|
+
avatar: {
|
|
67
|
+
src: "/testimonials/testimonial14/avatar-3.jpg",
|
|
68
|
+
alt: "Rupa Khaleda",
|
|
69
|
+
},
|
|
70
|
+
bgColor: "#ffffff",
|
|
71
|
+
rotation: "-rotate-2",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "sakhawat",
|
|
75
|
+
quote:
|
|
76
|
+
"The Connectors Program connected me with the right services quickly and respectfully.",
|
|
77
|
+
authorName: "Sakhawat Hasan",
|
|
78
|
+
authorRole: "Refugee Claimant and Job Seeker",
|
|
79
|
+
avatar: {
|
|
80
|
+
src: "/testimonials/testimonial14/avatar-4.jpg",
|
|
81
|
+
alt: "Sakhawat Hasan",
|
|
82
|
+
},
|
|
83
|
+
bgColor: "#f7ffd9",
|
|
84
|
+
rotation: "rotate-2",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Testimonial14, default } from "./Testimonial14";
|
|
@@ -0,0 +1,201 @@
|
|
|
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
|
+
testimonial15DefaultProps,
|
|
9
|
+
testimonial15PropTypes,
|
|
10
|
+
} from "./Testimonial15.propTypes";
|
|
11
|
+
|
|
12
|
+
function PlayIcon({ className = "" }) {
|
|
13
|
+
return (
|
|
14
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className={className}>
|
|
15
|
+
<path d="M8 5v14l11-7-11-7z" />
|
|
16
|
+
</svg>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ProgressRing({ current, total, className = "" }) {
|
|
21
|
+
const radius = 34;
|
|
22
|
+
const circumference = 2 * Math.PI * radius;
|
|
23
|
+
const progress = total > 0 ? (current + 1) / total : 0;
|
|
24
|
+
const offset = circumference * (1 - progress);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={["relative size-[76px]", className].join(" ")}>
|
|
28
|
+
<svg viewBox="0 0 76 76" className="size-full -rotate-90" aria-hidden="true">
|
|
29
|
+
<circle
|
|
30
|
+
cx="38"
|
|
31
|
+
cy="38"
|
|
32
|
+
r={radius}
|
|
33
|
+
fill="none"
|
|
34
|
+
stroke="rgba(255,255,255,0.2)"
|
|
35
|
+
strokeWidth="4"
|
|
36
|
+
/>
|
|
37
|
+
<circle
|
|
38
|
+
cx="38"
|
|
39
|
+
cy="38"
|
|
40
|
+
r={radius}
|
|
41
|
+
fill="none"
|
|
42
|
+
stroke="white"
|
|
43
|
+
strokeWidth="4"
|
|
44
|
+
strokeLinecap="round"
|
|
45
|
+
strokeDasharray={circumference}
|
|
46
|
+
strokeDashoffset={offset}
|
|
47
|
+
className="transition-all duration-500 ease-out motion-reduce:transition-none"
|
|
48
|
+
/>
|
|
49
|
+
</svg>
|
|
50
|
+
<span className="absolute inset-0 flex items-center justify-center text-base text-white">
|
|
51
|
+
{current + 1}/{total}
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Testimonial15 — Dark split layout with vertical image carousel and play control.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} props - See Testimonial15.propTypes.js.
|
|
61
|
+
*/
|
|
62
|
+
export function Testimonial15({
|
|
63
|
+
eyebrow = testimonial15DefaultProps.eyebrow,
|
|
64
|
+
headline = testimonial15DefaultProps.headline,
|
|
65
|
+
description = testimonial15DefaultProps.description,
|
|
66
|
+
slides = testimonial15DefaultProps.slides,
|
|
67
|
+
activeSlide: activeSlideProp = testimonial15DefaultProps.activeSlide,
|
|
68
|
+
onSlideChange,
|
|
69
|
+
onPlayClick,
|
|
70
|
+
className = "",
|
|
71
|
+
}) {
|
|
72
|
+
const isControlled = typeof activeSlideProp === "number";
|
|
73
|
+
const { activeSlide, goTo, next, prev } = useCarousel({
|
|
74
|
+
count: slides.length,
|
|
75
|
+
initialIndex: activeSlideProp,
|
|
76
|
+
loop: true,
|
|
77
|
+
onChange: onSlideChange,
|
|
78
|
+
});
|
|
79
|
+
const current = isControlled ? activeSlideProp : activeSlide;
|
|
80
|
+
|
|
81
|
+
const setSlide = (index) => {
|
|
82
|
+
if (isControlled) {
|
|
83
|
+
onSlideChange?.(index);
|
|
84
|
+
} else {
|
|
85
|
+
goTo(index);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!isControlled) {
|
|
91
|
+
goTo(activeSlideProp);
|
|
92
|
+
}
|
|
93
|
+
}, [activeSlideProp, goTo, isControlled]);
|
|
94
|
+
|
|
95
|
+
const active = slides[current] ?? slides[0];
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<section
|
|
99
|
+
className={[
|
|
100
|
+
"relative w-full overflow-hidden bg-[#1b1b1b]",
|
|
101
|
+
"px-4 py-16 sm:px-6 sm:py-20 md:px-10 lg:px-20 lg:py-24 xl:px-[80px]",
|
|
102
|
+
className,
|
|
103
|
+
]
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.join(" ")}
|
|
106
|
+
data-testimonial="testimonial15"
|
|
107
|
+
>
|
|
108
|
+
<div className="mx-auto flex w-full max-w-7xl flex-col gap-12 lg:flex-row lg:items-center lg:gap-16 xl:gap-24">
|
|
109
|
+
<div className="flex flex-1 flex-col gap-8 lg:max-w-md">
|
|
110
|
+
<header className="flex flex-col gap-5">
|
|
111
|
+
<p className="text-sm font-bold uppercase tracking-[0.2em] text-white/50">
|
|
112
|
+
{eyebrow}
|
|
113
|
+
</p>
|
|
114
|
+
<h2 className="text-3xl leading-tight text-white sm:text-4xl lg:text-[40px] lg:leading-[50px]">
|
|
115
|
+
{headline}
|
|
116
|
+
</h2>
|
|
117
|
+
</header>
|
|
118
|
+
|
|
119
|
+
{description ? (
|
|
120
|
+
<p className="max-w-md text-base leading-relaxed text-white/90 lg:text-base lg:leading-[26px]">
|
|
121
|
+
{description}
|
|
122
|
+
</p>
|
|
123
|
+
) : null}
|
|
124
|
+
|
|
125
|
+
<div className="flex items-center gap-4">
|
|
126
|
+
<ProgressRing current={current} total={slides.length} />
|
|
127
|
+
<div className="flex gap-2">
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
aria-label="Previous testimonial"
|
|
131
|
+
onClick={prev}
|
|
132
|
+
className="rounded-full border border-white/30 px-4 py-2 text-sm text-white transition-colors duration-200 ease-out hover:border-white focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
133
|
+
>
|
|
134
|
+
Prev
|
|
135
|
+
</button>
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
aria-label="Next testimonial"
|
|
139
|
+
onClick={next}
|
|
140
|
+
className="rounded-full border border-white/30 px-4 py-2 text-sm text-white transition-colors duration-200 ease-out hover:border-white focus-visible:outline-2 focus-visible:outline-offset-2"
|
|
141
|
+
>
|
|
142
|
+
Next
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="relative flex flex-1 items-center justify-center">
|
|
149
|
+
<div className="relative h-[min(70vh,540px)] w-full max-w-xl overflow-hidden">
|
|
150
|
+
<div
|
|
151
|
+
className="flex h-full flex-col transition-transform duration-500 ease-out motion-reduce:transition-none"
|
|
152
|
+
style={{ transform: `translateY(-${current * 100}%)` }}
|
|
153
|
+
>
|
|
154
|
+
{slides.map((slide, index) => (
|
|
155
|
+
<div
|
|
156
|
+
key={slide.id ?? index}
|
|
157
|
+
className="relative h-full w-full shrink-0 overflow-hidden rounded-lg"
|
|
158
|
+
>
|
|
159
|
+
<SafeImage
|
|
160
|
+
src={slide.image.src}
|
|
161
|
+
alt={slide.image.alt ?? `Testimonial ${index + 1}`}
|
|
162
|
+
fill
|
|
163
|
+
className={[
|
|
164
|
+
"object-cover object-center transition-opacity duration-500",
|
|
165
|
+
index === current ? "opacity-100" : "opacity-20",
|
|
166
|
+
].join(" ")}
|
|
167
|
+
sizes="(max-width:1024px) 100vw, 540px"
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
aria-label="Play video testimonial"
|
|
176
|
+
onClick={() => onPlayClick?.(active, current)}
|
|
177
|
+
className="absolute bottom-4 right-4 flex size-14 items-center justify-center rounded-full bg-white text-[#1b1b1b] shadow-lg transition-transform duration-200 ease-out hover:scale-105 focus-visible:outline-2 focus-visible:outline-offset-2 motion-reduce:transition-none motion-reduce:hover:scale-100"
|
|
178
|
+
>
|
|
179
|
+
<PlayIcon className="size-6" />
|
|
180
|
+
</button>
|
|
181
|
+
|
|
182
|
+
<div
|
|
183
|
+
className="absolute left-0 top-1/2 hidden -translate-x-1/2 -translate-y-1/2 lg:flex"
|
|
184
|
+
aria-hidden="true"
|
|
185
|
+
>
|
|
186
|
+
<div className="flex size-[100px] items-center justify-center rounded-full bg-white">
|
|
187
|
+
<span className="-rotate-90 text-xs font-bold uppercase tracking-[0.2em] text-[#1b1b1b]">
|
|
188
|
+
Drag
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</section>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
Testimonial15.propTypes = testimonial15PropTypes;
|
|
200
|
+
|
|
201
|
+
export default Testimonial15;
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
image: imageShape.isRequired,
|
|
11
|
+
videoUrl: PropTypes.string,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const testimonial15PropTypes = {
|
|
15
|
+
eyebrow: PropTypes.string,
|
|
16
|
+
headline: PropTypes.string,
|
|
17
|
+
description: PropTypes.string,
|
|
18
|
+
slides: PropTypes.arrayOf(slideShape),
|
|
19
|
+
activeSlide: PropTypes.number,
|
|
20
|
+
onSlideChange: PropTypes.func,
|
|
21
|
+
onPlayClick: PropTypes.func,
|
|
22
|
+
className: PropTypes.string,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const testimonial15DefaultProps = {
|
|
26
|
+
eyebrow: "/ Testimonials",
|
|
27
|
+
headline: "Our Clients Say",
|
|
28
|
+
description:
|
|
29
|
+
"It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal temperatu.",
|
|
30
|
+
slides: [
|
|
31
|
+
{
|
|
32
|
+
id: "slide-1",
|
|
33
|
+
image: {
|
|
34
|
+
src: "/testimonials/testimonial15/slide-1.jpg",
|
|
35
|
+
alt: "Client portrait one",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "slide-2",
|
|
40
|
+
image: {
|
|
41
|
+
src: "/testimonials/testimonial15/slide-2.jpg",
|
|
42
|
+
alt: "Client portrait two",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "slide-3",
|
|
47
|
+
image: {
|
|
48
|
+
src: "/testimonials/testimonial15/slide-3.jpg",
|
|
49
|
+
alt: "Client portrait three",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "slide-4",
|
|
54
|
+
image: {
|
|
55
|
+
src: "/testimonials/testimonial15/slide-1.jpg",
|
|
56
|
+
alt: "Client portrait four",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "slide-5",
|
|
61
|
+
image: {
|
|
62
|
+
src: "/testimonials/testimonial15/slide-2.jpg",
|
|
63
|
+
alt: "Client portrait five",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "slide-6",
|
|
68
|
+
image: {
|
|
69
|
+
src: "/testimonials/testimonial15/slide-3.jpg",
|
|
70
|
+
alt: "Client portrait six",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "slide-7",
|
|
75
|
+
image: {
|
|
76
|
+
src: "/testimonials/testimonial15/slide-1.jpg",
|
|
77
|
+
alt: "Client portrait seven",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "slide-8",
|
|
82
|
+
image: {
|
|
83
|
+
src: "/testimonials/testimonial15/slide-2.jpg",
|
|
84
|
+
alt: "Client portrait eight",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
activeSlide: 1,
|
|
89
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Testimonial15, default } from "./Testimonial15";
|
|
@@ -0,0 +1,193 @@
|
|
|
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
|
+
testimonial16DefaultProps,
|
|
9
|
+
testimonial16PropTypes,
|
|
10
|
+
} from "./Testimonial16.propTypes";
|
|
11
|
+
|
|
12
|
+
function QuoteIcon({ className = "" }) {
|
|
13
|
+
return (
|
|
14
|
+
<svg viewBox="0 0 100 100" fill="none" aria-hidden="true" className={className}>
|
|
15
|
+
<path
|
|
16
|
+
d="M24 72V44c0-12 6-22 18-30l8 12c-8 5-12 11-12 18h14v28H24zm40 0V44c0-12 6-22 18-30l8 12c-8 5-12 11-12 18h14v28H64z"
|
|
17
|
+
fill="#2f7a41"
|
|
18
|
+
fillOpacity="0.15"
|
|
19
|
+
stroke="#2f7a41"
|
|
20
|
+
strokeWidth="2"
|
|
21
|
+
/>
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ArrowIcon({ className = "" }) {
|
|
27
|
+
return (
|
|
28
|
+
<svg
|
|
29
|
+
viewBox="0 0 24 24"
|
|
30
|
+
fill="none"
|
|
31
|
+
stroke="currentColor"
|
|
32
|
+
strokeWidth="2"
|
|
33
|
+
strokeLinecap="round"
|
|
34
|
+
strokeLinejoin="round"
|
|
35
|
+
aria-hidden="true"
|
|
36
|
+
className={className}
|
|
37
|
+
>
|
|
38
|
+
<path d="M7 17L17 7" />
|
|
39
|
+
<path d="M9 7h8v8" />
|
|
40
|
+
</svg>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Testimonial16 — Split hero quote layout with image, arrows, and dot navigation.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} props - See Testimonial16.propTypes.js.
|
|
48
|
+
*/
|
|
49
|
+
export function Testimonial16({
|
|
50
|
+
headline = testimonial16DefaultProps.headline,
|
|
51
|
+
slides = testimonial16DefaultProps.slides,
|
|
52
|
+
activeSlide: activeSlideProp = testimonial16DefaultProps.activeSlide,
|
|
53
|
+
onSlideChange,
|
|
54
|
+
className = "",
|
|
55
|
+
}) {
|
|
56
|
+
const isControlled = typeof activeSlideProp === "number";
|
|
57
|
+
const { activeSlide, goTo, next, prev } = useCarousel({
|
|
58
|
+
count: slides.length,
|
|
59
|
+
initialIndex: activeSlideProp,
|
|
60
|
+
onChange: onSlideChange,
|
|
61
|
+
});
|
|
62
|
+
const current = isControlled ? activeSlideProp : activeSlide;
|
|
63
|
+
|
|
64
|
+
const setSlide = (index) => {
|
|
65
|
+
if (isControlled) {
|
|
66
|
+
onSlideChange?.(index);
|
|
67
|
+
} else {
|
|
68
|
+
goTo(index);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!isControlled) {
|
|
74
|
+
goTo(activeSlideProp);
|
|
75
|
+
}
|
|
76
|
+
}, [activeSlideProp, goTo, isControlled]);
|
|
77
|
+
|
|
78
|
+
const active = slides[current] ?? slides[0];
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<section
|
|
82
|
+
className={[
|
|
83
|
+
"relative w-full overflow-hidden bg-white",
|
|
84
|
+
"px-4 py-16 sm:px-6 sm:py-20 md:px-10 lg:px-20 lg:py-24 xl:px-[80px]",
|
|
85
|
+
className,
|
|
86
|
+
]
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.join(" ")}
|
|
89
|
+
data-testimonial="testimonial16"
|
|
90
|
+
>
|
|
91
|
+
<div
|
|
92
|
+
className="pointer-events-none absolute right-0 top-0 h-[min(80vh,680px)] w-[min(90vw,1246px)] translate-x-1/4 rounded-full bg-[radial-gradient(circle,rgba(47,122,65,0.12)_0%,transparent_70%)]"
|
|
93
|
+
aria-hidden="true"
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<div className="relative z-10 mx-auto flex w-full max-w-7xl flex-col gap-12 lg:gap-16">
|
|
97
|
+
<h2 className="max-w-4xl text-3xl font-medium uppercase leading-none tracking-[-0.02em] text-[#050708] sm:text-5xl md:text-6xl lg:text-7xl xl:text-[80px]">
|
|
98
|
+
{headline}
|
|
99
|
+
</h2>
|
|
100
|
+
|
|
101
|
+
<div className="flex flex-col gap-10 lg:flex-row lg:items-start lg:gap-16 xl:gap-24">
|
|
102
|
+
<div className="relative aspect-[679/400] w-full overflow-hidden bg-[#eee] lg:w-[45%] lg:shrink-0">
|
|
103
|
+
<SafeImage
|
|
104
|
+
src={active.image.src}
|
|
105
|
+
alt={active.image.alt ?? active.authorName}
|
|
106
|
+
fill
|
|
107
|
+
className="object-cover object-center transition-opacity duration-500 ease-out motion-reduce:transition-none"
|
|
108
|
+
sizes="(max-width:1024px) 100vw, 679px"
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div className="flex flex-1 flex-col gap-8 lg:gap-12">
|
|
113
|
+
<div className="flex items-start justify-between gap-4">
|
|
114
|
+
<QuoteIcon className="size-16 shrink-0 sm:size-20 lg:size-[100px]" />
|
|
115
|
+
<div className="flex items-center gap-5">
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
aria-label="Previous testimonial"
|
|
119
|
+
onClick={prev}
|
|
120
|
+
disabled={current === 0}
|
|
121
|
+
className="flex size-12 items-center justify-center rounded-full border border-[#050708]/70 text-[#050708]/70 transition-colors duration-200 ease-out hover:border-[#050708] hover:text-[#050708] focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-40"
|
|
122
|
+
>
|
|
123
|
+
<ArrowIcon className="size-5 -scale-x-100" />
|
|
124
|
+
</button>
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
aria-label="Next testimonial"
|
|
128
|
+
onClick={next}
|
|
129
|
+
disabled={current >= slides.length - 1}
|
|
130
|
+
className="flex size-12 items-center justify-center rounded-full bg-[#2f7a41] text-white transition-colors duration-200 ease-out hover:bg-[#276935] focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-40"
|
|
131
|
+
>
|
|
132
|
+
<ArrowIcon className="size-5" />
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<blockquote className="text-2xl leading-snug text-[#050708]/70 sm:text-3xl lg:text-[40px] lg:leading-[1.2]">
|
|
138
|
+
“{active.quote}”
|
|
139
|
+
</blockquote>
|
|
140
|
+
|
|
141
|
+
<div className="flex flex-col gap-5">
|
|
142
|
+
<div className="h-px w-full bg-black/10" aria-hidden="true" />
|
|
143
|
+
<div className="flex items-center gap-4">
|
|
144
|
+
{active.avatar?.src ? (
|
|
145
|
+
<div className="relative size-12 shrink-0 overflow-hidden rounded-full border border-black/10 sm:size-[50px]">
|
|
146
|
+
<SafeImage
|
|
147
|
+
src={active.avatar.src}
|
|
148
|
+
alt={active.avatar.alt ?? active.authorName}
|
|
149
|
+
fill
|
|
150
|
+
className="object-cover object-center"
|
|
151
|
+
sizes="50px"
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
) : null}
|
|
155
|
+
<div>
|
|
156
|
+
<p className="text-lg font-medium text-[#050708] sm:text-xl">
|
|
157
|
+
{active.authorName}
|
|
158
|
+
</p>
|
|
159
|
+
{active.authorTitle ? (
|
|
160
|
+
<p className="text-sm text-[#050708]/70">{active.authorTitle}</p>
|
|
161
|
+
) : null}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div className="flex items-center gap-1.5" role="tablist" aria-label="Testimonial slides">
|
|
167
|
+
{slides.map((slide, index) => (
|
|
168
|
+
<button
|
|
169
|
+
key={slide.id ?? index}
|
|
170
|
+
type="button"
|
|
171
|
+
role="tab"
|
|
172
|
+
aria-selected={index === current}
|
|
173
|
+
aria-label={`Go to slide ${index + 1}`}
|
|
174
|
+
onClick={() => setSlide(index)}
|
|
175
|
+
className={[
|
|
176
|
+
"rounded-full border border-[#050708] transition-all duration-300 motion-reduce:transition-none",
|
|
177
|
+
index === current
|
|
178
|
+
? "h-2 w-6 bg-[#050708]"
|
|
179
|
+
: "size-2 bg-transparent",
|
|
180
|
+
].join(" ")}
|
|
181
|
+
/>
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</section>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
Testimonial16.propTypes = testimonial16PropTypes;
|
|
192
|
+
|
|
193
|
+
export default Testimonial16;
|