HextorUI
block

Storefront — Home

Hero, featured collections and a product grid.

npx shadcn@latest add @hextor/store-home

Source

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