@blamejs/blamejs-shop 0.3.38 → 0.3.39

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 CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.3.x
10
10
 
11
+ - v0.3.39 (2026-05-31) — **Paginated lists no longer show an empty extra page at exact page boundaries.** Several paginated lists offered a next page even when the current page was the last one, if the list's length happened to be an exact multiple of the page size — following that next link landed on an empty page. This affected a customer's order history, loyalty history, and store-credit history, and the admin customer list. Each now checks whether a further item actually exists before offering a next page, so the next link appears only when there is genuinely more to show. **Fixed:** *No phantom next page on exactly-full lists* — A customer's order history, loyalty history, and store-credit history, and the admin customer list, offered a next page whenever the current page was full — so a list whose total was an exact multiple of the page size showed a next link that led to an empty page. These lists now look one item past the page before offering a next link, so it appears only when a further item exists. This matches the fix already in place for collection pages.
12
+
11
13
  - v0.3.38 (2026-05-31) — **Pre-order campaigns — reserve a product before it is released.** Products that are not yet released could not be sold or reserved. Operators can now run a pre-order campaign for a product from the admin console — setting its release date, price, and an optional reservation cap. While the campaign is open, the product page shows a Pre-order control with the release date and how many reservations remain, and a signed-in customer can reserve their quantity without paying upfront. Customers see and can cancel their reservations from their account, and when the operator launches the campaign on its release date, each reservation is turned into a pending order the customer pays for through normal checkout. **Added:** *Pre-order campaigns* — A Pre-orders screen in the admin console runs a campaign for a product's SKU — release date, price, an optional deposit, and a reservation cap — with launch and close controls (launch is available once the release date arrives). While a campaign is open, the product page shows a Pre-order control with the release date and remaining availability instead of the usual add-to-cart, and a signed-in customer can reserve their quantity (no charge at reservation time; reserving past the cap or on a closed campaign is refused). Customers manage their reservations under Pre-orders in their account and can cancel one, which frees that capacity. Launching the campaign converts each open reservation into a pending order the customer completes through normal checkout. A customer can only see and cancel their own reservations.
12
14
 
13
15
  - v0.3.37 (2026-05-31) — **Gift registries — create one, share it, and let people mark gifts as bought.** A customer can now create a gift registry for an occasion — a wedding, baby, or birthday — add the products they want with the quantity desired, and share a public link. Anyone with the link can open the registry without an account, see what is still needed, add an item to their own cart to buy it through normal checkout, or mark an item as already purchased so it stops showing as needed. Registries can be public, unlisted, or private, and a private registry is not viewable from its link. The shared page never reveals who owns the registry or who bought what, and is not indexed by search engines. **Added:** *Gift registries* — A new Registry section in the account area lets a customer create a registry (title, occasion, event date, and a public/unlisted/private visibility), add and remove the items they want with a desired quantity, edit its details, and close it. Each registry has a shareable public page at its own link where anyone — no account needed — can see the items and how many of each are still needed, add an item to their cart to buy it through the normal checkout, or mark an item as already purchased so the registry's progress updates. A private registry is not viewable from its link, the public page shows only the items (never the owner's identity, address, or who purchased what), and registry pages are excluded from search indexing.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.3.38",
2
+ "version": "0.3.39",
3
3
  "assets": {
4
4
  "css/admin.css": {
5
5
  "integrity": "sha384-1SIn6oAf1DjECbRfKENZasdKHJiywGdXR58wn0hsFGcdVHzUmvfgMkEz5ANIAZJ3",
package/lib/customers.js CHANGED
@@ -255,20 +255,27 @@ function create(opts) {
255
255
  }
256
256
  }
257
257
  var sql, params;
258
+ // Fetch one row beyond the page so the next cursor is emitted ONLY
259
+ // when a customer past this page actually exists. Keying the cursor
260
+ // off a full page alone (rows.length === limit) advertises a
261
+ // phantom next page when the roster size is an exact multiple of
262
+ // the limit — the console's "Next page" link then lands empty.
258
263
  if (cursorVals) {
259
264
  sql = "SELECT id, email_hash, display_name, created_at, updated_at FROM customers " +
260
265
  "WHERE (created_at < ?1 OR (created_at = ?1 AND id < ?2)) " +
261
266
  "ORDER BY created_at DESC, id DESC LIMIT ?3";
262
- params = [cursorVals[0], cursorVals[1], limit];
267
+ params = [cursorVals[0], cursorVals[1], limit + 1];
263
268
  } else {
264
269
  sql = "SELECT id, email_hash, display_name, created_at, updated_at FROM customers " +
265
270
  "ORDER BY created_at DESC, id DESC LIMIT ?1";
266
- params = [limit];
271
+ params = [limit + 1];
267
272
  }
268
- var rows = (await query(sql, params)).rows;
273
+ var fetched = (await query(sql, params)).rows;
274
+ var hasMore = fetched.length > limit;
275
+ var rows = hasMore ? fetched.slice(0, limit) : fetched;
269
276
  var last = rows[rows.length - 1];
270
277
  var next = null;
271
- if (last && rows.length === limit) {
278
+ if (last && hasMore) {
272
279
  next = b.pagination.encodeCursor({
273
280
  orderKey: CUSTOMERS_ORDER_KEY,
274
281
  vals: [last.created_at, last.id],
package/lib/loyalty.js CHANGED
@@ -450,11 +450,17 @@ function create(opts) {
450
450
  sql += " AND occurred_at < ?2";
451
451
  params.push(cursor);
452
452
  }
453
+ // Fetch one row beyond the page so the next cursor is emitted ONLY
454
+ // when an older transaction actually exists. Keying the cursor off
455
+ // a full page alone (rows.length === limit) advertises a phantom
456
+ // next page when the history length is an exact multiple of the
457
+ // limit — the "Older activity" link then lands on an empty page.
453
458
  sql += " ORDER BY occurred_at DESC LIMIT ?" + (params.length + 1);
454
- params.push(limit);
459
+ params.push(limit + 1);
455
460
  var r = await query(sql, params);
456
- var rows = r.rows;
457
- var nextCursor = rows.length === limit ? rows[rows.length - 1].occurred_at : null;
461
+ var hasMore = r.rows.length > limit;
462
+ var rows = hasMore ? r.rows.slice(0, limit) : r.rows;
463
+ var nextCursor = hasMore ? rows[rows.length - 1].occurred_at : null;
458
464
  return { rows: rows, next_cursor: nextCursor };
459
465
  },
460
466
 
package/lib/order.js CHANGED
@@ -412,19 +412,28 @@ function create(opts) {
412
412
  }
413
413
  }
414
414
  var sql, params;
415
+ // Fetch one row beyond the page so the next cursor is emitted ONLY
416
+ // when an order past this page actually exists. Keying the cursor
417
+ // off a full page alone (rows.length === limit) advertises a
418
+ // phantom next page when the customer's order count is an exact
419
+ // multiple of the limit — the "Load more" link then lands on an
420
+ // empty page.
415
421
  if (cursorVals) {
416
422
  sql = "SELECT * FROM orders WHERE customer_id = ?1 AND " +
417
423
  "(updated_at < ?2 OR (updated_at = ?2 AND id < ?3)) " +
418
424
  "ORDER BY updated_at DESC, id DESC LIMIT ?4";
419
- params = [customerId, cursorVals[0], cursorVals[1], limit];
425
+ params = [customerId, cursorVals[0], cursorVals[1], limit + 1];
420
426
  } else {
421
427
  sql = "SELECT * FROM orders WHERE customer_id = ?1 " +
422
428
  "ORDER BY updated_at DESC, id DESC LIMIT ?2";
423
- params = [customerId, limit];
429
+ params = [customerId, limit + 1];
424
430
  }
425
- var rows = (await query(sql, params)).rows;
431
+ var fetched = (await query(sql, params)).rows;
432
+ var hasMore = fetched.length > limit;
433
+ var rows = hasMore ? fetched.slice(0, limit) : fetched;
426
434
  // Hydrate ship_to_json + lines on each row so the renderer
427
- // doesn't need a separate trip per order.
435
+ // doesn't need a separate trip per order. The peeked row is sliced
436
+ // off first so it never costs a hydration trip.
428
437
  for (var i = 0; i < rows.length; i += 1) {
429
438
  rows[i].ship_to = JSON.parse(rows[i].ship_to_json);
430
439
  rows[i].lines = (await query(
@@ -434,7 +443,7 @@ function create(opts) {
434
443
  }
435
444
  var last = rows[rows.length - 1];
436
445
  var next = null;
437
- if (last && rows.length === limit) {
446
+ if (last && hasMore) {
438
447
  next = b.pagination.encodeCursor({
439
448
  orderKey: ORDER_ORDER_KEY,
440
449
  vals: [last.updated_at, last.id],
@@ -331,11 +331,17 @@ function create(opts) {
331
331
  sql += " AND occurred_at < ?2";
332
332
  params.push(cursor);
333
333
  }
334
+ // Fetch one row beyond the page so the next cursor is emitted ONLY
335
+ // when an older ledger entry actually exists. Keying the cursor off
336
+ // a full page alone (rows.length === limit) advertises a phantom
337
+ // next page when the ledger length is an exact multiple of the
338
+ // limit — the "Older activity" link then lands on an empty page.
334
339
  sql += " ORDER BY occurred_at DESC, id DESC LIMIT ?" + (params.length + 1);
335
- params.push(limit);
340
+ params.push(limit + 1);
336
341
  var r = await query(sql, params);
337
- var rows = r.rows;
338
- var nextCursor = rows.length === limit ? rows[rows.length - 1].occurred_at : null;
342
+ var hasMore = r.rows.length > limit;
343
+ var rows = hasMore ? r.rows.slice(0, limit) : r.rows;
344
+ var nextCursor = hasMore ? rows[rows.length - 1].occurred_at : null;
339
345
  return { rows: rows, next_cursor: nextCursor };
340
346
  },
341
347
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/blamejs-shop",
3
- "version": "0.3.38",
3
+ "version": "0.3.39",
4
4
  "description": "Open-source framework built on blamejs. Vendored stack, zero npm runtime deps, PQC-first crypto, security-on by default.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {