Bob AI: Integrate a small testimonials/social-proof block directly i

This commit is contained in:
2026-05-03 02:46:14 +03:00
parent bae3b63223
commit 766d974889
3 changed files with 88 additions and 27 deletions

View File

@@ -1,9 +1,11 @@
import { useRef } from "react";
import { useScroll, useTransform, motion } from "motion/react";
import { Star } from "lucide-react";
import Button from "@/components/ui/Button";
import HeroBackgroundSlot from "@/components/ui/HeroBackgroundSlot";
import TextAnimation from "@/components/ui/TextAnimation";
import ImageOrVideo from "@/components/ui/ImageOrVideo";
import AvatarGroup from "@/components/ui/AvatarGroup";
type HeroBillboardScrollProps = {
tag: string;
@@ -11,6 +13,11 @@ type HeroBillboardScrollProps = {
description: string;
primaryButton: { text: string; href: string };
secondaryButton: { text: string; href: string };
socialProof?: {
avatars?: { src: string }[];
rating?: number;
text: string;
};
} & ({ imageSrc: string; videoSrc?: never } | { videoSrc: string; imageSrc?: never });
const HeroBillboardScroll = ({
@@ -19,6 +26,7 @@ const HeroBillboardScroll = ({
description,
primaryButton,
secondaryButton,
socialProof,
imageSrc,
videoSrc,
}: HeroBillboardScrollProps) => {
@@ -29,11 +37,11 @@ const HeroBillboardScroll = ({
const scale = useTransform(scrollYProgress, [0, 1], [1.05, 1]);
return (
<section aria-label="Hero section" className="relative mb-20">
<section aria-label="Hero section" className="relative">
<HeroBackgroundSlot />
<div
ref={containerRef}
className="pt-25 pb-20 md:py-30 perspective-distant"
className="perspective-distant"
>
<div className="w-content-width mx-auto">
<div className="flex flex-col items-center gap-2 text-center">
@@ -59,6 +67,33 @@ const HeroBillboardScroll = ({
<Button text={primaryButton.text} href={primaryButton.href} variant="primary"/>
<Button text={secondaryButton.text} href={secondaryButton.href} variant="secondary"animationDelay={0.1} />
</div>
{socialProof && (
<motion.div
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: 0.4, duration: 0.6 }}
className="mt-8 flex flex-col items-center gap-3"
>
{socialProof.avatars && (
<AvatarGroup avatars={socialProof.avatars} size="sm" />
)}
<div className="flex flex-col items-center gap-1">
{socialProof.rating && (
<div className="flex items-center gap-1">
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className={`size-4 ${i < socialProof.rating! ?'text-yellow-500 fill-yellow-500' : 'text-foreground/20'}`}
/>
))}
</div>
)}
<span className="text-sm font-medium">{socialProof.text}</span>
</div>
</motion.div>
)}
</div>
</div>
@@ -77,4 +112,4 @@ const HeroBillboardScroll = ({
);
};
export default HeroBillboardScroll;
export default HeroBillboardScroll;

View File

@@ -9,40 +9,55 @@ interface AvatarGroupProps {
className?: string;
}
const SIZES = {
sm: "size-8 text-xs",
md: "size-10 text-sm",
lg: "size-12 text-base",
};
const AvatarGroup = ({
avatars,
max = 4,
size = "md",
label,
labelClassName = "",
className = "",
}: AvatarGroupProps) => {
const visibleAvatars = avatars.slice(0, max);
const remainingCount = avatars.length - max;
const AvatarGroup = ({ avatars, max = 5, size = "md", label, labelClassName, className = "" }: AvatarGroupProps) => {
const visible = avatars.slice(0, max);
const remaining = avatars.length - visible.length;
const sizeClasses = {
sm: "size-8 text-xs",
md: "size-10 text-sm",
lg: "size-12 text-base",
};
return (
<div className={cls("flex items-center gap-3", className)}>
<div className="flex items-center">
{visible.map((avatar, index) => (
<div
key={index}
<div className="flex items-center -space-x-3">
{visibleAvatars.map((avatar, i) => (
<img
key={i}
src={avatar.src}
alt={`Avatar ${i + 1}`}
className={cls(
"relative shrink-0 overflow-hidden rounded-full border-2 border-background",
SIZES[size],
index > 0 && "-ml-3"
"rounded-full border-2 border-background object-cover",
sizeClasses[size]
)}
/>
))}
{remainingCount > 0 && (
<div
className={cls(
"flex items-center justify-center rounded-full border-2 border-background bg-card text-foreground font-medium",
sizeClasses[size]
)}
>
<img src={avatar.src} alt="" className="h-full w-full object-cover" />
</div>
))}
{remaining > 0 && (
<div className={cls("flex items-center justify-center shrink-0 rounded-full border-2 border-background secondary-button font-medium text-secondary-cta-text -ml-3", SIZES[size])}>
+{remaining}
+{remainingCount}
</div>
)}
</div>
{label && <span className={cls("text-sm text-foreground", labelClassName)}>{label}</span>}
{label && (
<span className={cls("text-sm font-medium", labelClassName)}>
{label}
</span>
)}
</div>
);
};
export default AvatarGroup;
export default AvatarGroup;

View File

@@ -22,6 +22,17 @@ export default function HomePage() {
text: "Visit Us",
href: "#contact",
}}
socialProof={{
avatars: [
{ src: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?crop=entropy&cs=tinysrgb&fit=facearea&facepad=2&w=256&h=256&q=80" },
{ src: "https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?crop=entropy&cs=tinysrgb&fit=facearea&facepad=2&w=256&h=256&q=80" },
{ src: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?crop=entropy&cs=tinysrgb&fit=facearea&facepad=2&w=256&h=256&q=80" },
{ src: "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?crop=entropy&cs=tinysrgb&fit=facearea&facepad=2&w=256&h=256&q=80" },
{ src: "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?crop=entropy&cs=tinysrgb&fit=facearea&facepad=2&w=256&h=256&q=80" }
],
rating: 5,
text: "Loved by 2,000+ local foodies"
}}
imageSrc="https://images.unsplash.com/photo-1579359652478-bdcba0c995ed?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w4Mzc5ODl8MHwxfHNlYXJjaHwxMnx8cGFzdHJ5JTIwc2hvcCUyMGF0bW9zcGhlcmUlMjBuYXR1cmFsJTIwbGlnaHRpbmd8ZW58MXwwfHx8MTc3Nzc2NTMzNXww&ixlib=rb-4.1.0&q=80&w=1080"
/>
</div>
@@ -211,4 +222,4 @@ export default function HomePage() {
</div>
</>
);
}
}