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,43 @@
// <ProductInfo> — header block for the product detail page.
//
// Shows label, category, kind pill, description, and the "starting at"
// price. Lives in the right column of the two-column product page,
// above the buy panel.
function formatUSD(cents) {
if (!cents) return '$0.00';
return `$${(cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
function startingPriceCents(product) {
const sizes = product.sizes || [];
if (sizes.length === 0) return 0;
return sizes.reduce(
(min, s) => Math.min(min, s.basePriceCents || 0),
Number.POSITIVE_INFINITY,
);
}
export function ProductInfo({ product }) {
const kind = product.kind || 'customizable';
const kindLabel = kind === 'fixed' ? 'Ready-made' : 'Customizable';
const fromPrice = startingPriceCents(product);
return (
<div className="shop-info">
<div className="shop-info__head">
<span className={`shop-kind-pill shop-kind-pill--${kind}`}>{kindLabel}</span>
{product.category && (
<span className="shop-info__category">{product.category}</span>
)}
</div>
<h1 className="shop-info__label">{product.label}</h1>
<p className="shop-info__price">
<span className="shop-price-from">Starting at</span>{' '}
<strong className="shop-price-large">{formatUSD(fromPrice)}</strong>
</p>
{product.description && (
<p className="shop-info__description">{product.description}</p>
)}
</div>
);
}