@ikas/code-components-mcp 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/framework.json +60 -0
- package/data/section-templates.json +214 -0
- package/data/storefront-api.json +808 -49
- package/data/storefront-types.json +1 -1
- package/dist/index.js +81 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/data/storefront-api.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-02-
|
|
2
|
+
"generatedAt": "2026-02-18T10:46:45.063Z",
|
|
3
3
|
"functions": [
|
|
4
4
|
{
|
|
5
5
|
"name": "apiListBlog",
|
|
@@ -16131,15 +16131,147 @@
|
|
|
16131
16131
|
}
|
|
16132
16132
|
],
|
|
16133
16133
|
"codeExamples": [
|
|
16134
|
+
{
|
|
16135
|
+
"id": "account-addresses-section",
|
|
16136
|
+
"title": "Account Addresses Section (Complete)",
|
|
16137
|
+
"description": "Complete address management with list display, add/edit form (getIkasCustomerAddressForm/initAddressForm/setAddressForm*/submitAddressForm), and delete (deleteCustomerAddress). Uses isEmpty/isNotEmpty.",
|
|
16138
|
+
"code": "import { useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nfunction 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\nexport default observer(AccountAddressesSection);\n",
|
|
16139
|
+
"relatedFunctions": [
|
|
16140
|
+
"getIkasCustomerAddressForm",
|
|
16141
|
+
"initAddressForm",
|
|
16142
|
+
"submitAddressForm",
|
|
16143
|
+
"deleteCustomerAddress",
|
|
16144
|
+
"setAddressFormFirstName",
|
|
16145
|
+
"setAddressFormLastName",
|
|
16146
|
+
"setAddressFormPhone",
|
|
16147
|
+
"setAddressFormAddressLine1",
|
|
16148
|
+
"setAddressFormAddressLine2",
|
|
16149
|
+
"setAddressFormCity",
|
|
16150
|
+
"setAddressFormDistrict",
|
|
16151
|
+
"setAddressFormCountry",
|
|
16152
|
+
"setAddressFormPostalCode",
|
|
16153
|
+
"setAddressFormTitle",
|
|
16154
|
+
"customerStore",
|
|
16155
|
+
"isEmpty",
|
|
16156
|
+
"isNotEmpty"
|
|
16157
|
+
],
|
|
16158
|
+
"categories": [
|
|
16159
|
+
"Customer",
|
|
16160
|
+
"Account",
|
|
16161
|
+
"Form"
|
|
16162
|
+
],
|
|
16163
|
+
"files": [
|
|
16164
|
+
{
|
|
16165
|
+
"filename": "index.tsx",
|
|
16166
|
+
"content": "import { useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nfunction 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\nexport default observer(AccountAddressesSection);\n"
|
|
16167
|
+
},
|
|
16168
|
+
{
|
|
16169
|
+
"filename": "types.ts",
|
|
16170
|
+
"content": "export interface Props {\n title?: string;\n}\n"
|
|
16171
|
+
},
|
|
16172
|
+
{
|
|
16173
|
+
"filename": "styles.css",
|
|
16174
|
+
"content": ".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"
|
|
16175
|
+
},
|
|
16176
|
+
{
|
|
16177
|
+
"filename": "ikas-config-snippet.json",
|
|
16178
|
+
"content": "{ \"id\": \"account-addresses\", \"name\": \"Account Addresses\", \"type\": \"section\", \"props\": [{ \"name\": \"title\", \"displayName\": \"Title\", \"type\": \"TEXT\", \"defaultValue\": \"My Addresses\" }] }\n"
|
|
16179
|
+
}
|
|
16180
|
+
]
|
|
16181
|
+
},
|
|
16182
|
+
{
|
|
16183
|
+
"id": "account-info-section",
|
|
16184
|
+
"title": "Account Info Section (Complete)",
|
|
16185
|
+
"description": "Complete account info section with first name, last name, and phone fields. Uses getAccountInfoForm/initAccountInfoForm/setAccountInfoForm*/submitAccountInfoForm pattern.",
|
|
16186
|
+
"code": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\n\nfunction 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\nexport default observer(AccountInfoSection);\n",
|
|
16187
|
+
"relatedFunctions": [
|
|
16188
|
+
"getAccountInfoForm",
|
|
16189
|
+
"initAccountInfoForm",
|
|
16190
|
+
"setAccountInfoFormFirstName",
|
|
16191
|
+
"setAccountInfoFormLastName",
|
|
16192
|
+
"setAccountInfoFormPhone",
|
|
16193
|
+
"submitAccountInfoForm",
|
|
16194
|
+
"customerStore"
|
|
16195
|
+
],
|
|
16196
|
+
"categories": [
|
|
16197
|
+
"Customer",
|
|
16198
|
+
"Account",
|
|
16199
|
+
"Form"
|
|
16200
|
+
],
|
|
16201
|
+
"files": [
|
|
16202
|
+
{
|
|
16203
|
+
"filename": "index.tsx",
|
|
16204
|
+
"content": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\n\nfunction 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\nexport default observer(AccountInfoSection);\n"
|
|
16205
|
+
},
|
|
16206
|
+
{
|
|
16207
|
+
"filename": "types.ts",
|
|
16208
|
+
"content": "export interface Props {}\n"
|
|
16209
|
+
},
|
|
16210
|
+
{
|
|
16211
|
+
"filename": "styles.css",
|
|
16212
|
+
"content": ".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"
|
|
16213
|
+
},
|
|
16214
|
+
{
|
|
16215
|
+
"filename": "ikas-config-snippet.json",
|
|
16216
|
+
"content": "{ \"id\": \"account-info\", \"name\": \"Account Info\", \"type\": \"section\", \"props\": [] }\n"
|
|
16217
|
+
}
|
|
16218
|
+
]
|
|
16219
|
+
},
|
|
16220
|
+
{
|
|
16221
|
+
"id": "account-orders-section",
|
|
16222
|
+
"title": "Account Orders Section (Complete)",
|
|
16223
|
+
"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.",
|
|
16224
|
+
"code": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nfunction 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\nexport default observer(AccountOrdersSection);\n",
|
|
16225
|
+
"relatedFunctions": [
|
|
16226
|
+
"customerStore",
|
|
16227
|
+
"getOrders",
|
|
16228
|
+
"getIkasOrderFormattedTotalFinalPrice",
|
|
16229
|
+
"getIkasOrderDistinctItemCount",
|
|
16230
|
+
"getIkasOrderFormattedOrderedAt",
|
|
16231
|
+
"getIkasOrderPackageStatusTranslation",
|
|
16232
|
+
"getIkasOrderHref",
|
|
16233
|
+
"getIkasOrderLineVariantMainImage",
|
|
16234
|
+
"getDefaultSrc",
|
|
16235
|
+
"isEmpty",
|
|
16236
|
+
"isNotEmpty"
|
|
16237
|
+
],
|
|
16238
|
+
"categories": [
|
|
16239
|
+
"Customer",
|
|
16240
|
+
"Order",
|
|
16241
|
+
"Account"
|
|
16242
|
+
],
|
|
16243
|
+
"files": [
|
|
16244
|
+
{
|
|
16245
|
+
"filename": "index.tsx",
|
|
16246
|
+
"content": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nfunction 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\nexport default observer(AccountOrdersSection);\n"
|
|
16247
|
+
},
|
|
16248
|
+
{
|
|
16249
|
+
"filename": "types.ts",
|
|
16250
|
+
"content": "export interface Props {\n title?: string;\n emptyMessage?: string;\n}\n"
|
|
16251
|
+
},
|
|
16252
|
+
{
|
|
16253
|
+
"filename": "styles.css",
|
|
16254
|
+
"content": ".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"
|
|
16255
|
+
},
|
|
16256
|
+
{
|
|
16257
|
+
"filename": "ikas-config-snippet.json",
|
|
16258
|
+
"content": "{\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"
|
|
16259
|
+
}
|
|
16260
|
+
]
|
|
16261
|
+
},
|
|
16134
16262
|
{
|
|
16135
16263
|
"id": "add-to-cart",
|
|
16136
16264
|
"title": "Adding a Product to Cart",
|
|
16137
|
-
"description": "Check stock
|
|
16138
|
-
"code": "import {
|
|
16265
|
+
"description": "Check stock (hasProductVariantStock), verify add-to-cart enabled (isAddToCartEnabled), check bundle settings (hasBundleSettings), then addItemToCart with AddItemOptions returning IkasCartOperationResult with success/validationError. Also shows getSelectedProductVariantHref for product links.",
|
|
16266
|
+
"code": "import {\n addItemToCart,\n getSelectedProductVariant,\n getSelectedProductVariantHref,\n hasProductVariantStock,\n isAddToCartEnabled,\n hasBundleSettings,\n baseStore,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\n\n\n/**\n * Add to cart with full validation and result handling.\n * addItemToCart accepts optional AddItemOptions for quantity.\n * Returns IkasCartOperationResult with .success property.\n */\nasync function handleAddToCart(product: IkasProduct, quantity: number = 1) {\n // Check if add to cart is enabled for this product\n if (!isAddToCartEnabled(product)) {\n showToast(\"This product cannot be added to cart\");\n return;\n }\n\n const variant = getSelectedProductVariant(product);\n\n // Check stock availability\n if (!hasProductVariantStock(variant)) {\n showToast(\"Out of stock\");\n return;\n }\n\n // Check for bundle settings\n const hasBundle = variant ? hasBundleSettings(variant) : false;\n\n // addItemToCart(variant, product, initialQuantity, options?: AddItemOptions)\n // Returns IkasCartOperationResult { success: boolean, validationError?: string }\n const result = await addItemToCart(variant, product, quantity);\n\n if (result.success) {\n showToast(\"Added to cart!\");\n } else if (result.validationError === \"INSUFFICIENT_STOCK\") {\n showToast(\"Out of stock\");\n } else if (result.validationError === \"INVALID_PRODUCT_OPTION_VALUES\") {\n showToast(\"Please select all options\");\n }\n\n // Get the product link for \"view product\" functionality\n const productHref = getSelectedProductVariantHref(product);\n\n return result;\n}\n\nexport default handleAddToCart;\n",
|
|
16139
16267
|
"relatedFunctions": [
|
|
16140
16268
|
"addItemToCart",
|
|
16141
16269
|
"getSelectedProductVariant",
|
|
16142
|
-
"
|
|
16270
|
+
"getSelectedProductVariantHref",
|
|
16271
|
+
"hasProductVariantStock",
|
|
16272
|
+
"isAddToCartEnabled",
|
|
16273
|
+
"hasBundleSettings",
|
|
16274
|
+
"baseStore"
|
|
16143
16275
|
],
|
|
16144
16276
|
"categories": [
|
|
16145
16277
|
"Cart",
|
|
@@ -16154,7 +16286,6 @@
|
|
|
16154
16286
|
"relatedFunctions": [
|
|
16155
16287
|
"getIkasBlogHref",
|
|
16156
16288
|
"getIkasBlogFormattedDate",
|
|
16157
|
-
"getBlogListInitialData",
|
|
16158
16289
|
"hasBlogListNextPage",
|
|
16159
16290
|
"hasBlogListPrevPage",
|
|
16160
16291
|
"getBlogListNextPage",
|
|
@@ -16165,6 +16296,41 @@
|
|
|
16165
16296
|
"BlogList"
|
|
16166
16297
|
]
|
|
16167
16298
|
},
|
|
16299
|
+
{
|
|
16300
|
+
"id": "blog-list-section",
|
|
16301
|
+
"title": "Blog List Section (Complete)",
|
|
16302
|
+
"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.",
|
|
16303
|
+
"code": "import { observer } from \"@ikas/component-utils\";\nimport {\n IkasBlogList,\n hasBlogListNextPage,\n getBlogListNextPage,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction 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\nexport default observer(BlogListSection);\n",
|
|
16304
|
+
"relatedFunctions": [
|
|
16305
|
+
"hasBlogListNextPage",
|
|
16306
|
+
"getBlogListNextPage",
|
|
16307
|
+
"getIkasBlogFormattedDate",
|
|
16308
|
+
"getIkasBlogHref",
|
|
16309
|
+
"getDefaultSrc"
|
|
16310
|
+
],
|
|
16311
|
+
"categories": [
|
|
16312
|
+
"Blog",
|
|
16313
|
+
"Pagination"
|
|
16314
|
+
],
|
|
16315
|
+
"files": [
|
|
16316
|
+
{
|
|
16317
|
+
"filename": "index.tsx",
|
|
16318
|
+
"content": "import { observer } from \"@ikas/component-utils\";\nimport {\n IkasBlogList,\n hasBlogListNextPage,\n getBlogListNextPage,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction 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\nexport default observer(BlogListSection);\n"
|
|
16319
|
+
},
|
|
16320
|
+
{
|
|
16321
|
+
"filename": "types.ts",
|
|
16322
|
+
"content": "import { IkasBlogList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n blogList: IkasBlogList;\n title?: string;\n}\n"
|
|
16323
|
+
},
|
|
16324
|
+
{
|
|
16325
|
+
"filename": "styles.css",
|
|
16326
|
+
"content": ".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"
|
|
16327
|
+
},
|
|
16328
|
+
{
|
|
16329
|
+
"filename": "ikas-config-snippet.json",
|
|
16330
|
+
"content": "{\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"
|
|
16331
|
+
}
|
|
16332
|
+
]
|
|
16333
|
+
},
|
|
16168
16334
|
{
|
|
16169
16335
|
"id": "brand-category-list",
|
|
16170
16336
|
"title": "Brand and Category List",
|
|
@@ -16172,12 +16338,10 @@
|
|
|
16172
16338
|
"code": "import {\n getIkasBrandHref,\n hasBrandListNextPage,\n getBrandListNextPage,\n getIkasCategoryHref,\n getCategoryPath,\n hasCategoryListNextPage,\n getCategoryListNextPage,\n IkasBrand,\n IkasBrandList,\n IkasCategory,\n IkasCategoryList,\n} from \"@ikas/bp-storefront\";\n\nfunction BrandList({ brandList }: { brandList: IkasBrandList }) {\n return (\n <div>\n {brandList.data.map((brand: IkasBrand) => (\n <a key={brand.id} href={getIkasBrandHref(brand)}>\n {brand.name}\n </a>\n ))}\n {hasBrandListNextPage(brandList) && (\n <button onClick={() => getBrandListNextPage(brandList)}>Load More</button>\n )}\n </div>\n );\n}\n\nfunction CategoryList({ categoryList }: { categoryList: IkasCategoryList }) {\n return (\n <div>\n {categoryList.data.map((category: IkasCategory) => (\n <a key={category.id} href={getIkasCategoryHref(category)}>\n {getCategoryPath(category).map((c) => c.name).join(\" > \")}\n </a>\n ))}\n {hasCategoryListNextPage(categoryList) && (\n <button onClick={() => getCategoryListNextPage(categoryList)}>Load More</button>\n )}\n </div>\n );\n}\n\nexport { BrandList, CategoryList };\nexport default BrandList;\n",
|
|
16173
16339
|
"relatedFunctions": [
|
|
16174
16340
|
"getIkasBrandHref",
|
|
16175
|
-
"getBrandListInitialData",
|
|
16176
16341
|
"hasBrandListNextPage",
|
|
16177
16342
|
"getBrandListNextPage",
|
|
16178
16343
|
"getIkasCategoryHref",
|
|
16179
16344
|
"getCategoryPath",
|
|
16180
|
-
"getCategoryListInitialData",
|
|
16181
16345
|
"hasCategoryListNextPage",
|
|
16182
16346
|
"getCategoryListNextPage"
|
|
16183
16347
|
],
|
|
@@ -16188,20 +16352,53 @@
|
|
|
16188
16352
|
"CategoryList"
|
|
16189
16353
|
]
|
|
16190
16354
|
},
|
|
16355
|
+
{
|
|
16356
|
+
"id": "bundle-products",
|
|
16357
|
+
"title": "Bundle / Offer Products",
|
|
16358
|
+
"description": "Bundle product display with hasBundleSettings check, initBundleProducts initialization, getDisplayedProductGroups for group layout, and offer management (acceptProductOffer/rejectProductOffer/isAcceptedProductOffer). Supports variant selection within offers using isIkasVariantTypeColorSelection.",
|
|
16359
|
+
"code": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n hasBundleSettings,\n initBundleProducts,\n getDisplayedProductGroups,\n acceptProductOffer,\n rejectProductOffer,\n isAcceptedProductOffer,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n isIkasVariantTypeColorSelection,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getDefaultSrc,\n getProductVariantMainImage,\n IkasProduct,\n IkasImage,\n} from \"@ikas/bp-storefront\";\n\nfunction BundleProducts({ product }: { product: IkasProduct }) {\n const variant = getSelectedProductVariant(product);\n const hasBundle = variant ? (hasBundleSettings(variant) as unknown as boolean) : false;\n\n useEffect(() => {\n if (hasBundle) {\n initBundleProducts(product);\n }\n }, [hasBundle]);\n\n if (!hasBundle) return null;\n\n const productGroups = getDisplayedProductGroups(product);\n\n return (\n <div className=\"bundle-products\">\n <h3>Complete Your Purchase</h3>\n {productGroups.map((group: any, gi: number) => (\n <div key={gi} className=\"bundle-group\">\n {group.title && <h4>{group.title}</h4>}\n <div className=\"bundle-offers\">\n {group.offers?.map((offer: any) => {\n const isAccepted = isAcceptedProductOffer(offer) as unknown as boolean;\n const offerProduct = offer.product;\n if (!offerProduct) return null;\n\n const offerVariant = getSelectedProductVariant(offerProduct);\n const offerImage = getProductVariantMainImage(offerVariant) as unknown as IkasImage | null;\n const offerPrice = getProductVariantFormattedFinalPrice(offerVariant) as unknown as string;\n\n // Variant types for offer product\n const offerVariantTypes = getDisplayedProductVariantTypes(offerProduct);\n\n return (\n <div key={offer.id || offerProduct.id} className=\"bundle-offer-card\">\n {offerImage && (\n <img src={getDefaultSrc(offerImage)} alt={offerProduct.name} style={{ width: 80, height: 80, objectFit: \"cover\", borderRadius: 6 }} />\n )}\n <div className=\"bundle-offer-info\">\n <span className=\"bundle-offer-name\">{offerProduct.name}</span>\n <span className=\"bundle-offer-price\">{offerPrice}</span>\n\n {/* Variant selection for offer products */}\n {offerVariantTypes.map((vt: any) => {\n const isColor = isIkasVariantTypeColorSelection(vt.variantType);\n return (\n <div key={vt.variantType.id} style={{ display: \"flex\", gap: 4, marginTop: 4 }}>\n {vt.displayedVariantValues.map((dvv: any) => (\n <button\n key={dvv.variantValue.id}\n style={{\n width: isColor ? 24 : \"auto\",\n height: isColor ? 24 : \"auto\",\n borderRadius: isColor ? \"50%\" : 4,\n backgroundColor: isColor ? dvv.variantValue.colorCode : \"#fff\",\n border: dvv.isSelected ? \"2px solid #111\" : \"1px solid #ddd\",\n padding: isColor ? 0 : \"4px 8px\",\n fontSize: 12,\n cursor: \"pointer\",\n }}\n onClick={() => selectVariantValue(offerProduct, dvv.variantValue, { disableRoute: true })}\n >\n {!isColor && dvv.variantValue.name}\n </button>\n ))}\n </div>\n );\n })}\n </div>\n <button\n className={`bundle-offer-toggle ${isAccepted ? \"accepted\" : \"\"}`}\n onClick={() => isAccepted ? rejectProductOffer(offer) : acceptProductOffer(offer)}\n >\n {isAccepted ? \"Remove\" : \"Add\"}\n </button>\n </div>\n );\n })}\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nexport default observer(BundleProducts);\n",
|
|
16360
|
+
"relatedFunctions": [
|
|
16361
|
+
"hasBundleSettings",
|
|
16362
|
+
"initBundleProducts",
|
|
16363
|
+
"getDisplayedProductGroups",
|
|
16364
|
+
"acceptProductOffer",
|
|
16365
|
+
"rejectProductOffer",
|
|
16366
|
+
"isAcceptedProductOffer",
|
|
16367
|
+
"getDisplayedProductVariantTypes",
|
|
16368
|
+
"selectVariantValue",
|
|
16369
|
+
"isIkasVariantTypeColorSelection",
|
|
16370
|
+
"getSelectedProductVariant",
|
|
16371
|
+
"getProductVariantFormattedFinalPrice",
|
|
16372
|
+
"getProductVariantMainImage",
|
|
16373
|
+
"getDefaultSrc"
|
|
16374
|
+
],
|
|
16375
|
+
"categories": [
|
|
16376
|
+
"ProductDetail",
|
|
16377
|
+
"Cart"
|
|
16378
|
+
]
|
|
16379
|
+
},
|
|
16191
16380
|
{
|
|
16192
16381
|
"id": "cart-section",
|
|
16193
16382
|
"title": "Cart Section (Complete)",
|
|
16194
|
-
"description": "Complete cart section with line
|
|
16195
|
-
"code": "import { observer } from \"@ikas/component-utils\";\nimport {\n cartStore,\n changeItemQuantity,\n removeItem,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n
|
|
16383
|
+
"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. Uses observer for reactive cart updates.",
|
|
16384
|
+
"code": "import { observer } from \"@ikas/component-utils\";\nimport {\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\nfunction 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\nexport default observer(CartSection);\n",
|
|
16196
16385
|
"relatedFunctions": [
|
|
16197
16386
|
"cartStore",
|
|
16387
|
+
"hasCart",
|
|
16198
16388
|
"changeItemQuantity",
|
|
16199
16389
|
"removeItem",
|
|
16200
16390
|
"getIkasOrderFormattedTotalFinalPrice",
|
|
16201
16391
|
"getIkasOrderFormattedTotalPrice",
|
|
16202
|
-
"
|
|
16203
|
-
"
|
|
16204
|
-
"
|
|
16392
|
+
"getIkasOrderTotalItemCount",
|
|
16393
|
+
"getOrderLineItemFormattedFinalPriceWithQuantity",
|
|
16394
|
+
"getOrderLineItemFormattedPriceWithQuantity",
|
|
16395
|
+
"hasOrderLineItemDiscount",
|
|
16396
|
+
"getIkasOrderLineVariantMainImage",
|
|
16397
|
+
"getIkasOrderLineVariantHref",
|
|
16398
|
+
"getOrderAdjustmentDisplayName",
|
|
16399
|
+
"getOrderAdjustmentFormattedAmount",
|
|
16400
|
+
"getCheckoutUrlFromCartStore",
|
|
16401
|
+
"getDefaultSrc"
|
|
16205
16402
|
],
|
|
16206
16403
|
"categories": [
|
|
16207
16404
|
"Cart"
|
|
@@ -16209,7 +16406,7 @@
|
|
|
16209
16406
|
"files": [
|
|
16210
16407
|
{
|
|
16211
16408
|
"filename": "index.tsx",
|
|
16212
|
-
"content": "import { observer } from \"@ikas/component-utils\";\nimport {\n cartStore,\n changeItemQuantity,\n removeItem,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n
|
|
16409
|
+
"content": "import { observer } from \"@ikas/component-utils\";\nimport {\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\nfunction 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\nexport default observer(CartSection);\n"
|
|
16213
16410
|
},
|
|
16214
16411
|
{
|
|
16215
16412
|
"filename": "types.ts",
|
|
@@ -16217,7 +16414,7 @@
|
|
|
16217
16414
|
},
|
|
16218
16415
|
{
|
|
16219
16416
|
"filename": "styles.css",
|
|
16220
|
-
"content": ".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-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}\n\n.cart-item-
|
|
16417
|
+
"content": ".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"
|
|
16221
16418
|
},
|
|
16222
16419
|
{
|
|
16223
16420
|
"filename": "ikas-config-snippet.json",
|
|
@@ -16225,33 +16422,289 @@
|
|
|
16225
16422
|
}
|
|
16226
16423
|
]
|
|
16227
16424
|
},
|
|
16425
|
+
{
|
|
16426
|
+
"id": "contact-form-section",
|
|
16427
|
+
"title": "Contact Form Section (Complete)",
|
|
16428
|
+
"description": "Complete contact form section with name, email, phone, and message fields. Uses the initContactForm/setContactForm*/submitContactForm pattern with validation and success state.",
|
|
16429
|
+
"code": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getContactForm,\n initContactForm,\n setContactFormEmail,\n setContactFormFirstName,\n setContactFormLastName,\n setContactFormPhone,\n setContactFormMessage,\n submitContactForm,\n clearContactForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction 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\nexport default observer(ContactFormSection);\n",
|
|
16430
|
+
"relatedFunctions": [
|
|
16431
|
+
"initContactForm",
|
|
16432
|
+
"setContactFormEmail",
|
|
16433
|
+
"setContactFormFirstName",
|
|
16434
|
+
"setContactFormLastName",
|
|
16435
|
+
"setContactFormPhone",
|
|
16436
|
+
"setContactFormMessage",
|
|
16437
|
+
"submitContactForm",
|
|
16438
|
+
"clearContactForm"
|
|
16439
|
+
],
|
|
16440
|
+
"categories": [
|
|
16441
|
+
"Form",
|
|
16442
|
+
"Contact"
|
|
16443
|
+
],
|
|
16444
|
+
"files": [
|
|
16445
|
+
{
|
|
16446
|
+
"filename": "index.tsx",
|
|
16447
|
+
"content": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getContactForm,\n initContactForm,\n setContactFormEmail,\n setContactFormFirstName,\n setContactFormLastName,\n setContactFormPhone,\n setContactFormMessage,\n submitContactForm,\n clearContactForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction 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\nexport default observer(ContactFormSection);\n"
|
|
16448
|
+
},
|
|
16449
|
+
{
|
|
16450
|
+
"filename": "types.ts",
|
|
16451
|
+
"content": "export interface Props {\n title?: string;\n successMessage?: string;\n}\n"
|
|
16452
|
+
},
|
|
16453
|
+
{
|
|
16454
|
+
"filename": "styles.css",
|
|
16455
|
+
"content": ".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"
|
|
16456
|
+
},
|
|
16457
|
+
{
|
|
16458
|
+
"filename": "ikas-config-snippet.json",
|
|
16459
|
+
"content": "{\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"
|
|
16460
|
+
}
|
|
16461
|
+
]
|
|
16462
|
+
},
|
|
16463
|
+
{
|
|
16464
|
+
"id": "coupon-code",
|
|
16465
|
+
"title": "Coupon Code",
|
|
16466
|
+
"description": "Coupon code input with apply (initCouponCodeForm/setCouponCodeFormCouponCode/submitCouponCodeForm), remove (removeCouponCodeForm), and clear (clearCouponCodeForm). Reads applied coupon from IkasCart.couponCode.",
|
|
16467
|
+
"code": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n cartStore,\n customerStore,\n initCouponCodeForm,\n setCouponCodeFormCouponCode,\n submitCouponCodeForm,\n removeCouponCodeForm,\n clearCouponCodeForm,\n} from \"@ikas/bp-storefront\";\n\nfunction CouponCode() {\n const cart = cartStore.cart;\n const couponForm = customerStore.couponCodeForm;\n const appliedCoupon = cart?.couponCode;\n\n useEffect(() => {\n if (couponForm) {\n initCouponCodeForm(couponForm);\n }\n }, [couponForm]);\n\n if (!couponForm) return null;\n\n const handleApply = async (e: Event) => {\n e.preventDefault();\n await submitCouponCodeForm(couponForm);\n };\n\n const handleRemove = async () => {\n await removeCouponCodeForm(couponForm);\n };\n\n const handleClear = () => {\n clearCouponCodeForm(customerStore);\n };\n\n return (\n <div className=\"coupon-code\">\n {appliedCoupon ? (\n <div className=\"coupon-applied\">\n <span>Coupon applied: <strong>{appliedCoupon}</strong></span>\n <button onClick={handleRemove}>Remove</button>\n </div>\n ) : (\n <form className=\"coupon-form\" onSubmit={handleApply}>\n <input\n type=\"text\"\n placeholder={couponForm.couponCode?.placeholder ?? \"Enter coupon code\"}\n value={couponForm.couponCode?.value ?? \"\"}\n onInput={(e) => setCouponCodeFormCouponCode(couponForm, (e.target as HTMLInputElement).value)}\n className=\"coupon-input\"\n />\n <button type=\"submit\" className=\"coupon-apply-btn\" disabled={couponForm.isSubmitting}>\n {couponForm.isSubmitting ? \"Applying...\" : \"Apply\"}\n </button>\n </form>\n )}\n {couponForm.isFailure && couponForm.responseMessage && (\n <span className=\"coupon-error\">{couponForm.responseMessage}</span>\n )}\n </div>\n );\n}\n\nexport default observer(CouponCode);\n",
|
|
16468
|
+
"relatedFunctions": [
|
|
16469
|
+
"initCouponCodeForm",
|
|
16470
|
+
"setCouponCodeFormCouponCode",
|
|
16471
|
+
"submitCouponCodeForm",
|
|
16472
|
+
"removeCouponCodeForm",
|
|
16473
|
+
"clearCouponCodeForm",
|
|
16474
|
+
"cartStore",
|
|
16475
|
+
"customerStore"
|
|
16476
|
+
],
|
|
16477
|
+
"categories": [
|
|
16478
|
+
"Cart",
|
|
16479
|
+
"Form"
|
|
16480
|
+
]
|
|
16481
|
+
},
|
|
16228
16482
|
{
|
|
16229
16483
|
"id": "customer-auth",
|
|
16230
16484
|
"title": "Customer Authentication",
|
|
16231
|
-
"description": "
|
|
16232
|
-
"code": "import { customerStore, customerLogin,
|
|
16485
|
+
"description": "Complete customer auth patterns: waitForCustomerStoreInit for async init, email/password login, social login (socialLogin + SocialLoginProvider), handleSocialLogin callback with HandleSocialLoginReturnType status, logout, and getCurrentPath for routing context.",
|
|
16486
|
+
"code": "import {\n customerStore,\n hasCustomer,\n customerLogin,\n logout,\n waitForCustomerStoreInit,\n handleSocialLogin,\n socialLogin,\n getCurrentPath,\n Router,\n} from \"@ikas/bp-storefront\";\n\n/** Wait for customer store initialization before checking auth state */\nasync function ensureCustomerStoreReady() {\n await waitForCustomerStoreInit(customerStore);\n return hasCustomer(customerStore);\n}\n\n/** Standard email/password login */\nasync function loginWithEmail(email: string, password: string) {\n const result = await customerLogin(customerStore, email, password);\n if (result.isSuccess) {\n Router.navigateToPage(\"ACCOUNT\");\n }\n return result;\n}\n\n/** Social login — redirect to provider's OAuth page */\nasync function loginWithSocial(provider: \"GOOGLE\" | \"FACEBOOK\" | \"APPLE\") {\n // SocialLoginProvider enum values: \"GOOGLE\", \"FACEBOOK\", \"APPLE\"\n await socialLogin(customerStore, provider as any);\n // User is redirected to provider's login page\n}\n\n/** Handle social login callback — call on your login page to complete OAuth flow */\nasync function handleSocialLoginCallback() {\n const result = await handleSocialLogin(customerStore);\n // HandleSocialLoginReturnType has .status (\"success\" | \"fail\") and optional .message\n if (result.status === \"success\") {\n Router.navigateToPage(\"ACCOUNT\");\n } else {\n console.error(\"Social login failed:\", result.message);\n }\n return result;\n}\n\n/** Logout current customer */\nasync function logoutCustomer() {\n await logout(customerStore);\n Router.navigateToPage(\"LOGIN\");\n}\n\n/** Get current path for routing context */\nfunction checkCurrentPath() {\n const path = Router.getCurrentPath();\n return path;\n}\n\n/** Customer auth usage examples */\nasync function customerAuthExamples() {\n // 1. Wait for store to initialize before checking state\n const isReady = await ensureCustomerStoreReady();\n\n // 2. Check if logged in\n if (hasCustomer(customerStore)) {\n console.log(\"Welcome,\", customerStore.customer?.firstName);\n }\n\n // 3. Login with email\n await loginWithEmail(\"user@example.com\", \"password123\");\n\n // 4. Social login\n await loginWithSocial(\"GOOGLE\");\n\n // 5. Handle social login callback (on login page after redirect)\n await handleSocialLoginCallback();\n\n // 6. Logout\n await logoutCustomer();\n}\n\nexport default customerAuthExamples;\n",
|
|
16233
16487
|
"relatedFunctions": [
|
|
16488
|
+
"customerStore",
|
|
16234
16489
|
"customerLogin",
|
|
16235
16490
|
"logout",
|
|
16236
|
-
"hasCustomer"
|
|
16491
|
+
"hasCustomer",
|
|
16492
|
+
"waitForCustomerStoreInit",
|
|
16493
|
+
"handleSocialLogin",
|
|
16494
|
+
"socialLogin",
|
|
16495
|
+
"Router.navigate",
|
|
16496
|
+
"Router.navigateToPage",
|
|
16497
|
+
"Router.getCurrentPath"
|
|
16237
16498
|
],
|
|
16238
16499
|
"categories": [
|
|
16239
16500
|
"Customer"
|
|
16240
16501
|
]
|
|
16241
16502
|
},
|
|
16503
|
+
{
|
|
16504
|
+
"id": "faq-section",
|
|
16505
|
+
"title": "FAQ Accordion Section (Complete)",
|
|
16506
|
+
"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).",
|
|
16507
|
+
"code": "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",
|
|
16508
|
+
"relatedFunctions": [],
|
|
16509
|
+
"categories": [
|
|
16510
|
+
"Content",
|
|
16511
|
+
"FAQ"
|
|
16512
|
+
],
|
|
16513
|
+
"files": [
|
|
16514
|
+
{
|
|
16515
|
+
"filename": "index.tsx",
|
|
16516
|
+
"content": "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"
|
|
16517
|
+
},
|
|
16518
|
+
{
|
|
16519
|
+
"filename": "types.ts",
|
|
16520
|
+
"content": "export interface FaqItem {\n question: string;\n answer: string;\n}\n\nexport interface Props {\n title?: string;\n items?: FaqItem[];\n}\n"
|
|
16521
|
+
},
|
|
16522
|
+
{
|
|
16523
|
+
"filename": "styles.css",
|
|
16524
|
+
"content": ".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"
|
|
16525
|
+
},
|
|
16526
|
+
{
|
|
16527
|
+
"filename": "ikas-config-snippet.json",
|
|
16528
|
+
"content": "{\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"
|
|
16529
|
+
}
|
|
16530
|
+
]
|
|
16531
|
+
},
|
|
16242
16532
|
{
|
|
16243
16533
|
"id": "favorites",
|
|
16244
16534
|
"title": "Favorites / Wishlist",
|
|
16245
|
-
"description": "Add
|
|
16246
|
-
"code": "import {\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\n\nfunction FavoriteButton({ product }: { product: IkasProduct }) {\n const isFavorite = isFavoriteIkasProduct(product);\n const handleClick = async () => {\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n return (\n <button onClick={handleClick}>\n {isFavorite ? \"Remove from wishlist\" : \"Add to wishlist\"}\n </button>\n );\n}\n\nexport default FavoriteButton;\n",
|
|
16535
|
+
"description": "Add/remove products from the wishlist with isFavoriteIkasProduct toggle, and display the full favorites list using getFavoriteProducts with product card rendering (pricing, images, links).",
|
|
16536
|
+
"code": "import { useEffect, useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n getFavoriteProducts,\n getSelectedProductVariant,\n getSelectedProductVariantHref,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantMainImage,\n getDefaultSrc,\n customerStore,\n IkasProduct,\n IkasImage,\n} from \"@ikas/bp-storefront\";\n\n/** Toggle button for adding/removing a product from favorites */\nfunction FavoriteButton({ product }: { product: IkasProduct }) {\n const isFavorite = isFavoriteIkasProduct(product);\n const handleClick = async () => {\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n return (\n <button onClick={handleClick}>\n {isFavorite ? \"Remove from wishlist\" : \"Add to wishlist\"}\n </button>\n );\n}\n\n/** Full favorites list page */\nfunction FavoritesList() {\n const [favorites, setFavorites] = useState<IkasProduct[]>([]);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n getFavoriteProducts(customerStore).then((products) => {\n setFavorites(products ?? []);\n setLoading(false);\n });\n }, []);\n\n if (loading) return <p>Loading favorites...</p>;\n if (favorites.length === 0) return <p>No favorites yet.</p>;\n\n return (\n <div>\n {favorites.map((product) => {\n const variant = getSelectedProductVariant(product);\n const href = getSelectedProductVariantHref(product);\n const image = getProductVariantMainImage(variant) as unknown as IkasImage | null;\n const hasDiscount = hasProductVariantDiscount(variant) as unknown as boolean;\n const finalPrice = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const sellPrice = hasDiscount\n ? (getProductVariantFormattedSellPrice(variant) as unknown as string)\n : null;\n\n return (\n <div key={product.id} style={{ display: \"flex\", gap: 12, padding: \"12px 0\", borderBottom: \"1px solid #eee\" }}>\n {image && (\n <a href={href}>\n <img src={getDefaultSrc(image)} width={80} height={80} style={{ objectFit: \"cover\", borderRadius: 6 }} alt={product.name} />\n </a>\n )}\n <div>\n <a href={href} style={{ fontWeight: 600, color: \"#111\", textDecoration: \"none\" }}>{product.name}</a>\n <div>\n {sellPrice && <span style={{ textDecoration: \"line-through\", color: \"#999\", marginRight: 8 }}>{sellPrice}</span>}\n <span>{finalPrice}</span>\n </div>\n <button\n style={{ marginTop: 8, fontSize: 13, color: \"#e53935\", background: \"none\", border: \"none\", cursor: \"pointer\", padding: 0 }}\n onClick={() => removeIkasProductFromFavorites(product)}\n >\n Remove\n </button>\n </div>\n </div>\n );\n })}\n </div>\n );\n}\n\nexport { FavoriteButton, FavoritesList };\nexport default observer(FavoriteButton);\n",
|
|
16247
16537
|
"relatedFunctions": [
|
|
16248
16538
|
"isFavoriteIkasProduct",
|
|
16249
16539
|
"addIkasProductToFavorites",
|
|
16250
|
-
"removeIkasProductFromFavorites"
|
|
16540
|
+
"removeIkasProductFromFavorites",
|
|
16541
|
+
"getFavoriteProducts",
|
|
16542
|
+
"getSelectedProductVariant",
|
|
16543
|
+
"getSelectedProductVariantHref",
|
|
16544
|
+
"getProductVariantFormattedFinalPrice",
|
|
16545
|
+
"getProductVariantFormattedSellPrice",
|
|
16546
|
+
"hasProductVariantDiscount",
|
|
16547
|
+
"getProductVariantMainImage",
|
|
16548
|
+
"getDefaultSrc",
|
|
16549
|
+
"customerStore"
|
|
16550
|
+
],
|
|
16551
|
+
"categories": [
|
|
16552
|
+
"Customer",
|
|
16553
|
+
"ProductList"
|
|
16554
|
+
]
|
|
16555
|
+
},
|
|
16556
|
+
{
|
|
16557
|
+
"id": "favorites-page-section",
|
|
16558
|
+
"title": "Favorites Page Section (Complete)",
|
|
16559
|
+
"description": "Complete favorites page with getFavoriteProducts loading, product cards with pricing/images/links, and remove functionality.",
|
|
16560
|
+
"code": "import { useEffect, useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getFavoriteProducts,\n removeIkasProductFromFavorites,\n getSelectedProductVariant,\n getSelectedProductVariantHref,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantMainImage,\n getDefaultSrc,\n IkasProduct,\n IkasImage,\n} from \"@ikas/bp-storefront\";\n\nfunction 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 image = getProductVariantMainImage(variant) as unknown as IkasImage | 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\nexport default observer(FavoritesPageSection);\n",
|
|
16561
|
+
"relatedFunctions": [
|
|
16562
|
+
"getFavoriteProducts",
|
|
16563
|
+
"removeIkasProductFromFavorites",
|
|
16564
|
+
"getSelectedProductVariant",
|
|
16565
|
+
"getSelectedProductVariantHref",
|
|
16566
|
+
"getProductVariantFormattedFinalPrice",
|
|
16567
|
+
"getProductVariantFormattedSellPrice",
|
|
16568
|
+
"hasProductVariantDiscount",
|
|
16569
|
+
"getProductVariantMainImage",
|
|
16570
|
+
"getDefaultSrc",
|
|
16571
|
+
"customerStore"
|
|
16251
16572
|
],
|
|
16252
16573
|
"categories": [
|
|
16253
16574
|
"Customer",
|
|
16254
16575
|
"ProductList"
|
|
16576
|
+
],
|
|
16577
|
+
"files": [
|
|
16578
|
+
{
|
|
16579
|
+
"filename": "index.tsx",
|
|
16580
|
+
"content": "import { useEffect, useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getFavoriteProducts,\n removeIkasProductFromFavorites,\n getSelectedProductVariant,\n getSelectedProductVariantHref,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantMainImage,\n getDefaultSrc,\n IkasProduct,\n IkasImage,\n} from \"@ikas/bp-storefront\";\n\nfunction 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 image = getProductVariantMainImage(variant) as unknown as IkasImage | 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\nexport default observer(FavoritesPageSection);\n"
|
|
16581
|
+
},
|
|
16582
|
+
{
|
|
16583
|
+
"filename": "types.ts",
|
|
16584
|
+
"content": "export interface Props {}\n"
|
|
16585
|
+
},
|
|
16586
|
+
{
|
|
16587
|
+
"filename": "styles.css",
|
|
16588
|
+
"content": ".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"
|
|
16589
|
+
},
|
|
16590
|
+
{
|
|
16591
|
+
"filename": "ikas-config-snippet.json",
|
|
16592
|
+
"content": "{ \"id\": \"favorites-page\", \"name\": \"Favorites Page\", \"type\": \"section\", \"props\": [] }\n"
|
|
16593
|
+
}
|
|
16594
|
+
]
|
|
16595
|
+
},
|
|
16596
|
+
{
|
|
16597
|
+
"id": "footer-section",
|
|
16598
|
+
"title": "Footer Section (Complete)",
|
|
16599
|
+
"description": "Complete footer section with logo, navigation link columns, contact info, social media links, and copyright. Uses IkasNavigationLink for editable links.",
|
|
16600
|
+
"code": "import { IkasNavigationLink } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function FooterSection({\n logo,\n description,\n linkColumn1Title = \"Shop\",\n linkColumn1,\n linkColumn2Title = \"Company\",\n linkColumn2,\n copyright = \"All rights reserved.\",\n}: Props) {\n return (\n <footer className=\"footer-section\">\n <div className=\"footer-inner\">\n <div className=\"footer-grid\">\n {/* Brand Column */}\n <div className=\"footer-brand\">\n {logo?.src ? (\n <img src={logo.src} alt={logo.alt || \"Logo\"} className=\"footer-logo\" />\n ) : (\n <span className=\"footer-logo-text\">Store</span>\n )}\n {description && <p className=\"footer-description\">{description}</p>}\n </div>\n\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?.map((link: IkasNavigationLink, i: number) => (\n <a\n key={i}\n href={link.href}\n className=\"footer-link\"\n target={link.isTargetBlank ? \"_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?.map((link: IkasNavigationLink, i: number) => (\n <a\n key={i}\n href={link.href}\n className=\"footer-link\"\n target={link.isTargetBlank ? \"_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",
|
|
16601
|
+
"relatedFunctions": [],
|
|
16602
|
+
"categories": [
|
|
16603
|
+
"Navigation",
|
|
16604
|
+
"Footer"
|
|
16605
|
+
],
|
|
16606
|
+
"files": [
|
|
16607
|
+
{
|
|
16608
|
+
"filename": "index.tsx",
|
|
16609
|
+
"content": "import { IkasNavigationLink } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function FooterSection({\n logo,\n description,\n linkColumn1Title = \"Shop\",\n linkColumn1,\n linkColumn2Title = \"Company\",\n linkColumn2,\n copyright = \"All rights reserved.\",\n}: Props) {\n return (\n <footer className=\"footer-section\">\n <div className=\"footer-inner\">\n <div className=\"footer-grid\">\n {/* Brand Column */}\n <div className=\"footer-brand\">\n {logo?.src ? (\n <img src={logo.src} alt={logo.alt || \"Logo\"} className=\"footer-logo\" />\n ) : (\n <span className=\"footer-logo-text\">Store</span>\n )}\n {description && <p className=\"footer-description\">{description}</p>}\n </div>\n\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?.map((link: IkasNavigationLink, i: number) => (\n <a\n key={i}\n href={link.href}\n className=\"footer-link\"\n target={link.isTargetBlank ? \"_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?.map((link: IkasNavigationLink, i: number) => (\n <a\n key={i}\n href={link.href}\n className=\"footer-link\"\n target={link.isTargetBlank ? \"_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"
|
|
16610
|
+
},
|
|
16611
|
+
{
|
|
16612
|
+
"filename": "types.ts",
|
|
16613
|
+
"content": "import { IkasNavigationLink } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: { src: string; alt?: string };\n description?: string;\n linkColumn1Title?: string;\n linkColumn1?: IkasNavigationLink[];\n linkColumn2Title?: string;\n linkColumn2?: IkasNavigationLink[];\n copyright?: string;\n}\n"
|
|
16614
|
+
},
|
|
16615
|
+
{
|
|
16616
|
+
"filename": "styles.css",
|
|
16617
|
+
"content": ".footer-section {\n width: 100%;\n background-color: #f9fafb;\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"
|
|
16618
|
+
},
|
|
16619
|
+
{
|
|
16620
|
+
"filename": "ikas-config-snippet.json",
|
|
16621
|
+
"content": "{\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"
|
|
16622
|
+
}
|
|
16623
|
+
]
|
|
16624
|
+
},
|
|
16625
|
+
{
|
|
16626
|
+
"id": "forgot-password-section",
|
|
16627
|
+
"title": "Forgot Password Section (Complete)",
|
|
16628
|
+
"description": "Complete forgot password section with email input for password recovery. Uses the initForgotPasswordForm/setForgotPasswordFormEmail/submitForgotPasswordForm pattern with success and error states.",
|
|
16629
|
+
"code": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getForgotPasswordForm,\n initForgotPasswordForm,\n setForgotPasswordFormEmail,\n submitForgotPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction 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\nexport default observer(ForgotPasswordSection);\n",
|
|
16630
|
+
"relatedFunctions": [
|
|
16631
|
+
"initForgotPasswordForm",
|
|
16632
|
+
"setForgotPasswordFormEmail",
|
|
16633
|
+
"submitForgotPasswordForm",
|
|
16634
|
+
"customerStore",
|
|
16635
|
+
"Router.navigate",
|
|
16636
|
+
"Router.navigateToPage"
|
|
16637
|
+
],
|
|
16638
|
+
"categories": [
|
|
16639
|
+
"Customer",
|
|
16640
|
+
"Form"
|
|
16641
|
+
],
|
|
16642
|
+
"files": [
|
|
16643
|
+
{
|
|
16644
|
+
"filename": "index.tsx",
|
|
16645
|
+
"content": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getForgotPasswordForm,\n initForgotPasswordForm,\n setForgotPasswordFormEmail,\n submitForgotPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction 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\nexport default observer(ForgotPasswordSection);\n"
|
|
16646
|
+
},
|
|
16647
|
+
{
|
|
16648
|
+
"filename": "types.ts",
|
|
16649
|
+
"content": "export interface Props {\n successMessage?: string;\n}\n"
|
|
16650
|
+
},
|
|
16651
|
+
{
|
|
16652
|
+
"filename": "styles.css",
|
|
16653
|
+
"content": ".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"
|
|
16654
|
+
},
|
|
16655
|
+
{
|
|
16656
|
+
"filename": "ikas-config-snippet.json",
|
|
16657
|
+
"content": "{\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"
|
|
16658
|
+
}
|
|
16659
|
+
]
|
|
16660
|
+
},
|
|
16661
|
+
{
|
|
16662
|
+
"id": "header-section",
|
|
16663
|
+
"title": "Header Section (Complete)",
|
|
16664
|
+
"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. Uses observer for reactive cart badge and customer state.",
|
|
16665
|
+
"code": "import { useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nfunction 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?.src ? (\n <img src={logo.src} alt={logo.alt || \"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?.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?.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\nexport default observer(HeaderSection);\n",
|
|
16666
|
+
"relatedFunctions": [
|
|
16667
|
+
"cartStore",
|
|
16668
|
+
"customerStore",
|
|
16669
|
+
"hasCustomer",
|
|
16670
|
+
"hasCart",
|
|
16671
|
+
"getIkasOrderTotalItemCount",
|
|
16672
|
+
"getIkasOrderFormattedTotalPrice",
|
|
16673
|
+
"getIkasOrderLineVariantMainImage",
|
|
16674
|
+
"getIkasOrderLineVariantHref",
|
|
16675
|
+
"getOrderLineItemFormattedFinalPrice",
|
|
16676
|
+
"getOrderAdjustmentDisplayName",
|
|
16677
|
+
"getOrderAdjustmentFormattedAmount",
|
|
16678
|
+
"changeItemQuantity",
|
|
16679
|
+
"removeItem",
|
|
16680
|
+
"getCheckoutUrlFromCartStore",
|
|
16681
|
+
"getDefaultSrc",
|
|
16682
|
+
"Router.navigate",
|
|
16683
|
+
"Router.navigateToPage"
|
|
16684
|
+
],
|
|
16685
|
+
"categories": [
|
|
16686
|
+
"Navigation",
|
|
16687
|
+
"Header",
|
|
16688
|
+
"Cart",
|
|
16689
|
+
"Customer"
|
|
16690
|
+
],
|
|
16691
|
+
"files": [
|
|
16692
|
+
{
|
|
16693
|
+
"filename": "index.tsx",
|
|
16694
|
+
"content": "import { useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nfunction 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?.src ? (\n <img src={logo.src} alt={logo.alt || \"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?.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?.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\nexport default observer(HeaderSection);\n"
|
|
16695
|
+
},
|
|
16696
|
+
{
|
|
16697
|
+
"filename": "types.ts",
|
|
16698
|
+
"content": "import { IkasNavigationLink } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: { src: string; alt?: string };\n navigationLinks?: IkasNavigationLink[];\n announcementText?: string;\n announcementBgColor?: string;\n announcementTextColor?: string;\n}\n"
|
|
16699
|
+
},
|
|
16700
|
+
{
|
|
16701
|
+
"filename": "styles.css",
|
|
16702
|
+
"content": ".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"
|
|
16703
|
+
},
|
|
16704
|
+
{
|
|
16705
|
+
"filename": "ikas-config-snippet.json",
|
|
16706
|
+
"content": "{\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"
|
|
16707
|
+
}
|
|
16255
16708
|
]
|
|
16256
16709
|
},
|
|
16257
16710
|
{
|
|
@@ -16273,13 +16726,15 @@
|
|
|
16273
16726
|
{
|
|
16274
16727
|
"id": "login-section",
|
|
16275
16728
|
"title": "Login Section (Complete)",
|
|
16276
|
-
"description": "Complete login section with form validation, error display, loading state, forgot password link,
|
|
16277
|
-
"code": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getLoginForm,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction LoginSection({\n redirectAfterLogin = \"/account\",\n showForgotPassword = true,\n}: Props) {\n const loginForm = getLoginForm(customerStore);\n\n useEffect(() => {\n initLoginForm(loginForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitLoginForm(loginForm);\n if (success) {\n Router.navigate(redirectAfterLogin);\n }\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 <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\nexport default observer(LoginSection);\n",
|
|
16729
|
+
"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.",
|
|
16730
|
+
"code": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getLoginForm,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n handleSocialLogin,\n socialLogin,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction 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\nexport default observer(LoginSection);\n",
|
|
16278
16731
|
"relatedFunctions": [
|
|
16279
16732
|
"initLoginForm",
|
|
16280
16733
|
"setLoginFormEmail",
|
|
16281
16734
|
"setLoginFormPassword",
|
|
16282
16735
|
"submitLoginForm",
|
|
16736
|
+
"handleSocialLogin",
|
|
16737
|
+
"socialLogin",
|
|
16283
16738
|
"customerStore",
|
|
16284
16739
|
"Router.navigate",
|
|
16285
16740
|
"Router.navigateToPage"
|
|
@@ -16292,7 +16747,7 @@
|
|
|
16292
16747
|
"files": [
|
|
16293
16748
|
{
|
|
16294
16749
|
"filename": "index.tsx",
|
|
16295
|
-
"content": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getLoginForm,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction LoginSection({\n redirectAfterLogin = \"/account\",\n showForgotPassword = true,\n}: Props) {\n const loginForm = getLoginForm(customerStore);\n\n useEffect(() => {\n initLoginForm(loginForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitLoginForm(loginForm);\n if (success) {\n Router.navigate(redirectAfterLogin);\n }\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 <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\nexport default observer(LoginSection);\n"
|
|
16750
|
+
"content": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getLoginForm,\n initLoginForm,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n handleSocialLogin,\n socialLogin,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction 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\nexport default observer(LoginSection);\n"
|
|
16296
16751
|
},
|
|
16297
16752
|
{
|
|
16298
16753
|
"filename": "types.ts",
|
|
@@ -16311,31 +16766,117 @@
|
|
|
16311
16766
|
{
|
|
16312
16767
|
"id": "navigation",
|
|
16313
16768
|
"title": "Navigation",
|
|
16314
|
-
"description": "Navigate between pages using the Router",
|
|
16315
|
-
"code": "import {
|
|
16769
|
+
"description": "Navigate between pages using the Router (navigate, navigateToPage, goBack, getCurrentPath, getPageParams) and get entity href links (getSelectedProductVariantHref, getIkasCategoryHref, getIkasBlogHref, getIkasBrandHref, getIkasCategoryPathItemHref).",
|
|
16770
|
+
"code": "import {\n Router,\n getSelectedProductVariantHref,\n getIkasCategoryHref,\n getIkasBlogHref,\n getIkasBrandHref,\n getIkasCategoryPathItemHref,\n IkasProduct,\n IkasCategory,\n IkasBlog,\n IkasBrand,\n IkasCategoryPathItem,\n} from \"@ikas/bp-storefront\";\n\nfunction navigationExamples() {\n // === Router navigation ===\n\n // Navigate to cart\n Router.navigateToPage(\"CART\");\n\n // Navigate to checkout\n Router.navigateToPage(\"CHECKOUT\");\n\n // Navigate to product by slug\n Router.navigate(\"/products/my-product-slug\");\n\n // Go to login with redirect\n Router.navigateToPage(\"LOGIN\", undefined, { redirect: \"/account\" });\n\n // Go back\n Router.goBack();\n\n // Get current path\n const path = Router.getCurrentPath();\n\n // Get page parameters\n const params = Router.getPageParams();\n}\n\n/** Get href for various entity types */\nfunction hrefExamples(\n product: IkasProduct,\n category: IkasCategory,\n blog: IkasBlog,\n brand: IkasBrand,\n pathItem: IkasCategoryPathItem,\n) {\n // Product variant href — includes variant query params\n const productHref = getSelectedProductVariantHref(product);\n\n // Category href\n const categoryHref = getIkasCategoryHref(category);\n\n // Blog post href\n const blogHref = getIkasBlogHref(blog);\n\n // Brand page href\n const brandHref = getIkasBrandHref(brand);\n\n // Category breadcrumb path item href\n const pathItemHref = getIkasCategoryPathItemHref(pathItem);\n\n return { productHref, categoryHref, blogHref, brandHref, pathItemHref };\n}\n\nexport { navigationExamples, hrefExamples };\nexport default navigationExamples;\n",
|
|
16316
16771
|
"relatedFunctions": [
|
|
16317
16772
|
"Router.navigate",
|
|
16318
16773
|
"Router.navigateToPage",
|
|
16319
|
-
"Router.goBack"
|
|
16774
|
+
"Router.goBack",
|
|
16775
|
+
"Router.getCurrentPath",
|
|
16776
|
+
"Router.getPageParams",
|
|
16777
|
+
"getSelectedProductVariantHref",
|
|
16778
|
+
"getIkasCategoryHref",
|
|
16779
|
+
"getIkasBlogHref",
|
|
16780
|
+
"getIkasBrandHref",
|
|
16781
|
+
"getIkasCategoryPathItemHref"
|
|
16320
16782
|
],
|
|
16321
16783
|
"categories": [
|
|
16322
16784
|
"Navigation"
|
|
16323
16785
|
]
|
|
16324
16786
|
},
|
|
16325
16787
|
{
|
|
16326
|
-
"id": "order-
|
|
16327
|
-
"title": "Order
|
|
16328
|
-
"description": "
|
|
16329
|
-
"code": "import {\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderDisplayedPackages,\n
|
|
16788
|
+
"id": "order-detail-section",
|
|
16789
|
+
"title": "Order Detail Section (Complete)",
|
|
16790
|
+
"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.",
|
|
16791
|
+
"code": "import { useEffect, useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nfunction 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\nexport default observer(OrderDetailSection);\n",
|
|
16330
16792
|
"relatedFunctions": [
|
|
16793
|
+
"getOrder",
|
|
16331
16794
|
"getIkasOrderFormattedTotalFinalPrice",
|
|
16795
|
+
"getIkasOrderFormattedTotalPrice",
|
|
16332
16796
|
"getIkasOrderFormattedOrderedAt",
|
|
16797
|
+
"getIkasOrderFormattedShippingTotal",
|
|
16798
|
+
"getIkasOrderFormattedTotalTax",
|
|
16333
16799
|
"getIkasOrderDisplayedPackages",
|
|
16334
16800
|
"getIkasOrderDistinctItemCount",
|
|
16801
|
+
"getIkasOrderHref",
|
|
16802
|
+
"getIkasOrderPackageStatusTranslation",
|
|
16803
|
+
"getIkasOrderLineVariantMainImage",
|
|
16804
|
+
"getIkasOrderLineVariantHref",
|
|
16805
|
+
"getOrderLineItemFormattedFinalPriceWithQuantity",
|
|
16806
|
+
"getOrderLineItemFormattedPriceWithQuantity",
|
|
16807
|
+
"hasOrderLineItemDiscount",
|
|
16808
|
+
"getOrderAdjustmentDisplayName",
|
|
16809
|
+
"getOrderAdjustmentFormattedAmount",
|
|
16810
|
+
"getOrderAdjustmentIsDecrement",
|
|
16811
|
+
"getOrderTransactionFormattedAmount",
|
|
16812
|
+
"getOrderTransactionPaymentMethodTranslation",
|
|
16813
|
+
"getIkasOrderRefundableItems",
|
|
16814
|
+
"isIkasOrderRefundable",
|
|
16815
|
+
"getOrderLineItemRefundQuantity",
|
|
16816
|
+
"setOrderLineItemRefundQuantity",
|
|
16817
|
+
"refundOrder",
|
|
16818
|
+
"getDefaultSrc",
|
|
16819
|
+
"customerStore",
|
|
16820
|
+
"Router.getPageParams"
|
|
16821
|
+
],
|
|
16822
|
+
"categories": [
|
|
16823
|
+
"Order",
|
|
16824
|
+
"Customer",
|
|
16825
|
+
"Account"
|
|
16826
|
+
],
|
|
16827
|
+
"files": [
|
|
16828
|
+
{
|
|
16829
|
+
"filename": "index.tsx",
|
|
16830
|
+
"content": "import { useEffect, useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nfunction 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\nexport default observer(OrderDetailSection);\n"
|
|
16831
|
+
},
|
|
16832
|
+
{
|
|
16833
|
+
"filename": "types.ts",
|
|
16834
|
+
"content": "export interface Props {}\n"
|
|
16835
|
+
},
|
|
16836
|
+
{
|
|
16837
|
+
"filename": "styles.css",
|
|
16838
|
+
"content": ".order-detail-section { width: 100%; padding: 40px 24px; }\n.order-detail-inner { max-width: 800px; margin: 0 auto; }\n"
|
|
16839
|
+
},
|
|
16840
|
+
{
|
|
16841
|
+
"filename": "ikas-config-snippet.json",
|
|
16842
|
+
"content": "{ \"id\": \"order-detail\", \"name\": \"Order Detail\", \"type\": \"section\", \"props\": [] }\n"
|
|
16843
|
+
}
|
|
16844
|
+
]
|
|
16845
|
+
},
|
|
16846
|
+
{
|
|
16847
|
+
"id": "order-display",
|
|
16848
|
+
"title": "Order Detail Display",
|
|
16849
|
+
"description": "Complete order detail with getOrder fetch, line items with variant images/links/discount detection, order adjustments with isDecrement, payment transactions, totals (subtotal/shipping/tax/total), package status, and refund functionality (isIkasOrderRefundable, getIkasOrderRefundableItems, setOrderLineItemRefundQuantity, refundOrder).",
|
|
16850
|
+
"code": "import { useEffect, useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderFormattedShippingTotal,\n getIkasOrderFormattedTotalTax,\n getIkasOrderDisplayedPackages,\n getIkasOrderDistinctItemCount,\n getIkasOrderCustomerFullName,\n getIkasOrderHref,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n getOrderLineItemFormattedPriceWithQuantity,\n hasOrderLineItemDiscount,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getOrderAdjustmentIsDecrement,\n getOrderTransactionFormattedAmount,\n getOrderTransactionPaymentMethodTranslation,\n getIkasOrderRefundableItems,\n isIkasOrderRefundable,\n getOrderLineItemRefundQuantity,\n setOrderLineItemRefundQuantity,\n refundOrder,\n getDefaultSrc,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\n\nfunction OrderDetail({ orderId }: { orderId: string }) {\n const [order, setOrder] = useState<IkasOrder | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n getOrder(customerStore, orderId).then((o) => {\n setOrder(o);\n setLoading(false);\n });\n }, [orderId]);\n\n if (loading) return <p>Loading order...</p>;\n if (!order) return <p>Order not found.</p>;\n\n const packages = getIkasOrderDisplayedPackages(order);\n const lineItems = order.orderLineItems ?? [];\n const adjustments = order.orderAdjustments ?? [];\n const transactions = order.orderTransactions ?? [];\n const canRefund = isIkasOrderRefundable(order) as unknown as boolean;\n const refundableItems = canRefund ? getIkasOrderRefundableItems(order) : [];\n\n const handleRefund = async () => {\n const success = await refundOrder(customerStore, order);\n if (success) {\n // Refresh order\n const updated = await getOrder(customerStore, orderId);\n if (updated) setOrder(updated);\n }\n };\n\n return (\n <div>\n <h2>Order #{order.orderNumber}</h2>\n <p>Customer: {getIkasOrderCustomerFullName(order)}</p>\n <p>Date: {getIkasOrderFormattedOrderedAt(order)}</p>\n <p>Items: {getIkasOrderDistinctItemCount(order)}</p>\n\n {/* Package Status */}\n <h3>Packages</h3>\n {packages.map((pkg, i) => (\n <div key={i}>\n <p>Package {i + 1}: {getIkasOrderPackageStatusTranslation(order)}</p>\n </div>\n ))}\n\n {/* Line Items */}\n <h3>Items</h3>\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n const href = item.variant ? getIkasOrderLineVariantHref(item.variant) : undefined;\n const hasDiscount = hasOrderLineItemDiscount(item) as unknown as boolean;\n return (\n <div key={item.id} style={{ display: \"flex\", gap: 12, padding: \"8px 0\", borderBottom: \"1px solid #eee\" }}>\n {image && (\n <a href={href}>\n <img src={getDefaultSrc(image)} width={64} height={64} style={{ objectFit: \"cover\", borderRadius: 4 }} alt=\"\" />\n </a>\n )}\n <div>\n <a href={href} style={{ fontWeight: 600, textDecoration: \"none\", color: \"#111\" }}>\n {item.variant?.name}\n </a>\n <div>Qty: {item.quantity}</div>\n {hasDiscount && (\n <span style={{ textDecoration: \"line-through\", color: \"#999\", marginRight: 8 }}>\n {getOrderLineItemFormattedPriceWithQuantity(item)}\n </span>\n )}\n <span>{getOrderLineItemFormattedFinalPriceWithQuantity(item)}</span>\n </div>\n </div>\n );\n })}\n\n {/* Order Adjustments */}\n {adjustments.length > 0 && (\n <div>\n <h3>Adjustments</h3>\n {adjustments.map((adj: any, i: number) => (\n <div key={i} style={{ display: \"flex\", justifyContent: \"space-between\", padding: \"4px 0\" }}>\n <span>{getOrderAdjustmentDisplayName(adj)}</span>\n <span style={{ color: getOrderAdjustmentIsDecrement(adj) ? \"#2e7d32\" : \"#111\" }}>\n {getOrderAdjustmentFormattedAmount(adj)}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Payment Transactions */}\n {transactions.length > 0 && (\n <div>\n <h3>Payment</h3>\n {transactions.map((tx: any, i: number) => (\n <div key={i} style={{ display: \"flex\", justifyContent: \"space-between\", padding: \"4px 0\" }}>\n <span>{getOrderTransactionPaymentMethodTranslation(tx)}</span>\n <span>{getOrderTransactionFormattedAmount(tx)}</span>\n </div>\n ))}\n </div>\n )}\n\n {/* Order Totals */}\n <div style={{ borderTop: \"1px solid #eee\", paddingTop: 12, marginTop: 12 }}>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>Subtotal</span>\n <span>{getIkasOrderFormattedTotalPrice(order)}</span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>Shipping</span>\n <span>{getIkasOrderFormattedShippingTotal(order)}</span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>Tax</span>\n <span>{getIkasOrderFormattedTotalTax(order)}</span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\", fontWeight: 700, fontSize: 18, marginTop: 8 }}>\n <span>Total</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(order)}</span>\n </div>\n </div>\n\n {/* Refund Section */}\n {canRefund && (\n <div style={{ marginTop: 24, padding: 16, border: \"1px solid #eee\", borderRadius: 8 }}>\n <h3>Request Refund</h3>\n {refundableItems.map((item: any) => (\n <div key={item.id} style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\", padding: \"8px 0\" }}>\n <span>{item.variant?.name}</span>\n <input\n type=\"number\"\n min={0}\n max={item.quantity}\n value={getOrderLineItemRefundQuantity(item) ?? 0}\n onChange={(e) => setOrderLineItemRefundQuantity(Number((e.target as HTMLInputElement).value), item)}\n style={{ width: 60, textAlign: \"center\" }}\n />\n </div>\n ))}\n <button onClick={handleRefund} style={{ marginTop: 12, padding: \"10px 20px\", background: \"#111\", color: \"#fff\", border: \"none\", borderRadius: 6, cursor: \"pointer\" }}>\n Submit Refund Request\n </button>\n </div>\n )}\n\n <a href={getIkasOrderHref(order)} style={{ display: \"inline-block\", marginTop: 16 }}>View on store</a>\n </div>\n );\n}\n\nexport default observer(OrderDetail);\n",
|
|
16851
|
+
"relatedFunctions": [
|
|
16852
|
+
"getOrder",
|
|
16853
|
+
"getIkasOrderFormattedTotalFinalPrice",
|
|
16854
|
+
"getIkasOrderFormattedTotalPrice",
|
|
16855
|
+
"getIkasOrderFormattedOrderedAt",
|
|
16335
16856
|
"getIkasOrderFormattedShippingTotal",
|
|
16336
16857
|
"getIkasOrderFormattedTotalTax",
|
|
16858
|
+
"getIkasOrderDisplayedPackages",
|
|
16859
|
+
"getIkasOrderDistinctItemCount",
|
|
16337
16860
|
"getIkasOrderCustomerFullName",
|
|
16338
|
-
"getIkasOrderHref"
|
|
16861
|
+
"getIkasOrderHref",
|
|
16862
|
+
"getIkasOrderPackageStatusTranslation",
|
|
16863
|
+
"getIkasOrderLineVariantMainImage",
|
|
16864
|
+
"getIkasOrderLineVariantHref",
|
|
16865
|
+
"getOrderLineItemFormattedFinalPriceWithQuantity",
|
|
16866
|
+
"getOrderLineItemFormattedPriceWithQuantity",
|
|
16867
|
+
"hasOrderLineItemDiscount",
|
|
16868
|
+
"getOrderAdjustmentDisplayName",
|
|
16869
|
+
"getOrderAdjustmentFormattedAmount",
|
|
16870
|
+
"getOrderAdjustmentIsDecrement",
|
|
16871
|
+
"getOrderTransactionFormattedAmount",
|
|
16872
|
+
"getOrderTransactionPaymentMethodTranslation",
|
|
16873
|
+
"getIkasOrderRefundableItems",
|
|
16874
|
+
"isIkasOrderRefundable",
|
|
16875
|
+
"getOrderLineItemRefundQuantity",
|
|
16876
|
+
"setOrderLineItemRefundQuantity",
|
|
16877
|
+
"refundOrder",
|
|
16878
|
+
"getDefaultSrc",
|
|
16879
|
+
"customerStore"
|
|
16339
16880
|
],
|
|
16340
16881
|
"categories": [
|
|
16341
16882
|
"Order"
|
|
@@ -16344,24 +16885,54 @@
|
|
|
16344
16885
|
{
|
|
16345
16886
|
"id": "price-display",
|
|
16346
16887
|
"title": "Displaying Product Price with Discount",
|
|
16347
|
-
"description": "Show original price, discount percentage, and final price",
|
|
16348
|
-
"code": "import {\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantDiscountPercentage,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\n\nfunction PriceDisplay({ product }: { product: IkasProduct }) {\n const variant = getSelectedProductVariant(product);\n const hasDiscount = hasProductVariantDiscount(variant);\n return (\n <div>\n {hasDiscount && (\n <>\n <span style={{ textDecoration: \"line-through\" }}>\n {getProductVariantFormattedSellPrice(variant)}\n </span>\n <span
|
|
16888
|
+
"description": "Show original price, discount percentage, savings amount (getProductVariantFormattedDiscountAmount), and final price.",
|
|
16889
|
+
"code": "import {\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantDiscountPercentage,\n getProductVariantFormattedDiscountAmount,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\n\nfunction PriceDisplay({ product }: { product: IkasProduct }) {\n const variant = getSelectedProductVariant(product);\n const hasDiscount = hasProductVariantDiscount(variant);\n return (\n <div>\n {hasDiscount && (\n <>\n <span style={{ textDecoration: \"line-through\", color: \"#999\", marginRight: 8 }}>\n {getProductVariantFormattedSellPrice(variant)}\n </span>\n <span style={{ color: \"#e53935\", marginRight: 8 }}>\n -{getProductVariantDiscountPercentage(variant)}%\n </span>\n <span style={{ color: \"#2e7d32\", fontSize: 13 }}>\n Save {getProductVariantFormattedDiscountAmount(variant)}\n </span>\n </>\n )}\n <div style={{ fontSize: 24, fontWeight: 700, marginTop: 4 }}>\n {getProductVariantFormattedFinalPrice(variant)}\n </div>\n </div>\n );\n}\n\nexport default PriceDisplay;\n",
|
|
16349
16890
|
"relatedFunctions": [
|
|
16350
16891
|
"getSelectedProductVariant",
|
|
16351
16892
|
"getProductVariantFormattedFinalPrice",
|
|
16352
16893
|
"getProductVariantFormattedSellPrice",
|
|
16353
16894
|
"hasProductVariantDiscount",
|
|
16354
|
-
"getProductVariantDiscountPercentage"
|
|
16895
|
+
"getProductVariantDiscountPercentage",
|
|
16896
|
+
"getProductVariantFormattedDiscountAmount"
|
|
16897
|
+
],
|
|
16898
|
+
"categories": [
|
|
16899
|
+
"ProductDetail"
|
|
16900
|
+
]
|
|
16901
|
+
},
|
|
16902
|
+
{
|
|
16903
|
+
"id": "product-card",
|
|
16904
|
+
"title": "Product Card",
|
|
16905
|
+
"description": "Product card component with image, name, main variant display (getMainProductVariantType/getMainProductVariantValue), pricing with discount, stock badge, favorite toggle, and product link. Used in product grids and lists.",
|
|
16906
|
+
"code": "import { observer } from \"@ikas/component-utils\";\nimport {\n getSelectedProductVariant,\n getSelectedProductVariantHref,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n getProductVariantMainImage,\n getMainProductVariantType,\n getMainProductVariantValue,\n hasProductStock,\n hasProductVariantStock,\n hasProductVariantDiscount,\n hasBundleSettings,\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n customerStore,\n hasCustomer,\n getDefaultSrc,\n IkasProduct,\n IkasImage,\n} from \"@ikas/bp-storefront\";\n\nfunction ProductCard({ product }: { product: IkasProduct }) {\n const variant = getSelectedProductVariant(product);\n const href = getSelectedProductVariantHref(product);\n const image = getProductVariantMainImage(variant) as unknown as IkasImage | null;\n const finalPrice = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const hasDiscount = hasProductVariantDiscount(variant) as unknown as boolean;\n const sellPrice = hasDiscount ? (getProductVariantFormattedSellPrice(variant) as unknown as string) : null;\n const inStock = hasProductStock(product) as unknown as boolean;\n const isFavorite = isFavoriteIkasProduct(product);\n\n // Main variant type/value (e.g., \"Color: Red\")\n const mainType = getMainProductVariantType(product);\n const mainValue = getMainProductVariantValue(product);\n\n const handleFavorite = async (e: Event) => {\n e.preventDefault();\n e.stopPropagation();\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n\n return (\n <a href={href} className=\"product-card\" style={{ display: \"block\", textDecoration: \"none\", color: \"inherit\" }}>\n <div style={{ position: \"relative\" }}>\n {image && (\n <img src={getDefaultSrc(image)} alt={product.name} style={{ width: \"100%\", aspectRatio: \"1\", objectFit: \"cover\", borderRadius: 8, background: \"#f5f5f5\" }} />\n )}\n {!inStock && (\n <span style={{ position: \"absolute\", top: 8, left: 8, background: \"#111\", color: \"#fff\", fontSize: 11, padding: \"4px 8px\", borderRadius: 4 }}>Out of Stock</span>\n )}\n {hasDiscount && (\n <span style={{ position: \"absolute\", top: 8, right: 8, background: \"#e53935\", color: \"#fff\", fontSize: 11, padding: \"4px 8px\", borderRadius: 4 }}>Sale</span>\n )}\n <button\n onClick={handleFavorite}\n style={{ position: \"absolute\", bottom: 8, right: 8, background: \"#fff\", border: \"none\", borderRadius: \"50%\", width: 32, height: 32, cursor: \"pointer\", fontSize: 16 }}\n >\n {isFavorite ? \"\\u2665\" : \"\\u2661\"}\n </button>\n </div>\n <div style={{ padding: \"8px 0\" }}>\n <h3 style={{ fontSize: 14, fontWeight: 600, margin: \"0 0 4px 0\" }}>{product.name}</h3>\n {mainType && mainValue && (\n <span style={{ fontSize: 12, color: \"#666\" }}>{mainType.name}: {mainValue.name}</span>\n )}\n <div style={{ marginTop: 4 }}>\n {sellPrice && <span style={{ textDecoration: \"line-through\", color: \"#999\", marginRight: 8, fontSize: 13 }}>{sellPrice}</span>}\n <span style={{ fontSize: 15, fontWeight: 700 }}>{finalPrice}</span>\n </div>\n </div>\n </a>\n );\n}\n\nexport default observer(ProductCard);\n",
|
|
16907
|
+
"relatedFunctions": [
|
|
16908
|
+
"getSelectedProductVariant",
|
|
16909
|
+
"getSelectedProductVariantHref",
|
|
16910
|
+
"getProductVariantFormattedFinalPrice",
|
|
16911
|
+
"getProductVariantFormattedSellPrice",
|
|
16912
|
+
"getProductVariantMainImage",
|
|
16913
|
+
"getMainProductVariantType",
|
|
16914
|
+
"getMainProductVariantValue",
|
|
16915
|
+
"hasProductStock",
|
|
16916
|
+
"hasProductVariantStock",
|
|
16917
|
+
"hasProductVariantDiscount",
|
|
16918
|
+
"hasBundleSettings",
|
|
16919
|
+
"isFavoriteIkasProduct",
|
|
16920
|
+
"addIkasProductToFavorites",
|
|
16921
|
+
"removeIkasProductFromFavorites",
|
|
16922
|
+
"customerStore",
|
|
16923
|
+
"hasCustomer",
|
|
16924
|
+
"getDefaultSrc"
|
|
16355
16925
|
],
|
|
16356
16926
|
"categories": [
|
|
16927
|
+
"ProductList",
|
|
16357
16928
|
"ProductDetail"
|
|
16358
16929
|
]
|
|
16359
16930
|
},
|
|
16360
16931
|
{
|
|
16361
16932
|
"id": "product-detail-section",
|
|
16362
16933
|
"title": "Product Detail Section (Complete)",
|
|
16363
|
-
"description": "Complete product detail section with image gallery, variant selection, pricing with
|
|
16364
|
-
"code": "import { useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n hasProductStock,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantDiscountPercentage,\n isAddToCartEnabled,\n addItemToCart,\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n getProductVariantMainImage,\n getDefaultSrc,\n getThumbnailSrc,\n getSrc,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction 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\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 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\n // variant.images is IkasProductImage[] — use .image to get IkasImage for CDN helpers\n const mainImage = getProductVariantMainImage(selectedVariant) as unknown as IkasImage | undefined;\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\n const currentImage = images[selectedImageIndex] ?? images[0];\n const isFavorite = isFavoriteIkasProduct(product);\n\n const handleAddToCart = async () => {\n if (!canAddToCart || isAddingToCart) return;\n setIsAddingToCart(true);\n try {\n await addItemToCart(selectedVariant, product, 1);\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 <div className=\"product-detail-inner\">\n {/* Image Gallery */}\n <div className=\"product-gallery\">\n {currentImage && (\n <img\n className=\"product-main-image\"\n src={getDefaultSrc(currentImage)}\n srcSet={`${getSrc(currentImage, 400)} 400w, ${getSrc(currentImage, 800)} 800w, ${getSrc(currentImage, 1200)} 1200w`}\n sizes=\"(max-width: 600px) 400px, (max-width: 1024px) 800px, 1200px\"\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 {product.brand && <span className=\"product-brand\">{product.brand.name}</span>}\n <h1 className=\"product-name\">{product.name}</h1>\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 </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 {/* Actions */}\n <div className=\"product-actions\">\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 </div>\n </div>\n </section>\n );\n}\n\nexport default observer(ProductDetail);\n",
|
|
16934
|
+
"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.",
|
|
16935
|
+
"code": "import { useState, useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n hasProductStock,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantDiscountPercentage,\n getProductVariantFormattedDiscountAmount,\n isAddToCartEnabled,\n addItemToCart,\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n getProductVariantMainImage,\n getDefaultSrc,\n getThumbnailSrc,\n getSrc,\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\nfunction 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 mainImage = getProductVariantMainImage(selectedVariant) as unknown as IkasImage | undefined;\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={`${getSrc(currentImage, 400)} 400w, ${getSrc(currentImage, 800)} 800w, ${getSrc(currentImage, 1200)} 1200w`}\n sizes=\"(max-width: 600px) 400px, (max-width: 1024px) 800px, 1200px\"\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\nexport default observer(ProductDetail);\n",
|
|
16365
16936
|
"relatedFunctions": [
|
|
16366
16937
|
"getSelectedProductVariant",
|
|
16367
16938
|
"getDisplayedProductVariantTypes",
|
|
@@ -16372,6 +16943,7 @@
|
|
|
16372
16943
|
"getProductVariantFormattedSellPrice",
|
|
16373
16944
|
"hasProductVariantDiscount",
|
|
16374
16945
|
"getProductVariantDiscountPercentage",
|
|
16946
|
+
"getProductVariantFormattedDiscountAmount",
|
|
16375
16947
|
"isAddToCartEnabled",
|
|
16376
16948
|
"addItemToCart",
|
|
16377
16949
|
"isFavoriteIkasProduct",
|
|
@@ -16380,17 +16952,26 @@
|
|
|
16380
16952
|
"getProductVariantMainImage",
|
|
16381
16953
|
"getDefaultSrc",
|
|
16382
16954
|
"getThumbnailSrc",
|
|
16383
|
-
"getSrc"
|
|
16955
|
+
"getSrc",
|
|
16956
|
+
"getProductCategoryPath",
|
|
16957
|
+
"getIkasCategoryPathItemHref",
|
|
16958
|
+
"getIkasBrandHref",
|
|
16959
|
+
"getAttributeListValues",
|
|
16960
|
+
"hasBundleSettings",
|
|
16961
|
+
"initBundleProducts",
|
|
16962
|
+
"getDisplayedProductGroups",
|
|
16963
|
+
"isNotEmpty"
|
|
16384
16964
|
],
|
|
16385
16965
|
"categories": [
|
|
16386
16966
|
"ProductDetail",
|
|
16387
16967
|
"Cart",
|
|
16388
|
-
"Image"
|
|
16968
|
+
"Image",
|
|
16969
|
+
"Navigation"
|
|
16389
16970
|
],
|
|
16390
16971
|
"files": [
|
|
16391
16972
|
{
|
|
16392
16973
|
"filename": "index.tsx",
|
|
16393
|
-
"content": "import { useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n hasProductStock,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantDiscountPercentage,\n isAddToCartEnabled,\n addItemToCart,\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n getProductVariantMainImage,\n getDefaultSrc,\n getThumbnailSrc,\n getSrc,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction 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\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 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\n // variant.images is IkasProductImage[] — use .image to get IkasImage for CDN helpers\n const mainImage = getProductVariantMainImage(selectedVariant) as unknown as IkasImage | undefined;\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\n const currentImage = images[selectedImageIndex] ?? images[0];\n const isFavorite = isFavoriteIkasProduct(product);\n\n const handleAddToCart = async () => {\n if (!canAddToCart || isAddingToCart) return;\n setIsAddingToCart(true);\n try {\n await addItemToCart(selectedVariant, product, 1);\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 <div className=\"product-detail-inner\">\n {/* Image Gallery */}\n <div className=\"product-gallery\">\n {currentImage && (\n <img\n className=\"product-main-image\"\n src={getDefaultSrc(currentImage)}\n srcSet={`${getSrc(currentImage, 400)} 400w, ${getSrc(currentImage, 800)} 800w, ${getSrc(currentImage, 1200)} 1200w`}\n sizes=\"(max-width: 600px) 400px, (max-width: 1024px) 800px, 1200px\"\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 {product.brand && <span className=\"product-brand\">{product.brand.name}</span>}\n <h1 className=\"product-name\">{product.name}</h1>\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 </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 {/* Actions */}\n <div className=\"product-actions\">\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 </div>\n </div>\n </section>\n );\n}\n\nexport default observer(ProductDetail);\n"
|
|
16974
|
+
"content": "import { useState, useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n hasProductStock,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantDiscountPercentage,\n getProductVariantFormattedDiscountAmount,\n isAddToCartEnabled,\n addItemToCart,\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n getProductVariantMainImage,\n getDefaultSrc,\n getThumbnailSrc,\n getSrc,\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\nfunction 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 mainImage = getProductVariantMainImage(selectedVariant) as unknown as IkasImage | undefined;\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={`${getSrc(currentImage, 400)} 400w, ${getSrc(currentImage, 800)} 800w, ${getSrc(currentImage, 1200)} 1200w`}\n sizes=\"(max-width: 600px) 400px, (max-width: 1024px) 800px, 1200px\"\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\nexport default observer(ProductDetail);\n"
|
|
16394
16975
|
},
|
|
16395
16976
|
{
|
|
16396
16977
|
"filename": "types.ts",
|
|
@@ -16398,7 +16979,7 @@
|
|
|
16398
16979
|
},
|
|
16399
16980
|
{
|
|
16400
16981
|
"filename": "styles.css",
|
|
16401
|
-
"content": ".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}\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/* 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"
|
|
16982
|
+
"content": ".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"
|
|
16402
16983
|
},
|
|
16403
16984
|
{
|
|
16404
16985
|
"filename": "ikas-config-snippet.json",
|
|
@@ -16409,14 +16990,13 @@
|
|
|
16409
16990
|
{
|
|
16410
16991
|
"id": "product-filtering",
|
|
16411
16992
|
"title": "Product List Filtering",
|
|
16412
|
-
"description": "Display and interact with product filters
|
|
16413
|
-
"code": "import {\n getFilterDisplayedValues,\n
|
|
16993
|
+
"description": "Display and interact with product filters using handleFilterValueClick (toggling deselects), category-level filtering with onFilterCategoryClick, and getProductListFilterCategories for filter categories.",
|
|
16994
|
+
"code": "import { observer } from \"@ikas/component-utils\";\nimport {\n getFilterDisplayedValues,\n handleFilterValueClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n IkasProductList,\n IkasProductFilter,\n} from \"@ikas/bp-storefront\";\n\nfunction ProductFilter({\n productList,\n filter,\n}: {\n productList: IkasProductList;\n filter: IkasProductFilter;\n}) {\n const values = getFilterDisplayedValues(filter);\n const filterCategories = getProductListFilterCategories(productList);\n\n return (\n <div>\n <h3>{filter.name}</h3>\n\n {/* Filter values — toggling a selected value deselects it */}\n <ul>\n {values.map((v) => (\n <li key={v.name}>\n <label>\n <input\n type=\"checkbox\"\n checked={v.isSelected ?? false}\n onChange={() => handleFilterValueClick(productList, filter, v)}\n />\n {v.name}\n </label>\n </li>\n ))}\n </ul>\n\n {/* Category-level filtering */}\n {filterCategories.length > 0 && (\n <div style={{ marginTop: 16 }}>\n <h4>Categories</h4>\n {filterCategories.map((cat) => (\n <button\n key={cat.name}\n style={{\n fontWeight: cat.isSelected ? \"bold\" : \"normal\",\n marginRight: 8,\n }}\n onClick={() => onFilterCategoryClick(productList, cat, true)}\n >\n {cat.name}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport default observer(ProductFilter);\n",
|
|
16414
16995
|
"relatedFunctions": [
|
|
16415
16996
|
"getFilterDisplayedValues",
|
|
16416
|
-
"
|
|
16417
|
-
"
|
|
16418
|
-
"
|
|
16419
|
-
"isSwatchFilter"
|
|
16997
|
+
"handleFilterValueClick",
|
|
16998
|
+
"getProductListFilterCategories",
|
|
16999
|
+
"onFilterCategoryClick"
|
|
16420
17000
|
],
|
|
16421
17001
|
"categories": [
|
|
16422
17002
|
"ProductFilter",
|
|
@@ -16424,14 +17004,193 @@
|
|
|
16424
17004
|
"ProductList"
|
|
16425
17005
|
]
|
|
16426
17006
|
},
|
|
17007
|
+
{
|
|
17008
|
+
"id": "product-list-section",
|
|
17009
|
+
"title": "Product List Section (Complete)",
|
|
17010
|
+
"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. Uses observer for reactive updates.",
|
|
17011
|
+
"code": "import { useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n IkasProductList,\n IkasImage,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n setProductListVisiblePage,\n searchProductList,\n getCategoryPath,\n getIkasCategoryHref,\n isEmpty,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction 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 any);\n }\n };\n\n const handleSearch = (keyword: string) => {\n setSearchKeyword(keyword);\n searchProductList(productList, keyword);\n };\n\n return (\n <section className=\"product-list-section\">\n <div className=\"product-list-inner\">\n {/* Category Breadcrumb */}\n {categoryPath.length > 0 && (\n <nav className=\"product-list-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((cat: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\"> / </span>\n <a href={getIkasCategoryHref(cat)}>{cat.name}</a>\n </span>\n ))}\n </nav>\n )}\n\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">\n {category?.name || title}\n {category?.description && (\n <p className=\"product-list-description\">{category.description}</p>\n )}\n </h1>\n\n <div className=\"product-list-controls\">\n {/* Search */}\n <input\n className=\"product-list-search\"\n type=\"text\"\n placeholder=\"Search products...\"\n value={searchKeyword}\n onInput={(e) => handleSearch((e.target as HTMLInputElement).value)}\n />\n\n {/* Sort — use setSortType instead of direct mutation */}\n {sortOptions.length > 0 && (\n <select\n className=\"product-list-sort\"\n value={sortOptions.find((o) => o.isSelected)?.value ?? \"\"}\n onChange={(e) => handleSort((e.target as HTMLSelectElement).value)}\n >\n {sortOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )}\n </div>\n </div>\n\n <div className=\"product-list-layout\">\n {/* Filter Sidebar — mobile uses IkasThemeOverlay */}\n {showFilters && filterCategories.length > 0 && (\n <aside className=\"product-list-filters\">\n {filterCategories.map((category) => {\n const values = getFilterDisplayedValues(category);\n return (\n <div key={category.name} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{category.name}</h3>\n <div className=\"filter-values\">\n {values.map((filterValue) => (\n <label key={filterValue.name} className=\"filter-value\">\n <input\n type=\"checkbox\"\n checked={filterValue.isSelected}\n onChange={() =>\n handleFilterValueClick(productList, category, filterValue)\n }\n />\n <span>{filterValue.name}</span>\n {filterValue.count != null && (\n <span className=\"filter-count\">({filterValue.count})</span>\n )}\n </label>\n ))}\n </div>\n {/* Category-level filter click */}\n {category.isSelected !== undefined && (\n <button\n className=\"filter-category-btn\"\n onClick={() => onFilterCategoryClick(productList, category as any, true)}\n >\n {category.isSelected ? \"Clear\" : \"Apply\"}\n </button>\n )}\n </div>\n );\n })}\n </aside>\n )}\n\n {/* Product Grid — IkasThemeInfiniteScroller pattern for infinite scroll */}\n <div className=\"product-grid\">\n {isEmpty(products) && (\n <p className=\"product-grid-empty\">No products found.</p>\n )}\n {products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const image = getProductVariantMainImage(variant) as unknown as IkasImage | 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\nexport default observer(ProductListSection);\n",
|
|
17012
|
+
"relatedFunctions": [
|
|
17013
|
+
"getSelectedProductVariant",
|
|
17014
|
+
"getProductVariantFormattedFinalPrice",
|
|
17015
|
+
"getProductVariantMainImage",
|
|
17016
|
+
"getSelectedProductVariantHref",
|
|
17017
|
+
"getFilterDisplayedValues",
|
|
17018
|
+
"handleFilterValueClick",
|
|
17019
|
+
"getProductListFilterCategories",
|
|
17020
|
+
"onFilterCategoryClick",
|
|
17021
|
+
"getProductListSortOptions",
|
|
17022
|
+
"setSortType",
|
|
17023
|
+
"hasProductListNextPage",
|
|
17024
|
+
"hasProductListPrevPage",
|
|
17025
|
+
"getProductListNextPage",
|
|
17026
|
+
"getProductListPrevPage",
|
|
17027
|
+
"setProductListVisiblePage",
|
|
17028
|
+
"searchProductList",
|
|
17029
|
+
"getCategoryPath",
|
|
17030
|
+
"getIkasCategoryHref",
|
|
17031
|
+
"isEmpty",
|
|
17032
|
+
"getDefaultSrc"
|
|
17033
|
+
],
|
|
17034
|
+
"categories": [
|
|
17035
|
+
"ProductList",
|
|
17036
|
+
"Filtering",
|
|
17037
|
+
"Pagination",
|
|
17038
|
+
"Navigation"
|
|
17039
|
+
],
|
|
17040
|
+
"files": [
|
|
17041
|
+
{
|
|
17042
|
+
"filename": "index.tsx",
|
|
17043
|
+
"content": "import { useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n IkasProductList,\n IkasImage,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n setProductListVisiblePage,\n searchProductList,\n getCategoryPath,\n getIkasCategoryHref,\n isEmpty,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction 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 any);\n }\n };\n\n const handleSearch = (keyword: string) => {\n setSearchKeyword(keyword);\n searchProductList(productList, keyword);\n };\n\n return (\n <section className=\"product-list-section\">\n <div className=\"product-list-inner\">\n {/* Category Breadcrumb */}\n {categoryPath.length > 0 && (\n <nav className=\"product-list-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((cat: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\"> / </span>\n <a href={getIkasCategoryHref(cat)}>{cat.name}</a>\n </span>\n ))}\n </nav>\n )}\n\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">\n {category?.name || title}\n {category?.description && (\n <p className=\"product-list-description\">{category.description}</p>\n )}\n </h1>\n\n <div className=\"product-list-controls\">\n {/* Search */}\n <input\n className=\"product-list-search\"\n type=\"text\"\n placeholder=\"Search products...\"\n value={searchKeyword}\n onInput={(e) => handleSearch((e.target as HTMLInputElement).value)}\n />\n\n {/* Sort — use setSortType instead of direct mutation */}\n {sortOptions.length > 0 && (\n <select\n className=\"product-list-sort\"\n value={sortOptions.find((o) => o.isSelected)?.value ?? \"\"}\n onChange={(e) => handleSort((e.target as HTMLSelectElement).value)}\n >\n {sortOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )}\n </div>\n </div>\n\n <div className=\"product-list-layout\">\n {/* Filter Sidebar — mobile uses IkasThemeOverlay */}\n {showFilters && filterCategories.length > 0 && (\n <aside className=\"product-list-filters\">\n {filterCategories.map((category) => {\n const values = getFilterDisplayedValues(category);\n return (\n <div key={category.name} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{category.name}</h3>\n <div className=\"filter-values\">\n {values.map((filterValue) => (\n <label key={filterValue.name} className=\"filter-value\">\n <input\n type=\"checkbox\"\n checked={filterValue.isSelected}\n onChange={() =>\n handleFilterValueClick(productList, category, filterValue)\n }\n />\n <span>{filterValue.name}</span>\n {filterValue.count != null && (\n <span className=\"filter-count\">({filterValue.count})</span>\n )}\n </label>\n ))}\n </div>\n {/* Category-level filter click */}\n {category.isSelected !== undefined && (\n <button\n className=\"filter-category-btn\"\n onClick={() => onFilterCategoryClick(productList, category as any, true)}\n >\n {category.isSelected ? \"Clear\" : \"Apply\"}\n </button>\n )}\n </div>\n );\n })}\n </aside>\n )}\n\n {/* Product Grid — IkasThemeInfiniteScroller pattern for infinite scroll */}\n <div className=\"product-grid\">\n {isEmpty(products) && (\n <p className=\"product-grid-empty\">No products found.</p>\n )}\n {products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const image = getProductVariantMainImage(variant) as unknown as IkasImage | 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\nexport default observer(ProductListSection);\n"
|
|
17044
|
+
},
|
|
17045
|
+
{
|
|
17046
|
+
"filename": "types.ts",
|
|
17047
|
+
"content": "import { IkasProductList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n productList: IkasProductList;\n title?: string;\n showFilters?: boolean;\n}\n"
|
|
17048
|
+
},
|
|
17049
|
+
{
|
|
17050
|
+
"filename": "styles.css",
|
|
17051
|
+
"content": ".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"
|
|
17052
|
+
},
|
|
17053
|
+
{
|
|
17054
|
+
"filename": "ikas-config-snippet.json",
|
|
17055
|
+
"content": "{\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"
|
|
17056
|
+
}
|
|
17057
|
+
]
|
|
17058
|
+
},
|
|
17059
|
+
{
|
|
17060
|
+
"id": "product-reviews-section",
|
|
17061
|
+
"title": "Product Reviews Section (Complete)",
|
|
17062
|
+
"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. Uses observer for reactive review data.",
|
|
17063
|
+
"code": "import { useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nfunction 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\nexport default observer(ProductReviewsSection);\n",
|
|
17064
|
+
"relatedFunctions": [
|
|
17065
|
+
"getProductCustomerReviews",
|
|
17066
|
+
"getIkasProductCustomerReviewForm",
|
|
17067
|
+
"setCustomerReviewFormTitle",
|
|
17068
|
+
"setCustomerReviewFormStar",
|
|
17069
|
+
"setCustomerReviewFormComment",
|
|
17070
|
+
"submitCustomerReviewForm",
|
|
17071
|
+
"isCustomerReviewLoginRequired",
|
|
17072
|
+
"getIkasCustomerReviewFormattedDate",
|
|
17073
|
+
"customerStore",
|
|
17074
|
+
"hasCustomer",
|
|
17075
|
+
"Router.navigateToPage"
|
|
17076
|
+
],
|
|
17077
|
+
"categories": [
|
|
17078
|
+
"ProductDetail",
|
|
17079
|
+
"Form",
|
|
17080
|
+
"Customer"
|
|
17081
|
+
],
|
|
17082
|
+
"files": [
|
|
17083
|
+
{
|
|
17084
|
+
"filename": "index.tsx",
|
|
17085
|
+
"content": "import { useState } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\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\nfunction 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\nexport default observer(ProductReviewsSection);\n"
|
|
17086
|
+
},
|
|
17087
|
+
{
|
|
17088
|
+
"filename": "types.ts",
|
|
17089
|
+
"content": "import { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product: IkasProduct;\n title?: string;\n}\n"
|
|
17090
|
+
},
|
|
17091
|
+
{
|
|
17092
|
+
"filename": "styles.css",
|
|
17093
|
+
"content": ".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"
|
|
17094
|
+
},
|
|
17095
|
+
{
|
|
17096
|
+
"filename": "ikas-config-snippet.json",
|
|
17097
|
+
"content": "{\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"
|
|
17098
|
+
}
|
|
17099
|
+
]
|
|
17100
|
+
},
|
|
17101
|
+
{
|
|
17102
|
+
"id": "register-section",
|
|
17103
|
+
"title": "Register Section (Complete)",
|
|
17104
|
+
"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.",
|
|
17105
|
+
"code": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getRegisterForm,\n initRegisterForm,\n setRegisterFormEmail,\n setRegisterFormFirstName,\n setRegisterFormLastName,\n setRegisterFormPassword,\n submitRegisterForm,\n setRegisterFormIsMarketingAccepted,\n setRegisterFormIsMembershipAgreementAccepted,\n socialLogin,\n navigateToPage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction 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\nexport default observer(RegisterSection);\n",
|
|
17106
|
+
"relatedFunctions": [
|
|
17107
|
+
"initRegisterForm",
|
|
17108
|
+
"setRegisterFormEmail",
|
|
17109
|
+
"setRegisterFormFirstName",
|
|
17110
|
+
"setRegisterFormLastName",
|
|
17111
|
+
"setRegisterFormPassword",
|
|
17112
|
+
"setRegisterFormIsMarketingAccepted",
|
|
17113
|
+
"setRegisterFormIsMembershipAgreementAccepted",
|
|
17114
|
+
"submitRegisterForm",
|
|
17115
|
+
"socialLogin",
|
|
17116
|
+
"customerStore",
|
|
17117
|
+
"navigateToPage"
|
|
17118
|
+
],
|
|
17119
|
+
"categories": [
|
|
17120
|
+
"Customer",
|
|
17121
|
+
"Register",
|
|
17122
|
+
"Form"
|
|
17123
|
+
],
|
|
17124
|
+
"files": [
|
|
17125
|
+
{
|
|
17126
|
+
"filename": "index.tsx",
|
|
17127
|
+
"content": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getRegisterForm,\n initRegisterForm,\n setRegisterFormEmail,\n setRegisterFormFirstName,\n setRegisterFormLastName,\n setRegisterFormPassword,\n submitRegisterForm,\n setRegisterFormIsMarketingAccepted,\n setRegisterFormIsMembershipAgreementAccepted,\n socialLogin,\n navigateToPage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nfunction 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\nexport default observer(RegisterSection);\n"
|
|
17128
|
+
},
|
|
17129
|
+
{
|
|
17130
|
+
"filename": "types.ts",
|
|
17131
|
+
"content": "export interface Props {\n redirectAfterRegister?: string;\n}\n"
|
|
17132
|
+
},
|
|
17133
|
+
{
|
|
17134
|
+
"filename": "styles.css",
|
|
17135
|
+
"content": ".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"
|
|
17136
|
+
},
|
|
17137
|
+
{
|
|
17138
|
+
"filename": "ikas-config-snippet.json",
|
|
17139
|
+
"content": "{\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"
|
|
17140
|
+
}
|
|
17141
|
+
]
|
|
17142
|
+
},
|
|
17143
|
+
{
|
|
17144
|
+
"id": "reset-password-section",
|
|
17145
|
+
"title": "Reset Password Section (Complete)",
|
|
17146
|
+
"description": "Complete reset password section with new password and confirm password fields. Uses getRecoverPasswordForm/initRecoverPasswordForm/setRecoverPasswordFormPassword/setRecoverPasswordFormPasswordAgain/submitRecoverPasswordForm pattern.",
|
|
17147
|
+
"code": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getRecoverPasswordForm,\n initRecoverPasswordForm,\n setRecoverPasswordFormPassword,\n setRecoverPasswordFormPasswordAgain,\n submitRecoverPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\n\nfunction 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\nexport default observer(ResetPasswordSection);\n",
|
|
17148
|
+
"relatedFunctions": [
|
|
17149
|
+
"getRecoverPasswordForm",
|
|
17150
|
+
"initRecoverPasswordForm",
|
|
17151
|
+
"setRecoverPasswordFormPassword",
|
|
17152
|
+
"setRecoverPasswordFormPasswordAgain",
|
|
17153
|
+
"submitRecoverPasswordForm",
|
|
17154
|
+
"customerStore",
|
|
17155
|
+
"Router.navigateToPage"
|
|
17156
|
+
],
|
|
17157
|
+
"categories": [
|
|
17158
|
+
"Customer",
|
|
17159
|
+
"Form"
|
|
17160
|
+
],
|
|
17161
|
+
"files": [
|
|
17162
|
+
{
|
|
17163
|
+
"filename": "index.tsx",
|
|
17164
|
+
"content": "import { useEffect } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n customerStore,\n getRecoverPasswordForm,\n initRecoverPasswordForm,\n setRecoverPasswordFormPassword,\n setRecoverPasswordFormPasswordAgain,\n submitRecoverPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\n\nfunction 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\nexport default observer(ResetPasswordSection);\n"
|
|
17165
|
+
},
|
|
17166
|
+
{
|
|
17167
|
+
"filename": "types.ts",
|
|
17168
|
+
"content": "export interface Props {}\n"
|
|
17169
|
+
},
|
|
17170
|
+
{
|
|
17171
|
+
"filename": "styles.css",
|
|
17172
|
+
"content": ".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"
|
|
17173
|
+
},
|
|
17174
|
+
{
|
|
17175
|
+
"filename": "ikas-config-snippet.json",
|
|
17176
|
+
"content": "{\n \"id\": \"reset-password\",\n \"name\": \"Reset Password\",\n \"type\": \"section\",\n \"props\": []\n}\n"
|
|
17177
|
+
}
|
|
17178
|
+
]
|
|
17179
|
+
},
|
|
16427
17180
|
{
|
|
16428
17181
|
"id": "variant-selection",
|
|
16429
17182
|
"title": "Variant Selection",
|
|
16430
|
-
"description": "Display variant options (
|
|
16431
|
-
"code": "import { getDisplayedProductVariantTypes
|
|
17183
|
+
"description": "Display variant options with type-specific rendering: color swatches (isColorVariantValue), image thumbnails (isImageVariantValue + getIkasVariantValueThumbnailImage), or text buttons (isTextVariantValue). Uses selectVariantValue with disableRoute option.",
|
|
17184
|
+
"code": "import { observer } from \"@ikas/component-utils\";\nimport {\n getDisplayedProductVariantTypes,\n selectVariantValue,\n isColorVariantValue,\n isImageVariantValue,\n isTextVariantValue,\n getIkasVariantValueThumbnailImage,\n getDefaultSrc,\n isNotEmpty,\n IkasProduct,\n IkasImage,\n} from \"@ikas/bp-storefront\";\n\nfunction VariantSelector({ product }: { product: IkasProduct }) {\n const variantTypes = getDisplayedProductVariantTypes(product);\n\n if (!isNotEmpty(variantTypes)) return null;\n\n return (\n <div className=\"variant-selector\">\n {variantTypes.map((vt) => (\n <div key={vt.variantType.id} className=\"variant-group\">\n <label className=\"variant-group-label\">{vt.variantType.name}</label>\n <div className=\"variant-options\">\n {vt.displayedVariantValues.map((dvv) => {\n const vv = dvv.variantValue;\n\n // Color variant — show color swatch\n if (isColorVariantValue(vv)) {\n return (\n <button\n key={vv.id}\n className={`variant-color-btn ${dvv.isSelected ? \"selected\" : \"\"}`}\n disabled={!dvv.hasStock}\n onClick={() => selectVariantValue(product, vv, { disableRoute: true })}\n title={vv.name}\n style={{\n backgroundColor: vv.colorCode || \"#ccc\",\n }}\n />\n );\n }\n\n // Image variant — show thumbnail image\n if (isImageVariantValue(vv)) {\n const thumbImage = getIkasVariantValueThumbnailImage(vv) as unknown as IkasImage | undefined;\n return (\n <button\n key={vv.id}\n className={`variant-image-btn ${dvv.isSelected ? \"selected\" : \"\"}`}\n disabled={!dvv.hasStock}\n onClick={() => selectVariantValue(product, vv, { disableRoute: true })}\n title={vv.name}\n >\n {thumbImage && (\n <img src={getDefaultSrc(thumbImage)} alt={vv.name} />\n )}\n </button>\n );\n }\n\n // Text variant (default) — show text button\n return (\n <button\n key={vv.id}\n className={`variant-text-btn ${dvv.isSelected ? \"selected\" : \"\"}`}\n disabled={!dvv.hasStock}\n onClick={() => selectVariantValue(product, vv, { disableRoute: true })}\n >\n {vv.name}\n </button>\n );\n })}\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nexport default observer(VariantSelector);\n",
|
|
16432
17185
|
"relatedFunctions": [
|
|
16433
17186
|
"getDisplayedProductVariantTypes",
|
|
16434
|
-
"selectVariantValue"
|
|
17187
|
+
"selectVariantValue",
|
|
17188
|
+
"isColorVariantValue",
|
|
17189
|
+
"isImageVariantValue",
|
|
17190
|
+
"isTextVariantValue",
|
|
17191
|
+
"getIkasVariantValueThumbnailImage",
|
|
17192
|
+
"isNotEmpty",
|
|
17193
|
+
"getDefaultSrc"
|
|
16435
17194
|
],
|
|
16436
17195
|
"categories": [
|
|
16437
17196
|
"ProductDetail"
|