@wix/agent-skills 1.19.2 → 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-app/references/SERVICE_PLUGIN.md +14 -1
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +7 -6
- package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +4 -0
- package/skills/wix-app/references/service-plugin/AVAILABILITY-TIME-SLOTS-CONFIGURATION.md +118 -0
- package/skills/wix-app/references/service-plugin/BOOKING-POLICY.md +65 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-PRICING.md +71 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +11 -3
- package/skills/wix-app/references/service-plugin/BOOKINGS-VALIDATION.md +137 -0
- package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +23 -16
- package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +4 -0
- package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +4 -2
- package/skills/wix-app/references/service-plugin/REALTIME-PERMISSIONS.md +45 -0
- package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +15 -0
- package/skills/wix-app/references/service-plugin/TOOLS_PROVIDER.md +4 -0
- package/skills/wix-app/references/service-plugin/VALIDATIONS.md +4 -0
- package/skills/wix-headless/references/CAPABILITIES.md +2 -2
- package/skills/wix-headless/references/inline-recipes/experience-store.md +4 -3
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +98 -18
- package/skills/wix-headless/references/managed/DEPLOYMENT.md +8 -8
- 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 +28 -31
- 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 +55 -1
- 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 +371 -81
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx +37 -3
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/FilterPanel.tsx +266 -0
- 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 +136 -0
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -130
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -182
- 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 +53 -136
- 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 +73 -230
- 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/app/wix/storefront/types.ts +62 -4
- package/skills/wix-headless-fast/references/storefront/app-astro/layouts/SiteLayout.astro +21 -2
- 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 -5
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +24 -0
- package/skills/wix-manage/SKILL.md +4 -1
- package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
- package/skills/wix-manage/references/events/create-wix-event.md +46 -0
- package/skills/wix-manage/references/events/manage-wix-events.md +51 -1
- package/skills/wix-manage/references/google-ads/manage-campaign-lifecycle.md +6 -6
- package/skills/wix-manage/references/seo/manage-seo-suggestions.md +351 -0
- package/skills/wix-vibe-headless/install/deploy.cjs +33 -2
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +13 -4
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44.md +19 -4
- package/skills/wix-vibe-headless/platforms/generic.md +6 -0
- package/skills/wix-vibe-headless/references/_shared/nav/nav.tanstack.js +30 -0
- package/skills/wix-vibe-headless/references/_shared/routing.md +86 -0
- package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +44 -0
- package/skills/wix-vibe-headless/references/blog/app/components/PostCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/components/PostChips.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/CategoryPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/PostDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/TagPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/bookings/app/components/ServiceCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/bookings/app/pages/ServiceDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/events/app/components/EventCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/events/app/pages/EventDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/members/app/components/MemberMenu.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/app/components/RequireAuth.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/app/pages/Login.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/pages/CollectionPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/pages/ProjectDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/app/pages/PlanDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +36 -0
- package/skills/wix-vibe-headless/references/shared/app/lib/nav.js +9 -0
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +42 -41
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +7 -1
- 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/hooks/storefront/useProductDetail.ts
CHANGED
|
@@ -1,140 +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
|
-
/** Price to display right now (variant price once resolved, else the product range). */
|
|
34
|
-
price: string;
|
|
35
|
-
compareAtPrice: string | null;
|
|
36
|
-
/** False until every option is selected (and the resolved variant is in stock). */
|
|
37
|
-
canAdd: boolean;
|
|
38
|
-
quantity: number;
|
|
39
|
-
setQuantity: (n: number) => void;
|
|
40
|
-
/** Adds the resolved variant to the cart (opens the drawer). Throws on refusal. */
|
|
41
|
-
add: () => Promise<void>;
|
|
42
|
-
adding: boolean;
|
|
43
|
-
error: string | null;
|
|
44
|
-
}
|
|
20
|
+
export type UseProductDetail = ProductDetailState &
|
|
21
|
+
Pick<ProductDetailStore, "selectOption" | "setModifier" | "setQuantity" | "add">;
|
|
45
22
|
|
|
46
23
|
export function useProductDetail({ initial, slug }: UseProductDetailOptions): UseProductDetail {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const [modifierValues, setModifierValues] = useState<Record<string, string>>({});
|
|
51
|
-
const [quantity, setQuantity] = useState(1);
|
|
52
|
-
const [adding, setAdding] = useState(false);
|
|
53
|
-
const [error, setError] = useState<string | null>(null);
|
|
54
|
-
const { addToCart } = useCart();
|
|
55
|
-
|
|
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;
|
|
56
27
|
useEffect(() => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (!alive) return;
|
|
62
|
-
setProduct(p);
|
|
63
|
-
setNotFound(p === null);
|
|
64
|
-
})
|
|
65
|
-
.catch(() => alive && setNotFound(true));
|
|
66
|
-
return () => {
|
|
67
|
-
alive = false;
|
|
68
|
-
};
|
|
69
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
70
|
-
}, [slug]);
|
|
71
|
-
|
|
72
|
-
const variant = useMemo(
|
|
73
|
-
() => (product ? resolveVariant(product, selections) : null),
|
|
74
|
-
[product, selections],
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const optionGroups: OptionGroupView[] = useMemo(
|
|
78
|
-
() =>
|
|
79
|
-
(product?.options ?? []).map((o) => ({
|
|
80
|
-
...o,
|
|
81
|
-
choices: o.choices.map((c) => ({ ...c, selected: selections[o.name] === c.name })),
|
|
82
|
-
})),
|
|
83
|
-
[product, selections],
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const mandatoryModifiersFilled = (product?.modifiers ?? [])
|
|
87
|
-
.filter((m) => m.mandatory)
|
|
88
|
-
.every((m) => (modifierValues[m.key] ?? "").length > 0);
|
|
89
|
-
|
|
90
|
-
const canAdd =
|
|
91
|
-
!!product &&
|
|
92
|
-
product.availability !== "OUT_OF_STOCK" &&
|
|
93
|
-
variant !== null &&
|
|
94
|
-
variant.inStock &&
|
|
95
|
-
mandatoryModifiersFilled;
|
|
96
|
-
|
|
97
|
-
async function add(): Promise<void> {
|
|
98
|
-
if (!product || !variant) return;
|
|
99
|
-
setAdding(true);
|
|
100
|
-
setError(null);
|
|
101
|
-
try {
|
|
102
|
-
const choiceModifiers: Record<string, string> = {};
|
|
103
|
-
const textModifiers: Record<string, string> = {};
|
|
104
|
-
for (const m of product.modifiers) {
|
|
105
|
-
const value = modifierValues[m.key];
|
|
106
|
-
if (!value) continue;
|
|
107
|
-
if (m.type === "text") textModifiers[m.key] = value;
|
|
108
|
-
else choiceModifiers[m.key] = value;
|
|
109
|
-
}
|
|
110
|
-
await addToCart(product.id, variant.variantId, quantity, {
|
|
111
|
-
modifierChoices: choiceModifiers,
|
|
112
|
-
customTextFields: textModifiers,
|
|
113
|
-
});
|
|
114
|
-
} catch (e) {
|
|
115
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
116
|
-
throw e;
|
|
117
|
-
} finally {
|
|
118
|
-
setAdding(false);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
28
|
+
store.start();
|
|
29
|
+
return () => store.stop();
|
|
30
|
+
}, [store]);
|
|
31
|
+
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
122
32
|
return {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
modifierValues,
|
|
129
|
-
setModifier: (key, value) => setModifierValues((v) => ({ ...v, [key]: value })),
|
|
130
|
-
variant,
|
|
131
|
-
price: variant?.price || product?.price || "",
|
|
132
|
-
compareAtPrice: variant ? variant.compareAtPrice : (product?.compareAtPrice ?? null),
|
|
133
|
-
canAdd,
|
|
134
|
-
quantity,
|
|
135
|
-
setQuantity,
|
|
136
|
-
add,
|
|
137
|
-
adding,
|
|
138
|
-
error,
|
|
33
|
+
...state,
|
|
34
|
+
selectOption: store.selectOption,
|
|
35
|
+
setModifier: store.setModifier,
|
|
36
|
+
setQuantity: store.setQuantity,
|
|
37
|
+
add: store.add,
|
|
139
38
|
};
|
|
140
39
|
}
|
|
@@ -1,190 +1,34 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// chain, replacing the seed without a skeleton flash.
|
|
8
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
|
-
import {
|
|
10
|
-
CATALOG_SORTS,
|
|
11
|
-
fetchCategories,
|
|
12
|
-
searchCatalog,
|
|
13
|
-
type CatalogSort,
|
|
14
|
-
} from "../../wix/storefront/catalog";
|
|
15
|
-
import type { Category, ProductSummary } from "../../wix/storefront/types";
|
|
1
|
+
// React binding of the shop store (wix/storefront/shop-store.ts) — the listing state machine
|
|
2
|
+
// lives there, framework-free; this hook subscribes to one instance per mounted listing and
|
|
3
|
+
// exposes its state and actions under one name. Astro islands and React SPAs use this; a static
|
|
4
|
+
// page, Vue, or Svelte uses the store directly.
|
|
5
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
6
|
+
import { createShopStore, type ShopState, type ShopStore, type ShopStoreOptions } from "../../wix/storefront/shop-store";
|
|
16
7
|
|
|
17
|
-
export
|
|
8
|
+
export { SORTS, type ShopFilters } from "../../wix/storefront/shop-store";
|
|
18
9
|
|
|
19
|
-
export
|
|
20
|
-
minPrice?: number | string;
|
|
21
|
-
maxPrice?: number | string;
|
|
22
|
-
inStockOnly?: boolean;
|
|
23
|
-
/** Name search, max 100 chars. */
|
|
24
|
-
search?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface UseShopOptions {
|
|
28
|
-
initialProducts?: ProductSummary[];
|
|
29
|
-
initialCategories?: Category[];
|
|
30
|
-
pageSize?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface UseShop {
|
|
34
|
-
/** null while the first load is in flight — render skeletons, not an empty state. */
|
|
35
|
-
products: ProductSummary[] | null;
|
|
36
|
-
categories: Category[];
|
|
37
|
-
/** null = "all products". */
|
|
38
|
-
activeCategoryId: string | null;
|
|
39
|
-
setActiveCategoryId: (id: string | null) => void;
|
|
40
|
-
sort: CatalogSort;
|
|
41
|
-
setSort: (sort: CatalogSort) => void;
|
|
42
|
-
filters: ShopFilters;
|
|
43
|
-
setFilters: (filters: ShopFilters) => void;
|
|
44
|
-
loading: boolean;
|
|
45
|
-
error: string | null;
|
|
46
|
-
retry: () => void;
|
|
47
|
-
hasMore: boolean;
|
|
48
|
-
loadMore: () => Promise<void>;
|
|
49
|
-
loadingMore: boolean;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface PageState {
|
|
53
|
-
key: string | null;
|
|
54
|
-
products: ProductSummary[] | null;
|
|
55
|
-
cursor: string | null;
|
|
56
|
-
error: string | null;
|
|
57
|
-
loadingMore: boolean;
|
|
58
|
-
}
|
|
10
|
+
export type UseShopOptions = ShopStoreOptions;
|
|
59
11
|
|
|
60
|
-
export
|
|
61
|
-
|
|
62
|
-
initialCategories,
|
|
63
|
-
pageSize = 24,
|
|
64
|
-
}: UseShopOptions = {}): UseShop {
|
|
65
|
-
const [categories, setCategories] = useState<Category[]>(initialCategories ?? []);
|
|
66
|
-
const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null);
|
|
67
|
-
const [sort, setSort] = useState<CatalogSort>("featured");
|
|
68
|
-
const [filters, setFilters] = useState<ShopFilters>({});
|
|
69
|
-
const [attempt, setAttempt] = useState(0);
|
|
70
|
-
const [page, setPage] = useState<PageState>({
|
|
71
|
-
key: null,
|
|
72
|
-
products: initialProducts ?? null,
|
|
73
|
-
cursor: null,
|
|
74
|
-
error: null,
|
|
75
|
-
loadingMore: false,
|
|
76
|
-
});
|
|
77
|
-
const generation = useRef(0);
|
|
78
|
-
const pendingMore = useRef<object | null>(null);
|
|
79
|
-
|
|
80
|
-
const key = JSON.stringify([
|
|
81
|
-
pageSize,
|
|
82
|
-
activeCategoryId,
|
|
83
|
-
sort,
|
|
84
|
-
filters.minPrice ?? null,
|
|
85
|
-
filters.maxPrice ?? null,
|
|
86
|
-
!!filters.inStockOnly,
|
|
87
|
-
filters.search ?? "",
|
|
88
|
-
attempt,
|
|
89
|
-
]);
|
|
90
|
-
const currentKey = useRef(key);
|
|
91
|
-
currentKey.current = key;
|
|
92
|
-
|
|
93
|
-
useEffect(() => {
|
|
94
|
-
if (initialCategories) return;
|
|
95
|
-
let alive = true;
|
|
96
|
-
fetchCategories().then((list) => alive && setCategories(list)).catch(() => alive && setCategories([]));
|
|
97
|
-
return () => {
|
|
98
|
-
alive = false;
|
|
99
|
-
};
|
|
100
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
-
}, []);
|
|
12
|
+
export type UseShop = ShopState &
|
|
13
|
+
Pick<ShopStore, "setActiveCategoryId" | "setSort" | "setFilters" | "toggleChoice" | "clearFilters" | "retry" | "loadMore">;
|
|
102
14
|
|
|
15
|
+
export function useShop(options: UseShopOptions = {}): UseShop {
|
|
16
|
+
const ref = useRef<ShopStore | null>(null);
|
|
17
|
+
if (!ref.current) ref.current = createShopStore(options);
|
|
18
|
+
const store = ref.current;
|
|
103
19
|
useEffect(() => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// only a selection change with no seed shows skeletons.
|
|
109
|
-
setPage((p) => ({ key, products: p.key === null ? p.products : null, cursor: null, error: null, loadingMore: false }));
|
|
110
|
-
const [limit, categoryId, selectedSort, minPrice, maxPrice, inStockOnly, search] = JSON.parse(key);
|
|
111
|
-
searchCatalog({ limit, categoryId, sort: selectedSort, minPrice, maxPrice, inStockOnly, search })
|
|
112
|
-
.then((res) => {
|
|
113
|
-
if (alive && generation.current === id && currentKey.current === key) {
|
|
114
|
-
setPage({ key, products: res.products, cursor: res.nextCursor, error: null, loadingMore: false });
|
|
115
|
-
}
|
|
116
|
-
})
|
|
117
|
-
.catch((e) => {
|
|
118
|
-
if (alive && generation.current === id && currentKey.current === key) {
|
|
119
|
-
setPage({
|
|
120
|
-
key,
|
|
121
|
-
products: [],
|
|
122
|
-
cursor: null,
|
|
123
|
-
error: e instanceof Error ? e.message : "Couldn't load products.",
|
|
124
|
-
loadingMore: false,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
return () => {
|
|
129
|
-
alive = false;
|
|
130
|
-
generation.current++;
|
|
131
|
-
};
|
|
132
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
133
|
-
}, [key]);
|
|
134
|
-
|
|
135
|
-
const loadMore = useCallback(async () => {
|
|
136
|
-
if (page.key !== key || !page.cursor || pendingMore.current) return;
|
|
137
|
-
const request = { generation: generation.current, key };
|
|
138
|
-
pendingMore.current = request; // blocks repeated clicks before React rerenders
|
|
139
|
-
const isCurrent = () => generation.current === request.generation && currentKey.current === key;
|
|
140
|
-
setPage((p) => ({ ...p, loadingMore: true, error: null }));
|
|
141
|
-
try {
|
|
142
|
-
const res = await searchCatalog({ limit: pageSize, cursor: page.cursor });
|
|
143
|
-
if (isCurrent())
|
|
144
|
-
setPage((p) => {
|
|
145
|
-
const seen = new Set((p.products ?? []).map((product) => product.id));
|
|
146
|
-
return {
|
|
147
|
-
...p,
|
|
148
|
-
products: [
|
|
149
|
-
...(p.products ?? []),
|
|
150
|
-
...res.products.filter((product) => {
|
|
151
|
-
if (seen.has(product.id)) return false;
|
|
152
|
-
seen.add(product.id);
|
|
153
|
-
return true;
|
|
154
|
-
}),
|
|
155
|
-
],
|
|
156
|
-
cursor: res.nextCursor,
|
|
157
|
-
};
|
|
158
|
-
});
|
|
159
|
-
} catch (e) {
|
|
160
|
-
if (isCurrent())
|
|
161
|
-
setPage((p) => ({ ...p, error: e instanceof Error ? e.message : "Couldn't load more products." }));
|
|
162
|
-
} finally {
|
|
163
|
-
if (pendingMore.current === request) pendingMore.current = null;
|
|
164
|
-
if (isCurrent()) setPage((p) => ({ ...p, loadingMore: false }));
|
|
165
|
-
}
|
|
166
|
-
}, [key, page.key, page.cursor, pageSize]);
|
|
167
|
-
|
|
168
|
-
const retry = useCallback(() => setAttempt((n) => n + 1), []);
|
|
169
|
-
|
|
170
|
-
const current = page.key === key;
|
|
171
|
-
const seeded = page.key === null && page.products !== null; // SSR seed, first query in flight
|
|
172
|
-
// Seeded content on screen is not "loading" — skeletons are only for a genuinely empty wait.
|
|
173
|
-
const loading = (!current && !seeded) || (current && page.products === null);
|
|
20
|
+
store.start();
|
|
21
|
+
return () => store.stop();
|
|
22
|
+
}, [store]);
|
|
23
|
+
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
174
24
|
return {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
loading,
|
|
184
|
-
error: current ? page.error : null,
|
|
185
|
-
retry,
|
|
186
|
-
hasMore: current && !!page.cursor,
|
|
187
|
-
loadMore,
|
|
188
|
-
loadingMore: current && page.loadingMore,
|
|
25
|
+
...state,
|
|
26
|
+
setActiveCategoryId: store.setActiveCategoryId,
|
|
27
|
+
setSort: store.setSort,
|
|
28
|
+
setFilters: store.setFilters,
|
|
29
|
+
toggleChoice: store.toggleChoice,
|
|
30
|
+
clearFilters: store.clearFilters,
|
|
31
|
+
retry: store.retry,
|
|
32
|
+
loadMore: store.loadMore,
|
|
189
33
|
};
|
|
190
34
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Cart rules and DTO mapping — transport-agnostic, imported by both ./cart.ts (SDK) and the REST
|
|
2
|
+
// twin in references/storefront/rest/cart.ts (fetch). Raw Cart V2 entities may carry `_id` (SDK)
|
|
3
|
+
// or `id` (REST). Has its own formatMoney (a copy of ../money.ts) so it stands alone when stripped.
|
|
4
|
+
import type { Cart, CartLine } from "./types";
|
|
5
|
+
import type { ImgSrc, Raw } from "./catalog-core";
|
|
6
|
+
|
|
7
|
+
/** Public app id of the Wix Stores catalog — required inside every catalogReference. */
|
|
8
|
+
export const WIX_STORES_APP_ID = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
|
|
9
|
+
|
|
10
|
+
export const rawId = (raw: Raw | undefined | null): string => raw?._id ?? raw?.id ?? "";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Cart V2 money is ConvertedMoney { amount, convertedAmount } with NO formatted string, and the
|
|
14
|
+
* currency lives on the cart, not on the money. Format with the buyer's display currency when
|
|
15
|
+
* present, else the site's. Never hardcode "$" or assume USD.
|
|
16
|
+
*/
|
|
17
|
+
export function formatMoney(money: Raw | null | undefined, currencyCode: string | null | undefined): string {
|
|
18
|
+
const value = money?.convertedAmount ?? money?.amount;
|
|
19
|
+
if (value == null || value === "") return "";
|
|
20
|
+
const currency = currencyCode || "USD";
|
|
21
|
+
try {
|
|
22
|
+
return new Intl.NumberFormat(undefined, { style: "currency", currency }).format(Number(value));
|
|
23
|
+
} catch {
|
|
24
|
+
return `${value} ${currency}`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const cartCurrency = (raw: Raw | null | undefined): string =>
|
|
29
|
+
raw?.customerInfo?.currencyCode ?? raw?.businessInfo?.currencyCode ?? "";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* "Monthly plan · every 2 months · 6 payments" from the line's subscriptionInfo (Cart V2 carries
|
|
33
|
+
* the plan on the line); "" for a one-time purchase. Every word comes from the plan — a
|
|
34
|
+
* subscription line must read as one in the cart, never a generic "recurring".
|
|
35
|
+
*/
|
|
36
|
+
export function subscriptionTerms(info: Raw | undefined): string {
|
|
37
|
+
if (!info) return "";
|
|
38
|
+
const s: Raw = info.subscriptionSettings ?? {};
|
|
39
|
+
const unit: Record<string, string> = { DAY: "day", WEEK: "week", MONTH: "month", YEAR: "year" };
|
|
40
|
+
const parts: string[] = [];
|
|
41
|
+
const title = info.title?.original ?? info.title?.translated ?? "";
|
|
42
|
+
if (title) parts.push(title);
|
|
43
|
+
const u = unit[String(s.frequency ?? "")];
|
|
44
|
+
if (u) parts.push(`every ${s.interval && s.interval > 1 ? `${s.interval} ${u}s` : u}`);
|
|
45
|
+
if (!s.autoRenewal && s.billingCycles) parts.push(`${s.billingCycles} payments`);
|
|
46
|
+
return parts.join(" · ");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function toLine(raw: Raw, currency: string, imgSrc: ImgSrc): CartLine {
|
|
50
|
+
return {
|
|
51
|
+
lineItemId: rawId(raw), // the LINE id — what update/remove take, never the product id
|
|
52
|
+
productName: raw.name?.original ?? "",
|
|
53
|
+
quantity: raw.quantityInfo?.confirmedQuantity ?? 0,
|
|
54
|
+
unitPrice: formatMoney(raw.pricing?.unitPrice, currency),
|
|
55
|
+
linePrice: formatMoney(raw.pricing?.totalPrice, currency),
|
|
56
|
+
imageUrl: imgSrc(raw.attributes?.image, 300, 300),
|
|
57
|
+
descriptionLines: ((raw.attributes?.descriptionLines ?? []) as Raw[])
|
|
58
|
+
.map((d) => {
|
|
59
|
+
const label = d.name?.original ?? "", value = d.plainText?.original ?? d.colorInfo?.original ?? "";
|
|
60
|
+
return label && value ? `${label}: ${value}` : value || label;
|
|
61
|
+
})
|
|
62
|
+
.filter(Boolean),
|
|
63
|
+
status: raw.status ?? "IN_STOCK", // not IN_STOCK → the line can't be checked out as-is
|
|
64
|
+
subscription: subscriptionTerms(raw.subscriptionInfo),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function toCart(raw: Raw | null, subtotal: string, discount: string, imgSrc: ImgSrc): Cart {
|
|
69
|
+
const currency = cartCurrency(raw);
|
|
70
|
+
const lines = ((raw?.lineItems ?? []) as Raw[]).map((l) => toLine(l, currency, imgSrc));
|
|
71
|
+
return { lines, itemCount: lines.reduce((n, l) => n + l.quantity, 0), subtotal, discount, currency };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The after-discount subtotal and the CART-level discount from an estimate; "" when unknown. */
|
|
75
|
+
export function summaryTotals(estimate: Raw | null | undefined, raw: Raw): { subtotal: string; discount: string } {
|
|
76
|
+
const s = estimate?.summary?.priceSummary;
|
|
77
|
+
const currency = cartCurrency(raw);
|
|
78
|
+
const d = s?.discount;
|
|
79
|
+
return {
|
|
80
|
+
subtotal: formatMoney(s?.subtotal, currency),
|
|
81
|
+
discount: Number(d?.convertedAmount ?? d?.amount ?? 0) > 0 ? formatMoney(d, currency) : "",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The catalogReference.options object for an add — omits every key the buyer didn't use. */
|
|
86
|
+
export function addOptions({ variantId, modifierChoices, customTextFields, subscriptionOptionId, preorder }: {
|
|
87
|
+
variantId?: string | null; modifierChoices?: Record<string, string>; customTextFields?: Record<string, string>;
|
|
88
|
+
subscriptionOptionId?: string; preorder?: boolean;
|
|
89
|
+
}): Raw {
|
|
90
|
+
const o: Raw = {};
|
|
91
|
+
if (variantId) o.variantId = variantId; // REQUIRED for any product with options
|
|
92
|
+
if (modifierChoices && Object.keys(modifierChoices).length) o.options = modifierChoices; // MODIFIERS only, never Size/Color
|
|
93
|
+
if (customTextFields && Object.keys(customTextFields).length) o.customTextFields = customTextFields;
|
|
94
|
+
if (subscriptionOptionId) o.subscriptionOptionId = subscriptionOptionId;
|
|
95
|
+
if (preorder) o.preOrderRequested = true;
|
|
96
|
+
return o;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Read the add result — a refused add still returns 200: a line whose status isn't IN_STOCK, or no line, is a refusal. */
|
|
100
|
+
export function assertAdded(cart: Raw | null | undefined, productId: string, variantId?: string | null): void {
|
|
101
|
+
const line = ((cart?.lineItems ?? []) as Raw[]).find(
|
|
102
|
+
(l) => l.source?.catalogReference?.catalogItemId === productId && (!variantId || l.source?.catalogReference?.options?.variantId === variantId),
|
|
103
|
+
);
|
|
104
|
+
if (line?.status && line.status !== "IN_STOCK") {
|
|
105
|
+
throw new Error(`This item isn't available right now (${String(line.status).toLowerCase().replace(/_/g, " ")}).`);
|
|
106
|
+
}
|
|
107
|
+
if (!line || line.quantityInfo?.confirmedQuantity === 0) {
|
|
108
|
+
throw new Error("The item couldn't be added. Make sure every required selection was made (options for a product with variants, and all mandatory customizations).");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Refuse checkout for an empty cart or any line not IN_STOCK — say which. */
|
|
113
|
+
export function assertCheckoutable(raw: Raw | null): void {
|
|
114
|
+
const lines: Raw[] = raw?.lineItems ?? [];
|
|
115
|
+
if (!lines.length) throw new Error("Your cart is empty.");
|
|
116
|
+
const unavailable = lines.filter((l) => l.status && l.status !== "IN_STOCK");
|
|
117
|
+
if (unavailable.length) {
|
|
118
|
+
throw new Error(`Some items are no longer available: ${unavailable.map((l) => l.name?.original).filter(Boolean).join(", ")}.`);
|
|
119
|
+
}
|
|
120
|
+
if (!rawId(raw)) throw new Error("Checkout couldn't start: the cart has no id.");
|
|
121
|
+
}
|