13 Commits

Author SHA1 Message Date
b1c46ec7bf Merge version_8 into main
Merge version_8 into main
2026-02-27 10:11:39 +00:00
c91da9f81e Bob AI: Combine first and last name into a single name field in the 2026-02-27 10:10:58 +00:00
5ec6f0830a Merge version_7 into main
Merge version_7 into main
2026-02-27 10:07:33 +00:00
5280d6c07a Bob AI: Implement a 3D card flip animation on testimonial cards. Fro 2026-02-27 10:06:52 +00:00
9a15d5b959 Merge version_6 into main
Merge version_6 into main
2026-02-27 10:02:33 +00:00
887f24ecda Bob AI: Add a 3D card flip animation to testimonial cards. On the fr 2026-02-27 10:01:53 +00:00
2d1b894eab Merge version_5 into main
Merge version_5 into main
2026-02-27 09:58:50 +00:00
cf73b46800 Bob AI: Modify the carousel component to: 1) Slow down the auto-play 2026-02-27 09:58:09 +00:00
78c98816bb Merge version_4 into main
Merge version_4 into main
2026-02-27 08:51:15 +00:00
b9fae9dc4b Bob AI: Add a blurred background image of a track/athletic field to 2026-02-27 08:50:34 +00:00
338c2ffeff Merge version_3 into main
Merge version_3 into main
2026-02-27 08:48:34 +00:00
aed064ca08 Bob AI: Add a track/athletic background image to the hero section wi 2026-02-27 08:47:53 +00:00
9f9b653fd6 Merge version_2 into main
Merge version_2 into main
2026-02-27 08:46:15 +00:00
2 changed files with 186 additions and 34 deletions

View File

@@ -1,5 +1,6 @@
"use client";
import { useState, useRef } from "react";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
@@ -44,6 +45,8 @@ interface HeroBillboardCarouselProps {
buttons?: ButtonConfig[];
buttonAnimation?: ButtonAnimationType;
mediaItems: MediaItem[];
backgroundImageSrc?: string;
backgroundImageOpacity?: number;
ariaLabel?: string;
className?: string;
containerClassName?: string;
@@ -67,6 +70,8 @@ const HeroBillboardCarousel = ({
buttons,
buttonAnimation,
mediaItems,
backgroundImageSrc,
backgroundImageOpacity = 0.3,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
@@ -79,6 +84,8 @@ const HeroBillboardCarousel = ({
buttonTextClassName = "",
mediaWrapperClassName = "",
}: HeroBillboardCarouselProps) => {
const [isHovered, setIsHovered] = useState(false);
const carouselRef = useRef<HTMLDivElement>(null);
const renderCarouselItem = (item: MediaItem, index: number) => (
<div
key={index}
@@ -98,11 +105,24 @@ const HeroBillboardCarousel = ({
<section
aria-label={ariaLabel}
className={cls(
"relative w-full py-hero-page-padding md:h-svh md:py-0",
"relative w-full py-hero-page-padding md:h-svh md:py-0 overflow-hidden",
className
)}
>
<HeroBackgrounds {...background} />
{backgroundImageSrc && (
<div
className="absolute inset-0 z-0"
style={{
backgroundImage: `url('${backgroundImageSrc}')`,
backgroundSize: 'cover',
backgroundPosition: 'center',
opacity: backgroundImageOpacity,
filter: 'blur(40px)',
}}
aria-hidden="true"
/>
)}
<div className={cls(
"mx-auto flex flex-col gap-14 md:gap-10 relative z-10",
"w-full md:w-content-width md:h-full md:items-center md:justify-center",
@@ -129,7 +149,12 @@ const HeroBillboardCarousel = ({
center={true}
/>
<div className={cls("w-full -mx-[var(--content-padding)]", mediaWrapperClassName)}>
<div
ref={carouselRef}
className={cls("w-full -mx-[var(--content-padding)] relative z-20", mediaWrapperClassName)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<AutoCarousel
title=""
description=""
@@ -141,6 +166,8 @@ const HeroBillboardCarousel = ({
itemClassName="!w-55 md:!w-carousel-item-4"
ariaLabel="Hero carousel"
showTextBox={false}
autoPlayInterval={isHovered ? 0 : 5000}
pauseOnHover={true}
>
{mediaItems?.map(renderCarouselItem)}
</AutoCarousel>

View File

@@ -1,6 +1,6 @@
"use client";
import { memo } from "react";
import { memo, useState } from "react";
import CardStack from "@/components/cardStack/CardStack";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
@@ -8,6 +8,82 @@ import { Star } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, ButtonAnimationType, CardAnimationTypeWith3D, GridVariant, TitleSegment, TextboxLayout, InvertedBackground } from "@/components/cardStack/types";
const TestimonialCardFlip = memo(({ testimonial, className }: { testimonial: Testimonial; className?: string }) => {
const [isFlipped, setIsFlipped] = useState(false);
return (
<div
className={cls(
"h-full cursor-pointer perspective",
className
)}
onClick={() => setIsFlipped(!isFlipped)}
onMouseEnter={() => setIsFlipped(true)}
onMouseLeave={() => setIsFlipped(false)}
style={{
perspective: "1000px",
}}
>
<div
style={{
transformStyle: "preserve-3d",
transform: isFlipped ? "rotateY(180deg)" : "rotateY(0deg)",
transition: "transform 0.6s ease-in-out",
width: "100%",
height: "100%",
}}
>
{/* Front side - Name and Photo */}
<div
style={{
backfaceVisibility: "hidden",
WebkitBackfaceVisibility: "hidden",
}}
className="w-full h-full flex flex-col items-center justify-center p-6 bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 rounded-lg border border-slate-200 dark:border-slate-700"
>
{testimonial.imageSrc && (
<img
src={testimonial.imageSrc}
alt={testimonial.imageAlt || testimonial.name}
className="w-24 h-24 rounded-full object-cover mb-4 border-4 border-white dark:border-slate-700 shadow-lg"
/>
)}
<h3 className="text-lg font-semibold text-slate-900 dark:text-white text-center">
{testimonial.firstName} {testimonial.lastName}
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400 text-center mt-1">
{testimonial.role}
</p>
<p className="text-xs text-slate-500 dark:text-slate-500 text-center mt-1">
{testimonial.company}
</p>
<div className="flex gap-1 mt-3">
{Array.from({ length: testimonial.rating }).map((_, i) => (
<Star key={i} size={16} className="fill-yellow-400 text-yellow-400" />
))}
</div>
</div>
{/* Back side - Testimonial Text */}
<div
style={{
backfaceVisibility: "hidden",
WebkitBackfaceVisibility: "hidden",
transform: "rotateY(180deg)",
}}
className="absolute inset-0 w-full h-full flex flex-col items-center justify-center p-6 bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-900 dark:to-indigo-900 rounded-lg border border-blue-200 dark:border-indigo-700 shadow-lg"
>
<p className="text-sm text-slate-700 dark:text-slate-200 text-center leading-relaxed italic">
"{testimonial.testimonialText || "No testimonial text provided"}"
</p>
</div>
</div>
</div>
);
});
TestimonialCardFlip.displayName = "TestimonialCardFlip";
type Testimonial = {
id: string;
name: string;
@@ -18,6 +94,7 @@ type Testimonial = {
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
testimonialText?: string;
};
interface TestimonialCardOneProps {
@@ -50,6 +127,7 @@ interface TestimonialCardOneProps {
nameClassName?: string;
roleClassName?: string;
companyClassName?: string;
testimonialTextClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
@@ -69,6 +147,7 @@ interface TestimonialCardProps {
nameClassName?: string;
roleClassName?: string;
companyClassName?: string;
testimonialTextClassName?: string;
}
const TestimonialCard = memo(({
@@ -80,41 +159,85 @@ const TestimonialCard = memo(({
nameClassName = "",
roleClassName = "",
companyClassName = "",
testimonialTextClassName = "",
}: TestimonialCardProps) => {
return (
<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)}
/>
const [isFlipped, setIsFlipped] = useState(false);
<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}
/>
))}
return (
<div
className={cls("relative h-full rounded-theme-capped overflow-hidden group cursor-pointer", cardClassName)}
style={{
perspective: "1000px",
}}
onMouseEnter={() => setIsFlipped(true)}
onMouseLeave={() => setIsFlipped(false)}
onClick={() => setIsFlipped(!isFlipped)}
>
<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>
<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}
{/* Back of card */}
<div
style={{
backfaceVisibility: "hidden",
WebkitBackfaceVisibility: "hidden",
transform: "rotateY(180deg)",
}}
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-relaxed text-center", testimonialTextClassName)}>
{testimonial.testimonialText || "No testimonial text provided"}
</p>
</div>
</div>
@@ -154,6 +277,7 @@ const TestimonialCardOne = ({
nameClassName = "",
roleClassName = "",
companyClassName = "",
testimonialTextClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
@@ -208,6 +332,7 @@ const TestimonialCardOne = ({
nameClassName={nameClassName}
roleClassName={roleClassName}
companyClassName={companyClassName}
testimonialTextClassName={testimonialTextClassName}
/>
))}
</CardStack>