@wix/agent-skills 1.15.1 → 1.16.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/README.md +3 -7
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +4 -1
- package/skills/wix-app/references/APP_TOOLS.md +134 -0
- package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +1 -1
- package/skills/wix-app/references/DASHBOARD_PAGE.md +0 -10
- package/skills/wix-app/references/DATA_COLLECTION.md +1 -1
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +28 -9
- package/skills/wix-app/references/SERVICE_PLUGIN.md +12 -10
- package/skills/wix-app/references/app-tools/TOOLS.md +75 -0
- package/skills/wix-app/references/dashboard-page/DASHBOARD_API.md +2 -2
- package/skills/wix-app/references/editor-react-component/A11Y-REVIEW-REFERENCE.md +172 -0
- package/skills/wix-app/references/editor-react-component/A11Y-REVIEW.md +138 -0
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +23 -2
- package/skills/wix-app/references/editor-react-component/BRANDED-COMPONENTS.md +29 -0
- package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +1 -1
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +27 -2
- package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +5 -3
- package/skills/wix-app/references/editor-react-component/DIRECTIONALITY.md +18 -1
- package/skills/wix-app/references/editor-react-component/PARTS.md +15 -2
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +2 -3
- package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +13 -9
- package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +4 -0
- package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +4 -0
- package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +3 -1
- package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +4 -0
- package/skills/wix-app/references/service-plugin/TOOLS_PROVIDER.md +54 -0
- package/skills/wix-app/references/stores/QUERY.md +32 -0
- package/skills/wix-docs/SKILL.md +35 -3
- package/skills/wix-headless/SKILL.md +3 -1
- package/skills/wix-headless/references/CONTENT.md +36 -0
- package/skills/wix-headless/references/DESIGN.md +54 -0
- package/skills/wix-headless/references/DISCOVERY.md +24 -0
- package/skills/wix-headless/references/IMAGE_GENERATION.md +10 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +3 -2
- package/skills/wix-headless/references/inline-recipes/setup-online-store.md +32 -23
- package/skills/wix-headless/references/managed/CONNECT.md +1 -1
- package/skills/wix-headless/references/managed/CREATE.md +14 -0
- package/skills/wix-headless/references/managed/DEPLOYMENT.md +25 -0
- package/skills/wix-manage/SKILL.md +123 -7
- package/skills/wix-manage/references/analytics/analytics-dashboard-navigation.md +41 -0
- package/skills/wix-manage/references/app-installation/app-installation-dashboard-navigation.md +28 -0
- package/skills/wix-manage/references/blog/blog-dashboard-navigation.md +56 -0
- package/skills/wix-manage/references/blog/how-to-create-blog-posts.md +31 -6
- package/skills/wix-manage/references/bookings/booking-service-policy-setup.md +3 -1
- package/skills/wix-manage/references/bookings/bookings-dashboard-navigation.md +63 -0
- package/skills/wix-manage/references/bookings/diagnose-availability-issues.md +10 -1
- package/skills/wix-manage/references/cms/cms-dashboard-navigation.md +44 -0
- package/skills/wix-manage/references/cms/cms-data-items-crud.md +31 -3
- package/skills/wix-manage/references/cms/cms-publishing-flow.md +287 -0
- package/skills/wix-manage/references/cms/cms-schema-management.md +27 -1
- package/skills/wix-manage/references/contacts/contacts-dashboard-navigation.md +46 -0
- package/skills/wix-manage/references/contacts/create-a-contact.md +82 -0
- package/skills/wix-manage/references/contacts/update-a-contact.md +119 -0
- package/skills/wix-manage/references/dashboard-navigation/dashboard-navigation.md +82 -0
- package/skills/wix-manage/references/dashboard-navigation/routes.json +1733 -0
- package/skills/wix-manage/references/domains/domains-dashboard-navigation.md +32 -0
- package/skills/wix-manage/references/ecommerce/ecom-pricing.md +4 -4
- package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
- package/skills/wix-manage/references/ecommerce/gift-cards/ecom-gift-cards-goal-sell-gift-cards.md +338 -0
- package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-create-discount-rule.md +42 -78
- package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-flow-bundle-and-save.md +1 -1
- package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-flow-stock-mover.md +1 -1
- package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-flow-upsell-boost.md +1 -1
- package/skills/wix-manage/references/ecommerce/recommend-ecommerce-strategy.md +90 -92
- package/skills/wix-manage/references/events/create-wix-event.md +176 -0
- package/skills/wix-manage/references/events/manage-wix-events.md +174 -0
- package/skills/wix-manage/references/forms/forms-dashboard-navigation.md +44 -0
- package/skills/wix-manage/references/get-paid/get-paid-dashboard-navigation.md +52 -0
- package/skills/wix-manage/references/google-ads/billing-and-payment.md +80 -0
- package/skills/wix-manage/references/google-ads/create-performance-max-campaign.md +212 -0
- package/skills/wix-manage/references/google-ads/get-campaign-suggestions.md +110 -0
- package/skills/wix-manage/references/google-ads/google-ads-dashboard-navigation.md +25 -0
- package/skills/wix-manage/references/google-ads/install-and-create-account.md +155 -0
- package/skills/wix-manage/references/google-ads/manage-campaign-lifecycle.md +63 -0
- package/skills/wix-manage/references/google-ads/query-campaign-analytics.md +155 -0
- package/skills/wix-manage/references/marketing/marketing-dashboard-navigation.md +52 -0
- package/skills/wix-manage/references/pricing-plans/create-and-update-pricing-plans.md +99 -1
- package/skills/wix-manage/references/pricing-plans/pricing-plans-dashboard-navigation.md +45 -0
- package/skills/wix-manage/references/restaurants/restaurants-dashboard-navigation.md +68 -0
- package/skills/wix-manage/references/restaurants/wix-restaurants-setup.md +94 -125
- package/skills/wix-manage/references/site-properties/change-payment-currency-site-properties.md +48 -9
- package/skills/wix-manage/references/site-properties/site-properties-dashboard-navigation.md +31 -0
- package/skills/wix-manage/references/sites/create-headless-site.md +52 -0
- package/skills/wix-manage/references/sites/create-site-from-template.md +2 -25
- package/skills/wix-manage/references/sites/site-import.md +163 -190
- package/skills/wix-manage/references/sites/sites-dashboard-navigation.md +29 -0
- package/skills/wix-manage/references/stores/add-store-pages-to-site.md +17 -1
- package/skills/wix-manage/references/stores/find-products-query-and-search-catalog-v3.md +26 -8
- package/skills/wix-manage/references/stores/stores-dashboard-navigation.md +65 -0
- package/skills/wix-manage/references/stores/update-product-with-options.md +39 -7
- package/skills/wix-replatform/.publish-manifest.json +8 -0
- package/skills/wix-replatform/SKILL.md +693 -0
- package/skills/wix-replatform/lib/artifact-freshness.js +215 -0
- package/skills/wix-replatform/lib/completion-report.js +200 -0
- package/skills/wix-replatform/lib/config-env.js +59 -0
- package/skills/wix-replatform/lib/execution-manifest.js +185 -0
- package/skills/wix-replatform/lib/execution-state.js +173 -0
- package/skills/wix-replatform/lib/import-recovery.js +313 -0
- package/skills/wix-replatform/lib/local-state.js +642 -0
- package/skills/wix-replatform/lib/orchestration-preflight.js +143 -0
- package/skills/wix-replatform/lib/orchestration-router.js +122 -0
- package/skills/wix-replatform/lib/orchestration-state.js +455 -0
- package/skills/wix-replatform/lib/progress-log.js +224 -0
- package/skills/wix-replatform/lib/progress-log.sh +140 -0
- package/skills/wix-replatform/lib/url-preservation-state.js +391 -0
- package/skills/wix-replatform/resources/rp-discovery/SKILL.md +184 -0
- package/skills/wix-replatform/resources/rp-execute-import/SKILL.md +529 -0
- package/skills/wix-replatform/resources/rp-execute-setup/SKILL.md +316 -0
- package/skills/wix-replatform/resources/rp-execute-setup/scripts/mint-token.sh +64 -0
- package/skills/wix-replatform/resources/rp-import-codegen/SKILL.md +1114 -0
- package/skills/wix-replatform/resources/rp-mapper/SKILL.md +430 -0
- package/skills/wix-replatform/resources/rp-mapper/lib/mapping-resolve.js +217 -0
- package/skills/wix-replatform/resources/rp-mapper/scripts/resolve-mapping.js +148 -0
- package/skills/wix-replatform/resources/rp-mapper/wix-business-solutions-taxonomy.md +378 -0
- package/skills/wix-replatform/resources/rp-setup-discovery/SKILL.md +183 -0
- package/skills/wix-replatform/resources/rp-source-csv/SKILL.md +478 -0
- package/skills/wix-replatform/resources/rp-source-csv/lib/canonical-record.js +136 -0
- package/skills/wix-replatform/resources/rp-source-csv/lib/csv-fileset.js +0 -0
- package/skills/wix-replatform/resources/rp-source-csv/lib/csv-fingerprint.js +310 -0
- package/skills/wix-replatform/resources/rp-source-csv/lib/csv-layout.js +1081 -0
- package/skills/wix-replatform/resources/rp-source-csv/lib/csv-parse.js +618 -0
- package/skills/wix-replatform/resources/rp-source-csv/scripts/csv-discovery.js +949 -0
- package/skills/wix-replatform/resources/rp-source-csv/source-schema.example.json +196 -0
- package/skills/wix-replatform/resources/rp-source-csv/vendors/bigcommerce.json +66 -0
- package/skills/wix-replatform/resources/rp-source-csv/vendors/magento.json +60 -0
- package/skills/wix-replatform/resources/rp-source-csv/vendors/shopify.json +95 -0
- package/skills/wix-replatform/resources/rp-source-csv/vendors/woocommerce.json +82 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/SKILL.md +256 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-http.js +249 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-route-classifier.js +444 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/scripts/wp-discovery.js +774 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/source-schema.example.json +71 -0
- package/skills/wix-replatform/resources/rp-target-wix/SKILL.md +632 -0
- package/skills/wix-replatform/resources/rp-target-wix/SPEC-product-reviews-and-blog-authorship.md +330 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/README.md +21 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/domain.json +19 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/category.json +37 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/comment.json +37 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/post.json +39 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/tag.json +35 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/cms/domain.json +14 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/collection.json +35 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/data-item.json +36 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/reference.json +35 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/crm/domain.json +14 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/contact.json +131 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/email-subscription.json +37 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/label.json +37 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/domain.json +21 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-line-item.json +37 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-refund.json +37 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order.json +97 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/index.json +528 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/media/domain.json +14 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/media/entities/media-file.json +38 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/media/entities/media-folder.json +37 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/domain.json +13 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/badge.json +37 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/member.json +53 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/schema.json +84 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/domain.json +25 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/category.json +37 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/coupon.json +37 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/inventory-item.json +38 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/product-tag.json +37 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/product.json +90 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/audit-summary.js +64 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/contract-ledger.js +141 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/domain-knowledge.js +384 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/ndjson.js +260 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/stores-verification.js +238 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/wix-build.js +237 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/wix-target-spec.js +238 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/wix-writers.js +2219 -0
- package/skills/wix-replatform/resources/rp-target-wix/scripts/domain-knowledge-validate.js +31 -0
- package/skills/wix-replatform/resources/rp-target-wix/scripts/domain-knowledge.js +100 -0
- package/skills/wix-replatform/resources/rp-target-wix/scripts/verify-stores.js +107 -0
- package/skills/wix-replatform/resources/rp-telemetry/SKILL.md +344 -0
- package/skills/wix-replatform/resources/rp-telemetry/lib/bi-sink.js +250 -0
- package/skills/wix-replatform/resources/rp-telemetry/lib/telemetry-recorder.js +1927 -0
- package/skills/wix-replatform/resources/rp-telemetry/scripts/meter-from-audit.js +83 -0
- package/skills/wix-replatform/resources/rp-telemetry/scripts/rp-telemetry.js +141 -0
- package/skills/wix-replatform/scripts/artifact-freshness.js +76 -0
- package/skills/wix-replatform/scripts/execution-manifest-validate.js +43 -0
- package/skills/wix-replatform/scripts/execution-state-prepare.js +128 -0
- package/skills/wix-replatform/scripts/orchestration-preflight.js +48 -0
- package/skills/wix-replatform/scripts/orchestration-route.js +46 -0
- package/skills/wix-replatform/scripts/orchestration-state.js +245 -0
- package/skills/wix-vibe-headless/SKILL.md +48 -25
- package/skills/wix-vibe-headless/install/deploy.cjs +95 -0
- package/skills/wix-vibe-headless/install/pin-agents-md.cjs +34 -0
- package/skills/wix-vibe-headless/platforms/README.md +8 -4
- package/skills/wix-vibe-headless/platforms/base44.md +133 -98
- package/skills/wix-vibe-headless/platforms/generic.md +99 -64
- package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +230 -119
- package/skills/wix-vibe-headless/references/blog/app/components/PostCard.jsx +43 -0
- package/skills/wix-vibe-headless/references/blog/app/components/PostChips.jsx +25 -0
- package/skills/wix-vibe-headless/references/blog/app/components/PostGrid.jsx +15 -0
- package/skills/wix-vibe-headless/references/blog/app/context/TaxonomyContext.jsx +35 -0
- package/skills/wix-vibe-headless/references/blog/app/hooks/usePostDetail.js +38 -0
- package/skills/wix-vibe-headless/references/blog/app/pages/Blog.jsx +40 -0
- package/skills/wix-vibe-headless/references/blog/app/pages/CategoryPage.jsx +54 -0
- package/skills/wix-vibe-headless/references/blog/app/pages/PostDetail.jsx +65 -0
- package/skills/wix-vibe-headless/references/blog/app/pages/TagPage.jsx +50 -0
- package/skills/wix-vibe-headless/references/blog/seed/SEED.md +103 -0
- package/skills/wix-vibe-headless/references/blog/seed/seed-blog.js +270 -0
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +260 -149
- package/skills/wix-vibe-headless/references/bookings/app/components/BookingForm.jsx +65 -0
- package/skills/wix-vibe-headless/references/bookings/app/components/ServiceCard.jsx +47 -0
- package/skills/wix-vibe-headless/references/bookings/app/components/ServiceGrid.jsx +13 -0
- package/skills/wix-vibe-headless/references/bookings/app/components/SlotPicker.jsx +126 -0
- package/skills/wix-vibe-headless/references/bookings/app/hooks/useServiceDetail.js +108 -0
- package/skills/wix-vibe-headless/references/bookings/app/hooks/useServices.js +58 -0
- package/skills/wix-vibe-headless/references/bookings/app/lib/serviceFacts.js +95 -0
- package/skills/wix-vibe-headless/references/bookings/app/pages/ServiceDetail.jsx +94 -0
- package/skills/wix-vibe-headless/references/bookings/app/pages/Services.jsx +89 -0
- package/skills/wix-vibe-headless/references/bookings/{wix-bookings-checkout.js → app/rest/wix-bookings-checkout.js} +1 -1
- package/skills/wix-vibe-headless/references/bookings/{wix-bookings-services.js → app/rest/wix-bookings-services.js} +87 -11
- package/skills/wix-vibe-headless/references/bookings/seed/SEED.md +117 -0
- package/skills/wix-vibe-headless/references/bookings/seed/seed-bookings.js +333 -0
- package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +169 -147
- package/skills/wix-vibe-headless/references/cms/app/collection.config.js +31 -0
- package/skills/wix-vibe-headless/references/cms/app/lib/wixImage.js +18 -0
- package/skills/wix-vibe-headless/references/cms/{wix-cms.js → app/rest/wix-cms.js} +47 -2
- package/skills/wix-vibe-headless/references/cms/seed/SEED.md +145 -0
- package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +228 -131
- package/skills/wix-vibe-headless/references/events/app/components/CategoryFilter.jsx +23 -0
- package/skills/wix-vibe-headless/references/events/app/components/EventCard.jsx +43 -0
- package/skills/wix-vibe-headless/references/events/app/components/EventGrid.jsx +13 -0
- package/skills/wix-vibe-headless/references/events/app/components/EventRegistration.jsx +29 -0
- package/skills/wix-vibe-headless/references/events/app/components/RsvpForm.jsx +57 -0
- package/skills/wix-vibe-headless/references/events/app/components/TicketPicker.jsx +78 -0
- package/skills/wix-vibe-headless/references/events/app/hooks/useEventDetail.js +27 -0
- package/skills/wix-vibe-headless/references/events/app/hooks/useEventsList.js +54 -0
- package/skills/wix-vibe-headless/references/events/app/hooks/useRsvpForm.js +40 -0
- package/skills/wix-vibe-headless/references/events/app/hooks/useTicketing.js +81 -0
- package/skills/wix-vibe-headless/references/events/app/pages/EventDetail.jsx +48 -0
- package/skills/wix-vibe-headless/references/events/app/pages/Events.jsx +36 -0
- package/skills/wix-vibe-headless/references/events/{wix-events-browse.js → app/rest/wix-events-browse.js} +1 -1
- package/skills/wix-vibe-headless/references/events/seed/SEED.md +107 -0
- package/skills/wix-vibe-headless/references/events/seed/seed-events.js +251 -0
- package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +276 -188
- package/skills/wix-vibe-headless/references/members/app/components/LoginForm.jsx +101 -0
- package/skills/wix-vibe-headless/references/members/app/components/MemberMenu.jsx +33 -0
- package/skills/wix-vibe-headless/references/members/app/components/RequireAuth.jsx +21 -0
- package/skills/wix-vibe-headless/references/members/app/components/SocialButtons.jsx +29 -0
- package/skills/wix-vibe-headless/references/members/app/context/MemberContext.jsx +54 -0
- package/skills/wix-vibe-headless/references/members/app/hooks/useLoginForm.js +77 -0
- package/skills/wix-vibe-headless/references/members/app/pages/Account.jsx +36 -0
- package/skills/wix-vibe-headless/references/members/app/pages/Callback.jsx +32 -0
- package/skills/wix-vibe-headless/references/members/app/pages/Login.jsx +35 -0
- package/skills/wix-vibe-headless/references/members/{wix-members-auth.js → app/rest/wix-members-auth.js} +6 -3
- package/skills/wix-vibe-headless/references/members/install/Register.jsx +22 -0
- package/skills/wix-vibe-headless/references/members/seed/SEED.md +22 -0
- package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +233 -104
- package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionCard.jsx +31 -0
- package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionGrid.jsx +15 -0
- package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectCard.jsx +39 -0
- package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectGrid.jsx +15 -0
- package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectMedia.jsx +29 -0
- package/skills/wix-vibe-headless/references/portfolio/app/hooks/useCollectionProjects.js +35 -0
- package/skills/wix-vibe-headless/references/portfolio/app/hooks/usePortfolioGallery.js +27 -0
- package/skills/wix-vibe-headless/references/portfolio/app/hooks/useProjectDetail.js +25 -0
- package/skills/wix-vibe-headless/references/portfolio/app/pages/CollectionPage.jsx +39 -0
- package/skills/wix-vibe-headless/references/portfolio/app/pages/Portfolio.jsx +27 -0
- package/skills/wix-vibe-headless/references/portfolio/app/pages/ProjectDetail.jsx +60 -0
- package/skills/wix-vibe-headless/references/portfolio/seed/SEED.md +94 -0
- package/skills/wix-vibe-headless/references/portfolio/seed/seed-portfolio.js +236 -0
- package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +240 -117
- package/skills/wix-vibe-headless/references/pricing-plans/app/components/OrderCard.jsx +35 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanCard.jsx +48 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanGrid.jsx +16 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanPrice.jsx +35 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/hooks/useMyPlans.js +25 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/hooks/usePlanDetail.js +28 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/hooks/usePlans.js +30 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/pages/MyPlans.jsx +30 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/pages/PlanDetail.jsx +53 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/pages/Plans.jsx +28 -0
- package/skills/wix-vibe-headless/references/pricing-plans/seed/SEED.md +86 -0
- package/skills/wix-vibe-headless/references/pricing-plans/seed/seed-pricing-plans.js +251 -0
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +248 -124
- package/skills/wix-vibe-headless/references/restaurants/app/components/ItemDialog.jsx +81 -0
- package/skills/wix-vibe-headless/references/restaurants/app/components/MenuItemCard.jsx +71 -0
- package/skills/wix-vibe-headless/references/restaurants/app/components/MenuList.jsx +43 -0
- package/skills/wix-vibe-headless/references/restaurants/app/components/OrderCartButton.jsx +21 -0
- package/skills/wix-vibe-headless/references/restaurants/app/components/OrderCartDrawer.jsx +60 -0
- package/skills/wix-vibe-headless/references/restaurants/app/context/OrderCartContext.jsx +63 -0
- package/skills/wix-vibe-headless/references/restaurants/app/hooks/useItemOrder.js +32 -0
- package/skills/wix-vibe-headless/references/restaurants/app/hooks/useReservation.js +73 -0
- package/skills/wix-vibe-headless/references/restaurants/app/pages/Menu.jsx +28 -0
- package/skills/wix-vibe-headless/references/restaurants/app/pages/Reservations.jsx +102 -0
- package/skills/wix-vibe-headless/references/restaurants/seed/SEED.md +180 -0
- package/skills/wix-vibe-headless/references/restaurants/seed/seed-restaurants.js +452 -0
- package/skills/wix-vibe-headless/references/shared/app/components/WixManageBanner.jsx +56 -0
- package/skills/wix-vibe-headless/references/shared/{wix-client.js → app/rest/wix-client.js} +6 -8
- package/skills/wix-vibe-headless/references/shared/app/rest/wix-config.js +6 -0
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +263 -157
- package/skills/wix-vibe-headless/references/storefront/app/components/CartButton.jsx +20 -0
- package/skills/wix-vibe-headless/references/storefront/app/components/CartDrawer.jsx +150 -0
- package/skills/wix-vibe-headless/references/storefront/app/components/ProductCard.jsx +167 -0
- package/skills/wix-vibe-headless/references/storefront/app/components/ProductGallery.jsx +51 -0
- package/skills/wix-vibe-headless/references/storefront/app/components/ProductGrid.jsx +54 -0
- package/skills/wix-vibe-headless/references/storefront/app/components/VariantPicker.jsx +119 -0
- package/skills/wix-vibe-headless/references/storefront/app/context/CartContext.jsx +71 -0
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductDetail.js +109 -0
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useShop.js +98 -0
- package/skills/wix-vibe-headless/references/storefront/app/lib/storeImage.js +27 -0
- package/skills/wix-vibe-headless/references/storefront/app/pages/ProductDetail.jsx +139 -0
- package/skills/wix-vibe-headless/references/storefront/app/pages/Shop.jsx +75 -0
- package/skills/wix-vibe-headless/references/storefront/{wix-store-cart.js → app/rest/wix-store-cart.js} +1 -1
- package/skills/wix-vibe-headless/references/storefront/{wix-store-catalog.js → app/rest/wix-store-catalog.js} +20 -1
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +111 -0
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.js +299 -0
- /package/skills/wix-vibe-headless/references/blog/{wix-blog.js → app/rest/wix-blog.js} +0 -0
- /package/skills/wix-vibe-headless/references/events/{wix-events-registration.js → app/rest/wix-events-registration.js} +0 -0
- /package/skills/wix-vibe-headless/references/portfolio/{wix-portfolio.js → app/rest/wix-portfolio.js} +0 -0
- /package/skills/wix-vibe-headless/references/pricing-plans/{wix-pricing-plans.js → app/rest/wix-pricing-plans.js} +0 -0
- /package/skills/wix-vibe-headless/references/restaurants/{wix-restaurants-menu.js → app/rest/wix-restaurants-menu.js} +0 -0
- /package/skills/wix-vibe-headless/references/restaurants/{wix-restaurants-ordering.js → app/rest/wix-restaurants-ordering.js} +0 -0
- /package/skills/wix-vibe-headless/references/restaurants/{wix-restaurants-reservations.js → app/rest/wix-restaurants-reservations.js} +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Bookable-slot picker — pure UI. Slots, paging cursor, and the pick handler come from
|
|
2
|
+
// useServiceDetail (all the slot data wiring lives in that hook).
|
|
3
|
+
//
|
|
4
|
+
// One day at a time: a horizontal strip of the days that HAVE availability, then that day's times
|
|
5
|
+
// bucketed into morning / afternoon / evening. A fortnight of a busy calendar is well over a hundred
|
|
6
|
+
// slots, so listing every day at once buries the booking form and reads as a wall of numbers.
|
|
7
|
+
// Each button's key combines the start time with its schedule/event id so appointment and class slots
|
|
8
|
+
// stay distinct. Styled with base44 design tokens (shadcn Tailwind classes).
|
|
9
|
+
import { useEffect, useState } from "react";
|
|
10
|
+
|
|
11
|
+
const dayCls = "shrink-0 w-[68px] py-2 px-1 cursor-pointer text-center border rounded-sm transition-colors";
|
|
12
|
+
const dayIdle = "border-border bg-card text-foreground hover:border-foreground/40";
|
|
13
|
+
const dayActive = "border-primary bg-primary text-primary-foreground";
|
|
14
|
+
const timeCls = "py-2.5 px-2 cursor-pointer text-sm font-body border rounded-sm transition-colors";
|
|
15
|
+
const timeIdle = "border-border bg-card text-foreground hover:border-foreground/40";
|
|
16
|
+
const timeActive = "border-primary bg-primary text-primary-foreground";
|
|
17
|
+
|
|
18
|
+
// localStartDate is "YYYY-MM-DDThh:mm:ss" (no zone), so the first 10 chars are the day key and
|
|
19
|
+
// chars 11–13 are the local hour — no Date parsing needed to bucket by time of day.
|
|
20
|
+
const dayKey = (slot) => slot.localStartDate.slice(0, 10);
|
|
21
|
+
const hourOf = (slot) => Number(slot.localStartDate.slice(11, 13));
|
|
22
|
+
|
|
23
|
+
const BUCKETS = [
|
|
24
|
+
{ label: "Morning", test: (h) => h < 12 },
|
|
25
|
+
{ label: "Afternoon", test: (h) => h >= 12 && h < 17 },
|
|
26
|
+
{ label: "Evening", test: (h) => h >= 17 },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function groupByDay(slots) {
|
|
30
|
+
const days = new Map();
|
|
31
|
+
for (const slot of slots) {
|
|
32
|
+
const key = dayKey(slot);
|
|
33
|
+
if (!days.has(key)) days.set(key, []);
|
|
34
|
+
days.get(key).push(slot);
|
|
35
|
+
}
|
|
36
|
+
return [...days.entries()].map(([key, daySlots]) => ({ key, slots: daySlots }));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const fmtDay = (key, opts) => new Date(`${key}T00:00:00`).toLocaleDateString(undefined, opts);
|
|
40
|
+
const fmtTime = (slot) =>
|
|
41
|
+
new Date(slot.localStartDate).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
|
|
42
|
+
|
|
43
|
+
export default function SlotPicker({ slots, cursor, onLoadMore, selectedSlot, onPick }) {
|
|
44
|
+
const days = groupByDay(slots || []);
|
|
45
|
+
const [activeDay, setActiveDay] = useState(null);
|
|
46
|
+
|
|
47
|
+
// Follow the data: land on the first day with availability, and don't strand the view on a day that
|
|
48
|
+
// disappeared when a later page loaded.
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (days.length && !days.some((d) => d.key === activeDay)) setActiveDay(days[0].key);
|
|
51
|
+
}, [days, activeDay]);
|
|
52
|
+
|
|
53
|
+
if (!slots?.length) {
|
|
54
|
+
return <p className="text-muted-foreground">No available times in this range — try later.</p>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const current = days.find((d) => d.key === activeDay) ?? days[0];
|
|
58
|
+
const buckets = BUCKETS
|
|
59
|
+
.map((b) => ({ label: b.label, slots: current.slots.filter((s) => b.test(hourOf(s))) }))
|
|
60
|
+
.filter((b) => b.slots.length);
|
|
61
|
+
|
|
62
|
+
const pickDay = (key) => {
|
|
63
|
+
setActiveDay(key);
|
|
64
|
+
// Drop a time chosen on another day — otherwise the CTA names a time that isn't on screen.
|
|
65
|
+
if (selectedSlot && dayKey(selectedSlot) !== key) onPick(null);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="flex flex-col gap-5">
|
|
70
|
+
<div className="flex gap-2 overflow-x-auto" role="group" aria-label="Choose a day">
|
|
71
|
+
{days.map((d) => {
|
|
72
|
+
const active = d.key === current.key;
|
|
73
|
+
return (
|
|
74
|
+
<button key={d.key} type="button" aria-pressed={active} onClick={() => pickDay(d.key)}
|
|
75
|
+
className={`${dayCls} ${active ? dayActive : dayIdle}`}>
|
|
76
|
+
<span className="block text-[10px] uppercase tracking-wide opacity-70">
|
|
77
|
+
{fmtDay(d.key, { weekday: "short" })}
|
|
78
|
+
</span>
|
|
79
|
+
<span className="block text-lg font-semibold leading-tight">
|
|
80
|
+
{fmtDay(d.key, { day: "numeric" })}
|
|
81
|
+
</span>
|
|
82
|
+
</button>
|
|
83
|
+
);
|
|
84
|
+
})}
|
|
85
|
+
{/* Later dates are another page, not more of this one — fetch on demand from the strip's end. */}
|
|
86
|
+
{cursor && (
|
|
87
|
+
<button type="button" onClick={onLoadMore}
|
|
88
|
+
className={`${dayCls} ${dayIdle} text-[11px] leading-tight`}>
|
|
89
|
+
More<br />dates →
|
|
90
|
+
</button>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div>
|
|
95
|
+
<div className="flex flex-wrap items-baseline justify-between gap-2 mb-3">
|
|
96
|
+
<h3 className="m-0 font-display text-base">
|
|
97
|
+
{fmtDay(current.key, { weekday: "long", day: "numeric", month: "short" })}
|
|
98
|
+
</h3>
|
|
99
|
+
<span className="text-[13px] text-muted-foreground">
|
|
100
|
+
{current.slots.length} {current.slots.length === 1 ? "time" : "times"} free
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div className="flex flex-col gap-4">
|
|
105
|
+
{buckets.map((b) => (
|
|
106
|
+
<div key={b.label}>
|
|
107
|
+
<p className="m-0 mb-2 text-[11px] uppercase tracking-[0.12em] text-muted-foreground">{b.label}</p>
|
|
108
|
+
<div className="grid grid-cols-3 gap-2">
|
|
109
|
+
{b.slots.map((slot) => {
|
|
110
|
+
const active = selectedSlot?.localStartDate === slot.localStartDate;
|
|
111
|
+
return (
|
|
112
|
+
<button key={`${slot.localStartDate}-${slot.scheduleId || slot.eventInfo?.eventId}`}
|
|
113
|
+
type="button" aria-pressed={active} onClick={() => onPick(slot)}
|
|
114
|
+
className={`${timeCls} ${active ? timeActive : timeIdle}`}>
|
|
115
|
+
{fmtTime(slot)}
|
|
116
|
+
</button>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// useServiceDetail — all detail + booking-flow logic, no markup: load a service by id, list its
|
|
2
|
+
// bookable slots, hold the buyer's slot/contact/participant selections, then re-validate and book +
|
|
3
|
+
// check out in one step. The data paths here are the bug-prone part — keep them verbatim; the
|
|
4
|
+
// ServiceDetail page and the SlotPicker/BookingForm components only render what this returns.
|
|
5
|
+
import { useState, useEffect, useMemo, useCallback } from "react";
|
|
6
|
+
import { getService, listSlotsForService, getAvailableSlot } from "@/rest/wix-bookings-services";
|
|
7
|
+
import { bookAndCheckout } from "@/rest/wix-bookings-checkout";
|
|
8
|
+
import {
|
|
9
|
+
serviceCapacityLabel, serviceDuration, serviceLocationLabel, servicePriceLabel, serviceRescheduleNote,
|
|
10
|
+
} from "@/lib/serviceFacts";
|
|
11
|
+
|
|
12
|
+
// Local wall-clock "YYYY-MM-DDThh:mm:ss" (NO zone / Z) — the slot APIs interpret it in the
|
|
13
|
+
// visitor's timeZone. Sending a UTC `Z` timestamp to the slot APIs is wrong.
|
|
14
|
+
function localMidnight(daysFromNow = 0) {
|
|
15
|
+
const d = new Date();
|
|
16
|
+
d.setDate(d.getDate() + daysFromNow);
|
|
17
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
18
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T00:00:00`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useServiceDetail(serviceId) {
|
|
22
|
+
const [service, setService] = useState(null);
|
|
23
|
+
const [notFound, setNotFound] = useState(false);
|
|
24
|
+
const [slots, setSlots] = useState([]);
|
|
25
|
+
const [cursor, setCursor] = useState(null);
|
|
26
|
+
const [timeZone, setTimeZone] = useState(null);
|
|
27
|
+
const [selectedSlot, setSelectedSlot] = useState(null);
|
|
28
|
+
const [contact, setContact] = useState({ firstName: "", lastName: "", email: "", phone: "" });
|
|
29
|
+
const [participants, setParticipants] = useState(1);
|
|
30
|
+
const [submitting, setSubmitting] = useState(false);
|
|
31
|
+
const [error, setError] = useState(null);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
// getService returns null on a miss — show not-found, never invent a service.
|
|
35
|
+
getService(serviceId).then((s) => (s ? setService(s) : setNotFound(true)));
|
|
36
|
+
}, [serviceId]);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!service) return;
|
|
40
|
+
// listSlotsForService routes by service.type (APPOINTMENT → working hours, CLASS → sessions) and
|
|
41
|
+
// returns { slots, nextCursor }. The LIST call takes fromLocalDate / toLocalDate — a different
|
|
42
|
+
// arg naming than the single-slot re-validate call (getAvailableSlot: localStartDate/localEndDate).
|
|
43
|
+
listSlotsForService(service, { fromLocalDate: localMidnight(0), toLocalDate: localMidnight(14) })
|
|
44
|
+
.then(({ slots, nextCursor, timeZone }) => { setSlots(slots); setCursor(nextCursor); setTimeZone(timeZone); });
|
|
45
|
+
}, [service]);
|
|
46
|
+
|
|
47
|
+
const loadMoreSlots = useCallback(() => {
|
|
48
|
+
if (!service || !cursor) return;
|
|
49
|
+
listSlotsForService(service, { cursor }).then(({ slots: more, nextCursor }) => {
|
|
50
|
+
setSlots((s) => [...s, ...more]);
|
|
51
|
+
setCursor(nextCursor);
|
|
52
|
+
});
|
|
53
|
+
}, [service, cursor]);
|
|
54
|
+
|
|
55
|
+
// maxParticipantsPerBooking is the per-booking cap (commonly 1 → no selector; book exactly one).
|
|
56
|
+
// Never use slot.remainingCapacity as the per-buyer limit — a count above the policy makes
|
|
57
|
+
// createBooking fail.
|
|
58
|
+
const maxParticipants = service?.bookingPolicy?.participantsPolicy?.maxParticipantsPerBooking ?? 1;
|
|
59
|
+
|
|
60
|
+
const facts = useMemo(() => ({
|
|
61
|
+
price: servicePriceLabel(service),
|
|
62
|
+
minutes: serviceDuration(service),
|
|
63
|
+
capacity: serviceCapacityLabel(service),
|
|
64
|
+
location: serviceLocationLabel(service),
|
|
65
|
+
rescheduleNote: serviceRescheduleNote(service),
|
|
66
|
+
// Manual approval means the business confirms afterwards, so the CTA must not promise a booking.
|
|
67
|
+
needsApproval: service?.onlineBooking?.requireManualApproval === true,
|
|
68
|
+
}), [service]);
|
|
69
|
+
|
|
70
|
+
const setContactField = (k) => (e) => setContact((c) => ({ ...c, [k]: e.target.value }));
|
|
71
|
+
const canSubmit = Boolean(selectedSlot && contact.email && !submitting);
|
|
72
|
+
|
|
73
|
+
const submit = useCallback(async () => {
|
|
74
|
+
if (!selectedSlot || !service) return;
|
|
75
|
+
setSubmitting(true);
|
|
76
|
+
setError(null);
|
|
77
|
+
try {
|
|
78
|
+
// Re-validate right before booking — slots get taken. getAvailableSlot returns the slot or NULL
|
|
79
|
+
// (guard it); the single-slot call uses localStartDate / localEndDate (not from/toLocalDate).
|
|
80
|
+
const fresh = await getAvailableSlot(service.id, {
|
|
81
|
+
localStartDate: selectedSlot.localStartDate,
|
|
82
|
+
localEndDate: selectedSlot.localEndDate,
|
|
83
|
+
});
|
|
84
|
+
if (!fresh) {
|
|
85
|
+
setSelectedSlot(null);
|
|
86
|
+
setError("That time was just taken — please pick another.");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// bookAndCheckout = createBooking(slot, contact, options) then checkoutBooking; it returns
|
|
90
|
+
// { booking, checkoutUrl } — redirect straight to the hosted checkout, never a hand-built URL.
|
|
91
|
+
const { checkoutUrl } = await bookAndCheckout(fresh, contact, { totalParticipants: participants });
|
|
92
|
+
window.location.href = checkoutUrl;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
setError(e.message);
|
|
95
|
+
} finally {
|
|
96
|
+
setSubmitting(false);
|
|
97
|
+
}
|
|
98
|
+
}, [service, selectedSlot, contact, participants]);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
service, notFound, facts, timeZone,
|
|
102
|
+
slots, cursor, loadMoreSlots,
|
|
103
|
+
selectedSlot, pickSlot: setSelectedSlot,
|
|
104
|
+
contact, setContactField,
|
|
105
|
+
maxParticipants, participants, setParticipants,
|
|
106
|
+
submitting, error, canSubmit, submit,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// useServices — the services-list data, no markup: load one page of visitor-visible services, page
|
|
2
|
+
// with the returned offset, and expose the categories those services belong to so the page can offer
|
|
3
|
+
// a filter. queryServices returns an OBJECT ({ services, total, nextOffset }), NOT a bare array —
|
|
4
|
+
// destructure it (calling .map on the object throws). `total === 0` is the empty-state signal;
|
|
5
|
+
// `services === null` means still loading.
|
|
6
|
+
//
|
|
7
|
+
// The category menu is built from the first unfiltered page, so it lists only categories that have
|
|
8
|
+
// something bookable in them. Switching category re-queries server-side rather than filtering the
|
|
9
|
+
// loaded page, so a category whose services sit on page 2 still fills in.
|
|
10
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
11
|
+
import { categoriesOf, queryServices, queryServicesByCategory } from "@/rest/wix-bookings-services";
|
|
12
|
+
|
|
13
|
+
export function useServices({ limit = 24 } = {}) {
|
|
14
|
+
const [services, setServices] = useState(null);
|
|
15
|
+
const [total, setTotal] = useState(null);
|
|
16
|
+
const [nextOffset, setNextOffset] = useState(null);
|
|
17
|
+
const [allCategories, setAllCategories] = useState([]);
|
|
18
|
+
const [activeCategory, setActiveCategory] = useState(null); // null = every service
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
|
|
21
|
+
const load = useCallback(async (categoryId) => {
|
|
22
|
+
setServices(null);
|
|
23
|
+
setError(null);
|
|
24
|
+
try {
|
|
25
|
+
const res = categoryId
|
|
26
|
+
? await queryServicesByCategory(categoryId, { limit })
|
|
27
|
+
: await queryServices({ limit });
|
|
28
|
+
setServices(res.services);
|
|
29
|
+
setTotal(res.total);
|
|
30
|
+
setNextOffset(res.nextOffset);
|
|
31
|
+
// Only the unfiltered page can describe the whole menu; a filtered one lists one category.
|
|
32
|
+
if (!categoryId) setAllCategories(categoriesOf(res.services));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
setError(e?.message || "Couldn't load services.");
|
|
35
|
+
setServices([]);
|
|
36
|
+
}
|
|
37
|
+
}, [limit]);
|
|
38
|
+
|
|
39
|
+
useEffect(() => { load(activeCategory?.id ?? null); }, [activeCategory, load]);
|
|
40
|
+
|
|
41
|
+
const loadMore = useCallback(async () => {
|
|
42
|
+
if (nextOffset == null) return; // null on the last page
|
|
43
|
+
const res = activeCategory
|
|
44
|
+
? await queryServicesByCategory(activeCategory.id, { limit, offset: nextOffset })
|
|
45
|
+
: await queryServices({ limit, offset: nextOffset });
|
|
46
|
+
setServices((s) => [...(s || []), ...res.services]);
|
|
47
|
+
setNextOffset(res.nextOffset);
|
|
48
|
+
}, [activeCategory, limit, nextOffset]);
|
|
49
|
+
|
|
50
|
+
// One category is not a choice — hide the menu rather than show a single chip that does nothing.
|
|
51
|
+
const categories = useMemo(() => (allCategories.length > 1 ? allCategories : []), [allCategories]);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
services, total, nextOffset, loadMore, error,
|
|
55
|
+
categories, activeCategory, setActiveCategory,
|
|
56
|
+
retry: () => load(activeCategory?.id ?? null),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Small facts derived from a Service payload, shared by the card and the detail page so "60 MIN",
|
|
2
|
+
// the price, and "1-to-1" are computed once instead of drifting between the two call sites.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Session length in minutes for an APPOINTMENT, else null: a CLASS/COURSE leaves
|
|
6
|
+
* schedule.availabilityConstraints empty because its length belongs to the scheduled sessions, not
|
|
7
|
+
* the service. For a class, derive it from a fetched slot (localEndDate − localStartDate).
|
|
8
|
+
* @param {object} service
|
|
9
|
+
* @returns {number|null}
|
|
10
|
+
*/
|
|
11
|
+
export function serviceDuration(service) {
|
|
12
|
+
const c = service?.schedule?.availabilityConstraints;
|
|
13
|
+
return c?.sessionDurations?.[0] ?? c?.durations?.[0]?.minutes ?? null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Price label for every rateType. Reading payment.fixed alone renders nothing at all for a VARIED or
|
|
18
|
+
* NO_FEE service, which is why this branches instead.
|
|
19
|
+
* FIXED -> "€95.00" (formattedValue when present, else built from value+currency)
|
|
20
|
+
* VARIED -> "From €85.00" (payment.varied.minPrice, falling back to defaultPrice)
|
|
21
|
+
* NO_FEE -> "Free"
|
|
22
|
+
* CUSTOM -> "" (business sets a price at booking time — nothing to show)
|
|
23
|
+
* SUBSCRIPTION-> "" (paid via a pricing plan, not a per-booking price)
|
|
24
|
+
* @param {object} service
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function servicePriceLabel(service) {
|
|
28
|
+
const payment = service?.payment;
|
|
29
|
+
const money = (m) => m?.formattedValue
|
|
30
|
+
|| (m?.currency && new Intl.NumberFormat(undefined, { style: "currency", currency: m.currency }).format(Number(m.value)));
|
|
31
|
+
switch (payment?.rateType) {
|
|
32
|
+
case "FIXED":
|
|
33
|
+
return money(payment.fixed?.price) || "";
|
|
34
|
+
case "VARIED": {
|
|
35
|
+
const from = money(payment.varied?.minPrice ?? payment.varied?.defaultPrice);
|
|
36
|
+
return from ? `From ${from}` : "";
|
|
37
|
+
}
|
|
38
|
+
case "NO_FEE":
|
|
39
|
+
return "Free";
|
|
40
|
+
default:
|
|
41
|
+
return ""; // CUSTOM / SUBSCRIPTION — no single number to show
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* "1-to-1" for a single-person service, "Up to N" otherwise. defaultCapacity is the service's own
|
|
47
|
+
* cap — for a CLASS this is usually the room/session size, for an APPOINTMENT usually 1.
|
|
48
|
+
* @param {object} service
|
|
49
|
+
* @returns {string|null}
|
|
50
|
+
*/
|
|
51
|
+
export function serviceCapacityLabel(service) {
|
|
52
|
+
const cap = service?.defaultCapacity;
|
|
53
|
+
if (!cap) return null;
|
|
54
|
+
return cap === 1 ? "1-to-1" : `Up to ${cap}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Where the service happens, from locations[].type — NOT from payment.options, which says how a
|
|
59
|
+
* customer may *pay* (`online` there means "can pay online", not "happens online"). A video service
|
|
60
|
+
* is flagged by conferencing.enabled. `calculatedAddress` is present on BUSINESS/CUSTOM locations
|
|
61
|
+
* and empty for CUSTOMER; when it's there, prefer the actual place name.
|
|
62
|
+
* BUSINESS -> the business's address ("At Serene Spa" / "In person")
|
|
63
|
+
* CUSTOMER -> the customer's address ("At your location")
|
|
64
|
+
* CUSTOM -> a specific address for this service
|
|
65
|
+
* @param {object} service
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
export function serviceLocationLabel(service) {
|
|
69
|
+
if (service?.conferencing?.enabled) return "Online";
|
|
70
|
+
const types = new Set((service?.locations || []).map((l) => l?.type).filter(Boolean));
|
|
71
|
+
const named = (service?.locations || [])
|
|
72
|
+
.map((l) => l?.calculatedAddress?.formatted || l?.business?.name)
|
|
73
|
+
.find(Boolean);
|
|
74
|
+
if (named) return named;
|
|
75
|
+
if (types.has("CUSTOMER")) return "At your location";
|
|
76
|
+
if (types.has("BUSINESS") || types.has("CUSTOM")) return "In person";
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The reschedule footnote, e.g. "Free to reschedule up to 24 hours before." `enabled` without
|
|
82
|
+
* `limitLatestReschedule` means any time; with it, the window is latestRescheduleInMinutes. Empty
|
|
83
|
+
* when rescheduling is off, so the page shows no promise rather than a wrong one.
|
|
84
|
+
* @param {object} service
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
export function serviceRescheduleNote(service) {
|
|
88
|
+
const p = service?.bookingPolicy?.reschedulePolicy;
|
|
89
|
+
if (!p?.enabled) return "";
|
|
90
|
+
if (!p.limitLatestReschedule) return "Free to reschedule any time before your appointment.";
|
|
91
|
+
const hours = Math.round((p.latestRescheduleInMinutes ?? 0) / 60);
|
|
92
|
+
return hours > 0
|
|
93
|
+
? `Free to reschedule up to ${hours} hour${hours === 1 ? "" : "s"} before.`
|
|
94
|
+
: "Free to reschedule right up until your appointment.";
|
|
95
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Service detail + booking page — thin view over useServiceDetail (all logic lives in the hook):
|
|
2
|
+
// pick a slot, enter details, book → hosted checkout. Two columns: what you're booking on the left,
|
|
3
|
+
// the booking panel on the right, sticky on desktop so the times stay in reach while reading.
|
|
4
|
+
// Styled with base44 design tokens (shadcn Tailwind classes).
|
|
5
|
+
import { useParams } from "react-router-dom";
|
|
6
|
+
import { useServiceDetail } from "@/hooks/useServiceDetail";
|
|
7
|
+
import { mediaUrl } from "@/rest/wix-bookings-services";
|
|
8
|
+
import SlotPicker from "@/components/SlotPicker";
|
|
9
|
+
import BookingForm from "@/components/BookingForm";
|
|
10
|
+
|
|
11
|
+
export default function ServiceDetail() {
|
|
12
|
+
const { serviceId } = useParams();
|
|
13
|
+
const d = useServiceDetail(serviceId);
|
|
14
|
+
|
|
15
|
+
if (d.notFound) return <Centered>Service not found.</Centered>;
|
|
16
|
+
if (!d.service) return <Centered>Loading…</Centered>;
|
|
17
|
+
|
|
18
|
+
const image = mediaUrl(d.service.media?.mainMedia?.image ?? d.service.media?.items?.[0]?.image ?? d.service.media?.coverMedia?.image);
|
|
19
|
+
const { price, minutes, capacity, location, rescheduleNote, needsApproval } = d.facts;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<main className="max-w-[1200px] mx-auto p-4 grid gap-8 items-start lg:[grid-template-columns:1fr_minmax(360px,420px)]">
|
|
23
|
+
<div>
|
|
24
|
+
<div className="aspect-[4/3] bg-card rounded-lg overflow-hidden mb-5">
|
|
25
|
+
{image && <img src={image} alt={d.service.name} className="w-full h-full object-cover" />}
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
{d.service.category?.name && (
|
|
29
|
+
<p className="m-0 mb-1.5 text-[11px] uppercase tracking-[0.12em] text-muted-foreground">
|
|
30
|
+
{d.service.category.name}
|
|
31
|
+
</p>
|
|
32
|
+
)}
|
|
33
|
+
<h1 className="font-display m-0 mb-3">{d.service.name}</h1>
|
|
34
|
+
{d.service.description && (
|
|
35
|
+
<p className="text-muted-foreground leading-relaxed mb-6 max-w-[62ch]">{d.service.description}</p>
|
|
36
|
+
)}
|
|
37
|
+
|
|
38
|
+
{/* The three facts a buyer checks before picking a time. Each cell renders only when the
|
|
39
|
+
service actually carries it, so a class with no fixed duration doesn't leave a blank. */}
|
|
40
|
+
<dl className="m-0 flex flex-wrap border border-border rounded-lg overflow-hidden divide-x divide-border">
|
|
41
|
+
{minutes && <Fact label="Duration" value={`${minutes} minutes`} />}
|
|
42
|
+
{price && <Fact label="Price" value={price} />}
|
|
43
|
+
{location && <Fact label="Where" value={location} />}
|
|
44
|
+
{!minutes && capacity && <Fact label="Capacity" value={capacity} />}
|
|
45
|
+
</dl>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="min-w-0 bg-card border border-border rounded-lg p-5 lg:sticky lg:top-4">
|
|
49
|
+
<div className="flex flex-wrap items-baseline justify-between gap-2 mb-4">
|
|
50
|
+
<h2 className="font-display text-lg m-0">Choose a time</h2>
|
|
51
|
+
{/* The zone the API returned the times in — not a locally guessed one, so the label and
|
|
52
|
+
the times on screen can't disagree. */}
|
|
53
|
+
{d.timeZone && (
|
|
54
|
+
<span className="text-[13px] text-muted-foreground">Times in {d.timeZone.replace(/_/g, " ")}</span>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<SlotPicker
|
|
59
|
+
slots={d.slots} cursor={d.cursor} onLoadMore={d.loadMoreSlots}
|
|
60
|
+
selectedSlot={d.selectedSlot} onPick={d.pickSlot}
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
{d.selectedSlot && (
|
|
64
|
+
<div className="mt-6 pt-6 border-t border-border">
|
|
65
|
+
<BookingForm
|
|
66
|
+
contact={d.contact} setContactField={d.setContactField}
|
|
67
|
+
maxParticipants={d.maxParticipants} participants={d.participants} setParticipants={d.setParticipants}
|
|
68
|
+
onSubmit={d.submit} submitting={d.submitting} canSubmit={d.canSubmit} error={d.error}
|
|
69
|
+
needsApproval={needsApproval}
|
|
70
|
+
timeLabel={new Date(d.selectedSlot.localStartDate).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}
|
|
71
|
+
priceLabel={price}
|
|
72
|
+
/>
|
|
73
|
+
<p className="m-0 mt-3 text-[13px] text-muted-foreground text-center">
|
|
74
|
+
You'll pay on the next screen.{rescheduleNote ? ` ${rescheduleNote}` : ""}
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
</main>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function Fact({ label, value }) {
|
|
84
|
+
return (
|
|
85
|
+
<div className="flex-1 min-w-[110px] p-3">
|
|
86
|
+
<dt className="m-0 text-[11px] uppercase tracking-wide text-muted-foreground">{label}</dt>
|
|
87
|
+
<dd className="m-0 mt-0.5 font-semibold">{value}</dd>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function Centered({ children }) {
|
|
93
|
+
return <div className="p-12 text-center text-muted-foreground">{children}</div>;
|
|
94
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Services / listing page — lists visitor-visible services with a category filter, plus the empty
|
|
2
|
+
// and failed states. Thin view over useServices (all data lives in the hook). Styled with base44
|
|
3
|
+
// design tokens (shadcn Tailwind classes).
|
|
4
|
+
import { useServices } from "@/hooks/useServices";
|
|
5
|
+
import ServiceGrid from "@/components/ServiceGrid";
|
|
6
|
+
|
|
7
|
+
export default function Services() {
|
|
8
|
+
const s = useServices({ limit: 24 });
|
|
9
|
+
|
|
10
|
+
if (s.error) {
|
|
11
|
+
return (
|
|
12
|
+
<main className="max-w-[1200px] mx-auto p-4">
|
|
13
|
+
<div role="alert" className="flex flex-col items-center gap-3 py-16 text-center">
|
|
14
|
+
<p className="m-0 text-muted-foreground">{s.error}</p>
|
|
15
|
+
<button onClick={s.retry}
|
|
16
|
+
className="border border-border bg-card text-foreground rounded-sm py-2 px-4 cursor-pointer text-sm">
|
|
17
|
+
Try again
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
</main>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<main className="max-w-[1200px] mx-auto p-4">
|
|
26
|
+
<h1 className="font-display mb-4">{s.activeCategory?.name || "Services"}</h1>
|
|
27
|
+
|
|
28
|
+
{s.categories.length > 0 && (
|
|
29
|
+
<nav aria-label="Categories" className="flex flex-wrap gap-2 mb-6">
|
|
30
|
+
<CategoryChip active={!s.activeCategory} onClick={() => s.setActiveCategory(null)}>All</CategoryChip>
|
|
31
|
+
{s.categories.map((c) => (
|
|
32
|
+
<CategoryChip key={c.id} active={s.activeCategory?.id === c.id} onClick={() => s.setActiveCategory(c)}>
|
|
33
|
+
{c.name}
|
|
34
|
+
</CategoryChip>
|
|
35
|
+
))}
|
|
36
|
+
</nav>
|
|
37
|
+
)}
|
|
38
|
+
|
|
39
|
+
{s.services === null
|
|
40
|
+
? <ServiceGridSkeleton />
|
|
41
|
+
: <>
|
|
42
|
+
<ServiceGrid
|
|
43
|
+
services={s.services}
|
|
44
|
+
empty={s.activeCategory
|
|
45
|
+
? `Nothing in ${s.activeCategory.name} yet.`
|
|
46
|
+
: "No services yet — add one from your Wix dashboard."}
|
|
47
|
+
/>
|
|
48
|
+
{s.nextOffset != null && (
|
|
49
|
+
<div className="text-center mt-6">
|
|
50
|
+
<button onClick={s.loadMore}
|
|
51
|
+
className="px-5 py-2.5 cursor-pointer bg-card text-foreground border border-border rounded-sm">
|
|
52
|
+
Load more
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
</>}
|
|
57
|
+
</main>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function CategoryChip({ active, onClick, children }) {
|
|
62
|
+
return (
|
|
63
|
+
<button onClick={onClick} aria-pressed={active}
|
|
64
|
+
className={`py-1.5 px-3 cursor-pointer text-sm rounded-sm border ${
|
|
65
|
+
active ? "border-primary bg-primary text-primary-foreground" : "border-border bg-card text-foreground"
|
|
66
|
+
}`}>{children}</button>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Matches ServiceCard's 4:3 image + three text lines, so nothing shifts when the services land.
|
|
71
|
+
function ServiceGridSkeleton({ count = 6 }) {
|
|
72
|
+
return (
|
|
73
|
+
<div className="grid gap-4 grid-cols-2 md:[grid-template-columns:repeat(auto-fill,minmax(240px,1fr))]" aria-hidden="true">
|
|
74
|
+
{Array.from({ length: count }).map((_, i) => (
|
|
75
|
+
<div key={i} className="bg-card border border-border rounded-lg overflow-hidden">
|
|
76
|
+
<div className="aspect-[4/3] bg-muted animate-pulse" />
|
|
77
|
+
<div className="p-3 flex flex-col gap-2">
|
|
78
|
+
<div className="flex justify-between gap-3">
|
|
79
|
+
<div className="h-3.5 w-1/2 bg-muted rounded-sm animate-pulse" />
|
|
80
|
+
<div className="h-3.5 w-1/5 bg-muted rounded-sm animate-pulse" />
|
|
81
|
+
</div>
|
|
82
|
+
<div className="h-3 w-3/4 bg-muted rounded-sm animate-pulse" />
|
|
83
|
+
<div className="h-2.5 w-1/3 bg-muted rounded-sm animate-pulse" />
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -104,7 +104,7 @@ export async function createBooking(slot, contactDetails, { totalParticipants =
|
|
|
104
104
|
* Create an eCommerce checkout for a created booking and return the hosted checkout URL.
|
|
105
105
|
* Redirect the buyer there (window.location.href = ...). On return, the booking is confirmed.
|
|
106
106
|
* Throws if no redirect URL is produced.
|
|
107
|
-
* https://dev.wix.com/docs/
|
|
107
|
+
* https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/checkout.md
|
|
108
108
|
* @param {string} bookingId booking.id from createBooking().
|
|
109
109
|
* @returns {Promise<string>} The hosted-checkout URL to redirect to.
|
|
110
110
|
*/
|