From 1e0e278451834574919c8d03d24e60494727276c Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 17 Jun 2026 00:48:26 -0600 Subject: [PATCH] feat(admin): apply new design system to Products pages - List page: new PageHeader with 'OPERATIONS' eyebrow + 'Add product' CTA - New/Edit pages: PageHeader with eyebrow, icon, status badge (edit) - ProductsClient: EmptyState for zero products, AdminBadge for status, Fragment Mono on stats, token-based colors throughout - ProductTableBody: AdminBadge tones for status/taxable, token-based hover/selected states, Fragment Mono on price column - NewProductForm / ProductEditForm / ProductFormModal: surrounding chrome on tokens; save bars use .ha-btn-primary / .ha-btn-ghost - .atelier-* body preserved as editorial treatment --- src/app/admin/products/[id]/page.tsx | 72 ++++---- src/app/admin/products/new/page.tsx | 27 ++- src/app/admin/products/page.tsx | 18 +- src/components/admin/NewProductForm.tsx | 33 ++-- src/components/admin/ProductEditForm.tsx | 62 ++++--- src/components/admin/ProductFormModal.tsx | 16 +- src/components/admin/ProductTableBody.tsx | 60 ++++--- src/components/admin/ProductsClient.tsx | 198 +++++++++++----------- 8 files changed, 249 insertions(+), 237 deletions(-) diff --git a/src/app/admin/products/[id]/page.tsx b/src/app/admin/products/[id]/page.tsx index 2199004..e13a2b7 100644 --- a/src/app/admin/products/[id]/page.tsx +++ b/src/app/admin/products/[id]/page.tsx @@ -1,6 +1,8 @@ import { supabase } from "@/lib/supabase"; import ProductEditForm from "@/components/admin/ProductEditForm"; import { getAdminUser } from "@/lib/admin-permissions"; +import { PageHeader, AdminBadge } from "@/components/admin/design-system"; +import { Package as PackageIcon } from "lucide-react"; import AdminAccessDenied from "@/components/admin/AdminAccessDenied"; import { redirect } from "next/navigation"; import Link from "next/link"; @@ -51,15 +53,15 @@ export default async function ProductDetailPage({ params }: ProductDetailPagePro if (error || !product) { return ( -
+
-

Product not found

-
+          

Product not found

+
             {error?.message ?? "Product not found"}
           
← Back to Products @@ -69,58 +71,66 @@ export default async function ProductDetailPage({ params }: ProductDetailPagePro } return ( -
+
- - ← Back to Products - +
Operations
+ } + title="Edit product" + subtitle="Update product details, pricing, and availability." + actions={ + + {product.active ? "Active" : "Inactive"} + + } + /> -
+
+ + ← Back to Products + +
+ +
-

+

{product.brands?.name}

-

+

{product.name}

-

+

{product.description}

- - {product.active ? "Active" : "Inactive"} -
-

Price

-

+

Price

+

${Number(product.price).toFixed(2)}

-

Type

-

+

Type

+

{product.type}

-
-

Edit Product

-

+

+

Edit Product

+

Update product details, pricing, and availability.

diff --git a/src/app/admin/products/new/page.tsx b/src/app/admin/products/new/page.tsx index eaa7b19..4f7af0a 100644 --- a/src/app/admin/products/new/page.tsx +++ b/src/app/admin/products/new/page.tsx @@ -1,5 +1,7 @@ import { getAdminUser } from "@/lib/admin-permissions"; import NewProductForm from "@/components/admin/NewProductForm"; +import { PageHeader } from "@/components/admin/design-system"; +import { Package as PackageIcon } from "lucide-react"; import { getBrands } from "@/actions/admin/users"; import { redirect } from "next/navigation"; import Link from "next/link"; @@ -19,28 +21,25 @@ export default async function NewProductPage() { } return ( -
+
-
+
Operations
+ } + title="New product" + subtitle="Add a product to your catalog with pricing, type, and availability." + /> + +
← Back to Products
-
-

- Create Product -

- -

- {isPlatformAdmin - ? "Add a new product to any brand you administer." - : "Add a new product to your brand's catalog."} -

- +
( - - - - - - -); - // Shape ProductsClient expects (legacy columns mapped from the new schema). type ProductRow = { id: string; @@ -41,8 +31,8 @@ export default async function AdminProductsPage() { return (
-

Access Denied

-

You do not have permission to manage products.

+

Access Denied

+

You do not have permission to manage products.

); @@ -133,8 +123,8 @@ export default async function AdminProductsPage() { return (
-

Error loading products

-
+          

Error loading products

+
             {queryError}
           
diff --git a/src/components/admin/NewProductForm.tsx b/src/components/admin/NewProductForm.tsx index f279fa8..073b7a1 100644 --- a/src/components/admin/NewProductForm.tsx +++ b/src/components/admin/NewProductForm.tsx @@ -132,7 +132,7 @@ export default function NewProductForm({ defaultBrandId = "", brands = [], lockB return (
{error && ( -
+
{error}
)} @@ -182,7 +182,7 @@ export default function NewProductForm({ defaultBrandId = "", brands = [], lockB {lockBrand ? ( // brand_admin / store_employee — brand is fixed by their admin_users record -
+
{brandOptions.find((b) => b.id === brandId)?.name ?? brandId}
) : ( @@ -197,7 +197,7 @@ export default function NewProductForm({ defaultBrandId = "", brands = [], lockB /> )} {!lockBrand && brands.length === 0 && ( -

+

Loading available brands — if this persists, check the admin Brands settings.

)} @@ -237,8 +237,8 @@ export default function NewProductForm({ defaultBrandId = "", brands = [], lockB
- -

JPG, PNG, WebP · 1200px max width · max 5MB (ideally under 2MB)

+ +

JPG, PNG, WebP · 1200px max width · max 5MB (ideally under 2MB)

e.preventDefault()} @@ -250,28 +250,28 @@ export default function NewProductForm({ defaultBrandId = "", brands = [], lockB onClick={() => fileInputRef.current?.click()} className={` flex flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed p-6 cursor-pointer transition-colors - ${uploadError ? "border-red-400" : "border-zinc-600 hover:border-slate-400 hover:bg-zinc-900"} + ${uploadError ? "border-[var(--admin-danger)]" : "border-[var(--admin-border)] hover:border-[var(--admin-primary)] hover:bg-[var(--admin-bg)]"} ${uploading ? "opacity-50 pointer-events-none" : ""} `} > {uploading ? ( <> -
- Uploading... +
+ Uploading... ) : imagePreview ? ( <> - Click or drop to replace + Click or drop to replace ) : ( <> - + - Drag & drop or click to upload + Drag & drop or click to upload )}
- {uploadError &&

{uploadError}

} + {uploadError &&

{uploadError}

} {imagePreview && ( )}
-
+ {/* Save button bar — new design tokens */} +
Cancel diff --git a/src/components/admin/ProductEditForm.tsx b/src/components/admin/ProductEditForm.tsx index 8554206..75c8f2d 100644 --- a/src/components/admin/ProductEditForm.tsx +++ b/src/components/admin/ProductEditForm.tsx @@ -1,6 +1,7 @@ "use client"; import NextImage from "next/image"; +import Link from "next/link"; import { useState, useRef } from "react"; import { useRouter } from "next/navigation"; import { uploadProductImage, deleteProductImage } from "@/actions/products/upload-image"; @@ -147,13 +148,13 @@ export default function ProductEditForm({ product, brands }: ProductEditFormProp return (
{error && ( -
+
{error}
)} {saved && ( -
+
Product updated successfully.
)} @@ -205,13 +206,13 @@ export default function ProductEditForm({ product, brands }: ProductEditFormProp
- +
- + -

Tax is calculated at checkout for shipping orders in your brand's nexus states. Non-taxable items (e.g. cooler boxes, apparel) are always exempt.

+

Tax is calculated at checkout for shipping orders in your brand's nexus states. Non-taxable items (e.g. cooler boxes, apparel) are always exempt.

- -

JPG, PNG, WebP · 1200px max width · max 5MB (ideally under 2MB)

+ +

JPG, PNG, WebP · 1200px max width · max 5MB (ideally under 2MB)

e.preventDefault()} @@ -262,28 +263,28 @@ export default function ProductEditForm({ product, brands }: ProductEditFormProp onClick={() => fileInputRef.current?.click()} className={` flex flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed p-6 cursor-pointer transition-colors - ${dragOver ? "border-green-500 bg-green-900/30" : "border-zinc-600 hover:border-slate-400 hover:bg-zinc-900"} + ${dragOver ? "border-[var(--admin-primary)] bg-[var(--admin-primary-soft)]" : "border-[var(--admin-border)] hover:border-[var(--admin-primary)] hover:bg-[var(--admin-bg)]"} ${uploading ? "opacity-50 pointer-events-none" : ""} `} > {uploading ? ( <> -
- Uploading... +
+ Uploading... ) : imagePreview ? ( <> - Click or drop to replace + Click or drop to replace ) : ( <> - + - Drag & drop or click to upload + Drag & drop or click to upload )}
- {uploadError &&

{uploadError}

} + {uploadError &&

{uploadError}

} {imagePreview && ( )}
- + {/* Save button bar — new design tokens */} +
+ + + Cancel + +
); } diff --git a/src/components/admin/ProductFormModal.tsx b/src/components/admin/ProductFormModal.tsx index f07e7c4..f708126 100644 --- a/src/components/admin/ProductFormModal.tsx +++ b/src/components/admin/ProductFormModal.tsx @@ -247,7 +247,7 @@ export default function ProductFormModal({ if (e.target === e.currentTarget) onClose(); }} style={{ - backgroundColor: "rgba(28, 25, 23, 0.55)", + backgroundColor: "color-mix(in srgb, var(--admin-text-primary) 55%, transparent)", backdropFilter: "blur(8px)", WebkitBackdropFilter: "blur(8px)", }} @@ -256,7 +256,8 @@ export default function ProductFormModal({ role="dialog" aria-modal="true" aria-label={mode === "add" ? "Add product" : "Edit product"} - className="atelier-enter atelier-canvas relative w-full max-w-5xl max-h-[calc(100vh-1.5rem)] sm:max-h-[calc(100vh-3rem)] rounded-2xl shadow-[0_30px_80px_-20px_rgba(28,25,23,0.45)] flex flex-col overflow-hidden border border-stone-200/60" + className="atelier-enter atelier-canvas relative w-full max-w-5xl max-h-[calc(100vh-1.5rem)] sm:max-h-[calc(100vh-3rem)] rounded-2xl flex flex-col overflow-hidden border border-[var(--admin-border)]" + style={{ boxShadow: "0 30px 80px -20px color-mix(in srgb, var(--admin-text-primary) 45%, transparent)" }} > {/* grain overlay */}
@@ -357,7 +358,7 @@ export default function ProductFormModal({ {uploading ? (
-
+

Pouring into the cellar…

) : imagePreview ? ( @@ -390,7 +391,7 @@ export default function ProductFormModal({ <> {/* ornamental drop zone content */} {uploadError && ( -

+

{uploadError}

)} @@ -668,7 +669,7 @@ export default function ProductFormModal({ {/* FOOTER */} -
+
diff --git a/src/components/admin/ProductTableBody.tsx b/src/components/admin/ProductTableBody.tsx index b878d02..7026b7e 100644 --- a/src/components/admin/ProductTableBody.tsx +++ b/src/components/admin/ProductTableBody.tsx @@ -68,35 +68,35 @@ export default function ProductTableBody({ return ( <> {/* Filter bar — sits outside in the parent */} -
+
onSearchChange(e.target.value)} - className="flex-1 min-w-40 rounded-lg border border-zinc-600 bg-zinc-900 px-3 py-2 text-sm text-zinc-100 outline-none focus:border-slate-900" + className="flex-1 min-w-40 rounded-lg border border-[var(--admin-border)] bg-[var(--admin-card-bg)] px-3 py-2 text-sm text-[var(--admin-text-primary)] outline-none focus:border-[var(--admin-primary)]" /> -
+
{(["all", "active", "inactive"] as const).map((f) => ( ))}
- {filtered.length} + {filtered.length}
{/* Delete error banner */} {deleteError && ( -
+
{deleteError}
)} -
+ {filtered.length === 0 ? ( - - - + - @@ -165,7 +168,7 @@ export default function ProductTableBody({
Edit @@ -174,7 +177,7 @@ export default function ProductTableBody({ e.preventDefault(); setOpenMenu(openMenu === product.id ? null : product.id); }} - className="rounded-lg px-2 py-1.5 text-xs text-zinc-500 hover:bg-zinc-950" + className="rounded-lg px-2 py-1.5 text-xs text-[var(--admin-text-muted)] hover:bg-[var(--admin-bg)]" > @@ -187,10 +190,10 @@ export default function ProductTableBody({ className="fixed inset-0 z-10" onClick={() => { setOpenMenu(null); setConfirmDelete(null); }} /> -
+
@@ -201,21 +204,22 @@ export default function ProductTableBody({ {confirmDelete === product.id && ( <>
{ setConfirmDelete(null); setOpenMenu(null); }} /> -
-

+

+

Delete "{product.name}"?

-

+

This will remove the product. If it is attached to any orders, it will be hidden instead of deleted.

diff --git a/src/components/admin/ProductsClient.tsx b/src/components/admin/ProductsClient.tsx index 01a5e9e..a0be71a 100644 --- a/src/components/admin/ProductsClient.tsx +++ b/src/components/admin/ProductsClient.tsx @@ -9,7 +9,8 @@ import { updateProduct } from "@/actions/products/update-product"; import { deleteProduct } from "@/actions/products"; import { uploadProductImage } from "@/actions/products/upload-image"; import ProductFormModal, { type ProductFormValues } from "@/components/admin/ProductFormModal"; -import { PageHeader, AdminButton, AdminIconButton, AdminSearchInput, AdminFilterTabs, AdminViewModeTabs, useToast, Skeleton } from "@/components/admin/design-system"; +import { PageHeader, AdminButton, AdminSearchInput, AdminFilterTabs, AdminViewModeTabs, AdminBadge, EmptyState, useToast, Skeleton } from "@/components/admin/design-system"; +import { Package as PackageIconLucide, Inbox as InboxIcon } from "lucide-react"; type Product = { id: string; @@ -29,44 +30,13 @@ type ViewMode = "table" | "cards"; // Icons const Icons = { - search: (className: string) => ( - - - - - ), plus: (className: string) => ( ), - package: (className: string) => ( - - - - - - - ), - upload: (className: string) => ( - - - - - - ), }; -// Page header icon -const PackageIconHeader = () => ( - - - - - - -); - export default function ProductsClient({ products, brandId, @@ -253,34 +223,50 @@ export default function ProductsClient({ return (
- {/* Page Header */} + {/* Page Header — eyebrow + title + subtitle + primary CTA */} +
Operations
} + icon={} title="Products" - subtitle={`${filtered.length} product${filtered.length !== 1 ? "s" : ""}`} + subtitle="Manage your product catalog, pricing, and availability." actions={ - Add Product + Add product } /> {/* Stats Cards */}
-
-

Total

-

{isLoading ? : products.length}

+
+

Total

+

+ {isLoading ? : products.length} +

-
-

Active

-

{isLoading ? : activeCount}

+
+

Active

+

+ {isLoading ? : activeCount} +

-
-

Inactive

-

{isLoading ? : inactiveCount}

+
+

Inactive

+

+ {isLoading ? : inactiveCount} +

@@ -320,6 +306,7 @@ export default function ProductsClient({ onDeleteConfirm={(id) => handleDelete(id)} onDeleteCancel={() => setDeleteConfirm(null)} deletingId={deletingId} + onAdd={openAddModal} /> ) : ( handleDelete(id)} onDeleteCancel={() => setDeleteConfirm(null)} deletingId={deletingId} + onAdd={openAddModal} /> )} @@ -375,6 +363,7 @@ function TableView({ onDeleteConfirm, onDeleteCancel, deletingId, + onAdd, }: { products: Product[]; onEdit: (p: Product) => void; @@ -383,6 +372,7 @@ function TableView({ onDeleteConfirm: (id: string) => void; onDeleteCancel: () => void; deletingId: string | null; + onAdd: () => void; }) { // Track the position of the open row's three-dots button so the popup can be // rendered via portal at body level (escapes any overflow:hidden ancestors @@ -434,31 +424,32 @@ function TableView({ const openProduct = deleteConfirm ? products.find((p) => p.id === deleteConfirm) : null; return ( -
+
+ {search || statusFilter !== "all" ? "No products match your search." : "No products found."} @@ -120,43 +120,46 @@ export default function ProductTableBody({ filtered.map((product) => (
{product.name} -
- {product.description || No description} +
+ {product.description || No description}
+ {Array.isArray(product.brands) ? product.brands[0]?.name : product.brands?.name} {product.type}{product.type} + ${Number(product.price).toFixed(2)} - + {product.active ? "Active" : "Inactive"} {product.is_taxable === false ? ( - Non-taxable + Non-taxable ) : ( - Taxable + Taxable )}
- + - - - - + + + + {products.length === 0 ? ( - ) : ( products.map((product) => ( - + -
ProductTypePriceStatusProductTypePriceStatus
-
- {Icons.package("h-8 w-8 text-stone-400")} -
-

No products found

-

Try adjusting your filters

+
+ } + title="No products found" + description="Try adjusting your filters, or add your first product to get started." + action={{ label: "Add your first product", onClick: onAdd }} + />
{product.image_url ? ( @@ -472,40 +463,41 @@ function TableView({ />
) : ( -
- +
+
)}
-
- {product.description || No description} +
+ {product.description || No description}
- + {product.type} + ${Number(product.price).toFixed(2)} - + {product.active ? "Active" : "Inactive"} - + @@ -520,7 +512,7 @@ function TableView({ @@ -592,6 +584,7 @@ function CardView({ onDeleteConfirm, onDeleteCancel, deletingId, + onAdd, }: { products: Product[]; onEdit: (p: Product) => void; @@ -600,25 +593,27 @@ function CardView({ onDeleteConfirm: (id: string) => void; onDeleteCancel: () => void; deletingId: string | null; + onAdd: () => void; }) { return (
{products.length === 0 ? ( -
-
- {Icons.package("h-8 w-8 text-stone-400")} -
-

No products found

-

Try adjusting your filters

+
+ } + title="No products found" + description="Try adjusting your filters, or add your first product to get started." + action={{ label: "Add your first product", onClick: onAdd }} + />
) : ( products.map((product) => (
{/* Image */} -
+
{product.image_url ? ( ) : (
- +
)} {/* Status badge */}
- + {product.active ? "Active" : "Inactive"} - +
@@ -649,27 +642,30 @@ function CardView({ onClick={() => onEdit(product)} className="block w-full text-left" > -

+

{product.name}

-

+

{product.description || No description}

-
+
- Price -

+ Price +

${Number(product.price).toFixed(2)}

- + {product.type} {product.is_taxable && ( - + Tax )} @@ -687,7 +683,7 @@ function CardView({