Skip to content
Framework: Vite + React + TypeScriptDifficulty: intermediateFeatured

Ecommerce storefront

A product catalog with category filters and search, product detail view, and a cart kept in local state.

Catalog, product detail and cart screens driven by a typed in-memory product list. The cart is persisted to localStorage through a small hook, quantities can be adjusted inline, and the checkout form is explicitly a placeholder: it records the order locally and tells you where to plug in Stripe or Razorpay on the server.

  • react
  • ecommerce
  • cart
  • catalog
  • tailwind

This preview is the template's actual file tree and source code — exactly what is copied into a new project. Codexa does not publish screenshots or mock-ups of templates; run the project to see it rendered.

Files13 files

src/App.tsx114 lines
import { useMemo, useState } from "react";
import { categories, formatPrice, products, type Product } from "./products";
import { useCart } from "./cart";
import { Cart } from "./Cart";

type View = { kind: "catalog" } | { kind: "product"; id: string } | { kind: "cart" };

export default function App() {
  const [view, setView] = useState<View>({ kind: "catalog" });
  const [category, setCategory] = useState<(typeof categories)[number]>("All");
  const [query, setQuery] = useState("");
  const cart = useCart();

  const visible = useMemo(
    () => products.filter((p) => (category === "All" || p.category === category) && p.name.toLowerCase().includes(query.toLowerCase())),
    [category, query],
  );

  return (
    <div className="min-h-screen bg-white text-slate-900">
      <header className="sticky top-0 z-10 border-b border-slate-200 bg-white/90 backdrop-blur">
        <div className="mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-3 sm:px-6">
          <button type="button" onClick={() => setView({ kind: "catalog" })} className="font-semibold tracking-tight">
            Northwind Goods
          </button>
          <button type="button" onClick={() => setView({ kind: "cart" })} className="rounded-md border border-slate-300 px-3 py-1.5 text-sm hover:bg-slate-50" aria-label={"Open cart, " + cart.count + " items"}>
            Cart ({cart.count})
          </button>
        </div>
      </header>

      <main className="mx-auto max-w-6xl px-4 py-8 sm:px-6">
        {view.kind === "cart" ? (
          <Cart cart={cart} onBack={() => setView({ kind: "catalog" })} />
        ) : view.kind === "product" ? (
          <ProductDetail product={products.find((p) => p.id === view.id)} onAdd={cart.add} onBack={() => setView({ kind: "catalog" })} />
        ) : (
          <>
            <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
              <div className="flex flex-wrap gap-2" role="group" aria-label="Filter by category">
                {categories.map((c) => (
                  <button
                    key={c}
                    type="button"
                    onClick={() => setCategory(c)}
                    aria-pressed={category === c}
                    className={"rounded-full border px-3 py-1 text-sm " + (category === c ? "border-slate-900 bg-slate-900 text-white" : "border-slate-300 hover:bg-slate-50")}
                  >
                    {c}
                  </button>
                ))}
              </div>
              <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search products" aria-label="Search products" className="rounded-md border border-slate-300 px-3 py-1.5 text-sm sm:w-64" />
            </div>
            {visible.length === 0 ? (
              <p className="mt-10 text-center text-slate-500">No products match your search.</p>
            ) : (
              <ul className="mt-6 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
                {visible.map((p) => (
                  <li key={p.id} className="flex flex-col rounded-xl border border-slate-200 p-4">
                    <div aria-hidden="true" className="aspect-[4/3] rounded-lg bg-slate-100" />
                    <h2 className="mt-3 font-medium">{p.name}</h2>
                    <p className="text-sm text-slate-500">{p.category}</p>
                    <div className="mt-auto flex items-center justify-between pt-4">
                      <span className="font-semibold">{formatPrice(p.price)}</span>
                      <div className="flex gap-2">
                        <button type="button" onClick={() => setView({ kind: "product", id: p.id })} className="rounded-md border border-slate-300 px-3 py-1.5 text-sm hover:bg-slate-50">
                          Details
                        </button>
                        <button type="button" onClick={() => cart.add(p.id)} className="rounded-md bg-slate-900 px-3 py-1.5 text-sm text-white hover:bg-slate-700">
                          Add
                        </button>
                      </div>
                    </div>
                  </li>
                ))}
              </ul>
            )}
          </>
        )}
      </main>
    </div>
  );
}

function ProductDetail({ product, onAdd, onBack }: { product: Product | undefined; onAdd: (id: string) => void; onBack: () => void }) {
  if (!product) {
    return (
      <div>
        <p>Product not found.</p>
        <button type="button" onClick={onBack} className="mt-4 text-sm text-blue-600">
          Back to catalog
        </button>
      </div>
    );
  }
  return (
    <article className="grid gap-8 md:grid-cols-2">
      <div aria-hidden="true" className="aspect-square rounded-xl bg-slate-100" />
      <div>
        <button type="button" onClick={onBack} className="text-sm text-blue-600">
          ← Back to catalog
        </button>
        <h1 className="mt-3 text-3xl font-semibold tracking-tight">{product.name}</h1>
        <p className="mt-1 text-slate-500">{product.category}</p>
        <p className="mt-4 text-slate-700">{product.description}</p>
        <p className="mt-6 text-2xl font-semibold">{formatPrice(product.price)}</p>
        <button type="button" onClick={() => onAdd(product.id)} className="mt-4 rounded-md bg-slate-900 px-5 py-2.5 text-sm font-medium text-white hover:bg-slate-700">
          Add to cart
        </button>
      </div>
    </article>
  );
}

Dependencies

Documented from the template's package.json. The runtime has a prebuilt dependency set for Vite + React + TypeScript, so installs are fast.

PackageVersion
react^19.1.0
react-dom^19.1.0
vite^6.3.0
@vitejs/plugin-react^4.4.0
tailwindcss^4.1.0
@tailwindcss/vite^4.1.0
typescript^5.8.0

How it runs

Framework
Vite + React + TypeScript
Dev command
npm run dev
Build command
npm run build
Output directory
dist
Port
5173
Opens at
/
Difficulty
intermediate

Projects run in an isolated sandbox with a live preview. Read the projects documentation for details on settings, snapshots and deployment.

Need the raw data? GET /api/v1/templates/vite-react-storefront returns this template with its files.

Build on Ecommerce storefront

Create a free account, start from this template, and ask the AI agent to turn it into your app.