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
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.
| Package | Version |
|---|---|
| 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.