@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.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-collections/src/display.d.ts +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-collections/src/index.d.ts +2 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores-collections.mjs +2 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores.mjs +3 -2
- package/dist/cli.mjs +30 -30
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/sdk-reference.json +8 -2
- package/guides/trim-a-template.md +1 -1
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +2465 -2035
- package/templates/beauty-salon/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/beauty-salon/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/beauty-salon/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/beauty-salon/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/beauty-salon-owner/dev/index.ts +571 -0
- package/templates/beauty-salon-owner/himalaya.content.json +6 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/branded-lite/dev/index.ts +2989 -2197
- package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +46 -8
- package/templates/branded-lite/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/branded-lite/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/branded-lite/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/branded-lite/tier5-src/business-chat/index.ts +3 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/branded-lite/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/branded-lite/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/community-club/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/community-club/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/community-club-owner/dev/index.ts +571 -0
- package/templates/community-club-owner/himalaya.content.json +6 -0
- package/templates/community-club-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/community-club-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/community-club-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/events-meetups/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/events-meetups/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/events-meetups-owner/dev/index.ts +571 -0
- package/templates/events-meetups-owner/himalaya.content.json +6 -0
- package/templates/events-meetups-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/events-meetups-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/gym-fitness/dev/index.ts +2600 -2170
- package/templates/gym-fitness/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/gym-fitness/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/gym-fitness/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/gym-fitness/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/gym-fitness-owner/dev/index.ts +571 -0
- package/templates/gym-fitness-owner/himalaya.content.json +6 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/nonprofit-cause/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/nonprofit-cause/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/nonprofit-cause-owner/dev/index.ts +571 -0
- package/templates/nonprofit-cause-owner/himalaya.content.json +6 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/owner-lite/dev/index.ts +571 -0
- package/templates/owner-lite/himalaya.content.json +6 -0
- package/templates/owner-lite/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/owner-lite/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/owner-lite/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/podcast-creator/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/podcast-creator/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/podcast-creator-owner/dev/index.ts +571 -0
- package/templates/podcast-creator-owner/himalaya.content.json +6 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/portfolio-creative/dev/index.ts +2470 -2040
- package/templates/portfolio-creative/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/portfolio-creative/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/portfolio-creative/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/portfolio-creative/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/portfolio-creative-owner/dev/index.ts +571 -0
- package/templates/portfolio-creative-owner/himalaya.content.json +6 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/restaurant/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/restaurant/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/restaurant-owner/dev/index.ts +571 -0
- package/templates/restaurant-owner/himalaya.content.json +6 -0
- package/templates/restaurant-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/restaurant-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/retail-store/dev/index.ts +2583 -2137
- package/templates/retail-store/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/retail-store/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/retail-store/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/retail-store/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/retail-store/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/retail-store/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/retail-store-owner/dev/index.ts +571 -0
- package/templates/retail-store-owner/himalaya.content.json +6 -0
- package/templates/retail-store-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/retail-store-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/yoga-studio/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/yoga-studio-owner/dev/index.ts +571 -0
- package/templates/yoga-studio-owner/himalaya.content.json +6 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-store-collection.ts +0 -22
- package/templates/beauty-salon/tier5-src/himi-store-shop/index.ts +0 -173
- package/templates/branded-lite/tier5-src/_shared/himi-store-collection.ts +0 -22
- package/templates/branded-lite/tier5-src/himi-store-shop/index.ts +0 -173
- package/templates/gym-fitness/tier5-src/_shared/himi-store-collection.ts +0 -22
- package/templates/gym-fitness/tier5-src/himi-store-shop/index.ts +0 -173
- package/templates/portfolio-creative/tier5-src/_shared/himi-store-collection.ts +0 -22
- package/templates/portfolio-creative/tier5-src/himi-store-shop/index.ts +0 -173
- package/templates/retail-store/tier5-src/_shared/himi-store-collection.ts +0 -22
- 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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|