@blamejs/blamejs-shop 0.0.44 → 0.0.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/lib/storefront.js +73 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.0.x
|
|
10
10
|
|
|
11
|
+
- v0.0.45 (2026-05-22) — **Cart line items — thumbnail + product title + slug-linked anchor alongside the SKU chip.** Cart rows previously rendered only the SKU code, qty, unit, and total — the visitor had to remember what they actually added. Each line now leads with a 3.5rem rounded thumbnail (from the product's first `catalog.media` row), the product title in a bold inline label, and the SKU as a small monospace chip below. The whole product cell is an anchor back to the PDP so a visitor can re-enter the product page directly from the cart. Lines whose variant is missing media render the placeholder tile (dashed-border + the diagonal-stripe pattern the catalog empty-state uses). **Changed:** *`CART_LINE_EDITABLE` template — product cell replaces SKU-only cell* — First cell is now `<a class="cart-line__product-link" href="/products/<slug>">` wrapping a thumbnail + a `<span class="cart-line__product-meta">` that stacks the product title (bold) and SKU chip (monospace, on a `--bg-2` background). Hover on the link tints both the title and the link affordance to the accent color. The qty / unit / total / action cells are unchanged. · *`storefront.renderCart({ ..., product_lookup })` accepts a variant→product lookup* — Routes pass a `{ variant_id: { product, hero_media } }` map; lines render with the matching product's title + slug-linked URL + hero-media thumbnail. Lines without a lookup match (variant deleted, media not attached yet) fall through to a SKU-as-title display with the placeholder tile. The cart-table column header is now 'Product' instead of 'SKU'. · *`GET /cart` builds the product lookup* — After listing the cart's lines, the route walks each unique `variant_id` (cached by id so a cart with the same variant twice only hits the catalog once), pulls `catalog.variants.get` → `catalog.products.get` + `catalog.media.listForProduct`, and bundles the result for the renderer.
|
|
12
|
+
|
|
11
13
|
- v0.0.44 (2026-05-22) — **Package rename — `@blamejs/blamejs-shop` (scoped under the org alongside `@blamejs/core` + `@blamejs/exceptd-skills`).** The npm package is now published under the `blamejs` organization scope as `@blamejs/blamejs-shop`. Installation switches from `npm install blamejs-shop` to `npm install @blamejs/blamejs-shop`; the import surface is unchanged — `require("@blamejs/blamejs-shop")` returns the same shop primitive set the unscoped name did. The legacy unscoped package will be deprecated on the registry with a pointer to the new name so existing operators see the upgrade path on their next install. **Changed:** *`package.json#name` → `@blamejs/blamejs-shop`* — Matches the existing org-scoped packages (`@blamejs/core`, `@blamejs/exceptd-skills`). The publish workflow already passes `--access public` to `npm publish`, which is the access mode scoped packages need to land as publicly-installable. · *Install command* — `npm install @blamejs/blamejs-shop` replaces `npm install blamejs-shop`. The `require` path follows: `var bShop = require("@blamejs/blamejs-shop");`. The entry point + export shape are byte-for-byte identical to v0.0.43 — only the resolution path changes. **Migration:** *Operator upgrade path* — Operators on the unscoped `blamejs-shop` change one line in their `package.json` dependencies: `"blamejs-shop": "^0.0.43"` → `"@blamejs/blamejs-shop": "^0.0.44"`. Every `require("blamejs-shop")` call site needs the matching update (`require("@blamejs/blamejs-shop")`). The unscoped name will be deprecated on the registry with a pointer to the new scoped path so a fresh `npm install` on the old name surfaces the migration message in npm's stderr.
|
|
12
14
|
|
|
13
15
|
- v0.0.43 (2026-05-22) — **Search results — image-bearing product cards (consistent with the home grid).** The search results page rendered text-only `PRODUCT_CARD` tiles — title + price + 'View product →' link — while the home catalog grid (v0.0.42) shifted to clickable image-bearing `product-card` tiles. This release unifies the two surfaces: search results now go through the same `_buildProductCard(p)` picker so a hit lands a real hero-image tile when the product carries media, and the text-only fallback when it doesn't. The same `asset_prefix` override the home renderer accepts also works on the search renderer. **Changed:** *`storefront.renderSearch({ products, asset_prefix? })` uses `_buildProductCard`* — Same render-time picker the home grid uses — when a result product carries `hero_media`, the card renders as the image-bearing tile; without media, the text-only fallback. `asset_prefix` defaults to `/assets/` and can be overridden for operators on a different R2 binding or CDN remap. · *Search route fetches `catalog.media.listForProduct` per hit* — `GET /search` now calls the media lookup alongside the existing variants + prices loop. The first attached media row becomes `product.hero_media` on the data shape `renderSearch` consumes — same shape the home route v0.0.42 emits.
|
package/lib/storefront.js
CHANGED
|
@@ -711,9 +711,22 @@ var CART_LINE =
|
|
|
711
711
|
// /cart/lines/:id/remove). HTML forms don't natively support
|
|
712
712
|
// PATCH/DELETE so the framework routes use POST with verb-suffix
|
|
713
713
|
// paths.
|
|
714
|
+
// Editable cart line. The first cell carries a small media tile +
|
|
715
|
+
// the product title + the SKU code chip below it; without media,
|
|
716
|
+
// the tile drops to a dashed-border placeholder so the row's grid
|
|
717
|
+
// doesn't collapse. `product_url` is the slug-linked anchor so the
|
|
718
|
+
// visitor can re-enter the PDP from the cart without retyping.
|
|
714
719
|
var CART_LINE_EDITABLE =
|
|
715
720
|
"<tr>\n" +
|
|
716
|
-
" <td class=\"cart-
|
|
721
|
+
" <td class=\"cart-line__product\">\n" +
|
|
722
|
+
" <a class=\"cart-line__product-link\" href=\"{{product_url}}\">\n" +
|
|
723
|
+
" RAW_CART_LINE_THUMB\n" +
|
|
724
|
+
" <span class=\"cart-line__product-meta\">\n" +
|
|
725
|
+
" <span class=\"cart-line__product-title\">{{product_title}}</span>\n" +
|
|
726
|
+
" <code class=\"cart-line__sku-chip\">{{sku}}</code>\n" +
|
|
727
|
+
" </span>\n" +
|
|
728
|
+
" </a>\n" +
|
|
729
|
+
" </td>\n" +
|
|
717
730
|
" <td class=\"cart-line__qty\">\n" +
|
|
718
731
|
" <form method=\"post\" action=\"/cart/lines/{{line_id}}/update\" class=\"cart-line__update\">\n" +
|
|
719
732
|
" <input type=\"number\" name=\"qty\" value=\"{{qty}}\" min=\"1\" max=\"99\" class=\"cart-line__qty-input\" aria-label=\"Quantity\">\n" +
|
|
@@ -952,7 +965,7 @@ var CART_PAGE =
|
|
|
952
965
|
" <div class=\"cart-page__items\">\n" +
|
|
953
966
|
" <div class=\"table-scroll\">\n" +
|
|
954
967
|
" <table class=\"cart-table\">\n" +
|
|
955
|
-
" <thead><tr><th>
|
|
968
|
+
" <thead><tr><th>Product</th><th>Quantity</th><th>Unit</th><th>Total</th><th class=\"variant-table__action-h\">Action</th></tr></thead>\n" +
|
|
956
969
|
" <tbody>{{line_rows}}</tbody>\n" +
|
|
957
970
|
" </table>\n" +
|
|
958
971
|
" </div>\n" +
|
|
@@ -974,13 +987,27 @@ function renderCart(opts) {
|
|
|
974
987
|
var lines = opts.lines || [];
|
|
975
988
|
var totals = opts.totals || { subtotal_minor: 0, grand_total_minor: 0, currency: "USD" };
|
|
976
989
|
var shopName = opts.shop_name || "blamejs.shop";
|
|
990
|
+
var assetPrefix = opts.asset_prefix || "/assets/";
|
|
991
|
+
// `product_lookup` is { variant_id: { product, hero_media } } — the
|
|
992
|
+
// route handler bundles it in. Lines without an entry render with
|
|
993
|
+
// a dashed-placeholder tile + the SKU as the fallback title.
|
|
994
|
+
var lookup = opts.product_lookup || {};
|
|
977
995
|
var rendered = lines.map(function (l) {
|
|
996
|
+
var match = lookup[l.variant_id] || null;
|
|
997
|
+
var prod = match && match.product;
|
|
998
|
+
var hero = match && match.hero_media;
|
|
999
|
+
var imageUrl = hero ? assetPrefix + hero.r2_key : null;
|
|
1000
|
+
var imageAlt = hero ? (hero.alt_text || (prod && prod.title) || l.sku) : null;
|
|
978
1001
|
return {
|
|
979
|
-
id:
|
|
980
|
-
sku:
|
|
981
|
-
qty:
|
|
982
|
-
unit:
|
|
983
|
-
total:
|
|
1002
|
+
id: l.id,
|
|
1003
|
+
sku: l.sku,
|
|
1004
|
+
qty: String(l.qty),
|
|
1005
|
+
unit: pricing.format(l.unit_amount_minor, l.unit_currency),
|
|
1006
|
+
total: pricing.format(l.qty * l.unit_amount_minor, l.unit_currency),
|
|
1007
|
+
product_title: (prod && prod.title) || l.sku,
|
|
1008
|
+
product_url: prod ? ("/products/" + prod.slug) : "#",
|
|
1009
|
+
image_url: imageUrl,
|
|
1010
|
+
image_alt: imageAlt,
|
|
984
1011
|
};
|
|
985
1012
|
});
|
|
986
1013
|
var subtotal = pricing.format(totals.subtotal_minor, totals.currency);
|
|
@@ -997,10 +1024,23 @@ function renderCart(opts) {
|
|
|
997
1024
|
asset_css_main: opts.theme.assetUrl("css/main.css"),
|
|
998
1025
|
});
|
|
999
1026
|
}
|
|
1027
|
+
function _escAttr(s) {
|
|
1028
|
+
return String(s == null ? "" : s)
|
|
1029
|
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1030
|
+
}
|
|
1000
1031
|
var rows = rendered.map(function (l) {
|
|
1032
|
+
var thumb = l.image_url
|
|
1033
|
+
? "<span class=\"cart-line__thumb\"><img src=\"" + _escAttr(l.image_url) + "\" alt=\"" + _escAttr(l.image_alt) + "\" loading=\"lazy\"></span>"
|
|
1034
|
+
: "<span class=\"cart-line__thumb cart-line__thumb--empty\" aria-hidden=\"true\"></span>";
|
|
1001
1035
|
return _render(CART_LINE_EDITABLE, {
|
|
1002
|
-
sku:
|
|
1003
|
-
|
|
1036
|
+
sku: l.sku,
|
|
1037
|
+
qty: l.qty,
|
|
1038
|
+
unit: l.unit,
|
|
1039
|
+
total: l.total,
|
|
1040
|
+
line_id: l.id,
|
|
1041
|
+
product_title: l.product_title,
|
|
1042
|
+
product_url: l.product_url,
|
|
1043
|
+
}).replace("RAW_CART_LINE_THUMB", thumb);
|
|
1004
1044
|
}).join("");
|
|
1005
1045
|
if (!rows) rows = "<tr><td colspan=\"5\" class=\"empty\">Your cart is empty.</td></tr>";
|
|
1006
1046
|
var body = _render(CART_PAGE, {
|
|
@@ -1578,7 +1618,30 @@ function mount(router, deps) {
|
|
|
1578
1618
|
}
|
|
1579
1619
|
var lines = await deps.cart.listLines(c.id);
|
|
1580
1620
|
var totals = pricing.totals(c, lines, {});
|
|
1581
|
-
|
|
1621
|
+
// Build the variant_id → { product, hero_media } lookup the
|
|
1622
|
+
// renderer uses to decorate each line with a thumbnail + title.
|
|
1623
|
+
// Cache by variant_id so a cart with the same variant twice
|
|
1624
|
+
// only hits the catalog once.
|
|
1625
|
+
var productLookup = {};
|
|
1626
|
+
for (var i = 0; i < lines.length; i += 1) {
|
|
1627
|
+
var vId = lines[i].variant_id;
|
|
1628
|
+
if (productLookup[vId]) continue;
|
|
1629
|
+
var v = await deps.catalog.variants.get(vId);
|
|
1630
|
+
if (!v) { productLookup[vId] = null; continue; }
|
|
1631
|
+
var prod = await deps.catalog.products.get(v.product_id);
|
|
1632
|
+
var media = await deps.catalog.media.listForProduct(v.product_id);
|
|
1633
|
+
productLookup[vId] = {
|
|
1634
|
+
product: prod,
|
|
1635
|
+
hero_media: media.length ? media[0] : null,
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
_send(res, 200, renderCart({
|
|
1639
|
+
lines: lines,
|
|
1640
|
+
totals: totals,
|
|
1641
|
+
product_lookup: productLookup,
|
|
1642
|
+
shop_name: shopName,
|
|
1643
|
+
theme: theme,
|
|
1644
|
+
}));
|
|
1582
1645
|
});
|
|
1583
1646
|
|
|
1584
1647
|
// ---- checkout flow -------------------------------------------------
|
package/package.json
CHANGED