@ikas/code-components-mcp 0.36.0 → 0.40.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/section-templates.json +12 -12
- package/data/storefront-api.json +237 -25
- package/data/storefront-types.json +82 -7
- 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-23T11:53:46.539Z",
|
|
3
3
|
"functions": [
|
|
4
4
|
{
|
|
5
5
|
"name": "apiListBlog",
|
|
@@ -9700,6 +9700,34 @@
|
|
|
9700
9700
|
"IkasProductList"
|
|
9701
9701
|
]
|
|
9702
9702
|
},
|
|
9703
|
+
{
|
|
9704
|
+
"name": "isProductListPurchasedTogether",
|
|
9705
|
+
"displayName": "isPurchasedTogether",
|
|
9706
|
+
"signature": "function isProductListPurchasedTogether(list: IkasProductList): boolean",
|
|
9707
|
+
"description": "Checks whether the product list is of the PURCHASED_TOGETHER type (showing products purchased together).",
|
|
9708
|
+
"params": [
|
|
9709
|
+
{
|
|
9710
|
+
"name": "list",
|
|
9711
|
+
"description": "- The product list to check"
|
|
9712
|
+
}
|
|
9713
|
+
],
|
|
9714
|
+
"returns": "True if the product list type is PURCHASED_TOGETHER",
|
|
9715
|
+
"returnType": "boolean",
|
|
9716
|
+
"example": "```typescript\nimport { isProductListPurchasedTogether } from \"@ikas/bp-storefront\";\nif (isProductListPurchasedTogether(productList)) {\n // Show \"Purchased Together\" section\n}\n```",
|
|
9717
|
+
"categories": [
|
|
9718
|
+
"ProductList"
|
|
9719
|
+
],
|
|
9720
|
+
"related": [
|
|
9721
|
+
"isProductListStatic",
|
|
9722
|
+
"isProductListRecommended",
|
|
9723
|
+
"isProductListViewedTogether"
|
|
9724
|
+
],
|
|
9725
|
+
"isAsync": false,
|
|
9726
|
+
"isClass": false,
|
|
9727
|
+
"parameterTypes": [
|
|
9728
|
+
"IkasProductList"
|
|
9729
|
+
]
|
|
9730
|
+
},
|
|
9703
9731
|
{
|
|
9704
9732
|
"name": "isProductListSearch",
|
|
9705
9733
|
"displayName": "isSearch",
|
|
@@ -10103,6 +10131,21 @@
|
|
|
10103
10131
|
"IkasProductListSelectedFilterValues"
|
|
10104
10132
|
]
|
|
10105
10133
|
},
|
|
10134
|
+
{
|
|
10135
|
+
"name": "productListToggleFilterCollapsed",
|
|
10136
|
+
"displayName": "toggleFilterCollapsed",
|
|
10137
|
+
"signature": "function productListToggleFilterCollapsed(filter: IkasProductFilter): void",
|
|
10138
|
+
"description": "",
|
|
10139
|
+
"params": [],
|
|
10140
|
+
"returnType": "void",
|
|
10141
|
+
"categories": [],
|
|
10142
|
+
"related": [],
|
|
10143
|
+
"isAsync": false,
|
|
10144
|
+
"isClass": false,
|
|
10145
|
+
"parameterTypes": [
|
|
10146
|
+
"IkasProductFilter"
|
|
10147
|
+
]
|
|
10148
|
+
},
|
|
10106
10149
|
{
|
|
10107
10150
|
"name": "getProductListPrevPage",
|
|
10108
10151
|
"displayName": "getPrevPage",
|
|
@@ -13567,6 +13610,36 @@
|
|
|
13567
13610
|
"isAsync": false,
|
|
13568
13611
|
"isClass": false
|
|
13569
13612
|
},
|
|
13613
|
+
{
|
|
13614
|
+
"name": "bs_searchProductById",
|
|
13615
|
+
"displayName": "searchProductById",
|
|
13616
|
+
"signature": "async function bs_searchProductById(baseStore: BaseStore, params: { productId: string }): Promise<{\n isSuccess: boolean;\n product: IkasProduct | null;\n}>",
|
|
13617
|
+
"description": "",
|
|
13618
|
+
"params": [],
|
|
13619
|
+
"returnType": "Promise<{\n isSuccess: boolean;\n product: IkasProduct | null;\n}>",
|
|
13620
|
+
"categories": [],
|
|
13621
|
+
"related": [],
|
|
13622
|
+
"isAsync": true,
|
|
13623
|
+
"isClass": false,
|
|
13624
|
+
"parameterTypes": [
|
|
13625
|
+
"IkasProduct"
|
|
13626
|
+
]
|
|
13627
|
+
},
|
|
13628
|
+
{
|
|
13629
|
+
"name": "bs_searchProductsById",
|
|
13630
|
+
"displayName": "searchProductsById",
|
|
13631
|
+
"signature": "async function bs_searchProductsById(baseStore: BaseStore, params: { productIds: string[] }): Promise<{\n isSuccess: boolean;\n products: IkasProduct[] | null;\n}>",
|
|
13632
|
+
"description": "",
|
|
13633
|
+
"params": [],
|
|
13634
|
+
"returnType": "Promise<{\n isSuccess: boolean;\n products: IkasProduct[] | null;\n}>",
|
|
13635
|
+
"categories": [],
|
|
13636
|
+
"related": [],
|
|
13637
|
+
"isAsync": true,
|
|
13638
|
+
"isClass": false,
|
|
13639
|
+
"parameterTypes": [
|
|
13640
|
+
"IkasProduct"
|
|
13641
|
+
]
|
|
13642
|
+
},
|
|
13570
13643
|
{
|
|
13571
13644
|
"name": "getCheckoutUrlFromCartStore",
|
|
13572
13645
|
"displayName": "getCheckoutUrl",
|
|
@@ -15188,6 +15261,115 @@
|
|
|
15188
15261
|
"IkasCustomerStore"
|
|
15189
15262
|
]
|
|
15190
15263
|
},
|
|
15264
|
+
{
|
|
15265
|
+
"name": "cs_listEarningMethods",
|
|
15266
|
+
"displayName": "listEarningMethods",
|
|
15267
|
+
"signature": "async function cs_listEarningMethods(customerStore: IkasCustomerStore): Promise<IkasLoyaltyProgramEarningMethod[] | undefined>",
|
|
15268
|
+
"description": "",
|
|
15269
|
+
"params": [],
|
|
15270
|
+
"returnType": "Promise<IkasLoyaltyProgramEarningMethod[] | undefined>",
|
|
15271
|
+
"categories": [],
|
|
15272
|
+
"related": [],
|
|
15273
|
+
"isAsync": true,
|
|
15274
|
+
"isClass": false,
|
|
15275
|
+
"parameterTypes": [
|
|
15276
|
+
"IkasCustomerStore",
|
|
15277
|
+
"IkasLoyaltyProgramEarningMethod"
|
|
15278
|
+
]
|
|
15279
|
+
},
|
|
15280
|
+
{
|
|
15281
|
+
"name": "cs_listSpendingMethodsByCartId",
|
|
15282
|
+
"displayName": "listSpendingMethods",
|
|
15283
|
+
"signature": "async function cs_listSpendingMethodsByCartId(customerStore: IkasCustomerStore): Promise<IkasLoyaltyProgramSpendingMethod[] | undefined>",
|
|
15284
|
+
"description": "",
|
|
15285
|
+
"params": [],
|
|
15286
|
+
"returnType": "Promise<IkasLoyaltyProgramSpendingMethod[] | undefined>",
|
|
15287
|
+
"categories": [],
|
|
15288
|
+
"related": [],
|
|
15289
|
+
"isAsync": true,
|
|
15290
|
+
"isClass": false,
|
|
15291
|
+
"parameterTypes": [
|
|
15292
|
+
"IkasCustomerStore",
|
|
15293
|
+
"IkasLoyaltyProgramSpendingMethod"
|
|
15294
|
+
]
|
|
15295
|
+
},
|
|
15296
|
+
{
|
|
15297
|
+
"name": "cs_removeLoyaltyPointsFromCart",
|
|
15298
|
+
"displayName": "removeLoyaltyPointsFromCart",
|
|
15299
|
+
"signature": "async function cs_removeLoyaltyPointsFromCart(customerStore: IkasCustomerStore): Promise<void>",
|
|
15300
|
+
"description": "",
|
|
15301
|
+
"params": [],
|
|
15302
|
+
"returnType": "Promise<void>",
|
|
15303
|
+
"categories": [],
|
|
15304
|
+
"related": [],
|
|
15305
|
+
"isAsync": true,
|
|
15306
|
+
"isClass": false,
|
|
15307
|
+
"parameterTypes": [
|
|
15308
|
+
"IkasCustomerStore"
|
|
15309
|
+
]
|
|
15310
|
+
},
|
|
15311
|
+
{
|
|
15312
|
+
"name": "cs_getLoyaltyCustomerInfo",
|
|
15313
|
+
"displayName": "getLoyaltyCustomerInfo",
|
|
15314
|
+
"signature": "async function cs_getLoyaltyCustomerInfo(customerStore: IkasCustomerStore): Promise<IkasLoyaltyCustomerInfo | null>",
|
|
15315
|
+
"description": "",
|
|
15316
|
+
"params": [],
|
|
15317
|
+
"returnType": "Promise<IkasLoyaltyCustomerInfo | null>",
|
|
15318
|
+
"categories": [],
|
|
15319
|
+
"related": [],
|
|
15320
|
+
"isAsync": true,
|
|
15321
|
+
"isClass": false,
|
|
15322
|
+
"parameterTypes": [
|
|
15323
|
+
"IkasCustomerStore",
|
|
15324
|
+
"IkasLoyaltyCustomerInfo"
|
|
15325
|
+
]
|
|
15326
|
+
},
|
|
15327
|
+
{
|
|
15328
|
+
"name": "cs_listLoyaltyProgramTiers",
|
|
15329
|
+
"displayName": "listLoyaltyProgramTiers",
|
|
15330
|
+
"signature": "async function cs_listLoyaltyProgramTiers(customerStore: IkasCustomerStore): Promise<IkasLoyaltyProgramTier[] | undefined>",
|
|
15331
|
+
"description": "",
|
|
15332
|
+
"params": [],
|
|
15333
|
+
"returnType": "Promise<IkasLoyaltyProgramTier[] | undefined>",
|
|
15334
|
+
"categories": [],
|
|
15335
|
+
"related": [],
|
|
15336
|
+
"isAsync": true,
|
|
15337
|
+
"isClass": false,
|
|
15338
|
+
"parameterTypes": [
|
|
15339
|
+
"IkasCustomerStore",
|
|
15340
|
+
"IkasLoyaltyProgramTier"
|
|
15341
|
+
]
|
|
15342
|
+
},
|
|
15343
|
+
{
|
|
15344
|
+
"name": "cs_listLoyaltyProgramPointHistory",
|
|
15345
|
+
"displayName": "listLoyaltyProgramPointHistory",
|
|
15346
|
+
"signature": "async function cs_listLoyaltyProgramPointHistory(customerStore: IkasCustomerStore, params: ListLoyaltyProgramPointHistoryQueryParams): Promise<IkasLoyaltyProgramPointHistory[] | undefined>",
|
|
15347
|
+
"description": "",
|
|
15348
|
+
"params": [],
|
|
15349
|
+
"returnType": "Promise<IkasLoyaltyProgramPointHistory[] | undefined>",
|
|
15350
|
+
"categories": [],
|
|
15351
|
+
"related": [],
|
|
15352
|
+
"isAsync": true,
|
|
15353
|
+
"isClass": false,
|
|
15354
|
+
"parameterTypes": [
|
|
15355
|
+
"IkasCustomerStore",
|
|
15356
|
+
"IkasLoyaltyProgramPointHistory"
|
|
15357
|
+
]
|
|
15358
|
+
},
|
|
15359
|
+
{
|
|
15360
|
+
"name": "cs_useLoyaltyPoints",
|
|
15361
|
+
"signature": "async function cs_useLoyaltyPoints(customerStore: IkasCustomerStore, params: UseLoyaltyPointsQueryParams): Promise<CartV2 | null>",
|
|
15362
|
+
"description": "",
|
|
15363
|
+
"params": [],
|
|
15364
|
+
"returnType": "Promise<CartV2 | null>",
|
|
15365
|
+
"categories": [],
|
|
15366
|
+
"related": [],
|
|
15367
|
+
"isAsync": true,
|
|
15368
|
+
"isClass": false,
|
|
15369
|
+
"parameterTypes": [
|
|
15370
|
+
"IkasCustomerStore"
|
|
15371
|
+
]
|
|
15372
|
+
},
|
|
15191
15373
|
{
|
|
15192
15374
|
"name": "hasCustomer",
|
|
15193
15375
|
"signature": "function hasCustomer(customerStore: IkasCustomerStore): boolean",
|
|
@@ -15786,6 +15968,36 @@
|
|
|
15786
15968
|
"IkasOrder"
|
|
15787
15969
|
]
|
|
15788
15970
|
},
|
|
15971
|
+
{
|
|
15972
|
+
"name": "openIkasStorefrontWidget",
|
|
15973
|
+
"signature": "function openIkasStorefrontWidget(customerStore: IkasCustomerStore): void",
|
|
15974
|
+
"description": "",
|
|
15975
|
+
"params": [],
|
|
15976
|
+
"returnType": "void",
|
|
15977
|
+
"categories": [],
|
|
15978
|
+
"related": [],
|
|
15979
|
+
"isAsync": false,
|
|
15980
|
+
"isClass": false,
|
|
15981
|
+
"parameterTypes": [
|
|
15982
|
+
"IkasStorefrontWidget",
|
|
15983
|
+
"IkasCustomerStore"
|
|
15984
|
+
]
|
|
15985
|
+
},
|
|
15986
|
+
{
|
|
15987
|
+
"name": "closeIkasStorefrontWidget",
|
|
15988
|
+
"signature": "function closeIkasStorefrontWidget(customerStore: IkasCustomerStore): void",
|
|
15989
|
+
"description": "",
|
|
15990
|
+
"params": [],
|
|
15991
|
+
"returnType": "void",
|
|
15992
|
+
"categories": [],
|
|
15993
|
+
"related": [],
|
|
15994
|
+
"isAsync": false,
|
|
15995
|
+
"isClass": false,
|
|
15996
|
+
"parameterTypes": [
|
|
15997
|
+
"IkasStorefrontWidget",
|
|
15998
|
+
"IkasCustomerStore"
|
|
15999
|
+
]
|
|
16000
|
+
},
|
|
15789
16001
|
{
|
|
15790
16002
|
"name": "I18n.getLocale",
|
|
15791
16003
|
"signature": "static I18n.getLocale(): string",
|
|
@@ -16232,7 +16444,7 @@
|
|
|
16232
16444
|
"id": "account-info-section",
|
|
16233
16445
|
"title": "Account Info Section (Complete)",
|
|
16234
16446
|
"description": "Complete account info section with first name, last name, and phone fields. Uses getAccountInfoForm/initAccountInfoForm/setAccountInfoForm*/submitAccountInfoForm pattern.",
|
|
16235
|
-
"code": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountInfoSection({\n backgroundColor = \"#ffffff\",\n title = \"Account Information\",\n successMessage = \"Your information has been updated.\",\n submitButtonText = \"Save Changes\",\n submittingButtonText = \"Saving...\",\n}: Props) {\n const accountForm = getAccountInfoForm(customerStore);\n\n useEffect(() => {\n initAccountInfoForm(accountForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitAccountInfoForm(accountForm);\n };\n\n return (\n <section className=\"account-info-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"account-info-inner\">\n <h1 className=\"account-info-title\">{title}</h1>\n\n {accountForm.isSuccess && (\n <div className=\"account-info-success\">{successMessage}</div>\n )}\n {accountForm.isFailure && accountForm.responseMessage && (\n <div className=\"account-info-error\">{accountForm.responseMessage}</div>\n )}\n\n <form className=\"account-info-form\" onSubmit={handleSubmit}>\n <div className=\"account-info-row\">\n <div className=\"account-info-field\">\n
|
|
16447
|
+
"code": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountInfoSection({\n backgroundColor = \"#ffffff\",\n title = \"Account Information\",\n successMessage = \"Your information has been updated.\",\n submitButtonText = \"Save Changes\",\n submittingButtonText = \"Saving...\",\n}: Props) {\n const accountForm = getAccountInfoForm(customerStore);\n\n useEffect(() => {\n initAccountInfoForm(accountForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitAccountInfoForm(accountForm);\n };\n\n return (\n <section className=\"account-info-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"account-info-inner\">\n <h1 className=\"account-info-title\">{title}</h1>\n\n {accountForm.isSuccess && (\n <div className=\"account-info-success\">{successMessage}</div>\n )}\n {accountForm.isFailure && accountForm.responseMessage && (\n <div className=\"account-info-error\">{accountForm.responseMessage}</div>\n )}\n\n <form className=\"account-info-form\" onSubmit={handleSubmit}>\n <div className=\"account-info-row\">\n {accountForm.firstName && (\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 )}\n {accountForm.lastName && (\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 )}\n </div>\n {accountForm.phone && (\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 )}\n <button className=\"account-info-submit\" type=\"submit\" disabled={accountForm.isSubmitting}>\n {accountForm.isSubmitting ? submittingButtonText : submitButtonText}\n </button>\n </form>\n </div>\n </section>\n );\n}\n\n",
|
|
16236
16448
|
"relatedFunctions": [
|
|
16237
16449
|
"getAccountInfoForm",
|
|
16238
16450
|
"initAccountInfoForm",
|
|
@@ -16250,7 +16462,7 @@
|
|
|
16250
16462
|
"files": [
|
|
16251
16463
|
{
|
|
16252
16464
|
"filename": "index.tsx",
|
|
16253
|
-
"content": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountInfoSection({\n backgroundColor = \"#ffffff\",\n title = \"Account Information\",\n successMessage = \"Your information has been updated.\",\n submitButtonText = \"Save Changes\",\n submittingButtonText = \"Saving...\",\n}: Props) {\n const accountForm = getAccountInfoForm(customerStore);\n\n useEffect(() => {\n initAccountInfoForm(accountForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitAccountInfoForm(accountForm);\n };\n\n return (\n <section className=\"account-info-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"account-info-inner\">\n <h1 className=\"account-info-title\">{title}</h1>\n\n {accountForm.isSuccess && (\n <div className=\"account-info-success\">{successMessage}</div>\n )}\n {accountForm.isFailure && accountForm.responseMessage && (\n <div className=\"account-info-error\">{accountForm.responseMessage}</div>\n )}\n\n <form className=\"account-info-form\" onSubmit={handleSubmit}>\n <div className=\"account-info-row\">\n <div className=\"account-info-field\">\n
|
|
16465
|
+
"content": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountInfoSection({\n backgroundColor = \"#ffffff\",\n title = \"Account Information\",\n successMessage = \"Your information has been updated.\",\n submitButtonText = \"Save Changes\",\n submittingButtonText = \"Saving...\",\n}: Props) {\n const accountForm = getAccountInfoForm(customerStore);\n\n useEffect(() => {\n initAccountInfoForm(accountForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitAccountInfoForm(accountForm);\n };\n\n return (\n <section className=\"account-info-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"account-info-inner\">\n <h1 className=\"account-info-title\">{title}</h1>\n\n {accountForm.isSuccess && (\n <div className=\"account-info-success\">{successMessage}</div>\n )}\n {accountForm.isFailure && accountForm.responseMessage && (\n <div className=\"account-info-error\">{accountForm.responseMessage}</div>\n )}\n\n <form className=\"account-info-form\" onSubmit={handleSubmit}>\n <div className=\"account-info-row\">\n {accountForm.firstName && (\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 )}\n {accountForm.lastName && (\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 )}\n </div>\n {accountForm.phone && (\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 )}\n <button className=\"account-info-submit\" type=\"submit\" disabled={accountForm.isSubmitting}>\n {accountForm.isSubmitting ? submittingButtonText : submitButtonText}\n </button>\n </form>\n </div>\n </section>\n );\n}\n\n"
|
|
16254
16466
|
},
|
|
16255
16467
|
{
|
|
16256
16468
|
"filename": "types.ts",
|
|
@@ -16270,7 +16482,7 @@
|
|
|
16270
16482
|
"id": "account-orders-section",
|
|
16271
16483
|
"title": "Account Orders Section (Complete)",
|
|
16272
16484
|
"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.",
|
|
16273
|
-
"code": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getOrders,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderDistinctItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n isEmpty,\n isNotEmpty,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountOrdersSection({\n title = \"My Orders\",\n emptyMessage = \"You have no orders yet.\",\n backgroundColor = \"#ffffff\",\n shopButtonText = \"Start Shopping\",\n itemsText = \"items\",\n}: Props) {\n useEffect(() => {\n getOrders(customerStore);\n }, []);\n\n
|
|
16485
|
+
"code": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrders,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderDistinctItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n isEmpty,\n isNotEmpty,\n Router,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountOrdersSection({\n title = \"My Orders\",\n emptyMessage = \"You have no orders yet.\",\n backgroundColor = \"#ffffff\",\n shopButtonText = \"Start Shopping\",\n itemsText = \"items\",\n}: Props) {\n const [orders, setOrders] = useState<IkasOrder[]>([]);\n\n useEffect(() => {\n getOrders(customerStore).then(setOrders);\n }, []);\n\n return (\n <section className=\"orders-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"orders-inner\">\n <h1 className=\"orders-title\">{title}</h1>\n\n {isEmpty(orders) && (\n <div className=\"orders-empty\">\n <p>{emptyMessage}</p>\n <button\n className=\"orders-shop-btn\"\n onClick={() => Router.navigate(\"/\")}\n >\n {shopButtonText}\n </button>\n </div>\n )}\n\n {isNotEmpty(orders) && (\n <div className=\"orders-list\">\n {orders.map((order) => {\n // Show first line item thumbnail\n const firstItem = order.orderLineItems?.[0];\n const thumbnail = firstItem?.variant\n ? getIkasOrderLineVariantMainImage(firstItem.variant)\n : null;\n\n return (\n <a\n key={order.id}\n href={getIkasOrderHref(order)}\n className=\"order-card\"\n >\n <div className=\"order-card-header\">\n <span className=\"order-number\">Order #{order.orderNumber}</span>\n <span className=\"order-status\">\n {getIkasOrderPackageStatusTranslation(order)}\n </span>\n </div>\n <div className=\"order-card-body\">\n {thumbnail && (\n <img\n className=\"order-thumbnail\"\n src={getDefaultSrc(thumbnail)}\n alt=\"\"\n />\n )}\n <div className=\"order-card-details\">\n <span className=\"order-date\">\n {getIkasOrderFormattedOrderedAt(order)}\n </span>\n <span className=\"order-items\">\n {getIkasOrderDistinctItemCount(order)} {itemsText}\n </span>\n <span className=\"order-total\">\n {getIkasOrderFormattedTotalFinalPrice(order)}\n </span>\n </div>\n </div>\n </a>\n );\n })}\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
16274
16486
|
"relatedFunctions": [
|
|
16275
16487
|
"customerStore",
|
|
16276
16488
|
"getOrders",
|
|
@@ -16292,7 +16504,7 @@
|
|
|
16292
16504
|
"files": [
|
|
16293
16505
|
{
|
|
16294
16506
|
"filename": "index.tsx",
|
|
16295
|
-
"content": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getOrders,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderDistinctItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n isEmpty,\n isNotEmpty,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountOrdersSection({\n title = \"My Orders\",\n emptyMessage = \"You have no orders yet.\",\n backgroundColor = \"#ffffff\",\n shopButtonText = \"Start Shopping\",\n itemsText = \"items\",\n}: Props) {\n useEffect(() => {\n getOrders(customerStore);\n }, []);\n\n
|
|
16507
|
+
"content": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrders,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderDistinctItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n getIkasOrderLineVariantMainImage,\n getDefaultSrc,\n isEmpty,\n isNotEmpty,\n Router,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function AccountOrdersSection({\n title = \"My Orders\",\n emptyMessage = \"You have no orders yet.\",\n backgroundColor = \"#ffffff\",\n shopButtonText = \"Start Shopping\",\n itemsText = \"items\",\n}: Props) {\n const [orders, setOrders] = useState<IkasOrder[]>([]);\n\n useEffect(() => {\n getOrders(customerStore).then(setOrders);\n }, []);\n\n return (\n <section className=\"orders-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"orders-inner\">\n <h1 className=\"orders-title\">{title}</h1>\n\n {isEmpty(orders) && (\n <div className=\"orders-empty\">\n <p>{emptyMessage}</p>\n <button\n className=\"orders-shop-btn\"\n onClick={() => Router.navigate(\"/\")}\n >\n {shopButtonText}\n </button>\n </div>\n )}\n\n {isNotEmpty(orders) && (\n <div className=\"orders-list\">\n {orders.map((order) => {\n // Show first line item thumbnail\n const firstItem = order.orderLineItems?.[0];\n const thumbnail = firstItem?.variant\n ? getIkasOrderLineVariantMainImage(firstItem.variant)\n : null;\n\n return (\n <a\n key={order.id}\n href={getIkasOrderHref(order)}\n className=\"order-card\"\n >\n <div className=\"order-card-header\">\n <span className=\"order-number\">Order #{order.orderNumber}</span>\n <span className=\"order-status\">\n {getIkasOrderPackageStatusTranslation(order)}\n </span>\n </div>\n <div className=\"order-card-body\">\n {thumbnail && (\n <img\n className=\"order-thumbnail\"\n src={getDefaultSrc(thumbnail)}\n alt=\"\"\n />\n )}\n <div className=\"order-card-details\">\n <span className=\"order-date\">\n {getIkasOrderFormattedOrderedAt(order)}\n </span>\n <span className=\"order-items\">\n {getIkasOrderDistinctItemCount(order)} {itemsText}\n </span>\n <span className=\"order-total\">\n {getIkasOrderFormattedTotalFinalPrice(order)}\n </span>\n </div>\n </div>\n </a>\n );\n })}\n </div>\n )}\n </div>\n </section>\n );\n}\n\n"
|
|
16296
16508
|
},
|
|
16297
16509
|
{
|
|
16298
16510
|
"filename": "types.ts",
|
|
@@ -16349,7 +16561,7 @@
|
|
|
16349
16561
|
"id": "blog-list-section",
|
|
16350
16562
|
"title": "Blog List Section (Complete)",
|
|
16351
16563
|
"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.",
|
|
16352
|
-
"code": "import {\n IkasBlogList,\n hasBlogListNextPage,\n getBlogListNextPage,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function BlogListSection({\n blogList,\n title = \"Blog\",\n backgroundColor = \"#ffffff\",\n emptyMessage = \"No blog posts found.\",\n readMoreText = \"Read more\",\n loadMoreText = \"Load More\",\n}: Props) {\n if (!blogList) return null;\n\n const blogs = blogList.data ?? [];\n const hasNext = hasBlogListNextPage(blogList);\n // Note: dynavit uses only forward pagination (infinite scroll with IkasThemeInfiniteScroller)\n // No hasBlogListPrevPage/getBlogListPrevPage — scroll-only pattern\n\n return (\n <section className=\"blog-list-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"blog-list-inner\">\n <h1 className=\"blog-list-title\">{title}</h1>\n\n {blogs.length === 0 && (\n <p className=\"blog-list-empty\">{emptyMessage}</p>\n )}\n\n {/* Blog Grid — production uses IkasThemeInfiniteScroller for infinite scroll */}\n <div className=\"blog-grid\">\n {blogs.map((blog) => (\n <a\n key={blog.id}\n href={getIkasBlogHref(blog)}\n className=\"blog-card\"\n >\n {blog.image && (\n <div className=\"blog-card-image-wrap\">\n <img\n src={getDefaultSrc(blog.image)}\n alt={blog.title}\n className=\"blog-card-image\"\n />\n </div>\n )}\n <div className=\"blog-card-content\">\n <span className=\"blog-card-date\">\n {getIkasBlogFormattedDate(blog)}\n </span>\n <h3 className=\"blog-card-title\">{blog.title}</h3>\n {blog.
|
|
16564
|
+
"code": "import {\n IkasBlogList,\n hasBlogListNextPage,\n getBlogListNextPage,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function BlogListSection({\n blogList,\n title = \"Blog\",\n backgroundColor = \"#ffffff\",\n emptyMessage = \"No blog posts found.\",\n readMoreText = \"Read more\",\n loadMoreText = \"Load More\",\n}: Props) {\n if (!blogList) return null;\n\n const blogs = blogList.data ?? [];\n const hasNext = hasBlogListNextPage(blogList);\n // Note: dynavit uses only forward pagination (infinite scroll with IkasThemeInfiniteScroller)\n // No hasBlogListPrevPage/getBlogListPrevPage — scroll-only pattern\n\n return (\n <section className=\"blog-list-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"blog-list-inner\">\n <h1 className=\"blog-list-title\">{title}</h1>\n\n {blogs.length === 0 && (\n <p className=\"blog-list-empty\">{emptyMessage}</p>\n )}\n\n {/* Blog Grid — production uses IkasThemeInfiniteScroller for infinite scroll */}\n <div className=\"blog-grid\">\n {blogs.map((blog) => (\n <a\n key={blog.id}\n href={getIkasBlogHref(blog)}\n className=\"blog-card\"\n >\n {blog.image && (\n <div className=\"blog-card-image-wrap\">\n <img\n src={getDefaultSrc(blog.image)}\n alt={blog.title}\n className=\"blog-card-image\"\n />\n </div>\n )}\n <div className=\"blog-card-content\">\n <span className=\"blog-card-date\">\n {getIkasBlogFormattedDate(blog)}\n </span>\n <h3 className=\"blog-card-title\">{blog.title}</h3>\n {blog.shortDescription && (\n <p className=\"blog-card-summary\">{blog.shortDescription}</p>\n )}\n <span className=\"blog-card-read-more\">{readMoreText}</span>\n </div>\n </a>\n ))}\n </div>\n\n {/* Load More — infinite scroll pattern */}\n {hasNext && (\n <div className=\"blog-load-more\">\n <button\n className=\"blog-load-more-btn\"\n onClick={() => getBlogListNextPage(blogList)}\n >\n {loadMoreText}\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
16353
16565
|
"relatedFunctions": [
|
|
16354
16566
|
"hasBlogListNextPage",
|
|
16355
16567
|
"getBlogListNextPage",
|
|
@@ -16364,7 +16576,7 @@
|
|
|
16364
16576
|
"files": [
|
|
16365
16577
|
{
|
|
16366
16578
|
"filename": "index.tsx",
|
|
16367
|
-
"content": "import {\n IkasBlogList,\n hasBlogListNextPage,\n getBlogListNextPage,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function BlogListSection({\n blogList,\n title = \"Blog\",\n backgroundColor = \"#ffffff\",\n emptyMessage = \"No blog posts found.\",\n readMoreText = \"Read more\",\n loadMoreText = \"Load More\",\n}: Props) {\n if (!blogList) return null;\n\n const blogs = blogList.data ?? [];\n const hasNext = hasBlogListNextPage(blogList);\n // Note: dynavit uses only forward pagination (infinite scroll with IkasThemeInfiniteScroller)\n // No hasBlogListPrevPage/getBlogListPrevPage — scroll-only pattern\n\n return (\n <section className=\"blog-list-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"blog-list-inner\">\n <h1 className=\"blog-list-title\">{title}</h1>\n\n {blogs.length === 0 && (\n <p className=\"blog-list-empty\">{emptyMessage}</p>\n )}\n\n {/* Blog Grid — production uses IkasThemeInfiniteScroller for infinite scroll */}\n <div className=\"blog-grid\">\n {blogs.map((blog) => (\n <a\n key={blog.id}\n href={getIkasBlogHref(blog)}\n className=\"blog-card\"\n >\n {blog.image && (\n <div className=\"blog-card-image-wrap\">\n <img\n src={getDefaultSrc(blog.image)}\n alt={blog.title}\n className=\"blog-card-image\"\n />\n </div>\n )}\n <div className=\"blog-card-content\">\n <span className=\"blog-card-date\">\n {getIkasBlogFormattedDate(blog)}\n </span>\n <h3 className=\"blog-card-title\">{blog.title}</h3>\n {blog.
|
|
16579
|
+
"content": "import {\n IkasBlogList,\n hasBlogListNextPage,\n getBlogListNextPage,\n getIkasBlogFormattedDate,\n getIkasBlogHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function BlogListSection({\n blogList,\n title = \"Blog\",\n backgroundColor = \"#ffffff\",\n emptyMessage = \"No blog posts found.\",\n readMoreText = \"Read more\",\n loadMoreText = \"Load More\",\n}: Props) {\n if (!blogList) return null;\n\n const blogs = blogList.data ?? [];\n const hasNext = hasBlogListNextPage(blogList);\n // Note: dynavit uses only forward pagination (infinite scroll with IkasThemeInfiniteScroller)\n // No hasBlogListPrevPage/getBlogListPrevPage — scroll-only pattern\n\n return (\n <section className=\"blog-list-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"blog-list-inner\">\n <h1 className=\"blog-list-title\">{title}</h1>\n\n {blogs.length === 0 && (\n <p className=\"blog-list-empty\">{emptyMessage}</p>\n )}\n\n {/* Blog Grid — production uses IkasThemeInfiniteScroller for infinite scroll */}\n <div className=\"blog-grid\">\n {blogs.map((blog) => (\n <a\n key={blog.id}\n href={getIkasBlogHref(blog)}\n className=\"blog-card\"\n >\n {blog.image && (\n <div className=\"blog-card-image-wrap\">\n <img\n src={getDefaultSrc(blog.image)}\n alt={blog.title}\n className=\"blog-card-image\"\n />\n </div>\n )}\n <div className=\"blog-card-content\">\n <span className=\"blog-card-date\">\n {getIkasBlogFormattedDate(blog)}\n </span>\n <h3 className=\"blog-card-title\">{blog.title}</h3>\n {blog.shortDescription && (\n <p className=\"blog-card-summary\">{blog.shortDescription}</p>\n )}\n <span className=\"blog-card-read-more\">{readMoreText}</span>\n </div>\n </a>\n ))}\n </div>\n\n {/* Load More — infinite scroll pattern */}\n {hasNext && (\n <div className=\"blog-load-more\">\n <button\n className=\"blog-load-more-btn\"\n onClick={() => getBlogListNextPage(blogList)}\n >\n {loadMoreText}\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n"
|
|
16368
16580
|
},
|
|
16369
16581
|
{
|
|
16370
16582
|
"filename": "types.ts",
|
|
@@ -16436,7 +16648,7 @@
|
|
|
16436
16648
|
"id": "bundle-products",
|
|
16437
16649
|
"title": "Bundle / Offer Products",
|
|
16438
16650
|
"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.",
|
|
16439
|
-
"code": "import { useEffect } from \"preact/hooks\";\nimport {\n hasBundleSettings,\n initBundleProducts,\n getDisplayedProductGroups,\n acceptProductOffer,\n rejectProductOffer,\n isAcceptedProductOffer,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n isIkasVariantTypeColorSelection,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getDefaultSrc,\n getProductVariantMainImage,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function BundleProducts({\n product,\n title = \"Complete Your Purchase\",\n removeText = \"Remove\",\n addText = \"Add\",\n}: Props) {\n const variant = getSelectedProductVariant(product);\n const hasBundle = variant ? (hasBundleSettings(variant) as unknown as boolean) : false;\n\n useEffect(() => {\n if (hasBundle) {\n initBundleProducts(product);\n }\n }, [hasBundle]);\n\n if (!hasBundle) return null;\n\n const productGroups = getDisplayedProductGroups(product);\n\n return (\n <div className=\"bundle-products\">\n <h3>{title}</h3>\n {productGroups.map((group: any, gi: number) => (\n <div key={gi} className=\"bundle-group\">\n {group.title && <h4>{group.title}</h4>}\n <div className=\"bundle-offers\">\n {group.offers?.map((offer: any) => {\n const isAccepted = isAcceptedProductOffer(offer) as unknown as boolean;\n const offerProduct = offer.product;\n if (!offerProduct) return null;\n\n const offerVariant = getSelectedProductVariant(offerProduct);\n const offerProductImage = getProductVariantMainImage(offerVariant);\n const offerImage = offerProductImage?.image ?? null;\n const offerPrice = getProductVariantFormattedFinalPrice(offerVariant) as unknown as string;\n\n // Variant types for offer product\n const offerVariantTypes = getDisplayedProductVariantTypes(offerProduct);\n\n return (\n <div key={offer.id || offerProduct.id} className=\"bundle-offer-card\">\n {offerImage && (\n <img src={getDefaultSrc(offerImage)} alt={offerProduct.name} style={{ width: 80, height: 80, objectFit: \"cover\", borderRadius: 6 }} />\n )}\n <div className=\"bundle-offer-info\">\n <span className=\"bundle-offer-name\">{offerProduct.name}</span>\n <span className=\"bundle-offer-price\">{offerPrice}</span>\n\n {/* Variant selection for offer products */}\n {offerVariantTypes.map((vt: any) => {\n const isColor = isIkasVariantTypeColorSelection(vt.variantType);\n return (\n <div key={vt.variantType.id} style={{ display: \"flex\", gap: 4, marginTop: 4 }}>\n {vt.displayedVariantValues.map((dvv: any) => (\n <button\n key={dvv.variantValue.id}\n style={{\n width: isColor ? 24 : \"auto\",\n height: isColor ? 24 : \"auto\",\n borderRadius: isColor ? \"50%\" : 4,\n backgroundColor: isColor ? dvv.variantValue.colorCode : \"#fff\",\n border: dvv.isSelected ? \"2px solid #111\" : \"1px solid #ddd\",\n padding: isColor ? 0 : \"4px 8px\",\n fontSize: 12,\n cursor: \"pointer\",\n }}\n onClick={() => selectVariantValue(offerProduct, dvv.variantValue,
|
|
16651
|
+
"code": "import { useEffect } from \"preact/hooks\";\nimport {\n hasBundleSettings,\n initBundleProducts,\n getDisplayedProductGroups,\n acceptProductOffer,\n rejectProductOffer,\n isAcceptedProductOffer,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n isIkasVariantTypeColorSelection,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getDefaultSrc,\n getProductVariantMainImage,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function BundleProducts({\n product,\n title = \"Complete Your Purchase\",\n removeText = \"Remove\",\n addText = \"Add\",\n}: Props) {\n const variant = getSelectedProductVariant(product);\n const hasBundle = variant ? (hasBundleSettings(variant) as unknown as boolean) : false;\n\n useEffect(() => {\n if (hasBundle) {\n initBundleProducts(product);\n }\n }, [hasBundle]);\n\n if (!hasBundle) return null;\n\n const productGroups = getDisplayedProductGroups(product);\n\n return (\n <div className=\"bundle-products\">\n <h3>{title}</h3>\n {productGroups.map((group: any, gi: number) => (\n <div key={gi} className=\"bundle-group\">\n {group.title && <h4>{group.title}</h4>}\n <div className=\"bundle-offers\">\n {group.offers?.map((offer: any) => {\n const isAccepted = isAcceptedProductOffer(offer) as unknown as boolean;\n const offerProduct = offer.product;\n if (!offerProduct) return null;\n\n const offerVariant = getSelectedProductVariant(offerProduct);\n const offerProductImage = getProductVariantMainImage(offerVariant);\n const offerImage = offerProductImage?.image ?? null;\n const offerPrice = getProductVariantFormattedFinalPrice(offerVariant) as unknown as string;\n\n // Variant types for offer product\n const offerVariantTypes = getDisplayedProductVariantTypes(offerProduct);\n\n return (\n <div key={offer.id || offerProduct.id} className=\"bundle-offer-card\">\n {offerImage && (\n <img src={getDefaultSrc(offerImage)} alt={offerProduct.name} style={{ width: 80, height: 80, objectFit: \"cover\", borderRadius: 6 }} />\n )}\n <div className=\"bundle-offer-info\">\n <span className=\"bundle-offer-name\">{offerProduct.name}</span>\n <span className=\"bundle-offer-price\">{offerPrice}</span>\n\n {/* Variant selection for offer products */}\n {offerVariantTypes.map((vt: any) => {\n const isColor = isIkasVariantTypeColorSelection(vt.variantType);\n return (\n <div key={vt.variantType.id} style={{ display: \"flex\", gap: 4, marginTop: 4 }}>\n {vt.displayedVariantValues.map((dvv: any) => (\n <button\n key={dvv.variantValue.id}\n style={{\n width: isColor ? 24 : \"auto\",\n height: isColor ? 24 : \"auto\",\n borderRadius: isColor ? \"50%\" : 4,\n backgroundColor: isColor ? dvv.variantValue.colorCode : \"#fff\",\n border: dvv.isSelected ? \"2px solid #111\" : \"1px solid #ddd\",\n padding: isColor ? 0 : \"4px 8px\",\n fontSize: 12,\n cursor: \"pointer\",\n }}\n onClick={() => selectVariantValue(offerProduct, dvv.variantValue, true)}\n >\n {!isColor && dvv.variantValue.name}\n </button>\n ))}\n </div>\n );\n })}\n </div>\n <button\n className={`bundle-offer-toggle ${isAccepted ? \"accepted\" : \"\"}`}\n onClick={() => isAccepted ? rejectProductOffer(offer) : acceptProductOffer(offer)}\n >\n {isAccepted ? removeText : addText}\n </button>\n </div>\n );\n })}\n </div>\n </div>\n ))}\n </div>\n );\n}\n",
|
|
16440
16652
|
"relatedFunctions": [
|
|
16441
16653
|
"hasBundleSettings",
|
|
16442
16654
|
"initBundleProducts",
|
|
@@ -16506,7 +16718,7 @@
|
|
|
16506
16718
|
"id": "contact-form-section",
|
|
16507
16719
|
"title": "Contact Form Section (Complete)",
|
|
16508
16720
|
"description": "Complete contact form section with name, email, phone, and message fields. Uses the initContactForm/setContactForm*/submitContactForm pattern with validation and success state.",
|
|
16509
|
-
"code": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getContactForm,\n initContactForm,\n setContactFormEmail,\n setContactFormFirstName,\n setContactFormLastName,\n setContactFormPhone,\n setContactFormMessage,\n submitContactForm,\n clearContactForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ContactFormSection({\n title = \"Contact Us\",\n successMessage = \"Thank you! Your message has been sent.\",\n backgroundColor = \"#ffffff\",\n submitButtonText = \"Send Message\",\n submittingButtonText = \"Sending...\",\n}: Props) {\n const contactForm = getContactForm(customerStore);\n\n useEffect(() => {\n initContactForm(contactForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitContactForm(contactForm);\n if (success) {\n clearContactForm(
|
|
16721
|
+
"code": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getContactForm,\n initContactForm,\n setContactFormEmail,\n setContactFormFirstName,\n setContactFormLastName,\n setContactFormPhone,\n setContactFormMessage,\n submitContactForm,\n clearContactForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ContactFormSection({\n title = \"Contact Us\",\n successMessage = \"Thank you! Your message has been sent.\",\n backgroundColor = \"#ffffff\",\n submitButtonText = \"Send Message\",\n submittingButtonText = \"Sending...\",\n}: Props) {\n const contactForm = getContactForm(customerStore);\n\n useEffect(() => {\n initContactForm(contactForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitContactForm(contactForm);\n if (success) {\n clearContactForm(customerStore);\n }\n };\n\n return (\n <section className=\"contact-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"contact-inner\">\n <h1 className=\"contact-title\">{title}</h1>\n\n {contactForm.isSuccess && (\n <div className=\"contact-success-banner\">{successMessage}</div>\n )}\n\n {contactForm.isFailure && contactForm.responseMessage && (\n <div className=\"contact-error-banner\">{contactForm.responseMessage}</div>\n )}\n\n <form className=\"contact-form\" onSubmit={handleSubmit}>\n <div className=\"contact-row\">\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.firstName.label}</label>\n <input\n className={`contact-input ${contactForm.firstName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={contactForm.firstName.placeholder}\n value={contactForm.firstName.value}\n onInput={(e) =>\n setContactFormFirstName(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.firstName.hasError && contactForm.firstName.message && (\n <span className=\"contact-field-error\">{contactForm.firstName.message}</span>\n )}\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.lastName.label}</label>\n <input\n className={`contact-input ${contactForm.lastName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={contactForm.lastName.placeholder}\n value={contactForm.lastName.value}\n onInput={(e) =>\n setContactFormLastName(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.lastName.hasError && contactForm.lastName.message && (\n <span className=\"contact-field-error\">{contactForm.lastName.message}</span>\n )}\n </div>\n </div>\n\n <div className=\"contact-row\">\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.email.label}</label>\n <input\n className={`contact-input ${contactForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={contactForm.email.placeholder}\n value={contactForm.email.value}\n onInput={(e) =>\n setContactFormEmail(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.email.hasError && contactForm.email.message && (\n <span className=\"contact-field-error\">{contactForm.email.message}</span>\n )}\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.phone.label}</label>\n <input\n className={`contact-input ${contactForm.phone.hasError ? \"has-error\" : \"\"}`}\n type=\"tel\"\n placeholder={contactForm.phone.placeholder}\n value={contactForm.phone.value}\n onInput={(e) =>\n setContactFormPhone(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.phone.hasError && contactForm.phone.message && (\n <span className=\"contact-field-error\">{contactForm.phone.message}</span>\n )}\n </div>\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.message.label}</label>\n <textarea\n className={`contact-textarea ${contactForm.message.hasError ? \"has-error\" : \"\"}`}\n placeholder={contactForm.message.placeholder}\n value={contactForm.message.value}\n rows={5}\n onInput={(e) =>\n setContactFormMessage(contactForm, (e.target as HTMLTextAreaElement).value)\n }\n />\n {contactForm.message.hasError && contactForm.message.message && (\n <span className=\"contact-field-error\">{contactForm.message.message}</span>\n )}\n </div>\n\n <button\n className=\"contact-submit-btn\"\n type=\"submit\"\n disabled={contactForm.isSubmitting}\n >\n {contactForm.isSubmitting ? submittingButtonText : submitButtonText}\n </button>\n </form>\n </div>\n </section>\n );\n}\n\n",
|
|
16510
16722
|
"relatedFunctions": [
|
|
16511
16723
|
"initContactForm",
|
|
16512
16724
|
"setContactFormEmail",
|
|
@@ -16524,7 +16736,7 @@
|
|
|
16524
16736
|
"files": [
|
|
16525
16737
|
{
|
|
16526
16738
|
"filename": "index.tsx",
|
|
16527
|
-
"content": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getContactForm,\n initContactForm,\n setContactFormEmail,\n setContactFormFirstName,\n setContactFormLastName,\n setContactFormPhone,\n setContactFormMessage,\n submitContactForm,\n clearContactForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ContactFormSection({\n title = \"Contact Us\",\n successMessage = \"Thank you! Your message has been sent.\",\n backgroundColor = \"#ffffff\",\n submitButtonText = \"Send Message\",\n submittingButtonText = \"Sending...\",\n}: Props) {\n const contactForm = getContactForm(customerStore);\n\n useEffect(() => {\n initContactForm(contactForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitContactForm(contactForm);\n if (success) {\n clearContactForm(
|
|
16739
|
+
"content": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getContactForm,\n initContactForm,\n setContactFormEmail,\n setContactFormFirstName,\n setContactFormLastName,\n setContactFormPhone,\n setContactFormMessage,\n submitContactForm,\n clearContactForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ContactFormSection({\n title = \"Contact Us\",\n successMessage = \"Thank you! Your message has been sent.\",\n backgroundColor = \"#ffffff\",\n submitButtonText = \"Send Message\",\n submittingButtonText = \"Sending...\",\n}: Props) {\n const contactForm = getContactForm(customerStore);\n\n useEffect(() => {\n initContactForm(contactForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitContactForm(contactForm);\n if (success) {\n clearContactForm(customerStore);\n }\n };\n\n return (\n <section className=\"contact-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"contact-inner\">\n <h1 className=\"contact-title\">{title}</h1>\n\n {contactForm.isSuccess && (\n <div className=\"contact-success-banner\">{successMessage}</div>\n )}\n\n {contactForm.isFailure && contactForm.responseMessage && (\n <div className=\"contact-error-banner\">{contactForm.responseMessage}</div>\n )}\n\n <form className=\"contact-form\" onSubmit={handleSubmit}>\n <div className=\"contact-row\">\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.firstName.label}</label>\n <input\n className={`contact-input ${contactForm.firstName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={contactForm.firstName.placeholder}\n value={contactForm.firstName.value}\n onInput={(e) =>\n setContactFormFirstName(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.firstName.hasError && contactForm.firstName.message && (\n <span className=\"contact-field-error\">{contactForm.firstName.message}</span>\n )}\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.lastName.label}</label>\n <input\n className={`contact-input ${contactForm.lastName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={contactForm.lastName.placeholder}\n value={contactForm.lastName.value}\n onInput={(e) =>\n setContactFormLastName(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.lastName.hasError && contactForm.lastName.message && (\n <span className=\"contact-field-error\">{contactForm.lastName.message}</span>\n )}\n </div>\n </div>\n\n <div className=\"contact-row\">\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.email.label}</label>\n <input\n className={`contact-input ${contactForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={contactForm.email.placeholder}\n value={contactForm.email.value}\n onInput={(e) =>\n setContactFormEmail(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.email.hasError && contactForm.email.message && (\n <span className=\"contact-field-error\">{contactForm.email.message}</span>\n )}\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.phone.label}</label>\n <input\n className={`contact-input ${contactForm.phone.hasError ? \"has-error\" : \"\"}`}\n type=\"tel\"\n placeholder={contactForm.phone.placeholder}\n value={contactForm.phone.value}\n onInput={(e) =>\n setContactFormPhone(contactForm, (e.target as HTMLInputElement).value)\n }\n />\n {contactForm.phone.hasError && contactForm.phone.message && (\n <span className=\"contact-field-error\">{contactForm.phone.message}</span>\n )}\n </div>\n </div>\n\n <div className=\"contact-field\">\n <label className=\"contact-label\">{contactForm.message.label}</label>\n <textarea\n className={`contact-textarea ${contactForm.message.hasError ? \"has-error\" : \"\"}`}\n placeholder={contactForm.message.placeholder}\n value={contactForm.message.value}\n rows={5}\n onInput={(e) =>\n setContactFormMessage(contactForm, (e.target as HTMLTextAreaElement).value)\n }\n />\n {contactForm.message.hasError && contactForm.message.message && (\n <span className=\"contact-field-error\">{contactForm.message.message}</span>\n )}\n </div>\n\n <button\n className=\"contact-submit-btn\"\n type=\"submit\"\n disabled={contactForm.isSubmitting}\n >\n {contactForm.isSubmitting ? submittingButtonText : submitButtonText}\n </button>\n </form>\n </div>\n </section>\n );\n}\n\n"
|
|
16528
16740
|
},
|
|
16529
16741
|
{
|
|
16530
16742
|
"filename": "types.ts",
|
|
@@ -16544,7 +16756,7 @@
|
|
|
16544
16756
|
"id": "coupon-code",
|
|
16545
16757
|
"title": "Coupon Code",
|
|
16546
16758
|
"description": "Coupon code input with apply (initCouponCodeForm/setCouponCodeFormCouponCode/submitCouponCodeForm), remove (removeCouponCodeForm), and clear (clearCouponCodeForm). Reads applied coupon from IkasCart.couponCode.",
|
|
16547
|
-
"code": "import { useEffect } from \"preact/hooks\";\nimport {\n cartStore,\n customerStore,\n initCouponCodeForm,\n setCouponCodeFormCouponCode,\n submitCouponCodeForm,\n removeCouponCodeForm,\n clearCouponCodeForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function CouponCode({\n appliedLabel = \"Coupon applied:\",\n removeText = \"Remove\",\n placeholder = \"Enter coupon code\",\n applyButtonText = \"Apply\",\n applyingButtonText = \"Applying...\",\n}: Props) {\n const cart = cartStore.cart;\n const couponForm = customerStore.
|
|
16759
|
+
"code": "import { useEffect } from \"preact/hooks\";\nimport {\n cartStore,\n customerStore,\n initCouponCodeForm,\n setCouponCodeFormCouponCode,\n submitCouponCodeForm,\n removeCouponCodeForm,\n clearCouponCodeForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function CouponCode({\n appliedLabel = \"Coupon applied:\",\n removeText = \"Remove\",\n placeholder = \"Enter coupon code\",\n applyButtonText = \"Apply\",\n applyingButtonText = \"Applying...\",\n}: Props) {\n const cart = cartStore.cart;\n const couponForm = customerStore._couponCodeForm;\n const appliedCoupon = cart?.couponCode;\n\n useEffect(() => {\n if (couponForm) {\n initCouponCodeForm(couponForm);\n }\n }, [couponForm]);\n\n if (!couponForm) return null;\n\n const handleApply = async (e: Event) => {\n e.preventDefault();\n await submitCouponCodeForm(couponForm);\n };\n\n const handleRemove = async () => {\n await removeCouponCodeForm(couponForm);\n };\n\n const handleClear = () => {\n clearCouponCodeForm(customerStore);\n };\n\n return (\n <div className=\"coupon-code\">\n {appliedCoupon ? (\n <div className=\"coupon-applied\">\n <span>{appliedLabel} <strong>{appliedCoupon}</strong></span>\n <button onClick={handleRemove}>{removeText}</button>\n </div>\n ) : (\n <form className=\"coupon-form\" onSubmit={handleApply}>\n <input\n type=\"text\"\n placeholder={couponForm.couponCode?.placeholder ?? placeholder}\n value={couponForm.couponCode?.value ?? \"\"}\n onInput={(e) => setCouponCodeFormCouponCode(couponForm, (e.target as HTMLInputElement).value)}\n className=\"coupon-input\"\n />\n <button type=\"submit\" className=\"coupon-apply-btn\" disabled={couponForm.isSubmitting}>\n {couponForm.isSubmitting ? applyingButtonText : applyButtonText}\n </button>\n </form>\n )}\n {couponForm.isFailure && couponForm.responseMessage && (\n <span className=\"coupon-error\">{couponForm.responseMessage}</span>\n )}\n </div>\n );\n}\n",
|
|
16548
16760
|
"relatedFunctions": [
|
|
16549
16761
|
"initCouponCodeForm",
|
|
16550
16762
|
"setCouponCodeFormCouponCode",
|
|
@@ -16563,7 +16775,7 @@
|
|
|
16563
16775
|
"id": "customer-auth",
|
|
16564
16776
|
"title": "Customer Authentication",
|
|
16565
16777
|
"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.",
|
|
16566
|
-
"code": "import {\n customerStore,\n hasCustomer,\n customerLogin,\n logout,\n waitForCustomerStoreInit,\n handleSocialLogin,\n socialLogin,\n
|
|
16778
|
+
"code": "import {\n customerStore,\n hasCustomer,\n customerLogin,\n logout,\n waitForCustomerStoreInit,\n handleSocialLogin,\n socialLogin,\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",
|
|
16567
16779
|
"relatedFunctions": [
|
|
16568
16780
|
"customerStore",
|
|
16569
16781
|
"customerLogin",
|
|
@@ -16900,7 +17112,7 @@
|
|
|
16900
17112
|
"id": "order-detail-section",
|
|
16901
17113
|
"title": "Order Detail Section (Complete)",
|
|
16902
17114
|
"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.",
|
|
16903
|
-
"code": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderFormattedShippingTotal,\n getIkasOrderFormattedTotalTax,\n getIkasOrderDisplayedPackages,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n hasOrderLineItemDiscount,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getOrderAdjustmentIsDecrement,\n getOrderTransactionFormattedAmount,\n getOrderTransactionPaymentMethodTranslation,\n getIkasOrderRefundableItems,\n isIkasOrderRefundable,\n getOrderLineItemRefundQuantity,\n setOrderLineItemRefundQuantity,\n refundOrder,\n getDefaultSrc,\n Router,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function OrderDetailSection({\n backgroundColor = \"#ffffff\",\n loadingText = \"Loading...\",\n notFoundText = \"Order not found.\",\n shippingLabel = \"Shipping:\",\n taxLabel = \"Tax:\",\n subtotalLabel = \"Subtotal:\",\n totalLabel = \"Total:\",\n refundTitle = \"Refund\",\n submitRefundText = \"Submit Refund\",\n}: Props) {\n const [order, setOrder] = useState<IkasOrder | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n const params = Router.getPageParams();\n const orderId = params.id;\n if (orderId) {\n getOrder(customerStore, orderId).then((o) => {\n setOrder(o);\n setLoading(false);\n });\n }\n }, []);\n\n if (loading) return <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>{loadingText}</p></div></div>;\n if (!order) return <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>{notFoundText}</p></div></div>;\n\n const packages = getIkasOrderDisplayedPackages(order);\n const lineItems = order.orderLineItems ?? [];\n const adjustments = order.orderAdjustments ?? [];\n const transactions = order.
|
|
17115
|
+
"code": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderFormattedShippingTotal,\n getIkasOrderFormattedTotalTax,\n getIkasOrderDisplayedPackages,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n hasOrderLineItemDiscount,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getOrderAdjustmentIsDecrement,\n getOrderTransactionFormattedAmount,\n getOrderTransactionPaymentMethodTranslation,\n getIkasOrderRefundableItems,\n isIkasOrderRefundable,\n getOrderLineItemRefundQuantity,\n setOrderLineItemRefundQuantity,\n refundOrder,\n getDefaultSrc,\n Router,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function OrderDetailSection({\n backgroundColor = \"#ffffff\",\n loadingText = \"Loading...\",\n notFoundText = \"Order not found.\",\n shippingLabel = \"Shipping:\",\n taxLabel = \"Tax:\",\n subtotalLabel = \"Subtotal:\",\n totalLabel = \"Total:\",\n refundTitle = \"Refund\",\n submitRefundText = \"Submit Refund\",\n}: Props) {\n const [order, setOrder] = useState<IkasOrder | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n const params = Router.getPageParams();\n const orderId = params.id;\n if (orderId) {\n getOrder(customerStore, orderId).then((o) => {\n setOrder(o);\n setLoading(false);\n });\n }\n }, []);\n\n if (loading) return <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>{loadingText}</p></div></div>;\n if (!order) return <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>{notFoundText}</p></div></div>;\n\n const packages = getIkasOrderDisplayedPackages(order);\n const lineItems = order.orderLineItems ?? [];\n const adjustments = order.orderAdjustments ?? [];\n const transactions = order.transactions ?? [];\n const canRefund = isIkasOrderRefundable(order) as unknown as boolean;\n const refundableItems = canRefund ? getIkasOrderRefundableItems(order) : [];\n\n return (\n <section className=\"order-detail-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"order-detail-inner\">\n <h1>Order #{order.orderNumber}</h1>\n <p>{getIkasOrderFormattedOrderedAt(order)}</p>\n\n {packages.map((pkg, i) => (\n <span key={i} className=\"order-status-badge\">{getIkasOrderPackageStatusTranslation(order)}</span>\n ))}\n\n <div className=\"order-items\">\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n const href = item.variant ? getIkasOrderLineVariantHref(item.variant) : undefined;\n const hasDiscount = hasOrderLineItemDiscount(item) as unknown as boolean;\n return (\n <div key={item.id} className=\"order-item\">\n {image && <a href={href}><img src={getDefaultSrc(image)} width={64} height={64} style={{ objectFit: \"cover\", borderRadius: 4 }} alt=\"\" /></a>}\n <div>\n <a href={href}>{item.variant?.name}</a>\n <span> x{item.quantity}</span>\n <span> {getOrderLineItemFormattedFinalPriceWithQuantity(item)}</span>\n </div>\n </div>\n );\n })}\n </div>\n\n {adjustments.length > 0 && adjustments.map((adj: any, i: number) => (\n <div key={i}>\n <span>{getOrderAdjustmentDisplayName(adj)}: </span>\n <span style={{ color: getOrderAdjustmentIsDecrement(adj) ? \"green\" : \"inherit\" }}>{getOrderAdjustmentFormattedAmount(adj)}</span>\n </div>\n ))}\n\n {transactions.map((tx: any, i: number) => (\n <div key={i}>{getOrderTransactionPaymentMethodTranslation(tx)}: {getOrderTransactionFormattedAmount(tx)}</div>\n ))}\n\n <div style={{ marginTop: 16, borderTop: \"1px solid #eee\", paddingTop: 16 }}>\n <div>{shippingLabel} {getIkasOrderFormattedShippingTotal(order)}</div>\n <div>{taxLabel} {getIkasOrderFormattedTotalTax(order)}</div>\n <div>{subtotalLabel} {getIkasOrderFormattedTotalPrice(order)}</div>\n <div style={{ fontWeight: 700, fontSize: 18 }}>{totalLabel} {getIkasOrderFormattedTotalFinalPrice(order)}</div>\n </div>\n\n {canRefund && (\n <div style={{ marginTop: 24 }}>\n <h3>{refundTitle}</h3>\n {refundableItems.map((item: any) => (\n <div key={item.id}>\n {item.variant?.name}: <input type=\"number\" min={0} max={item.quantity} value={getOrderLineItemRefundQuantity(item) ?? 0}\n onChange={(e) => setOrderLineItemRefundQuantity(Number((e.target as HTMLInputElement).value), item)} style={{ width: 60 }} />\n </div>\n ))}\n <button onClick={() => refundOrder(customerStore, order)} style={{ marginTop: 8 }}>{submitRefundText}</button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
16904
17116
|
"relatedFunctions": [
|
|
16905
17117
|
"getOrder",
|
|
16906
17118
|
"getIkasOrderFormattedTotalFinalPrice",
|
|
@@ -16939,7 +17151,7 @@
|
|
|
16939
17151
|
"files": [
|
|
16940
17152
|
{
|
|
16941
17153
|
"filename": "index.tsx",
|
|
16942
|
-
"content": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderFormattedShippingTotal,\n getIkasOrderFormattedTotalTax,\n getIkasOrderDisplayedPackages,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n hasOrderLineItemDiscount,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getOrderAdjustmentIsDecrement,\n getOrderTransactionFormattedAmount,\n getOrderTransactionPaymentMethodTranslation,\n getIkasOrderRefundableItems,\n isIkasOrderRefundable,\n getOrderLineItemRefundQuantity,\n setOrderLineItemRefundQuantity,\n refundOrder,\n getDefaultSrc,\n Router,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function OrderDetailSection({\n backgroundColor = \"#ffffff\",\n loadingText = \"Loading...\",\n notFoundText = \"Order not found.\",\n shippingLabel = \"Shipping:\",\n taxLabel = \"Tax:\",\n subtotalLabel = \"Subtotal:\",\n totalLabel = \"Total:\",\n refundTitle = \"Refund\",\n submitRefundText = \"Submit Refund\",\n}: Props) {\n const [order, setOrder] = useState<IkasOrder | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n const params = Router.getPageParams();\n const orderId = params.id;\n if (orderId) {\n getOrder(customerStore, orderId).then((o) => {\n setOrder(o);\n setLoading(false);\n });\n }\n }, []);\n\n if (loading) return <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>{loadingText}</p></div></div>;\n if (!order) return <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>{notFoundText}</p></div></div>;\n\n const packages = getIkasOrderDisplayedPackages(order);\n const lineItems = order.orderLineItems ?? [];\n const adjustments = order.orderAdjustments ?? [];\n const transactions = order.
|
|
17154
|
+
"content": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderFormattedShippingTotal,\n getIkasOrderFormattedTotalTax,\n getIkasOrderDisplayedPackages,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n hasOrderLineItemDiscount,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getOrderAdjustmentIsDecrement,\n getOrderTransactionFormattedAmount,\n getOrderTransactionPaymentMethodTranslation,\n getIkasOrderRefundableItems,\n isIkasOrderRefundable,\n getOrderLineItemRefundQuantity,\n setOrderLineItemRefundQuantity,\n refundOrder,\n getDefaultSrc,\n Router,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function OrderDetailSection({\n backgroundColor = \"#ffffff\",\n loadingText = \"Loading...\",\n notFoundText = \"Order not found.\",\n shippingLabel = \"Shipping:\",\n taxLabel = \"Tax:\",\n subtotalLabel = \"Subtotal:\",\n totalLabel = \"Total:\",\n refundTitle = \"Refund\",\n submitRefundText = \"Submit Refund\",\n}: Props) {\n const [order, setOrder] = useState<IkasOrder | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n const params = Router.getPageParams();\n const orderId = params.id;\n if (orderId) {\n getOrder(customerStore, orderId).then((o) => {\n setOrder(o);\n setLoading(false);\n });\n }\n }, []);\n\n if (loading) return <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>{loadingText}</p></div></div>;\n if (!order) return <div className=\"order-detail-section\"><div className=\"order-detail-inner\"><p>{notFoundText}</p></div></div>;\n\n const packages = getIkasOrderDisplayedPackages(order);\n const lineItems = order.orderLineItems ?? [];\n const adjustments = order.orderAdjustments ?? [];\n const transactions = order.transactions ?? [];\n const canRefund = isIkasOrderRefundable(order) as unknown as boolean;\n const refundableItems = canRefund ? getIkasOrderRefundableItems(order) : [];\n\n return (\n <section className=\"order-detail-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"order-detail-inner\">\n <h1>Order #{order.orderNumber}</h1>\n <p>{getIkasOrderFormattedOrderedAt(order)}</p>\n\n {packages.map((pkg, i) => (\n <span key={i} className=\"order-status-badge\">{getIkasOrderPackageStatusTranslation(order)}</span>\n ))}\n\n <div className=\"order-items\">\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n const href = item.variant ? getIkasOrderLineVariantHref(item.variant) : undefined;\n const hasDiscount = hasOrderLineItemDiscount(item) as unknown as boolean;\n return (\n <div key={item.id} className=\"order-item\">\n {image && <a href={href}><img src={getDefaultSrc(image)} width={64} height={64} style={{ objectFit: \"cover\", borderRadius: 4 }} alt=\"\" /></a>}\n <div>\n <a href={href}>{item.variant?.name}</a>\n <span> x{item.quantity}</span>\n <span> {getOrderLineItemFormattedFinalPriceWithQuantity(item)}</span>\n </div>\n </div>\n );\n })}\n </div>\n\n {adjustments.length > 0 && adjustments.map((adj: any, i: number) => (\n <div key={i}>\n <span>{getOrderAdjustmentDisplayName(adj)}: </span>\n <span style={{ color: getOrderAdjustmentIsDecrement(adj) ? \"green\" : \"inherit\" }}>{getOrderAdjustmentFormattedAmount(adj)}</span>\n </div>\n ))}\n\n {transactions.map((tx: any, i: number) => (\n <div key={i}>{getOrderTransactionPaymentMethodTranslation(tx)}: {getOrderTransactionFormattedAmount(tx)}</div>\n ))}\n\n <div style={{ marginTop: 16, borderTop: \"1px solid #eee\", paddingTop: 16 }}>\n <div>{shippingLabel} {getIkasOrderFormattedShippingTotal(order)}</div>\n <div>{taxLabel} {getIkasOrderFormattedTotalTax(order)}</div>\n <div>{subtotalLabel} {getIkasOrderFormattedTotalPrice(order)}</div>\n <div style={{ fontWeight: 700, fontSize: 18 }}>{totalLabel} {getIkasOrderFormattedTotalFinalPrice(order)}</div>\n </div>\n\n {canRefund && (\n <div style={{ marginTop: 24 }}>\n <h3>{refundTitle}</h3>\n {refundableItems.map((item: any) => (\n <div key={item.id}>\n {item.variant?.name}: <input type=\"number\" min={0} max={item.quantity} value={getOrderLineItemRefundQuantity(item) ?? 0}\n onChange={(e) => setOrderLineItemRefundQuantity(Number((e.target as HTMLInputElement).value), item)} style={{ width: 60 }} />\n </div>\n ))}\n <button onClick={() => refundOrder(customerStore, order)} style={{ marginTop: 8 }}>{submitRefundText}</button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n"
|
|
16943
17155
|
},
|
|
16944
17156
|
{
|
|
16945
17157
|
"filename": "types.ts",
|
|
@@ -16959,7 +17171,7 @@
|
|
|
16959
17171
|
"id": "order-display",
|
|
16960
17172
|
"title": "Order Detail Display",
|
|
16961
17173
|
"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).",
|
|
16962
|
-
"code": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderFormattedShippingTotal,\n getIkasOrderFormattedTotalTax,\n getIkasOrderDisplayedPackages,\n getIkasOrderDistinctItemCount,\n getIkasOrderCustomerFullName,\n getIkasOrderHref,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n getOrderLineItemFormattedPriceWithQuantity,\n hasOrderLineItemDiscount,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getOrderAdjustmentIsDecrement,\n getOrderTransactionFormattedAmount,\n getOrderTransactionPaymentMethodTranslation,\n getIkasOrderRefundableItems,\n isIkasOrderRefundable,\n getOrderLineItemRefundQuantity,\n setOrderLineItemRefundQuantity,\n refundOrder,\n getDefaultSrc,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function OrderDetail({\n orderId,\n loadingText = \"Loading order...\",\n notFoundText = \"Order not found.\",\n packagesTitle = \"Packages\",\n itemsTitle = \"Items\",\n quantityLabel = \"Qty:\",\n adjustmentsTitle = \"Adjustments\",\n paymentTitle = \"Payment\",\n subtotalLabel = \"Subtotal\",\n shippingLabel = \"Shipping\",\n taxLabel = \"Tax\",\n totalLabel = \"Total\",\n refundTitle = \"Request Refund\",\n submitRefundText = \"Submit Refund Request\",\n}: Props) {\n const [order, setOrder] = useState<IkasOrder | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n getOrder(customerStore, orderId).then((o) => {\n setOrder(o);\n setLoading(false);\n });\n }, [orderId]);\n\n if (loading) return <p>{loadingText}</p>;\n if (!order) return <p>{notFoundText}</p>;\n\n const packages = getIkasOrderDisplayedPackages(order);\n const lineItems = order.orderLineItems ?? [];\n const adjustments = order.orderAdjustments ?? [];\n const transactions = order.
|
|
17174
|
+
"code": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderFormattedShippingTotal,\n getIkasOrderFormattedTotalTax,\n getIkasOrderDisplayedPackages,\n getIkasOrderDistinctItemCount,\n getIkasOrderCustomerFullName,\n getIkasOrderHref,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n getOrderLineItemFormattedPriceWithQuantity,\n hasOrderLineItemDiscount,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getOrderAdjustmentIsDecrement,\n getOrderTransactionFormattedAmount,\n getOrderTransactionPaymentMethodTranslation,\n getIkasOrderRefundableItems,\n isIkasOrderRefundable,\n getOrderLineItemRefundQuantity,\n setOrderLineItemRefundQuantity,\n refundOrder,\n getDefaultSrc,\n IkasOrder,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function OrderDetail({\n orderId,\n loadingText = \"Loading order...\",\n notFoundText = \"Order not found.\",\n packagesTitle = \"Packages\",\n itemsTitle = \"Items\",\n quantityLabel = \"Qty:\",\n adjustmentsTitle = \"Adjustments\",\n paymentTitle = \"Payment\",\n subtotalLabel = \"Subtotal\",\n shippingLabel = \"Shipping\",\n taxLabel = \"Tax\",\n totalLabel = \"Total\",\n refundTitle = \"Request Refund\",\n submitRefundText = \"Submit Refund Request\",\n}: Props) {\n const [order, setOrder] = useState<IkasOrder | null>(null);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n getOrder(customerStore, orderId).then((o) => {\n setOrder(o);\n setLoading(false);\n });\n }, [orderId]);\n\n if (loading) return <p>{loadingText}</p>;\n if (!order) return <p>{notFoundText}</p>;\n\n const packages = getIkasOrderDisplayedPackages(order);\n const lineItems = order.orderLineItems ?? [];\n const adjustments = order.orderAdjustments ?? [];\n const transactions = order.transactions ?? [];\n const canRefund = isIkasOrderRefundable(order) as unknown as boolean;\n const refundableItems = canRefund ? getIkasOrderRefundableItems(order) : [];\n\n const handleRefund = async () => {\n const success = await refundOrder(customerStore, order);\n if (success) {\n // Refresh order\n const updated = await getOrder(customerStore, orderId);\n if (updated) setOrder(updated);\n }\n };\n\n return (\n <div>\n <h2>Order #{order.orderNumber}</h2>\n <p>Customer: {getIkasOrderCustomerFullName(order)}</p>\n <p>Date: {getIkasOrderFormattedOrderedAt(order)}</p>\n <p>Items: {getIkasOrderDistinctItemCount(order)}</p>\n\n {/* Package Status */}\n <h3>{packagesTitle}</h3>\n {packages.map((pkg, i) => (\n <div key={i}>\n <p>Package {i + 1}: {getIkasOrderPackageStatusTranslation(order)}</p>\n </div>\n ))}\n\n {/* Line Items */}\n <h3>{itemsTitle}</h3>\n {lineItems.map((item) => {\n const image = item.variant ? getIkasOrderLineVariantMainImage(item.variant) : null;\n const href = item.variant ? getIkasOrderLineVariantHref(item.variant) : undefined;\n const hasDiscount = hasOrderLineItemDiscount(item) as unknown as boolean;\n return (\n <div key={item.id} style={{ display: \"flex\", gap: 12, padding: \"8px 0\", borderBottom: \"1px solid #eee\" }}>\n {image && (\n <a href={href}>\n <img src={getDefaultSrc(image)} width={64} height={64} style={{ objectFit: \"cover\", borderRadius: 4 }} alt=\"\" />\n </a>\n )}\n <div>\n <a href={href} style={{ fontWeight: 600, textDecoration: \"none\", color: \"#111\" }}>\n {item.variant?.name}\n </a>\n <div>{quantityLabel} {item.quantity}</div>\n {hasDiscount && (\n <span style={{ textDecoration: \"line-through\", color: \"#999\", marginRight: 8 }}>\n {getOrderLineItemFormattedPriceWithQuantity(item)}\n </span>\n )}\n <span>{getOrderLineItemFormattedFinalPriceWithQuantity(item)}</span>\n </div>\n </div>\n );\n })}\n\n {/* Order Adjustments */}\n {adjustments.length > 0 && (\n <div>\n <h3>{adjustmentsTitle}</h3>\n {adjustments.map((adj: any, i: number) => (\n <div key={i} style={{ display: \"flex\", justifyContent: \"space-between\", padding: \"4px 0\" }}>\n <span>{getOrderAdjustmentDisplayName(adj)}</span>\n <span style={{ color: getOrderAdjustmentIsDecrement(adj) ? \"#2e7d32\" : \"#111\" }}>\n {getOrderAdjustmentFormattedAmount(adj)}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Payment Transactions */}\n {transactions.length > 0 && (\n <div>\n <h3>{paymentTitle}</h3>\n {transactions.map((tx: any, i: number) => (\n <div key={i} style={{ display: \"flex\", justifyContent: \"space-between\", padding: \"4px 0\" }}>\n <span>{getOrderTransactionPaymentMethodTranslation(tx)}</span>\n <span>{getOrderTransactionFormattedAmount(tx)}</span>\n </div>\n ))}\n </div>\n )}\n\n {/* Order Totals */}\n <div style={{ borderTop: \"1px solid #eee\", paddingTop: 12, marginTop: 12 }}>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>{subtotalLabel}</span>\n <span>{getIkasOrderFormattedTotalPrice(order)}</span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>{shippingLabel}</span>\n <span>{getIkasOrderFormattedShippingTotal(order)}</span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\" }}>\n <span>{taxLabel}</span>\n <span>{getIkasOrderFormattedTotalTax(order)}</span>\n </div>\n <div style={{ display: \"flex\", justifyContent: \"space-between\", fontWeight: 700, fontSize: 18, marginTop: 8 }}>\n <span>{totalLabel}</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(order)}</span>\n </div>\n </div>\n\n {/* Refund Section */}\n {canRefund && (\n <div style={{ marginTop: 24, padding: 16, border: \"1px solid #eee\", borderRadius: 8 }}>\n <h3>{refundTitle}</h3>\n {refundableItems.map((item: any) => (\n <div key={item.id} style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\", padding: \"8px 0\" }}>\n <span>{item.variant?.name}</span>\n <input\n type=\"number\"\n min={0}\n max={item.quantity}\n value={getOrderLineItemRefundQuantity(item) ?? 0}\n onChange={(e) => setOrderLineItemRefundQuantity(Number((e.target as HTMLInputElement).value), item)}\n style={{ width: 60, textAlign: \"center\" }}\n />\n </div>\n ))}\n <button onClick={handleRefund} style={{ marginTop: 12, padding: \"10px 20px\", background: \"#111\", color: \"#fff\", border: \"none\", borderRadius: 6, cursor: \"pointer\" }}>\n {submitRefundText}\n </button>\n </div>\n )}\n\n <a href={getIkasOrderHref(order)} style={{ display: \"inline-block\", marginTop: 16 }}>View on store</a>\n </div>\n );\n}\n\n",
|
|
16963
17175
|
"relatedFunctions": [
|
|
16964
17176
|
"getOrder",
|
|
16965
17177
|
"getIkasOrderFormattedTotalFinalPrice",
|
|
@@ -17044,7 +17256,7 @@
|
|
|
17044
17256
|
"id": "product-detail-section",
|
|
17045
17257
|
"title": "Product Detail Section (Complete)",
|
|
17046
17258
|
"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.",
|
|
17047
|
-
"code": "import { useState, useEffect } from \"preact/hooks\";\nimport {\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n hasProductStock,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantDiscountPercentage,\n getProductVariantFormattedDiscountAmount,\n isAddToCartEnabled,\n addItemToCart,\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n getProductVariantMainImage,\n getDefaultSrc,\n getThumbnailSrc,\n getSrc,\n createMediaSrcset,\n getProductCategoryPath,\n getIkasCategoryPathItemHref,\n getIkasBrandHref,\n getAttributeListValues,\n hasBundleSettings,\n initBundleProducts,\n getDisplayedProductGroups,\n isNotEmpty,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductDetail({\n product,\n showFavoriteButton = true,\n addToCartButtonText = \"Add to Cart\",\n backgroundColor = \"#ffffff\",\n descriptionTitle = \"Description\",\n detailsTitle = \"Details\",\n bundleTitle = \"Frequently Bought Together\",\n outOfStockText = \"Out of Stock\",\n saveText = \"Save\",\n}: Props) {\n const [selectedImageIndex, setSelectedImageIndex] = useState(0);\n const [isAddingToCart, setIsAddingToCart] = useState(false);\n const [quantity, setQuantity] = useState(1);\n\n useEffect(() => {\n if (product) {\n const variant = getSelectedProductVariant(product);\n if (variant && hasBundleSettings(variant)) {\n initBundleProducts(product);\n }\n }\n }, [product]);\n\n if (!product) {\n return null;\n }\n\n const selectedVariant = getSelectedProductVariant(product) as any;\n const variantTypes = getDisplayedProductVariantTypes(product);\n const inStock = hasProductStock(product) as unknown as boolean;\n const variantInStock = hasProductVariantStock(selectedVariant) as unknown as boolean;\n const canAddToCart = isAddToCartEnabled(product) as unknown as boolean;\n\n // Pricing\n const hasDiscount = hasProductVariantDiscount(selectedVariant) as unknown as boolean;\n const finalPrice = getProductVariantFormattedFinalPrice(selectedVariant) as unknown as string;\n const originalPrice = hasDiscount\n ? (getProductVariantFormattedSellPrice(selectedVariant) as unknown as string)\n : null;\n const discountPercentage = hasDiscount\n ? (getProductVariantDiscountPercentage(selectedVariant) as unknown as number)\n : null;\n const discountAmount = hasDiscount\n ? (getProductVariantFormattedDiscountAmount(selectedVariant) as unknown as string)\n : null;\n\n // Breadcrumb\n const categoryPath = getProductCategoryPath(product);\n\n // Images — in production, dynavit uses IkasThemeSlider for image carousel\n const mainProductImage = getProductVariantMainImage(selectedVariant);\n const mainImage = mainProductImage?.image;\n const variantImages = selectedVariant?.images;\n const images: IkasImage[] = variantImages?.length\n ? variantImages\n .map((pi: any) => pi.image)\n .filter((img: any): img is IkasImage => img != null)\n : mainImage\n ? [mainImage]\n : [];\n const currentImage = images[selectedImageIndex] ?? images[0];\n\n // Favorites\n const isFavorite = isFavoriteIkasProduct(product);\n\n // Attributes\n const attributes = product.attributeList\n ? getAttributeListValues(product.attributeList)\n : [];\n\n // Bundle / Offer products\n const hasBundle = selectedVariant ? (hasBundleSettings(selectedVariant) as unknown as boolean) : false;\n const productGroups = hasBundle ? getDisplayedProductGroups(product) : [];\n\n const handleAddToCart = async () => {\n if (!canAddToCart || isAddingToCart) return;\n setIsAddingToCart(true);\n try {\n const result = await addItemToCart(selectedVariant, product, quantity);\n if (result.success) {\n setQuantity(1);\n }\n } finally {\n setIsAddingToCart(false);\n }\n };\n\n const handleToggleFavorite = async () => {\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n\n return (\n <section className=\"product-detail\" style={backgroundColor ? { backgroundColor } : undefined}>\n {/* Breadcrumb */}\n {isNotEmpty(categoryPath) && (\n <nav className=\"product-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((pathItem: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\">/</span>\n <a href={getIkasCategoryPathItemHref(pathItem)}>{pathItem.name}</a>\n </span>\n ))}\n <span className=\"breadcrumb-sep\">/</span>\n <span className=\"breadcrumb-current\">{product.name}</span>\n </nav>\n )}\n\n <div className=\"product-detail-inner\">\n {/* Image Gallery — production uses IkasThemeSlider for carousel */}\n <div className=\"product-gallery\">\n {currentImage && (\n <img\n className=\"product-main-image\"\n src={getDefaultSrc(currentImage)}\n srcSet={createMediaSrcset(currentImage)}\n sizes=\"(max-width: 768px) 100vw, 50vw\"\n alt={currentImage.altText || product.name}\n />\n )}\n {images.length > 1 && (\n <div className=\"product-thumbnails\">\n {images.map((img, i) => (\n <button\n key={img.id || i}\n className={`product-thumbnail-btn ${i === selectedImageIndex ? \"active\" : \"\"}`}\n onClick={() => setSelectedImageIndex(i)}\n >\n <img src={getThumbnailSrc(img)} alt={`${product.name} ${i + 1}`} />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Product Info */}\n <div className=\"product-info\">\n {/* Brand with link */}\n {product.brand && (\n <a className=\"product-brand\" href={getIkasBrandHref(product.brand)}>\n {product.brand.name}\n </a>\n )}\n <h1 className=\"product-name\">{product.name}</h1>\n\n {/* Average Rating */}\n {product.averageRating > 0 && (\n <div className=\"product-rating\">\n {\"★\".repeat(Math.round(product.averageRating))}\n {\"☆\".repeat(5 - Math.round(product.averageRating))}\n <span className=\"product-rating-value\">({product.averageRating.toFixed(1)})</span>\n </div>\n )}\n\n {/* Campaign badges */}\n {isNotEmpty(product.campaigns) && (\n <div className=\"product-campaigns\">\n {product.campaigns.map((campaign: any) => (\n <span key={campaign.id} className=\"product-campaign-badge\">\n {campaign.name}\n </span>\n ))}\n </div>\n )}\n\n {/* Pricing */}\n <div className=\"product-pricing\">\n <span className=\"product-final-price\">{finalPrice}</span>\n {originalPrice && <span className=\"product-original-price\">{originalPrice}</span>}\n {discountPercentage != null && (\n <span className=\"product-discount-badge\">-{discountPercentage}%</span>\n )}\n {discountAmount && (\n <span className=\"product-savings\">{saveText} {discountAmount}</span>\n )}\n </div>\n\n {/* Variant Selection */}\n {variantTypes.length > 0 && (\n <div className=\"product-variants\">\n {variantTypes.map((vt) => (\n <div key={vt.variantType.id} className=\"variant-group\">\n <span className=\"variant-group-label\">{vt.variantType.name}</span>\n <div className=\"variant-options\">\n {vt.displayedVariantValues.map((dvv) => (\n <button\n key={dvv.variantValue.id}\n className={`variant-option-btn ${dvv.isSelected ? \"selected\" : \"\"}`}\n disabled={!dvv.hasStock}\n onClick={() => selectVariantValue(product, dvv.variantValue)}\n >\n {dvv.variantValue.name}\n </button>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Stock notice */}\n {!inStock && <span className=\"out-of-stock-notice\">{outOfStockText}</span>}\n\n {/* Quantity + Add to Cart */}\n <div className=\"product-actions\">\n <div className=\"product-quantity\">\n <button onClick={() => setQuantity(Math.max(1, quantity - 1))}>-</button>\n <span>{quantity}</span>\n <button onClick={() => setQuantity(quantity + 1)}>+</button>\n </div>\n <button\n className=\"add-to-cart-btn\"\n disabled={!canAddToCart || isAddingToCart}\n onClick={handleAddToCart}\n >\n {isAddingToCart ? \"Adding...\" : !variantInStock ? outOfStockText : addToCartButtonText}\n </button>\n {showFavoriteButton && (\n <button\n className={`favorite-btn ${isFavorite ? \"is-favorite\" : \"\"}`}\n onClick={handleToggleFavorite}\n aria-label={isFavorite ? \"Remove from favorites\" : \"Add to favorites\"}\n >\n {isFavorite ? \"\\u2665\" : \"\\u2661\"}\n </button>\n )}\n </div>\n\n {/* Description */}\n {product.description && (\n <div className=\"product-description\">\n <h3>{descriptionTitle}</h3>\n <div dangerouslySetInnerHTML={{ __html: product.description }} />\n </div>\n )}\n\n {/* Product Attributes (e.g. ingredients, specs) */}\n {isNotEmpty(attributes) && (\n <div className=\"product-attributes\">\n <h3>{detailsTitle}</h3>\n {attributes.map((attr: any, i: number) => (\n <div key={i} className=\"attribute-row\">\n <span className=\"attribute-name\">{attr.name}</span>\n <span className=\"attribute-value\">{attr.value}</span>\n </div>\n ))}\n </div>\n )}\n\n {/* Bundle / Offer Products */}\n {isNotEmpty(productGroups) && (\n <div className=\"product-bundles\">\n <h3>{bundleTitle}</h3>\n {productGroups.map((group: any, i: number) => (\n <div key={i} className=\"bundle-group\">\n {group.title && <h4>{group.title}</h4>}\n <div className=\"bundle-items\">\n {group.products?.map((bp: any) => (\n <div key={bp.product?.id} className=\"bundle-item\">\n <span>{bp.product?.name}</span>\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </section>\n );\n}\n\n",
|
|
17259
|
+
"code": "import {\n addIkasProductToFavorites,\n addItemToCart,\n createMediaSrcset,\n getDefaultSrc,\n getDisplayedProductGroups,\n getDisplayedProductVariantTypes,\n getIkasBrandHref,\n getIkasCategoryPathItemHref,\n getProductCategoryPath,\n getProductVariantDiscountPercentage,\n getProductVariantFormattedDiscountAmount,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n getProductVariantMainImage,\n getSelectedProductVariant,\n getThumbnailSrc,\n hasBundleSettings,\n hasProductStock,\n hasProductVariantDiscount,\n hasProductVariantStock,\n IkasImage,\n initBundleProducts,\n isAddToCartEnabled,\n isFavoriteIkasProduct,\n isNotEmpty,\n removeIkasProductFromFavorites,\n selectVariantValue\n} from \"@ikas/bp-storefront\";\nimport { useEffect, useState } from \"preact/hooks\";\nimport { Props } from \"./types\";\n\nexport default function ProductDetail({\n product,\n showFavoriteButton = true,\n addToCartButtonText = \"Add to Cart\",\n backgroundColor = \"#ffffff\",\n descriptionTitle = \"Description\",\n detailsTitle = \"Details\",\n bundleTitle = \"Frequently Bought Together\",\n outOfStockText = \"Out of Stock\",\n saveText = \"Save\",\n}: Props) {\n const [selectedImageIndex, setSelectedImageIndex] = useState(0);\n const [isAddingToCart, setIsAddingToCart] = useState(false);\n const [quantity, setQuantity] = useState(1);\n\n useEffect(() => {\n if (product) {\n const variant = getSelectedProductVariant(product);\n if (variant && hasBundleSettings(variant)) {\n initBundleProducts(product);\n }\n }\n }, [product]);\n\n if (!product) {\n return null;\n }\n\n const selectedVariant = getSelectedProductVariant(product) as any;\n const variantTypes = getDisplayedProductVariantTypes(product);\n const inStock = hasProductStock(product) as unknown as boolean;\n const variantInStock = hasProductVariantStock(selectedVariant) as unknown as boolean;\n const canAddToCart = isAddToCartEnabled(product) as unknown as boolean;\n\n // Pricing\n const hasDiscount = hasProductVariantDiscount(selectedVariant) as unknown as boolean;\n const finalPrice = getProductVariantFormattedFinalPrice(selectedVariant) as unknown as string;\n const originalPrice = hasDiscount\n ? (getProductVariantFormattedSellPrice(selectedVariant) as unknown as string)\n : null;\n const discountPercentage = hasDiscount\n ? (getProductVariantDiscountPercentage(selectedVariant) as unknown as number)\n : null;\n const discountAmount = hasDiscount\n ? (getProductVariantFormattedDiscountAmount(selectedVariant) as unknown as string)\n : null;\n\n // Breadcrumb\n const categoryPath = getProductCategoryPath(product);\n\n // Images — in production, dynavit uses IkasThemeSlider for image carousel\n const mainProductImage = getProductVariantMainImage(selectedVariant);\n const mainImage = mainProductImage?.image;\n const variantImages = selectedVariant?.images;\n const images: IkasImage[] = variantImages?.length\n ? variantImages\n .map((pi: any) => pi.image)\n .filter((img: any): img is IkasImage => img != null)\n : mainImage\n ? [mainImage]\n : [];\n const currentImage = images[selectedImageIndex] ?? images[0];\n\n // Favorites\n const isFavorite = isFavoriteIkasProduct(product);\n\n // Attributes\n const attributes = product.attributes ?? [];\n\n // Bundle / Offer products\n const hasBundle = selectedVariant ? (hasBundleSettings(selectedVariant) as unknown as boolean) : false;\n const productGroups = hasBundle ? getDisplayedProductGroups(product) : [];\n\n const handleAddToCart = async () => {\n if (!canAddToCart || isAddingToCart) return;\n setIsAddingToCart(true);\n try {\n const result = await addItemToCart(selectedVariant, product, quantity);\n if (result.success) {\n setQuantity(1);\n }\n } finally {\n setIsAddingToCart(false);\n }\n };\n\n const handleToggleFavorite = async () => {\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n\n return (\n <section className=\"product-detail\" style={backgroundColor ? { backgroundColor } : undefined}>\n {/* Breadcrumb */}\n {isNotEmpty(categoryPath) && (\n <nav className=\"product-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((pathItem: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\">/</span>\n <a href={getIkasCategoryPathItemHref(pathItem)}>{pathItem.name}</a>\n </span>\n ))}\n <span className=\"breadcrumb-sep\">/</span>\n <span className=\"breadcrumb-current\">{product.name}</span>\n </nav>\n )}\n\n <div className=\"product-detail-inner\">\n {/* Image Gallery — production uses IkasThemeSlider for carousel */}\n <div className=\"product-gallery\">\n {currentImage && (\n <img\n className=\"product-main-image\"\n src={getDefaultSrc(currentImage)}\n srcSet={createMediaSrcset(currentImage)}\n sizes=\"(max-width: 768px) 100vw, 50vw\"\n alt={currentImage.altText || product.name}\n />\n )}\n {images.length > 1 && (\n <div className=\"product-thumbnails\">\n {images.map((img, i) => (\n <button\n key={img.id || i}\n className={`product-thumbnail-btn ${i === selectedImageIndex ? \"active\" : \"\"}`}\n onClick={() => setSelectedImageIndex(i)}\n >\n <img src={getThumbnailSrc(img)} alt={`${product.name} ${i + 1}`} />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Product Info */}\n <div className=\"product-info\">\n {/* Brand with link */}\n {product.brand && (\n <a className=\"product-brand\" href={getIkasBrandHref(product.brand)}>\n {product.brand.name}\n </a>\n )}\n <h1 className=\"product-name\">{product.name}</h1>\n\n {/* Average Rating */}\n {product.averageRating != null && product.averageRating > 0 && (\n <div className=\"product-rating\">\n {\"★\".repeat(Math.round(product.averageRating))}\n {\"☆\".repeat(5 - Math.round(product.averageRating))}\n <span className=\"product-rating-value\">({product.averageRating.toFixed(1)})</span>\n </div>\n )}\n\n {/* Campaign badges */}\n {isNotEmpty(product.campaigns) && (\n <div className=\"product-campaigns\">\n {product.campaigns!.map((campaign: any) => (\n <span key={campaign.id} className=\"product-campaign-badge\">\n {campaign.name}\n </span>\n ))}\n </div>\n )}\n\n {/* Pricing */}\n <div className=\"product-pricing\">\n <span className=\"product-final-price\">{finalPrice}</span>\n {originalPrice && <span className=\"product-original-price\">{originalPrice}</span>}\n {discountPercentage != null && (\n <span className=\"product-discount-badge\">-{discountPercentage}%</span>\n )}\n {discountAmount && (\n <span className=\"product-savings\">{saveText} {discountAmount}</span>\n )}\n </div>\n\n {/* Variant Selection */}\n {variantTypes.length > 0 && (\n <div className=\"product-variants\">\n {variantTypes.map((vt) => (\n <div key={vt.variantType.id} className=\"variant-group\">\n <span className=\"variant-group-label\">{vt.variantType.name}</span>\n <div className=\"variant-options\">\n {vt.displayedVariantValues.map((dvv) => (\n <button\n key={dvv.variantValue.id}\n className={`variant-option-btn ${dvv.isSelected ? \"selected\" : \"\"}`}\n disabled={!dvv.hasStock}\n onClick={() => selectVariantValue(product, dvv.variantValue)}\n >\n {dvv.variantValue.name}\n </button>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Stock notice */}\n {!inStock && <span className=\"out-of-stock-notice\">{outOfStockText}</span>}\n\n {/* Quantity + Add to Cart */}\n <div className=\"product-actions\">\n <div className=\"product-quantity\">\n <button onClick={() => setQuantity(Math.max(1, quantity - 1))}>-</button>\n <span>{quantity}</span>\n <button onClick={() => setQuantity(quantity + 1)}>+</button>\n </div>\n <button\n className=\"add-to-cart-btn\"\n disabled={!canAddToCart || isAddingToCart}\n onClick={handleAddToCart}\n >\n {isAddingToCart ? \"Adding...\" : !variantInStock ? outOfStockText : addToCartButtonText}\n </button>\n {showFavoriteButton && (\n <button\n className={`favorite-btn ${isFavorite ? \"is-favorite\" : \"\"}`}\n onClick={handleToggleFavorite}\n aria-label={isFavorite ? \"Remove from favorites\" : \"Add to favorites\"}\n >\n {isFavorite ? \"\\u2665\" : \"\\u2661\"}\n </button>\n )}\n </div>\n\n {/* Description */}\n {product.description && (\n <div className=\"product-description\">\n <h3>{descriptionTitle}</h3>\n <div dangerouslySetInnerHTML={{ __html: product.description }} />\n </div>\n )}\n\n {/* Product Attributes (e.g. ingredients, specs) */}\n {isNotEmpty(attributes) && (\n <div className=\"product-attributes\">\n <h3>{detailsTitle}</h3>\n {attributes.map((attr: any, i: number) => (\n <div key={i} className=\"attribute-row\">\n <span className=\"attribute-name\">{attr.productAttribute?.name}</span>\n <span className=\"attribute-value\">{attr.value}</span>\n </div>\n ))}\n </div>\n )}\n\n {/* Bundle / Offer Products */}\n {isNotEmpty(productGroups) && (\n <div className=\"product-bundles\">\n <h3>{bundleTitle}</h3>\n {productGroups.map((group: any, i: number) => (\n <div key={i} className=\"bundle-group\">\n {group.title && <h4>{group.title}</h4>}\n <div className=\"bundle-items\">\n {group.products?.map((bp: any) => (\n <div key={bp.product?.id} className=\"bundle-item\">\n <span>{bp.product?.name}</span>\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </section>\n );\n}\n\n",
|
|
17048
17260
|
"relatedFunctions": [
|
|
17049
17261
|
"getSelectedProductVariant",
|
|
17050
17262
|
"getDisplayedProductVariantTypes",
|
|
@@ -17084,7 +17296,7 @@
|
|
|
17084
17296
|
"files": [
|
|
17085
17297
|
{
|
|
17086
17298
|
"filename": "index.tsx",
|
|
17087
|
-
"content": "import { useState, useEffect } from \"preact/hooks\";\nimport {\n getSelectedProductVariant,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n hasProductVariantStock,\n hasProductStock,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n getProductVariantDiscountPercentage,\n getProductVariantFormattedDiscountAmount,\n isAddToCartEnabled,\n addItemToCart,\n isFavoriteIkasProduct,\n addIkasProductToFavorites,\n removeIkasProductFromFavorites,\n getProductVariantMainImage,\n getDefaultSrc,\n getThumbnailSrc,\n getSrc,\n createMediaSrcset,\n getProductCategoryPath,\n getIkasCategoryPathItemHref,\n getIkasBrandHref,\n getAttributeListValues,\n hasBundleSettings,\n initBundleProducts,\n getDisplayedProductGroups,\n isNotEmpty,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductDetail({\n product,\n showFavoriteButton = true,\n addToCartButtonText = \"Add to Cart\",\n backgroundColor = \"#ffffff\",\n descriptionTitle = \"Description\",\n detailsTitle = \"Details\",\n bundleTitle = \"Frequently Bought Together\",\n outOfStockText = \"Out of Stock\",\n saveText = \"Save\",\n}: Props) {\n const [selectedImageIndex, setSelectedImageIndex] = useState(0);\n const [isAddingToCart, setIsAddingToCart] = useState(false);\n const [quantity, setQuantity] = useState(1);\n\n useEffect(() => {\n if (product) {\n const variant = getSelectedProductVariant(product);\n if (variant && hasBundleSettings(variant)) {\n initBundleProducts(product);\n }\n }\n }, [product]);\n\n if (!product) {\n return null;\n }\n\n const selectedVariant = getSelectedProductVariant(product) as any;\n const variantTypes = getDisplayedProductVariantTypes(product);\n const inStock = hasProductStock(product) as unknown as boolean;\n const variantInStock = hasProductVariantStock(selectedVariant) as unknown as boolean;\n const canAddToCart = isAddToCartEnabled(product) as unknown as boolean;\n\n // Pricing\n const hasDiscount = hasProductVariantDiscount(selectedVariant) as unknown as boolean;\n const finalPrice = getProductVariantFormattedFinalPrice(selectedVariant) as unknown as string;\n const originalPrice = hasDiscount\n ? (getProductVariantFormattedSellPrice(selectedVariant) as unknown as string)\n : null;\n const discountPercentage = hasDiscount\n ? (getProductVariantDiscountPercentage(selectedVariant) as unknown as number)\n : null;\n const discountAmount = hasDiscount\n ? (getProductVariantFormattedDiscountAmount(selectedVariant) as unknown as string)\n : null;\n\n // Breadcrumb\n const categoryPath = getProductCategoryPath(product);\n\n // Images — in production, dynavit uses IkasThemeSlider for image carousel\n const mainProductImage = getProductVariantMainImage(selectedVariant);\n const mainImage = mainProductImage?.image;\n const variantImages = selectedVariant?.images;\n const images: IkasImage[] = variantImages?.length\n ? variantImages\n .map((pi: any) => pi.image)\n .filter((img: any): img is IkasImage => img != null)\n : mainImage\n ? [mainImage]\n : [];\n const currentImage = images[selectedImageIndex] ?? images[0];\n\n // Favorites\n const isFavorite = isFavoriteIkasProduct(product);\n\n // Attributes\n const attributes = product.attributeList\n ? getAttributeListValues(product.attributeList)\n : [];\n\n // Bundle / Offer products\n const hasBundle = selectedVariant ? (hasBundleSettings(selectedVariant) as unknown as boolean) : false;\n const productGroups = hasBundle ? getDisplayedProductGroups(product) : [];\n\n const handleAddToCart = async () => {\n if (!canAddToCart || isAddingToCart) return;\n setIsAddingToCart(true);\n try {\n const result = await addItemToCart(selectedVariant, product, quantity);\n if (result.success) {\n setQuantity(1);\n }\n } finally {\n setIsAddingToCart(false);\n }\n };\n\n const handleToggleFavorite = async () => {\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n\n return (\n <section className=\"product-detail\" style={backgroundColor ? { backgroundColor } : undefined}>\n {/* Breadcrumb */}\n {isNotEmpty(categoryPath) && (\n <nav className=\"product-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((pathItem: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\">/</span>\n <a href={getIkasCategoryPathItemHref(pathItem)}>{pathItem.name}</a>\n </span>\n ))}\n <span className=\"breadcrumb-sep\">/</span>\n <span className=\"breadcrumb-current\">{product.name}</span>\n </nav>\n )}\n\n <div className=\"product-detail-inner\">\n {/* Image Gallery — production uses IkasThemeSlider for carousel */}\n <div className=\"product-gallery\">\n {currentImage && (\n <img\n className=\"product-main-image\"\n src={getDefaultSrc(currentImage)}\n srcSet={createMediaSrcset(currentImage)}\n sizes=\"(max-width: 768px) 100vw, 50vw\"\n alt={currentImage.altText || product.name}\n />\n )}\n {images.length > 1 && (\n <div className=\"product-thumbnails\">\n {images.map((img, i) => (\n <button\n key={img.id || i}\n className={`product-thumbnail-btn ${i === selectedImageIndex ? \"active\" : \"\"}`}\n onClick={() => setSelectedImageIndex(i)}\n >\n <img src={getThumbnailSrc(img)} alt={`${product.name} ${i + 1}`} />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Product Info */}\n <div className=\"product-info\">\n {/* Brand with link */}\n {product.brand && (\n <a className=\"product-brand\" href={getIkasBrandHref(product.brand)}>\n {product.brand.name}\n </a>\n )}\n <h1 className=\"product-name\">{product.name}</h1>\n\n {/* Average Rating */}\n {product.averageRating > 0 && (\n <div className=\"product-rating\">\n {\"★\".repeat(Math.round(product.averageRating))}\n {\"☆\".repeat(5 - Math.round(product.averageRating))}\n <span className=\"product-rating-value\">({product.averageRating.toFixed(1)})</span>\n </div>\n )}\n\n {/* Campaign badges */}\n {isNotEmpty(product.campaigns) && (\n <div className=\"product-campaigns\">\n {product.campaigns.map((campaign: any) => (\n <span key={campaign.id} className=\"product-campaign-badge\">\n {campaign.name}\n </span>\n ))}\n </div>\n )}\n\n {/* Pricing */}\n <div className=\"product-pricing\">\n <span className=\"product-final-price\">{finalPrice}</span>\n {originalPrice && <span className=\"product-original-price\">{originalPrice}</span>}\n {discountPercentage != null && (\n <span className=\"product-discount-badge\">-{discountPercentage}%</span>\n )}\n {discountAmount && (\n <span className=\"product-savings\">{saveText} {discountAmount}</span>\n )}\n </div>\n\n {/* Variant Selection */}\n {variantTypes.length > 0 && (\n <div className=\"product-variants\">\n {variantTypes.map((vt) => (\n <div key={vt.variantType.id} className=\"variant-group\">\n <span className=\"variant-group-label\">{vt.variantType.name}</span>\n <div className=\"variant-options\">\n {vt.displayedVariantValues.map((dvv) => (\n <button\n key={dvv.variantValue.id}\n className={`variant-option-btn ${dvv.isSelected ? \"selected\" : \"\"}`}\n disabled={!dvv.hasStock}\n onClick={() => selectVariantValue(product, dvv.variantValue)}\n >\n {dvv.variantValue.name}\n </button>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Stock notice */}\n {!inStock && <span className=\"out-of-stock-notice\">{outOfStockText}</span>}\n\n {/* Quantity + Add to Cart */}\n <div className=\"product-actions\">\n <div className=\"product-quantity\">\n <button onClick={() => setQuantity(Math.max(1, quantity - 1))}>-</button>\n <span>{quantity}</span>\n <button onClick={() => setQuantity(quantity + 1)}>+</button>\n </div>\n <button\n className=\"add-to-cart-btn\"\n disabled={!canAddToCart || isAddingToCart}\n onClick={handleAddToCart}\n >\n {isAddingToCart ? \"Adding...\" : !variantInStock ? outOfStockText : addToCartButtonText}\n </button>\n {showFavoriteButton && (\n <button\n className={`favorite-btn ${isFavorite ? \"is-favorite\" : \"\"}`}\n onClick={handleToggleFavorite}\n aria-label={isFavorite ? \"Remove from favorites\" : \"Add to favorites\"}\n >\n {isFavorite ? \"\\u2665\" : \"\\u2661\"}\n </button>\n )}\n </div>\n\n {/* Description */}\n {product.description && (\n <div className=\"product-description\">\n <h3>{descriptionTitle}</h3>\n <div dangerouslySetInnerHTML={{ __html: product.description }} />\n </div>\n )}\n\n {/* Product Attributes (e.g. ingredients, specs) */}\n {isNotEmpty(attributes) && (\n <div className=\"product-attributes\">\n <h3>{detailsTitle}</h3>\n {attributes.map((attr: any, i: number) => (\n <div key={i} className=\"attribute-row\">\n <span className=\"attribute-name\">{attr.name}</span>\n <span className=\"attribute-value\">{attr.value}</span>\n </div>\n ))}\n </div>\n )}\n\n {/* Bundle / Offer Products */}\n {isNotEmpty(productGroups) && (\n <div className=\"product-bundles\">\n <h3>{bundleTitle}</h3>\n {productGroups.map((group: any, i: number) => (\n <div key={i} className=\"bundle-group\">\n {group.title && <h4>{group.title}</h4>}\n <div className=\"bundle-items\">\n {group.products?.map((bp: any) => (\n <div key={bp.product?.id} className=\"bundle-item\">\n <span>{bp.product?.name}</span>\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </section>\n );\n}\n\n"
|
|
17299
|
+
"content": "import {\n addIkasProductToFavorites,\n addItemToCart,\n createMediaSrcset,\n getDefaultSrc,\n getDisplayedProductGroups,\n getDisplayedProductVariantTypes,\n getIkasBrandHref,\n getIkasCategoryPathItemHref,\n getProductCategoryPath,\n getProductVariantDiscountPercentage,\n getProductVariantFormattedDiscountAmount,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n getProductVariantMainImage,\n getSelectedProductVariant,\n getThumbnailSrc,\n hasBundleSettings,\n hasProductStock,\n hasProductVariantDiscount,\n hasProductVariantStock,\n IkasImage,\n initBundleProducts,\n isAddToCartEnabled,\n isFavoriteIkasProduct,\n isNotEmpty,\n removeIkasProductFromFavorites,\n selectVariantValue\n} from \"@ikas/bp-storefront\";\nimport { useEffect, useState } from \"preact/hooks\";\nimport { Props } from \"./types\";\n\nexport default function ProductDetail({\n product,\n showFavoriteButton = true,\n addToCartButtonText = \"Add to Cart\",\n backgroundColor = \"#ffffff\",\n descriptionTitle = \"Description\",\n detailsTitle = \"Details\",\n bundleTitle = \"Frequently Bought Together\",\n outOfStockText = \"Out of Stock\",\n saveText = \"Save\",\n}: Props) {\n const [selectedImageIndex, setSelectedImageIndex] = useState(0);\n const [isAddingToCart, setIsAddingToCart] = useState(false);\n const [quantity, setQuantity] = useState(1);\n\n useEffect(() => {\n if (product) {\n const variant = getSelectedProductVariant(product);\n if (variant && hasBundleSettings(variant)) {\n initBundleProducts(product);\n }\n }\n }, [product]);\n\n if (!product) {\n return null;\n }\n\n const selectedVariant = getSelectedProductVariant(product) as any;\n const variantTypes = getDisplayedProductVariantTypes(product);\n const inStock = hasProductStock(product) as unknown as boolean;\n const variantInStock = hasProductVariantStock(selectedVariant) as unknown as boolean;\n const canAddToCart = isAddToCartEnabled(product) as unknown as boolean;\n\n // Pricing\n const hasDiscount = hasProductVariantDiscount(selectedVariant) as unknown as boolean;\n const finalPrice = getProductVariantFormattedFinalPrice(selectedVariant) as unknown as string;\n const originalPrice = hasDiscount\n ? (getProductVariantFormattedSellPrice(selectedVariant) as unknown as string)\n : null;\n const discountPercentage = hasDiscount\n ? (getProductVariantDiscountPercentage(selectedVariant) as unknown as number)\n : null;\n const discountAmount = hasDiscount\n ? (getProductVariantFormattedDiscountAmount(selectedVariant) as unknown as string)\n : null;\n\n // Breadcrumb\n const categoryPath = getProductCategoryPath(product);\n\n // Images — in production, dynavit uses IkasThemeSlider for image carousel\n const mainProductImage = getProductVariantMainImage(selectedVariant);\n const mainImage = mainProductImage?.image;\n const variantImages = selectedVariant?.images;\n const images: IkasImage[] = variantImages?.length\n ? variantImages\n .map((pi: any) => pi.image)\n .filter((img: any): img is IkasImage => img != null)\n : mainImage\n ? [mainImage]\n : [];\n const currentImage = images[selectedImageIndex] ?? images[0];\n\n // Favorites\n const isFavorite = isFavoriteIkasProduct(product);\n\n // Attributes\n const attributes = product.attributes ?? [];\n\n // Bundle / Offer products\n const hasBundle = selectedVariant ? (hasBundleSettings(selectedVariant) as unknown as boolean) : false;\n const productGroups = hasBundle ? getDisplayedProductGroups(product) : [];\n\n const handleAddToCart = async () => {\n if (!canAddToCart || isAddingToCart) return;\n setIsAddingToCart(true);\n try {\n const result = await addItemToCart(selectedVariant, product, quantity);\n if (result.success) {\n setQuantity(1);\n }\n } finally {\n setIsAddingToCart(false);\n }\n };\n\n const handleToggleFavorite = async () => {\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n\n return (\n <section className=\"product-detail\" style={backgroundColor ? { backgroundColor } : undefined}>\n {/* Breadcrumb */}\n {isNotEmpty(categoryPath) && (\n <nav className=\"product-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((pathItem: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\">/</span>\n <a href={getIkasCategoryPathItemHref(pathItem)}>{pathItem.name}</a>\n </span>\n ))}\n <span className=\"breadcrumb-sep\">/</span>\n <span className=\"breadcrumb-current\">{product.name}</span>\n </nav>\n )}\n\n <div className=\"product-detail-inner\">\n {/* Image Gallery — production uses IkasThemeSlider for carousel */}\n <div className=\"product-gallery\">\n {currentImage && (\n <img\n className=\"product-main-image\"\n src={getDefaultSrc(currentImage)}\n srcSet={createMediaSrcset(currentImage)}\n sizes=\"(max-width: 768px) 100vw, 50vw\"\n alt={currentImage.altText || product.name}\n />\n )}\n {images.length > 1 && (\n <div className=\"product-thumbnails\">\n {images.map((img, i) => (\n <button\n key={img.id || i}\n className={`product-thumbnail-btn ${i === selectedImageIndex ? \"active\" : \"\"}`}\n onClick={() => setSelectedImageIndex(i)}\n >\n <img src={getThumbnailSrc(img)} alt={`${product.name} ${i + 1}`} />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Product Info */}\n <div className=\"product-info\">\n {/* Brand with link */}\n {product.brand && (\n <a className=\"product-brand\" href={getIkasBrandHref(product.brand)}>\n {product.brand.name}\n </a>\n )}\n <h1 className=\"product-name\">{product.name}</h1>\n\n {/* Average Rating */}\n {product.averageRating != null && product.averageRating > 0 && (\n <div className=\"product-rating\">\n {\"★\".repeat(Math.round(product.averageRating))}\n {\"☆\".repeat(5 - Math.round(product.averageRating))}\n <span className=\"product-rating-value\">({product.averageRating.toFixed(1)})</span>\n </div>\n )}\n\n {/* Campaign badges */}\n {isNotEmpty(product.campaigns) && (\n <div className=\"product-campaigns\">\n {product.campaigns!.map((campaign: any) => (\n <span key={campaign.id} className=\"product-campaign-badge\">\n {campaign.name}\n </span>\n ))}\n </div>\n )}\n\n {/* Pricing */}\n <div className=\"product-pricing\">\n <span className=\"product-final-price\">{finalPrice}</span>\n {originalPrice && <span className=\"product-original-price\">{originalPrice}</span>}\n {discountPercentage != null && (\n <span className=\"product-discount-badge\">-{discountPercentage}%</span>\n )}\n {discountAmount && (\n <span className=\"product-savings\">{saveText} {discountAmount}</span>\n )}\n </div>\n\n {/* Variant Selection */}\n {variantTypes.length > 0 && (\n <div className=\"product-variants\">\n {variantTypes.map((vt) => (\n <div key={vt.variantType.id} className=\"variant-group\">\n <span className=\"variant-group-label\">{vt.variantType.name}</span>\n <div className=\"variant-options\">\n {vt.displayedVariantValues.map((dvv) => (\n <button\n key={dvv.variantValue.id}\n className={`variant-option-btn ${dvv.isSelected ? \"selected\" : \"\"}`}\n disabled={!dvv.hasStock}\n onClick={() => selectVariantValue(product, dvv.variantValue)}\n >\n {dvv.variantValue.name}\n </button>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Stock notice */}\n {!inStock && <span className=\"out-of-stock-notice\">{outOfStockText}</span>}\n\n {/* Quantity + Add to Cart */}\n <div className=\"product-actions\">\n <div className=\"product-quantity\">\n <button onClick={() => setQuantity(Math.max(1, quantity - 1))}>-</button>\n <span>{quantity}</span>\n <button onClick={() => setQuantity(quantity + 1)}>+</button>\n </div>\n <button\n className=\"add-to-cart-btn\"\n disabled={!canAddToCart || isAddingToCart}\n onClick={handleAddToCart}\n >\n {isAddingToCart ? \"Adding...\" : !variantInStock ? outOfStockText : addToCartButtonText}\n </button>\n {showFavoriteButton && (\n <button\n className={`favorite-btn ${isFavorite ? \"is-favorite\" : \"\"}`}\n onClick={handleToggleFavorite}\n aria-label={isFavorite ? \"Remove from favorites\" : \"Add to favorites\"}\n >\n {isFavorite ? \"\\u2665\" : \"\\u2661\"}\n </button>\n )}\n </div>\n\n {/* Description */}\n {product.description && (\n <div className=\"product-description\">\n <h3>{descriptionTitle}</h3>\n <div dangerouslySetInnerHTML={{ __html: product.description }} />\n </div>\n )}\n\n {/* Product Attributes (e.g. ingredients, specs) */}\n {isNotEmpty(attributes) && (\n <div className=\"product-attributes\">\n <h3>{detailsTitle}</h3>\n {attributes.map((attr: any, i: number) => (\n <div key={i} className=\"attribute-row\">\n <span className=\"attribute-name\">{attr.productAttribute?.name}</span>\n <span className=\"attribute-value\">{attr.value}</span>\n </div>\n ))}\n </div>\n )}\n\n {/* Bundle / Offer Products */}\n {isNotEmpty(productGroups) && (\n <div className=\"product-bundles\">\n <h3>{bundleTitle}</h3>\n {productGroups.map((group: any, i: number) => (\n <div key={i} className=\"bundle-group\">\n {group.title && <h4>{group.title}</h4>}\n <div className=\"bundle-items\">\n {group.products?.map((bp: any) => (\n <div key={bp.product?.id} className=\"bundle-item\">\n <span>{bp.product?.name}</span>\n </div>\n ))}\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </section>\n );\n}\n\n"
|
|
17088
17300
|
},
|
|
17089
17301
|
{
|
|
17090
17302
|
"filename": "types.ts",
|
|
@@ -17104,7 +17316,7 @@
|
|
|
17104
17316
|
"id": "product-filtering",
|
|
17105
17317
|
"title": "Product List Filtering",
|
|
17106
17318
|
"description": "Display-type-aware product filtering: checkboxes for BOX/LIST filters, color swatches for SWATCH filters (using getIkasFilterThumbnailImage and fv.colorCode), range buttons for NUMBER_RANGE_LIST filters (using handleNumberRangeOptionClick), and category-level filtering with onFilterCategoryClick.",
|
|
17107
|
-
"code": "import {\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getDefaultSrc,\n IkasProductList,\n IkasProductFilter,\n} from \"@ikas/bp-storefront\";\n\n/**\n * Display-type-aware product filter.\n *\n * Data flow is the same for every display type:\n * getFilterDisplayedValues(filter) → get sorted values\n * handleFilterValueClick(list, filter, fv) → toggle + refetch\n *\n * Only the **visual rendering** changes based on displayType:\n * BOX / LIST → checkboxes\n * SWATCH → color circles or thumbnail images\n * NUMBER_RANGE_LIST → predefined range buttons\n */\nexport default function 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 {/* SWATCH: render color circles / thumbnail images */}\n {isSwatchFilter(filter) ? (\n <div style={{ display: \"flex\", gap: 8, flexWrap: \"wrap\" }}>\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button\n key={fv.name}\n onClick={() => handleFilterValueClick(productList, filter, fv)}\n title={fv.name}\n style={{\n width: 32,\n height: 32,\n borderRadius: \"50%\",\n border: fv.isSelected === true ? \"2px solid #000\" : \"2px solid #ddd\",\n padding: 0,\n cursor: \"pointer\",\n overflow: \"hidden\",\n }}\n >\n {thumbnail ? (\n <img\n src={getDefaultSrc(thumbnail)}\n alt={fv.name}\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n />\n ) : (\n <span\n style={{\n display: \"block\",\n width: \"100%\",\n height: \"100%\",\n backgroundColor: fv.colorCode ?? \"#ccc\",\n borderRadius: \"50%\",\n }}\n />\n )}\n </button>\n );\n })}\n </div>\n ) : (\n /* BOX / LIST: render checkboxes (default) */\n <ul>\n {values.map((fv) => (\n <li key={fv.name}>\n <label>\n <input\n type=\"checkbox\"\n checked={fv.isSelected === true}\n onChange={() =>\n handleFilterValueClick(productList, filter, fv)\n }\n />\n {fv.name}\n {fv.
|
|
17319
|
+
"code": "import {\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getDefaultSrc,\n IkasProductList,\n IkasProductFilter,\n} from \"@ikas/bp-storefront\";\n\n/**\n * Display-type-aware product filter.\n *\n * Data flow is the same for every display type:\n * getFilterDisplayedValues(filter) → get sorted values\n * handleFilterValueClick(list, filter, fv) → toggle + refetch\n *\n * Only the **visual rendering** changes based on displayType:\n * BOX / LIST → checkboxes\n * SWATCH → color circles or thumbnail images\n * NUMBER_RANGE_LIST → predefined range buttons\n */\nexport default function 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 {/* SWATCH: render color circles / thumbnail images */}\n {isSwatchFilter(filter) ? (\n <div style={{ display: \"flex\", gap: 8, flexWrap: \"wrap\" }}>\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button\n key={fv.name}\n onClick={() => handleFilterValueClick(productList, filter, fv)}\n title={fv.name}\n style={{\n width: 32,\n height: 32,\n borderRadius: \"50%\",\n border: fv.isSelected === true ? \"2px solid #000\" : \"2px solid #ddd\",\n padding: 0,\n cursor: \"pointer\",\n overflow: \"hidden\",\n }}\n >\n {thumbnail ? (\n <img\n src={getDefaultSrc(thumbnail)}\n alt={fv.name}\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n />\n ) : (\n <span\n style={{\n display: \"block\",\n width: \"100%\",\n height: \"100%\",\n backgroundColor: fv.colorCode ?? \"#ccc\",\n borderRadius: \"50%\",\n }}\n />\n )}\n </button>\n );\n })}\n </div>\n ) : (\n /* BOX / LIST: render checkboxes (default) */\n <ul>\n {values.map((fv) => (\n <li key={fv.name}>\n <label>\n <input\n type=\"checkbox\"\n checked={fv.isSelected === true}\n onChange={() =>\n handleFilterValueClick(productList, filter, fv)\n }\n />\n {fv.name}\n {fv.resultCount != null && ` (${fv.resultCount})`}\n </label>\n </li>\n ))}\n </ul>\n )}\n\n {/* NUMBER_RANGE_LIST: render predefined range buttons */}\n {filter.numberRangeListOptions?.map((option) => (\n <button\n key={`${option.from}-${option.to}`}\n style={{\n fontWeight: option.isSelected ? \"bold\" : \"normal\",\n marginRight: 8,\n }}\n onClick={() =>\n handleNumberRangeOptionClick(productList, filter, option)\n }\n >\n {option.from} - {option.to ?? \"+\"}\n </button>\n ))}\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",
|
|
17108
17320
|
"relatedFunctions": [
|
|
17109
17321
|
"getFilterDisplayedValues",
|
|
17110
17322
|
"handleFilterValueClick",
|
|
@@ -17128,7 +17340,7 @@
|
|
|
17128
17340
|
"id": "product-list-section",
|
|
17129
17341
|
"title": "Product List Section (Complete)",
|
|
17130
17342
|
"description": "Complete product list section with category breadcrumb (getCategoryPath + getIkasCategoryHref), filter sidebar with handleFilterValueClick and onFilterCategoryClick, sort via setSortType (not direct mutation), search via searchProductList, product grid, and pagination with setProductListVisiblePage. Data is automatically reactive in root components via autorun().",
|
|
17131
|
-
"code": "import { useState } from \"preact/hooks\";\nimport {\n IkasProductList,\n IkasProductListSortType,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n setProductListVisiblePage,\n searchProductList,\n getCategoryPath,\n getIkasCategoryHref,\n isEmpty,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductListSection({\n productList,\n title = \"Products\",\n showFilters = true,\n backgroundColor = \"#ffffff\",\n emptyMessage = \"No products found.\",\n searchPlaceholder = \"Search products...\",\n categoriesTitle = \"Categories\",\n}: Props) {\n const [searchKeyword, setSearchKeyword] = useState(\"\");\n\n if (!productList) return null;\n\n const products = productList.data ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const sortOptions = getProductListSortOptions(productList);\n const hasNext = hasProductListNextPage(productList);\n const hasPrev = hasProductListPrevPage(productList);\n\n // Category breadcrumb path\n const category = productList.category;\n const categoryPath = category ? getCategoryPath(category) : [];\n\n const handleSort = (value: string) => {\n const selected = sortOptions.find((opt) => opt.value === value);\n if (selected) {\n setSortType(productList, selected.value as IkasProductListSortType);\n }\n };\n\n const handleSearch = (keyword: string) => {\n setSearchKeyword(keyword);\n searchProductList(productList, keyword);\n };\n\n return (\n <section className=\"product-list-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"product-list-inner\">\n {/* Category Breadcrumb */}\n {categoryPath.length > 0 && (\n <nav className=\"product-list-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((cat: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\"> / </span>\n <a href={getIkasCategoryHref(cat)}>{cat.name}</a>\n </span>\n ))}\n </nav>\n )}\n\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">\n {category?.name || title}\n {category?.description && (\n <p className=\"product-list-description\">{category.description}</p>\n )}\n </h1>\n\n <div className=\"product-list-controls\">\n {/* Search */}\n <input\n className=\"product-list-search\"\n type=\"text\"\n placeholder={searchPlaceholder}\n value={searchKeyword}\n onInput={(e) => handleSearch((e.target as HTMLInputElement).value)}\n />\n\n {/* Sort — use setSortType instead of direct mutation */}\n {sortOptions.length > 0 && (\n <select\n className=\"product-list-sort\"\n value={sortOptions.find((o) => o.isSelected)?.value ?? \"\"}\n onChange={(e) => handleSort((e.target as HTMLSelectElement).value)}\n >\n {sortOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )}\n </div>\n </div>\n\n <div className=\"product-list-layout\">\n {/* Filter Sidebar — display-type-aware rendering */}\n {showFilters && (productList.filters ?? []).length > 0 && (\n <aside className=\"product-list-filters\">\n {(productList.filters ?? []).map((filter) => {\n const values = getFilterDisplayedValues(filter);\n if (values.length === 0 && !filter.numberRangeListOptions?.length) return null;\n\n return (\n <div key={filter.id} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{filter.name}</h3>\n\n {/* SWATCH: color circles / thumbnail images */}\n {isSwatchFilter(filter) ? (\n <div className=\"filter-swatches\">\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button\n key={fv.name}\n className={`filter-swatch${fv.isSelected === true ? \" selected\" : \"\"}`}\n onClick={() => handleFilterValueClick(productList, filter, fv)}\n title={fv.name}\n >\n {thumbnail ? (\n <img src={getDefaultSrc(thumbnail)} alt={fv.name} />\n ) : (\n <span\n className=\"filter-swatch-color\"\n style={{ backgroundColor: fv.colorCode ?? \"#ccc\" }}\n />\n )}\n </button>\n );\n })}\n </div>\n ) : (\n /* BOX / LIST: checkboxes (default) */\n <div className=\"filter-values\">\n {values.map((fv) => (\n <label key={fv.name} className=\"filter-value\">\n <input\n type=\"checkbox\"\n checked={fv.isSelected === true}\n onChange={() =>\n handleFilterValueClick(productList, filter, fv)\n }\n />\n <span>{fv.name}</span>\n {fv.count != null && (\n <span className=\"filter-count\">({fv.count})</span>\n )}\n </label>\n ))}\n </div>\n )}\n\n {/* NUMBER_RANGE_LIST: predefined range buttons */}\n {filter.numberRangeListOptions?.map((option) => (\n <button\n key={`${option.from}-${option.to}`}\n className={`filter-range-btn${option.isSelected ? \" selected\" : \"\"}`}\n onClick={() => handleNumberRangeOptionClick(productList, filter, option)}\n >\n {option.from} - {option.to ?? \"+\"}\n </button>\n ))}\n </div>\n );\n })}\n\n {/* Category-level filtering */}\n {filterCategories.length > 0 && (\n <div className=\"filter-group\">\n <h3 className=\"filter-group-title\">{categoriesTitle}</h3>\n {filterCategories.map((cat) => (\n <button\n key={cat.name}\n className=\"filter-category-btn\"\n onClick={() => onFilterCategoryClick(productList, cat, true)}\n >\n {cat.name}\n </button>\n ))}\n </div>\n )}\n </aside>\n )}\n\n {/* Product Grid — IkasThemeInfiniteScroller pattern for infinite scroll */}\n <div className=\"product-grid\">\n {isEmpty(products) && (\n <p className=\"product-grid-empty\">{emptyMessage}</p>\n )}\n {products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const href = getSelectedProductVariantHref(product);\n\n return (\n <a key={product.id} href={href} className=\"product-card\">\n <div className=\"product-card-image-wrap\">\n {image && (\n <img\n src={getDefaultSrc(image)}\n alt={product.name}\n className=\"product-card-image\"\n />\n )}\n </div>\n <div className=\"product-card-info\">\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </div>\n </a>\n );\n })}\n </div>\n </div>\n\n {/* Pagination — includes setProductListVisiblePage for page jumping */}\n {(hasPrev || hasNext) && (\n <div className=\"product-list-pagination\">\n <button\n className=\"pagination-btn\"\n disabled={!hasPrev}\n onClick={() => getProductListPrevPage(productList)}\n >\n Previous\n </button>\n <span className=\"pagination-info\">\n Page {productList.page}\n </span>\n <button\n className=\"pagination-btn\"\n disabled={!hasNext}\n onClick={() => getProductListNextPage(productList)}\n >\n Next\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
17343
|
+
"code": "import { useState } from \"preact/hooks\";\nimport {\n IkasProductList,\n IkasProductListSortType,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n setProductListVisiblePage,\n searchProductList,\n getCategoryPath,\n getIkasCategoryHref,\n isEmpty,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductListSection({\n productList,\n title = \"Products\",\n showFilters = true,\n backgroundColor = \"#ffffff\",\n emptyMessage = \"No products found.\",\n searchPlaceholder = \"Search products...\",\n categoriesTitle = \"Categories\",\n}: Props) {\n const [searchKeyword, setSearchKeyword] = useState(\"\");\n\n if (!productList) return null;\n\n const products = productList.data ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const sortOptions = getProductListSortOptions(productList);\n const hasNext = hasProductListNextPage(productList);\n const hasPrev = hasProductListPrevPage(productList);\n\n // Category breadcrumb path\n const category = productList.category;\n const categoryPath = category ? getCategoryPath(category) : [];\n\n const handleSort = (value: string) => {\n const selected = sortOptions.find((opt) => opt.value === value);\n if (selected) {\n setSortType(productList, selected.value as IkasProductListSortType);\n }\n };\n\n const handleSearch = (keyword: string) => {\n setSearchKeyword(keyword);\n searchProductList(productList, keyword);\n };\n\n return (\n <section className=\"product-list-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"product-list-inner\">\n {/* Category Breadcrumb */}\n {categoryPath.length > 0 && (\n <nav className=\"product-list-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((cat: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\"> / </span>\n <a href={getIkasCategoryHref(cat)}>{cat.name}</a>\n </span>\n ))}\n </nav>\n )}\n\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">\n {category?.name || title}\n {category?.description && (\n <p className=\"product-list-description\">{category.description}</p>\n )}\n </h1>\n\n <div className=\"product-list-controls\">\n {/* Search */}\n <input\n className=\"product-list-search\"\n type=\"text\"\n placeholder={searchPlaceholder}\n value={searchKeyword}\n onInput={(e) => handleSearch((e.target as HTMLInputElement).value)}\n />\n\n {/* Sort — use setSortType instead of direct mutation */}\n {sortOptions.length > 0 && (\n <select\n className=\"product-list-sort\"\n value={sortOptions.find((o) => o.isSelected)?.value ?? \"\"}\n onChange={(e) => handleSort((e.target as HTMLSelectElement).value)}\n >\n {sortOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )}\n </div>\n </div>\n\n <div className=\"product-list-layout\">\n {/* Filter Sidebar — display-type-aware rendering */}\n {showFilters && (productList.filters ?? []).length > 0 && (\n <aside className=\"product-list-filters\">\n {(productList.filters ?? []).map((filter) => {\n const values = getFilterDisplayedValues(filter);\n if (values.length === 0 && !filter.numberRangeListOptions?.length) return null;\n\n return (\n <div key={filter.id} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{filter.name}</h3>\n\n {/* SWATCH: color circles / thumbnail images */}\n {isSwatchFilter(filter) ? (\n <div className=\"filter-swatches\">\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button\n key={fv.name}\n className={`filter-swatch${fv.isSelected === true ? \" selected\" : \"\"}`}\n onClick={() => handleFilterValueClick(productList, filter, fv)}\n title={fv.name}\n >\n {thumbnail ? (\n <img src={getDefaultSrc(thumbnail)} alt={fv.name} />\n ) : (\n <span\n className=\"filter-swatch-color\"\n style={{ backgroundColor: fv.colorCode ?? \"#ccc\" }}\n />\n )}\n </button>\n );\n })}\n </div>\n ) : (\n /* BOX / LIST: checkboxes (default) */\n <div className=\"filter-values\">\n {values.map((fv) => (\n <label key={fv.name} className=\"filter-value\">\n <input\n type=\"checkbox\"\n checked={fv.isSelected === true}\n onChange={() =>\n handleFilterValueClick(productList, filter, fv)\n }\n />\n <span>{fv.name}</span>\n {fv.resultCount != null && (\n <span className=\"filter-count\">({fv.resultCount})</span>\n )}\n </label>\n ))}\n </div>\n )}\n\n {/* NUMBER_RANGE_LIST: predefined range buttons */}\n {filter.numberRangeListOptions?.map((option) => (\n <button\n key={`${option.from}-${option.to}`}\n className={`filter-range-btn${option.isSelected ? \" selected\" : \"\"}`}\n onClick={() => handleNumberRangeOptionClick(productList, filter, option)}\n >\n {option.from} - {option.to ?? \"+\"}\n </button>\n ))}\n </div>\n );\n })}\n\n {/* Category-level filtering */}\n {filterCategories.length > 0 && (\n <div className=\"filter-group\">\n <h3 className=\"filter-group-title\">{categoriesTitle}</h3>\n {filterCategories.map((cat) => (\n <button\n key={cat.name}\n className=\"filter-category-btn\"\n onClick={() => onFilterCategoryClick(productList, cat, true)}\n >\n {cat.name}\n </button>\n ))}\n </div>\n )}\n </aside>\n )}\n\n {/* Product Grid — IkasThemeInfiniteScroller pattern for infinite scroll */}\n <div className=\"product-grid\">\n {isEmpty(products) && (\n <p className=\"product-grid-empty\">{emptyMessage}</p>\n )}\n {products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const href = getSelectedProductVariantHref(product);\n\n return (\n <a key={product.id} href={href} className=\"product-card\">\n <div className=\"product-card-image-wrap\">\n {image && (\n <img\n src={getDefaultSrc(image)}\n alt={product.name}\n className=\"product-card-image\"\n />\n )}\n </div>\n <div className=\"product-card-info\">\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </div>\n </a>\n );\n })}\n </div>\n </div>\n\n {/* Pagination — includes setProductListVisiblePage for page jumping */}\n {(hasPrev || hasNext) && (\n <div className=\"product-list-pagination\">\n <button\n className=\"pagination-btn\"\n disabled={!hasPrev}\n onClick={() => getProductListPrevPage(productList)}\n >\n Previous\n </button>\n <span className=\"pagination-info\">\n Page {productList.page}\n </span>\n <button\n className=\"pagination-btn\"\n disabled={!hasNext}\n onClick={() => getProductListNextPage(productList)}\n >\n Next\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n",
|
|
17132
17344
|
"relatedFunctions": [
|
|
17133
17345
|
"getSelectedProductVariant",
|
|
17134
17346
|
"getProductVariantFormattedFinalPrice",
|
|
@@ -17166,7 +17378,7 @@
|
|
|
17166
17378
|
"files": [
|
|
17167
17379
|
{
|
|
17168
17380
|
"filename": "index.tsx",
|
|
17169
|
-
"content": "import { useState } from \"preact/hooks\";\nimport {\n IkasProductList,\n IkasProductListSortType,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n setProductListVisiblePage,\n searchProductList,\n getCategoryPath,\n getIkasCategoryHref,\n isEmpty,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductListSection({\n productList,\n title = \"Products\",\n showFilters = true,\n backgroundColor = \"#ffffff\",\n emptyMessage = \"No products found.\",\n searchPlaceholder = \"Search products...\",\n categoriesTitle = \"Categories\",\n}: Props) {\n const [searchKeyword, setSearchKeyword] = useState(\"\");\n\n if (!productList) return null;\n\n const products = productList.data ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const sortOptions = getProductListSortOptions(productList);\n const hasNext = hasProductListNextPage(productList);\n const hasPrev = hasProductListPrevPage(productList);\n\n // Category breadcrumb path\n const category = productList.category;\n const categoryPath = category ? getCategoryPath(category) : [];\n\n const handleSort = (value: string) => {\n const selected = sortOptions.find((opt) => opt.value === value);\n if (selected) {\n setSortType(productList, selected.value as IkasProductListSortType);\n }\n };\n\n const handleSearch = (keyword: string) => {\n setSearchKeyword(keyword);\n searchProductList(productList, keyword);\n };\n\n return (\n <section className=\"product-list-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"product-list-inner\">\n {/* Category Breadcrumb */}\n {categoryPath.length > 0 && (\n <nav className=\"product-list-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((cat: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\"> / </span>\n <a href={getIkasCategoryHref(cat)}>{cat.name}</a>\n </span>\n ))}\n </nav>\n )}\n\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">\n {category?.name || title}\n {category?.description && (\n <p className=\"product-list-description\">{category.description}</p>\n )}\n </h1>\n\n <div className=\"product-list-controls\">\n {/* Search */}\n <input\n className=\"product-list-search\"\n type=\"text\"\n placeholder={searchPlaceholder}\n value={searchKeyword}\n onInput={(e) => handleSearch((e.target as HTMLInputElement).value)}\n />\n\n {/* Sort — use setSortType instead of direct mutation */}\n {sortOptions.length > 0 && (\n <select\n className=\"product-list-sort\"\n value={sortOptions.find((o) => o.isSelected)?.value ?? \"\"}\n onChange={(e) => handleSort((e.target as HTMLSelectElement).value)}\n >\n {sortOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )}\n </div>\n </div>\n\n <div className=\"product-list-layout\">\n {/* Filter Sidebar — display-type-aware rendering */}\n {showFilters && (productList.filters ?? []).length > 0 && (\n <aside className=\"product-list-filters\">\n {(productList.filters ?? []).map((filter) => {\n const values = getFilterDisplayedValues(filter);\n if (values.length === 0 && !filter.numberRangeListOptions?.length) return null;\n\n return (\n <div key={filter.id} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{filter.name}</h3>\n\n {/* SWATCH: color circles / thumbnail images */}\n {isSwatchFilter(filter) ? (\n <div className=\"filter-swatches\">\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button\n key={fv.name}\n className={`filter-swatch${fv.isSelected === true ? \" selected\" : \"\"}`}\n onClick={() => handleFilterValueClick(productList, filter, fv)}\n title={fv.name}\n >\n {thumbnail ? (\n <img src={getDefaultSrc(thumbnail)} alt={fv.name} />\n ) : (\n <span\n className=\"filter-swatch-color\"\n style={{ backgroundColor: fv.colorCode ?? \"#ccc\" }}\n />\n )}\n </button>\n );\n })}\n </div>\n ) : (\n /* BOX / LIST: checkboxes (default) */\n <div className=\"filter-values\">\n {values.map((fv) => (\n <label key={fv.name} className=\"filter-value\">\n <input\n type=\"checkbox\"\n checked={fv.isSelected === true}\n onChange={() =>\n handleFilterValueClick(productList, filter, fv)\n }\n />\n <span>{fv.name}</span>\n {fv.count != null && (\n <span className=\"filter-count\">({fv.count})</span>\n )}\n </label>\n ))}\n </div>\n )}\n\n {/* NUMBER_RANGE_LIST: predefined range buttons */}\n {filter.numberRangeListOptions?.map((option) => (\n <button\n key={`${option.from}-${option.to}`}\n className={`filter-range-btn${option.isSelected ? \" selected\" : \"\"}`}\n onClick={() => handleNumberRangeOptionClick(productList, filter, option)}\n >\n {option.from} - {option.to ?? \"+\"}\n </button>\n ))}\n </div>\n );\n })}\n\n {/* Category-level filtering */}\n {filterCategories.length > 0 && (\n <div className=\"filter-group\">\n <h3 className=\"filter-group-title\">{categoriesTitle}</h3>\n {filterCategories.map((cat) => (\n <button\n key={cat.name}\n className=\"filter-category-btn\"\n onClick={() => onFilterCategoryClick(productList, cat, true)}\n >\n {cat.name}\n </button>\n ))}\n </div>\n )}\n </aside>\n )}\n\n {/* Product Grid — IkasThemeInfiniteScroller pattern for infinite scroll */}\n <div className=\"product-grid\">\n {isEmpty(products) && (\n <p className=\"product-grid-empty\">{emptyMessage}</p>\n )}\n {products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const href = getSelectedProductVariantHref(product);\n\n return (\n <a key={product.id} href={href} className=\"product-card\">\n <div className=\"product-card-image-wrap\">\n {image && (\n <img\n src={getDefaultSrc(image)}\n alt={product.name}\n className=\"product-card-image\"\n />\n )}\n </div>\n <div className=\"product-card-info\">\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </div>\n </a>\n );\n })}\n </div>\n </div>\n\n {/* Pagination — includes setProductListVisiblePage for page jumping */}\n {(hasPrev || hasNext) && (\n <div className=\"product-list-pagination\">\n <button\n className=\"pagination-btn\"\n disabled={!hasPrev}\n onClick={() => getProductListPrevPage(productList)}\n >\n Previous\n </button>\n <span className=\"pagination-info\">\n Page {productList.page}\n </span>\n <button\n className=\"pagination-btn\"\n disabled={!hasNext}\n onClick={() => getProductListNextPage(productList)}\n >\n Next\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n"
|
|
17381
|
+
"content": "import { useState } from \"preact/hooks\";\nimport {\n IkasProductList,\n IkasProductListSortType,\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariantHref,\n getFilterDisplayedValues,\n handleFilterValueClick,\n handleNumberRangeOptionClick,\n getProductListFilterCategories,\n onFilterCategoryClick,\n isSwatchFilter,\n getIkasFilterThumbnailImage,\n getProductListSortOptions,\n setSortType,\n hasProductListNextPage,\n hasProductListPrevPage,\n getProductListNextPage,\n getProductListPrevPage,\n setProductListVisiblePage,\n searchProductList,\n getCategoryPath,\n getIkasCategoryHref,\n isEmpty,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductListSection({\n productList,\n title = \"Products\",\n showFilters = true,\n backgroundColor = \"#ffffff\",\n emptyMessage = \"No products found.\",\n searchPlaceholder = \"Search products...\",\n categoriesTitle = \"Categories\",\n}: Props) {\n const [searchKeyword, setSearchKeyword] = useState(\"\");\n\n if (!productList) return null;\n\n const products = productList.data ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const sortOptions = getProductListSortOptions(productList);\n const hasNext = hasProductListNextPage(productList);\n const hasPrev = hasProductListPrevPage(productList);\n\n // Category breadcrumb path\n const category = productList.category;\n const categoryPath = category ? getCategoryPath(category) : [];\n\n const handleSort = (value: string) => {\n const selected = sortOptions.find((opt) => opt.value === value);\n if (selected) {\n setSortType(productList, selected.value as IkasProductListSortType);\n }\n };\n\n const handleSearch = (keyword: string) => {\n setSearchKeyword(keyword);\n searchProductList(productList, keyword);\n };\n\n return (\n <section className=\"product-list-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"product-list-inner\">\n {/* Category Breadcrumb */}\n {categoryPath.length > 0 && (\n <nav className=\"product-list-breadcrumb\">\n <a href=\"/\">Home</a>\n {categoryPath.map((cat: any, i: number) => (\n <span key={i}>\n <span className=\"breadcrumb-sep\"> / </span>\n <a href={getIkasCategoryHref(cat)}>{cat.name}</a>\n </span>\n ))}\n </nav>\n )}\n\n <div className=\"product-list-header\">\n <h1 className=\"product-list-title\">\n {category?.name || title}\n {category?.description && (\n <p className=\"product-list-description\">{category.description}</p>\n )}\n </h1>\n\n <div className=\"product-list-controls\">\n {/* Search */}\n <input\n className=\"product-list-search\"\n type=\"text\"\n placeholder={searchPlaceholder}\n value={searchKeyword}\n onInput={(e) => handleSearch((e.target as HTMLInputElement).value)}\n />\n\n {/* Sort — use setSortType instead of direct mutation */}\n {sortOptions.length > 0 && (\n <select\n className=\"product-list-sort\"\n value={sortOptions.find((o) => o.isSelected)?.value ?? \"\"}\n onChange={(e) => handleSort((e.target as HTMLSelectElement).value)}\n >\n {sortOptions.map((opt) => (\n <option key={opt.value} value={opt.value}>{opt.label}</option>\n ))}\n </select>\n )}\n </div>\n </div>\n\n <div className=\"product-list-layout\">\n {/* Filter Sidebar — display-type-aware rendering */}\n {showFilters && (productList.filters ?? []).length > 0 && (\n <aside className=\"product-list-filters\">\n {(productList.filters ?? []).map((filter) => {\n const values = getFilterDisplayedValues(filter);\n if (values.length === 0 && !filter.numberRangeListOptions?.length) return null;\n\n return (\n <div key={filter.id} className=\"filter-group\">\n <h3 className=\"filter-group-title\">{filter.name}</h3>\n\n {/* SWATCH: color circles / thumbnail images */}\n {isSwatchFilter(filter) ? (\n <div className=\"filter-swatches\">\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button\n key={fv.name}\n className={`filter-swatch${fv.isSelected === true ? \" selected\" : \"\"}`}\n onClick={() => handleFilterValueClick(productList, filter, fv)}\n title={fv.name}\n >\n {thumbnail ? (\n <img src={getDefaultSrc(thumbnail)} alt={fv.name} />\n ) : (\n <span\n className=\"filter-swatch-color\"\n style={{ backgroundColor: fv.colorCode ?? \"#ccc\" }}\n />\n )}\n </button>\n );\n })}\n </div>\n ) : (\n /* BOX / LIST: checkboxes (default) */\n <div className=\"filter-values\">\n {values.map((fv) => (\n <label key={fv.name} className=\"filter-value\">\n <input\n type=\"checkbox\"\n checked={fv.isSelected === true}\n onChange={() =>\n handleFilterValueClick(productList, filter, fv)\n }\n />\n <span>{fv.name}</span>\n {fv.resultCount != null && (\n <span className=\"filter-count\">({fv.resultCount})</span>\n )}\n </label>\n ))}\n </div>\n )}\n\n {/* NUMBER_RANGE_LIST: predefined range buttons */}\n {filter.numberRangeListOptions?.map((option) => (\n <button\n key={`${option.from}-${option.to}`}\n className={`filter-range-btn${option.isSelected ? \" selected\" : \"\"}`}\n onClick={() => handleNumberRangeOptionClick(productList, filter, option)}\n >\n {option.from} - {option.to ?? \"+\"}\n </button>\n ))}\n </div>\n );\n })}\n\n {/* Category-level filtering */}\n {filterCategories.length > 0 && (\n <div className=\"filter-group\">\n <h3 className=\"filter-group-title\">{categoriesTitle}</h3>\n {filterCategories.map((cat) => (\n <button\n key={cat.name}\n className=\"filter-category-btn\"\n onClick={() => onFilterCategoryClick(productList, cat, true)}\n >\n {cat.name}\n </button>\n ))}\n </div>\n )}\n </aside>\n )}\n\n {/* Product Grid — IkasThemeInfiniteScroller pattern for infinite scroll */}\n <div className=\"product-grid\">\n {isEmpty(products) && (\n <p className=\"product-grid-empty\">{emptyMessage}</p>\n )}\n {products.map((product) => {\n const variant = getSelectedProductVariant(product);\n const productImage = getProductVariantMainImage(variant);\n const image = productImage?.image ?? null;\n const price = getProductVariantFormattedFinalPrice(variant) as unknown as string;\n const href = getSelectedProductVariantHref(product);\n\n return (\n <a key={product.id} href={href} className=\"product-card\">\n <div className=\"product-card-image-wrap\">\n {image && (\n <img\n src={getDefaultSrc(image)}\n alt={product.name}\n className=\"product-card-image\"\n />\n )}\n </div>\n <div className=\"product-card-info\">\n <h3 className=\"product-card-name\">{product.name}</h3>\n <span className=\"product-card-price\">{price}</span>\n </div>\n </a>\n );\n })}\n </div>\n </div>\n\n {/* Pagination — includes setProductListVisiblePage for page jumping */}\n {(hasPrev || hasNext) && (\n <div className=\"product-list-pagination\">\n <button\n className=\"pagination-btn\"\n disabled={!hasPrev}\n onClick={() => getProductListPrevPage(productList)}\n >\n Previous\n </button>\n <span className=\"pagination-info\">\n Page {productList.page}\n </span>\n <button\n className=\"pagination-btn\"\n disabled={!hasNext}\n onClick={() => getProductListNextPage(productList)}\n >\n Next\n </button>\n </div>\n )}\n </div>\n </section>\n );\n}\n\n"
|
|
17170
17382
|
},
|
|
17171
17383
|
{
|
|
17172
17384
|
"filename": "types.ts",
|
|
@@ -17186,7 +17398,7 @@
|
|
|
17186
17398
|
"id": "product-reviews-section",
|
|
17187
17399
|
"title": "Product Reviews Section (Complete)",
|
|
17188
17400
|
"description": "Complete product reviews section with review list display, star ratings, review submission form with login-required check. getProductCustomerReviews supports limit, page, and hasImage params. Review data is automatically reactive in root components via autorun().",
|
|
17189
|
-
"code": "import { useState } from \"preact/hooks\";\nimport {\n IkasProduct,\n getProductCustomerReviews,\n getIkasProductCustomerReviewForm,\n setCustomerReviewFormTitle,\n setCustomerReviewFormStar,\n setCustomerReviewFormComment,\n submitCustomerReviewForm,\n isCustomerReviewLoginRequired,\n getIkasCustomerReviewFormattedDate,\n customerStore,\n hasCustomer,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductReviewsSection({\n product,\n title = \"Customer Reviews\",\n backgroundColor = \"#ffffff\",\n writeReviewText = \"Write a Review\",\n ratingLabel = \"Rating\",\n emptyMessage = \"No reviews yet. Be the first to review!\",\n submitButtonText = \"Submit Review\",\n submittingButtonText = \"Submitting...\",\n cancelButtonText = \"Cancel\",\n}: Props) {\n const [showForm, setShowForm] = useState(false);\n\n if (
|
|
17401
|
+
"code": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n IkasProduct,\n getProductCustomerReviews,\n getIkasProductCustomerReviewForm,\n setCustomerReviewFormTitle,\n setCustomerReviewFormStar,\n setCustomerReviewFormComment,\n submitCustomerReviewForm,\n isCustomerReviewLoginRequired,\n getIkasCustomerReviewFormattedDate,\n customerStore,\n hasCustomer,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductReviewsSection({\n product,\n title = \"Customer Reviews\",\n backgroundColor = \"#ffffff\",\n writeReviewText = \"Write a Review\",\n ratingLabel = \"Rating\",\n emptyMessage = \"No reviews yet. Be the first to review!\",\n submitButtonText = \"Submit Review\",\n submittingButtonText = \"Submitting...\",\n cancelButtonText = \"Cancel\",\n}: Props) {\n const [showForm, setShowForm] = useState(false);\n const [reviews, setReviews] = useState<any[]>([]);\n\n useEffect(() => {\n if (product) {\n getProductCustomerReviews(product).then((list) => {\n setReviews(list?.data ?? []);\n });\n }\n }, [product]);\n\n if (!product) return null;\n\n const reviewForm = getIkasProductCustomerReviewForm(product);\n const loginRequired = isCustomerReviewLoginRequired(product) as unknown as boolean;\n const isLoggedIn = hasCustomer(customerStore) as unknown as boolean;\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitCustomerReviewForm(reviewForm);\n if (success) {\n setShowForm(false);\n }\n };\n\n const StarDisplay = ({ rating }: { rating: number }) => (\n <div className=\"reviews-stars\">\n {[1, 2, 3, 4, 5].map((star) => (\n <span key={star} className={star <= rating ? \"star-filled\" : \"star-empty\"}>\n ★\n </span>\n ))}\n </div>\n );\n\n return (\n <section className=\"reviews-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"reviews-inner\">\n <div className=\"reviews-header\">\n <h2 className=\"reviews-title\">\n {title} ({reviews.length})\n </h2>\n {!showForm && (\n <button\n className=\"reviews-write-btn\"\n onClick={() => {\n if (loginRequired && !isLoggedIn) {\n Router.navigateToPage(\"LOGIN\");\n } else {\n setShowForm(true);\n }\n }}\n >\n {writeReviewText}\n </button>\n )}\n </div>\n\n {/* Review Form */}\n {showForm && (\n <form className=\"review-form\" onSubmit={handleSubmit}>\n {reviewForm.isFailure && reviewForm.responseMessage && (\n <div className=\"review-form-error\">{reviewForm.responseMessage}</div>\n )}\n\n <div className=\"review-form-stars\">\n <span className=\"review-form-label\">{ratingLabel}</span>\n <div className=\"star-input\">\n {[1, 2, 3, 4, 5].map((star) => (\n <button\n key={star}\n type=\"button\"\n className={star <= Number(reviewForm.star.value) ? \"star-filled\" : \"star-empty\"}\n onClick={() => setCustomerReviewFormStar(reviewForm, String(star))}\n >\n ★\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"review-form-field\">\n <label className=\"review-form-label\">{reviewForm.title.label}</label>\n <input\n className=\"review-form-input\"\n type=\"text\"\n placeholder={reviewForm.title.placeholder}\n value={reviewForm.title.value}\n onInput={(e) =>\n setCustomerReviewFormTitle(reviewForm, (e.target as HTMLInputElement).value)\n }\n />\n {reviewForm.title.hasError && reviewForm.title.message && (\n <span className=\"review-form-error-text\">{reviewForm.title.message}</span>\n )}\n </div>\n\n <div className=\"review-form-field\">\n <label className=\"review-form-label\">{reviewForm.comment.label}</label>\n <textarea\n className=\"review-form-textarea\"\n placeholder={reviewForm.comment.placeholder}\n value={reviewForm.comment.value}\n rows={4}\n onInput={(e) =>\n setCustomerReviewFormComment(\n reviewForm,\n (e.target as HTMLTextAreaElement).value\n )\n }\n />\n {reviewForm.comment.hasError && reviewForm.comment.message && (\n <span className=\"review-form-error-text\">{reviewForm.comment.message}</span>\n )}\n </div>\n\n <div className=\"review-form-actions\">\n <button\n type=\"submit\"\n className=\"review-form-submit\"\n disabled={reviewForm.isSubmitting}\n >\n {reviewForm.isSubmitting ? submittingButtonText : submitButtonText}\n </button>\n <button\n type=\"button\"\n className=\"review-form-cancel\"\n onClick={() => setShowForm(false)}\n >\n {cancelButtonText}\n </button>\n </div>\n </form>\n )}\n\n {/* Review List */}\n {reviews.length === 0 && !showForm && (\n <p className=\"reviews-empty\">{emptyMessage}</p>\n )}\n\n <div className=\"review-list\">\n {reviews.map((review) => (\n <div key={review.id} className=\"review-card\">\n <div className=\"review-card-header\">\n <StarDisplay rating={review.star} />\n <span className=\"review-card-date\">\n {getIkasCustomerReviewFormattedDate(review)}\n </span>\n </div>\n <h4 className=\"review-card-title\">{review.title}</h4>\n <p className=\"review-card-comment\">{review.comment}</p>\n <span className=\"review-card-author\">{review.customerName}</span>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n\n",
|
|
17190
17402
|
"relatedFunctions": [
|
|
17191
17403
|
"getProductCustomerReviews",
|
|
17192
17404
|
"getIkasProductCustomerReviewForm",
|
|
@@ -17208,7 +17420,7 @@
|
|
|
17208
17420
|
"files": [
|
|
17209
17421
|
{
|
|
17210
17422
|
"filename": "index.tsx",
|
|
17211
|
-
"content": "import { useState } from \"preact/hooks\";\nimport {\n IkasProduct,\n getProductCustomerReviews,\n getIkasProductCustomerReviewForm,\n setCustomerReviewFormTitle,\n setCustomerReviewFormStar,\n setCustomerReviewFormComment,\n submitCustomerReviewForm,\n isCustomerReviewLoginRequired,\n getIkasCustomerReviewFormattedDate,\n customerStore,\n hasCustomer,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductReviewsSection({\n product,\n title = \"Customer Reviews\",\n backgroundColor = \"#ffffff\",\n writeReviewText = \"Write a Review\",\n ratingLabel = \"Rating\",\n emptyMessage = \"No reviews yet. Be the first to review!\",\n submitButtonText = \"Submit Review\",\n submittingButtonText = \"Submitting...\",\n cancelButtonText = \"Cancel\",\n}: Props) {\n const [showForm, setShowForm] = useState(false);\n\n if (
|
|
17423
|
+
"content": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n IkasProduct,\n getProductCustomerReviews,\n getIkasProductCustomerReviewForm,\n setCustomerReviewFormTitle,\n setCustomerReviewFormStar,\n setCustomerReviewFormComment,\n submitCustomerReviewForm,\n isCustomerReviewLoginRequired,\n getIkasCustomerReviewFormattedDate,\n customerStore,\n hasCustomer,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function ProductReviewsSection({\n product,\n title = \"Customer Reviews\",\n backgroundColor = \"#ffffff\",\n writeReviewText = \"Write a Review\",\n ratingLabel = \"Rating\",\n emptyMessage = \"No reviews yet. Be the first to review!\",\n submitButtonText = \"Submit Review\",\n submittingButtonText = \"Submitting...\",\n cancelButtonText = \"Cancel\",\n}: Props) {\n const [showForm, setShowForm] = useState(false);\n const [reviews, setReviews] = useState<any[]>([]);\n\n useEffect(() => {\n if (product) {\n getProductCustomerReviews(product).then((list) => {\n setReviews(list?.data ?? []);\n });\n }\n }, [product]);\n\n if (!product) return null;\n\n const reviewForm = getIkasProductCustomerReviewForm(product);\n const loginRequired = isCustomerReviewLoginRequired(product) as unknown as boolean;\n const isLoggedIn = hasCustomer(customerStore) as unknown as boolean;\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitCustomerReviewForm(reviewForm);\n if (success) {\n setShowForm(false);\n }\n };\n\n const StarDisplay = ({ rating }: { rating: number }) => (\n <div className=\"reviews-stars\">\n {[1, 2, 3, 4, 5].map((star) => (\n <span key={star} className={star <= rating ? \"star-filled\" : \"star-empty\"}>\n ★\n </span>\n ))}\n </div>\n );\n\n return (\n <section className=\"reviews-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"reviews-inner\">\n <div className=\"reviews-header\">\n <h2 className=\"reviews-title\">\n {title} ({reviews.length})\n </h2>\n {!showForm && (\n <button\n className=\"reviews-write-btn\"\n onClick={() => {\n if (loginRequired && !isLoggedIn) {\n Router.navigateToPage(\"LOGIN\");\n } else {\n setShowForm(true);\n }\n }}\n >\n {writeReviewText}\n </button>\n )}\n </div>\n\n {/* Review Form */}\n {showForm && (\n <form className=\"review-form\" onSubmit={handleSubmit}>\n {reviewForm.isFailure && reviewForm.responseMessage && (\n <div className=\"review-form-error\">{reviewForm.responseMessage}</div>\n )}\n\n <div className=\"review-form-stars\">\n <span className=\"review-form-label\">{ratingLabel}</span>\n <div className=\"star-input\">\n {[1, 2, 3, 4, 5].map((star) => (\n <button\n key={star}\n type=\"button\"\n className={star <= Number(reviewForm.star.value) ? \"star-filled\" : \"star-empty\"}\n onClick={() => setCustomerReviewFormStar(reviewForm, String(star))}\n >\n ★\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"review-form-field\">\n <label className=\"review-form-label\">{reviewForm.title.label}</label>\n <input\n className=\"review-form-input\"\n type=\"text\"\n placeholder={reviewForm.title.placeholder}\n value={reviewForm.title.value}\n onInput={(e) =>\n setCustomerReviewFormTitle(reviewForm, (e.target as HTMLInputElement).value)\n }\n />\n {reviewForm.title.hasError && reviewForm.title.message && (\n <span className=\"review-form-error-text\">{reviewForm.title.message}</span>\n )}\n </div>\n\n <div className=\"review-form-field\">\n <label className=\"review-form-label\">{reviewForm.comment.label}</label>\n <textarea\n className=\"review-form-textarea\"\n placeholder={reviewForm.comment.placeholder}\n value={reviewForm.comment.value}\n rows={4}\n onInput={(e) =>\n setCustomerReviewFormComment(\n reviewForm,\n (e.target as HTMLTextAreaElement).value\n )\n }\n />\n {reviewForm.comment.hasError && reviewForm.comment.message && (\n <span className=\"review-form-error-text\">{reviewForm.comment.message}</span>\n )}\n </div>\n\n <div className=\"review-form-actions\">\n <button\n type=\"submit\"\n className=\"review-form-submit\"\n disabled={reviewForm.isSubmitting}\n >\n {reviewForm.isSubmitting ? submittingButtonText : submitButtonText}\n </button>\n <button\n type=\"button\"\n className=\"review-form-cancel\"\n onClick={() => setShowForm(false)}\n >\n {cancelButtonText}\n </button>\n </div>\n </form>\n )}\n\n {/* Review List */}\n {reviews.length === 0 && !showForm && (\n <p className=\"reviews-empty\">{emptyMessage}</p>\n )}\n\n <div className=\"review-list\">\n {reviews.map((review) => (\n <div key={review.id} className=\"review-card\">\n <div className=\"review-card-header\">\n <StarDisplay rating={review.star} />\n <span className=\"review-card-date\">\n {getIkasCustomerReviewFormattedDate(review)}\n </span>\n </div>\n <h4 className=\"review-card-title\">{review.title}</h4>\n <p className=\"review-card-comment\">{review.comment}</p>\n <span className=\"review-card-author\">{review.customerName}</span>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n\n"
|
|
17212
17424
|
},
|
|
17213
17425
|
{
|
|
17214
17426
|
"filename": "types.ts",
|
|
@@ -17228,7 +17440,7 @@
|
|
|
17228
17440
|
"id": "register-section",
|
|
17229
17441
|
"title": "Register Section (Complete)",
|
|
17230
17442
|
"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.",
|
|
17231
|
-
"code": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getRegisterForm,\n initRegisterForm,\n setRegisterFormEmail,\n setRegisterFormFirstName,\n setRegisterFormLastName,\n setRegisterFormPassword,\n submitRegisterForm,\n setRegisterFormIsMarketingAccepted,\n setRegisterFormIsMembershipAgreementAccepted,\n socialLogin,\n
|
|
17443
|
+
"code": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getRegisterForm,\n initRegisterForm,\n setRegisterFormEmail,\n setRegisterFormFirstName,\n setRegisterFormLastName,\n setRegisterFormPassword,\n submitRegisterForm,\n setRegisterFormIsMarketingAccepted,\n setRegisterFormIsMembershipAgreementAccepted,\n socialLogin,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function RegisterSection({\n redirectAfterRegister = \"/account\",\n backgroundColor = \"#ffffff\",\n title = \"Create Account\",\n googleButtonText = \"Continue with Google\",\n facebookButtonText = \"Continue with Facebook\",\n dividerText = \"or\",\n marketingConsentText = \"I want to receive marketing emails and promotions\",\n agreementConsentText = \"I accept the membership agreement\",\n submitButtonText = \"Create Account\",\n submittingButtonText = \"Creating account...\",\n hasAccountText = \"Already have an account?\",\n signInLinkText = \"Sign in\",\n}: Props) {\n const registerForm = getRegisterForm(customerStore);\n\n useEffect(() => {\n initRegisterForm(registerForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitRegisterForm(registerForm);\n if (success) {\n Router.navigateToPage(\"ACCOUNT\");\n }\n };\n\n const handleSocialRegister = async (provider: \"GOOGLE\" | \"FACEBOOK\" | \"APPLE\") => {\n await socialLogin(customerStore, provider as any);\n };\n\n return (\n <section className=\"register-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"register-inner\">\n <h1 className=\"register-title\">{title}</h1>\n\n {registerForm.isFailure && registerForm.responseMessage && (\n <div className=\"register-error-banner\">{registerForm.responseMessage}</div>\n )}\n\n {/* Social Login Buttons */}\n <div className=\"register-social\">\n <button className=\"register-social-btn\" onClick={() => handleSocialRegister(\"GOOGLE\")}>\n {googleButtonText}\n </button>\n <button className=\"register-social-btn\" onClick={() => handleSocialRegister(\"FACEBOOK\")}>\n {facebookButtonText}\n </button>\n </div>\n\n <div className=\"register-divider\"><span>{dividerText}</span></div>\n\n <form className=\"register-form\" onSubmit={handleSubmit}>\n <div className=\"register-row\">\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.firstName.label}</label>\n <input\n className={`register-input ${registerForm.firstName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={registerForm.firstName.placeholder}\n value={registerForm.firstName.value}\n onInput={(e) =>\n setRegisterFormFirstName(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.firstName.hasError && registerForm.firstName.message && (\n <span className=\"register-field-error\">{registerForm.firstName.message}</span>\n )}\n </div>\n\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.lastName.label}</label>\n <input\n className={`register-input ${registerForm.lastName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={registerForm.lastName.placeholder}\n value={registerForm.lastName.value}\n onInput={(e) =>\n setRegisterFormLastName(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.lastName.hasError && registerForm.lastName.message && (\n <span className=\"register-field-error\">{registerForm.lastName.message}</span>\n )}\n </div>\n </div>\n\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.email.label}</label>\n <input\n className={`register-input ${registerForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={registerForm.email.placeholder}\n value={registerForm.email.value}\n onInput={(e) =>\n setRegisterFormEmail(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.email.hasError && registerForm.email.message && (\n <span className=\"register-field-error\">{registerForm.email.message}</span>\n )}\n </div>\n\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.password.label}</label>\n <input\n className={`register-input ${registerForm.password.hasError ? \"has-error\" : \"\"}`}\n type=\"password\"\n placeholder={registerForm.password.placeholder}\n value={registerForm.password.value}\n onInput={(e) =>\n setRegisterFormPassword(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.password.hasError && registerForm.password.message && (\n <span className=\"register-field-error\">{registerForm.password.message}</span>\n )}\n </div>\n\n {/* Marketing Consent — IkasFormItemBoolean */}\n <label className=\"register-checkbox\">\n <input\n type=\"checkbox\"\n checked={registerForm.isMarketingAccepted?.value ?? false}\n onChange={(e) =>\n setRegisterFormIsMarketingAccepted(registerForm, (e.target as HTMLInputElement).checked)\n }\n />\n <span>{marketingConsentText}</span>\n </label>\n\n {/* Membership Agreement — IkasFormItemBoolean */}\n <label className=\"register-checkbox\">\n <input\n type=\"checkbox\"\n checked={registerForm.isMembershipAgreementAccepted?.value ?? false}\n onChange={(e) =>\n setRegisterFormIsMembershipAgreementAccepted(registerForm, (e.target as HTMLInputElement).checked)\n }\n />\n <span>{agreementConsentText}</span>\n </label>\n\n <button\n className=\"register-submit-btn\"\n type=\"submit\"\n disabled={registerForm.isSubmitting}\n >\n {registerForm.isSubmitting ? submittingButtonText : submitButtonText}\n </button>\n </form>\n\n <p className=\"register-login-link\">\n {hasAccountText}{\" \"}\n <a\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n Router.navigateToPage(\"LOGIN\");\n }}\n >\n {signInLinkText}\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n",
|
|
17232
17444
|
"relatedFunctions": [
|
|
17233
17445
|
"initRegisterForm",
|
|
17234
17446
|
"setRegisterFormEmail",
|
|
@@ -17250,7 +17462,7 @@
|
|
|
17250
17462
|
"files": [
|
|
17251
17463
|
{
|
|
17252
17464
|
"filename": "index.tsx",
|
|
17253
|
-
"content": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getRegisterForm,\n initRegisterForm,\n setRegisterFormEmail,\n setRegisterFormFirstName,\n setRegisterFormLastName,\n setRegisterFormPassword,\n submitRegisterForm,\n setRegisterFormIsMarketingAccepted,\n setRegisterFormIsMembershipAgreementAccepted,\n socialLogin,\n
|
|
17465
|
+
"content": "import { useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getRegisterForm,\n initRegisterForm,\n setRegisterFormEmail,\n setRegisterFormFirstName,\n setRegisterFormLastName,\n setRegisterFormPassword,\n submitRegisterForm,\n setRegisterFormIsMarketingAccepted,\n setRegisterFormIsMembershipAgreementAccepted,\n socialLogin,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport default function RegisterSection({\n redirectAfterRegister = \"/account\",\n backgroundColor = \"#ffffff\",\n title = \"Create Account\",\n googleButtonText = \"Continue with Google\",\n facebookButtonText = \"Continue with Facebook\",\n dividerText = \"or\",\n marketingConsentText = \"I want to receive marketing emails and promotions\",\n agreementConsentText = \"I accept the membership agreement\",\n submitButtonText = \"Create Account\",\n submittingButtonText = \"Creating account...\",\n hasAccountText = \"Already have an account?\",\n signInLinkText = \"Sign in\",\n}: Props) {\n const registerForm = getRegisterForm(customerStore);\n\n useEffect(() => {\n initRegisterForm(registerForm);\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitRegisterForm(registerForm);\n if (success) {\n Router.navigateToPage(\"ACCOUNT\");\n }\n };\n\n const handleSocialRegister = async (provider: \"GOOGLE\" | \"FACEBOOK\" | \"APPLE\") => {\n await socialLogin(customerStore, provider as any);\n };\n\n return (\n <section className=\"register-section\" style={backgroundColor ? { backgroundColor } : undefined}>\n <div className=\"register-inner\">\n <h1 className=\"register-title\">{title}</h1>\n\n {registerForm.isFailure && registerForm.responseMessage && (\n <div className=\"register-error-banner\">{registerForm.responseMessage}</div>\n )}\n\n {/* Social Login Buttons */}\n <div className=\"register-social\">\n <button className=\"register-social-btn\" onClick={() => handleSocialRegister(\"GOOGLE\")}>\n {googleButtonText}\n </button>\n <button className=\"register-social-btn\" onClick={() => handleSocialRegister(\"FACEBOOK\")}>\n {facebookButtonText}\n </button>\n </div>\n\n <div className=\"register-divider\"><span>{dividerText}</span></div>\n\n <form className=\"register-form\" onSubmit={handleSubmit}>\n <div className=\"register-row\">\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.firstName.label}</label>\n <input\n className={`register-input ${registerForm.firstName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={registerForm.firstName.placeholder}\n value={registerForm.firstName.value}\n onInput={(e) =>\n setRegisterFormFirstName(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.firstName.hasError && registerForm.firstName.message && (\n <span className=\"register-field-error\">{registerForm.firstName.message}</span>\n )}\n </div>\n\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.lastName.label}</label>\n <input\n className={`register-input ${registerForm.lastName.hasError ? \"has-error\" : \"\"}`}\n type=\"text\"\n placeholder={registerForm.lastName.placeholder}\n value={registerForm.lastName.value}\n onInput={(e) =>\n setRegisterFormLastName(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.lastName.hasError && registerForm.lastName.message && (\n <span className=\"register-field-error\">{registerForm.lastName.message}</span>\n )}\n </div>\n </div>\n\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.email.label}</label>\n <input\n className={`register-input ${registerForm.email.hasError ? \"has-error\" : \"\"}`}\n type=\"email\"\n placeholder={registerForm.email.placeholder}\n value={registerForm.email.value}\n onInput={(e) =>\n setRegisterFormEmail(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.email.hasError && registerForm.email.message && (\n <span className=\"register-field-error\">{registerForm.email.message}</span>\n )}\n </div>\n\n <div className=\"register-field\">\n <label className=\"register-label\">{registerForm.password.label}</label>\n <input\n className={`register-input ${registerForm.password.hasError ? \"has-error\" : \"\"}`}\n type=\"password\"\n placeholder={registerForm.password.placeholder}\n value={registerForm.password.value}\n onInput={(e) =>\n setRegisterFormPassword(registerForm, (e.target as HTMLInputElement).value)\n }\n />\n {registerForm.password.hasError && registerForm.password.message && (\n <span className=\"register-field-error\">{registerForm.password.message}</span>\n )}\n </div>\n\n {/* Marketing Consent — IkasFormItemBoolean */}\n <label className=\"register-checkbox\">\n <input\n type=\"checkbox\"\n checked={registerForm.isMarketingAccepted?.value ?? false}\n onChange={(e) =>\n setRegisterFormIsMarketingAccepted(registerForm, (e.target as HTMLInputElement).checked)\n }\n />\n <span>{marketingConsentText}</span>\n </label>\n\n {/* Membership Agreement — IkasFormItemBoolean */}\n <label className=\"register-checkbox\">\n <input\n type=\"checkbox\"\n checked={registerForm.isMembershipAgreementAccepted?.value ?? false}\n onChange={(e) =>\n setRegisterFormIsMembershipAgreementAccepted(registerForm, (e.target as HTMLInputElement).checked)\n }\n />\n <span>{agreementConsentText}</span>\n </label>\n\n <button\n className=\"register-submit-btn\"\n type=\"submit\"\n disabled={registerForm.isSubmitting}\n >\n {registerForm.isSubmitting ? submittingButtonText : submitButtonText}\n </button>\n </form>\n\n <p className=\"register-login-link\">\n {hasAccountText}{\" \"}\n <a\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n Router.navigateToPage(\"LOGIN\");\n }}\n >\n {signInLinkText}\n </a>\n </p>\n </div>\n </section>\n );\n}\n\n"
|
|
17254
17466
|
},
|
|
17255
17467
|
{
|
|
17256
17468
|
"filename": "types.ts",
|
|
@@ -17307,7 +17519,7 @@
|
|
|
17307
17519
|
"id": "variant-selection",
|
|
17308
17520
|
"title": "Variant Selection",
|
|
17309
17521
|
"description": "Display variant options with type-specific rendering: color swatches (isColorVariantValue), image thumbnails (isImageVariantValue + getIkasVariantValueThumbnailImage), or text buttons (isTextVariantValue). Uses selectVariantValue with disableRoute option.",
|
|
17310
|
-
"code": "import {\n getDisplayedProductVariantTypes,\n selectVariantValue,\n isColorVariantValue,\n isImageVariantValue,\n isTextVariantValue,\n getIkasVariantValueThumbnailImage,\n getDefaultSrc,\n isNotEmpty,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\n\nexport default function 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,
|
|
17522
|
+
"code": "import {\n getDisplayedProductVariantTypes,\n selectVariantValue,\n isColorVariantValue,\n isImageVariantValue,\n isTextVariantValue,\n getIkasVariantValueThumbnailImage,\n getDefaultSrc,\n isNotEmpty,\n IkasProduct,\n} from \"@ikas/bp-storefront\";\n\nexport default function 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, 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);\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, 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, true)}\n >\n {vv.name}\n </button>\n );\n })}\n </div>\n </div>\n ))}\n </div>\n );\n}\n\n",
|
|
17311
17523
|
"relatedFunctions": [
|
|
17312
17524
|
"getDisplayedProductVariantTypes",
|
|
17313
17525
|
"selectVariantValue",
|