Product Catalog
Product catalog with categories, heat levels, filtering, search, comparison, and quick view for the Jose Madrid Salsa storefront
Product Catalog
The product catalog powers the public storefront and admin product management. Products support categories, heat levels, nutritional info, compare-at pricing, and tag-based organization.
Architecture
Data Model
Each product record includes:
| Field | Type | Description |
|---|---|---|
name | string | Display name |
slug | string | URL-safe identifier |
sku | string | Stock keeping unit |
price | Decimal | Current price |
compareAtPrice | Decimal? | Original price for sale badges |
heatLevel | enum | MILD, MEDIUM, HOT, EXTRA_HOT |
inventory | int | Units in stock |
featuredImage | string? | Primary image URL |
ingredients | string[] | Ingredient list |
nutritionalInfo | JSON? | Calories, sodium, fat, carbs, etc. |
isFeatured | boolean | Show on homepage |
isActive | boolean | Visible in storefront |
categoryId | string? | FK to Category |
Product Card
The ProductCard component (components/store/product-card.tsx) renders each product with:
- Featured and % OFF badges computed from
compareAtPrice - Heat level badge with color coding via
getHeatLevelColor() - Out of stock indicator when
inventory <= 0 - Low stock warning when
inventory <= 5 - Quick view modal (eye icon on hover) with full details and add-to-cart
- Wishlist toggle (heart icon, requires authentication)
- Comparison toggle (scale icon, max 4 products)
// Product interface used by ProductCard
interface Product {
id: string
name: string
slug: string
description: string | null
price: number
compareAtPrice?: number | null
featuredImage: string | null
heatLevel: string
sku: string
inventory: number
isFeatured: boolean
ingredients: string[] | null
nutritionalInfo?: { calories: number; sodiumMg: number; ... } | null
}Filtering and Search
The products page (app/(public)/products/page.tsx) is a server component that accepts URL search params:
| Param | Description |
|---|---|
category | Filter by category slug |
heatLevel | Filter by heat level |
search | Full-text search |
view | grid or list layout |
The server component calls getProducts() from lib/db/products.ts with these filters, then passes results to the ProductsClient component for interactive filtering and view toggling.
Search params are async in Next.js 15+. The page awaits searchParams before passing them to the query layer.
Product Comparison
Users can compare up to 4 products side-by-side using the comparison store (lib/store/comparison.ts). The comparison panel shows:
- Price differences
- Heat level comparison
- Ingredient lists
- Nutritional info (when available)
- Weight and dimensions
Admin Product Management
Admin users manage products at /admin/products with CRUD operations, image upload via UploadThing, category assignment, and inventory adjustments.
Collections
Collections (/admin/collections, under Products) are curated, marketing-facing groups of
products — "Gift Sets", "New Arrivals", "Staff Picks" — distinct from Categories (the taxonomy
each product belongs to exactly one of). A product can appear in many collections and a collection
holds many products, so the relation is many-to-many through the CollectionProduct join, which
also carries each product's display order within the collection.
- Model:
Collection(name, slug, description, image, SEO fields,isActive,sortOrder) mirrorsCategory; membership and per-collection order live onCollectionProduct(@@unique([collectionId, productId])). - Admin CRUD:
app/admin/collections(list + create/edit dialogs) andapp/api/admin/collections/**(products:read/products:write, audit-logged — the nav item, the API, and the Products parent menu all share the same permission so visibility and access line up). The form includes a searchable product multi-select; products are stored in the order selected. Deleting a collection detaches its products (the join rows cascade) — the products themselves are never deleted. - Storefront:
/collections/[slug]renders the collection's active products, in order, through the sameProductCardgrid as the catalog. Query:getCollectionBySluginlib/db/products.ts. - Slug + rows logic is in
lib/collections.ts(pure, tested). Migration20260815130000_add_collections.
How is this guide?
Last updated on