@wix/agent-skills 1.16.3 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +115 -12
- package/skills/wix-app/references/APP_MARKET_REVIEW.md +1 -1
- package/skills/wix-app/references/BACKEND_API.md +18 -0
- package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +6 -0
- package/skills/wix-app/references/DASHBOARD_MODAL.md +10 -2
- package/skills/wix-app/references/DASHBOARD_PAGE.md +32 -70
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +145 -127
- package/skills/wix-app/references/SITE_PLUGIN.md +14 -2
- package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +74 -68
- package/skills/wix-app/references/backend-event/COMMON-EVENTS.md +2 -2
- package/skills/wix-app/references/dashboard-page/COLLECTION_TOOLKIT.md +79 -0
- package/skills/wix-app/references/dashboard-page/ENTITY_PAGE_TOOLKIT.md +73 -0
- package/skills/wix-app/references/dashboard-page/PATTERNS_BUNDLE_READING.md +52 -0
- package/skills/wix-app/references/dashboard-page/UX_SUCCESS_MODEL.md +184 -0
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +182 -99
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +94 -49
- package/skills/wix-app/references/editor-react-component/BRANDED-COMPONENTS.md +16 -6
- package/skills/wix-app/references/editor-react-component/BROWSER-SUPPORT.md +56 -0
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +275 -0
- package/skills/wix-app/references/editor-react-component/COMPONENT-PREVIEW.md +35 -40
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +167 -445
- package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +56 -29
- package/skills/wix-app/references/editor-react-component/DIRECTIONALITY.md +53 -158
- package/skills/wix-app/references/editor-react-component/EDITOR-EXTENSION-CONFIGURATION.md +78 -0
- package/skills/wix-app/references/editor-react-component/FUNCTION-HANDLERS.md +42 -134
- package/skills/wix-app/references/editor-react-component/MANIFEST-ERRORS.md +11 -3
- package/skills/wix-app/references/editor-react-component/PARTS.md +46 -24
- package/skills/wix-app/references/editor-react-component/PROPS-VS-CSS.md +63 -179
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +79 -163
- package/skills/wix-app/references/editor-react-component/SITE-CONTEXT-HOOKS.md +36 -21
- package/skills/wix-app/references/editor-react-component/SSR.md +30 -99
- package/skills/wix-base44-connector/SKILL.md +156 -50
- package/skills/wix-base44-connector/references/CAPABILITY_MAP.md +78 -0
- package/skills/wix-base44-connector/scripts/utils.js +239 -45
- package/skills/wix-headless/entry/bootstrap.mjs +9 -2
- package/skills/wix-headless/entry/skill.md +1 -1
- package/skills/wix-headless/references/CAPABILITIES.md +9 -1
- package/skills/wix-headless/references/DESIGN.md +1 -0
- package/skills/wix-headless/references/DISCOVERY.md +2 -1
- package/skills/wix-headless/references/IMAGE_GENERATION.md +1 -0
- package/skills/wix-headless/references/SDK_HANDOFF.md +5 -2
- package/skills/wix-headless/references/SEED.md +2 -1
- package/skills/wix-headless/references/SETUP.md +1 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-blog.md +2 -2
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +68 -31
- package/skills/wix-headless/references/inline-recipes/how-to-code-forms.md +26 -10
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +8 -8
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-custom-login.md +1 -1
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-non-astro.md +1 -1
- package/skills/wix-headless/references/inline-recipes/how-to-code-pricing-plans.md +10 -8
- package/skills/wix-headless/references/inline-recipes/how-to-code-rentals.md +254 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-orders.md +27 -24
- package/skills/wix-headless/references/inline-recipes/setup-online-store.md +13 -0
- package/skills/wix-headless/references/inline-recipes/setup-rentals.md +254 -0
- package/skills/wix-headless/references/managed/AUTHENTICATION.md +5 -0
- package/skills/wix-headless/references/non-astro.md +1 -1
- package/skills/wix-headless-fast/SKILL.md +166 -0
- package/skills/wix-headless-fast/entry/skill.md +99 -0
- package/skills/wix-headless-fast/install/deploy.mjs +433 -0
- package/skills/wix-headless-fast/install/fast-path.mjs +141 -0
- package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +137 -0
- package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +137 -0
- package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +113 -0
- package/skills/wix-headless-fast/references/blog/app/components/blog/RichContent.tsx +42 -0
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +123 -0
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +70 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +99 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +76 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/types.ts +62 -0
- package/skills/wix-headless-fast/references/blog/app-astro/layouts/SiteLayout.astro +40 -0
- package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +99 -0
- package/skills/wix-headless-fast/references/blog/app-astro/pages/blog.astro +24 -0
- package/skills/wix-headless-fast/references/blog/lock/astro/package-lock.json +16156 -0
- package/skills/wix-headless-fast/references/blog/seed/SEED.md +72 -0
- package/skills/wix-headless-fast/references/blog/seed/seed-blog.mjs +287 -0
- package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +129 -0
- package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +143 -0
- package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServicesView.tsx +119 -0
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +138 -0
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +54 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +206 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +96 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +70 -0
- package/skills/wix-headless-fast/references/bookings/app-astro/layouts/SiteLayout.astro +40 -0
- package/skills/wix-headless-fast/references/bookings/app-astro/pages/services/[slug].astro +76 -0
- package/skills/wix-headless-fast/references/bookings/app-astro/pages/services.astro +22 -0
- package/skills/wix-headless-fast/references/bookings/lock/astro/package-lock.json +11962 -0
- package/skills/wix-headless-fast/references/bookings/seed/SEED.md +61 -0
- package/skills/wix-headless-fast/references/bookings/seed/seed-bookings.mjs +302 -0
- package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +179 -0
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +83 -0
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +57 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +151 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/types.ts +75 -0
- package/skills/wix-headless-fast/references/cms/app-astro/layouts/SiteLayout.astro +41 -0
- package/skills/wix-headless-fast/references/cms/lock/astro/package-lock.json +11262 -0
- package/skills/wix-headless-fast/references/cms/seed/SEED.md +103 -0
- package/skills/wix-headless-fast/references/cms/seed/seed-cms.mjs +324 -0
- package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +149 -0
- package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +66 -0
- package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +176 -0
- package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +118 -0
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +117 -0
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +59 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +136 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +128 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/types.ts +61 -0
- package/skills/wix-headless-fast/references/events/app-astro/layouts/SiteLayout.astro +40 -0
- package/skills/wix-headless-fast/references/events/app-astro/pages/event-confirmation.astro +12 -0
- package/skills/wix-headless-fast/references/events/app-astro/pages/events/[slug].astro +80 -0
- package/skills/wix-headless-fast/references/events/app-astro/pages/events.astro +21 -0
- package/skills/wix-headless-fast/references/events/lock/astro/package-lock.json +11884 -0
- package/skills/wix-headless-fast/references/events/seed/SEED.md +86 -0
- package/skills/wix-headless-fast/references/events/seed/seed-events.mjs +276 -0
- package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +150 -0
- package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +317 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +269 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +231 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/types.ts +135 -0
- package/skills/wix-headless-fast/references/forms/lock/astro/package-lock.json +10593 -0
- package/skills/wix-headless-fast/references/forms/seed/SEED.md +108 -0
- package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +320 -0
- package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +63 -0
- package/skills/wix-headless-fast/references/members/app/components/members/AccountView.tsx +60 -0
- package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +155 -0
- package/skills/wix-headless-fast/references/members/app/components/members/MemberMenu.tsx +78 -0
- package/skills/wix-headless-fast/references/members/app/components/members/RequireAuth.tsx +47 -0
- package/skills/wix-headless-fast/references/members/app/hooks/members/useMember.ts +66 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +88 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +53 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/member-store.ts +122 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +50 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/types.ts +21 -0
- package/skills/wix-headless-fast/references/members/app-astro/layouts/SiteLayout.astro +42 -0
- package/skills/wix-headless-fast/references/members/app-astro/pages/account.astro +10 -0
- package/skills/wix-headless-fast/references/members/app-astro/pages/login.astro +11 -0
- package/skills/wix-headless-fast/references/members/lock/astro/package-lock.json +10329 -0
- package/skills/wix-headless-fast/references/members/seed/SEED.md +39 -0
- package/skills/wix-headless-fast/references/members/seed/seed-members.mjs +94 -0
- package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +134 -0
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +110 -0
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +90 -0
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +115 -0
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +59 -0
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +39 -0
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +58 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +165 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/types.ts +55 -0
- package/skills/wix-headless-fast/references/portfolio/app-astro/layouts/SiteLayout.astro +40 -0
- package/skills/wix-headless-fast/references/portfolio/app-astro/pages/portfolio/[slug].astro +30 -0
- package/skills/wix-headless-fast/references/portfolio/app-astro/pages/portfolio.astro +21 -0
- package/skills/wix-headless-fast/references/portfolio/app-astro/pages/projects/[slug].astro +29 -0
- package/skills/wix-headless-fast/references/portfolio/lock/astro/package-lock.json +10197 -0
- package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +78 -0
- package/skills/wix-headless-fast/references/portfolio/seed/seed-portfolio.mjs +258 -0
- package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +133 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +58 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +126 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +37 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +39 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +75 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +45 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/types.ts +30 -0
- package/skills/wix-headless-fast/references/pricing-plans/app-astro/layouts/SiteLayout.astro +41 -0
- package/skills/wix-headless-fast/references/pricing-plans/app-astro/pages/plans/[slug].astro +40 -0
- package/skills/wix-headless-fast/references/pricing-plans/app-astro/pages/plans.astro +21 -0
- package/skills/wix-headless-fast/references/pricing-plans/lock/astro/package-lock.json +11226 -0
- package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +71 -0
- package/skills/wix-headless-fast/references/pricing-plans/seed/seed-pricing-plans.mjs +279 -0
- package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +165 -0
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +142 -0
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartButton.tsx +26 -0
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +121 -0
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/ReservationView.tsx +155 -0
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +55 -0
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useOrderCart.ts +43 -0
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +177 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +165 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/order-store.ts +103 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +254 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +146 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/types.ts +168 -0
- package/skills/wix-headless-fast/references/restaurants/app-astro/layouts/SiteLayout.astro +47 -0
- package/skills/wix-headless-fast/references/restaurants/app-astro/pages/menu.astro +22 -0
- package/skills/wix-headless-fast/references/restaurants/app-astro/pages/reservations.astro +12 -0
- package/skills/wix-headless-fast/references/restaurants/lock/astro/package-lock.json +12560 -0
- package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +81 -0
- package/skills/wix-headless-fast/references/restaurants/seed/seed-restaurants.mjs +479 -0
- package/skills/wix-headless-fast/references/shared/CONTENT.md +22 -0
- package/skills/wix-headless-fast/references/shared/CUSTOM_OPERATIONS.md +73 -0
- package/skills/wix-headless-fast/references/shared/DESIGN.md +26 -0
- package/skills/wix-headless-fast/references/shared/app/styles/global.css +52 -0
- package/skills/wix-headless-fast/references/shared/app/wix/config.ts +15 -0
- package/skills/wix-headless-fast/references/shared/app/wix/media.ts +27 -0
- package/skills/wix-headless-fast/references/shared/app/wix/money.ts +24 -0
- package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +76 -0
- package/skills/wix-headless-fast/references/shared/capabilities/media-upload/app/wix/media-upload/client.ts +46 -0
- package/skills/wix-headless-fast/references/shared/capabilities/media-upload/app-astro/pages/api/wix/media-upload.ts +46 -0
- package/skills/wix-headless-fast/references/shared/seed/images.mjs +145 -0
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +149 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartButton.tsx +26 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx +121 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductCard.tsx +100 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductDetailView.tsx +130 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductGrid.tsx +48 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ShopView.tsx +66 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/VariantPicker.tsx +103 -0
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useCart.ts +48 -0
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +140 -0
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +80 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-store.ts +101 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +221 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +215 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +109 -0
- package/skills/wix-headless-fast/references/storefront/app-astro/layouts/SiteLayout.astro +46 -0
- package/skills/wix-headless-fast/references/storefront/app-astro/pages/products/[slug].astro +56 -0
- package/skills/wix-headless-fast/references/storefront/app-astro/pages/shop.astro +23 -0
- package/skills/wix-headless-fast/references/storefront/lock/astro/package-lock.json +12247 -0
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +85 -0
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +415 -0
- package/skills/wix-manage/SKILL.md +28 -2
- package/skills/wix-manage/references/bookings/booking-system-integration-gaps.md +20 -13
- package/skills/wix-manage/references/bookings/end-to-end-booking-flow.md +24 -11
- package/skills/wix-manage/references/cms/cms-ecommerce-catalog-integration.md +5 -5
- package/skills/wix-manage/references/google-ads/manage-campaign-success-guide.md +141 -0
- package/skills/wix-manage/references/google-business-profile/connect-google-business-profile.md +134 -0
- package/skills/wix-manage/references/google-business-profile/manage-google-business-profile-locations.md +182 -0
- package/skills/wix-manage/references/marketing/create-and-publish-social-post.md +3 -0
- package/skills/wix-manage/references/seo/manage-seo-tags.md +315 -0
- package/skills/wix-manage/references/seo/manage-url-redirects.md +250 -0
- package/skills/wix-manage/references/sites/query-sites.md +72 -161
- package/skills/wix-manage/references/stores/create-product-catalog-v3.md +21 -6
- package/skills/wix-manage/references/stores/update-product-with-options.md +83 -0
- package/skills/wix-replatform/resources/rp-mapper/wix-business-solutions-taxonomy.md +5 -6
- package/skills/wix-vibe-headless/SKILL.md +18 -4
- package/skills/wix-vibe-headless/install/deploy.cjs +12 -4
- package/skills/wix-vibe-headless/install/pin-agents-md.cjs +2 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +16 -10
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +13 -8
- package/skills/wix-vibe-headless/platforms/base44.md +27 -62
- package/skills/wix-vibe-headless/platforms/generic.md +6 -3
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +45 -0
- package/skills/wix-vibe-headless/references/bookings/app/components/RentalDurationPicker.jsx +67 -0
- package/skills/wix-vibe-headless/references/bookings/app/hooks/useRentalDuration.js +128 -0
- package/skills/wix-vibe-headless/references/bookings/app/hooks/useServiceDetail.js +5 -3
- package/skills/wix-vibe-headless/references/bookings/app/rest/wix-bookings-checkout.js +9 -8
- package/skills/wix-vibe-headless/references/bookings/app/rest/wix-bookings-rentals.js +250 -0
- package/skills/wix-vibe-headless/references/bookings/app/rest/wix-bookings-services.js +12 -5
- package/skills/wix-vibe-headless/references/bookings/seed/SEED.md +41 -0
- package/skills/wix-vibe-headless/references/bookings/seed/seed-rentals.js +267 -0
- package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +6 -3
- package/skills/wix-vibe-headless/references/forms/INSTRUCTIONS.md +470 -0
- package/skills/wix-vibe-headless/references/forms/app/hooks/useWixForm.js +425 -0
- package/skills/wix-vibe-headless/references/forms/app/lib/wix-form-schema-utils.js +187 -0
- package/skills/wix-vibe-headless/references/forms/app/rest/wix-forms-submissions.js +238 -0
- package/skills/wix-vibe-headless/references/forms/app/rest/wix-forms.js +108 -0
- package/skills/wix-vibe-headless/references/forms/seed/SEED.md +1762 -0
- package/skills/wix-vibe-headless/references/rentals/INSTRUCTIONS.md +13 -0
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +4 -1
- package/skills/wix-vibe-headless/references/restaurants/app/components/OrderCartDrawer.jsx +23 -11
- package/skills/wix-vibe-headless/references/restaurants/app/context/OrderCartContext.jsx +1 -1
- package/skills/wix-vibe-headless/references/restaurants/app/rest/wix-restaurants-ordering.js +29 -31
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +152 -92
- package/skills/wix-vibe-headless/references/storefront/app/components/CartDrawer.jsx +37 -38
- package/skills/wix-vibe-headless/references/storefront/app/context/CartContext.jsx +2 -2
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductDetail.js +15 -8
- package/skills/wix-vibe-headless/references/storefront/app/lib/storeImage.js +37 -0
- package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-cart.js +35 -35
- package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-catalog.js +18 -6
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +30 -7
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.js +46 -10
- package/skills/wix-app/references/editor-react-component/A11Y-REVIEW-REFERENCE.md +0 -172
- package/skills/wix-app/references/editor-react-component/A11Y-REVIEW.md +0 -143
- package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +0 -516
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +0 -225
- package/skills/wix-app/references/editor-react-component/EDIT-FLOW.md +0 -6
- package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +0 -95
- package/skills/wix-vibe-headless/references/storefront/app/components/ProductCard.jsx +0 -110
- package/skills/wix-vibe-headless/references/storefront/app/components/ProductGallery.jsx +0 -51
- package/skills/wix-vibe-headless/references/storefront/app/components/ProductGrid.jsx +0 -49
- package/skills/wix-vibe-headless/references/storefront/app/components/VariantPicker.jsx +0 -92
- package/skills/wix-vibe-headless/references/storefront/app/pages/ProductDetail.jsx +0 -139
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// REFERENCE listing surface: category filter + services grid on the @theme tokens.
|
|
2
|
+
// Correct and complete; per the skill's model you design and build your own on useServices.
|
|
3
|
+
import type { ComponentType, ReactNode } from "react";
|
|
4
|
+
import { useServices } from "../../hooks/bookings/useServices";
|
|
5
|
+
import type { BookingCategory, ServiceSummary } from "../../wix/bookings/types";
|
|
6
|
+
|
|
7
|
+
export interface LinkLikeProps {
|
|
8
|
+
href: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const PlainLink = ({ href, className, children }: LinkLikeProps) => (
|
|
14
|
+
<a href={href} className={className}>
|
|
15
|
+
{children}
|
|
16
|
+
</a>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export interface ServiceCardProps {
|
|
20
|
+
service: ServiceSummary;
|
|
21
|
+
serviceHref?: (slug: string) => string;
|
|
22
|
+
LinkComponent?: ComponentType<LinkLikeProps>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ServiceCard({
|
|
26
|
+
service,
|
|
27
|
+
serviceHref = (slug) => `/services/${slug}`,
|
|
28
|
+
LinkComponent = PlainLink,
|
|
29
|
+
}: ServiceCardProps) {
|
|
30
|
+
return (
|
|
31
|
+
<LinkComponent href={serviceHref(service.slug)} className="group block no-underline">
|
|
32
|
+
<div className="relative aspect-[4/3] overflow-hidden rounded-lg bg-secondary">
|
|
33
|
+
{service.imageUrl && (
|
|
34
|
+
<img
|
|
35
|
+
src={service.imageUrl}
|
|
36
|
+
alt={service.name}
|
|
37
|
+
loading="lazy"
|
|
38
|
+
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
|
|
39
|
+
/>
|
|
40
|
+
)}
|
|
41
|
+
<span className="absolute left-3 top-3 rounded-full bg-background/90 px-3 py-1 text-xs font-semibold text-foreground backdrop-blur">
|
|
42
|
+
{service.type === "CLASS" ? "Class" : "Appointment"}
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
<p className="mt-3 text-sm font-medium text-foreground">{service.name}</p>
|
|
46
|
+
{service.tagLine && <p className="mt-0.5 text-xs text-muted-foreground">{service.tagLine}</p>}
|
|
47
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
48
|
+
{service.durationMinutes ? `${service.durationMinutes} min · ` : ""}
|
|
49
|
+
<span className="text-foreground">{service.price}</span>
|
|
50
|
+
</p>
|
|
51
|
+
</LinkComponent>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ServicesViewProps {
|
|
56
|
+
initialServices?: ServiceSummary[];
|
|
57
|
+
initialCategories?: BookingCategory[];
|
|
58
|
+
emptyMessage?: string;
|
|
59
|
+
serviceHref?: ServiceCardProps["serviceHref"];
|
|
60
|
+
LinkComponent?: ComponentType<LinkLikeProps>;
|
|
61
|
+
CardComponent?: ComponentType<ServiceCardProps>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const pill = (active: boolean) =>
|
|
65
|
+
`rounded-full border px-4 py-1.5 text-sm font-medium transition-colors ${
|
|
66
|
+
active
|
|
67
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
68
|
+
: "border-border text-foreground hover:bg-secondary"
|
|
69
|
+
}`;
|
|
70
|
+
|
|
71
|
+
export default function ServicesView({
|
|
72
|
+
initialServices,
|
|
73
|
+
initialCategories,
|
|
74
|
+
emptyMessage = "No services yet — check back soon.",
|
|
75
|
+
serviceHref,
|
|
76
|
+
LinkComponent,
|
|
77
|
+
CardComponent = ServiceCard,
|
|
78
|
+
}: ServicesViewProps) {
|
|
79
|
+
const { services, categories, activeCategoryId, setActiveCategoryId, error } = useServices({
|
|
80
|
+
initialServices,
|
|
81
|
+
initialCategories,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div>
|
|
86
|
+
{categories.length > 1 && (
|
|
87
|
+
<div className="mb-8 flex flex-wrap gap-2" role="group" aria-label="Categories">
|
|
88
|
+
<button type="button" className={pill(activeCategoryId === null)} onClick={() => setActiveCategoryId(null)}>
|
|
89
|
+
All
|
|
90
|
+
</button>
|
|
91
|
+
{categories.map((c) => (
|
|
92
|
+
<button key={c.id} type="button" className={pill(activeCategoryId === c.id)} onClick={() => setActiveCategoryId(c.id)}>
|
|
93
|
+
{c.name}
|
|
94
|
+
</button>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
{error && <p className="mb-4 text-sm text-red-600">{error}</p>}
|
|
99
|
+
{services === null ? (
|
|
100
|
+
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3" aria-busy="true">
|
|
101
|
+
{Array.from({ length: 6 }, (_, i) => (
|
|
102
|
+
<div key={i}>
|
|
103
|
+
<div className="aspect-[4/3] animate-pulse rounded-lg bg-secondary" />
|
|
104
|
+
<div className="mt-3 h-3.5 w-2/3 animate-pulse rounded bg-secondary" />
|
|
105
|
+
</div>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
) : services.length === 0 ? (
|
|
109
|
+
<p className="py-16 text-center text-muted-foreground">{emptyMessage}</p>
|
|
110
|
+
) : (
|
|
111
|
+
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
|
112
|
+
{services.map((s) => (
|
|
113
|
+
<CardComponent key={s.id} service={s} serviceHref={serviceHref} LinkComponent={LinkComponent} />
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// The whole booking state machine for one service: availability window (day-grouped slots,
|
|
2
|
+
// week paging, optional staff filter), the schema-driven form, and book(). All correctness
|
|
3
|
+
// (slot scheduleId vs eventId, ANY_RESOURCE, formSubmission, payment option, checkout-or-
|
|
4
|
+
// place) lives in the data layer — this hook orchestrates; you own how it looks.
|
|
5
|
+
import { useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { bookService, fetchBookingForm, fetchSlots } from "../../wix/bookings/booking";
|
|
7
|
+
import type { BookingFormField, BookingResult, ServiceDetail, Slot } from "../../wix/bookings/types";
|
|
8
|
+
|
|
9
|
+
export interface UseBookingFlow {
|
|
10
|
+
/** Slots grouped by day, in order — [{ dayKey, dayLabel, slots }]. null while loading. */
|
|
11
|
+
days: { dayKey: string; dayLabel: string; slots: Slot[] }[] | null;
|
|
12
|
+
/** The 7-day window start; page with nextWeek/prevWeek (prev clamps to today). */
|
|
13
|
+
windowStart: Date;
|
|
14
|
+
nextWeek: () => void;
|
|
15
|
+
prevWeek: () => void;
|
|
16
|
+
/** Staff filter — render a picker only when service.staff.length > 1. */
|
|
17
|
+
staffId: string | undefined;
|
|
18
|
+
setStaffId: (id: string | undefined) => void;
|
|
19
|
+
selectedSlot: Slot | null;
|
|
20
|
+
setSelectedSlot: (s: Slot | null) => void;
|
|
21
|
+
/** Schema-driven form fields (never empty — contact basics fallback). */
|
|
22
|
+
formFields: BookingFormField[];
|
|
23
|
+
values: Record<string, string>;
|
|
24
|
+
setValue: (target: string, value: string) => void;
|
|
25
|
+
/** True when a slot is selected and every field has a value. */
|
|
26
|
+
canBook: boolean;
|
|
27
|
+
/** Books the selected slot. On "redirect" the browser is already navigating. */
|
|
28
|
+
book: () => Promise<BookingResult>;
|
|
29
|
+
booking: boolean;
|
|
30
|
+
/** Set after a free/offline booking completes. */
|
|
31
|
+
confirmed: BookingResult | null;
|
|
32
|
+
error: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dayLabel = (dayKey: string): string => {
|
|
36
|
+
try {
|
|
37
|
+
return new Date(`${dayKey}T12:00:00`).toLocaleDateString(undefined, {
|
|
38
|
+
weekday: "short",
|
|
39
|
+
month: "short",
|
|
40
|
+
day: "numeric",
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
return dayKey;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function useBookingFlow(service: ServiceDetail): UseBookingFlow {
|
|
48
|
+
const [windowStart, setWindowStart] = useState<Date>(() => new Date());
|
|
49
|
+
const [staffId, setStaffId] = useState<string | undefined>(undefined);
|
|
50
|
+
const [slots, setSlots] = useState<Slot[] | null>(null);
|
|
51
|
+
const [selectedSlot, setSelectedSlot] = useState<Slot | null>(null);
|
|
52
|
+
const [formFields, setFormFields] = useState<BookingFormField[]>([]);
|
|
53
|
+
const [values, setValues] = useState<Record<string, string>>({});
|
|
54
|
+
const [booking, setBooking] = useState(false);
|
|
55
|
+
const [confirmed, setConfirmed] = useState<BookingResult | null>(null);
|
|
56
|
+
const [error, setError] = useState<string | null>(null);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
let alive = true;
|
|
60
|
+
setSlots(null);
|
|
61
|
+
setSelectedSlot(null);
|
|
62
|
+
fetchSlots(service, { from: windowStart, days: 7, staffId })
|
|
63
|
+
.then((s) => alive && setSlots(s))
|
|
64
|
+
.catch((e) => {
|
|
65
|
+
if (!alive) return;
|
|
66
|
+
setSlots([]);
|
|
67
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
68
|
+
});
|
|
69
|
+
return () => {
|
|
70
|
+
alive = false;
|
|
71
|
+
};
|
|
72
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
+
}, [service.id, windowStart, staffId]);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
let alive = true;
|
|
77
|
+
fetchBookingForm(service.formId).then((f) => alive && setFormFields(f));
|
|
78
|
+
return () => {
|
|
79
|
+
alive = false;
|
|
80
|
+
};
|
|
81
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
+
}, [service.formId]);
|
|
83
|
+
|
|
84
|
+
const days = useMemo(() => {
|
|
85
|
+
if (slots === null) return null;
|
|
86
|
+
const byDay = new Map<string, Slot[]>();
|
|
87
|
+
for (const s of slots) byDay.set(s.dayKey, [...(byDay.get(s.dayKey) ?? []), s]);
|
|
88
|
+
return [...byDay.entries()]
|
|
89
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
90
|
+
.map(([dayKey, daySlots]) => ({ dayKey, dayLabel: dayLabel(dayKey), slots: daySlots }));
|
|
91
|
+
}, [slots]);
|
|
92
|
+
|
|
93
|
+
const canBook =
|
|
94
|
+
!!selectedSlot && formFields.every((f) => (values[f.target] ?? "").trim().length > 0);
|
|
95
|
+
|
|
96
|
+
async function book(): Promise<BookingResult> {
|
|
97
|
+
if (!selectedSlot) throw new Error("Pick a time first.");
|
|
98
|
+
setBooking(true);
|
|
99
|
+
setError(null);
|
|
100
|
+
try {
|
|
101
|
+
const result = await bookService(service, selectedSlot, values, { staffId });
|
|
102
|
+
if (result.kind === "redirect") {
|
|
103
|
+
window.location.href = result.url;
|
|
104
|
+
} else {
|
|
105
|
+
setConfirmed(result);
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
} catch (e) {
|
|
109
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
110
|
+
throw e;
|
|
111
|
+
} finally {
|
|
112
|
+
setBooking(false);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
days,
|
|
118
|
+
windowStart,
|
|
119
|
+
nextWeek: () => setWindowStart((d) => new Date(d.getTime() + 7 * 24 * 3600 * 1000)),
|
|
120
|
+
prevWeek: () =>
|
|
121
|
+
setWindowStart((d) => {
|
|
122
|
+
const prev = new Date(d.getTime() - 7 * 24 * 3600 * 1000);
|
|
123
|
+
return prev < new Date() ? new Date() : prev;
|
|
124
|
+
}),
|
|
125
|
+
staffId,
|
|
126
|
+
setStaffId,
|
|
127
|
+
selectedSlot,
|
|
128
|
+
setSelectedSlot,
|
|
129
|
+
formFields,
|
|
130
|
+
values,
|
|
131
|
+
setValue: (target, value) => setValues((v) => ({ ...v, [target]: value })),
|
|
132
|
+
canBook,
|
|
133
|
+
book,
|
|
134
|
+
booking,
|
|
135
|
+
confirmed,
|
|
136
|
+
error,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Services listing + category filter. SSR-friendly: pass server-fetched data as `initial*`
|
|
2
|
+
// (Astro frontmatter / server component) and no client fetch happens; a SPA passes nothing.
|
|
3
|
+
// Category filtering is client-side (bookings catalogs are small and fully fetched).
|
|
4
|
+
import { useEffect, useMemo, useState } from "react";
|
|
5
|
+
import { fetchBookingCategories, fetchServices } from "../../wix/bookings/services";
|
|
6
|
+
import type { BookingCategory, ServiceSummary } from "../../wix/bookings/types";
|
|
7
|
+
|
|
8
|
+
export interface UseServicesOptions {
|
|
9
|
+
initialServices?: ServiceSummary[];
|
|
10
|
+
initialCategories?: BookingCategory[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseServices {
|
|
14
|
+
/** null while the first load is in flight — render skeletons, not an empty state. */
|
|
15
|
+
services: ServiceSummary[] | null;
|
|
16
|
+
categories: BookingCategory[];
|
|
17
|
+
activeCategoryId: string | null;
|
|
18
|
+
setActiveCategoryId: (id: string | null) => void;
|
|
19
|
+
error: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useServices({ initialServices, initialCategories }: UseServicesOptions = {}): UseServices {
|
|
23
|
+
const [all, setAll] = useState<ServiceSummary[] | null>(initialServices ?? null);
|
|
24
|
+
const [categories, setCategories] = useState<BookingCategory[]>(initialCategories ?? []);
|
|
25
|
+
const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null);
|
|
26
|
+
const [error, setError] = useState<string | null>(null);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let alive = true;
|
|
30
|
+
if (!initialServices) {
|
|
31
|
+
fetchServices()
|
|
32
|
+
.then((s) => alive && setAll(s))
|
|
33
|
+
.catch((e) => {
|
|
34
|
+
if (!alive) return;
|
|
35
|
+
setAll([]);
|
|
36
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (!initialCategories) {
|
|
40
|
+
fetchBookingCategories().then((c) => alive && setCategories(c));
|
|
41
|
+
}
|
|
42
|
+
return () => {
|
|
43
|
+
alive = false;
|
|
44
|
+
};
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
const services = useMemo(() => {
|
|
49
|
+
if (all === null) return null;
|
|
50
|
+
return activeCategoryId ? all.filter((s) => s.categoryId === activeCategoryId) : all;
|
|
51
|
+
}, [all, activeCategoryId]);
|
|
52
|
+
|
|
53
|
+
return { services, categories, activeCategoryId, setActiveCategoryId, error };
|
|
54
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Availability, the booking form, and the createBooking → Cart V2 → checkout-or-place
|
|
2
|
+
// sequence. The payload shapes here are exact and easy to get subtly wrong — copy as-is;
|
|
3
|
+
// extend by calling these exports, never by editing them. Failures are loud.
|
|
4
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/time-slots/time-slots-v2/list-availability-time-slots.md
|
|
5
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/time-slots/time-slots-v2/list-event-time-slots.md
|
|
6
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/bookings/bookings-writer-v2/create-booking.md
|
|
7
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/create-cart.md
|
|
8
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/calculate-cart.md
|
|
9
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/place-order.md
|
|
10
|
+
// docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md
|
|
11
|
+
// docs: https://dev.wix.com/docs/api-reference/crm/forms/form-schemas/get-form-summary.md
|
|
12
|
+
import {
|
|
13
|
+
availabilityTimeSlots,
|
|
14
|
+
eventTimeSlots,
|
|
15
|
+
bookings as bookingsModule,
|
|
16
|
+
} from "@wix/bookings";
|
|
17
|
+
import { createCart, calculateCart, placeOrder } from "@wix/auto_sdk_ecom_cart-v-2";
|
|
18
|
+
import { redirects as redirectsModule } from "@wix/redirects";
|
|
19
|
+
import { forms as formsModule } from "@wix/forms";
|
|
20
|
+
import { wixModule } from "../sdk";
|
|
21
|
+
import { BOOKINGS_APP_ID, STAFF_RESOURCE_TYPE_ID } from "./services";
|
|
22
|
+
import type { BookingFormField, BookingResult, ServiceDetail, Slot } from "./types";
|
|
23
|
+
|
|
24
|
+
const apptSlots = wixModule(availabilityTimeSlots);
|
|
25
|
+
const classSlots = wixModule(eventTimeSlots);
|
|
26
|
+
const bookings = wixModule(bookingsModule);
|
|
27
|
+
const cart = wixModule({ createCart, calculateCart, placeOrder });
|
|
28
|
+
const redirects = wixModule(redirectsModule);
|
|
29
|
+
const forms = wixModule(formsModule);
|
|
30
|
+
|
|
31
|
+
type Raw = Record<string, any>;
|
|
32
|
+
|
|
33
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
34
|
+
/** Local wall-clock "YYYY-MM-DDThh:mm:ss" (NO Z) — the format the availability APIs require. */
|
|
35
|
+
export function toLocalDateString(d: Date): string {
|
|
36
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toSlot(raw: Raw): Slot {
|
|
40
|
+
const start: string = raw.localStartDate ?? "";
|
|
41
|
+
const staff = (raw.availableResources ?? [])
|
|
42
|
+
.flatMap((ar: Raw) => ar.resources ?? [])
|
|
43
|
+
.map((r: Raw) => ({ id: r._id ?? "", name: r.name ?? "" }))
|
|
44
|
+
.filter((r: { id: string }) => r.id);
|
|
45
|
+
let label = start.slice(11, 16);
|
|
46
|
+
try {
|
|
47
|
+
label = new Date(start).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
|
|
48
|
+
} catch {
|
|
49
|
+
/* keep the hh:mm fallback */
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
startLocal: start,
|
|
53
|
+
endLocal: raw.localEndDate ?? "",
|
|
54
|
+
dayKey: start.slice(0, 10),
|
|
55
|
+
label,
|
|
56
|
+
scheduleId: raw.scheduleId ?? null,
|
|
57
|
+
eventId: raw.eventInfo?.eventId ?? null,
|
|
58
|
+
staff,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Bookable slots for a service in [from, to) — APPOINTMENT and CLASS use different APIs;
|
|
64
|
+
* this branches for you. Dates are local wall-clock; timeZone defaults to the visitor's.
|
|
65
|
+
*/
|
|
66
|
+
export async function fetchSlots(
|
|
67
|
+
service: Pick<ServiceDetail, "id" | "type">,
|
|
68
|
+
{
|
|
69
|
+
from = new Date(),
|
|
70
|
+
days = 7,
|
|
71
|
+
timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
72
|
+
staffId,
|
|
73
|
+
}: { from?: Date; days?: number; timeZone?: string; staffId?: string } = {},
|
|
74
|
+
): Promise<Slot[]> {
|
|
75
|
+
const to = new Date(from.getTime() + days * 24 * 3600 * 1000);
|
|
76
|
+
const fromLocalDate = toLocalDateString(from);
|
|
77
|
+
const toLocalDate = toLocalDateString(to);
|
|
78
|
+
|
|
79
|
+
if (service.type === "CLASS") {
|
|
80
|
+
const res: Raw = await classSlots.listEventTimeSlots({
|
|
81
|
+
serviceIds: [service.id],
|
|
82
|
+
fromLocalDate,
|
|
83
|
+
toLocalDate,
|
|
84
|
+
timeZone,
|
|
85
|
+
includeNonBookable: false,
|
|
86
|
+
...(staffId ? { eventFilter: { "resources.id": { $hasSome: [staffId] } } } : {}),
|
|
87
|
+
} as any);
|
|
88
|
+
return (res.timeSlots ?? []).map((t: Raw) => toSlot(t));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const res: Raw = await apptSlots.listAvailabilityTimeSlots({
|
|
92
|
+
serviceId: service.id,
|
|
93
|
+
fromLocalDate,
|
|
94
|
+
toLocalDate,
|
|
95
|
+
timeZone,
|
|
96
|
+
bookable: true,
|
|
97
|
+
cursorPaging: { limit: 100 },
|
|
98
|
+
includeResourceTypeIds: [STAFF_RESOURCE_TYPE_ID],
|
|
99
|
+
...(staffId
|
|
100
|
+
? { resourceTypes: [{ resourceTypeId: STAFF_RESOURCE_TYPE_ID, resourceIds: [staffId] }] }
|
|
101
|
+
: {}),
|
|
102
|
+
} as any);
|
|
103
|
+
return (res.timeSlots ?? []).map((t: Raw) => toSlot(t));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const FALLBACK_FIELDS: BookingFormField[] = [
|
|
107
|
+
{ target: "first_name", label: "First Name", type: "STRING" },
|
|
108
|
+
{ target: "last_name", label: "Last Name", type: "STRING" },
|
|
109
|
+
{ target: "email", label: "Email", type: "EMAIL" },
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The service's booking-form fields, flat and render-ready (values are keyed by `target`).
|
|
114
|
+
* ALWAYS returns a non-empty list — contact basics when the schema is missing/unusable —
|
|
115
|
+
* so the form can render unconditionally.
|
|
116
|
+
*/
|
|
117
|
+
export async function fetchBookingForm(formId: string | null): Promise<BookingFormField[]> {
|
|
118
|
+
if (!formId) return FALLBACK_FIELDS;
|
|
119
|
+
try {
|
|
120
|
+
const res: Raw = await forms.getFormSummary(formId);
|
|
121
|
+
const fields = (res.formSummary?.fields ?? [])
|
|
122
|
+
.filter((f: Raw) => !f.deleted)
|
|
123
|
+
.filter((f: Raw) => f.type && ["STRING", "EMAIL", "PHONE", "NUMBER", "URL"].includes(f.type))
|
|
124
|
+
.map((f: Raw) => ({
|
|
125
|
+
target: f.target ?? "",
|
|
126
|
+
label: f.label ?? f.target ?? "",
|
|
127
|
+
type: f.type,
|
|
128
|
+
...(Array.isArray(f.options) && f.options.length ? { options: f.options } : {}),
|
|
129
|
+
}))
|
|
130
|
+
.filter((f: BookingFormField) => f.target);
|
|
131
|
+
return fields.length ? fields : FALLBACK_FIELDS;
|
|
132
|
+
} catch {
|
|
133
|
+
return FALLBACK_FIELDS;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Book a slot: createBooking → createCart (holds the seat) → calculateCart →
|
|
139
|
+
* hosted checkout (paid) or placeOrder (free / pay-in-person). Call from the browser.
|
|
140
|
+
* `formValues` is the object your inputs wrote, keyed by field `target` — passed as the
|
|
141
|
+
* formSubmission DIRECTLY. Throws with a friendly message on refusal (slot taken, invalid
|
|
142
|
+
* form) — surface it, don't swallow it.
|
|
143
|
+
*/
|
|
144
|
+
export async function bookService(
|
|
145
|
+
service: ServiceDetail,
|
|
146
|
+
slot: Slot,
|
|
147
|
+
formValues: Record<string, unknown>,
|
|
148
|
+
{ staffId, timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone }: { staffId?: string; timeZone?: string } = {},
|
|
149
|
+
): Promise<BookingResult> {
|
|
150
|
+
const staff = staffId ? slot.staff.find((s) => s.id === staffId) : undefined;
|
|
151
|
+
const created: Raw = await bookings.createBooking(
|
|
152
|
+
{
|
|
153
|
+
selectedPaymentOption: service.paymentOption,
|
|
154
|
+
totalParticipants: 1,
|
|
155
|
+
bookedEntity: {
|
|
156
|
+
slot: {
|
|
157
|
+
serviceId: service.id,
|
|
158
|
+
scheduleId: slot.scheduleId ?? undefined, // APPOINTMENT: the SLOT's scheduleId (never service.schedule.id)
|
|
159
|
+
eventId: slot.eventId ?? undefined, // CLASS
|
|
160
|
+
startDate: slot.startLocal,
|
|
161
|
+
endDate: slot.endLocal,
|
|
162
|
+
timezone: timeZone,
|
|
163
|
+
...(staff
|
|
164
|
+
? { resource: { _id: staff.id, name: staff.name } }
|
|
165
|
+
: {
|
|
166
|
+
resourceSelections: [
|
|
167
|
+
{ resourceTypeId: STAFF_RESOURCE_TYPE_ID, selectionMethod: "ANY_RESOURCE" },
|
|
168
|
+
],
|
|
169
|
+
}),
|
|
170
|
+
location: { locationType: "OWNER_BUSINESS" },
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
} as any,
|
|
174
|
+
{ formSubmission: formValues } as any,
|
|
175
|
+
);
|
|
176
|
+
const bookingId: string | undefined = created.booking?._id;
|
|
177
|
+
if (!bookingId) throw new Error("The booking couldn't be created — the slot may have just been taken.");
|
|
178
|
+
|
|
179
|
+
const newCart: Raw = await cart.createCart({
|
|
180
|
+
catalogItems: [
|
|
181
|
+
{ quantity: 1, catalogReference: { catalogItemId: bookingId, appId: BOOKINGS_APP_ID } },
|
|
182
|
+
],
|
|
183
|
+
cart: { source: { channelType: "WEB" } },
|
|
184
|
+
} as any);
|
|
185
|
+
const cartId: string | undefined = newCart._id ?? newCart.cart?._id;
|
|
186
|
+
if (!cartId) throw new Error("The booking couldn't be reserved — please try again.");
|
|
187
|
+
|
|
188
|
+
const calc: Raw = await cart.calculateCart(cartId);
|
|
189
|
+
const total = Number(calc?.summary?.priceSummary?.total?.amount ?? 0);
|
|
190
|
+
const checkoutRequired =
|
|
191
|
+
service.cancellationFeeEnabled || (total > 0 && service.paymentOption !== "OFFLINE");
|
|
192
|
+
|
|
193
|
+
if (checkoutRequired) {
|
|
194
|
+
const origin = typeof window !== "undefined" ? window.location.origin : "";
|
|
195
|
+
const session: Raw = await redirects.createRedirectSession({
|
|
196
|
+
ecomCheckout: { checkoutId: cartId },
|
|
197
|
+
callbacks: { postFlowUrl: origin ? `${origin}/` : undefined },
|
|
198
|
+
});
|
|
199
|
+
const url = session?.redirectSession?.fullUrl;
|
|
200
|
+
if (!url) throw new Error("Checkout couldn't start — please try again.");
|
|
201
|
+
return { kind: "redirect", url };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const order: Raw = await cart.placeOrder(cartId);
|
|
205
|
+
return { kind: "confirmed", bookingId, orderId: order?.orderId ?? order?.order?._id ?? null };
|
|
206
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Service reads (Wix Bookings Services V2) — the only file that touches raw service
|
|
2
|
+
// entities. Everything it returns is a plain DTO from ./types. Copy as-is; extend by adding
|
|
3
|
+
// functions, not by editing these.
|
|
4
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/services-v2/query-services.md
|
|
5
|
+
// docs: https://dev.wix.com/docs/api-reference/business-solutions/bookings/services/categories-v2/query-categories.md
|
|
6
|
+
import { services as servicesModule, categoriesV2 } from "@wix/bookings";
|
|
7
|
+
import { wixModule } from "../sdk";
|
|
8
|
+
import { imgSrc } from "../media";
|
|
9
|
+
import type { BookingCategory, ServiceDetail, ServiceSummary, ServiceType } from "./types";
|
|
10
|
+
|
|
11
|
+
const services = wixModule(servicesModule);
|
|
12
|
+
const categories = wixModule(categoriesV2);
|
|
13
|
+
|
|
14
|
+
/** The Wix Bookings app id — the cart's catalogReference.appId and the services filter. */
|
|
15
|
+
export const BOOKINGS_APP_ID = "13d21c63-b5ec-5912-8397-c3a5ddb27a97";
|
|
16
|
+
/** Staff-member resource type id (ANY_RESOURCE fallback + staff filtering). */
|
|
17
|
+
export const STAFF_RESOURCE_TYPE_ID = "1cd44cf8-756f-41c3-bd90-3e2ffcaf1155";
|
|
18
|
+
|
|
19
|
+
type Raw = Record<string, any>;
|
|
20
|
+
|
|
21
|
+
function formatPrice(value: string | undefined, currency: string | undefined): string {
|
|
22
|
+
if (value == null || value === "" || Number(value) === 0) return "Free";
|
|
23
|
+
try {
|
|
24
|
+
return new Intl.NumberFormat(undefined, { style: "currency", currency: currency || "USD" }).format(Number(value));
|
|
25
|
+
} catch {
|
|
26
|
+
return `${value} ${currency ?? ""}`.trim();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toSummary(raw: Raw): ServiceSummary {
|
|
31
|
+
const price = raw.payment?.fixed?.price;
|
|
32
|
+
const free = raw.payment?.rateType === "NO_FEE" || !price?.value || Number(price.value) === 0;
|
|
33
|
+
return {
|
|
34
|
+
id: raw._id ?? "",
|
|
35
|
+
slug: raw.mainSlug?.name ?? raw.supportedSlugs?.[0]?.name ?? "",
|
|
36
|
+
name: raw.name ?? "",
|
|
37
|
+
tagLine: raw.tagLine ?? "",
|
|
38
|
+
type: (raw.type ?? "APPOINTMENT") as ServiceType,
|
|
39
|
+
price: free ? "Free" : formatPrice(price?.value, price?.currency),
|
|
40
|
+
free,
|
|
41
|
+
durationMinutes: raw.schedule?.availabilityConstraints?.sessionDurations?.[0] ?? null,
|
|
42
|
+
imageUrl: imgSrc(raw.media?.mainMedia?.image, 800, 800),
|
|
43
|
+
categoryId: raw.category?._id ?? null,
|
|
44
|
+
staff: (raw.staffMemberDetails?.staffMembers ?? [])
|
|
45
|
+
.map((m: Raw) => ({ id: m.staffMemberId ?? "", name: m.name ?? "" }))
|
|
46
|
+
.filter((m: { id: string }) => m.id),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toDetail(raw: Raw): ServiceDetail {
|
|
51
|
+
const summary = toSummary(raw);
|
|
52
|
+
const online = raw.payment?.options?.online === true;
|
|
53
|
+
const inPerson = raw.payment?.options?.inPerson === true;
|
|
54
|
+
// Derive — never hardcode "ONLINE": a free/pay-in-person service booked ONLINE gets
|
|
55
|
+
// rejected by the cart with INSUFFICIENT_INVENTORY.
|
|
56
|
+
const paymentOption: "ONLINE" | "OFFLINE" = !online && inPerson ? "OFFLINE" : "ONLINE";
|
|
57
|
+
return {
|
|
58
|
+
...summary,
|
|
59
|
+
description: raw.description ?? "",
|
|
60
|
+
formId: raw.form?._id ?? null,
|
|
61
|
+
paymentOption,
|
|
62
|
+
cancellationFeeEnabled: raw.bookingPolicy?.cancellationFeePolicy?.enabled === true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** List bookable services (hidden ones filtered), mapped to grid-ready DTOs. */
|
|
67
|
+
export async function fetchServices({ limit = 100 } = {}): Promise<ServiceSummary[]> {
|
|
68
|
+
const res = await services
|
|
69
|
+
.queryServices({ conditionalFields: ["STAFF_MEMBER_DETAILS"] as any })
|
|
70
|
+
.eq("appId", BOOKINGS_APP_ID)
|
|
71
|
+
.limit(limit)
|
|
72
|
+
.find();
|
|
73
|
+
return (res.items ?? []).filter((s: Raw) => !s.hidden).map((s: Raw) => toSummary(s));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Fetch one service by its URL slug (mainSlug.name). Null when not found. */
|
|
77
|
+
export async function fetchServiceBySlug(slug: string): Promise<ServiceDetail | null> {
|
|
78
|
+
const res = await services
|
|
79
|
+
.queryServices({ conditionalFields: ["STAFF_MEMBER_DETAILS"] as any })
|
|
80
|
+
.eq("mainSlug.name", slug)
|
|
81
|
+
.eq("appId", BOOKINGS_APP_ID)
|
|
82
|
+
.limit(1)
|
|
83
|
+
.find();
|
|
84
|
+
const raw = res.items?.[0];
|
|
85
|
+
return raw ? toDetail(raw as Raw) : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Service categories for a filter bar — non-fatal (empty array on failure). */
|
|
89
|
+
export async function fetchBookingCategories(): Promise<BookingCategory[]> {
|
|
90
|
+
try {
|
|
91
|
+
const res = await categories.queryCategories().find();
|
|
92
|
+
return (res.items ?? []).map((c: Raw) => ({ id: c._id ?? "", name: c.name ?? "" })).filter((c) => c.id);
|
|
93
|
+
} catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Bookings DTOs — the serializable shapes every hook, component, and page consumes.
|
|
2
|
+
// Plain JSON: safe as Astro island props or across server/client boundaries. Images are
|
|
3
|
+
// resolved https URLs; every displayable price is a ready formatted string.
|
|
4
|
+
|
|
5
|
+
export type ServiceType = "APPOINTMENT" | "CLASS";
|
|
6
|
+
|
|
7
|
+
/** A service as a listing/grid tile needs it. */
|
|
8
|
+
export interface ServiceSummary {
|
|
9
|
+
id: string;
|
|
10
|
+
slug: string;
|
|
11
|
+
name: string;
|
|
12
|
+
tagLine: string;
|
|
13
|
+
type: ServiceType;
|
|
14
|
+
/** Formatted price (e.g. "€75.00"); "Free" when the service has no fee. */
|
|
15
|
+
price: string;
|
|
16
|
+
free: boolean;
|
|
17
|
+
/** Session length in minutes (appointments; classes may have none). */
|
|
18
|
+
durationMinutes: number | null;
|
|
19
|
+
/** Resolved https URL ("" when the service has no image). */
|
|
20
|
+
imageUrl: string;
|
|
21
|
+
categoryId: string | null;
|
|
22
|
+
/** Bookable staff: id is the resource GUID used for filtering and booking. */
|
|
23
|
+
staff: { id: string; name: string }[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A service as the booking page needs it. */
|
|
27
|
+
export interface ServiceDetail extends ServiceSummary {
|
|
28
|
+
/** Description HTML/text as stored. */
|
|
29
|
+
description: string;
|
|
30
|
+
/** The @wix/forms booking-form id (null → the contact-basics fallback form). */
|
|
31
|
+
formId: string | null;
|
|
32
|
+
/** Derived payment option the booking must send ("ONLINE" | "OFFLINE"). */
|
|
33
|
+
paymentOption: "ONLINE" | "OFFLINE";
|
|
34
|
+
/** True → checkout is always required regardless of price (cancellation-fee policy). */
|
|
35
|
+
cancellationFeeEnabled: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface BookingCategory {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** One bookable time slot, appointment or class. */
|
|
44
|
+
export interface Slot {
|
|
45
|
+
/** Local wall-clock strings "YYYY-MM-DDThh:mm:ss" (no Z). */
|
|
46
|
+
startLocal: string;
|
|
47
|
+
endLocal: string;
|
|
48
|
+
/** "YYYY-MM-DD" — for grouping slots by day. */
|
|
49
|
+
dayKey: string;
|
|
50
|
+
/** Display label, e.g. "9:00 AM". */
|
|
51
|
+
label: string;
|
|
52
|
+
/** APPOINTMENT slots carry their own scheduleId; CLASS slots carry an eventId instead. */
|
|
53
|
+
scheduleId: string | null;
|
|
54
|
+
eventId: string | null;
|
|
55
|
+
/** Staff able to take this slot (may be empty — ANY_RESOURCE still books). */
|
|
56
|
+
staff: { id: string; name: string }[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A booking-form field (flat, from getFormSummary; values are keyed by `target`). */
|
|
60
|
+
export interface BookingFormField {
|
|
61
|
+
target: string;
|
|
62
|
+
label: string;
|
|
63
|
+
type: "STRING" | "EMAIL" | "PHONE" | "NUMBER" | "URL";
|
|
64
|
+
options?: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The outcome of book(): either the browser is being redirected, or it's done. */
|
|
68
|
+
export type BookingResult =
|
|
69
|
+
| { kind: "redirect"; url: string }
|
|
70
|
+
| { kind: "confirmed"; bookingId: string; orderId: string | null };
|