v0.7: shop storefront + routed app

AppRouter replaces the single-page App as the mounted root: shop routes
(/ and /shop → ShopHome, /shop/:productSlug → ProductPage, /cart →
CartStub, /customize → CustomizeRoute) plus the template-manager admin
surfaces (/admin/templates|products|featured|trash, v0.8 adds
/admin/inventory + RequireAdmin wrapping). App.jsx stays on disk one
release as the documented legacy fallback — nothing imports it.

New shop UI: components/shop/ (ShopHome/Hero/ProductGrid/ProductCard/
ProductPage/ProductGallery/ProductInfo/ProductBuyPanel/CartStub/
ShopHeader/TemplateGalleryForProduct), hooks (useShopProducts/
useShopProduct/useCart/useFacetCatalog), and shop.css + admin.css.

Header gains the module-gated Clear button (onCanClearChange /
onElementCountChange mirror the editor module's history-commit
signals); i18n keys follow. vite proxies /products to the Node server
(template-manager serves product imagery — without it the SPA fallback
returns index.html as an image) and adds a template-manager chunk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
khalid@traclabs.com
2026-09-01 23:28:58 -05:00
parent aba2dbedfb
commit 6599a5e68f
23 changed files with 5928 additions and 41 deletions

View File

@@ -0,0 +1,82 @@
// <ProductPage> — shop product detail at /shop/:productSlug.
//
// Composition:
// ShopHeader
// ├ ProductGallery (left column)
// └ ProductInfo + ProductBuyPanel (right column)
// TemplateGalleryForProduct (full width, customizable kind only)
//
// Loading / error / 404 states are handled inline (kept simple — no
// dedicated error boundary needed for a leaf route).
//
// The buy panel's "Customize this product" CTA scrolls the template
// gallery into view via a ref + scrollIntoView. The gallery section
// has id="templates" so the URL hash also works as a fallback.
import { useParams, Link } from 'react-router-dom';
import { useRef } from 'react';
import { ShopHeader } from './ShopHeader.jsx';
import { ProductGallery } from './ProductGallery.jsx';
import { ProductInfo } from './ProductInfo.jsx';
import { ProductBuyPanel } from './ProductBuyPanel.jsx';
import { TemplateGalleryForProduct } from './TemplateGalleryForProduct.jsx';
import { useShopProduct } from '../../hooks/useShopProduct.js';
export function ProductPage() {
const { productSlug } = useParams();
const { product, loading, error } = useShopProduct(productSlug);
const templatesRef = useRef(null);
const handleScrollToTemplates = () => {
if (templatesRef.current) {
templatesRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
return (
<div className="app-shell">
<ShopHeader />
<main className="shop-main">
{loading && (
<div className="shop-product-page shop-product-page--loading">
<p>Loading…</p>
</div>
)}
{error && (
<div className="shop-product-page shop-product-page--error">
<h1>Product not found</h1>
<p>
We couldn't find that product. <Link to="/shop">Back to the shop →</Link>
</p>
</div>
)}
{product && (
<>
<nav className="shop-breadcrumb" aria-label="Breadcrumb">
<Link to="/shop">← Shop</Link>
</nav>
<article className="shop-product-page">
<div className="shop-product-page__gallery">
<ProductGallery product={product} />
</div>
<div className="shop-product-page__details">
<ProductInfo product={product} />
<ProductBuyPanel
product={product}
onScrollToTemplates={handleScrollToTemplates}
/>
</div>
</article>
{product.kind === 'customizable' && (
<div ref={templatesRef}>
<TemplateGalleryForProduct product={product} />
</div>
)}
</>
)}
</main>
</div>
);
}