@ikas/code-components-mcp 0.34.0 → 0.35.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 +5 -5
- package/data/section-templates.json +62 -62
- package/data/storefront-api.json +82 -82
- package/data/storefront-types.json +1 -1
- package/package.json +1 -1
|
@@ -4,130 +4,130 @@
|
|
|
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\">My Addresses</h1>\n <button className=\"addresses-add-btn\" onClick={handleAddNew}>Add Address</button>\n </div>\n\n {showForm && <AddressFormComponent address={editingAddress} onDone={() => setShowForm(false)} />}\n\n {isEmpty(addresses) && !showForm && (\n <p className=\"addresses-empty\">No addresses saved yet.</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)}>Edit</button>\n <button className=\"address-delete-btn\" onClick={() => handleDelete(addr)}>Delete</button>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </section>\n );\n}\n\nfunction AddressFormComponent({ address, onDone }: { address?: any; onDone: () => void }) {\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>Title</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>First Name</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>Last Name</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>Phone</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>Address Line 1</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>Address Line 2</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>City</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>District</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>Postal Code</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>Country</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 ? \"Saving...\" : \"Save Address\"}\n </button>\n <button type=\"button\" onClick={onDone}>Cancel</button>\n </div>\n </form>\n );\n}\n\n",
|
|
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({ backgroundColor = \"#ffffff\" }: 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\">My Addresses</h1>\n <button className=\"addresses-add-btn\" onClick={handleAddNew}>Add Address</button>\n </div>\n\n {showForm && <AddressFormComponent address={editingAddress} onDone={() => setShowForm(false)} />}\n\n {isEmpty(addresses) && !showForm && (\n <p className=\"addresses-empty\">No addresses saved yet.</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)}>Edit</button>\n <button className=\"address-delete-btn\" onClick={() => handleDelete(addr)}>Delete</button>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </section>\n );\n}\n\nfunction AddressFormComponent({ address, onDone }: { address?: any; onDone: () => void }) {\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>Title</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>First Name</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>Last Name</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>Phone</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>Address Line 1</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>Address Line 2</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>City</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>District</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>Postal Code</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>Country</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 ? \"Saving...\" : \"Save Address\"}\n </button>\n <button type=\"button\" onClick={onDone}>Cancel</button>\n </div>\n </form>\n );\n}\n\n",
|
|
18
|
+
"types.ts": "export interface Props {\n title?: string;\n backgroundColor?: 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\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"defaultValue\": \"My Addresses\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\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\">Account Information</h1>\n\n {accountForm.isSuccess && (\n <div className=\"account-info-success\">Your information has been updated.</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 ? \"Saving...\" : \"Save Changes\"}\n </button>\n </form>\n </div>\n </section>\n );\n}\n\n",
|
|
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({ backgroundColor = \"#ffffff\" }: 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\">Account Information</h1>\n\n {accountForm.isSuccess && (\n <div className=\"account-info-success\">Your information has been updated.</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 ? \"Saving...\" : \"Save Changes\"}\n </button>\n </form>\n </div>\n </section>\n );\n}\n\n",
|
|
28
|
+
"types.ts": "export interface Props {\n backgroundColor?: 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}\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 Start Shopping\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)} items\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}\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}: 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 Start Shopping\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)} items\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}\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\": \"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 \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"blog-list": {
|
|
44
44
|
"title": "Blog List Section",
|
|
45
45
|
"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
46
|
"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\">No blog posts found.</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\">Read more</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 Load More\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
48
|
-
"types.ts": "import { IkasBlogList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogList: IkasBlogList;\n title?: string;\n}\n",
|
|
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 backgroundColor = \"#ffffff\",\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\">No blog posts found.</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\">Read more</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 Load More\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
48
|
+
"types.ts": "import { IkasBlogList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogList: IkasBlogList;\n title?: string;\n backgroundColor?: string;\n}\n",
|
|
49
49
|
"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"
|
|
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 \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
"blog-post": {
|
|
54
54
|
"title": "Blog Post Section",
|
|
55
55
|
"description": "Single blog post detail page with image, title, date, and HTML content.",
|
|
56
56
|
"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",
|
|
57
|
+
"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",
|
|
58
|
+
"types.ts": "import { IkasBlog } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogPost: IkasBlog;\n backgroundColor?: string;\n}\n",
|
|
59
59
|
"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 {
|
|
60
|
+
"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"
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
"cart-page": {
|
|
64
64
|
"title": "Cart Section",
|
|
65
65
|
"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
66
|
"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({ emptyCartMessage = \"Your cart is empty\" }: 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\">\n <div className=\"cart-inner\">\n <p className=\"cart-loading\">Loading cart...</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 Continue Shopping\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\">Shopping Cart ({totalItemCount} items)</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 Remove\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>Subtotal</span>\n <span>{getIkasOrderFormattedTotalPrice(cart!)}</span>\n </div>\n <div className=\"cart-summary-row cart-summary-total\">\n <span>Total</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(cart!)}</span>\n </div>\n <a\n className=\"cart-checkout-btn\"\n href={getCheckoutUrlFromCartStore(cartStore)}\n >\n Proceed to Checkout\n </a>\n </div>\n </div>\n </section>\n );\n}\n\n",
|
|
68
|
-
"types.ts": "export interface Props {\n emptyCartMessage?: string;\n}\n",
|
|
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({ emptyCartMessage = \"Your cart is empty\" ,\nbackgroundColor = \"#ffffff\",\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\">Loading cart...</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 Continue Shopping\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\">Shopping Cart ({totalItemCount} items)</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 Remove\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>Subtotal</span>\n <span>{getIkasOrderFormattedTotalPrice(cart!)}</span>\n </div>\n <div className=\"cart-summary-row cart-summary-total\">\n <span>Total</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(cart!)}</span>\n </div>\n <a\n className=\"cart-checkout-btn\"\n href={getCheckoutUrlFromCartStore(cartStore)}\n >\n Proceed to Checkout\n </a>\n </div>\n </div>\n </section>\n );\n}\n\n",
|
|
68
|
+
"types.ts": "export interface Props {\n emptyCartMessage?: string;\n backgroundColor?: string;\n}\n",
|
|
69
69
|
"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"
|
|
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 \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
73
|
"contact-form": {
|
|
74
74
|
"title": "Contact Form Section",
|
|
75
75
|
"description": "Complete contact form section with name, email, phone, and message fields. Uses the initContactForm/setContactForm*/submitContactForm pattern with validation and success state.",
|
|
76
76
|
"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 ? \"Sending...\" : \"Send Message\"}\n </button>\n </form>\n </div>\n </section>\n );\n}\n\n",
|
|
78
|
-
"types.ts": "export interface Props {\n title?: string;\n successMessage?: string;\n}\n",
|
|
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 backgroundColor = \"#ffffff\",\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 ? \"Sending...\" : \"Send Message\"}\n </button>\n </form>\n </div>\n </section>\n );\n}\n\n",
|
|
78
|
+
"types.ts": "export interface Props {\n title?: string;\n successMessage?: string;\n backgroundColor?: string;\n}\n",
|
|
79
79
|
"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"
|
|
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 \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"faq": {
|
|
84
84
|
"title": "FAQ Accordion Section",
|
|
85
85
|
"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
86
|
"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\">No questions added yet.</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",
|
|
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",
|
|
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 backgroundColor = \"#ffffff\",\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\">No questions added yet.</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",
|
|
88
|
+
"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}\n",
|
|
89
89
|
"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"
|
|
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 \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
93
|
"favorites-page": {
|
|
94
94
|
"title": "Favorites Page Section",
|
|
95
95
|
"description": "Complete favorites page with getFavoriteProducts loading, product cards with pricing/images/links, and remove functionality.",
|
|
96
96
|
"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>Loading...</p></div></section>;\n }\n\n return (\n <section className=\"favorites-page\">\n <div className=\"favorites-inner\">\n <h1 className=\"favorites-title\">My Favorites</h1>\n\n {favorites.length === 0 && <p className=\"favorites-empty\">No favorites yet.</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)}>Remove</button>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n </section>\n );\n}\n\n",
|
|
98
|
-
"types.ts": "export interface Props {}\n",
|
|
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\";\nimport { Props } from \"./types\";\n\nexport default function FavoritesPageSection({ backgroundColor = \"#ffffff\" }: 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>Loading...</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\">My Favorites</h1>\n\n {favorites.length === 0 && <p className=\"favorites-empty\">No favorites yet.</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)}>Remove</button>\n </div>\n </div>\n );\n })}\n </div>\n </div>\n </section>\n );\n}\n\n",
|
|
98
|
+
"types.ts": "export interface Props {\n backgroundColor?: string;\n}\n",
|
|
99
99
|
"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": "{
|
|
100
|
+
"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}\n"
|
|
101
101
|
}
|
|
102
102
|
},
|
|
103
103
|
"footer": {
|
|
104
104
|
"title": "Footer Section",
|
|
105
105
|
"description": "Complete footer section with logo, navigation link columns, contact info, social media links, and copyright. Uses IkasNavigationLink for editable links.",
|
|
106
106
|
"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 || \"Logo\"} className=\"footer-logo\" />\n ) : (\n <span className=\"footer-logo-text\">Store</span>\n )}\n {description && <p className=\"footer-description\">{description}</p>}\n </div>\n\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",
|
|
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"
|
|
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 backgroundColor = \"#f9fafb\",\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 || \"Logo\"} className=\"footer-logo\" />\n ) : (\n <span className=\"footer-logo-text\">Store</span>\n )}\n {description && <p className=\"footer-description\">{description}</p>}\n </div>\n\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",
|
|
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 backgroundColor?: string;\n}\n",
|
|
109
|
+
"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",
|
|
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 \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#f9fafb\"\n }\n ]\n}\n"
|
|
111
111
|
}
|
|
112
112
|
},
|
|
113
113
|
"forgot-password": {
|
|
114
114
|
"title": "Forgot Password Section",
|
|
115
115
|
"description": "Complete forgot password section with email input for password recovery. Uses the initForgotPasswordForm/setForgotPasswordFormEmail/submitForgotPasswordForm pattern with success and error states.",
|
|
116
116
|
"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\">Forgot Password</h1>\n <p className=\"forgot-subtitle\">\n Enter your email address and we'll send you a link to reset your password.\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 ? \"Sending...\" : \"Send Reset Link\"}\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 Back to Sign In\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
|
|
118
|
-
"types.ts": "export interface Props {\n successMessage?: string;\n}\n",
|
|
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 backgroundColor = \"#ffffff\",\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\">Forgot Password</h1>\n <p className=\"forgot-subtitle\">\n Enter your email address and we'll send you a link to reset your password.\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 ? \"Sending...\" : \"Send Reset Link\"}\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 Back to Sign In\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
|
|
118
|
+
"types.ts": "export interface Props {\n successMessage?: string;\n backgroundColor?: string;\n}\n",
|
|
119
119
|
"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"
|
|
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 \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
121
121
|
}
|
|
122
122
|
},
|
|
123
123
|
"header": {
|
|
124
124
|
"title": "Header Section",
|
|
125
125
|
"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
126
|
"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 || \"Store Logo\"} className=\"header-logo-img\" />\n ) : (\n <span className=\"header-logo-text\">Store</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",
|
|
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",
|
|
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 backgroundColor = \"#ffffff\",\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 || \"Store Logo\"} className=\"header-logo-img\" />\n ) : (\n <span className=\"header-logo-text\">Store</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",
|
|
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 backgroundColor?: string;\n}\n",
|
|
129
129
|
"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"
|
|
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 \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
133
|
"hero-banner": {
|
|
@@ -144,71 +144,71 @@
|
|
|
144
144
|
"title": "Login Section",
|
|
145
145
|
"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
146
|
"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\">Sign In</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 Continue with Google\n </button>\n <button className=\"login-social-btn\" onClick={() => handleSocialLoginClick(\"FACEBOOK\")}>\n Continue with Facebook\n </button>\n </div>\n\n <div className=\"login-divider\"><span>or</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 Forgot password?\n </a>\n )}\n\n <button\n className=\"login-submit-btn\"\n type=\"submit\"\n disabled={loginForm.isSubmitting}\n >\n {loginForm.isSubmitting ? \"Signing in...\" : \"Sign In\"}\n </button>\n </form>\n\n <p className=\"login-register-link\">\n Don't have an account?{\" \"}\n <a\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n Router.navigateToPage(\"REGISTER\");\n }}\n >\n Create one\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
|
|
148
|
-
"types.ts": "export interface Props {\n redirectAfterLogin?: string;\n showForgotPassword?: boolean;\n}\n",
|
|
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 backgroundColor = \"#ffffff\",\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\">Sign In</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 Continue with Google\n </button>\n <button className=\"login-social-btn\" onClick={() => handleSocialLoginClick(\"FACEBOOK\")}>\n Continue with Facebook\n </button>\n </div>\n\n <div className=\"login-divider\"><span>or</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 Forgot password?\n </a>\n )}\n\n <button\n className=\"login-submit-btn\"\n type=\"submit\"\n disabled={loginForm.isSubmitting}\n >\n {loginForm.isSubmitting ? \"Signing in...\" : \"Sign In\"}\n </button>\n </form>\n\n <p className=\"login-register-link\">\n Don't have an account?{\" \"}\n <a\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n Router.navigateToPage(\"REGISTER\");\n }}\n >\n Create one\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
|
|
148
|
+
"types.ts": "export interface Props {\n redirectAfterLogin?: string;\n showForgotPassword?: boolean;\n backgroundColor?: string;\n}\n",
|
|
149
149
|
"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"
|
|
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 \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
151
151
|
}
|
|
152
152
|
},
|
|
153
153
|
"order-detail": {
|
|
154
154
|
"title": "Order Detail Section",
|
|
155
155
|
"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
156
|
"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>Loading...</p></div></div>;\n if (!order) return <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>Order not found.</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\">\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>Shipping: {getIkasOrderFormattedShippingTotal(order)}</div>\n <div>Tax: {getIkasOrderFormattedTotalTax(order)}</div>\n <div>Subtotal: {getIkasOrderFormattedTotalPrice(order)}</div>\n <div style={{ fontWeight: 700, fontSize: 18 }}>Total: {getIkasOrderFormattedTotalFinalPrice(order)}</div>\n </div>\n\n {canRefund && (\n <div style={{ marginTop: 24 }}>\n <h3>Refund</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 }}>Submit Refund</button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
158
|
-
"types.ts": "export interface Props {}\n",
|
|
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\";\nimport { Props } from \"./types\";\n\nexport default function OrderDetailSection({ backgroundColor = \"#ffffff\" }: 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>Loading...</p></div></div>;\n if (!order) return <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>Order not found.</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>Shipping: {getIkasOrderFormattedShippingTotal(order)}</div>\n <div>Tax: {getIkasOrderFormattedTotalTax(order)}</div>\n <div>Subtotal: {getIkasOrderFormattedTotalPrice(order)}</div>\n <div style={{ fontWeight: 700, fontSize: 18 }}>Total: {getIkasOrderFormattedTotalFinalPrice(order)}</div>\n </div>\n\n {canRefund && (\n <div style={{ marginTop: 24 }}>\n <h3>Refund</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 }}>Submit Refund</button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
158
|
+
"types.ts": "export interface Props {\n backgroundColor?: string;\n}\n",
|
|
159
159
|
"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": "{
|
|
160
|
+
"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}\n"
|
|
161
161
|
}
|
|
162
162
|
},
|
|
163
163
|
"product-detail": {
|
|
164
164
|
"title": "Product Detail Section",
|
|
165
165
|
"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
166
|
"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",
|
|
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 backgroundColor = \"#ffffff\",\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\">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 backgroundColor?: string;\n}\n",
|
|
169
169
|
"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"
|
|
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 \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
171
171
|
}
|
|
172
172
|
},
|
|
173
173
|
"product-list": {
|
|
174
174
|
"title": "Product List Section",
|
|
175
175
|
"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
176
|
"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",
|
|
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 backgroundColor = \"#ffffff\",\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=\"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 backgroundColor?: string;\n}\n",
|
|
179
179
|
"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"
|
|
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 \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
181
181
|
}
|
|
182
182
|
},
|
|
183
183
|
"product-reviews": {
|
|
184
184
|
"title": "Product Reviews Section",
|
|
185
185
|
"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
186
|
"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 Write a Review\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\">Rating</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 ? \"Submitting...\" : \"Submit Review\"}\n </button>\n <button\n type=\"button\"\n className=\"review-form-cancel\"\n onClick={() => setShowForm(false)}\n >\n Cancel\n </button>\n </div>\n </form>\n )}\n\n {/* Review List */}\n {reviews.length === 0 && !showForm && (\n <p className=\"reviews-empty\">No reviews yet. Be the first to review!</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",
|
|
188
|
-
"types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n title?: string;\n}\n",
|
|
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 backgroundColor = \"#ffffff\",\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 Write a Review\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\">Rating</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 ? \"Submitting...\" : \"Submit Review\"}\n </button>\n <button\n type=\"button\"\n className=\"review-form-cancel\"\n onClick={() => setShowForm(false)}\n >\n Cancel\n </button>\n </div>\n </form>\n )}\n\n {/* Review List */}\n {reviews.length === 0 && !showForm && (\n <p className=\"reviews-empty\">No reviews yet. Be the first to review!</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",
|
|
188
|
+
"types.ts": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n title?: string;\n backgroundColor?: string;\n}\n",
|
|
189
189
|
"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"
|
|
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 \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
191
191
|
}
|
|
192
192
|
},
|
|
193
193
|
"register-page": {
|
|
194
194
|
"title": "Register Section",
|
|
195
195
|
"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
196
|
"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\">Create Account</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 Continue with Google\n </button>\n <button className=\"register-social-btn\" onClick={() => handleSocialRegister(\"FACEBOOK\")}>\n Continue with Facebook\n </button>\n </div>\n\n <div className=\"register-divider\"><span>or</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>I want to receive marketing emails and promotions</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>I accept the membership agreement</span>\n </label>\n\n <button\n className=\"register-submit-btn\"\n type=\"submit\"\n disabled={registerForm.isSubmitting}\n >\n {registerForm.isSubmitting ? \"Creating account...\" : \"Create Account\"}\n </button>\n </form>\n\n <p className=\"register-login-link\">\n Already have an account?{\" \"}\n <a\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n navigateToPage(\"LOGIN\");\n }}\n >\n Sign in\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
|
|
198
|
-
"types.ts": "export interface Props {\n redirectAfterRegister?: string;\n}\n",
|
|
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 backgroundColor = \"#ffffff\",\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\">Create Account</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 Continue with Google\n </button>\n <button className=\"register-social-btn\" onClick={() => handleSocialRegister(\"FACEBOOK\")}>\n Continue with Facebook\n </button>\n </div>\n\n <div className=\"register-divider\"><span>or</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>I want to receive marketing emails and promotions</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>I accept the membership agreement</span>\n </label>\n\n <button\n className=\"register-submit-btn\"\n type=\"submit\"\n disabled={registerForm.isSubmitting}\n >\n {registerForm.isSubmitting ? \"Creating account...\" : \"Create Account\"}\n </button>\n </form>\n\n <p className=\"register-login-link\">\n Already have an account?{\" \"}\n <a\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n navigateToPage(\"LOGIN\");\n }}\n >\n Sign in\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
|
|
198
|
+
"types.ts": "export interface Props {\n redirectAfterRegister?: string;\n backgroundColor?: string;\n}\n",
|
|
199
199
|
"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"
|
|
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 \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"defaultValue\": \"#ffffff\"\n }\n ]\n}\n"
|
|
201
201
|
}
|
|
202
202
|
},
|
|
203
203
|
"reset-password": {
|
|
204
204
|
"title": "Reset Password Section",
|
|
205
205
|
"description": "Complete reset password section with new password and confirm password fields. Uses getRecoverPasswordForm/initRecoverPasswordForm/setRecoverPasswordFormPassword/setRecoverPasswordFormPasswordAgain/submitRecoverPasswordForm pattern.",
|
|
206
206
|
"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\">Set New Password</h1>\n <p className=\"reset-subtitle\">Enter your new password below.</p>\n\n {recoverForm.isSuccess && (\n <div className=\"reset-success-banner\">Password has been reset successfully!</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 ? \"Resetting...\" : \"Reset Password\"}\n </button>\n </form>\n )}\n\n <p className=\"reset-back-link\">\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"LOGIN\"); }}>\n Back to Sign In\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
|
|
208
|
-
"types.ts": "export interface Props {}\n",
|
|
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\";\nimport { Props } from \"./types\";\n\nexport default function ResetPasswordSection({ backgroundColor = \"#ffffff\" }: 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\">Set New Password</h1>\n <p className=\"reset-subtitle\">Enter your new password below.</p>\n\n {recoverForm.isSuccess && (\n <div className=\"reset-success-banner\">Password has been reset successfully!</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 ? \"Resetting...\" : \"Reset Password\"}\n </button>\n </form>\n )}\n\n <p className=\"reset-back-link\">\n <a href=\"#\" onClick={(e) => { e.preventDefault(); Router.navigateToPage(\"LOGIN\"); }}>\n Back to Sign In\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
|
|
208
|
+
"types.ts": "export interface Props {\n backgroundColor?: string;\n}\n",
|
|
209
209
|
"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"
|
|
210
|
+
"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}\n"
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
|
-
}
|
|
214
|
+
}
|