@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,317 @@
|
|
|
1
|
+
// One form as state — schema in, validated submission out, minus the markup. This vertical
|
|
2
|
+
// ships no components: a form is schema-driven (the owner picks the fields), so every form has
|
|
3
|
+
// a different field set and no "contact form" component could ship for it. You render
|
|
4
|
+
// `form.fields`; see references/forms/INSTRUCTIONS.md.
|
|
5
|
+
//
|
|
6
|
+
// SSR-friendly: pass a server-fetched FormDto as `initialForm` (Astro frontmatter) and no
|
|
7
|
+
// client fetch happens; a SPA passes nothing and the hook loads it.
|
|
8
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
|
+
import { getForm } from "../../wix/forms/forms";
|
|
10
|
+
import {
|
|
11
|
+
createSubmission,
|
|
12
|
+
normalizePhone,
|
|
13
|
+
submissionErrors,
|
|
14
|
+
toSubmissionValues,
|
|
15
|
+
uploadFiles,
|
|
16
|
+
} from "../../wix/forms/submissions";
|
|
17
|
+
import type { FormDto, FormErrors, FormFieldDto, FormValues } from "../../wix/forms/types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The key in `errors` for a message that belongs to the FORM rather than one field — a schema
|
|
21
|
+
* that failed to load, or a rejection with no per-field violations. `@` cannot appear in a
|
|
22
|
+
* form `target`, so this never collides with a field's own error.
|
|
23
|
+
*/
|
|
24
|
+
export const FORM_ERROR = "@form";
|
|
25
|
+
|
|
26
|
+
/** The empty form: every field at a value of the shape its control binds to. */
|
|
27
|
+
function defaultValues(fields: FormFieldDto[]): FormValues {
|
|
28
|
+
const values: FormValues = {};
|
|
29
|
+
for (const f of fields) values[f.target] = Array.isArray(f.defaultValue) ? [...f.defaultValue] : f.defaultValue;
|
|
30
|
+
return values;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check one field's value against its own schema. Exported because it is a plain function —
|
|
35
|
+
* usable outside React, and the place to look when a message needs rewording.
|
|
36
|
+
*
|
|
37
|
+
* Every rule comes from the schema, never from a field's NAME. (The classic mistake is keying
|
|
38
|
+
* the email check on `target === "email"`; deriving it from `format` means an owner-added
|
|
39
|
+
* PHONE/URL/length rule is honored with no code change.)
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ A client check LAXER than the server's is worse than none — the visitor then learns about
|
|
42
|
+
* the problem only after a round trip, in the server's wording rather than yours.
|
|
43
|
+
*/
|
|
44
|
+
export function validateValue(field: FormFieldDto, value: unknown): string {
|
|
45
|
+
const v = String(value ?? "").trim();
|
|
46
|
+
const rules = field.validation;
|
|
47
|
+
|
|
48
|
+
if (field.required && !v) return `${field.label} is required.`;
|
|
49
|
+
if (!v) return ""; // optional and empty → fine
|
|
50
|
+
|
|
51
|
+
if (field.control === "number" || field.control === "rating") {
|
|
52
|
+
// The control hands back a string, so parse before comparing: "9" > 10 is false but
|
|
53
|
+
// "9" > "10" is true.
|
|
54
|
+
const n = Number(v);
|
|
55
|
+
if (!Number.isFinite(n)) return `${field.label} must be a number.`;
|
|
56
|
+
if (rules.minimum != null && n < rules.minimum) return `${field.label} must be ${rules.minimum} or more.`;
|
|
57
|
+
if (rules.maximum != null && n > rules.maximum) return `${field.label} must be ${rules.maximum} or less.`;
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (rules.minLength && v.length < rules.minLength)
|
|
62
|
+
return `${field.label} must be at least ${rules.minLength} characters.`;
|
|
63
|
+
if (rules.maxLength && v.length > rules.maxLength)
|
|
64
|
+
return `${field.label} must be at most ${rules.maxLength} characters.`;
|
|
65
|
+
if (rules.format === "EMAIL" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v))
|
|
66
|
+
return "Please enter a valid email address.";
|
|
67
|
+
if (rules.format === "URL" && !/^https?:\/\/.+/.test(v)) return "Please enter a valid URL.";
|
|
68
|
+
// PHONE is E.164 server-side: leading +, country code, digits. Strip formatting first —
|
|
69
|
+
// visitors add spaces, dashes and parens, and rejecting those is a UX bug, not validation.
|
|
70
|
+
if (rules.format === "PHONE" && !/^\+[1-9]\d{6,14}$/.test(normalizePhone(v)))
|
|
71
|
+
return "Use international format, starting with + and the country code.";
|
|
72
|
+
if (rules.pattern && !new RegExp(rules.pattern).test(v))
|
|
73
|
+
return `${field.label} is not in the expected format.`;
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* One field's error entries, keyed the way the controls are named. A plain field yields at most
|
|
79
|
+
* one (`target`); an ADDRESS yields one per failing subfield (`target/sub`).
|
|
80
|
+
*
|
|
81
|
+
* An address subfield gets the `required` check only — `country` and `subdivision` are
|
|
82
|
+
* country-dependent enums the schema does not enumerate, so their content is the server's call.
|
|
83
|
+
*/
|
|
84
|
+
function errorsForField(field: FormFieldDto, values: FormValues): FormErrors {
|
|
85
|
+
const errors: FormErrors = {};
|
|
86
|
+
|
|
87
|
+
if (field.control === "address") {
|
|
88
|
+
const parts = (values[field.target] ?? {}) as Record<string, unknown>;
|
|
89
|
+
for (const { sub, label, required } of field.addressParts) {
|
|
90
|
+
if (required && !String(parts[sub] ?? "").trim()) errors[`${field.target}/${sub}`] = `${label} is required.`;
|
|
91
|
+
}
|
|
92
|
+
return errors;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (field.control === "file" || field.control === "signature") {
|
|
96
|
+
// Files are File objects, which no string rule can judge — check the count instead.
|
|
97
|
+
const picked = ([] as unknown[]).concat(values[field.target] ?? []).filter(Boolean);
|
|
98
|
+
const limit = field.validation.fileLimit;
|
|
99
|
+
if (field.required && !picked.length) errors[field.target] = `${field.label} is required.`;
|
|
100
|
+
else if (limit && picked.length > limit)
|
|
101
|
+
errors[field.target] = `Attach at most ${limit} file${limit === 1 ? "" : "s"}.`;
|
|
102
|
+
return errors;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (field.inputType === "ARRAY") {
|
|
106
|
+
const picked = (Array.isArray(values[field.target]) ? (values[field.target] as unknown[]) : []).filter(Boolean);
|
|
107
|
+
const { minItems, maxItems } = field.validation;
|
|
108
|
+
if (field.required && !picked.length) errors[field.target] = `${field.label} is required.`;
|
|
109
|
+
else if (minItems && picked.length < minItems) errors[field.target] = `Choose at least ${minItems}.`;
|
|
110
|
+
else if (maxItems && picked.length > maxItems) errors[field.target] = `Choose at most ${maxItems}.`;
|
|
111
|
+
return errors;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (field.control === "checkbox") {
|
|
115
|
+
if (field.required && values[field.target] !== true) errors[field.target] = `${field.label} is required.`;
|
|
116
|
+
return errors;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const message = validateValue(field, values[field.target]);
|
|
120
|
+
if (message) errors[field.target] = message;
|
|
121
|
+
return errors;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function errorsForForm(fields: FormFieldDto[], values: FormValues): FormErrors {
|
|
125
|
+
const errors: FormErrors = {};
|
|
126
|
+
for (const field of fields) Object.assign(errors, errorsForField(field, values));
|
|
127
|
+
return errors;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Move focus to a control by input name. ⚠️ `namedItem` returns a RadioNodeList for a radio or
|
|
132
|
+
* checkbox group and an element for everything else — a guard checking only for an element
|
|
133
|
+
* silently skips every choice group.
|
|
134
|
+
*/
|
|
135
|
+
function focusControl(formEl: HTMLFormElement | null, name: string): void {
|
|
136
|
+
const control = formEl?.elements?.namedItem?.(name) as unknown;
|
|
137
|
+
const node =
|
|
138
|
+
typeof RadioNodeList !== "undefined" && control instanceof RadioNodeList
|
|
139
|
+
? (control[0] as HTMLElement | undefined)
|
|
140
|
+
: (control as HTMLElement | undefined);
|
|
141
|
+
node?.focus?.();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface UseWixFormOptions {
|
|
145
|
+
/** Server-fetched form (Astro frontmatter) — skips the client fetch entirely. */
|
|
146
|
+
initialForm?: FormDto;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface UseWixForm {
|
|
150
|
+
/** null while the schema is loading — render a skeleton, not an empty form. */
|
|
151
|
+
form: FormDto | null;
|
|
152
|
+
/** `target` → current value. Arrays for multi-choice and files, objects for an address. */
|
|
153
|
+
values: FormValues;
|
|
154
|
+
setValues: (next: FormValues | ((prev: FormValues) => FormValues)) => void;
|
|
155
|
+
/** Props for a text-ish control, ready to spread: `<input {...bind("email_a1")} />`. */
|
|
156
|
+
bind: (target: string) => {
|
|
157
|
+
name: string;
|
|
158
|
+
value: string;
|
|
159
|
+
onChange: (e: { target: { value: string } }) => void;
|
|
160
|
+
onBlur: () => void;
|
|
161
|
+
"aria-describedby": string;
|
|
162
|
+
"aria-invalid": true | undefined;
|
|
163
|
+
};
|
|
164
|
+
/** `onSubmit`. Resolves TRUE when the submission was created — that IS the success signal. */
|
|
165
|
+
submit: (event?: { preventDefault?: () => void; currentTarget?: unknown }) => Promise<boolean>;
|
|
166
|
+
/** One field, one address subfield, or the whole form when called with nothing. */
|
|
167
|
+
validate: (target?: string) => boolean;
|
|
168
|
+
errors: FormErrors;
|
|
169
|
+
/** Loading the schema, or submitting. */
|
|
170
|
+
loading: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function useWixForm(formId: string, options: UseWixFormOptions = {}): UseWixForm {
|
|
174
|
+
const { initialForm } = options;
|
|
175
|
+
const [form, setForm] = useState<FormDto | null>(initialForm ?? null);
|
|
176
|
+
const [values, setValues] = useState<FormValues>({});
|
|
177
|
+
const [errors, setErrors] = useState<FormErrors>({});
|
|
178
|
+
const [loading, setLoading] = useState(!initialForm);
|
|
179
|
+
// The empty form to reset to after a successful submit — the schema's own defaults.
|
|
180
|
+
const emptyRef = useRef<FormValues>({});
|
|
181
|
+
|
|
182
|
+
const fields = useMemo(() => form?.fields ?? [], [form]);
|
|
183
|
+
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (initialForm) return; // the SSR pass already answered this
|
|
186
|
+
let alive = true;
|
|
187
|
+
if (!formId) {
|
|
188
|
+
setLoading(false);
|
|
189
|
+
setErrors({ [FORM_ERROR]: "No formId — pass one from the seed's forms map." });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
setLoading(true);
|
|
193
|
+
getForm(formId)
|
|
194
|
+
.then((loaded) => {
|
|
195
|
+
if (!alive) return;
|
|
196
|
+
setForm(loaded);
|
|
197
|
+
setErrors({});
|
|
198
|
+
setLoading(false);
|
|
199
|
+
})
|
|
200
|
+
.catch((e: unknown) => {
|
|
201
|
+
if (!alive) return;
|
|
202
|
+
// Fail loudly. A form that cannot load is a setup problem — never fall back to a
|
|
203
|
+
// hand-built form, which would drop real enquiries silently.
|
|
204
|
+
setErrors({ [FORM_ERROR]: e instanceof Error ? e.message : "Could not load the form." });
|
|
205
|
+
setLoading(false);
|
|
206
|
+
});
|
|
207
|
+
return () => {
|
|
208
|
+
alive = false;
|
|
209
|
+
};
|
|
210
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
211
|
+
}, [formId]);
|
|
212
|
+
|
|
213
|
+
// Seed the controls once the schema is in: every control is controlled from the first render,
|
|
214
|
+
// so each target must hold a value of the right shape before any of them mount.
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
const empty = defaultValues(fields);
|
|
217
|
+
emptyRef.current = empty;
|
|
218
|
+
setValues(empty);
|
|
219
|
+
}, [fields]);
|
|
220
|
+
|
|
221
|
+
const validate = useCallback(
|
|
222
|
+
(target?: string) => {
|
|
223
|
+
if (!target) {
|
|
224
|
+
const all = errorsForForm(fields, values);
|
|
225
|
+
setErrors(all);
|
|
226
|
+
return Object.keys(all).length === 0;
|
|
227
|
+
}
|
|
228
|
+
const key = String(target);
|
|
229
|
+
const field = fields.find((f) => f.target === key.split("/")[0]);
|
|
230
|
+
if (!field) return true;
|
|
231
|
+
|
|
232
|
+
// The keys this call owns, so a re-check CLEARS what it fixed as well as flagging what it
|
|
233
|
+
// did not: one key for a plain field, or every subfield when an address itself is named.
|
|
234
|
+
const owned = key.includes("/")
|
|
235
|
+
? [key]
|
|
236
|
+
: field.control === "address"
|
|
237
|
+
? field.addressParts.map(({ sub }) => `${field.target}/${sub}`)
|
|
238
|
+
: [field.target];
|
|
239
|
+
|
|
240
|
+
const found = errorsForField(field, values);
|
|
241
|
+
setErrors((prev) => {
|
|
242
|
+
const next = { ...prev };
|
|
243
|
+
for (const k of owned) {
|
|
244
|
+
delete next[k];
|
|
245
|
+
if (found[k]) next[k] = found[k];
|
|
246
|
+
}
|
|
247
|
+
return next;
|
|
248
|
+
});
|
|
249
|
+
return owned.every((k) => !found[k]);
|
|
250
|
+
},
|
|
251
|
+
[fields, values],
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Covers input / textarea / select. A checkbox or radio group carries `checked` instead of
|
|
255
|
+
// `value`, and a file input cannot be controlled at all — wire those by hand, keeping the
|
|
256
|
+
// same `name`, `onBlur` and aria contract.
|
|
257
|
+
const bind = useCallback(
|
|
258
|
+
(target: string) => ({
|
|
259
|
+
name: target,
|
|
260
|
+
value: String(values[target] ?? ""),
|
|
261
|
+
onChange: (e: { target: { value: string } }) =>
|
|
262
|
+
setValues((prev) => ({ ...prev, [target]: e.target.value })),
|
|
263
|
+
onBlur: () => validate(target),
|
|
264
|
+
"aria-describedby": `err-${target}`,
|
|
265
|
+
"aria-invalid": errors[target] ? (true as const) : undefined,
|
|
266
|
+
}),
|
|
267
|
+
[values, errors, validate],
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const submit = useCallback(
|
|
271
|
+
async (event?: { preventDefault?: () => void; currentTarget?: unknown }) => {
|
|
272
|
+
event?.preventDefault?.();
|
|
273
|
+
// Capture the <form> NOW: React clears currentTarget once the handler returns, so reading
|
|
274
|
+
// it after the await below (to focus a server-rejected control) comes back null.
|
|
275
|
+
const formEl = (event?.currentTarget ?? null) as HTMLFormElement | null;
|
|
276
|
+
if (!form) return false;
|
|
277
|
+
|
|
278
|
+
// Client pass first, so the visitor gets inline feedback in OUR wording before a round trip.
|
|
279
|
+
const clientErrors = errorsForForm(fields, values);
|
|
280
|
+
if (Object.keys(clientErrors).length) {
|
|
281
|
+
setErrors(clientErrors);
|
|
282
|
+
// FOCUS the first invalid control — scrollIntoView moves the viewport and nothing else,
|
|
283
|
+
// leaving a keyboard or screen-reader user where they were.
|
|
284
|
+
focusControl(formEl, Object.keys(clientErrors)[0]);
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
setLoading(true);
|
|
289
|
+
try {
|
|
290
|
+
// Attachments go up FIRST — a File is not something the submission API takes, and its
|
|
291
|
+
// value is the upload URL this hands back. No file fields → nothing happens here.
|
|
292
|
+
const uploaded = await uploadFiles(form.id, fields, values);
|
|
293
|
+
setValues(uploaded); // keep the URLs, so a rejection on another field never re-uploads
|
|
294
|
+
await createSubmission(form.id, toSubmissionValues(fields, uploaded));
|
|
295
|
+
setErrors({});
|
|
296
|
+
setValues(emptyRef.current); // back to the schema's defaults, ready for another
|
|
297
|
+
return true;
|
|
298
|
+
} catch (e) {
|
|
299
|
+
const mapped = submissionErrors(e, fields);
|
|
300
|
+
if (Object.keys(mapped).length) {
|
|
301
|
+
setErrors(mapped);
|
|
302
|
+
focusControl(formEl, Object.keys(mapped)[0]);
|
|
303
|
+
} else {
|
|
304
|
+
setErrors({
|
|
305
|
+
[FORM_ERROR]: e instanceof Error ? e.message : "Could not send the form. Please try again.",
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
309
|
+
} finally {
|
|
310
|
+
setLoading(false);
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
[fields, form, values],
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
return { form, values, setValues, bind, submit, validate, errors, loading };
|
|
317
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// Wix Forms schema reads (@wix/forms `forms`) — the only file that touches a raw Form.
|
|
2
|
+
// Returns flat FormDto / FormFieldDto from ./types. Copy as-is; extend by adding functions.
|
|
3
|
+
//
|
|
4
|
+
// ⚠️ The visitor token is enough, and the spec says otherwise. Every schema read is listed
|
|
5
|
+
// under the owner scope `SCOPE.FORMS.VIEW-FORM`, and returns 200 on an anonymous visitor:
|
|
6
|
+
// Wix grants implicit visitor access so a published site can render its own forms. Do NOT add
|
|
7
|
+
// a backend, a connector token, or auth.elevate to make a form load.
|
|
8
|
+
//
|
|
9
|
+
// docs: https://dev.wix.com/docs/sdk/business-solutions/forms/forms/get-form.md
|
|
10
|
+
// docs: https://dev.wix.com/docs/sdk/business-solutions/forms/forms/list-forms.md
|
|
11
|
+
// docs: https://dev.wix.com/docs/api-reference/crm/forms/form-schemas/about-form-fields.md
|
|
12
|
+
import { forms as formsModule } from "@wix/forms";
|
|
13
|
+
import { wixModule } from "../sdk";
|
|
14
|
+
import type {
|
|
15
|
+
FormAddressPart,
|
|
16
|
+
FormChoice,
|
|
17
|
+
FormControl,
|
|
18
|
+
FormDto,
|
|
19
|
+
FormFieldDto,
|
|
20
|
+
FormValidation,
|
|
21
|
+
} from "./types";
|
|
22
|
+
|
|
23
|
+
const forms = wixModule(formsModule);
|
|
24
|
+
|
|
25
|
+
/** The Wix Forms app namespace. Every form this vertical touches lives here. */
|
|
26
|
+
export const FORMS_NAMESPACE = "wix.form_app.form";
|
|
27
|
+
|
|
28
|
+
type Raw = Record<string, any>;
|
|
29
|
+
|
|
30
|
+
// A field's settings nest TWO levels deep, under blocks named after its own enums:
|
|
31
|
+
// inputOptions.<inputType block>.<componentType block>.label
|
|
32
|
+
// These tables resolve those names. Spelling them at a call site is one typo away from
|
|
33
|
+
// silently reading back no label at all — which is the whole reason this file exists.
|
|
34
|
+
const INPUT_BLOCK: Record<string, string> = {
|
|
35
|
+
STRING: "stringOptions",
|
|
36
|
+
NUMBER: "numberOptions",
|
|
37
|
+
BOOLEAN: "booleanOptions",
|
|
38
|
+
ARRAY: "arrayOptions",
|
|
39
|
+
ADDRESS: "addressOptions",
|
|
40
|
+
WIX_FILE: "wixFileOptions",
|
|
41
|
+
PAYMENT: "paymentOptions",
|
|
42
|
+
SCHEDULING: "schedulingOptions",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const COMPONENT_BLOCK: Record<string, string> = {
|
|
46
|
+
TEXT_INPUT: "textInputOptions",
|
|
47
|
+
NUMBER_INPUT: "numberInputOptions",
|
|
48
|
+
RATING_INPUT: "ratingInputOptions",
|
|
49
|
+
PHONE_INPUT: "phoneInputOptions",
|
|
50
|
+
DATE_INPUT: "dateInputOptions",
|
|
51
|
+
DATE_PICKER: "datePickerOptions",
|
|
52
|
+
DATE_TIME: "dateTimeOptions",
|
|
53
|
+
TIME_INPUT: "timeInputOptions",
|
|
54
|
+
CHECKBOX: "checkboxOptions",
|
|
55
|
+
CHECKBOX_GROUP: "checkboxGroupOptions",
|
|
56
|
+
RADIO_GROUP: "radioGroupOptions",
|
|
57
|
+
DROPDOWN: "dropdownOptions",
|
|
58
|
+
TAGS: "tagsOptions",
|
|
59
|
+
MULTILINE_ADDRESS: "multilineAddressOptions",
|
|
60
|
+
FILE_UPLOAD: "fileUploadOptions",
|
|
61
|
+
SIGNATURE: "signatureOptions",
|
|
62
|
+
FIXED_PAYMENT: "fixedPaymentOptions",
|
|
63
|
+
PAYMENT_INPUT: "paymentInputOptions",
|
|
64
|
+
DONATION_INPUT: "donationInputOptions",
|
|
65
|
+
APPOINTMENT: "appointmentOptions",
|
|
66
|
+
SERVICES_DROPDOWN: "servicesDropdownOptions",
|
|
67
|
+
SERVICES_CHECKBOX_GROUP: "servicesCheckboxGroupOptions",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// componentType → the control to render. Several field kinds share one component (short and
|
|
71
|
+
// long answer are both TEXT_INPUT), so `format` and `inputType` refine it below.
|
|
72
|
+
const CONTROL: Record<string, FormControl> = {
|
|
73
|
+
TEXT_INPUT: "text",
|
|
74
|
+
NUMBER_INPUT: "number",
|
|
75
|
+
RATING_INPUT: "rating",
|
|
76
|
+
PHONE_INPUT: "phone",
|
|
77
|
+
DATE_INPUT: "date",
|
|
78
|
+
DATE_PICKER: "date",
|
|
79
|
+
DATE_TIME: "datetime",
|
|
80
|
+
TIME_INPUT: "time",
|
|
81
|
+
CHECKBOX: "checkbox",
|
|
82
|
+
CHECKBOX_GROUP: "checkboxGroup",
|
|
83
|
+
RADIO_GROUP: "radio",
|
|
84
|
+
DROPDOWN: "select",
|
|
85
|
+
TAGS: "tags",
|
|
86
|
+
MULTILINE_ADDRESS: "address",
|
|
87
|
+
FILE_UPLOAD: "file",
|
|
88
|
+
SIGNATURE: "signature",
|
|
89
|
+
FIXED_PAYMENT: "payment",
|
|
90
|
+
PAYMENT_INPUT: "payment",
|
|
91
|
+
DONATION_INPUT: "payment",
|
|
92
|
+
SERVICES_DROPDOWN: "select",
|
|
93
|
+
SERVICES_CHECKBOX_GROUP: "checkboxGroup",
|
|
94
|
+
APPOINTMENT: "appointment",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// An enum missing from a table means Wix added a type. Say so ONCE — a silent empty block
|
|
98
|
+
// renders a field labelled by its storage key with none of its settings.
|
|
99
|
+
const warned = new Set<string>();
|
|
100
|
+
function blockName(table: Record<string, string>, key: string | undefined, what: string): string | undefined {
|
|
101
|
+
const name = key ? table[key] : undefined;
|
|
102
|
+
if (!name && key && !warned.has(what + key)) {
|
|
103
|
+
warned.add(what + key);
|
|
104
|
+
console.warn(
|
|
105
|
+
`forms: no ${what} block mapped for "${key}" — that field renders without its label or ` +
|
|
106
|
+
`options. Add it to the table in app/wix/forms/forms.ts.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return name;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A BOOLEAN consent field labels itself with Ricos rich content; take its plain text. */
|
|
113
|
+
function plainText(label: unknown): string {
|
|
114
|
+
if (typeof label === "string") return label;
|
|
115
|
+
const nodes = (label as Raw)?.nodes;
|
|
116
|
+
if (!Array.isArray(nodes)) return "";
|
|
117
|
+
const walk = (list: Raw[]): string =>
|
|
118
|
+
list
|
|
119
|
+
.map((n) => (n?.textData?.text ?? "") + (Array.isArray(n?.nodes) ? walk(n.nodes) : ""))
|
|
120
|
+
.join("");
|
|
121
|
+
return walk(nodes).trim();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** `postalCode` → "Postal code". A subfield is a key; the schema carries no label for it. */
|
|
125
|
+
function humanizeSub(sub: string): string {
|
|
126
|
+
const words = sub.replace(/([A-Z])|(\d+)/g, " $1$2").toLowerCase().trim();
|
|
127
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function toField(raw: Raw): FormFieldDto {
|
|
131
|
+
const input = raw.inputOptions ?? {};
|
|
132
|
+
const inputType: string = input.inputType ?? "";
|
|
133
|
+
const optionsBlock: Raw = input[blockName(INPUT_BLOCK, inputType, "inputType") ?? ""] ?? {};
|
|
134
|
+
const componentType: string = optionsBlock.componentType ?? "";
|
|
135
|
+
const component: Raw = optionsBlock[blockName(COMPONENT_BLOCK, componentType, "componentType") ?? ""] ?? {};
|
|
136
|
+
const rules: Raw = optionsBlock.validation ?? {};
|
|
137
|
+
|
|
138
|
+
const validation: FormValidation = {
|
|
139
|
+
...(rules.format ? { format: rules.format } : {}),
|
|
140
|
+
...(rules.minLength != null ? { minLength: rules.minLength } : {}),
|
|
141
|
+
...(rules.maxLength != null ? { maxLength: rules.maxLength } : {}),
|
|
142
|
+
...(rules.pattern ? { pattern: rules.pattern } : {}),
|
|
143
|
+
// JSON-Schema spelling everywhere else in this block; minValue/maxValue is read too since
|
|
144
|
+
// the public reference documents neither and an older schema may carry them.
|
|
145
|
+
...((rules.minimum ?? rules.minValue) != null ? { minimum: rules.minimum ?? rules.minValue } : {}),
|
|
146
|
+
...((rules.maximum ?? rules.maxValue) != null ? { maximum: rules.maximum ?? rules.maxValue } : {}),
|
|
147
|
+
...(rules.fileLimit != null ? { fileLimit: rules.fileLimit } : {}),
|
|
148
|
+
...(rules.minItems != null ? { minItems: rules.minItems } : {}),
|
|
149
|
+
...(rules.maxItems != null ? { maxItems: rules.maxItems } : {}),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// TEXT_INPUT covers short answer, long answer, email, url and phone — and a long answer is
|
|
153
|
+
// NOT flagged on the component (no `multiline`, no `numberOfLines`). Only `identifier` says
|
|
154
|
+
// TEXT_AREA, which is why it is carried on the DTO.
|
|
155
|
+
const identifier: string = raw.identifier ?? "";
|
|
156
|
+
let control: FormControl = CONTROL[componentType] ?? "unknown";
|
|
157
|
+
// A product list is a PAYMENT field wearing a CHECKBOX_GROUP component — typed by its
|
|
158
|
+
// component it would look like a multi-choice and be bound to an array of strings, when what
|
|
159
|
+
// it submits is a payment structure. The input type wins.
|
|
160
|
+
if (inputType === "PAYMENT") control = "payment";
|
|
161
|
+
if (control === "text") {
|
|
162
|
+
if (identifier === "TEXT_AREA" || component.multiline || component.numberOfLines > 1) control = "textarea";
|
|
163
|
+
else if (rules.format === "EMAIL") control = "email";
|
|
164
|
+
else if (rules.format === "URL") control = "url";
|
|
165
|
+
else if (rules.format === "PHONE") control = "phone";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const choices: FormChoice[] = Array.isArray(component.options)
|
|
169
|
+
? component.options.map((o: Raw) => ({ value: String(o.value), label: String(o.label ?? o.value) }))
|
|
170
|
+
: [];
|
|
171
|
+
|
|
172
|
+
const addressParts: FormAddressPart[] = Object.entries((rules.fields ?? {}) as Raw)
|
|
173
|
+
// Only addressLine2 carries a visibility setting; hide it when the owner turned it off.
|
|
174
|
+
.filter(([sub]) => component.fieldSettings?.[sub]?.show !== false)
|
|
175
|
+
.map(([sub, cfg]) => ({ sub, label: humanizeSub(sub), required: (cfg as Raw)?.required ?? false }));
|
|
176
|
+
|
|
177
|
+
const target: string = input.target ?? "";
|
|
178
|
+
const label = plainText(component.label) || target;
|
|
179
|
+
|
|
180
|
+
// Every control is controlled from the first render, so each field starts at a value of the
|
|
181
|
+
// right SHAPE — [] for multi-choice and files, {} for an address, the owner's prefill else "".
|
|
182
|
+
// Wix spells the prefill `default` on most components and `defaultValue` on a rating.
|
|
183
|
+
const prefill = component.default ?? component.defaultValue;
|
|
184
|
+
const defaultValue =
|
|
185
|
+
control === "address" ? {} :
|
|
186
|
+
inputType === "ARRAY" || inputType === "WIX_FILE" ? [] :
|
|
187
|
+
// a consent checkbox is a BOOLEAN: "" would bind to `checked` as false and work by
|
|
188
|
+
// accident, but the DTO promises the shape its control writes back
|
|
189
|
+
inputType === "BOOLEAN" ? ((prefill as boolean | undefined) ?? false) :
|
|
190
|
+
(prefill as string | number | boolean | undefined) ?? "";
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
target,
|
|
194
|
+
label,
|
|
195
|
+
control,
|
|
196
|
+
required: input.required ?? false,
|
|
197
|
+
...(component.placeholder ? { placeholder: String(component.placeholder) } : {}),
|
|
198
|
+
...(component.description ? { description: plainText(component.description) } : {}),
|
|
199
|
+
defaultValue,
|
|
200
|
+
choices,
|
|
201
|
+
addressParts,
|
|
202
|
+
validation,
|
|
203
|
+
...(component.defaultCountryCode || rules.phoneOptions?.allowedCountryCodes?.[0]
|
|
204
|
+
? { phoneCountry: component.defaultCountryCode ?? rules.phoneOptions.allowedCountryCodes[0] }
|
|
205
|
+
: {}),
|
|
206
|
+
identifier,
|
|
207
|
+
inputType,
|
|
208
|
+
componentType,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Display order comes from `steps[].layout`, NOT from `formFields[]` array order. Sort WITHIN
|
|
214
|
+
* each step and concatenate in step order: `row` restarts at 0 in every step, so one sort
|
|
215
|
+
* across the flattened list interleaves them. A field the owner never placed sorts LAST — it
|
|
216
|
+
* still stores values. The submit button is `fieldType: "DISPLAY"`, so filtering to INPUT
|
|
217
|
+
* drops it automatically.
|
|
218
|
+
*/
|
|
219
|
+
function orderedInputs(raw: Raw): Raw[] {
|
|
220
|
+
const order = new Map<string, number>(
|
|
221
|
+
((raw.steps ?? []) as Raw[])
|
|
222
|
+
.flatMap((s) =>
|
|
223
|
+
((s.layout?.large?.items ?? s.layout?.medium?.items ?? s.layout?.small?.items ?? []) as Raw[])
|
|
224
|
+
.slice()
|
|
225
|
+
.sort((a, b) => a.row - b.row || a.column - b.column),
|
|
226
|
+
)
|
|
227
|
+
.map((item, i) => [item.fieldId as string, i] as const),
|
|
228
|
+
);
|
|
229
|
+
return ((raw.formFields ?? []) as Raw[])
|
|
230
|
+
.filter((f) => f.fieldType === "INPUT" && !f.hidden)
|
|
231
|
+
.sort((a, b) => (order.get(a.id) ?? Infinity) - (order.get(b.id) ?? Infinity));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function toForm(raw: Raw): FormDto {
|
|
235
|
+
const submit = ((raw.formFields ?? []) as Raw[]).find((f) => f.identifier === "SUBMIT_BUTTON");
|
|
236
|
+
return {
|
|
237
|
+
id: raw._id ?? raw.id,
|
|
238
|
+
name: raw.name ?? "",
|
|
239
|
+
fields: orderedInputs(raw).map(toField),
|
|
240
|
+
// The submit wording lives on a DISPLAY field, nested under pageNavigationOptions because
|
|
241
|
+
// one control drives both multi-page navigation and the final submit.
|
|
242
|
+
submitText: submit?.displayOptions?.pageNavigationOptions?.submitText ?? "",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Read one form by id. Throws when the id is wrong or the form was deleted — a form that
|
|
248
|
+
* cannot load is a setup problem, so fail loudly rather than rendering a hand-built fallback
|
|
249
|
+
* that would drop real enquiries silently.
|
|
250
|
+
*/
|
|
251
|
+
export async function getForm(formId: string): Promise<FormDto> {
|
|
252
|
+
const raw = (await forms.getForm(formId)) as Raw;
|
|
253
|
+
if (!raw) throw new Error(`forms: form "${formId}" not found.`);
|
|
254
|
+
return toForm(raw);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Every form on the site, in the Wix Forms namespace. Use ONE call for several forms on a page
|
|
259
|
+
* rather than a getForm each.
|
|
260
|
+
*
|
|
261
|
+
* ⚠️ Returns only ENABLED forms — a form the owner disabled vanishes from the listing rather
|
|
262
|
+
* than erroring. That is usually right for a public site.
|
|
263
|
+
*/
|
|
264
|
+
export async function listForms(): Promise<FormDto[]> {
|
|
265
|
+
// The namespace is POSITIONAL — an options object here is a type error, and untyped it would
|
|
266
|
+
// silently list nothing.
|
|
267
|
+
const res = (await forms.listForms(FORMS_NAMESPACE)) as Raw;
|
|
268
|
+
return ((res?.forms ?? []) as Raw[]).map(toForm);
|
|
269
|
+
}
|