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>
44 lines
1.5 KiB
JavaScript
44 lines
1.5 KiB
JavaScript
// <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>
|
|
);
|
|
}
|