@ikas/code-components-mcp 0.34.0 → 0.36.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 +29 -9
- package/data/section-templates.json +133 -61
- package/data/storefront-api.json +90 -90
- package/data/storefront-types.json +1 -1
- package/package.json +1 -1
|
@@ -4,130 +4,184 @@
|
|
|
4
4
|
"title": "404 Page Section",
|
|
5
5
|
"description": "Page not found section with message and navigation back to home.",
|
|
6
6
|
"files": {
|
|
7
|
-
"index.tsx": "import { Router } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport 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\nexport default NotFoundSection;\n",
|
|
8
|
-
"types.ts": "export interface Props {\n heading?: string;\n message?: string;\n buttonText?: string;\n}\n",
|
|
7
|
+
"index.tsx": "import { Router } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport 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 backgroundColor = \"#ffffff\",\n}: Props) {\n return (\n <section className=\"not-found-section\" style={backgroundColor ? { backgroundColor } : undefined}>\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\nexport default NotFoundSection;\n",
|
|
8
|
+
"types.ts": "export interface Props {\n heading?: string;\n message?: string;\n buttonText?: string;\n backgroundColor?: string;\n}\n",
|
|
9
9
|
"styles.css": ".not-found-section {\n width: 100%;\n padding: 80px 24px;\n text-align: center;\n}\n\n.not-found-inner {\n max-width: 480px;\n margin: 0 auto;\n}\n\n.not-found-code {\n font-size: 96px;\n font-weight: 800;\n color: #eee;\n line-height: 1;\n display: block;\n margin-bottom: 16px;\n}\n\n.not-found-heading {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 12px 0;\n}\n\n.not-found-message {\n font-size: 16px;\n color: #666;\n margin: 0 0 32px 0;\n line-height: 1.5;\n}\n\n.not-found-btn {\n display: inline-block;\n padding: 14px 32px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n}\n\n.not-found-btn:hover {\n background: #333;\n}\n",
|
|
10
|
-
"ikas-config-snippet.json": "{\n \"id\": \"not-found\",\n \"name\": \"404 Page\",\n \"type\": \"section\",\n \"props\": [\n {
|
|
10
|
+
"ikas-config-snippet.json": "{\n \"id\": \"not-found\",\n \"name\": \"404 Page\",\n \"type\": \"section\",\n \"props\": [\n {\n \"name\": \"heading\",\n \"displayName\": \"Heading\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Page Not Found\"\n },\n {\n \"name\": \"message\",\n \"displayName\": \"Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"The page you're looking for doesn't exist or has been moved.\"\n },\n {\n \"name\": \"buttonText\",\n \"displayName\": \"Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Back to Home\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"account-addresses": {
|
|
14
14
|
"title": "Account Addresses Section",
|
|
15
15
|
"description": "Complete address management with list display, add/edit form (getIkasCustomerAddressForm/initAddressForm/setAddressForm*/submitAddressForm), and delete (deleteCustomerAddress). Uses isEmpty/isNotEmpty.",
|
|
16
16
|
"files": {
|
|
17
|
-
"index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n customerStore,\n getIkasCustomerAddressForm,\n initAddressForm,\n submitAddressForm,\n deleteCustomerAddress,\n setAddressFormFirstName,\n setAddressFormLastName,\n setAddressFormPhone,\n setAddressFormAddressLine1,\n setAddressFormAddressLine2,\n setAddressFormCity,\n setAddressFormDistrict,\n setAddressFormCountry,\n setAddressFormPostalCode,\n setAddressFormTitle,\n isEmpty,\n isNotEmpty,\n} from \"@ikas/bp-storefront\";\n\nexport default function AccountAddressesSection() {\n const [showForm, setShowForm] = useState(false);\n const [editingAddress, setEditingAddress] = useState<any>(null);\n const addresses = customerStore.customer?.addresses ?? [];\n\n const handleAddNew = () => {\n setEditingAddress(null);\n setShowForm(true);\n };\n\n const handleEdit = (address: any) => {\n setEditingAddress(address);\n setShowForm(true);\n };\n\n const handleDelete = async (address: any) => {\n await deleteCustomerAddress(customerStore, address);\n };\n\n return (\n <section className=\"addresses-section\">\n <div className=\"addresses-inner\">\n <div className=\"addresses-header\">\n <h1 className=\"addresses-title\">
|
|
18
|
-
"types.ts": "export interface Props {\n title?: string;\n}\n",
|
|
17
|
+
"index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n customerStore,\n getIkasCustomerAddressForm,\n initAddressForm,\n submitAddressForm,\n deleteCustomerAddress,\n setAddressFormFirstName,\n setAddressFormLastName,\n setAddressFormPhone,\n setAddressFormAddressLine1,\n setAddressFormAddressLine2,\n setAddressFormCity,\n setAddressFormDistrict,\n setAddressFormCountry,\n setAddressFormPostalCode,\n setAddressFormTitle,\n isEmpty,\n isNotEmpty,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountAddressesSection({\n title = \"My Addresses\",\n backgroundColor = \"#ffffff\",\n addButtonText = \"Add Address\",\n emptyMessage = \"No addresses saved yet.\",\n editButtonText = \"Edit\",\n deleteButtonText = \"Delete\",\n saveButtonText = \"Save Address\",\n savingButtonText = \"Saving...\",\n cancelButtonText = \"Cancel\",\n}: Props) {\n const [showForm, setShowForm] = useState(false);\n const [editingAddress, setEditingAddress] = useState<any>(null);\n const addresses = customerStore.customer?.addresses ?? [];\n\n const handleAddNew = () => {\n setEditingAddress(null);\n setShowForm(true);\n };\n\n const handleEdit = (address: any) => {\n setEditingAddress(address);\n setShowForm(true);\n };\n\n const handleDelete = async (address: any) => {\n await deleteCustomerAddress(customerStore, address);\n };\n\n return (\n <section className=\"addresses-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"addresses-inner\">\n <div className=\"addresses-header\">\n <h1 className=\"addresses-title\">{title}</h1>\n <button className=\"addresses-add-btn\" onClick={handleAddNew}>{addButtonText}</button>\n </div>\n\n {showForm && (\n <AddressFormComponent\n address={editingAddress}\n onDone={() => setShowForm(false)}\n saveButtonText={saveButtonText}\n savingButtonText={savingButtonText}\n cancelButtonText={cancelButtonText}\n />\n )}\n\n {isEmpty(addresses) && !showForm && (\n <p className=\"addresses-empty\">{emptyMessage}</p>\n )}\n\n {isNotEmpty(addresses) && (\n <div className=\"addresses-grid\">\n {addresses.map((addr: any) => (\n <div key={addr.id} className=\"address-card\">\n {addr.title && <h3 className=\"address-card-title\">{addr.title}</h3>}\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.postalCode}</p>\n <p className=\"address-phone\">{addr.phone}</p>\n <div className=\"address-actions\">\n <button onClick={() => handleEdit(addr)}>{editButtonText}</button>\n <button className=\"address-delete-btn\" onClick={() => handleDelete(addr)}>{deleteButtonText}</button>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </section>\n );\n}\n\nfunction AddressFormComponent({\n address,\n onDone,\n saveButtonText = \"Save Address\",\n savingButtonText = \"Saving...\",\n cancelButtonText = \"Cancel\",\n}: {\n address?: any;\n onDone: () => void;\n saveButtonText?: string;\n savingButtonText?: string;\n cancelButtonText?: string;\n}) {\n const addressForm = address\n ? getIkasCustomerAddressForm(address)\n : getIkasCustomerAddressForm({} as any);\n\n const handleInit = async () => {\n await initAddressForm(addressForm, address);\n };\n\n useState(() => { handleInit(); });\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitAddressForm(addressForm);\n if (success) onDone();\n };\n\n return (\n <form className=\"address-form\" onSubmit={handleSubmit}>\n <div className=\"address-form-field\">\n <label>{addressForm.title?.label}</label>\n <input value={addressForm.title?.value ?? \"\"} onInput={(e) => setAddressFormTitle(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"address-form-row\">\n <div className=\"address-form-field\">\n <label>{addressForm.firstName?.label}</label>\n <input value={addressForm.firstName?.value ?? \"\"} onInput={(e) => setAddressFormFirstName(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"address-form-field\">\n <label>{addressForm.lastName?.label}</label>\n <input value={addressForm.lastName?.value ?? \"\"} onInput={(e) => setAddressFormLastName(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n </div>\n <div className=\"address-form-field\">\n <label>{addressForm.phone?.label}</label>\n <input value={addressForm.phone?.value ?? \"\"} onInput={(e) => setAddressFormPhone(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"address-form-field\">\n <label>{addressForm.addressLine1?.label}</label>\n <input value={addressForm.addressLine1?.value ?? \"\"} onInput={(e) => setAddressFormAddressLine1(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"address-form-field\">\n <label>{addressForm.addressLine2?.label}</label>\n <input value={addressForm.addressLine2?.value ?? \"\"} onInput={(e) => setAddressFormAddressLine2(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"address-form-row\">\n <div className=\"address-form-field\">\n <label>{addressForm.city?.label}</label>\n <input value={addressForm.city?.value ?? \"\"} onInput={(e) => setAddressFormCity(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"address-form-field\">\n <label>{addressForm.district?.label}</label>\n <input value={addressForm.district?.value ?? \"\"} onInput={(e) => setAddressFormDistrict(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n </div>\n <div className=\"address-form-row\">\n <div className=\"address-form-field\">\n <label>{addressForm.postalCode?.label}</label>\n <input value={addressForm.postalCode?.value ?? \"\"} onInput={(e) => setAddressFormPostalCode(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n <div className=\"address-form-field\">\n <label>{addressForm.country?.label}</label>\n <input value={addressForm.country?.value ?? \"\"} onInput={(e) => setAddressFormCountry(addressForm, (e.target as HTMLInputElement).value)} />\n </div>\n </div>\n <div className=\"address-form-actions\">\n <button type=\"submit\" disabled={addressForm.isSubmitting}>\n {addressForm.isSubmitting ? savingButtonText : saveButtonText}\n </button>\n <button type=\"button\" onClick={onDone}>{cancelButtonText}</button>\n </div>\n </form>\n );\n}\n\n",
|
|
18
|
+
"types.ts": "export interface Props {\n title?: string;\n backgroundColor?: string;\n addButtonText?: string;\n emptyMessage?: string;\n editButtonText?: string;\n deleteButtonText?: string;\n saveButtonText?: string;\n savingButtonText?: string;\n cancelButtonText?: string;\n}\n",
|
|
19
19
|
"styles.css": ".addresses-section { width: 100%; padding: 40px 24px; }\n.addresses-inner { max-width: 800px; margin: 0 auto; }\n.addresses-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }\n.addresses-title { font-size: 24px; font-weight: 700; color: #111; margin: 0; }\n.addresses-add-btn { padding: 10px 20px; font-size: 14px; font-weight: 600; color: #fff; background: #111; border: none; border-radius: 8px; cursor: pointer; }\n.addresses-empty { font-size: 16px; color: #666; text-align: center; padding: 48px 0; }\n.addresses-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }\n.address-card { padding: 20px; border: 1px solid #eee; border-radius: 8px; }\n.address-card-title { font-size: 14px; font-weight: 700; color: #111; margin: 0 0 8px 0; text-transform: uppercase; letter-spacing: 0.5px; }\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-actions { display: flex; gap: 12px; margin-top: 12px; }\n.address-actions button { font-size: 13px; background: none; border: none; cursor: pointer; padding: 0; color: #111; font-weight: 600; }\n.address-delete-btn { color: #e53935 !important; }\n.address-form { display: flex; flex-direction: column; gap: 12px; padding: 20px; border: 1px solid #eee; border-radius: 8px; margin-bottom: 24px; }\n.address-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }\n.address-form-field { display: flex; flex-direction: column; gap: 4px; }\n.address-form-field label { font-size: 13px; font-weight: 600; color: #333; }\n.address-form-field input { padding: 10px 12px; font-size: 14px; border: 1.5px solid #ddd; border-radius: 6px; outline: none; }\n.address-form-field input:focus { border-color: #111; }\n.address-form-actions { display: flex; gap: 12px; margin-top: 8px; }\n.address-form-actions button { padding: 10px 20px; font-size: 14px; font-weight: 600; border-radius: 6px; cursor: pointer; }\n.address-form-actions button[type=\"submit\"] { color: #fff; background: #111; border: none; }\n.address-form-actions button[type=\"button\"] { color: #333; background: #fff; border: 1.5px solid #ddd; }\n@media (max-width: 768px) { .addresses-grid { grid-template-columns: 1fr; } .address-form-row { grid-template-columns: 1fr; } }\n",
|
|
20
|
-
"ikas-config-snippet.json": "{
|
|
20
|
+
"ikas-config-snippet.json": "{\n \"id\": \"account-addresses\",\n \"name\": \"Account Addresses\",\n \"type\": \"section\",\n \"props\": [\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"My Addresses\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"addButtonText\",\n \"displayName\": \"Add Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Add Address\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"emptyMessage\",\n \"displayName\": \"Empty State Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"No addresses saved yet.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"editButtonText\",\n \"displayName\": \"Edit Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Edit\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"deleteButtonText\",\n \"displayName\": \"Delete Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Delete\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"saveButtonText\",\n \"displayName\": \"Save Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Save Address\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"savingButtonText\",\n \"displayName\": \"Saving Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Saving...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"cancelButtonText\",\n \"displayName\": \"Cancel Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Cancel\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\"\n }\n ]\n}\n"
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"account-info": {
|
|
24
24
|
"title": "Account Info Section",
|
|
25
25
|
"description": "Complete account info section with first name, last name, and phone fields. Uses getAccountInfoForm/initAccountInfoForm/setAccountInfoForm*/submitAccountInfoForm pattern.",
|
|
26
26
|
"files": {
|
|
27
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\n\nexport default function AccountInfoSection() {\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\">
|
|
28
|
-
"types.ts": "export interface Props {}\n",
|
|
27
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountInfoSection({\n backgroundColor = \"#ffffff\",\n title = \"Account Information\",\n successMessage = \"Your information has been updated.\",\n submitButtonText = \"Save Changes\",\n submittingButtonText = \"Saving...\",\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\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"account-info-inner\">\n <h1 className=\"account-info-title\">{title}</h1>\n\n {accountForm.isSuccess && (\n <div className=\"account-info-success\">{successMessage}</div>\n )}\n {accountForm.isFailure && accountForm.responseMessage && (\n <div className=\"account-info-error\">{accountForm.responseMessage}</div>\n )}\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 ? submittingButtonText : submitButtonText}\n </button>\n </form>\n </div>\n </section>\n );\n}\n\n",
|
|
28
|
+
"types.ts": "export interface Props {\n backgroundColor?: string;\n title?: string;\n successMessage?: string;\n submitButtonText?: string;\n submittingButtonText?: string;\n}\n",
|
|
29
29
|
"styles.css": ".account-info-section { width: 100%; padding: 40px 24px; }\n.account-info-inner { max-width: 480px; margin: 0 auto; }\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@media (max-width: 480px) { .account-info-row { grid-template-columns: 1fr; } }\n",
|
|
30
|
-
"ikas-config-snippet.json": "{
|
|
30
|
+
"ikas-config-snippet.json": "{\n \"id\": \"account-info\",\n \"name\": \"Account Info\",\n \"type\": \"section\",\n \"props\": [\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Account Information\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"successMessage\",\n \"displayName\": \"Success Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Your information has been updated.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submitButtonText\",\n \"displayName\": \"Submit Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Save Changes\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submittingButtonText\",\n \"displayName\": \"Submitting Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Saving...\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\"\n }\n ]\n}\n"
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"account-orders": {
|
|
34
34
|
"title": "Account Orders Section",
|
|
35
35
|
"description": "Complete account orders section showing order history with order number, date, distinct item count (getIkasOrderDistinctItemCount), total price, package status, and order line item thumbnails (getIkasOrderLineVariantMainImage). Uses isEmpty/isNotEmpty utilities.",
|
|
36
36
|
"files": {
|
|
37
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getOrders,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderDistinctItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n isEmpty,\n isNotEmpty,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountOrdersSection({\n title = \"My Orders\",\n emptyMessage = \"You have no orders yet.\",\n}: Props) {\n useEffect(() => {\n getOrders(customerStore);\n }, []);\n\n const orders = customerStore.orders ?? [];\n\n return (\n <section className=\"orders-section\">\n <div className=\"orders-inner\">\n <h1 className=\"orders-title\">{title}</h1>\n\n {isEmpty(orders) && (\n <div className=\"orders-empty\">\n <p>{emptyMessage}</p>\n <button\n className=\"orders-shop-btn\"\n onClick={() => Router.navigate(\"/\")}\n >\n
|
|
38
|
-
"types.ts": "export interface Props {\n title?: string;\n emptyMessage?: string;\n}\n",
|
|
37
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getOrders,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderDistinctItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n isEmpty,\n isNotEmpty,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountOrdersSection({\n title = \"My Orders\",\n emptyMessage = \"You have no orders yet.\",\n backgroundColor = \"#ffffff\",\n shopButtonText = \"Start Shopping\",\n itemsText = \"items\",\n}: Props) {\n useEffect(() => {\n getOrders(customerStore);\n }, []);\n\n const orders = customerStore.orders ?? [];\n\n return (\n <section className=\"orders-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"orders-inner\">\n <h1 className=\"orders-title\">{title}</h1>\n\n {isEmpty(orders) && (\n <div className=\"orders-empty\">\n <p>{emptyMessage}</p>\n <button\n className=\"orders-shop-btn\"\n onClick={() => Router.navigate(\"/\")}\n >\n {shopButtonText}\n </button>\n </div>\n )}\n\n {isNotEmpty(orders) && (\n <div className=\"orders-list\">\n {orders.map((order) => {\n // Show first line item thumbnail\n const firstItem = order.orderLineItems?.[0];\n const thumbnail = firstItem?.variant\n ? getIkasOrderLineVariantMainImage(firstItem.variant)\n : null;\n\n return (\n <a\n key={order.id}\n href={getIkasOrderHref(order)}\n className=\"order-card\"\n >\n <div className=\"order-card-header\">\n <span className=\"order-number\">Order #{order.orderNumber}</span>\n <span className=\"order-status\">\n {getIkasOrderPackageStatusTranslation(order)}\n </span>\n </div>\n <div className=\"order-card-body\">\n {thumbnail && (\n <img\n className=\"order-thumbnail\"\n src={getDefaultSrc(thumbnail)}\n alt=\"\"\n />\n )}\n <div className=\"order-card-details\">\n <span className=\"order-date\">\n {getIkasOrderFormattedOrderedAt(order)}\n </span>\n <span className=\"order-items\">\n {getIkasOrderDistinctItemCount(order)} {itemsText}\n </span>\n <span className=\"order-total\">\n {getIkasOrderFormattedTotalFinalPrice(order)}\n </span>\n </div>\n </div>\n </a>\n );\n })}\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
38
|
+
"types.ts": "export interface Props {\n title?: string;\n emptyMessage?: string;\n backgroundColor?: string;\n shopButtonText?: string;\n itemsText?: string;\n}\n",
|
|
39
39
|
"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 {\n font-size: 24px;\n font-weight: 700;\n color: #111;\n margin: 0 0 24px 0;\n}\n\n.orders-empty {\n text-align: center;\n padding: 48px 0;\n}\n\n.orders-empty p {\n font-size: 16px;\n color: #666;\n margin: 0 0 16px 0;\n}\n\n.orders-shop-btn {\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n color: #111;\n background: #fff;\n border: 1.5px solid #111;\n border-radius: 8px;\n cursor: pointer;\n}\n\n.orders-list {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.order-card {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 20px;\n border: 1px solid #eee;\n border-radius: 10px;\n text-decoration: none;\n color: inherit;\n transition: border-color 0.15s ease;\n}\n\n.order-card:hover {\n border-color: #ccc;\n}\n\n.order-card-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.order-number {\n font-size: 15px;\n font-weight: 600;\n color: #111;\n}\n\n.order-status {\n font-size: 12px;\n font-weight: 600;\n color: #fff;\n background: #111;\n padding: 4px 10px;\n border-radius: 12px;\n}\n\n.order-card-details {\n display: flex;\n gap: 24px;\n font-size: 14px;\n color: #666;\n}\n\n.order-total {\n font-weight: 600;\n color: #111;\n margin-left: auto;\n}\n\n@media (max-width: 480px) {\n .order-card-details {\n flex-direction: column;\n gap: 4px;\n }\n\n .order-total {\n margin-left: 0;\n }\n}\n",
|
|
40
|
-
"ikas-config-snippet.json": "{\n \"id\": \"account-orders\",\n \"name\": \"Account Orders\",\n \"type\": \"section\",\n \"entry\": \"./src/components/AccountOrders/index.tsx\",\n \"styles\": \"./src/components/AccountOrders/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"My Orders\"\n },\n {\n \"name\": \"emptyMessage\",\n \"displayName\": \"Empty State Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"You have no orders yet.\"\n }\n ]\n}\n"
|
|
40
|
+
"ikas-config-snippet.json": "{\n \"id\": \"account-orders\",\n \"name\": \"Account Orders\",\n \"type\": \"section\",\n \"entry\": \"./src/components/AccountOrders/index.tsx\",\n \"styles\": \"./src/components/AccountOrders/styles.css\",\n \"props\": [\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"My Orders\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"emptyMessage\",\n \"displayName\": \"Empty State Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"You have no orders yet.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"shopButtonText\",\n \"displayName\": \"Shop Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Start Shopping\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"itemsText\",\n \"displayName\": \"Items Label\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"items\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\"\n }\n ]\n}\n"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"add-to-cart": {
|
|
44
|
+
"title": "Adding a Product to Cart",
|
|
45
|
+
"description": "Check stock (hasProductVariantStock), verify add-to-cart enabled (isAddToCartEnabled), check bundle settings (hasBundleSettings), then addItemToCart with AddItemOptions returning IkasCartOperationResult with success/validationError. Also shows getSelectedProductVariantHref for product links.",
|
|
46
|
+
"files": {
|
|
47
|
+
"index.tsx": "import {\n addItemToCart,\n getSelectedProductVariant,\n getSelectedProductVariantHref,\n hasProductVariantStock,\n isAddToCartEnabled,\n hasBundleSettings,\n baseStore,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n\n/**\n * Add to cart with full validation and result handling.\n * addItemToCart accepts optional AddItemOptions for quantity.\n * Returns IkasCartOperationResult with .success property.\n */\nasync function handleAddToCart(\n product: IkasProduct,\n quantity: number = 1,\n {\n cannotAddText = \"This product cannot be added to cart\",\n outOfStockText = \"Out of stock\",\n addedText = \"Added to cart!\",\n selectOptionsText = \"Please select all options\",\n }: Props = {}\n) {\n // Check if add to cart is enabled for this product\n if (!isAddToCartEnabled(product)) {\n showToast(cannotAddText);\n return;\n }\n\n const variant = getSelectedProductVariant(product);\n\n // Check stock availability\n if (!hasProductVariantStock(variant)) {\n showToast(outOfStockText);\n return;\n }\n\n // Check for bundle settings\n const hasBundle = variant ? hasBundleSettings(variant) : false;\n\n // addItemToCart(variant, product, initialQuantity, options?: AddItemOptions)\n // Returns IkasCartOperationResult { success: boolean, validationError?: string }\n const result = await addItemToCart(variant, product, quantity);\n\n if (result.success) {\n showToast(addedText);\n } else if (result.validationError === \"INSUFFICIENT_STOCK\") {\n showToast(outOfStockText);\n } else if (result.validationError === \"INVALID_PRODUCT_OPTION_VALUES\") {\n showToast(selectOptionsText);\n }\n\n // Get the product link for \"view product\" functionality\n const productHref = getSelectedProductVariantHref(product);\n\n return result;\n}\n\nexport default handleAddToCart;\n",
|
|
48
|
+
"types.ts": "export interface Props {\n cannotAddText?: string;\n outOfStockText?: string;\n addedText?: string;\n selectOptionsText?: string;\n}\n",
|
|
49
|
+
"ikas-config-snippet.json": "{\n \"id\": \"add-to-cart\",\n \"name\": \"Add to Cart\",\n \"props\": [\n {\n \"id\": \"cannotAddText\",\n \"name\": \"Cannot Add Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"This product cannot be added to cart\"\n },\n {\n \"id\": \"outOfStockText\",\n \"name\": \"Out of Stock Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Out of stock\"\n },\n {\n \"id\": \"addedText\",\n \"name\": \"Added Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Added to cart!\"\n },\n {\n \"id\": \"selectOptionsText\",\n \"name\": \"Select Options Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Please select all options\"\n }\n ]\n}\n"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"blog-display": {
|
|
53
|
+
"title": "Blog List Display",
|
|
54
|
+
"description": "Display a blog list with links and formatted dates, with pagination support",
|
|
55
|
+
"files": {
|
|
56
|
+
"index.tsx": "import {\n getIkasBlogHref,\n getIkasBlogFormattedDate,\n hasBlogListNextPage,\n hasBlogListPrevPage,\n getBlogListNextPage,\n getBlogListPrevPage,\n IkasBlog,\n IkasBlogList,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction BlogList({\n blogList,\n previousText = \"Previous\",\n nextText = \"Next\",\n}: Props) {\n return (\n <div>\n {blogList.data.map((blog: IkasBlog) => (\n <article key={blog.id}>\n <a href={getIkasBlogHref(blog)}>\n <h2>{blog.title}</h2>\n </a>\n <time>{getIkasBlogFormattedDate(blog)}</time>\n </article>\n ))}\n\n <div>\n {hasBlogListPrevPage(blogList) && (\n <button onClick={() => getBlogListPrevPage(blogList)}>{previousText}</button>\n )}\n {hasBlogListNextPage(blogList) && (\n <button onClick={() => getBlogListNextPage(blogList)}>{nextText}</button>\n )}\n </div>\n </div>\n );\n}\n\nexport default BlogList;\n",
|
|
57
|
+
"types.ts": "import { IkasBlogList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogList: IkasBlogList;\n previousText?: string;\n nextText?: string;\n}\n",
|
|
58
|
+
"ikas-config-snippet.json": "{\n \"id\": \"blog-display\",\n \"name\": \"Blog Display\",\n \"type\": \"component\",\n \"entry\": \"./src/components/BlogDisplay/index.tsx\",\n \"styles\": \"./src/components/BlogDisplay/styles.css\",\n \"props\": [\n {\n \"name\": \"blogList\",\n \"displayName\": \"Blog List\",\n \"type\": \"BLOG_POST_LIST\",\n \"required\": true\n },\n {\n \"name\": \"previousText\",\n \"displayName\": \"Previous Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Previous\"\n },\n {\n \"name\": \"nextText\",\n \"displayName\": \"Next Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Next\"\n }\n ]\n}\n"
|
|
41
59
|
}
|
|
42
60
|
},
|
|
43
61
|
"blog-list": {
|
|
44
62
|
"title": "Blog List Section",
|
|
45
63
|
"description": "Complete blog list section with blog card grid, featured images, formatted dates, and forward-only pagination (infinite scroll pattern with IkasThemeInfiniteScroller). Uses hasBlogListNextPage/getBlogListNextPage only — no prev page in production.",
|
|
46
64
|
"files": {
|
|
47
|
-
"index.tsx": "import {\n IkasBlogList,\n hasBlogListNextPage,\n getBlogListNextPage,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function BlogListSection({\n blogList,\n title = \"Blog\",\n}: Props) {\n if (!blogList) return null;\n\n const blogs = blogList.data ?? [];\n const hasNext = hasBlogListNextPage(blogList);\n // Note: dynavit uses only forward pagination (infinite scroll with IkasThemeInfiniteScroller)\n // No hasBlogListPrevPage/getBlogListPrevPage — scroll-only pattern\n\n return (\n <section className=\"blog-list-section\">\n <div className=\"blog-list-inner\">\n <h1 className=\"blog-list-title\">{title}</h1>\n\n {blogs.length === 0 && (\n <p className=\"blog-list-empty\">
|
|
48
|
-
"types.ts": "import { IkasBlogList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogList: IkasBlogList;\n title?: string;\n}\n",
|
|
65
|
+
"index.tsx": "import {\n IkasBlogList,\n hasBlogListNextPage,\n getBlogListNextPage,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function BlogListSection({\n blogList,\n title = \"Blog\",\n backgroundColor = \"#ffffff\",\n emptyMessage = \"No blog posts found.\",\n readMoreText = \"Read more\",\n loadMoreText = \"Load More\",\n}: Props) {\n if (!blogList) return null;\n\n const blogs = blogList.data ?? [];\n const hasNext = hasBlogListNextPage(blogList);\n // Note: dynavit uses only forward pagination (infinite scroll with IkasThemeInfiniteScroller)\n // No hasBlogListPrevPage/getBlogListPrevPage — scroll-only pattern\n\n return (\n <section className=\"blog-list-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"blog-list-inner\">\n <h1 className=\"blog-list-title\">{title}</h1>\n\n {blogs.length === 0 && (\n <p className=\"blog-list-empty\">{emptyMessage}</p>\n )}\n\n {/* Blog Grid — production uses IkasThemeInfiniteScroller for infinite scroll */}\n <div className=\"blog-grid\">\n {blogs.map((blog) => (\n <a\n key={blog.id}\n href={getIkasBlogHref(blog)}\n className=\"blog-card\"\n >\n {blog.image && (\n <div className=\"blog-card-image-wrap\">\n <img\n src={getDefaultSrc(blog.image)}\n alt={blog.title}\n className=\"blog-card-image\"\n />\n </div>\n )}\n <div className=\"blog-card-content\">\n <span className=\"blog-card-date\">\n {getIkasBlogFormattedDate(blog)}\n </span>\n <h3 className=\"blog-card-title\">{blog.title}</h3>\n {blog.summary && (\n <p className=\"blog-card-summary\">{blog.summary}</p>\n )}\n <span className=\"blog-card-read-more\">{readMoreText}</span>\n </div>\n </a>\n ))}\n </div>\n\n {/* Load More — infinite scroll pattern */}\n {hasNext && (\n <div className=\"blog-load-more\">\n <button\n className=\"blog-load-more-btn\"\n onClick={() => getBlogListNextPage(blogList)}\n >\n {loadMoreText}\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
66
|
+
"types.ts": "import { IkasBlogList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogList: IkasBlogList;\n title?: string;\n backgroundColor?: string;\n emptyMessage?: string;\n readMoreText?: string;\n loadMoreText?: string;\n}\n",
|
|
49
67
|
"styles.css": ".blog-list-section {\n width: 100%;\n padding: 48px 24px;\n}\n\n.blog-list-inner {\n max-width: 1200px;\n margin: 0 auto;\n}\n\n.blog-list-title {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 32px 0;\n}\n\n.blog-list-empty {\n font-size: 16px;\n color: #666;\n text-align: center;\n padding: 48px 0;\n}\n\n.blog-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 32px;\n}\n\n.blog-card {\n text-decoration: none;\n color: inherit;\n display: flex;\n flex-direction: column;\n border-radius: 12px;\n overflow: hidden;\n border: 1px solid #eee;\n transition: box-shadow 0.2s ease;\n}\n\n.blog-card:hover {\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);\n}\n\n.blog-card-image-wrap {\n aspect-ratio: 16 / 10;\n overflow: hidden;\n background: #f5f5f5;\n}\n\n.blog-card-image {\n width: 100%;\n height: 100%;\n object-fit: cover;\n transition: transform 0.2s ease;\n}\n\n.blog-card:hover .blog-card-image {\n transform: scale(1.03);\n}\n\n.blog-card-content {\n padding: 20px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n flex: 1;\n}\n\n.blog-card-date {\n font-size: 12px;\n color: #999;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.blog-card-title {\n font-size: 18px;\n font-weight: 600;\n color: #111;\n margin: 0;\n line-height: 1.4;\n}\n\n.blog-card-summary {\n font-size: 14px;\n color: #666;\n line-height: 1.6;\n margin: 0;\n display: -webkit-box;\n -webkit-line-clamp: 3;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.blog-card-read-more {\n font-size: 14px;\n font-weight: 600;\n color: #111;\n margin-top: auto;\n padding-top: 8px;\n}\n\n/* Pagination */\n.blog-pagination {\n display: flex;\n justify-content: center;\n gap: 12px;\n margin-top: 40px;\n}\n\n.blog-pagination-btn {\n padding: 10px 24px;\n font-size: 14px;\n font-weight: 500;\n background: #fff;\n border: 1px solid #ddd;\n border-radius: 6px;\n cursor: pointer;\n}\n\n.blog-pagination-btn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.blog-pagination-btn:hover:not(:disabled) {\n border-color: #111;\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n .blog-grid {\n grid-template-columns: 1fr;\n gap: 24px;\n }\n}\n",
|
|
50
|
-
"ikas-config-snippet.json": "{\n \"id\": \"blog-list\",\n \"name\": \"Blog List\",\n \"type\": \"section\",\n \"entry\": \"./src/components/BlogList/index.tsx\",\n \"styles\": \"./src/components/BlogList/styles.css\",\n \"props\": [\n {\n \"name\": \"blogList\",\n \"displayName\": \"Blog List\",\n \"type\": \"BLOG_POST_LIST\",\n \"required\": true\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Blog\"\n }\n ]\n}\n"
|
|
68
|
+
"ikas-config-snippet.json": "{\n \"id\": \"blog-list\",\n \"name\": \"Blog List\",\n \"type\": \"section\",\n \"entry\": \"./src/components/BlogList/index.tsx\",\n \"styles\": \"./src/components/BlogList/styles.css\",\n \"props\": [\n {\n \"name\": \"blogList\",\n \"displayName\": \"Blog List\",\n \"type\": \"BLOG_POST_LIST\",\n \"required\": true\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Blog\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"emptyMessage\",\n \"displayName\": \"Empty State Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"No blog posts found.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"readMoreText\",\n \"displayName\": \"Read More Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Read more\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"loadMoreText\",\n \"displayName\": \"Load More Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Load More\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n { \"id\": \"texts\", \"name\": \"Texts\", \"description\": \"Customizable text labels\" }\n ]\n}\n"
|
|
51
69
|
}
|
|
52
70
|
},
|
|
53
71
|
"blog-post": {
|
|
54
72
|
"title": "Blog Post Section",
|
|
55
73
|
"description": "Single blog post detail page with image, title, date, and HTML content.",
|
|
56
74
|
"files": {
|
|
57
|
-
"index.tsx": "import {\n getIkasBlogFormattedDate,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport 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.blogContent.content }} />\n </div>\n </section>\n );\n}\n\nexport default BlogPostSection;\n",
|
|
58
|
-
"types.ts": "import { IkasBlog } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogPost: IkasBlog;\n}\n",
|
|
75
|
+
"index.tsx": "import {\n getIkasBlogFormattedDate,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function BlogPostSection({\n blogPost,\n backgroundColor = \"#ffffff\",\n}: Props) {\n if (!blogPost) return null;\n\n return (\n <section className=\"blog-post-section\" style={backgroundColor ? { backgroundColor } : undefined}>\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.blogContent.content }} />\n </div>\n </section>\n );\n}\n\nexport default BlogPostSection;\n",
|
|
76
|
+
"types.ts": "import { IkasBlog } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogPost: IkasBlog;\n backgroundColor?: string;\n}\n",
|
|
59
77
|
"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",
|
|
60
|
-
"ikas-config-snippet.json": "{\n \"id\": \"blog-post\",\n \"name\": \"Blog Post\",\n \"type\": \"section\",\n \"props\": [\n {
|
|
78
|
+
"ikas-config-snippet.json": "{\n \"id\": \"blog-post\",\n \"name\": \"Blog Post\",\n \"type\": \"section\",\n \"props\": [\n {\n \"name\": \"blogPost\",\n \"displayName\": \"Blog Post\",\n \"type\": \"BLOG\",\n \"required\": true\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"brand-category-list": {
|
|
82
|
+
"title": "Brand and Category List",
|
|
83
|
+
"description": "Display brands and categories with their links and pagination",
|
|
84
|
+
"files": {
|
|
85
|
+
"index.tsx": "import {\n getIkasBrandHref,\n hasBrandListNextPage,\n getBrandListNextPage,\n getIkasCategoryHref,\n getCategoryPath,\n hasCategoryListNextPage,\n getCategoryListNextPage,\n IkasBrand,\n IkasBrandList,\n IkasCategory,\n IkasCategoryList,\n} from \"@ikas/bp-storefront\";\nimport { Props, CategoryListProps } from \"./types\";\n\nfunction BrandList({ brandList, loadMoreText = \"Load More\" }: Props) {\n return (\n <div>\n {brandList.data.map((brand: IkasBrand) => (\n <a key={brand.id} href={getIkasBrandHref(brand)}>\n {brand.name}\n </a>\n ))}\n {hasBrandListNextPage(brandList) && (\n <button onClick={() => getBrandListNextPage(brandList)}>{loadMoreText}</button>\n )}\n </div>\n );\n}\n\nfunction CategoryList({ categoryList, loadMoreText = \"Load More\" }: CategoryListProps) {\n return (\n <div>\n {categoryList.data.map((category: IkasCategory) => (\n <a key={category.id} href={getIkasCategoryHref(category)}>\n {getCategoryPath(category).map((c) => c.name).join(\" > \")}\n </a>\n ))}\n {hasCategoryListNextPage(categoryList) && (\n <button onClick={() => getCategoryListNextPage(categoryList)}>{loadMoreText}</button>\n )}\n </div>\n );\n}\n\nexport { BrandList, CategoryList };\nexport default BrandList;\n",
|
|
86
|
+
"types.ts": "import { IkasBrandList, IkasCategoryList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n brandList: IkasBrandList;\n loadMoreText?: string;\n}\n\nexport interface CategoryListProps {\n categoryList: IkasCategoryList;\n loadMoreText?: string;\n}\n",
|
|
87
|
+
"ikas-config-snippet.json": "{\n \"id\": \"brand-category-list\",\n \"name\": \"Brand Category List\",\n \"props\": [\n {\n \"id\": \"loadMoreText\",\n \"name\": \"Load More Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Load More\"\n }\n ]\n}\n"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"bundle-products": {
|
|
91
|
+
"title": "Bundle / Offer Products",
|
|
92
|
+
"description": "Bundle product display with hasBundleSettings check, initBundleProducts initialization, getDisplayedProductGroups for group layout, and offer management (acceptProductOffer/rejectProductOffer/isAcceptedProductOffer). Supports variant selection within offers using isIkasVariantTypeColorSelection.",
|
|
93
|
+
"files": {
|
|
94
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n hasBundleSettings,\n initBundleProducts,\n getDisplayedProductGroups,\n acceptProductOffer,\n rejectProductOffer,\n isAcceptedProductOffer,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n isIkasVariantTypeColorSelection,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getDefaultSrc,\n getProductVariantMainImage,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function BundleProducts({\n product,\n title = \"Complete Your Purchase\",\n removeText = \"Remove\",\n addText = \"Add\",\n}: Props) {\n const variant = getSelectedProductVariant(product);\n const hasBundle = variant ? (hasBundleSettings(variant) as unknown as boolean) : false;\n\n useEffect(() => {\n if (hasBundle) {\n initBundleProducts(product);\n }\n }, [hasBundle]);\n\n if (!hasBundle) return null;\n\n const productGroups = getDisplayedProductGroups(product);\n\n return (\n <div className=\"bundle-products\">\n <h3>{title}</h3>\n {productGroups.map((group: any, gi: number) => (\n <div key={gi} className=\"bundle-group\">\n {group.title && <h4>{group.title}</h4>}\n <div className=\"bundle-offers\">\n {group.offers?.map((offer: any) => {\n const isAccepted = isAcceptedProductOffer(offer) as unknown as boolean;\n const offerProduct = offer.product;\n if (!offerProduct) return null;\n\n const offerVariant = getSelectedProductVariant(offerProduct);\n const offerProductImage = getProductVariantMainImage(offerVariant);\n const offerImage = offerProductImage?.image ?? null;\n const offerPrice = getProductVariantFormattedFinalPrice(offerVariant) as unknown as string;\n\n // Variant types for offer product\n const offerVariantTypes = getDisplayedProductVariantTypes(offerProduct);\n\n return (\n <div key={offer.id || offerProduct.id} className=\"bundle-offer-card\">\n {offerImage && (\n <img src={getDefaultSrc(offerImage)} alt={offerProduct.name} style={{ width: 80, height: 80, objectFit: \"cover\", borderRadius: 6 }} />\n )}\n <div className=\"bundle-offer-info\">\n <span className=\"bundle-offer-name\">{offerProduct.name}</span>\n <span className=\"bundle-offer-price\">{offerPrice}</span>\n\n {/* Variant selection for offer products */}\n {offerVariantTypes.map((vt: any) => {\n const isColor = isIkasVariantTypeColorSelection(vt.variantType);\n return (\n <div key={vt.variantType.id} style={{ display: \"flex\", gap: 4, marginTop: 4 }}>\n {vt.displayedVariantValues.map((dvv: any) => (\n <button\n key={dvv.variantValue.id}\n style={{\n width: isColor ? 24 : \"auto\",\n height: isColor ? 24 : \"auto\",\n borderRadius: isColor ? \"50%\" : 4,\n backgroundColor: isColor ? dvv.variantValue.colorCode : \"#fff\",\n border: dvv.isSelected ? \"2px solid #111\" : \"1px solid #ddd\",\n padding: isColor ? 0 : \"4px 8px\",\n fontSize: 12,\n cursor: \"pointer\",\n }}\n onClick={() => selectVariantValue(offerProduct, dvv.variantValue, { disableRoute: true })}\n >\n {!isColor && dvv.variantValue.name}\n </button>\n ))}\n </div>\n );\n })}\n </div>\n <button\n className={`bundle-offer-toggle ${isAccepted ? \"accepted\" : \"\"}`}\n onClick={() => isAccepted ? rejectProductOffer(offer) : acceptProductOffer(offer)}\n >\n {isAccepted ? removeText : addText}\n </button>\n </div>\n );\n })}\n </div>\n </div>\n ))}\n </div>\n );\n}\n",
|
|
95
|
+
"types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n title?: string;\n removeText?: string;\n addText?: string;\n}\n",
|
|
96
|
+
"ikas-config-snippet.json": "{\n \"id\": \"bundle-products\",\n \"name\": \"Bundle Products\",\n \"props\": [\n {\n \"id\": \"title\",\n \"name\": \"Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Complete Your Purchase\"\n },\n {\n \"id\": \"removeText\",\n \"name\": \"Remove Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Remove\"\n },\n {\n \"id\": \"addText\",\n \"name\": \"Add Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Add\"\n }\n ]\n}\n"
|
|
61
97
|
}
|
|
62
98
|
},
|
|
63
99
|
"cart-page": {
|
|
64
100
|
"title": "Cart Section",
|
|
65
101
|
"description": "Complete cart section with loading state, empty cart handling, line items with variant images/links/discount detection, quantity controls, order adjustments (coupons/discounts), subtotal/total, and checkout via getCheckoutUrlFromCartStore. Cart data is automatically reactive in root components via autorun().",
|
|
66
102
|
"files": {
|
|
67
|
-
"index.tsx": "import {\n cartStore,\n hasCart,\n changeItemQuantity,\n removeItem,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderTotalItemCount,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n getOrderLineItemFormattedPriceWithQuantity,\n hasOrderLineItemDiscount,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getCheckoutUrlFromCartStore,\n getDefaultSrc,\n Router,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function CartSection({
|
|
68
|
-
"types.ts": "export interface Props {\n emptyCartMessage?: string;\n}\n",
|
|
103
|
+
"index.tsx": "import {\n cartStore,\n hasCart,\n changeItemQuantity,\n removeItem,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderTotalItemCount,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n getOrderLineItemFormattedPriceWithQuantity,\n hasOrderLineItemDiscount,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getCheckoutUrlFromCartStore,\n getDefaultSrc,\n Router,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function CartSection({\n emptyCartMessage = \"Your cart is empty\",\n backgroundColor = \"#ffffff\",\n title = \"Shopping Cart\",\n loadingText = \"Loading cart...\",\n continueShoppingText = \"Continue Shopping\",\n removeButtonText = \"Remove\",\n subtotalLabel = \"Subtotal\",\n totalLabel = \"Total\",\n checkoutButtonText = \"Proceed to Checkout\",\n itemsText = \"items\",\n}: Props) {\n const cart = cartStore.cart;\n const isLoading = cartStore.isCartLoading;\n const cartHasItems = hasCart(cartStore) as unknown as boolean;\n const lineItems = cart?.orderLineItems ?? [];\n const adjustments = cart?.orderAdjustments ?? [];\n const totalItemCount = cart ? (getIkasOrderTotalItemCount(cart) as unknown as number) : 0;\n\n if (isLoading) {\n return (\n <section className=\"cart-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"cart-inner\">\n <p className=\"cart-loading\">{loadingText}</p>\n </div>\n </section>\n );\n }\n\n if (!cartHasItems) {\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(\"/\")}>\n {continueShoppingText}\n </button>\n </div>\n </section>\n );\n }\n\n const handleQuantityChange = async (item: IkasOrderLineItem, delta: number) => {\n const newQty = item.quantity + delta;\n if (newQty < 1) return;\n await changeItemQuantity(item, newQty);\n };\n\n const handleRemove = async (item: IkasOrderLineItem) => {\n await removeItem(item);\n };\n\n return (\n <section className=\"cart-section\">\n <div className=\"cart-inner\">\n <h1 className=\"cart-title\">{title} ({totalItemCount} {itemsText})</h1>\n\n <div className=\"cart-items\">\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n const href = item.variant ? getIkasOrderLineVariantHref(item.variant) : undefined;\n const hasDiscount = hasOrderLineItemDiscount(item) as unknown as boolean;\n return (\n <div key={item.id} className=\"cart-item\">\n {image && (\n <a href={href}>\n <img\n className=\"cart-item-image\"\n src={getDefaultSrc(image)}\n alt={item.variant?.name || \"Product\"}\n />\n </a>\n )}\n <div className=\"cart-item-info\">\n <a href={href} className=\"cart-item-name\">{item.variant?.name}</a>\n {hasDiscount && (\n <span className=\"cart-item-original-price\">\n {getOrderLineItemFormattedPriceWithQuantity(item)}\n </span>\n )}\n <span className={`cart-item-total ${hasDiscount ? \"has-discount\" : \"\"}`}>\n {getOrderLineItemFormattedFinalPriceWithQuantity(item)}\n </span>\n </div>\n <div className=\"cart-item-quantity\">\n <button onClick={() => handleQuantityChange(item, -1)}>-</button>\n <span>{item.quantity}</span>\n <button onClick={() => handleQuantityChange(item, 1)}>+</button>\n </div>\n <button className=\"cart-item-remove\" onClick={() => handleRemove(item)}>\n {removeButtonText}\n </button>\n </div>\n );\n })}\n </div>\n\n {/* Order Adjustments (discounts, coupons, shipping fees) */}\n {adjustments.length > 0 && (\n <div className=\"cart-adjustments\">\n {adjustments.map((adj: any, i: number) => (\n <div key={i} className=\"cart-adjustment-row\">\n <span>{getOrderAdjustmentDisplayName(adj)}</span>\n <span>{getOrderAdjustmentFormattedAmount(adj)}</span>\n </div>\n ))}\n </div>\n )}\n\n <div className=\"cart-summary\">\n <div className=\"cart-summary-row\">\n <span>{subtotalLabel}</span>\n <span>{getIkasOrderFormattedTotalPrice(cart!)}</span>\n </div>\n <div className=\"cart-summary-row cart-summary-total\">\n <span>{totalLabel}</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(cart!)}</span>\n </div>\n <a\n className=\"cart-checkout-btn\"\n href={getCheckoutUrlFromCartStore(cartStore)}\n >\n {checkoutButtonText}\n </a>\n </div>\n </div>\n </section>\n );\n}\n\n",
|
|
104
|
+
"types.ts": "export interface Props {\n emptyCartMessage?: string;\n backgroundColor?: string;\n title?: string;\n loadingText?: string;\n continueShoppingText?: string;\n removeButtonText?: string;\n subtotalLabel?: string;\n totalLabel?: string;\n checkoutButtonText?: string;\n itemsText?: string;\n}\n",
|
|
69
105
|
"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 {\n font-size: 24px;\n font-weight: 700;\n color: #111;\n margin: 0 0 24px 0;\n}\n\n.cart-empty {\n font-size: 16px;\n color: #666;\n text-align: center;\n padding: 48px 0;\n}\n\n.cart-loading {\n font-size: 16px;\n color: #666;\n text-align: center;\n padding: 48px 0;\n}\n\n.cart-continue-btn {\n display: block;\n margin: 0 auto;\n padding: 12px 24px;\n font-size: 14px;\n font-weight: 600;\n color: #111;\n background: #fff;\n border: 1.5px solid #111;\n border-radius: 8px;\n cursor: pointer;\n}\n\n/* Cart Items */\n.cart-items {\n display: flex;\n flex-direction: column;\n gap: 16px;\n margin-bottom: 32px;\n}\n\n.cart-item {\n display: flex;\n align-items: center;\n gap: 16px;\n padding: 16px;\n border: 1px solid #eee;\n border-radius: 8px;\n}\n\n.cart-item-image {\n width: 80px;\n height: 80px;\n object-fit: cover;\n border-radius: 6px;\n background-color: #f5f5f5;\n}\n\n.cart-item-info {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.cart-item-name {\n font-size: 14px;\n font-weight: 600;\n color: #111;\n text-decoration: none;\n}\n\n.cart-item-name:hover {\n text-decoration: underline;\n}\n\n.cart-item-original-price {\n font-size: 13px;\n color: #999;\n text-decoration: line-through;\n}\n\n.cart-item-total {\n font-size: 15px;\n font-weight: 600;\n color: #111;\n min-width: 80px;\n}\n\n.cart-item-total.has-discount {\n color: #e53935;\n}\n\n.cart-item-quantity {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.cart-item-quantity button {\n width: 32px;\n height: 32px;\n border: 1px solid #ddd;\n border-radius: 4px;\n background: #fff;\n cursor: pointer;\n font-size: 16px;\n}\n\n.cart-item-remove {\n padding: 4px 8px;\n font-size: 12px;\n color: #e53935;\n background: none;\n border: none;\n cursor: pointer;\n}\n\n/* Order Adjustments */\n.cart-adjustments {\n border-top: 1px solid #eee;\n padding: 16px 0;\n margin-bottom: 8px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.cart-adjustment-row {\n display: flex;\n justify-content: space-between;\n font-size: 14px;\n color: #555;\n}\n\n/* Cart Summary */\n.cart-summary {\n border-top: 1px solid #eee;\n padding-top: 24px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n align-items: flex-end;\n}\n\n.cart-summary-row {\n display: flex;\n justify-content: space-between;\n width: 280px;\n font-size: 14px;\n color: #555;\n}\n\n.cart-summary-total {\n font-size: 18px;\n font-weight: 700;\n color: #111;\n}\n\n.cart-checkout-btn {\n display: block;\n width: 280px;\n padding: 14px 24px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background-color: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n margin-top: 8px;\n text-align: center;\n text-decoration: none;\n}\n\n.cart-checkout-btn:hover {\n background-color: #333;\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n .cart-item {\n flex-wrap: wrap;\n }\n\n .cart-summary {\n align-items: stretch;\n }\n\n .cart-summary-row,\n .cart-checkout-btn {\n width: 100%;\n }\n}\n",
|
|
70
|
-
"ikas-config-snippet.json": "{\n \"id\": \"cart-page\",\n \"name\": \"Cart Page\",\n \"type\": \"section\",\n \"entry\": \"./src/components/CartSection/index.tsx\",\n \"styles\": \"./src/components/CartSection/styles.css\",\n \"props\": [\n {\n \"name\": \"emptyCartMessage\",\n \"displayName\": \"Empty Cart Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Your cart is empty\"\n }\n ]\n}\n"
|
|
106
|
+
"ikas-config-snippet.json": "{\n \"id\": \"cart-page\",\n \"name\": \"Cart Page\",\n \"type\": \"section\",\n \"entry\": \"./src/components/CartSection/index.tsx\",\n \"styles\": \"./src/components/CartSection/styles.css\",\n \"props\": [\n {\n \"name\": \"emptyCartMessage\",\n \"displayName\": \"Empty Cart Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Your cart is empty\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Shopping Cart\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"loadingText\",\n \"displayName\": \"Loading Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Loading cart...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"continueShoppingText\",\n \"displayName\": \"Continue Shopping Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Continue Shopping\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"removeButtonText\",\n \"displayName\": \"Remove Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Remove\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"subtotalLabel\",\n \"displayName\": \"Subtotal Label\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Subtotal\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"totalLabel\",\n \"displayName\": \"Total Label\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Total\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"checkoutButtonText\",\n \"displayName\": \"Checkout Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Proceed to Checkout\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"itemsText\",\n \"displayName\": \"Items Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"items\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\"\n }\n ]\n}\n"
|
|
71
107
|
}
|
|
72
108
|
},
|
|
73
109
|
"contact-form": {
|
|
74
110
|
"title": "Contact Form Section",
|
|
75
111
|
"description": "Complete contact form section with name, email, phone, and message fields. Uses the initContactForm/setContactForm*/submitContactForm pattern with validation and success state.",
|
|
76
112
|
"files": {
|
|
77
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getContactForm,\n initContactForm,\n setContactFormEmail,\n setContactFormFirstName,\n setContactFormLastName,\n setContactFormPhone,\n setContactFormMessage,\n submitContactForm,\n clearContactForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ContactFormSection({\n title = \"Contact Us\",\n successMessage = \"Thank you! Your message has been sent.\",\n}: Props) {\n const contactForm = getContactForm(customerStore);\n\n useEffect(() => {\n initContactForm(contactForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitContactForm(contactForm);\n if (success) {\n clearContactForm(contactForm);\n }\n };\n\n return (\n <section className=\"contact-section\">\n <div className=\"contact-inner\">\n <h1 className=\"contact-title\">{title}</h1>\n\n {contactForm.isSuccess && (\n <div className=\"contact-success-banner\">{successMessage}</div>\n )}\n\n {contactForm.isFailure && contactForm.responseMessage && (\n <div className=\"contact-error-banner\">{contactForm.responseMessage}</div>\n )}\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\n className={`contact-input ${contactForm.firstName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={contactForm.firstName.placeholder}\n value={contactForm.firstName.value}\n onInput={(e) =>\n setContactFormFirstName(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.firstName.hasError && contactForm.firstName.message && (\n <span className=\"contact-field-error\">{contactForm.firstName.message}</span>\n )}\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.lastName.label}</label>\n <input\n className={`contact-input ${contactForm.lastName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={contactForm.lastName.placeholder}\n value={contactForm.lastName.value}\n onInput={(e) =>\n setContactFormLastName(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.lastName.hasError && contactForm.lastName.message && (\n <span className=\"contact-field-error\">{contactForm.lastName.message}</span>\n )}\n </div>\n </div>\n\n <div className=\"contact-row\">\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.email.label}</label>\n <input\n className={`contact-input ${contactForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={contactForm.email.placeholder}\n value={contactForm.email.value}\n onInput={(e) =>\n setContactFormEmail(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.email.hasError && contactForm.email.message && (\n <span className=\"contact-field-error\">{contactForm.email.message}</span>\n )}\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.phone.label}</label>\n <input\n className={`contact-input ${contactForm.phone.hasError ? \"has-error\" : \"\"}`}\n type=\"tel\"\n placeholder={contactForm.phone.placeholder}\n value={contactForm.phone.value}\n onInput={(e) =>\n setContactFormPhone(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.phone.hasError && contactForm.phone.message && (\n <span className=\"contact-field-error\">{contactForm.phone.message}</span>\n )}\n </div>\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.message.label}</label>\n <textarea\n className={`contact-textarea ${contactForm.message.hasError ? \"has-error\" : \"\"}`}\n placeholder={contactForm.message.placeholder}\n value={contactForm.message.value}\n rows={5}\n onInput={(e) =>\n setContactFormMessage(contactForm, (e.target as HTMLTextAreaElement).value)\n }\n />\n {contactForm.message.hasError && contactForm.message.message && (\n <span className=\"contact-field-error\">{contactForm.message.message}</span>\n )}\n </div>\n\n <button\n className=\"contact-submit-btn\"\n type=\"submit\"\n disabled={contactForm.isSubmitting}\n >\n {contactForm.isSubmitting ?
|
|
78
|
-
"types.ts": "export interface Props {\n title?: string;\n successMessage?: string;\n}\n",
|
|
113
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getContactForm,\n initContactForm,\n setContactFormEmail,\n setContactFormFirstName,\n setContactFormLastName,\n setContactFormPhone,\n setContactFormMessage,\n submitContactForm,\n clearContactForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ContactFormSection({\n title = \"Contact Us\",\n successMessage = \"Thank you! Your message has been sent.\",\n backgroundColor = \"#ffffff\",\n submitButtonText = \"Send Message\",\n submittingButtonText = \"Sending...\",\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) {\n clearContactForm(contactForm);\n }\n };\n\n return (\n <section className=\"contact-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"contact-inner\">\n <h1 className=\"contact-title\">{title}</h1>\n\n {contactForm.isSuccess && (\n <div className=\"contact-success-banner\">{successMessage}</div>\n )}\n\n {contactForm.isFailure && contactForm.responseMessage && (\n <div className=\"contact-error-banner\">{contactForm.responseMessage}</div>\n )}\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\n className={`contact-input ${contactForm.firstName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={contactForm.firstName.placeholder}\n value={contactForm.firstName.value}\n onInput={(e) =>\n setContactFormFirstName(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.firstName.hasError && contactForm.firstName.message && (\n <span className=\"contact-field-error\">{contactForm.firstName.message}</span>\n )}\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.lastName.label}</label>\n <input\n className={`contact-input ${contactForm.lastName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={contactForm.lastName.placeholder}\n value={contactForm.lastName.value}\n onInput={(e) =>\n setContactFormLastName(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.lastName.hasError && contactForm.lastName.message && (\n <span className=\"contact-field-error\">{contactForm.lastName.message}</span>\n )}\n </div>\n </div>\n\n <div className=\"contact-row\">\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.email.label}</label>\n <input\n className={`contact-input ${contactForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={contactForm.email.placeholder}\n value={contactForm.email.value}\n onInput={(e) =>\n setContactFormEmail(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.email.hasError && contactForm.email.message && (\n <span className=\"contact-field-error\">{contactForm.email.message}</span>\n )}\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.phone.label}</label>\n <input\n className={`contact-input ${contactForm.phone.hasError ? \"has-error\" : \"\"}`}\n type=\"tel\"\n placeholder={contactForm.phone.placeholder}\n value={contactForm.phone.value}\n onInput={(e) =>\n setContactFormPhone(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.phone.hasError && contactForm.phone.message && (\n <span className=\"contact-field-error\">{contactForm.phone.message}</span>\n )}\n </div>\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.message.label}</label>\n <textarea\n className={`contact-textarea ${contactForm.message.hasError ? \"has-error\" : \"\"}`}\n placeholder={contactForm.message.placeholder}\n value={contactForm.message.value}\n rows={5}\n onInput={(e) =>\n setContactFormMessage(contactForm, (e.target as HTMLTextAreaElement).value)\n }\n />\n {contactForm.message.hasError && contactForm.message.message && (\n <span className=\"contact-field-error\">{contactForm.message.message}</span>\n )}\n </div>\n\n <button\n className=\"contact-submit-btn\"\n type=\"submit\"\n disabled={contactForm.isSubmitting}\n >\n {contactForm.isSubmitting ? submittingButtonText : submitButtonText}\n </button>\n </form>\n </div>\n </section>\n );\n}\n\n",
|
|
114
|
+
"types.ts": "export interface Props {\n title?: string;\n successMessage?: string;\n backgroundColor?: string;\n submitButtonText?: string;\n submittingButtonText?: string;\n}\n",
|
|
79
115
|
"styles.css": ".contact-section {\n width: 100%;\n padding: 64px 24px;\n}\n\n.contact-inner {\n max-width: 640px;\n margin: 0 auto;\n}\n\n.contact-title {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 24px 0;\n text-align: center;\n}\n\n.contact-success-banner {\n padding: 12px 16px;\n font-size: 14px;\n color: #1b5e20;\n background-color: #e8f5e9;\n border-radius: 8px;\n margin-bottom: 20px;\n text-align: center;\n}\n\n.contact-error-banner {\n padding: 12px 16px;\n font-size: 14px;\n color: #b71c1c;\n background-color: #ffebee;\n border-radius: 8px;\n margin-bottom: 20px;\n}\n\n.contact-form {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.contact-row {\n display: flex;\n gap: 12px;\n}\n\n.contact-row .contact-field {\n flex: 1;\n}\n\n.contact-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.contact-label {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n}\n\n.contact-input,\n.contact-textarea {\n padding: 12px 14px;\n font-size: 15px;\n border: 1.5px solid #ddd;\n border-radius: 8px;\n outline: none;\n transition: border-color 0.15s ease;\n font-family: inherit;\n}\n\n.contact-input:focus,\n.contact-textarea:focus {\n border-color: #111;\n}\n\n.contact-input.has-error,\n.contact-textarea.has-error {\n border-color: #e53935;\n}\n\n.contact-textarea {\n resize: vertical;\n min-height: 120px;\n}\n\n.contact-field-error {\n font-size: 12px;\n color: #e53935;\n}\n\n.contact-submit-btn {\n padding: 14px 24px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background-color: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s ease;\n margin-top: 8px;\n}\n\n.contact-submit-btn:hover:not(:disabled) {\n background-color: #333;\n}\n\n.contact-submit-btn:disabled {\n background-color: #ccc;\n cursor: not-allowed;\n}\n\n@media (max-width: 480px) {\n .contact-row {\n flex-direction: column;\n }\n}\n",
|
|
80
|
-
"ikas-config-snippet.json": "{\n \"id\": \"contact-form\",\n \"name\": \"Contact Form\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ContactForm/index.tsx\",\n \"styles\": \"./src/components/ContactForm/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Contact Us\"\n },\n {\n \"name\": \"successMessage\",\n \"displayName\": \"Success Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Thank you! Your message has been sent.\"\n }\n ]\n}\n"
|
|
116
|
+
"ikas-config-snippet.json": "{\n \"id\": \"contact-form\",\n \"name\": \"Contact Form\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ContactForm/index.tsx\",\n \"styles\": \"./src/components/ContactForm/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Contact Us\"\n },\n {\n \"name\": \"successMessage\",\n \"displayName\": \"Success Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Thank you! Your message has been sent.\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"submitButtonText\",\n \"displayName\": \"Submit Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Send Message\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submittingButtonText\",\n \"displayName\": \"Submitting Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Sending...\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n { \"id\": \"texts\", \"name\": \"Texts\", \"description\": \"Labels and text content\" }\n ]\n}\n"
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"coupon-code": {
|
|
120
|
+
"title": "Coupon Code",
|
|
121
|
+
"description": "Coupon code input with apply (initCouponCodeForm/setCouponCodeFormCouponCode/submitCouponCodeForm), remove (removeCouponCodeForm), and clear (clearCouponCodeForm). Reads applied coupon from IkasCart.couponCode.",
|
|
122
|
+
"files": {
|
|
123
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n cartStore,\n customerStore,\n initCouponCodeForm,\n setCouponCodeFormCouponCode,\n submitCouponCodeForm,\n removeCouponCodeForm,\n clearCouponCodeForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function CouponCode({\n appliedLabel = \"Coupon applied:\",\n removeText = \"Remove\",\n placeholder = \"Enter coupon code\",\n applyButtonText = \"Apply\",\n applyingButtonText = \"Applying...\",\n}: Props) {\n const cart = cartStore.cart;\n const couponForm = customerStore.couponCodeForm;\n const appliedCoupon = cart?.couponCode;\n\n useEffect(() => {\n if (couponForm) {\n initCouponCodeForm(couponForm);\n }\n }, [couponForm]);\n\n if (!couponForm) return null;\n\n const handleApply = async (e: Event) => {\n e.preventDefault();\n await submitCouponCodeForm(couponForm);\n };\n\n const handleRemove = async () => {\n await removeCouponCodeForm(couponForm);\n };\n\n const handleClear = () => {\n clearCouponCodeForm(customerStore);\n };\n\n return (\n <div className=\"coupon-code\">\n {appliedCoupon ? (\n <div className=\"coupon-applied\">\n <span>{appliedLabel} <strong>{appliedCoupon}</strong></span>\n <button onClick={handleRemove}>{removeText}</button>\n </div>\n ) : (\n <form className=\"coupon-form\" onSubmit={handleApply}>\n <input\n type=\"text\"\n placeholder={couponForm.couponCode?.placeholder ?? placeholder}\n value={couponForm.couponCode?.value ?? \"\"}\n onInput={(e) => setCouponCodeFormCouponCode(couponForm, (e.target as HTMLInputElement).value)}\n className=\"coupon-input\"\n />\n <button type=\"submit\" className=\"coupon-apply-btn\" disabled={couponForm.isSubmitting}>\n {couponForm.isSubmitting ? applyingButtonText : applyButtonText}\n </button>\n </form>\n )}\n {couponForm.isFailure && couponForm.responseMessage && (\n <span className=\"coupon-error\">{couponForm.responseMessage}</span>\n )}\n </div>\n );\n}\n",
|
|
124
|
+
"types.ts": "export interface Props {\n appliedLabel?: string;\n removeText?: string;\n placeholder?: string;\n applyButtonText?: string;\n applyingButtonText?: string;\n}\n",
|
|
125
|
+
"ikas-config-snippet.json": "{\n \"id\": \"coupon-code\",\n \"name\": \"Coupon Code\",\n \"props\": [\n {\n \"id\": \"appliedLabel\",\n \"name\": \"Applied Label\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Coupon applied:\",\n \"groupId\": \"texts\"\n },\n {\n \"id\": \"removeText\",\n \"name\": \"Remove Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Remove\",\n \"groupId\": \"texts\"\n },\n {\n \"id\": \"placeholder\",\n \"name\": \"Placeholder\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Enter coupon code\",\n \"groupId\": \"texts\"\n },\n {\n \"id\": \"applyButtonText\",\n \"name\": \"Apply Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Apply\",\n \"groupId\": \"texts\"\n },\n {\n \"id\": \"applyingButtonText\",\n \"name\": \"Applying Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Applying...\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\"\n }\n ]\n}\n"
|
|
81
126
|
}
|
|
82
127
|
},
|
|
83
128
|
"faq": {
|
|
84
129
|
"title": "FAQ Accordion Section",
|
|
85
130
|
"description": "Complete FAQ section with accordion-style question/answer pairs. Uses static TEXT props for questions and answers with toggle state management. Commonly reused across multiple pages (about, contact, product detail).",
|
|
86
131
|
"files": {
|
|
87
|
-
"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\n {items.length === 0 && (\n <p className=\"faq-empty\">
|
|
88
|
-
"types.ts": "export interface FaqItem {\n question: string;\n answer: string;\n}\n\nexport interface Props {\n title?: string;\n items?: FaqItem[];\n}\n",
|
|
132
|
+
"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 backgroundColor = \"#ffffff\",\n emptyMessage = \"No questions added yet.\",\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\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"faq-inner\">\n <h2 className=\"faq-title\">{title}</h2>\n\n {items.length === 0 && (\n <p className=\"faq-empty\">{emptyMessage}</p>\n )}\n\n <div className=\"faq-list\">\n {items.map((item: FaqItem, index: number) => {\n const isOpen = openIndex === index;\n return (\n <div\n key={index}\n className={`faq-item ${isOpen ? \"faq-item-open\" : \"\"}`}\n >\n <button\n className=\"faq-question\"\n onClick={() => handleToggle(index)}\n aria-expanded={isOpen}\n >\n <span>{item.question}</span>\n <span className=\"faq-icon\">{isOpen ? \"−\" : \"+\"}</span>\n </button>\n {isOpen && (\n <div className=\"faq-answer\">\n <p>{item.answer}</p>\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n </section>\n );\n}\n",
|
|
133
|
+
"types.ts": "export interface FaqItem {\n question: string;\n answer: string;\n}\n\nexport interface Props {\n title?: string;\n items?: FaqItem[];\n backgroundColor?: string;\n emptyMessage?: string;\n}\n",
|
|
89
134
|
"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 {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 32px 0;\n text-align: center;\n}\n\n.faq-empty {\n font-size: 16px;\n color: #666;\n text-align: center;\n}\n\n.faq-list {\n display: flex;\n flex-direction: column;\n}\n\n.faq-item {\n border-bottom: 1px solid #eee;\n}\n\n.faq-question {\n width: 100%;\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 20px 0;\n font-size: 16px;\n font-weight: 500;\n color: #111;\n background: none;\n border: none;\n cursor: pointer;\n text-align: left;\n gap: 16px;\n}\n\n.faq-question:hover {\n color: #333;\n}\n\n.faq-icon {\n font-size: 20px;\n font-weight: 300;\n flex-shrink: 0;\n color: #666;\n}\n\n.faq-answer {\n padding: 0 0 20px 0;\n}\n\n.faq-answer p {\n font-size: 15px;\n line-height: 1.7;\n color: #555;\n margin: 0;\n}\n\n.faq-item-open .faq-question {\n font-weight: 600;\n}\n",
|
|
90
|
-
"ikas-config-snippet.json": "{\n \"id\": \"faq\",\n \"name\": \"FAQ\",\n \"type\": \"section\",\n \"entry\": \"./src/components/FAQ/index.tsx\",\n \"styles\": \"./src/components/FAQ/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Frequently Asked Questions\"\n },\n {\n \"name\": \"items\",\n \"displayName\": \"FAQ Items\",\n \"type\": \"COMPONENT_LIST\",\n \"componentProps\": [\n {\n \"name\": \"question\",\n \"displayName\": \"Question\",\n \"type\": \"TEXT\",\n \"required\": true\n },\n {\n \"name\": \"answer\",\n \"displayName\": \"Answer\",\n \"type\": \"TEXT\",\n \"required\": true\n }\n ]\n }\n ]\n}\n"
|
|
135
|
+
"ikas-config-snippet.json": "{\n \"id\": \"faq\",\n \"name\": \"FAQ\",\n \"type\": \"section\",\n \"entry\": \"./src/components/FAQ/index.tsx\",\n \"styles\": \"./src/components/FAQ/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Frequently Asked Questions\"\n },\n {\n \"name\": \"items\",\n \"displayName\": \"FAQ Items\",\n \"type\": \"COMPONENT_LIST\",\n \"componentProps\": [\n {\n \"name\": \"question\",\n \"displayName\": \"Question\",\n \"type\": \"TEXT\",\n \"required\": true\n },\n {\n \"name\": \"answer\",\n \"displayName\": \"Answer\",\n \"type\": \"TEXT\",\n \"required\": true\n }\n ]\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"emptyMessage\",\n \"displayName\": \"Empty State Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"No questions added yet.\"\n }\n ]\n}\n"
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"favorites": {
|
|
139
|
+
"title": "Favorites / Wishlist",
|
|
140
|
+
"description": "Add/remove products from the wishlist with isFavoriteIkasProduct toggle, and display the full favorites list using getFavoriteProducts with product card rendering (pricing, images, links).",
|
|
141
|
+
"files": {
|
|
142
|
+
"index.tsx": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n getFavoriteProducts,\n getSelectedProductVariant,\n getSelectedProductVariantHref,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantMainImage,\n getDefaultSrc,\n customerStore,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\n/** Toggle button for adding/removing a product from favorites */\nexport default function FavoriteButton({\n product,\n removeText = \"Remove from wishlist\",\n addText = \"Add to wishlist\",\n}: Props) {\n const isFavorite = isFavoriteIkasProduct(product);\n const handleClick = async () => {\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n return (\n <button onClick={handleClick}>\n {isFavorite ? removeText : addText}\n </button>\n );\n}\n\n/** Full favorites list page */\nfunction FavoritesList({\n loadingText = \"Loading favorites...\",\n emptyMessage = \"No favorites yet.\",\n removeText = \"Remove from wishlist\",\n}: Omit<Props, \"product\">) {\n const [favorites, setFavorites] = useState<IkasProduct[]>([]);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n getFavoriteProducts(customerStore).then((products) => {\n setFavorites(products ?? []);\n setLoading(false);\n });\n }, []);\n\n if (loading) return <p>{loadingText}</p>;\n if (favorites.length === 0) return <p>{emptyMessage}</p>;\n\n return (\n <div>\n {favorites.map((product) => {\n const variant = getSelectedProductVariant(product);\n const href = getSelectedProductVariantHref(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const hasDiscount = hasProductVariantDiscount(variant) as unknown as boolean;\n const finalPrice = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const sellPrice = hasDiscount\n ? (getProductVariantFormattedSellPrice(variant) as unknown as string)\n : null;\n\n return (\n <div key={product.id} style={{ display: \"flex\", gap: 12, padding: \"12px 0\", borderBottom: \"1px solid #eee\" }}>\n {image && (\n <a href={href}>\n <img src={getDefaultSrc(image)} width={80} height={80} style={{ objectFit: \"cover\", borderRadius: 6 }} alt={product.name} />\n </a>\n )}\n <div>\n <a href={href} style={{ fontWeight: 600, color: \"#111\", textDecoration: \"none\" }}>{product.name}</a>\n <div>\n {sellPrice && <span style={{ textDecoration: \"line-through\", color: \"#999\", marginRight: 8 }}>{sellPrice}</span>}\n <span>{finalPrice}</span>\n </div>\n <button\n style={{ marginTop: 8, fontSize: 13, color: \"#e53935\", background: \"none\", border: \"none\", cursor: \"pointer\", padding: 0 }}\n onClick={() => removeIkasProductFromFavorites(product)}\n >\n {removeText}\n </button>\n </div>\n </div>\n );\n })}\n </div>\n );\n}\n\nexport { FavoriteButton, FavoritesList };\n",
|
|
143
|
+
"types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n removeText?: string;\n addText?: string;\n loadingText?: string;\n emptyMessage?: string;\n}\n",
|
|
144
|
+
"ikas-config-snippet.json": "{\n \"id\": \"favorites\",\n \"name\": \"Favorites\",\n \"props\": [\n {\n \"id\": \"removeText\",\n \"name\": \"Remove Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Remove from wishlist\"\n },\n {\n \"id\": \"addText\",\n \"name\": \"Add Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Add to wishlist\"\n },\n {\n \"id\": \"loadingText\",\n \"name\": \"Loading Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Loading favorites...\"\n },\n {\n \"id\": \"emptyMessage\",\n \"name\": \"Empty Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"No favorites yet.\"\n }\n ]\n}\n"
|
|
91
145
|
}
|
|
92
146
|
},
|
|
93
147
|
"favorites-page": {
|
|
94
148
|
"title": "Favorites Page Section",
|
|
95
149
|
"description": "Complete favorites page with getFavoriteProducts loading, product cards with pricing/images/links, and remove functionality.",
|
|
96
150
|
"files": {
|
|
97
|
-
"index.tsx": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getFavoriteProducts,\n removeIkasProductFromFavorites,\n getSelectedProductVariant,\n getSelectedProductVariantHref,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantMainImage,\n getDefaultSrc,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\n\nexport default function FavoritesPageSection() {\n const [favorites, setFavorites] = useState<IkasProduct[]>([]);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n getFavoriteProducts(customerStore).then((products) => {\n setFavorites(products ?? []);\n setLoading(false);\n });\n }, []);\n\n const handleRemove = async (product: IkasProduct) => {\n await removeIkasProductFromFavorites(product);\n setFavorites(favorites.filter((f) => f.id !== product.id));\n };\n\n if (loading) {\n return <section className=\"favorites-page\"><div className=\"favorites-inner\"><p>
|
|
98
|
-
"types.ts": "export interface Props {}\n",
|
|
151
|
+
"index.tsx": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getFavoriteProducts,\n removeIkasProductFromFavorites,\n getSelectedProductVariant,\n getSelectedProductVariantHref,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantMainImage,\n getDefaultSrc,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function FavoritesPageSection({\n backgroundColor = \"#ffffff\",\n title = \"My Favorites\",\n emptyMessage = \"No favorites yet.\",\n loadingText = \"Loading...\",\n removeButtonText = \"Remove\",\n}: Props) {\n const [favorites, setFavorites] = useState<IkasProduct[]>([]);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n getFavoriteProducts(customerStore).then((products) => {\n setFavorites(products ?? []);\n setLoading(false);\n });\n }, []);\n\n const handleRemove = async (product: IkasProduct) => {\n await removeIkasProductFromFavorites(product);\n setFavorites(favorites.filter((f) => f.id !== product.id));\n };\n\n if (loading) {\n return <section className=\"favorites-page\"><div className=\"favorites-inner\"><p>{loadingText}</p></div></section>;\n }\n\n return (\n <section className=\"favorites-page\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"favorites-inner\">\n <h1 className=\"favorites-title\">{title}</h1>\n\n {favorites.length === 0 && <p className=\"favorites-empty\">{emptyMessage}</p>}\n\n <div className=\"favorites-grid\">\n {favorites.map((product) => {\n const variant = getSelectedProductVariant(product);\n const href = getSelectedProductVariantHref(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const hasDiscount = hasProductVariantDiscount(variant) as unknown as boolean;\n const finalPrice = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const sellPrice = hasDiscount ? (getProductVariantFormattedSellPrice(variant) as unknown as string) : null;\n\n return (\n <div key={product.id} className=\"favorites-card\">\n {image && (\n <a href={href}><img className=\"favorites-card-img\" src={getDefaultSrc(image)} alt={product.name} /></a>\n )}\n <div className=\"favorites-card-info\">\n <a href={href} className=\"favorites-card-name\">{product.name}</a>\n <div className=\"favorites-card-price\">\n {sellPrice && <span className=\"favorites-card-old-price\">{sellPrice}</span>}\n <span>{finalPrice}</span>\n </div>\n <button className=\"favorites-card-remove\" onClick={() => handleRemove(product)}>{removeButtonText}</button>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n </section>\n );\n}\n\n",
|
|
152
|
+
"types.ts": "export interface Props {\n backgroundColor?: string;\n title?: string;\n emptyMessage?: string;\n loadingText?: string;\n removeButtonText?: string;\n}\n",
|
|
99
153
|
"styles.css": ".favorites-page { width: 100%; padding: 40px 24px; }\n.favorites-inner { max-width: 1000px; margin: 0 auto; }\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.favorites-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }\n.favorites-card { border: 1px solid #eee; border-radius: 8px; overflow: hidden; }\n.favorites-card-img { width: 100%; aspect-ratio: 1; object-fit: cover; background: #f5f5f5; display: block; }\n.favorites-card-info { padding: 12px; }\n.favorites-card-name { font-size: 14px; font-weight: 600; color: #111; text-decoration: none; display: block; margin-bottom: 4px; }\n.favorites-card-price { font-size: 15px; font-weight: 700; }\n.favorites-card-old-price { text-decoration: line-through; color: #999; margin-right: 8px; font-weight: 400; font-size: 13px; }\n.favorites-card-remove { font-size: 13px; color: #e53935; background: none; border: none; cursor: pointer; padding: 0; margin-top: 8px; }\n@media (max-width: 768px) { .favorites-grid { grid-template-columns: repeat(2, 1fr); } }\n@media (max-width: 480px) { .favorites-grid { grid-template-columns: 1fr; } }\n",
|
|
100
|
-
"ikas-config-snippet.json": "{
|
|
154
|
+
"ikas-config-snippet.json": "{\n \"id\": \"favorites-page\",\n \"name\": \"Favorites Page\",\n \"type\": \"section\",\n \"props\": [\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"My Favorites\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"emptyMessage\",\n \"displayName\": \"Empty State Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"No favorites yet.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"loadingText\",\n \"displayName\": \"Loading Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Loading...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"removeButtonText\",\n \"displayName\": \"Remove Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Remove\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\"\n }\n ]\n}\n"
|
|
101
155
|
}
|
|
102
156
|
},
|
|
103
157
|
"footer": {
|
|
104
158
|
"title": "Footer Section",
|
|
105
159
|
"description": "Complete footer section with logo, navigation link columns, contact info, social media links, and copyright. Uses IkasNavigationLink for editable links.",
|
|
106
160
|
"files": {
|
|
107
|
-
"index.tsx": "import { IkasNavigationLink, getDefaultSrc } 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 {/* Brand Column */}\n <div className=\"footer-brand\">\n {logo ? (\n <img src={getDefaultSrc(logo)} alt={logo.altText ||
|
|
108
|
-
"types.ts": "import { IkasNavigationLinkList, IkasImage } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: IkasImage | null;\n description?: string;\n linkColumn1Title?: string;\n linkColumn1?: IkasNavigationLinkList;\n linkColumn2Title?: string;\n linkColumn2?: IkasNavigationLinkList;\n copyright?: string;\n}\n",
|
|
109
|
-
"styles.css": ".footer-section {\n width: 100%;\n
|
|
110
|
-
"ikas-config-snippet.json": "{\n \"id\": \"footer\",\n \"name\": \"Footer\",\n \"type\": \"section\",\n \"entry\": \"./src/components/Footer/index.tsx\",\n \"styles\": \"./src/components/Footer/styles.css\",\n \"props\": [\n {\n \"name\": \"logo\",\n \"displayName\": \"Logo\",\n \"type\": \"IMAGE\"\n },\n {\n \"name\": \"description\",\n \"displayName\": \"Description\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Your one-stop shop for quality products.\"\n },\n {\n \"name\": \"linkColumn1Title\",\n \"displayName\": \"Column 1 Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Shop\"\n },\n {\n \"name\": \"linkColumn1\",\n \"displayName\": \"Column 1 Links\",\n \"type\": \"LIST_OF_LINK\"\n },\n {\n \"name\": \"linkColumn2Title\",\n \"displayName\": \"Column 2 Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Company\"\n },\n {\n \"name\": \"linkColumn2\",\n \"displayName\": \"Column 2 Links\",\n \"type\": \"LIST_OF_LINK\"\n },\n {\n \"name\": \"copyright\",\n \"displayName\": \"Copyright Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"All rights reserved.\"\n }\n ]\n}\n"
|
|
161
|
+
"index.tsx": "import { IkasNavigationLink, getDefaultSrc } 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 backgroundColor = \"#f9fafb\",\n logoFallbackText = \"Store\",\n}: Props) {\n return (\n <footer className=\"footer-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"footer-inner\">\n <div className=\"footer-grid\">\n {/* Brand Column */}\n <div className=\"footer-brand\">\n {logo ? (\n <img src={getDefaultSrc(logo)} alt={logo.altText || logoFallbackText} className=\"footer-logo\" />\n ) : (\n <span className=\"footer-logo-text\">{logoFallbackText}</span>\n )}\n {description && <p className=\"footer-description\">{description}</p>}\n </div>\n\n {/* Link Column 1 */}\n <div className=\"footer-link-column\">\n <h4 className=\"footer-column-title\">{linkColumn1Title}</h4>\n <nav className=\"footer-links\">\n {(linkColumn1?.links ?? []).map((link: IkasNavigationLink, i: number) => (\n <a\n key={i}\n href={link.href}\n className=\"footer-link\"\n target={link.openInNewTab ? \"_blank\" : undefined}\n >\n {link.label}\n </a>\n ))}\n </nav>\n </div>\n\n {/* Link Column 2 */}\n <div className=\"footer-link-column\">\n <h4 className=\"footer-column-title\">{linkColumn2Title}</h4>\n <nav className=\"footer-links\">\n {(linkColumn2?.links ?? []).map((link: IkasNavigationLink, i: number) => (\n <a\n key={i}\n href={link.href}\n className=\"footer-link\"\n target={link.openInNewTab ? \"_blank\" : undefined}\n >\n {link.label}\n </a>\n ))}\n </nav>\n </div>\n </div>\n\n {/* Copyright */}\n <div className=\"footer-bottom\">\n <p className=\"footer-copyright\">{copyright}</p>\n </div>\n </div>\n </footer>\n );\n}\n",
|
|
162
|
+
"types.ts": "import { IkasNavigationLinkList, IkasImage } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: IkasImage | null;\n description?: string;\n linkColumn1Title?: string;\n linkColumn1?: IkasNavigationLinkList;\n linkColumn2Title?: string;\n linkColumn2?: IkasNavigationLinkList;\n copyright?: string;\n backgroundColor?: string;\n logoFallbackText?: string;\n}\n",
|
|
163
|
+
"styles.css": ".footer-section {\n width: 100%;\n border-top: 1px solid #eee;\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: 48px;\n}\n\n/* Brand */\n.footer-brand {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.footer-logo {\n height: 36px;\n width: auto;\n}\n\n.footer-logo-text {\n font-size: 20px;\n font-weight: 700;\n color: #111;\n}\n\n.footer-description {\n font-size: 14px;\n color: #666;\n line-height: 1.6;\n max-width: 300px;\n margin: 0;\n}\n\n/* Link Columns */\n.footer-link-column {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.footer-column-title {\n font-size: 14px;\n font-weight: 700;\n color: #111;\n margin: 0;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.footer-links {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.footer-link {\n font-size: 14px;\n color: #666;\n text-decoration: none;\n transition: color 0.15s ease;\n}\n\n.footer-link:hover {\n color: #111;\n}\n\n/* Bottom */\n.footer-bottom {\n border-top: 1px solid #eee;\n padding-top: 24px;\n}\n\n.footer-copyright {\n font-size: 13px;\n color: #999;\n margin: 0;\n text-align: center;\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n .footer-grid {\n grid-template-columns: 1fr;\n gap: 32px;\n }\n}\n",
|
|
164
|
+
"ikas-config-snippet.json": "{\n \"id\": \"footer\",\n \"name\": \"Footer\",\n \"type\": \"section\",\n \"entry\": \"./src/components/Footer/index.tsx\",\n \"styles\": \"./src/components/Footer/styles.css\",\n \"props\": [\n {\n \"name\": \"logo\",\n \"displayName\": \"Logo\",\n \"type\": \"IMAGE\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"description\",\n \"displayName\": \"Description\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Your one-stop shop for quality products.\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"linkColumn1Title\",\n \"displayName\": \"Column 1 Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Shop\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"linkColumn1\",\n \"displayName\": \"Column 1 Links\",\n \"type\": \"LIST_OF_LINK\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"linkColumn2Title\",\n \"displayName\": \"Column 2 Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Company\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"linkColumn2\",\n \"displayName\": \"Column 2 Links\",\n \"type\": \"LIST_OF_LINK\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"copyright\",\n \"displayName\": \"Copyright Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"All rights reserved.\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#f9fafb\",\n \"groupId\": \"style\"\n },\n {\n \"name\": \"logoFallbackText\",\n \"displayName\": \"Logo Fallback Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Store\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n { \"id\": \"content\", \"name\": \"Content\", \"description\": \"Text, links, and media\" },\n { \"id\": \"style\", \"name\": \"Style\", \"description\": \"Colors and visual settings\" },\n { \"id\": \"texts\", \"name\": \"Texts\", \"description\": \"Customizable text labels\" }\n ]\n}\n"
|
|
111
165
|
}
|
|
112
166
|
},
|
|
113
167
|
"forgot-password": {
|
|
114
168
|
"title": "Forgot Password Section",
|
|
115
169
|
"description": "Complete forgot password section with email input for password recovery. Uses the initForgotPasswordForm/setForgotPasswordFormEmail/submitForgotPasswordForm pattern with success and error states.",
|
|
116
170
|
"files": {
|
|
117
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getForgotPasswordForm,\n initForgotPasswordForm,\n setForgotPasswordFormEmail,\n submitForgotPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ForgotPasswordSection({\n successMessage = \"Password reset link has been sent to your email.\",\n}: Props) {\n const forgotForm = getForgotPasswordForm(customerStore);\n\n useEffect(() => {\n initForgotPasswordForm(forgotForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitForgotPasswordForm(forgotForm);\n };\n\n return (\n <section className=\"forgot-section\">\n <div className=\"forgot-inner\">\n <h1 className=\"forgot-title\">
|
|
118
|
-
"types.ts": "export interface Props {\n successMessage?: string;\n}\n",
|
|
171
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getForgotPasswordForm,\n initForgotPasswordForm,\n setForgotPasswordFormEmail,\n submitForgotPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ForgotPasswordSection({\n successMessage = \"Password reset link has been sent to your email.\",\n backgroundColor = \"#ffffff\",\n title = \"Forgot Password\",\n subtitle = \"Enter your email address and we'll send you a link to reset your password.\",\n submitButtonText = \"Send Reset Link\",\n submittingButtonText = \"Sending...\",\n backToLoginText = \"Back to Sign In\",\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\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"forgot-inner\">\n <h1 className=\"forgot-title\">{title}</h1>\n <p className=\"forgot-subtitle\">\n {subtitle}\n </p>\n\n {forgotForm.isSuccess && (\n <div className=\"forgot-success-banner\">{successMessage}</div>\n )}\n\n {forgotForm.isFailure && forgotForm.responseMessage && (\n <div className=\"forgot-error-banner\">{forgotForm.responseMessage}</div>\n )}\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) =>\n setForgotPasswordFormEmail(forgotForm, (e.target as HTMLInputElement).value)\n }\n />\n {forgotForm.email.hasError && forgotForm.email.message && (\n <span className=\"forgot-field-error\">{forgotForm.email.message}</span>\n )}\n </div>\n\n <button\n className=\"forgot-submit-btn\"\n type=\"submit\"\n disabled={forgotForm.isSubmitting}\n >\n {forgotForm.isSubmitting ? submittingButtonText : submitButtonText}\n </button>\n </form>\n )}\n\n <p className=\"forgot-back-link\">\n <a\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n Router.navigateToPage(\"LOGIN\");\n }}\n >\n {backToLoginText}\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
|
|
172
|
+
"types.ts": "export interface Props {\n successMessage?: string;\n backgroundColor?: string;\n title?: string;\n subtitle?: string;\n submitButtonText?: string;\n submittingButtonText?: string;\n backToLoginText?: string;\n}\n",
|
|
119
173
|
"styles.css": ".forgot-section {\n width: 100%;\n padding: 64px 24px;\n min-height: 60vh;\n display: flex;\n align-items: center;\n}\n\n.forgot-inner {\n max-width: 400px;\n margin: 0 auto;\n width: 100%;\n}\n\n.forgot-title {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 8px 0;\n text-align: center;\n}\n\n.forgot-subtitle {\n font-size: 14px;\n color: #666;\n text-align: center;\n margin: 0 0 24px 0;\n line-height: 1.5;\n}\n\n.forgot-success-banner {\n padding: 12px 16px;\n font-size: 14px;\n color: #1b5e20;\n background-color: #e8f5e9;\n border-radius: 8px;\n margin-bottom: 20px;\n text-align: center;\n}\n\n.forgot-error-banner {\n padding: 12px 16px;\n font-size: 14px;\n color: #b71c1c;\n background-color: #ffebee;\n border-radius: 8px;\n margin-bottom: 20px;\n}\n\n.forgot-form {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.forgot-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.forgot-label {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n}\n\n.forgot-input {\n padding: 12px 14px;\n font-size: 15px;\n border: 1.5px solid #ddd;\n border-radius: 8px;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.forgot-input:focus {\n border-color: #111;\n}\n\n.forgot-input.has-error {\n border-color: #e53935;\n}\n\n.forgot-field-error {\n font-size: 12px;\n color: #e53935;\n}\n\n.forgot-submit-btn {\n padding: 14px 24px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background-color: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s ease;\n margin-top: 8px;\n}\n\n.forgot-submit-btn:hover:not(:disabled) {\n background-color: #333;\n}\n\n.forgot-submit-btn:disabled {\n background-color: #ccc;\n cursor: not-allowed;\n}\n\n.forgot-back-link {\n font-size: 14px;\n text-align: center;\n margin-top: 24px;\n}\n\n.forgot-back-link a {\n color: #111;\n font-weight: 600;\n text-decoration: none;\n}\n\n.forgot-back-link a:hover {\n text-decoration: underline;\n}\n",
|
|
120
|
-
"ikas-config-snippet.json": "{\n \"id\": \"forgot-password\",\n \"name\": \"Forgot Password\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ForgotPassword/index.tsx\",\n \"styles\": \"./src/components/ForgotPassword/styles.css\",\n \"props\": [\n {\n \"name\": \"successMessage\",\n \"displayName\": \"Success Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Password reset link has been sent to your email.\"\n }\n ]\n}\n"
|
|
174
|
+
"ikas-config-snippet.json": "{\n \"id\": \"forgot-password\",\n \"name\": \"Forgot Password\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ForgotPassword/index.tsx\",\n \"styles\": \"./src/components/ForgotPassword/styles.css\",\n \"props\": [\n {\n \"name\": \"successMessage\",\n \"displayName\": \"Success Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Password reset link has been sent to your email.\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Forgot Password\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"subtitle\",\n \"displayName\": \"Subtitle\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Enter your email address and we'll send you a link to reset your password.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submitButtonText\",\n \"displayName\": \"Submit Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Send Reset Link\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submittingButtonText\",\n \"displayName\": \"Submitting Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Sending...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"backToLoginText\",\n \"displayName\": \"Back To Login Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Back to Sign In\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\"\n }\n ]\n}\n"
|
|
121
175
|
}
|
|
122
176
|
},
|
|
123
177
|
"header": {
|
|
124
178
|
"title": "Header Section",
|
|
125
179
|
"description": "Complete header section with announcement bar, navigation with subLinks (mega-menu), logo, customer icon, mini-cart sidebar with item images/prices/quantity controls/adjustments/checkout, and mobile menu overlay. Cart badge and customer state are automatically reactive in root components via autorun().",
|
|
126
180
|
"files": {
|
|
127
|
-
"index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n cartStore,\n customerStore,\n hasCustomer,\n hasCart,\n getIkasOrderTotalItemCount,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderLineItemFormattedFinalPrice,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n changeItemQuantity,\n removeItem,\n getCheckoutUrlFromCartStore,\n getDefaultSrc,\n Router,\n IkasNavigationLink,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function HeaderSection({\n logo,\n navigationLinks,\n announcementText,\n announcementBgColor = \"#111\",\n announcementTextColor = \"#fff\",\n}: Props) {\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [cartSidebarOpen, setCartSidebarOpen] = useState(false);\n\n const cart = cartStore.cart;\n const isCartLoading = cartStore.isCartLoading;\n const cartHasItems = hasCart(cartStore) as unknown as boolean;\n const itemCount = cart ? (getIkasOrderTotalItemCount(cart) as unknown as number) : 0;\n const isLoggedIn = hasCustomer(customerStore) as unknown as boolean;\n const lineItems = cart?.orderLineItems ?? [];\n const adjustments = cart?.orderAdjustments ?? [];\n\n return (\n <section className=\"header-section\">\n {/* Announcement Bar */}\n {announcementText && (\n <div\n className=\"header-announcement\"\n style={{ backgroundColor: announcementBgColor, color: announcementTextColor }}\n >\n <span>{announcementText}</span>\n </div>\n )}\n\n {/* Main Header */}\n <div className=\"header-main\">\n <div className=\"header-inner\">\n {/* Mobile Hamburger */}\n <button\n className=\"header-hamburger\"\n onClick={() => setMobileMenuOpen(true)}\n aria-label=\"Open menu\"\n >\n <span /><span /><span />\n </button>\n\n {/* Logo */}\n <a className=\"header-logo\" href=\"/\" onClick={(e) => { e.preventDefault(); Router.navigate(\"/\"); }}>\n {logo ? (\n <img src={getDefaultSrc(logo)} alt={logo.altText ||
|
|
128
|
-
"types.ts": "import { IkasImage, IkasNavigationLinkList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: IkasImage | null;\n navigationLinks?: IkasNavigationLinkList;\n announcementText?: string;\n announcementBgColor?: string;\n announcementTextColor?: string;\n}\n",
|
|
181
|
+
"index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n cartStore,\n customerStore,\n hasCustomer,\n hasCart,\n getIkasOrderTotalItemCount,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderLineItemFormattedFinalPrice,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n changeItemQuantity,\n removeItem,\n getCheckoutUrlFromCartStore,\n getDefaultSrc,\n Router,\n IkasNavigationLink,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function HeaderSection({\n logo,\n navigationLinks,\n announcementText,\n announcementBgColor = \"#111\",\n announcementTextColor = \"#fff\",\n backgroundColor = \"#ffffff\",\n logoFallbackText = \"Store\",\n}: Props) {\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [cartSidebarOpen, setCartSidebarOpen] = useState(false);\n\n const cart = cartStore.cart;\n const isCartLoading = cartStore.isCartLoading;\n const cartHasItems = hasCart(cartStore) as unknown as boolean;\n const itemCount = cart ? (getIkasOrderTotalItemCount(cart) as unknown as number) : 0;\n const isLoggedIn = hasCustomer(customerStore) as unknown as boolean;\n const lineItems = cart?.orderLineItems ?? [];\n const adjustments = cart?.orderAdjustments ?? [];\n\n return (\n <section className=\"header-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n {/* Announcement Bar */}\n {announcementText && (\n <div\n className=\"header-announcement\"\n style={{ backgroundColor: announcementBgColor, color: announcementTextColor }}\n >\n <span>{announcementText}</span>\n </div>\n )}\n\n {/* Main Header */}\n <div className=\"header-main\">\n <div className=\"header-inner\">\n {/* Mobile Hamburger */}\n <button\n className=\"header-hamburger\"\n onClick={() => setMobileMenuOpen(true)}\n aria-label=\"Open menu\"\n >\n <span /><span /><span />\n </button>\n\n {/* Logo */}\n <a className=\"header-logo\" href=\"/\" onClick={(e) => { e.preventDefault(); Router.navigate(\"/\"); }}>\n {logo ? (\n <img src={getDefaultSrc(logo)} alt={logo.altText || logoFallbackText} className=\"header-logo-img\" />\n ) : (\n <span className=\"header-logo-text\">{logoFallbackText}</span>\n )}\n </a>\n\n {/* Desktop Navigation with subLinks */}\n <nav className=\"header-nav\">\n {(navigationLinks?.links ?? []).map((link: IkasNavigationLink, i: number) => (\n <div key={i} className=\"header-nav-item\">\n <a\n href={link.href}\n className=\"header-nav-link\"\n target={link.openInNewTab ? \"_blank\" : undefined}\n >\n {link.label}\n </a>\n {/* Mega-menu for subLinks */}\n {link.subLinks && link.subLinks.length > 0 && (\n <div className=\"header-submenu\">\n {link.subLinks.map((sub: IkasNavigationLink, j: number) => (\n <a\n key={j}\n href={sub.href}\n className=\"header-submenu-link\"\n target={sub.openInNewTab ? \"_blank\" : undefined}\n >\n {sub.label}\n </a>\n ))}\n </div>\n )}\n </div>\n ))}\n </nav>\n\n {/* Utility Icons */}\n <div className=\"header-icons\">\n <button\n className=\"header-icon-btn\"\n onClick={() => Router.navigateToPage(isLoggedIn ? \"ACCOUNT\" : \"LOGIN\")}\n aria-label=\"Account\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2\" />\n <circle cx=\"12\" cy=\"7\" r=\"4\" />\n </svg>\n </button>\n <button\n className=\"header-icon-btn header-cart-btn\"\n onClick={() => setCartSidebarOpen(true)}\n aria-label=\"Cart\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\">\n <path d=\"M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z\" />\n <line x1=\"3\" y1=\"6\" x2=\"21\" y2=\"6\" />\n <path d=\"M16 10a4 4 0 01-8 0\" />\n </svg>\n {itemCount > 0 && <span className=\"header-cart-badge\">{itemCount}</span>}\n </button>\n </div>\n </div>\n </div>\n\n {/* Mini-Cart Sidebar — production uses IkasThemeOverlay for visibility */}\n {cartSidebarOpen && (\n <div className=\"cart-sidebar-overlay\">\n <div className=\"cart-sidebar-backdrop\" onClick={() => setCartSidebarOpen(false)} />\n <div className=\"cart-sidebar\">\n <div className=\"cart-sidebar-header\">\n <h3>Cart ({itemCount})</h3>\n <button className=\"cart-sidebar-close\" onClick={() => setCartSidebarOpen(false)}>×</button>\n </div>\n\n {isCartLoading && <div className=\"cart-sidebar-loading\">Loading...</div>}\n\n {!cartHasItems && !isCartLoading && (\n <p className=\"cart-sidebar-empty\">Your cart is empty</p>\n )}\n\n <div className=\"cart-sidebar-items\">\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n const href = item.variant ? getIkasOrderLineVariantHref(item.variant) : undefined;\n return (\n <div key={item.id} className=\"cart-sidebar-item\">\n {image && (\n <a href={href}>\n <img className=\"cart-sidebar-item-img\" src={getDefaultSrc(image)} alt={item.variant?.name || \"\"} />\n </a>\n )}\n <div className=\"cart-sidebar-item-info\">\n <a href={href} className=\"cart-sidebar-item-name\">{item.variant?.name}</a>\n <span className=\"cart-sidebar-item-price\">{getOrderLineItemFormattedFinalPrice(item)}</span>\n <div className=\"cart-sidebar-item-qty\">\n <button onClick={() => changeItemQuantity(item, Math.max(1, item.quantity - 1))}>-</button>\n <span>{item.quantity}</span>\n <button onClick={() => changeItemQuantity(item, item.quantity + 1)}>+</button>\n </div>\n </div>\n <button className=\"cart-sidebar-item-remove\" onClick={() => removeItem(item)}>×</button>\n </div>\n );\n })}\n </div>\n\n {/* Order Adjustments (discounts, shipping, etc.) */}\n {adjustments.length > 0 && (\n <div className=\"cart-sidebar-adjustments\">\n {adjustments.map((adj: any, i: number) => (\n <div key={i} className=\"cart-sidebar-adj-row\">\n <span>{getOrderAdjustmentDisplayName(adj)}</span>\n <span>{getOrderAdjustmentFormattedAmount(adj)}</span>\n </div>\n ))}\n </div>\n )}\n\n {cartHasItems && cart && (\n <div className=\"cart-sidebar-footer\">\n <div className=\"cart-sidebar-total\">\n <span>Total</span>\n <span>{getIkasOrderFormattedTotalPrice(cart)}</span>\n </div>\n <a\n href={getCheckoutUrlFromCartStore(cartStore)}\n className=\"cart-sidebar-checkout-btn\"\n >\n Checkout\n </a>\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* Mobile Menu — production uses IkasThemeOverlay visible property */}\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)}>\n ×\n </button>\n <nav className=\"header-mobile-nav\">\n {(navigationLinks?.links ?? []).map((link: IkasNavigationLink, i: number) => (\n <div key={i}>\n <a\n href={link.href}\n className=\"header-mobile-link\"\n onClick={() => setMobileMenuOpen(false)}\n >\n {link.label}\n </a>\n {link.subLinks?.map((sub: IkasNavigationLink, j: number) => (\n <a\n key={j}\n href={sub.href}\n className=\"header-mobile-sublink\"\n onClick={() => setMobileMenuOpen(false)}\n >\n {sub.label}\n </a>\n ))}\n </div>\n ))}\n </nav>\n </div>\n </div>\n )}\n </section>\n );\n}\n\n",
|
|
182
|
+
"types.ts": "import { IkasImage, IkasNavigationLinkList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: IkasImage | null;\n navigationLinks?: IkasNavigationLinkList;\n announcementText?: string;\n announcementBgColor?: string;\n announcementTextColor?: string;\n backgroundColor?: string;\n logoFallbackText?: string;\n}\n",
|
|
129
183
|
"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 font-weight: 500;\n}\n\n.header-main {\n border-bottom: 1px solid #eee;\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/* Logo */\n.header-logo {\n text-decoration: none;\n flex-shrink: 0;\n}\n\n.header-logo-img {\n height: 40px;\n width: auto;\n display: block;\n}\n\n.header-logo-text {\n font-size: 22px;\n font-weight: 700;\n color: #111;\n}\n\n/* Navigation */\n.header-nav {\n display: flex;\n gap: 24px;\n flex: 1;\n justify-content: center;\n}\n\n.header-nav-link {\n font-size: 14px;\n font-weight: 500;\n color: #333;\n text-decoration: none;\n white-space: nowrap;\n transition: color 0.15s ease;\n}\n\n.header-nav-link:hover {\n color: #111;\n}\n\n/* Icons */\n.header-icons {\n display: flex;\n gap: 12px;\n align-items: center;\n flex-shrink: 0;\n}\n\n.header-icon-btn {\n background: none;\n border: none;\n cursor: pointer;\n color: #333;\n padding: 4px;\n position: relative;\n}\n\n.header-cart-badge {\n position: absolute;\n top: -4px;\n right: -6px;\n background: #111;\n color: #fff;\n font-size: 10px;\n font-weight: 700;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n/* Hamburger */\n.header-hamburger {\n display: none;\n flex-direction: column;\n gap: 4px;\n background: none;\n border: none;\n cursor: pointer;\n padding: 4px;\n}\n\n.header-hamburger span {\n display: block;\n width: 20px;\n height: 2px;\n background: #333;\n}\n\n/* Mobile Menu */\n.header-mobile-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 200;\n}\n\n.header-mobile-backdrop {\n position: absolute;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n}\n\n.header-mobile-menu {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n width: 280px;\n background: #fff;\n padding: 24px;\n overflow-y: auto;\n}\n\n.header-mobile-close {\n font-size: 28px;\n background: none;\n border: none;\n cursor: pointer;\n color: #333;\n margin-bottom: 16px;\n}\n\n.header-mobile-nav {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.header-mobile-link {\n font-size: 16px;\n font-weight: 500;\n color: #333;\n text-decoration: none;\n padding: 8px 0;\n border-bottom: 1px solid #f0f0f0;\n}\n\n/* SubMenu */\n.header-nav-item {\n position: relative;\n}\n\n.header-submenu {\n display: none;\n position: absolute;\n top: 100%;\n left: 0;\n background: #fff;\n box-shadow: 0 4px 12px rgba(0,0,0,0.1);\n border-radius: 8px;\n padding: 12px 0;\n min-width: 180px;\n z-index: 50;\n}\n\n.header-nav-item:hover .header-submenu {\n display: flex;\n flex-direction: column;\n}\n\n.header-submenu-link {\n padding: 8px 20px;\n font-size: 14px;\n color: #333;\n text-decoration: none;\n white-space: nowrap;\n}\n\n.header-submenu-link:hover {\n background: #f5f5f5;\n color: #111;\n}\n\n/* Cart Sidebar */\n.cart-sidebar-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 300;\n}\n\n.cart-sidebar-backdrop {\n position: absolute;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n}\n\n.cart-sidebar {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 380px;\n max-width: 100%;\n background: #fff;\n display: flex;\n flex-direction: column;\n box-shadow: -4px 0 12px rgba(0,0,0,0.1);\n}\n\n.cart-sidebar-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 16px 20px;\n border-bottom: 1px solid #eee;\n}\n\n.cart-sidebar-header h3 {\n font-size: 18px;\n font-weight: 700;\n margin: 0;\n}\n\n.cart-sidebar-close {\n font-size: 28px;\n background: none;\n border: none;\n cursor: pointer;\n color: #333;\n}\n\n.cart-sidebar-loading {\n padding: 24px 20px;\n text-align: center;\n color: #666;\n font-size: 14px;\n}\n\n.cart-sidebar-empty {\n padding: 48px 20px;\n text-align: center;\n color: #666;\n font-size: 14px;\n}\n\n.cart-sidebar-items {\n flex: 1;\n overflow-y: auto;\n padding: 12px 20px;\n}\n\n.cart-sidebar-item {\n display: flex;\n gap: 12px;\n padding: 12px 0;\n border-bottom: 1px solid #f5f5f5;\n position: relative;\n}\n\n.cart-sidebar-item-img {\n width: 64px;\n height: 64px;\n object-fit: cover;\n border-radius: 6px;\n background: #f5f5f5;\n}\n\n.cart-sidebar-item-info {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.cart-sidebar-item-name {\n font-size: 13px;\n font-weight: 600;\n color: #111;\n text-decoration: none;\n}\n\n.cart-sidebar-item-price {\n font-size: 13px;\n color: #666;\n}\n\n.cart-sidebar-item-qty {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-top: 4px;\n}\n\n.cart-sidebar-item-qty button {\n width: 24px;\n height: 24px;\n border: 1px solid #ddd;\n border-radius: 4px;\n background: #fff;\n cursor: pointer;\n font-size: 14px;\n}\n\n.cart-sidebar-item-qty span {\n font-size: 13px;\n min-width: 20px;\n text-align: center;\n}\n\n.cart-sidebar-item-remove {\n position: absolute;\n top: 12px;\n right: 0;\n background: none;\n border: none;\n font-size: 18px;\n color: #999;\n cursor: pointer;\n}\n\n.cart-sidebar-adjustments {\n padding: 8px 20px;\n border-top: 1px solid #eee;\n}\n\n.cart-sidebar-adj-row {\n display: flex;\n justify-content: space-between;\n font-size: 13px;\n color: #666;\n padding: 4px 0;\n}\n\n.cart-sidebar-footer {\n padding: 16px 20px;\n border-top: 1px solid #eee;\n}\n\n.cart-sidebar-total {\n display: flex;\n justify-content: space-between;\n font-size: 16px;\n font-weight: 700;\n color: #111;\n margin-bottom: 12px;\n}\n\n.cart-sidebar-checkout-btn {\n display: block;\n width: 100%;\n padding: 14px;\n font-size: 15px;\n font-weight: 600;\n color: #fff;\n background: #111;\n border: none;\n border-radius: 8px;\n text-align: center;\n text-decoration: none;\n cursor: pointer;\n}\n\n/* Mobile SubLinks */\n.header-mobile-sublink {\n font-size: 14px;\n color: #666;\n text-decoration: none;\n padding: 6px 0 6px 16px;\n display: block;\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n .header-hamburger {\n display: flex;\n }\n\n .header-nav {\n display: none;\n }\n}\n",
|
|
130
|
-
"ikas-config-snippet.json": "{\n \"id\": \"header\",\n \"name\": \"Header\",\n \"type\": \"section\",\n \"entry\": \"./src/components/Header/index.tsx\",\n \"styles\": \"./src/components/Header/styles.css\",\n \"props\": [\n {\n \"name\": \"logo\",\n \"displayName\": \"Logo\",\n \"type\": \"IMAGE\"\n },\n {\n \"name\": \"navigationLinks\",\n \"displayName\": \"Navigation Links\",\n \"type\": \"LIST_OF_LINK\"\n },\n {\n \"name\": \"announcementText\",\n \"displayName\": \"Announcement Text\",\n \"type\": \"TEXT\"\n },\n {\n \"name\": \"announcementBgColor\",\n \"displayName\": \"Announcement Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#111111\"\n },\n {\n \"name\": \"announcementTextColor\",\n \"displayName\": \"Announcement Text Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
184
|
+
"ikas-config-snippet.json": "{\n \"id\": \"header\",\n \"name\": \"Header\",\n \"type\": \"section\",\n \"entry\": \"./src/components/Header/index.tsx\",\n \"styles\": \"./src/components/Header/styles.css\",\n \"props\": [\n {\n \"name\": \"logo\",\n \"displayName\": \"Logo\",\n \"type\": \"IMAGE\",\n \"groupId\": \"navigation\"\n },\n {\n \"name\": \"navigationLinks\",\n \"displayName\": \"Navigation Links\",\n \"type\": \"LIST_OF_LINK\",\n \"groupId\": \"navigation\"\n },\n {\n \"name\": \"announcementText\",\n \"displayName\": \"Announcement Text\",\n \"type\": \"TEXT\",\n \"groupId\": \"announcement\"\n },\n {\n \"name\": \"announcementBgColor\",\n \"displayName\": \"Announcement Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#111111\",\n \"groupId\": \"announcement\"\n },\n {\n \"name\": \"announcementTextColor\",\n \"displayName\": \"Announcement Text Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\",\n \"groupId\": \"announcement\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\",\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"logoFallbackText\",\n \"displayName\": \"Logo Fallback Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Store\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n { \"id\": \"navigation\", \"name\": \"Navigation\", \"description\": \"Logo and navigation links\" },\n { \"id\": \"announcement\", \"name\": \"Announcement\", \"description\": \"Announcement bar text and colors\" },\n { \"id\": \"appearance\", \"name\": \"Appearance\", \"description\": \"Visual style settings\" },\n { \"id\": \"texts\", \"name\": \"Texts\", \"description\": \"Customizable text labels\" }\n ]\n}\n"
|
|
131
185
|
}
|
|
132
186
|
},
|
|
133
187
|
"hero-banner": {
|
|
@@ -144,70 +198,88 @@
|
|
|
144
198
|
"title": "Login Section",
|
|
145
199
|
"description": "Complete login section with form validation, error display, loading state, forgot password link, register redirect, social login (socialLogin + SocialLoginProvider), and handleSocialLogin callback with HandleSocialLoginReturnType status handling.",
|
|
146
200
|
"files": {
|
|
147
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getLoginForm,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n handleSocialLogin,\n socialLogin,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function LoginSection({\n redirectAfterLogin = \"/account\",\n showForgotPassword = true,\n}: Props) {\n const loginForm = getLoginForm(customerStore);\n\n useEffect(() => {\n initLoginForm(loginForm);\n // Handle social login callback if returning from OAuth redirect\n handleSocialLogin(customerStore).then((result) => {\n // HandleSocialLoginReturnType: { status: \"success\" | \"fail\", message?: string }\n if (result.status === \"success\") {\n Router.navigate(redirectAfterLogin);\n }\n });\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitLoginForm(loginForm);\n if (success) {\n Router.navigate(redirectAfterLogin);\n }\n };\n\n const handleSocialLoginClick = async (provider: \"GOOGLE\" | \"FACEBOOK\" | \"APPLE\") => {\n // SocialLoginProvider enum — redirects user to provider's OAuth page\n await socialLogin(customerStore, provider as any);\n };\n\n return (\n <section className=\"login-section\">\n <div className=\"login-inner\">\n <h1 className=\"login-title\">
|
|
148
|
-
"types.ts": "export interface Props {\n redirectAfterLogin?: string;\n showForgotPassword?: boolean;\n}\n",
|
|
201
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getLoginForm,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n handleSocialLogin,\n socialLogin,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function LoginSection({\n redirectAfterLogin = \"/account\",\n showForgotPassword = true,\n backgroundColor = \"#ffffff\",\n title = \"Sign In\",\n googleButtonText = \"Continue with Google\",\n facebookButtonText = \"Continue with Facebook\",\n dividerText = \"or\",\n forgotPasswordText = \"Forgot password?\",\n submitButtonText = \"Sign In\",\n submittingButtonText = \"Signing in...\",\n noAccountText = \"Don't have an account?\",\n createAccountLinkText = \"Create one\",\n}: Props) {\n const loginForm = getLoginForm(customerStore);\n\n useEffect(() => {\n initLoginForm(loginForm);\n // Handle social login callback if returning from OAuth redirect\n handleSocialLogin(customerStore).then((result) => {\n // HandleSocialLoginReturnType: { status: \"success\" | \"fail\", message?: string }\n if (result.status === \"success\") {\n Router.navigate(redirectAfterLogin);\n }\n });\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitLoginForm(loginForm);\n if (success) {\n Router.navigate(redirectAfterLogin);\n }\n };\n\n const handleSocialLoginClick = async (provider: \"GOOGLE\" | \"FACEBOOK\" | \"APPLE\") => {\n // SocialLoginProvider enum — redirects user to provider's OAuth page\n await socialLogin(customerStore, provider as any);\n };\n\n return (\n <section className=\"login-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"login-inner\">\n <h1 className=\"login-title\">{title}</h1>\n\n {loginForm.isFailure && loginForm.responseMessage && (\n <div className=\"login-error-banner\">{loginForm.responseMessage}</div>\n )}\n\n {/* Social Login Buttons */}\n <div className=\"login-social\">\n <button className=\"login-social-btn\" onClick={() => handleSocialLoginClick(\"GOOGLE\")}>\n {googleButtonText}\n </button>\n <button className=\"login-social-btn\" onClick={() => handleSocialLoginClick(\"FACEBOOK\")}>\n {facebookButtonText}\n </button>\n </div>\n\n <div className=\"login-divider\"><span>{dividerText}</span></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\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\n {showForgotPassword && (\n <a\n className=\"login-forgot-link\"\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n Router.navigateToPage(\"FORGOT_PASSWORD\");\n }}\n >\n {forgotPasswordText}\n </a>\n )}\n\n <button\n className=\"login-submit-btn\"\n type=\"submit\"\n disabled={loginForm.isSubmitting}\n >\n {loginForm.isSubmitting ? submittingButtonText : submitButtonText}\n </button>\n </form>\n\n <p className=\"login-register-link\">\n {noAccountText}{\" \"}\n <a\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n Router.navigateToPage(\"REGISTER\");\n }}\n >\n {createAccountLinkText}\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
|
|
202
|
+
"types.ts": "export interface Props {\n redirectAfterLogin?: string;\n showForgotPassword?: boolean;\n backgroundColor?: string;\n title?: string;\n googleButtonText?: string;\n facebookButtonText?: string;\n dividerText?: string;\n forgotPasswordText?: string;\n submitButtonText?: string;\n submittingButtonText?: string;\n noAccountText?: string;\n createAccountLinkText?: string;\n}\n",
|
|
149
203
|
"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 {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 24px 0;\n text-align: center;\n}\n\n.login-error-banner {\n padding: 12px 16px;\n font-size: 14px;\n color: #b71c1c;\n background-color: #ffebee;\n border-radius: 8px;\n margin-bottom: 20px;\n}\n\n.login-form {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.login-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.login-label {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n}\n\n.login-input {\n padding: 12px 14px;\n font-size: 15px;\n border: 1.5px solid #ddd;\n border-radius: 8px;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.login-input:focus {\n border-color: #111;\n}\n\n.login-input.has-error {\n border-color: #e53935;\n}\n\n.login-field-error {\n font-size: 12px;\n color: #e53935;\n}\n\n.login-forgot-link {\n font-size: 13px;\n color: #666;\n text-decoration: none;\n align-self: flex-end;\n}\n\n.login-forgot-link:hover {\n color: #111;\n}\n\n.login-submit-btn {\n padding: 14px 24px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background-color: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s ease;\n margin-top: 8px;\n}\n\n.login-submit-btn:hover:not(:disabled) {\n background-color: #333;\n}\n\n.login-submit-btn:disabled {\n background-color: #ccc;\n cursor: not-allowed;\n}\n\n.login-register-link {\n font-size: 14px;\n color: #666;\n text-align: center;\n margin-top: 24px;\n}\n\n.login-register-link a {\n color: #111;\n font-weight: 600;\n text-decoration: none;\n}\n\n.login-register-link a:hover {\n text-decoration: underline;\n}\n",
|
|
150
|
-
"ikas-config-snippet.json": "{\n \"id\": \"login-page\",\n \"name\": \"Login Page\",\n \"type\": \"section\",\n \"entry\": \"./src/components/LoginSection/index.tsx\",\n \"styles\": \"./src/components/LoginSection/styles.css\",\n \"props\": [\n {\n \"name\": \"redirectAfterLogin\",\n \"displayName\": \"Redirect After Login\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"/account\"\n },\n {\n \"name\": \"showForgotPassword\",\n \"displayName\": \"Show Forgot Password Link\",\n \"type\": \"BOOLEAN\",\n \"defaultValue\": true\n }\n ]\n}\n"
|
|
204
|
+
"ikas-config-snippet.json": "{\n \"id\": \"login-page\",\n \"name\": \"Login Page\",\n \"type\": \"section\",\n \"entry\": \"./src/components/LoginSection/index.tsx\",\n \"styles\": \"./src/components/LoginSection/styles.css\",\n \"props\": [\n {\n \"name\": \"redirectAfterLogin\",\n \"displayName\": \"Redirect After Login\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"/account\"\n },\n {\n \"name\": \"showForgotPassword\",\n \"displayName\": \"Show Forgot Password Link\",\n \"type\": \"BOOLEAN\",\n \"defaultValue\": true\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Sign In\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"googleButtonText\",\n \"displayName\": \"Google Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Continue with Google\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"facebookButtonText\",\n \"displayName\": \"Facebook Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Continue with Facebook\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"dividerText\",\n \"displayName\": \"Divider Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"or\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"forgotPasswordText\",\n \"displayName\": \"Forgot Password Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Forgot password?\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submitButtonText\",\n \"displayName\": \"Submit Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Sign In\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submittingButtonText\",\n \"displayName\": \"Submitting Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Signing in...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"noAccountText\",\n \"displayName\": \"No Account Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Don't have an account?\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"createAccountLinkText\",\n \"displayName\": \"Create Account Link Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Create one\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\"\n }\n ]\n}\n"
|
|
151
205
|
}
|
|
152
206
|
},
|
|
153
207
|
"order-detail": {
|
|
154
208
|
"title": "Order Detail Section",
|
|
155
209
|
"description": "Complete order detail section with getOrder fetch, line items with variant images/links/discount, order adjustments, payment transactions, totals, package status, and refund functionality. Uses getPageParams for order ID from URL.",
|
|
156
210
|
"files": {
|
|
157
|
-
"index.tsx": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderFormattedShippingTotal,\n getIkasOrderFormattedTotalTax,\n getIkasOrderDisplayedPackages,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n hasOrderLineItemDiscount,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getOrderAdjustmentIsDecrement,\n getOrderTransactionFormattedAmount,\n getOrderTransactionPaymentMethodTranslation,\n getIkasOrderRefundableItems,\n isIkasOrderRefundable,\n getOrderLineItemRefundQuantity,\n setOrderLineItemRefundQuantity,\n refundOrder,\n getDefaultSrc,\n Router,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\n\nexport default function OrderDetailSection() {\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 <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>
|
|
158
|
-
"types.ts": "export interface Props {}\n",
|
|
211
|
+
"index.tsx": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderFormattedShippingTotal,\n getIkasOrderFormattedTotalTax,\n getIkasOrderDisplayedPackages,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n hasOrderLineItemDiscount,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getOrderAdjustmentIsDecrement,\n getOrderTransactionFormattedAmount,\n getOrderTransactionPaymentMethodTranslation,\n getIkasOrderRefundableItems,\n isIkasOrderRefundable,\n getOrderLineItemRefundQuantity,\n setOrderLineItemRefundQuantity,\n refundOrder,\n getDefaultSrc,\n Router,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function OrderDetailSection({\n backgroundColor = \"#ffffff\",\n loadingText = \"Loading...\",\n notFoundText = \"Order not found.\",\n shippingLabel = \"Shipping:\",\n taxLabel = \"Tax:\",\n subtotalLabel = \"Subtotal:\",\n totalLabel = \"Total:\",\n refundTitle = \"Refund\",\n submitRefundText = \"Submit Refund\",\n}: 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 <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>{loadingText}</p></div></div>;\n if (!order) return <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>{notFoundText}</p></div></div>;\n\n const packages = getIkasOrderDisplayedPackages(order);\n const lineItems = order.orderLineItems ?? [];\n const adjustments = order.orderAdjustments ?? [];\n const transactions = order.orderTransactions ?? [];\n const canRefund = isIkasOrderRefundable(order) as unknown as boolean;\n const refundableItems = canRefund ? getIkasOrderRefundableItems(order) : [];\n\n return (\n <section className=\"order-detail-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"order-detail-inner\">\n <h1>Order #{order.orderNumber}</h1>\n <p>{getIkasOrderFormattedOrderedAt(order)}</p>\n\n {packages.map((pkg, i) => (\n <span key={i} className=\"order-status-badge\">{getIkasOrderPackageStatusTranslation(order)}</span>\n ))}\n\n <div className=\"order-items\">\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n const href = item.variant ? getIkasOrderLineVariantHref(item.variant) : undefined;\n const hasDiscount = hasOrderLineItemDiscount(item) as unknown as boolean;\n return (\n <div key={item.id} className=\"order-item\">\n {image && <a href={href}><img src={getDefaultSrc(image)} width={64} height={64} style={{ objectFit: \"cover\", borderRadius: 4 }} alt=\"\" /></a>}\n <div>\n <a href={href}>{item.variant?.name}</a>\n <span> x{item.quantity}</span>\n <span> {getOrderLineItemFormattedFinalPriceWithQuantity(item)}</span>\n </div>\n </div>\n );\n })}\n </div>\n\n {adjustments.length > 0 && adjustments.map((adj: any, i: number) => (\n <div key={i}>\n <span>{getOrderAdjustmentDisplayName(adj)}: </span>\n <span style={{ color: getOrderAdjustmentIsDecrement(adj) ? \"green\" : \"inherit\" }}>{getOrderAdjustmentFormattedAmount(adj)}</span>\n </div>\n ))}\n\n {transactions.map((tx: any, i: number) => (\n <div key={i}>{getOrderTransactionPaymentMethodTranslation(tx)}: {getOrderTransactionFormattedAmount(tx)}</div>\n ))}\n\n <div style={{ marginTop: 16, borderTop: \"1px solid #eee\", paddingTop: 16 }}>\n <div>{shippingLabel} {getIkasOrderFormattedShippingTotal(order)}</div>\n <div>{taxLabel} {getIkasOrderFormattedTotalTax(order)}</div>\n <div>{subtotalLabel} {getIkasOrderFormattedTotalPrice(order)}</div>\n <div style={{ fontWeight: 700, fontSize: 18 }}>{totalLabel} {getIkasOrderFormattedTotalFinalPrice(order)}</div>\n </div>\n\n {canRefund && (\n <div style={{ marginTop: 24 }}>\n <h3>{refundTitle}</h3>\n {refundableItems.map((item: any) => (\n <div key={item.id}>\n {item.variant?.name}: <input type=\"number\" min={0} max={item.quantity} value={getOrderLineItemRefundQuantity(item) ?? 0}\n onChange={(e) => setOrderLineItemRefundQuantity(Number((e.target as HTMLInputElement).value), item)} style={{ width: 60 }} />\n </div>\n ))}\n <button onClick={() => refundOrder(customerStore, order)} style={{ marginTop: 8 }}>{submitRefundText}</button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
212
|
+
"types.ts": "export interface Props {\n backgroundColor?: string;\n loadingText?: string;\n notFoundText?: string;\n shippingLabel?: string;\n taxLabel?: string;\n subtotalLabel?: string;\n totalLabel?: string;\n refundTitle?: string;\n submitRefundText?: string;\n}\n",
|
|
159
213
|
"styles.css": ".order-detail-section { width: 100%; padding: 40px 24px; }\n.order-detail-inner { max-width: 800px; margin: 0 auto; }\n",
|
|
160
|
-
"ikas-config-snippet.json": "{
|
|
214
|
+
"ikas-config-snippet.json": "{\n \"id\": \"order-detail\",\n \"name\": \"Order Detail\",\n \"type\": \"section\",\n \"props\": [\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"loadingText\",\n \"displayName\": \"Loading Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Loading...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"notFoundText\",\n \"displayName\": \"Not Found Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Order not found.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"shippingLabel\",\n \"displayName\": \"Shipping Label\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Shipping:\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"taxLabel\",\n \"displayName\": \"Tax Label\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Tax:\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"subtotalLabel\",\n \"displayName\": \"Subtotal Label\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Subtotal:\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"totalLabel\",\n \"displayName\": \"Total Label\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Total:\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"refundTitle\",\n \"displayName\": \"Refund Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Refund\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submitRefundText\",\n \"displayName\": \"Submit Refund Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Submit Refund\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\"\n }\n ]\n}\n"
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
"order-display": {
|
|
218
|
+
"title": "Order Detail Display",
|
|
219
|
+
"description": "Complete order detail with getOrder fetch, line items with variant images/links/discount detection, order adjustments with isDecrement, payment transactions, totals (subtotal/shipping/tax/total), package status, and refund functionality (isIkasOrderRefundable, getIkasOrderRefundableItems, setOrderLineItemRefundQuantity, refundOrder).",
|
|
220
|
+
"files": {
|
|
221
|
+
"index.tsx": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderFormattedShippingTotal,\n getIkasOrderFormattedTotalTax,\n getIkasOrderDisplayedPackages,\n getIkasOrderDistinctItemCount,\n getIkasOrderCustomerFullName,\n getIkasOrderHref,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n getOrderLineItemFormattedPriceWithQuantity,\n hasOrderLineItemDiscount,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getOrderAdjustmentIsDecrement,\n getOrderTransactionFormattedAmount,\n getOrderTransactionPaymentMethodTranslation,\n getIkasOrderRefundableItems,\n isIkasOrderRefundable,\n getOrderLineItemRefundQuantity,\n setOrderLineItemRefundQuantity,\n refundOrder,\n getDefaultSrc,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function OrderDetail({\n orderId,\n loadingText = \"Loading order...\",\n notFoundText = \"Order not found.\",\n packagesTitle = \"Packages\",\n itemsTitle = \"Items\",\n quantityLabel = \"Qty:\",\n adjustmentsTitle = \"Adjustments\",\n paymentTitle = \"Payment\",\n subtotalLabel = \"Subtotal\",\n shippingLabel = \"Shipping\",\n taxLabel = \"Tax\",\n totalLabel = \"Total\",\n refundTitle = \"Request Refund\",\n submitRefundText = \"Submit Refund Request\",\n}: Props) {\n const [order, setOrder] = useState<IkasOrder | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n getOrder(customerStore, orderId).then((o) => {\n setOrder(o);\n setLoading(false);\n });\n }, [orderId]);\n\n if (loading) return <p>{loadingText}</p>;\n if (!order) return <p>{notFoundText}</p>;\n\n const packages = getIkasOrderDisplayedPackages(order);\n const lineItems = order.orderLineItems ?? [];\n const adjustments = order.orderAdjustments ?? [];\n const transactions = order.orderTransactions ?? [];\n const canRefund = isIkasOrderRefundable(order) as unknown as boolean;\n const refundableItems = canRefund ? getIkasOrderRefundableItems(order) : [];\n\n const handleRefund = async () => {\n const success = await refundOrder(customerStore, order);\n if (success) {\n // Refresh order\n const updated = await getOrder(customerStore, orderId);\n if (updated) setOrder(updated);\n }\n };\n\n return (\n <div>\n <h2>Order #{order.orderNumber}</h2>\n <p>Customer: {getIkasOrderCustomerFullName(order)}</p>\n <p>Date: {getIkasOrderFormattedOrderedAt(order)}</p>\n <p>Items: {getIkasOrderDistinctItemCount(order)}</p>\n\n {/* Package Status */}\n <h3>{packagesTitle}</h3>\n {packages.map((pkg, i) => (\n <div key={i}>\n <p>Package {i + 1}: {getIkasOrderPackageStatusTranslation(order)}</p>\n </div>\n ))}\n\n {/* Line Items */}\n <h3>{itemsTitle}</h3>\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n const href = item.variant ? getIkasOrderLineVariantHref(item.variant) : undefined;\n const hasDiscount = hasOrderLineItemDiscount(item) as unknown as boolean;\n return (\n <div key={item.id} style={{ display: \"flex\", gap: 12, padding: \"8px 0\", borderBottom: \"1px solid #eee\" }}>\n {image && (\n <a href={href}>\n <img src={getDefaultSrc(image)} width={64} height={64} style={{ objectFit: \"cover\", borderRadius: 4 }} alt=\"\" />\n </a>\n )}\n <div>\n <a href={href} style={{ fontWeight: 600, textDecoration: \"none\", color: \"#111\" }}>\n {item.variant?.name}\n </a>\n <div>{quantityLabel} {item.quantity}</div>\n {hasDiscount && (\n <span style={{ textDecoration: \"line-through\", color: \"#999\", marginRight: 8 }}>\n {getOrderLineItemFormattedPriceWithQuantity(item)}\n </span>\n )}\n <span>{getOrderLineItemFormattedFinalPriceWithQuantity(item)}</span>\n </div>\n </div>\n );\n })}\n\n {/* Order Adjustments */}\n {adjustments.length > 0 && (\n <div>\n <h3>{adjustmentsTitle}</h3>\n {adjustments.map((adj: any, i: number) => (\n <div key={i} style={{ display: \"flex\", justifyContent: \"space-between\", padding: \"4px 0\" }}>\n <span>{getOrderAdjustmentDisplayName(adj)}</span>\n <span style={{ color: getOrderAdjustmentIsDecrement(adj) ? \"#2e7d32\" : \"#111\" }}>\n {getOrderAdjustmentFormattedAmount(adj)}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Payment Transactions */}\n {transactions.length > 0 && (\n <div>\n <h3>{paymentTitle}</h3>\n {transactions.map((tx: any, i: number) => (\n <div key={i} style={{ display: \"flex\", justifyContent: \"space-between\", padding: \"4px 0\" }}>\n <span>{getOrderTransactionPaymentMethodTranslation(tx)}</span>\n <span>{getOrderTransactionFormattedAmount(tx)}</span>\n </div>\n ))}\n </div>\n )}\n\n {/* Order Totals */}\n <div style={{ borderTop: \"1px solid #eee\", paddingTop: 12, marginTop: 12 }}>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>{subtotalLabel}</span>\n <span>{getIkasOrderFormattedTotalPrice(order)}</span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>{shippingLabel}</span>\n <span>{getIkasOrderFormattedShippingTotal(order)}</span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>{taxLabel}</span>\n <span>{getIkasOrderFormattedTotalTax(order)}</span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\", fontWeight: 700, fontSize: 18, marginTop: 8 }}>\n <span>{totalLabel}</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(order)}</span>\n </div>\n </div>\n\n {/* Refund Section */}\n {canRefund && (\n <div style={{ marginTop: 24, padding: 16, border: \"1px solid #eee\", borderRadius: 8 }}>\n <h3>{refundTitle}</h3>\n {refundableItems.map((item: any) => (\n <div key={item.id} style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\", padding: \"8px 0\" }}>\n <span>{item.variant?.name}</span>\n <input\n type=\"number\"\n min={0}\n max={item.quantity}\n value={getOrderLineItemRefundQuantity(item) ?? 0}\n onChange={(e) => setOrderLineItemRefundQuantity(Number((e.target as HTMLInputElement).value), item)}\n style={{ width: 60, textAlign: \"center\" }}\n />\n </div>\n ))}\n <button onClick={handleRefund} style={{ marginTop: 12, padding: \"10px 20px\", background: \"#111\", color: \"#fff\", border: \"none\", borderRadius: 6, cursor: \"pointer\" }}>\n {submitRefundText}\n </button>\n </div>\n )}\n\n <a href={getIkasOrderHref(order)} style={{ display: \"inline-block\", marginTop: 16 }}>View on store</a>\n </div>\n );\n}\n\n",
|
|
222
|
+
"types.ts": "export interface Props {\n orderId: string;\n loadingText?: string;\n notFoundText?: string;\n packagesTitle?: string;\n itemsTitle?: string;\n quantityLabel?: string;\n adjustmentsTitle?: string;\n paymentTitle?: string;\n subtotalLabel?: string;\n shippingLabel?: string;\n taxLabel?: string;\n totalLabel?: string;\n refundTitle?: string;\n submitRefundText?: string;\n}\n",
|
|
223
|
+
"ikas-config-snippet.json": "{\n \"id\": \"order-display\",\n \"name\": \"Order Display\",\n \"type\": \"component\",\n \"entry\": \"./src/components/OrderDisplay/index.tsx\",\n \"props\": [\n {\n \"name\": \"loadingText\",\n \"displayName\": \"Loading Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Loading order...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"notFoundText\",\n \"displayName\": \"Not Found Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Order not found.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"packagesTitle\",\n \"displayName\": \"Packages Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Packages\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"itemsTitle\",\n \"displayName\": \"Items Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Items\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"quantityLabel\",\n \"displayName\": \"Quantity Label\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Qty:\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"adjustmentsTitle\",\n \"displayName\": \"Adjustments Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Adjustments\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"paymentTitle\",\n \"displayName\": \"Payment Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Payment\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"subtotalLabel\",\n \"displayName\": \"Subtotal Label\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Subtotal\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"shippingLabel\",\n \"displayName\": \"Shipping Label\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Shipping\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"taxLabel\",\n \"displayName\": \"Tax Label\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Tax\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"totalLabel\",\n \"displayName\": \"Total Label\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Total\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"refundTitle\",\n \"displayName\": \"Refund Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Request Refund\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submitRefundText\",\n \"displayName\": \"Submit Refund Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Submit Refund Request\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n { \"id\": \"texts\", \"name\": \"Texts\", \"description\": \"Labels and text content\" }\n ]\n}\n"
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
"product-card": {
|
|
227
|
+
"title": "Product Card",
|
|
228
|
+
"description": "Product card component with image, name, main variant display (getMainProductVariantType/getMainProductVariantValue), pricing with discount, stock badge, favorite toggle, and product link. Used in product grids and lists.",
|
|
229
|
+
"files": {
|
|
230
|
+
"index.tsx": "import {\n getSelectedProductVariant,\n getSelectedProductVariantHref,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n getProductVariantMainImage,\n getMainProductVariantType,\n getMainProductVariantValue,\n hasProductStock,\n hasProductVariantStock,\n hasProductVariantDiscount,\n hasBundleSettings,\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n customerStore,\n hasCustomer,\n getDefaultSrc,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductCard({\n product,\n outOfStockText = \"Out of Stock\",\n saleText = \"Sale\",\n}: Props) {\n const variant = getSelectedProductVariant(product);\n const href = getSelectedProductVariantHref(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const finalPrice = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const hasDiscount = hasProductVariantDiscount(variant) as unknown as boolean;\n const sellPrice = hasDiscount ? (getProductVariantFormattedSellPrice(variant) as unknown as string) : null;\n const inStock = hasProductStock(product) as unknown as boolean;\n const isFavorite = isFavoriteIkasProduct(product);\n\n // Main variant type/value (e.g., \"Color: Red\")\n const mainType = getMainProductVariantType(product);\n const mainValue = getMainProductVariantValue(product);\n\n const handleFavorite = async (e: Event) => {\n e.preventDefault();\n e.stopPropagation();\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n\n return (\n <a href={href} className=\"product-card\" style={{ display: \"block\", textDecoration: \"none\", color: \"inherit\" }}>\n <div style={{ position: \"relative\" }}>\n {image && (\n <img src={getDefaultSrc(image)} alt={product.name} style={{ width: \"100%\", aspectRatio: \"1\", objectFit: \"cover\", borderRadius: 8, background: \"#f5f5f5\" }} />\n )}\n {!inStock && (\n <span style={{ position: \"absolute\", top: 8, left: 8, background: \"#111\", color: \"#fff\", fontSize: 11, padding: \"4px 8px\", borderRadius: 4 }}>{outOfStockText}</span>\n )}\n {hasDiscount && (\n <span style={{ position: \"absolute\", top: 8, right: 8, background: \"#e53935\", color: \"#fff\", fontSize: 11, padding: \"4px 8px\", borderRadius: 4 }}>{saleText}</span>\n )}\n <button\n onClick={handleFavorite}\n style={{ position: \"absolute\", bottom: 8, right: 8, background: \"#fff\", border: \"none\", borderRadius: \"50%\", width: 32, height: 32, cursor: \"pointer\", fontSize: 16 }}\n >\n {isFavorite ? \"\\u2665\" : \"\\u2661\"}\n </button>\n </div>\n <div style={{ padding: \"8px 0\" }}>\n <h3 style={{ fontSize: 14, fontWeight: 600, margin: \"0 0 4px 0\" }}>{product.name}</h3>\n {mainType && mainValue && (\n <span style={{ fontSize: 12, color: \"#666\" }}>{mainType.name}: {mainValue.name}</span>\n )}\n <div style={{ marginTop: 4 }}>\n {sellPrice && <span style={{ textDecoration: \"line-through\", color: \"#999\", marginRight: 8, fontSize: 13 }}>{sellPrice}</span>}\n <span style={{ fontSize: 15, fontWeight: 700 }}>{finalPrice}</span>\n </div>\n </div>\n </a>\n );\n}\n",
|
|
231
|
+
"types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n outOfStockText?: string;\n saleText?: string;\n}\n",
|
|
232
|
+
"ikas-config-snippet.json": "{\n \"id\": \"product-card\",\n \"name\": \"Product Card\",\n \"props\": [\n {\n \"id\": \"outOfStockText\",\n \"name\": \"Out of Stock Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Out of Stock\"\n },\n {\n \"id\": \"saleText\",\n \"name\": \"Sale Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Sale\"\n }\n ]\n}\n"
|
|
161
233
|
}
|
|
162
234
|
},
|
|
163
235
|
"product-detail": {
|
|
164
236
|
"title": "Product Detail Section",
|
|
165
237
|
"description": "Complete product detail section with breadcrumb, image gallery (IkasThemeSlider pattern), variant selection, pricing with discount amount, add-to-cart with result handling, favorites, brand link, product attributes, and bundle products. Based on production dynavit.json patterns.",
|
|
166
238
|
"files": {
|
|
167
|
-
"index.tsx": "import { useState, useEffect } from \"preact/hooks\";\nimport {\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n hasProductStock,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantDiscountPercentage,\n getProductVariantFormattedDiscountAmount,\n isAddToCartEnabled,\n addItemToCart,\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n getProductVariantMainImage,\n getDefaultSrc,\n getThumbnailSrc,\n getSrc,\n createMediaSrcset,\n getProductCategoryPath,\n getIkasCategoryPathItemHref,\n getIkasBrandHref,\n getAttributeListValues,\n hasBundleSettings,\n initBundleProducts,\n getDisplayedProductGroups,\n isNotEmpty,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductDetail({\n product,\n showFavoriteButton = true,\n addToCartButtonText = \"Add to Cart\",\n}: Props) {\n const [selectedImageIndex, setSelectedImageIndex] = useState(0);\n const [isAddingToCart, setIsAddingToCart] = useState(false);\n const [quantity, setQuantity] = useState(1);\n\n useEffect(() => {\n if (product) {\n const variant = getSelectedProductVariant(product);\n if (variant && hasBundleSettings(variant)) {\n initBundleProducts(product);\n }\n }\n }, [product]);\n\n if (!product) {\n return null;\n }\n\n const selectedVariant = getSelectedProductVariant(product) as any;\n const variantTypes = getDisplayedProductVariantTypes(product);\n const inStock = hasProductStock(product) as unknown as boolean;\n const variantInStock = hasProductVariantStock(selectedVariant) as unknown as boolean;\n const canAddToCart = isAddToCartEnabled(product) as unknown as boolean;\n\n // Pricing\n const hasDiscount = hasProductVariantDiscount(selectedVariant) as unknown as boolean;\n const finalPrice = getProductVariantFormattedFinalPrice(selectedVariant) as unknown as string;\n const originalPrice = hasDiscount\n ? (getProductVariantFormattedSellPrice(selectedVariant) as unknown as string)\n : null;\n const discountPercentage = hasDiscount\n ? (getProductVariantDiscountPercentage(selectedVariant) as unknown as number)\n : null;\n const discountAmount = hasDiscount\n ? (getProductVariantFormattedDiscountAmount(selectedVariant) as unknown as string)\n : null;\n\n // Breadcrumb\n const categoryPath = getProductCategoryPath(product);\n\n // Images — in production, dynavit uses IkasThemeSlider for image carousel\n const mainProductImage = getProductVariantMainImage(selectedVariant);\n const mainImage = mainProductImage?.image;\n const variantImages = selectedVariant?.images;\n const images: IkasImage[] = variantImages?.length\n ? variantImages\n .map((pi: any) => pi.image)\n .filter((img: any): img is IkasImage => img != null)\n : mainImage\n ? [mainImage]\n : [];\n const currentImage = images[selectedImageIndex] ?? images[0];\n\n // Favorites\n const isFavorite = isFavoriteIkasProduct(product);\n\n // Attributes\n const attributes = product.attributeList\n ? getAttributeListValues(product.attributeList)\n : [];\n\n // Bundle / Offer products\n const hasBundle = selectedVariant ? (hasBundleSettings(selectedVariant) as unknown as boolean) : false;\n const productGroups = hasBundle ? getDisplayedProductGroups(product) : [];\n\n const handleAddToCart = async () => {\n if (!canAddToCart || isAddingToCart) return;\n setIsAddingToCart(true);\n try {\n const result = await addItemToCart(selectedVariant, product, quantity);\n if (result.success) {\n setQuantity(1);\n }\n } finally {\n setIsAddingToCart(false);\n }\n };\n\n const handleToggleFavorite = async () => {\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n\n return (\n <section className=\"product-detail\">\n {/* Breadcrumb */}\n {isNotEmpty(categoryPath) && (\n <nav className=\"product-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((pathItem: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\">/</span>\n <a href={getIkasCategoryPathItemHref(pathItem)}>{pathItem.name}</a>\n </span>\n ))}\n <span className=\"breadcrumb-sep\">/</span>\n <span className=\"breadcrumb-current\">{product.name}</span>\n </nav>\n )}\n\n <div className=\"product-detail-inner\">\n {/* Image Gallery — production uses IkasThemeSlider for carousel */}\n <div className=\"product-gallery\">\n {currentImage && (\n <img\n className=\"product-main-image\"\n src={getDefaultSrc(currentImage)}\n srcSet={createMediaSrcset(currentImage)}\n sizes=\"(max-width: 768px) 100vw, 50vw\"\n alt={currentImage.altText || product.name}\n />\n )}\n {images.length > 1 && (\n <div className=\"product-thumbnails\">\n {images.map((img, i) => (\n <button\n key={img.id || i}\n className={`product-thumbnail-btn ${i === selectedImageIndex ? \"active\" : \"\"}`}\n onClick={() => setSelectedImageIndex(i)}\n >\n <img src={getThumbnailSrc(img)} alt={`${product.name} ${i + 1}`} />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Product Info */}\n <div className=\"product-info\">\n {/* Brand with link */}\n {product.brand && (\n <a className=\"product-brand\" href={getIkasBrandHref(product.brand)}>\n {product.brand.name}\n </a>\n )}\n <h1 className=\"product-name\">{product.name}</h1>\n\n {/* Average Rating */}\n {product.averageRating > 0 && (\n <div className=\"product-rating\">\n {\"★\".repeat(Math.round(product.averageRating))}\n {\"☆\".repeat(5 - Math.round(product.averageRating))}\n <span className=\"product-rating-value\">({product.averageRating.toFixed(1)})</span>\n </div>\n )}\n\n {/* Campaign badges */}\n {isNotEmpty(product.campaigns) && (\n <div className=\"product-campaigns\">\n {product.campaigns.map((campaign: any) => (\n <span key={campaign.id} className=\"product-campaign-badge\">\n {campaign.name}\n </span>\n ))}\n </div>\n )}\n\n {/* Pricing */}\n <div className=\"product-pricing\">\n <span className=\"product-final-price\">{finalPrice}</span>\n {originalPrice && <span className=\"product-original-price\">{originalPrice}</span>}\n {discountPercentage != null && (\n <span className=\"product-discount-badge\">-{discountPercentage}%</span>\n )}\n {discountAmount && (\n <span className=\"product-savings\">Save {discountAmount}</span>\n )}\n </div>\n\n {/* Variant Selection */}\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\n {/* Stock notice */}\n {!inStock && <span className=\"out-of-stock-notice\">Out of Stock</span>}\n\n {/* Quantity + Add to Cart */}\n <div className=\"product-actions\">\n <div className=\"product-quantity\">\n <button onClick={() => setQuantity(Math.max(1, quantity - 1))}>-</button>\n <span>{quantity}</span>\n <button onClick={() => setQuantity(quantity + 1)}>+</button>\n </div>\n <button\n className=\"add-to-cart-btn\"\n disabled={!canAddToCart || isAddingToCart}\n onClick={handleAddToCart}\n >\n {isAddingToCart ? \"Adding...\" : !variantInStock ? \"Out of Stock\" : addToCartButtonText}\n </button>\n {showFavoriteButton && (\n <button\n className={`favorite-btn ${isFavorite ? \"is-favorite\" : \"\"}`}\n onClick={handleToggleFavorite}\n aria-label={isFavorite ? \"Remove from favorites\" : \"Add to favorites\"}\n >\n {isFavorite ? \"\\u2665\" : \"\\u2661\"}\n </button>\n )}\n </div>\n\n {/* Description */}\n {product.description && (\n <div className=\"product-description\">\n <h3>Description</h3>\n <div dangerouslySetInnerHTML={{ __html: product.description }} />\n </div>\n )}\n\n {/* Product Attributes (e.g. ingredients, specs) */}\n {isNotEmpty(attributes) && (\n <div className=\"product-attributes\">\n <h3>Details</h3>\n {attributes.map((attr: any, i: number) => (\n <div key={i} className=\"attribute-row\">\n <span className=\"attribute-name\">{attr.name}</span>\n <span className=\"attribute-value\">{attr.value}</span>\n </div>\n ))}\n </div>\n )}\n\n {/* Bundle / Offer Products */}\n {isNotEmpty(productGroups) && (\n <div className=\"product-bundles\">\n <h3>Frequently Bought Together</h3>\n {productGroups.map((group: any, i: number) => (\n <div key={i} className=\"bundle-group\">\n {group.title && <h4>{group.title}</h4>}\n <div className=\"bundle-items\">\n {group.products?.map((bp: any) => (\n <div key={bp.product?.id} className=\"bundle-item\">\n <span>{bp.product?.name}</span>\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </section>\n );\n}\n\n",
|
|
168
|
-
"types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n showFavoriteButton?: boolean;\n addToCartButtonText?: string;\n}\n",
|
|
239
|
+
"index.tsx": "import { useState, useEffect } from \"preact/hooks\";\nimport {\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n hasProductStock,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantDiscountPercentage,\n getProductVariantFormattedDiscountAmount,\n isAddToCartEnabled,\n addItemToCart,\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n getProductVariantMainImage,\n getDefaultSrc,\n getThumbnailSrc,\n getSrc,\n createMediaSrcset,\n getProductCategoryPath,\n getIkasCategoryPathItemHref,\n getIkasBrandHref,\n getAttributeListValues,\n hasBundleSettings,\n initBundleProducts,\n getDisplayedProductGroups,\n isNotEmpty,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductDetail({\n product,\n showFavoriteButton = true,\n addToCartButtonText = \"Add to Cart\",\n backgroundColor = \"#ffffff\",\n descriptionTitle = \"Description\",\n detailsTitle = \"Details\",\n bundleTitle = \"Frequently Bought Together\",\n outOfStockText = \"Out of Stock\",\n saveText = \"Save\",\n}: Props) {\n const [selectedImageIndex, setSelectedImageIndex] = useState(0);\n const [isAddingToCart, setIsAddingToCart] = useState(false);\n const [quantity, setQuantity] = useState(1);\n\n useEffect(() => {\n if (product) {\n const variant = getSelectedProductVariant(product);\n if (variant && hasBundleSettings(variant)) {\n initBundleProducts(product);\n }\n }\n }, [product]);\n\n if (!product) {\n return null;\n }\n\n const selectedVariant = getSelectedProductVariant(product) as any;\n const variantTypes = getDisplayedProductVariantTypes(product);\n const inStock = hasProductStock(product) as unknown as boolean;\n const variantInStock = hasProductVariantStock(selectedVariant) as unknown as boolean;\n const canAddToCart = isAddToCartEnabled(product) as unknown as boolean;\n\n // Pricing\n const hasDiscount = hasProductVariantDiscount(selectedVariant) as unknown as boolean;\n const finalPrice = getProductVariantFormattedFinalPrice(selectedVariant) as unknown as string;\n const originalPrice = hasDiscount\n ? (getProductVariantFormattedSellPrice(selectedVariant) as unknown as string)\n : null;\n const discountPercentage = hasDiscount\n ? (getProductVariantDiscountPercentage(selectedVariant) as unknown as number)\n : null;\n const discountAmount = hasDiscount\n ? (getProductVariantFormattedDiscountAmount(selectedVariant) as unknown as string)\n : null;\n\n // Breadcrumb\n const categoryPath = getProductCategoryPath(product);\n\n // Images — in production, dynavit uses IkasThemeSlider for image carousel\n const mainProductImage = getProductVariantMainImage(selectedVariant);\n const mainImage = mainProductImage?.image;\n const variantImages = selectedVariant?.images;\n const images: IkasImage[] = variantImages?.length\n ? variantImages\n .map((pi: any) => pi.image)\n .filter((img: any): img is IkasImage => img != null)\n : mainImage\n ? [mainImage]\n : [];\n const currentImage = images[selectedImageIndex] ?? images[0];\n\n // Favorites\n const isFavorite = isFavoriteIkasProduct(product);\n\n // Attributes\n const attributes = product.attributeList\n ? getAttributeListValues(product.attributeList)\n : [];\n\n // Bundle / Offer products\n const hasBundle = selectedVariant ? (hasBundleSettings(selectedVariant) as unknown as boolean) : false;\n const productGroups = hasBundle ? getDisplayedProductGroups(product) : [];\n\n const handleAddToCart = async () => {\n if (!canAddToCart || isAddingToCart) return;\n setIsAddingToCart(true);\n try {\n const result = await addItemToCart(selectedVariant, product, quantity);\n if (result.success) {\n setQuantity(1);\n }\n } finally {\n setIsAddingToCart(false);\n }\n };\n\n const handleToggleFavorite = async () => {\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n\n return (\n <section className=\"product-detail\" style={backgroundColor ? { backgroundColor } : undefined}>\n {/* Breadcrumb */}\n {isNotEmpty(categoryPath) && (\n <nav className=\"product-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((pathItem: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\">/</span>\n <a href={getIkasCategoryPathItemHref(pathItem)}>{pathItem.name}</a>\n </span>\n ))}\n <span className=\"breadcrumb-sep\">/</span>\n <span className=\"breadcrumb-current\">{product.name}</span>\n </nav>\n )}\n\n <div className=\"product-detail-inner\">\n {/* Image Gallery — production uses IkasThemeSlider for carousel */}\n <div className=\"product-gallery\">\n {currentImage && (\n <img\n className=\"product-main-image\"\n src={getDefaultSrc(currentImage)}\n srcSet={createMediaSrcset(currentImage)}\n sizes=\"(max-width: 768px) 100vw, 50vw\"\n alt={currentImage.altText || product.name}\n />\n )}\n {images.length > 1 && (\n <div className=\"product-thumbnails\">\n {images.map((img, i) => (\n <button\n key={img.id || i}\n className={`product-thumbnail-btn ${i === selectedImageIndex ? \"active\" : \"\"}`}\n onClick={() => setSelectedImageIndex(i)}\n >\n <img src={getThumbnailSrc(img)} alt={`${product.name} ${i + 1}`} />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Product Info */}\n <div className=\"product-info\">\n {/* Brand with link */}\n {product.brand && (\n <a className=\"product-brand\" href={getIkasBrandHref(product.brand)}>\n {product.brand.name}\n </a>\n )}\n <h1 className=\"product-name\">{product.name}</h1>\n\n {/* Average Rating */}\n {product.averageRating > 0 && (\n <div className=\"product-rating\">\n {\"★\".repeat(Math.round(product.averageRating))}\n {\"☆\".repeat(5 - Math.round(product.averageRating))}\n <span className=\"product-rating-value\">({product.averageRating.toFixed(1)})</span>\n </div>\n )}\n\n {/* Campaign badges */}\n {isNotEmpty(product.campaigns) && (\n <div className=\"product-campaigns\">\n {product.campaigns.map((campaign: any) => (\n <span key={campaign.id} className=\"product-campaign-badge\">\n {campaign.name}\n </span>\n ))}\n </div>\n )}\n\n {/* Pricing */}\n <div className=\"product-pricing\">\n <span className=\"product-final-price\">{finalPrice}</span>\n {originalPrice && <span className=\"product-original-price\">{originalPrice}</span>}\n {discountPercentage != null && (\n <span className=\"product-discount-badge\">-{discountPercentage}%</span>\n )}\n {discountAmount && (\n <span className=\"product-savings\">{saveText} {discountAmount}</span>\n )}\n </div>\n\n {/* Variant Selection */}\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\n {/* Stock notice */}\n {!inStock && <span className=\"out-of-stock-notice\">{outOfStockText}</span>}\n\n {/* Quantity + Add to Cart */}\n <div className=\"product-actions\">\n <div className=\"product-quantity\">\n <button onClick={() => setQuantity(Math.max(1, quantity - 1))}>-</button>\n <span>{quantity}</span>\n <button onClick={() => setQuantity(quantity + 1)}>+</button>\n </div>\n <button\n className=\"add-to-cart-btn\"\n disabled={!canAddToCart || isAddingToCart}\n onClick={handleAddToCart}\n >\n {isAddingToCart ? \"Adding...\" : !variantInStock ? outOfStockText : addToCartButtonText}\n </button>\n {showFavoriteButton && (\n <button\n className={`favorite-btn ${isFavorite ? \"is-favorite\" : \"\"}`}\n onClick={handleToggleFavorite}\n aria-label={isFavorite ? \"Remove from favorites\" : \"Add to favorites\"}\n >\n {isFavorite ? \"\\u2665\" : \"\\u2661\"}\n </button>\n )}\n </div>\n\n {/* Description */}\n {product.description && (\n <div className=\"product-description\">\n <h3>{descriptionTitle}</h3>\n <div dangerouslySetInnerHTML={{ __html: product.description }} />\n </div>\n )}\n\n {/* Product Attributes (e.g. ingredients, specs) */}\n {isNotEmpty(attributes) && (\n <div className=\"product-attributes\">\n <h3>{detailsTitle}</h3>\n {attributes.map((attr: any, i: number) => (\n <div key={i} className=\"attribute-row\">\n <span className=\"attribute-name\">{attr.name}</span>\n <span className=\"attribute-value\">{attr.value}</span>\n </div>\n ))}\n </div>\n )}\n\n {/* Bundle / Offer Products */}\n {isNotEmpty(productGroups) && (\n <div className=\"product-bundles\">\n <h3>{bundleTitle}</h3>\n {productGroups.map((group: any, i: number) => (\n <div key={i} className=\"bundle-group\">\n {group.title && <h4>{group.title}</h4>}\n <div className=\"bundle-items\">\n {group.products?.map((bp: any) => (\n <div key={bp.product?.id} className=\"bundle-item\">\n <span>{bp.product?.name}</span>\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </section>\n );\n}\n\n",
|
|
240
|
+
"types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n showFavoriteButton?: boolean;\n addToCartButtonText?: string;\n backgroundColor?: string;\n descriptionTitle?: string;\n detailsTitle?: string;\n bundleTitle?: string;\n outOfStockText?: string;\n saveText?: string;\n}\n",
|
|
169
241
|
"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 align-items: start;\n}\n\n/* Image Gallery */\n.product-gallery {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.product-main-image {\n width: 100%;\n aspect-ratio: 1;\n object-fit: cover;\n border-radius: 8px;\n background-color: #f5f5f5;\n}\n\n.product-thumbnails {\n display: flex;\n gap: 8px;\n overflow-x: auto;\n}\n\n.product-thumbnail-btn {\n flex-shrink: 0;\n width: 72px;\n height: 72px;\n padding: 0;\n border: 2px solid transparent;\n border-radius: 6px;\n cursor: pointer;\n overflow: hidden;\n background: none;\n}\n\n.product-thumbnail-btn.active {\n border-color: #111;\n}\n\n.product-thumbnail-btn img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n/* Product Info */\n.product-info {\n display: flex;\n flex-direction: column;\n gap: 20px;\n}\n\n.product-brand {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: #666;\n text-decoration: none;\n}\n\n.product-brand:hover {\n color: #111;\n}\n\n.product-name {\n font-size: 28px;\n font-weight: 700;\n line-height: 1.2;\n color: #111;\n margin: 0;\n}\n\n/* Pricing */\n.product-pricing {\n display: flex;\n align-items: baseline;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.product-final-price {\n font-size: 26px;\n font-weight: 700;\n color: #111;\n}\n\n.product-original-price {\n font-size: 18px;\n color: #999;\n text-decoration: line-through;\n}\n\n.product-discount-badge {\n font-size: 13px;\n font-weight: 600;\n color: #fff;\n background-color: #e53935;\n padding: 2px 8px;\n border-radius: 4px;\n}\n\n/* Variants */\n.product-variants {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.variant-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.variant-group-label {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n}\n\n.variant-options {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.variant-option-btn {\n padding: 8px 16px;\n border: 1.5px solid #ddd;\n border-radius: 6px;\n background: #fff;\n font-size: 14px;\n cursor: pointer;\n transition: all 0.15s ease;\n color: #333;\n}\n\n.variant-option-btn:hover:not(:disabled) {\n border-color: #111;\n}\n\n.variant-option-btn.selected {\n border-color: #111;\n background-color: #111;\n color: #fff;\n}\n\n.variant-option-btn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n text-decoration: line-through;\n}\n\n/* Actions */\n.product-actions {\n display: flex;\n gap: 12px;\n align-items: stretch;\n}\n\n.add-to-cart-btn {\n flex: 1;\n padding: 14px 24px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background-color: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s ease;\n}\n\n.add-to-cart-btn:hover:not(:disabled) {\n background-color: #333;\n}\n\n.add-to-cart-btn:disabled {\n background-color: #ccc;\n cursor: not-allowed;\n}\n\n.favorite-btn {\n width: 48px;\n height: 48px;\n display: flex;\n align-items: center;\n justify-content: center;\n border: 1.5px solid #ddd;\n border-radius: 8px;\n background: #fff;\n cursor: pointer;\n font-size: 20px;\n transition: all 0.15s ease;\n flex-shrink: 0;\n}\n\n.favorite-btn:hover {\n border-color: #e53935;\n}\n\n.favorite-btn.is-favorite {\n color: #e53935;\n border-color: #e53935;\n}\n\n/* Description */\n.product-description {\n padding-top: 20px;\n border-top: 1px solid #eee;\n}\n\n.out-of-stock-notice {\n font-size: 14px;\n font-weight: 600;\n color: #e53935;\n}\n\n/* Breadcrumb */\n.product-breadcrumb {\n max-width: 1200px;\n margin: 0 auto 16px;\n padding: 0 24px;\n font-size: 13px;\n color: #666;\n}\n\n.product-breadcrumb a {\n color: #666;\n text-decoration: none;\n}\n\n.product-breadcrumb a:hover {\n color: #111;\n}\n\n.breadcrumb-sep {\n margin: 0 8px;\n color: #ccc;\n}\n\n.breadcrumb-current {\n color: #111;\n}\n\n/* Rating */\n.product-rating {\n color: #f5a623;\n font-size: 16px;\n}\n\n.product-rating-value {\n font-size: 14px;\n color: #666;\n margin-left: 8px;\n}\n\n/* Campaigns */\n.product-campaigns {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.product-campaign-badge {\n font-size: 12px;\n font-weight: 600;\n color: #fff;\n background: #1976d2;\n padding: 4px 10px;\n border-radius: 4px;\n}\n\n/* Savings */\n.product-savings {\n font-size: 14px;\n font-weight: 600;\n color: #2e7d32;\n}\n\n/* Quantity */\n.product-quantity {\n display: flex;\n align-items: center;\n gap: 8px;\n border: 1.5px solid #ddd;\n border-radius: 8px;\n overflow: hidden;\n}\n\n.product-quantity button {\n width: 40px;\n height: 48px;\n border: none;\n background: #fff;\n font-size: 18px;\n cursor: pointer;\n}\n\n.product-quantity span {\n width: 32px;\n text-align: center;\n font-size: 15px;\n font-weight: 600;\n}\n\n/* Attributes */\n.product-attributes {\n padding-top: 20px;\n border-top: 1px solid #eee;\n}\n\n.attribute-row {\n display: flex;\n justify-content: space-between;\n padding: 8px 0;\n font-size: 14px;\n border-bottom: 1px solid #f5f5f5;\n}\n\n.attribute-name {\n color: #666;\n font-weight: 500;\n}\n\n.attribute-value {\n color: #111;\n}\n\n/* Bundle Products */\n.product-bundles {\n padding-top: 20px;\n border-top: 1px solid #eee;\n}\n\n.bundle-group {\n margin-top: 12px;\n}\n\n.bundle-items {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.bundle-item {\n padding: 12px;\n border: 1px solid #eee;\n border-radius: 6px;\n font-size: 14px;\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n .product-detail-inner {\n grid-template-columns: 1fr;\n gap: 32px;\n }\n\n .product-name {\n font-size: 22px;\n }\n\n .product-final-price {\n font-size: 22px;\n }\n\n .product-actions {\n flex-wrap: wrap;\n }\n}\n",
|
|
170
|
-
"ikas-config-snippet.json": "{\n \"id\": \"product-detail\",\n \"name\": \"Product Detail\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ProductDetail/index.tsx\",\n \"styles\": \"./src/components/ProductDetail/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": true\n },\n {\n \"name\": \"showFavoriteButton\",\n \"displayName\": \"Show Favorite Button\",\n \"type\": \"BOOLEAN\",\n \"defaultValue\": true\n },\n {\n \"name\": \"addToCartButtonText\",\n \"displayName\": \"Add to Cart Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Add to Cart\"\n }\n ]\n}\n"
|
|
242
|
+
"ikas-config-snippet.json": "{\n \"id\": \"product-detail\",\n \"name\": \"Product Detail\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ProductDetail/index.tsx\",\n \"styles\": \"./src/components/ProductDetail/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": true,\n \"groupId\": \"data\"\n },\n {\n \"name\": \"showFavoriteButton\",\n \"displayName\": \"Show Favorite Button\",\n \"type\": \"BOOLEAN\",\n \"defaultValue\": true,\n \"groupId\": \"content\"\n },\n {\n \"name\": \"addToCartButtonText\",\n \"displayName\": \"Add to Cart Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Add to Cart\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\",\n \"groupId\": \"style\"\n },\n {\n \"name\": \"descriptionTitle\",\n \"displayName\": \"Description Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Description\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"detailsTitle\",\n \"displayName\": \"Details Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Details\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"bundleTitle\",\n \"displayName\": \"Bundle Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Frequently Bought Together\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"outOfStockText\",\n \"displayName\": \"Out of Stock Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Out of Stock\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"saveText\",\n \"displayName\": \"Save Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Save\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n { \"id\": \"data\", \"name\": \"Data\", \"description\": \"Product data source\" },\n { \"id\": \"content\", \"name\": \"Content\", \"description\": \"Text and toggle settings\" },\n { \"id\": \"style\", \"name\": \"Style\", \"description\": \"Colors and visual settings\" },\n { \"id\": \"texts\", \"name\": \"Texts\", \"description\": \"Labels and text content\" }\n ]\n}\n"
|
|
171
243
|
}
|
|
172
244
|
},
|
|
173
245
|
"product-list": {
|
|
174
246
|
"title": "Product List Section",
|
|
175
247
|
"description": "Complete product list section with category breadcrumb (getCategoryPath + getIkasCategoryHref), filter sidebar with handleFilterValueClick and onFilterCategoryClick, sort via setSortType (not direct mutation), search via searchProductList, product grid, and pagination with setProductListVisiblePage. Data is automatically reactive in root components via autorun().",
|
|
176
248
|
"files": {
|
|
177
|
-
"index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n IkasProductList,\n IkasProductListSortType,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n setProductListVisiblePage,\n searchProductList,\n getCategoryPath,\n getIkasCategoryHref,\n isEmpty,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductListSection({\n productList,\n title = \"Products\",\n showFilters = true,\n}: Props) {\n const [searchKeyword, setSearchKeyword] = useState(\"\");\n\n if (!productList) return null;\n\n const products = productList.data ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const sortOptions = getProductListSortOptions(productList);\n const hasNext = hasProductListNextPage(productList);\n const hasPrev = hasProductListPrevPage(productList);\n\n // Category breadcrumb path\n const category = productList.category;\n const categoryPath = category ? getCategoryPath(category) : [];\n\n const handleSort = (value: string) => {\n const selected = sortOptions.find((opt) => opt.value === value);\n if (selected) {\n setSortType(productList, selected.value as IkasProductListSortType);\n }\n };\n\n const handleSearch = (keyword: string) => {\n setSearchKeyword(keyword);\n searchProductList(productList, keyword);\n };\n\n return (\n <section className=\"product-list-section\">\n <div className=\"product-list-inner\">\n {/* Category Breadcrumb */}\n {categoryPath.length > 0 && (\n <nav className=\"product-list-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((cat: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\"> / </span>\n <a href={getIkasCategoryHref(cat)}>{cat.name}</a>\n </span>\n ))}\n </nav>\n )}\n\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">\n {category?.name || title}\n {category?.description && (\n <p className=\"product-list-description\">{category.description}</p>\n )}\n </h1>\n\n <div className=\"product-list-controls\">\n {/* Search */}\n <input\n className=\"product-list-search\"\n type=\"text\"\n placeholder=\"Search products...\"\n value={searchKeyword}\n onInput={(e) => handleSearch((e.target as HTMLInputElement).value)}\n />\n\n {/* Sort — use setSortType instead of direct mutation */}\n {sortOptions.length > 0 && (\n <select\n className=\"product-list-sort\"\n value={sortOptions.find((o) => o.isSelected)?.value ?? \"\"}\n onChange={(e) => handleSort((e.target as HTMLSelectElement).value)}\n >\n {sortOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )}\n </div>\n </div>\n\n <div className=\"product-list-layout\">\n {/* Filter Sidebar — display-type-aware rendering */}\n {showFilters && (productList.filters ?? []).length > 0 && (\n <aside className=\"product-list-filters\">\n {(productList.filters ?? []).map((filter) => {\n const values = getFilterDisplayedValues(filter);\n if (values.length === 0 && !filter.numberRangeListOptions?.length) return null;\n\n return (\n <div key={filter.id} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{filter.name}</h3>\n\n {/* SWATCH: color circles / thumbnail images */}\n {isSwatchFilter(filter) ? (\n <div className=\"filter-swatches\">\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button\n key={fv.name}\n className={`filter-swatch${fv.isSelected === true ? \" selected\" : \"\"}`}\n onClick={() => handleFilterValueClick(productList, filter, fv)}\n title={fv.name}\n >\n {thumbnail ? (\n <img src={getDefaultSrc(thumbnail)} alt={fv.name} />\n ) : (\n <span\n className=\"filter-swatch-color\"\n style={{ backgroundColor: fv.colorCode ?? \"#ccc\" }}\n />\n )}\n </button>\n );\n })}\n </div>\n ) : (\n /* BOX / LIST: checkboxes (default) */\n <div className=\"filter-values\">\n {values.map((fv) => (\n <label key={fv.name} className=\"filter-value\">\n <input\n type=\"checkbox\"\n checked={fv.isSelected === true}\n onChange={() =>\n handleFilterValueClick(productList, filter, fv)\n }\n />\n <span>{fv.name}</span>\n {fv.count != null && (\n <span className=\"filter-count\">({fv.count})</span>\n )}\n </label>\n ))}\n </div>\n )}\n\n {/* NUMBER_RANGE_LIST: predefined range buttons */}\n {filter.numberRangeListOptions?.map((option) => (\n <button\n key={`${option.from}-${option.to}`}\n className={`filter-range-btn${option.isSelected ? \" selected\" : \"\"}`}\n onClick={() => handleNumberRangeOptionClick(productList, filter, option)}\n >\n {option.from} - {option.to ?? \"+\"}\n </button>\n ))}\n </div>\n );\n })}\n\n {/* Category-level filtering */}\n {filterCategories.length > 0 && (\n <div className=\"filter-group\">\n <h3 className=\"filter-group-title\">Categories</h3>\n {filterCategories.map((cat) => (\n <button\n key={cat.name}\n className=\"filter-category-btn\"\n onClick={() => onFilterCategoryClick(productList, cat, true)}\n >\n {cat.name}\n </button>\n ))}\n </div>\n )}\n </aside>\n )}\n\n {/* Product Grid — IkasThemeInfiniteScroller pattern for infinite scroll */}\n <div className=\"product-grid\">\n {isEmpty(products) && (\n <p className=\"product-grid-empty\">No products found.</p>\n )}\n {products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const href = getSelectedProductVariantHref(product);\n\n return (\n <a key={product.id} href={href} className=\"product-card\">\n <div className=\"product-card-image-wrap\">\n {image && (\n <img\n src={getDefaultSrc(image)}\n alt={product.name}\n className=\"product-card-image\"\n />\n )}\n </div>\n <div className=\"product-card-info\">\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </div>\n </a>\n );\n })}\n </div>\n </div>\n\n {/* Pagination — includes setProductListVisiblePage for page jumping */}\n {(hasPrev || hasNext) && (\n <div className=\"product-list-pagination\">\n <button\n className=\"pagination-btn\"\n disabled={!hasPrev}\n onClick={() => getProductListPrevPage(productList)}\n >\n Previous\n </button>\n <span className=\"pagination-info\">\n Page {productList.page}\n </span>\n <button\n className=\"pagination-btn\"\n disabled={!hasNext}\n onClick={() => getProductListNextPage(productList)}\n >\n Next\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
178
|
-
"types.ts": "import { IkasProductList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n productList: IkasProductList;\n title?: string;\n showFilters?: boolean;\n}\n",
|
|
249
|
+
"index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n IkasProductList,\n IkasProductListSortType,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n setProductListVisiblePage,\n searchProductList,\n getCategoryPath,\n getIkasCategoryHref,\n isEmpty,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductListSection({\n productList,\n title = \"Products\",\n showFilters = true,\n backgroundColor = \"#ffffff\",\n emptyMessage = \"No products found.\",\n searchPlaceholder = \"Search products...\",\n categoriesTitle = \"Categories\",\n}: Props) {\n const [searchKeyword, setSearchKeyword] = useState(\"\");\n\n if (!productList) return null;\n\n const products = productList.data ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const sortOptions = getProductListSortOptions(productList);\n const hasNext = hasProductListNextPage(productList);\n const hasPrev = hasProductListPrevPage(productList);\n\n // Category breadcrumb path\n const category = productList.category;\n const categoryPath = category ? getCategoryPath(category) : [];\n\n const handleSort = (value: string) => {\n const selected = sortOptions.find((opt) => opt.value === value);\n if (selected) {\n setSortType(productList, selected.value as IkasProductListSortType);\n }\n };\n\n const handleSearch = (keyword: string) => {\n setSearchKeyword(keyword);\n searchProductList(productList, keyword);\n };\n\n return (\n <section className=\"product-list-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"product-list-inner\">\n {/* Category Breadcrumb */}\n {categoryPath.length > 0 && (\n <nav className=\"product-list-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((cat: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\"> / </span>\n <a href={getIkasCategoryHref(cat)}>{cat.name}</a>\n </span>\n ))}\n </nav>\n )}\n\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">\n {category?.name || title}\n {category?.description && (\n <p className=\"product-list-description\">{category.description}</p>\n )}\n </h1>\n\n <div className=\"product-list-controls\">\n {/* Search */}\n <input\n className=\"product-list-search\"\n type=\"text\"\n placeholder={searchPlaceholder}\n value={searchKeyword}\n onInput={(e) => handleSearch((e.target as HTMLInputElement).value)}\n />\n\n {/* Sort — use setSortType instead of direct mutation */}\n {sortOptions.length > 0 && (\n <select\n className=\"product-list-sort\"\n value={sortOptions.find((o) => o.isSelected)?.value ?? \"\"}\n onChange={(e) => handleSort((e.target as HTMLSelectElement).value)}\n >\n {sortOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )}\n </div>\n </div>\n\n <div className=\"product-list-layout\">\n {/* Filter Sidebar — display-type-aware rendering */}\n {showFilters && (productList.filters ?? []).length > 0 && (\n <aside className=\"product-list-filters\">\n {(productList.filters ?? []).map((filter) => {\n const values = getFilterDisplayedValues(filter);\n if (values.length === 0 && !filter.numberRangeListOptions?.length) return null;\n\n return (\n <div key={filter.id} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{filter.name}</h3>\n\n {/* SWATCH: color circles / thumbnail images */}\n {isSwatchFilter(filter) ? (\n <div className=\"filter-swatches\">\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button\n key={fv.name}\n className={`filter-swatch${fv.isSelected === true ? \" selected\" : \"\"}`}\n onClick={() => handleFilterValueClick(productList, filter, fv)}\n title={fv.name}\n >\n {thumbnail ? (\n <img src={getDefaultSrc(thumbnail)} alt={fv.name} />\n ) : (\n <span\n className=\"filter-swatch-color\"\n style={{ backgroundColor: fv.colorCode ?? \"#ccc\" }}\n />\n )}\n </button>\n );\n })}\n </div>\n ) : (\n /* BOX / LIST: checkboxes (default) */\n <div className=\"filter-values\">\n {values.map((fv) => (\n <label key={fv.name} className=\"filter-value\">\n <input\n type=\"checkbox\"\n checked={fv.isSelected === true}\n onChange={() =>\n handleFilterValueClick(productList, filter, fv)\n }\n />\n <span>{fv.name}</span>\n {fv.count != null && (\n <span className=\"filter-count\">({fv.count})</span>\n )}\n </label>\n ))}\n </div>\n )}\n\n {/* NUMBER_RANGE_LIST: predefined range buttons */}\n {filter.numberRangeListOptions?.map((option) => (\n <button\n key={`${option.from}-${option.to}`}\n className={`filter-range-btn${option.isSelected ? \" selected\" : \"\"}`}\n onClick={() => handleNumberRangeOptionClick(productList, filter, option)}\n >\n {option.from} - {option.to ?? \"+\"}\n </button>\n ))}\n </div>\n );\n })}\n\n {/* Category-level filtering */}\n {filterCategories.length > 0 && (\n <div className=\"filter-group\">\n <h3 className=\"filter-group-title\">{categoriesTitle}</h3>\n {filterCategories.map((cat) => (\n <button\n key={cat.name}\n className=\"filter-category-btn\"\n onClick={() => onFilterCategoryClick(productList, cat, true)}\n >\n {cat.name}\n </button>\n ))}\n </div>\n )}\n </aside>\n )}\n\n {/* Product Grid — IkasThemeInfiniteScroller pattern for infinite scroll */}\n <div className=\"product-grid\">\n {isEmpty(products) && (\n <p className=\"product-grid-empty\">{emptyMessage}</p>\n )}\n {products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const href = getSelectedProductVariantHref(product);\n\n return (\n <a key={product.id} href={href} className=\"product-card\">\n <div className=\"product-card-image-wrap\">\n {image && (\n <img\n src={getDefaultSrc(image)}\n alt={product.name}\n className=\"product-card-image\"\n />\n )}\n </div>\n <div className=\"product-card-info\">\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </div>\n </a>\n );\n })}\n </div>\n </div>\n\n {/* Pagination — includes setProductListVisiblePage for page jumping */}\n {(hasPrev || hasNext) && (\n <div className=\"product-list-pagination\">\n <button\n className=\"pagination-btn\"\n disabled={!hasPrev}\n onClick={() => getProductListPrevPage(productList)}\n >\n Previous\n </button>\n <span className=\"pagination-info\">\n Page {productList.page}\n </span>\n <button\n className=\"pagination-btn\"\n disabled={!hasNext}\n onClick={() => getProductListNextPage(productList)}\n >\n Next\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
250
|
+
"types.ts": "import { IkasProductList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n productList: IkasProductList;\n title?: string;\n showFilters?: boolean;\n backgroundColor?: string;\n emptyMessage?: string;\n searchPlaceholder?: string;\n categoriesTitle?: string;\n}\n",
|
|
179
251
|
"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 {\n font-size: 24px;\n font-weight: 700;\n color: #111;\n margin: 0;\n}\n\n.product-list-sort {\n padding: 8px 12px;\n font-size: 14px;\n border: 1px solid #ddd;\n border-radius: 6px;\n background: #fff;\n cursor: pointer;\n}\n\n.product-list-layout {\n display: flex;\n gap: 32px;\n}\n\n/* Filters */\n.product-list-filters {\n width: 240px;\n flex-shrink: 0;\n}\n\n.filter-group {\n margin-bottom: 24px;\n}\n\n.filter-group-title {\n font-size: 14px;\n font-weight: 600;\n color: #111;\n margin: 0 0 12px 0;\n}\n\n.filter-values {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.filter-value {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 14px;\n color: #555;\n cursor: pointer;\n}\n\n.filter-count {\n color: #999;\n font-size: 12px;\n}\n\n/* Product Grid */\n.product-grid {\n flex: 1;\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 24px;\n}\n\n.product-grid-empty {\n grid-column: 1 / -1;\n text-align: center;\n color: #666;\n padding: 48px 0;\n}\n\n.product-card {\n text-decoration: none;\n color: inherit;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.product-card-image-wrap {\n aspect-ratio: 1;\n overflow: hidden;\n border-radius: 8px;\n background: #f5f5f5;\n}\n\n.product-card-image {\n width: 100%;\n height: 100%;\n object-fit: cover;\n transition: transform 0.2s ease;\n}\n\n.product-card:hover .product-card-image {\n transform: scale(1.03);\n}\n\n.product-card-info {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.product-card-name {\n font-size: 14px;\n font-weight: 500;\n color: #111;\n margin: 0;\n}\n\n.product-card-price {\n font-size: 15px;\n font-weight: 600;\n color: #111;\n}\n\n/* Pagination */\n.product-list-pagination {\n display: flex;\n justify-content: center;\n gap: 12px;\n margin-top: 40px;\n}\n\n.pagination-btn {\n padding: 10px 24px;\n font-size: 14px;\n font-weight: 500;\n background: #fff;\n border: 1px solid #ddd;\n border-radius: 6px;\n cursor: pointer;\n}\n\n.pagination-btn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.pagination-btn:hover:not(:disabled) {\n border-color: #111;\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n .product-list-layout {\n flex-direction: column;\n }\n\n .product-list-filters {\n width: 100%;\n }\n\n .product-grid {\n grid-template-columns: repeat(2, 1fr);\n gap: 16px;\n }\n}\n",
|
|
180
|
-
"ikas-config-snippet.json": "{\n \"id\": \"product-list\",\n \"name\": \"Product List\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ProductList/index.tsx\",\n \"styles\": \"./src/components/ProductList/styles.css\",\n \"props\": [\n {\n \"name\": \"productList\",\n \"displayName\": \"Product List\",\n \"type\": \"PRODUCT_LIST\",\n \"required\": true\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Products\"\n },\n {\n \"name\": \"showFilters\",\n \"displayName\": \"Show Filters\",\n \"type\": \"BOOLEAN\",\n \"defaultValue\": true\n }\n ]\n}\n"
|
|
252
|
+
"ikas-config-snippet.json": "{\n \"id\": \"product-list\",\n \"name\": \"Product List\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ProductList/index.tsx\",\n \"styles\": \"./src/components/ProductList/styles.css\",\n \"props\": [\n {\n \"name\": \"productList\",\n \"displayName\": \"Product List\",\n \"type\": \"PRODUCT_LIST\",\n \"required\": true\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Products\"\n },\n {\n \"name\": \"showFilters\",\n \"displayName\": \"Show Filters\",\n \"type\": \"BOOLEAN\",\n \"defaultValue\": true\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"emptyMessage\",\n \"displayName\": \"Empty Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"No products found.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"searchPlaceholder\",\n \"displayName\": \"Search Placeholder\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Search products...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"categoriesTitle\",\n \"displayName\": \"Categories Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Categories\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n { \"id\": \"texts\", \"name\": \"Texts\", \"description\": \"Labels and text content\" }\n ]\n}\n"
|
|
181
253
|
}
|
|
182
254
|
},
|
|
183
255
|
"product-reviews": {
|
|
184
256
|
"title": "Product Reviews Section",
|
|
185
257
|
"description": "Complete product reviews section with review list display, star ratings, review submission form with login-required check. getProductCustomerReviews supports limit, page, and hasImage params. Review data is automatically reactive in root components via autorun().",
|
|
186
258
|
"files": {
|
|
187
|
-
"index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n IkasProduct,\n getProductCustomerReviews,\n getIkasProductCustomerReviewForm,\n setCustomerReviewFormTitle,\n setCustomerReviewFormStar,\n setCustomerReviewFormComment,\n submitCustomerReviewForm,\n isCustomerReviewLoginRequired,\n getIkasCustomerReviewFormattedDate,\n customerStore,\n hasCustomer,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductReviewsSection({\n product,\n title = \"Customer Reviews\",\n}: Props) {\n const [showForm, setShowForm] = useState(false);\n\n if (!product) return null;\n\n const reviews = getProductCustomerReviews(product) ?? [];\n const reviewForm = getIkasProductCustomerReviewForm(product);\n const loginRequired = isCustomerReviewLoginRequired() as unknown as boolean;\n const isLoggedIn = hasCustomer(customerStore) as unknown as boolean;\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitCustomerReviewForm(reviewForm);\n if (success) {\n setShowForm(false);\n }\n };\n\n const StarDisplay = ({ rating }: { rating: number }) => (\n <div className=\"reviews-stars\">\n {[1, 2, 3, 4, 5].map((star) => (\n <span key={star} className={star <= rating ? \"star-filled\" : \"star-empty\"}>\n ★\n </span>\n ))}\n </div>\n );\n\n return (\n <section className=\"reviews-section\">\n <div className=\"reviews-inner\">\n <div className=\"reviews-header\">\n <h2 className=\"reviews-title\">\n {title} ({reviews.length})\n </h2>\n {!showForm && (\n <button\n className=\"reviews-write-btn\"\n onClick={() => {\n if (loginRequired && !isLoggedIn) {\n Router.navigateToPage(\"LOGIN\");\n } else {\n setShowForm(true);\n }\n }}\n >\n
|
|
188
|
-
"types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n title?: string;\n}\n",
|
|
259
|
+
"index.tsx": "import { useState } from \"preact/hooks\";\nimport {\n IkasProduct,\n getProductCustomerReviews,\n getIkasProductCustomerReviewForm,\n setCustomerReviewFormTitle,\n setCustomerReviewFormStar,\n setCustomerReviewFormComment,\n submitCustomerReviewForm,\n isCustomerReviewLoginRequired,\n getIkasCustomerReviewFormattedDate,\n customerStore,\n hasCustomer,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductReviewsSection({\n product,\n title = \"Customer Reviews\",\n backgroundColor = \"#ffffff\",\n writeReviewText = \"Write a Review\",\n ratingLabel = \"Rating\",\n emptyMessage = \"No reviews yet. Be the first to review!\",\n submitButtonText = \"Submit Review\",\n submittingButtonText = \"Submitting...\",\n cancelButtonText = \"Cancel\",\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) {\n setShowForm(false);\n }\n };\n\n const StarDisplay = ({ rating }: { rating: number }) => (\n <div className=\"reviews-stars\">\n {[1, 2, 3, 4, 5].map((star) => (\n <span key={star} className={star <= rating ? \"star-filled\" : \"star-empty\"}>\n ★\n </span>\n ))}\n </div>\n );\n\n return (\n <section className=\"reviews-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"reviews-inner\">\n <div className=\"reviews-header\">\n <h2 className=\"reviews-title\">\n {title} ({reviews.length})\n </h2>\n {!showForm && (\n <button\n className=\"reviews-write-btn\"\n onClick={() => {\n if (loginRequired && !isLoggedIn) {\n Router.navigateToPage(\"LOGIN\");\n } else {\n setShowForm(true);\n }\n }}\n >\n {writeReviewText}\n </button>\n )}\n </div>\n\n {/* Review Form */}\n {showForm && (\n <form className=\"review-form\" onSubmit={handleSubmit}>\n {reviewForm.isFailure && reviewForm.responseMessage && (\n <div className=\"review-form-error\">{reviewForm.responseMessage}</div>\n )}\n\n <div className=\"review-form-stars\">\n <span className=\"review-form-label\">{ratingLabel}</span>\n <div className=\"star-input\">\n {[1, 2, 3, 4, 5].map((star) => (\n <button\n key={star}\n type=\"button\"\n className={star <= reviewForm.star.value ? \"star-filled\" : \"star-empty\"}\n onClick={() => setCustomerReviewFormStar(reviewForm, star)}\n >\n ★\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"review-form-field\">\n <label className=\"review-form-label\">{reviewForm.title.label}</label>\n <input\n className=\"review-form-input\"\n type=\"text\"\n placeholder={reviewForm.title.placeholder}\n value={reviewForm.title.value}\n onInput={(e) =>\n setCustomerReviewFormTitle(reviewForm, (e.target as HTMLInputElement).value)\n }\n />\n {reviewForm.title.hasError && reviewForm.title.message && (\n <span className=\"review-form-error-text\">{reviewForm.title.message}</span>\n )}\n </div>\n\n <div className=\"review-form-field\">\n <label className=\"review-form-label\">{reviewForm.comment.label}</label>\n <textarea\n className=\"review-form-textarea\"\n placeholder={reviewForm.comment.placeholder}\n value={reviewForm.comment.value}\n rows={4}\n onInput={(e) =>\n setCustomerReviewFormComment(\n reviewForm,\n (e.target as HTMLTextAreaElement).value\n )\n }\n />\n {reviewForm.comment.hasError && reviewForm.comment.message && (\n <span className=\"review-form-error-text\">{reviewForm.comment.message}</span>\n )}\n </div>\n\n <div className=\"review-form-actions\">\n <button\n type=\"submit\"\n className=\"review-form-submit\"\n disabled={reviewForm.isSubmitting}\n >\n {reviewForm.isSubmitting ? submittingButtonText : submitButtonText}\n </button>\n <button\n type=\"button\"\n className=\"review-form-cancel\"\n onClick={() => setShowForm(false)}\n >\n {cancelButtonText}\n </button>\n </div>\n </form>\n )}\n\n {/* Review List */}\n {reviews.length === 0 && !showForm && (\n <p className=\"reviews-empty\">{emptyMessage}</p>\n )}\n\n <div className=\"review-list\">\n {reviews.map((review) => (\n <div key={review.id} className=\"review-card\">\n <div className=\"review-card-header\">\n <StarDisplay rating={review.star} />\n <span className=\"review-card-date\">\n {getIkasCustomerReviewFormattedDate(review)}\n </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\n",
|
|
260
|
+
"types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n title?: string;\n backgroundColor?: string;\n writeReviewText?: string;\n ratingLabel?: string;\n emptyMessage?: string;\n submitButtonText?: string;\n submittingButtonText?: string;\n cancelButtonText?: string;\n}\n",
|
|
189
261
|
"styles.css": ".reviews-section {\n width: 100%;\n padding: 48px 24px;\n border-top: 1px solid #eee;\n}\n\n.reviews-inner {\n max-width: 800px;\n margin: 0 auto;\n}\n\n.reviews-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 24px;\n}\n\n.reviews-title {\n font-size: 22px;\n font-weight: 700;\n color: #111;\n margin: 0;\n}\n\n.reviews-write-btn {\n padding: 10px 20px;\n font-size: 14px;\n font-weight: 600;\n color: #fff;\n background: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n}\n\n.reviews-write-btn:hover {\n background: #333;\n}\n\n.reviews-empty {\n text-align: center;\n color: #666;\n padding: 32px 0;\n font-size: 15px;\n}\n\n/* Stars */\n.reviews-stars,\n.star-input {\n display: flex;\n gap: 2px;\n}\n\n.star-filled {\n color: #f59e0b;\n font-size: 16px;\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n}\n\n.star-empty {\n color: #ddd;\n font-size: 16px;\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n}\n\n.star-input .star-filled,\n.star-input .star-empty {\n font-size: 24px;\n}\n\n/* Review Form */\n.review-form {\n background: #f9fafb;\n border-radius: 12px;\n padding: 24px;\n margin-bottom: 32px;\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.review-form-error {\n padding: 10px 14px;\n font-size: 13px;\n color: #b71c1c;\n background: #ffebee;\n border-radius: 6px;\n}\n\n.review-form-stars {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.review-form-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.review-form-label {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n}\n\n.review-form-input,\n.review-form-textarea {\n padding: 10px 12px;\n font-size: 14px;\n border: 1.5px solid #ddd;\n border-radius: 6px;\n outline: none;\n font-family: inherit;\n}\n\n.review-form-input:focus,\n.review-form-textarea:focus {\n border-color: #111;\n}\n\n.review-form-textarea {\n resize: vertical;\n min-height: 100px;\n}\n\n.review-form-error-text {\n font-size: 12px;\n color: #e53935;\n}\n\n.review-form-actions {\n display: flex;\n gap: 12px;\n}\n\n.review-form-submit {\n padding: 10px 20px;\n font-size: 14px;\n font-weight: 600;\n color: #fff;\n background: #111;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n}\n\n.review-form-submit:disabled {\n background: #ccc;\n cursor: not-allowed;\n}\n\n.review-form-cancel {\n padding: 10px 20px;\n font-size: 14px;\n color: #666;\n background: none;\n border: 1px solid #ddd;\n border-radius: 6px;\n cursor: pointer;\n}\n\n/* Review List */\n.review-list {\n display: flex;\n flex-direction: column;\n gap: 20px;\n}\n\n.review-card {\n padding: 20px 0;\n border-bottom: 1px solid #f0f0f0;\n}\n\n.review-card:last-child {\n border-bottom: none;\n}\n\n.review-card-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n\n.review-card-date {\n font-size: 12px;\n color: #999;\n}\n\n.review-card-title {\n font-size: 15px;\n font-weight: 600;\n color: #111;\n margin: 0 0 6px 0;\n}\n\n.review-card-comment {\n font-size: 14px;\n color: #555;\n line-height: 1.6;\n margin: 0 0 8px 0;\n}\n\n.review-card-author {\n font-size: 13px;\n color: #888;\n font-weight: 500;\n}\n",
|
|
190
|
-
"ikas-config-snippet.json": "{\n \"id\": \"product-reviews\",\n \"name\": \"Product Reviews\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ProductReviews/index.tsx\",\n \"styles\": \"./src/components/ProductReviews/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": true\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Customer Reviews\"\n }\n ]\n}\n"
|
|
262
|
+
"ikas-config-snippet.json": "{\n \"id\": \"product-reviews\",\n \"name\": \"Product Reviews\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ProductReviews/index.tsx\",\n \"styles\": \"./src/components/ProductReviews/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": true\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Customer Reviews\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"writeReviewText\",\n \"displayName\": \"Write Review Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Write a Review\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"ratingLabel\",\n \"displayName\": \"Rating Label\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Rating\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"emptyMessage\",\n \"displayName\": \"Empty Reviews Message\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"No reviews yet. Be the first to review!\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submitButtonText\",\n \"displayName\": \"Submit Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Submit Review\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submittingButtonText\",\n \"displayName\": \"Submitting Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Submitting...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"cancelButtonText\",\n \"displayName\": \"Cancel Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Cancel\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n { \"id\": \"texts\", \"name\": \"Texts\", \"description\": \"Labels and text content\" }\n ]\n}\n"
|
|
191
263
|
}
|
|
192
264
|
},
|
|
193
265
|
"register-page": {
|
|
194
266
|
"title": "Register Section",
|
|
195
267
|
"description": "Complete registration section with first name, last name, email, password, marketing consent (setRegisterFormIsMarketingAccepted), membership agreement (setRegisterFormIsMembershipAgreementAccepted), and social login (socialLogin + SocialLoginProvider). Uses navigateToPage for navigation.",
|
|
196
268
|
"files": {
|
|
197
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getRegisterForm,\n initRegisterForm,\n setRegisterFormEmail,\n setRegisterFormFirstName,\n setRegisterFormLastName,\n setRegisterFormPassword,\n submitRegisterForm,\n setRegisterFormIsMarketingAccepted,\n setRegisterFormIsMembershipAgreementAccepted,\n socialLogin,\n navigateToPage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function RegisterSection({\n redirectAfterRegister = \"/account\",\n}: Props) {\n const registerForm = getRegisterForm(customerStore);\n\n useEffect(() => {\n initRegisterForm(registerForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitRegisterForm(registerForm);\n if (success) {\n navigateToPage(\"ACCOUNT\");\n }\n };\n\n const handleSocialRegister = async (provider: \"GOOGLE\" | \"FACEBOOK\" | \"APPLE\") => {\n await socialLogin(customerStore, provider as any);\n };\n\n return (\n <section className=\"register-section\">\n <div className=\"register-inner\">\n <h1 className=\"register-title\">
|
|
198
|
-
"types.ts": "export interface Props {\n redirectAfterRegister?: string;\n}\n",
|
|
269
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getRegisterForm,\n initRegisterForm,\n setRegisterFormEmail,\n setRegisterFormFirstName,\n setRegisterFormLastName,\n setRegisterFormPassword,\n submitRegisterForm,\n setRegisterFormIsMarketingAccepted,\n setRegisterFormIsMembershipAgreementAccepted,\n socialLogin,\n navigateToPage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function RegisterSection({\n redirectAfterRegister = \"/account\",\n backgroundColor = \"#ffffff\",\n title = \"Create Account\",\n googleButtonText = \"Continue with Google\",\n facebookButtonText = \"Continue with Facebook\",\n dividerText = \"or\",\n marketingConsentText = \"I want to receive marketing emails and promotions\",\n agreementConsentText = \"I accept the membership agreement\",\n submitButtonText = \"Create Account\",\n submittingButtonText = \"Creating account...\",\n hasAccountText = \"Already have an account?\",\n signInLinkText = \"Sign in\",\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) {\n navigateToPage(\"ACCOUNT\");\n }\n };\n\n const handleSocialRegister = async (provider: \"GOOGLE\" | \"FACEBOOK\" | \"APPLE\") => {\n await socialLogin(customerStore, provider as any);\n };\n\n return (\n <section className=\"register-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"register-inner\">\n <h1 className=\"register-title\">{title}</h1>\n\n {registerForm.isFailure && registerForm.responseMessage && (\n <div className=\"register-error-banner\">{registerForm.responseMessage}</div>\n )}\n\n {/* Social Login Buttons */}\n <div className=\"register-social\">\n <button className=\"register-social-btn\" onClick={() => handleSocialRegister(\"GOOGLE\")}>\n {googleButtonText}\n </button>\n <button className=\"register-social-btn\" onClick={() => handleSocialRegister(\"FACEBOOK\")}>\n {facebookButtonText}\n </button>\n </div>\n\n <div className=\"register-divider\"><span>{dividerText}</span></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) =>\n setRegisterFormFirstName(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.firstName.hasError && registerForm.firstName.message && (\n <span className=\"register-field-error\">{registerForm.firstName.message}</span>\n )}\n </div>\n\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) =>\n setRegisterFormLastName(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.lastName.hasError && registerForm.lastName.message && (\n <span className=\"register-field-error\">{registerForm.lastName.message}</span>\n )}\n </div>\n </div>\n\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) =>\n setRegisterFormEmail(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.email.hasError && registerForm.email.message && (\n <span className=\"register-field-error\">{registerForm.email.message}</span>\n )}\n </div>\n\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) =>\n setRegisterFormPassword(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.password.hasError && registerForm.password.message && (\n <span className=\"register-field-error\">{registerForm.password.message}</span>\n )}\n </div>\n\n {/* Marketing Consent — IkasFormItemBoolean */}\n <label className=\"register-checkbox\">\n <input\n type=\"checkbox\"\n checked={registerForm.isMarketingAccepted?.value ?? false}\n onChange={(e) =>\n setRegisterFormIsMarketingAccepted(registerForm, (e.target as HTMLInputElement).checked)\n }\n />\n <span>{marketingConsentText}</span>\n </label>\n\n {/* Membership Agreement — IkasFormItemBoolean */}\n <label className=\"register-checkbox\">\n <input\n type=\"checkbox\"\n checked={registerForm.isMembershipAgreementAccepted?.value ?? false}\n onChange={(e) =>\n setRegisterFormIsMembershipAgreementAccepted(registerForm, (e.target as HTMLInputElement).checked)\n }\n />\n <span>{agreementConsentText}</span>\n </label>\n\n <button\n className=\"register-submit-btn\"\n type=\"submit\"\n disabled={registerForm.isSubmitting}\n >\n {registerForm.isSubmitting ? submittingButtonText : submitButtonText}\n </button>\n </form>\n\n <p className=\"register-login-link\">\n {hasAccountText}{\" \"}\n <a\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n navigateToPage(\"LOGIN\");\n }}\n >\n {signInLinkText}\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
|
|
270
|
+
"types.ts": "export interface Props {\n redirectAfterRegister?: string;\n backgroundColor?: string;\n title?: string;\n googleButtonText?: string;\n facebookButtonText?: string;\n dividerText?: string;\n marketingConsentText?: string;\n agreementConsentText?: string;\n submitButtonText?: string;\n submittingButtonText?: string;\n hasAccountText?: string;\n signInLinkText?: string;\n}\n",
|
|
199
271
|
"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 {\n font-size: 28px;\n font-weight: 700;\n color: #111;\n margin: 0 0 24px 0;\n text-align: center;\n}\n\n.register-error-banner {\n padding: 12px 16px;\n font-size: 14px;\n color: #b71c1c;\n background-color: #ffebee;\n border-radius: 8px;\n margin-bottom: 20px;\n}\n\n.register-form {\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n.register-row {\n display: flex;\n gap: 12px;\n}\n\n.register-row .register-field {\n flex: 1;\n}\n\n.register-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.register-label {\n font-size: 14px;\n font-weight: 600;\n color: #333;\n}\n\n.register-input {\n padding: 12px 14px;\n font-size: 15px;\n border: 1.5px solid #ddd;\n border-radius: 8px;\n outline: none;\n transition: border-color 0.15s ease;\n}\n\n.register-input:focus {\n border-color: #111;\n}\n\n.register-input.has-error {\n border-color: #e53935;\n}\n\n.register-field-error {\n font-size: 12px;\n color: #e53935;\n}\n\n.register-submit-btn {\n padding: 14px 24px;\n font-size: 16px;\n font-weight: 600;\n color: #fff;\n background-color: #111;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n transition: background-color 0.15s ease;\n margin-top: 8px;\n}\n\n.register-submit-btn:hover:not(:disabled) {\n background-color: #333;\n}\n\n.register-submit-btn:disabled {\n background-color: #ccc;\n cursor: not-allowed;\n}\n\n.register-login-link {\n font-size: 14px;\n color: #666;\n text-align: center;\n margin-top: 24px;\n}\n\n.register-login-link a {\n color: #111;\n font-weight: 600;\n text-decoration: none;\n}\n\n.register-login-link a:hover {\n text-decoration: underline;\n}\n\n@media (max-width: 480px) {\n .register-row {\n flex-direction: column;\n }\n}\n",
|
|
200
|
-
"ikas-config-snippet.json": "{\n \"id\": \"register-page\",\n \"name\": \"Register Page\",\n \"type\": \"section\",\n \"entry\": \"./src/components/RegisterSection/index.tsx\",\n \"styles\": \"./src/components/RegisterSection/styles.css\",\n \"props\": [\n {\n \"name\": \"redirectAfterRegister\",\n \"displayName\": \"Redirect After Register\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"/account\"\n }\n ]\n}\n"
|
|
272
|
+
"ikas-config-snippet.json": "{\n \"id\": \"register-page\",\n \"name\": \"Register Page\",\n \"type\": \"section\",\n \"entry\": \"./src/components/RegisterSection/index.tsx\",\n \"styles\": \"./src/components/RegisterSection/styles.css\",\n \"props\": [\n {\n \"name\": \"redirectAfterRegister\",\n \"displayName\": \"Redirect After Register\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"/account\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Create Account\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"googleButtonText\",\n \"displayName\": \"Google Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Continue with Google\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"facebookButtonText\",\n \"displayName\": \"Facebook Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Continue with Facebook\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"dividerText\",\n \"displayName\": \"Divider Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"or\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"marketingConsentText\",\n \"displayName\": \"Marketing Consent Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"I want to receive marketing emails and promotions\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"agreementConsentText\",\n \"displayName\": \"Agreement Consent Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"I accept the membership agreement\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submitButtonText\",\n \"displayName\": \"Submit Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Create Account\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submittingButtonText\",\n \"displayName\": \"Submitting Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Creating account...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"hasAccountText\",\n \"displayName\": \"Has Account Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Already have an account?\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"signInLinkText\",\n \"displayName\": \"Sign In Link Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Sign in\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\"\n }\n ]\n}\n"
|
|
201
273
|
}
|
|
202
274
|
},
|
|
203
275
|
"reset-password": {
|
|
204
276
|
"title": "Reset Password Section",
|
|
205
277
|
"description": "Complete reset password section with new password and confirm password fields. Uses getRecoverPasswordForm/initRecoverPasswordForm/setRecoverPasswordFormPassword/setRecoverPasswordFormPasswordAgain/submitRecoverPasswordForm pattern.",
|
|
206
278
|
"files": {
|
|
207
|
-
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getRecoverPasswordForm,\n initRecoverPasswordForm,\n setRecoverPasswordFormPassword,\n setRecoverPasswordFormPasswordAgain,\n submitRecoverPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\n\nexport default function ResetPasswordSection() {\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) {\n Router.navigateToPage(\"LOGIN\");\n }\n };\n\n return (\n <section className=\"reset-section\">\n <div className=\"reset-inner\">\n <h1 className=\"reset-title\">
|
|
208
|
-
"types.ts": "export interface Props {}\n",
|
|
279
|
+
"index.tsx": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getRecoverPasswordForm,\n initRecoverPasswordForm,\n setRecoverPasswordFormPassword,\n setRecoverPasswordFormPasswordAgain,\n submitRecoverPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ResetPasswordSection({\n backgroundColor = \"#ffffff\",\n title = \"Set New Password\",\n subtitle = \"Enter your new password below.\",\n successMessageText = \"Password has been reset successfully!\",\n submitButtonText = \"Reset Password\",\n submittingButtonText = \"Resetting...\",\n backToLoginText = \"Back to Sign In\",\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) {\n Router.navigateToPage(\"LOGIN\");\n }\n };\n\n return (\n <section className=\"reset-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"reset-inner\">\n <h1 className=\"reset-title\">{title}</h1>\n <p className=\"reset-subtitle\">{subtitle}</p>\n\n {recoverForm.isSuccess && (\n <div className=\"reset-success-banner\">{successMessageText}</div>\n )}\n {recoverForm.isFailure && recoverForm.responseMessage && (\n <div className=\"reset-error-banner\">{recoverForm.responseMessage}</div>\n )}\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) =>\n setRecoverPasswordFormPassword(recoverForm, (e.target as HTMLInputElement).value)\n }\n />\n {recoverForm.password.hasError && recoverForm.password.message && (\n <span className=\"reset-field-error\">{recoverForm.password.message}</span>\n )}\n </div>\n\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) =>\n setRecoverPasswordFormPasswordAgain(recoverForm, (e.target as HTMLInputElement).value)\n }\n />\n {recoverForm.passwordAgain.hasError && recoverForm.passwordAgain.message && (\n <span className=\"reset-field-error\">{recoverForm.passwordAgain.message}</span>\n )}\n </div>\n\n <button className=\"reset-submit-btn\" type=\"submit\" disabled={recoverForm.isSubmitting}>\n {recoverForm.isSubmitting ? submittingButtonText : submitButtonText}\n </button>\n </form>\n )}\n\n <p className=\"reset-back-link\">\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"LOGIN\"); }}>\n {backToLoginText}\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
|
|
280
|
+
"types.ts": "export interface Props {\n backgroundColor?: string;\n title?: string;\n subtitle?: string;\n successMessageText?: string;\n submitButtonText?: string;\n submittingButtonText?: string;\n backToLoginText?: string;\n}\n",
|
|
209
281
|
"styles.css": ".reset-section { width: 100%; padding: 64px 24px; }\n.reset-inner { max-width: 400px; margin: 0 auto; }\n.reset-title { font-size: 28px; font-weight: 700; color: #111; margin: 0 0 8px 0; text-align: center; }\n.reset-subtitle { font-size: 15px; color: #666; text-align: center; margin: 0 0 24px 0; }\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",
|
|
210
|
-
"ikas-config-snippet.json": "{\n \"id\": \"reset-password\",\n \"name\": \"Reset Password\",\n \"type\": \"section\",\n \"props\": []\n}\n"
|
|
282
|
+
"ikas-config-snippet.json": "{\n \"id\": \"reset-password\",\n \"name\": \"Reset Password\",\n \"type\": \"section\",\n \"props\": [\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Set New Password\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"subtitle\",\n \"displayName\": \"Subtitle\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Enter your new password below.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"successMessageText\",\n \"displayName\": \"Success Message Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Password has been reset successfully!\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submitButtonText\",\n \"displayName\": \"Submit Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Reset Password\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submittingButtonText\",\n \"displayName\": \"Submitting Button Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Resetting...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"backToLoginText\",\n \"displayName\": \"Back To Login Text\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"Back to Sign In\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\"\n }\n ]\n}\n"
|
|
211
283
|
}
|
|
212
284
|
}
|
|
213
285
|
}
|