@ikas/code-components-mcp 0.31.0 → 0.33.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/data/framework.json
CHANGED
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"common-pitfalls": {
|
|
139
139
|
"title": "Common Pitfalls",
|
|
140
140
|
"description": "Frequent mistakes LLMs and developers make when building ikas code components",
|
|
141
|
-
"content": "## Common Pitfalls\n\nThese are the most common mistakes when building ikas code components. Avoid them for correct, working code.\n\n### 1. Root Component Should NOT Use observer\n\nThe ikas runtime wraps root component renders in `autorun()`, making them automatically reactive. Wrapping a root export with `observer()` is redundant and misleading.\n\n**Wrong** — observer on root export:\n```tsx\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\n\nconst CartSection = observer(function CartSection({ title }: Props) {\n const items = cartStore.cart?.orderLineItems ?? [];\n return <section>{title}: {items.length} items</section>;\n});\nexport default CartSection;\n```\n\n**Correct** — plain named export:\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\n\nexport default function CartSection({ title }: Props) {\n const items = cartStore.cart?.orderLineItems ?? [];\n return <section>{title}: {items.length} items</section>;\n}\n```\n\n### 2. Observer Sub-Component Naming\n\nWhen using `observer()` on **sub-components**, use a named function expression — not an arrow function — for proper DevTools display names.\n\n**Wrong** — arrow function loses display name:\n```tsx\nconst CartBadge = observer(() => {\n return <span>{cartStore.cart?.orderLineItems.length ?? 0}</span>;\n});\n```\n\n**Correct** — named function expression:\n```tsx\nconst CartBadge = observer(function CartBadge() {\n return <span>{cartStore.cart?.orderLineItems.length ?? 0}</span>;\n});\n```\n\n### 3. Mutation Semantics\n\nMany storefront functions (122+) return `void` and **mutate their arguments in place**. Do NOT try to capture a return value:\n\n```tsx\n// WRONG — selectVariantValue returns void, not a new product\nconst updated = selectVariantValue(product, value);\n\n// CORRECT — mutates product in place, observer re-renders automatically\nselectVariantValue(product, dvv.variantValue);\n```\n\nOther mutation functions: `initLoginForm()`, `setLoginFormEmail()`, `clearFilter()`, `selectFilterValue()`.\n\n### 4. CSS Scoping Limits\n\nOnly **class selectors** in `styles.css` are reliably scoped. Element selectors are NOT scoped:\n\n```css\n/* SAFE — scoped to your component */\n.product-card { padding: 16px; }\n.product-card .title { font-size: 18px; }\n\n/* UNSAFE — NOT reliably scoped, may affect other components */\ndiv { margin: 0; }\nh1 { font-size: 24px; }\n```\n\nAlways use class selectors for all styles.\n\n### 5. Prop Null Handling\n\nProps from the editor can be `undefined` when the store owner hasn't set them. Always use optional chaining:\n\n```tsx\n// WRONG — will crash if product is undefined\n<h1>{props.product.name}</h1>\n\n// CORRECT\n<h1>{props.product?.name}</h1>\n{props.heroImage && <img src={getDefaultSrc(props.heroImage)} />}\n```\n\n### 6. IkasProductImage vs IkasImage\n\n`variant.images` is `IkasProductImage[]`, NOT `IkasImage[]`. You must access the `.image` property to get the `IkasImage` needed by CDN helpers:\n\n```tsx\n// WRONG — productImage is IkasProductImage, not IkasImage\ngetDefaultSrc(productImage);\n\n// CORRECT — access .image to get IkasImage\ngetDefaultSrc(productImage.image);\n\n// Full pattern:\nconst images: IkasImage[] = variant.images\n .map((pi) => pi.image)\n .filter((img): img is IkasImage => img != null);\n```\n\n### 7. Type Assertion Pattern\n\nSome storefront functions have type inference gaps. Use `as unknown as` casts when needed — this is a known pattern:\n\n```tsx\nconst inStock = hasProductStock(product) as unknown as boolean;\nconst finalPrice = getProductVariantFormattedFinalPrice(variant) as unknown as string;\nconst canAddToCart = isAddToCartEnabled(product) as unknown as boolean;\n```\n\nThis applies to functions like `hasProductStock`, `hasProductVariantStock`, `isAddToCartEnabled`, `hasProductVariantDiscount`, `getProductVariantDiscountPercentage`, `getProductVariantFormattedFinalPrice`, `getProductVariantFormattedSellPrice`, `getProductVariantFormattedDiscountAmount`, and `getProductVariantFormattedCampaignPrice`.\n\nNote: `getProductVariantMainImage` returns `IkasProductImage | undefined` (NOT `IkasImage`). Access `.image` to get the `IkasImage` for CDN helpers like `getDefaultSrc()`.\n\n### 8. Store Data Null Safety\n\nStore data (`customerStore.customer`, `cartStore.cart`, `baseStore`) is `null` before initialization completes. Always guard access:\n\n```tsx\n// WRONG — crashes if customer is null\n<h1>{customerStore.customer.firstName}</h1>\n\n// CORRECT — null check first\nif (!customerStore.customer) return <div>Loading...</div>;\n<h1>{customerStore.customer.firstName}</h1>\n\n// Also correct — optional chaining\n<h1>{customerStore.customer?.firstName ?? \"Guest\"}</h1>\n```\n\nSame for `cartStore.cart` — always use `cartStore.cart?.orderLineItems ?? []`.\n\n### 9. ProductList/BlogList Data Access\n\n`productList.
|
|
141
|
+
"content": "## Common Pitfalls\n\nThese are the most common mistakes when building ikas code components. Avoid them for correct, working code.\n\n### 1. Root Component Should NOT Use observer\n\nThe ikas runtime wraps root component renders in `autorun()`, making them automatically reactive. Wrapping a root export with `observer()` is redundant and misleading.\n\n**Wrong** — observer on root export:\n```tsx\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\n\nconst CartSection = observer(function CartSection({ title }: Props) {\n const items = cartStore.cart?.orderLineItems ?? [];\n return <section>{title}: {items.length} items</section>;\n});\nexport default CartSection;\n```\n\n**Correct** — plain named export:\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\n\nexport default function CartSection({ title }: Props) {\n const items = cartStore.cart?.orderLineItems ?? [];\n return <section>{title}: {items.length} items</section>;\n}\n```\n\n### 2. Observer Sub-Component Naming\n\nWhen using `observer()` on **sub-components**, use a named function expression — not an arrow function — for proper DevTools display names.\n\n**Wrong** — arrow function loses display name:\n```tsx\nconst CartBadge = observer(() => {\n return <span>{cartStore.cart?.orderLineItems.length ?? 0}</span>;\n});\n```\n\n**Correct** — named function expression:\n```tsx\nconst CartBadge = observer(function CartBadge() {\n return <span>{cartStore.cart?.orderLineItems.length ?? 0}</span>;\n});\n```\n\n### 3. Mutation Semantics\n\nMany storefront functions (122+) return `void` and **mutate their arguments in place**. Do NOT try to capture a return value:\n\n```tsx\n// WRONG — selectVariantValue returns void, not a new product\nconst updated = selectVariantValue(product, value);\n\n// CORRECT — mutates product in place, observer re-renders automatically\nselectVariantValue(product, dvv.variantValue);\n```\n\nOther mutation functions: `initLoginForm()`, `setLoginFormEmail()`, `clearFilter()`, `selectFilterValue()`.\n\n### 4. CSS Scoping Limits\n\nOnly **class selectors** in `styles.css` are reliably scoped. Element selectors are NOT scoped:\n\n```css\n/* SAFE — scoped to your component */\n.product-card { padding: 16px; }\n.product-card .title { font-size: 18px; }\n\n/* UNSAFE — NOT reliably scoped, may affect other components */\ndiv { margin: 0; }\nh1 { font-size: 24px; }\n```\n\nAlways use class selectors for all styles.\n\n### 5. Prop Null Handling\n\nProps from the editor can be `undefined` when the store owner hasn't set them. Always use optional chaining:\n\n```tsx\n// WRONG — will crash if product is undefined\n<h1>{props.product.name}</h1>\n\n// CORRECT\n<h1>{props.product?.name}</h1>\n{props.heroImage && <img src={getDefaultSrc(props.heroImage)} />}\n```\n\n### 6. IkasProductImage vs IkasImage\n\n`variant.images` is `IkasProductImage[]`, NOT `IkasImage[]`. You must access the `.image` property to get the `IkasImage` needed by CDN helpers:\n\n```tsx\n// WRONG — productImage is IkasProductImage, not IkasImage\ngetDefaultSrc(productImage);\n\n// CORRECT — access .image to get IkasImage\ngetDefaultSrc(productImage.image);\n\n// Full pattern:\nconst images: IkasImage[] = variant.images\n .map((pi) => pi.image)\n .filter((img): img is IkasImage => img != null);\n```\n\n### 7. Type Assertion Pattern\n\nSome storefront functions have type inference gaps. Use `as unknown as` casts when needed — this is a known pattern:\n\n```tsx\nconst inStock = hasProductStock(product) as unknown as boolean;\nconst finalPrice = getProductVariantFormattedFinalPrice(variant) as unknown as string;\nconst canAddToCart = isAddToCartEnabled(product) as unknown as boolean;\n```\n\nThis applies to functions like `hasProductStock`, `hasProductVariantStock`, `isAddToCartEnabled`, `hasProductVariantDiscount`, `getProductVariantDiscountPercentage`, `getProductVariantFormattedFinalPrice`, `getProductVariantFormattedSellPrice`, `getProductVariantFormattedDiscountAmount`, and `getProductVariantFormattedCampaignPrice`.\n\nNote: `getProductVariantMainImage` returns `IkasProductImage | undefined` (NOT `IkasImage`). Access `.image` to get the `IkasImage` for CDN helpers like `getDefaultSrc()`.\n\n### 8. Store Data Null Safety\n\nStore data (`customerStore.customer`, `cartStore.cart`, `baseStore`) is `null` before initialization completes. Always guard access:\n\n```tsx\n// WRONG — crashes if customer is null\n<h1>{customerStore.customer.firstName}</h1>\n\n// CORRECT — null check first\nif (!customerStore.customer) return <div>Loading...</div>;\n<h1>{customerStore.customer.firstName}</h1>\n\n// Also correct — optional chaining\n<h1>{customerStore.customer?.firstName ?? \"Guest\"}</h1>\n```\n\nSame for `cartStore.cart` — always use `cartStore.cart?.orderLineItems ?? []`.\n\n### 9. ProductList/BlogList Data Access\n\n`productList.data` is the correct way to access products in a product list. Similarly, `blogList.data` for blogs. The display names `.products` / `.blogs` are only used by the blueprint editor — they do NOT exist at runtime:\n\n```tsx\n// CORRECT — use .data for both product lists and blog lists\nconst products = productList?.data ?? [];\nconst blogs = blogList?.data ?? [];\n\n// WRONG — .products / .blogs are display names, not actual fields\nconst products = productList?.products ?? [];\nconst blogs = blogList?.blogs ?? [];\n```\n\n### 10. Form Field Access Pattern\n\nForm fields are objects with `.value`, `.label`, `.hasError`, `.message`. Never access the field directly as a primitive:\n\n```tsx\n// WRONG — loginForm.email is a field object, not a string\n<input value={loginForm.email} />\n\n// CORRECT — access .value for the actual value\n<input value={loginForm.email.value} />\n{loginForm.email.hasError && <span>{loginForm.email.message}</span>}\n```\n\n### 11. Optional Chaining for Editor Props\n\nAll props from `ikas.config.json` can be `undefined` in the editor before the store owner sets them. Always use optional chaining and defaults:\n\n```tsx\n// WRONG — will crash in the editor\n<h1>{props.title}</h1>\n<img src={getDefaultSrc(props.image)} />\n{props.links.links.map(...)}\n\n// CORRECT — safe access with defaults\n<h1>{props.title ?? \"Default Title\"}</h1>\n{props.image && <img src={getDefaultSrc(props.image)} />}\n{(props.links?.links ?? []).map(...)}\n```\n\n### 12. Event Handler Typing\n\nPreact uses different event types than React. Use `(e: Event)` not `(e: React.ChangeEvent)`. Access values with casting. Preact uses `onInput` not `onChange` for text inputs:\n\n```tsx\n// WRONG — React patterns don't work in Preact\nonChange={(e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}\n\n// CORRECT — Preact event handling\nonInput={(e: Event) => setValue((e.target as HTMLInputElement).value)}\n```\n\nFor select elements:\n```tsx\nonChange={(e: Event) => setOption((e.target as HTMLSelectElement).value)}\n```\n\n### 13. Function Parameter Order\n\nMany storefront functions take specific parameter orders. Always verify with `get_function_doc()` before using:\n\n```tsx\n// WRONG — submitLoginForm only takes the form, not the store\nsubmitLoginForm(customerStore, loginForm);\n\n// CORRECT\nsubmitLoginForm(loginForm);\n\n// WRONG — wrong parameter order for addItemToCart\naddItemToCart(product, variant, 1);\n\n// CORRECT — variant first, then product, then quantity\naddItemToCart(variant, product, 1);\n\n// WRONG — selectVariantValue takes product and variantValue\nselectVariantValue(variant, value);\n\n// CORRECT\nselectVariantValue(product, dvv.variantValue);\n```\n\nWhen in doubt, use the `get_function_doc(functionName)` MCP tool to check the exact signature.",
|
|
142
142
|
"tags": [
|
|
143
143
|
"pitfalls",
|
|
144
144
|
"gotchas",
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
"sub-component-patterns": {
|
|
157
157
|
"title": "Sub-Component File Organization",
|
|
158
158
|
"description": "How to organize sub-components in their own folders under src/sub-components/ with proper imports and CSS",
|
|
159
|
-
"content": "## Sub-Component File Organization\n\n**ALWAYS create sub-components in `src/sub-components/` with their own folder containing `index.tsx` and `styles.css`. NEVER create flat .tsx files inside a component folder.**\n\n### Directory Structure\n\n```\nsrc/\n├── components/\n│ ├── ProductList/\n│ │ ├── index.tsx # imports from ../../sub-components/...\n│ │ ├── types.ts # auto-generated (CLI-managed)\n│ │ └── styles.css # @import \"../../sub-components/.../styles.css\"\n│ └── index.ts\n└── sub-components/\n ├── ProductCard/\n │ ├── index.tsx # sub-component\n │ └── styles.css # sub-component styles\n └── FilterSidebar/\n ├── index.tsx # sub-component\n └── styles.css # sub-component styles\n```\n\n### Key Rules\n\n1. **`src/components/`** = registered components (listed in `ikas.config.json`)\n2. **`src/sub-components/`** = internal helpers (NOT in `ikas.config.json`)\n3. Sub-components do **NOT** have `types.ts` — define `Props` interface inline in `index.tsx`\n4. Sub-components that read MobX stores need `observer()` from `@ikas/component-utils`\n5. Sub-components that only receive props do NOT need `observer()`\n6. CSS is scoped with the parent component's `.cc_` prefix (same scope — no conflicts)\n7. Sub-components can be shared across multiple parent sections\n\n### CSS Import Pattern\n\nIn the parent component's `styles.css`, import sub-component styles with `@import`:\n\n```css\n/* src/components/ProductList/styles.css */\n@import \"../../sub-components/ProductCard/styles.css\";\n@import \"../../sub-components/FilterSidebar/styles.css\";\n\n.product-list-section {\n width: 100%;\n padding: 40px 24px;\n}\n```\n\nThe build system's CSS import resolver recursively resolves `@import` statements with relative paths. All imported CSS gets scoped with the parent component's `.cc_{componentId}` prefix.\n\n### TSX Import Pattern\n\n```tsx\n// src/components/ProductList/index.tsx\nimport ProductCard from \"../../sub-components/ProductCard\";\nimport FilterSidebar from \"../../sub-components/FilterSidebar\";\nimport { Props } from \"./types\";\n\nexport default function ProductListSection({ productList, title }: Props) {\n const products = productList?.
|
|
159
|
+
"content": "## Sub-Component File Organization\n\n**ALWAYS create sub-components in `src/sub-components/` with their own folder containing `index.tsx` and `styles.css`. NEVER create flat .tsx files inside a component folder.**\n\n### Directory Structure\n\n```\nsrc/\n├── components/\n│ ├── ProductList/\n│ │ ├── index.tsx # imports from ../../sub-components/...\n│ │ ├── types.ts # auto-generated (CLI-managed)\n│ │ └── styles.css # @import \"../../sub-components/.../styles.css\"\n│ └── index.ts\n└── sub-components/\n ├── ProductCard/\n │ ├── index.tsx # sub-component\n │ └── styles.css # sub-component styles\n └── FilterSidebar/\n ├── index.tsx # sub-component\n └── styles.css # sub-component styles\n```\n\n### Key Rules\n\n1. **`src/components/`** = registered components (listed in `ikas.config.json`)\n2. **`src/sub-components/`** = internal helpers (NOT in `ikas.config.json`)\n3. Sub-components do **NOT** have `types.ts` — define `Props` interface inline in `index.tsx`\n4. Sub-components that read MobX stores need `observer()` from `@ikas/component-utils`\n5. Sub-components that only receive props do NOT need `observer()`\n6. CSS is scoped with the parent component's `.cc_` prefix (same scope — no conflicts)\n7. Sub-components can be shared across multiple parent sections\n\n### CSS Import Pattern\n\nIn the parent component's `styles.css`, import sub-component styles with `@import`:\n\n```css\n/* src/components/ProductList/styles.css */\n@import \"../../sub-components/ProductCard/styles.css\";\n@import \"../../sub-components/FilterSidebar/styles.css\";\n\n.product-list-section {\n width: 100%;\n padding: 40px 24px;\n}\n```\n\nThe build system's CSS import resolver recursively resolves `@import` statements with relative paths. All imported CSS gets scoped with the parent component's `.cc_{componentId}` prefix.\n\n### TSX Import Pattern\n\n```tsx\n// src/components/ProductList/index.tsx\nimport ProductCard from \"../../sub-components/ProductCard\";\nimport FilterSidebar from \"../../sub-components/FilterSidebar\";\nimport { Props } from \"./types\";\n\nexport default function ProductListSection({ productList, title }: Props) {\n const products = productList?.data ?? [];\n return (\n <section className=\"product-list-section\">\n <FilterSidebar productList={productList} />\n <div className=\"product-grid\">\n {products.map((product) => (\n <ProductCard key={product.id} product={product} />\n ))}\n </div>\n </section>\n );\n}\n```\n\n### Sub-Component Example\n\n```tsx\n// src/sub-components/ProductCard/index.tsx\nimport {\n IkasProduct,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\ninterface Props {\n product: IkasProduct;\n}\n\nexport default function ProductCard({ product }: Props) {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n\n return (\n <a href={getSelectedProductVariantHref(product)} className=\"product-card\">\n {image && <img src={getDefaultSrc(image)} alt={product.name} className=\"product-card-image\" />}\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </a>\n );\n}\n```\n\nNotice:\n- `Props` is defined inline — no separate `types.ts` file\n- No `observer()` needed because this component only uses props passed from the parent\n- Default export like registered components\n\n### When to Extract Sub-Components\n\n- A section's `index.tsx` exceeds ~150 lines\n- A distinct UI block (card, sidebar, modal) has its own styles and logic\n- The same UI block is used by multiple parent sections\n- A part of the component independently reads MobX stores (extract + wrap with `observer()`)\n\n### When observer() Is Needed on Sub-Components\n\n```tsx\n// src/sub-components/CartBadge/index.tsx\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\n\n// This sub-component independently reads a MobX store → needs observer()\nconst CartBadge = observer(function CartBadge() {\n const count = cartStore.cart?.orderLineItems.length ?? 0;\n return <span className=\"cart-badge\">{count}</span>;\n});\n\nexport default CartBadge;\n```",
|
|
160
160
|
"tags": [
|
|
161
161
|
"sub-component",
|
|
162
162
|
"folder",
|
|
@@ -226,8 +226,8 @@
|
|
|
226
226
|
},
|
|
227
227
|
"product-list-patterns": {
|
|
228
228
|
"title": "Product List & Filtering Patterns",
|
|
229
|
-
"description": "Category pages, product filtering, sorting, pagination, and search patterns",
|
|
230
|
-
"content": "## Product List & Filtering Patterns\n\nProduct list sections power category pages, search results, and collection displays. They combine filtering, sorting, and pagination.\n\n### Product List Prop Setup\n\nIn `ikas.config.json`, use the `PRODUCT_LIST` prop type:\n```json\n{\n \"name\": \"productList\",\n \"displayName\": \"Product List\",\n \"type\": \"PRODUCT_LIST\",\n \"required\": true\n}\n```\n\nThis provides an `IkasProductList` object with products, filters, sorting, and pagination data.\n\n### Displaying Products\n\n```tsx\nimport {\n IkasProductList,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\n// In your component:\nconst products = productList?.
|
|
229
|
+
"description": "Category pages, product filtering with display-type-aware rendering (swatch, number range, checkbox), sorting, pagination, and search patterns",
|
|
230
|
+
"content": "## Product List & Filtering Patterns\n\nProduct list sections power category pages, search results, and collection displays. They combine filtering, sorting, and pagination.\n\n### Product List Prop Setup\n\nIn `ikas.config.json`, use the `PRODUCT_LIST` prop type:\n```json\n{\n \"name\": \"productList\",\n \"displayName\": \"Product List\",\n \"type\": \"PRODUCT_LIST\",\n \"required\": true\n}\n```\n\nThis provides an `IkasProductList` object with products, filters, sorting, and pagination data.\n\n### Displaying Products\n\n```tsx\nimport {\n IkasProductList,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\n// In your component:\nconst products = productList?.data ?? [];\n\n{products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const href = getSelectedProductVariantHref(product);\n\n return (\n <a key={product.id} href={href} className=\"product-card\">\n {image && <img src={getDefaultSrc(image)} alt={product.name} />}\n <h3>{product.name}</h3>\n <span>{price}</span>\n </a>\n );\n})}\n```\n\n### Filtering\n\nFilters let customers narrow down products by attributes (size, color, price, etc.). Each filter has a `displayType` that determines how it should be rendered. **Always check the display type and render accordingly** — do not render all filters as checkboxes.\n\n#### Display Types\n\n- **BOX / LIST** — Standard checkbox filters (size, brand, etc.)\n- **SWATCH** — Color/pattern swatches with `fv.colorCode` or thumbnail images\n- **NUMBER_RANGE_LIST** — Predefined price/number range buttons (e.g., $0-$50, $50-$100)\n\n#### Universal Data Pattern\n\nThe data flow is the same for all display types:\n- `getFilterDisplayedValues(filter)` — get sorted filter values (works for ALL types)\n- `handleFilterValueClick(productList, filter, filterValue)` — toggle selection + auto-refetch (works for BOX/LIST/SWATCH)\n- `handleNumberRangeOptionClick(productList, filter, option)` — toggle a range option + auto-refetch (for NUMBER_RANGE_LIST)\n\n#### Imports\n\n```tsx\nimport {\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n getProductListFilterCategories,\n isBoxFilter,\n isListFilter,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getDefaultSrc,\n IkasProductList,\n IkasProductFilter,\n} from \"@ikas/bp-storefront\";\n```\n\n#### Display-Type-Aware Rendering\n\n```tsx\nconst filters = productList.filters ?? [];\n\n{filters.map((filter) => {\n const values = getFilterDisplayedValues(filter);\n if (values.length === 0 && !filter.numberRangeListOptions?.length) return null;\n\n return (\n <div key={filter.id}>\n <h4>{filter.name}</h4>\n\n {/* SWATCH: render color circles / thumbnail images */}\n {isSwatchFilter(filter) ? (\n <div className=\"swatch-grid\">\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button\n key={fv.name}\n className={fv.isSelected === true ? \"swatch selected\" : \"swatch\"}\n onClick={() => handleFilterValueClick(productList, filter, fv)}\n title={fv.name}\n >\n {thumbnail ? (\n <img src={getDefaultSrc(thumbnail)} alt={fv.name} />\n ) : (\n <span\n className=\"swatch-color\"\n style={{ backgroundColor: fv.colorCode ?? \"#ccc\" }}\n />\n )}\n </button>\n );\n })}\n </div>\n ) : (\n /* BOX / LIST: render checkboxes */\n values.map((fv) => (\n <label key={fv.name}>\n <input\n type=\"checkbox\"\n checked={fv.isSelected === true}\n onChange={() => handleFilterValueClick(productList, filter, fv)}\n />\n {fv.name} {fv.count != null && `(${fv.count})`}\n </label>\n ))\n )}\n\n {/* NUMBER_RANGE_LIST: render predefined range buttons */}\n {filter.numberRangeListOptions?.map((option) => (\n <button\n key={`${option.from}-${option.to}`}\n className={option.isSelected ? \"range-btn selected\" : \"range-btn\"}\n onClick={() => handleNumberRangeOptionClick(productList, filter, option)}\n >\n {option.from} - {option.to ?? \"+\"}\n </button>\n ))}\n </div>\n );\n})}\n```\n\n### Sorting\n\n```tsx\nimport { getProductListSortOptions, setSortType, IkasProductListSortType } from \"@ikas/bp-storefront\";\n\nconst sortOptions = getProductListSortOptions(productList);\n\n<select\n value={productList.sort}\n onChange={(e) => {\n setSortType(productList, (e.target as HTMLSelectElement).value as IkasProductListSortType);\n }}\n>\n {sortOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n</select>\n```\n\n### Pagination\n\n```tsx\nimport {\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n} from \"@ikas/bp-storefront\";\n\nconst hasNext = hasProductListNextPage(productList);\nconst hasPrev = hasProductListPrevPage(productList);\n\n<div className=\"pagination\">\n <button disabled={!hasPrev} onClick={() => getProductListPrevPage(productList)}>\n Previous\n </button>\n <button disabled={!hasNext} onClick={() => getProductListNextPage(productList)}>\n Next\n </button>\n</div>\n```\n\n### Key Functions Reference\n\n| Function | Purpose |\n|----------|---------|\n| `getProductListFilterCategories(list)` | Get available filter categories |\n| `getFilterDisplayedValues(filter)` | Get sorted filter values — works for ALL display types |\n| `handleFilterValueClick(list, filter, value)` | Toggle a filter value + auto-refetch (BOX/LIST/SWATCH) |\n| `handleNumberRangeOptionClick(list, filter, option)` | Toggle a range option + auto-refetch (NUMBER_RANGE_LIST) |\n| `isBoxFilter(filter)` | Check if filter displayType is BOX |\n| `isListFilter(filter)` | Check if filter displayType is LIST |\n| `isSwatchFilter(filter)` | Check if filter displayType is SWATCH |\n| `isCustomValueFilter(filter)` | Check if filter displayType is CUSTOM_VALUE |\n| `getIkasFilterThumbnailImage(fv)` | Get swatch thumbnail IkasImage from a filter value |\n| `getProductListSortOptions(list)` | Get sort dropdown options |\n| `hasProductListNextPage(list)` / `getProductListNextPage(list)` | Next page |\n| `hasProductListPrevPage(list)` / `getProductListPrevPage(list)` | Previous page |",
|
|
231
231
|
"tags": [
|
|
232
232
|
"product-list",
|
|
233
233
|
"filtering",
|
|
@@ -235,7 +235,9 @@
|
|
|
235
235
|
"pagination",
|
|
236
236
|
"category",
|
|
237
237
|
"search",
|
|
238
|
-
"grid"
|
|
238
|
+
"grid",
|
|
239
|
+
"swatch",
|
|
240
|
+
"filter-display-type"
|
|
239
241
|
]
|
|
240
242
|
},
|
|
241
243
|
"cart-patterns": {
|
|
@@ -312,7 +314,7 @@
|
|
|
312
314
|
"blog-patterns": {
|
|
313
315
|
"title": "Blog Patterns",
|
|
314
316
|
"description": "Blog list display, blog post detail, pagination, date formatting, and category filtering",
|
|
315
|
-
"content": "## Blog Patterns\n\nBlog sections display articles with pagination and optional category filtering.\n\n### Blog List Prop Setup\n\nIn `ikas.config.json`, use the `BLOG_POST_LIST` prop type or a dedicated blog list config:\n\n```json\n{\n \"name\": \"blogList\",\n \"displayName\": \"Blog List\",\n \"type\": \"BLOG_POST_LIST\"\n}\n```\n\n### Blog List Display\n\n```tsx\nimport {\n IkasBlogList,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n hasBlogListNextPage,\n getBlogListNextPage,\n hasBlogListPrevPage,\n getBlogListPrevPage,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\n// In your component:\nconst blogs = blogList?.
|
|
317
|
+
"content": "## Blog Patterns\n\nBlog sections display articles with pagination and optional category filtering.\n\n### Blog List Prop Setup\n\nIn `ikas.config.json`, use the `BLOG_POST_LIST` prop type or a dedicated blog list config:\n\n```json\n{\n \"name\": \"blogList\",\n \"displayName\": \"Blog List\",\n \"type\": \"BLOG_POST_LIST\"\n}\n```\n\n### Blog List Display\n\n```tsx\nimport {\n IkasBlogList,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n hasBlogListNextPage,\n getBlogListNextPage,\n hasBlogListPrevPage,\n getBlogListPrevPage,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\n// In your component:\nconst blogs = blogList?.data ?? [];\n\n{blogs.map((blog) => (\n <a key={blog.id} href={getIkasBlogHref(blog)} className=\"blog-card\">\n {blog.image && (\n <img src={getDefaultSrc(blog.image)} alt={blog.title} />\n )}\n <div className=\"blog-card-content\">\n <span className=\"blog-date\">{getIkasBlogFormattedDate(blog)}</span>\n <h3>{blog.title}</h3>\n <p>{blog.summary}</p>\n </div>\n </a>\n))}\n```\n\n### Blog Pagination\n\n```tsx\nconst hasNext = hasBlogListNextPage(blogList);\nconst hasPrev = hasBlogListPrevPage(blogList);\n\n<div className=\"blog-pagination\">\n {hasPrev && (\n <button onClick={() => getBlogListPrevPage(blogList)}>Previous</button>\n )}\n {hasNext && (\n <button onClick={() => getBlogListNextPage(blogList)}>Next</button>\n )}\n</div>\n```\n\n### Blog Post Detail\n\nFor a blog post detail page, use the `BLOG_POST` prop type:\n\n```json\n{\n \"name\": \"blogPost\",\n \"displayName\": \"Blog Post\",\n \"type\": \"BLOG_POST\"\n}\n```\n\n```tsx\ninterface Props {\n blogPost: IkasBlogPost;\n}\n\nfunction BlogPostDetail({ blogPost }: Props) {\n if (!blogPost) return null;\n\n return (\n <article className=\"blog-post\">\n {blogPost.image && (\n <img src={getDefaultSrc(blogPost.image)} alt={blogPost.title} />\n )}\n <h1>{blogPost.title}</h1>\n <span className=\"date\">{getIkasBlogFormattedDate(blogPost)}</span>\n <div\n className=\"blog-content\"\n dangerouslySetInnerHTML={{ __html: blogPost.content }}\n />\n </article>\n );\n}\n```\n\n### Key Functions\n\n| Function | Purpose |\n|----------|---------|\n| `getIkasBlogFormattedDate(blog)` | Format blog post date |\n| `getIkasBlogHref(blog)` | Get URL for a blog post |\n| `hasBlogListNextPage(list)` | Check if next page exists |\n| `getBlogListNextPage(list)` | Fetch next page (mutates list) |\n| `hasBlogListPrevPage(list)` | Check if previous page exists |\n| `getBlogListPrevPage(list)` | Fetch previous page (mutates list) |",
|
|
316
318
|
"tags": [
|
|
317
319
|
"blog",
|
|
318
320
|
"article",
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
"title": "Product List Section",
|
|
45
45
|
"description": "Product grid with filters, sorting, and pagination. Demonstrates sub-component pattern: ProductCard and FilterSidebar are extracted into src/sub-components/.",
|
|
46
46
|
"files": {
|
|
47
|
-
"index.tsx": "import {\n getProductListSortOptions,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n} from \"@ikas/bp-storefront\";\nimport ProductCard from \"../../sub-components/ProductCard\";\nimport FilterSidebar from \"../../sub-components/FilterSidebar\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function ProductListSection({\n productList,\n title = \"Products\",\n showFilters = true,\n}: Props) {\n if (!productList) return null;\n\n const products = productList.
|
|
47
|
+
"index.tsx": "import {\n IkasProductListSortType,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n} from \"@ikas/bp-storefront\";\nimport ProductCard from \"../../sub-components/ProductCard\";\nimport FilterSidebar from \"../../sub-components/FilterSidebar\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function ProductListSection({\n productList,\n title = \"Products\",\n showFilters = true,\n}: Props) {\n if (!productList) return null;\n\n const products = productList.data ?? [];\n const sortOptions = getProductListSortOptions(productList);\n const hasNext = hasProductListNextPage(productList);\n const hasPrev = hasProductListPrevPage(productList);\n\n return (\n <section className=\"product-list-section\">\n <div className=\"product-list-inner\">\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">{title}</h1>\n {sortOptions.length > 0 && (\n <select className=\"product-list-sort\" value={productList.sort} onChange={(e) => { setSortType(productList, (e.target as HTMLSelectElement).value as IkasProductListSortType); }}>\n {sortOptions.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}\n </select>\n )}\n </div>\n <div className=\"product-list-layout\">\n {showFilters && <FilterSidebar productList={productList} />}\n <div className=\"product-grid\">\n {products.length === 0 && <p className=\"product-grid-empty\">No products found.</p>}\n {products.map((product) => (\n <ProductCard key={product.id} product={product} />\n ))}\n </div>\n </div>\n {(hasPrev || hasNext) && (\n <div className=\"product-list-pagination\">\n <button disabled={!hasPrev} onClick={() => getProductListPrevPage(productList)}>Previous</button>\n <button disabled={!hasNext} onClick={() => getProductListNextPage(productList)}>Next</button>\n </div>\n )}\n </div>\n </section>\n );\n}\n",
|
|
48
48
|
"types.ts": "import { IkasProductList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n productList: IkasProductList;\n title?: string;\n showFilters?: boolean;\n}\n",
|
|
49
49
|
"styles.css": "@import \"../../sub-components/ProductCard/styles.css\";\n@import \"../../sub-components/FilterSidebar/styles.css\";\n\n.product-list-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.product-list-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.product-list-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 24px;\n}\n\n.product-list-title { font-size: 24px; font-weight: 700; color: #111; margin: 0; }\n.product-list-sort { padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }\n\n.product-list-layout {\n display: flex;\n gap: 32px;\n}\n\n.product-grid {\n flex: 1;\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 24px;\n}\n\n.product-grid-empty { font-size: 16px; color: #666; grid-column: 1 / -1; text-align: center; padding: 48px 0; }\n\n.product-list-pagination { display: flex; justify-content: center; gap: 12px; margin-top: 32px; }\n.product-list-pagination button { padding: 10px 20px; border: 1px solid #ddd; border-radius: 6px; background: #fff; cursor: pointer; font-size: 14px; }\n.product-list-pagination button:disabled { opacity: 0.4; cursor: not-allowed; }\n\n@media (max-width: 768px) {\n .product-list-filters { display: none; }\n .product-grid { grid-template-columns: repeat(2, 1fr); gap: 16px; }\n}\n",
|
|
50
50
|
"ikas-config-snippet.json": "{\n \"id\": \"product-list\",\n \"name\": \"Product List\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"productList\", \"displayName\": \"Product List\", \"type\": \"PRODUCT_LIST\", \"required\": true },\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"Products\" },\n { \"name\": \"showFilters\", \"displayName\": \"Show Filters\", \"type\": \"BOOLEAN\", \"defaultValue\": true }\n ]\n}\n",
|
|
51
51
|
"src/sub-components/ProductCard/index.tsx": "import {\n IkasProduct,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\n// Sub-component: Props defined inline (no types.ts needed)\ninterface Props {\n product: IkasProduct;\n}\n\n// No observer() needed — this component only uses props from parent\nexport default function ProductCard({ product }: Props) {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n\n return (\n <a href={getSelectedProductVariantHref(product)} className=\"product-card\">\n {image && <img src={getDefaultSrc(image)} alt={product.name} className=\"product-card-image\" />}\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </a>\n );\n}\n",
|
|
52
52
|
"src/sub-components/ProductCard/styles.css": ".product-card { text-decoration: none; color: inherit; }\n.product-card-image { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 8px; background: #f5f5f5; }\n.product-card-name { font-size: 14px; font-weight: 500; color: #111; margin: 10px 0 4px; }\n.product-card-price { font-size: 14px; font-weight: 600; color: #111; }\n",
|
|
53
|
-
"src/sub-components/FilterSidebar/index.tsx": "import {\n IkasProductList,\n getFilterDisplayedValues,\n handleFilterValueClick,\n
|
|
54
|
-
"src/sub-components/FilterSidebar/styles.css": ".product-list-filters { width: 220px; flex-shrink: 0; }\n.filter-group { margin-bottom: 24px; }\n.filter-group-title { font-size: 14px; font-weight: 600; margin: 0 0 12px 0; }\n.filter-value { display: flex; align-items: center; gap: 8px; font-size: 14px; color: #555; cursor: pointer; padding: 4px 0; }\n"
|
|
53
|
+
"src/sub-components/FilterSidebar/index.tsx": "import {\n IkasProductList,\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\n// Sub-component: Props defined inline (no types.ts needed)\ninterface Props {\n productList: IkasProductList;\n}\n\n// No observer() needed — this component only uses props from parent\nexport default function FilterSidebar({ productList }: Props) {\n const filters = productList.filters ?? [];\n if (filters.length === 0) return null;\n\n return (\n <aside className=\"product-list-filters\">\n {filters.map((filter) => {\n const values = getFilterDisplayedValues(filter);\n if (values.length === 0 && !filter.numberRangeListOptions?.length) return null;\n\n return (\n <div key={filter.id} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{filter.name}</h3>\n {isSwatchFilter(filter) ? (\n <div className=\"filter-swatches\">\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button key={fv.name} className={`filter-swatch${fv.isSelected === true ? \" selected\" : \"\"}`} onClick={() => handleFilterValueClick(productList, filter, fv)} title={fv.name}>\n {thumbnail ? <img src={getDefaultSrc(thumbnail)} alt={fv.name} /> : <span className=\"filter-swatch-color\" style={{ backgroundColor: fv.colorCode ?? \"#ccc\" }} />}\n </button>\n );\n })}\n </div>\n ) : (\n values.map((fv) => (\n <label key={fv.name} className=\"filter-value\">\n <input type=\"checkbox\" checked={fv.isSelected === true} onChange={() => handleFilterValueClick(productList, filter, fv)} />\n <span>{fv.name}</span>\n {fv.resultCount != null && <span className=\"filter-count\">({fv.resultCount})</span>}\n </label>\n ))\n )}\n {filter.numberRangeListOptions?.map((option) => (\n <button key={`${option.from}-${option.to}`} className={`filter-range-btn${option.isSelected ? \" selected\" : \"\"}`} onClick={() => handleNumberRangeOptionClick(productList, filter, option)}>\n {option.from} - {option.to ?? \"+\"}\n </button>\n ))}\n </div>\n );\n })}\n </aside>\n );\n}\n",
|
|
54
|
+
"src/sub-components/FilterSidebar/styles.css": ".product-list-filters { width: 220px; flex-shrink: 0; }\n.filter-group { margin-bottom: 24px; }\n.filter-group-title { font-size: 14px; font-weight: 600; margin: 0 0 12px 0; }\n.filter-value { display: flex; align-items: center; gap: 8px; font-size: 14px; color: #555; cursor: pointer; padding: 4px 0; }\n.filter-count { color: #999; font-size: 12px; }\n.filter-swatches { display: flex; flex-wrap: wrap; gap: 8px; }\n.filter-swatch { width: 32px; height: 32px; border-radius: 50%; border: 2px solid #ddd; padding: 0; cursor: pointer; overflow: hidden; background: none; }\n.filter-swatch.selected { border-color: #000; }\n.filter-swatch img { width: 100%; height: 100%; object-fit: cover; }\n.filter-swatch-color { display: block; width: 100%; height: 100%; border-radius: 50%; }\n.filter-range-btn { padding: 6px 12px; border: 1px solid #ddd; border-radius: 4px; background: #fff; cursor: pointer; font-size: 13px; margin: 4px 4px 4px 0; }\n.filter-range-btn.selected { border-color: #000; font-weight: 600; }\n"
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
"cart": {
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"title": "Blog List Section",
|
|
149
149
|
"description": "Blog post grid with images, dates, summaries, and pagination",
|
|
150
150
|
"files": {
|
|
151
|
-
"index.tsx": "import {\n hasBlogListNextPage,\n getBlogListNextPage,\n hasBlogListPrevPage,\n getBlogListPrevPage,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function BlogListSection({\n blogList,\n title = \"Blog\",\n}: Props) {\n if (!blogList) return null;\n\n const blogs = blogList.
|
|
151
|
+
"index.tsx": "import {\n hasBlogListNextPage,\n getBlogListNextPage,\n hasBlogListPrevPage,\n getBlogListPrevPage,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// NOTE: Do NOT wrap this root export with observer(). The ikas runtime handles\n// root reactivity via autorun(). Only use observer() on extracted sub-components.\nexport default function BlogListSection({\n blogList,\n title = \"Blog\",\n}: Props) {\n if (!blogList) return null;\n\n const blogs = blogList.data ?? [];\n const hasNext = hasBlogListNextPage(blogList);\n const hasPrev = hasBlogListPrevPage(blogList);\n\n return (\n <section className=\"blog-list-section\">\n <div className=\"blog-list-inner\">\n <h1 className=\"blog-list-title\">{title}</h1>\n {blogs.length === 0 && <p className=\"blog-list-empty\">No blog posts found.</p>}\n <div className=\"blog-grid\">\n {blogs.map((blog) => (\n <a key={blog.id} href={getIkasBlogHref(blog)} className=\"blog-card\">\n {blog.image && (\n <div className=\"blog-card-image-wrap\">\n <img src={getDefaultSrc(blog.image)} alt={blog.title} className=\"blog-card-image\" />\n </div>\n )}\n <div className=\"blog-card-content\">\n <span className=\"blog-card-date\">{getIkasBlogFormattedDate(blog)}</span>\n <h3 className=\"blog-card-title\">{blog.title}</h3>\n {blog.summary && <p className=\"blog-card-summary\">{blog.summary}</p>}\n <span className=\"blog-card-read-more\">Read more</span>\n </div>\n </a>\n ))}\n </div>\n {(hasPrev || hasNext) && (\n <div className=\"blog-pagination\">\n <button className=\"blog-pagination-btn\" disabled={!hasPrev} onClick={() => getBlogListPrevPage(blogList)}>Previous</button>\n <button className=\"blog-pagination-btn\" disabled={!hasNext} onClick={() => getBlogListNextPage(blogList)}>Next</button>\n </div>\n )}\n </div>\n </section>\n );\n}\n",
|
|
152
152
|
"types.ts": "import { IkasBlogList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogList: IkasBlogList;\n title?: string;\n}\n",
|
|
153
153
|
"styles.css": ".blog-list-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.blog-list-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.blog-list-title { font-size: 24px; font-weight: 700; color: #111; margin: 0 0 24px 0; }\n.blog-list-empty { font-size: 16px; color: #666; text-align: center; padding: 48px 0; }\n\n.blog-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 32px;\n}\n\n.blog-card { text-decoration: none; color: inherit; }\n.blog-card-image-wrap { border-radius: 8px; overflow: hidden; margin-bottom: 16px; }\n.blog-card-image { width: 100%; aspect-ratio: 16/9; object-fit: cover; display: block; }\n.blog-card-date { font-size: 13px; color: #999; }\n.blog-card-title { font-size: 18px; font-weight: 600; color: #111; margin: 6px 0 8px; }\n.blog-card-summary { font-size: 14px; color: #666; line-height: 1.5; margin: 0 0 8px; }\n.blog-card-read-more { font-size: 14px; font-weight: 600; color: #111; }\n\n.blog-pagination { display: flex; justify-content: center; gap: 12px; margin-top: 32px; }\n.blog-pagination-btn { padding: 10px 20px; border: 1px solid #ddd; border-radius: 6px; background: #fff; cursor: pointer; font-size: 14px; }\n.blog-pagination-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n\n@media (max-width: 768px) {\n .blog-grid { grid-template-columns: 1fr; gap: 24px; }\n}\n",
|
|
154
154
|
"ikas-config-snippet.json": "{\n \"id\": \"blog-list\",\n \"name\": \"Blog List\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"blogList\", \"displayName\": \"Blog List\", \"type\": \"BLOG_POST_LIST\", \"required\": true },\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"Blog\" }\n ]\n}\n"
|
package/data/storefront-api.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-02-
|
|
2
|
+
"generatedAt": "2026-02-20T06:42:35.816Z",
|
|
3
3
|
"functions": [
|
|
4
4
|
{
|
|
5
5
|
"name": "apiListBlog",
|
|
@@ -17022,13 +17022,20 @@
|
|
|
17022
17022
|
{
|
|
17023
17023
|
"id": "product-filtering",
|
|
17024
17024
|
"title": "Product List Filtering",
|
|
17025
|
-
"description": "Display
|
|
17026
|
-
"code": "import {\n getFilterDisplayedValues,\n handleFilterValueClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n IkasProductList,\n IkasProductFilter,\n} from \"@ikas/bp-storefront\";\n\nexport default function ProductFilter({\n productList,\n filter,\n}: {\n productList: IkasProductList;\n filter: IkasProductFilter;\n}) {\n const values = getFilterDisplayedValues(filter);\n const filterCategories = getProductListFilterCategories(productList);\n\n return (\n <div>\n <h3>{filter.name}</h3>\n\n {/*
|
|
17025
|
+
"description": "Display-type-aware product filtering: checkboxes for BOX/LIST filters, color swatches for SWATCH filters (using getIkasFilterThumbnailImage and fv.colorCode), range buttons for NUMBER_RANGE_LIST filters (using handleNumberRangeOptionClick), and category-level filtering with onFilterCategoryClick.",
|
|
17026
|
+
"code": "import {\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getDefaultSrc,\n IkasProductList,\n IkasProductFilter,\n} from \"@ikas/bp-storefront\";\n\n/**\n * Display-type-aware product filter.\n *\n * Data flow is the same for every display type:\n * getFilterDisplayedValues(filter) → get sorted values\n * handleFilterValueClick(list, filter, fv) → toggle + refetch\n *\n * Only the **visual rendering** changes based on displayType:\n * BOX / LIST → checkboxes\n * SWATCH → color circles or thumbnail images\n * NUMBER_RANGE_LIST → predefined range buttons\n */\nexport default function ProductFilter({\n productList,\n filter,\n}: {\n productList: IkasProductList;\n filter: IkasProductFilter;\n}) {\n const values = getFilterDisplayedValues(filter);\n const filterCategories = getProductListFilterCategories(productList);\n\n return (\n <div>\n <h3>{filter.name}</h3>\n\n {/* SWATCH: render color circles / thumbnail images */}\n {isSwatchFilter(filter) ? (\n <div style={{ display: \"flex\", gap: 8, flexWrap: \"wrap\" }}>\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button\n key={fv.name}\n onClick={() => handleFilterValueClick(productList, filter, fv)}\n title={fv.name}\n style={{\n width: 32,\n height: 32,\n borderRadius: \"50%\",\n border: fv.isSelected === true ? \"2px solid #000\" : \"2px solid #ddd\",\n padding: 0,\n cursor: \"pointer\",\n overflow: \"hidden\",\n }}\n >\n {thumbnail ? (\n <img\n src={getDefaultSrc(thumbnail)}\n alt={fv.name}\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n />\n ) : (\n <span\n style={{\n display: \"block\",\n width: \"100%\",\n height: \"100%\",\n backgroundColor: fv.colorCode ?? \"#ccc\",\n borderRadius: \"50%\",\n }}\n />\n )}\n </button>\n );\n })}\n </div>\n ) : (\n /* BOX / LIST: render checkboxes (default) */\n <ul>\n {values.map((fv) => (\n <li key={fv.name}>\n <label>\n <input\n type=\"checkbox\"\n checked={fv.isSelected === true}\n onChange={() =>\n handleFilterValueClick(productList, filter, fv)\n }\n />\n {fv.name}\n {fv.count != null && ` (${fv.count})`}\n </label>\n </li>\n ))}\n </ul>\n )}\n\n {/* NUMBER_RANGE_LIST: render predefined range buttons */}\n {filter.numberRangeListOptions?.map((option) => (\n <button\n key={`${option.from}-${option.to}`}\n style={{\n fontWeight: option.isSelected ? \"bold\" : \"normal\",\n marginRight: 8,\n }}\n onClick={() =>\n handleNumberRangeOptionClick(productList, filter, option)\n }\n >\n {option.from} - {option.to ?? \"+\"}\n </button>\n ))}\n\n {/* Category-level filtering */}\n {filterCategories.length > 0 && (\n <div style={{ marginTop: 16 }}>\n <h4>Categories</h4>\n {filterCategories.map((cat) => (\n <button\n key={cat.name}\n style={{\n fontWeight: cat.isSelected ? \"bold\" : \"normal\",\n marginRight: 8,\n }}\n onClick={() => onFilterCategoryClick(productList, cat, true)}\n >\n {cat.name}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n",
|
|
17027
17027
|
"relatedFunctions": [
|
|
17028
17028
|
"getFilterDisplayedValues",
|
|
17029
17029
|
"handleFilterValueClick",
|
|
17030
|
+
"handleNumberRangeOptionClick",
|
|
17030
17031
|
"getProductListFilterCategories",
|
|
17031
|
-
"onFilterCategoryClick"
|
|
17032
|
+
"onFilterCategoryClick",
|
|
17033
|
+
"isBoxFilter",
|
|
17034
|
+
"isListFilter",
|
|
17035
|
+
"isSwatchFilter",
|
|
17036
|
+
"isCustomValueFilter",
|
|
17037
|
+
"getIkasFilterThumbnailImage",
|
|
17038
|
+
"getDefaultSrc"
|
|
17032
17039
|
],
|
|
17033
17040
|
"categories": [
|
|
17034
17041
|
"ProductFilter",
|
|
@@ -17040,7 +17047,7 @@
|
|
|
17040
17047
|
"id": "product-list-section",
|
|
17041
17048
|
"title": "Product List Section (Complete)",
|
|
17042
17049
|
"description": "Complete product list section with category breadcrumb (getCategoryPath + getIkasCategoryHref), filter sidebar with handleFilterValueClick and onFilterCategoryClick, sort via setSortType (not direct mutation), search via searchProductList, product grid, and pagination with setProductListVisiblePage. Data is automatically reactive in root components via autorun().",
|
|
17043
|
-
"code": "import { useState } from \"preact/hooks\";\nimport {\n IkasProductList,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n setProductListVisiblePage,\n searchProductList,\n getCategoryPath,\n getIkasCategoryHref,\n isEmpty,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductListSection({\n productList,\n title = \"Products\",\n showFilters = true,\n}: Props) {\n const [searchKeyword, setSearchKeyword] = useState(\"\");\n\n if (!productList) return null;\n\n const products = productList.products ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const sortOptions = getProductListSortOptions(productList);\n const hasNext = hasProductListNextPage(productList);\n const hasPrev = hasProductListPrevPage(productList);\n\n // Category breadcrumb path\n const category = productList.category;\n const categoryPath = category ? getCategoryPath(category) : [];\n\n const handleSort = (value: string) => {\n const selected = sortOptions.find((opt) => opt.value === value);\n if (selected) {\n setSortType(productList, selected.value as any);\n }\n };\n\n const handleSearch = (keyword: string) => {\n setSearchKeyword(keyword);\n searchProductList(productList, keyword);\n };\n\n return (\n <section className=\"product-list-section\">\n <div className=\"product-list-inner\">\n {/* Category Breadcrumb */}\n {categoryPath.length > 0 && (\n <nav className=\"product-list-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((cat: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\"> / </span>\n <a href={getIkasCategoryHref(cat)}>{cat.name}</a>\n </span>\n ))}\n </nav>\n )}\n\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">\n {category?.name || title}\n {category?.description && (\n <p className=\"product-list-description\">{category.description}</p>\n )}\n </h1>\n\n <div className=\"product-list-controls\">\n {/* Search */}\n <input\n className=\"product-list-search\"\n type=\"text\"\n placeholder=\"Search products...\"\n value={searchKeyword}\n onInput={(e) => handleSearch((e.target as HTMLInputElement).value)}\n />\n\n {/* Sort — use setSortType instead of direct mutation */}\n {sortOptions.length > 0 && (\n <select\n className=\"product-list-sort\"\n value={sortOptions.find((o) => o.isSelected)?.value ?? \"\"}\n onChange={(e) => handleSort((e.target as HTMLSelectElement).value)}\n >\n {sortOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )}\n </div>\n </div>\n\n <div className=\"product-list-layout\">\n {/* Filter Sidebar — mobile uses IkasThemeOverlay */}\n {showFilters && filterCategories.length > 0 && (\n <aside className=\"product-list-filters\">\n {filterCategories.map((category) => {\n const values = getFilterDisplayedValues(category);\n return (\n <div key={category.name} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{category.name}</h3>\n <div className=\"filter-values\">\n {values.map((filterValue) => (\n <label key={filterValue.name} className=\"filter-value\">\n <input\n type=\"checkbox\"\n checked={filterValue.isSelected}\n onChange={() =>\n handleFilterValueClick(productList, category, filterValue)\n }\n />\n <span>{filterValue.name}</span>\n {filterValue.count != null && (\n <span className=\"filter-count\">({filterValue.count})</span>\n )}\n </label>\n ))}\n </div>\n {/* Category-level filter click */}\n {category.isSelected !== undefined && (\n <button\n className=\"filter-category-btn\"\n onClick={() => onFilterCategoryClick(productList, category as any, true)}\n >\n {category.isSelected ? \"Clear\" : \"Apply\"}\n </button>\n )}\n </div>\n );\n })}\n </aside>\n )}\n\n {/* Product Grid — IkasThemeInfiniteScroller pattern for infinite scroll */}\n <div className=\"product-grid\">\n {isEmpty(products) && (\n <p className=\"product-grid-empty\">No products found.</p>\n )}\n {products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const href = getSelectedProductVariantHref(product);\n\n return (\n <a key={product.id} href={href} className=\"product-card\">\n <div className=\"product-card-image-wrap\">\n {image && (\n <img\n src={getDefaultSrc(image)}\n alt={product.name}\n className=\"product-card-image\"\n />\n )}\n </div>\n <div className=\"product-card-info\">\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </div>\n </a>\n );\n })}\n </div>\n </div>\n\n {/* Pagination — includes setProductListVisiblePage for page jumping */}\n {(hasPrev || hasNext) && (\n <div className=\"product-list-pagination\">\n <button\n className=\"pagination-btn\"\n disabled={!hasPrev}\n onClick={() => getProductListPrevPage(productList)}\n >\n Previous\n </button>\n <span className=\"pagination-info\">\n Page {productList.page}\n </span>\n <button\n className=\"pagination-btn\"\n disabled={!hasNext}\n onClick={() => getProductListNextPage(productList)}\n >\n Next\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
17050
|
+
"code": "import { useState } from \"preact/hooks\";\nimport {\n IkasProductList,\n IkasProductListSortType,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n setProductListVisiblePage,\n searchProductList,\n getCategoryPath,\n getIkasCategoryHref,\n isEmpty,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductListSection({\n productList,\n title = \"Products\",\n showFilters = true,\n}: Props) {\n const [searchKeyword, setSearchKeyword] = useState(\"\");\n\n if (!productList) return null;\n\n const products = productList.data ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const sortOptions = getProductListSortOptions(productList);\n const hasNext = hasProductListNextPage(productList);\n const hasPrev = hasProductListPrevPage(productList);\n\n // Category breadcrumb path\n const category = productList.category;\n const categoryPath = category ? getCategoryPath(category) : [];\n\n const handleSort = (value: string) => {\n const selected = sortOptions.find((opt) => opt.value === value);\n if (selected) {\n setSortType(productList, selected.value as IkasProductListSortType);\n }\n };\n\n const handleSearch = (keyword: string) => {\n setSearchKeyword(keyword);\n searchProductList(productList, keyword);\n };\n\n return (\n <section className=\"product-list-section\">\n <div className=\"product-list-inner\">\n {/* Category Breadcrumb */}\n {categoryPath.length > 0 && (\n <nav className=\"product-list-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((cat: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\"> / </span>\n <a href={getIkasCategoryHref(cat)}>{cat.name}</a>\n </span>\n ))}\n </nav>\n )}\n\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">\n {category?.name || title}\n {category?.description && (\n <p className=\"product-list-description\">{category.description}</p>\n )}\n </h1>\n\n <div className=\"product-list-controls\">\n {/* Search */}\n <input\n className=\"product-list-search\"\n type=\"text\"\n placeholder=\"Search products...\"\n value={searchKeyword}\n onInput={(e) => handleSearch((e.target as HTMLInputElement).value)}\n />\n\n {/* Sort — use setSortType instead of direct mutation */}\n {sortOptions.length > 0 && (\n <select\n className=\"product-list-sort\"\n value={sortOptions.find((o) => o.isSelected)?.value ?? \"\"}\n onChange={(e) => handleSort((e.target as HTMLSelectElement).value)}\n >\n {sortOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )}\n </div>\n </div>\n\n <div className=\"product-list-layout\">\n {/* Filter Sidebar — display-type-aware rendering */}\n {showFilters && (productList.filters ?? []).length > 0 && (\n <aside className=\"product-list-filters\">\n {(productList.filters ?? []).map((filter) => {\n const values = getFilterDisplayedValues(filter);\n if (values.length === 0 && !filter.numberRangeListOptions?.length) return null;\n\n return (\n <div key={filter.id} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{filter.name}</h3>\n\n {/* SWATCH: color circles / thumbnail images */}\n {isSwatchFilter(filter) ? (\n <div className=\"filter-swatches\">\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button\n key={fv.name}\n className={`filter-swatch${fv.isSelected === true ? \" selected\" : \"\"}`}\n onClick={() => handleFilterValueClick(productList, filter, fv)}\n title={fv.name}\n >\n {thumbnail ? (\n <img src={getDefaultSrc(thumbnail)} alt={fv.name} />\n ) : (\n <span\n className=\"filter-swatch-color\"\n style={{ backgroundColor: fv.colorCode ?? \"#ccc\" }}\n />\n )}\n </button>\n );\n })}\n </div>\n ) : (\n /* BOX / LIST: checkboxes (default) */\n <div className=\"filter-values\">\n {values.map((fv) => (\n <label key={fv.name} className=\"filter-value\">\n <input\n type=\"checkbox\"\n checked={fv.isSelected === true}\n onChange={() =>\n handleFilterValueClick(productList, filter, fv)\n }\n />\n <span>{fv.name}</span>\n {fv.count != null && (\n <span className=\"filter-count\">({fv.count})</span>\n )}\n </label>\n ))}\n </div>\n )}\n\n {/* NUMBER_RANGE_LIST: predefined range buttons */}\n {filter.numberRangeListOptions?.map((option) => (\n <button\n key={`${option.from}-${option.to}`}\n className={`filter-range-btn${option.isSelected ? \" selected\" : \"\"}`}\n onClick={() => handleNumberRangeOptionClick(productList, filter, option)}\n >\n {option.from} - {option.to ?? \"+\"}\n </button>\n ))}\n </div>\n );\n })}\n\n {/* Category-level filtering */}\n {filterCategories.length > 0 && (\n <div className=\"filter-group\">\n <h3 className=\"filter-group-title\">Categories</h3>\n {filterCategories.map((cat) => (\n <button\n key={cat.name}\n className=\"filter-category-btn\"\n onClick={() => onFilterCategoryClick(productList, cat, true)}\n >\n {cat.name}\n </button>\n ))}\n </div>\n )}\n </aside>\n )}\n\n {/* Product Grid — IkasThemeInfiniteScroller pattern for infinite scroll */}\n <div className=\"product-grid\">\n {isEmpty(products) && (\n <p className=\"product-grid-empty\">No products found.</p>\n )}\n {products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const href = getSelectedProductVariantHref(product);\n\n return (\n <a key={product.id} href={href} className=\"product-card\">\n <div className=\"product-card-image-wrap\">\n {image && (\n <img\n src={getDefaultSrc(image)}\n alt={product.name}\n className=\"product-card-image\"\n />\n )}\n </div>\n <div className=\"product-card-info\">\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </div>\n </a>\n );\n })}\n </div>\n </div>\n\n {/* Pagination — includes setProductListVisiblePage for page jumping */}\n {(hasPrev || hasNext) && (\n <div className=\"product-list-pagination\">\n <button\n className=\"pagination-btn\"\n disabled={!hasPrev}\n onClick={() => getProductListPrevPage(productList)}\n >\n Previous\n </button>\n <span className=\"pagination-info\">\n Page {productList.page}\n </span>\n <button\n className=\"pagination-btn\"\n disabled={!hasNext}\n onClick={() => getProductListNextPage(productList)}\n >\n Next\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
17044
17051
|
"relatedFunctions": [
|
|
17045
17052
|
"getSelectedProductVariant",
|
|
17046
17053
|
"getProductVariantFormattedFinalPrice",
|
|
@@ -17048,8 +17055,14 @@
|
|
|
17048
17055
|
"getSelectedProductVariantHref",
|
|
17049
17056
|
"getFilterDisplayedValues",
|
|
17050
17057
|
"handleFilterValueClick",
|
|
17058
|
+
"handleNumberRangeOptionClick",
|
|
17051
17059
|
"getProductListFilterCategories",
|
|
17052
17060
|
"onFilterCategoryClick",
|
|
17061
|
+
"isBoxFilter",
|
|
17062
|
+
"isListFilter",
|
|
17063
|
+
"isSwatchFilter",
|
|
17064
|
+
"isCustomValueFilter",
|
|
17065
|
+
"getIkasFilterThumbnailImage",
|
|
17053
17066
|
"getProductListSortOptions",
|
|
17054
17067
|
"setSortType",
|
|
17055
17068
|
"hasProductListNextPage",
|
|
@@ -17072,7 +17085,7 @@
|
|
|
17072
17085
|
"files": [
|
|
17073
17086
|
{
|
|
17074
17087
|
"filename": "index.tsx",
|
|
17075
|
-
"content": "import { useState } from \"preact/hooks\";\nimport {\n IkasProductList,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n setProductListVisiblePage,\n searchProductList,\n getCategoryPath,\n getIkasCategoryHref,\n isEmpty,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductListSection({\n productList,\n title = \"Products\",\n showFilters = true,\n}: Props) {\n const [searchKeyword, setSearchKeyword] = useState(\"\");\n\n if (!productList) return null;\n\n const products = productList.products ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const sortOptions = getProductListSortOptions(productList);\n const hasNext = hasProductListNextPage(productList);\n const hasPrev = hasProductListPrevPage(productList);\n\n // Category breadcrumb path\n const category = productList.category;\n const categoryPath = category ? getCategoryPath(category) : [];\n\n const handleSort = (value: string) => {\n const selected = sortOptions.find((opt) => opt.value === value);\n if (selected) {\n setSortType(productList, selected.value as any);\n }\n };\n\n const handleSearch = (keyword: string) => {\n setSearchKeyword(keyword);\n searchProductList(productList, keyword);\n };\n\n return (\n <section className=\"product-list-section\">\n <div className=\"product-list-inner\">\n {/* Category Breadcrumb */}\n {categoryPath.length > 0 && (\n <nav className=\"product-list-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((cat: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\"> / </span>\n <a href={getIkasCategoryHref(cat)}>{cat.name}</a>\n </span>\n ))}\n </nav>\n )}\n\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">\n {category?.name || title}\n {category?.description && (\n <p className=\"product-list-description\">{category.description}</p>\n )}\n </h1>\n\n <div className=\"product-list-controls\">\n {/* Search */}\n <input\n className=\"product-list-search\"\n type=\"text\"\n placeholder=\"Search products...\"\n value={searchKeyword}\n onInput={(e) => handleSearch((e.target as HTMLInputElement).value)}\n />\n\n {/* Sort — use setSortType instead of direct mutation */}\n {sortOptions.length > 0 && (\n <select\n className=\"product-list-sort\"\n value={sortOptions.find((o) => o.isSelected)?.value ?? \"\"}\n onChange={(e) => handleSort((e.target as HTMLSelectElement).value)}\n >\n {sortOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )}\n </div>\n </div>\n\n <div className=\"product-list-layout\">\n {/* Filter Sidebar — mobile uses IkasThemeOverlay */}\n {showFilters && filterCategories.length > 0 && (\n <aside className=\"product-list-filters\">\n {filterCategories.map((category) => {\n const values = getFilterDisplayedValues(category);\n return (\n <div key={category.name} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{category.name}</h3>\n <div className=\"filter-values\">\n {values.map((filterValue) => (\n <label key={filterValue.name} className=\"filter-value\">\n <input\n type=\"checkbox\"\n checked={filterValue.isSelected}\n onChange={() =>\n handleFilterValueClick(productList, category, filterValue)\n }\n />\n <span>{filterValue.name}</span>\n {filterValue.count != null && (\n <span className=\"filter-count\">({filterValue.count})</span>\n )}\n </label>\n ))}\n </div>\n {/* Category-level filter click */}\n {category.isSelected !== undefined && (\n <button\n className=\"filter-category-btn\"\n onClick={() => onFilterCategoryClick(productList, category as any, true)}\n >\n {category.isSelected ? \"Clear\" : \"Apply\"}\n </button>\n )}\n </div>\n );\n })}\n </aside>\n )}\n\n {/* Product Grid — IkasThemeInfiniteScroller pattern for infinite scroll */}\n <div className=\"product-grid\">\n {isEmpty(products) && (\n <p className=\"product-grid-empty\">No products found.</p>\n )}\n {products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const href = getSelectedProductVariantHref(product);\n\n return (\n <a key={product.id} href={href} className=\"product-card\">\n <div className=\"product-card-image-wrap\">\n {image && (\n <img\n src={getDefaultSrc(image)}\n alt={product.name}\n className=\"product-card-image\"\n />\n )}\n </div>\n <div className=\"product-card-info\">\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </div>\n </a>\n );\n })}\n </div>\n </div>\n\n {/* Pagination — includes setProductListVisiblePage for page jumping */}\n {(hasPrev || hasNext) && (\n <div className=\"product-list-pagination\">\n <button\n className=\"pagination-btn\"\n disabled={!hasPrev}\n onClick={() => getProductListPrevPage(productList)}\n >\n Previous\n </button>\n <span className=\"pagination-info\">\n Page {productList.page}\n </span>\n <button\n className=\"pagination-btn\"\n disabled={!hasNext}\n onClick={() => getProductListNextPage(productList)}\n >\n Next\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n"
|
|
17088
|
+
"content": "import { useState } from \"preact/hooks\";\nimport {\n IkasProductList,\n IkasProductListSortType,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n setProductListVisiblePage,\n searchProductList,\n getCategoryPath,\n getIkasCategoryHref,\n isEmpty,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductListSection({\n productList,\n title = \"Products\",\n showFilters = true,\n}: Props) {\n const [searchKeyword, setSearchKeyword] = useState(\"\");\n\n if (!productList) return null;\n\n const products = productList.data ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const sortOptions = getProductListSortOptions(productList);\n const hasNext = hasProductListNextPage(productList);\n const hasPrev = hasProductListPrevPage(productList);\n\n // Category breadcrumb path\n const category = productList.category;\n const categoryPath = category ? getCategoryPath(category) : [];\n\n const handleSort = (value: string) => {\n const selected = sortOptions.find((opt) => opt.value === value);\n if (selected) {\n setSortType(productList, selected.value as IkasProductListSortType);\n }\n };\n\n const handleSearch = (keyword: string) => {\n setSearchKeyword(keyword);\n searchProductList(productList, keyword);\n };\n\n return (\n <section className=\"product-list-section\">\n <div className=\"product-list-inner\">\n {/* Category Breadcrumb */}\n {categoryPath.length > 0 && (\n <nav className=\"product-list-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((cat: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\"> / </span>\n <a href={getIkasCategoryHref(cat)}>{cat.name}</a>\n </span>\n ))}\n </nav>\n )}\n\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">\n {category?.name || title}\n {category?.description && (\n <p className=\"product-list-description\">{category.description}</p>\n )}\n </h1>\n\n <div className=\"product-list-controls\">\n {/* Search */}\n <input\n className=\"product-list-search\"\n type=\"text\"\n placeholder=\"Search products...\"\n value={searchKeyword}\n onInput={(e) => handleSearch((e.target as HTMLInputElement).value)}\n />\n\n {/* Sort — use setSortType instead of direct mutation */}\n {sortOptions.length > 0 && (\n <select\n className=\"product-list-sort\"\n value={sortOptions.find((o) => o.isSelected)?.value ?? \"\"}\n onChange={(e) => handleSort((e.target as HTMLSelectElement).value)}\n >\n {sortOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )}\n </div>\n </div>\n\n <div className=\"product-list-layout\">\n {/* Filter Sidebar — display-type-aware rendering */}\n {showFilters && (productList.filters ?? []).length > 0 && (\n <aside className=\"product-list-filters\">\n {(productList.filters ?? []).map((filter) => {\n const values = getFilterDisplayedValues(filter);\n if (values.length === 0 && !filter.numberRangeListOptions?.length) return null;\n\n return (\n <div key={filter.id} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{filter.name}</h3>\n\n {/* SWATCH: color circles / thumbnail images */}\n {isSwatchFilter(filter) ? (\n <div className=\"filter-swatches\">\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button\n key={fv.name}\n className={`filter-swatch${fv.isSelected === true ? \" selected\" : \"\"}`}\n onClick={() => handleFilterValueClick(productList, filter, fv)}\n title={fv.name}\n >\n {thumbnail ? (\n <img src={getDefaultSrc(thumbnail)} alt={fv.name} />\n ) : (\n <span\n className=\"filter-swatch-color\"\n style={{ backgroundColor: fv.colorCode ?? \"#ccc\" }}\n />\n )}\n </button>\n );\n })}\n </div>\n ) : (\n /* BOX / LIST: checkboxes (default) */\n <div className=\"filter-values\">\n {values.map((fv) => (\n <label key={fv.name} className=\"filter-value\">\n <input\n type=\"checkbox\"\n checked={fv.isSelected === true}\n onChange={() =>\n handleFilterValueClick(productList, filter, fv)\n }\n />\n <span>{fv.name}</span>\n {fv.count != null && (\n <span className=\"filter-count\">({fv.count})</span>\n )}\n </label>\n ))}\n </div>\n )}\n\n {/* NUMBER_RANGE_LIST: predefined range buttons */}\n {filter.numberRangeListOptions?.map((option) => (\n <button\n key={`${option.from}-${option.to}`}\n className={`filter-range-btn${option.isSelected ? \" selected\" : \"\"}`}\n onClick={() => handleNumberRangeOptionClick(productList, filter, option)}\n >\n {option.from} - {option.to ?? \"+\"}\n </button>\n ))}\n </div>\n );\n })}\n\n {/* Category-level filtering */}\n {filterCategories.length > 0 && (\n <div className=\"filter-group\">\n <h3 className=\"filter-group-title\">Categories</h3>\n {filterCategories.map((cat) => (\n <button\n key={cat.name}\n className=\"filter-category-btn\"\n onClick={() => onFilterCategoryClick(productList, cat, true)}\n >\n {cat.name}\n </button>\n ))}\n </div>\n )}\n </aside>\n )}\n\n {/* Product Grid — IkasThemeInfiniteScroller pattern for infinite scroll */}\n <div className=\"product-grid\">\n {isEmpty(products) && (\n <p className=\"product-grid-empty\">No products found.</p>\n )}\n {products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const href = getSelectedProductVariantHref(product);\n\n return (\n <a key={product.id} href={href} className=\"product-card\">\n <div className=\"product-card-image-wrap\">\n {image && (\n <img\n src={getDefaultSrc(image)}\n alt={product.name}\n className=\"product-card-image\"\n />\n )}\n </div>\n <div className=\"product-card-info\">\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </div>\n </a>\n );\n })}\n </div>\n </div>\n\n {/* Pagination — includes setProductListVisiblePage for page jumping */}\n {(hasPrev || hasNext) && (\n <div className=\"product-list-pagination\">\n <button\n className=\"pagination-btn\"\n disabled={!hasPrev}\n onClick={() => getProductListPrevPage(productList)}\n >\n Previous\n </button>\n <span className=\"pagination-info\">\n Page {productList.page}\n </span>\n <button\n className=\"pagination-btn\"\n disabled={!hasNext}\n onClick={() => getProductListNextPage(productList)}\n >\n Next\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n"
|
|
17076
17089
|
},
|
|
17077
17090
|
{
|
|
17078
17091
|
"filename": "types.ts",
|