@wix/himalaya-cli 0.1497.0 → 0.1499.0

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.
Files changed (146) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-collections/src/display.d.ts +1 -1
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-collections/src/index.d.ts +2 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +4 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores-collections.mjs +2 -2
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores.mjs +3 -2
  6. package/dist/cli.mjs +30 -30
  7. package/dist/preview-assets.generated.bin +0 -0
  8. package/guides/sdk-reference.json +8 -2
  9. package/guides/trim-a-template.md +1 -1
  10. package/package.json +2 -2
  11. package/templates/beauty-salon/dev/index.ts +2465 -2035
  12. package/templates/beauty-salon/tier5-src/_shared/himi-store-categories.ts +102 -0
  13. package/templates/beauty-salon/tier5-src/_shared/himi-store-facets.ts +478 -0
  14. package/templates/beauty-salon/tier5-src/_shared/iap-presentation.ts +158 -15
  15. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +42 -2
  16. package/templates/beauty-salon/tier5-src/himi-branded-lite-shop/index.ts +691 -0
  17. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +2 -1
  18. package/templates/beauty-salon/tier5-src/himi-store-saved/index.ts +1 -0
  19. package/templates/beauty-salon-owner/dev/index.ts +571 -0
  20. package/templates/beauty-salon-owner/himalaya.content.json +6 -0
  21. package/templates/beauty-salon-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  22. package/templates/beauty-salon-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  23. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  24. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  25. package/templates/branded-lite/dev/index.ts +2989 -2197
  26. package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +46 -8
  27. package/templates/branded-lite/tier5-src/_shared/himi-store-categories.ts +102 -0
  28. package/templates/branded-lite/tier5-src/_shared/himi-store-facets.ts +478 -0
  29. package/templates/branded-lite/tier5-src/_shared/iap-presentation.ts +158 -15
  30. package/templates/branded-lite/tier5-src/business-chat/index.ts +3 -2
  31. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +42 -2
  32. package/templates/branded-lite/tier5-src/himi-branded-lite-shop/index.ts +691 -0
  33. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +2 -1
  34. package/templates/branded-lite/tier5-src/himi-store-saved/index.ts +1 -0
  35. package/templates/community-club/tier5-src/_shared/himi-store-categories.ts +102 -0
  36. package/templates/community-club/tier5-src/_shared/iap-presentation.ts +158 -15
  37. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +42 -2
  38. package/templates/community-club-owner/dev/index.ts +571 -0
  39. package/templates/community-club-owner/himalaya.content.json +6 -0
  40. package/templates/community-club-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  41. package/templates/community-club-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  42. package/templates/community-club-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  43. package/templates/community-club-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  44. package/templates/events-meetups/tier5-src/_shared/himi-store-categories.ts +102 -0
  45. package/templates/events-meetups/tier5-src/_shared/iap-presentation.ts +158 -15
  46. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +42 -2
  47. package/templates/events-meetups-owner/dev/index.ts +571 -0
  48. package/templates/events-meetups-owner/himalaya.content.json +6 -0
  49. package/templates/events-meetups-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  50. package/templates/events-meetups-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  51. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  52. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  53. package/templates/gym-fitness/dev/index.ts +2600 -2170
  54. package/templates/gym-fitness/tier5-src/_shared/himi-store-categories.ts +102 -0
  55. package/templates/gym-fitness/tier5-src/_shared/himi-store-facets.ts +478 -0
  56. package/templates/gym-fitness/tier5-src/_shared/iap-presentation.ts +158 -15
  57. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +42 -2
  58. package/templates/gym-fitness/tier5-src/himi-branded-lite-shop/index.ts +691 -0
  59. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +2 -1
  60. package/templates/gym-fitness/tier5-src/himi-store-saved/index.ts +1 -0
  61. package/templates/gym-fitness-owner/dev/index.ts +571 -0
  62. package/templates/gym-fitness-owner/himalaya.content.json +6 -0
  63. package/templates/gym-fitness-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  64. package/templates/gym-fitness-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  65. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  66. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  67. package/templates/nonprofit-cause/tier5-src/_shared/himi-store-categories.ts +102 -0
  68. package/templates/nonprofit-cause/tier5-src/_shared/iap-presentation.ts +158 -15
  69. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +42 -2
  70. package/templates/nonprofit-cause-owner/dev/index.ts +571 -0
  71. package/templates/nonprofit-cause-owner/himalaya.content.json +6 -0
  72. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  73. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  74. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  75. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  76. package/templates/owner-lite/dev/index.ts +571 -0
  77. package/templates/owner-lite/himalaya.content.json +6 -0
  78. package/templates/owner-lite/tier5-src/_shared/owner-today-rollup.ts +307 -0
  79. package/templates/owner-lite/tier5-src/_shared/owner-widget-reads.ts +23 -0
  80. package/templates/owner-lite/tier5-src/himi-owner-lite-today/index.ts +8 -20
  81. package/templates/owner-lite/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  82. package/templates/podcast-creator/tier5-src/_shared/himi-store-categories.ts +102 -0
  83. package/templates/podcast-creator/tier5-src/_shared/iap-presentation.ts +158 -15
  84. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +42 -2
  85. package/templates/podcast-creator-owner/dev/index.ts +571 -0
  86. package/templates/podcast-creator-owner/himalaya.content.json +6 -0
  87. package/templates/podcast-creator-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  88. package/templates/podcast-creator-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  89. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  90. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  91. package/templates/portfolio-creative/dev/index.ts +2470 -2040
  92. package/templates/portfolio-creative/tier5-src/_shared/himi-store-categories.ts +102 -0
  93. package/templates/portfolio-creative/tier5-src/_shared/himi-store-facets.ts +478 -0
  94. package/templates/portfolio-creative/tier5-src/_shared/iap-presentation.ts +158 -15
  95. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +42 -2
  96. package/templates/portfolio-creative/tier5-src/himi-branded-lite-shop/index.ts +691 -0
  97. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +2 -1
  98. package/templates/portfolio-creative/tier5-src/himi-store-saved/index.ts +1 -0
  99. package/templates/portfolio-creative-owner/dev/index.ts +571 -0
  100. package/templates/portfolio-creative-owner/himalaya.content.json +6 -0
  101. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  102. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  103. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  104. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  105. package/templates/restaurant/tier5-src/_shared/himi-store-categories.ts +102 -0
  106. package/templates/restaurant/tier5-src/_shared/iap-presentation.ts +158 -15
  107. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +42 -2
  108. package/templates/restaurant-owner/dev/index.ts +571 -0
  109. package/templates/restaurant-owner/himalaya.content.json +6 -0
  110. package/templates/restaurant-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  111. package/templates/restaurant-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  112. package/templates/restaurant-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  113. package/templates/restaurant-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  114. package/templates/retail-store/dev/index.ts +2583 -2137
  115. package/templates/retail-store/tier5-src/_shared/himi-store-categories.ts +102 -0
  116. package/templates/retail-store/tier5-src/_shared/himi-store-facets.ts +478 -0
  117. package/templates/retail-store/tier5-src/_shared/iap-presentation.ts +158 -15
  118. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +42 -2
  119. package/templates/retail-store/tier5-src/himi-branded-lite-shop/index.ts +691 -0
  120. package/templates/retail-store/tier5-src/himi-store-product/index.ts +2 -1
  121. package/templates/retail-store/tier5-src/himi-store-saved/index.ts +1 -0
  122. package/templates/retail-store-owner/dev/index.ts +571 -0
  123. package/templates/retail-store-owner/himalaya.content.json +6 -0
  124. package/templates/retail-store-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  125. package/templates/retail-store-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  126. package/templates/retail-store-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  127. package/templates/retail-store-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  128. package/templates/yoga-studio/tier5-src/_shared/himi-store-categories.ts +102 -0
  129. package/templates/yoga-studio/tier5-src/_shared/iap-presentation.ts +158 -15
  130. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +42 -2
  131. package/templates/yoga-studio-owner/dev/index.ts +571 -0
  132. package/templates/yoga-studio-owner/himalaya.content.json +6 -0
  133. package/templates/yoga-studio-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  134. package/templates/yoga-studio-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  135. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  136. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  137. package/templates/beauty-salon/tier5-src/_shared/himi-store-collection.ts +0 -22
  138. package/templates/beauty-salon/tier5-src/himi-store-shop/index.ts +0 -173
  139. package/templates/branded-lite/tier5-src/_shared/himi-store-collection.ts +0 -22
  140. package/templates/branded-lite/tier5-src/himi-store-shop/index.ts +0 -173
  141. package/templates/gym-fitness/tier5-src/_shared/himi-store-collection.ts +0 -22
  142. package/templates/gym-fitness/tier5-src/himi-store-shop/index.ts +0 -173
  143. package/templates/portfolio-creative/tier5-src/_shared/himi-store-collection.ts +0 -22
  144. package/templates/portfolio-creative/tier5-src/himi-store-shop/index.ts +0 -173
  145. package/templates/retail-store/tier5-src/_shared/himi-store-collection.ts +0 -22
  146. package/templates/retail-store/tier5-src/himi-store-shop/index.ts +0 -173
@@ -160,6 +160,13 @@ function safeAvatar(value: unknown): string {
160
160
  return match && !/[@%]/.test(match[1]!) ? value : "";
161
161
  }
162
162
 
163
+ /** A card's picture, only a plain https URL. Wix sends a 200-point thumbnail; a card shows it about 270 wide on a
164
+ * dense screen, so a Wix media URL asks its image service for a larger copy instead of being blown up. */
165
+ function cardImage(value: unknown): string {
166
+ const url = safeAvatar(value);
167
+ return /^https:\/\/static\.wixstatic\.com\/media\//i.test(url) ? url.replace(/\/v1\/fit\/w_\d+,h_\d+,/i, "/v1/fit/w_800,h_800,") : url;
168
+ }
169
+
163
170
  function initials(value: string): string {
164
171
  const words = value.trim().split(/\s+/).filter(Boolean);
165
172
  return words.length > 1 ? `${words[0]![0]}${words[words.length - 1]![0]}`.toUpperCase() : (words[0]?.slice(0, 2) ?? "").toUpperCase();
@@ -316,8 +323,10 @@ const isSafeLink = (url: string): boolean => SAFE_LINK.test(url) && !/[\u0000-\u
316
323
 
317
324
  /** Owner tools (a product, a coupon, a scheduling link) reach a member as plain text: a title, detail
318
325
  * lines and `Label: https://…` lines. On a phone that is a wall of text with a URL nobody can tap, so a
319
- * link line becomes a button and a coupon code its own block. Only plain https links are lifted. */
320
- export function extractBusinessMessageExtras(markdown: string): { text: string; links: { label: string; url: string }[]; couponCode: string } {
326
+ * link line becomes a button and a coupon code its own block. Only plain https links are lifted. A message shown
327
+ * without a card (an AI answer) has no coupon block, so `liftCoupon: false` leaves its code in the text. */
328
+ export function extractBusinessMessageExtras(markdown: string, options: { liftCoupon?: boolean } = {}): { text: string; links: { label: string; url: string }[]; couponCode: string } {
329
+ const liftCoupon = options.liftCoupon ?? true;
321
330
  const kept: string[] = [];
322
331
  const links: { label: string; url: string }[] = [];
323
332
  let couponCode = "";
@@ -343,7 +352,7 @@ export function extractBusinessMessageExtras(markdown: string): { text: string;
343
352
  }
344
353
  }
345
354
  const code = /^(.*?)[,;]?\s*\b[Cc]ode:\s*([A-Z0-9_-]{3,32})$/.exec(trimmed);
346
- if (code && !couponCode) {
355
+ if (code && liftCoupon && !couponCode) {
347
356
  couponCode = code[2]!;
348
357
  if (code[1]!.trim()) kept.push(code[1]!.trim());
349
358
  continue;
@@ -383,6 +392,14 @@ export function presentBusinessMessages<T extends Pick<BusinessMessage, "id" | "
383
392
  noCoupon: boolean;
384
393
  productSlug: string;
385
394
  productName: string;
395
+ isCard: boolean;
396
+ notCard: boolean;
397
+ noSuggestions: boolean;
398
+ noActions: boolean;
399
+ cardTitle: string;
400
+ cardDetail: string;
401
+ /** A card with neither title nor detail (a file, a coupon with no line): its text block is hidden, not an empty gap. */
402
+ noCardText: boolean;
386
403
  })[] {
387
404
  const metadata = new Map<string, RecordValue>();
388
405
  for (const value of raw) {
@@ -410,14 +427,15 @@ export function presentBusinessMessages<T extends Pick<BusinessMessage, "id" | "
410
427
  : "";
411
428
  if (key) previousDay = key;
412
429
 
413
- const imageUrl = imagePart?.url || "";
414
430
  const template = object(message.templateData);
431
+ // A product the owner sends from the Inbox carries its picture on the card itself (`templateData.image`).
432
+ const imageUrl = imagePart?.url || cardImage(template.image);
415
433
  const standardEventText = message.messageType === "template/standard-event"
416
434
  ? [template.title, ...(Array.isArray(template.lines) ? template.lines : [])]
417
435
  .filter((value): value is string => typeof value === "string" && !!value.trim())
418
436
  .join("\n")
419
437
  : "";
420
- const bodyText = imageUrl
438
+ const bodyText = imagePart?.url
421
439
  ? row.parts.filter((part) => part.kind === "text").map((part) => [part.text, ...(part.fields ?? []).map((field) => `${field.name}: ${field.value}`)].filter(Boolean).join("\n")).filter(Boolean).join("\n")
422
440
  : standardEventText || row.plainText;
423
441
  const senderAvatar = safeAvatar(image.url);
@@ -434,7 +452,8 @@ export function presentBusinessMessages<T extends Pick<BusinessMessage, "id" | "
434
452
  const bodyRuns = htmlMessageRuns(message) ?? plainTextRuns;
435
453
  const bodyMarkdown = markdownFromRuns(bodyRuns);
436
454
  const buttons = messageButtons(message, row.id);
437
- const extras = mine ? { text: bodyMarkdown, links: [], couponCode: "" } : extractBusinessMessageExtras(bodyMarkdown);
455
+ // An AI answer is never a card, and only a card shows the coupon block, so its code stays in its text.
456
+ const extras = mine ? { text: bodyMarkdown, links: [], couponCode: "" } : extractBusinessMessageExtras(bodyMarkdown, { liftCoupon: !isAiGenerated });
438
457
  // A card is a title and detail lines: Wix sends events as one, and an owner's product, coupon or scheduling link
439
458
  // arrives as plain text of the same shape. Its title leads in bold, and a coupon's title is its code, which
440
459
  // the coupon block already shows.
@@ -447,10 +466,21 @@ export function presentBusinessMessages<T extends Pick<BusinessMessage, "id" | "
447
466
  const titled = cardLike && extras.couponCode && first === extras.couponCode ? lines.slice(1).join("\n")
448
467
  : cardLike && lines.length > 1 && !first.startsWith("**") ? `**${first}**\n${lines.slice(1).join("\n")}` : extras.text;
449
468
  // A product card links to its store page; the slug lets the worker find the product and show its picture.
450
- const productPage = cardLike ? extras.links.map((link) => /^https:\/\/[^/?#]+(?:\/[^/?#]+)*?\/product-page\/([^/?#]+)/i.exec(link.url)?.[1]).find(Boolean) : undefined;
469
+ // The link is a line of the text or, from the Inbox's product tool, a template button.
470
+ // Only a card shows the picture, so an AI answer (never a card) does not look one up.
471
+ const productPage = cardLike && !isAiGenerated ? [...extras.links, ...articleActions(message, row.id)].map((link) => /^https:\/\/[^/?#]+(?:\/[^/?#]+)*?\/product-page\/([^/?#]+)/i.exec(link.url)?.[1]).find(Boolean) : undefined;
451
472
  // A file, recording or video has no inline player here: it gets a button that opens it.
452
473
  const attachmentLinks = row.parts.filter((part) => (part.kind === "file" || part.kind === "audio" || part.kind === "video") && !!part.url)
453
474
  .map((part, at) => ({ id: `${row.id}:attachment-${at}`, label: (part.fileName || part.caption || `Open ${part.kind}`).slice(0, 40), url: part.url as string, style: "secondary" as const }));
475
+ // The card look (a framed box with a centred title) is for what an owner tool sends: a product, coupon, scheduling
476
+ // or pay link, an event, or a file. A coupon's title is its code, which the coupon block already shows.
477
+ // An AI answer is text with link pills, as on the site's Smart Chat, even when it is shaped like a card.
478
+ const isCard = !mine && !isAiGenerated && (cardLike || !!extras.couponCode || (attachmentLinks.length > 0 && !imageUrl));
479
+ const cardTitle = cardLike && !(extras.couponCode && first === extras.couponCode) ? first.replace(/^\*\*(.+)\*\*$/, "$1") : "";
480
+ // A product card shows its name and price, as on the site's Smart Chat; the "4 variants | In stock" summary the
481
+ // Inbox adds to the text is left out of it.
482
+ const cardLines = cardTitle ? lines.slice(1) : titled.split("\n");
483
+ const cardDetail = (productPage ? cardLines.filter((line) => !/^\s*\d+\s+variants?\b|\|\s*(?:in|out of)\s+stock\s*$/i.test(line)) : cardLines).join("\n");
454
484
  const articles = [...articleActions(message, row.id), ...attachmentLinks, ...extras.links.map((link, at) => ({ id: `${row.id}:link-${at}`, label: link.label, url: link.url, style: /\b(view|buy|shop|book|pay|order|checkout|reserve|schedule)\b/i.test(link.label) ? "primary" as const : "secondary" as const }))]
455
485
  .filter((action, at, all) => all.findIndex((other) => other.url === action.url) === at);
456
486
 
@@ -463,7 +493,8 @@ export function presentBusinessMessages<T extends Pick<BusinessMessage, "id" | "
463
493
  senderInitials: typeof display.initials === "string" && display.initials.trim() ? display.initials.trim() : initials(senderName),
464
494
  senderBadge: participantId && !mine ? isAiGenerated ? "AI assistant" : "Staff Response" : "",
465
495
  imageUrl,
466
- imageDescription: imagePart?.caption || "Image attachment",
496
+ // A card's own picture is named by the card (a product's name), not as an anonymous attachment.
497
+ imageDescription: imagePart?.caption || (imageUrl && !imagePart?.url && first ? first.replace(/\*/g, "") : "Image attachment"),
467
498
  noImage: !imageUrl,
468
499
  bodyText,
469
500
  bodyRuns,
@@ -472,6 +503,13 @@ export function presentBusinessMessages<T extends Pick<BusinessMessage, "id" | "
472
503
  noCoupon: !extras.couponCode,
473
504
  productSlug: productPage ?? "",
474
505
  productName: productPage ? first : "",
506
+ isCard,
507
+ notCard: !isCard,
508
+ noSuggestions: buttons.length === 0,
509
+ noActions: articles.length === 0,
510
+ cardTitle,
511
+ noCardText: !cardTitle && !cardDetail.trim(),
512
+ cardDetail,
475
513
  isAiGenerated,
476
514
  suggestions: buttons,
477
515
  articleActions: articles,
@@ -0,0 +1,102 @@
1
+ // Store categories for the Branded Lite Shop chip row and the Home "Shop by category"
2
+ // rail — one reader, so both list the same categories in the same order.
3
+ //
4
+ // Catalog V3 Categories first, V1 collections only when V3 THROWS (a legacy V1 site):
5
+ // a V3 success with zero rows is an empty catalogue, and V1 on a V3 site answers 428.
6
+ // Same rule as wix-spaces-collections; see the reasoning there.
7
+ import { categoryImage, queryCollections, queryStoreCategories, type StoreCategoriesResponse, type StoreCategory, type WixCollection } from "@himalaya/wix-stores-collections";
8
+ import { wixMediaUrl } from "@himalaya/wix-media";
9
+
10
+ export interface StoreCategoryCard {
11
+ id: string;
12
+ name: string;
13
+ image: string;
14
+ hasImage: boolean;
15
+ noImage: boolean;
16
+ }
17
+
18
+ /** "All Products" is a Wix system collection — every product is in it, so it is
19
+ * the Shop's own "All" chip, not a category a shopper picks. */
20
+ const SYSTEM_NAMES = ["all products"];
21
+ const SYSTEM_COLLECTION_IDS = ["00000000-000000-000000-000000000001"];
22
+
23
+ function card(id: string, name: string, image: string): StoreCategoryCard {
24
+ return { id, name, image, hasImage: !!image, noImage: !image };
25
+ }
26
+
27
+ function fromCategory(c: StoreCategory): StoreCategoryCard {
28
+ return card(c.id ?? "", (c.name ?? "").trim(), categoryImage(c));
29
+ }
30
+
31
+ function fromCollection(c: WixCollection): StoreCategoryCard {
32
+ const raw = c as unknown as { id?: string; name?: string; media?: { mainMedia?: { image?: { url?: string }; thumbnail?: { url?: string } } } };
33
+ const media = raw.media?.mainMedia;
34
+ return card(raw.id ?? "", (raw.name ?? "").trim(), wixMediaUrl(media?.image?.url ?? media?.thumbnail?.url ?? ""));
35
+ }
36
+
37
+ function visible(rows: StoreCategoryCard[]): StoreCategoryCard[] {
38
+ const seen = new Set<string>();
39
+ return rows.filter((r) => {
40
+ if (!r.id || !r.name || SYSTEM_COLLECTION_IDS.includes(r.id) || SYSTEM_NAMES.includes(r.name.toLowerCase()) || seen.has(r.id)) return false;
41
+ seen.add(r.id);
42
+ return true;
43
+ });
44
+ }
45
+
46
+ const PAGE_SIZE = 50;
47
+ const MAX_PAGES = 100;
48
+
49
+ async function loadLegacyCategories(): Promise<StoreCategoryCard[]> {
50
+ const cards: StoreCategoryCard[] = [];
51
+ const seen = new Set<string>();
52
+ let offset = 0;
53
+ let total: number | undefined;
54
+ for (let page = 0; page < MAX_PAGES; page++) {
55
+ const r = await queryCollections({ query: { paging: { limit: PAGE_SIZE, offset } } });
56
+ const rows = r.collections ?? [];
57
+ const reportedTotal = r.totalResults ?? r.metadata?.total;
58
+ if (reportedTotal != null) total = Math.max(total ?? 0, reportedTotal);
59
+ if (!rows.length) {
60
+ if (total != null && seen.size < total) throw new Error("Category pagination is incomplete");
61
+ return visible(cards);
62
+ }
63
+ const priorCount = seen.size;
64
+ rows.forEach(row => { if (row.id) seen.add(row.id); });
65
+ if (page > 0 && seen.size === priorCount) throw new Error("Category pagination made no progress");
66
+ cards.push(...rows.filter(c => c.visible !== false).map(fromCollection));
67
+ offset += rows.length;
68
+ if (total != null ? offset >= total : rows.length < PAGE_SIZE) {
69
+ if (total != null && seen.size < total) throw new Error("Category pagination is incomplete");
70
+ return visible(cards);
71
+ }
72
+ }
73
+ throw new Error("Category pagination exceeded the page limit");
74
+ }
75
+
76
+ /** The store's browsable categories in site order. Rejects incomplete reads so callers
77
+ * can retain their last good catalog instead of invalidating a later category. */
78
+ export async function loadStoreCategories(): Promise<StoreCategoryCard[]> {
79
+ let r: StoreCategoriesResponse;
80
+ try {
81
+ r = await queryStoreCategories(PAGE_SIZE);
82
+ } catch { return loadLegacyCategories(); /* Only the initial failure can identify a legacy V1 site. */ }
83
+ const cards: StoreCategoryCard[] = [];
84
+ const cursors = new Set<string>();
85
+ const seen = new Set<string>();
86
+ let total = 0;
87
+ for (let page = 0; page < MAX_PAGES; page++) {
88
+ const categories = r.categories ?? [];
89
+ categories.forEach(c => { if (c.id) seen.add(c.id); });
90
+ total = Math.max(total, r.pagingMetadata?.total ?? 0);
91
+ cards.push(...categories.filter(c => c.visible !== false).map(fromCategory));
92
+ const next = r.pagingMetadata?.cursors?.next;
93
+ if (!next) {
94
+ if (seen.size < total) throw new Error("Category pagination is incomplete");
95
+ return visible(cards);
96
+ }
97
+ if (cursors.has(next)) throw new Error("Category pagination repeated a cursor");
98
+ cursors.add(next);
99
+ if (page + 1 < MAX_PAGES) r = await queryStoreCategories(PAGE_SIZE, next);
100
+ }
101
+ throw new Error("Category pagination exceeded the page limit");
102
+ }