Testimonials 1
Testimonials 1 é um componente de layout para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
SmoothUI
MIT
layout
O que é
Testimonials 1 é um componente de layout para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i motion
Licença
Este componente vem de SmoothUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Testimonials 1
"use client";
import { getAvatarUrl, getTestimonials } from "@/lib/smoothui-data";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useEffect, useRef, useState } from "react";
const TESTIMONIAL_COUNT = 4;
const AVATAR_SIZE = 96;
const DURATION = 5000; // ms
const BAR_WIDTH = 50;
const CIRCLE_SIZE = 12;
const BORDER_RADIUS_ACTIVE = 8;
const BORDER_RADIUS_INACTIVE = 999;
const MILLISECONDS_TO_SECONDS = 1000;
const testimonials = getTestimonials(TESTIMONIAL_COUNT).map((testimonial) => ({
avatar: testimonial.avatar,
name: testimonial.name,
quote: testimonial.content || "",
role: testimonial.role,
}));
export function TestimonialsSimple() {
const shouldReduceMotion = useReducedMotion();
const [index, setIndex] = useState(0);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
timeoutRef.current = setTimeout(() => {
setIndex((prev) => (prev + 1) % testimonials.length);
}, DURATION);
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return (
<section className="relative flex flex-col items-center bg-background py-16">
<div className="flex w-full max-w-5xl flex-col items-center justify-center px-4">
<div className="min-h-[120px] w-full">
<AnimatePresence mode="wait">
<motion.blockquote
animate={
shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
}
className="mb-8 text-center font-semibold text-2xl text-foreground leading-tight md:text-4xl"
exit={
shouldReduceMotion
? { opacity: 0, transition: { duration: 0 } }
: { opacity: 0, y: -30 }
}
initial={
shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 30 }
}
key={index}
transition={
shouldReduceMotion
? { duration: 0 }
: { duration: 0.5, type: "spring" as const }
}
>
“{testimonials[index].quote}”
</motion.blockquote>
</AnimatePresence>
</div>
<div className="flex w-full max-w-lg items-center justify-center gap-8 pt-8">
<AnimatePresence initial={false} mode="wait">
<motion.div
animate={
shouldReduceMotion
? { opacity: 1 }
: { filter: "blur(0px)", opacity: 1 }
}
className="flex items-center gap-4"
exit={
shouldReduceMotion
? { opacity: 0, transition: { duration: 0 } }
: { filter: "blur(8px)", opacity: 0 }
}
initial={
shouldReduceMotion
? { opacity: 1 }
: { filter: "blur(8px)", opacity: 0 }
}
key={index}
transition={
shouldReduceMotion
? { duration: 0 }
: { duration: 0.5, type: "spring" as const }
}
>
<img
alt={`${testimonials[index].name} avatar`}
className="h-12 w-12 rounded-full border bg-foreground/10 object-cover"
draggable={false}
height={48}
src={getAvatarUrl(testimonials[index].avatar, AVATAR_SIZE)}
width={48}
/>
<div className="mx-4 h-8 border-muted-foreground/30 border-l" />
<div className="text-left">
<div className="font-medium text-foreground text-lg italic">
{testimonials[index].name}
</div>
<div className="text-base text-muted-foreground">
{testimonials[index].role}
</div>
</div>
</motion.div>
</AnimatePresence>
</div>
{/* Progress Bar & Circles Indicator */}
<div className="mx-auto mt-8 flex w-full max-w-lg justify-center gap-3">
{testimonials.map((testimonial, i) => {
const isActive = i === index;
return (
<motion.span
animate={
shouldReduceMotion
? {
borderRadius: isActive
? BORDER_RADIUS_ACTIVE
: BORDER_RADIUS_INACTIVE,
height: CIRCLE_SIZE,
width: isActive ? BAR_WIDTH : CIRCLE_SIZE,
}
: {
borderRadius: isActive
? BORDER_RADIUS_ACTIVE
: BORDER_RADIUS_INACTIVE,
height: CIRCLE_SIZE,
width: isActive ? BAR_WIDTH : CIRCLE_SIZE,
}
}
className="relative block overflow-hidden bg-foreground/10"
initial={false}
key={`testimonial-${testimonial.name}-${i}`}
layout={!shouldReduceMotion}
style={{
border: "none",
maxWidth: BAR_WIDTH,
minWidth: CIRCLE_SIZE,
}}
transition={
shouldReduceMotion
? { duration: 0 }
: {
damping: 30,
duration: 0.4,
stiffness: 300,
type: "spring" as const,
}
}
>
{isActive && (
<motion.div
animate={{ width: "100%" }}
className="absolute top-0 left-0 h-full rounded-lg bg-brand"
exit={{ width: 0 }}
initial={
shouldReduceMotion ? { width: "100%" } : { width: 0 }
}
key={index}
transition={
shouldReduceMotion
? { duration: 0 }
: {
duration: DURATION / MILLISECONDS_TO_SECONDS,
ease: "linear",
}
}
/>
)}
</motion.span>
);
})}
</div>
</div>
</section>
);
}
export default TestimonialsSimple;