Bob AI: Add a 3D card flip animation to testimonial cards. On the fr

This commit is contained in:
2026-02-27 10:01:53 +00:00
parent 2d1b894eab
commit 887f24ecda

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { memo } from "react"; import { memo, useState } from "react";
import CardStack from "@/components/cardStack/CardStack"; import CardStack from "@/components/cardStack/CardStack";
import MediaContent from "@/components/shared/MediaContent"; import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils"; import { cls } from "@/lib/utils";
@@ -18,6 +18,7 @@ type Testimonial = {
videoSrc?: string; videoSrc?: string;
imageAlt?: string; imageAlt?: string;
videoAriaLabel?: string; videoAriaLabel?: string;
testimonialText?: string;
}; };
interface TestimonialCardOneProps { interface TestimonialCardOneProps {
@@ -50,6 +51,7 @@ interface TestimonialCardOneProps {
nameClassName?: string; nameClassName?: string;
roleClassName?: string; roleClassName?: string;
companyClassName?: string; companyClassName?: string;
testimonialTextClassName?: string;
gridClassName?: string; gridClassName?: string;
carouselClassName?: string; carouselClassName?: string;
controlsClassName?: string; controlsClassName?: string;
@@ -69,6 +71,7 @@ interface TestimonialCardProps {
nameClassName?: string; nameClassName?: string;
roleClassName?: string; roleClassName?: string;
companyClassName?: string; companyClassName?: string;
testimonialTextClassName?: string;
} }
const TestimonialCard = memo(({ const TestimonialCard = memo(({
@@ -80,41 +83,85 @@ const TestimonialCard = memo(({
nameClassName = "", nameClassName = "",
roleClassName = "", roleClassName = "",
companyClassName = "", companyClassName = "",
testimonialTextClassName = "",
}: TestimonialCardProps) => { }: TestimonialCardProps) => {
return ( const [isFlipped, setIsFlipped] = useState(false);
<div className={cls("relative h-full rounded-theme-capped overflow-hidden group", cardClassName)}>
<MediaContent
imageSrc={testimonial.imageSrc}
videoSrc={testimonial.videoSrc}
imageAlt={testimonial.imageAlt || testimonial.name}
videoAriaLabel={testimonial.videoAriaLabel || testimonial.name}
imageClassName={cls("relative z-1 w-full h-full object-cover!", imageClassName)}
/>
<div className={cls("!absolute z-1 bottom-6 left-6 right-6 card backdrop-blur-xs p-6 flex flex-col gap-3 rounded-theme-capped", overlayClassName)}> return (
<div className={cls("relative z-1 flex gap-1", ratingClassName)}> <div
{Array.from({ length: 5 }).map((_, index) => ( className={cls("relative h-full rounded-theme-capped overflow-hidden group cursor-pointer", cardClassName)}
<Star style={{
key={index} perspective: "1000px",
className={cls( }}
"h-5 w-auto text-accent", onMouseEnter={() => setIsFlipped(true)}
index < testimonial.rating ? "fill-accent" : "fill-transparent" onMouseLeave={() => setIsFlipped(false)}
)} onClick={() => setIsFlipped(!isFlipped)}
strokeWidth={1.5} >
/> <div
))} style={{
transformStyle: "preserve-3d",
transform: isFlipped ? "rotateY(180deg)" : "rotateY(0deg)",
transition: "transform 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55)",
width: "100%",
height: "100%",
}}
>
{/* Front of card */}
<div
style={{
backfaceVisibility: "hidden",
WebkitBackfaceVisibility: "hidden",
}}
className="w-full h-full"
>
<MediaContent
imageSrc={testimonial.imageSrc}
videoSrc={testimonial.videoSrc}
imageAlt={testimonial.imageAlt || testimonial.name}
videoAriaLabel={testimonial.videoAriaLabel || testimonial.name}
imageClassName={cls("relative z-1 w-full h-full object-cover!", imageClassName)}
/>
<div className={cls("!absolute z-1 bottom-6 left-6 right-6 card backdrop-blur-xs p-6 flex flex-col gap-3 rounded-theme-capped", overlayClassName)}>
<div className={cls("relative z-1 flex gap-1", ratingClassName)}>
{Array.from({ length: 5 }).map((_, index) => (
<Star
key={index}
className={cls(
"h-5 w-auto text-accent",
index < testimonial.rating ? "fill-accent" : "fill-transparent"
)}
strokeWidth={1.5}
/>
))}
</div>
<h3 className={cls("relative z-1 text-2xl font-medium text-foreground leading-[1.1] mt-1", nameClassName)}>
{testimonial.name}
</h3>
<div className="relative z-1 flex flex-col gap-1">
<p className={cls("text-base text-foreground leading-[1.1]", roleClassName)}>
{testimonial.role}
</p>
<p className={cls("text-base text-foreground leading-[1.1]", companyClassName)}>
{testimonial.company}
</p>
</div>
</div>
</div> </div>
<h3 className={cls("relative z-1 text-2xl font-medium text-foreground leading-[1.1] mt-1", nameClassName)}> {/* Back of card */}
{testimonial.name} <div
</h3> style={{
backfaceVisibility: "hidden",
<div className="relative z-1 flex flex-col gap-1"> WebkitBackfaceVisibility: "hidden",
<p className={cls("text-base text-foreground leading-[1.1]", roleClassName)}> transform: "rotateY(180deg)",
{testimonial.role} }}
</p> className="absolute inset-0 w-full h-full card backdrop-blur-xs rounded-theme-capped p-6 flex flex-col justify-center items-center"
<p className={cls("text-base text-foreground leading-[1.1]", companyClassName)}> >
{testimonial.company} <p className={cls("text-base text-foreground leading-relaxed text-center", testimonialTextClassName)}>
{testimonial.testimonialText || "No testimonial text provided"}
</p> </p>
</div> </div>
</div> </div>
@@ -154,6 +201,7 @@ const TestimonialCardOne = ({
nameClassName = "", nameClassName = "",
roleClassName = "", roleClassName = "",
companyClassName = "", companyClassName = "",
testimonialTextClassName = "",
gridClassName = "", gridClassName = "",
carouselClassName = "", carouselClassName = "",
controlsClassName = "", controlsClassName = "",
@@ -208,6 +256,7 @@ const TestimonialCardOne = ({
nameClassName={nameClassName} nameClassName={nameClassName}
roleClassName={roleClassName} roleClassName={roleClassName}
companyClassName={companyClassName} companyClassName={companyClassName}
testimonialTextClassName={testimonialTextClassName}
/> />
))} ))}
</CardStack> </CardStack>