Merge version_2_1777765451570 into main #1
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user