@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,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import SafeImage from "../../atoms/SafeImage";
|
|
4
|
+
import {
|
|
5
|
+
testimonial05DefaultProps,
|
|
6
|
+
testimonial05PropTypes,
|
|
7
|
+
} from "./Testimonial05.propTypes";
|
|
8
|
+
|
|
9
|
+
function QuoteIcon({ className = "" }) {
|
|
10
|
+
return (
|
|
11
|
+
<svg
|
|
12
|
+
viewBox="0 0 80 80"
|
|
13
|
+
fill="none"
|
|
14
|
+
aria-hidden="true"
|
|
15
|
+
className={className}
|
|
16
|
+
>
|
|
17
|
+
<path
|
|
18
|
+
d="M18 44c0-10 6-18 16-22l4 8c-6 2-10 6-10 12h10V56H12V44h6zm34 0c0-10 6-18 16-22l4 8c-6 2-10 6-10 12h10V56H46V44h6z"
|
|
19
|
+
fill="currentColor"
|
|
20
|
+
/>
|
|
21
|
+
</svg>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Testimonial05 — Featured quote card with overlapping portrait and accent quote mark.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} props - See Testimonial05.propTypes.js.
|
|
29
|
+
*/
|
|
30
|
+
export function Testimonial05({
|
|
31
|
+
quote = testimonial05DefaultProps.quote,
|
|
32
|
+
authorName = testimonial05DefaultProps.authorName,
|
|
33
|
+
authorRole = testimonial05DefaultProps.authorRole,
|
|
34
|
+
portrait = testimonial05DefaultProps.portrait,
|
|
35
|
+
className = "",
|
|
36
|
+
}) {
|
|
37
|
+
return (
|
|
38
|
+
<section
|
|
39
|
+
className={[
|
|
40
|
+
"relative w-full overflow-hidden bg-white",
|
|
41
|
+
"px-4 py-12 sm:px-6 sm:py-16 md:px-10 lg:px-20 lg:py-[106px] xl:px-[80px]",
|
|
42
|
+
className,
|
|
43
|
+
]
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.join(" ")}
|
|
46
|
+
data-testimonial="testimonial05"
|
|
47
|
+
>
|
|
48
|
+
<div className="relative mx-auto w-full max-w-[1182px]">
|
|
49
|
+
<div className="relative bg-[#f7f8fa] px-6 py-10 sm:px-10 sm:py-12 lg:px-20 lg:py-10">
|
|
50
|
+
<QuoteIcon className="absolute -left-2 top-6 size-16 text-[#efa90c] sm:-left-4 sm:top-8 sm:size-20 lg:left-0 lg:top-1/2 lg:size-[100px] lg:-translate-y-1/2" />
|
|
51
|
+
|
|
52
|
+
<div className="relative z-10 flex flex-col gap-8 pr-0 sm:gap-10 lg:max-w-[799px] lg:gap-0 lg:justify-between lg:min-h-[294px] lg:pl-4">
|
|
53
|
+
<blockquote className="font-serif text-xl leading-[1.24] tracking-[-0.002em] text-[#010623] sm:text-2xl md:text-[28px] lg:text-[34px] lg:leading-[42px]">
|
|
54
|
+
{quote}
|
|
55
|
+
</blockquote>
|
|
56
|
+
|
|
57
|
+
<div className="flex flex-col gap-2">
|
|
58
|
+
<p className="font-serif text-lg leading-7 text-[#010623] sm:text-xl lg:text-[22px]">
|
|
59
|
+
{authorName}
|
|
60
|
+
</p>
|
|
61
|
+
{authorRole ? (
|
|
62
|
+
<p className="text-sm leading-[22px] text-[#414c5e] sm:text-base">
|
|
63
|
+
{authorRole}
|
|
64
|
+
</p>
|
|
65
|
+
) : null}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{portrait?.src ? (
|
|
71
|
+
<div className="relative mx-auto mt-6 h-56 w-full max-w-sm overflow-hidden sm:h-64 lg:absolute lg:right-0 lg:top-1/2 lg:mt-0 lg:h-[228px] lg:w-[342px] lg:max-w-none lg:-translate-y-1/2">
|
|
72
|
+
<SafeImage
|
|
73
|
+
src={portrait.src}
|
|
74
|
+
alt={portrait.alt ?? authorName}
|
|
75
|
+
fill
|
|
76
|
+
className="object-cover object-center"
|
|
77
|
+
sizes="(max-width: 1024px) 90vw, 342px"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
) : null}
|
|
81
|
+
</div>
|
|
82
|
+
</section>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Testimonial05.propTypes = testimonial05PropTypes;
|
|
87
|
+
|
|
88
|
+
export default Testimonial05;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import PropTypes from "prop-types";
|
|
2
|
+
|
|
3
|
+
const imageShape = PropTypes.shape({
|
|
4
|
+
src: PropTypes.string.isRequired,
|
|
5
|
+
alt: PropTypes.string,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const testimonial05PropTypes = {
|
|
9
|
+
/** Main testimonial quote text. */
|
|
10
|
+
quote: PropTypes.string,
|
|
11
|
+
/** Author full name. */
|
|
12
|
+
authorName: PropTypes.string,
|
|
13
|
+
/** Author role or title. */
|
|
14
|
+
authorRole: PropTypes.string,
|
|
15
|
+
/** Portrait image shown overlapping the card edge. */
|
|
16
|
+
portrait: imageShape,
|
|
17
|
+
className: PropTypes.string,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const testimonial05DefaultProps = {
|
|
21
|
+
quote:
|
|
22
|
+
"The foundation of our success lies in trust, innovation, and unwavering commitment to excellence. We strive to empower our clients with seamless solutions, ensuring growth and long-term prosperity.",
|
|
23
|
+
authorName: "Ahsanur Rahman",
|
|
24
|
+
authorRole: "Chief Executive Officer",
|
|
25
|
+
portrait: {
|
|
26
|
+
src: "/testimonials/testimonial05/portrait.jpg",
|
|
27
|
+
alt: "Ahsanur Rahman",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Testimonial05, default } from "./Testimonial05";
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import SafeImage from "../../atoms/SafeImage";
|
|
6
|
+
import { useCarousel } from "../../hooks/useCarousel";
|
|
7
|
+
import {
|
|
8
|
+
testimonial06DefaultProps,
|
|
9
|
+
testimonial06PropTypes,
|
|
10
|
+
} from "./Testimonial06.propTypes";
|
|
11
|
+
|
|
12
|
+
function getCardsPerView() {
|
|
13
|
+
if (typeof window === "undefined") return 1;
|
|
14
|
+
if (window.matchMedia("(min-width: 1280px)").matches) return 4;
|
|
15
|
+
if (window.matchMedia("(min-width: 1024px)").matches) return 3;
|
|
16
|
+
if (window.matchMedia("(min-width: 640px)").matches) return 2;
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ChevronIcon({ direction = "right", className = "" }) {
|
|
21
|
+
return (
|
|
22
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
|
|
23
|
+
<path
|
|
24
|
+
d={direction === "right" ? "M9 6l6 6-6 6" : "M15 6l-6 6 6 6"}
|
|
25
|
+
stroke="currentColor"
|
|
26
|
+
strokeWidth="1.5"
|
|
27
|
+
strokeLinecap="round"
|
|
28
|
+
strokeLinejoin="round"
|
|
29
|
+
/>
|
|
30
|
+
</svg>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Testimonial06 — VisaVerse client stories carousel with rounded cards and page counter.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} props - See Testimonial06.propTypes.js.
|
|
38
|
+
*/
|
|
39
|
+
export function Testimonial06({
|
|
40
|
+
headlinePrefix = testimonial06DefaultProps.headlinePrefix,
|
|
41
|
+
headlineAccent = testimonial06DefaultProps.headlineAccent,
|
|
42
|
+
description = testimonial06DefaultProps.description,
|
|
43
|
+
ctaText = testimonial06DefaultProps.ctaText,
|
|
44
|
+
ctaHref = testimonial06DefaultProps.ctaHref,
|
|
45
|
+
onCtaClick,
|
|
46
|
+
testimonials = testimonial06DefaultProps.testimonials,
|
|
47
|
+
activeSlide: activeSlideProp = testimonial06DefaultProps.activeSlide,
|
|
48
|
+
onSlideChange,
|
|
49
|
+
autoPlayMs = testimonial06DefaultProps.autoPlayMs,
|
|
50
|
+
className = "",
|
|
51
|
+
}) {
|
|
52
|
+
const [perView, setPerView] = useState(4);
|
|
53
|
+
const maxIndex = Math.max(0, testimonials.length - 1);
|
|
54
|
+
|
|
55
|
+
const handleChange = useCallback(
|
|
56
|
+
(index) => {
|
|
57
|
+
onSlideChange?.(index);
|
|
58
|
+
},
|
|
59
|
+
[onSlideChange]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const { activeSlide, goTo, next, prev, pause, resume } = useCarousel({
|
|
63
|
+
count: testimonials.length,
|
|
64
|
+
initialIndex: activeSlideProp,
|
|
65
|
+
autoPlayMs,
|
|
66
|
+
onChange: handleChange,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const update = () => setPerView(getCardsPerView());
|
|
71
|
+
update();
|
|
72
|
+
window.addEventListener("resize", update);
|
|
73
|
+
return () => window.removeEventListener("resize", update);
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
goTo(Math.min(activeSlideProp, maxIndex));
|
|
78
|
+
}, [activeSlideProp, goTo, maxIndex]);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (activeSlide > maxIndex) goTo(maxIndex);
|
|
82
|
+
}, [activeSlide, goTo, maxIndex]);
|
|
83
|
+
|
|
84
|
+
const CtaTag = ctaHref ? "a" : "button";
|
|
85
|
+
const cardStepPercent = 100 / perView;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<section
|
|
89
|
+
className={[
|
|
90
|
+
"relative w-full overflow-hidden bg-white",
|
|
91
|
+
"px-4 py-16 sm:px-6 sm:py-20 md:px-10 lg:px-20 lg:py-[100px] xl:px-[80px]",
|
|
92
|
+
className,
|
|
93
|
+
]
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.join(" ")}
|
|
96
|
+
data-testimonial="testimonial06"
|
|
97
|
+
onMouseEnter={pause}
|
|
98
|
+
onMouseLeave={resume}
|
|
99
|
+
>
|
|
100
|
+
<div className="mx-auto flex w-full max-w-[1840px] flex-col items-center gap-12 sm:gap-16 lg:gap-20">
|
|
101
|
+
<header className="flex w-full max-w-[910px] flex-col items-center gap-8 text-center sm:gap-10">
|
|
102
|
+
<h2 className="text-3xl font-semibold leading-tight tracking-[-0.02em] text-[#242424] sm:text-4xl lg:text-[54px] lg:leading-[52px]">
|
|
103
|
+
{headlinePrefix}
|
|
104
|
+
<span className="text-[#283aff]">{headlineAccent}</span>
|
|
105
|
+
</h2>
|
|
106
|
+
|
|
107
|
+
<div className="flex flex-col items-center gap-6 sm:gap-8">
|
|
108
|
+
{description ? (
|
|
109
|
+
<p className="max-w-[910px] text-base leading-normal tracking-[-0.02em] text-[#242424] sm:text-lg lg:text-xl">
|
|
110
|
+
{description}
|
|
111
|
+
</p>
|
|
112
|
+
) : null}
|
|
113
|
+
|
|
114
|
+
{ctaText ? (
|
|
115
|
+
<CtaTag
|
|
116
|
+
{...(ctaHref
|
|
117
|
+
? { href: ctaHref }
|
|
118
|
+
: { type: "button", onClick: onCtaClick })}
|
|
119
|
+
className="inline-flex h-12 items-center justify-center rounded-full bg-[#283aff] px-5 text-base font-medium tracking-[-0.02em] text-white transition-colors duration-200 ease-out hover:bg-[#1f2ecc] focus-visible:outline-2 focus-visible:outline-offset-2 sm:text-lg"
|
|
120
|
+
>
|
|
121
|
+
{ctaText}
|
|
122
|
+
</CtaTag>
|
|
123
|
+
) : null}
|
|
124
|
+
</div>
|
|
125
|
+
</header>
|
|
126
|
+
|
|
127
|
+
<div className="w-full overflow-hidden">
|
|
128
|
+
<div
|
|
129
|
+
className="flex gap-5 transition-transform duration-500 ease-out motion-reduce:transition-none"
|
|
130
|
+
style={{ transform: `translateX(-${activeSlide * cardStepPercent}%)` }}
|
|
131
|
+
>
|
|
132
|
+
{testimonials.map((item) => (
|
|
133
|
+
<article
|
|
134
|
+
key={item.id ?? item.authorName}
|
|
135
|
+
className="flex w-full shrink-0 flex-col justify-between rounded-[40px] border-2 border-[#e5e7eb] bg-white p-6 sm:w-1/2 sm:p-8 lg:w-1/3 lg:min-h-[340px] xl:w-1/4"
|
|
136
|
+
>
|
|
137
|
+
<div className="flex items-center gap-5">
|
|
138
|
+
{item.avatar?.src ? (
|
|
139
|
+
<div className="relative size-14 shrink-0 overflow-hidden rounded-full sm:size-[70px]">
|
|
140
|
+
<SafeImage
|
|
141
|
+
src={item.avatar.src}
|
|
142
|
+
alt={item.avatar.alt ?? item.authorName}
|
|
143
|
+
fill
|
|
144
|
+
className="object-cover object-center"
|
|
145
|
+
sizes="70px"
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
) : null}
|
|
149
|
+
<div className="min-w-0 text-left">
|
|
150
|
+
<p className="text-lg font-medium tracking-[-0.02em] text-[#242424] sm:text-xl lg:text-2xl">
|
|
151
|
+
{item.authorName}
|
|
152
|
+
</p>
|
|
153
|
+
{item.authorLocation ? (
|
|
154
|
+
<p className="text-base tracking-[-0.02em] text-[#242424] sm:text-lg lg:text-xl">
|
|
155
|
+
{item.authorLocation}
|
|
156
|
+
</p>
|
|
157
|
+
) : null}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<p className="mt-8 text-base leading-normal tracking-[-0.02em] text-[#242424] sm:mt-10 sm:text-lg">
|
|
162
|
+
{item.quote}
|
|
163
|
+
</p>
|
|
164
|
+
</article>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
{testimonials.length > 1 ? (
|
|
170
|
+
<div className="inline-flex items-center gap-4 rounded-full border border-[#f3f4f6] bg-white px-2 py-1.5">
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
aria-label="Previous testimonials"
|
|
174
|
+
onClick={prev}
|
|
175
|
+
disabled={activeSlide === 0}
|
|
176
|
+
className="flex size-10 items-center justify-center rounded-full text-[#242424] transition-colors duration-200 ease-out hover:bg-[#f3f4f6] focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-40"
|
|
177
|
+
>
|
|
178
|
+
<ChevronIcon direction="left" className="size-5" />
|
|
179
|
+
</button>
|
|
180
|
+
|
|
181
|
+
<p className="min-w-[3rem] text-center text-base font-medium tracking-[-0.02em] text-[#242424] sm:text-lg">
|
|
182
|
+
{activeSlide + 1}/{testimonials.length}
|
|
183
|
+
</p>
|
|
184
|
+
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
aria-label="Next testimonials"
|
|
188
|
+
onClick={next}
|
|
189
|
+
disabled={activeSlide >= maxIndex}
|
|
190
|
+
className="flex size-10 items-center justify-center rounded-full text-[#242424] transition-colors duration-200 ease-out hover:bg-[#f3f4f6] focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-40"
|
|
191
|
+
>
|
|
192
|
+
<ChevronIcon direction="right" className="size-5" />
|
|
193
|
+
</button>
|
|
194
|
+
</div>
|
|
195
|
+
) : null}
|
|
196
|
+
</div>
|
|
197
|
+
</section>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
Testimonial06.propTypes = testimonial06PropTypes;
|
|
202
|
+
|
|
203
|
+
export default Testimonial06;
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
authorLocation: PropTypes.string,
|
|
13
|
+
avatar: imageShape,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const testimonial06PropTypes = {
|
|
17
|
+
/** Section headline before the accent span. */
|
|
18
|
+
headlinePrefix: PropTypes.string,
|
|
19
|
+
/** Accent portion of the headline (rendered in brand blue). */
|
|
20
|
+
headlineAccent: PropTypes.string,
|
|
21
|
+
/** Supporting description below the headline. */
|
|
22
|
+
description: PropTypes.string,
|
|
23
|
+
/** Primary CTA label. */
|
|
24
|
+
ctaText: PropTypes.string,
|
|
25
|
+
/** CTA link; renders a button when omitted. */
|
|
26
|
+
ctaHref: PropTypes.string,
|
|
27
|
+
/** CTA click handler when no href is provided. */
|
|
28
|
+
onCtaClick: PropTypes.func,
|
|
29
|
+
/** Testimonial cards for the carousel. */
|
|
30
|
+
testimonials: PropTypes.arrayOf(testimonialItemShape),
|
|
31
|
+
/** Controlled active page index (0-based). */
|
|
32
|
+
activeSlide: PropTypes.number,
|
|
33
|
+
/** Called when the active page changes. */
|
|
34
|
+
onSlideChange: PropTypes.func,
|
|
35
|
+
/** Auto-advance interval in ms; 0 = off. */
|
|
36
|
+
autoPlayMs: PropTypes.number,
|
|
37
|
+
className: PropTypes.string,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const testimonial06DefaultProps = {
|
|
41
|
+
headlinePrefix: "Hear from Our ",
|
|
42
|
+
headlineAccent: "Happy Clients",
|
|
43
|
+
description:
|
|
44
|
+
"At Visaverse, we're passionate about every individual immigration journey. Read the inspiring stories of individuals who have successfully achieved their Canadian dreams with us!",
|
|
45
|
+
ctaText: "Start Your Journey",
|
|
46
|
+
ctaHref: "#",
|
|
47
|
+
testimonials: [
|
|
48
|
+
{
|
|
49
|
+
id: "maria",
|
|
50
|
+
quote:
|
|
51
|
+
"I was overwhelmed with the idea of immigrating to Canada, but VisaVerse made it all so simple. They explained every step in detail, addressed my concerns, and provided the exact services I needed.",
|
|
52
|
+
authorName: "Maria Farnandez",
|
|
53
|
+
authorLocation: "Brazil",
|
|
54
|
+
avatar: {
|
|
55
|
+
src: "/testimonials/testimonial06/avatar-maria.jpg",
|
|
56
|
+
alt: "Maria Farnandez",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "sarah",
|
|
61
|
+
quote:
|
|
62
|
+
"VisaVerse made my Permanent Residency journey a breeze! From the initial consultation to receiving my PR, they guided me step by step. Their expertise and clear communication ensured there were no surprises along the way.",
|
|
63
|
+
authorName: "Sarah Singh",
|
|
64
|
+
authorLocation: "India",
|
|
65
|
+
avatar: {
|
|
66
|
+
src: "/testimonials/testimonial06/avatar-sarah.jpg",
|
|
67
|
+
alt: "Sarah Singh",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "ahmed",
|
|
72
|
+
quote:
|
|
73
|
+
"The team at VisaVerse was outstanding! They handled everything from paperwork to follow-ups with the Canadian authorities. It felt like I was in good hands the entire time.",
|
|
74
|
+
authorName: "Ahmed Usman",
|
|
75
|
+
authorLocation: "UAE",
|
|
76
|
+
avatar: {
|
|
77
|
+
src: "/testimonials/testimonial06/avatar-ahmed.jpg",
|
|
78
|
+
alt: "Ahmed Usman",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "danzel",
|
|
83
|
+
quote:
|
|
84
|
+
"VisaVerse was professional, attentive, and reliable throughout the entire process. From the first consultation to finally receiving my work permit, they were always available to answer questions and ensure everything was on track.",
|
|
85
|
+
authorName: "Danzel Raymond",
|
|
86
|
+
authorLocation: "USA",
|
|
87
|
+
avatar: {
|
|
88
|
+
src: "/testimonials/testimonial06/avatar-danzel.jpg",
|
|
89
|
+
alt: "Danzel Raymond",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "lisa",
|
|
94
|
+
quote:
|
|
95
|
+
"Their personalized approach made all the difference. Every question was answered patiently, and I always knew what to expect next in my immigration process.",
|
|
96
|
+
authorName: "Lisa Chen",
|
|
97
|
+
authorLocation: "China",
|
|
98
|
+
avatar: {
|
|
99
|
+
src: "/testimonials/testimonial06/avatar-lisa.jpg",
|
|
100
|
+
alt: "Lisa Chen",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
activeSlide: 0,
|
|
105
|
+
autoPlayMs: 0,
|
|
106
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Testimonial06, default } from "./Testimonial06";
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import SafeImage from "../../atoms/SafeImage";
|
|
6
|
+
import { useCarousel } from "../../hooks/useCarousel";
|
|
7
|
+
import {
|
|
8
|
+
testimonial07DefaultProps,
|
|
9
|
+
testimonial07PropTypes,
|
|
10
|
+
} from "./Testimonial07.propTypes";
|
|
11
|
+
|
|
12
|
+
const DRAG_THRESHOLD = 48;
|
|
13
|
+
|
|
14
|
+
function ChevronIcon({ direction = "right", className = "" }) {
|
|
15
|
+
return (
|
|
16
|
+
<svg viewBox="0 0 30 30" fill="none" aria-hidden="true" className={className}>
|
|
17
|
+
<path
|
|
18
|
+
d={direction === "right" ? "M12 8l6 7-6 7" : "M18 8l-6 7 6 7"}
|
|
19
|
+
stroke="currentColor"
|
|
20
|
+
strokeWidth="1.5"
|
|
21
|
+
strokeLinecap="round"
|
|
22
|
+
strokeLinejoin="round"
|
|
23
|
+
/>
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Testimonial07 — Minimal serif headline slider with quote, portrait, and dot indicators.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} props - See Testimonial07.propTypes.js.
|
|
32
|
+
*/
|
|
33
|
+
export function Testimonial07({
|
|
34
|
+
headline = testimonial07DefaultProps.headline,
|
|
35
|
+
testimonials = testimonial07DefaultProps.testimonials,
|
|
36
|
+
activeSlide: activeSlideProp = testimonial07DefaultProps.activeSlide,
|
|
37
|
+
onSlideChange,
|
|
38
|
+
autoPlayMs = testimonial07DefaultProps.autoPlayMs,
|
|
39
|
+
className = "",
|
|
40
|
+
}) {
|
|
41
|
+
const maxIndex = Math.max(0, testimonials.length - 1);
|
|
42
|
+
const { activeSlide, goTo, next, prev, pause, resume } = useCarousel({
|
|
43
|
+
count: testimonials.length,
|
|
44
|
+
initialIndex: activeSlideProp,
|
|
45
|
+
loop: true,
|
|
46
|
+
autoPlayMs,
|
|
47
|
+
onChange: onSlideChange,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const [dragOffset, setDragOffset] = useState(0);
|
|
51
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
52
|
+
const dragStartX = useRef(0);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
goTo(Math.min(activeSlideProp, maxIndex));
|
|
56
|
+
}, [activeSlideProp, goTo, maxIndex]);
|
|
57
|
+
|
|
58
|
+
const finishDrag = (delta) => {
|
|
59
|
+
setIsDragging(false);
|
|
60
|
+
setDragOffset(0);
|
|
61
|
+
resume();
|
|
62
|
+
|
|
63
|
+
if (delta < -DRAG_THRESHOLD) {
|
|
64
|
+
next();
|
|
65
|
+
} else if (delta > DRAG_THRESHOLD) {
|
|
66
|
+
prev();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handlePointerDown = (event) => {
|
|
71
|
+
dragStartX.current = event.clientX;
|
|
72
|
+
setIsDragging(true);
|
|
73
|
+
pause();
|
|
74
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handlePointerMove = (event) => {
|
|
78
|
+
if (!isDragging) return;
|
|
79
|
+
setDragOffset(event.clientX - dragStartX.current);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handlePointerUp = (event) => {
|
|
83
|
+
if (!isDragging) return;
|
|
84
|
+
finishDrag(event.clientX - dragStartX.current);
|
|
85
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
86
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handlePointerCancel = (event) => {
|
|
91
|
+
if (!isDragging) return;
|
|
92
|
+
finishDrag(event.clientX - dragStartX.current);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const trackTransform = isDragging
|
|
96
|
+
? `translateX(calc(-${activeSlide * 100}% + ${dragOffset}px))`
|
|
97
|
+
: `translateX(-${activeSlide * 100}%)`;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<section
|
|
101
|
+
className={[
|
|
102
|
+
"relative w-full overflow-hidden bg-white",
|
|
103
|
+
"px-4 py-14 sm:px-6 sm:py-16 md:px-10 lg:px-20 lg:py-[100px] xl:px-[80px]",
|
|
104
|
+
className,
|
|
105
|
+
]
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.join(" ")}
|
|
108
|
+
data-testimonial="testimonial07"
|
|
109
|
+
onMouseEnter={pause}
|
|
110
|
+
onMouseLeave={resume}
|
|
111
|
+
>
|
|
112
|
+
<div className="mx-auto flex w-full max-w-[1366px] flex-col gap-10 lg:gap-14">
|
|
113
|
+
<h2 className="text-center font-serif text-2xl font-medium uppercase leading-tight tracking-[0.02em] text-[#1b1b1b] sm:text-3xl lg:text-[40px] lg:leading-[50px]">
|
|
114
|
+
{headline}
|
|
115
|
+
</h2>
|
|
116
|
+
|
|
117
|
+
<div className="relative">
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
aria-label="Previous testimonial"
|
|
121
|
+
onClick={prev}
|
|
122
|
+
className="absolute left-0 top-1/2 z-10 hidden -translate-y-1/2 p-2 text-[#1b1b1b] transition-opacity duration-200 ease-out hover:opacity-70 focus-visible:outline-2 focus-visible:outline-offset-2 sm:flex"
|
|
123
|
+
>
|
|
124
|
+
<ChevronIcon direction="left" className="size-7 lg:size-8" />
|
|
125
|
+
</button>
|
|
126
|
+
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
aria-label="Next testimonial"
|
|
130
|
+
onClick={next}
|
|
131
|
+
className="absolute right-0 top-1/2 z-10 hidden -translate-y-1/2 p-2 text-[#1b1b1b] transition-opacity duration-200 ease-out hover:opacity-70 focus-visible:outline-2 focus-visible:outline-offset-2 sm:flex"
|
|
132
|
+
>
|
|
133
|
+
<ChevronIcon direction="right" className="size-7 lg:size-8" />
|
|
134
|
+
</button>
|
|
135
|
+
|
|
136
|
+
<div
|
|
137
|
+
className="cursor-grab overflow-hidden px-0 touch-pan-y active:cursor-grabbing sm:px-12 lg:px-16"
|
|
138
|
+
onPointerDown={handlePointerDown}
|
|
139
|
+
onPointerMove={handlePointerMove}
|
|
140
|
+
onPointerUp={handlePointerUp}
|
|
141
|
+
onPointerCancel={handlePointerCancel}
|
|
142
|
+
>
|
|
143
|
+
<div
|
|
144
|
+
className={[
|
|
145
|
+
"flex motion-reduce:transition-none",
|
|
146
|
+
isDragging
|
|
147
|
+
? "transition-none"
|
|
148
|
+
: "transition-transform duration-500 ease-out",
|
|
149
|
+
].join(" ")}
|
|
150
|
+
style={{ transform: trackTransform }}
|
|
151
|
+
>
|
|
152
|
+
{testimonials.map((item, index) => (
|
|
153
|
+
<article
|
|
154
|
+
key={item.id ?? item.authorName}
|
|
155
|
+
className="flex w-full shrink-0 flex-col gap-10 lg:flex-row lg:items-center lg:justify-between lg:gap-16"
|
|
156
|
+
>
|
|
157
|
+
<div
|
|
158
|
+
key={index === activeSlide ? `content-${activeSlide}` : item.id}
|
|
159
|
+
className={[
|
|
160
|
+
"flex max-w-xl flex-col gap-8 lg:max-w-[442px]",
|
|
161
|
+
index === activeSlide ? "animate-fade-in" : "",
|
|
162
|
+
].join(" ")}
|
|
163
|
+
>
|
|
164
|
+
<blockquote className="text-lg font-light leading-[1.5] text-[#1b1b1b] sm:text-xl">
|
|
165
|
+
“{item.quote}”
|
|
166
|
+
</blockquote>
|
|
167
|
+
|
|
168
|
+
<div>
|
|
169
|
+
<p className="text-lg font-bold text-[#1b1b1b]">
|
|
170
|
+
{item.authorName}
|
|
171
|
+
</p>
|
|
172
|
+
{item.authorRole ? (
|
|
173
|
+
<p className="text-xs font-light uppercase tracking-[0.08em] text-[#1b1b1b]">
|
|
174
|
+
{item.authorRole}
|
|
175
|
+
</p>
|
|
176
|
+
) : null}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
{item.image?.src ? (
|
|
181
|
+
<div
|
|
182
|
+
key={index === activeSlide ? `photo-${activeSlide}` : item.id}
|
|
183
|
+
className={[
|
|
184
|
+
"relative mx-auto size-[220px] shrink-0 overflow-hidden bg-[#c4c4c4] sm:size-[250px] lg:mx-0 lg:size-[278px]",
|
|
185
|
+
index === activeSlide ? "animate-fade-in" : "",
|
|
186
|
+
].join(" ")}
|
|
187
|
+
>
|
|
188
|
+
<SafeImage
|
|
189
|
+
src={item.image.src}
|
|
190
|
+
alt={item.image.alt ?? item.authorName}
|
|
191
|
+
fill
|
|
192
|
+
className="object-cover object-center transition-transform duration-500 ease-out motion-reduce:transition-none"
|
|
193
|
+
sizes="(max-width: 1024px) 80vw, 278px"
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
) : null}
|
|
197
|
+
</article>
|
|
198
|
+
))}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div className="flex items-center justify-center gap-2">
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
aria-label="Previous testimonial"
|
|
207
|
+
onClick={prev}
|
|
208
|
+
className="flex p-2 text-[#1b1b1b] transition-opacity duration-200 ease-out hover:opacity-70 focus-visible:outline-2 focus-visible:outline-offset-2 sm:hidden"
|
|
209
|
+
>
|
|
210
|
+
<ChevronIcon direction="left" className="size-6" />
|
|
211
|
+
</button>
|
|
212
|
+
|
|
213
|
+
{testimonials.map((item, index) => (
|
|
214
|
+
<button
|
|
215
|
+
key={item.id ?? item.authorName}
|
|
216
|
+
type="button"
|
|
217
|
+
aria-label={`Go to slide ${index + 1}`}
|
|
218
|
+
aria-current={activeSlide === index ? "true" : undefined}
|
|
219
|
+
onClick={() => goTo(index)}
|
|
220
|
+
className={[
|
|
221
|
+
"rounded-full transition-all duration-300 ease-out motion-reduce:transition-none",
|
|
222
|
+
activeSlide === index
|
|
223
|
+
? "size-2.5 bg-[#408ec2]"
|
|
224
|
+
: "size-2 bg-[#eeeeee] hover:bg-[#d8d8d8]",
|
|
225
|
+
].join(" ")}
|
|
226
|
+
/>
|
|
227
|
+
))}
|
|
228
|
+
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
aria-label="Next testimonial"
|
|
232
|
+
onClick={next}
|
|
233
|
+
className="flex p-2 text-[#1b1b1b] transition-opacity duration-200 ease-out hover:opacity-70 focus-visible:outline-2 focus-visible:outline-offset-2 sm:hidden"
|
|
234
|
+
>
|
|
235
|
+
<ChevronIcon direction="right" className="size-6" />
|
|
236
|
+
</button>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</section>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
Testimonial07.propTypes = testimonial07PropTypes;
|
|
244
|
+
|
|
245
|
+
export default Testimonial07;
|