@wix/agent-skills 1.16.0 → 1.16.2
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/package.json +1 -1
- package/skills/wix-app/SKILL.md +72 -8
- package/skills/wix-app/references/CODE_QUALITY.md +0 -1
- package/skills/wix-app/references/DASHBOARD_MODAL.md +21 -14
- package/skills/wix-app/references/DASHBOARD_PAGE.md +30 -184
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +19 -3
- package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +172 -0
- package/skills/wix-app/references/dashboard-page/WDS_LAYOUT.md +187 -0
- package/skills/wix-app/references/editor-react-component/A11Y-REVIEW.md +12 -7
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +31 -2
- package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +6 -1
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +60 -0
- package/skills/wix-app/references/editor-react-component/DIRECTIONALITY.md +6 -6
- package/skills/wix-app/references/editor-react-component/FUNCTION-HANDLERS.md +167 -0
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +2 -2
- package/skills/wix-app/references/editor-react-component/SITE-CONTEXT-HOOKS.md +187 -0
- package/skills/wix-app/scripts/scan-a11y-code.cjs +777 -0
- package/skills/wix-app/scripts/scan-a11y-eslint.cjs +201 -0
- package/skills/wix-base44-connector/SKILL.md +185 -0
- package/skills/wix-base44-connector/scripts/utils.js +212 -0
- package/skills/wix-design-system/SKILL.md +19 -15
- package/skills/wix-docs/SKILL.md +75 -1
- package/skills/wix-headless/references/CAPABILITIES.md +16 -3
- package/skills/wix-headless/references/SDK_HANDOFF.md +1 -1
- package/skills/wix-headless/references/SEED.md +1 -1
- package/skills/wix-headless/references/inline-recipes/how-to-code-forms.md +560 -102
- package/skills/wix-headless/references/inline-recipes/setup-forms.md +210 -143
- package/skills/wix-manage/SKILL.md +20 -17
- package/skills/wix-manage/references/accessibility/scan-site-accessibility.md +98 -0
- package/skills/wix-manage/references/analytics/analytics-dashboard-navigation.md +1 -2
- package/skills/wix-manage/references/domains/domain-search-purchase-and-connect.md +2603 -0
- package/skills/wix-manage/references/events/create-wix-event.md +112 -90
- package/skills/wix-manage/references/events/manage-wix-events.md +33 -94
- package/skills/wix-manage/references/sites/create-headless-site.md +1 -0
- package/skills/wix-manage/references/sites/manage-oauth-apps.md +154 -0
- package/skills/wix-manage/references/sites/read-site-context.md +125 -0
- package/skills/wix-manage/references/sites/site-import.md +67 -15
- package/skills/wix-manage/references/stores/create-product-catalog-v3.md +273 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/wix-writers.js +1 -1
- package/skills/wix-vibe-headless/SKILL.md +2 -1
- package/skills/wix-vibe-headless/install/deploy.cjs +63 -3
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +104 -0
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +74 -0
- package/skills/wix-vibe-headless/platforms/base44.md +15 -40
- package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +7 -11
- package/skills/wix-vibe-headless/references/blog/seed/SEED.md +1 -1
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +7 -11
- package/skills/wix-vibe-headless/references/bookings/seed/SEED.md +1 -1
- package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +1 -2
- package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +7 -12
- package/skills/wix-vibe-headless/references/events/seed/SEED.md +1 -1
- package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +7 -11
- package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +7 -11
- package/skills/wix-vibe-headless/references/portfolio/seed/SEED.md +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +7 -11
- package/skills/wix-vibe-headless/references/pricing-plans/seed/SEED.md +1 -1
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +7 -11
- package/skills/wix-vibe-headless/references/restaurants/seed/SEED.md +1 -1
- package/skills/wix-vibe-headless/references/shared/app/components/WixManageBanner.jsx +1 -1
- package/skills/wix-vibe-headless/references/shared/app/rest/wix-config.js +5 -3
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +75 -13
- package/skills/wix-vibe-headless/references/storefront/app/components/ProductCard.jsx +29 -86
- package/skills/wix-vibe-headless/references/storefront/app/components/ProductGrid.jsx +3 -8
- package/skills/wix-vibe-headless/references/storefront/app/components/VariantPicker.jsx +78 -105
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +92 -0
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useVariantOptions.js +76 -0
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +1 -1
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.js +44 -5
- package/skills/wix-app/references/AUTO_PATTERNS_DASHBOARD.md +0 -326
- package/skills/wix-app/references/auto-patterns-dashboard/action-cell.md +0 -115
- package/skills/wix-app/references/auto-patterns-dashboard/app-config-structure.md +0 -264
- package/skills/wix-app/references/auto-patterns-dashboard/app-context.md +0 -37
- package/skills/wix-app/references/auto-patterns-dashboard/bulk-actions.md +0 -86
- package/skills/wix-app/references/auto-patterns-dashboard/collection-page-actions.md +0 -119
- package/skills/wix-app/references/auto-patterns-dashboard/collection-page.md +0 -103
- package/skills/wix-app/references/auto-patterns-dashboard/custom-actions-override.md +0 -76
- package/skills/wix-app/references/auto-patterns-dashboard/custom-columns-override.md +0 -82
- package/skills/wix-app/references/auto-patterns-dashboard/custom-components-override.md +0 -89
- package/skills/wix-app/references/auto-patterns-dashboard/custom-header-override.md +0 -124
- package/skills/wix-app/references/auto-patterns-dashboard/custom-sections-override.md +0 -91
- package/skills/wix-app/references/auto-patterns-dashboard/custom-slots-override.md +0 -71
- package/skills/wix-app/references/auto-patterns-dashboard/entity-page-actions.md +0 -70
- package/skills/wix-app/references/auto-patterns-dashboard/entity-page-view-actions.md +0 -73
- package/skills/wix-app/references/auto-patterns-dashboard/entity-page.md +0 -98
- package/skills/wix-app/references/auto-patterns-dashboard/example-patterns.json +0 -278
- package/skills/wix-app/references/auto-patterns-dashboard/pages-configuration.md +0 -94
- package/skills/wix-app/references/auto-patterns-dashboard/resolved-action.md +0 -43
- package/skills/wix-app/references/auto-patterns-dashboard/sdk-utilities.md +0 -66
- package/skills/wix-app/references/auto-patterns-dashboard/views.md +0 -76
- package/skills/wix-app/scripts/generate-auto-patterns.js +0 -510
- package/skills/wix-manage/references/domains/domain-search-and-purchase.md +0 -437
- package/skills/wix-manage/references/stores/create-product-from-image.md +0 -1031
- package/skills/wix-manage/references/stores/create-product-with-options-catalog-v3.md +0 -623
- package/skills/wix-manage/references/stores/setup-online-store-catalog-v3.md +0 -106
|
@@ -1,118 +1,91 @@
|
|
|
1
|
-
// PDP choice controls
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// PDP choice controls built on useVariantOptions — swap this component for your own if you want
|
|
2
|
+
// a different look; the data contract (optionGroups / modifierGroups) stays the same.
|
|
3
|
+
// Styled with base44 design tokens (shadcn Tailwind classes).
|
|
4
|
+
import { useVariantOptions } from "@/hooks/useVariantOptions";
|
|
4
5
|
|
|
5
6
|
const chipBase = "py-1.5 px-3 cursor-pointer text-sm font-body border rounded-sm";
|
|
6
7
|
const chipIdle = "border-border bg-card text-foreground";
|
|
7
8
|
const chipActive = "border-primary bg-primary text-primary-foreground";
|
|
8
9
|
const label = "block mb-1.5 text-[13px] font-semibold text-muted-foreground";
|
|
9
10
|
|
|
10
|
-
// A colour/swatch option renders as swatches, a text option as pills — the render type is on the
|
|
11
|
-
// option (`optionRenderType`) and mirrored per choice (`choiceType`: ONE_COLOR vs CHOICE_TEXT).
|
|
12
|
-
// Colour choices carry `colorCode` (hex) and usually `linkedMedia`, so a swatch is real colour, and
|
|
13
|
-
// picking one moves the gallery to that colour's photo.
|
|
14
|
-
const isColorChoice = (option, choice) =>
|
|
15
|
-
option?.optionRenderType === "COLOR_CHOICES" ||
|
|
16
|
-
option?.optionRenderType === "SWATCH_CHOICES" ||
|
|
17
|
-
choice?.choiceType === "ONE_COLOR";
|
|
18
|
-
|
|
19
|
-
function OptionSelector({ option, selected, onSelect }) {
|
|
20
|
-
// `visible: false` is a choice the merchant retired — it stays in the payload, so filter it out or
|
|
21
|
-
// the buyer can pick something the catalogue no longer offers.
|
|
22
|
-
const choices = (option.choicesSettings?.choices || []).filter((c) => c.visible !== false);
|
|
23
|
-
if (!choices.length) return null;
|
|
24
|
-
const swatches = choices.some((c) => isColorChoice(option, c) && c.colorCode);
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<div className="mb-4">
|
|
28
|
-
<label className={label}>
|
|
29
|
-
{option.name}
|
|
30
|
-
{/* With swatches the chosen colour's name isn't otherwise on screen — echo it. */}
|
|
31
|
-
{swatches && selected && (
|
|
32
|
-
<span className="ml-1.5 font-normal text-foreground">
|
|
33
|
-
{choices.find((c) => c.choiceId === selected)?.name}
|
|
34
|
-
</span>
|
|
35
|
-
)}
|
|
36
|
-
</label>
|
|
37
|
-
<div className="flex flex-wrap gap-2">
|
|
38
|
-
{choices.map((c) => {
|
|
39
|
-
const active = selected === c.choiceId;
|
|
40
|
-
const out = c.inStock === false;
|
|
41
|
-
if (swatches && c.colorCode) {
|
|
42
|
-
return (
|
|
43
|
-
<button
|
|
44
|
-
key={c.choiceId}
|
|
45
|
-
type="button"
|
|
46
|
-
disabled={out}
|
|
47
|
-
onClick={() => onSelect(option.id, c.choiceId)}
|
|
48
|
-
aria-pressed={active}
|
|
49
|
-
aria-label={`${c.name}${out ? " (out of stock)" : ""}`}
|
|
50
|
-
title={c.name}
|
|
51
|
-
className={`relative w-9 h-9 rounded-full cursor-pointer transition-shadow ring-1 ring-inset ring-black/15 ${
|
|
52
|
-
active ? "ring-2 ring-offset-2 ring-offset-background ring-primary" : ""
|
|
53
|
-
} ${out ? "opacity-40 cursor-not-allowed" : "hover:ring-2 hover:ring-offset-1 hover:ring-offset-background hover:ring-border"}`}
|
|
54
|
-
style={{ backgroundColor: c.colorCode }}
|
|
55
|
-
>
|
|
56
|
-
{/* A struck line reads as "unavailable" without hiding which colour it was. */}
|
|
57
|
-
{out && (
|
|
58
|
-
<span aria-hidden="true" className="absolute inset-0 grid place-items-center">
|
|
59
|
-
<span className="w-full h-px bg-destructive rotate-45" />
|
|
60
|
-
</span>
|
|
61
|
-
)}
|
|
62
|
-
</button>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
return (
|
|
66
|
-
<button
|
|
67
|
-
key={c.choiceId}
|
|
68
|
-
type="button"
|
|
69
|
-
disabled={out}
|
|
70
|
-
aria-pressed={active}
|
|
71
|
-
onClick={() => onSelect(option.id, c.choiceId)}
|
|
72
|
-
className={`${chipBase} ${active ? chipActive : chipIdle} ${out ? "opacity-40 line-through" : ""}`}
|
|
73
|
-
>
|
|
74
|
-
{c.name}
|
|
75
|
-
</button>
|
|
76
|
-
);
|
|
77
|
-
})}
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function ModifierSelector({ modifier, value, onChange }) {
|
|
84
|
-
const key = modifier.modifierRenderType === "FREE_TEXT" ? modifier.freeTextSettings?.key : modifier.key;
|
|
85
|
-
if (modifier.modifierRenderType === "FREE_TEXT") {
|
|
86
|
-
return (
|
|
87
|
-
<div className="mb-4">
|
|
88
|
-
<label className={label}>{modifier.name}{modifier.mandatory && " *"}</label>
|
|
89
|
-
<input value={value || ""} onChange={(e) => onChange(key, e.target.value)}
|
|
90
|
-
className="w-full py-2 px-3 font-body border border-input rounded-sm bg-background text-foreground" />
|
|
91
|
-
</div>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
return (
|
|
95
|
-
<div className="mb-4">
|
|
96
|
-
<label className={label}>{modifier.name}{modifier.mandatory && " *"}</label>
|
|
97
|
-
<div className="flex flex-wrap gap-2">
|
|
98
|
-
{modifier.choicesSettings?.choices?.map((c) => (
|
|
99
|
-
<button key={c.key} aria-pressed={value === c.key} onClick={() => onChange(key, c.key)}
|
|
100
|
-
className={`${chipBase} ${value === c.key ? chipActive : chipIdle}`}>{c.name}</button>
|
|
101
|
-
))}
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
11
|
export default function VariantPicker({ options, modifiers, selectedOptions, selectOption, modifierValues, setModifier }) {
|
|
12
|
+
const { optionGroups, modifierGroups } = useVariantOptions(options, modifiers, selectedOptions, modifierValues);
|
|
13
|
+
|
|
108
14
|
return (
|
|
109
15
|
<>
|
|
110
|
-
{
|
|
111
|
-
<
|
|
16
|
+
{optionGroups.map((group) => (
|
|
17
|
+
<div key={group.id} className="mb-4">
|
|
18
|
+
<label className={label}>
|
|
19
|
+
{group.name}
|
|
20
|
+
{group.isColor && (
|
|
21
|
+
<span className="ml-1.5 font-normal text-foreground">
|
|
22
|
+
{group.choices.find((c) => c.selected)?.name}
|
|
23
|
+
</span>
|
|
24
|
+
)}
|
|
25
|
+
</label>
|
|
26
|
+
<div className="flex flex-wrap gap-2">
|
|
27
|
+
{group.choices.map((c) =>
|
|
28
|
+
c.isColorSwatch ? (
|
|
29
|
+
<button
|
|
30
|
+
key={c.choiceId}
|
|
31
|
+
type="button"
|
|
32
|
+
disabled={!c.inStock}
|
|
33
|
+
onClick={() => selectOption(group.id, c.choiceId)}
|
|
34
|
+
aria-pressed={c.selected}
|
|
35
|
+
aria-label={`${c.name}${!c.inStock ? " (out of stock)" : ""}`}
|
|
36
|
+
title={c.name}
|
|
37
|
+
className={`relative w-9 h-9 rounded-full cursor-pointer transition-shadow ring-1 ring-inset ring-black/15 ${
|
|
38
|
+
c.selected ? "ring-2 ring-offset-2 ring-offset-background ring-primary" : ""
|
|
39
|
+
} ${!c.inStock ? "opacity-40 cursor-not-allowed" : "hover:ring-2 hover:ring-offset-1 hover:ring-offset-background hover:ring-border"}`}
|
|
40
|
+
style={{ backgroundColor: c.colorCode }}
|
|
41
|
+
>
|
|
42
|
+
{!c.inStock && (
|
|
43
|
+
<span aria-hidden="true" className="absolute inset-0 grid place-items-center">
|
|
44
|
+
<span className="w-full h-px bg-destructive rotate-45" />
|
|
45
|
+
</span>
|
|
46
|
+
)}
|
|
47
|
+
</button>
|
|
48
|
+
) : (
|
|
49
|
+
<button
|
|
50
|
+
key={c.choiceId}
|
|
51
|
+
type="button"
|
|
52
|
+
disabled={!c.inStock}
|
|
53
|
+
aria-pressed={c.selected}
|
|
54
|
+
onClick={() => selectOption(group.id, c.choiceId)}
|
|
55
|
+
className={`${chipBase} ${c.selected ? chipActive : chipIdle} ${!c.inStock ? "opacity-40 line-through" : ""}`}
|
|
56
|
+
>
|
|
57
|
+
{c.name}
|
|
58
|
+
</button>
|
|
59
|
+
)
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
112
63
|
))}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
64
|
+
|
|
65
|
+
{modifierGroups.map((m) => (
|
|
66
|
+
<div key={m.key} className="mb-4">
|
|
67
|
+
<label className={label}>{m.name}{m.mandatory && " *"}</label>
|
|
68
|
+
{m.type === "text" ? (
|
|
69
|
+
<input
|
|
70
|
+
value={m.value}
|
|
71
|
+
onChange={(e) => setModifier(m.key, e.target.value)}
|
|
72
|
+
className="w-full py-2 px-3 font-body border border-input rounded-sm bg-background text-foreground"
|
|
73
|
+
/>
|
|
74
|
+
) : (
|
|
75
|
+
<div className="flex flex-wrap gap-2">
|
|
76
|
+
{m.choices.map((c) => (
|
|
77
|
+
<button
|
|
78
|
+
key={c.key}
|
|
79
|
+
aria-pressed={c.selected}
|
|
80
|
+
onClick={() => setModifier(m.key, c.key)}
|
|
81
|
+
className={`${chipBase} ${c.selected ? chipActive : chipIdle}`}
|
|
82
|
+
>
|
|
83
|
+
{c.name}
|
|
84
|
+
</button>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
116
89
|
))}
|
|
117
90
|
</>
|
|
118
91
|
);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Headless data layer for a product grid tile.
|
|
2
|
+
// Normalises raw Wix product fields into render-agnostic structures so you can build
|
|
3
|
+
// whatever card UI you want (image layout, badge style, price display, quick-add trigger)
|
|
4
|
+
// without touching the badge-priority logic, price-range maths, or colour-dot extraction.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// const { isSoldOut, leftBadges, promoBadge, priceDisplay, compareAtDisplay,
|
|
8
|
+
// colors, optionLabel, isQuickAddable, image, hoverImage } = useProductCard(product);
|
|
9
|
+
// // then render however you want, call addToCart(product.id) from useCart() for quick-add.
|
|
10
|
+
|
|
11
|
+
import { useMemo } from "react";
|
|
12
|
+
import { productImage, productGallery } from "@/lib/storeImage";
|
|
13
|
+
|
|
14
|
+
function discountPercent(product) {
|
|
15
|
+
const now = Number(product?.actualPriceRange?.minValue?.amount);
|
|
16
|
+
const was = Number(product?.compareAtPriceRange?.minValue?.amount);
|
|
17
|
+
if (!now || !was || was <= now) return null;
|
|
18
|
+
return Math.round(((was - now) / was) * 100);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useProductCard(product) {
|
|
22
|
+
return useMemo(() => {
|
|
23
|
+
const status = product?.inventory?.availabilityStatus;
|
|
24
|
+
const isSoldOut = status === "OUT_OF_STOCK";
|
|
25
|
+
// preorderStatus is only meaningful when the product is out of stock
|
|
26
|
+
const isPreorder = isSoldOut && product?.inventory?.preorderStatus === "ENABLED";
|
|
27
|
+
const isPartiallyOutOfStock = status === "PARTIALLY_OUT_OF_STOCK";
|
|
28
|
+
|
|
29
|
+
// Left-side badges (stacked, top-left of the image): stock / pre-order state.
|
|
30
|
+
const leftBadges = [];
|
|
31
|
+
if (isPreorder) leftBadges.push({ type: "pre-order", label: "Pre-order" });
|
|
32
|
+
else if (isSoldOut) leftBadges.push({ type: "sold-out", label: "Sold out" });
|
|
33
|
+
if (isPartiallyOutOfStock) leftBadges.push({ type: "limited-stock", label: "Limited stock" });
|
|
34
|
+
|
|
35
|
+
// Right-side promo badge: discount % beats a merchant ribbon (a "Sale" ribbon is redundant
|
|
36
|
+
// when the % is already showing). Only one at a time.
|
|
37
|
+
const discount = discountPercent(product);
|
|
38
|
+
const ribbon = product?.ribbon?.name;
|
|
39
|
+
const promoBadge = discount
|
|
40
|
+
? { type: "discount", label: `-${discount}%` }
|
|
41
|
+
: ribbon
|
|
42
|
+
? { type: "ribbon", label: ribbon }
|
|
43
|
+
: null;
|
|
44
|
+
|
|
45
|
+
// Price: show a min–max range when variants span different prices so the PDP doesn't
|
|
46
|
+
// appear to raise the price once a variant is selected.
|
|
47
|
+
const priceMin = product?.actualPriceRange?.minValue?.formattedAmount;
|
|
48
|
+
const priceMax = product?.actualPriceRange?.maxValue?.formattedAmount;
|
|
49
|
+
const compareAt = product?.compareAtPriceRange?.minValue?.formattedAmount;
|
|
50
|
+
const priceDisplay = priceMax && priceMax !== priceMin ? `${priceMin} – ${priceMax}` : priceMin;
|
|
51
|
+
const compareAtDisplay = compareAt && compareAt !== priceMin ? compareAt : null;
|
|
52
|
+
|
|
53
|
+
// Options preview for the tile summary row.
|
|
54
|
+
// Colour options → real hex dots (more informative than "3 colours").
|
|
55
|
+
// Non-colour options → "3 sizes · 2 materials" (pluralised from the merchant's own name).
|
|
56
|
+
const labels = [], colors = [];
|
|
57
|
+
for (const o of product?.options || []) {
|
|
58
|
+
const choices = (o.choicesSettings?.choices || []).filter((c) => c.visible !== false);
|
|
59
|
+
if (!choices.length) continue;
|
|
60
|
+
const swatches = choices.map((c) => c.colorCode).filter(Boolean);
|
|
61
|
+
if (swatches.length) { colors.push(...swatches); continue; }
|
|
62
|
+
const n = o.name.toLowerCase();
|
|
63
|
+
labels.push(`${choices.length} ${choices.length === 1 || n.endsWith("s") ? n : `${n}s`}`);
|
|
64
|
+
}
|
|
65
|
+
const optionLabel = labels.join(" · ");
|
|
66
|
+
const hasOptions = (product?.options?.length || 0) > 0;
|
|
67
|
+
|
|
68
|
+
// Quick-add is only safe for single-variant products (no option choices to resolve).
|
|
69
|
+
// Sold-out with pre-order still shows a CTA, but it links to the PDP, not quick-add.
|
|
70
|
+
const isQuickAddable = !hasOptions && !isSoldOut;
|
|
71
|
+
|
|
72
|
+
// Images: normalised through lib/storeImage so URLs are consistent across the tile,
|
|
73
|
+
// the PDP gallery, and the cart. Hover image is the second gallery shot (if one exists).
|
|
74
|
+
const image = productImage(product);
|
|
75
|
+
const hoverImage = productGallery(product)[1]?.url ?? null;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
isSoldOut,
|
|
79
|
+
isPreorder,
|
|
80
|
+
isPartiallyOutOfStock,
|
|
81
|
+
leftBadges, // [{ type: 'pre-order'|'sold-out'|'limited-stock', label }]
|
|
82
|
+
promoBadge, // { type: 'discount'|'ribbon', label } | null
|
|
83
|
+
priceDisplay, // formatted price (range or single value)
|
|
84
|
+
compareAtDisplay, // formatted compare-at price | null
|
|
85
|
+
colors, // hex strings — render as dots; the tile shows up to however many you want
|
|
86
|
+
optionLabel, // "3 sizes · 2 materials" or empty string
|
|
87
|
+
isQuickAddable,
|
|
88
|
+
image, // primary image URL | null
|
|
89
|
+
hoverImage, // second image URL for hover effect | null
|
|
90
|
+
};
|
|
91
|
+
}, [product]);
|
|
92
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Headless data layer for product option/variant/modifier picking.
|
|
2
|
+
// Normalises raw Wix product options + modifiers into flat, render-agnostic structures so you
|
|
3
|
+
// can build whatever UI you want (pills, radios, dropdowns, swatches) without touching the
|
|
4
|
+
// data-edge-case logic (visible filtering, color detection, modifier key resolution).
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// const { optionGroups, modifierGroups } = useVariantOptions(
|
|
8
|
+
// options, modifiers, selectedOptions, modifierValues
|
|
9
|
+
// );
|
|
10
|
+
// // then map over optionGroups/modifierGroups to render your own controls and call
|
|
11
|
+
// // selectOption(optionId, choiceId) / setModifier(key, value) from useProductDetail.
|
|
12
|
+
|
|
13
|
+
import { useMemo } from "react";
|
|
14
|
+
|
|
15
|
+
export function useVariantOptions(options, modifiers, selectedOptions, modifierValues) {
|
|
16
|
+
const optionGroups = useMemo(() =>
|
|
17
|
+
(options || []).map((option) => {
|
|
18
|
+
const rawChoices = option.choicesSettings?.choices || [];
|
|
19
|
+
// `visible: false` = a choice the merchant retired — filter it so buyers can't select it.
|
|
20
|
+
const choices = rawChoices
|
|
21
|
+
.filter((c) => c.visible !== false)
|
|
22
|
+
.map((c) => ({
|
|
23
|
+
choiceId: c.choiceId,
|
|
24
|
+
name: c.name,
|
|
25
|
+
colorCode: c.colorCode || null,
|
|
26
|
+
// true when this choice should render as a colour swatch (circle) rather than a pill.
|
|
27
|
+
isColorSwatch: !!(
|
|
28
|
+
(option.optionRenderType === "COLOR_CHOICES" ||
|
|
29
|
+
option.optionRenderType === "SWATCH_CHOICES" ||
|
|
30
|
+
c.choiceType === "ONE_COLOR") &&
|
|
31
|
+
c.colorCode
|
|
32
|
+
),
|
|
33
|
+
inStock: c.inStock !== false,
|
|
34
|
+
selected: selectedOptions[option.id] === c.choiceId,
|
|
35
|
+
}));
|
|
36
|
+
return {
|
|
37
|
+
id: option.id,
|
|
38
|
+
name: option.name,
|
|
39
|
+
// true if ANY choice in this group is a colour swatch — lets you branch on group type.
|
|
40
|
+
isColor: choices.some((c) => c.isColorSwatch),
|
|
41
|
+
choices,
|
|
42
|
+
};
|
|
43
|
+
}),
|
|
44
|
+
[options, selectedOptions]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const modifierGroups = useMemo(() =>
|
|
48
|
+
(modifiers || []).map((m) => {
|
|
49
|
+
const isText = m.modifierRenderType === "FREE_TEXT";
|
|
50
|
+
const key = isText ? m.freeTextSettings?.key : m.key;
|
|
51
|
+
if (isText) {
|
|
52
|
+
return {
|
|
53
|
+
key,
|
|
54
|
+
name: m.name,
|
|
55
|
+
mandatory: !!m.mandatory,
|
|
56
|
+
type: "text",
|
|
57
|
+
value: modifierValues[key] || "",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
key,
|
|
62
|
+
name: m.name,
|
|
63
|
+
mandatory: !!m.mandatory,
|
|
64
|
+
type: "choices",
|
|
65
|
+
choices: (m.choicesSettings?.choices || []).map((c) => ({
|
|
66
|
+
key: c.key,
|
|
67
|
+
name: c.name,
|
|
68
|
+
selected: modifierValues[key] === c.key,
|
|
69
|
+
})),
|
|
70
|
+
};
|
|
71
|
+
}),
|
|
72
|
+
[modifiers, modifierValues]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return { optionGroups, modifierGroups };
|
|
76
|
+
}
|
|
@@ -6,7 +6,7 @@ seed operation. Load it and call **`setupStore` — the one-call path** — with
|
|
|
6
6
|
|
|
7
7
|
```js
|
|
8
8
|
// build-time exec_tool
|
|
9
|
-
const { accessToken } = await base44.asServiceRole.connectors.getConnection("wix");
|
|
9
|
+
const { accessToken } = await base44.asServiceRole.connectors.getConnection("wix");
|
|
10
10
|
const fs = require("fs");
|
|
11
11
|
// exec_tool's require can return EMPTY exports for these build-time modules — load the file itself:
|
|
12
12
|
const seed = (() => { const m = { exports: {} };
|
|
@@ -82,14 +82,51 @@ async function waitForCatalogV3(ctx, { attempts = 40, delayMs = 2000 } = {}) {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
//
|
|
85
|
+
// description string -> Wix rich-text node tree.
|
|
86
|
+
//
|
|
87
|
+
// Descriptions arrive as HTML as often as not: asked to describe a product, a model writes
|
|
88
|
+
// <p>…</p><strong>Care:</strong><br/>. The writable field is `description` (Ricos nodes) — the HTML
|
|
89
|
+
// `plainDescription` the storefront renders is read-only, derived from them. So markup dropped into
|
|
90
|
+
// a single TEXT node is stored as literal text, comes back escaped, and the PDP shows the tags to
|
|
91
|
+
// the buyer. Convert the tags a model actually emits; a string with no tags stays one paragraph.
|
|
92
|
+
const HTML_ENTITIES = { "&": "&", "<": "<", ">": ">", """: '"', "'": "'", " ": " " };
|
|
93
|
+
|
|
94
|
+
function decodeEntities(s) {
|
|
95
|
+
return s.replace(/&(?:amp|lt|gt|quot|#39|nbsp);/g, (m) => HTML_ENTITIES[m] ?? m);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// One paragraph's inner HTML -> TEXT nodes, carrying bold/italic as Ricos decorations.
|
|
99
|
+
function mkTextNodes(html) {
|
|
100
|
+
const nodes = [];
|
|
101
|
+
let bold = 0, italic = 0, last = 0, m;
|
|
102
|
+
const tag = /<(\/?)(strong|b|em|i)\s*\/?>/gi;
|
|
103
|
+
const push = (raw) => {
|
|
104
|
+
const text = decodeEntities(raw.replace(/<[^>]*>/g, ""));
|
|
105
|
+
if (!text) return;
|
|
106
|
+
const decorations = [];
|
|
107
|
+
if (bold > 0) decorations.push({ type: "BOLD" });
|
|
108
|
+
if (italic > 0) decorations.push({ type: "ITALIC" });
|
|
109
|
+
nodes.push({ type: "TEXT", textData: { text, decorations } });
|
|
110
|
+
};
|
|
111
|
+
while ((m = tag.exec(html)) !== null) {
|
|
112
|
+
push(html.slice(last, m.index));
|
|
113
|
+
const step = m[1] ? -1 : 1;
|
|
114
|
+
if (/^(strong|b)$/i.test(m[2])) bold = Math.max(0, bold + step);
|
|
115
|
+
else italic = Math.max(0, italic + step);
|
|
116
|
+
last = tag.lastIndex;
|
|
117
|
+
}
|
|
118
|
+
push(html.slice(last));
|
|
119
|
+
return nodes.length ? nodes : [{ type: "TEXT", textData: { text: "", decorations: [] } }];
|
|
120
|
+
}
|
|
121
|
+
|
|
86
122
|
function mkDesc(text, i) {
|
|
123
|
+
const blocks = String(text ?? "").split(/<\/p\s*>|<br\s*\/?>/i).map((b) => b.trim()).filter(Boolean);
|
|
87
124
|
return {
|
|
88
|
-
nodes: [{
|
|
89
|
-
type: "PARAGRAPH", id: `desc-${i}`,
|
|
90
|
-
nodes:
|
|
125
|
+
nodes: (blocks.length ? blocks : [""]).map((block, n) => ({
|
|
126
|
+
type: "PARAGRAPH", id: `desc-${i}-${n}`,
|
|
127
|
+
nodes: mkTextNodes(block),
|
|
91
128
|
paragraphData: { textStyle: { textAlignment: "AUTO" } },
|
|
92
|
-
}
|
|
129
|
+
})),
|
|
93
130
|
metadata: { version: 1, id: `desc-meta-${i}` },
|
|
94
131
|
};
|
|
95
132
|
}
|
|
@@ -157,6 +194,8 @@ async function listProducts(ctx) {
|
|
|
157
194
|
* Bulk-create products.
|
|
158
195
|
* @param products [{ name, description, price, compareAtPrice?, quantity,
|
|
159
196
|
* options?: [{ name, type?:"text"|"color", choices:["8","9"] | [{name,colorCode}] }] }]
|
|
197
|
+
* description: plain text, or simple HTML (`<p>`, `<br/>`, `<strong>`, `<em>`) — converted to
|
|
198
|
+
* Wix rich text here, so the storefront renders paragraphs and bold rather than tag text.
|
|
160
199
|
* options = ONLY things the buyer selects-and-buys (Size, Color) -> become variants.
|
|
161
200
|
* Display-only attributes go in name/category/description, NOT options. Default: no options.
|
|
162
201
|
* visible/physicalProperties/variant-expansion handled here. `quantity` is the stock created.
|