@ikas/code-components-mcp 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/framework.json +83 -2
- package/data/section-templates.json +214 -0
- package/data/storefront-api.json +2154 -1051
- package/data/storefront-types.json +3846 -3638
- package/dist/index.js +108 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
{
|
|
2
|
+
"templates": {
|
|
3
|
+
"header": {
|
|
4
|
+
"title": "Header Section",
|
|
5
|
+
"description": "Site header with logo, navigation links, cart/account icons, and mobile menu",
|
|
6
|
+
"files": {
|
|
7
|
+
"index.tsx": "import { useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n cartStore,\n customerStore,\n hasCustomer,\n Router,\n IkasNavigationLink,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nconst HeaderSection = observer(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?.src ? (\n <img src={logo.src} alt={logo.alt || \"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\nexport default HeaderSection;\n",
|
|
8
|
+
"types.ts": "import { IkasNavigationLink } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: { src: string; alt?: string };\n navigationLinks?: IkasNavigationLink[];\n announcementText?: string;\n}\n",
|
|
9
|
+
"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
|
+
"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"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"footer": {
|
|
14
|
+
"title": "Footer Section",
|
|
15
|
+
"description": "Site footer with logo, link columns, and copyright text",
|
|
16
|
+
"files": {
|
|
17
|
+
"index.tsx": "import { IkasNavigationLink } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function FooterSection({\n logo,\n description,\n linkColumn1Title = \"Shop\",\n linkColumn1,\n linkColumn2Title = \"Company\",\n linkColumn2,\n copyright = \"All rights reserved.\",\n}: Props) {\n return (\n <footer className=\"footer-section\">\n <div className=\"footer-inner\">\n <div className=\"footer-grid\">\n <div className=\"footer-brand\">\n {logo?.src ? (\n <img src={logo.src} alt={logo.alt || \"Logo\"} className=\"footer-logo\" />\n ) : (\n <span className=\"footer-logo-text\">Store</span>\n )}\n {description && <p className=\"footer-description\">{description}</p>}\n </div>\n <div className=\"footer-link-column\">\n <h4 className=\"footer-column-title\">{linkColumn1Title}</h4>\n <nav className=\"footer-links\">\n {linkColumn1?.map((link: IkasNavigationLink, i: number) => (\n <a key={i} href={link.href} className=\"footer-link\" target={link.isTargetBlank ? \"_blank\" : undefined}>\n {link.label}\n </a>\n ))}\n </nav>\n </div>\n <div className=\"footer-link-column\">\n <h4 className=\"footer-column-title\">{linkColumn2Title}</h4>\n <nav className=\"footer-links\">\n {linkColumn2?.map((link: IkasNavigationLink, i: number) => (\n <a key={i} href={link.href} className=\"footer-link\" target={link.isTargetBlank ? \"_blank\" : undefined}>\n {link.label}\n </a>\n ))}\n </nav>\n </div>\n </div>\n <div className=\"footer-bottom\">\n <p className=\"footer-copyright\">{copyright}</p>\n </div>\n </div>\n </footer>\n );\n}\n",
|
|
18
|
+
"types.ts": "import { IkasNavigationLink } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: { src: string; alt?: string };\n description?: string;\n linkColumn1Title?: string;\n linkColumn1?: IkasNavigationLink[];\n linkColumn2Title?: string;\n linkColumn2?: IkasNavigationLink[];\n copyright?: string;\n}\n",
|
|
19
|
+
"styles.css": ".footer-section {\n width: 100%;\n background: #111;\n color: #fff;\n padding: 48px 24px 24px;\n}\n\n.footer-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.footer-grid {\n display: grid;\n grid-template-columns: 2fr 1fr 1fr;\n gap: 48px;\n margin-bottom: 32px;\n}\n\n.footer-logo { height: 36px; width: auto; }\n.footer-logo-text { font-size: 20px; font-weight: 700; }\n.footer-description { font-size: 14px; color: #aaa; margin-top: 12px; line-height: 1.6; }\n\n.footer-column-title { font-size: 14px; font-weight: 600; margin: 0 0 16px 0; text-transform: uppercase; letter-spacing: 0.05em; }\n.footer-links { display: flex; flex-direction: column; gap: 10px; }\n.footer-link { font-size: 14px; color: #aaa; text-decoration: none; }\n.footer-link:hover { color: #fff; }\n\n.footer-bottom { border-top: 1px solid #333; padding-top: 20px; }\n.footer-copyright { font-size: 13px; color: #666; margin: 0; }\n\n@media (max-width: 768px) {\n .footer-grid { grid-template-columns: 1fr; gap: 32px; }\n}\n",
|
|
20
|
+
"ikas-config-snippet.json": "{\n \"id\": \"footer\",\n \"name\": \"Footer\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"logo\", \"displayName\": \"Logo\", \"type\": \"IMAGE\" },\n { \"name\": \"description\", \"displayName\": \"Description\", \"type\": \"TEXT\" },\n { \"name\": \"linkColumn1Title\", \"displayName\": \"Column 1 Title\", \"type\": \"TEXT\", \"defaultValue\": \"Shop\" },\n { \"name\": \"linkColumn1\", \"displayName\": \"Column 1 Links\", \"type\": \"LIST_OF_LINK\" },\n { \"name\": \"linkColumn2Title\", \"displayName\": \"Column 2 Title\", \"type\": \"TEXT\", \"defaultValue\": \"Company\" },\n { \"name\": \"linkColumn2\", \"displayName\": \"Column 2 Links\", \"type\": \"LIST_OF_LINK\" },\n { \"name\": \"copyright\", \"displayName\": \"Copyright Text\", \"type\": \"TEXT\", \"defaultValue\": \"All rights reserved.\" }\n ]\n}\n"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"product-detail": {
|
|
24
|
+
"title": "Product Detail Section",
|
|
25
|
+
"description": "Product page with image gallery, variant selection, pricing, add-to-cart, and favorites",
|
|
26
|
+
"files": {
|
|
27
|
+
"index.tsx": "import { useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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 IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nconst ProductDetail = observer(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 mainImage = getProductVariantMainImage(variant) as unknown as IkasImage | undefined;\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])} 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\nexport default ProductDetail;\n",
|
|
28
|
+
"types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n addToCartButtonText?: string;\n}\n",
|
|
29
|
+
"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
|
+
"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"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"product-list": {
|
|
34
|
+
"title": "Product List Section",
|
|
35
|
+
"description": "Product grid with filters, sorting, and pagination for category/search pages",
|
|
36
|
+
"files": {
|
|
37
|
+
"index.tsx": "import { observer } from \"@ikas/component-utils\";\nimport {\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\nconst ProductListSection = observer(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 image = getProductVariantMainImage(variant) as unknown as IkasImage | 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\nexport default ProductListSection;\n",
|
|
38
|
+
"types.ts": "import { IkasProductList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n productList: IkasProductList;\n title?: string;\n showFilters?: boolean;\n}\n",
|
|
39
|
+
"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
|
+
"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"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"cart": {
|
|
44
|
+
"title": "Cart Section",
|
|
45
|
+
"description": "Shopping cart with line items, quantity controls, totals, and checkout button",
|
|
46
|
+
"files": {
|
|
47
|
+
"index.tsx": "import { observer } from \"@ikas/component-utils\";\nimport {\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\nconst CartSection = observer(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\nexport default CartSection;\n",
|
|
48
|
+
"types.ts": "export interface Props {\n emptyCartMessage?: string;\n}\n",
|
|
49
|
+
"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
|
+
"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"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"login": {
|
|
54
|
+
"title": "Login Section",
|
|
55
|
+
"description": "Customer login form with email/password fields, forgot password link, and register link",
|
|
56
|
+
"files": {
|
|
57
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nconst LoginSection = observer(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\nexport default LoginSection;\n",
|
|
58
|
+
"types.ts": "export interface Props {\n redirectAfterLogin?: string;\n}\n",
|
|
59
|
+
"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
|
+
"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"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"register": {
|
|
64
|
+
"title": "Register Section",
|
|
65
|
+
"description": "Customer registration form with name, email, and password fields",
|
|
66
|
+
"files": {
|
|
67
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nconst RegisterSection = observer(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\nexport default RegisterSection;\n",
|
|
68
|
+
"types.ts": "export interface Props {\n redirectAfterRegister?: string;\n}\n",
|
|
69
|
+
"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
|
+
"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"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"forgot-password": {
|
|
74
|
+
"title": "Forgot Password Section",
|
|
75
|
+
"description": "Password reset form with email input and success/error states",
|
|
76
|
+
"files": {
|
|
77
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getForgotPasswordForm,\n initForgotPasswordForm,\n setForgotPasswordFormEmail,\n submitForgotPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nconst ForgotPasswordSection = observer(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\nexport default ForgotPasswordSection;\n",
|
|
78
|
+
"types.ts": "export interface Props {\n successMessage?: string;\n}\n",
|
|
79
|
+
"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
|
+
"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"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"account-orders": {
|
|
84
|
+
"title": "Account Orders Section",
|
|
85
|
+
"description": "Customer order history list with order details and status",
|
|
86
|
+
"files": {
|
|
87
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nconst AccountOrdersSection = observer(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\nexport default AccountOrdersSection;\n",
|
|
88
|
+
"types.ts": "export interface Props {\n title?: string;\n emptyMessage?: string;\n}\n",
|
|
89
|
+
"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
|
+
"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"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"account-addresses": {
|
|
94
|
+
"title": "Account Addresses Section",
|
|
95
|
+
"description": "Customer address list with add/delete functionality",
|
|
96
|
+
"files": {
|
|
97
|
+
"index.tsx": "import { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n deleteAddress,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nconst AccountAddressesSection = observer(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\nexport default AccountAddressesSection;\n",
|
|
98
|
+
"types.ts": "export interface Props {\n title?: string;\n}\n",
|
|
99
|
+
"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
|
+
"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"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"favorites": {
|
|
104
|
+
"title": "Favorites Section",
|
|
105
|
+
"description": "Customer favorites/wishlist with product cards and remove functionality",
|
|
106
|
+
"files": {
|
|
107
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nconst FavoritesSection = observer(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 image = getProductVariantMainImage(variant) as unknown as IkasImage | 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\nexport default FavoritesSection;\n",
|
|
108
|
+
"types.ts": "export interface Props {\n title?: string;\n}\n",
|
|
109
|
+
"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
|
+
"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"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"contact-form": {
|
|
114
|
+
"title": "Contact Form Section",
|
|
115
|
+
"description": "Contact form with name, email, phone, and message fields",
|
|
116
|
+
"files": {
|
|
117
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nconst ContactFormSection = observer(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\nexport default ContactFormSection;\n",
|
|
118
|
+
"types.ts": "export interface Props {\n title?: string;\n successMessage?: string;\n}\n",
|
|
119
|
+
"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
|
+
"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"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"faq": {
|
|
124
|
+
"title": "FAQ Section",
|
|
125
|
+
"description": "Accordion-style frequently asked questions section",
|
|
126
|
+
"files": {
|
|
127
|
+
"index.tsx": "import { useState } from \"preact/hooks\";\nimport { Props, FaqItem } from \"./types\";\n\nexport default function FaqSection({\n title = \"Frequently Asked Questions\",\n items = [],\n}: Props) {\n const [openIndex, setOpenIndex] = useState<number | null>(null);\n\n const handleToggle = (index: number) => {\n setOpenIndex(openIndex === index ? null : index);\n };\n\n return (\n <section className=\"faq-section\">\n <div className=\"faq-inner\">\n <h2 className=\"faq-title\">{title}</h2>\n {items.length === 0 && <p className=\"faq-empty\">No questions added yet.</p>}\n <div className=\"faq-list\">\n {items.map((item: FaqItem, index: number) => {\n const isOpen = openIndex === index;\n return (\n <div key={index} className={`faq-item ${isOpen ? \"faq-item-open\" : \"\"}`}>\n <button className=\"faq-question\" onClick={() => handleToggle(index)} aria-expanded={isOpen}>\n <span>{item.question}</span>\n <span className=\"faq-icon\">{isOpen ? \"\\u2212\" : \"+\"}</span>\n </button>\n {isOpen && (\n <div className=\"faq-answer\"><p>{item.answer}</p></div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n </section>\n );\n}\n",
|
|
128
|
+
"types.ts": "export interface FaqItem {\n question: string;\n answer: string;\n}\n\nexport interface Props {\n title?: string;\n items?: FaqItem[];\n}\n",
|
|
129
|
+
"styles.css": ".faq-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.faq-inner {\n max-width: 720px;\n margin: 0 auto;\n}\n\n.faq-title { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 32px 0; text-align: center; }\n.faq-empty { font-size: 16px; color: #666; text-align: center; }\n.faq-list { display: flex; flex-direction: column; }\n.faq-item { border-bottom: 1px solid #eee; }\n.faq-question { width: 100%; display: flex; justify-content: space-between; align-items: center; padding: 20px 0; font-size: 16px; font-weight: 500; color: #111; background: none; border: none; cursor: pointer; text-align: left; gap: 16px; }\n.faq-question:hover { color: #333; }\n.faq-icon { font-size: 20px; font-weight: 300; flex-shrink: 0; color: #666; }\n.faq-answer { padding: 0 0 20px 0; }\n.faq-answer p { font-size: 15px; line-height: 1.7; color: #555; margin: 0; }\n.faq-item-open .faq-question { font-weight: 600; }\n",
|
|
130
|
+
"ikas-config-snippet.json": "{\n \"id\": \"faq\",\n \"name\": \"FAQ\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"Frequently Asked Questions\" }\n ]\n}\n"
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"blog-list": {
|
|
134
|
+
"title": "Blog List Section",
|
|
135
|
+
"description": "Blog post grid with images, dates, summaries, and pagination",
|
|
136
|
+
"files": {
|
|
137
|
+
"index.tsx": "import { observer } from \"@ikas/component-utils\";\nimport {\n hasBlogListNextPage,\n getBlogListNextPage,\n hasBlogListPrevPage,\n getBlogListPrevPage,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nconst BlogListSection = observer(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\nexport default BlogListSection;\n",
|
|
138
|
+
"types.ts": "import { IkasBlogList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogList: IkasBlogList;\n title?: string;\n}\n",
|
|
139
|
+
"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
|
+
"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"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"blog-post": {
|
|
144
|
+
"title": "Blog Post Section",
|
|
145
|
+
"description": "Single blog post detail page with image, title, date, and HTML content",
|
|
146
|
+
"files": {
|
|
147
|
+
"index.tsx": "import {\n getIkasBlogFormattedDate,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function BlogPostSection({\n blogPost,\n}: Props) {\n if (!blogPost) return null;\n\n return (\n <section className=\"blog-post-section\">\n <div className=\"blog-post-inner\">\n {blogPost.image && (\n <img src={getDefaultSrc(blogPost.image)} alt={blogPost.title} className=\"blog-post-hero\" />\n )}\n <h1 className=\"blog-post-title\">{blogPost.title}</h1>\n <span className=\"blog-post-date\">{getIkasBlogFormattedDate(blogPost)}</span>\n <div className=\"blog-post-content\" dangerouslySetInnerHTML={{ __html: blogPost.content }} />\n </div>\n </section>\n );\n}\n",
|
|
148
|
+
"types.ts": "import { IkasBlogPost } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogPost: IkasBlogPost;\n}\n",
|
|
149
|
+
"styles.css": ".blog-post-section {\n width: 100%;\n padding: 40px 24px;\n}\n\n.blog-post-inner {\n max-width: 720px;\n margin: 0 auto;\n}\n\n.blog-post-hero { width: 100%; aspect-ratio: 16/9; object-fit: cover; border-radius: 8px; margin-bottom: 32px; }\n.blog-post-title { font-size: 32px; font-weight: 700; color: #111; margin: 0 0 8px 0; line-height: 1.3; }\n.blog-post-date { font-size: 14px; color: #999; display: block; margin-bottom: 32px; }\n.blog-post-content { font-size: 16px; line-height: 1.8; color: #333; }\n.blog-post-content img { max-width: 100%; height: auto; border-radius: 8px; margin: 24px 0; }\n.blog-post-content h2 { font-size: 24px; font-weight: 700; margin: 32px 0 16px; }\n.blog-post-content h3 { font-size: 20px; font-weight: 600; margin: 24px 0 12px; }\n.blog-post-content p { margin: 0 0 16px 0; }\n",
|
|
150
|
+
"ikas-config-snippet.json": "{\n \"id\": \"blog-post\",\n \"name\": \"Blog Post\",\n \"type\": \"section\",\n \"props\": [\n { \"name\": \"blogPost\", \"displayName\": \"Blog Post\", \"type\": \"BLOG_POST\", \"required\": true }\n ]\n}\n"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"product-reviews": {
|
|
154
|
+
"title": "Product Reviews Section",
|
|
155
|
+
"description": "Product reviews display with star ratings and review submission form",
|
|
156
|
+
"files": {
|
|
157
|
+
"index.tsx": "import { useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nconst ProductReviewsSection = observer(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\nexport default ProductReviewsSection;\n",
|
|
158
|
+
"types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n title?: string;\n}\n",
|
|
159
|
+
"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
|
+
"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"
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
"hero-banner": {
|
|
164
|
+
"title": "Hero Banner Section",
|
|
165
|
+
"description": "Full-width hero banner with heading, subtitle, CTA button, and background image",
|
|
166
|
+
"files": {
|
|
167
|
+
"index.tsx": "import { getDefaultSrc, Router } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function HeroBannerSection({\n heading = \"Welcome to Our Store\",\n subtitle,\n buttonText = \"Shop Now\",\n buttonLink = \"/\",\n backgroundImage,\n backgroundColor = \"#111\",\n textColor = \"#fff\",\n}: Props) {\n const bgStyle: Record<string, string> = { backgroundColor, color: textColor };\n if (backgroundImage?.src) {\n bgStyle.backgroundImage = `url(${backgroundImage.src})`;\n bgStyle.backgroundSize = \"cover\";\n bgStyle.backgroundPosition = \"center\";\n }\n\n return (\n <section className=\"hero-banner\" style={bgStyle}>\n <div className=\"hero-inner\">\n <h1 className=\"hero-heading\">{heading}</h1>\n {subtitle && <p className=\"hero-subtitle\">{subtitle}</p>}\n {buttonText && (\n <button\n className=\"hero-cta\"\n onClick={() => Router.navigate(buttonLink)}\n >\n {buttonText}\n </button>\n )}\n </div>\n </section>\n );\n}\n",
|
|
168
|
+
"types.ts": "export interface Props {\n heading?: string;\n subtitle?: string;\n buttonText?: string;\n buttonLink?: string;\n backgroundImage?: { src: string; alt?: string };\n backgroundColor?: string;\n textColor?: string;\n}\n",
|
|
169
|
+
"styles.css": ".hero-banner {\n width: 100%;\n min-height: 480px;\n display: flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 64px 24px;\n}\n\n.hero-inner {\n max-width: 720px;\n}\n\n.hero-heading {\n font-size: 48px;\n font-weight: 800;\n margin: 0 0 16px 0;\n line-height: 1.1;\n}\n\n.hero-subtitle {\n font-size: 18px;\n opacity: 0.85;\n margin: 0 0 32px 0;\n line-height: 1.5;\n}\n\n.hero-cta {\n display: inline-block;\n padding: 14px 32px;\n font-size: 16px;\n font-weight: 600;\n color: #111;\n background: #fff;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: opacity 0.15s;\n}\n\n.hero-cta:hover {\n opacity: 0.9;\n}\n\n@media (max-width: 768px) {\n .hero-banner { min-height: 360px; padding: 48px 24px; }\n .hero-heading { font-size: 32px; }\n .hero-subtitle { font-size: 16px; }\n}\n",
|
|
170
|
+
"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
|
+
}
|
|
172
|
+
},
|
|
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
|
+
"reset-password": {
|
|
184
|
+
"title": "Reset Password Section",
|
|
185
|
+
"description": "Password reset form with new password and confirm fields, success/error states",
|
|
186
|
+
"files": {
|
|
187
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nconst ResetPasswordSection = observer(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\nexport default ResetPasswordSection;\n",
|
|
188
|
+
"types.ts": "export interface Props {\n successMessage?: string;\n}\n",
|
|
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
|
+
"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"
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"account-info": {
|
|
194
|
+
"title": "Account Info Section",
|
|
195
|
+
"description": "Account information edit form with first name, last name, and phone",
|
|
196
|
+
"files": {
|
|
197
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nconst AccountInfoSection = observer(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\nexport default AccountInfoSection;\n",
|
|
198
|
+
"types.ts": "export interface Props {\n title?: string;\n}\n",
|
|
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
|
+
"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"
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"order-detail": {
|
|
204
|
+
"title": "Order Detail Section",
|
|
205
|
+
"description": "Single order detail page with line items, adjustments, transactions, and totals",
|
|
206
|
+
"files": {
|
|
207
|
+
"index.tsx": "import { useEffect, useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nconst OrderDetailSection = observer(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\nexport default OrderDetailSection;\n",
|
|
208
|
+
"types.ts": "export interface Props {}\n",
|
|
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
|
+
"ikas-config-snippet.json": "{\n \"id\": \"order-detail\",\n \"name\": \"Order Detail\",\n \"type\": \"section\",\n \"props\": []\n}\n"
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|