@wix/agent-skills 1.17.0 → 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 +14 -3
- package/skills/wix-app/references/SITE_PLUGIN.md +12 -0
- package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +74 -68
- 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/ANIMATED-COMPONENTS.md +12 -11
- package/skills/wix-app/references/editor-react-component/BRANDED-COMPONENTS.md +1 -0
- package/skills/wix-app/references/editor-react-component/BROWSER-SUPPORT.md +56 -0
- package/skills/wix-app/references/editor-react-component/COMPONENT-PREVIEW.md +16 -8
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +19 -1
- package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +3 -0
- package/skills/wix-base44-connector/SKILL.md +133 -44
- package/skills/wix-base44-connector/references/CAPABILITY_MAP.md +78 -0
- package/skills/wix-base44-connector/scripts/utils.js +216 -30
- 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 +4 -1
- 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 +26 -8
- 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-rentals.md +254 -0
- 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/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 +18 -2
- 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/stores/create-product-catalog-v3.md +21 -6
- package/skills/wix-manage/references/stores/update-product-with-options.md +83 -0
- 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 +3 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +3 -1
- package/skills/wix-vibe-headless/platforms/base44.md +23 -61
- package/skills/wix-vibe-headless/platforms/generic.md +6 -3
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +43 -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-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/storefront/INSTRUCTIONS.md +137 -84
- 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-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-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
|
@@ -149,15 +149,16 @@ function buildOptions(options = []) {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
// full Cartesian product of variants, each priced/stocked from the product; visible:true baked in
|
|
152
|
-
function expandVariants(options = [], { price, compareAtPrice, quantity }) {
|
|
152
|
+
function expandVariants(options = [], { price, compareAtPrice, quantity }, digitalFileId) {
|
|
153
153
|
const base = {
|
|
154
154
|
price: {
|
|
155
155
|
actualPrice: { amount: String(price) },
|
|
156
156
|
...(compareAtPrice ? { compareAtPrice: { amount: String(compareAtPrice) } } : {}),
|
|
157
157
|
},
|
|
158
158
|
visible: true,
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
...(digitalFileId
|
|
160
|
+
? { digitalProperties: { digitalFile: { id: digitalFileId } }, inventoryItem: { inStock: true } }
|
|
161
|
+
: { physicalProperties: {}, inventoryItem: { quantity: quantity ?? 0, preorderInfo: { enabled: false } } }),
|
|
161
162
|
};
|
|
162
163
|
if (!options.length) return [base];
|
|
163
164
|
let combos = [[]];
|
|
@@ -170,6 +171,33 @@ function expandVariants(options = [], { price, compareAtPrice, quantity }) {
|
|
|
170
171
|
return combos.map((choices) => ({ ...base, choices }));
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
// A digital variant is SELLABLE only with BOTH a file and stock: without the file the cart rejects
|
|
175
|
+
// it as ITEM_NOT_FOUND_IN_CATALOG, without stock as "exceeds available inventory" — and either way
|
|
176
|
+
// the product reads back visible and in the catalog, so nothing surfaces until a buyer tries to buy.
|
|
177
|
+
// `digitalFileUrl` is the only way into DIGITAL here, which makes the file-less product unbuildable.
|
|
178
|
+
// The bytes are PUT, not imported by url: an uploaded file is READY at once, while an imported one
|
|
179
|
+
// stays PENDING and the cart rejects the product until it settles.
|
|
180
|
+
const FILE_MIME = { pdf: "application/pdf", zip: "application/zip", epub: "application/epub+zip",
|
|
181
|
+
mp3: "audio/mpeg", wav: "audio/wav", mp4: "video/mp4", png: "image/png", jpg: "image/jpeg" };
|
|
182
|
+
|
|
183
|
+
async function uploadDigitalFile(ctx, url, fileName) {
|
|
184
|
+
const mimeType = FILE_MIME[(fileName.split(".").pop() || "").toLowerCase()];
|
|
185
|
+
if (!mimeType) throw new Error(`digitalFileName needs one of these extensions (${Object.keys(FILE_MIME).join(", ")}): ${fileName}`);
|
|
186
|
+
const { uploadUrl } = await req(ctx, "/site-media/v1/files/generate-upload-url", { body: { mimeType, fileName } });
|
|
187
|
+
const src = await fetch(url);
|
|
188
|
+
if (!src.ok) throw new Error(`digitalFileUrl ${url} -> ${src.status}`);
|
|
189
|
+
const res = await fetch(uploadUrl, {
|
|
190
|
+
method: "PUT", headers: { "Content-Type": mimeType }, body: Buffer.from(await src.arrayBuffer()),
|
|
191
|
+
});
|
|
192
|
+
const json = await res.json().catch(() => ({}));
|
|
193
|
+
const id = (json.file || json)?.id;
|
|
194
|
+
if (!res.ok || !id) throw new Error(`digital file upload failed (${res.status}): ${JSON.stringify(json).slice(0, 200)}`);
|
|
195
|
+
return id;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const digitalFileName = (p) =>
|
|
199
|
+
p.digitalFileName || decodeURIComponent(new URL(p.digitalFileUrl).pathname.split("/").pop() || "");
|
|
200
|
+
|
|
173
201
|
// ---- exported operations ----
|
|
174
202
|
|
|
175
203
|
async function installStoresApp(ctx) {
|
|
@@ -193,7 +221,10 @@ async function listProducts(ctx) {
|
|
|
193
221
|
/**
|
|
194
222
|
* Bulk-create products.
|
|
195
223
|
* @param products [{ name, description, price, compareAtPrice?, quantity,
|
|
196
|
-
* options?: [{ name, type?:"text"|"color", choices:["8","9"] | [{name,colorCode}] }]
|
|
224
|
+
* options?: [{ name, type?:"text"|"color", choices:["8","9"] | [{name,colorCode}] }],
|
|
225
|
+
* digitalFileUrl?, digitalFileName? }]
|
|
226
|
+
* digitalFileUrl: makes the product a DIGITAL download — the file is uploaded and the variant
|
|
227
|
+
* created with both the file and stock (see uploadDigitalFile). `quantity` is ignored.
|
|
197
228
|
* description: plain text, or simple HTML (`<p>`, `<br/>`, `<strong>`, `<em>`) — converted to
|
|
198
229
|
* Wix rich text here, so the storefront renders paragraphs and bold rather than tag text.
|
|
199
230
|
* options = ONLY things the buyer selects-and-buys (Size, Color) -> become variants.
|
|
@@ -202,17 +233,20 @@ async function listProducts(ctx) {
|
|
|
202
233
|
* @returns [{ id, slug, revision }]
|
|
203
234
|
*/
|
|
204
235
|
async function bulkCreateProducts(ctx, products) {
|
|
236
|
+
const fileIds = await Promise.all(products.map((p) =>
|
|
237
|
+
p.digitalFileUrl ? uploadDigitalFile(ctx, p.digitalFileUrl, digitalFileName(p)) : null));
|
|
205
238
|
const body = {
|
|
206
239
|
returnEntity: true,
|
|
207
240
|
products: products.map((p, i) => ({
|
|
208
241
|
name: p.name,
|
|
209
|
-
|
|
210
|
-
|
|
242
|
+
// DIGITAL drops physicalProperties and can't be POS-visible (DIGITAL_PRODUCT_CANNOT_BE_VISIBLE_IN_POS).
|
|
243
|
+
...(fileIds[i]
|
|
244
|
+
? { productType: "DIGITAL" }
|
|
245
|
+
: { productType: "PHYSICAL", physicalProperties: {}, visibleInPos: true }),
|
|
211
246
|
visible: true,
|
|
212
|
-
visibleInPos: true,
|
|
213
247
|
description: mkDesc(p.description, i),
|
|
214
248
|
options: buildOptions(p.options),
|
|
215
|
-
variantsInfo: { variants: expandVariants(p.options, p) },
|
|
249
|
+
variantsInfo: { variants: expandVariants(p.options, p, fileIds[i]) },
|
|
216
250
|
})),
|
|
217
251
|
};
|
|
218
252
|
const r = await req(ctx, "/stores/v3/bulk/products-with-inventory/create", { body });
|
|
@@ -221,6 +255,7 @@ async function bulkCreateProducts(ctx, products) {
|
|
|
221
255
|
id: x.item?.id, slug: x.item?.slug, revision: x.item?.revision,
|
|
222
256
|
variantId: x.item?.variantsInfo?.variants?.[0]?.id,
|
|
223
257
|
hasOptions: (products[i]?.options?.length ?? 0) > 0,
|
|
258
|
+
isDigital: !!fileIds[i],
|
|
224
259
|
quantity: products[i]?.quantity ?? 0,
|
|
225
260
|
}));
|
|
226
261
|
await stockOptionlessProducts(ctx, created);
|
|
@@ -233,7 +268,7 @@ async function bulkCreateProducts(ctx, products) {
|
|
|
233
268
|
// are already stocked by the create above, so they're skipped. Backfills the default variantId from a
|
|
234
269
|
// query if the create response didn't return it.
|
|
235
270
|
async function stockOptionlessProducts(ctx, created) {
|
|
236
|
-
const need = created.filter((p) => !p.hasOptions && p.id);
|
|
271
|
+
const need = created.filter((p) => !p.hasOptions && !p.isDigital && p.id); // digital variants ship inStock from the create
|
|
237
272
|
if (!need.length) return;
|
|
238
273
|
const missing = need.filter((p) => !p.variantId).map((p) => p.id);
|
|
239
274
|
if (missing.length) {
|
|
@@ -300,7 +335,8 @@ async function attachProductImages(ctx, items) {
|
|
|
300
335
|
* the DEFAULT path — call it once instead of the individual functions.
|
|
301
336
|
*
|
|
302
337
|
* @param plan {{
|
|
303
|
-
* products: [{ name, description, price, compareAtPrice?, quantity, options?, imageUrl?, altText
|
|
338
|
+
* products: [{ name, description, price, compareAtPrice?, quantity, options?, imageUrl?, altText?,
|
|
339
|
+
* digitalFileUrl?, digitalFileName? }],
|
|
304
340
|
* categories?: { [categoryName]: string[] }, // map of category name -> product NAMES in it
|
|
305
341
|
* }}
|
|
306
342
|
* @returns { products: [{id,slug,revision,name}], categories: [{id,name}], imagesAttached: number }
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
// Reference implementation of a product grid tile built on useProductCard.
|
|
2
|
-
// Styled with base44 design tokens (shadcn Tailwind classes: bg-card / text-foreground / border-border).
|
|
3
|
-
// Build your own card on useProductCard instead — read this for inspiration, don't use it directly.
|
|
4
|
-
import { useState } from "react";
|
|
5
|
-
import { Link } from "react-router-dom";
|
|
6
|
-
import { useCart } from "@/context/CartContext";
|
|
7
|
-
import { useProductCard } from "@/hooks/useProductCard";
|
|
8
|
-
|
|
9
|
-
const badge = "px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide rounded-full";
|
|
10
|
-
const badgePromo = `${badge} bg-primary text-primary-foreground`;
|
|
11
|
-
const badgeInfo = `${badge} bg-card text-foreground border border-border`;
|
|
12
|
-
const badgeBlocked = `${badge} bg-destructive text-destructive-foreground`;
|
|
13
|
-
|
|
14
|
-
const badgeClass = { "pre-order": badgeInfo, "sold-out": badgeBlocked, "limited-stock": badgeInfo };
|
|
15
|
-
|
|
16
|
-
export default function ProductCard({ product }) {
|
|
17
|
-
const { addToCart } = useCart();
|
|
18
|
-
const [adding, setAdding] = useState(false);
|
|
19
|
-
|
|
20
|
-
const {
|
|
21
|
-
isSoldOut, isPreorder, leftBadges, promoBadge,
|
|
22
|
-
priceDisplay, compareAtDisplay,
|
|
23
|
-
colors, optionLabel,
|
|
24
|
-
isQuickAddable, image, hoverImage,
|
|
25
|
-
} = useProductCard(product);
|
|
26
|
-
|
|
27
|
-
const quickAdd = async () => {
|
|
28
|
-
setAdding(true);
|
|
29
|
-
await addToCart(product.id);
|
|
30
|
-
setAdding(false);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<div className="group relative flex flex-col bg-card border border-border rounded-lg overflow-hidden shadow-sm">
|
|
35
|
-
<div className="relative aspect-square bg-background">
|
|
36
|
-
<Link to={`/product/${product.slug}`} aria-label={product.name} className="block w-full h-full">
|
|
37
|
-
{image ? (
|
|
38
|
-
<>
|
|
39
|
-
<img src={image} alt={product.name} loading="lazy"
|
|
40
|
-
className={`w-full h-full object-cover ${hoverImage ? "transition-opacity duration-300 md:group-hover:opacity-0" : ""}`} />
|
|
41
|
-
{hoverImage && (
|
|
42
|
-
<img src={hoverImage} alt="" aria-hidden="true" loading="lazy"
|
|
43
|
-
className="hidden md:block absolute inset-0 w-full h-full object-cover opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
|
|
44
|
-
)}
|
|
45
|
-
</>
|
|
46
|
-
) : (
|
|
47
|
-
<div className="w-full h-full bg-muted flex items-center justify-center" aria-hidden="true">
|
|
48
|
-
<svg viewBox="0 0 24 24" className="w-8 h-8 text-muted-foreground/40" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
49
|
-
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><path d="m21 15-5-5L5 21" />
|
|
50
|
-
</svg>
|
|
51
|
-
</div>
|
|
52
|
-
)}
|
|
53
|
-
</Link>
|
|
54
|
-
|
|
55
|
-
<div className="absolute top-2 left-2 flex flex-col items-start gap-1 pointer-events-none">
|
|
56
|
-
{leftBadges.map((b) => (
|
|
57
|
-
<span key={b.type} className={badgeClass[b.type]}>{b.label}</span>
|
|
58
|
-
))}
|
|
59
|
-
</div>
|
|
60
|
-
<div className="absolute top-2 right-2 pointer-events-none">
|
|
61
|
-
{promoBadge && <span className={badgePromo}>{promoBadge.label}</span>}
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
{(!isSoldOut || isPreorder) && (
|
|
65
|
-
<div className="absolute inset-x-2 bottom-2 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100">
|
|
66
|
-
{isQuickAddable ? (
|
|
67
|
-
<button onClick={quickAdd} disabled={adding}
|
|
68
|
-
className="w-full py-2.5 px-4 rounded-full bg-primary text-primary-foreground border-none text-sm font-semibold cursor-pointer transition-opacity hover:opacity-90 disabled:opacity-60 disabled:cursor-wait">
|
|
69
|
-
{adding ? "Adding…" : "Quick add"}
|
|
70
|
-
</button>
|
|
71
|
-
) : (
|
|
72
|
-
<Link to={`/product/${product.slug}`}
|
|
73
|
-
className="block w-full text-center py-2.5 px-4 rounded-full bg-primary text-primary-foreground no-underline text-sm font-semibold">
|
|
74
|
-
{isSoldOut ? "Pre-order" : "Choose options"}
|
|
75
|
-
</Link>
|
|
76
|
-
)}
|
|
77
|
-
</div>
|
|
78
|
-
)}
|
|
79
|
-
</div>
|
|
80
|
-
|
|
81
|
-
<div className="p-3 flex flex-col gap-1">
|
|
82
|
-
<div className="flex flex-col gap-1 md:flex-row md:items-baseline md:justify-between md:gap-3">
|
|
83
|
-
<h3 className="m-0 font-display text-[15px] font-semibold line-clamp-2">
|
|
84
|
-
<Link to={`/product/${product.slug}`} className="no-underline text-foreground">{product.name}</Link>
|
|
85
|
-
</h3>
|
|
86
|
-
<div className="flex gap-2 items-baseline flex-wrap md:justify-end">
|
|
87
|
-
<span className="font-semibold whitespace-nowrap">{priceDisplay}</span>
|
|
88
|
-
{compareAtDisplay && (
|
|
89
|
-
<span className="text-muted-foreground line-through text-[13px] whitespace-nowrap">{compareAtDisplay}</span>
|
|
90
|
-
)}
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
|
|
94
|
-
{(colors.length > 0 || optionLabel) && (
|
|
95
|
-
<div className="flex items-center gap-2 text-[13px] text-muted-foreground min-h-[18px]">
|
|
96
|
-
{colors.length > 0 && (
|
|
97
|
-
<span className="flex items-center gap-1" aria-label={`${colors.length} colors available`}>
|
|
98
|
-
{colors.slice(0, 5).map((c, i) => (
|
|
99
|
-
<span key={`${c}-${i}`} className="w-3 h-3 rounded-full ring-1 ring-inset ring-black/20" style={{ backgroundColor: c }} />
|
|
100
|
-
))}
|
|
101
|
-
{colors.length > 5 && <span>+{colors.length - 5}</span>}
|
|
102
|
-
</span>
|
|
103
|
-
)}
|
|
104
|
-
{optionLabel && <span>{optionLabel}</span>}
|
|
105
|
-
</div>
|
|
106
|
-
)}
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// PDP image gallery — main image plus thumbnails from productGallery(product), which puts
|
|
2
|
-
// media.main first and drops the duplicate of it that media.itemsInfo.items carries. A product with a
|
|
3
|
-
// single image renders just the main frame, so this is safe for a one-photo catalog.
|
|
4
|
-
// Styled with base44 design tokens (shadcn Tailwind classes).
|
|
5
|
-
import { useEffect, useState } from "react";
|
|
6
|
-
|
|
7
|
-
export default function ProductGallery({ images, name, focusUrl }) {
|
|
8
|
-
const [index, setIndex] = useState(0);
|
|
9
|
-
// Reset when navigating between products — the component stays mounted across a slug change.
|
|
10
|
-
// `images` must keep a stable identity per product (the PDP memoises it), or every parent render —
|
|
11
|
-
// a quantity bump, an option click — would land here and throw away the image the buyer picked.
|
|
12
|
-
useEffect(() => { setIndex(0); }, [images]);
|
|
13
|
-
|
|
14
|
-
// Picking a colour swatch shows that colour: focusUrl is the choice's linked photo. Runs after the
|
|
15
|
-
// reset effect, so a first paint with a colour pre-selected opens on the right image.
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
const i = images?.findIndex((img) => img.url === focusUrl) ?? -1;
|
|
18
|
-
if (i >= 0) setIndex(i);
|
|
19
|
-
}, [focusUrl, images]);
|
|
20
|
-
|
|
21
|
-
const current = images?.[index];
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<div className="flex flex-col gap-3">
|
|
25
|
-
<div className="aspect-square bg-card rounded-lg overflow-hidden">
|
|
26
|
-
{current ? (
|
|
27
|
-
<img src={current.url} alt={current.altText || name} className="w-full h-full object-cover" />
|
|
28
|
-
) : (
|
|
29
|
-
<div className="w-full h-full bg-muted flex items-center justify-center" aria-hidden="true">
|
|
30
|
-
<svg viewBox="0 0 24 24" className="w-10 h-10 text-muted-foreground/40" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
31
|
-
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><path d="m21 15-5-5L5 21" />
|
|
32
|
-
</svg>
|
|
33
|
-
</div>
|
|
34
|
-
)}
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
{images?.length > 1 && (
|
|
38
|
-
<div className="flex gap-2 flex-wrap" role="group" aria-label={`${name} images`}>
|
|
39
|
-
{images.map((img, i) => (
|
|
40
|
-
<button key={img.url} onClick={() => setIndex(i)} aria-label={`Show image ${i + 1}`} aria-current={i === index}
|
|
41
|
-
className={`w-16 h-16 rounded-sm overflow-hidden cursor-pointer bg-card border ${
|
|
42
|
-
i === index ? "border-primary" : "border-border"
|
|
43
|
-
}`}>
|
|
44
|
-
<img src={img.url} alt="" loading="lazy" className="w-full h-full object-cover" />
|
|
45
|
-
</button>
|
|
46
|
-
))}
|
|
47
|
-
</div>
|
|
48
|
-
)}
|
|
49
|
-
</div>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// Reference implementation of a product grid — 2-col mobile, auto-fill desktop, with a skeleton
|
|
2
|
-
// loading state and a descriptive empty state. Build your own layout instead (see INSTRUCTIONS.md
|
|
3
|
-
// STEP 3) — read this for the skeleton/empty-state patterns, don't use it directly.
|
|
4
|
-
import ProductCard from "./ProductCard";
|
|
5
|
-
|
|
6
|
-
// Skeleton tiles match ProductCard's aspect-square + two text lines, so nothing shifts when the real
|
|
7
|
-
// products land.
|
|
8
|
-
export function ProductGridSkeleton({ count = 8 }) {
|
|
9
|
-
return (
|
|
10
|
-
<div className="grid gap-4 grid-cols-2 md:[grid-template-columns:repeat(auto-fill,minmax(220px,1fr))]" aria-hidden="true">
|
|
11
|
-
{Array.from({ length: count }).map((_, i) => (
|
|
12
|
-
<div key={i} className="bg-card border border-border rounded-lg overflow-hidden">
|
|
13
|
-
<div className="aspect-square bg-muted animate-pulse" />
|
|
14
|
-
<div className="p-3 flex flex-col gap-2">
|
|
15
|
-
<div className="flex justify-between gap-3">
|
|
16
|
-
<div className="h-3.5 w-1/2 bg-muted rounded-sm animate-pulse" />
|
|
17
|
-
<div className="h-3.5 w-1/5 bg-muted rounded-sm animate-pulse" />
|
|
18
|
-
</div>
|
|
19
|
-
<div className="h-3 w-1/3 bg-muted rounded-sm animate-pulse" />
|
|
20
|
-
</div>
|
|
21
|
-
</div>
|
|
22
|
-
))}
|
|
23
|
-
</div>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export default function ProductGrid({ products, loading, empty = "No products yet.", emptyHint }) {
|
|
28
|
-
if (loading) return <ProductGridSkeleton />;
|
|
29
|
-
|
|
30
|
-
if (!products?.length) {
|
|
31
|
-
return (
|
|
32
|
-
<div className="flex flex-col items-center justify-center text-center gap-3 py-16 px-4">
|
|
33
|
-
<svg width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"
|
|
34
|
-
className="text-muted-foreground/40" aria-hidden="true">
|
|
35
|
-
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z" /><path d="M3 6h18" />
|
|
36
|
-
<path d="M16 10a4 4 0 0 1-8 0" />
|
|
37
|
-
</svg>
|
|
38
|
-
<h2 className="font-display m-0 text-lg font-semibold">{empty}</h2>
|
|
39
|
-
{emptyHint && <p className="m-0 text-muted-foreground max-w-[46ch]">{emptyHint}</p>}
|
|
40
|
-
</div>
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div className="grid gap-4 grid-cols-2 md:[grid-template-columns:repeat(auto-fill,minmax(220px,1fr))]">
|
|
46
|
-
{products.map((p) => <ProductCard key={p.id} product={p} />)}
|
|
47
|
-
</div>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
// PDP choice controls built on useVariantOptions — swap this component for your own if you want
|
|
2
|
-
// a different look; the data contract (optionGroups / modifierGroups) stays the same.
|
|
3
|
-
// Styled with base44 design tokens (shadcn Tailwind classes).
|
|
4
|
-
import { useVariantOptions } from "@/hooks/useVariantOptions";
|
|
5
|
-
|
|
6
|
-
const chipBase = "py-1.5 px-3 cursor-pointer text-sm font-body border rounded-sm";
|
|
7
|
-
const chipIdle = "border-border bg-card text-foreground";
|
|
8
|
-
const chipActive = "border-primary bg-primary text-primary-foreground";
|
|
9
|
-
const label = "block mb-1.5 text-[13px] font-semibold text-muted-foreground";
|
|
10
|
-
|
|
11
|
-
export default function VariantPicker({ options, modifiers, selectedOptions, selectOption, modifierValues, setModifier }) {
|
|
12
|
-
const { optionGroups, modifierGroups } = useVariantOptions(options, modifiers, selectedOptions, modifierValues);
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<>
|
|
16
|
-
{optionGroups.map((group) => (
|
|
17
|
-
<div key={group.id} className="mb-4">
|
|
18
|
-
<label className={label}>
|
|
19
|
-
{group.name}
|
|
20
|
-
{group.isColor && (
|
|
21
|
-
<span className="ml-1.5 font-normal text-foreground">
|
|
22
|
-
{group.choices.find((c) => c.selected)?.name}
|
|
23
|
-
</span>
|
|
24
|
-
)}
|
|
25
|
-
</label>
|
|
26
|
-
<div className="flex flex-wrap gap-2">
|
|
27
|
-
{group.choices.map((c) =>
|
|
28
|
-
c.isColorSwatch ? (
|
|
29
|
-
<button
|
|
30
|
-
key={c.choiceId}
|
|
31
|
-
type="button"
|
|
32
|
-
disabled={!c.inStock}
|
|
33
|
-
onClick={() => selectOption(group.id, c.choiceId)}
|
|
34
|
-
aria-pressed={c.selected}
|
|
35
|
-
aria-label={`${c.name}${!c.inStock ? " (out of stock)" : ""}`}
|
|
36
|
-
title={c.name}
|
|
37
|
-
className={`relative w-9 h-9 rounded-full cursor-pointer transition-shadow ring-1 ring-inset ring-black/15 ${
|
|
38
|
-
c.selected ? "ring-2 ring-offset-2 ring-offset-background ring-primary" : ""
|
|
39
|
-
} ${!c.inStock ? "opacity-40 cursor-not-allowed" : "hover:ring-2 hover:ring-offset-1 hover:ring-offset-background hover:ring-border"}`}
|
|
40
|
-
style={{ backgroundColor: c.colorCode }}
|
|
41
|
-
>
|
|
42
|
-
{!c.inStock && (
|
|
43
|
-
<span aria-hidden="true" className="absolute inset-0 grid place-items-center">
|
|
44
|
-
<span className="w-full h-px bg-destructive rotate-45" />
|
|
45
|
-
</span>
|
|
46
|
-
)}
|
|
47
|
-
</button>
|
|
48
|
-
) : (
|
|
49
|
-
<button
|
|
50
|
-
key={c.choiceId}
|
|
51
|
-
type="button"
|
|
52
|
-
disabled={!c.inStock}
|
|
53
|
-
aria-pressed={c.selected}
|
|
54
|
-
onClick={() => selectOption(group.id, c.choiceId)}
|
|
55
|
-
className={`${chipBase} ${c.selected ? chipActive : chipIdle} ${!c.inStock ? "opacity-40 line-through" : ""}`}
|
|
56
|
-
>
|
|
57
|
-
{c.name}
|
|
58
|
-
</button>
|
|
59
|
-
)
|
|
60
|
-
)}
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
))}
|
|
64
|
-
|
|
65
|
-
{modifierGroups.map((m) => (
|
|
66
|
-
<div key={m.key} className="mb-4">
|
|
67
|
-
<label className={label}>{m.name}{m.mandatory && " *"}</label>
|
|
68
|
-
{m.type === "text" ? (
|
|
69
|
-
<input
|
|
70
|
-
value={m.value}
|
|
71
|
-
onChange={(e) => setModifier(m.key, e.target.value)}
|
|
72
|
-
className="w-full py-2 px-3 font-body border border-input rounded-sm bg-background text-foreground"
|
|
73
|
-
/>
|
|
74
|
-
) : (
|
|
75
|
-
<div className="flex flex-wrap gap-2">
|
|
76
|
-
{m.choices.map((c) => (
|
|
77
|
-
<button
|
|
78
|
-
key={c.key}
|
|
79
|
-
aria-pressed={c.selected}
|
|
80
|
-
onClick={() => setModifier(m.key, c.key)}
|
|
81
|
-
className={`${chipBase} ${c.selected ? chipActive : chipIdle}`}
|
|
82
|
-
>
|
|
83
|
-
{c.name}
|
|
84
|
-
</button>
|
|
85
|
-
))}
|
|
86
|
-
</div>
|
|
87
|
-
)}
|
|
88
|
-
</div>
|
|
89
|
-
))}
|
|
90
|
-
</>
|
|
91
|
-
);
|
|
92
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
// PDP — thin view over useProductDetail (all logic lives in the hook). plainDescription is HTML
|
|
2
|
-
// despite the name; render via dangerouslySetInnerHTML. Styled with base44 design tokens (shadcn Tailwind classes).
|
|
3
|
-
//
|
|
4
|
-
// The price shown follows the selection: once options resolve to a variant the hook returns that
|
|
5
|
-
// variant's price, so a picked size can cost more than the "from" figure the grid tile showed.
|
|
6
|
-
import { useMemo } from "react";
|
|
7
|
-
import { Link, useParams } from "react-router-dom";
|
|
8
|
-
import { useProductDetail } from "@/hooks/useProductDetail";
|
|
9
|
-
import { productGallery } from "@/lib/storeImage";
|
|
10
|
-
import ProductGallery from "@/components/ProductGallery";
|
|
11
|
-
import VariantPicker from "@/components/VariantPicker";
|
|
12
|
-
|
|
13
|
-
export default function ProductDetail() {
|
|
14
|
-
const { slug } = useParams();
|
|
15
|
-
const d = useProductDetail(slug);
|
|
16
|
-
// Memoised so the gallery keeps its selected image across re-renders (see ProductGallery).
|
|
17
|
-
const images = useMemo(() => productGallery(d.product), [d.product]);
|
|
18
|
-
|
|
19
|
-
if (d.error) {
|
|
20
|
-
return (
|
|
21
|
-
<Centered>
|
|
22
|
-
<p className="m-0">{d.error}</p>
|
|
23
|
-
<button onClick={d.retry} className="border border-border bg-card text-foreground rounded-sm py-2 px-4 cursor-pointer text-sm">Try again</button>
|
|
24
|
-
</Centered>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
if (d.notFound) {
|
|
28
|
-
return (
|
|
29
|
-
<Centered>
|
|
30
|
-
<p className="m-0">We couldn't find that product.</p>
|
|
31
|
-
<Link to="/shop" className="text-primary">Back to shop</Link>
|
|
32
|
-
</Centered>
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
if (!d.product) return <ProductDetailSkeleton />;
|
|
36
|
-
|
|
37
|
-
const compareAt = d.product?.compareAtPriceRange?.minValue?.formattedAmount;
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<main className="max-w-[1200px] mx-auto p-4">
|
|
41
|
-
<nav aria-label="Breadcrumb" className="mb-4 text-sm text-muted-foreground">
|
|
42
|
-
<Link to="/shop" className="text-muted-foreground hover:text-foreground">Shop</Link>
|
|
43
|
-
<span aria-hidden="true"> / </span>
|
|
44
|
-
<span className="text-foreground">{d.product.name}</span>
|
|
45
|
-
</nav>
|
|
46
|
-
|
|
47
|
-
<div className="grid gap-8 [grid-template-columns:repeat(auto-fit,minmax(320px,1fr))]">
|
|
48
|
-
<ProductGallery images={images} name={d.product.name} focusUrl={d.focusMediaUrl} />
|
|
49
|
-
|
|
50
|
-
<div>
|
|
51
|
-
<h1 className="font-display m-0 mb-2">{d.product.name}</h1>
|
|
52
|
-
|
|
53
|
-
<div className="flex items-baseline gap-2 mb-4">
|
|
54
|
-
<p className="text-[22px] font-semibold m-0">{d.price}</p>
|
|
55
|
-
{compareAt && compareAt !== d.price && (
|
|
56
|
-
<span className="text-muted-foreground line-through">{compareAt}</span>
|
|
57
|
-
)}
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
{d.product.plainDescription && (
|
|
61
|
-
<div className="text-muted-foreground leading-relaxed mb-6"
|
|
62
|
-
dangerouslySetInnerHTML={{ __html: d.product.plainDescription }} />
|
|
63
|
-
)}
|
|
64
|
-
|
|
65
|
-
<VariantPicker
|
|
66
|
-
options={d.options} modifiers={d.modifiers}
|
|
67
|
-
selectedOptions={d.selectedOptions} selectOption={d.selectOption}
|
|
68
|
-
modifierValues={d.modifierValues} setModifier={d.setModifier}
|
|
69
|
-
/>
|
|
70
|
-
|
|
71
|
-
{/* Tell the buyer WHY the button is dead: an unresolvable combination reads as a broken page. */}
|
|
72
|
-
{d.options.length > 0 && !d.variant && (
|
|
73
|
-
<p className="text-[13px] text-muted-foreground m-0 mt-1">Pick an option from each list to continue.</p>
|
|
74
|
-
)}
|
|
75
|
-
|
|
76
|
-
<div className="flex items-stretch gap-3 mt-4">
|
|
77
|
-
<QuantityStepper value={d.quantity} onChange={d.setQuantity} disabled={!d.inStock} />
|
|
78
|
-
<button disabled={!d.canAdd || d.adding} onClick={d.submit}
|
|
79
|
-
className="flex-1 h-12 px-6 bg-primary text-primary-foreground border-none rounded-md text-[15px] font-semibold cursor-pointer transition-opacity hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
80
|
-
{d.adding ? "Adding…" : d.inStock ? "Add to cart" : "Out of stock"}
|
|
81
|
-
</button>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
</main>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Segmented −/+ stepper, height-matched to the Add-to-cart button so the row reads as one control.
|
|
90
|
-
// A native number input is avoided on purpose: its spinners are ~10px, OS-rendered (so they ignore
|
|
91
|
-
// the app's tokens), effectively untappable, and they accept "e"/"+"/"1e5" plus scroll-wheel edits.
|
|
92
|
-
// The value stays typeable — inputMode="numeric" brings up a keypad — but only digits survive, and
|
|
93
|
-
// blur restores 1 when the field is left empty. `tabular-nums` keeps the width steady across digits.
|
|
94
|
-
function QuantityStepper({ value, onChange, disabled }) {
|
|
95
|
-
const set = (n) => onChange(Math.max(1, n));
|
|
96
|
-
return (
|
|
97
|
-
<div data-disabled={disabled || undefined}
|
|
98
|
-
className="inline-flex items-stretch h-12 rounded-md border border-input bg-background overflow-hidden data-[disabled]:opacity-50">
|
|
99
|
-
<StepButton label="Decrease quantity" disabled={disabled || value <= 1} onClick={() => set(value - 1)}>−</StepButton>
|
|
100
|
-
<label className="sr-only" htmlFor="pdp-qty">Quantity</label>
|
|
101
|
-
<input id="pdp-qty" type="text" inputMode="numeric" autoComplete="off" disabled={disabled}
|
|
102
|
-
value={value} aria-live="polite"
|
|
103
|
-
onChange={(e) => { const n = e.target.value.replace(/\D/g, ""); onChange(n === "" ? "" : Math.max(1, Number(n))); }}
|
|
104
|
-
onBlur={(e) => { if (!e.target.value) set(1); }}
|
|
105
|
-
className="w-12 text-center bg-transparent text-foreground font-semibold tabular-nums border-x border-input outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:cursor-not-allowed" />
|
|
106
|
-
<StepButton label="Increase quantity" disabled={disabled} onClick={() => set(Number(value || 0) + 1)}>+</StepButton>
|
|
107
|
-
</div>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function StepButton({ children, label, onClick, disabled }) {
|
|
112
|
-
return (
|
|
113
|
-
<button type="button" onClick={onClick} disabled={disabled} aria-label={label}
|
|
114
|
-
className="w-11 grid place-items-center text-lg leading-none text-muted-foreground bg-transparent border-none cursor-pointer transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent">
|
|
115
|
-
{children}
|
|
116
|
-
</button>
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function ProductDetailSkeleton() {
|
|
121
|
-
return (
|
|
122
|
-
<main className="max-w-[1200px] mx-auto p-4" aria-busy="true">
|
|
123
|
-
<div className="grid gap-8 [grid-template-columns:repeat(auto-fit,minmax(320px,1fr))]">
|
|
124
|
-
<div className="aspect-square bg-muted rounded-lg animate-pulse" />
|
|
125
|
-
<div className="flex flex-col gap-3">
|
|
126
|
-
<div className="h-7 w-2/3 bg-muted rounded-sm animate-pulse" />
|
|
127
|
-
<div className="h-6 w-24 bg-muted rounded-sm animate-pulse" />
|
|
128
|
-
<div className="h-4 w-full bg-muted rounded-sm animate-pulse mt-3" />
|
|
129
|
-
<div className="h-4 w-5/6 bg-muted rounded-sm animate-pulse" />
|
|
130
|
-
<div className="h-11 w-full bg-muted rounded-sm animate-pulse mt-6" />
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
</main>
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function Centered({ children }) {
|
|
138
|
-
return <div className="p-12 flex flex-col items-center gap-3 text-center text-muted-foreground">{children}</div>;
|
|
139
|
-
}
|