@ikas/code-components-mcp 0.27.0 → 0.28.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
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"component-structure": {
|
|
52
52
|
"title": "Component Structure",
|
|
53
53
|
"description": "How to write a Preact component for ikas",
|
|
54
|
-
"content": "Each component consists of three files. There are two patterns: **components** (child elements) and **sections** (page-level containers).\n\n## Component Pattern (child elements like buttons, cards, badges)\n\n### index.tsx - Component Implementation\n```tsx\nimport { Props } from \"./types\";\n\nexport default function MyComponent({ title, showButton }: Props) {\n return (\n <div className=\"my-component\">\n <h1>{title}</h1>\n {showButton && <button>Click me</button>}\n </div>\n );\n}\n```\n\n### types.ts - Props Interface\n```typescript\nexport interface Props {\n title: string; // TEXT prop -> string\n showButton?: boolean; // optional BOOLEAN prop\n count: number; // NUMBER prop -> number\n}\n```\n\n### styles.css\n```css\n.my-component {\n padding: 16px;\n}\n\n.my-component h1 {\n font-size: 24px;\n}\n```\n\n## Section Pattern (page-level containers like headers, hero banners, footers)\n\nSections use a `<section>` root element, full-width styling, and a `Props` interface. In `ikas.config.json` they have `\"type\": \"section\"`.\n\n### index.tsx - Section Implementation\n```tsx\nimport { Props } from \"./types\";\n\nexport default function HeroBanner({ heading, subtitle, backgroundColor }: Props) {\n return (\n <section className=\"hero-banner\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"hero-banner-inner\">\n <h2>{heading}</h2>\n {subtitle && <p>{subtitle}</p>}\n </div>\n </section>\n );\n}\n```\n\n### types.ts - Section Props Interface\n```typescript\nexport interface Props {\n heading: string;\n subtitle?: string;\n backgroundColor?: string;\n}\n```\n\n### styles.css - Full-width section styles\n```css\n.hero-banner {\n width: 100%;\n padding: 64px 24px;\n}\n\n.hero-banner-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n```\n\n## Key Rules\n- Export the component as `default export`\n- Use Preact (not React) - but JSX syntax is the same\n- Import types from `./types` for props\n- Use `className` not `class` for CSS classes\n- Storefront functions and types come from `@ikas/bp-storefront`\n- CSS classes are automatically scoped to your component at build time. Use plain CSS class selectors - they won't conflict with other components or the page.\n\n##
|
|
54
|
+
"content": "Each component consists of three files. There are two patterns: **components** (child elements) and **sections** (page-level containers).\n\n## Component Pattern (child elements like buttons, cards, badges)\n\n### index.tsx - Component Implementation\n```tsx\nimport { Props } from \"./types\";\n\nexport default function MyComponent({ title, showButton }: Props) {\n return (\n <div className=\"my-component\">\n <h1>{title}</h1>\n {showButton && <button>Click me</button>}\n </div>\n );\n}\n```\n\n### types.ts - Props Interface\n```typescript\nexport interface Props {\n title: string; // TEXT prop -> string\n showButton?: boolean; // optional BOOLEAN prop\n count: number; // NUMBER prop -> number\n}\n```\n\n### styles.css\n```css\n.my-component {\n padding: 16px;\n}\n\n.my-component h1 {\n font-size: 24px;\n}\n```\n\n## Section Pattern (page-level containers like headers, hero banners, footers)\n\nSections use a `<section>` root element, full-width styling, and a `Props` interface. In `ikas.config.json` they have `\"type\": \"section\"`.\n\n### index.tsx - Section Implementation\n```tsx\nimport { Props } from \"./types\";\n\nexport default function HeroBanner({ heading, subtitle, backgroundColor }: Props) {\n return (\n <section className=\"hero-banner\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"hero-banner-inner\">\n <h2>{heading}</h2>\n {subtitle && <p>{subtitle}</p>}\n </div>\n </section>\n );\n}\n```\n\n### types.ts - Section Props Interface\n```typescript\nexport interface Props {\n heading: string;\n subtitle?: string;\n backgroundColor?: string;\n}\n```\n\n### styles.css - Full-width section styles\n```css\n.hero-banner {\n width: 100%;\n padding: 64px 24px;\n}\n\n.hero-banner-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n```\n\n## Key Rules\n- Export the component as `default export`\n- Use Preact (not React) - but JSX syntax is the same\n- Import types from `./types` for props\n- Use `className` not `class` for CSS classes\n- Storefront functions and types come from `@ikas/bp-storefront`\n- CSS classes are automatically scoped to your component at build time. Use plain CSS class selectors - they won't conflict with other components or the page.\n\n## Root Components Are Automatically Reactive\n\nThe ikas runtime wraps every root component render in a MobX `autorun()`, so **root exports are already reactive** — they automatically re-render when any MobX store they read (`cartStore`, `customerStore`, etc.) changes. You do **not** need `observer()` on root component exports.\n\n### Correct: Plain root export reading stores\n```tsx\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function CartSummary({ title }: Props) {\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n return (\n <section className=\"cart-summary\">\n <h2>{title}</h2>\n <p>{itemCount} items in cart</p>\n </section>\n );\n}\n```\n\n### Anti-pattern: Do NOT wrap root exports with observer\n```tsx\n// WRONG — observer() is redundant on root components\nconst CartSummary = observer(function CartSummary({ title }: Props) {\n ...\n});\nexport default CartSummary;\n```\n\n## Using observer for Sub-Components\n\nWhen you extract a **sub-component** that independently reads MobX stores, wrap it with `observer()` from `@ikas/component-utils` so it re-renders when store data changes.\n\n### When to use observer\n- A **sub-component** (not the root export) reads `cartStore.cart`, `customerStore.customer`, or any other MobX observable\n- You extract part of a component into a separate function that needs independent reactivity\n\n### When observer is NOT needed\n- **Root component exports** — the ikas runtime handles reactivity via `autorun()`\n- Components that only use props passed from parent — no store reads\n- Static components with no reactive data\n\n### Example: Root export with observer sub-component\n```tsx\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// Sub-component: needs observer() for independent reactivity\nconst CartBadge = observer(function CartBadge() {\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n return <span className=\"cart-badge\">{itemCount}</span>;\n});\n\n// Root export: NO observer needed — autorun() handles reactivity\nexport default function Header({ logo }: Props) {\n return (\n <header>\n <img src={logo} alt=\"Logo\" />\n <CartBadge />\n </header>\n );\n}\n```\n\n**Important:** When using `observer` on sub-components, define the component as a named function expression (not arrow function) and assign it to a `const`. This ensures proper display names in React DevTools.",
|
|
55
55
|
"tags": [
|
|
56
56
|
"component",
|
|
57
57
|
"preact",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"imports": {
|
|
110
110
|
"title": "Import Patterns",
|
|
111
111
|
"description": "How to import storefront functions and types",
|
|
112
|
-
"content": "### Storefront Functions & Types\nImport API functions and TypeScript types from `@ikas/bp-storefront`:\n```typescript\nimport {\n // Product functions\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n getProductVariantFormattedFinalPrice,\n \n // Cart functions\n addItemToCart,\n cartStore,\n \n // Navigation\n Router,\n \n // Customer\n customerStore,\n customerLogin,\n hasCustomer,\n \n // Favorites\n addIkasProductToFavorites,\n isFavoriteIkasProduct,\n\n // Type models\n IkasProduct,\n IkasProductVariant,\n IkasCart,\n IkasOrderLineItem,\n IkasCustomer,\n IkasOrder,\n IkasCategory,\n IkasBrand,\n IkasBlogPost\n} from \"@ikas/bp-storefront\";\n```\n\n### Store Instances\nPre-initialized MobX stores:\n```typescript\nimport { cartStore, customerStore } from \"@ikas/bp-storefront\";\n\n// Cart data\ncartStore.cart?.orderLineItems\n\n// Customer data \ncustomerStore.customer?.email\n```\n\n### Observer (for reactive updates)\n```typescript\nimport { observer } from \"@ikas/component-utils\";\
|
|
112
|
+
"content": "### Storefront Functions & Types\nImport API functions and TypeScript types from `@ikas/bp-storefront`:\n```typescript\nimport {\n // Product functions\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n getProductVariantFormattedFinalPrice,\n \n // Cart functions\n addItemToCart,\n cartStore,\n \n // Navigation\n Router,\n \n // Customer\n customerStore,\n customerLogin,\n hasCustomer,\n \n // Favorites\n addIkasProductToFavorites,\n isFavoriteIkasProduct,\n\n // Type models\n IkasProduct,\n IkasProductVariant,\n IkasCart,\n IkasOrderLineItem,\n IkasCustomer,\n IkasOrder,\n IkasCategory,\n IkasBrand,\n IkasBlogPost\n} from \"@ikas/bp-storefront\";\n```\n\n### Store Instances\nPre-initialized MobX stores:\n```typescript\nimport { cartStore, customerStore } from \"@ikas/bp-storefront\";\n\n// Cart data\ncartStore.cart?.orderLineItems\n\n// Customer data \ncustomerStore.customer?.email\n```\n\n### Observer (for sub-components with reactive updates)\n```typescript\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n// Sub-component: needs observer() for independent reactivity\nconst CartBadge = observer(function CartBadge() {\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n return <span>Cart: {itemCount}</span>;\n});\n\n// Root export: plain function — ikas runtime handles reactivity via autorun()\nexport default function MySection({ title }: Props) {\n return (\n <div>\n <h1>{title}</h1>\n <CartBadge />\n </div>\n );\n}\n```\n\nDo **NOT** wrap root component exports with `observer()`. The ikas runtime handles root reactivity automatically via `autorun()`. Only wrap **sub-components** with `observer()` when they independently read from stores (`cartStore`, `customerStore`).",
|
|
113
113
|
"tags": [
|
|
114
114
|
"import",
|
|
115
115
|
"storefront",
|
|
@@ -136,25 +136,25 @@
|
|
|
136
136
|
"common-pitfalls": {
|
|
137
137
|
"title": "Common Pitfalls",
|
|
138
138
|
"description": "Frequent mistakes LLMs and developers make when building ikas code components",
|
|
139
|
-
"content": "## Common Pitfalls\n\nThese are the most common mistakes when building ikas code components. Avoid them for correct, working code.\n\n### 1.
|
|
139
|
+
"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.products` is the correct way to access products in a product list (NOT `productList.data`). Similarly, `blogList.blogs` for blogs:\n\n```tsx\n// CORRECT\nconst products = productList?.products ?? [];\nconst blogs = blogList?.blogs ?? [];\n\n// WRONG — .data does not exist on these types\nconst products = productList?.data ?? [];\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.",
|
|
140
140
|
"tags": ["pitfalls", "gotchas", "mistakes", "observer", "mutation", "css", "types", "null-safety", "forms", "events", "parameters"]
|
|
141
141
|
},
|
|
142
142
|
"ai-workflow": {
|
|
143
143
|
"title": "AI Agent Workflow for Building ikas Components",
|
|
144
144
|
"description": "Step-by-step guide for AI coding agents building ikas storefront components using CLI commands and MCP tools",
|
|
145
|
-
"content": "## AI Agent Workflow\n\nThis is the recommended step-by-step workflow for AI agents building ikas code components. Follow these steps in order for reliable, error-free results.\n\n**IMPORTANT: NEVER create or edit `types.ts` manually — it is auto-generated by the CLI.** The CLI commands below update BOTH `ikas.config.json` AND `types.ts` automatically.\n\n### Step 1: Create Component with Props\n\nUse `get_section_template(sectionType)` to get a starter template — it includes a ready-to-run CLI command with `--props`.\n\nThen run the single `add-component --props` command to create the component scaffold WITH all props in one step:\n```bash\nnpx ikas-component config add-component --name \"HeroSection\" --type section --props '[{\"name\":\"title\",\"displayName\":\"Title\",\"type\":\"TEXT\",\"required\":true},{\"name\":\"backgroundImage\",\"displayName\":\"Background Image\",\"type\":\"IMAGE\"},{\"name\":\"showButton\",\"displayName\":\"Show Button\",\"type\":\"BOOLEAN\"}]'\n```\n\nThis creates the component directory with `index.tsx`, `types.ts` (with correct Props interface), `styles.css`, updates `ikas.config.json`, and updates the barrel export. The output is JSON:\n```json\n{\"success\": true, \"componentId\": \"abc123-hero-section\", \"componentName\": \"HeroSection\", \"type\": \"section\", \"propsCount\": 3, \"files\": [...]}\n```\n\nThe `--props` flag accepts a JSON array of prop objects. Each prop needs `name` + `type` at minimum. `displayName` is auto-generated from camelCase name if omitted (e.g. `backgroundImage` → `\"Background Image\"`).\n\nTo add more props later, use `add-prop`:\n```bash\nnpx ikas-component config add-prop --component \"HeroSection\" --name \"subtitle\" --displayName \"Subtitle\" --type TEXT\n```\n\n### Step 2: Get Reference Material\n\nBefore writing component code, use MCP tools to get the right patterns:\n- `get_section_template(sectionType)` — Get a starter template for common section types (product-detail, cart, login, header, footer, etc.)\n- `get_framework_guide(\"common-pitfalls\")` — Review common mistakes to avoid\n- `get_framework_guide(\"component-structure\")` — Review component structure patterns\n- `get_function_doc(functionName)` — Look up exact function signatures before using them\n- `get_model_guide(typeName)` — Get comprehensive info about a model type\n\n### Step 3: Write the Component Code\n\nEdit `src/components/{ComponentName}/index.tsx` with the implementation. **Do NOT edit `types.ts`** — it was already generated correctly in Step 1. Key rules:\n- Import props from `./types` (auto-generated in Step 1)\n- Import storefront functions from `@ikas/bp-storefront`\n-
|
|
145
|
+
"content": "## AI Agent Workflow\n\nThis is the recommended step-by-step workflow for AI agents building ikas code components. Follow these steps in order for reliable, error-free results.\n\n**IMPORTANT: NEVER create or edit `types.ts` manually — it is auto-generated by the CLI.** The CLI commands below update BOTH `ikas.config.json` AND `types.ts` automatically.\n\n### Step 1: Create Component with Props\n\nUse `get_section_template(sectionType)` to get a starter template — it includes a ready-to-run CLI command with `--props`.\n\nThen run the single `add-component --props` command to create the component scaffold WITH all props in one step:\n```bash\nnpx ikas-component config add-component --name \"HeroSection\" --type section --props '[{\"name\":\"title\",\"displayName\":\"Title\",\"type\":\"TEXT\",\"required\":true},{\"name\":\"backgroundImage\",\"displayName\":\"Background Image\",\"type\":\"IMAGE\"},{\"name\":\"showButton\",\"displayName\":\"Show Button\",\"type\":\"BOOLEAN\"}]'\n```\n\nThis creates the component directory with `index.tsx`, `types.ts` (with correct Props interface), `styles.css`, updates `ikas.config.json`, and updates the barrel export. The output is JSON:\n```json\n{\"success\": true, \"componentId\": \"abc123-hero-section\", \"componentName\": \"HeroSection\", \"type\": \"section\", \"propsCount\": 3, \"files\": [...]}\n```\n\nThe `--props` flag accepts a JSON array of prop objects. Each prop needs `name` + `type` at minimum. `displayName` is auto-generated from camelCase name if omitted (e.g. `backgroundImage` → `\"Background Image\"`).\n\nTo add more props later, use `add-prop`:\n```bash\nnpx ikas-component config add-prop --component \"HeroSection\" --name \"subtitle\" --displayName \"Subtitle\" --type TEXT\n```\n\n### Step 2: Get Reference Material\n\nBefore writing component code, use MCP tools to get the right patterns:\n- `get_section_template(sectionType)` — Get a starter template for common section types (product-detail, cart, login, header, footer, etc.)\n- `get_framework_guide(\"common-pitfalls\")` — Review common mistakes to avoid\n- `get_framework_guide(\"component-structure\")` — Review component structure patterns\n- `get_function_doc(functionName)` — Look up exact function signatures before using them\n- `get_model_guide(typeName)` — Get comprehensive info about a model type\n\n### Step 3: Write the Component Code\n\nEdit `src/components/{ComponentName}/index.tsx` with the implementation. **Do NOT edit `types.ts`** — it was already generated correctly in Step 1. Key rules:\n- Import props from `./types` (auto-generated in Step 1)\n- Import storefront functions from `@ikas/bp-storefront`\n- Root export should be a plain named function: `export default function X({ ... }: Props) { ... }`\n- Only wrap sub-components with `observer()` when they independently read MobX stores\n- Use optional chaining for all props: `props.title ?? \"Default\"`\n- Use `as unknown as boolean` cast for functions like `hasProductStock`, `isAddToCartEnabled`\n\n### Step 4: Write Styles\n\nEdit `src/components/{ComponentName}/styles.css`. Key rules:\n- Use class selectors only (`.my-class`), never bare element selectors\n- CSS is auto-scoped at build time — no manual namespacing needed\n- For sections: `width: 100%; padding: 64px 24px;` with inner max-width container\n\n### Step 5: Verify with Type Checking\n\nRun the lightweight type checker:\n```bash\nnpx ikas-component check --json\n```\n\nSuccess output:\n```json\n{\"success\": true, \"errors\": []}\n```\n\nError output:\n```json\n{\"success\": false, \"errorCount\": 2, \"errors\": [{\"file\": \"src/components/Hero/index.tsx\", \"line\": 15, \"column\": 3, \"code\": \"TS2339\", \"message\": \"Property 'title' does not exist on type 'Props'\"}]}\n```\n\n### Step 6: Fix Errors and Re-check\n\nFor each error:\n1. Read the file and line number from the error\n2. Fix the issue (common fixes below)\n3. Re-run `npx ikas-component check --json`\n\n**Common error fixes:**\n- `Property 'x' does not exist on type 'Props'` — Prop wasn't added via CLI. Run `npx ikas-component config add-prop`.\n- `Cannot find module '@ikas/bp-storefront'` — Normal in type-check if node_modules not fully installed. Focus on component-level errors.\n- `Type 'X' is not assignable to type 'Y'` — Check function signature with `get_function_doc()`.\n\n### Step 7: Full Build Validation\n\nOnce type checking passes, run the full build:\n```bash\nnpx ikas-component build\n```\n\nThis runs type checking + esbuild compilation + CSS scoping.\n\n### Quick Reference: CLI Commands\n\n| Command | Purpose |\n|---------|--------|\n| `npx ikas-component config add-component --name X --type section --props '[...]'` | **Primary** — create component with all props |\n| `npx ikas-component config add-component --name X --type section` | Create component with no props |\n| `npx ikas-component config add-prop --component X --name y --displayName Y --type TEXT` | Add a prop incrementally |\n| `npx ikas-component config update-prop --component X --prop y --required true` | Update a prop |\n| `npx ikas-component config remove-prop --component X --prop y` | Remove a prop |\n| `npx ikas-component config remove-component --name X` | Remove a component |\n| `npx ikas-component config list` | List all components and props |\n| `npx ikas-component check --json` | Type-check with JSON output |\n| `npx ikas-component build` | Full production build |\n\n### Quick Reference: MCP Tools\n\n| Tool | When to Use |\n|------|------------|\n| `get_section_template(type)` | Starting a new section — get starter code + CLI command |\n| `get_framework_guide(topic)` | Understanding patterns, pitfalls, architecture |\n| `get_function_doc(name)` | Looking up exact function signature |\n| `get_model_guide(type)` | Working with a model type (IkasProduct, IkasCart, etc.) |\n| `get_prop_types()` | Checking available prop types for ikas.config.json |\n| `search_docs(query)` | Finding relevant functions/docs by keyword |",
|
|
146
146
|
"tags": ["ai", "workflow", "agent", "cli", "steps", "guide", "automation"]
|
|
147
147
|
},
|
|
148
148
|
"form-handling": {
|
|
149
149
|
"title": "Form Handling",
|
|
150
150
|
"description": "How to use the form model pattern for login, registration, address, and other forms",
|
|
151
|
-
"content": "## Form Handling Pattern\n\nikas storefront uses a consistent form model pattern across all form types: `init*Form()` → `set*FormField()` → `submit*Form()`.\n\n### Form Model Structure\n\nEach form field has:\n```typescript\n{\n value: string; // Current field value\n hasError: boolean; // Whether validation failed\n message?: string; // Error message to display\n label: string; // Display label\n placeholder: string; // Input placeholder\n}\n```\n\nEach form tracks:\n```typescript\n{\n isSubmitted: boolean; // Has submit been attempted\n isSubmitting: boolean; // Is submission in progress\n isSuccess?: boolean; // Did last submit succeed\n isFailure?: boolean; // Did last submit fail\n responseMessage?: string; // Server response message\n}\n```\n\n### Login Form Example\n\n```tsx\nimport { useEffect } from \"preact/hooks\";\nimport {
|
|
151
|
+
"content": "## Form Handling Pattern\n\nikas storefront uses a consistent form model pattern across all form types: `init*Form()` → `set*FormField()` → `submit*Form()`.\n\n### Form Model Structure\n\nEach form field has:\n```typescript\n{\n value: string; // Current field value\n hasError: boolean; // Whether validation failed\n message?: string; // Error message to display\n label: string; // Display label\n placeholder: string; // Input placeholder\n}\n```\n\nEach form tracks:\n```typescript\n{\n isSubmitted: boolean; // Has submit been attempted\n isSubmitting: boolean; // Is submission in progress\n isSuccess?: boolean; // Did last submit succeed\n isFailure?: boolean; // Did last submit fail\n responseMessage?: string; // Server response message\n}\n```\n\n### Login Form Example\n\n```tsx\nimport { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n Router,\n} from \"@ikas/bp-storefront\";\n\n// Root export — no observer() needed, ikas runtime handles reactivity via autorun()\nexport default function LoginForm() {\n const loginForm = customerStore.loginForm;\n\n useEffect(() => {\n initLoginForm(loginForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitLoginForm(loginForm);\n if (success) {\n Router.navigate(\"/account\");\n }\n };\n\n return (\n <form onSubmit={handleSubmit}>\n {loginForm.isFailure && <div>{loginForm.responseMessage}</div>}\n\n <input\n type=\"email\"\n value={loginForm.email.value}\n onInput={(e) => setLoginFormEmail(loginForm, e.target.value)}\n />\n {loginForm.email.hasError && <span>{loginForm.email.message}</span>}\n\n <input\n type=\"password\"\n value={loginForm.password.value}\n onInput={(e) => setLoginFormPassword(loginForm, e.target.value)}\n />\n {loginForm.password.hasError && <span>{loginForm.password.message}</span>}\n\n <button type=\"submit\" disabled={loginForm.isSubmitting}>\n {loginForm.isSubmitting ? \"Signing in...\" : \"Sign In\"}\n </button>\n </form>\n );\n}\n```\n\n### Available Form Types\n\n| Form | Init | Setter Functions | Submit |\n|------|------|-----------------|--------|\n| Login | `initLoginForm(form)` | `setLoginFormEmail()`, `setLoginFormPassword()` | `submitLoginForm(form)` |\n| Register | `initRegisterForm(form)` | `setRegisterFormEmail()`, `setRegisterFormPassword()`, `setRegisterFormFirstName()`, `setRegisterFormLastName()` | `submitRegisterForm(form)` |\n| Forgot Password | `initForgotPasswordForm(form)` | `setForgotPasswordFormEmail()` | `submitForgotPasswordForm(form)` |\n| Address | `initAddressForm(form)` | `setAddressFormField()` for each field | `submitAddressForm(form)` |\n| Contact | `initContactForm(form)` | `setContactFormField()` for each field | `submitContactForm(form)` |\n| Account Info | `initAccountInfoForm(form)` | `setAccountInfoFormField()` for each field | `submitAccountInfoForm(form)` |\n| Newsletter | `initNewsletterSubscriptionForm(form)` | `setNewsletterEmail()` | `submitNewsletterSubscriptionForm(form)` |\n| Coupon Code | — | `setCouponCode()` | `applyCouponCode()` |\n\n### Key Rules\n\n1. **Always call init first** — `init*Form()` sets up default field values, labels, and placeholders\n2. **Setter functions auto-validate** — if `form.isSubmitted` is true, calling any setter re-validates the form automatically\n3. **Check field.hasError for display** — show error messages only when `field.hasError` is true\n4. **Root components are automatically reactive** — forms use MobX observables which are automatically tracked in root components via `autorun()`. Use `observer()` only if you extract form logic into a sub-component\n5. **Submit returns boolean** — `submit*Form()` returns `true` on success, `false` on validation error or server failure",
|
|
152
152
|
"tags": ["form", "login", "register", "address", "validation", "input"]
|
|
153
153
|
},
|
|
154
154
|
"async-data-patterns": {
|
|
155
155
|
"title": "Async Data & Loading Patterns",
|
|
156
156
|
"description": "How to handle async operations, loading states, and store data in ikas components",
|
|
157
|
-
"content": "## Async Data & Loading Patterns\n\n### 1. Store State is Null Until Initialized\n\nStore data (`cartStore.cart`, `customerStore.customer`) is `null` until the store initializes. Use
|
|
157
|
+
"content": "## Async Data & Loading Patterns\n\n### 1. Store State is Null Until Initialized\n\n**Note:** Root component exports are automatically reactive via the ikas runtime's `autorun()` — no `observer()` needed. The examples below show `observer()` on **sub-components** that independently read store data.\n\nStore data (`cartStore.cart`, `customerStore.customer`) is `null` until the store initializes. Use null-safe access:\n\n```tsx\nimport { observer } from \"@ikas/component-utils\";\nimport { cartStore } from \"@ikas/bp-storefront\";\n\n// Sub-component: needs observer() for independent reactivity\nconst CartBadge = observer(function CartBadge() {\n // cart is null until initialized — use optional chaining\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n return <span className=\"cart-badge\">{itemCount}</span>;\n});\n```\n\n### 2. Loading Pattern for Async Operations\n\nUse `useState` for local loading flags. Always use `try/finally` to clear the loading state:\n\n```tsx\nimport { useState } from \"preact/hooks\";\nimport { addItemToCart } from \"@ikas/bp-storefront\";\n\nconst [isLoading, setIsLoading] = useState(false);\n\nconst handleAddToCart = async () => {\n if (isLoading) return; // prevent double-click\n setIsLoading(true);\n try {\n await addItemToCart(variant, product, 1);\n } finally {\n setIsLoading(false);\n }\n};\n\n// In JSX:\n<button disabled={isLoading} onClick={handleAddToCart}>\n {isLoading ? \"Adding...\" : \"Add to Cart\"}\n</button>\n```\n\n### 3. Cart Operation Results\n\n`addItemToCart` returns a result object. Check for validation errors:\n\n```tsx\nconst result = await addItemToCart(variant, product, 1);\nif (result.success) {\n // Item added successfully\n} else if (result.validationError === \"INSUFFICIENT_STOCK\") {\n // Not enough stock\n} else if (result.validationError === \"INVALID_PRODUCT_OPTION_VALUES\") {\n // Variant options not fully selected\n}\n```\n\n### 4. Form Submission Results\n\nForm submit functions return `boolean` and set status flags on the form:\n\n```tsx\nconst success = await submitLoginForm(loginForm);\nif (success) {\n // loginForm.isSuccess is true\n Router.navigate(\"/account\");\n} else {\n // loginForm.isFailure is true\n // loginForm.responseMessage has the error message\n console.log(loginForm.responseMessage);\n}\n```\n\n### 5. Observer Auto Re-rendering\n\nStore updates automatically trigger re-renders. Root components get this via `autorun()`, sub-components via `observer()`. You do NOT need manual state management for store-derived values:\n\n```tsx\n// WRONG — unnecessary useState for store data\nconst [cartItems, setCartItems] = useState([]);\nuseEffect(() => { setCartItems(cartStore.cart?.orderLineItems ?? []); }, [cartStore.cart]);\n\n// CORRECT — observer sub-component for independent reactivity\nconst CartList = observer(function CartList() {\n const items = cartStore.cart?.orderLineItems ?? [];\n return (\n <div>\n {items.map((item) => (\n <div key={item.id}>{item.variant?.name}</div>\n ))}\n </div>\n );\n});\n\n// Root export uses CartList — no observer needed on root\nexport default function CartPage({ title }: Props) {\n return (\n <section>\n <h1>{title}</h1>\n <CartList />\n </section>\n );\n}\n```\n\n### 6. Pagination / Data Fetching\n\nList stores (blog, brand, category) use async pagination functions:\n\n```tsx\nimport { hasBlogListNextPage, getBlogListNextPage } from \"@ikas/bp-storefront\";\n\n// Check if more pages exist\nif (hasBlogListNextPage(blogList)) {\n // Fetch next page — mutates blogList in place, observer re-renders\n await getBlogListNextPage(blogList);\n}\n```\n\nThese functions mutate the list model in place. If your component uses `observer()`, it will re-render automatically when new data arrives.",
|
|
158
158
|
"tags": ["async", "loading", "error", "stores", "cart", "fetch"]
|
|
159
159
|
},
|
|
160
160
|
"product-detail-patterns": {
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"templates": {
|
|
3
|
+
"404": {
|
|
4
|
+
"title": "404 Page Section",
|
|
5
|
+
"description": "Page not found section with message and navigation back to home",
|
|
6
|
+
"files": {
|
|
7
|
+
"index.tsx": "import { Router } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function NotFoundSection({\n heading = \"Page Not Found\",\n message = \"The page you're looking for doesn't exist or has been moved.\",\n buttonText = \"Back to Home\",\n}: Props) {\n return (\n <section className=\"not-found-section\">\n <div className=\"not-found-inner\">\n <span className=\"not-found-code\">404</span>\n <h1 className=\"not-found-heading\">{heading}</h1>\n <p className=\"not-found-message\">{message}</p>\n <button\n className=\"not-found-btn\"\n onClick={() => Router.navigate(\"/\")}\n >\n {buttonText}\n </button>\n </div>\n </section>\n );\n}\n",
|
|
8
|
+
"types.ts": "export interface Props {\n heading?: string;\n message?: string;\n buttonText?: string;\n}\n",
|
|
9
|
+
"styles.css": ".not-found-section {\n width: 100%;\n padding: 80px 24px;\n text-align: center;\n}\n\n.not-found-inner {\n max-width: 480px;\n margin: 0 auto;\n}\n\n.not-found-code {\n font-size: 96px;\n font-weight: 800;\n color: #eee;\n line-height: 1;\n display: block;\n margin-bottom: 16px;\n}\n\n.not-found-heading {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 12px 0;\n}\n\n.not-found-message {\n font-size: 16px;\n color: #666;\n margin: 0 0 32px 0;\n line-height: 1.5;\n}\n\n.not-found-btn {\n display: inline-block;\n padding: 14px 32px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n}\n\n.not-found-btn:hover {\n background: #333;\n}\n",
|
|
10
|
+
"ikas-config-snippet.json": "{\n \"id\": \"not-found\",\n \"name\": \"404 Page\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"heading\", \"displayName\": \"Heading\", \"type\": \"TEXT\", \"defaultValue\": \"Page Not Found\" },\n { \"name\": \"message\", \"displayName\": \"Message\", \"type\": \"TEXT\", \"defaultValue\": \"The page you're looking for doesn't exist or has been moved.\" },\n { \"name\": \"buttonText\", \"displayName\": \"Button Text\", \"type\": \"TEXT\", \"defaultValue\": \"Back to Home\" }\n ]\n}\n"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
3
13
|
"header": {
|
|
4
14
|
"title": "Header Section",
|
|
5
15
|
"description": "Site header with logo, navigation links, cart/account icons, and mobile menu",
|
|
6
16
|
"files": {
|
|
7
|
-
"index.tsx": "import { useState } from \"preact/hooks\";\nimport {
|
|
17
|
+
"index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n cartStore,\n customerStore,\n hasCustomer,\n Router,\n IkasNavigationLink,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function HeaderSection({\n logo,\n navigationLinks,\n announcementText,\n}: Props) {\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const itemCount = cartStore.cart?.orderLineItems.length ?? 0;\n const isLoggedIn = hasCustomer(customerStore) as unknown as boolean;\n\n return (\n <section className=\"header-section\">\n {announcementText && (\n <div className=\"header-announcement\">\n <span>{announcementText}</span>\n </div>\n )}\n <div className=\"header-main\">\n <div className=\"header-inner\">\n <button\n className=\"header-hamburger\"\n onClick={() => setMobileMenuOpen(true)}\n aria-label=\"Open menu\"\n >\n <span /><span /><span />\n </button>\n <a className=\"header-logo\" href=\"/\">\n {logo ? (\n <img src={getDefaultSrc(logo)} alt={logo.altText || \"Logo\"} className=\"header-logo-img\" />\n ) : (\n <span className=\"header-logo-text\">Store</span>\n )}\n </a>\n <nav className=\"header-nav\">\n {navigationLinks?.map((link: IkasNavigationLink, i: number) => (\n <a key={i} href={link.href} className=\"header-nav-link\">\n {link.label}\n </a>\n ))}\n </nav>\n <div className=\"header-icons\">\n <button\n className=\"header-icon-btn\"\n onClick={() => Router.navigateToPage(isLoggedIn ? \"ACCOUNT\" : \"LOGIN\")}\n >\n Account\n </button>\n <button className=\"header-icon-btn\" onClick={() => Router.navigateToPage(\"CART\")}>\n Cart{itemCount > 0 && <span className=\"header-cart-badge\">{itemCount}</span>}\n </button>\n </div>\n </div>\n </div>\n {mobileMenuOpen && (\n <div className=\"header-mobile-overlay\">\n <div className=\"header-mobile-backdrop\" onClick={() => setMobileMenuOpen(false)} />\n <div className=\"header-mobile-menu\">\n <button className=\"header-mobile-close\" onClick={() => setMobileMenuOpen(false)}>×</button>\n <nav className=\"header-mobile-nav\">\n {navigationLinks?.map((link: IkasNavigationLink, i: number) => (\n <a key={i} href={link.href} className=\"header-mobile-link\" onClick={() => setMobileMenuOpen(false)}>\n {link.label}\n </a>\n ))}\n </nav>\n </div>\n </div>\n )}\n </section>\n );\n}\n",
|
|
8
18
|
"types.ts": "import { IkasNavigationLink, IkasImage } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: IkasImage | null;\n navigationLinks?: IkasNavigationLink[];\n announcementText?: string;\n}\n",
|
|
9
19
|
"styles.css": ".header-section {\n width: 100%;\n position: sticky;\n top: 0;\n z-index: 100;\n background: #fff;\n}\n\n.header-announcement {\n text-align: center;\n padding: 8px 16px;\n font-size: 13px;\n background: #111;\n color: #fff;\n}\n\n.header-inner {\n max-width: 1200px;\n margin: 0 auto;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 24px;\n gap: 24px;\n}\n\n.header-logo { text-decoration: none; flex-shrink: 0; }\n.header-logo-img { height: 40px; width: auto; }\n.header-logo-text { font-size: 22px; font-weight: 700; color: #111; }\n\n.header-nav { display: flex; gap: 24px; flex: 1; justify-content: center; }\n.header-nav-link { font-size: 14px; font-weight: 500; color: #333; text-decoration: none; }\n\n.header-icons { display: flex; gap: 12px; align-items: center; }\n.header-icon-btn { background: none; border: none; cursor: pointer; color: #333; padding: 4px; position: relative; }\n.header-cart-badge { position: absolute; top: -4px; right: -6px; background: #111; color: #fff; font-size: 10px; width: 18px; height: 18px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }\n\n.header-hamburger { display: none; flex-direction: column; gap: 4px; background: none; border: none; cursor: pointer; }\n.header-hamburger span { display: block; width: 20px; height: 2px; background: #333; }\n\n.header-mobile-overlay { position: fixed; inset: 0; z-index: 200; }\n.header-mobile-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.4); }\n.header-mobile-menu { position: absolute; top: 0; left: 0; bottom: 0; width: 280px; background: #fff; padding: 24px; overflow-y: auto; }\n.header-mobile-close { font-size: 28px; background: none; border: none; cursor: pointer; margin-bottom: 16px; }\n.header-mobile-nav { display: flex; flex-direction: column; gap: 16px; }\n.header-mobile-link { font-size: 16px; font-weight: 500; color: #333; text-decoration: none; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }\n\n@media (max-width: 768px) {\n .header-hamburger { display: flex; }\n .header-nav { display: none; }\n}\n",
|
|
10
20
|
"ikas-config-snippet.json": "{\n \"id\": \"header\",\n \"name\": \"Header\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"logo\", \"displayName\": \"Logo\", \"type\": \"IMAGE\" },\n { \"name\": \"navigationLinks\", \"displayName\": \"Navigation Links\", \"type\": \"LIST_OF_LINK\" },\n { \"name\": \"announcementText\", \"displayName\": \"Announcement Text\", \"type\": \"TEXT\" }\n ]\n}\n"
|
|
@@ -24,7 +34,7 @@
|
|
|
24
34
|
"title": "Product Detail Section",
|
|
25
35
|
"description": "Product page with image gallery, variant selection, pricing, add-to-cart, and favorites",
|
|
26
36
|
"files": {
|
|
27
|
-
"index.tsx": "import { useState } from \"preact/hooks\";\nimport {
|
|
37
|
+
"index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n hasProductStock,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n isAddToCartEnabled,\n addItemToCart,\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n getProductVariantMainImage,\n getDefaultSrc,\n createMediaSrcset,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductDetail({\n product,\n addToCartButtonText = \"Add to Cart\",\n}: Props) {\n const [isAddingToCart, setIsAddingToCart] = useState(false);\n\n if (!product) return null;\n\n const variant = getSelectedProductVariant(product) as any;\n const variantTypes = getDisplayedProductVariantTypes(product);\n const inStock = hasProductStock(product) as unknown as boolean;\n const variantInStock = hasProductVariantStock(variant) as unknown as boolean;\n const canAddToCart = isAddToCartEnabled(product) as unknown as boolean;\n const hasDiscount = hasProductVariantDiscount(variant) as unknown as boolean;\n const finalPrice = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const originalPrice = hasDiscount ? (getProductVariantFormattedSellPrice(variant) as unknown as string) : null;\n const mainProductImage = getProductVariantMainImage(variant);\n const mainImage = mainProductImage?.image;\n const images: IkasImage[] = variant?.images?.length\n ? variant.images.map((pi: any) => pi.image).filter((img: any): img is IkasImage => img != null)\n : mainImage ? [mainImage] : [];\n const isFav = isFavoriteIkasProduct(product);\n\n const handleAddToCart = async () => {\n if (!canAddToCart || isAddingToCart) return;\n setIsAddingToCart(true);\n try {\n await addItemToCart(variant, product, 1);\n } finally {\n setIsAddingToCart(false);\n }\n };\n\n const toggleFav = async () => {\n if (isFav) await removeIkasProductFromFavorites(product);\n else await addIkasProductToFavorites(product);\n };\n\n return (\n <section className=\"product-detail\">\n <div className=\"product-detail-inner\">\n <div className=\"product-gallery\">\n {images[0] && <img className=\"product-main-image\" src={getDefaultSrc(images[0])} srcSet={createMediaSrcset(images[0])} sizes=\"(max-width: 768px) 100vw, 50vw\" alt={product.name} />}\n </div>\n <div className=\"product-info\">\n <h1 className=\"product-name\">{product.name}</h1>\n <div className=\"product-pricing\">\n <span className=\"product-final-price\">{finalPrice}</span>\n {originalPrice && <span className=\"product-original-price\">{originalPrice}</span>}\n </div>\n {variantTypes.length > 0 && (\n <div className=\"product-variants\">\n {variantTypes.map((vt) => (\n <div key={vt.variantType.id} className=\"variant-group\">\n <span className=\"variant-group-label\">{vt.variantType.name}</span>\n <div className=\"variant-options\">\n {vt.displayedVariantValues.map((dvv) => (\n <button\n key={dvv.variantValue.id}\n className={`variant-option-btn ${dvv.isSelected ? \"selected\" : \"\"}`}\n disabled={!dvv.hasStock}\n onClick={() => selectVariantValue(product, dvv.variantValue)}\n >\n {dvv.variantValue.name}\n </button>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n {!inStock && <span className=\"out-of-stock-notice\">Out of Stock</span>}\n <div className=\"product-actions\">\n <button className=\"add-to-cart-btn\" disabled={!canAddToCart || isAddingToCart} onClick={handleAddToCart}>\n {isAddingToCart ? \"Adding...\" : !variantInStock ? \"Out of Stock\" : addToCartButtonText}\n </button>\n <button className={`favorite-btn ${isFav ? \"is-favorite\" : \"\"}`} onClick={toggleFav}>\n {isFav ? \"\\u2665\" : \"\\u2661\"}\n </button>\n </div>\n {product.description && (\n <div className=\"product-description\" dangerouslySetInnerHTML={{ __html: product.description }} />\n )}\n </div>\n </div>\n </section>\n );\n}\n",
|
|
28
38
|
"types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n addToCartButtonText?: string;\n}\n",
|
|
29
39
|
"styles.css": ".product-detail {\n width: 100%;\n padding: 40px 24px;\n}\n\n.product-detail-inner {\n max-width: 1200px;\n margin: 0 auto;\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 48px;\n}\n\n.product-main-image {\n width: 100%;\n aspect-ratio: 1;\n object-fit: cover;\n border-radius: 8px;\n background: #f5f5f5;\n}\n\n.product-name { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 16px 0; }\n\n.product-pricing { display: flex; align-items: center; gap: 12px; margin-bottom: 24px; }\n.product-final-price { font-size: 22px; font-weight: 700; color: #111; }\n.product-original-price { font-size: 16px; color: #999; text-decoration: line-through; }\n\n.variant-group { margin-bottom: 16px; }\n.variant-group-label { font-size: 14px; font-weight: 600; color: #333; display: block; margin-bottom: 8px; }\n.variant-options { display: flex; gap: 8px; flex-wrap: wrap; }\n.variant-option-btn { padding: 8px 16px; border: 1.5px solid #ddd; border-radius: 6px; background: #fff; cursor: pointer; font-size: 14px; }\n.variant-option-btn.selected { border-color: #111; font-weight: 600; }\n.variant-option-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n\n.out-of-stock-notice { color: #e53935; font-size: 14px; font-weight: 600; }\n\n.product-actions { display: flex; gap: 12px; margin: 24px 0; }\n.add-to-cart-btn { flex: 1; padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; }\n.add-to-cart-btn:disabled { background: #ccc; cursor: not-allowed; }\n.favorite-btn { width: 48px; height: 48px; border: 1.5px solid #ddd; border-radius: 8px; background: #fff; cursor: pointer; font-size: 20px; }\n.favorite-btn.is-favorite { color: #e53935; border-color: #e53935; }\n\n.product-description { font-size: 15px; line-height: 1.7; color: #555; margin-top: 24px; }\n\n@media (max-width: 768px) {\n .product-detail-inner { grid-template-columns: 1fr; gap: 24px; }\n}\n",
|
|
30
40
|
"ikas-config-snippet.json": "{\n \"id\": \"product-detail\",\n \"name\": \"Product Detail\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"product\", \"displayName\": \"Product\", \"type\": \"PRODUCT\", \"required\": true },\n { \"name\": \"addToCartButtonText\", \"displayName\": \"Add to Cart Button Text\", \"type\": \"TEXT\", \"defaultValue\": \"Add to Cart\" }\n ]\n}\n"
|
|
@@ -34,7 +44,7 @@
|
|
|
34
44
|
"title": "Product List Section",
|
|
35
45
|
"description": "Product grid with filters, sorting, and pagination for category/search pages",
|
|
36
46
|
"files": {
|
|
37
|
-
"index.tsx": "import {
|
|
47
|
+
"index.tsx": "import {\n IkasImage,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n getProductListFilterCategories,\n getProductListSortOptions,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\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 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 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) => { productList.sort = (e.target as HTMLSelectElement).value; }}>\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 && filterCategories.length > 0 && (\n <aside className=\"product-list-filters\">\n {filterCategories.map((cat) => {\n const values = getFilterDisplayedValues(cat);\n return (\n <div key={cat.name} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{cat.name}</h3>\n {values.map((fv) => (\n <label key={fv.name} className=\"filter-value\">\n <input type=\"checkbox\" checked={fv.isSelected} onChange={() => handleFilterValueClick(productList, cat, fv)} />\n <span>{fv.name}</span>\n </label>\n ))}\n </div>\n );\n })}\n </aside>\n )}\n <div className=\"product-grid\">\n {products.length === 0 && <p className=\"product-grid-empty\">No products found.</p>}\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 return (\n <a key={product.id} 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 </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",
|
|
38
48
|
"types.ts": "import { IkasProductList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n productList: IkasProductList;\n title?: string;\n showFilters?: boolean;\n}\n",
|
|
39
49
|
"styles.css": ".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-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\n.product-grid {\n flex: 1;\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 24px;\n}\n\n.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\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",
|
|
40
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"
|
|
@@ -44,7 +54,7 @@
|
|
|
44
54
|
"title": "Cart Section",
|
|
45
55
|
"description": "Shopping cart with line items, quantity controls, totals, and checkout button",
|
|
46
56
|
"files": {
|
|
47
|
-
"index.tsx": "import {
|
|
57
|
+
"index.tsx": "import {\n cartStore,\n changeItemQuantity,\n removeItem,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getOrderLineItemFormattedFinalPrice,\n getOrderLineItemFormattedUnitPrice,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n Router,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function CartSection({\n emptyCartMessage = \"Your cart is empty\",\n}: Props) {\n const cart = cartStore.cart;\n const lineItems = cart?.orderLineItems ?? [];\n\n if (lineItems.length === 0) {\n return (\n <section className=\"cart-section\">\n <div className=\"cart-inner\">\n <p className=\"cart-empty\">{emptyCartMessage}</p>\n <button className=\"cart-continue-btn\" onClick={() => Router.navigate(\"/\")}>Continue Shopping</button>\n </div>\n </section>\n );\n }\n\n const handleQty = async (item: IkasOrderLineItem, delta: number) => {\n const newQty = item.quantity + delta;\n if (newQty < 1) return;\n await changeItemQuantity(item, newQty);\n };\n\n return (\n <section className=\"cart-section\">\n <div className=\"cart-inner\">\n <h1 className=\"cart-title\">Shopping Cart ({lineItems.length})</h1>\n <div className=\"cart-items\">\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n return (\n <div key={item.id} className=\"cart-item\">\n {image && <img className=\"cart-item-image\" src={getDefaultSrc(image)} alt={item.variant?.name || \"Product\"} />}\n <div className=\"cart-item-info\">\n <span className=\"cart-item-name\">{item.variant?.name}</span>\n <span className=\"cart-item-unit-price\">{getOrderLineItemFormattedUnitPrice(item)}</span>\n </div>\n <div className=\"cart-item-quantity\">\n <button onClick={() => handleQty(item, -1)}>-</button>\n <span>{item.quantity}</span>\n <button onClick={() => handleQty(item, 1)}>+</button>\n </div>\n <span className=\"cart-item-total\">{getOrderLineItemFormattedFinalPrice(item)}</span>\n <button className=\"cart-item-remove\" onClick={() => removeItem(item)}>Remove</button>\n </div>\n );\n })}\n </div>\n <div className=\"cart-summary\">\n <div className=\"cart-summary-row\">\n <span>Subtotal</span>\n <span>{getIkasOrderFormattedTotalPrice(cart!)}</span>\n </div>\n <div className=\"cart-summary-row cart-summary-total\">\n <span>Total</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(cart!)}</span>\n </div>\n <button className=\"cart-checkout-btn\" onClick={() => Router.navigateToPage(\"CHECKOUT\")}>Proceed to Checkout</button>\n </div>\n </div>\n </section>\n );\n}\n",
|
|
48
58
|
"types.ts": "export interface Props {\n emptyCartMessage?: string;\n}\n",
|
|
49
59
|
"styles.css": ".cart-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.cart-inner {\n max-width: 960px;\n margin: 0 auto;\n}\n\n.cart-title { font-size: 24px; font-weight: 700; color: #111; margin: 0 0 24px 0; }\n.cart-empty { font-size: 16px; color: #666; text-align: center; padding: 48px 0; }\n.cart-continue-btn { display: block; margin: 0 auto; padding: 12px 24px; font-size: 14px; font-weight: 600; color: #111; background: #fff; border: 1.5px solid #111; border-radius: 8px; cursor: pointer; }\n\n.cart-items { display: flex; flex-direction: column; gap: 16px; margin-bottom: 32px; }\n.cart-item { display: flex; align-items: center; gap: 16px; padding: 16px; border: 1px solid #eee; border-radius: 8px; }\n.cart-item-image { width: 80px; height: 80px; object-fit: cover; border-radius: 6px; background: #f5f5f5; }\n.cart-item-info { flex: 1; display: flex; flex-direction: column; gap: 4px; }\n.cart-item-name { font-size: 14px; font-weight: 600; color: #111; }\n.cart-item-unit-price { font-size: 13px; color: #666; }\n.cart-item-quantity { display: flex; align-items: center; gap: 8px; }\n.cart-item-quantity button { width: 32px; height: 32px; border: 1px solid #ddd; border-radius: 4px; background: #fff; cursor: pointer; }\n.cart-item-total { font-size: 15px; font-weight: 600; color: #111; min-width: 80px; text-align: right; }\n.cart-item-remove { padding: 4px 8px; font-size: 12px; color: #e53935; background: none; border: none; cursor: pointer; }\n\n.cart-summary { border-top: 1px solid #eee; padding-top: 24px; display: flex; flex-direction: column; gap: 12px; align-items: flex-end; }\n.cart-summary-row { display: flex; justify-content: space-between; width: 280px; font-size: 14px; color: #555; }\n.cart-summary-total { font-size: 18px; font-weight: 700; color: #111; }\n.cart-checkout-btn { width: 280px; padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; margin-top: 8px; }\n\n@media (max-width: 768px) {\n .cart-item { flex-wrap: wrap; }\n .cart-summary { align-items: stretch; }\n .cart-summary-row, .cart-checkout-btn { width: 100%; }\n}\n",
|
|
50
60
|
"ikas-config-snippet.json": "{\n \"id\": \"cart\",\n \"name\": \"Cart\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"emptyCartMessage\", \"displayName\": \"Empty Cart Message\", \"type\": \"TEXT\", \"defaultValue\": \"Your cart is empty\" }\n ]\n}\n"
|
|
@@ -54,7 +64,7 @@
|
|
|
54
64
|
"title": "Login Section",
|
|
55
65
|
"description": "Customer login form with email/password fields, forgot password link, and register link",
|
|
56
66
|
"files": {
|
|
57
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {
|
|
67
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getLoginForm,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function LoginSection({\n redirectAfterLogin = \"/account\",\n}: Props) {\n const loginForm = getLoginForm(customerStore);\n\n useEffect(() => {\n initLoginForm(loginForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitLoginForm(loginForm);\n if (success) Router.navigate(redirectAfterLogin);\n };\n\n return (\n <section className=\"login-section\">\n <div className=\"login-inner\">\n <h1 className=\"login-title\">Sign In</h1>\n {loginForm.isFailure && loginForm.responseMessage && (\n <div className=\"login-error-banner\">{loginForm.responseMessage}</div>\n )}\n <form className=\"login-form\" onSubmit={handleSubmit}>\n <div className=\"login-field\">\n <label className=\"login-label\">{loginForm.email.label}</label>\n <input\n className={`login-input ${loginForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={loginForm.email.placeholder}\n value={loginForm.email.value}\n onInput={(e) => setLoginFormEmail(loginForm, (e.target as HTMLInputElement).value)}\n />\n {loginForm.email.hasError && loginForm.email.message && (\n <span className=\"login-field-error\">{loginForm.email.message}</span>\n )}\n </div>\n <div className=\"login-field\">\n <label className=\"login-label\">{loginForm.password.label}</label>\n <input\n className={`login-input ${loginForm.password.hasError ? \"has-error\" : \"\"}`}\n type=\"password\"\n placeholder={loginForm.password.placeholder}\n value={loginForm.password.value}\n onInput={(e) => setLoginFormPassword(loginForm, (e.target as HTMLInputElement).value)}\n />\n {loginForm.password.hasError && loginForm.password.message && (\n <span className=\"login-field-error\">{loginForm.password.message}</span>\n )}\n </div>\n <a className=\"login-forgot-link\" href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"FORGOT_PASSWORD\"); }}>Forgot password?</a>\n <button className=\"login-submit-btn\" type=\"submit\" disabled={loginForm.isSubmitting}>\n {loginForm.isSubmitting ? \"Signing in...\" : \"Sign In\"}\n </button>\n </form>\n <p className=\"login-register-link\">\n Don't have an account?{\" \"}\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"REGISTER\"); }}>Create one</a>\n </p>\n </div>\n </section>\n );\n}\n",
|
|
58
68
|
"types.ts": "export interface Props {\n redirectAfterLogin?: string;\n}\n",
|
|
59
69
|
"styles.css": ".login-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.login-inner {\n max-width: 400px;\n margin: 0 auto;\n}\n\n.login-title { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 24px 0; text-align: center; }\n.login-error-banner { padding: 12px 16px; font-size: 14px; color: #b71c1c; background: #ffebee; border-radius: 8px; margin-bottom: 20px; }\n.login-form { display: flex; flex-direction: column; gap: 16px; }\n.login-field { display: flex; flex-direction: column; gap: 6px; }\n.login-label { font-size: 14px; font-weight: 600; color: #333; }\n.login-input { padding: 12px 14px; font-size: 15px; border: 1.5px solid #ddd; border-radius: 8px; outline: none; }\n.login-input:focus { border-color: #111; }\n.login-input.has-error { border-color: #e53935; }\n.login-field-error { font-size: 12px; color: #e53935; }\n.login-forgot-link { font-size: 13px; color: #666; text-decoration: none; align-self: flex-end; }\n.login-submit-btn { padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; margin-top: 8px; }\n.login-submit-btn:disabled { background: #ccc; cursor: not-allowed; }\n.login-register-link { font-size: 14px; color: #666; text-align: center; margin-top: 24px; }\n.login-register-link a { color: #111; font-weight: 600; text-decoration: none; }\n",
|
|
60
70
|
"ikas-config-snippet.json": "{\n \"id\": \"login\",\n \"name\": \"Login\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"redirectAfterLogin\", \"displayName\": \"Redirect After Login\", \"type\": \"TEXT\", \"defaultValue\": \"/account\" }\n ]\n}\n"
|
|
@@ -64,7 +74,7 @@
|
|
|
64
74
|
"title": "Register Section",
|
|
65
75
|
"description": "Customer registration form with name, email, and password fields",
|
|
66
76
|
"files": {
|
|
67
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {
|
|
77
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getRegisterForm,\n initRegisterForm,\n setRegisterFormEmail,\n setRegisterFormFirstName,\n setRegisterFormLastName,\n setRegisterFormPassword,\n submitRegisterForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function RegisterSection({\n redirectAfterRegister = \"/account\",\n}: Props) {\n const registerForm = getRegisterForm(customerStore);\n\n useEffect(() => {\n initRegisterForm(registerForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitRegisterForm(registerForm);\n if (success) Router.navigate(redirectAfterRegister);\n };\n\n return (\n <section className=\"register-section\">\n <div className=\"register-inner\">\n <h1 className=\"register-title\">Create Account</h1>\n {registerForm.isFailure && registerForm.responseMessage && (\n <div className=\"register-error-banner\">{registerForm.responseMessage}</div>\n )}\n <form className=\"register-form\" onSubmit={handleSubmit}>\n <div className=\"register-row\">\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.firstName.label}</label>\n <input\n className={`register-input ${registerForm.firstName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={registerForm.firstName.placeholder}\n value={registerForm.firstName.value}\n onInput={(e) => setRegisterFormFirstName(registerForm, (e.target as HTMLInputElement).value)}\n />\n {registerForm.firstName.hasError && registerForm.firstName.message && (\n <span className=\"register-field-error\">{registerForm.firstName.message}</span>\n )}\n </div>\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.lastName.label}</label>\n <input\n className={`register-input ${registerForm.lastName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={registerForm.lastName.placeholder}\n value={registerForm.lastName.value}\n onInput={(e) => setRegisterFormLastName(registerForm, (e.target as HTMLInputElement).value)}\n />\n {registerForm.lastName.hasError && registerForm.lastName.message && (\n <span className=\"register-field-error\">{registerForm.lastName.message}</span>\n )}\n </div>\n </div>\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.email.label}</label>\n <input\n className={`register-input ${registerForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={registerForm.email.placeholder}\n value={registerForm.email.value}\n onInput={(e) => setRegisterFormEmail(registerForm, (e.target as HTMLInputElement).value)}\n />\n {registerForm.email.hasError && registerForm.email.message && (\n <span className=\"register-field-error\">{registerForm.email.message}</span>\n )}\n </div>\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.password.label}</label>\n <input\n className={`register-input ${registerForm.password.hasError ? \"has-error\" : \"\"}`}\n type=\"password\"\n placeholder={registerForm.password.placeholder}\n value={registerForm.password.value}\n onInput={(e) => setRegisterFormPassword(registerForm, (e.target as HTMLInputElement).value)}\n />\n {registerForm.password.hasError && registerForm.password.message && (\n <span className=\"register-field-error\">{registerForm.password.message}</span>\n )}\n </div>\n <button className=\"register-submit-btn\" type=\"submit\" disabled={registerForm.isSubmitting}>\n {registerForm.isSubmitting ? \"Creating account...\" : \"Create Account\"}\n </button>\n </form>\n <p className=\"register-login-link\">\n Already have an account?{\" \"}\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"LOGIN\"); }}>Sign in</a>\n </p>\n </div>\n </section>\n );\n}\n",
|
|
68
78
|
"types.ts": "export interface Props {\n redirectAfterRegister?: string;\n}\n",
|
|
69
79
|
"styles.css": ".register-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.register-inner {\n max-width: 480px;\n margin: 0 auto;\n}\n\n.register-title { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 24px 0; text-align: center; }\n.register-error-banner { padding: 12px 16px; font-size: 14px; color: #b71c1c; background: #ffebee; border-radius: 8px; margin-bottom: 20px; }\n.register-form { display: flex; flex-direction: column; gap: 16px; }\n.register-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }\n.register-field { display: flex; flex-direction: column; gap: 6px; }\n.register-label { font-size: 14px; font-weight: 600; color: #333; }\n.register-input { padding: 12px 14px; font-size: 15px; border: 1.5px solid #ddd; border-radius: 8px; outline: none; }\n.register-input:focus { border-color: #111; }\n.register-input.has-error { border-color: #e53935; }\n.register-field-error { font-size: 12px; color: #e53935; }\n.register-submit-btn { padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; margin-top: 8px; }\n.register-submit-btn:disabled { background: #ccc; cursor: not-allowed; }\n.register-login-link { font-size: 14px; color: #666; text-align: center; margin-top: 24px; }\n.register-login-link a { color: #111; font-weight: 600; text-decoration: none; }\n\n@media (max-width: 480px) {\n .register-row { grid-template-columns: 1fr; }\n}\n",
|
|
70
80
|
"ikas-config-snippet.json": "{\n \"id\": \"register\",\n \"name\": \"Register\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"redirectAfterRegister\", \"displayName\": \"Redirect After Register\", \"type\": \"TEXT\", \"defaultValue\": \"/account\" }\n ]\n}\n"
|
|
@@ -74,7 +84,7 @@
|
|
|
74
84
|
"title": "Forgot Password Section",
|
|
75
85
|
"description": "Password reset form with email input and success/error states",
|
|
76
86
|
"files": {
|
|
77
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {
|
|
87
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getForgotPasswordForm,\n initForgotPasswordForm,\n setForgotPasswordFormEmail,\n submitForgotPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ForgotPasswordSection({\n successMessage = \"Password reset link has been sent to your email.\",\n}: Props) {\n const forgotForm = getForgotPasswordForm(customerStore);\n\n useEffect(() => {\n initForgotPasswordForm(forgotForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitForgotPasswordForm(forgotForm);\n };\n\n return (\n <section className=\"forgot-section\">\n <div className=\"forgot-inner\">\n <h1 className=\"forgot-title\">Forgot Password</h1>\n <p className=\"forgot-subtitle\">Enter your email and we'll send you a reset link.</p>\n {forgotForm.isSuccess && <div className=\"forgot-success-banner\">{successMessage}</div>}\n {forgotForm.isFailure && forgotForm.responseMessage && (\n <div className=\"forgot-error-banner\">{forgotForm.responseMessage}</div>\n )}\n {!forgotForm.isSuccess && (\n <form className=\"forgot-form\" onSubmit={handleSubmit}>\n <div className=\"forgot-field\">\n <label className=\"forgot-label\">{forgotForm.email.label}</label>\n <input\n className={`forgot-input ${forgotForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={forgotForm.email.placeholder}\n value={forgotForm.email.value}\n onInput={(e) => setForgotPasswordFormEmail(forgotForm, (e.target as HTMLInputElement).value)}\n />\n {forgotForm.email.hasError && forgotForm.email.message && (\n <span className=\"forgot-field-error\">{forgotForm.email.message}</span>\n )}\n </div>\n <button className=\"forgot-submit-btn\" type=\"submit\" disabled={forgotForm.isSubmitting}>\n {forgotForm.isSubmitting ? \"Sending...\" : \"Send Reset Link\"}\n </button>\n </form>\n )}\n <p className=\"forgot-back-link\">\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"LOGIN\"); }}>Back to Sign In</a>\n </p>\n </div>\n </section>\n );\n}\n",
|
|
78
88
|
"types.ts": "export interface Props {\n successMessage?: string;\n}\n",
|
|
79
89
|
"styles.css": ".forgot-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.forgot-inner {\n max-width: 400px;\n margin: 0 auto;\n}\n\n.forgot-title { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 8px 0; text-align: center; }\n.forgot-subtitle { font-size: 15px; color: #666; text-align: center; margin: 0 0 24px 0; }\n.forgot-success-banner { padding: 12px 16px; font-size: 14px; color: #1b5e20; background: #e8f5e9; border-radius: 8px; margin-bottom: 20px; }\n.forgot-error-banner { padding: 12px 16px; font-size: 14px; color: #b71c1c; background: #ffebee; border-radius: 8px; margin-bottom: 20px; }\n.forgot-form { display: flex; flex-direction: column; gap: 16px; }\n.forgot-field { display: flex; flex-direction: column; gap: 6px; }\n.forgot-label { font-size: 14px; font-weight: 600; color: #333; }\n.forgot-input { padding: 12px 14px; font-size: 15px; border: 1.5px solid #ddd; border-radius: 8px; outline: none; }\n.forgot-input:focus { border-color: #111; }\n.forgot-input.has-error { border-color: #e53935; }\n.forgot-field-error { font-size: 12px; color: #e53935; }\n.forgot-submit-btn { padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; margin-top: 8px; }\n.forgot-submit-btn:disabled { background: #ccc; cursor: not-allowed; }\n.forgot-back-link { font-size: 14px; color: #666; text-align: center; margin-top: 24px; }\n.forgot-back-link a { color: #111; font-weight: 600; text-decoration: none; }\n",
|
|
80
90
|
"ikas-config-snippet.json": "{\n \"id\": \"forgot-password\",\n \"name\": \"Forgot Password\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"successMessage\", \"displayName\": \"Success Message\", \"type\": \"TEXT\", \"defaultValue\": \"Password reset link has been sent to your email.\" }\n ]\n}\n"
|
|
@@ -84,7 +94,7 @@
|
|
|
84
94
|
"title": "Account Orders Section",
|
|
85
95
|
"description": "Customer order history list with order details and status",
|
|
86
96
|
"files": {
|
|
87
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {
|
|
97
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getOrders,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderTotalItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountOrdersSection({\n title = \"My Orders\",\n emptyMessage = \"You have no orders yet.\",\n}: Props) {\n useEffect(() => {\n getOrders(customerStore);\n }, []);\n\n const orders = customerStore.orders ?? [];\n\n return (\n <section className=\"orders-section\">\n <div className=\"orders-inner\">\n <h1 className=\"orders-title\">{title}</h1>\n {orders.length === 0 && (\n <div className=\"orders-empty\">\n <p>{emptyMessage}</p>\n <button className=\"orders-shop-btn\" onClick={() => Router.navigate(\"/\")}>Start Shopping</button>\n </div>\n )}\n <div className=\"orders-list\">\n {orders.map((order) => (\n <a key={order.id} href={getIkasOrderHref(order)} className=\"order-card\">\n <div className=\"order-card-header\">\n <span className=\"order-number\">Order #{order.orderNumber}</span>\n <span className=\"order-status\">{getIkasOrderPackageStatusTranslation(order)}</span>\n </div>\n <div className=\"order-card-details\">\n <span className=\"order-date\">{getIkasOrderFormattedOrderedAt(order)}</span>\n <span className=\"order-items\">{getIkasOrderTotalItemCount(order)} items</span>\n <span className=\"order-total\">{getIkasOrderFormattedTotalFinalPrice(order)}</span>\n </div>\n </a>\n ))}\n </div>\n </div>\n </section>\n );\n}\n",
|
|
88
98
|
"types.ts": "export interface Props {\n title?: string;\n emptyMessage?: string;\n}\n",
|
|
89
99
|
"styles.css": ".orders-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.orders-inner {\n max-width: 800px;\n margin: 0 auto;\n}\n\n.orders-title { font-size: 24px; font-weight: 700; color: #111; margin: 0 0 24px 0; }\n\n.orders-empty { text-align: center; padding: 48px 0; }\n.orders-empty p { font-size: 16px; color: #666; margin: 0 0 16px 0; }\n.orders-shop-btn { padding: 12px 24px; font-size: 14px; font-weight: 600; color: #111; background: #fff; border: 1.5px solid #111; border-radius: 8px; cursor: pointer; }\n\n.orders-list { display: flex; flex-direction: column; gap: 12px; }\n.order-card { display: block; text-decoration: none; color: inherit; padding: 20px; border: 1px solid #eee; border-radius: 8px; transition: border-color 0.15s; }\n.order-card:hover { border-color: #111; }\n.order-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }\n.order-number { font-size: 15px; font-weight: 600; color: #111; }\n.order-status { font-size: 13px; font-weight: 500; color: #1976d2; background: #e3f2fd; padding: 4px 10px; border-radius: 12px; }\n.order-card-details { display: flex; gap: 24px; font-size: 14px; color: #666; }\n",
|
|
90
100
|
"ikas-config-snippet.json": "{\n \"id\": \"account-orders\",\n \"name\": \"Account Orders\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"My Orders\" },\n { \"name\": \"emptyMessage\", \"displayName\": \"Empty Message\", \"type\": \"TEXT\", \"defaultValue\": \"You have no orders yet.\" }\n ]\n}\n"
|
|
@@ -94,7 +104,7 @@
|
|
|
94
104
|
"title": "Account Addresses Section",
|
|
95
105
|
"description": "Customer address list with add/delete functionality",
|
|
96
106
|
"files": {
|
|
97
|
-
"index.tsx": "import {
|
|
107
|
+
"index.tsx": "import {\n customerStore,\n deleteAddress,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountAddressesSection({\n title = \"My Addresses\",\n}: Props) {\n const addresses = customerStore.customer?.addresses ?? [];\n\n const handleDelete = async (addressId: string) => {\n await deleteAddress(customerStore, addressId);\n };\n\n return (\n <section className=\"addresses-section\">\n <div className=\"addresses-inner\">\n <h1 className=\"addresses-title\">{title}</h1>\n {addresses.length === 0 && (\n <p className=\"addresses-empty\">No addresses saved yet.</p>\n )}\n <div className=\"addresses-grid\">\n {addresses.map((addr) => (\n <div key={addr.id} className=\"address-card\">\n <p className=\"address-name\">{addr.firstName} {addr.lastName}</p>\n <p className=\"address-line\">{addr.addressLine1}</p>\n {addr.addressLine2 && <p className=\"address-line\">{addr.addressLine2}</p>}\n <p className=\"address-line\">{addr.city}, {addr.state?.name} {addr.postalCode}</p>\n <p className=\"address-phone\">{addr.phone}</p>\n <button className=\"address-delete-btn\" onClick={() => handleDelete(addr.id)}>Delete</button>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n",
|
|
98
108
|
"types.ts": "export interface Props {\n title?: string;\n}\n",
|
|
99
109
|
"styles.css": ".addresses-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.addresses-inner {\n max-width: 800px;\n margin: 0 auto;\n}\n\n.addresses-title { font-size: 24px; font-weight: 700; color: #111; margin: 0 0 24px 0; }\n.addresses-empty { font-size: 16px; color: #666; text-align: center; padding: 48px 0; }\n\n.addresses-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 16px;\n}\n\n.address-card { padding: 20px; border: 1px solid #eee; border-radius: 8px; }\n.address-name { font-size: 15px; font-weight: 600; color: #111; margin: 0 0 8px 0; }\n.address-line { font-size: 14px; color: #555; margin: 0 0 4px 0; }\n.address-phone { font-size: 14px; color: #555; margin: 8px 0; }\n.address-delete-btn { font-size: 13px; color: #e53935; background: none; border: none; cursor: pointer; padding: 0; margin-top: 8px; }\n\n@media (max-width: 768px) {\n .addresses-grid { grid-template-columns: 1fr; }\n}\n",
|
|
100
110
|
"ikas-config-snippet.json": "{\n \"id\": \"account-addresses\",\n \"name\": \"Account Addresses\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"My Addresses\" }\n ]\n}\n"
|
|
@@ -104,7 +114,7 @@
|
|
|
104
114
|
"title": "Favorites Section",
|
|
105
115
|
"description": "Customer favorites/wishlist with product cards and remove functionality",
|
|
106
116
|
"files": {
|
|
107
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {
|
|
117
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getFavoriteProducts,\n removeIkasProductFromFavorites,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getDefaultSrc,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function FavoritesSection({\n title = \"My Favorites\",\n}: Props) {\n useEffect(() => {\n getFavoriteProducts(customerStore);\n }, []);\n\n const favorites = customerStore.favoriteProducts ?? [];\n\n return (\n <section className=\"favorites-section\">\n <div className=\"favorites-inner\">\n <h1 className=\"favorites-title\">{title}</h1>\n {favorites.length === 0 && (\n <p className=\"favorites-empty\">You haven't added any favorites yet.</p>\n )}\n <div className=\"favorites-grid\">\n {favorites.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 return (\n <div key={product.id} className=\"favorites-card\">\n <a href={getSelectedProductVariantHref(product)} className=\"favorites-card-link\">\n {image && <img src={getDefaultSrc(image)} alt={product.name} className=\"favorites-card-image\" />}\n <h3 className=\"favorites-card-name\">{product.name}</h3>\n <span className=\"favorites-card-price\">{price}</span>\n </a>\n <button className=\"favorites-remove-btn\" onClick={() => removeIkasProductFromFavorites(product)}>Remove</button>\n </div>\n );\n })}\n </div>\n </div>\n </section>\n );\n}\n",
|
|
108
118
|
"types.ts": "export interface Props {\n title?: string;\n}\n",
|
|
109
119
|
"styles.css": ".favorites-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.favorites-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.favorites-title { font-size: 24px; font-weight: 700; color: #111; margin: 0 0 24px 0; }\n.favorites-empty { font-size: 16px; color: #666; text-align: center; padding: 48px 0; }\n\n.favorites-grid {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 24px;\n}\n\n.favorites-card { position: relative; }\n.favorites-card-link { text-decoration: none; color: inherit; }\n.favorites-card-image { width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 8px; background: #f5f5f5; }\n.favorites-card-name { font-size: 14px; font-weight: 500; color: #111; margin: 10px 0 4px; }\n.favorites-card-price { font-size: 14px; font-weight: 600; color: #111; }\n.favorites-remove-btn { font-size: 13px; color: #e53935; background: none; border: none; cursor: pointer; padding: 0; margin-top: 8px; }\n\n@media (max-width: 768px) {\n .favorites-grid { grid-template-columns: repeat(2, 1fr); gap: 16px; }\n}\n",
|
|
110
120
|
"ikas-config-snippet.json": "{\n \"id\": \"favorites\",\n \"name\": \"Favorites\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"My Favorites\" }\n ]\n}\n"
|
|
@@ -114,7 +124,7 @@
|
|
|
114
124
|
"title": "Contact Form Section",
|
|
115
125
|
"description": "Contact form with name, email, phone, and message fields",
|
|
116
126
|
"files": {
|
|
117
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {
|
|
127
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getContactForm,\n initContactForm,\n setContactFormEmail,\n setContactFormFirstName,\n setContactFormLastName,\n setContactFormPhone,\n setContactFormMessage,\n submitContactForm,\n clearContactForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ContactFormSection({\n title = \"Contact Us\",\n successMessage = \"Thank you! Your message has been sent.\",\n}: Props) {\n const contactForm = getContactForm(customerStore);\n\n useEffect(() => {\n initContactForm(contactForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitContactForm(contactForm);\n if (success) clearContactForm(contactForm);\n };\n\n return (\n <section className=\"contact-section\">\n <div className=\"contact-inner\">\n <h1 className=\"contact-title\">{title}</h1>\n {contactForm.isSuccess && <div className=\"contact-success-banner\">{successMessage}</div>}\n {contactForm.isFailure && contactForm.responseMessage && (\n <div className=\"contact-error-banner\">{contactForm.responseMessage}</div>\n )}\n <form className=\"contact-form\" onSubmit={handleSubmit}>\n <div className=\"contact-row\">\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.firstName.label}</label>\n <input className={`contact-input ${contactForm.firstName.hasError ? \"has-error\" : \"\"}`} type=\"text\" placeholder={contactForm.firstName.placeholder} value={contactForm.firstName.value} onInput={(e) => setContactFormFirstName(contactForm, (e.target as HTMLInputElement).value)} />\n {contactForm.firstName.hasError && contactForm.firstName.message && <span className=\"contact-field-error\">{contactForm.firstName.message}</span>}\n </div>\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.lastName.label}</label>\n <input className={`contact-input ${contactForm.lastName.hasError ? \"has-error\" : \"\"}`} type=\"text\" placeholder={contactForm.lastName.placeholder} value={contactForm.lastName.value} onInput={(e) => setContactFormLastName(contactForm, (e.target as HTMLInputElement).value)} />\n {contactForm.lastName.hasError && contactForm.lastName.message && <span className=\"contact-field-error\">{contactForm.lastName.message}</span>}\n </div>\n </div>\n <div className=\"contact-row\">\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.email.label}</label>\n <input className={`contact-input ${contactForm.email.hasError ? \"has-error\" : \"\"}`} type=\"email\" placeholder={contactForm.email.placeholder} value={contactForm.email.value} onInput={(e) => setContactFormEmail(contactForm, (e.target as HTMLInputElement).value)} />\n {contactForm.email.hasError && contactForm.email.message && <span className=\"contact-field-error\">{contactForm.email.message}</span>}\n </div>\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.phone.label}</label>\n <input className={`contact-input ${contactForm.phone.hasError ? \"has-error\" : \"\"}`} type=\"tel\" placeholder={contactForm.phone.placeholder} value={contactForm.phone.value} onInput={(e) => setContactFormPhone(contactForm, (e.target as HTMLInputElement).value)} />\n {contactForm.phone.hasError && contactForm.phone.message && <span className=\"contact-field-error\">{contactForm.phone.message}</span>}\n </div>\n </div>\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.message.label}</label>\n <textarea className={`contact-textarea ${contactForm.message.hasError ? \"has-error\" : \"\"}`} placeholder={contactForm.message.placeholder} value={contactForm.message.value} rows={5} onInput={(e) => setContactFormMessage(contactForm, (e.target as HTMLTextAreaElement).value)} />\n {contactForm.message.hasError && contactForm.message.message && <span className=\"contact-field-error\">{contactForm.message.message}</span>}\n </div>\n <button className=\"contact-submit-btn\" type=\"submit\" disabled={contactForm.isSubmitting}>\n {contactForm.isSubmitting ? \"Sending...\" : \"Send Message\"}\n </button>\n </form>\n </div>\n </section>\n );\n}\n",
|
|
118
128
|
"types.ts": "export interface Props {\n title?: string;\n successMessage?: string;\n}\n",
|
|
119
129
|
"styles.css": ".contact-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.contact-inner {\n max-width: 600px;\n margin: 0 auto;\n}\n\n.contact-title { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 24px 0; text-align: center; }\n.contact-success-banner { padding: 12px 16px; font-size: 14px; color: #1b5e20; background: #e8f5e9; border-radius: 8px; margin-bottom: 20px; }\n.contact-error-banner { padding: 12px 16px; font-size: 14px; color: #b71c1c; background: #ffebee; border-radius: 8px; margin-bottom: 20px; }\n.contact-form { display: flex; flex-direction: column; gap: 16px; }\n.contact-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }\n.contact-field { display: flex; flex-direction: column; gap: 6px; }\n.contact-label { font-size: 14px; font-weight: 600; color: #333; }\n.contact-input { padding: 12px 14px; font-size: 15px; border: 1.5px solid #ddd; border-radius: 8px; outline: none; }\n.contact-textarea { padding: 12px 14px; font-size: 15px; border: 1.5px solid #ddd; border-radius: 8px; outline: none; resize: vertical; }\n.contact-input:focus, .contact-textarea:focus { border-color: #111; }\n.contact-input.has-error, .contact-textarea.has-error { border-color: #e53935; }\n.contact-field-error { font-size: 12px; color: #e53935; }\n.contact-submit-btn { padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; margin-top: 8px; }\n.contact-submit-btn:disabled { background: #ccc; cursor: not-allowed; }\n\n@media (max-width: 480px) {\n .contact-row { grid-template-columns: 1fr; }\n}\n",
|
|
120
130
|
"ikas-config-snippet.json": "{\n \"id\": \"contact-form\",\n \"name\": \"Contact Form\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"Contact Us\" },\n { \"name\": \"successMessage\", \"displayName\": \"Success Message\", \"type\": \"TEXT\", \"defaultValue\": \"Thank you! Your message has been sent.\" }\n ]\n}\n"
|
|
@@ -134,7 +144,7 @@
|
|
|
134
144
|
"title": "Blog List Section",
|
|
135
145
|
"description": "Blog post grid with images, dates, summaries, and pagination",
|
|
136
146
|
"files": {
|
|
137
|
-
"index.tsx": "import {
|
|
147
|
+
"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\nexport default function BlogListSection({\n blogList,\n title = \"Blog\",\n}: Props) {\n if (!blogList) return null;\n\n const blogs = blogList.blogs ?? [];\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",
|
|
138
148
|
"types.ts": "import { IkasBlogList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogList: IkasBlogList;\n title?: string;\n}\n",
|
|
139
149
|
"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",
|
|
140
150
|
"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"
|
|
@@ -154,7 +164,7 @@
|
|
|
154
164
|
"title": "Product Reviews Section",
|
|
155
165
|
"description": "Product reviews display with star ratings and review submission form",
|
|
156
166
|
"files": {
|
|
157
|
-
"index.tsx": "import { useState } from \"preact/hooks\";\nimport {
|
|
167
|
+
"index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n getProductCustomerReviews,\n getIkasProductCustomerReviewForm,\n setCustomerReviewFormTitle,\n setCustomerReviewFormStar,\n setCustomerReviewFormComment,\n submitCustomerReviewForm,\n isCustomerReviewLoginRequired,\n getIkasCustomerReviewFormattedDate,\n customerStore,\n hasCustomer,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductReviewsSection({\n product,\n title = \"Customer Reviews\",\n}: Props) {\n const [showForm, setShowForm] = useState(false);\n\n if (!product) return null;\n\n const reviews = getProductCustomerReviews(product) ?? [];\n const reviewForm = getIkasProductCustomerReviewForm(product);\n const loginRequired = isCustomerReviewLoginRequired() as unknown as boolean;\n const isLoggedIn = hasCustomer(customerStore) as unknown as boolean;\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitCustomerReviewForm(reviewForm);\n if (success) setShowForm(false);\n };\n\n return (\n <section className=\"reviews-section\">\n <div className=\"reviews-inner\">\n <div className=\"reviews-header\">\n <h2 className=\"reviews-title\">{title} ({reviews.length})</h2>\n {!showForm && (\n <button className=\"reviews-write-btn\" onClick={() => {\n if (loginRequired && !isLoggedIn) Router.navigateToPage(\"LOGIN\");\n else setShowForm(true);\n }}>Write a Review</button>\n )}\n </div>\n {showForm && (\n <form className=\"review-form\" onSubmit={handleSubmit}>\n <div className=\"review-form-stars\">\n <span className=\"review-form-label\">Rating</span>\n <div className=\"star-input\">\n {[1, 2, 3, 4, 5].map((star) => (\n <button key={star} type=\"button\" className={star <= reviewForm.star.value ? \"star-filled\" : \"star-empty\"} onClick={() => setCustomerReviewFormStar(reviewForm, star)}>\\u2605</button>\n ))}\n </div>\n </div>\n <div className=\"review-form-field\">\n <label className=\"review-form-label\">{reviewForm.title.label}</label>\n <input className=\"review-form-input\" type=\"text\" placeholder={reviewForm.title.placeholder} value={reviewForm.title.value} onInput={(e) => setCustomerReviewFormTitle(reviewForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"review-form-field\">\n <label className=\"review-form-label\">{reviewForm.comment.label}</label>\n <textarea className=\"review-form-textarea\" placeholder={reviewForm.comment.placeholder} value={reviewForm.comment.value} rows={4} onInput={(e) => setCustomerReviewFormComment(reviewForm, (e.target as HTMLTextAreaElement).value)} />\n </div>\n <div className=\"review-form-actions\">\n <button type=\"submit\" className=\"review-form-submit\" disabled={reviewForm.isSubmitting}>{reviewForm.isSubmitting ? \"Submitting...\" : \"Submit Review\"}</button>\n <button type=\"button\" className=\"review-form-cancel\" onClick={() => setShowForm(false)}>Cancel</button>\n </div>\n </form>\n )}\n {reviews.length === 0 && !showForm && <p className=\"reviews-empty\">No reviews yet. Be the first to review!</p>}\n <div className=\"review-list\">\n {reviews.map((review) => (\n <div key={review.id} className=\"review-card\">\n <div className=\"review-card-header\">\n <div className=\"reviews-stars\">\n {[1, 2, 3, 4, 5].map((s) => <span key={s} className={s <= review.star ? \"star-filled\" : \"star-empty\"}>\\u2605</span>)}\n </div>\n <span className=\"review-card-date\">{getIkasCustomerReviewFormattedDate(review)}</span>\n </div>\n <h4 className=\"review-card-title\">{review.title}</h4>\n <p className=\"review-card-comment\">{review.comment}</p>\n <span className=\"review-card-author\">{review.customerName}</span>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n",
|
|
158
168
|
"types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n title?: string;\n}\n",
|
|
159
169
|
"styles.css": ".reviews-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.reviews-inner {\n max-width: 800px;\n margin: 0 auto;\n}\n\n.reviews-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }\n.reviews-title { font-size: 22px; font-weight: 700; color: #111; margin: 0; }\n.reviews-write-btn { padding: 10px 20px; font-size: 14px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 6px; cursor: pointer; }\n.reviews-empty { font-size: 16px; color: #666; text-align: center; padding: 32px 0; }\n\n.review-form { padding: 24px; border: 1px solid #eee; border-radius: 8px; margin-bottom: 24px; display: flex; flex-direction: column; gap: 16px; }\n.review-form-label { font-size: 14px; font-weight: 600; color: #333; }\n.review-form-input { padding: 10px 14px; border: 1.5px solid #ddd; border-radius: 6px; font-size: 14px; outline: none; }\n.review-form-textarea { padding: 10px 14px; border: 1.5px solid #ddd; border-radius: 6px; font-size: 14px; outline: none; resize: vertical; }\n.review-form-field { display: flex; flex-direction: column; gap: 6px; }\n.review-form-stars { display: flex; align-items: center; gap: 12px; }\n.star-input { display: flex; gap: 4px; }\n.star-input button { background: none; border: none; font-size: 24px; cursor: pointer; padding: 0; }\n.star-filled { color: #f59e0b; }\n.star-empty { color: #ddd; }\n.review-form-actions { display: flex; gap: 12px; }\n.review-form-submit { padding: 10px 20px; font-size: 14px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 6px; cursor: pointer; }\n.review-form-submit:disabled { background: #ccc; }\n.review-form-cancel { padding: 10px 20px; font-size: 14px; color: #666; background: none; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; }\n\n.review-list { display: flex; flex-direction: column; gap: 16px; }\n.review-card { padding: 20px; border-bottom: 1px solid #eee; }\n.review-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }\n.reviews-stars { display: flex; gap: 2px; font-size: 16px; }\n.review-card-date { font-size: 13px; color: #999; }\n.review-card-title { font-size: 16px; font-weight: 600; color: #111; margin: 0 0 6px 0; }\n.review-card-comment { font-size: 14px; color: #555; line-height: 1.6; margin: 0 0 8px 0; }\n.review-card-author { font-size: 13px; color: #999; }\n",
|
|
160
170
|
"ikas-config-snippet.json": "{\n \"id\": \"product-reviews\",\n \"name\": \"Product Reviews\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"product\", \"displayName\": \"Product\", \"type\": \"PRODUCT\", \"required\": true },\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"Customer Reviews\" }\n ]\n}\n"
|
|
@@ -170,21 +180,11 @@
|
|
|
170
180
|
"ikas-config-snippet.json": "{\n \"id\": \"hero-banner\",\n \"name\": \"Hero Banner\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"heading\", \"displayName\": \"Heading\", \"type\": \"TEXT\", \"defaultValue\": \"Welcome to Our Store\" },\n { \"name\": \"subtitle\", \"displayName\": \"Subtitle\", \"type\": \"TEXT\" },\n { \"name\": \"buttonText\", \"displayName\": \"Button Text\", \"type\": \"TEXT\", \"defaultValue\": \"Shop Now\" },\n { \"name\": \"buttonLink\", \"displayName\": \"Button Link\", \"type\": \"TEXT\", \"defaultValue\": \"/\" },\n { \"name\": \"backgroundImage\", \"displayName\": \"Background Image\", \"type\": \"IMAGE\" },\n { \"name\": \"backgroundColor\", \"displayName\": \"Background Color\", \"type\": \"COLOR\", \"defaultValue\": \"#111\" },\n { \"name\": \"textColor\", \"displayName\": \"Text Color\", \"type\": \"COLOR\", \"defaultValue\": \"#fff\" }\n ]\n}\n"
|
|
171
181
|
}
|
|
172
182
|
},
|
|
173
|
-
"404": {
|
|
174
|
-
"title": "404 Page Section",
|
|
175
|
-
"description": "Page not found section with message and navigation back to home",
|
|
176
|
-
"files": {
|
|
177
|
-
"index.tsx": "import { Router } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function NotFoundSection({\n heading = \"Page Not Found\",\n message = \"The page you're looking for doesn't exist or has been moved.\",\n buttonText = \"Back to Home\",\n}: Props) {\n return (\n <section className=\"not-found-section\">\n <div className=\"not-found-inner\">\n <span className=\"not-found-code\">404</span>\n <h1 className=\"not-found-heading\">{heading}</h1>\n <p className=\"not-found-message\">{message}</p>\n <button\n className=\"not-found-btn\"\n onClick={() => Router.navigate(\"/\")}\n >\n {buttonText}\n </button>\n </div>\n </section>\n );\n}\n",
|
|
178
|
-
"types.ts": "export interface Props {\n heading?: string;\n message?: string;\n buttonText?: string;\n}\n",
|
|
179
|
-
"styles.css": ".not-found-section {\n width: 100%;\n padding: 80px 24px;\n text-align: center;\n}\n\n.not-found-inner {\n max-width: 480px;\n margin: 0 auto;\n}\n\n.not-found-code {\n font-size: 96px;\n font-weight: 800;\n color: #eee;\n line-height: 1;\n display: block;\n margin-bottom: 16px;\n}\n\n.not-found-heading {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 12px 0;\n}\n\n.not-found-message {\n font-size: 16px;\n color: #666;\n margin: 0 0 32px 0;\n line-height: 1.5;\n}\n\n.not-found-btn {\n display: inline-block;\n padding: 14px 32px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n}\n\n.not-found-btn:hover {\n background: #333;\n}\n",
|
|
180
|
-
"ikas-config-snippet.json": "{\n \"id\": \"not-found\",\n \"name\": \"404 Page\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"heading\", \"displayName\": \"Heading\", \"type\": \"TEXT\", \"defaultValue\": \"Page Not Found\" },\n { \"name\": \"message\", \"displayName\": \"Message\", \"type\": \"TEXT\", \"defaultValue\": \"The page you're looking for doesn't exist or has been moved.\" },\n { \"name\": \"buttonText\", \"displayName\": \"Button Text\", \"type\": \"TEXT\", \"defaultValue\": \"Back to Home\" }\n ]\n}\n"
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
183
|
"reset-password": {
|
|
184
184
|
"title": "Reset Password Section",
|
|
185
185
|
"description": "Password reset form with new password and confirm fields, success/error states",
|
|
186
186
|
"files": {
|
|
187
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {
|
|
187
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getRecoverPasswordForm,\n initRecoverPasswordForm,\n setRecoverPasswordFormPassword,\n setRecoverPasswordFormPasswordAgain,\n submitRecoverPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ResetPasswordSection({\n successMessage = \"Password has been reset successfully.\",\n}: Props) {\n const recoverForm = getRecoverPasswordForm(customerStore);\n\n useEffect(() => {\n initRecoverPasswordForm(recoverForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitRecoverPasswordForm(recoverForm);\n if (success) Router.navigateToPage(\"LOGIN\");\n };\n\n return (\n <section className=\"reset-section\">\n <div className=\"reset-inner\">\n <h1 className=\"reset-title\">Set New Password</h1>\n {recoverForm.isSuccess && <div className=\"reset-success-banner\">{successMessage}</div>}\n {recoverForm.isFailure && recoverForm.responseMessage && (\n <div className=\"reset-error-banner\">{recoverForm.responseMessage}</div>\n )}\n {!recoverForm.isSuccess && (\n <form className=\"reset-form\" onSubmit={handleSubmit}>\n <div className=\"reset-field\">\n <label className=\"reset-label\">{recoverForm.password.label}</label>\n <input\n className={`reset-input ${recoverForm.password.hasError ? \"has-error\" : \"\"}`}\n type=\"password\"\n placeholder={recoverForm.password.placeholder}\n value={recoverForm.password.value}\n onInput={(e) => setRecoverPasswordFormPassword(recoverForm, (e.target as HTMLInputElement).value)}\n />\n {recoverForm.password.hasError && recoverForm.password.message && (\n <span className=\"reset-field-error\">{recoverForm.password.message}</span>\n )}\n </div>\n <div className=\"reset-field\">\n <label className=\"reset-label\">{recoverForm.passwordAgain.label}</label>\n <input\n className={`reset-input ${recoverForm.passwordAgain.hasError ? \"has-error\" : \"\"}`}\n type=\"password\"\n placeholder={recoverForm.passwordAgain.placeholder}\n value={recoverForm.passwordAgain.value}\n onInput={(e) => setRecoverPasswordFormPasswordAgain(recoverForm, (e.target as HTMLInputElement).value)}\n />\n {recoverForm.passwordAgain.hasError && recoverForm.passwordAgain.message && (\n <span className=\"reset-field-error\">{recoverForm.passwordAgain.message}</span>\n )}\n </div>\n <button className=\"reset-submit-btn\" type=\"submit\" disabled={recoverForm.isSubmitting}>\n {recoverForm.isSubmitting ? \"Resetting...\" : \"Reset Password\"}\n </button>\n </form>\n )}\n <p className=\"reset-back-link\">\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"LOGIN\"); }}>Back to Sign In</a>\n </p>\n </div>\n </section>\n );\n}\n",
|
|
188
188
|
"types.ts": "export interface Props {\n successMessage?: string;\n}\n",
|
|
189
189
|
"styles.css": ".reset-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.reset-inner {\n max-width: 400px;\n margin: 0 auto;\n}\n\n.reset-title { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 24px 0; text-align: center; }\n.reset-success-banner { padding: 12px 16px; font-size: 14px; color: #1b5e20; background: #e8f5e9; border-radius: 8px; margin-bottom: 20px; }\n.reset-error-banner { padding: 12px 16px; font-size: 14px; color: #b71c1c; background: #ffebee; border-radius: 8px; margin-bottom: 20px; }\n.reset-form { display: flex; flex-direction: column; gap: 16px; }\n.reset-field { display: flex; flex-direction: column; gap: 6px; }\n.reset-label { font-size: 14px; font-weight: 600; color: #333; }\n.reset-input { padding: 12px 14px; font-size: 15px; border: 1.5px solid #ddd; border-radius: 8px; outline: none; }\n.reset-input:focus { border-color: #111; }\n.reset-input.has-error { border-color: #e53935; }\n.reset-field-error { font-size: 12px; color: #e53935; }\n.reset-submit-btn { padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; margin-top: 8px; }\n.reset-submit-btn:disabled { background: #ccc; cursor: not-allowed; }\n.reset-back-link { font-size: 14px; color: #666; text-align: center; margin-top: 24px; }\n.reset-back-link a { color: #111; font-weight: 600; text-decoration: none; }\n",
|
|
190
190
|
"ikas-config-snippet.json": "{\n \"id\": \"reset-password\",\n \"name\": \"Reset Password\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"successMessage\", \"displayName\": \"Success Message\", \"type\": \"TEXT\", \"defaultValue\": \"Password has been reset successfully.\" }\n ]\n}\n"
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
"title": "Account Info Section",
|
|
195
195
|
"description": "Account information edit form with first name, last name, and phone",
|
|
196
196
|
"files": {
|
|
197
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {
|
|
197
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountInfoSection({\n title = \"Account Information\",\n}: Props) {\n const accountForm = getAccountInfoForm(customerStore);\n\n useEffect(() => {\n initAccountInfoForm(accountForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitAccountInfoForm(accountForm);\n };\n\n return (\n <section className=\"account-info-section\">\n <div className=\"account-info-inner\">\n <h1 className=\"account-info-title\">{title}</h1>\n {accountForm.isSuccess && <div className=\"account-info-success\">Your information has been updated.</div>}\n {accountForm.isFailure && accountForm.responseMessage && (\n <div className=\"account-info-error\">{accountForm.responseMessage}</div>\n )}\n <form className=\"account-info-form\" onSubmit={handleSubmit}>\n <div className=\"account-info-row\">\n <div className=\"account-info-field\">\n <label className=\"account-info-label\">{accountForm.firstName.label}</label>\n <input\n className={`account-info-input ${accountForm.firstName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n value={accountForm.firstName.value}\n onInput={(e) => setAccountInfoFormFirstName(accountForm, (e.target as HTMLInputElement).value)}\n />\n {accountForm.firstName.hasError && accountForm.firstName.message && (\n <span className=\"account-info-field-error\">{accountForm.firstName.message}</span>\n )}\n </div>\n <div className=\"account-info-field\">\n <label className=\"account-info-label\">{accountForm.lastName.label}</label>\n <input\n className={`account-info-input ${accountForm.lastName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n value={accountForm.lastName.value}\n onInput={(e) => setAccountInfoFormLastName(accountForm, (e.target as HTMLInputElement).value)}\n />\n {accountForm.lastName.hasError && accountForm.lastName.message && (\n <span className=\"account-info-field-error\">{accountForm.lastName.message}</span>\n )}\n </div>\n </div>\n <div className=\"account-info-field\">\n <label className=\"account-info-label\">{accountForm.phone.label}</label>\n <input\n className={`account-info-input ${accountForm.phone.hasError ? \"has-error\" : \"\"}`}\n type=\"tel\"\n value={accountForm.phone.value}\n onInput={(e) => setAccountInfoFormPhone(accountForm, (e.target as HTMLInputElement).value)}\n />\n {accountForm.phone.hasError && accountForm.phone.message && (\n <span className=\"account-info-field-error\">{accountForm.phone.message}</span>\n )}\n </div>\n <button className=\"account-info-submit\" type=\"submit\" disabled={accountForm.isSubmitting}>\n {accountForm.isSubmitting ? \"Saving...\" : \"Save Changes\"}\n </button>\n </form>\n </div>\n </section>\n );\n}\n",
|
|
198
198
|
"types.ts": "export interface Props {\n title?: string;\n}\n",
|
|
199
199
|
"styles.css": ".account-info-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.account-info-inner {\n max-width: 480px;\n margin: 0 auto;\n}\n\n.account-info-title { font-size: 24px; font-weight: 700; color: #111; margin: 0 0 24px 0; }\n.account-info-success { padding: 12px 16px; font-size: 14px; color: #1b5e20; background: #e8f5e9; border-radius: 8px; margin-bottom: 20px; }\n.account-info-error { padding: 12px 16px; font-size: 14px; color: #b71c1c; background: #ffebee; border-radius: 8px; margin-bottom: 20px; }\n.account-info-form { display: flex; flex-direction: column; gap: 16px; }\n.account-info-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }\n.account-info-field { display: flex; flex-direction: column; gap: 6px; }\n.account-info-label { font-size: 14px; font-weight: 600; color: #333; }\n.account-info-input { padding: 12px 14px; font-size: 15px; border: 1.5px solid #ddd; border-radius: 8px; outline: none; }\n.account-info-input:focus { border-color: #111; }\n.account-info-input.has-error { border-color: #e53935; }\n.account-info-field-error { font-size: 12px; color: #e53935; }\n.account-info-submit { padding: 14px 24px; font-size: 16px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; margin-top: 8px; }\n.account-info-submit:disabled { background: #ccc; cursor: not-allowed; }\n\n@media (max-width: 480px) {\n .account-info-row { grid-template-columns: 1fr; }\n}\n",
|
|
200
200
|
"ikas-config-snippet.json": "{\n \"id\": \"account-info\",\n \"name\": \"Account Info\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"Account Information\" }\n ]\n}\n"
|
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
"title": "Order Detail Section",
|
|
205
205
|
"description": "Single order detail page with line items, adjustments, transactions, and totals",
|
|
206
206
|
"files": {
|
|
207
|
-
"index.tsx": "import { useEffect, useState } from \"preact/hooks\";\nimport {
|
|
207
|
+
"index.tsx": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderDisplayedPackages,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderLineVariantMainImage,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getDefaultSrc,\n Router,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function OrderDetailSection(_props: Props) {\n const [order, setOrder] = useState<IkasOrder | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n const params = Router.getPageParams();\n const orderId = params.id;\n if (orderId) {\n getOrder(customerStore, orderId).then((o) => {\n setOrder(o);\n setLoading(false);\n });\n }\n }, []);\n\n if (loading) return <section className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>Loading...</p></div></section>;\n if (!order) return <section className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>Order not found.</p></div></section>;\n\n const packages = getIkasOrderDisplayedPackages(order);\n const lineItems = order.orderLineItems ?? [];\n const adjustments = order.orderAdjustments ?? [];\n\n return (\n <section className=\"order-detail-section\">\n <div className=\"order-detail-inner\">\n <h1 className=\"order-detail-title\">Order #{order.orderNumber}</h1>\n <p className=\"order-detail-date\">{getIkasOrderFormattedOrderedAt(order)}</p>\n {packages.map((pkg, i) => (\n <span key={i} className=\"order-status-badge\">{getIkasOrderPackageStatusTranslation(order)}</span>\n ))}\n <div className=\"order-items\">\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n return (\n <div key={item.id} className=\"order-item\">\n {image && <img className=\"order-item-image\" src={getDefaultSrc(image)} alt=\"\" />}\n <div className=\"order-item-info\">\n <span className=\"order-item-name\">{item.variant?.name}</span>\n <span className=\"order-item-qty\">x{item.quantity}</span>\n <span className=\"order-item-price\">{getOrderLineItemFormattedFinalPriceWithQuantity(item)}</span>\n </div>\n </div>\n );\n })}\n </div>\n {adjustments.length > 0 && (\n <div className=\"order-adjustments\">\n {adjustments.map((adj: any, i: number) => (\n <div key={i} className=\"order-adjustment-row\">\n <span>{getOrderAdjustmentDisplayName(adj)}</span>\n <span>{getOrderAdjustmentFormattedAmount(adj)}</span>\n </div>\n ))}\n </div>\n )}\n <div className=\"order-total-row\">\n <span>Total</span>\n <span className=\"order-total-value\">{getIkasOrderFormattedTotalFinalPrice(order)}</span>\n </div>\n </div>\n </section>\n );\n}\n",
|
|
208
208
|
"types.ts": "export interface Props {}\n",
|
|
209
209
|
"styles.css": ".order-detail-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.order-detail-inner {\n max-width: 800px;\n margin: 0 auto;\n}\n\n.order-detail-title { font-size: 24px; font-weight: 700; color: #111; margin: 0 0 4px 0; }\n.order-detail-date { font-size: 14px; color: #999; margin: 0 0 16px 0; }\n.order-status-badge { display: inline-block; font-size: 13px; font-weight: 500; color: #1976d2; background: #e3f2fd; padding: 4px 10px; border-radius: 12px; margin-bottom: 24px; }\n\n.order-items { display: flex; flex-direction: column; gap: 12px; margin-bottom: 24px; }\n.order-item { display: flex; align-items: center; gap: 12px; padding: 12px; border: 1px solid #eee; border-radius: 8px; }\n.order-item-image { width: 64px; height: 64px; object-fit: cover; border-radius: 4px; background: #f5f5f5; }\n.order-item-info { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }\n.order-item-name { font-size: 14px; font-weight: 600; color: #111; }\n.order-item-qty { font-size: 13px; color: #666; }\n.order-item-price { font-size: 14px; font-weight: 600; color: #111; }\n\n.order-adjustments { margin-bottom: 16px; }\n.order-adjustment-row { display: flex; justify-content: space-between; font-size: 14px; color: #555; padding: 4px 0; }\n\n.order-total-row { display: flex; justify-content: space-between; border-top: 1px solid #eee; padding-top: 16px; font-size: 18px; font-weight: 700; color: #111; }\n",
|
|
210
210
|
"ikas-config-snippet.json": "{\n \"id\": \"order-detail\",\n \"name\": \"Order Detail\",\n \"type\": \"section\",\n \"props\": []\n}\n"
|
package/data/storefront-api.json
CHANGED