@wix/agent-skills 1.16.1 → 1.16.3

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 (73) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +72 -8
  3. package/skills/wix-app/references/CODE_QUALITY.md +0 -1
  4. package/skills/wix-app/references/DASHBOARD_MODAL.md +21 -14
  5. package/skills/wix-app/references/DASHBOARD_PAGE.md +30 -184
  6. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +14 -6
  7. package/skills/wix-app/references/EMBEDDED_SCRIPT.md +6 -0
  8. package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +172 -0
  9. package/skills/wix-app/references/dashboard-page/WDS_LAYOUT.md +187 -0
  10. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +32 -2
  11. package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +38 -1
  12. package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +27 -4
  13. package/skills/wix-app/references/editor-react-component/FUNCTION-HANDLERS.md +167 -0
  14. package/skills/wix-app/references/editor-react-component/MANIFEST-ERRORS.md +19 -0
  15. package/skills/wix-app/references/editor-react-component/PARTS.md +1 -2
  16. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +2 -2
  17. package/skills/wix-base44-connector/SKILL.md +185 -0
  18. package/skills/wix-base44-connector/scripts/utils.js +212 -0
  19. package/skills/wix-design-system/SKILL.md +19 -15
  20. package/skills/wix-docs/SKILL.md +75 -1
  21. package/skills/wix-manage/SKILL.md +16 -18
  22. package/skills/wix-manage/references/bookings/create-class-service.md +1 -1
  23. package/skills/wix-manage/references/domains/domain-search-purchase-and-connect.md +2603 -0
  24. package/skills/wix-manage/references/events/create-wix-event.md +112 -90
  25. package/skills/wix-manage/references/events/manage-wix-events.md +33 -94
  26. package/skills/wix-manage/references/rich-content/author-ricos-rich-content.md +182 -5
  27. package/skills/wix-manage/references/sites/create-headless-site.md +1 -0
  28. package/skills/wix-manage/references/sites/manage-oauth-apps.md +154 -0
  29. package/skills/wix-manage/references/sites/read-site-context.md +125 -0
  30. package/skills/wix-manage/references/stores/create-product-catalog-v3.md +273 -0
  31. package/skills/wix-vibe-headless/install/deploy.cjs +1 -2
  32. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +104 -0
  33. package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +74 -0
  34. package/skills/wix-vibe-headless/platforms/base44.md +3 -3
  35. package/skills/wix-vibe-headless/references/blog/seed/SEED.md +1 -1
  36. package/skills/wix-vibe-headless/references/bookings/seed/SEED.md +1 -1
  37. package/skills/wix-vibe-headless/references/events/seed/SEED.md +1 -1
  38. package/skills/wix-vibe-headless/references/portfolio/seed/SEED.md +1 -1
  39. package/skills/wix-vibe-headless/references/pricing-plans/seed/SEED.md +1 -1
  40. package/skills/wix-vibe-headless/references/restaurants/seed/SEED.md +1 -1
  41. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +68 -2
  42. package/skills/wix-vibe-headless/references/storefront/app/components/ProductCard.jsx +29 -86
  43. package/skills/wix-vibe-headless/references/storefront/app/components/ProductGrid.jsx +3 -8
  44. package/skills/wix-vibe-headless/references/storefront/app/components/VariantPicker.jsx +78 -105
  45. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +92 -0
  46. package/skills/wix-vibe-headless/references/storefront/app/hooks/useVariantOptions.js +76 -0
  47. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +1 -1
  48. package/skills/wix-app/references/AUTO_PATTERNS_DASHBOARD.md +0 -326
  49. package/skills/wix-app/references/auto-patterns-dashboard/action-cell.md +0 -115
  50. package/skills/wix-app/references/auto-patterns-dashboard/app-config-structure.md +0 -264
  51. package/skills/wix-app/references/auto-patterns-dashboard/app-context.md +0 -37
  52. package/skills/wix-app/references/auto-patterns-dashboard/bulk-actions.md +0 -86
  53. package/skills/wix-app/references/auto-patterns-dashboard/collection-page-actions.md +0 -119
  54. package/skills/wix-app/references/auto-patterns-dashboard/collection-page.md +0 -103
  55. package/skills/wix-app/references/auto-patterns-dashboard/custom-actions-override.md +0 -76
  56. package/skills/wix-app/references/auto-patterns-dashboard/custom-columns-override.md +0 -82
  57. package/skills/wix-app/references/auto-patterns-dashboard/custom-components-override.md +0 -89
  58. package/skills/wix-app/references/auto-patterns-dashboard/custom-header-override.md +0 -124
  59. package/skills/wix-app/references/auto-patterns-dashboard/custom-sections-override.md +0 -91
  60. package/skills/wix-app/references/auto-patterns-dashboard/custom-slots-override.md +0 -71
  61. package/skills/wix-app/references/auto-patterns-dashboard/entity-page-actions.md +0 -70
  62. package/skills/wix-app/references/auto-patterns-dashboard/entity-page-view-actions.md +0 -73
  63. package/skills/wix-app/references/auto-patterns-dashboard/entity-page.md +0 -98
  64. package/skills/wix-app/references/auto-patterns-dashboard/example-patterns.json +0 -278
  65. package/skills/wix-app/references/auto-patterns-dashboard/pages-configuration.md +0 -94
  66. package/skills/wix-app/references/auto-patterns-dashboard/resolved-action.md +0 -43
  67. package/skills/wix-app/references/auto-patterns-dashboard/sdk-utilities.md +0 -66
  68. package/skills/wix-app/references/auto-patterns-dashboard/views.md +0 -76
  69. package/skills/wix-app/scripts/generate-auto-patterns.js +0 -510
  70. package/skills/wix-manage/references/domains/domain-search-and-purchase.md +0 -437
  71. package/skills/wix-manage/references/stores/create-product-from-image.md +0 -1031
  72. package/skills/wix-manage/references/stores/create-product-with-options-catalog-v3.md +0 -623
  73. package/skills/wix-manage/references/stores/setup-online-store-catalog-v3.md +0 -106
@@ -1,72 +1,29 @@
1
- // Grid tile. Styled with base44 design tokens (shadcn Tailwind classes: bg-card / text-foreground /
2
- // border-border) — re-skin via the app's design tokens (src/index.css :root/.dark), not this JSX.
3
- // The price / stock / ribbon field paths are load-bearing; image URLs go through lib/storeImage so the
4
- // grid, the PDP gallery and the cart all normalise them the same way.
5
- //
6
- // Everything here comes from the list query — the tile costs no extra request. What the list query
7
- // does NOT carry is a stock COUNT (inventory has availabilityStatus only), so "Limited stock" is as
8
- // precise as a tile can honestly get; "Only 2 left" would need per-variant inventory per product.
1
+ // Reference implementation of a product grid tile built on useProductCard.
2
+ // Styled with base44 design tokens (shadcn Tailwind classes: bg-card / text-foreground / border-border).
3
+ // Build your own card on useProductCard instead — read this for inspiration, don't use it directly.
9
4
  import { useState } from "react";
10
5
  import { Link } from "react-router-dom";
11
- import { productGallery, productImage } from "@/lib/storeImage";
12
6
  import { useCart } from "@/context/CartContext";
7
+ import { useProductCard } from "@/hooks/useProductCard";
13
8
 
14
- // Percent off, from the raw amounts — the formatted strings ("€129.00") can't be subtracted.
15
- function discountPercent(product) {
16
- const now = Number(product?.actualPriceRange?.minValue?.amount);
17
- const was = Number(product?.compareAtPriceRange?.minValue?.amount);
18
- if (!now || !was || was <= now) return null;
19
- return Math.round(((was - now) / was) * 100);
20
- }
21
-
22
- // What the buyer gets to choose, previewed on the tile: colour options become real dots (the colours
23
- // say more than "3 colors" could), everything else becomes "4 sizes". Pluralised from the option's own
24
- // name, so the wording follows the merchant's catalogue rather than a hardcoded English word.
25
- function optionsPreview(product) {
26
- const labels = [], colors = [];
27
- for (const o of product?.options || []) {
28
- const choices = (o.choicesSettings?.choices || []).filter((c) => c.visible !== false);
29
- if (!choices.length) continue;
30
- const swatches = choices.map((c) => c.colorCode).filter(Boolean);
31
- if (swatches.length) { colors.push(...swatches); continue; }
32
- const n = o.name.toLowerCase();
33
- labels.push(`${choices.length} ${choices.length === 1 || n.endsWith("s") ? n : `${n}s`}`);
34
- }
35
- return { label: labels.join(" · "), colors };
36
- }
37
-
38
- // One rule for every chip on the tile, so a new badge has an obvious home instead of a fresh colour:
39
- // promotional shouts in the brand colour, informational sits quietly on a card surface, and only a
40
- // blocking state is destructive. Each background travels with its own foreground token.
41
9
  const badge = "px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide rounded-full";
42
- const badgePromo = `${badge} bg-primary text-primary-foreground`;
43
- const badgeInfo = `${badge} bg-card text-foreground border border-border`;
44
- const badgeBlocked = `${badge} bg-destructive text-destructive-foreground`;
10
+ const badgePromo = `${badge} bg-primary text-primary-foreground`;
11
+ const badgeInfo = `${badge} bg-card text-foreground border border-border`;
12
+ const badgeBlocked = `${badge} bg-destructive text-destructive-foreground`;
13
+
14
+ const badgeClass = { "pre-order": badgeInfo, "sold-out": badgeBlocked, "limited-stock": badgeInfo };
45
15
 
46
16
  export default function ProductCard({ product }) {
47
17
  const { addToCart } = useCart();
48
18
  const [adding, setAdding] = useState(false);
49
19
 
50
- const image = productImage(product);
51
- const hoverImage = productGallery(product)[1]?.url;
52
- const price = product?.actualPriceRange?.minValue?.formattedAmount;
53
- const priceMax = product?.actualPriceRange?.maxValue?.formattedAmount;
54
- const compareAt = product?.compareAtPriceRange?.minValue?.formattedAmount;
55
-
56
- const status = product?.inventory?.availabilityStatus;
57
- const soldOut = status === "OUT_OF_STOCK";
58
- const preorder = product?.inventory?.preorderStatus === "ENABLED";
59
- const discount = discountPercent(product);
60
- // A merchant-set ribbon ("New", "Best Seller") is the catalogue's own badge, so it beats anything
61
- // derived here — a "new" label computed from createdDate would flag the whole catalogue at once
62
- // right after seeding. Suppressed when a discount badge is showing, since that ribbon is "Sale".
63
- const ribbon = product?.ribbon?.name;
64
- const { label: optionLabel, colors } = optionsPreview(product);
65
- const hasOptions = (product?.options?.length || 0) > 0;
20
+ const {
21
+ isSoldOut, isPreorder, leftBadges, promoBadge,
22
+ priceDisplay, compareAtDisplay,
23
+ colors, optionLabel,
24
+ isQuickAddable, image, hoverImage,
25
+ } = useProductCard(product);
66
26
 
67
- // No options means a single variant, which the cart resolves from the product id alone — so the
68
- // tile can add straight to the cart with no extra request. The drawer opening is the confirmation
69
- // (and carries the reason when it fails), which is why there's no "Added ✓" state to fake here.
70
27
  const quickAdd = async () => {
71
28
  setAdding(true);
72
29
  await addToCart(product.id);
@@ -79,11 +36,8 @@ export default function ProductCard({ product }) {
79
36
  <Link to={`/product/${product.slug}`} aria-label={product.name} className="block w-full h-full">
80
37
  {image ? (
81
38
  <>
82
- {/* Fade out only when there IS a second shot behind it — a one-photo product would
83
- otherwise hover to an empty frame. */}
84
39
  <img src={image} alt={product.name} loading="lazy"
85
40
  className={`w-full h-full object-cover ${hoverImage ? "transition-opacity duration-300 md:group-hover:opacity-0" : ""}`} />
86
- {/* Second catalogue shot on hover. `hidden` below md so phones never fetch it. */}
87
41
  {hoverImage && (
88
42
  <img src={hoverImage} alt="" aria-hidden="true" loading="lazy"
89
43
  className="hidden md:block absolute inset-0 w-full h-full object-cover opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
@@ -99,51 +53,40 @@ export default function ProductCard({ product }) {
99
53
  </Link>
100
54
 
101
55
  <div className="absolute top-2 left-2 flex flex-col items-start gap-1 pointer-events-none">
102
- {soldOut && preorder && <span className={badgeInfo}>Pre-order</span>}
103
- {soldOut && !preorder && <span className={badgeBlocked}>Sold out</span>}
104
- {status === "PARTIALLY_OUT_OF_STOCK" && <span className={badgeInfo}>Limited stock</span>}
56
+ {leftBadges.map((b) => (
57
+ <span key={b.type} className={badgeClass[b.type]}>{b.label}</span>
58
+ ))}
105
59
  </div>
106
- {/* Discount and ribbon are mutually exclusive, so both can claim the promotional slot. */}
107
60
  <div className="absolute top-2 right-2 pointer-events-none">
108
- {discount ? <span className={badgePromo}>−{discount}%</span>
109
- : ribbon ? <span className={badgePromo}>{ribbon}</span> : null}
61
+ {promoBadge && <span className={badgePromo}>{promoBadge.label}</span>}
110
62
  </div>
111
63
 
112
- {/* Sold out with no pre-order has nothing to offer, so no control appears at all. */}
113
- {(!soldOut || preorder) && (
64
+ {(!isSoldOut || isPreorder) && (
114
65
  <div className="absolute inset-x-2 bottom-2 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100">
115
- {/* Same tokens as the PDP's Add-to-cart button: one action, one treatment, so the tile's
116
- CTA is the brand colour rather than "whatever the text colour is". */}
117
- {hasOptions || soldOut ? (
118
- <Link to={`/product/${product.slug}`}
119
- className="block w-full text-center py-2.5 px-4 rounded-full bg-primary text-primary-foreground no-underline text-sm font-semibold">
120
- {soldOut ? "Pre-order" : "Choose options"}
121
- </Link>
122
- ) : (
66
+ {isQuickAddable ? (
123
67
  <button onClick={quickAdd} disabled={adding}
124
68
  className="w-full py-2.5 px-4 rounded-full bg-primary text-primary-foreground border-none text-sm font-semibold cursor-pointer transition-opacity hover:opacity-90 disabled:opacity-60 disabled:cursor-wait">
125
69
  {adding ? "Adding…" : "Quick add"}
126
70
  </button>
71
+ ) : (
72
+ <Link to={`/product/${product.slug}`}
73
+ className="block w-full text-center py-2.5 px-4 rounded-full bg-primary text-primary-foreground no-underline text-sm font-semibold">
74
+ {isSoldOut ? "Pre-order" : "Choose options"}
75
+ </Link>
127
76
  )}
128
77
  </div>
129
78
  )}
130
79
  </div>
131
80
 
132
81
  <div className="p-3 flex flex-col gap-1">
133
- {/* Side by side once the tile is wide enough; stacked on a phone's two-up grid, where a long
134
- name wrapping to three lines against a top-right price reads as broken. */}
135
82
  <div className="flex flex-col gap-1 md:flex-row md:items-baseline md:justify-between md:gap-3">
136
- {/* Clamped to two lines: a 220px tile turns a long product name into three or four ragged
137
- lines beside a top-right price. The full name is on the tile's link and the PDP. */}
138
83
  <h3 className="m-0 font-display text-[15px] font-semibold line-clamp-2">
139
84
  <Link to={`/product/${product.slug}`} className="no-underline text-foreground">{product.name}</Link>
140
85
  </h3>
141
86
  <div className="flex gap-2 items-baseline flex-wrap md:justify-end">
142
- {/* A product whose variants differ in price spans a range — showing only minValue reads as
143
- the price of everything, then the PDP appears to raise it once a variant is picked. */}
144
- <span className="font-semibold whitespace-nowrap">{priceMax && priceMax !== price ? `${price} – ${priceMax}` : price}</span>
145
- {compareAt && compareAt !== price && (
146
- <span className="text-muted-foreground line-through text-[13px] whitespace-nowrap">{compareAt}</span>
87
+ <span className="font-semibold whitespace-nowrap">{priceDisplay}</span>
88
+ {compareAtDisplay && (
89
+ <span className="text-muted-foreground line-through text-[13px] whitespace-nowrap">{compareAtDisplay}</span>
147
90
  )}
148
91
  </div>
149
92
  </div>
@@ -1,11 +1,6 @@
1
- // Responsive product grid, with the two states a fresh store spends most of its life in: loading and
2
- // empty. Styled with base44 design tokens (shadcn Tailwind classes).
3
- //
4
- // Two columns on phones — a 220px minmax leaves room for only one, and a one-up catalog scrolls
5
- // forever — then auto-fill from md up.
6
- //
7
- // The empty state carries weight here — a catalog is seeded separately from the build, so it is
8
- // routinely empty when the merchant first opens the page. Say why, rather than showing a bare line.
1
+ // Reference implementation of a product grid — 2-col mobile, auto-fill desktop, with a skeleton
2
+ // loading state and a descriptive empty state. Build your own layout instead (see INSTRUCTIONS.md
3
+ // STEP 3) — read this for the skeleton/empty-state patterns, don't use it directly.
9
4
  import ProductCard from "./ProductCard";
10
5
 
11
6
  // Skeleton tiles match ProductCard's aspect-square + two text lines, so nothing shifts when the real
@@ -1,118 +1,91 @@
1
- // PDP choice controls: one OptionSelector per product.options[] (variant choices) and one
2
- // ModifierSelector per product.modifiers[] (TEXT_CHOICES → buttons, FREE_TEXT → input). Driven by
3
- // useProductDetail — pass its selection state/handlers in. Styled with base44 design tokens (shadcn Tailwind classes).
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
- {options.map((o) => (
111
- <OptionSelector key={o.id} option={o} selected={selectedOptions[o.id]} onSelect={selectOption} />
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
- {modifiers.map((m) => (
114
- <ModifierSelector key={m.key || m.freeTextSettings?.key} modifier={m}
115
- value={modifierValues[m.key || m.freeTextSettings?.key]} onChange={setModifier} />
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"); // Base44 (generic: use $TOKEN)
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: {} };