Todos os componentes

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.

Abrir no catálogo

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

index.tsx
"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 }
              }
            >
              &ldquo;{testimonials[index].quote}&rdquo;
            </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;

Componentes parecidos