block
Storefront — Home
Hero, featured collections and a product grid.
npx shadcn@latest add @hextor/store-homeSource
import Image from "next/image";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ProductGrid } from "@/registry/hextor/components/store/storefront/product-grid";
import {
StoreHeader,
type StoreHeaderProps,
} from "@/registry/hextor/components/store/storefront/store-header";
import {
StoreFooter,
type StoreFooterProps,
} from "@/registry/hextor/components/store/storefront/store-footer";
import { cn } from "@/registry/hextor/lib/utils";
import type { Collection, Product, ProductImage } from "@/registry/hextor/lib/store";
import { resolveLabels, type HomeLabels } from "@/registry/hextor/components/store/storefront/home-labels";
export interface HomePageProps {
hero: {
eyebrow?: string;
heading: string;
subheading?: string;
image?: ProductImage;
ctaHref: string;
};
featuredCollections: Collection[];
featuredProducts: Product[];
collectionHrefFor?: (collection: Collection) => string;
collectionsHref?: string;
productsHref?: string;
header: StoreHeaderProps;
footer: StoreFooterProps;
labels?: Partial<HomeLabels>;
className?: string;
}
/**
* The storefront landing page: hero, featured collections, a product grid.
* Data arrives entirely through props — no fetch, no router hook — so this
* file works unmodified whether the consumer's data comes from Shopify's
* Storefront API, a headless CMS, or the bundled fixtures.
*/
export default function HomePage({
hero,
featuredCollections,
featuredProducts,
collectionHrefFor,
collectionsHref = "/collections",
productsHref = "/products",
header,
footer,
labels,
className,
}: HomePageProps) {
const copy = resolveLabels(labels);
return (
<div className={cn("flex min-h-screen flex-col", className)}>
<StoreHeader {...header} />
<main className="flex flex-1 flex-col gap-16 pb-16">
<section className="relative flex min-h-[420px] items-center overflow-hidden bg-muted">
{hero.image && (
<Image
src={hero.image.url}
alt={hero.image.alt}
fill
priority
sizes="100vw"
className="object-cover"
/>
)}
<div className={cn("relative z-10 mx-auto flex w-full max-w-6xl flex-col gap-4 px-4 sm:px-6", hero.image && "text-white")}>
{hero.eyebrow && (
<span className="text-sm font-medium tracking-wide uppercase opacity-80">{hero.eyebrow}</span>
)}
<h1 className="max-w-xl font-heading text-4xl font-semibold text-balance sm:text-5xl">
{hero.heading}
</h1>
{hero.subheading && <p className="max-w-md text-base opacity-90">{hero.subheading}</p>}
<div>
<Button size="lg" render={<Link href={hero.ctaHref}>{copy.heroCta}</Link>} />
</div>
</div>
</section>
{featuredCollections.length > 0 && (
<section className="mx-auto flex w-full max-w-6xl flex-col gap-4 px-4 sm:px-6">
<div className="flex items-center justify-between">
<h2 className="font-heading text-xl font-medium">{copy.featuredCollectionsHeading}</h2>
<Link href={collectionsHref} className="text-sm text-muted-foreground hover:text-foreground">
{copy.viewAllLabel}
</Link>
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
{featuredCollections.map((collection) => (
<Link
key={collection.id}
href={collectionHrefFor?.(collection) ?? `/collections/${collection.handle}`}
className="group block"
>
<Card className="gap-0 overflow-hidden py-0">
<div className="relative aspect-4/3 bg-muted">
{collection.image && (
<Image
src={collection.image.url}
alt={collection.image.alt}
fill
sizes="(min-width: 1024px) 25vw, (min-width: 640px) 33vw, 50vw"
className="object-cover transition-transform duration-300 group-hover:scale-105"
/>
)}
</div>
<div className="px-3 py-2.5">
<span className="text-sm font-medium">{collection.title}</span>
</div>
</Card>
</Link>
))}
</div>
</section>
)}
<section className="mx-auto flex w-full max-w-6xl flex-col gap-4 px-4 sm:px-6">
<div className="flex items-center justify-between">
<h2 className="font-heading text-xl font-medium">{copy.featuredProductsHeading}</h2>
<Link href={productsHref} className="text-sm text-muted-foreground hover:text-foreground">
{copy.viewAllLabel}
</Link>
</div>
<ProductGrid products={featuredProducts} />
</section>
</main>
<StoreFooter {...footer} />
</div>
);
}
Docs
The landing page. All data arrives through props on the view, so swapping the fixtures in the route file for your own queries is the only change it needs.
Ships with sample data from @hextor/store-fixtures so it renders a real shop the moment it installs — a template you have to wire up before you can look at it is a template nobody evaluates. The route file is the only place that import appears.
Dependencies
@hextor/store-ui@hextor/store-fixtures