@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,80 @@
|
|
|
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
|
+
image: imageShape,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const testimonial07PropTypes = {
|
|
17
|
+
/** Centered uppercase headline. */
|
|
18
|
+
headline: PropTypes.string,
|
|
19
|
+
/** Slides for the testimonial carousel. */
|
|
20
|
+
testimonials: PropTypes.arrayOf(testimonialItemShape),
|
|
21
|
+
/** Controlled active slide index. */
|
|
22
|
+
activeSlide: PropTypes.number,
|
|
23
|
+
/** Called when the active slide changes. */
|
|
24
|
+
onSlideChange: PropTypes.func,
|
|
25
|
+
/** Auto-advance interval in ms; 0 = off. */
|
|
26
|
+
autoPlayMs: PropTypes.number,
|
|
27
|
+
className: PropTypes.string,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const testimonial07DefaultProps = {
|
|
31
|
+
headline: "Our Clients say",
|
|
32
|
+
testimonials: [
|
|
33
|
+
{
|
|
34
|
+
id: "beccaty",
|
|
35
|
+
quote:
|
|
36
|
+
"Simplifying the complex with carefully-crafted video & animated content. Lorem Ipsum is simply dummy text of the lorem printing and typesetting industry simplifying the complex with carefully lorem Ipsum is simply dummy.",
|
|
37
|
+
authorName: "Beccaty",
|
|
38
|
+
authorRole: "KYEL Fitness",
|
|
39
|
+
image: {
|
|
40
|
+
src: "/testimonials/testimonial07/photo.jpg",
|
|
41
|
+
alt: "Beccaty reviewing content on a phone",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "james",
|
|
46
|
+
quote:
|
|
47
|
+
"Their team delivered beyond expectations. Every frame felt intentional and the storytelling elevated our brand in ways we did not anticipate.",
|
|
48
|
+
authorName: "James Porter",
|
|
49
|
+
authorRole: "Northline Media",
|
|
50
|
+
image: {
|
|
51
|
+
src: "/testimonials/testimonial07/photo-2.jpg",
|
|
52
|
+
alt: "James Porter",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "nina",
|
|
57
|
+
quote:
|
|
58
|
+
"Working with them was seamless from concept to final cut. They listened, iterated quickly, and produced content our audience genuinely loves.",
|
|
59
|
+
authorName: "Nina Alvarez",
|
|
60
|
+
authorRole: "Studio Kinetic",
|
|
61
|
+
image: {
|
|
62
|
+
src: "/testimonials/testimonial07/photo-3.jpg",
|
|
63
|
+
alt: "Nina Alvarez",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "omar",
|
|
68
|
+
quote:
|
|
69
|
+
"The animation quality and attention to detail set them apart. Our campaign metrics improved significantly after launch.",
|
|
70
|
+
authorName: "Omar Haddad",
|
|
71
|
+
authorRole: "Pulse Creative",
|
|
72
|
+
image: {
|
|
73
|
+
src: "/testimonials/testimonial07/photo.jpg",
|
|
74
|
+
alt: "Omar Haddad",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
activeSlide: 0,
|
|
79
|
+
autoPlayMs: 0,
|
|
80
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Testimonial07, default } from "./Testimonial07";
|
|
@@ -0,0 +1,188 @@
|
|
|
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
|
+
testimonial08DefaultProps,
|
|
9
|
+
testimonial08PropTypes,
|
|
10
|
+
} from "./Testimonial08.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="M18 52c0-12 7-22 18-28l5 10c-7 3-12 8-12 16h12v14H10V52h8zm38 0c0-12 7-22 18-28l5 10c-7 3-12 8-12 16h12v14H48V52h8z"
|
|
17
|
+
fill="currentColor"
|
|
18
|
+
opacity="0.4"
|
|
19
|
+
/>
|
|
20
|
+
</svg>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function NavArrowButton({ direction, onClick, disabled }) {
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
aria-label={direction === "prev" ? "Previous testimonial" : "Next testimonial"}
|
|
29
|
+
onClick={onClick}
|
|
30
|
+
disabled={disabled}
|
|
31
|
+
className="flex size-14 items-center justify-center rounded-full border border-white/30 text-white transition-colors duration-200 ease-out hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-40 sm:size-[60px]"
|
|
32
|
+
>
|
|
33
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="size-6">
|
|
34
|
+
<path
|
|
35
|
+
d={direction === "prev" ? "M15 6l-6 6 6 6" : "M9 6l6 6-6 6"}
|
|
36
|
+
stroke="currentColor"
|
|
37
|
+
strokeWidth="1.5"
|
|
38
|
+
strokeLinecap="round"
|
|
39
|
+
strokeLinejoin="round"
|
|
40
|
+
/>
|
|
41
|
+
</svg>
|
|
42
|
+
</button>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Testimonial08 — NHCS split-background testimonial carousel with chamfered portrait.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} props - See Testimonial08.propTypes.js.
|
|
50
|
+
*/
|
|
51
|
+
export function Testimonial08({
|
|
52
|
+
headline = testimonial08DefaultProps.headline,
|
|
53
|
+
testimonials = testimonial08DefaultProps.testimonials,
|
|
54
|
+
activeSlide: activeSlideProp = testimonial08DefaultProps.activeSlide,
|
|
55
|
+
onSlideChange,
|
|
56
|
+
autoPlayMs = testimonial08DefaultProps.autoPlayMs,
|
|
57
|
+
className = "",
|
|
58
|
+
}) {
|
|
59
|
+
const maxIndex = Math.max(0, testimonials.length - 1);
|
|
60
|
+
const { activeSlide, goTo, next, prev, pause, resume } = useCarousel({
|
|
61
|
+
count: testimonials.length,
|
|
62
|
+
initialIndex: activeSlideProp,
|
|
63
|
+
loop: true,
|
|
64
|
+
autoPlayMs,
|
|
65
|
+
onChange: onSlideChange,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
goTo(Math.min(activeSlideProp, maxIndex));
|
|
70
|
+
}, [activeSlideProp, goTo, maxIndex]);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<section
|
|
74
|
+
className={[
|
|
75
|
+
"relative w-full overflow-hidden",
|
|
76
|
+
className,
|
|
77
|
+
]
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.join(" ")}
|
|
80
|
+
data-testimonial="testimonial08"
|
|
81
|
+
onMouseEnter={pause}
|
|
82
|
+
onMouseLeave={resume}
|
|
83
|
+
>
|
|
84
|
+
<div className="absolute inset-0 bg-[#f6f6f6]" aria-hidden="true" />
|
|
85
|
+
<div
|
|
86
|
+
className="absolute inset-x-0 top-0 h-[58%] bg-[#001028] sm:h-[62%] lg:h-[691px] lg:max-h-[70%]"
|
|
87
|
+
aria-hidden="true"
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
<div className="relative px-4 py-14 sm:px-6 sm:py-16 md:px-10 lg:px-20 lg:py-24 xl:px-[80px]">
|
|
91
|
+
<div className="mx-auto flex w-full max-w-[1366px] flex-col gap-10 lg:gap-16">
|
|
92
|
+
<div className="flex flex-col gap-8 lg:flex-row lg:items-start lg:justify-between">
|
|
93
|
+
<div className="max-w-3xl">
|
|
94
|
+
<h2 className="font-serif text-3xl leading-tight text-white sm:text-4xl lg:text-[48px] lg:leading-[60px]">
|
|
95
|
+
{headline}
|
|
96
|
+
</h2>
|
|
97
|
+
<div className="mt-5 h-1 w-16 bg-[#c70017]" aria-hidden="true" />
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div className="flex flex-row gap-3 lg:flex-col lg:gap-4">
|
|
101
|
+
<NavArrowButton direction="prev" onClick={prev} disabled={false} />
|
|
102
|
+
<NavArrowButton direction="next" onClick={next} disabled={false} />
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div className="overflow-hidden">
|
|
107
|
+
<div
|
|
108
|
+
className="flex transition-transform duration-500 ease-out motion-reduce:transition-none"
|
|
109
|
+
style={{ transform: `translateX(-${activeSlide * 100}%)` }}
|
|
110
|
+
>
|
|
111
|
+
{testimonials.map((item) => (
|
|
112
|
+
<article
|
|
113
|
+
key={item.id ?? item.authorName}
|
|
114
|
+
className="flex w-full shrink-0 flex-col gap-10 lg:flex-row lg:items-end lg:gap-12 xl:gap-20"
|
|
115
|
+
>
|
|
116
|
+
{item.image?.src ? (
|
|
117
|
+
<div
|
|
118
|
+
className="relative mx-auto h-72 w-full max-w-md shrink-0 overflow-hidden sm:h-80 lg:mx-0 lg:h-[440px] lg:w-[445px] lg:max-w-none"
|
|
119
|
+
style={{
|
|
120
|
+
clipPath:
|
|
121
|
+
"polygon(18% 0, 100% 0, 100% 100%, 0 100%, 0 18%)",
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<SafeImage
|
|
125
|
+
src={item.image.src}
|
|
126
|
+
alt={item.image.alt ?? item.authorName}
|
|
127
|
+
fill
|
|
128
|
+
className="object-cover object-center"
|
|
129
|
+
sizes="(max-width: 1024px) 90vw, 445px"
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
) : null}
|
|
133
|
+
|
|
134
|
+
<div className="relative flex flex-1 flex-col gap-8 pb-4 lg:pb-10">
|
|
135
|
+
<QuoteIcon className="size-16 text-[#c70017] sm:size-20 lg:size-[100px]" />
|
|
136
|
+
|
|
137
|
+
<blockquote className="max-w-2xl font-serif text-lg italic leading-relaxed text-white sm:text-xl lg:text-2xl lg:leading-9">
|
|
138
|
+
“{item.quote}”
|
|
139
|
+
</blockquote>
|
|
140
|
+
|
|
141
|
+
<div className="relative pl-0 lg:pl-36">
|
|
142
|
+
<div
|
|
143
|
+
className="absolute left-0 top-1/2 hidden h-px w-36 -translate-y-1/2 bg-[#c70017] lg:block"
|
|
144
|
+
aria-hidden="true"
|
|
145
|
+
/>
|
|
146
|
+
<p className="text-lg font-medium text-white sm:text-xl">
|
|
147
|
+
{item.authorName}
|
|
148
|
+
</p>
|
|
149
|
+
{item.authorRole ? (
|
|
150
|
+
<p className="mt-1 text-sm text-white/90 sm:text-base">
|
|
151
|
+
{item.authorRole}
|
|
152
|
+
</p>
|
|
153
|
+
) : null}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</article>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{testimonials.length > 1 ? (
|
|
162
|
+
<div className="flex justify-center gap-2 lg:justify-start">
|
|
163
|
+
{testimonials.map((item, index) => (
|
|
164
|
+
<button
|
|
165
|
+
key={item.id ?? item.authorName}
|
|
166
|
+
type="button"
|
|
167
|
+
aria-label={`Go to slide ${index + 1}`}
|
|
168
|
+
aria-current={activeSlide === index ? "true" : undefined}
|
|
169
|
+
onClick={() => goTo(index)}
|
|
170
|
+
className={[
|
|
171
|
+
"h-1 rounded-full transition-all duration-300 ease-out motion-reduce:transition-none",
|
|
172
|
+
activeSlide === index
|
|
173
|
+
? "w-8 bg-[#c70017]"
|
|
174
|
+
: "w-4 bg-white/30 hover:bg-white/50",
|
|
175
|
+
].join(" ")}
|
|
176
|
+
/>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
) : null}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</section>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
Testimonial08.propTypes = testimonial08PropTypes;
|
|
187
|
+
|
|
188
|
+
export default Testimonial08;
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
image: imageShape,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const testimonial08PropTypes = {
|
|
17
|
+
/** Section headline. */
|
|
18
|
+
headline: PropTypes.string,
|
|
19
|
+
/** Slides for the testimonial carousel. */
|
|
20
|
+
testimonials: PropTypes.arrayOf(testimonialItemShape),
|
|
21
|
+
/** Controlled active slide index. */
|
|
22
|
+
activeSlide: PropTypes.number,
|
|
23
|
+
/** Called when the active slide changes. */
|
|
24
|
+
onSlideChange: PropTypes.func,
|
|
25
|
+
/** Auto-advance interval in ms; 0 = off. */
|
|
26
|
+
autoPlayMs: PropTypes.number,
|
|
27
|
+
className: PropTypes.string,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const testimonial08DefaultProps = {
|
|
31
|
+
headline: "Let's hear from our students and parents.",
|
|
32
|
+
testimonials: [
|
|
33
|
+
{
|
|
34
|
+
id: "mark",
|
|
35
|
+
quote:
|
|
36
|
+
"It's more of a family rather than a school relationship. There's a lot of respect between the students and a huge sense of responsibility.",
|
|
37
|
+
authorName: "Mark Ashmore",
|
|
38
|
+
authorRole: "Student at NHCS",
|
|
39
|
+
image: {
|
|
40
|
+
src: "/testimonials/testimonial08/student-1.jpg",
|
|
41
|
+
alt: "Mark Ashmore studying at a desk",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "sophia",
|
|
46
|
+
quote:
|
|
47
|
+
"The teachers genuinely care about every student. I feel supported academically and personally every single day.",
|
|
48
|
+
authorName: "Sophia Nguyen",
|
|
49
|
+
authorRole: "Parent at NHCS",
|
|
50
|
+
image: {
|
|
51
|
+
src: "/testimonials/testimonial08/student-2.jpg",
|
|
52
|
+
alt: "Sophia Nguyen",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "elijah",
|
|
57
|
+
quote:
|
|
58
|
+
"NHCS gave our daughter confidence and a love for learning. The community here is warm, inclusive, and inspiring.",
|
|
59
|
+
authorName: "Elijah Brooks",
|
|
60
|
+
authorRole: "Parent at NHCS",
|
|
61
|
+
image: {
|
|
62
|
+
src: "/testimonials/testimonial08/student-3.jpg",
|
|
63
|
+
alt: "Elijah Brooks",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
activeSlide: 0,
|
|
68
|
+
autoPlayMs: 0,
|
|
69
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Testimonial08, default } from "./Testimonial08";
|
|
@@ -0,0 +1,214 @@
|
|
|
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
|
+
testimonial09DefaultProps,
|
|
8
|
+
testimonial09PropTypes,
|
|
9
|
+
} from "./Testimonial09.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
|
+
/**
|
|
31
|
+
* Testimonial09 — Horizontal bordered card carousel with avatar attribution.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} props - See Testimonial09.propTypes.js.
|
|
34
|
+
*/
|
|
35
|
+
export function Testimonial09({
|
|
36
|
+
eyebrow = testimonial09DefaultProps.eyebrow,
|
|
37
|
+
headline = testimonial09DefaultProps.headline,
|
|
38
|
+
testimonials = testimonial09DefaultProps.testimonials,
|
|
39
|
+
activeSlide: controlledSlide,
|
|
40
|
+
onSlideChange,
|
|
41
|
+
autoPlayMs = testimonial09DefaultProps.autoPlayMs,
|
|
42
|
+
className = "",
|
|
43
|
+
}) {
|
|
44
|
+
const isControlled = controlledSlide !== undefined;
|
|
45
|
+
|
|
46
|
+
const handleChange = useCallback(
|
|
47
|
+
(index) => {
|
|
48
|
+
onSlideChange?.(index);
|
|
49
|
+
},
|
|
50
|
+
[onSlideChange]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const [slidesPerView, setSlidesPerView] = useState(1);
|
|
54
|
+
const slideCount = Math.max(1, testimonials.length - slidesPerView + 1);
|
|
55
|
+
const maxSlide = slideCount - 1;
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
activeSlide: carouselSlide,
|
|
59
|
+
next,
|
|
60
|
+
prev,
|
|
61
|
+
pause,
|
|
62
|
+
resume,
|
|
63
|
+
goTo,
|
|
64
|
+
} = useCarousel({
|
|
65
|
+
count: slideCount,
|
|
66
|
+
initialIndex: isControlled ? controlledSlide : 0,
|
|
67
|
+
loop: false,
|
|
68
|
+
autoPlayMs,
|
|
69
|
+
onChange: handleChange,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const activeSlide = isControlled ? controlledSlide : carouselSlide;
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const updateSlidesPerView = () => {
|
|
76
|
+
const width = window.innerWidth;
|
|
77
|
+
if (width >= 1280) setSlidesPerView(4);
|
|
78
|
+
else if (width >= 1024) setSlidesPerView(3);
|
|
79
|
+
else if (width >= 768) setSlidesPerView(2);
|
|
80
|
+
else setSlidesPerView(1);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
updateSlidesPerView();
|
|
84
|
+
window.addEventListener("resize", updateSlidesPerView);
|
|
85
|
+
return () => window.removeEventListener("resize", updateSlidesPerView);
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (isControlled) goTo(controlledSlide);
|
|
90
|
+
}, [controlledSlide, goTo, isControlled]);
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (carouselSlide > maxSlide) goTo(maxSlide);
|
|
94
|
+
}, [carouselSlide, goTo, maxSlide]);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<section
|
|
98
|
+
className={[
|
|
99
|
+
"relative w-full overflow-hidden bg-white",
|
|
100
|
+
"px-4 py-16 sm:px-6 sm:py-20 md:px-10 lg:px-20 lg:py-[100px] xl:px-[80px]",
|
|
101
|
+
className,
|
|
102
|
+
]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join(" ")}
|
|
105
|
+
data-testimonial="testimonial09"
|
|
106
|
+
onMouseEnter={pause}
|
|
107
|
+
onMouseLeave={resume}
|
|
108
|
+
>
|
|
109
|
+
<div className="mx-auto flex w-full max-w-[1532px] flex-col gap-10 sm:gap-12 lg:gap-[48px]">
|
|
110
|
+
<header className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
|
|
111
|
+
<div className="flex max-w-xl flex-col gap-6 lg:gap-12">
|
|
112
|
+
{eyebrow ? (
|
|
113
|
+
<p className="text-xs font-medium uppercase tracking-[0.08em] text-[#1b2127]">
|
|
114
|
+
{eyebrow}
|
|
115
|
+
</p>
|
|
116
|
+
) : null}
|
|
117
|
+
<h2 className="font-serif text-2xl leading-[1.2] tracking-[-0.02em] text-[#1b2127] sm:text-[28px] lg:text-[32px]">
|
|
118
|
+
{headline}
|
|
119
|
+
</h2>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div className="flex items-center gap-3">
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
aria-label="Previous testimonial"
|
|
126
|
+
onClick={prev}
|
|
127
|
+
disabled={activeSlide === 0}
|
|
128
|
+
className="flex size-10 items-center justify-center rounded-full border border-[#e6eaed] text-[#1b2127] transition-colors duration-200 ease-out hover:bg-[#f7f7f7] focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-30"
|
|
129
|
+
>
|
|
130
|
+
<ChevronIcon direction="left" className="size-5" />
|
|
131
|
+
</button>
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
aria-label="Next testimonial"
|
|
135
|
+
onClick={next}
|
|
136
|
+
disabled={activeSlide >= maxSlide}
|
|
137
|
+
className="flex size-10 items-center justify-center rounded-full border border-[#e6eaed] text-[#1b2127] transition-colors duration-200 ease-out hover:bg-[#f7f7f7] focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-30"
|
|
138
|
+
>
|
|
139
|
+
<ChevronIcon direction="right" className="size-5" />
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
</header>
|
|
143
|
+
|
|
144
|
+
<div className="-mx-4 overflow-hidden sm:-mx-6 md:-mx-10 lg:mx-0">
|
|
145
|
+
<div
|
|
146
|
+
className="flex transition-transform duration-500 ease-out motion-reduce:transition-none"
|
|
147
|
+
style={{
|
|
148
|
+
transform: `translateX(-${activeSlide * (100 / slidesPerView)}%)`,
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
{testimonials.map((item, index) => (
|
|
152
|
+
<article
|
|
153
|
+
key={item.id ?? item.authorName}
|
|
154
|
+
className={[
|
|
155
|
+
"flex shrink-0 flex-col justify-between border border-[#e6eaed] p-6 sm:p-8 lg:min-h-[400px] lg:p-[30px] xl:min-h-[496px]",
|
|
156
|
+
index < testimonials.length - 1 ? "border-r-0" : "",
|
|
157
|
+
].join(" ")}
|
|
158
|
+
style={{ width: `${100 / slidesPerView}%` }}
|
|
159
|
+
>
|
|
160
|
+
<p className="text-base leading-[1.4] tracking-[0.02em] text-[#1b2127]">
|
|
161
|
+
“{item.quote}”
|
|
162
|
+
</p>
|
|
163
|
+
|
|
164
|
+
<div className="mt-10 flex flex-col gap-3">
|
|
165
|
+
{item.avatar ? (
|
|
166
|
+
<div className="relative size-14 shrink-0 overflow-hidden sm:size-[60px]">
|
|
167
|
+
<SafeImage
|
|
168
|
+
src={item.avatar.src}
|
|
169
|
+
alt={item.avatar.alt ?? item.authorName}
|
|
170
|
+
fill
|
|
171
|
+
className="object-cover object-center"
|
|
172
|
+
sizes="60px"
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
) : null}
|
|
176
|
+
<p className="font-serif text-lg leading-none tracking-[-0.02em] text-[#1b2127] sm:text-xl">
|
|
177
|
+
{item.authorName}
|
|
178
|
+
</p>
|
|
179
|
+
{item.authorRole ? (
|
|
180
|
+
<p className="text-xs font-medium uppercase tracking-[0.08em] text-[#767f84]">
|
|
181
|
+
{item.authorRole}
|
|
182
|
+
</p>
|
|
183
|
+
) : null}
|
|
184
|
+
</div>
|
|
185
|
+
</article>
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div className="flex justify-center gap-2">
|
|
191
|
+
{Array.from({ length: slideCount }).map((_, index) => (
|
|
192
|
+
<button
|
|
193
|
+
key={index}
|
|
194
|
+
type="button"
|
|
195
|
+
aria-label={`Go to slide ${index + 1}`}
|
|
196
|
+
aria-current={activeSlide === index ? "true" : undefined}
|
|
197
|
+
onClick={() => goTo(index)}
|
|
198
|
+
className={[
|
|
199
|
+
"h-1.5 rounded-full transition-all duration-300 ease-out motion-reduce:transition-none",
|
|
200
|
+
activeSlide === index
|
|
201
|
+
? "w-6 bg-[#1b2127]"
|
|
202
|
+
: "w-1.5 bg-[#e6eaed] hover:bg-[#767f84]",
|
|
203
|
+
].join(" ")}
|
|
204
|
+
/>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</section>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
Testimonial09.propTypes = testimonial09PropTypes;
|
|
213
|
+
|
|
214
|
+
export default Testimonial09;
|
|
@@ -0,0 +1,105 @@
|
|
|
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 testimonial09PropTypes = {
|
|
17
|
+
/** Small uppercase label above the headline. */
|
|
18
|
+
eyebrow: PropTypes.string,
|
|
19
|
+
/** Section headline. */
|
|
20
|
+
headline: PropTypes.string,
|
|
21
|
+
/** Testimonial cards for the carousel. */
|
|
22
|
+
testimonials: PropTypes.arrayOf(testimonialItemShape),
|
|
23
|
+
/** Controlled active slide index. */
|
|
24
|
+
activeSlide: PropTypes.number,
|
|
25
|
+
/** Called when the active slide changes. */
|
|
26
|
+
onSlideChange: PropTypes.func,
|
|
27
|
+
/** Auto-advance interval in ms; 0 = off. */
|
|
28
|
+
autoPlayMs: PropTypes.number,
|
|
29
|
+
className: PropTypes.string,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const testimonial09DefaultProps = {
|
|
33
|
+
eyebrow: "/ testimonials",
|
|
34
|
+
headline: "Stories of trust, comfort, and everyday living from the people.",
|
|
35
|
+
testimonials: [
|
|
36
|
+
{
|
|
37
|
+
id: "farhan-ahmed",
|
|
38
|
+
quote:
|
|
39
|
+
"From the very first meeting, the entire experience felt thoughtful, transparent, and professionally managed. Suvastu truly understands modern family living.",
|
|
40
|
+
authorName: "Farhan Ahmed",
|
|
41
|
+
authorRole: "Homeowner, Gulshan",
|
|
42
|
+
avatar: {
|
|
43
|
+
src: "/testimonials/testimonial09/avatar-2.jpg",
|
|
44
|
+
alt: "Farhan Ahmed",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "nusrat-rahman",
|
|
49
|
+
quote:
|
|
50
|
+
"What stood out most was the attention to detail — from the architectural planning to the finishing quality. Every space feels intentional.",
|
|
51
|
+
authorName: "Nusrat Rahman",
|
|
52
|
+
authorRole: "Resident, Banani",
|
|
53
|
+
avatar: {
|
|
54
|
+
src: "/testimonials/testimonial09/avatar-3.jpg",
|
|
55
|
+
alt: "Nusrat Rahman",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "arif-chowdhury",
|
|
60
|
+
quote:
|
|
61
|
+
"The community here is vibrant and welcoming, making it easy to feel at home from day one.",
|
|
62
|
+
authorName: "Arif Chowdhury",
|
|
63
|
+
authorRole: "Professional, Gulshan",
|
|
64
|
+
avatar: {
|
|
65
|
+
src: "/testimonials/testimonial09/avatar-4.jpg",
|
|
66
|
+
alt: "Arif Chowdhury",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "sadia-islam",
|
|
71
|
+
quote:
|
|
72
|
+
"I appreciate the eco-friendly designs integrated throughout the development. It truly makes a difference in daily living.",
|
|
73
|
+
authorName: "Sadia Islam",
|
|
74
|
+
authorRole: "Environmentalist, Dhanmondi",
|
|
75
|
+
avatar: {
|
|
76
|
+
src: "/testimonials/testimonial09/avatar-1.jpg",
|
|
77
|
+
alt: "Sadia Islam",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "mehjabin-karim",
|
|
82
|
+
quote:
|
|
83
|
+
"The handover process was smooth, organized, and trustworthy. Their commitment to quality and communication gave us real confidence.",
|
|
84
|
+
authorName: "Mehjabin Karim",
|
|
85
|
+
authorRole: "Homeowner, Uttara",
|
|
86
|
+
avatar: {
|
|
87
|
+
src: "/testimonials/testimonial09/avatar-5.jpg",
|
|
88
|
+
alt: "Mehjabin Karim",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "rafiul-islam",
|
|
93
|
+
quote:
|
|
94
|
+
"Suvastu creates homes that feel timeless. The design, atmosphere, and overall living experience feel elevated in every way.",
|
|
95
|
+
authorName: "Rafiul Islam",
|
|
96
|
+
authorRole: "Resident, Dhaka",
|
|
97
|
+
avatar: {
|
|
98
|
+
src: "/testimonials/testimonial09/avatar-6.jpg",
|
|
99
|
+
alt: "Rafiul Islam",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
activeSlide: 0,
|
|
104
|
+
autoPlayMs: 0,
|
|
105
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Testimonial09, default } from "./Testimonial09";
|