-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTestimonials.tsx
More file actions
199 lines (188 loc) · 7.3 KB
/
Copy pathTestimonials.tsx
File metadata and controls
199 lines (188 loc) · 7.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
"use client";
import { useState } from "react";
import Image from "next/image";
import StarRating from "@/components/ui/StarRating";
import Reveal from "@/components/ui/Reveal";
/**
* Orbit slot positions, as a percentage of the square orbit container's
* side. Loosely derived from the Figma node `101:281` ("Group
* 1000002331") ellipses `42`, `45`, `46`, which sit near the top-right,
* left, and bottom-right of the orbit rings respectively — mirrored here
* as three fixed tab positions so a given testimonial always orbits in
* the same spot instead of reshuffling on click.
*/
const SLOTS = [
{ top: "4%", left: "58%" },
{ top: "36%", left: "0%" },
{ top: "78%", left: "64%" },
] as const;
const TESTIMONIALS = [
{
avatar: "/testimonials/avatar-1.jpg",
name: "Mariana Tanaka",
role: "Diretora financeira",
headline: "Fechei o mês no azul pela primeira vez em anos",
quote:
"Eu tinha conta em três bancos e nunca sabia o saldo real. Conectei tudo em dez minutos e no primeiro mês descobri R$ 640 em assinaturas que eu nem lembrava que existiam.",
rating: 5,
},
{
avatar: "/testimonials/avatar-2.jpg",
name: "Rodrigo Alencar",
role: "Gerente de produto",
headline: "Finalmente um orçamento que eu consigo manter",
quote:
"O aviso de 80% do limite mudou tudo. Antes eu descobria o estrago quando a fatura chegava; agora ajusto no meio do mês e ainda sobra para investir.",
rating: 5,
},
{
avatar: "/testimonials/avatar-3.jpg",
name: "Camila Duarte",
role: "Designer freelancer",
headline: "Separar o dinheiro da PJ ficou simples",
quote:
"Como freelancer, minha renda varia todo mês. As metas por categoria me mostram quanto posso tirar sem comprometer os impostos. Só senti falta de um relatório anual para o contador.",
rating: 4,
},
] as const;
export default function Testimonials() {
const [activeIndex, setActiveIndex] = useState(0);
const active = TESTIMONIALS[activeIndex];
function selectByOffset(offset: number) {
setActiveIndex(
(current) =>
(current + offset + TESTIMONIALS.length) % TESTIMONIALS.length,
);
}
return (
<section
id="testimonials"
className="relative mx-auto max-w-(--spacing-container) px-6 pt-8 pb-24 lg:px-0 lg:pt-[40px] lg:pb-[180px]"
>
<Reveal className="mx-auto max-w-[455px] text-center lg:mx-0 lg:max-w-none lg:text-left">
<p className="font-display text-[18px] leading-[28px] font-medium tracking-[2.88px] text-accent uppercase">
depoimentos
</p>
<h2 className="mt-1 font-display text-[36px] leading-[40px] font-bold lg:text-[48px] lg:leading-[48px]">
O que nossos usuários dizem
</h2>
</Reveal>
<div className="mt-12 grid items-center gap-12 lg:grid-cols-[600px_1fr] lg:gap-12">
<Reveal
variant="zoom"
className="relative mx-auto aspect-square w-full max-w-[420px] lg:max-w-[600px]"
>
<div className="absolute inset-[7%] rounded-full border border-black/10" />
<div className="absolute inset-[3.5%] rounded-full border border-black/10" />
<div className="absolute inset-0 rounded-full border border-black/10" />
<div
role="tablist"
aria-label="Depoimentos"
className="absolute inset-0"
>
{TESTIMONIALS.map((testimonial, index) => {
const isActive = index === activeIndex;
return (
<button
key={testimonial.name}
type="button"
role="tab"
id={`testimonial-tab-${index}`}
aria-selected={isActive}
aria-controls="testimonial-panel"
tabIndex={isActive ? 0 : -1}
onClick={() => setActiveIndex(index)}
onKeyDown={(event) => {
if (event.key === "ArrowRight") {
event.preventDefault();
selectByOffset(1);
} else if (event.key === "ArrowLeft") {
event.preventDefault();
selectByOffset(-1);
}
}}
className={`absolute h-[18%] w-[18%] overflow-hidden rounded-full transition-opacity ${
isActive
? "ring-2 ring-accent ring-offset-2"
: "opacity-60 hover:opacity-100"
}`}
style={SLOTS[index]}
>
<Image
src={testimonial.avatar}
alt={testimonial.name}
fill
sizes="80px"
className="object-cover"
/>
</button>
);
})}
</div>
<div
aria-hidden
className="absolute top-1/2 left-1/2 h-[46%] w-[46%] -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-full"
>
<Image
src={active.avatar}
alt=""
fill
sizes="(min-width: 1024px) 280px, 200px"
className="object-cover"
/>
</div>
<div
aria-hidden
className="absolute top-1/2 left-1/2 flex h-[13%] w-[13%] translate-x-[70%] -translate-y-[130%] items-center justify-center rounded-full bg-accent"
>
<Image src="/icons/quote.svg" alt="" width={20} height={16} />
</div>
</Reveal>
<Reveal
delay={120}
id="testimonial-panel"
role="tabpanel"
aria-labelledby={`testimonial-tab-${activeIndex}`}
className="mx-auto max-w-[442px] text-center lg:mx-0 lg:max-w-[442px] lg:text-left"
>
{/* Keyed on the active tab so switching testimonials replays the
crossfade instead of swapping the text in place. */}
<div
key={activeIndex}
className="animate-reveal-in motion-reduce:animate-none"
>
<h3 className="font-display text-[24px] leading-[32px] font-bold lg:text-[28px] lg:leading-[36px]">
{active.headline}
</h3>
<StarRating
value={active.rating}
className="mt-4 justify-center lg:justify-start"
/>
<p className="mt-4 font-display text-[18px] leading-[28px] font-medium text-black/50">
“{active.quote}”
</p>
<div className="mt-6 flex items-center justify-center gap-3 lg:justify-start">
<div className="relative h-10 w-10 overflow-hidden rounded-full">
<Image
src={active.avatar}
alt=""
fill
sizes="40px"
className="object-cover"
/>
</div>
<div className="text-left">
<p className="font-display text-[16px] leading-[22px] font-semibold">
{active.name}
</p>
<p className="font-display text-[14px] leading-[20px] font-medium text-black/50">
{active.role}
</p>
</div>
</div>
</div>
</Reveal>
</div>
</section>
);
}