@ikas/code-components-mcp 1.4.0-beta.139 → 1.4.0-beta.140
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/data/framework.json +2 -32
- package/data/section-templates/add-to-cart/index.tsx +2 -16
- package/data/section-templates/add-to-cart/sub-components/QuantitySelector/index.tsx +3 -11
- package/data/section-templates/cart-section/sub-components/CartItem/index.tsx +24 -57
- package/data/section-templates/cart-section/sub-components/CartItem/styles.css +0 -14
- package/data/section-templates/cart-section/sub-components/QuantitySelector/index.tsx +3 -11
- package/data/section-templates/header-section/sub-components/CartItem/index.tsx +24 -57
- package/data/section-templates/header-section/sub-components/CartItem/styles.css +0 -14
- package/data/section-templates/header-section/sub-components/QuantitySelector/index.tsx +3 -11
- package/data/section-templates/image-handling/ikas-config-snippet.json +0 -1
- package/data/section-templates/navigation/sub-components/CartItem/index.tsx +24 -57
- package/data/section-templates/navigation/sub-components/CartItem/styles.css +0 -14
- package/data/section-templates/navigation/sub-components/QuantitySelector/index.tsx +3 -11
- package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/index.tsx +2 -16
- package/data/section-templates/product-detail-section/ikas-config-snippet.json +0 -1
- package/data/section-templates/product-detail-section/sub-components/QuantitySelector/index.tsx +3 -11
- package/data/storefront-api.json +1 -1
- package/data/storefront-types.json +1 -1
- package/package.json +1 -1
- package/data/section-templates/add-to-cart/utils/cartLimits.ts +0 -23
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/components/BackInStockForm/index.tsx +0 -92
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/ikas-config-snippet.json +0 -159
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/index.tsx +0 -141
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/styles.css +0 -47
- package/data/section-templates/product-detail-section/children/ProductDetailBackInStock/types.ts +0 -25
- package/data/section-templates/product-detail-section/sub-components/Modal/index.tsx +0 -120
- package/data/section-templates/product-detail-section/sub-components/Modal/styles.css +0 -70
- package/data/section-templates/product-detail-section/utils/cartLimits.ts +0 -23
package/data/framework.json
CHANGED
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
"product-detail-patterns": {
|
|
252
252
|
"title": "Product Detail Patterns (Serel Reference)",
|
|
253
253
|
"description": "Serel ProductDetail with 12 child components in two COMPONENT_LIST slots, ProductGallery local component, Breadcrumb navigation, and component slot architecture",
|
|
254
|
-
"content": "## Product Detail Patterns (Serel Reference)\n\nThe serel ProductDetail section is the most complex section in the theme, with **12 child components** distributed across **two COMPONENT_LIST slots** (`components` and `bottomComponents`), plus a local `ProductGallery` component and Breadcrumb navigation.\n\n### Two-Slot Architecture\n\nThe section declares two separate COMPONENT_LIST props:\n\n```json\n{\n \"props\": [\n {\n \"name\": \"components\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\n \"nb34u3yu-product-detail-name-favorite\",\n \"nb34u3yu-product-detail-sku\",\n \"nb34u3yu-product-detail-prices\",\n \"nb34u3yu-product-detail-product-group\",\n \"nb34u3yu-product-detail-variant\",\n \"nb34u3yu-product-detail-add-to-cart\",\n \"nb34u3yu-product-detail-features\",\n \"nb34u3yu-product-detail-description\",\n \"nb34u3yu-product-detail-bundle-product\",\n \"nb34u3yu-product-detail-option-set\",\n \"nb34u3yu-product-detail-offer\",\n \"nb34u3yu-product-detail-bundle-furniture\"\n ]\n },\n {\n \"name\": \"bottomComponents\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\"nb34u3yu-product-detail-description\"]\n }\n ]\n}\n```\n\n### Section Layout\n\n```tsx\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport Breadcrumb from \"../../sub-components/Breadcrumb\";\nimport ProductGallery from \"./ProductGallery\"; // local component\nimport { Props } from \"./types\";\n\nexport function ProductDetail({ product, components, bottomComponents, backgroundColor, ...props }: Props) {\n if (!product) return null;\n return (\n <section className=\"product-detail\" style={backgroundColor ? { backgroundColor } : undefined}>\n <Breadcrumb product={product} />\n <div className=\"product-detail-inner\">\n <div className=\"product-detail-gallery\">\n <ProductGallery product={product} />\n </div>\n <div className=\"product-detail-info\">\n <IkasComponentRenderer id=\"product-detail-components\" components={components} parentProps={props} />\n </div>\n </div>\n <div className=\"product-detail-bottom\">\n <IkasComponentRenderer id=\"product-detail-bottom-components\" components={bottomComponents} parentProps={props} />\n </div>\n </section>\n );\n}\nexport default ProductDetail;\n```\n\n### 12 Child Components\n\n| Child Component | Purpose |\n|-----------------|--------|\n| ProductDetailNameFavorite | Product name + favorites toggle |\n| ProductDetailSku | SKU display |\n| ProductDetailPrices | Price display with discounts |\n| ProductDetailProductGroup | Product group/collection links |\n| ProductDetailVariant | Variant selector (size, color) |\n| ProductDetailAddToCart | Quantity selector + add-to-cart button |\n| ProductDetailFeatures | Feature list (uses FeatureItem children) |\n| ProductDetailDescription | Collapsible description (uses CollapsibleContent children) |\n| ProductDetailBundleProduct | Bundle product display |\n| ProductDetailBundleFurniture | Bundle furniture configuration |\n| ProductDetailOptionSet | Custom option sets |\n| ProductDetailOffer | Special offer display |\n\n### ProductGallery (Local Component)\n\nProductGallery is defined locally within the ProductDetail folder (not a registered component or sub-component). It handles image display with thumbnail navigation, zoom, and responsive srcset:\n\n```tsx\nconst images = (variant?.images ?? [])\n .map((pi) => pi.image)\n .filter((img): img is IkasImage => img != null);\n```\n\n### Breadcrumb Navigation\n\nThe Breadcrumb sub-component (from `src/sub-components/Breadcrumb/`) renders category path navigation:\n\n```tsx\nimport { getProductCategoryPath, getIkasCategoryHref } from \"@ikas/bp-storefront\";\nconst categoryPath = getProductCategoryPath(product);\n```\n\n###
|
|
254
|
+
"content": "## Product Detail Patterns (Serel Reference)\n\nThe serel ProductDetail section is the most complex section in the theme, with **12 child components** distributed across **two COMPONENT_LIST slots** (`components` and `bottomComponents`), plus a local `ProductGallery` component and Breadcrumb navigation.\n\n### Two-Slot Architecture\n\nThe section declares two separate COMPONENT_LIST props:\n\n```json\n{\n \"props\": [\n {\n \"name\": \"components\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\n \"nb34u3yu-product-detail-name-favorite\",\n \"nb34u3yu-product-detail-sku\",\n \"nb34u3yu-product-detail-prices\",\n \"nb34u3yu-product-detail-product-group\",\n \"nb34u3yu-product-detail-variant\",\n \"nb34u3yu-product-detail-add-to-cart\",\n \"nb34u3yu-product-detail-features\",\n \"nb34u3yu-product-detail-description\",\n \"nb34u3yu-product-detail-bundle-product\",\n \"nb34u3yu-product-detail-option-set\",\n \"nb34u3yu-product-detail-offer\",\n \"nb34u3yu-product-detail-bundle-furniture\"\n ]\n },\n {\n \"name\": \"bottomComponents\",\n \"type\": \"COMPONENT_LIST\",\n \"filteredComponentIds\": [\"nb34u3yu-product-detail-description\"]\n }\n ]\n}\n```\n\n### Section Layout\n\n```tsx\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport Breadcrumb from \"../../sub-components/Breadcrumb\";\nimport ProductGallery from \"./ProductGallery\"; // local component\nimport { Props } from \"./types\";\n\nexport function ProductDetail({ product, components, bottomComponents, backgroundColor, ...props }: Props) {\n if (!product) return null;\n return (\n <section className=\"product-detail\" style={backgroundColor ? { backgroundColor } : undefined}>\n <Breadcrumb product={product} />\n <div className=\"product-detail-inner\">\n <div className=\"product-detail-gallery\">\n <ProductGallery product={product} />\n </div>\n <div className=\"product-detail-info\">\n <IkasComponentRenderer id=\"product-detail-components\" components={components} parentProps={props} />\n </div>\n </div>\n <div className=\"product-detail-bottom\">\n <IkasComponentRenderer id=\"product-detail-bottom-components\" components={bottomComponents} parentProps={props} />\n </div>\n </section>\n );\n}\nexport default ProductDetail;\n```\n\n### 12 Child Components\n\n| Child Component | Purpose |\n|-----------------|--------|\n| ProductDetailNameFavorite | Product name + favorites toggle |\n| ProductDetailSku | SKU display |\n| ProductDetailPrices | Price display with discounts |\n| ProductDetailProductGroup | Product group/collection links |\n| ProductDetailVariant | Variant selector (size, color) |\n| ProductDetailAddToCart | Quantity selector + add-to-cart button |\n| ProductDetailFeatures | Feature list (uses FeatureItem children) |\n| ProductDetailDescription | Collapsible description (uses CollapsibleContent children) |\n| ProductDetailBundleProduct | Bundle product display |\n| ProductDetailBundleFurniture | Bundle furniture configuration |\n| ProductDetailOptionSet | Custom option sets |\n| ProductDetailOffer | Special offer display |\n\n### ProductGallery (Local Component)\n\nProductGallery is defined locally within the ProductDetail folder (not a registered component or sub-component). It handles image display with thumbnail navigation, zoom, and responsive srcset:\n\n```tsx\nconst images = (variant?.images ?? [])\n .map((pi) => pi.image)\n .filter((img): img is IkasImage => img != null);\n```\n\n### Breadcrumb Navigation\n\nThe Breadcrumb sub-component (from `src/sub-components/Breadcrumb/`) renders category path navigation:\n\n```tsx\nimport { getProductCategoryPath, getIkasCategoryHref } from \"@ikas/bp-storefront\";\nconst categoryPath = getProductCategoryPath(product);\n```\n\n### Key Pattern: Multiple COMPONENT_LIST Slots\n\nUsing two slots (`components` and `bottomComponents`) allows the store owner to arrange child components differently:\n- `components` — displayed next to the gallery in a side panel\n- `bottomComponents` — displayed full-width below the gallery\n\nThis pattern can be applied to any section that needs multiple layout regions.",
|
|
255
255
|
"tags": [
|
|
256
256
|
"product",
|
|
257
257
|
"detail",
|
|
@@ -266,36 +266,6 @@
|
|
|
266
266
|
"serel"
|
|
267
267
|
]
|
|
268
268
|
},
|
|
269
|
-
"back-in-stock-patterns": {
|
|
270
|
-
"title": "Back-in-Stock Notification Patterns",
|
|
271
|
-
"description": "Notify-me-when-available flow for out-of-stock variants: the login-required vs email-form branches, the notification form lifecycle, and the persistent reminder-saved state",
|
|
272
|
-
"content": "## Back-in-Stock Notification Patterns\n\nWhen a variant is out of stock, the add-to-cart CTA is a dead end unless you offer a \"notify me when it's back\" flow. ikas ships the full API for this, wired up in the `ProductDetailBackInStock` child of the product-detail section. Fetch it with:\n\n```\nget_section_child({ section: \"product-detail-section\", name: \"ProductDetailBackInStock\", kind: \"children\" })\n```\n\n### When to show it\n\nRender the notify-me UI only when the variant is out of stock AND back-in-stock is enabled — otherwise render nothing and let `ProductDetailAddToCart` own the CTA:\n\n```tsx\nconst selectedVariant = getSelectedProductVariant(product);\nconst isOutOfStock = !hasProductStock(product) || !hasProductVariantStock(selectedVariant);\nif (!isOutOfStock) return null;\nif (!getProductVariantIsBackInStockEnabled(selectedVariant)) return null;\n```\n\n### Two branches\n\n`getProductVariantIsBackInStockCustomerLoginRequired(variant)` selects the flow:\n\n- **Login required** — a logged-in customer is registered immediately with their own email; an anonymous visitor is sent to login first.\n- **No login required** — collect an email through the notification form.\n\n```tsx\nconst handleNotify = async () => {\n if (getProductVariantIsBackInStockCustomerLoginRequired(selectedVariant)) {\n const email = customerStore.customer?.email;\n if (email) {\n await saveProductVariantBackInStockReminder(selectedVariant, email);\n } else {\n Router.navigateToPage(\"LOGIN\");\n }\n return;\n }\n setEmailModalOpen(true); // anonymous email form\n};\n```\n\n### The email form (form lifecycle)\n\nThe no-login branch uses the standard ikas form model — get the form, init once, drive the input, submit:\n\n```tsx\nconst form = getProductVariantBackInStockForm(variant);\nuseEffect(() => { initBackInStockNotificationForm(form, variant); }, [form, variant]);\n\n<Input\n value={form.email?.value ?? \"\"}\n onInput={(e) => setBackInStockNotificationFormEmail(form, (e.target as HTMLInputElement).value)}\n/>\n\n// returns boolean; sets form.isSubmitting / isSuccess / isFailure / responseMessage\nconst ok = await submitBackInStockNotificationForm(form);\n```\n\nWrap the form component in `observer()` so it re-renders as the customer types and as the status flags flip — it reads the MobX-backed form.\n\n### Persistent \"saved\" state\n\n`variant.isBackInStockReminderSaved` is `true` once a reminder exists for that variant. Seed your CTA from it, and reset when the selected variant changes:\n\n```tsx\nconst [saved, setSaved] = useState(false);\nuseEffect(() => { setSaved(!!selectedVariant?.isBackInStockReminderSaved); }, [selectedVariant?.id]);\n```\n\nShow a non-interactive \"we'll notify you\" label instead of the button when `saved` is true.\n\n### Function reference\n\n| Function | Purpose |\n|----------|---------|\n| `getProductVariantIsBackInStockEnabled(variant)` | Gate: is back-in-stock enabled for this variant |\n| `getProductVariantIsBackInStockCustomerLoginRequired(variant)` | Branch: login-required vs email-form |\n| `saveProductVariantBackInStockReminder(variant, email)` | Register a reminder directly (login branch) |\n| `getProductVariantBackInStockForm(variant)` | Get-or-init the notification form model |\n| `initBackInStockNotificationForm(form, variant)` | Reset form fields + validation flags |\n| `setBackInStockNotificationFormEmail(form, value)` | Update the email field (re-validates after first submit) |\n| `submitBackInStockNotificationForm(form)` | Validate + save; returns boolean, sets status flags |\n| `variant.isBackInStockReminderSaved` | Persisted flag: a reminder already exists |",
|
|
273
|
-
"tags": [
|
|
274
|
-
"back-in-stock",
|
|
275
|
-
"notify-me",
|
|
276
|
-
"out-of-stock",
|
|
277
|
-
"variant",
|
|
278
|
-
"form",
|
|
279
|
-
"reminder",
|
|
280
|
-
"product-detail",
|
|
281
|
-
"conversion"
|
|
282
|
-
]
|
|
283
|
-
},
|
|
284
|
-
"product-options": {
|
|
285
|
-
"title": "Product Options (Customization) Patterns",
|
|
286
|
-
"description": "Personalization option sets: async load, type-aware rendering for all option types, conditional child options, file upload, and submit-time validation with reset",
|
|
287
|
-
"content": "## Product Options (Customization) Patterns\n\nProduct options let customers personalize a product before adding to cart — text, checkbox, choice, color/date picker, and file uploads, including parent/child (conditional) options and per-option prices. The reference theme implements the whole matrix in the `ProductDetailOptionSet` child of the product-detail section. Fetch it with:\n\n```\nget_section_child({ section: \"product-detail-section\", name: \"ProductDetailOptionSet\", kind: \"children\" })\n```\n\n### Load, then render displayed options\n\nThe option set is fetched async — render nothing until it resolves, then render only the currently displayed options (child options appear or hide based on the parent selection):\n\n```tsx\nuseEffect(() => { if (product) getProductOptionSet(product).then(() => setLoaded(true)); }, [product]);\n\nconst optionSet = product.productOptionSet;\nconst displayed = getDisplayedOptions(optionSet) as IkasProductOption[];\n```\n\n### Type-aware rendering\n\nEach option has a type — do not render everything as a text input. Switch on the SDK type guards and render a dedicated control per type:\n\n| Guard | Control |\n|-------|---------|\n| `isTextOption` / `isTextAreaOption` | text input / textarea |\n| `isCheckboxOption` | checkbox |\n| `isChoiceOption` | choice (box / select / swatch by display style) |\n| `isColorPickerOption` | color picker |\n| `isDatePickerOption` | date picker |\n| `isFileOption` | file upload |\n\nChild options render recursively from `getDisplayedChildOptions(option)`.\n\n### File upload\n\nFile options carry extension and min/max-count constraints. Upload through the SDK and surface its errors (file size, file type, max files):\n\n```tsx\nawait productOptionFileUpload(option, files);\n```\n\n### Validation + reset\n\nValidate on submit (used by add-to-cart), and reset after a successful add:\n\n```tsx\n// in add-to-cart, before adding:\nif (!validateOptionSet(product.productOptionSet, errorText)) return; // dispatches \"ikas:show-option-errors\"\n\n// after a successful add:\ninitProductOptionSetValues(product.productOptionSet);\nwindow.dispatchEvent(new CustomEvent(\"ikas:reset-option-state\"));\n```\n\nThe option-set component listens for `ikas:show-option-errors` / `ikas:reset-option-state` to toggle its error display; validation itself is `hasValidProductOptionSetValues(...)` under the hood.\n\n### Key rules\n\n1. Gate render on the async `getProductOptionSet(product)` resolving.\n2. Render from `getDisplayedOptions(...)`, never a hardcoded list — child visibility is dynamic.\n3. Switch on the type guards; each type has its own control.\n4. Validate with `validateOptionSet` at add-to-cart; reset with `initProductOptionSetValues` after a successful add.",
|
|
288
|
-
"tags": [
|
|
289
|
-
"product-options",
|
|
290
|
-
"customization",
|
|
291
|
-
"personalization",
|
|
292
|
-
"option-set",
|
|
293
|
-
"file-upload",
|
|
294
|
-
"child-options",
|
|
295
|
-
"validation",
|
|
296
|
-
"product-detail"
|
|
297
|
-
]
|
|
298
|
-
},
|
|
299
269
|
"product-list-patterns": {
|
|
300
270
|
"title": "Product List & Filtering Patterns",
|
|
301
271
|
"description": "Category pages, product filtering with display-type-aware rendering (swatch, number range, checkbox), sorting, pagination, and search patterns",
|
|
@@ -315,7 +285,7 @@
|
|
|
315
285
|
"cart-patterns": {
|
|
316
286
|
"title": "Cart Management Patterns (Serel Reference)",
|
|
317
287
|
"description": "Serel CartPage section with CartItem sub-component, OrderSummary/CouponCode/EmptyState local components, cartStore patterns, and checkout flow",
|
|
318
|
-
"content": "## Cart Management Patterns (Serel Reference)\n\nThe serel CartPage is a section that uses local internal components (OrderSummary, CouponCode, EmptyState) and the shared `CartItem` sub-component from `src/sub-components/CartItem/`. Unlike most sections, CartPage does NOT use IkasComponentRenderer — it directly composes its UI from sub-components.\n\n### CartPage Section Structure\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport CartItem from \"../../sub-components/CartItem\";\nimport { Props } from \"./types\";\n\nexport function CartPage({ backgroundColor, emptyCartText, ...props }: Props) {\n const cart = cartStore.cart;\n const lineItems = cart?.orderLineItems ?? [];\n const isEmpty = lineItems.length === 0;\n\n if (isEmpty) return <EmptyState text={emptyCartText} />;\n\n return (\n <section className=\"cart-page\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"cart-page-inner\">\n <div className=\"cart-items\">\n {lineItems.map((item) => (\n <CartItem key={item.id} item={item} />\n ))}\n </div>\n <div className=\"cart-sidebar\">\n <CouponCode cart={cart} />\n <OrderSummary cart={cart} />\n </div>\n </div>\n </section>\n );\n}\nexport default CartPage;\n```\n\n### CartItem Sub-Component\n\nThe `CartItem` sub-component (in `src/sub-components/CartItem/`) displays a single line item with image, variant name, quantity controls, price, and remove button:\n\n```tsx\nimport {\n getOrderLineItemFormattedFinalPrice,\n getOrderLineItemFormattedUnitPrice,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n changeItemQuantity,\n removeItem,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport QuantitySelector from \"../QuantitySelector\";\n\ninterface Props { item: IkasOrderLineItem; }\n\nexport default function CartItem({ item }: Props) {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n\n return (\n <div className=\"cart-item\">\n {image && <img src={getDefaultSrc(image)} alt={item.variant?.name} />}\n <div className=\"cart-item-info\">\n <span>{item.variant?.name}</span>\n <span>{getOrderLineItemFormattedUnitPrice(item)}</span>\n </div>\n <QuantitySelector\n quantity={item.quantity}\n onIncrement={() => changeItemQuantity(item, item.quantity + 1)}\n onDecrement={() => changeItemQuantity(item, Math.max(1, item.quantity - 1))}\n />\n <span>{getOrderLineItemFormattedFinalPrice(item)}</span>\n <button onClick={() => removeItem(item)}>Remove</button>\n </div>\n );\n}\n```\n\n### OrderSummary Local Component\n\nDisplays cart totals using `getIkasOrderFormattedTotalFinalPrice(cart)` and `getIkasOrderFormattedTotalPrice(cart)`, plus a checkout button:\n\n```tsx\nimport { Router, getIkasOrderFormattedTotalFinalPrice } from \"@ikas/bp-storefront\";\n\nfunction OrderSummary({ cart }) {\n return (\n <div className=\"order-summary\">\n <span>Total: {getIkasOrderFormattedTotalFinalPrice(cart)}</span>\n <button onClick={() => Router.navigateToPage(\"CHECKOUT\")}>Checkout</button>\n </div>\n );\n}\n```\n\n### CouponCode Local Component\n\nCoupon code uses the standard form model. Get the form from `customerStore` via `getCouponCodeForm`, init it once, drive the input with `setCouponCodeFormCouponCode`, and submit/remove with the form helpers. Do NOT call `saveCart`/`apiSaveCart` or any deep `@ikas/bp-storefront/dist/...` import — the coupon has dedicated public exports:\n\n```tsx\nimport { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getCouponCodeForm,\n initCouponCodeForm,\n setCouponCodeFormCouponCode,\n submitCouponCodeForm,\n removeCouponCodeForm,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\n\n// observer() because this sub-component reads the MobX coupon form\nconst CouponCode = observer(function CouponCode({ appliedCoupon }) {\n const couponForm = getCouponCodeForm(customerStore);\n\n useEffect(() => {\n initCouponCodeForm(couponForm);\n }, [couponForm]);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitCouponCodeForm(couponForm);\n };\n\n if (appliedCoupon) {\n return (\n <div className=\"coupon\">\n <span>{appliedCoupon}</span>\n <button type=\"button\" onClick={() => removeCouponCodeForm(couponForm)}>Remove</button>\n </div>\n );\n }\n\n return (\n <form className=\"coupon\" onSubmit={handleSubmit}>\n <input\n placeholder={couponForm.couponCode?.placeholder ?? \"Coupon code\"}\n value={couponForm.couponCode?.value ?? \"\"}\n onInput={(e) =>\n setCouponCodeFormCouponCode(couponForm, (e.target as HTMLInputElement).value)\n }\n />\n <button type=\"submit\" disabled={couponForm.isSubmitting}>\n {couponForm.isSubmitting ? \"Applying...\" : \"Apply\"}\n </button>\n {couponForm.isFailure && couponForm.responseMessage && (\n <p className=\"coupon-error\">{couponForm.responseMessage}</p>\n )}\n </form>\n );\n});\n```\n\n### cartStore Usage Patterns\n\nThe `cartStore` is a MobX store available globally. Root components are automatically reactive via autorun, so reads from `cartStore.cart` trigger re-renders:\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\n\n// Always null-safe:\nconst cart = cartStore.cart;\nconst lineItems = cart?.orderLineItems ?? [];\nconst itemCount = lineItems.length;\nconst isEmpty = itemCount === 0;\n```\n\n###
|
|
288
|
+
"content": "## Cart Management Patterns (Serel Reference)\n\nThe serel CartPage is a section that uses local internal components (OrderSummary, CouponCode, EmptyState) and the shared `CartItem` sub-component from `src/sub-components/CartItem/`. Unlike most sections, CartPage does NOT use IkasComponentRenderer — it directly composes its UI from sub-components.\n\n### CartPage Section Structure\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport CartItem from \"../../sub-components/CartItem\";\nimport { Props } from \"./types\";\n\nexport function CartPage({ backgroundColor, emptyCartText, ...props }: Props) {\n const cart = cartStore.cart;\n const lineItems = cart?.orderLineItems ?? [];\n const isEmpty = lineItems.length === 0;\n\n if (isEmpty) return <EmptyState text={emptyCartText} />;\n\n return (\n <section className=\"cart-page\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"cart-page-inner\">\n <div className=\"cart-items\">\n {lineItems.map((item) => (\n <CartItem key={item.id} item={item} />\n ))}\n </div>\n <div className=\"cart-sidebar\">\n <CouponCode cart={cart} />\n <OrderSummary cart={cart} />\n </div>\n </div>\n </section>\n );\n}\nexport default CartPage;\n```\n\n### CartItem Sub-Component\n\nThe `CartItem` sub-component (in `src/sub-components/CartItem/`) displays a single line item with image, variant name, quantity controls, price, and remove button:\n\n```tsx\nimport {\n getOrderLineItemFormattedFinalPrice,\n getOrderLineItemFormattedUnitPrice,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n changeItemQuantity,\n removeItem,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport QuantitySelector from \"../QuantitySelector\";\n\ninterface Props { item: IkasOrderLineItem; }\n\nexport default function CartItem({ item }: Props) {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n\n return (\n <div className=\"cart-item\">\n {image && <img src={getDefaultSrc(image)} alt={item.variant?.name} />}\n <div className=\"cart-item-info\">\n <span>{item.variant?.name}</span>\n <span>{getOrderLineItemFormattedUnitPrice(item)}</span>\n </div>\n <QuantitySelector\n quantity={item.quantity}\n onIncrement={() => changeItemQuantity(item, item.quantity + 1)}\n onDecrement={() => changeItemQuantity(item, Math.max(1, item.quantity - 1))}\n />\n <span>{getOrderLineItemFormattedFinalPrice(item)}</span>\n <button onClick={() => removeItem(item)}>Remove</button>\n </div>\n );\n}\n```\n\n### OrderSummary Local Component\n\nDisplays cart totals using `getIkasOrderFormattedTotalFinalPrice(cart)` and `getIkasOrderFormattedTotalPrice(cart)`, plus a checkout button:\n\n```tsx\nimport { Router, getIkasOrderFormattedTotalFinalPrice } from \"@ikas/bp-storefront\";\n\nfunction OrderSummary({ cart }) {\n return (\n <div className=\"order-summary\">\n <span>Total: {getIkasOrderFormattedTotalFinalPrice(cart)}</span>\n <button onClick={() => Router.navigateToPage(\"CHECKOUT\")}>Checkout</button>\n </div>\n );\n}\n```\n\n### CouponCode Local Component\n\nCoupon code uses the standard form model. Get the form from `customerStore` via `getCouponCodeForm`, init it once, drive the input with `setCouponCodeFormCouponCode`, and submit/remove with the form helpers. Do NOT call `saveCart`/`apiSaveCart` or any deep `@ikas/bp-storefront/dist/...` import — the coupon has dedicated public exports:\n\n```tsx\nimport { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getCouponCodeForm,\n initCouponCodeForm,\n setCouponCodeFormCouponCode,\n submitCouponCodeForm,\n removeCouponCodeForm,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\n\n// observer() because this sub-component reads the MobX coupon form\nconst CouponCode = observer(function CouponCode({ appliedCoupon }) {\n const couponForm = getCouponCodeForm(customerStore);\n\n useEffect(() => {\n initCouponCodeForm(couponForm);\n }, [couponForm]);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitCouponCodeForm(couponForm);\n };\n\n if (appliedCoupon) {\n return (\n <div className=\"coupon\">\n <span>{appliedCoupon}</span>\n <button type=\"button\" onClick={() => removeCouponCodeForm(couponForm)}>Remove</button>\n </div>\n );\n }\n\n return (\n <form className=\"coupon\" onSubmit={handleSubmit}>\n <input\n placeholder={couponForm.couponCode?.placeholder ?? \"Coupon code\"}\n value={couponForm.couponCode?.value ?? \"\"}\n onInput={(e) =>\n setCouponCodeFormCouponCode(couponForm, (e.target as HTMLInputElement).value)\n }\n />\n <button type=\"submit\" disabled={couponForm.isSubmitting}>\n {couponForm.isSubmitting ? \"Applying...\" : \"Apply\"}\n </button>\n {couponForm.isFailure && couponForm.responseMessage && (\n <p className=\"coupon-error\">{couponForm.responseMessage}</p>\n )}\n </form>\n );\n});\n```\n\n### cartStore Usage Patterns\n\nThe `cartStore` is a MobX store available globally. Root components are automatically reactive via autorun, so reads from `cartStore.cart` trigger re-renders:\n\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\n\n// Always null-safe:\nconst cart = cartStore.cart;\nconst lineItems = cart?.orderLineItems ?? [];\nconst itemCount = lineItems.length;\nconst isEmpty = itemCount === 0;\n```\n\n### Key Pattern: Section Without IkasComponentRenderer\n\nCartPage demonstrates that not all sections need COMPONENT_LIST slots. Simple sections can directly compose sub-components and local components when the layout is fixed and does not need editor-level child component placement.",
|
|
319
289
|
"tags": [
|
|
320
290
|
"cart",
|
|
321
291
|
"checkout",
|
|
@@ -10,14 +10,13 @@ import {
|
|
|
10
10
|
IkasBundleSettings,
|
|
11
11
|
IkasStorefrontConfig,
|
|
12
12
|
} from "@ikas/bp-storefront";
|
|
13
|
-
import {
|
|
13
|
+
import { useState } from "preact/hooks";
|
|
14
14
|
import { Props } from "./types";
|
|
15
15
|
import Button from "../../sub-components/Button";
|
|
16
16
|
import QuantitySelector from "../../sub-components/QuantitySelector";
|
|
17
17
|
import PayWithIkas from "./components/PayWithIkas";
|
|
18
18
|
import { isBundleOutOfStock } from "../../utils/bundle";
|
|
19
19
|
import { validateOptionSet } from "../../utils/optionSet";
|
|
20
|
-
import { getProductCartLimits } from "../../utils/cartLimits";
|
|
21
20
|
import { showToast } from "../../utils/toast";
|
|
22
21
|
|
|
23
22
|
export function ProductDetailAddToCart({
|
|
@@ -38,12 +37,6 @@ export function ProductDetailAddToCart({
|
|
|
38
37
|
const [quantity, setQuantity] = useState(1);
|
|
39
38
|
const [isAddingToCart, setIsAddingToCart] = useState(false);
|
|
40
39
|
|
|
41
|
-
// Seed the quantity to the product's minimum-per-cart when the product changes.
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
if (!product) return;
|
|
44
|
-
setQuantity(getProductCartLimits(product).min);
|
|
45
|
-
}, [product?.id]);
|
|
46
|
-
|
|
47
40
|
if (!product) return null;
|
|
48
41
|
|
|
49
42
|
const editLineID =
|
|
@@ -66,8 +59,6 @@ export function ProductDetailAddToCart({
|
|
|
66
59
|
|
|
67
60
|
const isDisabled = isOutOfStock || isAddingToCart;
|
|
68
61
|
|
|
69
|
-
const { min: minQuantity, max: maxQuantity } = getProductCartLimits(product);
|
|
70
|
-
|
|
71
62
|
const payWithIkasUrl = IkasStorefrontConfig.getPayWithIkasUrl();
|
|
72
63
|
const routing = IkasStorefrontConfig.getCurrentRouting();
|
|
73
64
|
const showPayWithIkas =
|
|
@@ -128,12 +119,7 @@ export function ProductDetailAddToCart({
|
|
|
128
119
|
>
|
|
129
120
|
<div className="kombos-pd-atc__actions">
|
|
130
121
|
{!hideQuantityInput && !isOutOfStock && (
|
|
131
|
-
<QuantitySelector
|
|
132
|
-
value={quantity}
|
|
133
|
-
onChange={setQuantity}
|
|
134
|
-
min={minQuantity}
|
|
135
|
-
max={maxQuantity}
|
|
136
|
-
/>
|
|
122
|
+
<QuantitySelector value={quantity} onChange={setQuantity} />
|
|
137
123
|
)}
|
|
138
124
|
<Button
|
|
139
125
|
variant="primary"
|
|
@@ -5,27 +5,20 @@ interface Props {
|
|
|
5
5
|
value: number;
|
|
6
6
|
onChange: (value: number) => void;
|
|
7
7
|
size?: "default" | "sm";
|
|
8
|
-
min?: number;
|
|
9
|
-
max?: number;
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
export default function QuantitySelector({
|
|
13
11
|
value,
|
|
14
12
|
onChange,
|
|
15
13
|
size = "default",
|
|
16
|
-
min = 1,
|
|
17
|
-
max,
|
|
18
14
|
}: Props) {
|
|
19
|
-
const canDecrease = value > min;
|
|
20
|
-
const canIncrease = max == null || value < max;
|
|
21
|
-
|
|
22
15
|
return (
|
|
23
16
|
<div className={cx("kombos-pd__qty", size === "sm" && "kombos-pd__qty--sm")}>
|
|
24
17
|
<button
|
|
25
18
|
type="button"
|
|
26
19
|
className="kombos-pd__qty-btn"
|
|
27
|
-
onClick={() => onChange(Math.max(
|
|
28
|
-
disabled={
|
|
20
|
+
onClick={() => onChange(Math.max(1, value - 1))}
|
|
21
|
+
disabled={value <= 1}
|
|
29
22
|
aria-label="Decrease quantity"
|
|
30
23
|
>
|
|
31
24
|
<MinusSVG />
|
|
@@ -38,8 +31,7 @@ export default function QuantitySelector({
|
|
|
38
31
|
<button
|
|
39
32
|
type="button"
|
|
40
33
|
className="kombos-pd__qty-btn"
|
|
41
|
-
onClick={() => onChange(
|
|
42
|
-
disabled={!canIncrease}
|
|
34
|
+
onClick={() => onChange(value + 1)}
|
|
43
35
|
aria-label="Increase quantity"
|
|
44
36
|
>
|
|
45
37
|
<PlusSVG />
|
|
@@ -14,8 +14,6 @@ import {
|
|
|
14
14
|
getOrderLineItemFormattedOverridenPriceWithQuantity,
|
|
15
15
|
hasOrderLineItemDiscount,
|
|
16
16
|
getOrderLineItemOverridenPriceWithQuantity,
|
|
17
|
-
cartStore,
|
|
18
|
-
isOrderLineItemAutoCreated,
|
|
19
17
|
} from "@ikas/bp-storefront";
|
|
20
18
|
import { resolveAspectRatio, resolveObjectFit } from "../../utils/media";
|
|
21
19
|
import { cx } from "../../utils/cx";
|
|
@@ -27,7 +25,6 @@ interface CartItemProps {
|
|
|
27
25
|
variant?: "sidebar" | "page";
|
|
28
26
|
aspectRatio?: AspectRatio;
|
|
29
27
|
objectFit?: ObjectFit;
|
|
30
|
-
giftLabel?: string;
|
|
31
28
|
}
|
|
32
29
|
|
|
33
30
|
const CartItem = observer(function CartItem({
|
|
@@ -36,25 +33,9 @@ const CartItem = observer(function CartItem({
|
|
|
36
33
|
variant = "sidebar",
|
|
37
34
|
aspectRatio,
|
|
38
35
|
objectFit,
|
|
39
|
-
giftLabel = "Gift",
|
|
40
36
|
}: CartItemProps) {
|
|
41
37
|
const isPage = variant === "page";
|
|
42
38
|
|
|
43
|
-
// Lines a campaign added automatically (e.g. a "buy 1 get 1" gift) cannot be
|
|
44
|
-
// edited or removed on their own, so render them read-only with a badge.
|
|
45
|
-
const isAutoCreated =
|
|
46
|
-
!!cartStore.cart && isOrderLineItemAutoCreated(cartStore.cart, item);
|
|
47
|
-
const giftBadge = isAutoCreated ? (
|
|
48
|
-
<span className="kombos-cart-item__gift-badge text-xs-medium">
|
|
49
|
-
{giftLabel}
|
|
50
|
-
</span>
|
|
51
|
-
) : null;
|
|
52
|
-
const staticQuantity = (
|
|
53
|
-
<span className="kombos-cart-item__qty-static text-sm-medium">
|
|
54
|
-
×{item.quantity}
|
|
55
|
-
</span>
|
|
56
|
-
);
|
|
57
|
-
|
|
58
39
|
const mediaStyle: Record<string, string> = {
|
|
59
40
|
aspectRatio: resolveAspectRatio(aspectRatio),
|
|
60
41
|
objectFit: resolveObjectFit(objectFit),
|
|
@@ -122,7 +103,6 @@ const CartItem = observer(function CartItem({
|
|
|
122
103
|
<a href={href} className={nameClass}>
|
|
123
104
|
{item.variant?.name}
|
|
124
105
|
</a>
|
|
125
|
-
{giftBadge}
|
|
126
106
|
{variantValues.length > 0 && (
|
|
127
107
|
<p className="kombos-cart-item__variant-text text-sm-regular">
|
|
128
108
|
{variantValues
|
|
@@ -133,22 +113,19 @@ const CartItem = observer(function CartItem({
|
|
|
133
113
|
</p>
|
|
134
114
|
)}
|
|
135
115
|
</div>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
</button>
|
|
144
|
-
)}
|
|
116
|
+
<button
|
|
117
|
+
className="kombos-cart-item__remove"
|
|
118
|
+
onClick={() => onRemove(item)}
|
|
119
|
+
aria-label="Remove"
|
|
120
|
+
>
|
|
121
|
+
<TrashSVG />
|
|
122
|
+
</button>
|
|
145
123
|
</div>
|
|
146
124
|
) : (
|
|
147
125
|
<>
|
|
148
126
|
<a href={href} className={nameClass}>
|
|
149
127
|
{item.variant?.name}
|
|
150
128
|
</a>
|
|
151
|
-
{giftBadge}
|
|
152
129
|
|
|
153
130
|
{variantValues.length > 0 && (
|
|
154
131
|
<div className="kombos-cart-item__variants">
|
|
@@ -174,15 +151,11 @@ const CartItem = observer(function CartItem({
|
|
|
174
151
|
|
|
175
152
|
{isPage ? (
|
|
176
153
|
<div className="kombos-cart-item__bottom">
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
value={item.quantity}
|
|
183
|
-
onChange={(qty) => changeItemQuantity(item, qty)}
|
|
184
|
-
/>
|
|
185
|
-
)}
|
|
154
|
+
<QuantitySelector
|
|
155
|
+
size="sm"
|
|
156
|
+
value={item.quantity}
|
|
157
|
+
onChange={(qty) => changeItemQuantity(item, qty)}
|
|
158
|
+
/>
|
|
186
159
|
<div className="kombos-cart-item__prices">
|
|
187
160
|
{hasDiscount && (
|
|
188
161
|
<span className="kombos-cart-item__original-price text-sm-regular-strike">
|
|
@@ -220,24 +193,18 @@ const CartItem = observer(function CartItem({
|
|
|
220
193
|
</div>
|
|
221
194
|
|
|
222
195
|
<div className="kombos-cart-item__actions">
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
aria-label="Remove"
|
|
236
|
-
>
|
|
237
|
-
<TrashSVG />
|
|
238
|
-
</button>
|
|
239
|
-
</>
|
|
240
|
-
)}
|
|
196
|
+
<QuantitySelector
|
|
197
|
+
size="sm"
|
|
198
|
+
value={item.quantity}
|
|
199
|
+
onChange={(qty) => changeItemQuantity(item, qty)}
|
|
200
|
+
/>
|
|
201
|
+
<button
|
|
202
|
+
className="kombos-cart-item__remove"
|
|
203
|
+
onClick={() => onRemove(item)}
|
|
204
|
+
aria-label="Remove"
|
|
205
|
+
>
|
|
206
|
+
<TrashSVG />
|
|
207
|
+
</button>
|
|
241
208
|
</div>
|
|
242
209
|
</>
|
|
243
210
|
)}
|
|
@@ -168,17 +168,3 @@
|
|
|
168
168
|
flex-shrink: 0;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
/* Auto-created (campaign gift) line: badge + read-only quantity */
|
|
172
|
-
.kombos-cart-item__gift-badge {
|
|
173
|
-
display: inline-flex;
|
|
174
|
-
align-self: flex-start;
|
|
175
|
-
padding: 0.125rem 0.5rem;
|
|
176
|
-
border-radius: 999px;
|
|
177
|
-
background: var(--kombos-gray-100);
|
|
178
|
-
color: var(--kombos-gray-500);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.kombos-cart-item__qty-static {
|
|
182
|
-
color: var(--kombos-gray-500);
|
|
183
|
-
}
|
|
184
|
-
|
|
@@ -5,27 +5,20 @@ interface Props {
|
|
|
5
5
|
value: number;
|
|
6
6
|
onChange: (value: number) => void;
|
|
7
7
|
size?: "default" | "sm";
|
|
8
|
-
min?: number;
|
|
9
|
-
max?: number;
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
export default function QuantitySelector({
|
|
13
11
|
value,
|
|
14
12
|
onChange,
|
|
15
13
|
size = "default",
|
|
16
|
-
min = 1,
|
|
17
|
-
max,
|
|
18
14
|
}: Props) {
|
|
19
|
-
const canDecrease = value > min;
|
|
20
|
-
const canIncrease = max == null || value < max;
|
|
21
|
-
|
|
22
15
|
return (
|
|
23
16
|
<div className={cx("kombos-pd__qty", size === "sm" && "kombos-pd__qty--sm")}>
|
|
24
17
|
<button
|
|
25
18
|
type="button"
|
|
26
19
|
className="kombos-pd__qty-btn"
|
|
27
|
-
onClick={() => onChange(Math.max(
|
|
28
|
-
disabled={
|
|
20
|
+
onClick={() => onChange(Math.max(1, value - 1))}
|
|
21
|
+
disabled={value <= 1}
|
|
29
22
|
aria-label="Decrease quantity"
|
|
30
23
|
>
|
|
31
24
|
<MinusSVG />
|
|
@@ -38,8 +31,7 @@ export default function QuantitySelector({
|
|
|
38
31
|
<button
|
|
39
32
|
type="button"
|
|
40
33
|
className="kombos-pd__qty-btn"
|
|
41
|
-
onClick={() => onChange(
|
|
42
|
-
disabled={!canIncrease}
|
|
34
|
+
onClick={() => onChange(value + 1)}
|
|
43
35
|
aria-label="Increase quantity"
|
|
44
36
|
>
|
|
45
37
|
<PlusSVG />
|
|
@@ -14,8 +14,6 @@ import {
|
|
|
14
14
|
getOrderLineItemFormattedOverridenPriceWithQuantity,
|
|
15
15
|
hasOrderLineItemDiscount,
|
|
16
16
|
getOrderLineItemOverridenPriceWithQuantity,
|
|
17
|
-
cartStore,
|
|
18
|
-
isOrderLineItemAutoCreated,
|
|
19
17
|
} from "@ikas/bp-storefront";
|
|
20
18
|
import { resolveAspectRatio, resolveObjectFit } from "../../utils/media";
|
|
21
19
|
import { cx } from "../../utils/cx";
|
|
@@ -27,7 +25,6 @@ interface CartItemProps {
|
|
|
27
25
|
variant?: "sidebar" | "page";
|
|
28
26
|
aspectRatio?: AspectRatio;
|
|
29
27
|
objectFit?: ObjectFit;
|
|
30
|
-
giftLabel?: string;
|
|
31
28
|
}
|
|
32
29
|
|
|
33
30
|
const CartItem = observer(function CartItem({
|
|
@@ -36,25 +33,9 @@ const CartItem = observer(function CartItem({
|
|
|
36
33
|
variant = "sidebar",
|
|
37
34
|
aspectRatio,
|
|
38
35
|
objectFit,
|
|
39
|
-
giftLabel = "Gift",
|
|
40
36
|
}: CartItemProps) {
|
|
41
37
|
const isPage = variant === "page";
|
|
42
38
|
|
|
43
|
-
// Lines a campaign added automatically (e.g. a "buy 1 get 1" gift) cannot be
|
|
44
|
-
// edited or removed on their own, so render them read-only with a badge.
|
|
45
|
-
const isAutoCreated =
|
|
46
|
-
!!cartStore.cart && isOrderLineItemAutoCreated(cartStore.cart, item);
|
|
47
|
-
const giftBadge = isAutoCreated ? (
|
|
48
|
-
<span className="kombos-cart-item__gift-badge text-xs-medium">
|
|
49
|
-
{giftLabel}
|
|
50
|
-
</span>
|
|
51
|
-
) : null;
|
|
52
|
-
const staticQuantity = (
|
|
53
|
-
<span className="kombos-cart-item__qty-static text-sm-medium">
|
|
54
|
-
×{item.quantity}
|
|
55
|
-
</span>
|
|
56
|
-
);
|
|
57
|
-
|
|
58
39
|
const mediaStyle: Record<string, string> = {
|
|
59
40
|
aspectRatio: resolveAspectRatio(aspectRatio),
|
|
60
41
|
objectFit: resolveObjectFit(objectFit),
|
|
@@ -122,7 +103,6 @@ const CartItem = observer(function CartItem({
|
|
|
122
103
|
<a href={href} className={nameClass}>
|
|
123
104
|
{item.variant?.name}
|
|
124
105
|
</a>
|
|
125
|
-
{giftBadge}
|
|
126
106
|
{variantValues.length > 0 && (
|
|
127
107
|
<p className="kombos-cart-item__variant-text text-sm-regular">
|
|
128
108
|
{variantValues
|
|
@@ -133,22 +113,19 @@ const CartItem = observer(function CartItem({
|
|
|
133
113
|
</p>
|
|
134
114
|
)}
|
|
135
115
|
</div>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
</button>
|
|
144
|
-
)}
|
|
116
|
+
<button
|
|
117
|
+
className="kombos-cart-item__remove"
|
|
118
|
+
onClick={() => onRemove(item)}
|
|
119
|
+
aria-label="Remove"
|
|
120
|
+
>
|
|
121
|
+
<TrashSVG />
|
|
122
|
+
</button>
|
|
145
123
|
</div>
|
|
146
124
|
) : (
|
|
147
125
|
<>
|
|
148
126
|
<a href={href} className={nameClass}>
|
|
149
127
|
{item.variant?.name}
|
|
150
128
|
</a>
|
|
151
|
-
{giftBadge}
|
|
152
129
|
|
|
153
130
|
{variantValues.length > 0 && (
|
|
154
131
|
<div className="kombos-cart-item__variants">
|
|
@@ -174,15 +151,11 @@ const CartItem = observer(function CartItem({
|
|
|
174
151
|
|
|
175
152
|
{isPage ? (
|
|
176
153
|
<div className="kombos-cart-item__bottom">
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
value={item.quantity}
|
|
183
|
-
onChange={(qty) => changeItemQuantity(item, qty)}
|
|
184
|
-
/>
|
|
185
|
-
)}
|
|
154
|
+
<QuantitySelector
|
|
155
|
+
size="sm"
|
|
156
|
+
value={item.quantity}
|
|
157
|
+
onChange={(qty) => changeItemQuantity(item, qty)}
|
|
158
|
+
/>
|
|
186
159
|
<div className="kombos-cart-item__prices">
|
|
187
160
|
{hasDiscount && (
|
|
188
161
|
<span className="kombos-cart-item__original-price text-sm-regular-strike">
|
|
@@ -220,24 +193,18 @@ const CartItem = observer(function CartItem({
|
|
|
220
193
|
</div>
|
|
221
194
|
|
|
222
195
|
<div className="kombos-cart-item__actions">
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
aria-label="Remove"
|
|
236
|
-
>
|
|
237
|
-
<TrashSVG />
|
|
238
|
-
</button>
|
|
239
|
-
</>
|
|
240
|
-
)}
|
|
196
|
+
<QuantitySelector
|
|
197
|
+
size="sm"
|
|
198
|
+
value={item.quantity}
|
|
199
|
+
onChange={(qty) => changeItemQuantity(item, qty)}
|
|
200
|
+
/>
|
|
201
|
+
<button
|
|
202
|
+
className="kombos-cart-item__remove"
|
|
203
|
+
onClick={() => onRemove(item)}
|
|
204
|
+
aria-label="Remove"
|
|
205
|
+
>
|
|
206
|
+
<TrashSVG />
|
|
207
|
+
</button>
|
|
241
208
|
</div>
|
|
242
209
|
</>
|
|
243
210
|
)}
|
|
@@ -168,17 +168,3 @@
|
|
|
168
168
|
flex-shrink: 0;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
/* Auto-created (campaign gift) line: badge + read-only quantity */
|
|
172
|
-
.kombos-cart-item__gift-badge {
|
|
173
|
-
display: inline-flex;
|
|
174
|
-
align-self: flex-start;
|
|
175
|
-
padding: 0.125rem 0.5rem;
|
|
176
|
-
border-radius: 999px;
|
|
177
|
-
background: var(--kombos-gray-100);
|
|
178
|
-
color: var(--kombos-gray-500);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.kombos-cart-item__qty-static {
|
|
182
|
-
color: var(--kombos-gray-500);
|
|
183
|
-
}
|
|
184
|
-
|
|
@@ -5,27 +5,20 @@ interface Props {
|
|
|
5
5
|
value: number;
|
|
6
6
|
onChange: (value: number) => void;
|
|
7
7
|
size?: "default" | "sm";
|
|
8
|
-
min?: number;
|
|
9
|
-
max?: number;
|
|
10
8
|
}
|
|
11
9
|
|
|
12
10
|
export default function QuantitySelector({
|
|
13
11
|
value,
|
|
14
12
|
onChange,
|
|
15
13
|
size = "default",
|
|
16
|
-
min = 1,
|
|
17
|
-
max,
|
|
18
14
|
}: Props) {
|
|
19
|
-
const canDecrease = value > min;
|
|
20
|
-
const canIncrease = max == null || value < max;
|
|
21
|
-
|
|
22
15
|
return (
|
|
23
16
|
<div className={cx("kombos-pd__qty", size === "sm" && "kombos-pd__qty--sm")}>
|
|
24
17
|
<button
|
|
25
18
|
type="button"
|
|
26
19
|
className="kombos-pd__qty-btn"
|
|
27
|
-
onClick={() => onChange(Math.max(
|
|
28
|
-
disabled={
|
|
20
|
+
onClick={() => onChange(Math.max(1, value - 1))}
|
|
21
|
+
disabled={value <= 1}
|
|
29
22
|
aria-label="Decrease quantity"
|
|
30
23
|
>
|
|
31
24
|
<MinusSVG />
|
|
@@ -38,8 +31,7 @@ export default function QuantitySelector({
|
|
|
38
31
|
<button
|
|
39
32
|
type="button"
|
|
40
33
|
className="kombos-pd__qty-btn"
|
|
41
|
-
onClick={() => onChange(
|
|
42
|
-
disabled={!canIncrease}
|
|
34
|
+
onClick={() => onChange(value + 1)}
|
|
43
35
|
aria-label="Increase quantity"
|
|
44
36
|
>
|
|
45
37
|
<PlusSVG />
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
"<id-of-ProductDetailProductGroup>",
|
|
40
40
|
"<id-of-ProductDetailVariant>",
|
|
41
41
|
"<id-of-ProductDetailAddToCart>",
|
|
42
|
-
"<id-of-ProductDetailBackInStock>",
|
|
43
42
|
"<id-of-ProductDetailFeatures>",
|
|
44
43
|
"<id-of-ProductDetailDescription>",
|
|
45
44
|
"<id-of-ProductDetailBundleProduct>",
|