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:
43
src/components/shop/ProductInfo.jsx
Normal file
43
src/components/shop/ProductInfo.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user