
A two-column hero using shadcn/ui Avatar and Button rows where overlapped faces, star score, and review total sit beside a tall rounded cover image.
block/hero3/hero3.tsx
import { ArrowRight, Star } from "lucide-react";
import { cn } from "cn";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
interface Image {
src: string;
alt: string;
srcDark?: string;
}
interface Button {
text: string;
url: string;
icon?: React.ReactNode;
}
interface Buttons {
primary?: Button;
secondary?: Button;
}
interface HeroBasicProps {
heading: string;
description: string;
buttons?: Buttons;
image: Image;
className?: string;
}
interface Hero3Props extends HeroBasicProps {
reviews?: Hero3Reviews;
}
type Props = Partial<Hero3Props>;
const person1 = {
name: "Jack Walsh",
firstName: "Jack",
lastName: "Walsh",
initials: "JW",
email: "jack.walsh@example.com",
username: "jackwalsh",
role: "Product Designer",
company: "Cartwright",
location: "Brisbane, Australia",
avatar: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/avatars/avatar1.webp",
};
const person2 = {
name: "Ilze Pērkone",
firstName: "Ilze",
lastName: "Pērkone",
initials: "MK",
email: "iperkone@example.com",
username: "iperkone",
role: "Brand Director",
company: "Kivit",
location: "Rīga, Latvia",
avatar: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/avatars/avatar2.webp",
};
const person3 = {
name: "Vernon Desilets",
firstName: "Vernon",
lastName: "Desilets",
initials: "VD",
email: "vernod@example.com",
username: "vernod",
role: "Creative Director",
company: "Soho Logix",
location: "Paris, France",
avatar: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/avatars/avatar3.webp",
};
const person4 = {
name: "Lín Wàngshū",
firstName: "Lín",
lastName: "Wangshu",
initials: "LW",
email: "linwang4@example.com",
username: "liwang",
role: "Graphic Designer",
company: "Tenxcloud",
location: "Shanghai, China",
avatar: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/avatars/avatar4.webp",
};
const person5 = {
name: "Rajesh Bhatt",
firstName: "Rajesh",
lastName: "Bhatt",
initials: "RB",
email: "rajesh.bhatt@example.com",
username: "rajeshbhatt",
role: "UX Researcher",
company: "Mindtree",
location: "Mumbai, India",
avatar: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/avatars/avatar5.webp",
};
const defaultProps: Hero3Props = {
heading: "The CRM platform for modern product teams.",
description: "An open-source project management platform built for modern product teams. Plan, track, and ship with confidence.",
buttons: {
primary: { text: "Get started", url: "#" },
secondary: { text: "Learn more", url: "#" },
},
image: {
src: "https://cdn.shadcnblocks.com/shadcnblocks/image-set/modern/saas-details/saas-detail-1-16x9.png",
alt: "Product dashboard",
},
reviews: {
count: 200,
rating: 5.0,
avatars: [person1, person2, person3, person4, person5].map((person) => ({
src: person.avatar,
alt: person.name,
})),
},
};
const MAX_REVIEW_AVATARS = 5;
interface Hero3Reviews {
count: number;
rating: number;
avatars: { src: string; alt: string }[];
}
const Hero3 = (props: Props) => {
const { heading, description, buttons, reviews, image, className } = {
...defaultProps,
...props,
};
const reviewAvatars = (reviews?.avatars ?? []).slice(0, MAX_REVIEW_AVATARS);
return (
<section className={cn("py-32", className)}>
<div className="container mx-auto">
<div className="grid items-center gap-10 lg:grid-cols-2 lg:gap-20">
<div className="mx-auto flex max-w-5xl flex-col items-center gap-6 text-center md:ml-auto lg:order-1 lg:items-start lg:text-left">
<h1 className="max-w-xl text-4xl font-bold tracking-tight text-pretty lg:max-w-3xl lg:text-6xl xl:text-7xl">
{heading}
</h1>
<p className="max-w-5xl text-balance text-muted-foreground lg:text-xl">
{description}
</p>
{reviews && (
<div className="flex w-fit flex-col items-center gap-4 sm:flex-row">
<span className="inline-flex items-center -space-x-3">
{reviewAvatars.map((avatar, index) => (
<Avatar
key={index}
className="size-10 bg-background ring-2 ring-background after:hidden"
>
<AvatarImage src={avatar.src} alt={avatar.alt} />
</Avatar>
))}
</span>
<div>
<div className="flex items-center gap-1">
{[...Array(5)].map((_, index) => (
<Star
key={index}
className="size-5 fill-none stroke-amber-500 dark:stroke-amber-400"
/>
))}
<span className="ml-1 font-semibold text-foreground">
{reviews.rating.toFixed(1)}
</span>
</div>
<p className="text-left font-medium text-muted-foreground">
from {reviews.count}+ reviews
</p>
</div>
</div>
)}
<div className="flex w-full flex-col justify-center gap-2 sm:flex-row lg:justify-start">
{buttons?.primary && (
<Button asChild size="lg" className="w-full sm:w-auto">
<a href={buttons.primary.url}>
{buttons.primary.text}
<ArrowRight className="size-4" />
</a>
</Button>
)}
{buttons?.secondary && (
<Button
asChild
variant="outline"
size="lg"
className="w-full bg-background sm:w-auto dark:bg-background"
>
<a href={buttons.secondary.url}>{buttons.secondary.text}</a>
</Button>
)}
</div>
</div>
<div className="flex w-full lg:order-2">
{image.srcDark ? (
<>
<img
src={image.src}
alt={image.alt}
className="aspect-video max-h-[600px] w-full rounded-md border border-border object-cover object-top lg:max-h-[800px] dark:hidden"
/>
<img
src={image.srcDark}
alt={image.alt}
className="hidden aspect-video max-h-[600px] w-full rounded-md border border-border object-cover object-top lg:max-h-[800px] dark:block"
/>
</>
) : (
<img
src={image.src}
alt={image.alt}
className="aspect-video max-h-[600px] w-full rounded-md border border-border object-cover object-top lg:max-h-[800px]"
/>
)}
</div>
</div>
</div>
</section>
);
};
export { Hero3 };