@wix/agent-skills 1.20.0 → 1.20.1
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-headless-fast/SKILL.md +200 -34
- package/skills/wix-headless-fast/entry/skill.md +10 -2
- package/skills/wix-headless-fast/install/attach.mjs +253 -0
- package/skills/wix-headless-fast/install/deploy.mjs +109 -9
- package/skills/wix-headless-fast/install/fast-path.mjs +29 -34
- package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
- package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
- package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
- package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
- package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
- package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
- package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
- package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
- package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
- package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
- package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
- package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
- package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
- package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
- package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
- package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
- package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
- package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
- package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
- package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
- package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
- package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
- package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
- package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
- package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
- package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
- package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
- package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
- package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
- package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
- package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
- package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
- package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
- package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
- package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
- package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
- package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
- package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
- package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
- package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
- package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
- package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
- package/skills/wix-headless-fast/references/shared/app/wix/media.ts +45 -4
- package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
- package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
- package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
- package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +182 -63
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +29 -58
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -163
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -300
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +49 -162
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +47 -370
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
- package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
- package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -9
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +6 -35
- package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +0 -40
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
- package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// The purchase controls for one product — option groups (swatches for a color option, pills
|
|
2
|
+
// otherwise, out-of-stock choices disabled), choice modifiers, an optional quantity stepper, and
|
|
3
|
+
// the buy button gated by the hook: disabled with a plain reason ("Choose Size") until every
|
|
4
|
+
// choice is made, "Pre-order" when the resolved variant is pre-orderable, the hook's error inline.
|
|
5
|
+
// QuickAdd's picker and your product page mount the same component, so buying behaves the same
|
|
6
|
+
// in the gallery and on the PDP. Wire as-is; style via the tokens.
|
|
7
|
+
//
|
|
8
|
+
// const d = useProductDetail({ initial }); // the PDP
|
|
9
|
+
// <OptionPicker detail={d} showQuantity />
|
|
10
|
+
//
|
|
11
|
+
// Price, name, gallery, description, and layout stay yours — render d.price / d.compareAtPrice
|
|
12
|
+
// beside this (the range until every option is picked, then the variant's price).
|
|
13
|
+
import type { UseProductDetail } from "../../hooks/storefront/useProductDetail";
|
|
14
|
+
|
|
15
|
+
export default function OptionPicker({
|
|
16
|
+
detail: d,
|
|
17
|
+
showQuantity = false,
|
|
18
|
+
onAdded,
|
|
19
|
+
}: {
|
|
20
|
+
detail: UseProductDetail;
|
|
21
|
+
/** The PDP shows a quantity stepper; a tile picker adds one. */
|
|
22
|
+
showQuantity?: boolean;
|
|
23
|
+
/** Called after a successful add (a tile picker closes itself here). */
|
|
24
|
+
onAdded?: () => void;
|
|
25
|
+
}) {
|
|
26
|
+
if (!d.product) return null;
|
|
27
|
+
return (
|
|
28
|
+
<div className="flex flex-col gap-3">
|
|
29
|
+
{d.optionGroups.map((g) => (
|
|
30
|
+
<fieldset key={g.id}>
|
|
31
|
+
<legend className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">{g.name}</legend>
|
|
32
|
+
<div className="flex flex-wrap gap-1.5">
|
|
33
|
+
{g.choices.map((c) =>
|
|
34
|
+
g.isColor && c.colorCode ? (
|
|
35
|
+
<button key={c.choiceId} type="button" aria-label={c.name} title={c.name} aria-pressed={c.selected}
|
|
36
|
+
disabled={!c.inStock} onClick={() => d.selectOption(g.name, c.name)}
|
|
37
|
+
className={`h-8 w-8 rounded-full border-2 disabled:opacity-30 ${c.selected ? "border-foreground" : "border-border"}`}
|
|
38
|
+
style={{ backgroundColor: c.colorCode }} />
|
|
39
|
+
) : (
|
|
40
|
+
<button key={c.choiceId} type="button" aria-pressed={c.selected} disabled={!c.inStock}
|
|
41
|
+
onClick={() => d.selectOption(g.name, c.name)}
|
|
42
|
+
className={`rounded-full border px-3 py-1 text-sm disabled:line-through disabled:opacity-40 ${c.selected ? "border-foreground bg-foreground text-background" : "border-border"}`}>
|
|
43
|
+
{c.name}
|
|
44
|
+
</button>
|
|
45
|
+
),
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
</fieldset>
|
|
49
|
+
))}
|
|
50
|
+
{d.product.modifiers.filter((m) => m.type === "choices").map((m) => (
|
|
51
|
+
<fieldset key={m.key}>
|
|
52
|
+
<legend className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
53
|
+
{m.name}{m.mandatory ? " *" : ""}
|
|
54
|
+
</legend>
|
|
55
|
+
<div className="flex flex-wrap gap-1.5">
|
|
56
|
+
{m.choices.map((c) => (
|
|
57
|
+
<button key={c.key} type="button" aria-pressed={d.modifierValues[m.key] === c.key}
|
|
58
|
+
onClick={() => d.setModifier(m.key, c.key)}
|
|
59
|
+
className={`rounded-full border px-3 py-1 text-sm ${d.modifierValues[m.key] === c.key ? "border-foreground bg-foreground text-background" : "border-border"}`}>
|
|
60
|
+
{c.name}
|
|
61
|
+
</button>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</fieldset>
|
|
65
|
+
))}
|
|
66
|
+
{d.product.modifiers.filter((m) => m.type === "text").map((m) => (
|
|
67
|
+
<label key={m.key} className="flex flex-col gap-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
68
|
+
{m.name}{m.mandatory ? " *" : ""}
|
|
69
|
+
<input type="text" value={d.modifierValues[m.key] ?? ""} onChange={(e) => d.setModifier(m.key, e.target.value)}
|
|
70
|
+
className="rounded-md border border-border bg-background px-3 py-2 text-sm normal-case tracking-normal text-foreground" />
|
|
71
|
+
</label>
|
|
72
|
+
))}
|
|
73
|
+
<div className="flex items-stretch gap-2">
|
|
74
|
+
{showQuantity && (
|
|
75
|
+
<div className="flex items-center rounded-full border border-border">
|
|
76
|
+
<button type="button" aria-label="Decrease quantity" disabled={d.quantity <= 1} onClick={() => d.setQuantity(d.quantity - 1)}
|
|
77
|
+
className="px-3 py-2 text-sm disabled:opacity-40">−</button>
|
|
78
|
+
<span className="min-w-6 text-center text-sm tabular-nums">{d.quantity}</span>
|
|
79
|
+
<button type="button" aria-label="Increase quantity" onClick={() => d.setQuantity(d.quantity + 1)} className="px-3 py-2 text-sm">+</button>
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
disabled={!d.canAdd || d.adding}
|
|
85
|
+
onClick={() => d.add().then(() => onAdded?.()).catch(() => {})}
|
|
86
|
+
className="flex-1 rounded-full bg-primary py-2.5 text-sm font-semibold text-primary-foreground disabled:opacity-50"
|
|
87
|
+
>
|
|
88
|
+
{d.adding ? "Adding…" : d.isPreorder ? "Pre-order" : "Add to cart"}
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
{d.blockedReason && !d.canAdd && <p className="text-xs text-muted-foreground">{d.blockedReason}</p>}
|
|
92
|
+
{d.error && <p className="text-xs text-red-600">{d.error}</p>}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx
CHANGED
|
@@ -3,25 +3,45 @@
|
|
|
3
3
|
// • options / choice mods → Quick Add: a picker on the card (bottom sheet on small screens)
|
|
4
4
|
// • free-text modifier → the product page (the gallery can't collect the text)
|
|
5
5
|
// Mount as the LAST ROW of the tile's text block (under name and price), as a direct child of the
|
|
6
|
-
// tile root that carries `relative` — the picker anchors to that root and takes its
|
|
7
|
-
//
|
|
6
|
+
// tile root that carries `relative flex flex-col` — the picker anchors to that root and takes its
|
|
7
|
+
// width, and the control pins itself to the tile's bottom (mt-auto) so the action row lines up
|
|
8
|
+
// across a grid row whether or not a neighbour carries swatches or a struck price. Never overlay
|
|
9
|
+
// it on the image, never wrap it in a narrower positioned box.
|
|
8
10
|
// <QuickAdd product={p} /> Wire as-is; style via the tokens.
|
|
9
11
|
import { useEffect, useRef, useState } from "react";
|
|
10
12
|
import { useCart } from "../../hooks/storefront/useCart";
|
|
11
13
|
import { useProductDetail } from "../../hooks/storefront/useProductDetail";
|
|
12
14
|
import type { ProductSummary } from "../../wix/storefront/types";
|
|
15
|
+
import OptionPicker from "./OptionPicker";
|
|
13
16
|
|
|
14
17
|
export default function QuickAdd({ product }: { product: ProductSummary }) {
|
|
18
|
+
return (
|
|
19
|
+
// A tile is often one big <a>. A click inside the buy control must never become a navigation
|
|
20
|
+
// to the product page — this boundary cancels the link's default for everything below it
|
|
21
|
+
// (the button, the picker, the sheet). Still: keep the control OUTSIDE the tile's link.
|
|
22
|
+
<div
|
|
23
|
+
className="contents"
|
|
24
|
+
onClick={(e) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
e.stopPropagation();
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<QuickAddControl product={product} />
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function QuickAddControl({ product }: { product: ProductSummary }) {
|
|
15
35
|
const { addToCart, busy } = useCart();
|
|
16
36
|
const [open, setOpen] = useState(false);
|
|
17
37
|
const [error, setError] = useState<string | null>(null);
|
|
18
38
|
|
|
19
39
|
if (product.availability === "OUT_OF_STOCK" && !product.preorder) {
|
|
20
|
-
return <span className="text-sm text-muted-foreground">Out of stock</span>;
|
|
40
|
+
return <span className="mt-auto block pt-3 text-sm text-muted-foreground">Out of stock</span>;
|
|
21
41
|
}
|
|
22
42
|
if (product.quickAddable) {
|
|
23
43
|
return (
|
|
24
|
-
<div>
|
|
44
|
+
<div className="mt-auto pt-3">
|
|
25
45
|
<button
|
|
26
46
|
type="button"
|
|
27
47
|
disabled={busy}
|
|
@@ -39,7 +59,7 @@ export default function QuickAdd({ product }: { product: ProductSummary }) {
|
|
|
39
59
|
// Options to pick, or a single variant that isn't plainly in stock (pre-order, partial stock):
|
|
40
60
|
// the picker resolves it through useProductDetail, exactly as the PDP would.
|
|
41
61
|
return (
|
|
42
|
-
|
|
62
|
+
<div className="mt-auto pt-3">
|
|
43
63
|
<button
|
|
44
64
|
type="button"
|
|
45
65
|
onClick={() => setOpen(true)}
|
|
@@ -48,11 +68,12 @@ export default function QuickAdd({ product }: { product: ProductSummary }) {
|
|
|
48
68
|
{product.optionsSummary ? "Choose options" : product.preorder ? "Pre-order" : "Add to cart"}
|
|
49
69
|
</button>
|
|
50
70
|
{open && <QuickAddPicker product={product} onClose={() => setOpen(false)} />}
|
|
51
|
-
|
|
71
|
+
</div>
|
|
52
72
|
);
|
|
53
73
|
}
|
|
54
74
|
|
|
55
|
-
// The picker fetches the full product only when it opens — cards never carry PDP data.
|
|
75
|
+
// The picker fetches the full product only when it opens — cards never carry PDP data. Its controls
|
|
76
|
+
// are the shipped OptionPicker, the same component the PDP mounts.
|
|
56
77
|
function QuickAddPicker({ product, onClose }: { product: ProductSummary; onClose: () => void }) {
|
|
57
78
|
const d = useProductDetail({ slug: product.slug });
|
|
58
79
|
const panelRef = useRef<HTMLElement>(null);
|
|
@@ -108,57 +129,7 @@ function QuickAddPicker({ product, onClose }: { product: ProductSummary; onClose
|
|
|
108
129
|
</a>
|
|
109
130
|
)}
|
|
110
131
|
|
|
111
|
-
{d.product && !needsPdp &&
|
|
112
|
-
<div className="flex flex-col gap-3">
|
|
113
|
-
{d.optionGroups.map((g) => (
|
|
114
|
-
<fieldset key={g.id}>
|
|
115
|
-
<legend className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">{g.name}</legend>
|
|
116
|
-
<div className="flex flex-wrap gap-1.5">
|
|
117
|
-
{g.choices.map((c) =>
|
|
118
|
-
g.isColor && c.colorCode ? (
|
|
119
|
-
<button key={c.choiceId} type="button" aria-label={c.name} title={c.name} aria-pressed={c.selected}
|
|
120
|
-
disabled={!c.inStock} onClick={() => d.selectOption(g.name, c.name)}
|
|
121
|
-
className={`h-8 w-8 rounded-full border-2 disabled:opacity-30 ${c.selected ? "border-foreground" : "border-border"}`}
|
|
122
|
-
style={{ backgroundColor: c.colorCode }} />
|
|
123
|
-
) : (
|
|
124
|
-
<button key={c.choiceId} type="button" aria-pressed={c.selected} disabled={!c.inStock}
|
|
125
|
-
onClick={() => d.selectOption(g.name, c.name)}
|
|
126
|
-
className={`rounded-full border px-3 py-1 text-sm disabled:line-through disabled:opacity-40 ${c.selected ? "border-foreground bg-foreground text-background" : "border-border"}`}>
|
|
127
|
-
{c.name}
|
|
128
|
-
</button>
|
|
129
|
-
),
|
|
130
|
-
)}
|
|
131
|
-
</div>
|
|
132
|
-
</fieldset>
|
|
133
|
-
))}
|
|
134
|
-
{d.product.modifiers.filter((m) => m.type === "choices").map((m) => (
|
|
135
|
-
<fieldset key={m.key}>
|
|
136
|
-
<legend className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
137
|
-
{m.name}{m.mandatory ? " *" : ""}
|
|
138
|
-
</legend>
|
|
139
|
-
<div className="flex flex-wrap gap-1.5">
|
|
140
|
-
{m.choices.map((c) => (
|
|
141
|
-
<button key={c.key} type="button" aria-pressed={d.modifierValues[m.key] === c.key}
|
|
142
|
-
onClick={() => d.setModifier(m.key, c.key)}
|
|
143
|
-
className={`rounded-full border px-3 py-1 text-sm ${d.modifierValues[m.key] === c.key ? "border-foreground bg-foreground text-background" : "border-border"}`}>
|
|
144
|
-
{c.name}
|
|
145
|
-
</button>
|
|
146
|
-
))}
|
|
147
|
-
</div>
|
|
148
|
-
</fieldset>
|
|
149
|
-
))}
|
|
150
|
-
<button
|
|
151
|
-
type="button"
|
|
152
|
-
disabled={!d.canAdd || d.adding}
|
|
153
|
-
onClick={() => d.add().then(onClose).catch(() => {})}
|
|
154
|
-
className="rounded-full bg-primary py-2.5 text-sm font-semibold text-primary-foreground disabled:opacity-50"
|
|
155
|
-
>
|
|
156
|
-
{d.adding ? "Adding…" : d.isPreorder ? "Pre-order" : "Add to cart"}
|
|
157
|
-
</button>
|
|
158
|
-
{d.blockedReason && !d.canAdd && <p className="text-xs text-muted-foreground">{d.blockedReason}</p>}
|
|
159
|
-
{d.error && <p className="text-xs text-red-600">{d.error}</p>}
|
|
160
|
-
</div>
|
|
161
|
-
)}
|
|
132
|
+
{d.product && !needsPdp && <OptionPicker detail={d} onAdded={onClose} />}
|
|
162
133
|
</section>
|
|
163
134
|
</>
|
|
164
135
|
);
|
package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts
CHANGED
|
@@ -1,173 +1,39 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// React binding of the product-detail store (wix/storefront/product-detail-store.ts) — option
|
|
2
|
+
// selection → variant resolution → add-to-cart lives there, framework-free. Always drive a PDP or
|
|
3
|
+
// quick-add picker through this hook (or the store): resolving variants by hand is where
|
|
4
|
+
// "added variants[0] regardless of the buyer's choice" comes from.
|
|
4
5
|
//
|
|
5
|
-
// SSR-friendly: pass the server-fetched ProductDetail as `initial` (Astro/Next); in a SPA
|
|
6
|
-
//
|
|
7
|
-
import { useEffect,
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
// SSR-friendly: pass the server-fetched ProductDetail as `initial` (Astro/Next); in a SPA pass
|
|
7
|
+
// the slug and it fetches on mount.
|
|
8
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
9
|
+
import {
|
|
10
|
+
createProductDetailStore,
|
|
11
|
+
type ProductDetailState,
|
|
12
|
+
type ProductDetailStore,
|
|
13
|
+
type ProductDetailStoreOptions,
|
|
14
|
+
} from "../../wix/storefront/product-detail-store";
|
|
11
15
|
|
|
12
|
-
export
|
|
13
|
-
initial?: ProductDetail | null;
|
|
14
|
-
slug?: string;
|
|
15
|
-
}
|
|
16
|
+
export { type OptionGroupView } from "../../wix/storefront/product-detail-store";
|
|
16
17
|
|
|
17
|
-
export
|
|
18
|
-
choices: (ProductOption["choices"][number] & { selected: boolean })[];
|
|
19
|
-
}
|
|
18
|
+
export type UseProductDetailOptions = ProductDetailStoreOptions;
|
|
20
19
|
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
product: ProductDetail | null;
|
|
24
|
-
notFound: boolean;
|
|
25
|
-
/** Option groups with per-choice `selected`, ready to render as pills/swatches. */
|
|
26
|
-
optionGroups: OptionGroupView[];
|
|
27
|
-
selectOption: (optionName: string, choiceName: string) => void;
|
|
28
|
-
/** Modifier inputs keyed by modifier key. */
|
|
29
|
-
modifierValues: Record<string, string>;
|
|
30
|
-
setModifier: (key: string, value: string) => void;
|
|
31
|
-
/** The resolved variant; null while the selection is incomplete. */
|
|
32
|
-
variant: ProductVariant | null;
|
|
33
|
-
/**
|
|
34
|
-
* Price to display right now: the product RANGE ("€24.99 – €34.99", one value when equal) until
|
|
35
|
-
* every option is picked, then the selected variant's price (an automatic discount applied).
|
|
36
|
-
*/
|
|
37
|
-
price: string;
|
|
38
|
-
/** Struck "was" price — only once a variant is resolved (or for a single-price product), never beside a range. */
|
|
39
|
-
compareAtPrice: string | null;
|
|
40
|
-
/** The resolved variant is out of stock but pre-orderable — label the action "Pre-order". */
|
|
41
|
-
isPreorder: boolean;
|
|
42
|
-
/** False until every option is selected and the resolved variant is in stock or pre-orderable. */
|
|
43
|
-
canAdd: boolean;
|
|
44
|
-
/**
|
|
45
|
-
* Why the action is disabled — neutral guidance to render beside it ("Choose Size"), not an
|
|
46
|
-
* error; null when the buyer can add. Promote to error styling only after they try to buy.
|
|
47
|
-
*/
|
|
48
|
-
blockedReason: string | null;
|
|
49
|
-
quantity: number;
|
|
50
|
-
setQuantity: (n: number) => void;
|
|
51
|
-
/** Adds the resolved variant to the cart (opens the drawer). Throws on refusal. */
|
|
52
|
-
add: () => Promise<void>;
|
|
53
|
-
adding: boolean;
|
|
54
|
-
error: string | null;
|
|
55
|
-
}
|
|
20
|
+
export type UseProductDetail = ProductDetailState &
|
|
21
|
+
Pick<ProductDetailStore, "selectOption" | "setModifier" | "setQuantity" | "add">;
|
|
56
22
|
|
|
57
23
|
export function useProductDetail({ initial, slug }: UseProductDetailOptions): UseProductDetail {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const [modifierValues, setModifierValues] = useState<Record<string, string>>({});
|
|
62
|
-
const [quantity, setQuantity] = useState(1);
|
|
63
|
-
const [adding, setAdding] = useState(false);
|
|
64
|
-
const [error, setError] = useState<string | null>(null);
|
|
65
|
-
const { addToCart } = useCart();
|
|
66
|
-
|
|
24
|
+
const ref = useRef<{ slug?: string; store: ProductDetailStore } | null>(null);
|
|
25
|
+
if (!ref.current || ref.current.slug !== slug) ref.current = { slug, store: createProductDetailStore({ initial, slug }) };
|
|
26
|
+
const store = ref.current.store;
|
|
67
27
|
useEffect(() => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (!alive) return;
|
|
73
|
-
setProduct(p);
|
|
74
|
-
setNotFound(p === null);
|
|
75
|
-
})
|
|
76
|
-
.catch(() => alive && setNotFound(true));
|
|
77
|
-
return () => {
|
|
78
|
-
alive = false;
|
|
79
|
-
};
|
|
80
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
81
|
-
}, [slug]);
|
|
82
|
-
|
|
83
|
-
const variant = useMemo(
|
|
84
|
-
() => (product ? resolveVariant(product, selections) : null),
|
|
85
|
-
[product, selections],
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
const optionGroups: OptionGroupView[] = useMemo(
|
|
89
|
-
() =>
|
|
90
|
-
(product?.options ?? []).map((o) => ({
|
|
91
|
-
...o,
|
|
92
|
-
choices: o.choices.map((c) => ({ ...c, selected: selections[o.name] === c.name })),
|
|
93
|
-
})),
|
|
94
|
-
[product, selections],
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
const missingModifier = (product?.modifiers ?? []).find(
|
|
98
|
-
(m) => m.mandatory && (modifierValues[m.key] ?? "").length === 0,
|
|
99
|
-
);
|
|
100
|
-
const missingOptions = (product?.options ?? []).filter((o) => !selections[o.name]).map((o) => o.name);
|
|
101
|
-
const selectionComplete = missingOptions.length === 0;
|
|
102
|
-
|
|
103
|
-
// In stock OR pre-orderable counts as buyable; a preorder add carries preOrderRequested.
|
|
104
|
-
const isPreorder = !!variant && !variant.inStock && variant.preorderEnabled;
|
|
105
|
-
const available = !!variant && (variant.inStock || variant.preorderEnabled);
|
|
106
|
-
|
|
107
|
-
const canAdd = !!product && available && !missingModifier;
|
|
108
|
-
|
|
109
|
-
const blockedReason: string | null = !product
|
|
110
|
-
? null
|
|
111
|
-
: !selectionComplete
|
|
112
|
-
? `Choose ${missingOptions.join(" and ")}`
|
|
113
|
-
: !variant
|
|
114
|
-
? "This combination isn't available"
|
|
115
|
-
: !available
|
|
116
|
-
? "Out of stock"
|
|
117
|
-
: missingModifier
|
|
118
|
-
? `Add ${missingModifier.name}`
|
|
119
|
-
: null;
|
|
120
|
-
|
|
121
|
-
// Before every option is picked, the range — never an empty price, never a lone struck minimum.
|
|
122
|
-
const isRange = !!product && product.price !== product.maxPrice && !!product.maxPrice;
|
|
123
|
-
const rangeDisplay = product ? (isRange ? `${product.price} – ${product.maxPrice}` : product.price) : "";
|
|
124
|
-
|
|
125
|
-
async function add(): Promise<void> {
|
|
126
|
-
if (!product || !variant) return;
|
|
127
|
-
setAdding(true);
|
|
128
|
-
setError(null);
|
|
129
|
-
try {
|
|
130
|
-
const choiceModifiers: Record<string, string> = {};
|
|
131
|
-
const textModifiers: Record<string, string> = {};
|
|
132
|
-
for (const m of product.modifiers) {
|
|
133
|
-
const value = modifierValues[m.key];
|
|
134
|
-
if (!value) continue;
|
|
135
|
-
if (m.type === "text") textModifiers[m.key] = value;
|
|
136
|
-
else choiceModifiers[m.key] = value;
|
|
137
|
-
}
|
|
138
|
-
await addToCart(product.id, variant.variantId, quantity, {
|
|
139
|
-
modifierChoices: choiceModifiers,
|
|
140
|
-
customTextFields: textModifiers,
|
|
141
|
-
preorder: isPreorder,
|
|
142
|
-
});
|
|
143
|
-
} catch (e) {
|
|
144
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
145
|
-
throw e;
|
|
146
|
-
} finally {
|
|
147
|
-
setAdding(false);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
28
|
+
store.start();
|
|
29
|
+
return () => store.stop();
|
|
30
|
+
}, [store]);
|
|
31
|
+
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
151
32
|
return {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
setQuantity(1); // the ceiling belongs to the newly resolved variant
|
|
158
|
-
},
|
|
159
|
-
modifierValues,
|
|
160
|
-
setModifier: (key, value) => setModifierValues((v) => ({ ...v, [key]: value })),
|
|
161
|
-
variant,
|
|
162
|
-
price: variant?.price || rangeDisplay,
|
|
163
|
-
compareAtPrice: variant ? variant.compareAtPrice : isRange ? null : (product?.compareAtPrice ?? null),
|
|
164
|
-
isPreorder,
|
|
165
|
-
canAdd,
|
|
166
|
-
blockedReason,
|
|
167
|
-
quantity,
|
|
168
|
-
setQuantity,
|
|
169
|
-
add,
|
|
170
|
-
adding,
|
|
171
|
-
error,
|
|
33
|
+
...state,
|
|
34
|
+
selectOption: store.selectOption,
|
|
35
|
+
setModifier: store.setModifier,
|
|
36
|
+
setQuantity: store.setQuantity,
|
|
37
|
+
add: store.add,
|
|
172
38
|
};
|
|
173
39
|
}
|