@ikas/code-components-mcp 0.104.0 → 0.106.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.
Files changed (790) hide show
  1. package/data/section-templates/account-info-section/_meta.json +4 -0
  2. package/data/section-templates/account-info-section/children/AccountAddresses/components/AddressCard/index.tsx +55 -0
  3. package/data/section-templates/account-info-section/children/AccountAddresses/components/AddressCard/styles.css +46 -0
  4. package/data/section-templates/account-info-section/children/AccountAddresses/components/AddressModal/index.tsx +301 -0
  5. package/data/section-templates/account-info-section/children/AccountAddresses/components/AddressModal/styles.css +22 -0
  6. package/data/section-templates/account-info-section/children/AccountAddresses/ikas-config-snippet.json +122 -0
  7. package/data/section-templates/account-info-section/children/AccountAddresses/index.tsx +127 -0
  8. package/data/section-templates/account-info-section/children/AccountAddresses/styles.css +41 -0
  9. package/data/section-templates/account-info-section/children/AccountAddresses/types.ts +14 -0
  10. package/data/section-templates/account-info-section/children/AccountFavorites/ikas-config-snippet.json +57 -0
  11. package/data/section-templates/account-info-section/children/AccountFavorites/index.tsx +82 -0
  12. package/data/section-templates/account-info-section/children/AccountFavorites/styles.css +52 -0
  13. package/data/section-templates/account-info-section/children/AccountFavorites/types.ts +6 -0
  14. package/data/section-templates/account-info-section/children/AccountInfoContent/ikas-config-snippet.json +66 -0
  15. package/data/section-templates/account-info-section/children/AccountInfoContent/index.tsx +134 -0
  16. package/data/section-templates/account-info-section/children/AccountInfoContent/styles.css +57 -0
  17. package/data/section-templates/account-info-section/children/AccountInfoContent/types.ts +7 -0
  18. package/data/section-templates/account-info-section/children/AccountOrderDetail/components/OrderHeader/index.tsx +78 -0
  19. package/data/section-templates/account-info-section/children/AccountOrderDetail/components/OrderHeader/styles.css +46 -0
  20. package/data/section-templates/account-info-section/children/AccountOrderDetail/components/OrderItemRow/index.tsx +20 -0
  21. package/data/section-templates/account-info-section/children/AccountOrderDetail/components/OrderItemRow/styles.css +16 -0
  22. package/data/section-templates/account-info-section/children/AccountOrderDetail/components/OrderLineItemDisplay/index.tsx +112 -0
  23. package/data/section-templates/account-info-section/children/AccountOrderDetail/components/OrderLineItemDisplay/styles.css +86 -0
  24. package/data/section-templates/account-info-section/children/AccountOrderDetail/components/OrderSidebar/index.tsx +195 -0
  25. package/data/section-templates/account-info-section/children/AccountOrderDetail/components/OrderSidebar/styles.css +93 -0
  26. package/data/section-templates/account-info-section/children/AccountOrderDetail/components/PackageGroup/index.tsx +156 -0
  27. package/data/section-templates/account-info-section/children/AccountOrderDetail/components/PackageGroup/styles.css +100 -0
  28. package/data/section-templates/account-info-section/children/AccountOrderDetail/components/ReturnItemRow/index.tsx +56 -0
  29. package/data/section-templates/account-info-section/children/AccountOrderDetail/components/ReturnItemRow/styles.css +57 -0
  30. package/data/section-templates/account-info-section/children/AccountOrderDetail/components/ReturnView/index.tsx +159 -0
  31. package/data/section-templates/account-info-section/children/AccountOrderDetail/components/ReturnView/styles.css +29 -0
  32. package/data/section-templates/account-info-section/children/AccountOrderDetail/ikas-config-snippet.json +283 -0
  33. package/data/section-templates/account-info-section/children/AccountOrderDetail/index.tsx +244 -0
  34. package/data/section-templates/account-info-section/children/AccountOrderDetail/styles.css +125 -0
  35. package/data/section-templates/account-info-section/children/AccountOrderDetail/types.ts +31 -0
  36. package/data/section-templates/account-info-section/children/AccountOrders/components/OrderCard/index.tsx +122 -0
  37. package/data/section-templates/account-info-section/children/AccountOrders/components/OrderCard/styles.css +114 -0
  38. package/data/section-templates/account-info-section/children/AccountOrders/ikas-config-snippet.json +85 -0
  39. package/data/section-templates/account-info-section/children/AccountOrders/index.tsx +105 -0
  40. package/data/section-templates/account-info-section/children/AccountOrders/styles.css +36 -0
  41. package/data/section-templates/account-info-section/children/AccountOrders/types.ts +10 -0
  42. package/data/section-templates/account-info-section/components/AccountSidebar/index.tsx +171 -0
  43. package/data/section-templates/account-info-section/components/AccountSidebar/styles.css +144 -0
  44. package/data/section-templates/account-info-section/global-types.ts +13 -0
  45. package/data/section-templates/account-info-section/hooks/useScrollLock.ts +20 -0
  46. package/data/section-templates/account-info-section/ikas-config-snippet.json +69 -0
  47. package/data/section-templates/account-info-section/index.tsx +91 -0
  48. package/data/section-templates/account-info-section/styles.css +35 -0
  49. package/data/section-templates/account-info-section/sub-components/Breadcrumb/index.tsx +57 -0
  50. package/data/section-templates/account-info-section/sub-components/Breadcrumb/styles.css +49 -0
  51. package/data/section-templates/account-info-section/sub-components/Button/index.tsx +52 -0
  52. package/data/section-templates/account-info-section/sub-components/Button/styles.css +114 -0
  53. package/data/section-templates/account-info-section/sub-components/Checkbox/index.tsx +42 -0
  54. package/data/section-templates/account-info-section/sub-components/Checkbox/styles.css +65 -0
  55. package/data/section-templates/account-info-section/sub-components/ConfirmModal/index.tsx +60 -0
  56. package/data/section-templates/account-info-section/sub-components/ConfirmModal/styles.css +20 -0
  57. package/data/section-templates/account-info-section/sub-components/FormItem/index.tsx +66 -0
  58. package/data/section-templates/account-info-section/sub-components/FormItem/styles.css +38 -0
  59. package/data/section-templates/account-info-section/sub-components/Input/index.tsx +69 -0
  60. package/data/section-templates/account-info-section/sub-components/Input/styles.css +162 -0
  61. package/data/section-templates/account-info-section/sub-components/Modal/index.tsx +118 -0
  62. package/data/section-templates/account-info-section/sub-components/Modal/styles.css +70 -0
  63. package/data/section-templates/account-info-section/sub-components/PageLoader/index.tsx +14 -0
  64. package/data/section-templates/account-info-section/sub-components/PageLoader/styles.css +12 -0
  65. package/data/section-templates/account-info-section/sub-components/ProductCard/index.tsx +276 -0
  66. package/data/section-templates/account-info-section/sub-components/ProductCard/styles.css +99 -0
  67. package/data/section-templates/account-info-section/sub-components/Select/index.tsx +86 -0
  68. package/data/section-templates/account-info-section/sub-components/Select/styles.css +110 -0
  69. package/data/section-templates/account-info-section/sub-components/SkeletonField/index.tsx +12 -0
  70. package/data/section-templates/account-info-section/sub-components/SkeletonField/styles.css +29 -0
  71. package/data/section-templates/account-info-section/sub-components/SpinnerIcon/index.tsx +10 -0
  72. package/data/section-templates/account-info-section/sub-components/SpinnerIcon/styles.css +8 -0
  73. package/data/section-templates/account-info-section/sub-components/Tag/index.tsx +21 -0
  74. package/data/section-templates/account-info-section/sub-components/Tag/styles.css +30 -0
  75. package/data/section-templates/account-info-section/sub-components/icons/index.tsx +981 -0
  76. package/data/section-templates/account-info-section/types.ts +8 -0
  77. package/data/section-templates/account-info-section/utils/cx.ts +4 -0
  78. package/data/section-templates/account-info-section/utils/fullName.ts +6 -0
  79. package/data/section-templates/account-info-section/utils/media.ts +36 -0
  80. package/data/section-templates/account-info-section/utils/orderStatus.ts +28 -0
  81. package/data/section-templates/account-info-section/utils/toast.ts +5 -0
  82. package/data/section-templates/add-to-cart/_meta.json +4 -0
  83. package/data/section-templates/add-to-cart/components/PayWithIkas/index.tsx +34 -0
  84. package/data/section-templates/add-to-cart/components/PayWithIkas/styles.css +4 -0
  85. package/data/section-templates/add-to-cart/hooks/usePayWithIkas.ts +114 -0
  86. package/data/section-templates/add-to-cart/ikas-config-snippet.json +139 -0
  87. package/data/section-templates/add-to-cart/index.tsx +146 -0
  88. package/data/section-templates/add-to-cart/styles.css +27 -0
  89. package/data/section-templates/add-to-cart/sub-components/Button/index.tsx +52 -0
  90. package/data/section-templates/add-to-cart/sub-components/Button/styles.css +114 -0
  91. package/data/section-templates/add-to-cart/sub-components/QuantitySelector/index.tsx +41 -0
  92. package/data/section-templates/add-to-cart/sub-components/QuantitySelector/styles.css +51 -0
  93. package/data/section-templates/add-to-cart/sub-components/icons/index.tsx +981 -0
  94. package/data/section-templates/add-to-cart/types.ts +17 -0
  95. package/data/section-templates/add-to-cart/utils/bundle.ts +70 -0
  96. package/data/section-templates/add-to-cart/utils/cx.ts +4 -0
  97. package/data/section-templates/add-to-cart/utils/optionSet.ts +17 -0
  98. package/data/section-templates/add-to-cart/utils/toast.ts +5 -0
  99. package/data/section-templates/blog-home-section/_meta.json +4 -0
  100. package/data/section-templates/blog-home-section/components/BlogCard/index.tsx +89 -0
  101. package/data/section-templates/blog-home-section/components/BlogCard/styles.css +121 -0
  102. package/data/section-templates/blog-home-section/global-types.ts +13 -0
  103. package/data/section-templates/blog-home-section/ikas-config-snippet.json +95 -0
  104. package/data/section-templates/blog-home-section/index.tsx +124 -0
  105. package/data/section-templates/blog-home-section/styles.css +118 -0
  106. package/data/section-templates/blog-home-section/sub-components/Pagination/index.tsx +107 -0
  107. package/data/section-templates/blog-home-section/sub-components/Pagination/styles.css +88 -0
  108. package/data/section-templates/blog-home-section/sub-components/icons/index.tsx +981 -0
  109. package/data/section-templates/blog-home-section/types.ts +14 -0
  110. package/data/section-templates/blog-home-section/utils/cx.ts +4 -0
  111. package/data/section-templates/blog-home-section/utils/fullName.ts +6 -0
  112. package/data/section-templates/blog-home-section/utils/media.ts +36 -0
  113. package/data/section-templates/blog-home-section/utils/pagination.ts +29 -0
  114. package/data/section-templates/blog-post-section/_meta.json +4 -0
  115. package/data/section-templates/blog-post-section/global-types.ts +13 -0
  116. package/data/section-templates/blog-post-section/ikas-config-snippet.json +75 -0
  117. package/data/section-templates/blog-post-section/index.tsx +128 -0
  118. package/data/section-templates/blog-post-section/styles.css +120 -0
  119. package/data/section-templates/blog-post-section/sub-components/Breadcrumb/index.tsx +57 -0
  120. package/data/section-templates/blog-post-section/sub-components/Breadcrumb/styles.css +49 -0
  121. package/data/section-templates/blog-post-section/sub-components/icons/index.tsx +981 -0
  122. package/data/section-templates/blog-post-section/types.ts +11 -0
  123. package/data/section-templates/blog-post-section/utils/cx.ts +4 -0
  124. package/data/section-templates/blog-post-section/utils/fullName.ts +6 -0
  125. package/data/section-templates/blog-post-section/utils/media.ts +36 -0
  126. package/data/section-templates/bundle-products/_meta.json +4 -0
  127. package/data/section-templates/bundle-products/components/BundleProductItem/index.tsx +169 -0
  128. package/data/section-templates/bundle-products/components/BundleProductItem/styles.css +141 -0
  129. package/data/section-templates/bundle-products/components/BundleSkeletonLoading/index.tsx +35 -0
  130. package/data/section-templates/bundle-products/components/BundleSkeletonLoading/styles.css +85 -0
  131. package/data/section-templates/bundle-products/components/FurnitureRow/index.tsx +51 -0
  132. package/data/section-templates/bundle-products/components/FurnitureRow/styles.css +30 -0
  133. package/data/section-templates/bundle-products/components/FurnitureView/index.tsx +54 -0
  134. package/data/section-templates/bundle-products/components/FurnitureView/styles.css +22 -0
  135. package/data/section-templates/bundle-products/global-types.ts +13 -0
  136. package/data/section-templates/bundle-products/hooks/useBundleProducts.ts +75 -0
  137. package/data/section-templates/bundle-products/ikas-config-snippet.json +130 -0
  138. package/data/section-templates/bundle-products/index.tsx +101 -0
  139. package/data/section-templates/bundle-products/styles.css +20 -0
  140. package/data/section-templates/bundle-products/sub-components/Badge/index.tsx +208 -0
  141. package/data/section-templates/bundle-products/sub-components/Badge/styles.css +129 -0
  142. package/data/section-templates/bundle-products/sub-components/BundleMedia/index.tsx +72 -0
  143. package/data/section-templates/bundle-products/sub-components/BundleQuantityBox/index.tsx +73 -0
  144. package/data/section-templates/bundle-products/sub-components/BundleQuantityBox/styles.css +43 -0
  145. package/data/section-templates/bundle-products/sub-components/VariantBadge/index.tsx +153 -0
  146. package/data/section-templates/bundle-products/sub-components/VariantBadge/styles.css +47 -0
  147. package/data/section-templates/bundle-products/sub-components/icons/index.tsx +981 -0
  148. package/data/section-templates/bundle-products/types.ts +17 -0
  149. package/data/section-templates/bundle-products/utils/bundle.ts +70 -0
  150. package/data/section-templates/bundle-products/utils/cx.ts +4 -0
  151. package/data/section-templates/bundle-products/utils/media.ts +36 -0
  152. package/data/section-templates/cart-section/_meta.json +4 -0
  153. package/data/section-templates/cart-section/components/CouponCode/index.tsx +108 -0
  154. package/data/section-templates/cart-section/components/CouponCode/styles.css +68 -0
  155. package/data/section-templates/cart-section/components/EmptyState/index.tsx +31 -0
  156. package/data/section-templates/cart-section/components/EmptyState/styles.css +18 -0
  157. package/data/section-templates/cart-section/components/OrderSummary/index.tsx +106 -0
  158. package/data/section-templates/cart-section/components/OrderSummary/styles.css +70 -0
  159. package/data/section-templates/cart-section/global-types.ts +13 -0
  160. package/data/section-templates/cart-section/ikas-config-snippet.json +130 -0
  161. package/data/section-templates/cart-section/index.tsx +107 -0
  162. package/data/section-templates/cart-section/styles.css +54 -0
  163. package/data/section-templates/cart-section/sub-components/Button/index.tsx +52 -0
  164. package/data/section-templates/cart-section/sub-components/Button/styles.css +114 -0
  165. package/data/section-templates/cart-section/sub-components/CartItem/components/BundleProductItem/index.tsx +59 -0
  166. package/data/section-templates/cart-section/sub-components/CartItem/components/BundleProductItem/styles.css +24 -0
  167. package/data/section-templates/cart-section/sub-components/CartItem/components/BundleProducts/index.tsx +55 -0
  168. package/data/section-templates/cart-section/sub-components/CartItem/components/BundleProducts/styles.css +30 -0
  169. package/data/section-templates/cart-section/sub-components/CartItem/components/ItemOptions/index.tsx +31 -0
  170. package/data/section-templates/cart-section/sub-components/CartItem/components/ItemOptions/styles.css +6 -0
  171. package/data/section-templates/cart-section/sub-components/CartItem/components/OptionValueDisplay/index.tsx +79 -0
  172. package/data/section-templates/cart-section/sub-components/CartItem/components/OptionValueDisplay/styles.css +28 -0
  173. package/data/section-templates/cart-section/sub-components/CartItem/index.tsx +216 -0
  174. package/data/section-templates/cart-section/sub-components/CartItem/styles.css +170 -0
  175. package/data/section-templates/cart-section/sub-components/Input/index.tsx +69 -0
  176. package/data/section-templates/cart-section/sub-components/Input/styles.css +162 -0
  177. package/data/section-templates/cart-section/sub-components/PageLoader/index.tsx +14 -0
  178. package/data/section-templates/cart-section/sub-components/PageLoader/styles.css +12 -0
  179. package/data/section-templates/cart-section/sub-components/QuantitySelector/index.tsx +41 -0
  180. package/data/section-templates/cart-section/sub-components/QuantitySelector/styles.css +51 -0
  181. package/data/section-templates/cart-section/sub-components/SpinnerIcon/index.tsx +10 -0
  182. package/data/section-templates/cart-section/sub-components/SpinnerIcon/styles.css +8 -0
  183. package/data/section-templates/cart-section/sub-components/icons/index.tsx +981 -0
  184. package/data/section-templates/cart-section/types.ts +15 -0
  185. package/data/section-templates/cart-section/utils/cx.ts +4 -0
  186. package/data/section-templates/cart-section/utils/media.ts +36 -0
  187. package/data/section-templates/category-images-section/_meta.json +4 -0
  188. package/data/section-templates/category-images-section/children/CategoryImageItem/components/Card/index.tsx +64 -0
  189. package/data/section-templates/category-images-section/children/CategoryImageItem/components/Card/styles.css +56 -0
  190. package/data/section-templates/category-images-section/children/CategoryImageItem/ikas-config-snippet.json +93 -0
  191. package/data/section-templates/category-images-section/children/CategoryImageItem/index.tsx +64 -0
  192. package/data/section-templates/category-images-section/children/CategoryImageItem/styles.css +10 -0
  193. package/data/section-templates/category-images-section/children/CategoryImageItem/types.ts +15 -0
  194. package/data/section-templates/category-images-section/global-types.ts +13 -0
  195. package/data/section-templates/category-images-section/ikas-config-snippet.json +69 -0
  196. package/data/section-templates/category-images-section/index.tsx +62 -0
  197. package/data/section-templates/category-images-section/styles.css +38 -0
  198. package/data/section-templates/category-images-section/types.ts +10 -0
  199. package/data/section-templates/category-images-section/utils/cx.ts +4 -0
  200. package/data/section-templates/category-images-section/utils/media.ts +36 -0
  201. package/data/section-templates/category-list-section/_meta.json +4 -0
  202. package/data/section-templates/category-list-section/children/CardProductName/ikas-config-snippet.json +21 -0
  203. package/data/section-templates/category-list-section/children/CardProductName/index.tsx +25 -0
  204. package/data/section-templates/category-list-section/children/CardProductName/styles.css +22 -0
  205. package/data/section-templates/category-list-section/children/CardProductName/types.ts +6 -0
  206. package/data/section-templates/category-list-section/children/CardProductPrice/ikas-config-snippet.json +15 -0
  207. package/data/section-templates/category-list-section/children/CardProductPrice/index.tsx +30 -0
  208. package/data/section-templates/category-list-section/children/CardProductPrice/styles.css +13 -0
  209. package/data/section-templates/category-list-section/children/CardProductPrice/types.ts +5 -0
  210. package/data/section-templates/category-list-section/children/CardProductVariants/ikas-config-snippet.json +15 -0
  211. package/data/section-templates/category-list-section/children/CardProductVariants/index.tsx +10 -0
  212. package/data/section-templates/category-list-section/children/CardProductVariants/styles.css +1 -0
  213. package/data/section-templates/category-list-section/children/CardProductVariants/types.ts +5 -0
  214. package/data/section-templates/category-list-section/components/CategoryListControls/index.tsx +129 -0
  215. package/data/section-templates/category-list-section/components/CategoryListControls/styles.css +99 -0
  216. package/data/section-templates/category-list-section/components/FilterBoxValues/index.tsx +42 -0
  217. package/data/section-templates/category-list-section/components/FilterBoxValues/styles.css +27 -0
  218. package/data/section-templates/category-list-section/components/FilterCategoryList/index.tsx +43 -0
  219. package/data/section-templates/category-list-section/components/FilterCategoryList/styles.css +20 -0
  220. package/data/section-templates/category-list-section/components/FilterGroupValues/index.tsx +114 -0
  221. package/data/section-templates/category-list-section/components/FilterGroupValues/styles.css +1 -0
  222. package/data/section-templates/category-list-section/components/FilterListValues/index.tsx +54 -0
  223. package/data/section-templates/category-list-section/components/FilterListValues/styles.css +22 -0
  224. package/data/section-templates/category-list-section/components/FilterRangeListValues/index.tsx +50 -0
  225. package/data/section-templates/category-list-section/components/FilterRangeListValues/styles.css +25 -0
  226. package/data/section-templates/category-list-section/components/FilterRangeValues/index.tsx +189 -0
  227. package/data/section-templates/category-list-section/components/FilterRangeValues/styles.css +89 -0
  228. package/data/section-templates/category-list-section/components/FilterSidebar/index.tsx +92 -0
  229. package/data/section-templates/category-list-section/components/FilterSidebar/styles.css +27 -0
  230. package/data/section-templates/category-list-section/components/FilterSwatchValues/index.tsx +63 -0
  231. package/data/section-templates/category-list-section/components/FilterSwatchValues/styles.css +48 -0
  232. package/data/section-templates/category-list-section/components/MobileFilterModal/index.tsx +146 -0
  233. package/data/section-templates/category-list-section/components/MobileFilterModal/styles.css +133 -0
  234. package/data/section-templates/category-list-section/global-types.ts +13 -0
  235. package/data/section-templates/category-list-section/hooks/useColumnPreference.ts +26 -0
  236. package/data/section-templates/category-list-section/hooks/useInfiniteScroll.ts +49 -0
  237. package/data/section-templates/category-list-section/hooks/usePageTracking.ts +56 -0
  238. package/data/section-templates/category-list-section/hooks/useScrollLock.ts +20 -0
  239. package/data/section-templates/category-list-section/ikas-config-snippet.json +240 -0
  240. package/data/section-templates/category-list-section/index.tsx +333 -0
  241. package/data/section-templates/category-list-section/styles.css +110 -0
  242. package/data/section-templates/category-list-section/sub-components/Badge/index.tsx +208 -0
  243. package/data/section-templates/category-list-section/sub-components/Badge/styles.css +129 -0
  244. package/data/section-templates/category-list-section/sub-components/Breadcrumb/index.tsx +57 -0
  245. package/data/section-templates/category-list-section/sub-components/Breadcrumb/styles.css +49 -0
  246. package/data/section-templates/category-list-section/sub-components/Button/index.tsx +52 -0
  247. package/data/section-templates/category-list-section/sub-components/Button/styles.css +114 -0
  248. package/data/section-templates/category-list-section/sub-components/Checkbox/index.tsx +42 -0
  249. package/data/section-templates/category-list-section/sub-components/Checkbox/styles.css +65 -0
  250. package/data/section-templates/category-list-section/sub-components/CollapsibleGroup/index.tsx +52 -0
  251. package/data/section-templates/category-list-section/sub-components/CollapsibleGroup/styles.css +51 -0
  252. package/data/section-templates/category-list-section/sub-components/Input/index.tsx +69 -0
  253. package/data/section-templates/category-list-section/sub-components/Input/styles.css +162 -0
  254. package/data/section-templates/category-list-section/sub-components/Pagination/index.tsx +107 -0
  255. package/data/section-templates/category-list-section/sub-components/Pagination/styles.css +88 -0
  256. package/data/section-templates/category-list-section/sub-components/ProductCard/index.tsx +276 -0
  257. package/data/section-templates/category-list-section/sub-components/ProductCard/styles.css +99 -0
  258. package/data/section-templates/category-list-section/sub-components/Select/index.tsx +86 -0
  259. package/data/section-templates/category-list-section/sub-components/Select/styles.css +110 -0
  260. package/data/section-templates/category-list-section/sub-components/SpinnerIcon/index.tsx +10 -0
  261. package/data/section-templates/category-list-section/sub-components/SpinnerIcon/styles.css +8 -0
  262. package/data/section-templates/category-list-section/sub-components/Tag/index.tsx +21 -0
  263. package/data/section-templates/category-list-section/sub-components/Tag/styles.css +30 -0
  264. package/data/section-templates/category-list-section/sub-components/VariantBadge/index.tsx +153 -0
  265. package/data/section-templates/category-list-section/sub-components/VariantBadge/styles.css +47 -0
  266. package/data/section-templates/category-list-section/sub-components/icons/index.tsx +981 -0
  267. package/data/section-templates/category-list-section/types.ts +29 -0
  268. package/data/section-templates/category-list-section/utils/cx.ts +4 -0
  269. package/data/section-templates/category-list-section/utils/media.ts +36 -0
  270. package/data/section-templates/category-list-section/utils/pagination.ts +29 -0
  271. package/data/section-templates/category-list-section/utils/toast.ts +5 -0
  272. package/data/section-templates/component-renderer/_meta.json +4 -0
  273. package/data/section-templates/component-renderer/additional/Features/index.tsx +25 -0
  274. package/data/section-templates/component-renderer/additional/Features/styles.css +39 -0
  275. package/data/section-templates/component-renderer/additional/Features/types.ts +4 -0
  276. package/data/section-templates/component-renderer/additional/ProductDetail/index.tsx +92 -0
  277. package/data/section-templates/component-renderer/additional/ProductDetail/styles.css +58 -0
  278. package/data/section-templates/component-renderer/additional/ProductDetail/types.ts +11 -0
  279. package/data/section-templates/component-renderer/global-types.ts +13 -0
  280. package/data/section-templates/component-renderer/hooks/useToast.ts +27 -0
  281. package/data/section-templates/component-renderer/ikas-config-snippet.json +44 -0
  282. package/data/section-templates/component-renderer/index.tsx +53 -0
  283. package/data/section-templates/component-renderer/styles.css +6 -0
  284. package/data/section-templates/component-renderer/sub-components/Breadcrumb/index.tsx +57 -0
  285. package/data/section-templates/component-renderer/sub-components/Breadcrumb/styles.css +49 -0
  286. package/data/section-templates/component-renderer/sub-components/Toast/index.tsx +257 -0
  287. package/data/section-templates/component-renderer/sub-components/Toast/styles.css +3 -0
  288. package/data/section-templates/component-renderer/sub-components/icons/index.tsx +981 -0
  289. package/data/section-templates/component-renderer/types.ts +5 -0
  290. package/data/section-templates/component-renderer/utils/cx.ts +4 -0
  291. package/data/section-templates/email-verification-section/_meta.json +4 -0
  292. package/data/section-templates/email-verification-section/ikas-config-snippet.json +143 -0
  293. package/data/section-templates/email-verification-section/index.tsx +168 -0
  294. package/data/section-templates/email-verification-section/styles.css +118 -0
  295. package/data/section-templates/email-verification-section/sub-components/Button/index.tsx +52 -0
  296. package/data/section-templates/email-verification-section/sub-components/Button/styles.css +114 -0
  297. package/data/section-templates/email-verification-section/sub-components/FormItem/index.tsx +66 -0
  298. package/data/section-templates/email-verification-section/sub-components/FormItem/styles.css +38 -0
  299. package/data/section-templates/email-verification-section/sub-components/Input/index.tsx +69 -0
  300. package/data/section-templates/email-verification-section/sub-components/Input/styles.css +162 -0
  301. package/data/section-templates/email-verification-section/sub-components/SpinnerIcon/index.tsx +10 -0
  302. package/data/section-templates/email-verification-section/sub-components/SpinnerIcon/styles.css +8 -0
  303. package/data/section-templates/email-verification-section/sub-components/icons/index.tsx +981 -0
  304. package/data/section-templates/email-verification-section/types.ts +16 -0
  305. package/data/section-templates/email-verification-section/utils/cx.ts +4 -0
  306. package/data/section-templates/favorites/_meta.json +4 -0
  307. package/data/section-templates/favorites/ikas-config-snippet.json +72 -0
  308. package/data/section-templates/favorites/index.tsx +66 -0
  309. package/data/section-templates/favorites/styles.css +45 -0
  310. package/data/section-templates/favorites/sub-components/icons/index.tsx +981 -0
  311. package/data/section-templates/favorites/types.ts +10 -0
  312. package/data/section-templates/favorites/utils/cx.ts +4 -0
  313. package/data/section-templates/features-section/_meta.json +4 -0
  314. package/data/section-templates/features-section/children/FeatureItem/ikas-config-snippet.json +21 -0
  315. package/data/section-templates/features-section/children/FeatureItem/index.tsx +27 -0
  316. package/data/section-templates/features-section/children/FeatureItem/styles.css +19 -0
  317. package/data/section-templates/features-section/children/FeatureItem/types.ts +6 -0
  318. package/data/section-templates/features-section/ikas-config-snippet.json +25 -0
  319. package/data/section-templates/features-section/index.tsx +25 -0
  320. package/data/section-templates/features-section/styles.css +39 -0
  321. package/data/section-templates/features-section/types.ts +4 -0
  322. package/data/section-templates/footer-section/_meta.json +4 -0
  323. package/data/section-templates/footer-section/children/SocialMediaIcon/ikas-config-snippet.json +21 -0
  324. package/data/section-templates/footer-section/children/SocialMediaIcon/index.tsx +26 -0
  325. package/data/section-templates/footer-section/children/SocialMediaIcon/styles.css +17 -0
  326. package/data/section-templates/footer-section/children/SocialMediaIcon/types.ts +6 -0
  327. package/data/section-templates/footer-section/ikas-config-snippet.json +108 -0
  328. package/data/section-templates/footer-section/index.tsx +154 -0
  329. package/data/section-templates/footer-section/styles.css +175 -0
  330. package/data/section-templates/footer-section/sub-components/icons/index.tsx +981 -0
  331. package/data/section-templates/footer-section/types.ts +14 -0
  332. package/data/section-templates/footer-section/utils/cx.ts +4 -0
  333. package/data/section-templates/forgot-password-section/_meta.json +4 -0
  334. package/data/section-templates/forgot-password-section/components/ForgotPasswordForm/index.tsx +129 -0
  335. package/data/section-templates/forgot-password-section/components/ForgotPasswordForm/styles.css +0 -0
  336. package/data/section-templates/forgot-password-section/hooks/useRedirectIfLoggedIn.ts +35 -0
  337. package/data/section-templates/forgot-password-section/ikas-config-snippet.json +117 -0
  338. package/data/section-templates/forgot-password-section/index.tsx +30 -0
  339. package/data/section-templates/forgot-password-section/styles.css +85 -0
  340. package/data/section-templates/forgot-password-section/sub-components/Button/index.tsx +52 -0
  341. package/data/section-templates/forgot-password-section/sub-components/Button/styles.css +114 -0
  342. package/data/section-templates/forgot-password-section/sub-components/FormItem/index.tsx +66 -0
  343. package/data/section-templates/forgot-password-section/sub-components/FormItem/styles.css +38 -0
  344. package/data/section-templates/forgot-password-section/sub-components/Input/index.tsx +69 -0
  345. package/data/section-templates/forgot-password-section/sub-components/Input/styles.css +162 -0
  346. package/data/section-templates/forgot-password-section/sub-components/PageLoader/index.tsx +14 -0
  347. package/data/section-templates/forgot-password-section/sub-components/PageLoader/styles.css +12 -0
  348. package/data/section-templates/forgot-password-section/sub-components/SpinnerIcon/index.tsx +10 -0
  349. package/data/section-templates/forgot-password-section/sub-components/SpinnerIcon/styles.css +8 -0
  350. package/data/section-templates/forgot-password-section/sub-components/icons/index.tsx +981 -0
  351. package/data/section-templates/forgot-password-section/types.ts +12 -0
  352. package/data/section-templates/forgot-password-section/utils/cx.ts +4 -0
  353. package/data/section-templates/header-section/_meta.json +4 -0
  354. package/data/section-templates/header-section/children/Announcements/ikas-config-snippet.json +62 -0
  355. package/data/section-templates/header-section/children/Announcements/index.tsx +91 -0
  356. package/data/section-templates/header-section/children/Announcements/styles.css +45 -0
  357. package/data/section-templates/header-section/children/Announcements/types.ts +7 -0
  358. package/data/section-templates/header-section/children/CookieBar/ikas-config-snippet.json +79 -0
  359. package/data/section-templates/header-section/children/CookieBar/index.tsx +78 -0
  360. package/data/section-templates/header-section/children/CookieBar/styles.css +111 -0
  361. package/data/section-templates/header-section/children/CookieBar/types.ts +9 -0
  362. package/data/section-templates/header-section/children/Navbar/components/CartSidebar/index.tsx +203 -0
  363. package/data/section-templates/header-section/children/Navbar/components/CartSidebar/styles.css +175 -0
  364. package/data/section-templates/header-section/children/Navbar/components/MobileMenu/index.tsx +198 -0
  365. package/data/section-templates/header-section/children/Navbar/components/MobileMenu/styles.css +122 -0
  366. package/data/section-templates/header-section/children/Navbar/components/NavItem/index.tsx +65 -0
  367. package/data/section-templates/header-section/children/Navbar/components/SearchModal/index.tsx +267 -0
  368. package/data/section-templates/header-section/children/Navbar/components/SearchModal/styles.css +182 -0
  369. package/data/section-templates/header-section/children/Navbar/ikas-config-snippet.json +315 -0
  370. package/data/section-templates/header-section/children/Navbar/index.tsx +250 -0
  371. package/data/section-templates/header-section/children/Navbar/styles.css +243 -0
  372. package/data/section-templates/header-section/children/Navbar/types.ts +36 -0
  373. package/data/section-templates/header-section/global-types.ts +13 -0
  374. package/data/section-templates/header-section/hooks/useScrollLock.ts +20 -0
  375. package/data/section-templates/header-section/hooks/useToast.ts +27 -0
  376. package/data/section-templates/header-section/ikas-config-snippet.json +44 -0
  377. package/data/section-templates/header-section/index.tsx +53 -0
  378. package/data/section-templates/header-section/styles.css +6 -0
  379. package/data/section-templates/header-section/sub-components/Button/index.tsx +52 -0
  380. package/data/section-templates/header-section/sub-components/Button/styles.css +114 -0
  381. package/data/section-templates/header-section/sub-components/CartItem/components/BundleProductItem/index.tsx +59 -0
  382. package/data/section-templates/header-section/sub-components/CartItem/components/BundleProductItem/styles.css +24 -0
  383. package/data/section-templates/header-section/sub-components/CartItem/components/BundleProducts/index.tsx +55 -0
  384. package/data/section-templates/header-section/sub-components/CartItem/components/BundleProducts/styles.css +30 -0
  385. package/data/section-templates/header-section/sub-components/CartItem/components/ItemOptions/index.tsx +31 -0
  386. package/data/section-templates/header-section/sub-components/CartItem/components/ItemOptions/styles.css +6 -0
  387. package/data/section-templates/header-section/sub-components/CartItem/components/OptionValueDisplay/index.tsx +79 -0
  388. package/data/section-templates/header-section/sub-components/CartItem/components/OptionValueDisplay/styles.css +28 -0
  389. package/data/section-templates/header-section/sub-components/CartItem/index.tsx +216 -0
  390. package/data/section-templates/header-section/sub-components/CartItem/styles.css +170 -0
  391. package/data/section-templates/header-section/sub-components/Input/index.tsx +69 -0
  392. package/data/section-templates/header-section/sub-components/Input/styles.css +162 -0
  393. package/data/section-templates/header-section/sub-components/ProductCard/index.tsx +276 -0
  394. package/data/section-templates/header-section/sub-components/ProductCard/styles.css +99 -0
  395. package/data/section-templates/header-section/sub-components/QuantitySelector/index.tsx +41 -0
  396. package/data/section-templates/header-section/sub-components/QuantitySelector/styles.css +51 -0
  397. package/data/section-templates/header-section/sub-components/SpinnerIcon/index.tsx +10 -0
  398. package/data/section-templates/header-section/sub-components/SpinnerIcon/styles.css +8 -0
  399. package/data/section-templates/header-section/sub-components/Tag/index.tsx +21 -0
  400. package/data/section-templates/header-section/sub-components/Tag/styles.css +30 -0
  401. package/data/section-templates/header-section/sub-components/Toast/index.tsx +257 -0
  402. package/data/section-templates/header-section/sub-components/Toast/styles.css +3 -0
  403. package/data/section-templates/header-section/sub-components/icons/index.tsx +981 -0
  404. package/data/section-templates/header-section/types.ts +5 -0
  405. package/data/section-templates/header-section/utils/cx.ts +4 -0
  406. package/data/section-templates/header-section/utils/media.ts +36 -0
  407. package/data/section-templates/header-section/utils/toast.ts +5 -0
  408. package/data/section-templates/hero-slider-section/_meta.json +4 -0
  409. package/data/section-templates/hero-slider-section/children/HeroSliderItem/ikas-config-snippet.json +239 -0
  410. package/data/section-templates/hero-slider-section/children/HeroSliderItem/index.tsx +231 -0
  411. package/data/section-templates/hero-slider-section/children/HeroSliderItem/styles.css +152 -0
  412. package/data/section-templates/hero-slider-section/children/HeroSliderItem/types.ts +33 -0
  413. package/data/section-templates/hero-slider-section/global-types.ts +13 -0
  414. package/data/section-templates/hero-slider-section/ikas-config-snippet.json +67 -0
  415. package/data/section-templates/hero-slider-section/index.tsx +110 -0
  416. package/data/section-templates/hero-slider-section/styles.css +129 -0
  417. package/data/section-templates/hero-slider-section/sub-components/SliderArrow/index.tsx +26 -0
  418. package/data/section-templates/hero-slider-section/sub-components/SliderArrow/styles.css +24 -0
  419. package/data/section-templates/hero-slider-section/sub-components/icons/index.tsx +981 -0
  420. package/data/section-templates/hero-slider-section/types.ts +8 -0
  421. package/data/section-templates/hero-slider-section/utils/cx.ts +4 -0
  422. package/data/section-templates/hero-slider-section/utils/media.ts +36 -0
  423. package/data/section-templates/image-handling/_meta.json +4 -0
  424. package/data/section-templates/image-handling/components/ProductGallery/index.tsx +316 -0
  425. package/data/section-templates/image-handling/components/ProductGallery/styles.css +213 -0
  426. package/data/section-templates/image-handling/global-types.ts +13 -0
  427. package/data/section-templates/image-handling/ikas-config-snippet.json +80 -0
  428. package/data/section-templates/image-handling/index.tsx +92 -0
  429. package/data/section-templates/image-handling/styles.css +58 -0
  430. package/data/section-templates/image-handling/sub-components/Breadcrumb/index.tsx +57 -0
  431. package/data/section-templates/image-handling/sub-components/Breadcrumb/styles.css +49 -0
  432. package/data/section-templates/image-handling/sub-components/SliderArrow/index.tsx +26 -0
  433. package/data/section-templates/image-handling/sub-components/SliderArrow/styles.css +24 -0
  434. package/data/section-templates/image-handling/sub-components/icons/index.tsx +981 -0
  435. package/data/section-templates/image-handling/types.ts +11 -0
  436. package/data/section-templates/image-handling/utils/cx.ts +4 -0
  437. package/data/section-templates/image-handling/utils/media.ts +36 -0
  438. package/data/section-templates/login-section/_meta.json +4 -0
  439. package/data/section-templates/login-section/components/LoginForm/index.tsx +181 -0
  440. package/data/section-templates/login-section/components/LoginForm/styles.css +0 -0
  441. package/data/section-templates/login-section/hooks/useRedirectIfLoggedIn.ts +35 -0
  442. package/data/section-templates/login-section/ikas-config-snippet.json +169 -0
  443. package/data/section-templates/login-section/index.tsx +37 -0
  444. package/data/section-templates/login-section/styles.css +129 -0
  445. package/data/section-templates/login-section/sub-components/Button/index.tsx +52 -0
  446. package/data/section-templates/login-section/sub-components/Button/styles.css +114 -0
  447. package/data/section-templates/login-section/sub-components/FormItem/index.tsx +66 -0
  448. package/data/section-templates/login-section/sub-components/FormItem/styles.css +38 -0
  449. package/data/section-templates/login-section/sub-components/Input/index.tsx +69 -0
  450. package/data/section-templates/login-section/sub-components/Input/styles.css +162 -0
  451. package/data/section-templates/login-section/sub-components/PageLoader/index.tsx +14 -0
  452. package/data/section-templates/login-section/sub-components/PageLoader/styles.css +12 -0
  453. package/data/section-templates/login-section/sub-components/SocialLoginButton/index.tsx +24 -0
  454. package/data/section-templates/login-section/sub-components/SocialLoginButton/styles.css +19 -0
  455. package/data/section-templates/login-section/sub-components/SpinnerIcon/index.tsx +10 -0
  456. package/data/section-templates/login-section/sub-components/SpinnerIcon/styles.css +8 -0
  457. package/data/section-templates/login-section/sub-components/icons/index.tsx +981 -0
  458. package/data/section-templates/login-section/types.ts +17 -0
  459. package/data/section-templates/login-section/utils/cx.ts +4 -0
  460. package/data/section-templates/navigation/_meta.json +4 -0
  461. package/data/section-templates/navigation/components/CartSidebar/index.tsx +203 -0
  462. package/data/section-templates/navigation/components/CartSidebar/styles.css +175 -0
  463. package/data/section-templates/navigation/components/MobileMenu/index.tsx +198 -0
  464. package/data/section-templates/navigation/components/MobileMenu/styles.css +122 -0
  465. package/data/section-templates/navigation/components/NavItem/index.tsx +65 -0
  466. package/data/section-templates/navigation/components/SearchModal/index.tsx +267 -0
  467. package/data/section-templates/navigation/components/SearchModal/styles.css +182 -0
  468. package/data/section-templates/navigation/global-types.ts +13 -0
  469. package/data/section-templates/navigation/hooks/useScrollLock.ts +20 -0
  470. package/data/section-templates/navigation/ikas-config-snippet.json +315 -0
  471. package/data/section-templates/navigation/index.tsx +250 -0
  472. package/data/section-templates/navigation/styles.css +243 -0
  473. package/data/section-templates/navigation/sub-components/Button/index.tsx +52 -0
  474. package/data/section-templates/navigation/sub-components/Button/styles.css +114 -0
  475. package/data/section-templates/navigation/sub-components/CartItem/components/BundleProductItem/index.tsx +59 -0
  476. package/data/section-templates/navigation/sub-components/CartItem/components/BundleProductItem/styles.css +24 -0
  477. package/data/section-templates/navigation/sub-components/CartItem/components/BundleProducts/index.tsx +55 -0
  478. package/data/section-templates/navigation/sub-components/CartItem/components/BundleProducts/styles.css +30 -0
  479. package/data/section-templates/navigation/sub-components/CartItem/components/ItemOptions/index.tsx +31 -0
  480. package/data/section-templates/navigation/sub-components/CartItem/components/ItemOptions/styles.css +6 -0
  481. package/data/section-templates/navigation/sub-components/CartItem/components/OptionValueDisplay/index.tsx +79 -0
  482. package/data/section-templates/navigation/sub-components/CartItem/components/OptionValueDisplay/styles.css +28 -0
  483. package/data/section-templates/navigation/sub-components/CartItem/index.tsx +216 -0
  484. package/data/section-templates/navigation/sub-components/CartItem/styles.css +170 -0
  485. package/data/section-templates/navigation/sub-components/Input/index.tsx +69 -0
  486. package/data/section-templates/navigation/sub-components/Input/styles.css +162 -0
  487. package/data/section-templates/navigation/sub-components/ProductCard/index.tsx +276 -0
  488. package/data/section-templates/navigation/sub-components/ProductCard/styles.css +99 -0
  489. package/data/section-templates/navigation/sub-components/QuantitySelector/index.tsx +41 -0
  490. package/data/section-templates/navigation/sub-components/QuantitySelector/styles.css +51 -0
  491. package/data/section-templates/navigation/sub-components/SpinnerIcon/index.tsx +10 -0
  492. package/data/section-templates/navigation/sub-components/SpinnerIcon/styles.css +8 -0
  493. package/data/section-templates/navigation/sub-components/Tag/index.tsx +21 -0
  494. package/data/section-templates/navigation/sub-components/Tag/styles.css +30 -0
  495. package/data/section-templates/navigation/sub-components/icons/index.tsx +981 -0
  496. package/data/section-templates/navigation/types.ts +36 -0
  497. package/data/section-templates/navigation/utils/cx.ts +4 -0
  498. package/data/section-templates/navigation/utils/media.ts +36 -0
  499. package/data/section-templates/navigation/utils/toast.ts +5 -0
  500. package/data/section-templates/not-found-section/_meta.json +4 -0
  501. package/data/section-templates/not-found-section/ikas-config-snippet.json +44 -0
  502. package/data/section-templates/not-found-section/index.tsx +39 -0
  503. package/data/section-templates/not-found-section/styles.css +46 -0
  504. package/data/section-templates/not-found-section/sub-components/Button/index.tsx +52 -0
  505. package/data/section-templates/not-found-section/sub-components/Button/styles.css +114 -0
  506. package/data/section-templates/not-found-section/sub-components/icons/index.tsx +981 -0
  507. package/data/section-templates/not-found-section/types.ts +7 -0
  508. package/data/section-templates/not-found-section/utils/cx.ts +4 -0
  509. package/data/section-templates/product-detail-section/_meta.json +4 -0
  510. package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/components/PayWithIkas/index.tsx +34 -0
  511. package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/components/PayWithIkas/styles.css +4 -0
  512. package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/ikas-config-snippet.json +139 -0
  513. package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/index.tsx +146 -0
  514. package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/styles.css +27 -0
  515. package/data/section-templates/product-detail-section/children/ProductDetailAddToCart/types.ts +17 -0
  516. package/data/section-templates/product-detail-section/children/ProductDetailBundleFurniture/components/BundleFurnitureRow/index.tsx +164 -0
  517. package/data/section-templates/product-detail-section/children/ProductDetailBundleFurniture/components/BundleFurnitureSection/index.tsx +134 -0
  518. package/data/section-templates/product-detail-section/children/ProductDetailBundleFurniture/components/BundleFurnitureSection/styles.css +188 -0
  519. package/data/section-templates/product-detail-section/children/ProductDetailBundleFurniture/ikas-config-snippet.json +156 -0
  520. package/data/section-templates/product-detail-section/children/ProductDetailBundleFurniture/index.tsx +61 -0
  521. package/data/section-templates/product-detail-section/children/ProductDetailBundleFurniture/styles.css +12 -0
  522. package/data/section-templates/product-detail-section/children/ProductDetailBundleFurniture/types.ts +20 -0
  523. package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/components/BundleProductItem/index.tsx +169 -0
  524. package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/components/BundleProductItem/styles.css +141 -0
  525. package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/components/BundleSkeletonLoading/index.tsx +35 -0
  526. package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/components/BundleSkeletonLoading/styles.css +85 -0
  527. package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/components/FurnitureRow/index.tsx +51 -0
  528. package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/components/FurnitureRow/styles.css +30 -0
  529. package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/components/FurnitureView/index.tsx +54 -0
  530. package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/components/FurnitureView/styles.css +22 -0
  531. package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/ikas-config-snippet.json +130 -0
  532. package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/index.tsx +101 -0
  533. package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/styles.css +20 -0
  534. package/data/section-templates/product-detail-section/children/ProductDetailBundleProduct/types.ts +17 -0
  535. package/data/section-templates/product-detail-section/children/ProductDetailDescription/ikas-config-snippet.json +100 -0
  536. package/data/section-templates/product-detail-section/children/ProductDetailDescription/index.tsx +56 -0
  537. package/data/section-templates/product-detail-section/children/ProductDetailDescription/styles.css +21 -0
  538. package/data/section-templates/product-detail-section/children/ProductDetailDescription/types.ts +12 -0
  539. package/data/section-templates/product-detail-section/children/ProductDetailFeatures/ikas-config-snippet.json +69 -0
  540. package/data/section-templates/product-detail-section/children/ProductDetailFeatures/index.tsx +40 -0
  541. package/data/section-templates/product-detail-section/children/ProductDetailFeatures/styles.css +17 -0
  542. package/data/section-templates/product-detail-section/children/ProductDetailFeatures/types.ts +9 -0
  543. package/data/section-templates/product-detail-section/children/ProductDetailNameFavorite/ikas-config-snippet.json +72 -0
  544. package/data/section-templates/product-detail-section/children/ProductDetailNameFavorite/index.tsx +66 -0
  545. package/data/section-templates/product-detail-section/children/ProductDetailNameFavorite/styles.css +45 -0
  546. package/data/section-templates/product-detail-section/children/ProductDetailNameFavorite/types.ts +10 -0
  547. package/data/section-templates/product-detail-section/children/ProductDetailOffer/components/OfferCard/index.tsx +209 -0
  548. package/data/section-templates/product-detail-section/children/ProductDetailOffer/components/OfferCard/styles.css +146 -0
  549. package/data/section-templates/product-detail-section/children/ProductDetailOffer/components/OfferSummary/index.tsx +175 -0
  550. package/data/section-templates/product-detail-section/children/ProductDetailOffer/ikas-config-snippet.json +183 -0
  551. package/data/section-templates/product-detail-section/children/ProductDetailOffer/index.tsx +199 -0
  552. package/data/section-templates/product-detail-section/children/ProductDetailOffer/styles.css +211 -0
  553. package/data/section-templates/product-detail-section/children/ProductDetailOffer/types.ts +23 -0
  554. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionCheckbox/index.tsx +52 -0
  555. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionCheckbox/styles.css +19 -0
  556. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionChoice/components/ChoiceBox/index.tsx +60 -0
  557. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionChoice/components/ChoiceBox/styles.css +38 -0
  558. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionChoice/components/ChoiceSelect/index.tsx +102 -0
  559. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionChoice/components/ChoiceSelect/styles.css +34 -0
  560. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionChoice/components/ChoiceSwatch/index.tsx +121 -0
  561. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionChoice/components/ChoiceSwatch/styles.css +87 -0
  562. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionChoice/index.tsx +57 -0
  563. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionColorPicker/index.tsx +54 -0
  564. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionColorPicker/styles.css +0 -0
  565. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionDatePicker/index.tsx +124 -0
  566. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionDatePicker/styles.css +1 -0
  567. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionFile/index.tsx +217 -0
  568. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionFile/styles.css +87 -0
  569. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionRenderer/index.tsx +133 -0
  570. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionText/index.tsx +60 -0
  571. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionText/styles.css +1 -0
  572. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionTextarea/index.tsx +74 -0
  573. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/components/OptionTextarea/styles.css +10 -0
  574. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/ikas-config-snippet.json +159 -0
  575. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/index.tsx +99 -0
  576. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/styles.css +30 -0
  577. package/data/section-templates/product-detail-section/children/ProductDetailOptionSet/types.ts +20 -0
  578. package/data/section-templates/product-detail-section/children/ProductDetailPrices/ikas-config-snippet.json +66 -0
  579. package/data/section-templates/product-detail-section/children/ProductDetailPrices/index.tsx +62 -0
  580. package/data/section-templates/product-detail-section/children/ProductDetailPrices/styles.css +32 -0
  581. package/data/section-templates/product-detail-section/children/ProductDetailPrices/types.ts +9 -0
  582. package/data/section-templates/product-detail-section/children/ProductDetailProductGroup/ikas-config-snippet.json +66 -0
  583. package/data/section-templates/product-detail-section/children/ProductDetailProductGroup/index.tsx +74 -0
  584. package/data/section-templates/product-detail-section/children/ProductDetailProductGroup/styles.css +33 -0
  585. package/data/section-templates/product-detail-section/children/ProductDetailProductGroup/types.ts +9 -0
  586. package/data/section-templates/product-detail-section/children/ProductDetailSku/ikas-config-snippet.json +79 -0
  587. package/data/section-templates/product-detail-section/children/ProductDetailSku/index.tsx +38 -0
  588. package/data/section-templates/product-detail-section/children/ProductDetailSku/styles.css +16 -0
  589. package/data/section-templates/product-detail-section/children/ProductDetailSku/types.ts +10 -0
  590. package/data/section-templates/product-detail-section/children/ProductDetailVariant/ikas-config-snippet.json +73 -0
  591. package/data/section-templates/product-detail-section/children/ProductDetailVariant/index.tsx +38 -0
  592. package/data/section-templates/product-detail-section/children/ProductDetailVariant/styles.css +14 -0
  593. package/data/section-templates/product-detail-section/children/ProductDetailVariant/types.ts +11 -0
  594. package/data/section-templates/product-detail-section/components/ProductGallery/index.tsx +316 -0
  595. package/data/section-templates/product-detail-section/components/ProductGallery/styles.css +213 -0
  596. package/data/section-templates/product-detail-section/global-types.ts +13 -0
  597. package/data/section-templates/product-detail-section/hooks/useBundleProducts.ts +75 -0
  598. package/data/section-templates/product-detail-section/hooks/usePayWithIkas.ts +114 -0
  599. package/data/section-templates/product-detail-section/hooks/useScrollLock.ts +20 -0
  600. package/data/section-templates/product-detail-section/ikas-config-snippet.json +80 -0
  601. package/data/section-templates/product-detail-section/index.tsx +92 -0
  602. package/data/section-templates/product-detail-section/styles.css +58 -0
  603. package/data/section-templates/product-detail-section/sub-components/Badge/index.tsx +208 -0
  604. package/data/section-templates/product-detail-section/sub-components/Badge/styles.css +129 -0
  605. package/data/section-templates/product-detail-section/sub-components/Breadcrumb/index.tsx +57 -0
  606. package/data/section-templates/product-detail-section/sub-components/Breadcrumb/styles.css +49 -0
  607. package/data/section-templates/product-detail-section/sub-components/BundleMedia/index.tsx +72 -0
  608. package/data/section-templates/product-detail-section/sub-components/BundleQuantityBox/index.tsx +73 -0
  609. package/data/section-templates/product-detail-section/sub-components/BundleQuantityBox/styles.css +43 -0
  610. package/data/section-templates/product-detail-section/sub-components/Button/index.tsx +52 -0
  611. package/data/section-templates/product-detail-section/sub-components/Button/styles.css +114 -0
  612. package/data/section-templates/product-detail-section/sub-components/Checkbox/index.tsx +42 -0
  613. package/data/section-templates/product-detail-section/sub-components/Checkbox/styles.css +65 -0
  614. package/data/section-templates/product-detail-section/sub-components/CollapsibleGroup/index.tsx +52 -0
  615. package/data/section-templates/product-detail-section/sub-components/CollapsibleGroup/styles.css +51 -0
  616. package/data/section-templates/product-detail-section/sub-components/ColorInput/index.tsx +33 -0
  617. package/data/section-templates/product-detail-section/sub-components/ColorInput/styles.css +53 -0
  618. package/data/section-templates/product-detail-section/sub-components/FormItem/index.tsx +66 -0
  619. package/data/section-templates/product-detail-section/sub-components/FormItem/styles.css +38 -0
  620. package/data/section-templates/product-detail-section/sub-components/ImagePreviewModal/index.tsx +55 -0
  621. package/data/section-templates/product-detail-section/sub-components/ImagePreviewModal/styles.css +50 -0
  622. package/data/section-templates/product-detail-section/sub-components/Input/index.tsx +69 -0
  623. package/data/section-templates/product-detail-section/sub-components/Input/styles.css +162 -0
  624. package/data/section-templates/product-detail-section/sub-components/QuantitySelector/index.tsx +41 -0
  625. package/data/section-templates/product-detail-section/sub-components/QuantitySelector/styles.css +51 -0
  626. package/data/section-templates/product-detail-section/sub-components/Select/index.tsx +86 -0
  627. package/data/section-templates/product-detail-section/sub-components/Select/styles.css +110 -0
  628. package/data/section-templates/product-detail-section/sub-components/SliderArrow/index.tsx +26 -0
  629. package/data/section-templates/product-detail-section/sub-components/SliderArrow/styles.css +24 -0
  630. package/data/section-templates/product-detail-section/sub-components/Tag/index.tsx +21 -0
  631. package/data/section-templates/product-detail-section/sub-components/Tag/styles.css +30 -0
  632. package/data/section-templates/product-detail-section/sub-components/Textarea/index.tsx +45 -0
  633. package/data/section-templates/product-detail-section/sub-components/Textarea/styles.css +82 -0
  634. package/data/section-templates/product-detail-section/sub-components/Toggle/index.tsx +46 -0
  635. package/data/section-templates/product-detail-section/sub-components/Toggle/styles.css +86 -0
  636. package/data/section-templates/product-detail-section/sub-components/VariantBadge/index.tsx +153 -0
  637. package/data/section-templates/product-detail-section/sub-components/VariantBadge/styles.css +47 -0
  638. package/data/section-templates/product-detail-section/sub-components/icons/index.tsx +981 -0
  639. package/data/section-templates/product-detail-section/types.ts +11 -0
  640. package/data/section-templates/product-detail-section/utils/bundle.ts +70 -0
  641. package/data/section-templates/product-detail-section/utils/cx.ts +4 -0
  642. package/data/section-templates/product-detail-section/utils/media.ts +36 -0
  643. package/data/section-templates/product-detail-section/utils/optionPrice.ts +19 -0
  644. package/data/section-templates/product-detail-section/utils/optionSet.ts +17 -0
  645. package/data/section-templates/product-detail-section/utils/toast.ts +5 -0
  646. package/data/section-templates/product-pricing/_meta.json +4 -0
  647. package/data/section-templates/product-pricing/ikas-config-snippet.json +66 -0
  648. package/data/section-templates/product-pricing/index.tsx +62 -0
  649. package/data/section-templates/product-pricing/styles.css +32 -0
  650. package/data/section-templates/product-pricing/sub-components/Tag/index.tsx +21 -0
  651. package/data/section-templates/product-pricing/sub-components/Tag/styles.css +30 -0
  652. package/data/section-templates/product-pricing/types.ts +9 -0
  653. package/data/section-templates/product-reviews-section/_meta.json +4 -0
  654. package/data/section-templates/product-reviews-section/hooks/useScrollLock.ts +20 -0
  655. package/data/section-templates/product-reviews-section/ikas-config-snippet.json +136 -0
  656. package/data/section-templates/product-reviews-section/index.tsx +205 -0
  657. package/data/section-templates/product-reviews-section/styles.css +43 -0
  658. package/data/section-templates/product-reviews-section/sub-components/Button/index.tsx +52 -0
  659. package/data/section-templates/product-reviews-section/sub-components/Button/styles.css +114 -0
  660. package/data/section-templates/product-reviews-section/sub-components/FormItem/index.tsx +66 -0
  661. package/data/section-templates/product-reviews-section/sub-components/FormItem/styles.css +38 -0
  662. package/data/section-templates/product-reviews-section/sub-components/ImagePreviewModal/index.tsx +55 -0
  663. package/data/section-templates/product-reviews-section/sub-components/ImagePreviewModal/styles.css +50 -0
  664. package/data/section-templates/product-reviews-section/sub-components/Input/index.tsx +69 -0
  665. package/data/section-templates/product-reviews-section/sub-components/Input/styles.css +162 -0
  666. package/data/section-templates/product-reviews-section/sub-components/Modal/index.tsx +118 -0
  667. package/data/section-templates/product-reviews-section/sub-components/Modal/styles.css +70 -0
  668. package/data/section-templates/product-reviews-section/sub-components/PageLoader/index.tsx +14 -0
  669. package/data/section-templates/product-reviews-section/sub-components/PageLoader/styles.css +12 -0
  670. package/data/section-templates/product-reviews-section/sub-components/Pagination/index.tsx +107 -0
  671. package/data/section-templates/product-reviews-section/sub-components/Pagination/styles.css +88 -0
  672. package/data/section-templates/product-reviews-section/sub-components/ReviewCard/index.tsx +103 -0
  673. package/data/section-templates/product-reviews-section/sub-components/ReviewCard/styles.css +95 -0
  674. package/data/section-templates/product-reviews-section/sub-components/ReviewForm/index.tsx +115 -0
  675. package/data/section-templates/product-reviews-section/sub-components/ReviewForm/styles.css +11 -0
  676. package/data/section-templates/product-reviews-section/sub-components/ReviewSummary/index.tsx +65 -0
  677. package/data/section-templates/product-reviews-section/sub-components/ReviewSummary/styles.css +86 -0
  678. package/data/section-templates/product-reviews-section/sub-components/SpinnerIcon/index.tsx +10 -0
  679. package/data/section-templates/product-reviews-section/sub-components/SpinnerIcon/styles.css +8 -0
  680. package/data/section-templates/product-reviews-section/sub-components/StarRating/index.tsx +76 -0
  681. package/data/section-templates/product-reviews-section/sub-components/StarRating/styles.css +40 -0
  682. package/data/section-templates/product-reviews-section/sub-components/Textarea/index.tsx +45 -0
  683. package/data/section-templates/product-reviews-section/sub-components/Textarea/styles.css +82 -0
  684. package/data/section-templates/product-reviews-section/sub-components/icons/index.tsx +981 -0
  685. package/data/section-templates/product-reviews-section/types.ts +18 -0
  686. package/data/section-templates/product-reviews-section/utils/cx.ts +4 -0
  687. package/data/section-templates/product-reviews-section/utils/fullName.ts +6 -0
  688. package/data/section-templates/product-reviews-section/utils/pagination.ts +29 -0
  689. package/data/section-templates/product-reviews-section/utils/toast.ts +5 -0
  690. package/data/section-templates/product-slider-section/_meta.json +4 -0
  691. package/data/section-templates/product-slider-section/children/CardProductName/ikas-config-snippet.json +21 -0
  692. package/data/section-templates/product-slider-section/children/CardProductName/index.tsx +25 -0
  693. package/data/section-templates/product-slider-section/children/CardProductName/styles.css +22 -0
  694. package/data/section-templates/product-slider-section/children/CardProductName/types.ts +6 -0
  695. package/data/section-templates/product-slider-section/children/CardProductPrice/ikas-config-snippet.json +15 -0
  696. package/data/section-templates/product-slider-section/children/CardProductPrice/index.tsx +30 -0
  697. package/data/section-templates/product-slider-section/children/CardProductPrice/styles.css +13 -0
  698. package/data/section-templates/product-slider-section/children/CardProductPrice/types.ts +5 -0
  699. package/data/section-templates/product-slider-section/children/CardProductVariants/ikas-config-snippet.json +15 -0
  700. package/data/section-templates/product-slider-section/children/CardProductVariants/index.tsx +10 -0
  701. package/data/section-templates/product-slider-section/children/CardProductVariants/styles.css +1 -0
  702. package/data/section-templates/product-slider-section/children/CardProductVariants/types.ts +5 -0
  703. package/data/section-templates/product-slider-section/global-types.ts +13 -0
  704. package/data/section-templates/product-slider-section/ikas-config-snippet.json +121 -0
  705. package/data/section-templates/product-slider-section/index.tsx +151 -0
  706. package/data/section-templates/product-slider-section/styles.css +105 -0
  707. package/data/section-templates/product-slider-section/sub-components/Badge/index.tsx +208 -0
  708. package/data/section-templates/product-slider-section/sub-components/Badge/styles.css +129 -0
  709. package/data/section-templates/product-slider-section/sub-components/Button/index.tsx +52 -0
  710. package/data/section-templates/product-slider-section/sub-components/Button/styles.css +114 -0
  711. package/data/section-templates/product-slider-section/sub-components/ProductCard/index.tsx +276 -0
  712. package/data/section-templates/product-slider-section/sub-components/ProductCard/styles.css +99 -0
  713. package/data/section-templates/product-slider-section/sub-components/SliderArrow/index.tsx +26 -0
  714. package/data/section-templates/product-slider-section/sub-components/SliderArrow/styles.css +24 -0
  715. package/data/section-templates/product-slider-section/sub-components/SpinnerIcon/index.tsx +10 -0
  716. package/data/section-templates/product-slider-section/sub-components/SpinnerIcon/styles.css +8 -0
  717. package/data/section-templates/product-slider-section/sub-components/Tag/index.tsx +21 -0
  718. package/data/section-templates/product-slider-section/sub-components/Tag/styles.css +30 -0
  719. package/data/section-templates/product-slider-section/sub-components/VariantBadge/index.tsx +153 -0
  720. package/data/section-templates/product-slider-section/sub-components/VariantBadge/styles.css +47 -0
  721. package/data/section-templates/product-slider-section/sub-components/icons/index.tsx +981 -0
  722. package/data/section-templates/product-slider-section/types.ts +16 -0
  723. package/data/section-templates/product-slider-section/utils/cx.ts +4 -0
  724. package/data/section-templates/product-slider-section/utils/media.ts +36 -0
  725. package/data/section-templates/product-slider-section/utils/toast.ts +5 -0
  726. package/data/section-templates/recover-password-section/_meta.json +4 -0
  727. package/data/section-templates/recover-password-section/components/RecoverPasswordForm/index.tsx +133 -0
  728. package/data/section-templates/recover-password-section/components/RecoverPasswordForm/styles.css +0 -0
  729. package/data/section-templates/recover-password-section/hooks/useRedirectIfLoggedIn.ts +35 -0
  730. package/data/section-templates/recover-password-section/ikas-config-snippet.json +111 -0
  731. package/data/section-templates/recover-password-section/index.tsx +30 -0
  732. package/data/section-templates/recover-password-section/styles.css +93 -0
  733. package/data/section-templates/recover-password-section/sub-components/Button/index.tsx +52 -0
  734. package/data/section-templates/recover-password-section/sub-components/Button/styles.css +114 -0
  735. package/data/section-templates/recover-password-section/sub-components/FormItem/index.tsx +66 -0
  736. package/data/section-templates/recover-password-section/sub-components/FormItem/styles.css +38 -0
  737. package/data/section-templates/recover-password-section/sub-components/Input/index.tsx +69 -0
  738. package/data/section-templates/recover-password-section/sub-components/Input/styles.css +162 -0
  739. package/data/section-templates/recover-password-section/sub-components/PageLoader/index.tsx +14 -0
  740. package/data/section-templates/recover-password-section/sub-components/PageLoader/styles.css +12 -0
  741. package/data/section-templates/recover-password-section/sub-components/SpinnerIcon/index.tsx +10 -0
  742. package/data/section-templates/recover-password-section/sub-components/SpinnerIcon/styles.css +8 -0
  743. package/data/section-templates/recover-password-section/sub-components/icons/index.tsx +981 -0
  744. package/data/section-templates/recover-password-section/types.ts +12 -0
  745. package/data/section-templates/recover-password-section/utils/cx.ts +4 -0
  746. package/data/section-templates/register-section/_meta.json +4 -0
  747. package/data/section-templates/register-section/components/RegisterForm/index.tsx +326 -0
  748. package/data/section-templates/register-section/components/RegisterForm/styles.css +0 -0
  749. package/data/section-templates/register-section/hooks/useRedirectIfLoggedIn.ts +35 -0
  750. package/data/section-templates/register-section/ikas-config-snippet.json +244 -0
  751. package/data/section-templates/register-section/index.tsx +30 -0
  752. package/data/section-templates/register-section/styles.css +152 -0
  753. package/data/section-templates/register-section/sub-components/Button/index.tsx +52 -0
  754. package/data/section-templates/register-section/sub-components/Button/styles.css +114 -0
  755. package/data/section-templates/register-section/sub-components/Checkbox/index.tsx +42 -0
  756. package/data/section-templates/register-section/sub-components/Checkbox/styles.css +65 -0
  757. package/data/section-templates/register-section/sub-components/FormItem/index.tsx +66 -0
  758. package/data/section-templates/register-section/sub-components/FormItem/styles.css +38 -0
  759. package/data/section-templates/register-section/sub-components/Input/index.tsx +69 -0
  760. package/data/section-templates/register-section/sub-components/Input/styles.css +162 -0
  761. package/data/section-templates/register-section/sub-components/PageLoader/index.tsx +14 -0
  762. package/data/section-templates/register-section/sub-components/PageLoader/styles.css +12 -0
  763. package/data/section-templates/register-section/sub-components/SocialLoginButton/index.tsx +24 -0
  764. package/data/section-templates/register-section/sub-components/SocialLoginButton/styles.css +19 -0
  765. package/data/section-templates/register-section/sub-components/SpinnerIcon/index.tsx +10 -0
  766. package/data/section-templates/register-section/sub-components/SpinnerIcon/styles.css +8 -0
  767. package/data/section-templates/register-section/sub-components/icons/index.tsx +981 -0
  768. package/data/section-templates/register-section/types.ts +26 -0
  769. package/data/section-templates/register-section/utils/cx.ts +4 -0
  770. package/data/section-templates/rich-text-section/_meta.json +4 -0
  771. package/data/section-templates/rich-text-section/ikas-config-snippet.json +22 -0
  772. package/data/section-templates/rich-text-section/index.tsx +25 -0
  773. package/data/section-templates/rich-text-section/styles.css +51 -0
  774. package/data/section-templates/rich-text-section/types.ts +4 -0
  775. package/data/section-templates/variant-selection/_meta.json +4 -0
  776. package/data/section-templates/variant-selection/ikas-config-snippet.json +73 -0
  777. package/data/section-templates/variant-selection/index.tsx +38 -0
  778. package/data/section-templates/variant-selection/styles.css +14 -0
  779. package/data/section-templates/variant-selection/sub-components/Badge/index.tsx +208 -0
  780. package/data/section-templates/variant-selection/sub-components/Badge/styles.css +129 -0
  781. package/data/section-templates/variant-selection/sub-components/VariantBadge/index.tsx +153 -0
  782. package/data/section-templates/variant-selection/sub-components/VariantBadge/styles.css +47 -0
  783. package/data/section-templates/variant-selection/types.ts +11 -0
  784. package/data/section-templates/variant-selection/utils/cx.ts +4 -0
  785. package/data/storefront-api.json +2 -2024
  786. package/data/storefront-types.json +1 -1
  787. package/dist/index.js +40 -11
  788. package/dist/index.js.map +1 -1
  789. package/package.json +5 -3
  790. package/data/section-templates.json +0 -928
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-04-09T15:20:24.668Z",
2
+ "generatedAt": "2026-04-10T07:29:39.067Z",
3
3
  "functions": [
4
4
  {
5
5
  "name": "apiListBlog",
@@ -16401,2027 +16401,5 @@
16401
16401
  "isClass": false
16402
16402
  }
16403
16403
  ],
16404
- "codeExamples": [
16405
- {
16406
- "id": "account-info-section",
16407
- "title": "Account Info Section",
16408
- "description": "Account dashboard with tabbed navigation for profile info, orders, addresses, and favorites. Uses IkasComponentRenderer for child components (AccountInfoContent, AccountOrders, AccountAddresses, AccountFavorites, AccountOrderDetail).",
16409
- "code": "import { useState, useEffect } from \"preact/hooks\";\nimport {\n logout,\n customerStore,\n waitForCustomerStoreInit,\n Router,\n IkasThemePageType,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport AccountSidebar from \"./components/AccountSidebar\";\nimport PageLoader from \"../../sub-components/PageLoader\";\nimport type { NavItem } from \"./components/AccountSidebar\";\n\nconst PAGE_TYPE_MAP: Record<Exclude<NavItem, \"logout\">, IkasThemePageType> = {\n account: \"ACCOUNT\",\n orders: \"ORDERS\",\n addresses: \"ADDRESSES\",\n favorites: \"FAVORITE_PRODUCTS\",\n};\n\nfunction getActiveTabFromPath(): NavItem {\n const path = Router.getCurrentPath();\n if (path.includes(\"/orders\")) return \"orders\";\n if (path.includes(\"/addresses\")) return \"addresses\";\n if (path.includes(\"/favorite-products\")) return \"favorites\";\n return \"account\";\n}\n\nexport function AccountInfo(props: Props) {\n const [isChecking, setIsChecking] = useState(true);\n\n useEffect(() => {\n waitForCustomerStoreInit(customerStore).then(() => {\n if (!customerStore.customer) {\n Router.navigateToPage(\"LOGIN\");\n } else {\n setIsChecking(false);\n }\n });\n }, []);\n\n const {\n accountInfoLabel = \"Hesap Bilgilerim\",\n ordersLabel = \"Siparislerim\",\n addressesLabel = \"Adreslerim\",\n favoritesLabel = \"Favorilerim\",\n logoutLabel = \"Cikis Yap\",\n components,\n } = props;\n const activeTab = getActiveTabFromPath();\n\n const handleNavigate = async (item: NavItem) => {\n if (item === \"logout\") {\n await logout(customerStore);\n Router.navigateToPage(\"INDEX\");\n return;\n }\n Router.navigateToPage(PAGE_TYPE_MAP[item]);\n };\n\n return (\n <section className=\"kombos-account-info\">\n <div className=\"kombos-account-info__container kombos-container\">\n <AccountSidebar\n activeItem={activeTab}\n onNavigate={handleNavigate}\n accountInfoLabel={accountInfoLabel}\n ordersLabel={ordersLabel}\n addressesLabel={addressesLabel}\n favoritesLabel={favoritesLabel}\n logoutLabel={logoutLabel}\n />\n\n <div className=\"kombos-account-info__content\">\n {isChecking ? (\n <PageLoader />\n ) : (\n <IkasComponentRenderer\n id=\"account-info\"\n components={components}\n parentProps={props}\n />\n )}\n </div>\n </div>\n </section>\n );\n}\n\nexport default AccountInfo;\n",
16410
- "relatedFunctions": [
16411
- "logout",
16412
- "customerStore",
16413
- "waitForCustomerStoreInit",
16414
- "Router",
16415
- "getAccountInfoForm",
16416
- "initAccountInfoForm",
16417
- "setAccountInfoFormFirstName",
16418
- "setAccountInfoFormLastName",
16419
- "setAccountInfoFormPhone",
16420
- "submitAccountInfoForm",
16421
- "getOrders",
16422
- "deleteCustomerAddress",
16423
- "getFavoriteProducts",
16424
- "getProductOptionSet",
16425
- "getOrderDetailsOfPage",
16426
- "getIkasOrderDisplayedPackages",
16427
- "isIkasOrderRefundable",
16428
- "getIkasOrderProductFiles",
16429
- "getOrderProductFiles",
16430
- "getOrderTransactions",
16431
- "getDigitalProductFileDownloadUrl",
16432
- "getIkasOrderRefundableItems"
16433
- ],
16434
- "categories": [
16435
- "Customer",
16436
- "Account"
16437
- ],
16438
- "files": [
16439
- {
16440
- "filename": "children/AccountAddresses/components/AddressCard/index.tsx",
16441
- "content": "import {\n IkasCustomerAddress,\n getCustomerAddressText,\n} from \"@ikas/bp-storefront\";\nimport { getFullName } from \"../../../../utils/fullName\";\n\ninterface Props {\n address: IkasCustomerAddress;\n editButtonText: string;\n deleteButtonText: string;\n onEdit: () => void;\n onDelete: () => void;\n}\n\nexport default function AddressCard({\n address,\n editButtonText,\n deleteButtonText,\n onEdit,\n onDelete,\n}: Props) {\n const fullName = getFullName(address?.firstName, address?.lastName);\n\n return (\n <div className=\"kombos-address-card\">\n <div className=\"kombos-address-card__header\">\n <span className=\"kombos-address-card__title text-sm-medium\">\n {address.title}\n </span>\n <div className=\"kombos-address-card__actions\">\n <button\n type=\"button\"\n className=\"kombos-address-card__edit text-sm-semibold\"\n onClick={onEdit}\n >\n {editButtonText}\n </button>\n <button\n type=\"button\"\n className=\"kombos-address-card__delete text-sm-semibold\"\n onClick={onDelete}\n >\n {deleteButtonText}\n </button>\n </div>\n </div>\n <div className=\"kombos-address-card__body\">\n <span className=\"text-sm-medium\">{fullName}</span>\n <span className=\"text-sm-regular\">\n {getCustomerAddressText(address)}\n </span>\n </div>\n </div>\n );\n}\n"
16442
- },
16443
- {
16444
- "filename": "children/AccountAddresses/components/AddressCard/styles.css",
16445
- "content": "/* ── Address Card ── */\n.kombos-address-card {\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n padding: 1.5rem;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.kombos-address-card__header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.kombos-address-card__title {\n color: var(--kombos-gray-900);\n}\n\n.kombos-address-card__actions {\n display: flex;\n gap: 0.75rem;\n align-items: center;\n}\n\n.kombos-address-card__edit,\n.kombos-address-card__delete {\n text-decoration: underline;\n color: var(--kombos-gray-900);\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n}\n\n.kombos-address-card__delete {\n color: var(--kombos-gray-500);\n}\n\n.kombos-address-card__body {\n display: flex;\n flex-direction: column;\n gap: 0.375rem;\n color: var(--kombos-gray-900);\n}\n"
16446
- },
16447
- {
16448
- "filename": "children/AccountAddresses/components/AddressModal/index.tsx",
16449
- "content": "import { Fragment } from \"preact\";\nimport { useState, useEffect, useRef } from \"preact/hooks\";\nimport {\n type AddressForm,\n type IkasFormItem,\n type IkasFormItemOption,\n type IkasFormFreeText,\n type AddressFormItem,\n IkasCustomerAddress,\n getIkasCustomerAddressForm,\n getEmptyAddressForm,\n initAddressForm,\n submitAddressForm,\n setAddressFormFirstName,\n setAddressFormLastName,\n setAddressFormIdentityNumber,\n setAddressFormPhone,\n setAddressFormAddressLine1,\n setAddressFormAddressLine2,\n setAddressFormCountry,\n setAddressFormState,\n setAddressFormCity,\n setAddressFormDistrict,\n setAddressFormRegion,\n setAddressFormPostalCode,\n setAddressFormTitle,\n customerStore,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport Modal from \"../../../../sub-components/Modal\";\nimport FormItem from \"../../../../sub-components/FormItem\";\nimport Input from \"../../../../sub-components/Input\";\nimport Select from \"../../../../sub-components/Select\";\nimport SkeletonField from \"../../../../sub-components/SkeletonField\";\n\ninterface Props {\n address?: IkasCustomerAddress;\n title: string;\n saveButtonText: string;\n savingButtonText: string;\n cancelButtonText: string;\n onClose: () => void;\n onSaved: () => void;\n}\n\ntype FormFieldSetter = (form: AddressForm, value: string) => void;\n\nconst SKELETON_ROWS: AddressFormItem[][] = [\n [\"firstName\", \"lastName\"],\n [\"identityNumber\"],\n [\"phone\"],\n [\"addressLine1\"],\n [\"addressLine2\"],\n [\"country\"],\n [\"state\", \"city\"],\n [\"district\", \"postalCode\"],\n];\n\nconst getFieldStatus = (field?: { hasError: boolean }): \"default\" | \"error\" =>\n field?.hasError ? \"error\" : \"default\";\n\nconst getFieldHelper = (field?: { hasError: boolean; message?: string }) =>\n field?.hasError ? field.message : undefined;\n\nconst AddressModal = observer(function AddressModal({\n address,\n title,\n saveButtonText,\n savingButtonText,\n cancelButtonText,\n onClose,\n onSaved,\n}: Props) {\n const onSavedRef = useRef(onSaved);\n onSavedRef.current = onSaved;\n\n const [addressForm] = useState<AddressForm>(() =>\n address\n ? getIkasCustomerAddressForm(address)\n : getEmptyAddressForm(customerStore),\n );\n\n const [isLoading, setIsLoading] = useState(true);\n\n useEffect(() => {\n initAddressForm(addressForm, address).then(() => setIsLoading(false));\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitAddressForm(addressForm);\n if (success) {\n onSavedRef.current();\n onClose();\n }\n };\n\n const renderInput = (\n field: IkasFormItem | undefined,\n setter: FormFieldSetter,\n id: string,\n type?: string,\n ) => {\n if (!field?.label) return null;\n return (\n <FormItem\n label={field.label}\n htmlFor={id}\n status={getFieldStatus(field)}\n helper={getFieldHelper(field)}\n >\n <Input\n id={id}\n type={type}\n value={field.value ?? \"\"}\n placeholder={field.placeholder}\n onInput={(e: Event) =>\n setter(addressForm, (e.target as HTMLInputElement).value)\n }\n />\n </FormItem>\n );\n };\n\n const renderSelect = (\n field: IkasFormFreeText | undefined,\n options: IkasFormItemOption[] | undefined,\n setter: FormFieldSetter,\n id: string,\n ) => {\n if (!field?.label) return null;\n\n if (field.isFreeText || !options?.length) {\n return renderInput(field, setter, id);\n }\n\n return (\n <FormItem\n label={field.label}\n htmlFor={id}\n status={getFieldStatus(field)}\n helper={getFieldHelper(field)}\n >\n <Select\n id={id}\n options={[{ value: \"\", label: field.placeholder ?? \"\" }, ...options]}\n value={field.value ?? \"\"}\n onChange={(e: Event) =>\n setter(addressForm, (e.target as HTMLSelectElement).value)\n }\n disabled={field.isLoading}\n />\n </FormItem>\n );\n };\n\n const renderFormField = (key: AddressFormItem) => {\n switch (key) {\n case \"firstName\":\n return renderInput(\n addressForm.firstName,\n setAddressFormFirstName,\n \"addr-first-name\",\n );\n case \"lastName\":\n return renderInput(\n addressForm.lastName,\n setAddressFormLastName,\n \"addr-last-name\",\n );\n case \"identityNumber\":\n return renderInput(\n addressForm.identityNumber,\n setAddressFormIdentityNumber,\n \"addr-identity\",\n );\n case \"phone\":\n return renderInput(\n addressForm.phone,\n setAddressFormPhone,\n \"addr-phone\",\n \"tel\",\n );\n case \"addressLine1\":\n return renderInput(\n addressForm.addressLine1,\n setAddressFormAddressLine1,\n \"addr-line1\",\n );\n case \"addressLine2\":\n return renderInput(\n addressForm.addressLine2,\n setAddressFormAddressLine2,\n \"addr-line2\",\n );\n case \"country\":\n return renderSelect(\n addressForm.country,\n addressForm.countryOptions,\n setAddressFormCountry,\n \"addr-country\",\n );\n case \"state\":\n return renderSelect(\n addressForm.state,\n addressForm.stateOptions,\n setAddressFormState,\n \"addr-state\",\n );\n case \"city\":\n return renderSelect(\n addressForm.city,\n addressForm.cityOptions,\n setAddressFormCity,\n \"addr-city\",\n );\n case \"district\":\n return renderSelect(\n addressForm.district,\n addressForm.districtOptions,\n setAddressFormDistrict,\n \"addr-district\",\n );\n case \"region\":\n return addressForm.regionOptions?.length\n ? renderSelect(\n addressForm.region,\n addressForm.regionOptions,\n setAddressFormRegion,\n \"addr-region\",\n )\n : null;\n case \"postalCode\":\n return renderInput(\n addressForm.postalCode,\n setAddressFormPostalCode,\n \"addr-postal\",\n );\n default:\n return null;\n }\n };\n\n const renderRowLayout = (\n rows: AddressFormItem[][],\n renderItem: (key: AddressFormItem) => preact.JSX.Element | null,\n ) =>\n rows.map((row) => {\n const visibleKeys = row.filter((key) => addressForm[key] != null);\n if (visibleKeys.length === 0) return null;\n const rowKey = visibleKeys.join(\"-\");\n if (visibleKeys.length === 1) {\n return <Fragment key={rowKey}>{renderItem(visibleKeys[0])}</Fragment>;\n }\n return (\n <div key={rowKey} className=\"kombos-address-modal__row\">\n {visibleKeys.map((key) => (\n <Fragment key={key}>{renderItem(key)}</Fragment>\n ))}\n </div>\n );\n });\n\n return (\n <Modal\n onClose={onClose}\n title={title}\n cancelText={cancelButtonText}\n okText={addressForm.isSubmitting ? savingButtonText : saveButtonText}\n okButtonProps={{\n type: \"submit\",\n form: \"kombos-address-form\",\n disabled: addressForm.isSubmitting || isLoading,\n }}\n >\n {isLoading ? (\n <div className=\"kombos-address-modal__form\">\n <SkeletonField labelWidth=\"40%\" />\n {renderRowLayout(SKELETON_ROWS, () => (\n <SkeletonField labelWidth=\"40%\" />\n ))}\n </div>\n ) : (\n <form\n id=\"kombos-address-form\"\n className=\"kombos-address-modal__form\"\n onSubmit={handleSubmit}\n >\n {renderInput(\n addressForm.title,\n setAddressFormTitle,\n \"addr-title\",\n )}\n {renderRowLayout(addressForm.addressFormat ?? [], renderFormField)}\n </form>\n )}\n </Modal>\n );\n});\n\nexport default AddressModal;\n"
16450
- },
16451
- {
16452
- "filename": "children/AccountAddresses/components/AddressModal/styles.css",
16453
- "content": "/* ── Address Modal (form-specific styles) ── */\n.kombos-address-modal__form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.kombos-address-modal__row {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n@media (min-width: 768px) {\n .kombos-address-modal__row {\n flex-direction: row;\n }\n\n .kombos-address-modal__row > * {\n flex: 1;\n }\n}"
16454
- },
16455
- {
16456
- "filename": "children/AccountAddresses/ikas-config-snippet.json",
16457
- "content": "{\n \"id\": \"{{PROJECT_ID}}-account-addresses\",\n \"name\": \"AccountAddresses\",\n \"type\": \"component\",\n \"entry\": \"./src/components/AccountAddresses/index.tsx\",\n \"styles\": \"./src/components/AccountAddresses/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"My Addresses\",\n \"groupId\": \"general\"\n },\n {\n \"name\": \"emptyMessage\",\n \"displayName\": \"Empty State Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Henüz adres eklenmedi.\",\n \"groupId\": \"general\"\n },\n {\n \"name\": \"addButtonText\",\n \"displayName\": \"Add Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Address Add\",\n \"groupId\": \"general\"\n },\n {\n \"name\": \"editButtonText\",\n \"displayName\": \"Edit Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Edit\",\n \"groupId\": \"general\"\n },\n {\n \"name\": \"modalTitleAdd\",\n \"displayName\": \"Modal Title (Ekleme)\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Address Add\",\n \"groupId\": \"addressModal\"\n },\n {\n \"name\": \"modalTitleEdit\",\n \"displayName\": \"Modal Title (Edit)\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Adresi Edit\",\n \"groupId\": \"addressModal\"\n },\n {\n \"name\": \"saveButtonText\",\n \"displayName\": \"Save Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Save\",\n \"groupId\": \"addressModal\"\n },\n {\n \"name\": \"savingButtonText\",\n \"displayName\": \"Saving Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Kaydediliyor...\",\n \"groupId\": \"addressModal\"\n },\n {\n \"name\": \"cancelButtonText\",\n \"displayName\": \"Cancel Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Iptal\",\n \"groupId\": \"addressModal\"\n },\n {\n \"name\": \"deleteConfirmTitle\",\n \"displayName\": \"Delete Confirmation Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Adresi Delete\",\n \"groupId\": \"deleteModal\"\n },\n {\n \"name\": \"deleteConfirmMessage\",\n \"displayName\": \"Delete Confirmation Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Bu adresi silmek istediginize emin misiniz?\",\n \"groupId\": \"deleteModal\"\n },\n {\n \"name\": \"deleteButtonText\",\n \"displayName\": \"Delete Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Delete\",\n \"groupId\": \"deleteModal\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"general\",\n \"name\": \"General\",\n \"description\": \"Page başlığı, boş durum mesajı ve buton metinleri\"\n },\n {\n \"id\": \"addressModal\",\n \"name\": \"Address Form Modal\",\n \"description\": \"Address ekleme ve düzenleme modalındaki başlık ve buton metinleri\"\n },\n {\n \"id\": \"deleteModal\",\n \"name\": \"Delete Confirmation Modal\",\n \"description\": \"Address silme onay modalındaki başlık, mesaj ve buton metinleri\"\n }\n ]\n}"
16458
- },
16459
- {
16460
- "filename": "children/AccountAddresses/index.tsx",
16461
- "content": "import { useState } from \"preact/hooks\";\nimport {\n customerStore,\n IkasCustomerAddress,\n deleteCustomerAddress,\n} from \"@ikas/bp-storefront\";\nimport Button from \"../../sub-components/Button\";\nimport AddressCard from \"./components/AddressCard\";\nimport AddressModal from \"./components/AddressModal\";\nimport ConfirmModal from \"../../sub-components/ConfirmModal\";\nimport { Props } from \"./types\";\n\nexport function AccountAddresses({\n title = \"Adreslerim\",\n addButtonText = \"Adres Ekle\",\n emptyMessage = \"Henüz adres eklenmedi.\",\n editButtonText = \"Düzenle\",\n deleteButtonText = \"Sil\",\n saveButtonText = \"Kaydet\",\n savingButtonText = \"Kaydediliyor...\",\n cancelButtonText = \"Iptal\",\n modalTitleAdd = \"Adres Ekle\",\n modalTitleEdit = \"Adresi Düzenle\",\n deleteConfirmMessage = \"Bu adresi silmek istediginize emin misiniz?\",\n deleteConfirmTitle = \"Adresi Sil\",\n}: Props) {\n const [modalOpen, setModalOpen] = useState(false);\n const [editingAddress, setEditingAddress] = useState<\n IkasCustomerAddress | undefined\n >(undefined);\n const [deletingAddress, setDeletingAddress] = useState<\n IkasCustomerAddress | undefined\n >(undefined);\n\n const addresses = customerStore.customer?.addresses ?? [];\n\n const handleAdd = () => {\n setEditingAddress(undefined);\n setModalOpen(true);\n };\n\n const handleEdit = (addr: IkasCustomerAddress) => {\n setEditingAddress(addr);\n setModalOpen(true);\n };\n\n const handleDeleteConfirm = async () => {\n if (!deletingAddress) return;\n await deleteCustomerAddress(customerStore, deletingAddress);\n setDeletingAddress(undefined);\n };\n\n const handleModalClose = () => {\n setModalOpen(false);\n setEditingAddress(undefined);\n };\n\n return (\n <div className=\"kombos-account-addresses\">\n <h1 className=\"kombos-account-addresses__title text-md-medium\">{title}</h1>\n\n {addresses.length === 0 ? (\n <div className=\"kombos-account-addresses__empty-state\">\n <p className=\"kombos-account-addresses__empty text-sm-medium\">{emptyMessage}</p>\n <Button\n variant=\"primary\"\n size=\"s\"\n onClick={handleAdd}\n className=\"kombos-account-addresses__add\"\n >\n {addButtonText}\n </Button>\n </div>\n ) : (\n <>\n <div className=\"kombos-account-addresses__grid\">\n {addresses.map((addr) => (\n <AddressCard\n key={addr.id}\n address={addr}\n editButtonText={editButtonText}\n deleteButtonText={deleteButtonText}\n onEdit={() => handleEdit(addr)}\n onDelete={() => setDeletingAddress(addr)}\n />\n ))}\n </div>\n\n <Button\n variant=\"primary\"\n size=\"s\"\n onClick={handleAdd}\n className=\"kombos-account-addresses__add\"\n >\n {addButtonText}\n </Button>\n </>\n )}\n\n {modalOpen && (\n <AddressModal\n address={editingAddress}\n title={editingAddress ? modalTitleEdit : modalTitleAdd}\n saveButtonText={saveButtonText}\n savingButtonText={savingButtonText}\n cancelButtonText={cancelButtonText}\n onClose={handleModalClose}\n onSaved={handleModalClose}\n />\n )}\n\n {deletingAddress && (\n <ConfirmModal\n title={deleteConfirmTitle}\n message={deleteConfirmMessage}\n cancelText={cancelButtonText}\n confirmText={deleteButtonText}\n confirmVariant=\"dangerous\"\n onClose={() => setDeletingAddress(undefined)}\n onConfirm={handleDeleteConfirm}\n />\n )}\n </div>\n );\n}\n\nexport default AccountAddresses;\n"
16462
- },
16463
- {
16464
- "filename": "children/AccountAddresses/styles.css",
16465
- "content": "/* ── Account Addresses ── */\n.kombos-account-addresses {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.kombos-account-addresses__title {\n color: var(--kombos-gray-900);\n}\n\n.kombos-account-addresses__add {\n width: fit-content;\n min-width: 12.5rem;\n}\n\n/* Cards grid: 1 col mobile, 2 col desktop */\n.kombos-account-addresses__grid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 1.5rem;\n}\n\n@media (min-width: 768px) {\n .kombos-account-addresses__grid {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n/* Empty state */\n.kombos-account-addresses__empty-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1.5rem;\n padding: 3rem 0;\n}\n\n.kombos-account-addresses__empty {\n color: var(--kombos-gray-500);\n}\n"
16466
- },
16467
- {
16468
- "filename": "children/AccountAddresses/types.ts",
16469
- "content": "export interface Props {\n title?: string;\n emptyMessage?: string;\n addButtonText?: string;\n editButtonText?: string;\n modalTitleAdd?: string;\n modalTitleEdit?: string;\n saveButtonText?: string;\n savingButtonText?: string;\n cancelButtonText?: string;\n deleteConfirmTitle?: string;\n deleteConfirmMessage?: string;\n deleteButtonText?: string;\n}\n"
16470
- },
16471
- {
16472
- "filename": "children/AccountFavorites/ikas-config-snippet.json",
16473
- "content": "{\n \"id\": \"{{PROJECT_ID}}-account-favorites\",\n \"name\": \"AccountFavorites\",\n \"type\": \"component\",\n \"entry\": \"./src/components/AccountFavorites/index.tsx\",\n \"styles\": \"./src/components/AccountFavorites/styles.css\",\n \"props\": [\n {\n \"name\": \"favoritesLabel\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"My Favorites\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"emptyFavoritesText\",\n \"displayName\": \"Empty List Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Henuz favori urun eklemediniz.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"addToCartText\",\n \"displayName\": \"Cart Add Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Add to Cart\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Components\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"privateVarMap\": {\n \"product\": {\n \"id\": \"pvm_1772803530689_1\",\n \"typeId\": \"@ikas/bp-storefront-models-IkasProduct\"\n }\n },\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-card-product-price\",\n \"{{PROJECT_ID}}-card-product-variants\",\n \"{{PROJECT_ID}}-card-product-name\"\n ]\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Bileşende görünen tüm metinler\"\n }\n ]\n}"
16474
- },
16475
- {
16476
- "filename": "children/AccountFavorites/index.tsx",
16477
- "content": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getFavoriteProducts,\n IkasProduct,\n getProductOptionSet,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport ProductCard from \"../../sub-components/ProductCard\";\nimport PageLoader from \"../../sub-components/PageLoader\";\nimport { Props } from \"./types\";\n\nexport function AccountFavorites(props: Props) {\n const {\n favoritesLabel = \"Favorilerim\",\n emptyFavoritesText = \"Henuz favori urun eklemediniz.\",\n addToCartText = \"Sepete Ekle\",\n components,\n } = props;\n const [favorites, setFavorites] = useState<IkasProduct[]>([]);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n getFavoriteProducts(customerStore).then((products) => {\n const items = products ?? [];\n items.forEach((p) => getProductOptionSet(p));\n setFavorites(items);\n setLoading(false);\n });\n }, []);\n\n return (\n <div className=\"kombos-account-favorites\">\n <h1 className=\"kombos-account-favorites__title text-md-medium\">\n {favoritesLabel}\n </h1>\n\n {loading ? (\n <PageLoader />\n ) : (\n <>\n {favorites.length === 0 && (\n <p className=\"kombos-account-favorites__empty text-sm-regular\">\n {emptyFavoritesText}\n </p>\n )}\n\n {favorites.length > 0 && (\n <div className=\"kombos-account-favorites__grid\">\n {favorites.map((product) => (\n <div\n key={product.id}\n className=\"kombos-account-favorites__card\"\n >\n <ProductCard\n product={product}\n addToCartText={addToCartText}\n sizes=\"(max-width: 767px) calc(50vw - 36px), (max-width: 1023px) calc(33.3vw - 32px), calc(33.3vw - 48px)\"\n onFavoriteRemove={() =>\n setFavorites((prev) =>\n prev.filter((f) => f.id !== product.id),\n )\n }\n />\n <IkasComponentRenderer\n id={`account-favorites-product-${product.id}`}\n components={components}\n parentProps={props}\n map={{ product }}\n className=\"kombos-account-favorites__card-content\"\n />\n </div>\n ))}\n </div>\n )}\n </>\n )}\n </div>\n );\n}\n\nexport default AccountFavorites;\n"
16478
- },
16479
- {
16480
- "filename": "children/AccountFavorites/styles.css",
16481
- "content": "/* ── Account Favorites ── */\n.kombos-account-favorites {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.kombos-account-favorites__title {\n display: block;\n color: var(--kombos-gray-900);\n}\n\n.kombos-account-favorites__empty {\n color: var(--kombos-gray-500);\n padding: 3rem 0;\n text-align: center;\n}\n\n.kombos-account-favorites__grid {\n width: 100%;\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n grid-template-rows: 1fr;\n gap: 1rem;\n}\n\n.kombos-account-favorites__card {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n min-width: 0;\n overflow: hidden;\n}\n\n.kombos-account-favorites__card-content {\n display: flex;\n flex-direction: column;\n}\n\n/* ── Tablet (768px+) ── */\n@media (min-width: 768px) {\n .kombos-account-favorites__grid {\n grid-template-columns: repeat(3, 1fr);\n }\n}\n\n/* ── Desktop (1024px+) ── */\n@media (min-width: 1024px) {\n .kombos-account-favorites__grid {\n gap: 1.5rem;\n }\n}\n"
16482
- },
16483
- {
16484
- "filename": "children/AccountFavorites/types.ts",
16485
- "content": "export interface Props {\n favoritesLabel?: string;\n emptyFavoritesText?: string;\n addToCartText?: string;\n components?: any;\n}\n"
16486
- },
16487
- {
16488
- "filename": "children/AccountInfoContent/ikas-config-snippet.json",
16489
- "content": "{\n \"id\": \"{{PROJECT_ID}}-account-info-content\",\n \"name\": \"AccountInfoContent\",\n \"type\": \"component\",\n \"entry\": \"./src/components/AccountInfoContent/index.tsx\",\n \"styles\": \"./src/components/AccountInfoContent/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Account Bilgilerim\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"successMessage\",\n \"displayName\": \"Success Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Bilgileriniz guncellendi.\",\n \"groupId\": \"messages\"\n },\n {\n \"name\": \"submitButtonText\",\n \"displayName\": \"Save Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Save\",\n \"groupId\": \"buttons\"\n },\n {\n \"name\": \"submittingButtonText\",\n \"displayName\": \"Saving Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Kaydediliyor...\",\n \"groupId\": \"buttons\"\n },\n {\n \"name\": \"emailLabel\",\n \"displayName\": \"Email\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Email\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Page başlığı ve form etiketleri\"\n },\n {\n \"id\": \"buttons\",\n \"name\": \"Buttons\",\n \"description\": \"Save butonu metinleri\"\n },\n {\n \"id\": \"messages\",\n \"name\": \"Messages\",\n \"description\": \"Success ve hata geri bildirim mesajları\"\n }\n ]\n}"
16490
- },
16491
- {
16492
- "filename": "children/AccountInfoContent/index.tsx",
16493
- "content": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getAccountInfoForm,\n initAccountInfoForm,\n setAccountInfoFormFirstName,\n setAccountInfoFormLastName,\n setAccountInfoFormPhone,\n submitAccountInfoForm,\n} from \"@ikas/bp-storefront\";\nimport FormItem from \"../../sub-components/FormItem\";\nimport Input from \"../../sub-components/Input\";\nimport Button from \"../../sub-components/Button\";\nimport SkeletonField from \"../../sub-components/SkeletonField\";\nimport { Props } from \"./types\";\n\nexport function AccountInfoContent({\n title = \"Hesap Bilgilerim\",\n successMessage = \"Bilgileriniz guncellendi.\",\n submitButtonText = \"Kaydet\",\n submittingButtonText = \"Kaydediliyor...\",\n emailLabel = \"E-posta\",\n}: Props) {\n const accountForm = getAccountInfoForm(customerStore);\n const [isLoading, setIsLoading] = useState(true);\n\n useEffect(() => {\n initAccountInfoForm(accountForm).finally(() => setIsLoading(false));\n }, []);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitAccountInfoForm(accountForm);\n };\n\n function renderFormField(\n field: typeof accountForm.firstName,\n id: string,\n setter: (form: typeof accountForm, value: string) => void,\n inputProps?: Record<string, unknown>,\n ) {\n if (!field) return null;\n const status = field.hasError ? \"error\" : \"default\";\n return (\n <FormItem\n label={field.label}\n htmlFor={id}\n status={status}\n helper={field.hasError ? field.message : undefined}\n >\n <Input\n id={id}\n value={field.value ?? \"\"}\n onInput={(e: Event) =>\n setter(accountForm, (e.target as HTMLInputElement).value)\n }\n {...inputProps}\n />\n </FormItem>\n );\n }\n\n return (\n <div className=\"kombos-account-info-content\">\n <h1 className=\"kombos-account-info-content__title text-md-medium\">{title}</h1>\n\n {accountForm.isSuccess && (\n <div className=\"kombos-account-info-content__alert kombos-account-info-content__alert--success text-sm-regular\">\n {successMessage}\n </div>\n )}\n {accountForm.isFailure && accountForm.responseMessage && (\n <div className=\"kombos-account-info-content__alert kombos-account-info-content__alert--error text-sm-regular\">\n {accountForm.responseMessage}\n </div>\n )}\n\n {isLoading ? (\n <div className=\"kombos-account-info-content__form\">\n <div className=\"kombos-account-info-content__row\">\n <SkeletonField />\n <SkeletonField />\n </div>\n <div className=\"kombos-account-info-content__row\">\n <SkeletonField />\n <SkeletonField />\n </div>\n </div>\n ) : (\n <form className=\"kombos-account-info-content__form\" onSubmit={handleSubmit}>\n <div className=\"kombos-account-info-content__row\">\n {renderFormField(\n accountForm.firstName,\n \"account-first-name\",\n setAccountInfoFormFirstName,\n )}\n {renderFormField(\n accountForm.lastName,\n \"account-last-name\",\n setAccountInfoFormLastName,\n )}\n </div>\n\n <div className=\"kombos-account-info-content__row\">\n <FormItem label={emailLabel} htmlFor=\"account-email\">\n <Input\n id=\"account-email\"\n value={customerStore.customer?.email ?? \"\"}\n disabled\n />\n </FormItem>\n\n {renderFormField(\n accountForm.phone,\n \"account-phone\",\n setAccountInfoFormPhone,\n { type: \"tel\" },\n )}\n </div>\n\n <Button\n variant=\"primary\"\n loading={accountForm.isSubmitting}\n className=\"kombos-account-info-content__submit\"\n >\n {accountForm.isSubmitting ? submittingButtonText : submitButtonText}\n </Button>\n </form>\n )}\n </div>\n );\n}\n\nexport default AccountInfoContent;\n"
16494
- },
16495
- {
16496
- "filename": "children/AccountInfoContent/styles.css",
16497
- "content": "/* ── Account Info Content ── */\n.kombos-account-info-content {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.kombos-account-info-content__title {\n color: var(--kombos-gray-900);\n}\n\n/* ── Alerts ── */\n.kombos-account-info-content__alert {\n width: 100%;\n padding: 0.75rem 1rem;\n border-radius: 6px;\n}\n\n.kombos-account-info-content__alert--success {\n background: rgba(18, 183, 106, 0.08);\n color: var(--kombos-success);\n}\n\n.kombos-account-info-content__alert--error {\n background: rgba(255, 60, 72, 0.08);\n color: var(--kombos-error);\n}\n\n/* ── Form ── */\n.kombos-account-info-content__form {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.kombos-account-info-content__row {\n display: grid;\n grid-template-columns: 1fr;\n gap: 1rem;\n}\n\n.kombos-account-info-content__submit {\n width: fit-content;\n min-width: 12.5rem;\n margin-left: auto;\n}\n\n/* ── Tablet (768px+) ── */\n@media (min-width: 768px) {\n .kombos-account-info-content__row {\n grid-template-columns: 1fr 1fr;\n gap: 1.5rem;\n }\n}\n\n"
16498
- },
16499
- {
16500
- "filename": "children/AccountInfoContent/types.ts",
16501
- "content": "export interface Props {\n title?: string;\n successMessage?: string;\n submitButtonText?: string;\n submittingButtonText?: string;\n emailLabel?: string;\n}\n"
16502
- },
16503
- {
16504
- "filename": "children/AccountOrderDetail/components/OrderHeader/index.tsx",
16505
- "content": "import {\n IkasOrder,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n} from \"@ikas/bp-storefront\";\nimport { CopySVG } from \"../../../../sub-components/icons\";\nimport { getStatusColor } from \"../../../../utils/orderStatus\";\nimport { showToast } from \"../../../../utils/toast\";\n\ninterface Props {\n order: IkasOrder;\n orderNoLabel: string;\n orderStatusLabel: string;\n orderDateLabel: string;\n copiedText: string;\n}\n\nexport default function OrderHeader({\n order,\n orderNoLabel,\n orderStatusLabel,\n orderDateLabel,\n copiedText,\n}: Props) {\n const statusLabel = getIkasOrderPackageStatusTranslation(order);\n const statusColor = getStatusColor(order);\n const date = getIkasOrderFormattedOrderedAt(order);\n\n const handleCopy = async () => {\n if (order.orderNumber) {\n try {\n await navigator.clipboard.writeText(order.orderNumber);\n showToast(copiedText, \"success\");\n } catch {\n // Clipboard API may fail on non-HTTPS or without permissions\n }\n }\n };\n\n return (\n <div className=\"kombos-order-detail-header\">\n <div className=\"kombos-order-detail-header__info\">\n {order.orderNumber && (\n <div className=\"kombos-order-detail-header__row\">\n <span className=\"text-sm-regular kombos-order-detail-header__label\">\n {orderNoLabel}{\" \"}\n <span className=\"text-sm-medium\">{order.orderNumber}</span>\n </span>\n <button\n type=\"button\"\n className=\"kombos-order-detail-header__copy\"\n onClick={handleCopy}\n aria-label=\"Copy order number\"\n >\n <CopySVG />\n </button>\n </div>\n )}\n {statusLabel && (\n <p className=\"text-sm-regular kombos-order-detail-header__label\">\n {orderStatusLabel}{\" \"}\n <span\n className=\"text-sm-medium\"\n style={statusColor ? { color: statusColor } : undefined}\n >\n {statusLabel}\n </span>\n </p>\n )}\n {date && (\n <p className=\"text-sm-regular kombos-order-detail-header__label\">\n {orderDateLabel} {date}\n </p>\n )}\n </div>\n </div>\n );\n}\n"
16506
- },
16507
- {
16508
- "filename": "children/AccountOrderDetail/components/OrderHeader/styles.css",
16509
- "content": ".kombos-order-detail-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding-bottom: 1.5rem;\n border-bottom: 1px solid var(--kombos-gray-100);\n width: 100%;\n}\n\n.kombos-order-detail-header__info {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.kombos-order-detail-header__row {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n}\n\n.kombos-order-detail-header__label {\n color: var(--kombos-gray-900);\n}\n\n.kombos-order-detail-header__copy {\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n color: var(--kombos-gray-500);\n font-size: 1rem;\n transition: color 0.15s ease;\n}\n\n.kombos-order-detail-header__copy:hover {\n color: var(--kombos-gray-700);\n}\n\n.kombos-order-detail-header__copy:focus-visible {\n outline: 2px solid var(--kombos-gray-900);\n outline-offset: 2px;\n}\n"
16510
- },
16511
- {
16512
- "filename": "children/AccountOrderDetail/components/OrderItemRow/index.tsx",
16513
- "content": "import { IkasOrderLineItem } from \"@ikas/bp-storefront\";\nimport OrderLineItemDisplay from \"../OrderLineItemDisplay\";\n\ninterface Props {\n item: IkasOrderLineItem;\n quantityLabel: string;\n}\n\nexport default function OrderItemRow({ item, quantityLabel }: Props) {\n return (\n <div className=\"kombos-order-detail-item\">\n <div className=\"kombos-order-detail-item__row\">\n <OrderLineItemDisplay item={item} />\n <span className=\"kombos-order-detail-item__qty text-sm-regular\">\n {item.quantity} {quantityLabel}\n </span>\n </div>\n </div>\n );\n}\n"
16514
- },
16515
- {
16516
- "filename": "children/AccountOrderDetail/components/OrderItemRow/styles.css",
16517
- "content": ".kombos-order-detail-item {\n padding-bottom: 1.5rem;\n border-bottom: 1px solid var(--kombos-gray-100);\n}\n\n.kombos-order-detail-item__row {\n display: flex;\n align-items: center;\n gap: 1rem;\n flex: 1;\n}\n\n.kombos-order-detail-item__qty {\n color: var(--kombos-gray-900);\n white-space: nowrap;\n}\n"
16518
- },
16519
- {
16520
- "filename": "children/AccountOrderDetail/components/OrderLineItemDisplay/index.tsx",
16521
- "content": "import {\n IkasOrderLineItem,\n getIkasOrderLineVariantMainImage,\n getIkasOrderLineVariantHref,\n getThumbnailSrc,\n getDefaultSrc,\n getOrderLineItemFormattedFinalPriceWithQuantity,\n getOrderLineItemFormattedPriceWithQuantity,\n hasOrderLineItemDiscount,\n} from \"@ikas/bp-storefront\";\nimport { NoProductSVG } from \"../../../../sub-components/icons\";\n\ninterface Props {\n item: IkasOrderLineItem;\n}\n\nexport default function OrderLineItemDisplay({ item }: Props) {\n const image = item.variant\n ? getIkasOrderLineVariantMainImage(item.variant)\n : null;\n const imgSrc = image && !image.isVideo ? getThumbnailSrc(image) : null;\n const videoSrc = image?.isVideo ? getDefaultSrc(image) : null;\n const hasDiscount = hasOrderLineItemDiscount(item);\n const finalPrice = getOrderLineItemFormattedFinalPriceWithQuantity(item);\n const originalPrice = hasDiscount\n ? getOrderLineItemFormattedPriceWithQuantity(item)\n : null;\n\n const variantValues = item.variant?.variantValues ?? [];\n const href = item.variant\n ? getIkasOrderLineVariantHref(item.variant)\n : undefined;\n\n const ImageWrap = (\n <div className=\"kombos-order-detail-line__img-wrap\">\n {videoSrc ? (\n <video\n src={videoSrc}\n className=\"kombos-order-detail-line__video\"\n muted\n loop\n autoPlay\n playsInline\n >\n <track kind=\"captions\" />\n </video>\n ) : imgSrc ? (\n <img\n src={imgSrc}\n alt={item.variant?.name ?? \"\"}\n loading=\"lazy\"\n className=\"kombos-order-detail-line__img\"\n />\n ) : (\n <div className=\"kombos-order-detail-line__placeholder\">\n <NoProductSVG />\n </div>\n )}\n </div>\n );\n\n const Name = (\n <p className=\"kombos-order-detail-line__name text-sm-medium\">\n {item.variant?.name}\n </p>\n );\n\n return (\n <>\n {href ? (\n <a href={href} className=\"kombos-order-detail-line__img-link\">\n {ImageWrap}\n </a>\n ) : (\n ImageWrap\n )}\n <div className=\"kombos-order-detail-line__details\">\n {href ? (\n <a href={href} className=\"kombos-order-detail-line__name-link\">\n {Name}\n </a>\n ) : (\n Name\n )}\n {variantValues.length > 0 &&\n variantValues.map((v) => (\n <div\n key={v.variantTypeId}\n className=\"kombos-order-detail-line__variant\"\n >\n <span className=\"kombos-order-detail-line__variant-label text-sm-regular\">\n {v.variantTypeName}:\n </span>\n <span className=\"kombos-order-detail-line__variant-value text-sm-regular\">\n {v.variantValueName}\n </span>\n </div>\n ))}\n <div className=\"kombos-order-detail-line__prices\">\n <span className=\"kombos-order-detail-line__price text-sm-medium\">\n {finalPrice}\n </span>\n {originalPrice && (\n <span className=\"kombos-order-detail-line__price-old text-sm-regular-strike\">\n {originalPrice}\n </span>\n )}\n </div>\n </div>\n </>\n );\n}\n"
16522
- },
16523
- {
16524
- "filename": "children/AccountOrderDetail/components/OrderLineItemDisplay/styles.css",
16525
- "content": ".kombos-order-detail-line__img-wrap {\n width: 4.25rem;\n height: 4.25rem;\n min-width: 4.25rem;\n border-radius: 6px;\n border: 1px solid var(--kombos-gray-100);\n overflow: hidden;\n}\n\n.kombos-order-detail-line__img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n border-radius: 6px;\n}\n\n.kombos-order-detail-line__video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n border-radius: 6px;\n}\n\n.kombos-order-detail-line__placeholder {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--kombos-gray-100);\n color: var(--kombos-gray-300);\n font-size: 2rem;\n}\n\n.kombos-order-detail-line__details {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n flex: 1;\n}\n\n.kombos-order-detail-line__img-link {\n display: contents;\n}\n\n.kombos-order-detail-line__name-link {\n text-decoration: none;\n color: inherit;\n}\n\n.kombos-order-detail-line__name-link:hover .kombos-order-detail-line__name {\n text-decoration: underline;\n}\n\n.kombos-order-detail-line__name {\n color: var(--kombos-gray-900);\n width: fit-content;\n}\n\n.kombos-order-detail-line__variant {\n display: flex;\n gap: 0.375rem;\n}\n\n.kombos-order-detail-line__variant-label {\n color: var(--kombos-gray-500);\n}\n\n.kombos-order-detail-line__variant-value {\n color: var(--kombos-gray-900);\n}\n\n.kombos-order-detail-line__prices {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n}\n\n.kombos-order-detail-line__price {\n color: var(--kombos-gray-900);\n}\n\n.kombos-order-detail-line__price-old {\n color: var(--kombos-gray-500);\n text-decoration: line-through;\n}\n"
16526
- },
16527
- {
16528
- "filename": "children/AccountOrderDetail/components/OrderSidebar/index.tsx",
16529
- "content": "import {\n IkasOrder,\n IkasOrderAddress,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getIkasOrderFormattedShippingTotal,\n getOrderTransactionFormattedAmount,\n getOrderTransactionPaymentMethodTranslation,\n getIkasOrderDisplayedAdjustments,\n getOrderAdjustmentFormattedAmount,\n getOrderAdjustmentDisplayName,\n getOrderAddressText,\n} from \"@ikas/bp-storefront\";\nimport { getFullName } from \"../../../../utils/fullName\";\n\ninterface Props {\n order: IkasOrder;\n deliveryAddressLabel: string;\n billingAddressLabel: string;\n paymentInfoLabel: string;\n summaryLabel: string;\n subtotalLabel: string;\n shippingLabel: string;\n totalLabel: string;\n taxIncludedText: string;\n installmentText: string;\n singlePaymentText: string;\n}\n\nfunction AddressBlock({\n label,\n address,\n}: {\n label: string;\n address: IkasOrderAddress | null;\n}) {\n if (!address) return null;\n const fullName = getFullName(address.firstName, address.lastName);\n const addressText = getOrderAddressText(address);\n\n return (\n <div className=\"kombos-order-detail-sidebar__section\">\n <p className=\"kombos-order-detail-sidebar__section-title text-sm-medium\">\n {label}\n </p>\n <div className=\"kombos-order-detail-sidebar__address\">\n {fullName && (\n <p className=\"kombos-order-detail-sidebar__address-line text-sm-regular\">\n {fullName}\n </p>\n )}\n {addressText && (\n <p className=\"kombos-order-detail-sidebar__address-line text-sm-regular\">\n {addressText}\n </p>\n )}\n </div>\n </div>\n );\n}\n\nexport default function OrderSidebar({\n order,\n deliveryAddressLabel,\n billingAddressLabel,\n paymentInfoLabel,\n summaryLabel,\n subtotalLabel,\n shippingLabel,\n totalLabel,\n taxIncludedText,\n installmentText,\n singlePaymentText,\n}: Props) {\n const transactions = order.transactions ?? [];\n const totalFinalPrice = getIkasOrderFormattedTotalFinalPrice(order);\n const totalPrice = getIkasOrderFormattedTotalPrice(order);\n const shippingTotal = getIkasOrderFormattedShippingTotal(order);\n const adjustments = getIkasOrderDisplayedAdjustments(order);\n\n return (\n <div className=\"kombos-order-detail-sidebar\">\n <AddressBlock\n label={deliveryAddressLabel}\n address={order.shippingAddress}\n />\n\n <div className=\"kombos-order-detail-sidebar__divider\" />\n\n <AddressBlock\n label={billingAddressLabel}\n address={order.billingAddress}\n />\n\n <div className=\"kombos-order-detail-sidebar__divider\" />\n\n {transactions.length > 0 && (\n <>\n <div className=\"kombos-order-detail-sidebar__section\">\n <p className=\"kombos-order-detail-sidebar__section-title text-sm-medium\">\n {paymentInfoLabel}\n </p>\n <div className=\"kombos-order-detail-sidebar__payment\">\n {transactions.map((t) => {\n const methodLabel =\n getOrderTransactionPaymentMethodTranslation(t);\n const amount = getOrderTransactionFormattedAmount(t);\n const detail = t.paymentMethodDetail;\n const lastFour = detail?.lastFourDigits ?? \"\";\n const cardInfo = detail\n ? `${detail.cardAssociation ?? \"\"} **** **** ****${lastFour}`.trim()\n : null;\n const installmentInfo = detail?.installment?.installmentCount\n ? detail.installment.installmentCount > 1\n ? `${detail.installment.installmentCount} ${installmentText}`\n : singlePaymentText\n : null;\n\n return (\n <div\n key={t.id}\n className=\"kombos-order-detail-sidebar__payment-block\"\n >\n <div className=\"kombos-order-detail-sidebar__payment-row\">\n <span className=\"text-sm-regular\">{methodLabel}</span>\n <span className=\"text-sm-regular\">{amount}</span>\n </div>\n {(installmentInfo || cardInfo) && (\n <div className=\"kombos-order-detail-sidebar__payment-row\">\n {installmentInfo && (\n <span className=\"text-sm-regular\">\n {installmentInfo}\n </span>\n )}\n {cardInfo && (\n <span className=\"text-sm-regular\">{cardInfo}</span>\n )}\n </div>\n )}\n </div>\n );\n })}\n </div>\n </div>\n <div className=\"kombos-order-detail-sidebar__divider\" />\n </>\n )}\n\n <div className=\"kombos-order-detail-sidebar__section\">\n <p className=\"kombos-order-detail-sidebar__section-title text-sm-medium\">\n {summaryLabel}\n </p>\n <div className=\"kombos-order-detail-sidebar__summary\">\n <div className=\"kombos-order-detail-sidebar__summary-row\">\n <span className=\"kombos-order-detail-sidebar__summary-label text-sm-regular\">\n {subtotalLabel}\n </span>\n <span className=\"kombos-order-detail-sidebar__summary-value text-sm-regular\">\n {totalPrice}\n </span>\n </div>\n <div className=\"kombos-order-detail-sidebar__summary-row\">\n <span className=\"kombos-order-detail-sidebar__summary-label text-sm-regular\">\n {shippingLabel}\n </span>\n <span className=\"kombos-order-detail-sidebar__summary-value text-sm-regular\">\n {shippingTotal}\n </span>\n </div>\n {adjustments?.map((adj, idx) => (\n <div key={idx} className=\"kombos-order-detail-sidebar__summary-row\">\n <span className=\"kombos-order-detail-sidebar__summary-label text-sm-regular\">\n {getOrderAdjustmentDisplayName(adj)}\n </span>\n <span className=\"kombos-order-detail-sidebar__summary-value text-sm-regular\">\n {getOrderAdjustmentFormattedAmount(adj)}\n </span>\n </div>\n ))}\n <div className=\"kombos-order-detail-sidebar__summary-row kombos-order-detail-sidebar__summary-row--total\">\n <div className=\"kombos-order-detail-sidebar__total-label\">\n <span className=\"text-sm-medium\">{totalLabel}</span>\n <span className=\"kombos-order-detail-sidebar__tax-note text-sm-medium\">\n {taxIncludedText}\n </span>\n </div>\n <span className=\"kombos-order-detail-sidebar__summary-value text-sm-medium\">\n {totalFinalPrice}\n </span>\n </div>\n </div>\n </div>\n </div>\n );\n}\n"
16530
- },
16531
- {
16532
- "filename": "children/AccountOrderDetail/components/OrderSidebar/styles.css",
16533
- "content": ".kombos-order-detail-sidebar {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n width: 100%;\n}\n\n@media (min-width: 1024px) {\n .kombos-order-detail-sidebar {\n width: 22.25rem;\n min-width: 22.25rem;\n }\n}\n\n.kombos-order-detail-sidebar__divider {\n height: 1px;\n background: var(--kombos-gray-100);\n width: 100%;\n}\n\n.kombos-order-detail-sidebar__section {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.kombos-order-detail-sidebar__section-title {\n color: var(--kombos-gray-900);\n}\n\n.kombos-order-detail-sidebar__address {\n display: flex;\n flex-direction: column;\n gap: 0.375rem;\n}\n\n.kombos-order-detail-sidebar__address-line {\n color: var(--kombos-gray-900);\n}\n\n.kombos-order-detail-sidebar__payment {\n display: flex;\n flex-direction: column;\n gap: 0.375rem;\n}\n\n.kombos-order-detail-sidebar__payment-block {\n display: flex;\n flex-direction: column;\n gap: 0.375rem;\n}\n\n.kombos-order-detail-sidebar__payment-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n color: var(--kombos-gray-900);\n}\n\n.kombos-order-detail-sidebar__summary {\n display: flex;\n flex-direction: column;\n gap: 0.375rem;\n}\n\n.kombos-order-detail-sidebar__summary-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n.kombos-order-detail-sidebar__summary-label {\n color: var(--kombos-gray-900);\n}\n\n.kombos-order-detail-sidebar__summary-value {\n color: var(--kombos-gray-900);\n text-align: right;\n}\n\n.kombos-order-detail-sidebar__total-label {\n display: flex;\n align-items: baseline;\n gap: 0.375rem;\n}\n\n.kombos-order-detail-sidebar__total-label .text-sm-medium:first-child {\n color: var(--kombos-gray-900);\n}\n\n.kombos-order-detail-sidebar__tax-note {\n color: var(--kombos-gray-500);\n}\n"
16534
- },
16535
- {
16536
- "filename": "children/AccountOrderDetail/components/PackageGroup/index.tsx",
16537
- "content": "import { useState, useMemo } from \"preact/hooks\";\nimport { IkasDisplayedPackage, IkasOrderLineItem } from \"@ikas/bp-storefront\";\nimport { CaretUpSVG, CaretDownSVG } from \"../../../../sub-components/icons\";\nimport OrderItemRow from \"../OrderItemRow\";\n\ntype GroupedEntry =\n | {\n isBundleOrder: true;\n bundleLineId: string;\n bundleName?: string | null;\n items: IkasOrderLineItem[];\n }\n | { isBundleOrder: false; items: IkasOrderLineItem[] };\n\nfunction groupOrderLineItems(items: IkasOrderLineItem[]): GroupedEntry[] {\n const groups: GroupedEntry[] = [];\n\n items.forEach((item) => {\n const settings = item.bundleProductSettings;\n\n if (settings) {\n const existing = groups.find(\n (g): g is Extract<GroupedEntry, { isBundleOrder: true }> =>\n g.isBundleOrder && g.bundleLineId === settings.bundleLineId,\n );\n if (existing) {\n existing.items.push(item);\n } else {\n groups.push({\n isBundleOrder: true,\n bundleLineId: settings.bundleLineId,\n bundleName: settings.name,\n items: [item],\n });\n }\n } else {\n groups.push({ isBundleOrder: false, items: [item] });\n }\n });\n\n return groups;\n}\n\ninterface Props {\n pkg: IkasDisplayedPackage;\n quantityLabel: string;\n cargoCompanyLabel: string;\n trackingNumberLabel: string;\n}\n\nexport default function PackageGroup({\n pkg,\n quantityLabel,\n cargoCompanyLabel,\n trackingNumberLabel,\n}: Props) {\n const [open, setOpen] = useState(true);\n const itemCount = pkg.orderLineItems?.length ?? 0;\n const grouped = useMemo(\n () => groupOrderLineItems(pkg.orderLineItems ?? []),\n [pkg.orderLineItems],\n );\n\n return (\n <div className=\"kombos-order-detail-pkg\">\n <button\n type=\"button\"\n className=\"kombos-order-detail-pkg__header\"\n onClick={() => setOpen(!open)}\n aria-expanded={open}\n >\n <div className=\"kombos-order-detail-pkg__header-left\">\n <span className=\"kombos-order-detail-pkg__status text-sm-medium\">\n {pkg.statusTranslation}\n </span>\n <span className=\"kombos-order-detail-pkg__count text-sm-medium\">\n ({itemCount})\n </span>\n </div>\n <span className=\"kombos-order-detail-pkg__caret\">\n {open ? <CaretUpSVG /> : <CaretDownSVG />}\n </span>\n </button>\n\n {open && (\n <div className=\"kombos-order-detail-pkg__body\">\n <div className=\"kombos-order-detail-pkg__items\">\n {grouped.map((group, idx) =>\n group.isBundleOrder ? (\n <div\n key={group.bundleLineId}\n className=\"kombos-order-detail-pkg__bundle\"\n >\n {group.bundleName && (\n <span className=\"kombos-order-detail-pkg__bundle-name text-sm-medium\">\n {group.bundleName}\n </span>\n )}\n <div className=\"kombos-order-detail-pkg__bundle-items\">\n {group.items.map((item) => (\n <OrderItemRow\n key={item.id}\n item={item}\n quantityLabel={quantityLabel}\n />\n ))}\n </div>\n </div>\n ) : (\n group.items.map((item) => (\n <OrderItemRow\n key={item.id}\n item={item}\n quantityLabel={quantityLabel}\n />\n ))\n ),\n )}\n </div>\n\n {pkg.trackingInfo && (\n <div className=\"kombos-order-detail-pkg__tracking\">\n {pkg.trackingInfo.cargoCompany && (\n <p className=\"kombos-order-detail-pkg__tracking-line text-sm-regular\">\n <span>{cargoCompanyLabel} </span>\n <span className=\"text-sm-medium\">\n {pkg.trackingInfo.cargoCompany}\n </span>\n </p>\n )}\n {pkg.trackingInfo.trackingNumber && (\n <p className=\"kombos-order-detail-pkg__tracking-line text-sm-regular\">\n <span>{trackingNumberLabel} </span>\n {pkg.trackingInfo.trackingLink ? (\n <a\n href={pkg.trackingInfo.trackingLink}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"kombos-order-detail-pkg__tracking-link text-sm-medium\"\n >\n {pkg.trackingInfo.trackingNumber}\n </a>\n ) : (\n <span className=\"text-sm-medium\">\n {pkg.trackingInfo.trackingNumber}\n </span>\n )}\n </p>\n )}\n </div>\n )}\n </div>\n )}\n </div>\n );\n}\n"
16538
- },
16539
- {
16540
- "filename": "children/AccountOrderDetail/components/PackageGroup/styles.css",
16541
- "content": ".kombos-order-detail-pkg {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n width: 100%;\n}\n\n.kombos-order-detail-pkg__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--kombos-gray-100);\n border: none;\n border-radius: 6px;\n padding: 0.375rem 0.75rem;\n cursor: pointer;\n width: 100%;\n}\n\n.kombos-order-detail-pkg__header:focus-visible {\n outline: 2px solid var(--kombos-gray-900);\n outline-offset: 2px;\n}\n\n.kombos-order-detail-pkg__header-left {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.kombos-order-detail-pkg__status {\n color: var(--kombos-gray-900);\n}\n\n.kombos-order-detail-pkg__count {\n color: var(--kombos-gray-900);\n}\n\n.kombos-order-detail-pkg__caret {\n display: flex;\n align-items: center;\n color: var(--kombos-gray-900);\n font-size: 1rem;\n}\n\n.kombos-order-detail-pkg__body {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.kombos-order-detail-pkg__items {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.kombos-order-detail-pkg__tracking {\n display: flex;\n flex-direction: column;\n gap: 0.375rem;\n}\n\n.kombos-order-detail-pkg__tracking-line {\n color: var(--kombos-gray-900);\n}\n\n.kombos-order-detail-pkg__tracking-link {\n color: var(--kombos-gray-900);\n text-decoration: underline;\n}\n\n.kombos-order-detail-pkg__tracking-link:hover {\n color: var(--kombos-gray-700);\n}\n\n/* Bundle group */\n.kombos-order-detail-pkg__bundle {\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n padding: 1rem;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.kombos-order-detail-pkg__bundle-name {\n color: var(--kombos-gray-900);\n}\n\n.kombos-order-detail-pkg__bundle-items {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.kombos-order-detail-pkg__bundle-items .kombos-order-detail-item:last-child {\n padding-bottom: 0;\n border-bottom: none;\n}\n"
16542
- },
16543
- {
16544
- "filename": "children/AccountOrderDetail/components/ReturnItemRow/index.tsx",
16545
- "content": "import { IkasOrderLineItem } from \"@ikas/bp-storefront\";\nimport Checkbox from \"../../../../sub-components/Checkbox\";\nimport { MinusSVG, PlusSVG } from \"../../../../sub-components/icons\";\nimport OrderLineItemDisplay from \"../OrderLineItemDisplay\";\n\ninterface Props {\n item: IkasOrderLineItem;\n refundQty: number;\n onToggle: () => void;\n onDecrease: () => void;\n onIncrease: () => void;\n}\n\nexport default function ReturnItemRow({\n item,\n refundQty,\n onToggle,\n onDecrease,\n onIncrease,\n}: Props) {\n const selected = refundQty > 0;\n\n return (\n <div className=\"kombos-order-detail-return__item\">\n <div className=\"kombos-order-detail-return__item-row\">\n <Checkbox checked={selected} onChange={onToggle} />\n <OrderLineItemDisplay item={item} />\n {selected && item.quantity > 1 && (\n <div className=\"kombos-order-detail-return__qty\">\n <button\n type=\"button\"\n className=\"kombos-order-detail-return__qty-btn\"\n onClick={onDecrease}\n disabled={refundQty <= 1}\n aria-label=\"Decrease refund quantity\"\n >\n <MinusSVG />\n </button>\n <span className=\"kombos-order-detail-return__qty-value text-md-medium\">\n {refundQty}\n </span>\n <button\n type=\"button\"\n className=\"kombos-order-detail-return__qty-btn kombos-order-detail-return__qty-btn--plus\"\n onClick={onIncrease}\n disabled={refundQty >= item.quantity}\n aria-label=\"Increase refund quantity\"\n >\n <PlusSVG />\n </button>\n </div>\n )}\n </div>\n </div>\n );\n}\n"
16546
- },
16547
- {
16548
- "filename": "children/AccountOrderDetail/components/ReturnItemRow/styles.css",
16549
- "content": ".kombos-order-detail-return__item {\n padding-bottom: 1.5rem;\n border-bottom: 1px solid var(--kombos-gray-100);\n}\n\n.kombos-order-detail-return__item-row {\n display: flex;\n align-items: center;\n gap: 1.5rem;\n flex: 1;\n}\n\n/* Quantity selector for return */\n.kombos-order-detail-return__qty {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.kombos-order-detail-return__qty-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n background: var(--kombos-white);\n cursor: pointer;\n color: var(--kombos-gray-900);\n font-size: 1rem;\n padding: 0;\n}\n\n.kombos-order-detail-return__qty-btn--plus {\n background: var(--kombos-gray-50);\n}\n\n.kombos-order-detail-return__qty-btn:hover:not(:disabled) {\n border-color: var(--kombos-gray-300);\n}\n\n.kombos-order-detail-return__qty-btn:disabled {\n color: var(--kombos-gray-300);\n cursor: not-allowed;\n}\n\n.kombos-order-detail-return__qty-btn:focus-visible {\n outline: 2px solid var(--kombos-gray-900);\n outline-offset: 2px;\n}\n\n.kombos-order-detail-return__qty-value {\n min-width: 2rem;\n text-align: center;\n color: var(--kombos-gray-900);\n}\n"
16550
- },
16551
- {
16552
- "filename": "children/AccountOrderDetail/components/ReturnView/index.tsx",
16553
- "content": "import { useState } from \"preact/hooks\";\nimport {\n IkasOrder,\n IkasOrderLineItem,\n customerStore,\n refundOrder,\n Router,\n getIkasOrderRefundableItems,\n setOrderLineItemRefundQuantity,\n isIkasOrderRefundable,\n} from \"@ikas/bp-storefront\";\nimport Breadcrumb from \"../../../../sub-components/Breadcrumb\";\nimport Button from \"../../../../sub-components/Button\";\nimport { showToast } from \"../../../../utils/toast\";\nimport OrderHeader from \"../OrderHeader\";\nimport ReturnItemRow from \"../ReturnItemRow\";\n\ninterface Props {\n order: IkasOrder;\n breadcrumbOrdersLabel: string;\n breadcrumbOrderLabel: string;\n orderNoLabel: string;\n orderStatusLabel: string;\n orderDateLabel: string;\n returnRequestTitle: string;\n returnSubmitText: string;\n returningButtonText: string;\n returnSuccessText: string;\n returnErrorText: string;\n copiedText: string;\n onBack: () => void;\n onSuccess: () => void;\n}\n\nfunction ReturnView({\n order,\n breadcrumbOrdersLabel,\n breadcrumbOrderLabel,\n orderNoLabel,\n orderStatusLabel,\n orderDateLabel,\n returnRequestTitle,\n returnSubmitText,\n returningButtonText,\n returnSuccessText,\n returnErrorText,\n copiedText,\n onBack,\n onSuccess,\n}: Props) {\n const [submitting, setSubmitting] = useState(false);\n const [quantities, setQuantities] = useState<Record<string, number>>({});\n const refundableItems = getIkasOrderRefundableItems(order);\n\n const orderRefundable = isIkasOrderRefundable(order);\n\n const disabled = !orderRefundable || submitting;\n\n const updateQuantity = (item: IkasOrderLineItem, next: number) => {\n const current = quantities[item.id] ?? 0;\n if (next === current) return;\n setOrderLineItemRefundQuantity(next || null, item);\n if (next <= 0) {\n const { [item.id]: _, ...rest } = quantities;\n setQuantities(rest);\n } else {\n setQuantities({ ...quantities, [item.id]: next });\n }\n };\n\n const handleToggle = (item: IkasOrderLineItem) => {\n const current = quantities[item.id] ?? 0;\n updateQuantity(item, current > 0 ? 0 : 1);\n };\n\n const handleDecrease = (item: IkasOrderLineItem) => {\n const current = quantities[item.id] ?? 0;\n updateQuantity(item, Math.max(0, current - 1));\n };\n\n const handleIncrease = (item: IkasOrderLineItem) => {\n const current = quantities[item.id] ?? 0;\n updateQuantity(item, Math.min(item.quantity, current + 1));\n };\n\n const handleSubmit = async () => {\n if (disabled) return;\n setSubmitting(true);\n try {\n const success = await refundOrder(customerStore, order);\n if (success) {\n showToast(returnSuccessText, \"success\");\n onSuccess();\n } else {\n showToast(returnErrorText, \"error\");\n }\n } catch {\n showToast(returnErrorText, \"error\");\n } finally {\n setSubmitting(false);\n }\n };\n\n return (\n <div className=\"kombos-order-detail-return\">\n <Breadcrumb\n items={[\n {\n label: breadcrumbOrdersLabel,\n onClick: () => Router.navigateToPage(\"ORDERS\"),\n },\n {\n label: `${order.orderNumber} ${breadcrumbOrderLabel}`,\n onClick: onBack,\n },\n { label: returnRequestTitle },\n ]}\n />\n\n <div className=\"kombos-order-detail-return__content\">\n <OrderHeader\n order={order}\n orderNoLabel={orderNoLabel}\n orderStatusLabel={orderStatusLabel}\n orderDateLabel={orderDateLabel}\n copiedText={copiedText}\n />\n\n <div className=\"kombos-order-detail-return__items\">\n {refundableItems.map((item) => (\n <ReturnItemRow\n key={item.id}\n item={item}\n refundQty={quantities[item.id] ?? 0}\n onToggle={() => handleToggle(item)}\n onDecrease={() => handleDecrease(item)}\n onIncrease={() => handleIncrease(item)}\n />\n ))}\n </div>\n\n <div className=\"kombos-order-detail-return__footer\">\n <Button\n variant=\"primary\"\n size=\"s\"\n onClick={handleSubmit}\n loading={submitting}\n disabled={disabled}\n className=\"kombos-order-detail-return__submit-btn\"\n >\n {submitting ? returningButtonText : returnSubmitText}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n\nexport default ReturnView;\n"
16554
- },
16555
- {
16556
- "filename": "children/AccountOrderDetail/components/ReturnView/styles.css",
16557
- "content": ".kombos-order-detail-return {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n width: 100%;\n}\n\n\n.kombos-order-detail-return__content {\n display: flex;\n flex-direction: column;\n gap: 2rem;\n}\n\n.kombos-order-detail-return__items {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n/* Footer */\n.kombos-order-detail-return__footer {\n display: flex;\n justify-content: flex-end;\n}\n\n.kombos-order-detail-return__submit-btn {\n width: 100%;\n}\n"
16558
- },
16559
- {
16560
- "filename": "children/AccountOrderDetail/ikas-config-snippet.json",
16561
- "content": "{\n \"id\": \"{{PROJECT_ID}}-account-order-detail\",\n \"name\": \"AccountOrderDetail\",\n \"type\": \"component\",\n \"entry\": \"./src/components/AccountOrderDetail/index.tsx\",\n \"styles\": \"./src/components/AccountOrderDetail/styles.css\",\n \"props\": [\n {\n \"name\": \"breadcrumbOrdersLabel\",\n \"displayName\": \"Breadcrumb Orders Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"My Orders\",\n \"groupId\": \"navigation\"\n },\n {\n \"name\": \"breadcrumbOrderLabel\",\n \"displayName\": \"Breadcrumb Order Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Nolu Order\",\n \"groupId\": \"navigation\"\n },\n {\n \"name\": \"orderNoLabel\",\n \"displayName\": \"Order Number Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Order No:\",\n \"groupId\": \"orderInfo\"\n },\n {\n \"name\": \"orderStatusLabel\",\n \"displayName\": \"Order Status Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Order Durumu:\",\n \"groupId\": \"orderInfo\"\n },\n {\n \"name\": \"orderDateLabel\",\n \"displayName\": \"Order Date Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Order Tarihi:\",\n \"groupId\": \"orderInfo\"\n },\n {\n \"name\": \"returnButtonText\",\n \"displayName\": \"Return Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Return Et\",\n \"groupId\": \"returnRequest\"\n },\n {\n \"name\": \"returningButtonText\",\n \"displayName\": \"Return Processing Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Return talebi gönderiliyor...\",\n \"groupId\": \"returnRequest\"\n },\n {\n \"name\": \"returnRequestTitle\",\n \"displayName\": \"Return Request Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Return Request\",\n \"groupId\": \"returnRequest\"\n },\n {\n \"name\": \"returnSubmitText\",\n \"displayName\": \"Return Submit Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Return Et\",\n \"groupId\": \"returnRequest\"\n },\n {\n \"name\": \"deliveryAddressLabel\",\n \"displayName\": \"Delivery Adresi Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Delivery Adresi\",\n \"groupId\": \"addressPayment\"\n },\n {\n \"name\": \"billingAddressLabel\",\n \"displayName\": \"Invoice Adresi Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Invoice Adresi\",\n \"groupId\": \"addressPayment\"\n },\n {\n \"name\": \"paymentInfoLabel\",\n \"displayName\": \"Payment Bilgisi Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Payment Info\",\n \"groupId\": \"addressPayment\"\n },\n {\n \"name\": \"summaryLabel\",\n \"displayName\": \"Summary Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Summary\",\n \"groupId\": \"summary\"\n },\n {\n \"name\": \"subtotalLabel\",\n \"displayName\": \"Subtotal Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Subtotal\",\n \"groupId\": \"summary\"\n },\n {\n \"name\": \"shippingLabel\",\n \"displayName\": \"Shipping Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Shipping\",\n \"groupId\": \"summary\"\n },\n {\n \"name\": \"totalLabel\",\n \"displayName\": \"Total Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Total\",\n \"groupId\": \"summary\"\n },\n {\n \"name\": \"taxIncludedText\",\n \"displayName\": \"Tax Included Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"*vergiler dahil\",\n \"groupId\": \"summary\"\n },\n {\n \"name\": \"cargoCompanyLabel\",\n \"displayName\": \"Shipping Company Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Shipping Şirketi:\",\n \"groupId\": \"shipping\"\n },\n {\n \"name\": \"trackingNumberLabel\",\n \"displayName\": \"Tracking Number Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Tracking Numarası:\",\n \"groupId\": \"shipping\"\n },\n {\n \"name\": \"quantityLabel\",\n \"displayName\": \"Quantity Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"adet\",\n \"groupId\": \"orderInfo\"\n },\n {\n \"name\": \"errorText\",\n \"displayName\": \"Error Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Order bulunamadı.\",\n \"groupId\": \"states\"\n },\n {\n \"name\": \"backToOrdersText\",\n \"displayName\": \"Siparişlere Return Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Siparişlerime Return\",\n \"groupId\": \"navigation\"\n },\n {\n \"name\": \"returnSuccessText\",\n \"displayName\": \"Return Success Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Return talebiniz başarıyla oluşturuldu.\",\n \"groupId\": \"returnRequest\"\n },\n {\n \"name\": \"returnErrorText\",\n \"displayName\": \"Return Error Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Return talebi oluşturulurken bir hata oluştu.\",\n \"groupId\": \"returnRequest\"\n },\n {\n \"name\": \"copiedText\",\n \"displayName\": \"Copied Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Kopyalandı!\",\n \"groupId\": \"shipping\"\n },\n {\n \"name\": \"installmentText\",\n \"displayName\": \"Installment Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Installment\",\n \"groupId\": \"addressPayment\"\n },\n {\n \"name\": \"singlePaymentText\",\n \"displayName\": \"Tek Çekim Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Tek Çekim\",\n \"groupId\": \"addressPayment\"\n },\n {\n \"name\": \"downloadSectionTitle\",\n \"displayName\": \"Downloadable Files Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Downloadable Files\",\n \"groupId\": \"downloads\"\n },\n {\n \"name\": \"downloadButtonText\",\n \"displayName\": \"Download Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Download\",\n \"groupId\": \"downloads\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"orderInfo\",\n \"name\": \"Order Info\",\n \"description\": \"Order detay başlığında gösterilen etiketler (sipariş no, durum, tarih)\"\n },\n {\n \"id\": \"navigation\",\n \"name\": \"Navigation\",\n \"description\": \"Breadcrumb ve geri dönüş butonundaki metinler\"\n },\n {\n \"id\": \"addressPayment\",\n \"name\": \"Address & Payment\",\n \"description\": \"Delivery adresi, fatura adresi ve ödeme bilgileri başlıkları\"\n },\n {\n \"id\": \"summary\",\n \"name\": \"Order Summary\",\n \"description\": \"Search toplam, kargo, toplam gibi özet satır etiketleri\"\n },\n {\n \"id\": \"shipping\",\n \"name\": \"Shipping Info\",\n \"description\": \"Paket kargo şirketi, takip numarası ve kopyalama geri bildirimi\"\n },\n {\n \"id\": \"returnRequest\",\n \"name\": \"Return Request\",\n \"description\": \"Return ekranındaki başlık, buton ve bildirim metinleri\"\n },\n {\n \"id\": \"states\",\n \"name\": \"Status Messages\",\n \"description\": \"Upload ve hata durumlarında gösterilen metinler\"\n },\n {\n \"id\": \"downloads\",\n \"name\": \"Downloadable Files\",\n \"description\": \"Dijital ürün indirme bölümündeki başlık ve buton metinleri\"\n }\n ]\n}"
16562
- },
16563
- {
16564
- "filename": "children/AccountOrderDetail/index.tsx",
16565
- "content": "import { useEffect, useState } from \"preact/hooks\";\nimport {\n customerStore,\n IkasOrder,\n IkasProductFile,\n IkasOrderTransaction,\n getOrderDetailsOfPage,\n getIkasOrderDisplayedPackages,\n isIkasOrderRefundable,\n getIkasOrderProductFiles,\n getOrderProductFiles,\n getOrderTransactions,\n getDigitalProductFileDownloadUrl,\n Router,\n getIkasOrderRefundableItems,\n} from \"@ikas/bp-storefront\";\nimport PageLoader from \"../../sub-components/PageLoader\";\nimport Button from \"../../sub-components/Button\";\nimport Breadcrumb from \"../../sub-components/Breadcrumb\";\nimport { DownloadSVG } from \"../../sub-components/icons\";\nimport OrderHeader from \"./components/OrderHeader\";\nimport PackageGroup from \"./components/PackageGroup\";\nimport OrderSidebar from \"./components/OrderSidebar\";\nimport ReturnView from \"./components/ReturnView\";\nimport { Props } from \"./types\";\n\nexport function AccountOrderDetail({\n breadcrumbOrdersLabel = \"Siparişlerim\",\n breadcrumbOrderLabel = \"Nolu Sipariş\",\n orderNoLabel = \"Sipariş No:\",\n orderStatusLabel = \"Sipariş Durumu:\",\n orderDateLabel = \"Sipariş Tarihi:\",\n returnButtonText = \"İade Et\",\n returningButtonText = \"İade talebi gönderiliyor...\",\n returnRequestTitle = \"İade Talebi\",\n returnSubmitText = \"İade Et\",\n deliveryAddressLabel = \"Teslimat Adresi\",\n billingAddressLabel = \"Fatura Adresi\",\n paymentInfoLabel = \"Ödeme Bilgileri\",\n summaryLabel = \"Özet\",\n subtotalLabel = \"Ara Toplam\",\n shippingLabel = \"Kargo\",\n totalLabel = \"Toplam\",\n taxIncludedText = \"*vergiler dahil\",\n installmentText = \"Taksit\",\n singlePaymentText = \"Tek Çekim\",\n cargoCompanyLabel = \"Kargo Şirketi:\",\n trackingNumberLabel = \"Takip Numarası:\",\n quantityLabel = \"adet\",\n errorText = \"Sipariş bulunamadı.\",\n backToOrdersText = \"Siparişlerime Dön\",\n returnSuccessText = \"İade talebiniz başarıyla oluşturuldu.\",\n returnErrorText = \"İade talebi oluşturulurken bir hata oluştu.\",\n copiedText = \"Kopyalandı!\",\n downloadSectionTitle = \"İndirilebilir Dosyalar\",\n downloadButtonText = \"İndir\",\n}: Props) {\n const [order, setOrder] = useState<IkasOrder | null>(null);\n const [loading, setLoading] = useState(true);\n const [view, setView] = useState<\"detail\" | \"return\">(\"detail\");\n const [files, setFiles] = useState<IkasProductFile[]>([]);\n const [transactions, setTransactions] = useState<IkasOrderTransaction[]>([]);\n useEffect(() => {\n let cancelled = false;\n (async () => {\n try {\n const result = await getOrderDetailsOfPage(customerStore);\n if (!cancelled && result) {\n setOrder(result);\n const fileIds = getIkasOrderProductFiles(result);\n if (fileIds.length > 0) {\n const [productFiles, txns] = await Promise.all([\n getOrderProductFiles(customerStore, fileIds),\n getOrderTransactions(customerStore, { orderId: result.id }),\n ]);\n if (!cancelled) {\n setFiles(productFiles);\n setTransactions(txns);\n }\n }\n }\n } finally {\n if (!cancelled) setLoading(false);\n }\n })();\n return () => {\n cancelled = true;\n };\n }, []);\n\n if (loading) {\n return <PageLoader />;\n }\n\n if (!order) {\n return (\n <div className=\"kombos-order-detail\">\n <div className=\"kombos-order-detail__empty\">\n <p className=\"kombos-order-detail__empty-text text-sm-regular\">\n {errorText}\n </p>\n <Button\n variant=\"secondary\"\n size=\"s\"\n onClick={() => Router.navigateToPage(\"ORDERS\")}\n >\n {backToOrdersText}\n </Button>\n </div>\n </div>\n );\n }\n\n if (view === \"return\") {\n return (\n <div className=\"kombos-order-detail\">\n <ReturnView\n order={order}\n breadcrumbOrdersLabel={breadcrumbOrdersLabel}\n breadcrumbOrderLabel={breadcrumbOrderLabel}\n orderNoLabel={orderNoLabel}\n orderStatusLabel={orderStatusLabel}\n orderDateLabel={orderDateLabel}\n returnRequestTitle={returnRequestTitle}\n returnSubmitText={returnSubmitText}\n returningButtonText={returningButtonText}\n returnSuccessText={returnSuccessText}\n returnErrorText={returnErrorText}\n copiedText={copiedText}\n onBack={() => setView(\"detail\")}\n onSuccess={() => setView(\"detail\")}\n />\n </div>\n );\n }\n\n const packages = getIkasOrderDisplayedPackages(order);\n const canRefund = getIkasOrderRefundableItems(order).length > 0;\n // const canRefund = isIkasOrderRefundable(order);\n const isDownloadable =\n transactions.length > 0 &&\n transactions.every((t) => t.status === \"SUCCESS\");\n\n return (\n <div className=\"kombos-order-detail\">\n <Breadcrumb\n items={[\n {\n label: breadcrumbOrdersLabel,\n onClick: () => Router.navigateToPage(\"ORDERS\"),\n },\n { label: `${order.orderNumber} ${breadcrumbOrderLabel}` },\n ]}\n />\n\n <div className=\"kombos-order-detail__content\">\n <div className=\"kombos-order-detail__header-row\">\n <OrderHeader\n order={order}\n orderNoLabel={orderNoLabel}\n orderStatusLabel={orderStatusLabel}\n orderDateLabel={orderDateLabel}\n copiedText={copiedText}\n />\n {canRefund && (\n <Button\n variant=\"secondary\"\n size=\"s\"\n onClick={() => setView(\"return\")}\n className=\"kombos-order-detail__return-btn\"\n >\n {returnButtonText}\n </Button>\n )}\n </div>\n\n <div className=\"kombos-order-detail__body\">\n <div className=\"kombos-order-detail__packages\">\n {files.length > 0 && (\n <div className=\"kombos-order-detail__downloads\">\n <h3 className=\"kombos-order-detail__downloads-title text-md-semibold\">\n {downloadSectionTitle}\n </h3>\n <div className=\"kombos-order-detail__downloads-list\">\n {files.map((file) => (\n <div\n key={file.id}\n className=\"kombos-order-detail__download-item\"\n >\n <span className=\"kombos-order-detail__download-name text-sm-regular\">\n {file.name}\n </span>\n <Button\n variant=\"secondary\"\n size=\"xs\"\n icon={<DownloadSVG />}\n className=\"kombos-order-detail__download-btn\"\n disabled={!isDownloadable}\n onClick={() =>\n getDigitalProductFileDownloadUrl(\n customerStore,\n order,\n file,\n )\n }\n >\n {downloadButtonText}\n </Button>\n </div>\n ))}\n </div>\n </div>\n )}\n {packages.map((pkg) => (\n <PackageGroup\n key={pkg.id}\n pkg={pkg}\n quantityLabel={quantityLabel}\n cargoCompanyLabel={cargoCompanyLabel}\n trackingNumberLabel={trackingNumberLabel}\n />\n ))}\n </div>\n\n <OrderSidebar\n order={order}\n deliveryAddressLabel={deliveryAddressLabel}\n billingAddressLabel={billingAddressLabel}\n paymentInfoLabel={paymentInfoLabel}\n summaryLabel={summaryLabel}\n subtotalLabel={subtotalLabel}\n shippingLabel={shippingLabel}\n totalLabel={totalLabel}\n taxIncludedText={taxIncludedText}\n installmentText={installmentText}\n singlePaymentText={singlePaymentText}\n />\n </div>\n </div>\n </div>\n );\n}\n\nexport default AccountOrderDetail;\n"
16566
- },
16567
- {
16568
- "filename": "children/AccountOrderDetail/styles.css",
16569
- "content": "/* AccountOrderDetail — Root (component, not section) */\n.kombos-order-detail {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n width: 100%;\n}\n\n/* Empty / Error */\n.kombos-order-detail__empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1rem;\n padding: 4rem 0;\n}\n\n.kombos-order-detail__empty-text {\n color: var(--kombos-gray-500);\n}\n\n/* Content */\n.kombos-order-detail__content {\n display: flex;\n flex-direction: column;\n gap: 2rem;\n}\n\n/* Header row (order info + return button) */\n.kombos-order-detail__header-row {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.kombos-order-detail__return-btn {\n align-self: flex-start;\n}\n\n@media (min-width: 768px) {\n .kombos-order-detail__header-row {\n flex-direction: row;\n align-items: flex-start;\n justify-content: space-between;\n }\n\n .kombos-order-detail__header-row .kombos-order-detail-header {\n border-bottom: none;\n padding-bottom: 0;\n }\n\n .kombos-order-detail__return-btn {\n align-self: auto;\n flex-shrink: 0;\n }\n}\n\n/* Body (packages + sidebar) */\n.kombos-order-detail__body {\n display: flex;\n flex-direction: column;\n gap: 2.5rem;\n}\n\n.kombos-order-detail__packages {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n flex: 1;\n}\n\n/* Digital Downloads */\n.kombos-order-detail__downloads {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n padding: 1rem;\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n}\n\n.kombos-order-detail__downloads-title {\n color: var(--kombos-gray-900);\n}\n\n.kombos-order-detail__downloads-list {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.kombos-order-detail__download-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 0.5rem 0;\n border-bottom: 1px solid var(--kombos-gray-100);\n}\n\n.kombos-order-detail__download-item:last-child {\n border-bottom: none;\n padding-bottom: 0;\n}\n\n.kombos-order-detail__download-name {\n color: var(--kombos-gray-700);\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.kombos-order-detail__download-btn {\n flex-shrink: 0;\n}\n\n@media (min-width: 1024px) {\n .kombos-order-detail__body {\n flex-direction: row;\n gap: 6rem;\n }\n}\n"
16570
- },
16571
- {
16572
- "filename": "children/AccountOrderDetail/types.ts",
16573
- "content": "export interface Props {\n breadcrumbOrdersLabel?: string;\n breadcrumbOrderLabel?: string;\n orderNoLabel?: string;\n orderStatusLabel?: string;\n orderDateLabel?: string;\n returnButtonText?: string;\n returningButtonText?: string;\n returnRequestTitle?: string;\n returnSubmitText?: string;\n deliveryAddressLabel?: string;\n billingAddressLabel?: string;\n paymentInfoLabel?: string;\n summaryLabel?: string;\n subtotalLabel?: string;\n shippingLabel?: string;\n totalLabel?: string;\n taxIncludedText?: string;\n cargoCompanyLabel?: string;\n trackingNumberLabel?: string;\n quantityLabel?: string;\n errorText?: string;\n backToOrdersText?: string;\n returnSuccessText?: string;\n returnErrorText?: string;\n copiedText?: string;\n installmentText?: string;\n singlePaymentText?: string;\n downloadSectionTitle?: string;\n downloadButtonText?: string;\n}\n"
16574
- },
16575
- {
16576
- "filename": "children/AccountOrders/components/OrderCard/index.tsx",
16577
- "content": "import {\n IkasOrder,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderDistinctItemCount,\n getIkasOrderFormattedOrderedAt,\n getIkasOrderPackageStatusTranslation,\n getIkasOrderHref,\n getIkasOrderLineVariantMainImage,\n getThumbnailSrc,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport Button from \"../../../../sub-components/Button\";\nimport { NoProductSVG } from \"../../../../sub-components/icons\";\nimport { getStatusColor } from \"../../../../utils/orderStatus\";\n\nconst MAX_THUMBS = 4;\n\ninterface Props {\n order: IkasOrder;\n detailButtonText: string;\n orderNoText: string;\n itemsText: string;\n}\n\nexport default function OrderCard({\n order,\n detailButtonText,\n orderNoText,\n itemsText,\n}: Props) {\n const date = getIkasOrderFormattedOrderedAt(order);\n const statusLabel = getIkasOrderPackageStatusTranslation(order);\n const statusColor = getStatusColor(order);\n const itemCount = getIkasOrderDistinctItemCount(order);\n const totalPrice = getIkasOrderFormattedTotalFinalPrice(order);\n const href = getIkasOrderHref(order);\n\n const thumbs = (order.orderLineItems ?? [])\n .map((item) => {\n const image = getIkasOrderLineVariantMainImage(item.variant);\n return {\n id: item.id,\n src: image && !image.isVideo ? getThumbnailSrc(image) : null,\n videoSrc: image?.isVideo ? getDefaultSrc(image) : null,\n isVideo: image?.isVideo ?? false,\n alt: item.variant?.name ?? \"\",\n };\n })\n .slice(0, MAX_THUMBS);\n\n return (\n <div className=\"kombos-account-orders__card\">\n <div className=\"kombos-account-orders__card-info\">\n <div className=\"kombos-account-orders__card-meta\">\n {date && (\n <span className=\"kombos-account-orders__card-date text-sm-regular\">\n {date}\n </span>\n )}\n {statusLabel && (\n <span\n className=\"kombos-account-orders__card-status text-sm-medium\"\n style={statusColor ? { color: statusColor } : undefined}\n >\n {statusLabel}\n </span>\n )}\n {order.orderNumber && (\n <span className=\"kombos-account-orders__card-order-no text-sm-regular\">\n {orderNoText}{\" \"}\n <span className=\"text-sm-medium\">{order.orderNumber}</span>\n </span>\n )}\n <span className=\"kombos-account-orders__card-summary text-sm-regular\">\n <span className=\"text-sm-medium\">{itemCount}</span> {itemsText}\n {\" - \"}\n <span className=\"text-sm-medium\">{totalPrice}</span>\n </span>\n </div>\n\n {thumbs.length > 0 && (\n <div className=\"kombos-account-orders__card-thumbs\">\n {thumbs.map((t) => (\n <div key={t.id} className=\"kombos-account-orders__thumb\">\n {t.videoSrc ? (\n <video\n src={t.videoSrc}\n className=\"kombos-account-orders__thumb-video\"\n muted\n loop\n autoPlay\n playsInline\n >\n <track kind=\"captions\" />\n </video>\n ) : t.src ? (\n <img src={t.src} alt={t.alt} loading=\"lazy\" />\n ) : (\n <div className=\"kombos-account-orders__thumb-placeholder\">\n <NoProductSVG />\n </div>\n )}\n </div>\n ))}\n </div>\n )}\n </div>\n\n <div className=\"kombos-account-orders__card-action\">\n <a\n href={href}\n className=\"kombos-account-orders__detail-link\"\n aria-label={`${detailButtonText} - ${orderNoText} ${order.orderNumber}`}\n >\n <Button variant=\"secondary\" size=\"s\" tabIndex={-1} aria-hidden=\"true\">\n {detailButtonText}\n </Button>\n </a>\n </div>\n </div>\n );\n}\n"
16578
- },
16579
- {
16580
- "filename": "children/AccountOrders/components/OrderCard/styles.css",
16581
- "content": "/* ── Order Card ── */\n\n.kombos-account-orders__card {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n border-top: 1px solid var(--kombos-gray-200);\n border-bottom: 1px solid var(--kombos-gray-200);\n padding: 1.5rem 0;\n}\n\n.kombos-account-orders__card + .kombos-account-orders__card {\n border-top: none;\n}\n\n.kombos-account-orders__card-info {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.kombos-account-orders__card-meta {\n display: flex;\n flex-direction: column;\n gap: 0.375rem;\n}\n\n.kombos-account-orders__card-date {\n color: var(--kombos-gray-900);\n}\n\n.kombos-account-orders__card-status {\n color: var(--kombos-gray-700);\n}\n\n.kombos-account-orders__card-order-no {\n color: var(--kombos-gray-900);\n}\n\n.kombos-account-orders__card-summary {\n color: var(--kombos-gray-900);\n}\n\n/* ── Thumbnails ── */\n\n.kombos-account-orders__card-thumbs {\n display: flex;\n flex-direction: row;\n gap: 0.5rem;\n}\n\n.kombos-account-orders__thumb {\n width: 3.5rem;\n height: 3.5rem;\n border-radius: 4px;\n overflow: hidden;\n flex-shrink: 0;\n}\n\n.kombos-account-orders__thumb img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.kombos-account-orders__thumb-video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.kombos-account-orders__thumb-placeholder {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--kombos-gray-100);\n color: var(--kombos-gray-300);\n font-size: 1.5rem;\n}\n\n/* ── Card Action ── */\n\n.kombos-account-orders__card-action {\n flex-shrink: 0;\n}\n\n.kombos-account-orders__detail-link {\n text-decoration: none;\n display: block;\n}\n\n.kombos-account-orders__detail-link button {\n width: 100%;\n}\n\n/* ── Responsive: Tablet+ ── */\n\n@media (min-width: 768px) {\n .kombos-account-orders__card {\n flex-direction: row;\n justify-content: space-between;\n }\n\n .kombos-account-orders__thumb {\n width: 4.5rem;\n height: 4.5rem;\n }\n\n .kombos-account-orders__detail-link button {\n width: auto;\n }\n}\n"
16582
- },
16583
- {
16584
- "filename": "children/AccountOrders/ikas-config-snippet.json",
16585
- "content": "{\n \"id\": \"{{PROJECT_ID}}-account-orders\",\n \"name\": \"AccountOrders\",\n \"type\": \"component\",\n \"entry\": \"./src/components/AccountOrders/index.tsx\",\n \"styles\": \"./src/components/AccountOrders/styles.css\",\n \"props\": [\n {\n \"name\": \"ordersLabel\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"My Orders\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"emptyText\",\n \"displayName\": \"Empty State Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Henüz siparişiniz bulunmuyor.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"detailButtonText\",\n \"displayName\": \"Detail Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Detaya Git\",\n \"groupId\": \"cardTexts\"\n },\n {\n \"name\": \"orderNoText\",\n \"displayName\": \"Order Number Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Order No:\",\n \"groupId\": \"cardTexts\"\n },\n {\n \"name\": \"itemsText\",\n \"displayName\": \"Product Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Product\",\n \"groupId\": \"cardTexts\"\n },\n {\n \"name\": \"shopButtonText\",\n \"displayName\": \"Shopping Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Start Shopping\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"errorText\",\n \"displayName\": \"Error Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Orders yüklenirken bir hata oluştu.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"retryButtonText\",\n \"displayName\": \"Again Dene Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Try Again\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"General Texts\",\n \"description\": \"Page başlığı, yükleme ve boş durum mesajları\"\n },\n {\n \"id\": \"cardTexts\",\n \"name\": \"Order Kartı Texts\",\n \"description\": \"Her sipariş kartında görünen etiket ve buton metinleri\"\n }\n ]\n}"
16586
- },
16587
- {
16588
- "filename": "children/AccountOrders/index.tsx",
16589
- "content": "import { useCallback, useEffect, useRef, useState } from \"preact/hooks\";\nimport {\n customerStore,\n getOrders,\n IkasOrder,\n Router,\n} from \"@ikas/bp-storefront\";\nimport Button from \"../../sub-components/Button\";\nimport PageLoader from \"../../sub-components/PageLoader\";\nimport OrderCard from \"./components/OrderCard\";\nimport { Props } from \"./types\";\n\nexport function AccountOrders({\n ordersLabel = \"Siparişlerim\",\n emptyText = \"Henüz siparişiniz bulunmuyor.\",\n detailButtonText = \"Detaya Git\",\n orderNoText = \"Sipariş No:\",\n itemsText = \"Ürün\",\n shopButtonText = \"Alışverişe Başla\",\n errorText = \"Siparişler yüklenirken bir hata oluştu.\",\n retryButtonText = \"Tekrar Dene\",\n}: Props) {\n const [orders, setOrders] = useState<IkasOrder[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState(false);\n const controllerRef = useRef<AbortController | null>(null);\n\n const fetchOrders = useCallback(() => {\n controllerRef.current?.abort();\n const controller = new AbortController();\n controllerRef.current = controller;\n const { signal } = controller;\n\n setLoading(true);\n setError(false);\n\n (async () => {\n try {\n const result = await getOrders(customerStore);\n if (signal.aborted) return;\n setOrders(result);\n } catch {\n if (signal.aborted) return;\n setError(true);\n } finally {\n if (!signal.aborted) setLoading(false);\n }\n })();\n }, []);\n\n useEffect(() => {\n fetchOrders();\n return () => controllerRef.current?.abort();\n }, [fetchOrders]);\n\n const hasOrders = orders.length > 0;\n\n return (\n <div className=\"kombos-account-orders\">\n <h1 className=\"kombos-account-orders__title text-md-medium\">\n {`${ordersLabel}${hasOrders ? ` (${orders.length})` : \"\"}`}\n </h1>\n\n {loading ? (\n <PageLoader />\n ) : error ? (\n <div className=\"kombos-account-orders__error\">\n <p className=\"kombos-account-orders__error-text text-sm-regular\">\n {errorText}\n </p>\n <Button variant=\"primary\" size=\"s\" onClick={fetchOrders}>\n {retryButtonText}\n </Button>\n </div>\n ) : !hasOrders ? (\n <div className=\"kombos-account-orders__empty\">\n <p className=\"kombos-account-orders__empty-text text-sm-regular\">\n {emptyText}\n </p>\n <Button\n variant=\"primary\"\n size=\"s\"\n onClick={() => Router.navigate(\"/\")}\n >\n {shopButtonText}\n </Button>\n </div>\n ) : (\n <div className=\"kombos-account-orders__list\">\n {orders.map((order) => (\n <OrderCard\n key={order.id}\n order={order}\n detailButtonText={detailButtonText}\n orderNoText={orderNoText}\n itemsText={itemsText}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport default AccountOrders;\n"
16590
- },
16591
- {
16592
- "filename": "children/AccountOrders/styles.css",
16593
- "content": "/* ── Account Orders ── */\n\n.kombos-account-orders {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.kombos-account-orders__title {\n color: var(--kombos-gray-900);\n}\n\n/* ── Empty / Error State ── */\n\n.kombos-account-orders__empty,\n.kombos-account-orders__error {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1rem;\n padding: 3rem 0;\n}\n\n.kombos-account-orders__empty-text,\n.kombos-account-orders__error-text {\n color: var(--kombos-gray-500);\n text-align: center;\n}\n\n/* ── Order List ── */\n\n.kombos-account-orders__list {\n display: flex;\n flex-direction: column;\n width: 100%;\n}\n"
16594
- },
16595
- {
16596
- "filename": "children/AccountOrders/types.ts",
16597
- "content": "export interface Props {\n ordersLabel?: string;\n emptyText?: string;\n detailButtonText?: string;\n orderNoText?: string;\n itemsText?: string;\n shopButtonText?: string;\n errorText?: string;\n retryButtonText?: string;\n}\n"
16598
- },
16599
- {
16600
- "filename": "components/AccountSidebar/index.tsx",
16601
- "content": "import { useState, useRef, useEffect } from \"preact/hooks\";\nimport type { FunctionComponent } from \"preact\";\nimport { cx } from \"../../../../utils/cx\";\nimport {\n User1SVG,\n Package1SVG,\n MapPin1SVG,\n Star1SVG,\n SignOut1SVG,\n CaretDownSVG,\n CheckSVG,\n} from \"../../../../sub-components/icons\";\n\nexport type NavItem =\n | \"account\"\n | \"orders\"\n | \"addresses\"\n | \"favorites\"\n | \"logout\";\n\ninterface NavItemConfig {\n id: NavItem;\n labelKey: keyof Labels;\n icon: FunctionComponent<{ className?: string }>;\n}\n\ntype Labels = Pick<\n Props,\n | \"accountInfoLabel\"\n | \"ordersLabel\"\n | \"addressesLabel\"\n | \"favoritesLabel\"\n | \"logoutLabel\"\n>;\n\nconst NAV_ITEMS: NavItemConfig[] = [\n { id: \"account\", labelKey: \"accountInfoLabel\", icon: User1SVG },\n { id: \"orders\", labelKey: \"ordersLabel\", icon: Package1SVG },\n { id: \"addresses\", labelKey: \"addressesLabel\", icon: MapPin1SVG },\n { id: \"favorites\", labelKey: \"favoritesLabel\", icon: Star1SVG },\n { id: \"logout\", labelKey: \"logoutLabel\", icon: SignOut1SVG },\n];\n\ninterface Props {\n activeItem: NavItem;\n onNavigate: (item: NavItem) => void;\n accountInfoLabel: string;\n ordersLabel: string;\n addressesLabel: string;\n favoritesLabel: string;\n logoutLabel: string;\n}\n\nexport default function AccountSidebar({\n activeItem,\n onNavigate,\n ...labels\n}: Props) {\n const [dropdownOpen, setDropdownOpen] = useState(false);\n const dropdownRef = useRef<HTMLDivElement>(null);\n\n const activeConfig = NAV_ITEMS.find((item) => item.id === activeItem);\n\n useEffect(() => {\n function handleClickOutside(e: MouseEvent) {\n if (\n dropdownRef.current &&\n !dropdownRef.current.contains(e.target as Node)\n ) {\n setDropdownOpen(false);\n }\n }\n document.addEventListener(\"mousedown\", handleClickOutside);\n return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n }, []);\n\n const handleItemClick = (id: NavItem) => {\n setDropdownOpen(false);\n onNavigate(id);\n };\n\n return (\n <>\n {/* Desktop sidebar */}\n <nav className=\"kombos-account-sidebar\">\n <ul className=\"kombos-account-sidebar__list\">\n {NAV_ITEMS.map(({ id, labelKey, icon: Icon }) => {\n const cls = cx(\n \"kombos-account-sidebar__item text-sm-medium\",\n id === activeItem && \"kombos-account-sidebar__item--active\",\n id === \"logout\" && \"kombos-account-sidebar__item--logout\",\n );\n\n return (\n <li key={id}>\n <button\n className={cls}\n onClick={() => handleItemClick(id)}\n >\n <Icon className=\"kombos-account-sidebar__icon\" />\n {labels[labelKey]}\n </button>\n </li>\n );\n })}\n </ul>\n </nav>\n\n {/* Mobile dropdown */}\n <div className=\"kombos-account-dropdown\" ref={dropdownRef}>\n <div\n className={cx(\n \"kombos-account-dropdown__box\",\n dropdownOpen && \"kombos-account-dropdown__box--open\",\n )}\n >\n <button\n className={cx(\n \"kombos-account-dropdown__trigger text-md-medium\",\n dropdownOpen && \"kombos-account-dropdown__trigger--open\",\n )}\n onClick={() => setDropdownOpen((prev) => !prev)}\n aria-expanded={dropdownOpen}\n >\n <span className=\"kombos-account-dropdown__trigger-left\">\n {activeConfig && (\n <activeConfig.icon className=\"kombos-account-icon\" />\n )}\n <span>{activeConfig && labels[activeConfig.labelKey]}</span>\n </span>\n <CaretDownSVG\n className={cx(\n \"kombos-account-icon kombos-account-dropdown__caret\",\n dropdownOpen && \"kombos-account-dropdown__caret--open\",\n )}\n />\n </button>\n\n {dropdownOpen && (\n <ul className=\"kombos-account-dropdown__menu\">\n {NAV_ITEMS.map(({ id, labelKey, icon: Icon }) => {\n const isActive = id === activeItem;\n\n return (\n <li key={id}>\n <button\n className={cx(\n \"kombos-account-dropdown__menu-item\",\n isActive\n ? \"kombos-account-dropdown__menu-item--active text-sm-semibold\"\n : \"text-sm-medium\",\n id === \"logout\" && \"kombos-account-dropdown__menu-item--logout\",\n )}\n onClick={() => handleItemClick(id)}\n >\n <span className=\"kombos-account-dropdown__menu-item-left\">\n <Icon className=\"kombos-account-icon\" />\n {labels[labelKey]}\n </span>\n {isActive && <CheckSVG className=\"kombos-account-icon\" />}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n </div>\n </>\n );\n}\n"
16602
- },
16603
- {
16604
- "filename": "components/AccountSidebar/styles.css",
16605
- "content": "/* ===== AccountSidebar ===== */\n\n/* --- Shared icon --- */\n.kombos-account-icon {\n font-size: 1.5rem;\n flex-shrink: 0;\n}\n\n/* --- Desktop sidebar --- */\n.kombos-account-sidebar {\n display: none;\n flex-shrink: 0;\n}\n\n.kombos-account-sidebar__list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.kombos-account-sidebar__item {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n width: 100%;\n padding: 0;\n border: none;\n background: none;\n cursor: pointer;\n color: var(--kombos-gray-600);\n transition: color 0.15s;\n}\n\n.kombos-account-sidebar__item svg {\n flex-shrink: 0;\n font-size: 1rem;\n}\n\n.kombos-account-sidebar__item:hover {\n color: var(--kombos-gray-700);\n}\n\n.kombos-account-sidebar__item--active {\n color: var(--kombos-gray-900);\n}\n\n.kombos-account-sidebar__item.kombos-account-sidebar__item--logout,\n.kombos-account-sidebar__item.kombos-account-sidebar__item--logout:hover {\n color: var(--kombos-error);\n}\n\n/* --- Mobile dropdown --- */\n.kombos-account-dropdown {\n display: block;\n}\n\n.kombos-account-dropdown__box {\n border: 1px solid var(--kombos-gray-100);\n border-radius: 6px;\n overflow: hidden;\n}\n\n.kombos-account-dropdown__trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n padding: 0.625rem 0.875rem;\n border: none;\n background: var(--kombos-white);\n cursor: pointer;\n color: var(--kombos-gray-900);\n}\n\n.kombos-account-dropdown__trigger--open {\n border-bottom: 1px solid var(--kombos-gray-200);\n}\n\n.kombos-account-dropdown__trigger-left {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.kombos-account-dropdown__caret {\n transition: transform 0.2s;\n}\n\n.kombos-account-dropdown__caret--open {\n transform: rotate(180deg);\n}\n\n.kombos-account-dropdown__menu {\n list-style: none;\n margin: 0;\n padding: 0;\n background: var(--kombos-white);\n}\n\n.kombos-account-dropdown__menu-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n padding: 0.75rem;\n border: none;\n background: none;\n cursor: pointer;\n color: var(--kombos-gray-900);\n transition: background-color 0.15s;\n}\n\n.kombos-account-dropdown__menu-item:hover {\n background-color: var(--kombos-gray-50);\n}\n\n.kombos-account-dropdown__menu-item--active {\n background: var(--kombos-gray-50);\n}\n\n.kombos-account-dropdown__menu-item--logout {\n color: var(--kombos-error);\n}\n\n.kombos-account-dropdown__menu-item-left {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n/* --- Responsive --- */\n@media (min-width: 1024px) {\n .kombos-account-sidebar {\n display: block;\n width: fit-content;\n }\n\n .kombos-account-dropdown {\n display: none;\n }\n}\n"
16606
- },
16607
- {
16608
- "filename": "ikas-config-snippet.json",
16609
- "content": "{\n \"id\": \"{{PROJECT_ID}}-account-info\",\n \"name\": \"AccountInfo\",\n \"type\": \"section\",\n \"entry\": \"./src/components/AccountInfo/index.tsx\",\n \"styles\": \"./src/components/AccountInfo/styles.css\",\n \"props\": [\n {\n \"name\": \"accountInfoLabel\",\n \"displayName\": \"Account Info\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Account Bilgilerim\",\n \"groupId\": \"sidebar\"\n },\n {\n \"name\": \"ordersLabel\",\n \"displayName\": \"Orders\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Siparislerim\",\n \"groupId\": \"sidebar\"\n },\n {\n \"name\": \"addressesLabel\",\n \"displayName\": \"Addresses\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"My Addresses\",\n \"groupId\": \"sidebar\"\n },\n {\n \"name\": \"favoritesLabel\",\n \"displayName\": \"Favorites\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"My Favorites\",\n \"groupId\": \"sidebar\"\n },\n {\n \"name\": \"logoutLabel\",\n \"displayName\": \"Sign Out\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Cikis Yap\",\n \"groupId\": \"sidebar\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Components\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-account-info-content\",\n \"{{PROJECT_ID}}-account-orders\",\n \"{{PROJECT_ID}}-account-addresses\",\n \"{{PROJECT_ID}}-account-favorites\",\n \"{{PROJECT_ID}}-account-order-detail\"\n ]\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"sidebar\",\n \"name\": \"Border Menüsü\",\n \"description\": \"Border menüsündeki sekme etiketleri\"\n }\n ]\n}"
16610
- },
16611
- {
16612
- "filename": "index.tsx",
16613
- "content": "import { useState, useEffect } from \"preact/hooks\";\nimport {\n logout,\n customerStore,\n waitForCustomerStoreInit,\n Router,\n IkasThemePageType,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport AccountSidebar from \"./components/AccountSidebar\";\nimport PageLoader from \"../../sub-components/PageLoader\";\nimport type { NavItem } from \"./components/AccountSidebar\";\n\nconst PAGE_TYPE_MAP: Record<Exclude<NavItem, \"logout\">, IkasThemePageType> = {\n account: \"ACCOUNT\",\n orders: \"ORDERS\",\n addresses: \"ADDRESSES\",\n favorites: \"FAVORITE_PRODUCTS\",\n};\n\nfunction getActiveTabFromPath(): NavItem {\n const path = Router.getCurrentPath();\n if (path.includes(\"/orders\")) return \"orders\";\n if (path.includes(\"/addresses\")) return \"addresses\";\n if (path.includes(\"/favorite-products\")) return \"favorites\";\n return \"account\";\n}\n\nexport function AccountInfo(props: Props) {\n const [isChecking, setIsChecking] = useState(true);\n\n useEffect(() => {\n waitForCustomerStoreInit(customerStore).then(() => {\n if (!customerStore.customer) {\n Router.navigateToPage(\"LOGIN\");\n } else {\n setIsChecking(false);\n }\n });\n }, []);\n\n const {\n accountInfoLabel = \"Hesap Bilgilerim\",\n ordersLabel = \"Siparislerim\",\n addressesLabel = \"Adreslerim\",\n favoritesLabel = \"Favorilerim\",\n logoutLabel = \"Cikis Yap\",\n components,\n } = props;\n const activeTab = getActiveTabFromPath();\n\n const handleNavigate = async (item: NavItem) => {\n if (item === \"logout\") {\n await logout(customerStore);\n Router.navigateToPage(\"INDEX\");\n return;\n }\n Router.navigateToPage(PAGE_TYPE_MAP[item]);\n };\n\n return (\n <section className=\"kombos-account-info\">\n <div className=\"kombos-account-info__container kombos-container\">\n <AccountSidebar\n activeItem={activeTab}\n onNavigate={handleNavigate}\n accountInfoLabel={accountInfoLabel}\n ordersLabel={ordersLabel}\n addressesLabel={addressesLabel}\n favoritesLabel={favoritesLabel}\n logoutLabel={logoutLabel}\n />\n\n <div className=\"kombos-account-info__content\">\n {isChecking ? (\n <PageLoader />\n ) : (\n <IkasComponentRenderer\n id=\"account-info\"\n components={components}\n parentProps={props}\n />\n )}\n </div>\n </div>\n </section>\n );\n}\n\nexport default AccountInfo;\n"
16614
- },
16615
- {
16616
- "filename": "styles.css",
16617
- "content": "/* ── Account Info Section (Layout) ── */\n.kombos-account-info {\n width: 100%;\n}\n\n.kombos-account-info__container {\n padding-top: 1rem;\n padding-bottom: 1rem;\n display: flex;\n flex-direction: column;\n gap: 2rem;\n}\n\n.kombos-account-info__content {\n flex: 1;\n min-width: 0;\n}\n\n/* ── Tablet (768px+) ── */\n@media (min-width: 768px) {\n .kombos-account-info__container {\n padding-top: 1.5rem;\n padding-bottom: 1.5rem;\n }\n}\n\n/* ── Desktop (1024px+) ── */\n@media (min-width: 1024px) {\n .kombos-account-info__container {\n padding-top: 2rem;\n padding-bottom: 2rem;\n flex-direction: row;\n gap: 7.75rem;\n }\n}\n"
16618
- },
16619
- {
16620
- "filename": "types.ts",
16621
- "content": "export interface Props {\n accountInfoLabel?: string;\n ordersLabel?: string;\n addressesLabel?: string;\n favoritesLabel?: string;\n logoutLabel?: string;\n components?: any;\n}\n"
16622
- }
16623
- ]
16624
- },
16625
- {
16626
- "id": "add-to-cart",
16627
- "title": "Add to Cart Pattern",
16628
- "description": "Complete add-to-cart implementation with stock validation, variant selection, option sets, bundle product support, and quantity controls. Shows addItemToCart with AddItemOptions, hasProductVariantStock, isAddToCartEnabled, hasBundleSettings, and toast notifications.",
16629
- "code": "import {\n addItemToCart,\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n getSelectedProductVariant,\n hasProductStock,\n hasProductVariantStock,\n isAddToCartEnabled,\n initProductOptionSetValues,\n IkasBundleSettings,\n IkasStorefrontConfig,\n} from \"@ikas/bp-storefront\";\nimport { useState } from \"preact/hooks\";\nimport { Props } from \"./types\";\nimport Button from \"../../sub-components/Button\";\nimport QuantitySelector from \"../../sub-components/QuantitySelector\";\nimport PayWithIkas from \"./components/PayWithIkas\";\nimport { isBundleOutOfStock } from \"../../utils/bundle\";\nimport { validateOptionSet } from \"../../utils/optionSet\";\nimport { showToast } from \"../../utils/toast\";\n\nexport function ProductDetailAddToCart({\n product,\n addToCartButtonText = \"Sepete Ekle\",\n addingToCartText = \"Ekleniyor...\",\n outOfStockText = \"Tükendi\",\n errorMessage = \"Ürün sepete eklenemedi\",\n optionSetErrorMessage = \"Lütfen gerekli seçenekleri doldurun\",\n updateCartButtonText = \"Güncelle\",\n updatingCartText = \"Güncelleniyor...\",\n hideQuantityInput = false,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n}: Props) {\n const [quantity, setQuantity] = useState(1);\n const [isAddingToCart, setIsAddingToCart] = useState(false);\n\n if (!product) return null;\n\n const editLineID =\n typeof window !== \"undefined\"\n ? new URLSearchParams(window.location.search).get(\"editLineID\")\n : null;\n const isEditMode = !!editLineID;\n\n const selectedVariant = getSelectedProductVariant(product);\n const bundleSettings = selectedVariant?.bundleSettings as\n | IkasBundleSettings\n | undefined;\n const isBundle = !!bundleSettings;\n\n const isEnabled = isAddToCartEnabled(product);\n\n const isOutOfStock = isBundle\n ? isBundleOutOfStock(bundleSettings) || !isEnabled\n : !hasProductStock(product) || !hasProductVariantStock(selectedVariant);\n\n const isDisabled = isOutOfStock || isAddingToCart;\n\n const payWithIkasUrl = IkasStorefrontConfig.getPayWithIkasUrl();\n const routing = IkasStorefrontConfig.getCurrentRouting();\n const showPayWithIkas =\n !isOutOfStock &&\n !!payWithIkasUrl &&\n routing?.locale === \"tr\" &&\n routing?.currencyCode === \"TRY\";\n\n const handleAddToCart = async () => {\n if (isDisabled) return;\n\n if (!validateOptionSet(product.productOptionSet, optionSetErrorMessage))\n return;\n\n if (!isAddToCartEnabled(product)) {\n showToast(errorMessage, \"error\");\n return;\n }\n\n setIsAddingToCart(true);\n try {\n const result = await addItemToCart(selectedVariant, product, quantity);\n\n if (result.success) {\n if (product.productOptionSet) {\n initProductOptionSetValues(product.productOptionSet);\n }\n window.dispatchEvent(new CustomEvent(\"ikas:reset-option-state\"));\n window.dispatchEvent(new CustomEvent(\"ikas:open-cart-sidebar\"));\n } else {\n showToast(errorMessage, \"error\");\n }\n } finally {\n setIsAddingToCart(false);\n }\n };\n\n const getButtonText = () => {\n if (isEditMode) {\n if (isAddingToCart) return updatingCartText;\n if (isOutOfStock) return outOfStockText;\n return updateCartButtonText;\n }\n if (isAddingToCart) return addingToCartText;\n if (isOutOfStock) return outOfStockText;\n return addToCartButtonText;\n };\n\n return (\n <div\n className=\"kombos-pd-atc\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n <div className=\"kombos-pd-atc__actions\">\n {!hideQuantityInput && !isOutOfStock && (\n <QuantitySelector value={quantity} onChange={setQuantity} />\n )}\n <Button\n variant=\"primary\"\n size=\"s\"\n className=\"kombos-pd-atc__btn\"\n disabled={isDisabled}\n onClick={handleAddToCart}\n >\n {getButtonText()}\n </Button>\n </div>\n {showPayWithIkas && (\n <PayWithIkas\n product={product}\n quantity={quantity}\n isEnabled={isEnabled}\n payWithIkasUrl={payWithIkasUrl!}\n />\n )}\n </div>\n );\n}\n\nexport default ProductDetailAddToCart;\n",
16630
- "relatedFunctions": [
16631
- "addItemToCart",
16632
- "getFormattedMarginTopSize",
16633
- "getFormattedMarginBottomSize",
16634
- "getSelectedProductVariant",
16635
- "hasProductStock",
16636
- "hasProductVariantStock",
16637
- "isAddToCartEnabled",
16638
- "initProductOptionSetValues"
16639
- ],
16640
- "categories": [
16641
- "Cart",
16642
- "ProductDetail"
16643
- ],
16644
- "files": [
16645
- {
16646
- "filename": "components/PayWithIkas/index.tsx",
16647
- "content": "import { IkasProduct } from \"@ikas/bp-storefront\";\nimport { usePayWithIkas } from \"../../../../hooks/usePayWithIkas\";\n\ninterface Props {\n product: IkasProduct;\n quantity: number;\n isEnabled: boolean;\n payWithIkasUrl: string;\n}\n\nexport default function PayWithIkas({\n product,\n quantity,\n isEnabled,\n payWithIkasUrl,\n}: Props) {\n const { iframeRef, iframeSrc, iframeHeight } = usePayWithIkas({\n product,\n quantity,\n isEnabled,\n payWithIkasUrl,\n });\n\n return (\n <iframe\n ref={iframeRef}\n className=\"kombos-pd-atc__ikas-pay\"\n title=\"Pay with ikas\"\n src={iframeSrc}\n width=\"100%\"\n height={iframeHeight}\n />\n );\n}\n"
16648
- },
16649
- {
16650
- "filename": "components/PayWithIkas/styles.css",
16651
- "content": ".kombos-pd-atc__ikas-pay {\n display: block;\n border: none;\n}\n"
16652
- },
16653
- {
16654
- "filename": "ikas-config-snippet.json",
16655
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-add-to-cart\",\n \"name\": \"ProductDetailAddToCart\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailAddToCart/index.tsx\",\n \"styles\": \"./src/components/ProductDetailAddToCart/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"addToCartButtonText\",\n \"displayName\": \"Cart Add Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Add to Cart\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"addingToCartText\",\n \"displayName\": \"Cart Adding Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Ekleniyor...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"outOfStockText\",\n \"displayName\": \"Sold Out Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sold Out\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"hideQuantityInput\",\n \"displayName\": \"Quantity Selector Hide\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"settings\"\n },\n {\n \"name\": \"errorMessage\",\n \"displayName\": \"Error Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Product sepete eklenemedi\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"optionSetErrorMessage\",\n \"displayName\": \"Option Set Error Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Lütfen gerekli seçenekleri doldurun\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"updateCartButtonText\",\n \"displayName\": \"Sepeti Update Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Update\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"updatingCartText\",\n \"displayName\": \"Cart Updating Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Güncelleniyor...\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n },\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Button ve durum metinleri\"\n },\n {\n \"id\": \"settings\",\n \"name\": \"Settings\",\n \"description\": \"Bileşen davranışını kontrol eden ayarlar\"\n }\n ]\n}"
16656
- },
16657
- {
16658
- "filename": "index.tsx",
16659
- "content": "import {\n addItemToCart,\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n getSelectedProductVariant,\n hasProductStock,\n hasProductVariantStock,\n isAddToCartEnabled,\n initProductOptionSetValues,\n IkasBundleSettings,\n IkasStorefrontConfig,\n} from \"@ikas/bp-storefront\";\nimport { useState } from \"preact/hooks\";\nimport { Props } from \"./types\";\nimport Button from \"../../sub-components/Button\";\nimport QuantitySelector from \"../../sub-components/QuantitySelector\";\nimport PayWithIkas from \"./components/PayWithIkas\";\nimport { isBundleOutOfStock } from \"../../utils/bundle\";\nimport { validateOptionSet } from \"../../utils/optionSet\";\nimport { showToast } from \"../../utils/toast\";\n\nexport function ProductDetailAddToCart({\n product,\n addToCartButtonText = \"Sepete Ekle\",\n addingToCartText = \"Ekleniyor...\",\n outOfStockText = \"Tükendi\",\n errorMessage = \"Ürün sepete eklenemedi\",\n optionSetErrorMessage = \"Lütfen gerekli seçenekleri doldurun\",\n updateCartButtonText = \"Güncelle\",\n updatingCartText = \"Güncelleniyor...\",\n hideQuantityInput = false,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n}: Props) {\n const [quantity, setQuantity] = useState(1);\n const [isAddingToCart, setIsAddingToCart] = useState(false);\n\n if (!product) return null;\n\n const editLineID =\n typeof window !== \"undefined\"\n ? new URLSearchParams(window.location.search).get(\"editLineID\")\n : null;\n const isEditMode = !!editLineID;\n\n const selectedVariant = getSelectedProductVariant(product);\n const bundleSettings = selectedVariant?.bundleSettings as\n | IkasBundleSettings\n | undefined;\n const isBundle = !!bundleSettings;\n\n const isEnabled = isAddToCartEnabled(product);\n\n const isOutOfStock = isBundle\n ? isBundleOutOfStock(bundleSettings) || !isEnabled\n : !hasProductStock(product) || !hasProductVariantStock(selectedVariant);\n\n const isDisabled = isOutOfStock || isAddingToCart;\n\n const payWithIkasUrl = IkasStorefrontConfig.getPayWithIkasUrl();\n const routing = IkasStorefrontConfig.getCurrentRouting();\n const showPayWithIkas =\n !isOutOfStock &&\n !!payWithIkasUrl &&\n routing?.locale === \"tr\" &&\n routing?.currencyCode === \"TRY\";\n\n const handleAddToCart = async () => {\n if (isDisabled) return;\n\n if (!validateOptionSet(product.productOptionSet, optionSetErrorMessage))\n return;\n\n if (!isAddToCartEnabled(product)) {\n showToast(errorMessage, \"error\");\n return;\n }\n\n setIsAddingToCart(true);\n try {\n const result = await addItemToCart(selectedVariant, product, quantity);\n\n if (result.success) {\n if (product.productOptionSet) {\n initProductOptionSetValues(product.productOptionSet);\n }\n window.dispatchEvent(new CustomEvent(\"ikas:reset-option-state\"));\n window.dispatchEvent(new CustomEvent(\"ikas:open-cart-sidebar\"));\n } else {\n showToast(errorMessage, \"error\");\n }\n } finally {\n setIsAddingToCart(false);\n }\n };\n\n const getButtonText = () => {\n if (isEditMode) {\n if (isAddingToCart) return updatingCartText;\n if (isOutOfStock) return outOfStockText;\n return updateCartButtonText;\n }\n if (isAddingToCart) return addingToCartText;\n if (isOutOfStock) return outOfStockText;\n return addToCartButtonText;\n };\n\n return (\n <div\n className=\"kombos-pd-atc\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n <div className=\"kombos-pd-atc__actions\">\n {!hideQuantityInput && !isOutOfStock && (\n <QuantitySelector value={quantity} onChange={setQuantity} />\n )}\n <Button\n variant=\"primary\"\n size=\"s\"\n className=\"kombos-pd-atc__btn\"\n disabled={isDisabled}\n onClick={handleAddToCart}\n >\n {getButtonText()}\n </Button>\n </div>\n {showPayWithIkas && (\n <PayWithIkas\n product={product}\n quantity={quantity}\n isEnabled={isEnabled}\n payWithIkasUrl={payWithIkasUrl!}\n />\n )}\n </div>\n );\n}\n\nexport default ProductDetailAddToCart;\n"
16660
- },
16661
- {
16662
- "filename": "styles.css",
16663
- "content": "/* ===== ProductDetailAddToCart ===== */\n\n.kombos-pd-atc {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n.kombos-pd-atc__actions {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n}\n\n.kombos-pd-atc__btn {\n flex: 1;\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd-atc {\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n}\n"
16664
- },
16665
- {
16666
- "filename": "types.ts",
16667
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n addToCartButtonText?: string;\n addingToCartText?: string;\n outOfStockText?: string;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n hideQuantityInput?: boolean;\n errorMessage?: string;\n optionSetErrorMessage?: string;\n updateCartButtonText?: string;\n updatingCartText?: string;\n}\n"
16668
- }
16669
- ]
16670
- },
16671
- {
16672
- "id": "blog-home-section",
16673
- "title": "Blog Home Section",
16674
- "description": "Blog listing page with post cards, category filtering, pagination, and responsive grid layout. Supports blog category navigation.",
16675
- "code": "import {\n IkasBlogList,\n IkasBlog,\n getBlogListPage,\n getBlogListPageCount,\n hasBlogListPrevPage,\n hasBlogListNextPage,\n getIkasBlogCategoryHref,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport { cx } from \"../../utils/cx\";\nimport Pagination from \"../../sub-components/Pagination\";\nimport BlogCard from \"./components/BlogCard\";\nimport { Props } from \"./types\";\n\nexport function BlogHome({\n blogList,\n blogCategoryList,\n title = \"Blog\",\n description,\n allCategoriesText = \"Tümünü Gör\",\n readMoreText = \"Devamını Oku\",\n emptyMessage = \"Henüz blog yazısı bulunmuyor.\",\n aspectRatio,\n objectFit,\n}: Props) {\n if (!blogList) return null;\n\n const activeCategoryId = blogList.filterCategoryId;\n const categories = blogCategoryList?.data ?? [];\n const blogs = blogList.data ?? [];\n\n return (\n <section className=\"kombos-blog-home\">\n <div className=\"kombos-container\">\n <div className=\"kombos-blog-home__header\">\n <h1 className=\"kombos-blog-home__title text-xl-semibold md:display-xs-semibold\">\n {title}\n </h1>\n {description && (\n <p className=\"kombos-blog-home__description text-md-regular\">\n {description}\n </p>\n )}\n </div>\n\n {categories.length > 0 && (\n <div className=\"kombos-blog-home__categories\">\n <a\n href=\"/blog\"\n className={cx(\n \"kombos-blog-home__cat-btn\",\n \"text-sm-medium\",\n !activeCategoryId && \"kombos-blog-home__cat-btn--active\",\n )}\n >\n {allCategoriesText}\n </a>\n {categories.map((cat) => (\n <a\n key={cat.id}\n href={getIkasBlogCategoryHref(cat)}\n className={cx(\n \"kombos-blog-home__cat-btn\",\n \"text-sm-medium\",\n activeCategoryId === cat.id &&\n \"kombos-blog-home__cat-btn--active\",\n )}\n >\n {cat.name}\n </a>\n ))}\n </div>\n )}\n\n {blogs.length === 0 ? (\n <p className=\"kombos-blog-home__empty text-md-semibold\">\n {emptyMessage}\n </p>\n ) : (\n <div className=\"kombos-blog-home__grid\">\n {blogs.map((blog: IkasBlog) => (\n <BlogCard\n key={blog.id}\n blog={blog}\n readMoreText={readMoreText}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n />\n ))}\n </div>\n )}\n\n <BlogPagination blogList={blogList} />\n </div>\n </section>\n );\n}\n\nconst BlogPagination = observer(function BlogPagination({\n blogList,\n}: {\n blogList: IkasBlogList;\n}) {\n const currentPage = blogList.page ?? 1;\n const totalPages = getBlogListPageCount(blogList);\n\n const goToPage = (page: number) => {\n getBlogListPage(blogList, page);\n window.scrollTo({ top: 0, behavior: \"smooth\" });\n };\n\n return (\n <Pagination\n currentPage={currentPage}\n totalPages={totalPages}\n hasPrev={hasBlogListPrevPage(blogList)}\n hasNext={hasBlogListNextPage(blogList)}\n onPageChange={goToPage}\n />\n );\n});\n\nexport default BlogHome;\n",
16676
- "relatedFunctions": [
16677
- "getBlogListPage",
16678
- "getBlogListPageCount",
16679
- "hasBlogListPrevPage",
16680
- "hasBlogListNextPage",
16681
- "getIkasBlogCategoryHref"
16682
- ],
16683
- "categories": [
16684
- "Blog"
16685
- ],
16686
- "files": [
16687
- {
16688
- "filename": "components/BlogCard/index.tsx",
16689
- "content": "import {\n IkasBlog,\n getIkasBlogHref,\n getIkasBlogFormattedDate,\n getIkasBlogCategoryHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { resolveAspectRatio, resolveObjectFit } from \"../../../../utils/media\";\nimport { getFullName } from \"../../../../utils/fullName\";\nimport { CaretRightSVG } from \"../../../../sub-components/icons\";\nimport type { AspectRatio, ObjectFit } from \"../../../../global-types\";\n\ninterface Props {\n blog: IkasBlog;\n readMoreText: string;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n}\n\nexport default function BlogCard({\n blog,\n readMoreText,\n aspectRatio,\n objectFit,\n}: Props) {\n const href = getIkasBlogHref(blog);\n const imageSrc = blog.image ? getDefaultSrc(blog.image) : \"\";\n const writerName = blog.writer\n ? getFullName(blog.writer.firstName, blog.writer.lastName)\n : \"\";\n\n return (\n <div className=\"kombos-blog-card\">\n <a href={href} className=\"kombos-blog-card__image-link\">\n <div\n className=\"kombos-blog-card__image-wrap\"\n style={{ aspectRatio: resolveAspectRatio(aspectRatio) }}\n >\n {imageSrc && (\n <img\n src={imageSrc}\n alt={blog.title}\n className=\"kombos-blog-card__image\"\n loading=\"lazy\"\n style={{ objectFit: resolveObjectFit(objectFit) }}\n />\n )}\n </div>\n </a>\n <div className=\"kombos-blog-card__content\">\n {blog.category?.name && (\n <a\n href={getIkasBlogCategoryHref(blog.category)}\n className=\"kombos-blog-card__category text-xs-semibold\"\n >\n {blog.category.name}\n </a>\n )}\n <h2 className=\"kombos-blog-card__title text-md-semibold md:text-lg-semibold\">\n {blog.title}\n </h2>\n {blog.shortDescription && (\n <p className=\"kombos-blog-card__desc text-sm-regular\">\n {blog.shortDescription}\n </p>\n )}\n <div className=\"kombos-blog-card__footer\">\n <div className=\"kombos-blog-card__meta text-xs-regular\">\n <time>{getIkasBlogFormattedDate(blog)}</time>\n {writerName && (\n <>\n <span className=\"kombos-blog-card__dot\">&middot;</span>\n <span>{writerName}</span>\n </>\n )}\n </div>\n <a\n href={href}\n className=\"kombos-blog-card__read-more text-sm-semibold\"\n aria-label={`${readMoreText} - ${blog.title}`}\n >\n {readMoreText}\n <CaretRightSVG />\n </a>\n </div>\n </div>\n </div>\n );\n}\n"
16690
- },
16691
- {
16692
- "filename": "components/BlogCard/styles.css",
16693
- "content": "/* ===== BlogCard ===== */\n.kombos-blog-card {\n display: flex;\n flex-direction: column;\n color: inherit;\n border-radius: 8px;\n overflow: hidden;\n border: 1px solid var(--kombos-gray-200);\n}\n\n/* Image link */\n.kombos-blog-card__image-link {\n display: block;\n text-decoration: none;\n}\n\n/* Image wrapper */\n.kombos-blog-card__image-wrap {\n position: relative;\n overflow: hidden;\n background: var(--kombos-gray-100);\n}\n\n.kombos-blog-card__image {\n width: 100%;\n height: 100%;\n}\n\n/* Content area */\n.kombos-blog-card__content {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n padding: 1rem;\n flex: 1;\n}\n\n/* Category badge link */\n.kombos-blog-card__category {\n display: inline-block;\n align-self: flex-start;\n border: 1px solid var(--kombos-gray-200);\n border-radius: 100px;\n padding: 0.25rem 0.75rem;\n text-decoration: none;\n color: var(--kombos-gray-700);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n transition: background-color 0.15s ease, color 0.15s ease,\n border-color 0.15s ease;\n}\n\n.kombos-blog-card__category:hover {\n background: var(--kombos-gray-900);\n color: var(--kombos-white);\n border-color: var(--kombos-gray-900);\n}\n\n/* Title — 2 line clamp */\n.kombos-blog-card__title {\n margin: 0;\n color: var(--kombos-gray-900);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Description — 3 line clamp */\n.kombos-blog-card__desc {\n margin: 0;\n color: var(--kombos-gray-500);\n display: -webkit-box;\n -webkit-line-clamp: 3;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Footer */\n.kombos-blog-card__footer {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n margin-top: auto;\n padding-top: 0.5rem;\n}\n\n@media (min-width: 768px) {\n .kombos-blog-card__footer {\n flex-direction: row;\n justify-content: space-between;\n align-items: center;\n }\n}\n\n/* Meta (date + author) */\n.kombos-blog-card__meta {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n color: var(--kombos-gray-500);\n}\n\n.kombos-blog-card__dot {\n color: var(--kombos-gray-400);\n}\n\n/* Read more link */\n.kombos-blog-card__read-more {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n color: var(--kombos-gray-700);\n text-decoration: none;\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n.kombos-blog-card__read-more:hover {\n text-decoration: underline;\n}\n"
16694
- },
16695
- {
16696
- "filename": "ikas-config-snippet.json",
16697
- "content": "{\n \"id\": \"{{PROJECT_ID}}-blog-home\",\n \"name\": \"BlogHome\",\n \"type\": \"section\",\n \"entry\": \"./src/components/BlogHome/index.tsx\",\n \"styles\": \"./src/components/BlogHome/styles.css\",\n \"props\": [\n {\n \"name\": \"blogList\",\n \"displayName\": \"Blog Posts\",\n \"type\": \"BLOG_LIST\",\n \"required\": true,\n \"groupId\": \"content\"\n },\n {\n \"name\": \"blogCategoryList\",\n \"displayName\": \"Blog Kategorileri\",\n \"type\": \"BLOG_CATEGORY_LIST\",\n \"required\": false,\n \"groupId\": \"content\"\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Blog\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"allCategoriesText\",\n \"displayName\": \"Tümü Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Tumunu Gor\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"readMoreText\",\n \"displayName\": \"Devamını Oku Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Devamini Oku\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"emptyMessage\",\n \"displayName\": \"Empty State Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Henuz blog yazisi bulunmuyor.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"aspectRatio\",\n \"displayName\": \"En Boy Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"image-settings\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"objectFit\",\n \"displayName\": \"Gorsel Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"image-settings\",\n \"enumTypeId\": \"GrylMqHxui\"\n },\n {\n \"name\": \"description\",\n \"displayName\": \"Description\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"groupId\": \"content\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"content\",\n \"name\": \"Content\",\n \"description\": \"Blog yazıları, kategoriler ve sayfa başlığı\"\n },\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Buton, etiket ve boş durum metinleri\"\n },\n {\n \"id\": \"image-settings\",\n \"name\": \"Image Settings\",\n \"description\": \"Blog kart görselleri için en boy oranı ve sığdırma\"\n }\n ]\n}"
16698
- },
16699
- {
16700
- "filename": "index.tsx",
16701
- "content": "import {\n IkasBlogList,\n IkasBlog,\n getBlogListPage,\n getBlogListPageCount,\n hasBlogListPrevPage,\n hasBlogListNextPage,\n getIkasBlogCategoryHref,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport { cx } from \"../../utils/cx\";\nimport Pagination from \"../../sub-components/Pagination\";\nimport BlogCard from \"./components/BlogCard\";\nimport { Props } from \"./types\";\n\nexport function BlogHome({\n blogList,\n blogCategoryList,\n title = \"Blog\",\n description,\n allCategoriesText = \"Tümünü Gör\",\n readMoreText = \"Devamını Oku\",\n emptyMessage = \"Henüz blog yazısı bulunmuyor.\",\n aspectRatio,\n objectFit,\n}: Props) {\n if (!blogList) return null;\n\n const activeCategoryId = blogList.filterCategoryId;\n const categories = blogCategoryList?.data ?? [];\n const blogs = blogList.data ?? [];\n\n return (\n <section className=\"kombos-blog-home\">\n <div className=\"kombos-container\">\n <div className=\"kombos-blog-home__header\">\n <h1 className=\"kombos-blog-home__title text-xl-semibold md:display-xs-semibold\">\n {title}\n </h1>\n {description && (\n <p className=\"kombos-blog-home__description text-md-regular\">\n {description}\n </p>\n )}\n </div>\n\n {categories.length > 0 && (\n <div className=\"kombos-blog-home__categories\">\n <a\n href=\"/blog\"\n className={cx(\n \"kombos-blog-home__cat-btn\",\n \"text-sm-medium\",\n !activeCategoryId && \"kombos-blog-home__cat-btn--active\",\n )}\n >\n {allCategoriesText}\n </a>\n {categories.map((cat) => (\n <a\n key={cat.id}\n href={getIkasBlogCategoryHref(cat)}\n className={cx(\n \"kombos-blog-home__cat-btn\",\n \"text-sm-medium\",\n activeCategoryId === cat.id &&\n \"kombos-blog-home__cat-btn--active\",\n )}\n >\n {cat.name}\n </a>\n ))}\n </div>\n )}\n\n {blogs.length === 0 ? (\n <p className=\"kombos-blog-home__empty text-md-semibold\">\n {emptyMessage}\n </p>\n ) : (\n <div className=\"kombos-blog-home__grid\">\n {blogs.map((blog: IkasBlog) => (\n <BlogCard\n key={blog.id}\n blog={blog}\n readMoreText={readMoreText}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n />\n ))}\n </div>\n )}\n\n <BlogPagination blogList={blogList} />\n </div>\n </section>\n );\n}\n\nconst BlogPagination = observer(function BlogPagination({\n blogList,\n}: {\n blogList: IkasBlogList;\n}) {\n const currentPage = blogList.page ?? 1;\n const totalPages = getBlogListPageCount(blogList);\n\n const goToPage = (page: number) => {\n getBlogListPage(blogList, page);\n window.scrollTo({ top: 0, behavior: \"smooth\" });\n };\n\n return (\n <Pagination\n currentPage={currentPage}\n totalPages={totalPages}\n hasPrev={hasBlogListPrevPage(blogList)}\n hasNext={hasBlogListNextPage(blogList)}\n onPageChange={goToPage}\n />\n );\n});\n\nexport default BlogHome;\n"
16702
- },
16703
- {
16704
- "filename": "styles.css",
16705
- "content": "/* ===== BlogHome Section ===== */\n.kombos-blog-home {\n width: 100%;\n padding: 2rem 0;\n}\n\n.kombos-blog-home .kombos-container {\n display: flex;\n flex-direction: column;\n gap: 2rem;\n}\n\n/* Header (title + description) */\n.kombos-blog-home__header {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 0.5rem;\n text-align: center;\n}\n\n.kombos-blog-home__title {\n margin: 0;\n color: var(--kombos-gray-900);\n}\n\n.kombos-blog-home__description {\n margin: 0;\n color: var(--kombos-gray-500);\n}\n\n/* Category navigation */\n.kombos-blog-home__categories {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n overflow-x: auto;\n scrollbar-width: none;\n -webkit-overflow-scrolling: touch;\n padding-bottom: 0.25rem;\n}\n\n.kombos-blog-home__categories::-webkit-scrollbar {\n display: none;\n}\n\n.kombos-blog-home__cat-btn {\n padding: 0.5rem 1rem;\n border: 1px solid var(--kombos-gray-200);\n border-radius: 100px;\n cursor: pointer;\n white-space: nowrap;\n transition: background-color 0.15s ease, color 0.15s ease,\n border-color 0.15s ease;\n background: transparent;\n color: var(--kombos-gray-700);\n text-decoration: none;\n}\n\n.kombos-blog-home__cat-btn:hover {\n border-color: var(--kombos-gray-300);\n}\n\n.kombos-blog-home__cat-btn--active {\n background: var(--kombos-gray-900);\n color: var(--kombos-white);\n border-color: var(--kombos-gray-900);\n}\n\n.kombos-blog-home__cat-btn--active:hover {\n background: var(--kombos-gray-800);\n border-color: var(--kombos-gray-800);\n}\n\n/* Empty state */\n.kombos-blog-home__empty {\n text-align: center;\n color: var(--kombos-gray-900);\n padding: 3rem 0;\n margin: 0;\n}\n\n/* Blog grid */\n.kombos-blog-home__grid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 1.5rem;\n}\n\n/* Pagination spacing */\n.kombos-blog-home .kombos-pagination {\n padding-top: 1rem;\n}\n\n@media (min-width: 768px) {\n .kombos-blog-home {\n padding: 2.5rem 0;\n }\n\n .kombos-blog-home__grid {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n@media (min-width: 1024px) {\n .kombos-blog-home {\n padding: 3rem 0;\n }\n\n .kombos-blog-home .kombos-container {\n gap: 2.5rem;\n }\n\n .kombos-blog-home__grid {\n grid-template-columns: repeat(3, 1fr);\n }\n}\n"
16706
- },
16707
- {
16708
- "filename": "types.ts",
16709
- "content": "import type { IkasBlogList, IkasBlogCategoryList } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../global-types\";\n\nexport interface Props {\n blogList: IkasBlogList;\n blogCategoryList?: IkasBlogCategoryList;\n title?: string;\n allCategoriesText?: string;\n readMoreText?: string;\n emptyMessage?: string;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n description?: string;\n}\n"
16710
- }
16711
- ]
16712
- },
16713
- {
16714
- "id": "blog-post-section",
16715
- "title": "Blog Post Section",
16716
- "description": "Individual blog post display with title, date, author, featured image, and rich text content. Includes breadcrumb navigation and related posts.",
16717
- "code": "import {\n getIkasBlogFormattedDate,\n getIkasBlogCategoryHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\nimport { Props } from \"./types\";\nimport Breadcrumb, { BreadcrumbItem } from \"../../sub-components/Breadcrumb\";\nimport { resolveAspectRatio, resolveObjectFit } from \"../../utils/media\";\nimport { getFullName } from \"../../utils/fullName\";\n\nexport function BlogPost({\n blogPost,\n homeText = \"Ana Sayfa\",\n blogText = \"Blog Yazıları\",\n tagsTitle = \"Etiketler\",\n aspectRatio,\n objectFit,\n}: Props) {\n if (!blogPost) return null;\n\n const heroSrc = blogPost.image ? getDefaultSrc(blogPost.image) : \"\";\n const date = getIkasBlogFormattedDate(blogPost);\n const categoryName = blogPost.category?.name;\n const categoryHref = blogPost.category\n ? getIkasBlogCategoryHref(blogPost.category)\n : \"\";\n const tags = blogPost.tags ?? [];\n const hasContent = blogPost.blogContent?.content;\n\n const writerName = getFullName(blogPost.writer?.firstName, blogPost.writer?.lastName);\n\n const breadcrumbItems: BreadcrumbItem[] = [\n { label: homeText, href: \"/\" },\n { label: blogText, href: \"/blog\" },\n ];\n if (categoryName && categoryHref) {\n breadcrumbItems.push({ label: categoryName, href: categoryHref });\n }\n breadcrumbItems.push({ label: blogPost.title });\n\n return (\n <section className=\"kombos-bp\">\n <div className=\"kombos-container\">\n <div className=\"kombos-bp__article\">\n <Breadcrumb items={breadcrumbItems} size=\"xs\" />\n\n {heroSrc && (\n <div className=\"kombos-bp__hero-wrap\">\n <img\n src={heroSrc}\n alt={blogPost.title}\n className=\"kombos-bp__hero-img\"\n style={{\n aspectRatio: resolveAspectRatio(aspectRatio),\n objectFit: resolveObjectFit(objectFit),\n }}\n />\n </div>\n )}\n\n <div className=\"kombos-bp__header\">\n <div className=\"kombos-bp__meta\">\n {categoryName && categoryHref && (\n <a\n href={categoryHref}\n className=\"kombos-bp__category text-xs-semibold\"\n >\n {categoryName}\n </a>\n )}\n {date && (\n <span className=\"kombos-bp__date text-xs-regular\">{date}</span>\n )}\n {writerName && (\n <span className=\"kombos-bp__author text-xs-regular\">\n {writerName}\n </span>\n )}\n </div>\n\n <h1 className=\"kombos-bp__title text-xl-semibold md:display-xs-semibold\">\n {blogPost.title}\n </h1>\n\n {blogPost.shortDescription && (\n <p className=\"kombos-bp__desc text-md-regular\">\n {blogPost.shortDescription}\n </p>\n )}\n </div>\n\n {hasContent && (\n <div\n className=\"kombos-bp__content kombos-richtext\"\n dangerouslySetInnerHTML={{\n __html: blogPost.blogContent.content,\n }}\n />\n )}\n\n {tags.length > 0 && (\n <>\n <hr className=\"kombos-bp__divider\" />\n <div className=\"kombos-bp__tags\">\n <span className=\"kombos-bp__tags-label text-sm-semibold\">\n {tagsTitle}\n </span>\n <div className=\"kombos-bp__tags-list\">\n {tags.map((tag) => (\n <span\n key={tag.id}\n className=\"kombos-bp__tag text-xs-medium\"\n >\n {tag.name}\n </span>\n ))}\n </div>\n </div>\n </>\n )}\n </div>\n </div>\n </section>\n );\n}\n\nexport default BlogPost;\n",
16718
- "relatedFunctions": [
16719
- "getIkasBlogFormattedDate",
16720
- "getIkasBlogCategoryHref",
16721
- "getDefaultSrc"
16722
- ],
16723
- "categories": [
16724
- "Blog"
16725
- ],
16726
- "files": [
16727
- {
16728
- "filename": "ikas-config-snippet.json",
16729
- "content": "{\n \"id\": \"{{PROJECT_ID}}-blog-post\",\n \"name\": \"BlogPost\",\n \"type\": \"section\",\n \"entry\": \"./src/components/BlogPost/index.tsx\",\n \"styles\": \"./src/components/BlogPost/styles.css\",\n \"props\": [\n {\n \"name\": \"blogPost\",\n \"displayName\": \"Blog Post\",\n \"type\": \"BLOG\",\n \"required\": true,\n \"groupId\": \"content\"\n },\n {\n \"name\": \"homeText\",\n \"displayName\": \"Home Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Ana Page\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"blogText\",\n \"displayName\": \"Blog Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Blog Posts\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"tagsTitle\",\n \"displayName\": \"Labels Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Labels\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"aspectRatio\",\n \"displayName\": \"En Boy Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"appearance\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"objectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"appearance\",\n \"enumTypeId\": \"GrylMqHxui\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"content\",\n \"name\": \"Content\",\n \"description\": \"Blog yazısı veri kaynağı\"\n },\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Section genelinde kullanılan metin etiketleri\",\n \"children\": []\n },\n {\n \"id\": \"appearance\",\n \"name\": \"View\",\n \"children\": [],\n \"description\": \"Image ve stil ayarları\"\n }\n ]\n}"
16730
- },
16731
- {
16732
- "filename": "index.tsx",
16733
- "content": "import {\n getIkasBlogFormattedDate,\n getIkasBlogCategoryHref,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\n\nimport { Props } from \"./types\";\nimport Breadcrumb, { BreadcrumbItem } from \"../../sub-components/Breadcrumb\";\nimport { resolveAspectRatio, resolveObjectFit } from \"../../utils/media\";\nimport { getFullName } from \"../../utils/fullName\";\n\nexport function BlogPost({\n blogPost,\n homeText = \"Ana Sayfa\",\n blogText = \"Blog Yazıları\",\n tagsTitle = \"Etiketler\",\n aspectRatio,\n objectFit,\n}: Props) {\n if (!blogPost) return null;\n\n const heroSrc = blogPost.image ? getDefaultSrc(blogPost.image) : \"\";\n const date = getIkasBlogFormattedDate(blogPost);\n const categoryName = blogPost.category?.name;\n const categoryHref = blogPost.category\n ? getIkasBlogCategoryHref(blogPost.category)\n : \"\";\n const tags = blogPost.tags ?? [];\n const hasContent = blogPost.blogContent?.content;\n\n const writerName = getFullName(blogPost.writer?.firstName, blogPost.writer?.lastName);\n\n const breadcrumbItems: BreadcrumbItem[] = [\n { label: homeText, href: \"/\" },\n { label: blogText, href: \"/blog\" },\n ];\n if (categoryName && categoryHref) {\n breadcrumbItems.push({ label: categoryName, href: categoryHref });\n }\n breadcrumbItems.push({ label: blogPost.title });\n\n return (\n <section className=\"kombos-bp\">\n <div className=\"kombos-container\">\n <div className=\"kombos-bp__article\">\n <Breadcrumb items={breadcrumbItems} size=\"xs\" />\n\n {heroSrc && (\n <div className=\"kombos-bp__hero-wrap\">\n <img\n src={heroSrc}\n alt={blogPost.title}\n className=\"kombos-bp__hero-img\"\n style={{\n aspectRatio: resolveAspectRatio(aspectRatio),\n objectFit: resolveObjectFit(objectFit),\n }}\n />\n </div>\n )}\n\n <div className=\"kombos-bp__header\">\n <div className=\"kombos-bp__meta\">\n {categoryName && categoryHref && (\n <a\n href={categoryHref}\n className=\"kombos-bp__category text-xs-semibold\"\n >\n {categoryName}\n </a>\n )}\n {date && (\n <span className=\"kombos-bp__date text-xs-regular\">{date}</span>\n )}\n {writerName && (\n <span className=\"kombos-bp__author text-xs-regular\">\n {writerName}\n </span>\n )}\n </div>\n\n <h1 className=\"kombos-bp__title text-xl-semibold md:display-xs-semibold\">\n {blogPost.title}\n </h1>\n\n {blogPost.shortDescription && (\n <p className=\"kombos-bp__desc text-md-regular\">\n {blogPost.shortDescription}\n </p>\n )}\n </div>\n\n {hasContent && (\n <div\n className=\"kombos-bp__content kombos-richtext\"\n dangerouslySetInnerHTML={{\n __html: blogPost.blogContent.content,\n }}\n />\n )}\n\n {tags.length > 0 && (\n <>\n <hr className=\"kombos-bp__divider\" />\n <div className=\"kombos-bp__tags\">\n <span className=\"kombos-bp__tags-label text-sm-semibold\">\n {tagsTitle}\n </span>\n <div className=\"kombos-bp__tags-list\">\n {tags.map((tag) => (\n <span\n key={tag.id}\n className=\"kombos-bp__tag text-xs-medium\"\n >\n {tag.name}\n </span>\n ))}\n </div>\n </div>\n </>\n )}\n </div>\n </div>\n </section>\n );\n}\n\nexport default BlogPost;\n"
16734
- },
16735
- {
16736
- "filename": "styles.css",
16737
- "content": ".kombos-bp {\n width: 100%;\n}\n\n.kombos-bp__article {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n padding: 2rem 0;\n}\n\n/* Hero image */\n.kombos-bp__hero-wrap {\n overflow: hidden;\n border-radius: 8px;\n}\n\n.kombos-bp__hero-img {\n width: 100%;\n height: 100%;\n}\n\n/* Header group (meta + title + description) */\n.kombos-bp__header {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n/* Meta row */\n.kombos-bp__meta {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n\n.kombos-bp__category {\n color: var(--kombos-gray-900);\n text-decoration: none;\n transition: color 0.15s ease;\n}\n\n.kombos-bp__category:hover {\n color: var(--kombos-gray-500);\n}\n\n.kombos-bp__date {\n color: var(--kombos-gray-500);\n}\n\n.kombos-bp__author {\n color: var(--kombos-gray-500);\n}\n\n.kombos-bp__meta > * + *::before {\n content: \"\\00B7\";\n margin-right: 0.5rem;\n color: var(--kombos-gray-300);\n}\n\n/* Title */\n.kombos-bp__title {\n margin: 0;\n color: var(--kombos-gray-900);\n}\n\n/* Description */\n.kombos-bp__desc {\n margin: 0;\n color: var(--kombos-gray-500);\n}\n\n/* Divider */\n.kombos-bp__divider {\n border: none;\n border-top: 1px solid var(--kombos-gray-200);\n margin: 0;\n}\n\n/* Content */\n.kombos-bp__content {\n line-height: 1.7;\n}\n\n/* Tags */\n.kombos-bp__tags {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.kombos-bp__tags-label {\n color: var(--kombos-gray-900);\n}\n\n.kombos-bp__tags-list {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n\n.kombos-bp__tag {\n display: inline-flex;\n align-items: center;\n padding: 0.25rem 0.75rem;\n border: 1px solid var(--kombos-gray-200);\n border-radius: 100px;\n color: var(--kombos-gray-700);\n}\n\n\n/* Desktop: center article */\n@media (min-width: 1024px) {\n .kombos-bp__article {\n max-width: 45rem;\n margin-left: auto;\n margin-right: auto;\n }\n}\n"
16738
- },
16739
- {
16740
- "filename": "types.ts",
16741
- "content": "import type { IkasBlog } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../global-types\";\n\nexport interface Props {\n blogPost: IkasBlog | null;\n homeText?: string;\n blogText?: string;\n tagsTitle?: string;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n}\n"
16742
- }
16743
- ]
16744
- },
16745
- {
16746
- "id": "bundle-products",
16747
- "title": "Bundle Products Pattern",
16748
- "description": "Bundle product display and selection. Shows hasBundleSettings, initBundleProducts, bundle variant selection, bundle pricing, and the useBundleProducts hook for managing bundle state.",
16749
- "code": "import {\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport { useBundleProducts } from \"../../hooks/useBundleProducts\";\nimport BundleProductItem from \"./components/BundleProductItem\";\nimport BundleSkeletonLoading from \"./components/BundleSkeletonLoading\";\nimport FurnitureView from \"./components/FurnitureView\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction buildMarginStyles(\n props: Pick<\n Props,\n | \"mobileMarginTop\"\n | \"mobileMarginBottom\"\n | \"desktopMarginTop\"\n | \"desktopMarginBottom\"\n >,\n) {\n return {\n \"--mobile-mt\": getFormattedMarginTopSize(props.mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(props.mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(props.desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(props.desktopMarginBottom),\n } as Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// Main component\n// ---------------------------------------------------------------------------\n\nexport function ProductDetailBundleProduct({\n product,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n isBundleFurniture,\n bundleProductWithoutLink,\n quantityLabel = \"Adet\",\n outOfStockText = \"Stokta yok\",\n productContentTitle = \"Takım içeriği\",\n aspectRatio,\n objectFit,\n}: Props) {\n const { isLoading, selectedVariant, bundleSettings, sortedProducts } =\n useBundleProducts(product);\n\n if (!product || !selectedVariant || !bundleSettings) return null;\n\n const marginStyles = buildMarginStyles({\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n });\n\n const skeletonCount = sortedProducts.length || 3;\n\n if (isBundleFurniture) {\n return (\n <FurnitureView\n marginStyles={marginStyles}\n isLoading={isLoading}\n skeletonCount={skeletonCount}\n sortedProducts={sortedProducts}\n productContentTitle={productContentTitle}\n bundleProductWithoutLink={bundleProductWithoutLink}\n quantityLabel={quantityLabel}\n />\n );\n }\n\n return (\n <div className=\"kombos-bundle\" style={marginStyles}>\n <div className=\"kombos-bundle__list\">\n {isLoading ? (\n <BundleSkeletonLoading count={skeletonCount} />\n ) : (\n sortedProducts.map((bp) => (\n <BundleProductItem\n key={bp.id}\n bundleProduct={bp}\n quantityLabel={quantityLabel}\n outOfStockText={outOfStockText}\n bundleProductWithoutLink={bundleProductWithoutLink}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n />\n ))\n )}\n </div>\n </div>\n );\n}\n\nexport default ProductDetailBundleProduct;\n",
16750
- "relatedFunctions": [
16751
- "getFormattedMarginTopSize",
16752
- "getFormattedMarginBottomSize"
16753
- ],
16754
- "categories": [
16755
- "Product",
16756
- "ProductDetail"
16757
- ],
16758
- "files": [
16759
- {
16760
- "filename": "components/BundleProductItem/index.tsx",
16761
- "content": "import {\n IkasBundleProduct,\n getSelectedProductVariant,\n getProductHref,\n shouldDisplayBundleProductPrice,\n getBundleProductFinalPrice,\n getBundleProductSellPrice,\n getBundleProductFormattedFinalPrice,\n getBundleProductFormattedSellPrice,\n getBundleProductFormattedFinalPriceWithQuantity,\n getBundleProductFormattedSellPriceWithQuantity,\n getBundleProductFinalPriceWithQuantity,\n getBundleProductSellPriceWithQuantity,\n hasProductVariantStock,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport VariantBadge from \"../../../../sub-components/VariantBadge\";\nimport BundleQuantityBox from \"../../../../sub-components/BundleQuantityBox\";\nimport BundleMedia from \"../../../../sub-components/BundleMedia\";\nimport { adjustBundleProductQuantity } from \"../../../../utils/bundle\";\nimport type { AspectRatio, ObjectFit } from \"../../../../global-types\";\n\ninterface Props {\n bundleProduct: IkasBundleProduct;\n quantityLabel: string;\n outOfStockText: string;\n bundleProductWithoutLink?: boolean;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n}\n\nconst BundleProductItem = observer(function BundleProductItem({\n bundleProduct,\n quantityLabel,\n outOfStockText,\n bundleProductWithoutLink,\n aspectRatio = \"Square\",\n objectFit = \"Cover\",\n}: Props) {\n const product = bundleProduct.product;\n if (!product) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\n const hasStock = selectedVariant\n ? hasProductVariantStock(selectedVariant)\n : false;\n\n const productHref = getProductHref(product);\n\n const showPrice = shouldDisplayBundleProductPrice(bundleProduct);\n const unitFinalPrice = getBundleProductFinalPrice(bundleProduct);\n const unitSellPrice = getBundleProductSellPrice(bundleProduct);\n const hasDiscount = showPrice && unitFinalPrice !== unitSellPrice;\n\n const hasQuantity = bundleProduct.quantity > 1;\n\n const totalFinalPrice = getBundleProductFinalPriceWithQuantity(bundleProduct);\n const totalSellPrice = getBundleProductSellPriceWithQuantity(bundleProduct);\n const hasTotalDiscount =\n showPrice &&\n totalFinalPrice !== totalSellPrice &&\n bundleProduct.quantity > 0;\n\n const ImageWrapper = bundleProductWithoutLink ? \"div\" : \"a\";\n const imageWrapperProps = bundleProductWithoutLink\n ? {}\n : { href: productHref, \"aria-label\": product.name };\n\n const NameWrapper = bundleProductWithoutLink ? \"span\" : \"a\";\n const nameWrapperProps = bundleProductWithoutLink\n ? {}\n : { href: productHref };\n\n return (\n <div className=\"kombos-bundle-item\">\n {/* Image */}\n <ImageWrapper\n className=\"kombos-bundle-item__image-wrap\"\n {...imageWrapperProps}\n >\n <BundleMedia\n variant={selectedVariant}\n alt={product.name}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n wrapperClassName=\"kombos-bundle-item__media-inner\"\n mediaClassName=\"kombos-bundle-item__image\"\n placeholderClassName=\"kombos-bundle-item__image-placeholder\"\n />\n </ImageWrapper>\n {/* Info */}\n <div className=\"kombos-bundle-item__info\">\n {/* Header: name + total price (quantity × unit) */}\n <div className=\"kombos-bundle-item__header\">\n <NameWrapper\n className=\"kombos-bundle-item__name text-sm-semibold\"\n {...nameWrapperProps}\n >\n {product.name}\n </NameWrapper>\n {showPrice && hasQuantity && (\n <div className=\"kombos-bundle-item__total-price\">\n {hasTotalDiscount && (\n <span className=\"kombos-bundle-item__total-sell text-sm-regular-strike\">\n {getBundleProductFormattedSellPriceWithQuantity(\n bundleProduct,\n )}\n </span>\n )}\n <span className=\"kombos-bundle-item__total-final text-sm-semibold\">\n {getBundleProductFormattedFinalPriceWithQuantity(bundleProduct)}\n </span>\n </div>\n )}\n {showPrice && !hasQuantity && (\n <div className=\"kombos-bundle-item__unit-price\">\n {hasDiscount && (\n <span className=\"kombos-bundle-item__sell-price text-sm-regular-strike\">\n {getBundleProductFormattedSellPrice(bundleProduct)}\n </span>\n )}\n <span className=\"kombos-bundle-item__final-price text-sm-semibold\">\n {getBundleProductFormattedFinalPrice(bundleProduct)}\n </span>\n </div>\n )}\n </div>\n\n {/* Variants */}\n <VariantBadge\n product={product}\n disableRoute\n size=\"xs\"\n onSelect={() => adjustBundleProductQuantity(bundleProduct)}\n />\n\n {/* Bottom: quantity + unit price + out of stock */}\n <div className=\"kombos-bundle-item__bottom\">\n <div className=\"kombos-bundle-item__qty-row\">\n <span className=\"kombos-bundle-item__qty-label text-xs-regular\">\n {quantityLabel}\n </span>\n <BundleQuantityBox bundleProduct={bundleProduct} />\n {showPrice && bundleProduct.quantity > 1 && (\n <div className=\"kombos-bundle-item__unit-price\">\n {hasDiscount && (\n <span className=\"kombos-bundle-item__sell-price text-sm-regular-strike\">\n {getBundleProductFormattedSellPrice(bundleProduct)}\n </span>\n )}\n <span className=\"kombos-bundle-item__final-price text-sm-semibold\">\n {getBundleProductFormattedFinalPrice(bundleProduct)}\n </span>\n </div>\n )}\n </div>\n\n {!hasStock && (\n <span className=\"kombos-bundle-item__out-of-stock text-xs-medium\">\n {outOfStockText}\n </span>\n )}\n </div>\n </div>\n </div>\n );\n});\n\nexport default BundleProductItem;\n"
16762
- },
16763
- {
16764
- "filename": "components/BundleProductItem/styles.css",
16765
- "content": "/* ===== BundleProductItem ===== */\n.kombos-bundle-item {\n display: flex;\n gap: 0.75rem;\n padding: 0.75rem 0;\n border-bottom: 1px solid var(--kombos-gray-100);\n}\n\n.kombos-bundle-item__image-wrap {\n display: block;\n width: 6.25rem;\n min-width: 6.25rem;\n height: fit-content;\n border-radius: 6px;\n overflow: hidden;\n background-color: var(--kombos-gray-50);\n text-decoration: none;\n}\n\n.kombos-bundle-item__image {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.kombos-bundle-item__image-placeholder {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n font-size: 2rem;\n color: var(--kombos-gray-300);\n}\n\n.kombos-bundle-item__info {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n flex: 1;\n min-width: 0;\n}\n\n.kombos-bundle-item__header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 0.75rem;\n}\n\n.kombos-bundle-item__name {\n color: var(--kombos-gray-900);\n text-decoration: none;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n width: fit-content;\n}\n\na.kombos-bundle-item__name:hover {\n text-decoration: underline;\n}\n\n.kombos-bundle-item__unit-price {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n flex-shrink: 0;\n white-space: nowrap;\n margin-left: auto;\n}\n\n\n.kombos-bundle-item__final-price {\n color: var(--kombos-gray-900);\n}\n\n.kombos-bundle-item__sell-price {\n color: var(--kombos-gray-500);\n}\n\n.kombos-bundle-item__bottom {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n margin-top: auto;\n}\n\n.kombos-bundle-item__qty-row {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.kombos-bundle-item__qty-label {\n color: var(--kombos-gray-500);\n}\n\n.kombos-bundle-item__total-price {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n flex-shrink: 0;\n white-space: nowrap;\n}\n\n.kombos-bundle-item__total-final {\n color: var(--kombos-gray-900);\n}\n\n.kombos-bundle-item__total-sell {\n color: var(--kombos-gray-500);\n}\n\n.kombos-bundle-item__out-of-stock {\n color: var(--kombos-error);\n}\n\n/* ===== Mobile adjustments (< 768px) ===== */\n@media (max-width: 767px) {\n .kombos-bundle-item__image-wrap {\n width: 4.5rem;\n min-width: 4.5rem;\n }\n\n .kombos-bundle-item__header {\n flex-direction: column;\n gap: 0.25rem;\n }\n\n .kombos-bundle-item__unit-price {\n margin-left: 0;\n }\n\n .kombos-bundle-item__qty-row {\n flex-wrap: wrap;\n }\n}\n\n"
16766
- },
16767
- {
16768
- "filename": "components/BundleSkeletonLoading/index.tsx",
16769
- "content": "interface Props {\n count?: number;\n variant?: \"card\" | \"table\";\n}\n\nexport default function BundleSkeletonLoading({ count = 3, variant = \"card\" }: Props) {\n if (variant === \"table\") {\n return (\n <div className=\"kombos-bundle-skeleton-table\">\n <div className=\"kombos-bundle-skeleton-table__header kombos-bundle-skeleton__shimmer\" />\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className=\"kombos-bundle-skeleton-table__row\">\n <div className=\"kombos-bundle-skeleton__bar kombos-bundle-skeleton__bar--wide kombos-bundle-skeleton__shimmer\" />\n <div className=\"kombos-bundle-skeleton__bar kombos-bundle-skeleton__bar--price kombos-bundle-skeleton__shimmer\" />\n </div>\n ))}\n </div>\n );\n }\n\n return (\n <div className=\"kombos-bundle-skeleton\">\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className=\"kombos-bundle-skeleton__item\">\n <div className=\"kombos-bundle-skeleton__image kombos-bundle-skeleton__shimmer\" />\n <div className=\"kombos-bundle-skeleton__info\">\n <div className=\"kombos-bundle-skeleton__bar kombos-bundle-skeleton__bar--wide kombos-bundle-skeleton__shimmer\" />\n <div className=\"kombos-bundle-skeleton__bar kombos-bundle-skeleton__bar--medium kombos-bundle-skeleton__shimmer\" />\n <div className=\"kombos-bundle-skeleton__bar kombos-bundle-skeleton__bar--narrow kombos-bundle-skeleton__shimmer\" />\n </div>\n </div>\n ))}\n </div>\n );\n}\n"
16770
- },
16771
- {
16772
- "filename": "components/BundleSkeletonLoading/styles.css",
16773
- "content": "/* ===== BundleSkeletonLoading ===== */\n.kombos-bundle-skeleton {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.kombos-bundle-skeleton__item {\n display: flex;\n gap: 0.75rem;\n padding: 0.75rem 0;\n border-bottom: 1px solid var(--kombos-gray-100);\n}\n\n.kombos-bundle-skeleton__image {\n width: 6.25rem;\n min-width: 6.25rem;\n height: 4.75rem;\n border-radius: 6px;\n background-color: var(--kombos-gray-100);\n}\n\n.kombos-bundle-skeleton__info {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n flex: 1;\n}\n\n.kombos-bundle-skeleton__bar {\n height: 0.875rem;\n border-radius: 4px;\n background-color: var(--kombos-gray-100);\n}\n\n.kombos-bundle-skeleton__bar--wide {\n width: 70%;\n}\n\n.kombos-bundle-skeleton__bar--medium {\n width: 50%;\n}\n\n.kombos-bundle-skeleton__bar--narrow {\n width: 35%;\n}\n\n.kombos-bundle-skeleton__bar--price {\n width: 4.5rem;\n margin-left: auto;\n}\n\n/* ===== Table variant ===== */\n.kombos-bundle-skeleton-table {\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n overflow: hidden;\n}\n\n.kombos-bundle-skeleton-table__header {\n height: 2.75rem;\n background-color: var(--kombos-gray-200);\n}\n\n.kombos-bundle-skeleton-table__row {\n display: flex;\n align-items: center;\n padding: 0.75rem 1rem;\n border-bottom: 1px solid var(--kombos-gray-100);\n}\n\n.kombos-bundle-skeleton-table__row:last-child {\n border-bottom: none;\n}\n\n.kombos-bundle-skeleton__shimmer {\n background: linear-gradient(\n 90deg,\n var(--kombos-gray-100) 25%,\n var(--kombos-gray-50) 50%,\n var(--kombos-gray-100) 75%\n );\n background-size: 200% 100%;\n animation: kombos-shimmer 1.5s ease-in-out infinite;\n}\n"
16774
- },
16775
- {
16776
- "filename": "components/FurnitureRow/index.tsx",
16777
- "content": "import {\n shouldDisplayBundleProductPrice,\n getBundleProductFormattedFinalPrice,\n getProductHref,\n IkasBundleProduct,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\n\ninterface Props {\n bundleProduct: IkasBundleProduct;\n withoutLink?: boolean;\n quantityLabel: string;\n}\n\nconst FurnitureRow = observer(function FurnitureRow({\n bundleProduct,\n withoutLink,\n quantityLabel,\n}: Props) {\n const product = bundleProduct.product;\n if (!product || bundleProduct.quantity === 0) return null;\n\n const showPrice = shouldDisplayBundleProductPrice(bundleProduct);\n const formattedPrice = showPrice\n ? getBundleProductFormattedFinalPrice(bundleProduct)\n : \"\";\n const productHref = getProductHref(product);\n\n const NameTag = withoutLink ? \"span\" : \"a\";\n const nameProps = withoutLink ? {} : { href: productHref };\n\n return (\n <tr className=\"kombos-bundle-table__row\">\n <td className=\"kombos-bundle-table__cell kombos-bundle-table__cell--name\">\n <NameTag\n className=\"kombos-bundle-table__link text-sm-regular\"\n {...nameProps}\n >\n {product.name}\n </NameTag>\n </td>\n <td className=\"kombos-bundle-table__cell kombos-bundle-table__cell--price text-sm-medium\">\n {showPrice && formattedPrice\n ? `${bundleProduct.quantity} x ${formattedPrice}`\n : `${quantityLabel} ${bundleProduct.quantity}`}\n </td>\n </tr>\n );\n});\n\nexport default FurnitureRow;\n"
16778
- },
16779
- {
16780
- "filename": "components/FurnitureRow/styles.css",
16781
- "content": ".kombos-bundle-table__row {\n border-bottom: 1px solid var(--kombos-gray-200);\n}\n\n.kombos-bundle-table__row:last-child {\n border-bottom: none;\n}\n\n.kombos-bundle-table__cell {\n padding: 0.75rem 1rem;\n}\n\n.kombos-bundle-table__cell--name {\n color: var(--kombos-gray-900);\n}\n\n.kombos-bundle-table__link {\n color: inherit;\n text-decoration: none;\n}\n\na.kombos-bundle-table__link:hover {\n text-decoration: underline;\n}\n\n.kombos-bundle-table__cell--price {\n color: var(--kombos-gray-900);\n text-align: right;\n white-space: nowrap;\n}\n"
16782
- },
16783
- {
16784
- "filename": "components/FurnitureView/index.tsx",
16785
- "content": "import { IkasBundleProduct } from \"@ikas/bp-storefront\";\nimport BundleSkeletonLoading from \"../BundleSkeletonLoading\";\nimport FurnitureRow from \"../FurnitureRow\";\n\ninterface Props {\n marginStyles: Record<string, string>;\n isLoading: boolean;\n skeletonCount: number;\n sortedProducts: IkasBundleProduct[];\n productContentTitle: string;\n bundleProductWithoutLink?: boolean;\n quantityLabel: string;\n}\n\nexport default function FurnitureView({\n marginStyles,\n isLoading,\n skeletonCount,\n sortedProducts,\n productContentTitle,\n bundleProductWithoutLink,\n quantityLabel,\n}: Props) {\n return (\n <div\n className=\"kombos-bundle\"\n style={{ ...marginStyles, \"--kombos-bundle-table-border\": \"var(--kombos-gray-900)\" }}\n >\n {isLoading ? (\n <BundleSkeletonLoading count={skeletonCount} variant=\"table\" />\n ) : (\n <div className=\"kombos-bundle-table\">\n <div className=\"kombos-bundle-table__header\">\n <span className=\"kombos-bundle-table__title text-md-semibold\">\n {productContentTitle}\n </span>\n </div>\n <table className=\"kombos-bundle-table__table\">\n <tbody>\n {sortedProducts.map((bp) => (\n <FurnitureRow\n key={bp.id}\n bundleProduct={bp}\n withoutLink={bundleProductWithoutLink}\n quantityLabel={quantityLabel}\n />\n ))}\n </tbody>\n </table>\n </div>\n )}\n </div>\n );\n}\n"
16786
- },
16787
- {
16788
- "filename": "components/FurnitureView/styles.css",
16789
- "content": ".kombos-bundle-table {\n border: 1px solid var(--kombos-bundle-table-border, var(--kombos-gray-900));\n border-radius: 6px;\n overflow: hidden;\n}\n\n.kombos-bundle-table__header {\n display: flex;\n align-items: center;\n padding: 0.75rem 1rem;\n background-color: var(--kombos-gray-900);\n color: var(--kombos-white);\n}\n\n.kombos-bundle-table__title {\n color: inherit;\n}\n\n.kombos-bundle-table__table {\n width: 100%;\n border-collapse: collapse;\n}\n"
16790
- },
16791
- {
16792
- "filename": "ikas-config-snippet.json",
16793
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-bundle-product\",\n \"name\": \"ProductDetailBundleProduct\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailBundleProduct/index.tsx\",\n \"styles\": \"./src/components/ProductDetailBundleProduct/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"isBundleFurniture\",\n \"displayName\": \"Furniture Bundle Mode\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"settings\"\n },\n {\n \"name\": \"bundleProductWithoutLink\",\n \"displayName\": \"Product Link Close\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"settings\"\n },\n {\n \"name\": \"quantityLabel\",\n \"displayName\": \"Quantity Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Quantity\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"outOfStockText\",\n \"displayName\": \"Stokta Yok Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Stokta yok\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"productContentTitle\",\n \"displayName\": \"Set Content Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Set içeriği\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"aspectRatio\",\n \"displayName\": \"En Boy Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"appearance\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"objectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"appearance\",\n \"enumTypeId\": \"GrylMqHxui\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n },\n {\n \"id\": \"settings\",\n \"name\": \"Settings\",\n \"description\": \"Bundle davranışını kontrol eden ayarlar\"\n },\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Bileşende görüntülenen metin etiketleri\"\n }\n ]\n}"
16794
- },
16795
- {
16796
- "filename": "index.tsx",
16797
- "content": "import {\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport { useBundleProducts } from \"../../hooks/useBundleProducts\";\nimport BundleProductItem from \"./components/BundleProductItem\";\nimport BundleSkeletonLoading from \"./components/BundleSkeletonLoading\";\nimport FurnitureView from \"./components/FurnitureView\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction buildMarginStyles(\n props: Pick<\n Props,\n | \"mobileMarginTop\"\n | \"mobileMarginBottom\"\n | \"desktopMarginTop\"\n | \"desktopMarginBottom\"\n >,\n) {\n return {\n \"--mobile-mt\": getFormattedMarginTopSize(props.mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(props.mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(props.desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(props.desktopMarginBottom),\n } as Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// Main component\n// ---------------------------------------------------------------------------\n\nexport function ProductDetailBundleProduct({\n product,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n isBundleFurniture,\n bundleProductWithoutLink,\n quantityLabel = \"Adet\",\n outOfStockText = \"Stokta yok\",\n productContentTitle = \"Takım içeriği\",\n aspectRatio,\n objectFit,\n}: Props) {\n const { isLoading, selectedVariant, bundleSettings, sortedProducts } =\n useBundleProducts(product);\n\n if (!product || !selectedVariant || !bundleSettings) return null;\n\n const marginStyles = buildMarginStyles({\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n });\n\n const skeletonCount = sortedProducts.length || 3;\n\n if (isBundleFurniture) {\n return (\n <FurnitureView\n marginStyles={marginStyles}\n isLoading={isLoading}\n skeletonCount={skeletonCount}\n sortedProducts={sortedProducts}\n productContentTitle={productContentTitle}\n bundleProductWithoutLink={bundleProductWithoutLink}\n quantityLabel={quantityLabel}\n />\n );\n }\n\n return (\n <div className=\"kombos-bundle\" style={marginStyles}>\n <div className=\"kombos-bundle__list\">\n {isLoading ? (\n <BundleSkeletonLoading count={skeletonCount} />\n ) : (\n sortedProducts.map((bp) => (\n <BundleProductItem\n key={bp.id}\n bundleProduct={bp}\n quantityLabel={quantityLabel}\n outOfStockText={outOfStockText}\n bundleProductWithoutLink={bundleProductWithoutLink}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n />\n ))\n )}\n </div>\n </div>\n );\n}\n\nexport default ProductDetailBundleProduct;\n"
16798
- },
16799
- {
16800
- "filename": "styles.css",
16801
- "content": "/* ===== ProductDetailBundleProduct ===== */\n.kombos-bundle {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n.kombos-bundle__list {\n display: flex;\n flex-direction: column;\n}\n\n@media (min-width: 1024px) {\n .kombos-bundle {\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n}\n"
16802
- },
16803
- {
16804
- "filename": "types.ts",
16805
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../global-types\";\n\nexport interface Props {\n product?: IkasProduct | null;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n isBundleFurniture?: boolean;\n bundleProductWithoutLink?: boolean;\n quantityLabel?: string;\n outOfStockText?: string;\n productContentTitle?: string;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n}\n"
16806
- }
16807
- ]
16808
- },
16809
- {
16810
- "id": "cart-section",
16811
- "title": "Cart Section",
16812
- "description": "Full shopping cart page with item list, quantity controls, item removal, coupon code input, order summary with adjustments, and checkout button. Uses cartStore for reactive cart state.",
16813
- "code": "import {\n cartStore,\n hasCart,\n removeItem,\n getIkasOrderTotalItemCount,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport PageLoader from \"../../sub-components/PageLoader\";\nimport CartItem from \"../../sub-components/CartItem\";\nimport OrderSummary from \"./components/OrderSummary\";\nimport EmptyState from \"./components/EmptyState\";\n\nexport function CartPage({\n title = \"Sepet\",\n emptyCartText = \"Sepetinizde Ürün Bulunmamaktadır\",\n subtotalLabel = \"Ara Toplam\",\n totalLabel = \"Toplam\",\n checkoutButtonText = \"Siparişi Tamamla\",\n itemsText = \"ürün\",\n couponToggleText = \"Hediye Kodunu Kullan!\",\n couponPlaceholder = \"Kupon kodu girin\",\n couponApplyText = \"Uygula\",\n couponRemoveText = \"Kaldır\",\n couponApplyingText = \"Uygulanıyor...\",\n orderSummaryTitle = \"Sipariş Özeti\",\n continueShoppingText = \"Alışverişe Başla\",\n}: Props) {\n const cart = cartStore.cart;\n const isLoading = cartStore.isCartLoading;\n const cartHasItems = hasCart(cartStore);\n const lineItems = (cart?.orderLineItems ?? []).filter(\n (item) => !item?.deleted,\n );\n const totalItemCount = cart ? getIkasOrderTotalItemCount(cart) : 0;\n\n const handleRemove = async (item: IkasOrderLineItem) => {\n await removeItem(item);\n };\n\n if (isLoading) {\n return <PageLoader />;\n }\n\n if (!cartHasItems) {\n return (\n <section className=\"cart-page\">\n <div className=\"kombos-container cart-page__container\">\n <header className=\"cart-page__header\">\n <h1 className=\"cart-page__title text-xl-semibold md:display-xs-semibold\">{title}</h1>\n </header>\n <EmptyState\n emptyCartText={emptyCartText}\n continueShoppingText={continueShoppingText}\n />\n </div>\n </section>\n );\n }\n\n return (\n <section className=\"cart-page\">\n <div className=\"kombos-container cart-page__container\">\n <header className=\"cart-page__header\">\n <h1 className=\"cart-page__title text-xl-semibold md:display-xs-semibold\">\n {title}{\" \"}\n <span className=\"cart-page__count text-lg-regular\">\n ({totalItemCount} {itemsText})\n </span>\n </h1>\n </header>\n\n <div className=\"cart-page__layout\">\n <div className=\"cart-page__items-col\">\n <div className=\"cart-page__items\">\n {lineItems.map((item) => (\n <CartItem\n key={item.id}\n item={item}\n onRemove={handleRemove}\n variant=\"page\"\n />\n ))}\n </div>\n </div>\n\n {cart && (\n <OrderSummary\n cart={cart}\n title={orderSummaryTitle}\n subtotalLabel={subtotalLabel}\n totalLabel={totalLabel}\n checkoutButtonText={checkoutButtonText}\n couponToggleText={couponToggleText}\n couponPlaceholder={couponPlaceholder}\n couponApplyText={couponApplyText}\n couponRemoveText={couponRemoveText}\n couponApplyingText={couponApplyingText}\n />\n )}\n </div>\n </div>\n </section>\n );\n}\n\nexport default CartPage;\n",
16814
- "relatedFunctions": [
16815
- "cartStore",
16816
- "hasCart",
16817
- "removeItem",
16818
- "getIkasOrderTotalItemCount"
16819
- ],
16820
- "categories": [
16821
- "Cart"
16822
- ],
16823
- "files": [
16824
- {
16825
- "filename": "components/CouponCode/index.tsx",
16826
- "content": "import { useState, useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getCouponCodeForm,\n initCouponCodeForm,\n setCouponCodeFormCouponCode,\n submitCouponCodeForm,\n removeCouponCodeForm,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport Button from \"../../../../sub-components/Button\";\nimport Input from \"../../../../sub-components/Input\";\n\ninterface Props {\n appliedCoupon?: string | null;\n toggleText: string;\n placeholder: string;\n applyText: string;\n removeText: string;\n applyingText: string;\n}\n\nconst CouponCode = observer(function CouponCode({\n appliedCoupon,\n toggleText,\n placeholder,\n applyText,\n removeText,\n applyingText,\n}: Props) {\n const [isOpen, setIsOpen] = useState(false);\n const couponForm = getCouponCodeForm(customerStore);\n\n useEffect(() => {\n initCouponCodeForm(couponForm);\n }, [couponForm]);\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitCouponCodeForm(couponForm);\n };\n\n const handleRemove = async () => {\n await removeCouponCodeForm(couponForm);\n setIsOpen(false);\n };\n\n if (appliedCoupon) {\n return (\n <div className=\"coupon-code\">\n <div className=\"coupon-code__applied\">\n <span className=\"coupon-code__value text-sm-medium\">\n {appliedCoupon}\n </span>\n <button\n type=\"button\"\n className=\"coupon-code__remove text-sm-medium\"\n onClick={handleRemove}\n >\n {removeText}\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"coupon-code\">\n {!isOpen ? (\n <button\n type=\"button\"\n className=\"coupon-code__toggle text-sm-semibold\"\n onClick={() => setIsOpen(true)}\n >\n {toggleText}\n </button>\n ) : (\n <form className=\"coupon-code__form\" onSubmit={handleSubmit}>\n <Input\n placeholder={couponForm.couponCode?.placeholder ?? placeholder}\n value={couponForm.couponCode?.value ?? \"\"}\n onInput={(e: Event) =>\n setCouponCodeFormCouponCode(\n couponForm,\n (e.target as HTMLInputElement).value,\n )\n }\n status={couponForm.isFailure ? \"error\" : undefined}\n />\n <Button\n variant=\"secondary\"\n size=\"xs\"\n disabled={couponForm.isSubmitting}\n >\n {couponForm.isSubmitting ? applyingText : applyText}\n </Button>\n </form>\n )}\n {couponForm.isFailure && couponForm.responseMessage && (\n <p className=\"coupon-code__error text-xs-regular\">\n {couponForm.responseMessage}\n </p>\n )}\n </div>\n );\n});\n\nexport default CouponCode;\n"
16827
- },
16828
- {
16829
- "filename": "components/CouponCode/styles.css",
16830
- "content": "/* CouponCode */\n.coupon-code {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.coupon-code__toggle {\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n color: var(--kombos-gray-900);\n text-decoration: underline;\n text-align: left;\n}\n\n.coupon-code__toggle:hover {\n color: var(--kombos-gray-700);\n}\n\n.coupon-code__form {\n display: flex;\n gap: 0.5rem;\n align-items: stretch;\n}\n\n.coupon-code__form > :first-child {\n flex: 1;\n min-width: 0;\n}\n\n.coupon-code__form > :last-child {\n flex-shrink: 0;\n white-space: nowrap;\n}\n\n.coupon-code__applied {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.625rem 0.875rem;\n background-color: var(--kombos-gray-50);\n border-radius: 6px;\n min-height: 2.75rem;\n}\n\n.coupon-code__value {\n color: var(--kombos-gray-900);\n}\n\n.coupon-code__remove {\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n color: var(--kombos-gray-500);\n text-decoration: underline;\n}\n\n.coupon-code__remove:hover {\n color: var(--kombos-gray-700);\n}\n\n.coupon-code__error {\n color: var(--kombos-error);\n margin: 0;\n}\n"
16831
- },
16832
- {
16833
- "filename": "components/EmptyState/index.tsx",
16834
- "content": "import { Router } from \"@ikas/bp-storefront\";\nimport { Handbag1SVG } from \"../../../../sub-components/icons\";\nimport Button from \"../../../../sub-components/Button\";\n\ninterface Props {\n emptyCartText: string;\n continueShoppingText: string;\n}\n\nexport default function EmptyState({\n emptyCartText,\n continueShoppingText,\n}: Props) {\n const handleContinueShopping = () => {\n Router.navigate(\"/\");\n };\n\n return (\n <div className=\"cart-page__empty\">\n <span className=\"cart-page__empty-icon\">\n <Handbag1SVG />\n </span>\n <p className=\"cart-page__empty-text text-xl-semibold md:display-xs-semibold\">\n {emptyCartText}\n </p>\n <Button variant=\"primary\" size=\"s\" onClick={handleContinueShopping}>\n {continueShoppingText}\n </Button>\n </div>\n );\n}\n"
16835
- },
16836
- {
16837
- "filename": "components/EmptyState/styles.css",
16838
- "content": "/* EmptyState */\n.cart-page__empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1.5rem;\n padding: 5rem 0;\n}\n\n.cart-page__empty-icon {\n font-size: 3rem;\n color: var(--kombos-gray-300);\n}\n\n.cart-page__empty-text {\n color: var(--kombos-gray-700);\n margin: 0;\n}\n"
16839
- },
16840
- {
16841
- "filename": "components/OrderSummary/index.tsx",
16842
- "content": "import {\n cartStore,\n IkasCart,\n getIkasOrderFormattedTotalFinalPrice,\n getIkasOrderFormattedTotalPrice,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n getOrderAdjustmentIsDecrement,\n getCheckoutUrlFromCartStore,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport Button from \"../../../../sub-components/Button\";\nimport CouponCode from \"../CouponCode\";\nimport { cx } from \"../../../../utils/cx\";\n\ninterface Props {\n cart: IkasCart;\n title: string;\n subtotalLabel: string;\n totalLabel: string;\n checkoutButtonText: string;\n couponToggleText: string;\n couponPlaceholder: string;\n couponApplyText: string;\n couponRemoveText: string;\n couponApplyingText: string;\n}\n\nconst OrderSummary = observer(function OrderSummary({\n cart,\n title,\n subtotalLabel,\n totalLabel,\n checkoutButtonText,\n couponToggleText,\n couponPlaceholder,\n couponApplyText,\n couponRemoveText,\n couponApplyingText,\n}: Props) {\n const adjustments = cart.orderAdjustments ?? [];\n\n return (\n <div className=\"order-summary\">\n <h2 className=\"order-summary__title text-md-semibold md:text-lg-semibold\">{title}</h2>\n\n <div className=\"order-summary__rows\">\n <div className=\"order-summary__row\">\n <span className=\"text-md-regular\">{subtotalLabel}</span>\n <span className=\"text-md-semibold\">\n {getIkasOrderFormattedTotalPrice(cart)}\n </span>\n </div>\n\n {adjustments.map((adj, i) => {\n const isDecrement = !!getOrderAdjustmentIsDecrement(adj);\n\n return (\n <div key={i} className=\"order-summary__row\">\n <span className=\"text-sm-regular\">\n {getOrderAdjustmentDisplayName(adj)}\n </span>\n <span\n className={cx(\n \"order-summary__adjustment-amount text-sm-semibold\",\n isDecrement && \"order-summary__adjustment-amount--discount\",\n )}\n >\n {getOrderAdjustmentFormattedAmount(adj)}\n </span>\n </div>\n );\n })}\n </div>\n\n <div className=\"order-summary__divider\" />\n\n <div className=\"order-summary__row order-summary__total\">\n <span className=\"text-md-semibold md:text-lg-semibold\">{totalLabel}</span>\n <span className=\"text-md-semibold md:text-lg-semibold\">\n {getIkasOrderFormattedTotalFinalPrice(cart)}\n </span>\n </div>\n\n <CouponCode\n appliedCoupon={cart.couponCode}\n toggleText={couponToggleText}\n placeholder={couponPlaceholder}\n applyText={couponApplyText}\n removeText={couponRemoveText}\n applyingText={couponApplyingText}\n />\n\n <a\n className=\"order-summary__checkout-link\"\n href={getCheckoutUrlFromCartStore(cartStore)}\n >\n <Button variant=\"primary\" className=\"order-summary__checkout-btn\">\n {checkoutButtonText}\n </Button>\n </a>\n </div>\n );\n});\n\nexport default OrderSummary;\n"
16843
- },
16844
- {
16845
- "filename": "components/OrderSummary/styles.css",
16846
- "content": "/* OrderSummary */\n.order-summary {\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n padding: 1.25rem;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n background-color: var(--kombos-white);\n min-width: 0;\n}\n\n@media (min-width: 768px) {\n .order-summary {\n padding: 1.5rem;\n }\n}\n\n@media (min-width: 1024px) {\n .order-summary {\n position: sticky;\n top: 7rem;\n }\n}\n\n.order-summary__title {\n margin: 0;\n color: var(--kombos-gray-900);\n}\n\n.order-summary__rows {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.order-summary__row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n color: var(--kombos-gray-700);\n}\n\n.order-summary__adjustment-amount {\n color: var(--kombos-gray-500);\n}\n\n.order-summary__adjustment-amount--discount {\n color: var(--kombos-error);\n}\n\n.order-summary__divider {\n height: 1px;\n background-color: var(--kombos-gray-200);\n}\n\n.order-summary__total {\n color: var(--kombos-gray-900);\n}\n\n/* Checkout */\n.order-summary__checkout-link {\n display: block;\n text-decoration: none;\n}\n\n.order-summary__checkout-btn {\n display: flex;\n width: 100%;\n}\n"
16847
- },
16848
- {
16849
- "filename": "ikas-config-snippet.json",
16850
- "content": "{\n \"id\": \"{{PROJECT_ID}}-cart-page\",\n \"name\": \"CartPage\",\n \"type\": \"section\",\n \"entry\": \"./src/components/CartPage/index.tsx\",\n \"styles\": \"./src/components/CartPage/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Cart\",\n \"groupId\": \"page\"\n },\n {\n \"name\": \"emptyCartText\",\n \"displayName\": \"Empty Cart Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Your cart is empty\",\n \"groupId\": \"page\"\n },\n {\n \"name\": \"subtotalLabel\",\n \"displayName\": \"Subtotal Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Subtotal\",\n \"groupId\": \"summary\"\n },\n {\n \"name\": \"totalLabel\",\n \"displayName\": \"Total Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Total\",\n \"groupId\": \"summary\"\n },\n {\n \"name\": \"checkoutButtonText\",\n \"displayName\": \"Payment Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Complete Order\",\n \"groupId\": \"summary\"\n },\n {\n \"name\": \"itemsText\",\n \"displayName\": \"Product Count Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"ürün\",\n \"groupId\": \"page\"\n },\n {\n \"name\": \"couponPlaceholder\",\n \"displayName\": \"Coupon Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Enter coupon code\",\n \"groupId\": \"coupon\"\n },\n {\n \"name\": \"couponApplyText\",\n \"displayName\": \"Coupon Apply Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Apply\",\n \"groupId\": \"coupon\"\n },\n {\n \"name\": \"couponRemoveText\",\n \"displayName\": \"Coupon Remove Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Remove\",\n \"groupId\": \"coupon\"\n },\n {\n \"name\": \"couponApplyingText\",\n \"displayName\": \"Coupon Applying Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Uygulanıyor...\",\n \"groupId\": \"coupon\"\n },\n {\n \"name\": \"orderSummaryTitle\",\n \"displayName\": \"Order Summary Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Order Summary\",\n \"groupId\": \"summary\"\n },\n {\n \"name\": \"couponToggleText\",\n \"displayName\": \"Coupon Toggle Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Use Gift Code!\",\n \"groupId\": \"coupon\"\n },\n {\n \"name\": \"continueShoppingText\",\n \"displayName\": \"Continue Shopping Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Start Shopping\",\n \"groupId\": \"page\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"page\",\n \"name\": \"Page\",\n \"description\": \"Page başlığı, yükleme ve boş sepet metinleri\"\n },\n {\n \"id\": \"summary\",\n \"name\": \"Order Summary\",\n \"description\": \"Order özeti başlığı, fiyat etiketleri ve ödeme butonu\"\n },\n {\n \"id\": \"coupon\",\n \"name\": \"Coupon Code\",\n \"description\": \"Coupon kodu toggle, giriş alanı ve buton metinleri\"\n }\n ]\n}"
16851
- },
16852
- {
16853
- "filename": "index.tsx",
16854
- "content": "import {\n cartStore,\n hasCart,\n removeItem,\n getIkasOrderTotalItemCount,\n IkasOrderLineItem,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport PageLoader from \"../../sub-components/PageLoader\";\nimport CartItem from \"../../sub-components/CartItem\";\nimport OrderSummary from \"./components/OrderSummary\";\nimport EmptyState from \"./components/EmptyState\";\n\nexport function CartPage({\n title = \"Sepet\",\n emptyCartText = \"Sepetinizde Ürün Bulunmamaktadır\",\n subtotalLabel = \"Ara Toplam\",\n totalLabel = \"Toplam\",\n checkoutButtonText = \"Siparişi Tamamla\",\n itemsText = \"ürün\",\n couponToggleText = \"Hediye Kodunu Kullan!\",\n couponPlaceholder = \"Kupon kodu girin\",\n couponApplyText = \"Uygula\",\n couponRemoveText = \"Kaldır\",\n couponApplyingText = \"Uygulanıyor...\",\n orderSummaryTitle = \"Sipariş Özeti\",\n continueShoppingText = \"Alışverişe Başla\",\n}: Props) {\n const cart = cartStore.cart;\n const isLoading = cartStore.isCartLoading;\n const cartHasItems = hasCart(cartStore);\n const lineItems = (cart?.orderLineItems ?? []).filter(\n (item) => !item?.deleted,\n );\n const totalItemCount = cart ? getIkasOrderTotalItemCount(cart) : 0;\n\n const handleRemove = async (item: IkasOrderLineItem) => {\n await removeItem(item);\n };\n\n if (isLoading) {\n return <PageLoader />;\n }\n\n if (!cartHasItems) {\n return (\n <section className=\"cart-page\">\n <div className=\"kombos-container cart-page__container\">\n <header className=\"cart-page__header\">\n <h1 className=\"cart-page__title text-xl-semibold md:display-xs-semibold\">{title}</h1>\n </header>\n <EmptyState\n emptyCartText={emptyCartText}\n continueShoppingText={continueShoppingText}\n />\n </div>\n </section>\n );\n }\n\n return (\n <section className=\"cart-page\">\n <div className=\"kombos-container cart-page__container\">\n <header className=\"cart-page__header\">\n <h1 className=\"cart-page__title text-xl-semibold md:display-xs-semibold\">\n {title}{\" \"}\n <span className=\"cart-page__count text-lg-regular\">\n ({totalItemCount} {itemsText})\n </span>\n </h1>\n </header>\n\n <div className=\"cart-page__layout\">\n <div className=\"cart-page__items-col\">\n <div className=\"cart-page__items\">\n {lineItems.map((item) => (\n <CartItem\n key={item.id}\n item={item}\n onRemove={handleRemove}\n variant=\"page\"\n />\n ))}\n </div>\n </div>\n\n {cart && (\n <OrderSummary\n cart={cart}\n title={orderSummaryTitle}\n subtotalLabel={subtotalLabel}\n totalLabel={totalLabel}\n checkoutButtonText={checkoutButtonText}\n couponToggleText={couponToggleText}\n couponPlaceholder={couponPlaceholder}\n couponApplyText={couponApplyText}\n couponRemoveText={couponRemoveText}\n couponApplyingText={couponApplyingText}\n />\n )}\n </div>\n </div>\n </section>\n );\n}\n\nexport default CartPage;\n"
16855
- },
16856
- {
16857
- "filename": "styles.css",
16858
- "content": "/* CartPage */\n.cart-page {\n width: 100%;\n}\n\n.cart-page__container {\n padding-top: 1rem;\n padding-bottom: 1rem;\n}\n\n/* Header */\n.cart-page__header {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n margin-bottom: 1rem;\n}\n\n/* Title */\n.cart-page__title {\n margin: 0;\n color: var(--kombos-gray-900);\n}\n\n.cart-page__count {\n color: var(--kombos-gray-500);\n}\n\n/* Two-column Layout */\n.cart-page__layout {\n display: grid;\n grid-template-columns: minmax(0, 1fr);\n gap: 2rem;\n}\n\n@media (min-width: 1024px) {\n .cart-page__layout {\n grid-template-columns: minmax(0, 1fr) 22.5rem;\n gap: 2.5rem;\n align-items: start;\n }\n}\n\n/* Cart Items Column */\n.cart-page__items-col {\n display: flex;\n flex-direction: column;\n min-width: 0;\n}\n\n.cart-page__items {\n display: flex;\n flex-direction: column;\n}\n"
16859
- },
16860
- {
16861
- "filename": "types.ts",
16862
- "content": "export interface Props {\n title?: string;\n emptyCartText?: string;\n subtotalLabel?: string;\n totalLabel?: string;\n checkoutButtonText?: string;\n itemsText?: string;\n couponPlaceholder?: string;\n couponApplyText?: string;\n couponRemoveText?: string;\n couponApplyingText?: string;\n orderSummaryTitle?: string;\n couponToggleText?: string;\n continueShoppingText?: string;\n}\n"
16863
- }
16864
- ]
16865
- },
16866
- {
16867
- "id": "category-images-section",
16868
- "title": "Category Images Section",
16869
- "description": "Grid of category image cards with links. Uses IkasComponentRenderer to render CategoryImageItem children with configurable images, titles, and navigation links.",
16870
- "code": "// import { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nconst COLUMNS_MAP: Record<Props[\"desktopColumns\"] & string, number> = {\n One: 1,\n Two: 2,\n Three: 3,\n Four: 4,\n Five: 5,\n};\n\nexport function CategoryImages(props: Props) {\n const {\n title,\n titleColor,\n backgroundColor,\n desktopColumns,\n mobileColumns,\n components,\n } = props;\n\n const count = components?.length ?? 0;\n if (count === 0) return null;\n\n const desktopCols = desktopColumns ? COLUMNS_MAP[desktopColumns] : count;\n const mobileCols = mobileColumns ? COLUMNS_MAP[mobileColumns] : 1;\n\n return (\n <section\n className=\"kombos-category-images\"\n style={backgroundColor ? { backgroundColor } : undefined}\n >\n <div className=\"kombos-category-images__container kombos-container\">\n {title && (\n <h2\n className=\"kombos-category-images__title text-xl-medium md:display-xs-medium\"\n style={titleColor ? { color: titleColor } : undefined}\n >\n {title}\n </h2>\n )}\n\n <div\n className=\"kombos-category-images__grid\"\n style={{\n \"--desktop-cols\": String(desktopCols),\n \"--mobile-cols\": String(mobileCols),\n }}\n >\n <IkasComponentRenderer\n id=\"category-images\"\n components={components}\n parentProps={props}\n />\n </div>\n </div>\n </section>\n );\n}\n\nexport default CategoryImages;\n",
16871
- "relatedFunctions": [],
16872
- "categories": [
16873
- "Category",
16874
- "Layout"
16875
- ],
16876
- "files": [
16877
- {
16878
- "filename": "children/CategoryImageItem/components/Card/index.tsx",
16879
- "content": "import {\n IkasImage,\n getDefaultSrc,\n createMediaSrcset,\n} from \"@ikas/bp-storefront\";\nimport { cx } from \"../../../../utils/cx\";\n\ninterface Props {\n img: IkasImage;\n cssClass: string;\n objectFit: string;\n aspectRatio?: string;\n overlay: boolean;\n label?: string;\n href?: string;\n openInNewTab?: boolean;\n}\n\nexport default function Card({\n img,\n cssClass,\n objectFit,\n aspectRatio,\n overlay,\n label,\n href,\n openInNewTab,\n}: Props) {\n const cardClass = cx(\"kombos-category-image-item__card\", cssClass);\n const Tag = href ? \"a\" : \"div\";\n const linkProps = href\n ? {\n href,\n target: openInNewTab ? \"_blank\" : undefined,\n rel: openInNewTab ? \"noopener noreferrer\" : undefined,\n }\n : {};\n\n return (\n <Tag className={cardClass} {...linkProps}>\n <div\n className=\"kombos-category-image-item__img-wrap\"\n style={aspectRatio ? { aspectRatio, height: \"auto\" } : undefined}\n >\n <img\n className=\"kombos-category-image-item__img\"\n src={getDefaultSrc(img)}\n srcSet={createMediaSrcset(img)}\n sizes=\"(max-width: 1023px) 100vw, 50vw\"\n alt={label ? \"\" : img.altText || \"Category\"}\n style={{ objectFit }}\n loading=\"lazy\"\n decoding=\"async\"\n />\n </div>\n {overlay && <div className=\"kombos-category-image-item__overlay\" />}\n {label && (\n <span className=\"kombos-category-image-item__label display-xs-semibold md:display-sm-semibold\">\n {label}\n </span>\n )}\n </Tag>\n );\n}\n"
16880
- },
16881
- {
16882
- "filename": "children/CategoryImageItem/components/Card/styles.css",
16883
- "content": ".kombos-category-image-item__card {\n position: relative;\n overflow: hidden;\n height: 100%;\n text-decoration: none;\n color: inherit;\n}\n\n.kombos-category-image-item__card--desktop {\n display: none;\n}\n\n.kombos-category-image-item__card--mobile {\n display: block;\n}\n\n.kombos-category-image-item__img-wrap {\n width: 100%;\n height: 100%;\n}\n\n.kombos-category-image-item__img {\n display: block;\n width: 100%;\n height: 100%;\n transition: transform 0.4s ease;\n}\n\n.kombos-category-image-item__overlay {\n position: absolute;\n inset: 0;\n background: rgba(0, 0, 0, 0.2);\n pointer-events: none;\n}\n\n.kombos-category-image-item__label {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--kombos-white);\n text-align: center;\n pointer-events: none;\n padding: 1.5rem;\n}\n\n@media (min-width: 1024px) {\n .kombos-category-image-item__card--desktop {\n display: block;\n }\n\n .kombos-category-image-item__card--mobile {\n display: none;\n }\n}\n"
16884
- },
16885
- {
16886
- "filename": "children/CategoryImageItem/ikas-config-snippet.json",
16887
- "content": "{\n \"id\": \"{{PROJECT_ID}}-category-image-item\",\n \"name\": \"CategoryImageItem\",\n \"type\": \"component\",\n \"entry\": \"./src/components/CategoryImageItem/index.tsx\",\n \"styles\": \"./src/components/CategoryImageItem/styles.css\",\n \"props\": [\n {\n \"name\": \"image\",\n \"displayName\": \"Image\",\n \"type\": \"IMAGE\",\n \"required\": false,\n \"groupId\": \"desktop\"\n },\n {\n \"name\": \"link\",\n \"displayName\": \"Link\",\n \"type\": \"LINK\",\n \"required\": false,\n \"groupId\": \"desktop\"\n },\n {\n \"name\": \"overlay\",\n \"displayName\": \"Dark Overlay\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"desktop\"\n },\n {\n \"name\": \"aspectRatio\",\n \"displayName\": \"En Boy Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"objectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"enumTypeId\": \"GrylMqHxui\"\n },\n {\n \"name\": \"mobileImage\",\n \"displayName\": \"Image\",\n \"type\": \"IMAGE\",\n \"required\": false,\n \"groupId\": \"mobile\"\n },\n {\n \"name\": \"mobileLink\",\n \"displayName\": \"Link\",\n \"type\": \"LINK\",\n \"required\": false,\n \"groupId\": \"mobile\"\n },\n {\n \"name\": \"mobileOverlay\",\n \"displayName\": \"Dark Overlay\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"mobile\"\n },\n {\n \"name\": \"mobileAspectRatio\",\n \"displayName\": \"En Boy Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"mobileObjectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"enumTypeId\": \"GrylMqHxui\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop Settings\"\n },\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile Settings\"\n }\n ]\n}"
16888
- },
16889
- {
16890
- "filename": "children/CategoryImageItem/index.tsx",
16891
- "content": "import { Props } from \"./types\";\nimport { resolveAspectRatio, resolveObjectFit } from \"../../utils/media\";\nimport Card from \"./components/Card\";\n\nexport function CategoryImageItem({\n image,\n link,\n overlay = false,\n aspectRatio,\n objectFit,\n mobileImage,\n mobileLink,\n mobileOverlay,\n mobileAspectRatio,\n mobileObjectFit,\n}: Props) {\n const desktopImg = image || mobileImage;\n const mobileImg = mobileImage || image;\n\n if (!desktopImg && !mobileImg) return null;\n\n const desktopFit = resolveObjectFit(objectFit);\n const mobileFit = mobileObjectFit\n ? resolveObjectFit(mobileObjectFit)\n : desktopFit;\n\n const desktopAR = aspectRatio ? resolveAspectRatio(aspectRatio) : undefined;\n const mobileAR = mobileAspectRatio\n ? resolveAspectRatio(mobileAspectRatio)\n : desktopAR;\n\n const mobOverlay = mobileOverlay ?? overlay;\n\n return (\n <div className=\"kombos-category-image-item\">\n {desktopImg && (\n <Card\n img={desktopImg}\n cssClass=\"kombos-category-image-item__card--desktop\"\n objectFit={desktopFit}\n aspectRatio={desktopAR}\n overlay={overlay}\n label={link?.label}\n href={link?.href || undefined}\n openInNewTab={link?.openInNewTab}\n />\n )}\n {mobileImg && (\n <Card\n img={mobileImg}\n cssClass=\"kombos-category-image-item__card--mobile\"\n objectFit={mobileFit}\n aspectRatio={mobileAR}\n overlay={mobOverlay}\n label={mobileLink?.label}\n href={mobileLink?.href || undefined}\n openInNewTab={mobileLink?.openInNewTab}\n />\n )}\n </div>\n );\n}\n\nexport default CategoryImageItem;\n"
16892
- },
16893
- {
16894
- "filename": "children/CategoryImageItem/styles.css",
16895
- "content": ".kombos-category-image-item {\n position: relative;\n overflow: hidden;\n border-radius: 6px;\n height: 100%;\n}\n\n.kombos-category-image-item:hover .kombos-category-image-item__img {\n transform: scale(1.15);\n}\n"
16896
- },
16897
- {
16898
- "filename": "children/CategoryImageItem/types.ts",
16899
- "content": "import type { IkasImage, IkasNavigationLink } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../global-types\";\n\nexport interface Props {\n image?: IkasImage | null;\n link?: IkasNavigationLink | null;\n overlay?: boolean;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n mobileImage?: IkasImage | null;\n mobileLink?: IkasNavigationLink | null;\n mobileOverlay?: boolean;\n mobileAspectRatio?: AspectRatio;\n mobileObjectFit?: ObjectFit;\n}\n"
16900
- },
16901
- {
16902
- "filename": "ikas-config-snippet.json",
16903
- "content": "{\n \"id\": \"{{PROJECT_ID}}-category-images\",\n \"name\": \"CategoryImages\",\n \"type\": \"section\",\n \"entry\": \"./src/components/CategoryImages/index.tsx\",\n \"styles\": \"./src/components/CategoryImages/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Categories\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"titleColor\",\n \"displayName\": \"Title Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"content\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"defaultValue\": \"#ffffff\",\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"desktopColumns\",\n \"displayName\": \"Desktop Column Count\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"appearance\",\n \"enumTypeId\": \"Tkt3wfuJ5F\"\n },\n {\n \"name\": \"mobileColumns\",\n \"displayName\": \"Mobile Column Count\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"appearance\",\n \"enumTypeId\": \"Tkt3wfuJ5F\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Categories\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-category-image-item\"\n ]\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"content\",\n \"name\": \"Content\",\n \"description\": \"Section başlığı ve renk ayarları\"\n },\n {\n \"id\": \"appearance\",\n \"name\": \"View\",\n \"description\": \"Arkaplan, sütun sayısı ve görsel ayarları\"\n }\n ]\n}"
16904
- },
16905
- {
16906
- "filename": "index.tsx",
16907
- "content": "// import { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nconst COLUMNS_MAP: Record<Props[\"desktopColumns\"] & string, number> = {\n One: 1,\n Two: 2,\n Three: 3,\n Four: 4,\n Five: 5,\n};\n\nexport function CategoryImages(props: Props) {\n const {\n title,\n titleColor,\n backgroundColor,\n desktopColumns,\n mobileColumns,\n components,\n } = props;\n\n const count = components?.length ?? 0;\n if (count === 0) return null;\n\n const desktopCols = desktopColumns ? COLUMNS_MAP[desktopColumns] : count;\n const mobileCols = mobileColumns ? COLUMNS_MAP[mobileColumns] : 1;\n\n return (\n <section\n className=\"kombos-category-images\"\n style={backgroundColor ? { backgroundColor } : undefined}\n >\n <div className=\"kombos-category-images__container kombos-container\">\n {title && (\n <h2\n className=\"kombos-category-images__title text-xl-medium md:display-xs-medium\"\n style={titleColor ? { color: titleColor } : undefined}\n >\n {title}\n </h2>\n )}\n\n <div\n className=\"kombos-category-images__grid\"\n style={{\n \"--desktop-cols\": String(desktopCols),\n \"--mobile-cols\": String(mobileCols),\n }}\n >\n <IkasComponentRenderer\n id=\"category-images\"\n components={components}\n parentProps={props}\n />\n </div>\n </div>\n </section>\n );\n}\n\nexport default CategoryImages;\n"
16908
- },
16909
- {
16910
- "filename": "styles.css",
16911
- "content": ".kombos-category-images {\n width: 100%;\n}\n\n/* Mobile-first: base = mobile (375px) */\n.kombos-category-images__container {\n display: flex;\n flex-direction: column;\n gap: 1.25rem;\n padding-top: 1rem;\n padding-bottom: 1rem;\n}\n\n.kombos-category-images__title {\n margin: 0;\n color: var(--kombos-gray-900);\n}\n\n.kombos-category-images__grid {\n display: grid;\n grid-template-columns: repeat(var(--mobile-cols, 1), 1fr);\n grid-auto-rows: 1fr;\n gap: 1rem;\n}\n\n/* Desktop */\n@media (min-width: 1024px) {\n .kombos-category-images__container {\n gap: 2rem;\n padding-top: 2rem;\n padding-bottom: 2rem;\n }\n\n .kombos-category-images__grid {\n grid-template-columns: repeat(var(--desktop-cols, 4), 1fr);\n gap: 1.5rem;\n }\n}\n"
16912
- },
16913
- {
16914
- "filename": "types.ts",
16915
- "content": "import type { NumberOfColumns } from \"../../global-types\";\n\nexport interface Props {\n title?: string;\n titleColor?: string;\n backgroundColor?: string;\n desktopColumns?: NumberOfColumns;\n mobileColumns?: NumberOfColumns;\n components?: any;\n}\n"
16916
- }
16917
- ]
16918
- },
16919
- {
16920
- "id": "category-list-section",
16921
- "title": "Category / Product List Section",
16922
- "description": "Full product listing page with category sidebar, filters, sorting, pagination, and responsive grid. Uses ProductCard with IkasComponentRenderer for customizable card content. Supports infinite scroll and column preference.",
16923
- "code": "import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"preact/hooks\";\nimport {\n IkasProductListSortType,\n getProductListSortOptions,\n hasProductListNextPage,\n setSortType,\n getCategoryPath,\n getIkasCategoryHref,\n getFilterDisplayedValues,\n isStockFilter,\n hasProductListPrevPage,\n getProductListPrevPage,\n getProductListPage,\n setProductListVisiblePage,\n searchProductList,\n getProductOptionSet,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport ProductCard from \"../../sub-components/ProductCard\";\nimport Breadcrumb from \"../../sub-components/Breadcrumb\";\nimport type { BreadcrumbItem } from \"../../sub-components/Breadcrumb\";\nimport Button from \"../../sub-components/Button\";\nimport SpinnerIcon from \"../../sub-components/SpinnerIcon\";\nimport CategoryListControls from \"./components/CategoryListControls\";\nimport FilterSidebar from \"./components/FilterSidebar\";\nimport MobileFilterModal from \"./components/MobileFilterModal\";\nimport Pagination from \"../../sub-components/Pagination\";\nimport { useColumnPreference } from \"../../hooks/useColumnPreference\";\nimport { useInfiniteScroll } from \"../../hooks/useInfiniteScroll\";\nimport { usePageTracking } from \"../../hooks/usePageTracking\";\n\nexport function CategoryList(props: Props) {\n const {\n productList,\n emptyMessage = \"Ürün bulunamadı.\",\n searchPlaceholder = \"Ara\",\n clearFiltersText = \"Filtreleri Temizle\",\n addToCartText = \"Sepete Ekle\",\n addedToCartText = \"Eklendi\",\n outOfStockText = \"Tükendi\",\n showProductsText = \"ÜRÜNÜ GÖR\",\n filtersText = \"Filtreler\",\n columnText = \"Sütun\",\n badgeText,\n homepageText = \"Anasayfa\",\n showFilters = false,\n showSearch = false,\n aspectRatio,\n objectFit,\n isInfinity = false,\n loadPrevPageText = \"Önceki sayfayı yükle\",\n pageTitle: pageTitleProp,\n productCountText = \"ürün\",\n hideAddToCartButton,\n hideBreadcrumb = false,\n isBrandPage = false,\n components,\n } = props;\n\n const [filterModalOpen, setFilterModalOpen] = useState(false);\n const sectionRef = useRef<HTMLElement>(null);\n\n const { columns, toggleColumns } = useColumnPreference();\n const products = productList?.data ?? [];\n\n const { sentinelRef } = useInfiniteScroll({\n isEnabled: isInfinity,\n productList,\n });\n\n const { gridRef } = usePageTracking({\n isEnabled: isInfinity,\n productList,\n productCount: products.length,\n });\n\n useEffect(() => {\n if (!productList) return;\n const items = productList.data ?? [];\n items.forEach((p) => {\n if (!p.productOptionSet) getProductOptionSet(p);\n });\n }, [productList?.data?.length]);\n\n const scrollToTop = useCallback(() => {\n sectionRef.current?.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n }, []);\n\n const handleLoadPrevPage = useCallback(() => {\n getProductListPrevPage(productList);\n setProductListVisiblePage(productList, (productList.minPage ?? 1) - 1);\n scrollToTop();\n }, [productList, scrollToTop]);\n\n const openFilterModal = useCallback(() => setFilterModalOpen(true), []);\n const closeFilterModal = useCallback(() => setFilterModalOpen(false), []);\n\n const handleSearch = useCallback(\n (e: Event) => {\n searchProductList(productList, (e.target as HTMLInputElement).value);\n },\n [productList],\n );\n\n const handleSort = useCallback(\n (e: Event) => {\n const value = (e.target as HTMLSelectElement).value;\n setSortType(productList, value as IkasProductListSortType);\n },\n [productList],\n );\n\n const sortSelectOptions = useMemo(\n () =>\n getProductListSortOptions(productList).map((o) => ({\n label: o.label,\n value: o.value,\n })),\n [productList],\n );\n\n const hasVisibleFilters = useMemo(() => {\n if (!showFilters) return false;\n const hasFilterValues = productList?.filters?.some((filter) => {\n const values = getFilterDisplayedValues(filter);\n return values.length > 0 && !isStockFilter(filter);\n });\n const hasCategoryFilters =\n productList?.filterCategories && productList.filterCategories.length > 0;\n return hasFilterValues || hasCategoryFilters;\n }, [showFilters, productList?.filters, productList?.filterCategories]);\n\n const selectedFiltersCount = useMemo(() => {\n const selectedCategories =\n productList?.filterCategories?.filter((c) => c.isSelected).length ?? 0;\n return (\n ((productList?.filters &&\n productList.filters\n .map((filter) => {\n if (filter.numberRangeListOptions) {\n return filter.numberRangeListOptions.filter((v) => v.isSelected)\n .length;\n }\n const displayedValues = getFilterDisplayedValues(filter);\n return displayedValues.filter((v) => v.isSelected).length;\n })\n .reduce((a, b) => a + b, 0)) ||\n 0) + selectedCategories\n );\n }, [productList?.filters, productList?.filterCategories]);\n\n if (!productList) return null;\n\n const category = productList.category;\n const brand = productList.brand;\n const selectedSort = getProductListSortOptions(productList).find(\n (o) => o.isSelected,\n );\n const pageTitle = pageTitleProp || category?.name || brand?.name || \"\";\n\n const breadcrumbItems = useMemo(() => {\n if (!category) return [];\n const path = getCategoryPath(category);\n if (path.length === 0) return [];\n return [\n { label: homepageText, href: \"/\" } as BreadcrumbItem,\n ...path.map(\n (cat: any) =>\n ({\n label: cat.name,\n href: getIkasCategoryHref(cat),\n }) as BreadcrumbItem,\n ),\n ];\n }, [category, homepageText]);\n\n const startPage = productList.minPage ?? productList.page ?? 1;\n const limit = productList.limit || products.length;\n\n return (\n <section ref={sectionRef} className=\"kombos-category-list\">\n <div className=\"kombos-container\">\n {!hideBreadcrumb && breadcrumbItems.length > 0 && (\n <div className=\"kombos-category-list__top\">\n <Breadcrumb\n items={breadcrumbItems}\n className=\"kombos-category-list__breadcrumb\"\n />\n </div>\n )}\n\n <CategoryListControls\n pageTitle={pageTitle}\n productCount={productList.count}\n productCountText={productCountText}\n sortOptions={sortSelectOptions}\n selectedSortValue={selectedSort?.value ?? \"\"}\n onSort={handleSort}\n columns={columns}\n onToggleColumns={toggleColumns}\n columnText={columnText}\n hasVisibleFilters={!!hasVisibleFilters}\n selectedFiltersCount={selectedFiltersCount}\n filtersText={filtersText}\n onOpenFilterModal={openFilterModal}\n showSearch={showSearch}\n searchPlaceholder={searchPlaceholder}\n searchKeyword={productList.searchKeyword ?? \"\"}\n onSearch={handleSearch}\n />\n\n <div className=\"kombos-category-list__main\">\n {hasVisibleFilters && (\n <div className=\"kombos-category-list__sidebar\">\n <FilterSidebar\n productList={productList}\n searchPlaceholder={searchPlaceholder}\n clearFiltersText={clearFiltersText}\n showSearch={showSearch}\n onFilterChange={isInfinity ? undefined : scrollToTop}\n isBrandPage={isBrandPage}\n />\n </div>\n )}\n\n <div className=\"kombos-category-list__grid-area\">\n {hasProductListPrevPage(productList) && isInfinity && (\n <div className=\"kombos-category-list__prev-wrap\">\n <Button\n size=\"xs\"\n disabled={productList.isLoading}\n onClick={handleLoadPrevPage}\n icon={productList.isLoading ? <SpinnerIcon /> : undefined}\n >\n {loadPrevPageText}\n </Button>\n </div>\n )}\n\n {products.length === 0 ? (\n <p className=\"kombos-category-list__empty text-md-semibold\">\n {emptyMessage}\n </p>\n ) : (\n <div\n ref={gridRef}\n className={`kombos-category-list__grid kombos-category-list__grid--cols-${columns}`}\n >\n {products.map((product, index) => {\n const isPageStart = isInfinity && index % limit === 0;\n const pageNum = startPage + Math.floor(index / limit);\n\n return (\n <div\n key={`${product.id}-${index}`}\n className=\"kombos-category-list__card\"\n >\n <ProductCard\n product={product}\n addToCartText={addToCartText}\n addedToCartText={addedToCartText}\n outOfStockText={outOfStockText}\n badgeText={badgeText}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n dataPage={isPageStart ? pageNum : undefined}\n sizes={`(max-width: 767px) calc((100vw - 48px) / 2), (max-width: 1023px) calc((100vw - 88px) / 2), calc((100vw - ${columns === 3 ? 464 : 488}px) / ${columns})`}\n hideAddToCartButton={hideAddToCartButton}\n priority={index < 4}\n />\n <IkasComponentRenderer\n id={`category-list-product-${product.id}`}\n components={components}\n parentProps={props}\n map={{ product }}\n className=\"kombos-category-list__card-content\"\n />\n </div>\n );\n })}\n </div>\n )}\n\n {isInfinity && hasProductListNextPage(productList) && (\n <div\n ref={sentinelRef}\n className=\"kombos-category-list__sentinel\"\n />\n )}\n\n {!isInfinity && (\n <div className=\"kombos-category-list__pagination\">\n <Pagination\n currentPage={productList.page ?? 1}\n totalPages={Math.ceil(\n (productList.count ?? 0) / (productList.limit || 20),\n )}\n hasPrev={hasProductListPrevPage(productList)}\n hasNext={hasProductListNextPage(productList)}\n onPageChange={(page) => {\n getProductListPage(productList, page);\n scrollToTop();\n }}\n />\n </div>\n )}\n </div>\n </div>\n </div>\n\n {hasVisibleFilters && (\n <MobileFilterModal\n productList={productList}\n isOpen={filterModalOpen}\n onClose={closeFilterModal}\n clearFiltersText={clearFiltersText}\n showProductsText={showProductsText}\n filtersText={filtersText}\n isBrandPage={isBrandPage}\n />\n )}\n </section>\n );\n}\n\nexport default CategoryList;\n",
16924
- "relatedFunctions": [
16925
- "getProductListSortOptions",
16926
- "hasProductListNextPage",
16927
- "setSortType",
16928
- "getCategoryPath",
16929
- "getIkasCategoryHref",
16930
- "getFilterDisplayedValues",
16931
- "isStockFilter",
16932
- "hasProductListPrevPage",
16933
- "getProductListPrevPage",
16934
- "getProductListPage",
16935
- "setProductListVisiblePage",
16936
- "searchProductList",
16937
- "getProductOptionSet",
16938
- "getSelectedProductVariant",
16939
- "getProductVariantFormattedFinalPrice",
16940
- "getProductVariantFormattedSellPrice",
16941
- "hasProductVariantDiscount",
16942
- "getProductHref"
16943
- ],
16944
- "categories": [
16945
- "Product",
16946
- "ProductList",
16947
- "Category",
16948
- "Filtering"
16949
- ],
16950
- "files": [
16951
- {
16952
- "filename": "children/CardProductName/ikas-config-snippet.json",
16953
- "content": "{\n \"id\": \"{{PROJECT_ID}}-card-product-name\",\n \"name\": \"CardProductName\",\n \"type\": \"component\",\n \"entry\": \"./src/components/CardProductName/index.tsx\",\n \"styles\": \"./src/components/CardProductName/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"singleLineName\",\n \"displayName\": \"Single Line Name\",\n \"type\": \"BOOLEAN\",\n \"required\": false\n }\n ]\n}"
16954
- },
16955
- {
16956
- "filename": "children/CardProductName/index.tsx",
16957
- "content": "import { getProductHref } from \"@ikas/bp-storefront\";\nimport { cx } from \"../../utils/cx\";\nimport { Props } from \"./types\";\n\nexport function CardProductName({ product, singleLineName }: Props) {\n if (!product) return null;\n\n const productHref = getProductHref(product);\n\n return (\n <span className=\"kombos-card-product-name\">\n <a\n href={productHref}\n className={cx(\n \"kombos-card-product-name__link text-sm-medium md:text-md-medium\",\n singleLineName && \"kombos-card-product-name__link--single-line\",\n )}\n >\n {product.name}\n </a>\n </span>\n );\n}\n\nexport default CardProductName;\n"
16958
- },
16959
- {
16960
- "filename": "children/CardProductName/styles.css",
16961
- "content": ".kombos-card-product-name {\n display: block;\n width: fit-content;\n max-width: 100%;\n}\n\n.kombos-card-product-name__link {\n display: block;\n color: var(--kombos-gray-900);\n text-decoration: none;\n max-width: 100%;\n}\n\n.kombos-card-product-name__link:hover {\n text-decoration: underline;\n}\n\n.kombos-card-product-name__link--single-line {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n"
16962
- },
16963
- {
16964
- "filename": "children/CardProductName/types.ts",
16965
- "content": "import type { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n singleLineName?: boolean;\n}\n"
16966
- },
16967
- {
16968
- "filename": "children/CardProductPrice/ikas-config-snippet.json",
16969
- "content": "{\n \"id\": \"{{PROJECT_ID}}-card-product-price\",\n \"name\": \"CardProductPrice\",\n \"type\": \"component\",\n \"entry\": \"./src/components/CardProductPrice/index.tsx\",\n \"styles\": \"./src/components/CardProductPrice/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n }\n ]\n}"
16970
- },
16971
- {
16972
- "filename": "children/CardProductPrice/index.tsx",
16973
- "content": "import {\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function CardProductPrice({ product }: Props) {\n if (!product) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\n\n const hasDiscount = hasProductVariantDiscount(selectedVariant);\n\n return (\n <div className=\"kombos-card-product-price\">\n <span className=\"kombos-card-product-price__current text-sm-medium sm:text-md-medium md:text-xl-medium\">\n {getProductVariantFormattedFinalPrice(selectedVariant)}\n </span>\n {hasDiscount && (\n <span className=\"kombos-card-product-price__old text-xs-regular-strike sm:text-sm-regular-strike md:text-md-regular-strike\">\n {getProductVariantFormattedSellPrice(selectedVariant)}\n </span>\n )}\n </div>\n );\n}\n\nexport default CardProductPrice;\n"
16974
- },
16975
- {
16976
- "filename": "children/CardProductPrice/styles.css",
16977
- "content": ".kombos-card-product-price {\n display: flex;\n align-items: baseline;\n gap: 0.5rem;\n}\n\n.kombos-card-product-price__current {\n color: var(--kombos-gray-900);\n}\n\n.kombos-card-product-price__old {\n color: var(--kombos-gray-500);\n}\n"
16978
- },
16979
- {
16980
- "filename": "children/CardProductPrice/types.ts",
16981
- "content": "import type { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n}\n"
16982
- },
16983
- {
16984
- "filename": "children/CardProductVariants/ikas-config-snippet.json",
16985
- "content": "{\n \"id\": \"{{PROJECT_ID}}-card-product-variants\",\n \"name\": \"CardProductVariants\",\n \"type\": \"component\",\n \"entry\": \"./src/components/CardProductVariants/index.tsx\",\n \"styles\": \"./src/components/CardProductVariants/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n }\n ]\n}"
16986
- },
16987
- {
16988
- "filename": "children/CardProductVariants/index.tsx",
16989
- "content": "import { Props } from \"./types\";\nimport VariantBadge from \"../../sub-components/VariantBadge\";\n\nexport function CardProductVariants({ product }: Props) {\n if (!product) return null;\n\n return <VariantBadge product={product} size=\"s\" scrollable disableRoute />;\n}\n\nexport default CardProductVariants;\n"
16990
- },
16991
- {
16992
- "filename": "children/CardProductVariants/styles.css",
16993
- "content": "/* CardProductVariants — styles handled by VariantBadge sub-component */\n"
16994
- },
16995
- {
16996
- "filename": "children/CardProductVariants/types.ts",
16997
- "content": "import type { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n}\n"
16998
- },
16999
- {
17000
- "filename": "components/CategoryListControls/index.tsx",
17001
- "content": "import { observer } from \"@ikas/component-utils\";\nimport Select from \"../../../../sub-components/Select\";\nimport Button from \"../../../../sub-components/Button\";\nimport Input from \"../../../../sub-components/Input\";\nimport {\n SlidersHorizontalSVG,\n Column3SVG,\n Column4SVG,\n MagnifyingGlass1SVG,\n} from \"../../../../sub-components/icons\";\n\ninterface Props {\n pageTitle: string;\n productCount: number;\n productCountText: string;\n sortOptions: { label: string; value: string }[];\n selectedSortValue: string;\n onSort: (e: Event) => void;\n columns: 3 | 4;\n onToggleColumns: () => void;\n columnText: string;\n hasVisibleFilters: boolean;\n selectedFiltersCount: number;\n filtersText: string;\n onOpenFilterModal: () => void;\n showSearch: boolean;\n searchPlaceholder: string;\n searchKeyword: string;\n onSearch: (e: Event) => void;\n}\n\nconst CategoryListControls = observer(function CategoryListControls({\n pageTitle,\n productCount,\n productCountText,\n sortOptions,\n selectedSortValue,\n onSort,\n columns,\n onToggleColumns,\n columnText,\n hasVisibleFilters,\n selectedFiltersCount,\n filtersText,\n onOpenFilterModal,\n showSearch,\n searchPlaceholder,\n searchKeyword,\n onSearch,\n}: Props) {\n return (\n <div className=\"kombos-category-list-controls\">\n {pageTitle && (\n <h1 className=\"kombos-category-list-controls__title display-xs-medium md:display-sm-medium\">\n {pageTitle}\n {productCount > 0 && (\n <span className=\"kombos-category-list-controls__count text-md-regular\">\n ({productCount} {productCountText})\n </span>\n )}\n </h1>\n )}\n\n {sortOptions.length > 0 && (\n <div className=\"kombos-category-list-controls__sort-wrap\">\n <Select\n size=\"xs\"\n options={sortOptions}\n value={selectedSortValue}\n onChange={onSort}\n aria-label=\"Sort by\"\n />\n </div>\n )}\n\n <Button\n variant=\"secondary\"\n size=\"xs\"\n className=\"kombos-category-list-controls__col-btn\"\n onClick={onToggleColumns}\n aria-label={`${columns === 4 ? 3 : 4} sütun`}\n icon={columns === 3 ? <Column3SVG /> : <Column4SVG />}\n >\n {columnText}\n </Button>\n\n <div className=\"kombos-category-list-controls__mobile-controls\">\n {sortOptions.length > 0 && (\n <div className=\"kombos-category-list-controls__mobile-sort-wrap\">\n <Select\n size=\"xs\"\n className=\"kombos-category-list-controls__mobile-sort-select\"\n options={sortOptions}\n value={selectedSortValue}\n onChange={onSort}\n aria-label=\"Sort by\"\n />\n </div>\n )}\n {hasVisibleFilters && (\n <Button\n variant=\"secondary\"\n size=\"xs\"\n className=\"kombos-category-list-controls__filter-btn\"\n onClick={onOpenFilterModal}\n icon={<SlidersHorizontalSVG />}\n >\n {filtersText}\n {selectedFiltersCount > 0 && ` (${selectedFiltersCount})`}\n </Button>\n )}\n </div>\n\n {showSearch && (\n <div className=\"kombos-category-list-controls__mobile-search\">\n <Input\n leftIcon={<MagnifyingGlass1SVG />}\n placeholder={searchPlaceholder}\n value={searchKeyword}\n onInput={onSearch}\n size=\"xs\"\n />\n </div>\n )}\n </div>\n );\n});\n\nexport default CategoryListControls;\n"
17002
- },
17003
- {
17004
- "filename": "components/CategoryListControls/styles.css",
17005
- "content": "/* ===== CategoryListControls ===== */\n\n.kombos-category-list-controls {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n padding-top: 1rem;\n}\n\n.kombos-category-list-controls__title {\n color: var(--kombos-gray-900);\n margin: 0 0 0.75rem;\n width: 100%;\n display: flex;\n align-items: center;\n}\n\n.kombos-category-list-controls__count {\n color: var(--kombos-gray-500);\n margin-left: 0.5rem;\n margin-top: 0.375rem;\n}\n\n/* Desktop sort — hidden on mobile */\n.kombos-category-list-controls__sort-wrap {\n display: none;\n}\n\n/* Column toggle — hidden on mobile */\n.kombos-category-list-controls__col-btn {\n display: none;\n}\n\n/* Mobile controls */\n.kombos-category-list-controls__mobile-controls {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 1.5rem;\n width: 100%;\n}\n\n.kombos-category-list-controls__mobile-sort-wrap {\n}\n\n.kombos-category-list-controls__mobile-sort-select {\n width: 100%;\n}\n\n.kombos-category-list-controls__filter-btn {\n flex: 1;\n}\n\n.kombos-category-list-controls__filter-btn .kombos-icon {\n font-size: 1rem;\n}\n\n/* Mobile search — visible on mobile, hidden on desktop */\n.kombos-category-list-controls__mobile-search {\n width: 100%;\n margin-top: 0.75rem;\n}\n\n/* ===== Desktop (>= 1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-category-list-controls {\n padding-top: 1.5rem;\n }\n\n /* Title inline on the left */\n .kombos-category-list-controls__title {\n width: auto;\n margin: 0;\n margin-right: auto;\n }\n\n /* Show desktop sort, hide mobile controls */\n .kombos-category-list-controls__sort-wrap {\n display: block;\n }\n\n .kombos-category-list-controls__mobile-controls {\n display: none;\n }\n\n .kombos-category-list-controls__mobile-search {\n display: none;\n }\n\n /* Column toggle button */\n .kombos-category-list-controls__col-btn {\n display: flex;\n margin-left: 0.5rem;\n }\n\n .kombos-category-list-controls__col-btn .kombos-icon {\n font-size: 1.25rem;\n }\n}\n"
17006
- },
17007
- {
17008
- "filename": "components/FilterBoxValues/index.tsx",
17009
- "content": "import {\n IkasProductList,\n IkasProductFilter,\n IkasApplicableProductFilterValue,\n handleFilterValueClick,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport { cx } from \"../../../../utils/cx\";\n\ninterface Props {\n productList: IkasProductList;\n filter: IkasProductFilter;\n values: IkasApplicableProductFilterValue[];\n onFilterChange?: () => void;\n}\n\nconst FilterBoxValues = observer(function FilterBoxValues({\n productList,\n filter,\n values,\n onFilterChange,\n}: Props) {\n return (\n <div className=\"kombos-filter-box-values\">\n {values.map((fv) => (\n <button\n key={fv.name}\n type=\"button\"\n className={cx(\"kombos-filter-box-values__item\", \"text-md-medium\", fv.isSelected === true && \"kombos-filter-box-values__item--active\")}\n onClick={() => {\n handleFilterValueClick(productList, filter, fv);\n onFilterChange?.();\n }}\n >\n {fv.name}\n </button>\n ))}\n </div>\n );\n});\n\nexport default FilterBoxValues;\n"
17010
- },
17011
- {
17012
- "filename": "components/FilterBoxValues/styles.css",
17013
- "content": "/* ===== Filter Box Values ===== */\n\n.kombos-filter-box-values {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n\n.kombos-filter-box-values__item {\n padding: 0.3125rem 0.6875rem;\n border-radius: 6px;\n border: 1px solid var(--kombos-gray-200);\n background: var(--kombos-white);\n color: var(--kombos-gray-700);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.kombos-filter-box-values__item:hover:not(.kombos-filter-box-values__item--active) {\n border-color: var(--kombos-gray-300);\n}\n\n.kombos-filter-box-values__item--active {\n background: var(--kombos-gray-900);\n color: var(--kombos-white);\n border-color: var(--kombos-gray-900);\n}\n"
17014
- },
17015
- {
17016
- "filename": "components/FilterCategoryList/index.tsx",
17017
- "content": "import {\n IkasProductList,\n IkasFilterCategory,\n onFilterCategoryClick,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport { cx } from \"../../../../utils/cx\";\n\ninterface Props {\n productList: IkasProductList;\n categories: IkasFilterCategory[];\n className?: string;\n onFilterChange?: () => void;\n isBrandPage?: boolean;\n}\n\nconst FilterCategoryList = observer(function FilterCategoryList({\n productList,\n categories,\n className,\n onFilterChange,\n isBrandPage = false,\n}: Props) {\n return (\n <div className={cx(\"kombos-filter-category-list\", className)}>\n {categories.map((cat) => (\n <button\n key={cat.name}\n type=\"button\"\n className=\"kombos-filter-category-list__link text-sm-medium\"\n onClick={() => {\n onFilterCategoryClick(productList, cat, isBrandPage);\n onFilterChange?.();\n }}\n >\n {cat.name}\n </button>\n ))}\n </div>\n );\n});\n\nexport default FilterCategoryList;\n"
17018
- },
17019
- {
17020
- "filename": "components/FilterCategoryList/styles.css",
17021
- "content": "/* ===== Filter Category List ===== */\n.kombos-filter-category-list {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.kombos-filter-category-list__link {\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n text-align: left;\n color: var(--kombos-gray-700);\n width: fit-content;\n}\n\n.kombos-filter-category-list__link:hover {\n text-decoration: underline;\n}\n"
17022
- },
17023
- {
17024
- "filename": "components/FilterGroupValues/index.tsx",
17025
- "content": "import { useMemo } from \"preact/hooks\";\nimport {\n IkasProductList,\n IkasProductFilter,\n getFilterDisplayedValues,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport CollapsibleGroup from \"../../../../sub-components/CollapsibleGroup\";\nimport FilterBoxValues from \"../FilterBoxValues\";\nimport FilterSwatchValues from \"../FilterSwatchValues\";\nimport FilterListValues from \"../FilterListValues\";\nimport FilterRangeListValues from \"../FilterRangeListValues\";\nimport FilterRangeValues from \"../FilterRangeValues\";\n\ninterface Props {\n productList: IkasProductList;\n filter: IkasProductFilter;\n showResultCount?: boolean;\n className?: string;\n onFilterChange?: () => void;\n}\n\nconst FilterGroupValues = observer(function FilterGroupValues({\n productList,\n filter,\n showResultCount = false,\n className,\n onFilterChange,\n}: Props) {\n const defaultOpen = useMemo(() => {\n const settings = filter.settings;\n if (!settings) return true;\n const isDesktop =\n typeof window !== \"undefined\" && window.innerWidth >= 1024;\n return isDesktop\n ? !settings.showCollapsedOnDesktop\n : !settings.showCollapsedOnMobile;\n }, [filter.id]);\n\n const values = getFilterDisplayedValues(filter);\n const hasRangeOptions = !!filter.numberRangeListOptions?.length;\n const { displayType } = filter;\n\n if (\n values.length === 0 &&\n !hasRangeOptions &&\n displayType !== \"NUMBER_RANGE\"\n ) {\n return null;\n }\n\n const renderFilterContent = () => {\n switch (displayType) {\n case \"BOX\":\n return (\n <FilterBoxValues\n productList={productList}\n filter={filter}\n values={values}\n onFilterChange={onFilterChange}\n />\n );\n case \"SWATCH\":\n return (\n <FilterSwatchValues\n productList={productList}\n filter={filter}\n values={values}\n onFilterChange={onFilterChange}\n />\n );\n case \"NUMBER_RANGE_LIST\":\n return (\n <FilterRangeListValues\n productList={productList}\n filter={filter}\n onFilterChange={onFilterChange}\n />\n );\n case \"NUMBER_RANGE\":\n return (\n <FilterRangeValues\n productList={productList}\n filter={filter}\n onFilterChange={onFilterChange}\n />\n );\n case \"LIST\":\n return (\n <FilterListValues\n productList={productList}\n filter={filter}\n values={values}\n showResultCount={showResultCount}\n onFilterChange={onFilterChange}\n />\n );\n default:\n return null;\n }\n };\n\n return (\n <CollapsibleGroup\n title={filter.name}\n defaultOpen={defaultOpen}\n className={className}\n >\n {renderFilterContent()}\n </CollapsibleGroup>\n );\n});\n\nexport default FilterGroupValues;\n"
17026
- },
17027
- {
17028
- "filename": "components/FilterGroupValues/styles.css",
17029
- "content": "/* ===== Filter Group Values ===== */\n"
17030
- },
17031
- {
17032
- "filename": "components/FilterListValues/index.tsx",
17033
- "content": "import {\n IkasProductList,\n IkasProductFilter,\n IkasApplicableProductFilterValue,\n handleFilterValueClick,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport Checkbox from \"../../../../sub-components/Checkbox\";\n\ninterface Props {\n productList: IkasProductList;\n filter: IkasProductFilter;\n values: IkasApplicableProductFilterValue[];\n showResultCount?: boolean;\n onFilterChange?: () => void;\n}\n\nconst FilterListValues = observer(function FilterListValues({\n productList,\n filter,\n values,\n showResultCount = false,\n onFilterChange,\n}: Props) {\n return (\n <div className=\"kombos-filter-list-values\">\n {values.map((fv) => (\n <div\n key={fv.name}\n className=\"kombos-filter-list-values__item\"\n onClick={() => {\n handleFilterValueClick(productList, filter, fv);\n onFilterChange?.();\n }}\n >\n <Checkbox\n checked={fv.isSelected === true}\n aria-label={fv.name}\n />\n <span className=\"kombos-filter-list-values__label text-sm-regular\">\n {fv.name}\n </span>\n {showResultCount && fv.resultCount != null && (\n <span className=\"kombos-filter-list-values__count text-xs-regular\">\n ({fv.resultCount})\n </span>\n )}\n </div>\n ))}\n </div>\n );\n});\n\nexport default FilterListValues;\n"
17034
- },
17035
- {
17036
- "filename": "components/FilterListValues/styles.css",
17037
- "content": "/* ===== Filter List Values ===== */\n\n.kombos-filter-list-values {\n display: flex;\n flex-direction: column;\n gap: 0.625rem;\n}\n\n.kombos-filter-list-values__item {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n cursor: pointer;\n}\n\n.kombos-filter-list-values__label {\n color: var(--kombos-gray-700);\n}\n\n.kombos-filter-list-values__count {\n color: var(--kombos-gray-400);\n}\n"
17038
- },
17039
- {
17040
- "filename": "components/FilterRangeListValues/index.tsx",
17041
- "content": "import {\n IkasProductList,\n IkasProductFilter,\n handleNumberRangeOptionClick,\n getProductListCurrencySymbol,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport { cx } from \"../../../../utils/cx\";\n\ninterface Props {\n productList: IkasProductList;\n filter: IkasProductFilter;\n onFilterChange?: () => void;\n}\n\nconst FilterRangeListValues = observer(function FilterRangeListValues({\n productList,\n filter,\n onFilterChange,\n}: Props) {\n const options = filter.numberRangeListOptions;\n if (!options?.length) return null;\n\n const suffix =\n filter.type === \"PRICE\"\n ? getProductListCurrencySymbol(productList)\n : undefined;\n\n return (\n <div className=\"kombos-filter-range-list-values\">\n {options.map((option) => (\n <button\n key={`${option.from}-${option.to}`}\n type=\"button\"\n className={cx(\"kombos-filter-range-list-values__item\", \"text-sm-regular\", option.isSelected && \"kombos-filter-range-list-values__item--active\")}\n onClick={() => {\n handleNumberRangeOptionClick(productList, filter, option);\n onFilterChange?.();\n }}\n >\n {option.to\n ? `${option.from} - ${option.to}${suffix ? ` ${suffix}` : \"\"}`\n : `+ ${option.from}${suffix ? ` ${suffix}` : \"\"}`}\n </button>\n ))}\n </div>\n );\n});\n\nexport default FilterRangeListValues;\n"
17042
- },
17043
- {
17044
- "filename": "components/FilterRangeListValues/styles.css",
17045
- "content": "/* ===== Filter Range List Values ===== */\n\n.kombos-filter-range-list-values {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n\n.kombos-filter-range-list-values__item {\n padding: 0.375rem 0.75rem;\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n background: var(--kombos-white);\n cursor: pointer;\n color: var(--kombos-gray-700);\n}\n\n.kombos-filter-range-list-values__item:hover {\n border-color: var(--kombos-gray-300);\n}\n\n.kombos-filter-range-list-values__item--active {\n border-color: var(--kombos-gray-900);\n color: var(--kombos-gray-900);\n}\n"
17046
- },
17047
- {
17048
- "filename": "components/FilterRangeValues/index.tsx",
17049
- "content": "import { useState, useRef, useEffect, useCallback } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport {\n IkasProductList,\n IkasProductFilter,\n onNumberRangeFilterChange,\n getProductListInitialData,\n getProductListCurrencySymbol,\n} from \"@ikas/bp-storefront\";\n\ninterface Props {\n productList: IkasProductList;\n filter: IkasProductFilter;\n onFilterChange?: () => void;\n}\n\nconst FilterRangeValues = observer(function FilterRangeValues({\n productList,\n filter,\n onFilterChange,\n}: Props) {\n const limit = filter.numberRangeLimit;\n const range = filter.numberRange;\n\n const minBound = limit?.from ?? 0;\n const maxBound = limit?.to ?? 100;\n\n const [localFrom, setLocalFrom] = useState(range?.from ?? minBound);\n const [localTo, setLocalTo] = useState(range?.to ?? maxBound);\n const [dragging, setDragging] = useState<\"from\" | \"to\" | null>(null);\n\n const trackRef = useRef<HTMLDivElement>(null);\n\n // Sync local state when filter changes externally (e.g. clear all filters)\n useEffect(() => {\n if (!dragging) {\n setLocalFrom(range?.from ?? minBound);\n setLocalTo(range?.to ?? maxBound);\n }\n }, [range?.from, range?.to, minBound, maxBound]);\n\n if (minBound === maxBound) return null;\n\n const suffix =\n filter.type === \"DISCOUNT_RATIO\"\n ? \"%\"\n : getProductListCurrencySymbol(productList);\n\n const valueToPercent = (val: number) =>\n ((val - minBound) / (maxBound - minBound)) * 100;\n\n const percentToValue = (pct: number) =>\n Math.round(minBound + (pct / 100) * (maxBound - minBound));\n\n const clientXToPercent = (clientX: number) => {\n const track = trackRef.current;\n if (!track) return 0;\n const rect = track.getBoundingClientRect();\n const raw = ((clientX - rect.left) / rect.width) * 100;\n return Math.max(0, Math.min(100, raw));\n };\n\n const applyFilter = useCallback(\n (from: number, to: number) => {\n onNumberRangeFilterChange(filter, { from, to });\n getProductListInitialData(productList);\n onFilterChange?.();\n },\n [filter, productList, onFilterChange],\n );\n\n const handlePointerDown = (handle: \"from\" | \"to\", e: PointerEvent) => {\n e.preventDefault();\n setDragging(handle);\n (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);\n };\n\n const handlePointerMove = (handle: \"from\" | \"to\", e: PointerEvent) => {\n if (dragging !== handle) return;\n const pct = clientXToPercent(e.clientX);\n const val = percentToValue(pct);\n\n if (handle === \"from\") {\n setLocalFrom(Math.min(val, localTo));\n } else {\n setLocalTo(Math.max(val, localFrom));\n }\n };\n\n const handlePointerUp = (handle: \"from\" | \"to\") => {\n if (dragging !== handle) return;\n setDragging(null);\n applyFilter(localFrom, localTo);\n };\n\n const handleTrackClick = (e: MouseEvent) => {\n // Ignore clicks on handles\n if ((e.target as HTMLElement).closest(\".kombos-range-slider__handle\"))\n return;\n\n const pct = clientXToPercent(e.clientX);\n const val = percentToValue(pct);\n const distFrom = Math.abs(val - localFrom);\n const distTo = Math.abs(val - localTo);\n\n let newFrom = localFrom;\n let newTo = localTo;\n\n if (distFrom <= distTo) {\n newFrom = Math.min(val, localTo);\n setLocalFrom(newFrom);\n } else {\n newTo = Math.max(val, localFrom);\n setLocalTo(newTo);\n }\n\n applyFilter(newFrom, newTo);\n };\n\n const fromPct = valueToPercent(localFrom);\n const toPct = valueToPercent(localTo);\n\n return (\n <div className=\"kombos-range-slider\">\n <div className=\"kombos-range-slider__values\">\n <span\n className=\"kombos-range-slider__value-label text-xs-semibold\"\n style={{ left: `${fromPct}%` }}\n >\n {localFrom}\n </span>\n <span\n className=\"kombos-range-slider__value-label text-xs-semibold\"\n style={{ left: `${toPct}%` }}\n >\n {localTo}\n </span>\n </div>\n\n <div\n ref={trackRef}\n className=\"kombos-range-slider__track\"\n onClick={handleTrackClick}\n >\n <div\n className=\"kombos-range-slider__fill\"\n style={{ left: `${fromPct}%`, right: `${100 - toPct}%` }}\n />\n <div\n className=\"kombos-range-slider__handle\"\n role=\"slider\"\n aria-label=\"Minimum\"\n aria-valuemin={minBound}\n aria-valuemax={maxBound}\n aria-valuenow={localFrom}\n tabIndex={0}\n style={{ left: `${fromPct}%` }}\n onPointerDown={(e) => handlePointerDown(\"from\", e)}\n onPointerMove={(e) => handlePointerMove(\"from\", e)}\n onPointerUp={() => handlePointerUp(\"from\")}\n />\n <div\n className=\"kombos-range-slider__handle\"\n role=\"slider\"\n aria-label=\"Maximum\"\n aria-valuemin={minBound}\n aria-valuemax={maxBound}\n aria-valuenow={localTo}\n tabIndex={0}\n style={{ left: `${toPct}%` }}\n onPointerDown={(e) => handlePointerDown(\"to\", e)}\n onPointerMove={(e) => handlePointerMove(\"to\", e)}\n onPointerUp={() => handlePointerUp(\"to\")}\n />\n </div>\n\n <div className=\"kombos-range-slider__bounds\">\n <span className=\"kombos-range-slider__bound text-xs-regular\">\n {`${minBound} ${suffix}`}\n </span>\n <span className=\"kombos-range-slider__bound text-xs-regular\">\n {`${maxBound} ${suffix}`}\n </span>\n </div>\n </div>\n );\n});\n\nexport default FilterRangeValues;\n"
17050
- },
17051
- {
17052
- "filename": "components/FilterRangeValues/styles.css",
17053
- "content": "/* ===== Range Slider ===== */\n\n.kombos-range-slider {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n padding: 0.25rem 0.625rem;\n}\n\n/* Value labels row */\n.kombos-range-slider__values {\n position: relative;\n height: 1rem;\n}\n\n.kombos-range-slider__value-label {\n position: absolute;\n transform: translateX(-50%);\n color: var(--kombos-gray-900);\n white-space: nowrap;\n pointer-events: none;\n user-select: none;\n}\n\n/* Track */\n.kombos-range-slider__track {\n position: relative;\n height: 1.5rem;\n cursor: pointer;\n display: flex;\n align-items: center;\n}\n\n.kombos-range-slider__track::before {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n height: 0.25rem;\n border-radius: 2px;\n background: var(--kombos-gray-200);\n}\n\n/* Filled area between handles */\n.kombos-range-slider__fill {\n position: absolute;\n height: 0.25rem;\n border-radius: 2px;\n background: var(--kombos-gray-900);\n pointer-events: none;\n}\n\n/* Drag handles */\n.kombos-range-slider__handle {\n position: absolute;\n width: 1rem;\n height: 1rem;\n border-radius: 50%;\n background: var(--kombos-gray-900);\n border: 2px solid var(--kombos-white);\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);\n transform: translateX(-50%);\n cursor: grab;\n touch-action: none;\n z-index: 1;\n outline: none;\n}\n\n.kombos-range-slider__handle:active {\n cursor: grabbing;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);\n}\n\n.kombos-range-slider__handle:focus-visible {\n box-shadow: 0 0 0 3px rgba(20, 20, 26, 0.15);\n}\n\n/* Min/Max bound labels */\n.kombos-range-slider__bounds {\n display: flex;\n justify-content: space-between;\n width: calc(100% + 1rem);\n margin-left: -0.5rem;\n}\n\n.kombos-range-slider__bound {\n color: var(--kombos-gray-500);\n user-select: none;\n}\n"
17054
- },
17055
- {
17056
- "filename": "components/FilterSidebar/index.tsx",
17057
- "content": "import {\n IkasProductList,\n getProductListFilterCategories,\n searchProductList,\n clearProductListFilters,\n hasProductListAppliedFilters,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport Input from \"../../../../sub-components/Input\";\nimport { MagnifyingGlass1SVG } from \"../../../../sub-components/icons\";\nimport CollapsibleGroup from \"../../../../sub-components/CollapsibleGroup\";\nimport FilterCategoryList from \"../FilterCategoryList\";\nimport FilterGroupValues from \"../FilterGroupValues\";\n\ninterface Props {\n productList: IkasProductList;\n searchPlaceholder: string;\n clearFiltersText: string;\n showSearch?: boolean;\n onFilterChange?: () => void;\n isBrandPage?: boolean;\n}\n\nconst FilterSidebar = observer(function FilterSidebar({\n productList,\n searchPlaceholder,\n clearFiltersText,\n showSearch = true,\n onFilterChange,\n isBrandPage = false,\n}: Props) {\n const filters = productList.filters ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const hasApplied = hasProductListAppliedFilters(productList);\n\n const handleSearch = (keyword: string) => {\n searchProductList(productList, keyword);\n };\n\n const handleClear = () => {\n clearProductListFilters(productList);\n onFilterChange?.();\n };\n\n return (\n <aside className=\"kombos-filter-sidebar\">\n {showSearch && (\n <div className=\"kombos-filter-sidebar__search\">\n <Input\n leftIcon={<MagnifyingGlass1SVG />}\n placeholder={searchPlaceholder}\n value={productList.searchKeyword ?? \"\"}\n onInput={(e) => handleSearch((e.target as HTMLInputElement).value)}\n size=\"xs\"\n />\n </div>\n )}\n\n {filterCategories.length > 0 && (\n <CollapsibleGroup title={productList.category?.name ?? \"\"} defaultOpen>\n <FilterCategoryList\n productList={productList}\n categories={filterCategories}\n isBrandPage={isBrandPage}\n />\n </CollapsibleGroup>\n )}\n\n {filters.map((filter) => (\n <FilterGroupValues\n key={filter.id}\n productList={productList}\n filter={filter}\n showResultCount\n onFilterChange={onFilterChange}\n />\n ))}\n\n {hasApplied && (\n <button\n type=\"button\"\n className=\"kombos-filter-sidebar__clear text-sm-medium\"\n onClick={handleClear}\n >\n {clearFiltersText}\n </button>\n )}\n </aside>\n );\n});\n\nexport default FilterSidebar;\n"
17058
- },
17059
- {
17060
- "filename": "components/FilterSidebar/styles.css",
17061
- "content": "/* ===== Filter Sidebar ===== */\n.kombos-filter-sidebar {\n width: 100%;\n flex-shrink: 0;\n display: flex;\n flex-direction: column;\n}\n\n.kombos-filter-sidebar__search {\n padding-bottom: 0.25rem;\n}\n\n/* Clear */\n.kombos-filter-sidebar__clear {\n margin-top: 1rem;\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n color: var(--kombos-gray-500);\n text-decoration: underline;\n text-align: left;\n}\n\n.kombos-filter-sidebar__clear:hover {\n color: var(--kombos-gray-700);\n}\n"
17062
- },
17063
- {
17064
- "filename": "components/FilterSwatchValues/index.tsx",
17065
- "content": "import {\n IkasProductList,\n IkasProductFilter,\n IkasApplicableProductFilterValue,\n handleFilterValueClick,\n getIkasFilterThumbnailImage,\n getDefaultSrc,\n createMediaSrcset,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport { cx } from \"../../../../utils/cx\";\n\ninterface Props {\n productList: IkasProductList;\n filter: IkasProductFilter;\n values: IkasApplicableProductFilterValue[];\n onFilterChange?: () => void;\n}\n\nconst FilterSwatchValues = observer(function FilterSwatchValues({\n productList,\n filter,\n values,\n onFilterChange,\n}: Props) {\n return (\n <div className=\"kombos-filter-swatch-values\">\n {values.map((fv) => {\n const thumbnail = getIkasFilterThumbnailImage(fv);\n return (\n <button\n key={fv.name}\n type=\"button\"\n className={cx(\"kombos-filter-swatch-values__item\", \"text-md-medium\", fv.isSelected === true && \"kombos-filter-swatch-values__item--active\")}\n onClick={() => {\n handleFilterValueClick(productList, filter, fv);\n onFilterChange?.();\n }}\n title={fv.name}\n >\n {thumbnail ? (\n <img\n src={getDefaultSrc(thumbnail)}\n srcSet={createMediaSrcset(thumbnail)}\n sizes=\"(max-width: 1024px) 100vw, 100px\"\n alt=\"\"\n className=\"kombos-filter-swatch-values__img\"\n />\n ) : fv.colorCode ? (\n <span\n className=\"kombos-filter-swatch-values__color\"\n style={{ backgroundColor: fv.colorCode }}\n />\n ) : null}\n <span>{fv.name}</span>\n </button>\n );\n })}\n </div>\n );\n});\n\nexport default FilterSwatchValues;\n"
17066
- },
17067
- {
17068
- "filename": "components/FilterSwatchValues/styles.css",
17069
- "content": "/* ===== Filter Swatch Values ===== */\n\n.kombos-filter-swatch-values {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n\n.kombos-filter-swatch-values__item {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.3125rem 0.6875rem;\n border-radius: 6px;\n border: 1px solid var(--kombos-gray-200);\n background: var(--kombos-white);\n cursor: pointer;\n color: var(--kombos-gray-700);\n transition: all 0.15s ease;\n}\n\n.kombos-filter-swatch-values__item:hover:not(.kombos-filter-swatch-values__item--active) {\n border-color: var(--kombos-gray-300);\n}\n\n.kombos-filter-swatch-values__item--active {\n background: var(--kombos-gray-900);\n color: var(--kombos-white);\n border-color: var(--kombos-gray-900);\n}\n\n.kombos-filter-swatch-values__img {\n width: 1rem;\n height: 1rem;\n border-radius: 50%;\n object-fit: cover;\n}\n\n.kombos-filter-swatch-values__color {\n width: 1rem;\n height: 1rem;\n border-radius: 50%;\n border: 1px solid var(--kombos-gray-200);\n}\n\n.kombos-filter-swatch-values__item--active .kombos-filter-swatch-values__color {\n border-color: var(--kombos-white);\n}\n"
17070
- },
17071
- {
17072
- "filename": "components/MobileFilterModal/index.tsx",
17073
- "content": "import { useState, useEffect, useCallback } from \"preact/hooks\";\nimport { useScrollLock } from \"../../../../hooks/useScrollLock\";\nimport {\n IkasProductList,\n getProductListFilterCategories,\n clearProductListFilters,\n hasProductListAppliedFilters,\n getSelectedFilterValues,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport { cx } from \"../../../../utils/cx\";\nimport { XSVG } from \"../../../../sub-components/icons\";\nimport CollapsibleGroup from \"../../../../sub-components/CollapsibleGroup\";\nimport FilterCategoryList from \"../FilterCategoryList\";\nimport FilterGroupValues from \"../FilterGroupValues\";\n\ninterface Props {\n productList: IkasProductList;\n isOpen: boolean;\n onClose: () => void;\n clearFiltersText: string;\n showProductsText: string;\n filtersText: string;\n isBrandPage?: boolean;\n}\n\nconst MobileFilterModal = observer(function MobileFilterModal({\n productList,\n isOpen,\n onClose,\n clearFiltersText,\n showProductsText,\n filtersText,\n isBrandPage = false,\n}: Props) {\n const [open, setOpen] = useState(false);\n\n useScrollLock(isOpen);\n\n useEffect(() => {\n if (!isOpen) return;\n requestAnimationFrame(() => setOpen(true));\n return () => setOpen(false);\n }, [isOpen]);\n\n const handleClose = useCallback(() => {\n setOpen(false);\n setTimeout(onClose, 300);\n }, [onClose]);\n\n const filters = productList.filters ?? [];\n const filterCategories = getProductListFilterCategories(productList);\n const hasApplied = hasProductListAppliedFilters(productList);\n const selectedValues = getSelectedFilterValues(productList);\n const activeFilterCount = selectedValues?.length ?? 0;\n const totalProducts = productList.count ?? 0;\n\n const handleClear = () => {\n clearProductListFilters(productList);\n };\n\n const isLoading = productList.isLoading;\n\n if (!isOpen) return null;\n\n return (\n <div\n className={cx(\n \"kombos-mobile-filter\",\n open && \"kombos-mobile-filter--open\",\n )}\n >\n <div className=\"kombos-mobile-filter__backdrop\" onClick={handleClose} />\n <div className=\"kombos-mobile-filter__panel\">\n {/* Header */}\n <div className=\"kombos-mobile-filter__header\">\n <span className=\"kombos-mobile-filter__title text-md-medium\">\n {filtersText}\n {activeFilterCount > 0 && ` (${activeFilterCount})`}\n </span>\n <button\n type=\"button\"\n className=\"kombos-mobile-filter__close\"\n onClick={handleClose}\n aria-label=\"Close\"\n >\n <XSVG />\n </button>\n </div>\n\n {/* Scrollable content */}\n <div className=\"kombos-mobile-filter__content\">\n {filterCategories.length > 0 && (\n <CollapsibleGroup\n title={productList.category?.name ?? \"\"}\n defaultOpen\n >\n <FilterCategoryList\n productList={productList}\n categories={filterCategories}\n isBrandPage={isBrandPage}\n />\n </CollapsibleGroup>\n )}\n\n {filters.map((filter) => (\n <FilterGroupValues\n key={filter.id}\n productList={productList}\n filter={filter}\n />\n ))}\n </div>\n\n {/* Sticky footer */}\n <div className=\"kombos-mobile-filter__footer\">\n {hasApplied && (\n <div className=\"kombos-mobile-filter__clear-row\">\n <button\n type=\"button\"\n className=\"kombos-mobile-filter__clear-btn text-xs-semibold\"\n onClick={handleClear}\n >\n {clearFiltersText}\n </button>\n </div>\n )}\n <button\n type=\"button\"\n className=\"kombos-mobile-filter__show-btn text-sm-semibold\"\n onClick={handleClose}\n disabled={isLoading}\n >\n {isLoading ? (\n <span className=\"kombos-mobile-filter__spinner\" />\n ) : (\n `${totalProducts} ${showProductsText}`\n )}\n </button>\n </div>\n </div>\n </div>\n );\n});\n\nexport default MobileFilterModal;\n"
17074
- },
17075
- {
17076
- "filename": "components/MobileFilterModal/styles.css",
17077
- "content": "/* ===== Mobile Filter Modal ===== */\n.kombos-mobile-filter {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n z-index: var(--kombos-z-overlay);\n display: flex;\n justify-content: flex-end;\n}\n\n.kombos-mobile-filter__backdrop {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: rgba(0, 0, 0, 0.35);\n opacity: 0;\n transition: opacity 0.3s ease;\n}\n\n.kombos-mobile-filter--open .kombos-mobile-filter__backdrop {\n opacity: 1;\n}\n\n.kombos-mobile-filter__panel {\n position: relative;\n width: 100%;\n max-width: 25rem;\n height: 100%;\n background: var(--kombos-white);\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n transition: transform 0.3s ease;\n}\n\n.kombos-mobile-filter--open .kombos-mobile-filter__panel {\n transform: translateX(0);\n}\n\n/* Header */\n.kombos-mobile-filter__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 1rem 1.25rem;\n border-bottom: 1px solid var(--kombos-gray-200);\n}\n\n.kombos-mobile-filter__title {\n color: var(--kombos-gray-900);\n}\n\n.kombos-mobile-filter__close {\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n font-size: 1.5rem;\n color: var(--kombos-gray-700);\n display: flex;\n align-items: center;\n}\n\n/* Content */\n.kombos-mobile-filter__content {\n flex: 1;\n overflow-y: auto;\n padding: 0 1.25rem;\n}\n\n/* Footer */\n.kombos-mobile-filter__footer {\n display: flex;\n flex-direction: column;\n}\n\n.kombos-mobile-filter__clear-row {\n display: flex;\n align-items: center;\n padding: 1rem 0.625rem;\n background: var(--kombos-white);\n border-top: 1px solid var(--kombos-gray-200);\n}\n\n.kombos-mobile-filter__clear-btn {\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n color: var(--kombos-gray-400);\n text-decoration: underline;\n white-space: nowrap;\n}\n\n.kombos-mobile-filter__show-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 3.125rem;\n padding: 0.625rem 1.25rem;\n background: var(--kombos-gray-900);\n color: var(--kombos-white);\n border: none;\n cursor: pointer;\n text-transform: uppercase;\n}\n\n.kombos-mobile-filter__show-btn:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n/* Spinner */\n.kombos-mobile-filter__spinner {\n display: inline-block;\n width: 1.125rem;\n height: 1.125rem;\n border: 2px solid rgba(255, 255, 255, 0.3);\n border-top-color: var(--kombos-white);\n border-radius: 50%;\n animation: kombos-mobile-filter-spin 0.6s linear infinite;\n}\n\n@keyframes kombos-mobile-filter-spin {\n to {\n transform: rotate(360deg);\n }\n}\n"
17078
- },
17079
- {
17080
- "filename": "ikas-config-snippet.json",
17081
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-list\",\n \"name\": \"CategoryList\",\n \"type\": \"section\",\n \"entry\": \"./src/components/CategoryList/index.tsx\",\n \"styles\": \"./src/components/CategoryList/styles.css\",\n \"props\": [\n {\n \"name\": \"productList\",\n \"displayName\": \"Product Listesi\",\n \"type\": \"PRODUCT_LIST\",\n \"required\": true,\n \"groupId\": \"data\"\n },\n {\n \"name\": \"pageTitle\",\n \"displayName\": \"Page Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"productCountText\",\n \"displayName\": \"Product Count Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"ürün\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"emptyMessage\",\n \"displayName\": \"Empty State Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Product bulunamadı.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"searchPlaceholder\",\n \"displayName\": \"Search Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Search\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"clearFiltersText\",\n \"displayName\": \"Filters Temizle Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Filters Temizle\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"addToCartText\",\n \"displayName\": \"Cart Add Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Add to Cart\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"addedToCartText\",\n \"displayName\": \"Cart Added Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Added\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"outOfStockText\",\n \"displayName\": \"Sold Out Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sold Out\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"showProductsText\",\n \"displayName\": \"Products Show Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"ÜRÜNÜ GÖR\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"filtersText\",\n \"displayName\": \"Filters Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Filters\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"homepageText\",\n \"displayName\": \"Home Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Home\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"badgeText\",\n \"displayName\": \"Label Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"showFilters\",\n \"displayName\": \"Filters Show\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"filters\"\n },\n {\n \"name\": \"showSearch\",\n \"displayName\": \"Search Show\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"filters\"\n },\n {\n \"name\": \"aspectRatio\",\n \"displayName\": \"Image En Boy Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"image-settings\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"objectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"image-settings\",\n \"enumTypeId\": \"GrylMqHxui\"\n },\n {\n \"name\": \"isInfinity\",\n \"displayName\": \"Infinite Scroll\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"pagination\"\n },\n {\n \"name\": \"columnText\",\n \"displayName\": \"Column Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Column\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"loadPrevPageText\",\n \"displayName\": \"Load Previous Page Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Önceki sayfayı yükle\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"hideAddToCartButton\",\n \"displayName\": \"Cart Add Butonunu Hide\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"card-settings\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Components\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"privateVarMap\": {\n \"product\": {\n \"id\": \"pvm_1772808304776_3\",\n \"typeId\": \"@ikas/bp-storefront-models-IkasProduct\"\n }\n },\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-card-product-price\",\n \"{{PROJECT_ID}}-card-product-variants\",\n \"{{PROJECT_ID}}-card-product-name\"\n ]\n },\n {\n \"name\": \"hideBreadcrumb\",\n \"displayName\": \"Hide Breadcrumb\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"isBrandPage\",\n \"displayName\": \"Is This a Brand Page?\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"data\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"data\",\n \"name\": \"Data\",\n \"description\": \"Product listesi veri kaynağı\"\n },\n {\n \"id\": \"appearance\",\n \"name\": \"View\",\n \"description\": \"Background plan rengi, ürün kartı görsel oranı ve kaplama ayarları\",\n \"children\": [\n {\n \"id\": \"image-settings\",\n \"name\": \"Image Settings\",\n \"description\": \"Product kartı görsel oranı ve sığdırma ayarları\"\n },\n {\n \"id\": \"card-settings\",\n \"name\": \"Product Kartı Settings\",\n \"description\": \"Product kartı görünüm ayarları\"\n }\n ]\n },\n {\n \"id\": \"filters\",\n \"name\": \"Filters\",\n \"description\": \"Product filtreleme ve arama özelliklerini açıp kapatma\"\n },\n {\n \"id\": \"pagination\",\n \"name\": \"Pagination\",\n \"description\": \"Klasik sayfalama veya sonsuz kaydırma davranışı\"\n },\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Button etiketleri, boş durum mesajları ve arayüz metinleri\"\n }\n ]\n}"
17082
- },
17083
- {
17084
- "filename": "index.tsx",
17085
- "content": "import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"preact/hooks\";\nimport {\n IkasProductListSortType,\n getProductListSortOptions,\n hasProductListNextPage,\n setSortType,\n getCategoryPath,\n getIkasCategoryHref,\n getFilterDisplayedValues,\n isStockFilter,\n hasProductListPrevPage,\n getProductListPrevPage,\n getProductListPage,\n setProductListVisiblePage,\n searchProductList,\n getProductOptionSet,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport ProductCard from \"../../sub-components/ProductCard\";\nimport Breadcrumb from \"../../sub-components/Breadcrumb\";\nimport type { BreadcrumbItem } from \"../../sub-components/Breadcrumb\";\nimport Button from \"../../sub-components/Button\";\nimport SpinnerIcon from \"../../sub-components/SpinnerIcon\";\nimport CategoryListControls from \"./components/CategoryListControls\";\nimport FilterSidebar from \"./components/FilterSidebar\";\nimport MobileFilterModal from \"./components/MobileFilterModal\";\nimport Pagination from \"../../sub-components/Pagination\";\nimport { useColumnPreference } from \"../../hooks/useColumnPreference\";\nimport { useInfiniteScroll } from \"../../hooks/useInfiniteScroll\";\nimport { usePageTracking } from \"../../hooks/usePageTracking\";\n\nexport function CategoryList(props: Props) {\n const {\n productList,\n emptyMessage = \"Ürün bulunamadı.\",\n searchPlaceholder = \"Ara\",\n clearFiltersText = \"Filtreleri Temizle\",\n addToCartText = \"Sepete Ekle\",\n addedToCartText = \"Eklendi\",\n outOfStockText = \"Tükendi\",\n showProductsText = \"ÜRÜNÜ GÖR\",\n filtersText = \"Filtreler\",\n columnText = \"Sütun\",\n badgeText,\n homepageText = \"Anasayfa\",\n showFilters = false,\n showSearch = false,\n aspectRatio,\n objectFit,\n isInfinity = false,\n loadPrevPageText = \"Önceki sayfayı yükle\",\n pageTitle: pageTitleProp,\n productCountText = \"ürün\",\n hideAddToCartButton,\n hideBreadcrumb = false,\n isBrandPage = false,\n components,\n } = props;\n\n const [filterModalOpen, setFilterModalOpen] = useState(false);\n const sectionRef = useRef<HTMLElement>(null);\n\n const { columns, toggleColumns } = useColumnPreference();\n const products = productList?.data ?? [];\n\n const { sentinelRef } = useInfiniteScroll({\n isEnabled: isInfinity,\n productList,\n });\n\n const { gridRef } = usePageTracking({\n isEnabled: isInfinity,\n productList,\n productCount: products.length,\n });\n\n useEffect(() => {\n if (!productList) return;\n const items = productList.data ?? [];\n items.forEach((p) => {\n if (!p.productOptionSet) getProductOptionSet(p);\n });\n }, [productList?.data?.length]);\n\n const scrollToTop = useCallback(() => {\n sectionRef.current?.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n }, []);\n\n const handleLoadPrevPage = useCallback(() => {\n getProductListPrevPage(productList);\n setProductListVisiblePage(productList, (productList.minPage ?? 1) - 1);\n scrollToTop();\n }, [productList, scrollToTop]);\n\n const openFilterModal = useCallback(() => setFilterModalOpen(true), []);\n const closeFilterModal = useCallback(() => setFilterModalOpen(false), []);\n\n const handleSearch = useCallback(\n (e: Event) => {\n searchProductList(productList, (e.target as HTMLInputElement).value);\n },\n [productList],\n );\n\n const handleSort = useCallback(\n (e: Event) => {\n const value = (e.target as HTMLSelectElement).value;\n setSortType(productList, value as IkasProductListSortType);\n },\n [productList],\n );\n\n const sortSelectOptions = useMemo(\n () =>\n getProductListSortOptions(productList).map((o) => ({\n label: o.label,\n value: o.value,\n })),\n [productList],\n );\n\n const hasVisibleFilters = useMemo(() => {\n if (!showFilters) return false;\n const hasFilterValues = productList?.filters?.some((filter) => {\n const values = getFilterDisplayedValues(filter);\n return values.length > 0 && !isStockFilter(filter);\n });\n const hasCategoryFilters =\n productList?.filterCategories && productList.filterCategories.length > 0;\n return hasFilterValues || hasCategoryFilters;\n }, [showFilters, productList?.filters, productList?.filterCategories]);\n\n const selectedFiltersCount = useMemo(() => {\n const selectedCategories =\n productList?.filterCategories?.filter((c) => c.isSelected).length ?? 0;\n return (\n ((productList?.filters &&\n productList.filters\n .map((filter) => {\n if (filter.numberRangeListOptions) {\n return filter.numberRangeListOptions.filter((v) => v.isSelected)\n .length;\n }\n const displayedValues = getFilterDisplayedValues(filter);\n return displayedValues.filter((v) => v.isSelected).length;\n })\n .reduce((a, b) => a + b, 0)) ||\n 0) + selectedCategories\n );\n }, [productList?.filters, productList?.filterCategories]);\n\n if (!productList) return null;\n\n const category = productList.category;\n const brand = productList.brand;\n const selectedSort = getProductListSortOptions(productList).find(\n (o) => o.isSelected,\n );\n const pageTitle = pageTitleProp || category?.name || brand?.name || \"\";\n\n const breadcrumbItems = useMemo(() => {\n if (!category) return [];\n const path = getCategoryPath(category);\n if (path.length === 0) return [];\n return [\n { label: homepageText, href: \"/\" } as BreadcrumbItem,\n ...path.map(\n (cat: any) =>\n ({\n label: cat.name,\n href: getIkasCategoryHref(cat),\n }) as BreadcrumbItem,\n ),\n ];\n }, [category, homepageText]);\n\n const startPage = productList.minPage ?? productList.page ?? 1;\n const limit = productList.limit || products.length;\n\n return (\n <section ref={sectionRef} className=\"kombos-category-list\">\n <div className=\"kombos-container\">\n {!hideBreadcrumb && breadcrumbItems.length > 0 && (\n <div className=\"kombos-category-list__top\">\n <Breadcrumb\n items={breadcrumbItems}\n className=\"kombos-category-list__breadcrumb\"\n />\n </div>\n )}\n\n <CategoryListControls\n pageTitle={pageTitle}\n productCount={productList.count}\n productCountText={productCountText}\n sortOptions={sortSelectOptions}\n selectedSortValue={selectedSort?.value ?? \"\"}\n onSort={handleSort}\n columns={columns}\n onToggleColumns={toggleColumns}\n columnText={columnText}\n hasVisibleFilters={!!hasVisibleFilters}\n selectedFiltersCount={selectedFiltersCount}\n filtersText={filtersText}\n onOpenFilterModal={openFilterModal}\n showSearch={showSearch}\n searchPlaceholder={searchPlaceholder}\n searchKeyword={productList.searchKeyword ?? \"\"}\n onSearch={handleSearch}\n />\n\n <div className=\"kombos-category-list__main\">\n {hasVisibleFilters && (\n <div className=\"kombos-category-list__sidebar\">\n <FilterSidebar\n productList={productList}\n searchPlaceholder={searchPlaceholder}\n clearFiltersText={clearFiltersText}\n showSearch={showSearch}\n onFilterChange={isInfinity ? undefined : scrollToTop}\n isBrandPage={isBrandPage}\n />\n </div>\n )}\n\n <div className=\"kombos-category-list__grid-area\">\n {hasProductListPrevPage(productList) && isInfinity && (\n <div className=\"kombos-category-list__prev-wrap\">\n <Button\n size=\"xs\"\n disabled={productList.isLoading}\n onClick={handleLoadPrevPage}\n icon={productList.isLoading ? <SpinnerIcon /> : undefined}\n >\n {loadPrevPageText}\n </Button>\n </div>\n )}\n\n {products.length === 0 ? (\n <p className=\"kombos-category-list__empty text-md-semibold\">\n {emptyMessage}\n </p>\n ) : (\n <div\n ref={gridRef}\n className={`kombos-category-list__grid kombos-category-list__grid--cols-${columns}`}\n >\n {products.map((product, index) => {\n const isPageStart = isInfinity && index % limit === 0;\n const pageNum = startPage + Math.floor(index / limit);\n\n return (\n <div\n key={`${product.id}-${index}`}\n className=\"kombos-category-list__card\"\n >\n <ProductCard\n product={product}\n addToCartText={addToCartText}\n addedToCartText={addedToCartText}\n outOfStockText={outOfStockText}\n badgeText={badgeText}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n dataPage={isPageStart ? pageNum : undefined}\n sizes={`(max-width: 767px) calc((100vw - 48px) / 2), (max-width: 1023px) calc((100vw - 88px) / 2), calc((100vw - ${columns === 3 ? 464 : 488}px) / ${columns})`}\n hideAddToCartButton={hideAddToCartButton}\n priority={index < 4}\n />\n <IkasComponentRenderer\n id={`category-list-product-${product.id}`}\n components={components}\n parentProps={props}\n map={{ product }}\n className=\"kombos-category-list__card-content\"\n />\n </div>\n );\n })}\n </div>\n )}\n\n {isInfinity && hasProductListNextPage(productList) && (\n <div\n ref={sentinelRef}\n className=\"kombos-category-list__sentinel\"\n />\n )}\n\n {!isInfinity && (\n <div className=\"kombos-category-list__pagination\">\n <Pagination\n currentPage={productList.page ?? 1}\n totalPages={Math.ceil(\n (productList.count ?? 0) / (productList.limit || 20),\n )}\n hasPrev={hasProductListPrevPage(productList)}\n hasNext={hasProductListNextPage(productList)}\n onPageChange={(page) => {\n getProductListPage(productList, page);\n scrollToTop();\n }}\n />\n </div>\n )}\n </div>\n </div>\n </div>\n\n {hasVisibleFilters && (\n <MobileFilterModal\n productList={productList}\n isOpen={filterModalOpen}\n onClose={closeFilterModal}\n clearFiltersText={clearFiltersText}\n showProductsText={showProductsText}\n filtersText={filtersText}\n isBrandPage={isBrandPage}\n />\n )}\n </section>\n );\n}\n\nexport default CategoryList;\n"
17086
- },
17087
- {
17088
- "filename": "styles.css",
17089
- "content": "/* ===== Category List Section ===== */\n.kombos-category-list {\n width: 100%;\n}\n\n.kombos-category-list__prev-wrap {\n display: flex;\n justify-content: center;\n margin-bottom: 1rem;\n}\n\n/* Top: Breadcrumb + Title */\n.kombos-category-list__top {\n padding-top: 1rem;\n}\n\n.kombos-category-list__breadcrumb {\n margin-bottom: 0;\n}\n\n/* Main layout */\n.kombos-category-list__main {\n display: flex;\n gap: 2rem;\n padding-top: 1.5rem;\n padding-bottom: 3rem;\n}\n\n/* Sidebar — hidden on mobile */\n.kombos-category-list__sidebar {\n display: none;\n}\n\n/* Grid area */\n.kombos-category-list__grid-area {\n flex: 1;\n min-width: 0;\n}\n\n.kombos-category-list__grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 1rem;\n}\n\n.kombos-category-list__empty {\n color: var(--kombos-gray-900);\n text-align: center;\n padding: 3rem 0;\n}\n\n.kombos-category-list__pagination {\n margin-top: 2rem;\n}\n\n.kombos-category-list__card {\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.kombos-category-list__card-content {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n/* Sentinel for IntersectionObserver (infinite scroll) */\n.kombos-category-list__sentinel {\n height: 1px;\n}\n\n/* ===== Tablet (>= 768px) ===== */\n@media (min-width: 768px) {\n .kombos-category-list__grid {\n gap: 1.5rem;\n }\n}\n\n/* ===== Desktop (>= 1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-category-list__top {\n padding-top: 1.5rem;\n }\n\n .kombos-category-list__main {\n padding-top: 1.5rem;\n padding-bottom: 4rem;\n }\n\n /* Show sidebar — sticky so it stays visible while scrolling */\n .kombos-category-list__sidebar {\n width: 15rem;\n display: block;\n position: sticky;\n top: 0.75rem;\n max-height: calc(100vh - 2.5rem);\n overflow-y: auto;\n }\n\n /* Grid columns — default 4 on desktop */\n .kombos-category-list__grid--cols-3 {\n grid-template-columns: repeat(3, 1fr);\n }\n\n .kombos-category-list__grid--cols-4 {\n grid-template-columns: repeat(4, 1fr);\n }\n}\n"
17090
- },
17091
- {
17092
- "filename": "types.ts",
17093
- "content": "import type { IkasProductList } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../global-types\";\n\nexport interface Props {\n productList: IkasProductList;\n pageTitle?: string;\n productCountText?: string;\n emptyMessage?: string;\n searchPlaceholder?: string;\n clearFiltersText?: string;\n addToCartText?: string;\n addedToCartText?: string;\n outOfStockText?: string;\n showProductsText?: string;\n filtersText?: string;\n homepageText?: string;\n badgeText?: string;\n showFilters?: boolean;\n showSearch?: boolean;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n isInfinity?: boolean;\n columnText?: string;\n loadPrevPageText?: string;\n hideAddToCartButton?: boolean;\n components?: any;\n hideBreadcrumb?: boolean;\n isBrandPage?: boolean;\n}\n"
17094
- }
17095
- ]
17096
- },
17097
- {
17098
- "id": "component-renderer",
17099
- "title": "IkasComponentRenderer Pattern",
17100
- "description": "How sections use IkasComponentRenderer to render child component slots. Shows COMPONENT_LIST prop usage, filteredComponentIds for restricting children, parentProps for data passing, and the component slot architecture.",
17101
- "code": "// import { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport { useEffect, useCallback } from \"preact/hooks\";\nimport { Props } from \"./types\";\nimport { ToastContainer } from \"../../sub-components/Toast\";\nimport { useToast } from \"../../hooks/useToast\";\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\n\nexport function Header(props: Props) {\n const {\n backgroundColor = \"var(--kombos-white)\",\n borderColor = \"var(--kombos-gray-100)\",\n components,\n } = props;\n\n const toast = useToast();\n\n const handleShowToast = useCallback(\n (e: Event) => {\n const detail = (e as CustomEvent).detail as {\n message: string;\n variant: \"success\" | \"error\";\n };\n toast.show(detail.message, detail.variant);\n },\n [toast.show],\n );\n\n useEffect(() => {\n window.addEventListener(\"ikas:show-toast\", handleShowToast);\n return () => window.removeEventListener(\"ikas:show-toast\", handleShowToast);\n }, [handleShowToast]);\n\n return (\n <>\n <section\n className=\"kombos-header\"\n style={{\n backgroundColor,\n borderColor,\n }}\n >\n <IkasComponentRenderer\n id=\"header\"\n components={components}\n parentProps={props}\n />\n </section>\n <ToastContainer toasts={toast.toasts} onDismiss={toast.dismiss} />\n </>\n );\n}\n\nexport default Header;\n",
17102
- "relatedFunctions": [
17103
- "getIkasCategoryPathItemHref",
17104
- "getProductCategoryPath",
17105
- "getProductVariantMainImage",
17106
- "getSelectedProductVariant",
17107
- "isNotEmpty"
17108
- ],
17109
- "categories": [
17110
- "Architecture"
17111
- ],
17112
- "files": [
17113
- {
17114
- "filename": "additional/Features/index.tsx",
17115
- "content": "// import { IkasComponentRenderer } from \"@ikas/bp-storefront\";\n\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function Features(props: Props) {\n const { backgroundColor, components } = props;\n\n return (\n <section\n className=\"kombos-features\"\n style={backgroundColor ? { backgroundColor } : undefined}\n >\n <div className=\"kombos-container kombos-features__container\">\n <IkasComponentRenderer\n id=\"features\"\n components={components}\n parentProps={props}\n />\n </div>\n </section>\n );\n}\n\nexport default Features;\n"
17116
- },
17117
- {
17118
- "filename": "additional/Features/styles.css",
17119
- "content": ".kombos-features {\n width: 100%;\n}\n\n.kombos-features__container {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 1.5rem;\n padding: 2rem 1rem;\n}\n\n.kombos-features__container > * > * > * {\n width: calc(50% - 0.75rem);\n flex-shrink: 0;\n}\n\n@media (min-width: 640px) {\n .kombos-features__container > * > * > * {\n width: calc(33.33% - 1rem);\n }\n}\n\n@media (min-width: 768px) {\n .kombos-features__container {\n padding: 3rem 2rem;\n }\n}\n\n@media (min-width: 1024px) {\n .kombos-features__container {\n padding: 3rem 4.5rem;\n }\n\n .kombos-features__container > * > * > * {\n width: auto;\n flex: 1 0 calc(16.66% - 1.25rem);\n }\n}\n"
17120
- },
17121
- {
17122
- "filename": "additional/Features/types.ts",
17123
- "content": "export interface Props {\n backgroundColor?: string;\n components?: any;\n}\n"
17124
- },
17125
- {
17126
- "filename": "additional/ProductDetail/index.tsx",
17127
- "content": "import {\n getIkasCategoryPathItemHref,\n getProductCategoryPath,\n getProductVariantMainImage,\n getSelectedProductVariant,\n IkasImage,\n isNotEmpty,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport Breadcrumb from \"../../sub-components/Breadcrumb\";\nimport type { BreadcrumbItem } from \"../../sub-components/Breadcrumb\";\nimport ProductGallery from \"./components/ProductGallery\";\n\nexport function ProductDetail(props: Props) {\n const {\n product,\n components,\n aspectRatio,\n objectFit,\n bottomComponents,\n homepageText = \"Anasayfa\",\n } = props;\n if (!product) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\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\n const categoryPath = getProductCategoryPath(product);\n\n return (\n <section className=\"kombos-pd\">\n <div className=\"kombos-container kombos-pd__container\">\n <Breadcrumb\n items={[\n { label: homepageText, href: \"/\" } as BreadcrumbItem,\n ...(isNotEmpty(categoryPath)\n ? categoryPath.map(\n (pathItem: any) =>\n ({\n label: pathItem.name,\n href: getIkasCategoryPathItemHref(pathItem),\n }) as BreadcrumbItem,\n )\n : []),\n { label: product.name } as BreadcrumbItem,\n ]}\n size=\"xs\"\n className=\"kombos-pd__breadcrumb\"\n />\n\n <div className=\"kombos-pd__layout\">\n <ProductGallery\n images={images}\n productName={product.name}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n />\n\n <div className=\"kombos-pd__info\">\n <IkasComponentRenderer\n id=\"product-detail-info\"\n components={components}\n parentProps={props}\n />\n </div>\n </div>\n\n {bottomComponents && (\n <div className=\"kombos-pd__bottom\">\n <IkasComponentRenderer\n id=\"product-detail-bottom\"\n components={bottomComponents}\n parentProps={props}\n />\n </div>\n )}\n </div>\n </section>\n );\n}\n\nexport default ProductDetail;\n"
17128
- },
17129
- {
17130
- "filename": "additional/ProductDetail/styles.css",
17131
- "content": "/* ===== Product Detail Section ===== */\n\n.kombos-pd {\n width: 100%;\n}\n\n/* ---- Breadcrumb (mobile only, hidden on desktop) ---- */\n.kombos-pd__breadcrumb {\n padding-top: 1rem;\n}\n\n/* ---- Layout ---- */\n.kombos-pd__layout {\n display: flex;\n flex-direction: column;\n padding-top: 1rem;\n}\n\n/* ---- Product Info ---- */\n.kombos-pd__info {\n display: flex;\n flex-direction: column;\n padding-top: 1rem;\n padding-bottom: 1rem;\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd__container {\n padding-top: 2rem;\n padding-bottom: 2rem;\n }\n\n .kombos-pd__breadcrumb {\n padding-top: 0;\n padding-bottom: 1.5rem;\n }\n\n .kombos-pd__layout {\n display: grid;\n grid-template-columns: 7fr 5fr;\n gap: 2rem;\n padding-top: 0;\n }\n\n /* Gallery — sticky on desktop */\n .kombos-pd__gallery {\n position: sticky;\n top: 0.75rem;\n align-self: start;\n }\n\n /* Info — right side */\n .kombos-pd__info {\n min-width: 0;\n padding: 0;\n }\n}\n"
17132
- },
17133
- {
17134
- "filename": "additional/ProductDetail/types.ts",
17135
- "content": "import type { IkasProduct } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../global-types\";\n\nexport interface Props {\n product?: IkasProduct | null;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n components?: any;\n bottomComponents?: any;\n homepageText?: string;\n}\n"
17136
- },
17137
- {
17138
- "filename": "ikas-config-snippet.json",
17139
- "content": "{\n \"id\": \"{{PROJECT_ID}}-header\",\n \"name\": \"Header\",\n \"type\": \"section\",\n \"entry\": \"./src/components/Header/index.tsx\",\n \"styles\": \"./src/components/Header/styles.css\",\n \"props\": [\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"defaultValue\": \"#ffffff\",\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"borderColor\",\n \"displayName\": \"Border Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"defaultValue\": \"#f6f6f6\",\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Components\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-navbar\",\n \"{{PROJECT_ID}}-announcements\",\n \"{{PROJECT_ID}}-cookie-bar\"\n ]\n }\n ],\n \"isHeader\": true,\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View\",\n \"description\": \"Background ve kenarlık renkleri\"\n }\n ]\n}"
17140
- },
17141
- {
17142
- "filename": "index.tsx",
17143
- "content": "// import { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport { useEffect, useCallback } from \"preact/hooks\";\nimport { Props } from \"./types\";\nimport { ToastContainer } from \"../../sub-components/Toast\";\nimport { useToast } from \"../../hooks/useToast\";\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\n\nexport function Header(props: Props) {\n const {\n backgroundColor = \"var(--kombos-white)\",\n borderColor = \"var(--kombos-gray-100)\",\n components,\n } = props;\n\n const toast = useToast();\n\n const handleShowToast = useCallback(\n (e: Event) => {\n const detail = (e as CustomEvent).detail as {\n message: string;\n variant: \"success\" | \"error\";\n };\n toast.show(detail.message, detail.variant);\n },\n [toast.show],\n );\n\n useEffect(() => {\n window.addEventListener(\"ikas:show-toast\", handleShowToast);\n return () => window.removeEventListener(\"ikas:show-toast\", handleShowToast);\n }, [handleShowToast]);\n\n return (\n <>\n <section\n className=\"kombos-header\"\n style={{\n backgroundColor,\n borderColor,\n }}\n >\n <IkasComponentRenderer\n id=\"header\"\n components={components}\n parentProps={props}\n />\n </section>\n <ToastContainer toasts={toast.toasts} onDismiss={toast.dismiss} />\n </>\n );\n}\n\nexport default Header;\n"
17144
- },
17145
- {
17146
- "filename": "styles.css",
17147
- "content": "/* ===== Header Section ===== */\n.kombos-header {\n width: 100%;\n background-color: var(--kombos-white);\n border-bottom: 1px solid var(--kombos-gray-200);\n}\n"
17148
- },
17149
- {
17150
- "filename": "types.ts",
17151
- "content": "export interface Props {\n backgroundColor?: string;\n borderColor?: string;\n components?: any;\n}\n"
17152
- }
17153
- ]
17154
- },
17155
- {
17156
- "id": "email-verification-section",
17157
- "title": "Email Verification Section",
17158
- "description": "Customer email verification page. Reads verification token from URL, calls customerStore.verifyEmail(), shows success or error state with configurable messages.",
17159
- "code": "import { useState, useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n activateCustomer,\n resendCustomerActivationMail,\n Router,\n} from \"@ikas/bp-storefront\";\n\nimport { Props } from \"./types\";\nimport { cx } from \"../../utils/cx\";\nimport { CheckCircleSVG, XCircleSVG } from \"../../sub-components/icons\";\nimport SpinnerIcon from \"../../sub-components/SpinnerIcon\";\nimport Button from \"../../sub-components/Button\";\nimport FormItem from \"../../sub-components/FormItem\";\nimport Input from \"../../sub-components/Input\";\n\ntype Status = \"loading\" | \"success\" | \"error\";\ntype ResendStatus = \"idle\" | \"sending\" | \"success\" | \"error\";\n\nexport function CustomerEmailVerification({\n title = \"E-posta Doğrulama\",\n successTitle = \"E-posta Doğrulandı\",\n successMessage = \"E-posta adresiniz başarıyla doğrulandı. Artık giriş yapabilirsiniz.\",\n errorTitle = \"Doğrulama Başarısız\",\n errorMessage = \"E-posta doğrulama bağlantısı geçersiz veya süresi dolmuş.\",\n loadingMessage = \"E-posta adresiniz doğrulanıyor, lütfen bekleyin...\",\n loginButtonText = \"Giriş Yap\",\n resendTitle = \"Doğrulama E-postasını Tekrar Gönder\",\n emailLabel = \"E-posta\",\n emailPlaceholder = \"ornek@email.com\",\n resendButtonText = \"Tekrar Gönder\",\n resendingButtonText = \"Gönderiliyor...\",\n resendSuccessMessage = \"Doğrulama e-postası başarıyla gönderildi.\",\n resendErrorMessage = \"E-posta gönderilemedi. Lütfen tekrar deneyin.\",\n}: Props) {\n const [status, setStatus] = useState<Status>(\"loading\");\n const [email, setEmail] = useState(\"\");\n const [resendStatus, setResendStatus] = useState<ResendStatus>(\"idle\");\n\n useEffect(() => {\n let cancelled = false;\n\n activateCustomer(customerStore)\n .then((success) => {\n if (!cancelled) setStatus(success ? \"success\" : \"error\");\n })\n .catch(() => {\n if (!cancelled) setStatus(\"error\");\n });\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n const resending = resendStatus === \"sending\";\n\n const handleResend = async () => {\n const trimmed = email.trim();\n if (!trimmed || resending) return;\n setResendStatus(\"sending\");\n\n const success = await resendCustomerActivationMail(customerStore, trimmed);\n setResendStatus(success ? \"success\" : \"error\");\n };\n\n return (\n <section className=\"customer-email-verification\">\n <div className=\"customer-email-verification__wrapper kombos-container\">\n <div className=\"customer-email-verification__container\">\n {status === \"loading\" && (\n <div className=\"customer-email-verification__state\">\n <h1 className=\"customer-email-verification__title text-xl-medium md:display-xs-medium\">\n {title}\n </h1>\n <SpinnerIcon className=\"customer-email-verification__spinner\" />\n <p className=\"customer-email-verification__state-text text-md-regular\">\n {loadingMessage}\n </p>\n </div>\n )}\n\n {status === \"success\" && (\n <div className=\"customer-email-verification__state\">\n <div className=\"customer-email-verification__icon customer-email-verification__icon--success\">\n <CheckCircleSVG />\n </div>\n <h1 className=\"customer-email-verification__title text-xl-medium md:display-xs-medium\">\n {successTitle}\n </h1>\n <p className=\"customer-email-verification__message text-md-regular\">{successMessage}</p>\n <Button\n className=\"customer-email-verification__action-btn\"\n variant=\"primary\"\n size=\"s\"\n onClick={() => Router.navigateToPage(\"LOGIN\")}\n >\n {loginButtonText}\n </Button>\n </div>\n )}\n\n {status === \"error\" && (\n <div className=\"customer-email-verification__state\">\n <div className=\"customer-email-verification__icon customer-email-verification__icon--error\">\n <XCircleSVG />\n </div>\n <h1 className=\"customer-email-verification__title text-xl-medium md:display-xs-medium\">\n {errorTitle}\n </h1>\n <p className=\"customer-email-verification__message text-md-regular\">{errorMessage}</p>\n\n <div className=\"customer-email-verification__resend\">\n <h2 className=\"customer-email-verification__resend-title text-md-semibold md:text-lg-semibold\">\n {resendTitle}\n </h2>\n <form\n className=\"customer-email-verification__resend-form\"\n onSubmit={(e) => {\n e.preventDefault();\n handleResend();\n }}\n >\n <FormItem label={emailLabel} htmlFor=\"customer-email-verification-email\">\n <Input\n id=\"customer-email-verification-email\"\n type=\"email\"\n name=\"email\"\n autoComplete=\"email\"\n placeholder={emailPlaceholder}\n value={email}\n onInput={(e) =>\n setEmail((e.target as HTMLInputElement).value)\n }\n />\n </FormItem>\n <Button\n className=\"customer-email-verification__resend-btn\"\n variant=\"primary\"\n size=\"s\"\n disabled={resending || !email.trim()}\n >\n {resending ? resendingButtonText : resendButtonText}\n </Button>\n </form>\n\n {(resendStatus === \"success\" || resendStatus === \"error\") && (\n <div\n className={cx(\n \"customer-email-verification__banner text-sm-regular\",\n `customer-email-verification__banner--${resendStatus}`,\n )}\n >\n {resendStatus === \"success\"\n ? resendSuccessMessage\n : resendErrorMessage}\n </div>\n )}\n </div>\n </div>\n )}\n </div>\n </div>\n </section>\n );\n}\n\nexport default CustomerEmailVerification;\n",
17160
- "relatedFunctions": [
17161
- "customerStore",
17162
- "activateCustomer",
17163
- "resendCustomerActivationMail",
17164
- "Router"
17165
- ],
17166
- "categories": [
17167
- "Customer",
17168
- "Auth"
17169
- ],
17170
- "files": [
17171
- {
17172
- "filename": "ikas-config-snippet.json",
17173
- "content": "{\n \"id\": \"{{PROJECT_ID}}-customer-email-verification\",\n \"name\": \"CustomerEmailVerification\",\n \"type\": \"section\",\n \"entry\": \"./src/components/CustomerEmailVerification/index.tsx\",\n \"styles\": \"./src/components/CustomerEmailVerification/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Account Aktivasyonu\",\n \"groupId\": \"loading\"\n },\n {\n \"name\": \"successTitle\",\n \"displayName\": \"Success Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Hesabınız Aktifleştirildi!\",\n \"groupId\": \"success\"\n },\n {\n \"name\": \"successMessage\",\n \"displayName\": \"Success Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Hesabınız başarıyla aktifleştirildi. Login yaparak alışverişe başlayabilirsiniz.\",\n \"groupId\": \"success\"\n },\n {\n \"name\": \"errorTitle\",\n \"displayName\": \"Error Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Aktivasyon Failed\",\n \"groupId\": \"error\"\n },\n {\n \"name\": \"errorMessage\",\n \"displayName\": \"Error Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Aktivasyon bağlantınız geçersiz veya süresi dolmuş olabilir.\",\n \"groupId\": \"error\"\n },\n {\n \"name\": \"loadingMessage\",\n \"displayName\": \"Loading Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Hesabınız aktifleştiriliyor...\",\n \"groupId\": \"loading\"\n },\n {\n \"name\": \"loginButtonText\",\n \"displayName\": \"Login Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sign In\",\n \"groupId\": \"success\"\n },\n {\n \"name\": \"resendTitle\",\n \"displayName\": \"Again Submit Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Aktivasyon Mailini Again Submit\",\n \"groupId\": \"resend\"\n },\n {\n \"name\": \"emailLabel\",\n \"displayName\": \"Email Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Email\",\n \"groupId\": \"resend\"\n },\n {\n \"name\": \"emailPlaceholder\",\n \"displayName\": \"Email Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"ornek@email.com\",\n \"groupId\": \"resend\"\n },\n {\n \"name\": \"resendButtonText\",\n \"displayName\": \"Again Submit Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Resend\",\n \"groupId\": \"resend\"\n },\n {\n \"name\": \"resendingButtonText\",\n \"displayName\": \"Submitting Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Gönderiliyor...\",\n \"groupId\": \"resend\"\n },\n {\n \"name\": \"resendSuccessMessage\",\n \"displayName\": \"Again Submission Success Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Aktivasyon maili tekrar gönderildi. Lütfen gelen kutunuzu kontrol edin.\",\n \"groupId\": \"resend\"\n },\n {\n \"name\": \"resendErrorMessage\",\n \"displayName\": \"Again Submission Error Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Mail gönderilemedi. Lütfen tekrar deneyin.\",\n \"groupId\": \"resend\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"loading\",\n \"name\": \"Loading\",\n \"description\": \"Email doğrulama işlemi devam ederken gösterilen başlık ve mesaj metinleri\"\n },\n {\n \"id\": \"success\",\n \"name\": \"Success\",\n \"description\": \"Doğrulama başarılı olduğunda gösterilen başlık, mesaj ve giriş butonu metinleri\"\n },\n {\n \"id\": \"error\",\n \"name\": \"Error\",\n \"description\": \"Doğrulama başarısız olduğunda gösterilen başlık ve hata mesajı metinleri\"\n },\n {\n \"id\": \"resend\",\n \"name\": \"Resend\",\n \"description\": \"Doğrulama e-postasını tekrar göndermek için kullanılan form alanı, buton ve durum mesajı metinleri\"\n }\n ]\n}"
17174
- },
17175
- {
17176
- "filename": "index.tsx",
17177
- "content": "import { useState, useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n activateCustomer,\n resendCustomerActivationMail,\n Router,\n} from \"@ikas/bp-storefront\";\n\nimport { Props } from \"./types\";\nimport { cx } from \"../../utils/cx\";\nimport { CheckCircleSVG, XCircleSVG } from \"../../sub-components/icons\";\nimport SpinnerIcon from \"../../sub-components/SpinnerIcon\";\nimport Button from \"../../sub-components/Button\";\nimport FormItem from \"../../sub-components/FormItem\";\nimport Input from \"../../sub-components/Input\";\n\ntype Status = \"loading\" | \"success\" | \"error\";\ntype ResendStatus = \"idle\" | \"sending\" | \"success\" | \"error\";\n\nexport function CustomerEmailVerification({\n title = \"E-posta Doğrulama\",\n successTitle = \"E-posta Doğrulandı\",\n successMessage = \"E-posta adresiniz başarıyla doğrulandı. Artık giriş yapabilirsiniz.\",\n errorTitle = \"Doğrulama Başarısız\",\n errorMessage = \"E-posta doğrulama bağlantısı geçersiz veya süresi dolmuş.\",\n loadingMessage = \"E-posta adresiniz doğrulanıyor, lütfen bekleyin...\",\n loginButtonText = \"Giriş Yap\",\n resendTitle = \"Doğrulama E-postasını Tekrar Gönder\",\n emailLabel = \"E-posta\",\n emailPlaceholder = \"ornek@email.com\",\n resendButtonText = \"Tekrar Gönder\",\n resendingButtonText = \"Gönderiliyor...\",\n resendSuccessMessage = \"Doğrulama e-postası başarıyla gönderildi.\",\n resendErrorMessage = \"E-posta gönderilemedi. Lütfen tekrar deneyin.\",\n}: Props) {\n const [status, setStatus] = useState<Status>(\"loading\");\n const [email, setEmail] = useState(\"\");\n const [resendStatus, setResendStatus] = useState<ResendStatus>(\"idle\");\n\n useEffect(() => {\n let cancelled = false;\n\n activateCustomer(customerStore)\n .then((success) => {\n if (!cancelled) setStatus(success ? \"success\" : \"error\");\n })\n .catch(() => {\n if (!cancelled) setStatus(\"error\");\n });\n\n return () => {\n cancelled = true;\n };\n }, []);\n\n const resending = resendStatus === \"sending\";\n\n const handleResend = async () => {\n const trimmed = email.trim();\n if (!trimmed || resending) return;\n setResendStatus(\"sending\");\n\n const success = await resendCustomerActivationMail(customerStore, trimmed);\n setResendStatus(success ? \"success\" : \"error\");\n };\n\n return (\n <section className=\"customer-email-verification\">\n <div className=\"customer-email-verification__wrapper kombos-container\">\n <div className=\"customer-email-verification__container\">\n {status === \"loading\" && (\n <div className=\"customer-email-verification__state\">\n <h1 className=\"customer-email-verification__title text-xl-medium md:display-xs-medium\">\n {title}\n </h1>\n <SpinnerIcon className=\"customer-email-verification__spinner\" />\n <p className=\"customer-email-verification__state-text text-md-regular\">\n {loadingMessage}\n </p>\n </div>\n )}\n\n {status === \"success\" && (\n <div className=\"customer-email-verification__state\">\n <div className=\"customer-email-verification__icon customer-email-verification__icon--success\">\n <CheckCircleSVG />\n </div>\n <h1 className=\"customer-email-verification__title text-xl-medium md:display-xs-medium\">\n {successTitle}\n </h1>\n <p className=\"customer-email-verification__message text-md-regular\">{successMessage}</p>\n <Button\n className=\"customer-email-verification__action-btn\"\n variant=\"primary\"\n size=\"s\"\n onClick={() => Router.navigateToPage(\"LOGIN\")}\n >\n {loginButtonText}\n </Button>\n </div>\n )}\n\n {status === \"error\" && (\n <div className=\"customer-email-verification__state\">\n <div className=\"customer-email-verification__icon customer-email-verification__icon--error\">\n <XCircleSVG />\n </div>\n <h1 className=\"customer-email-verification__title text-xl-medium md:display-xs-medium\">\n {errorTitle}\n </h1>\n <p className=\"customer-email-verification__message text-md-regular\">{errorMessage}</p>\n\n <div className=\"customer-email-verification__resend\">\n <h2 className=\"customer-email-verification__resend-title text-md-semibold md:text-lg-semibold\">\n {resendTitle}\n </h2>\n <form\n className=\"customer-email-verification__resend-form\"\n onSubmit={(e) => {\n e.preventDefault();\n handleResend();\n }}\n >\n <FormItem label={emailLabel} htmlFor=\"customer-email-verification-email\">\n <Input\n id=\"customer-email-verification-email\"\n type=\"email\"\n name=\"email\"\n autoComplete=\"email\"\n placeholder={emailPlaceholder}\n value={email}\n onInput={(e) =>\n setEmail((e.target as HTMLInputElement).value)\n }\n />\n </FormItem>\n <Button\n className=\"customer-email-verification__resend-btn\"\n variant=\"primary\"\n size=\"s\"\n disabled={resending || !email.trim()}\n >\n {resending ? resendingButtonText : resendButtonText}\n </Button>\n </form>\n\n {(resendStatus === \"success\" || resendStatus === \"error\") && (\n <div\n className={cx(\n \"customer-email-verification__banner text-sm-regular\",\n `customer-email-verification__banner--${resendStatus}`,\n )}\n >\n {resendStatus === \"success\"\n ? resendSuccessMessage\n : resendErrorMessage}\n </div>\n )}\n </div>\n </div>\n )}\n </div>\n </div>\n </section>\n );\n}\n\nexport default CustomerEmailVerification;\n"
17178
- },
17179
- {
17180
- "filename": "styles.css",
17181
- "content": ".customer-email-verification {\n width: 100%;\n}\n\n.customer-email-verification__wrapper {\n display: flex;\n justify-content: center;\n align-items: flex-start;\n padding-top: 1rem;\n padding-bottom: 1rem;\n}\n\n.customer-email-verification__container {\n width: 100%;\n max-width: 29rem;\n}\n\n.customer-email-verification__state {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1rem;\n text-align: center;\n}\n\n.customer-email-verification__spinner {\n font-size: 2.5rem;\n color: var(--kombos-gray-500);\n}\n\n.customer-email-verification__state-text {\n color: var(--kombos-gray-500);\n margin: 0;\n}\n\n.customer-email-verification__icon {\n font-size: 3rem;\n}\n\n.customer-email-verification__icon--success {\n color: var(--kombos-success);\n}\n\n.customer-email-verification__icon--error {\n color: var(--kombos-error);\n}\n\n.customer-email-verification__title {\n color: var(--kombos-gray-900);\n margin: 0;\n}\n\n.customer-email-verification__message {\n color: var(--kombos-gray-500);\n margin: 0;\n}\n\n.customer-email-verification__action-btn {\n width: 100%;\n margin-top: 0.5rem;\n}\n\n.customer-email-verification__resend {\n width: 100%;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n margin-top: 1rem;\n padding-top: 1.5rem;\n border-top: 1px solid var(--kombos-gray-200);\n}\n\n.customer-email-verification__resend-title {\n color: var(--kombos-gray-900);\n margin: 0;\n text-align: left;\n}\n\n.customer-email-verification__resend-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.customer-email-verification__resend-btn {\n width: 100%;\n}\n\n.customer-email-verification__banner {\n padding: 0.75rem 1rem;\n border-radius: 6px;\n}\n\n.customer-email-verification__banner--success {\n background: rgba(18, 183, 106, 0.08);\n border: 1px solid var(--kombos-success);\n color: var(--kombos-success);\n}\n\n.customer-email-verification__banner--error {\n background: rgba(255, 60, 72, 0.08);\n border: 1px solid var(--kombos-error);\n color: var(--kombos-error);\n}\n\n@media (min-width: 768px) {\n .customer-email-verification__wrapper {\n padding-top: 1.5rem;\n padding-bottom: 1.5rem;\n }\n}\n\n@media (min-width: 1024px) {\n .customer-email-verification__wrapper {\n padding-top: 2rem;\n padding-bottom: 2rem;\n }\n}\n"
17182
- },
17183
- {
17184
- "filename": "types.ts",
17185
- "content": "export interface Props {\n title?: string;\n successTitle?: string;\n successMessage?: string;\n errorTitle?: string;\n errorMessage?: string;\n loadingMessage?: string;\n loginButtonText?: string;\n resendTitle?: string;\n emailLabel?: string;\n emailPlaceholder?: string;\n resendButtonText?: string;\n resendingButtonText?: string;\n resendSuccessMessage?: string;\n resendErrorMessage?: string;\n}\n"
17186
- }
17187
- ]
17188
- },
17189
- {
17190
- "id": "favorites",
17191
- "title": "Favorites Pattern",
17192
- "description": "Product favorites (wishlist) implementation with toggle functionality. Shows isFavoriteIkasProduct, addFavoriteProduct, removeFavoriteProduct, and customer authentication checks for favorites.",
17193
- "code": "import {\n addIkasProductToFavorites,\n customerStore,\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n hasCustomer,\n isFavoriteIkasProduct,\n removeIkasProductFromFavorites,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport { Heart2SVG, HeartFilledSVG } from \"../../sub-components/icons\";\n\nexport function ProductDetailNameFavorite({\n product,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n hideFavoriteButton,\n}: Props) {\n if (!product) return null;\n\n const isFavorite = isFavoriteIkasProduct(product);\n\n const handleToggleFavorite = async () => {\n const isLoggedIn = hasCustomer(customerStore);\n\n if (!isLoggedIn) {\n Router.navigateToPage(\"LOGIN\");\n return;\n }\n\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n\n return (\n <div\n className=\"kombos-pd-name__row\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n <h1 className=\"kombos-pd-name__title text-xl-medium lg:display-xs-medium\">{product.name}</h1>\n {!hideFavoriteButton && (\n <button\n type=\"button\"\n className=\"kombos-pd-name__fav-btn\"\n onClick={handleToggleFavorite}\n aria-label={isFavorite ? \"Remove from favorites\" : \"Add to favorites\"}\n >\n {isFavorite ? <HeartFilledSVG /> : <Heart2SVG />}\n </button>\n )}\n </div>\n );\n}\n\nexport default ProductDetailNameFavorite;\n",
17194
- "relatedFunctions": [
17195
- "addIkasProductToFavorites",
17196
- "customerStore",
17197
- "getFormattedMarginTopSize",
17198
- "getFormattedMarginBottomSize",
17199
- "hasCustomer",
17200
- "isFavoriteIkasProduct",
17201
- "removeIkasProductFromFavorites",
17202
- "Router"
17203
- ],
17204
- "categories": [
17205
- "Product",
17206
- "Customer"
17207
- ],
17208
- "files": [
17209
- {
17210
- "filename": "ikas-config-snippet.json",
17211
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-name-favorite\",\n \"name\": \"ProductDetailNameFavorite\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailNameFavorite/index.tsx\",\n \"styles\": \"./src/components/ProductDetailNameFavorite/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"hideFavoriteButton\",\n \"displayName\": \"Favorite Butonunu Hide\",\n \"type\": \"BOOLEAN\",\n \"required\": false\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n }\n ]\n}"
17212
- },
17213
- {
17214
- "filename": "index.tsx",
17215
- "content": "import {\n addIkasProductToFavorites,\n customerStore,\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n hasCustomer,\n isFavoriteIkasProduct,\n removeIkasProductFromFavorites,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport { Heart2SVG, HeartFilledSVG } from \"../../sub-components/icons\";\n\nexport function ProductDetailNameFavorite({\n product,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n hideFavoriteButton,\n}: Props) {\n if (!product) return null;\n\n const isFavorite = isFavoriteIkasProduct(product);\n\n const handleToggleFavorite = async () => {\n const isLoggedIn = hasCustomer(customerStore);\n\n if (!isLoggedIn) {\n Router.navigateToPage(\"LOGIN\");\n return;\n }\n\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n\n return (\n <div\n className=\"kombos-pd-name__row\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n <h1 className=\"kombos-pd-name__title text-xl-medium lg:display-xs-medium\">{product.name}</h1>\n {!hideFavoriteButton && (\n <button\n type=\"button\"\n className=\"kombos-pd-name__fav-btn\"\n onClick={handleToggleFavorite}\n aria-label={isFavorite ? \"Remove from favorites\" : \"Add to favorites\"}\n >\n {isFavorite ? <HeartFilledSVG /> : <Heart2SVG />}\n </button>\n )}\n </div>\n );\n}\n\nexport default ProductDetailNameFavorite;\n"
17216
- },
17217
- {
17218
- "filename": "styles.css",
17219
- "content": "/* ===== ProductDetailNameFavorite ===== */\n\n.kombos-pd-name__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.5rem;\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n.kombos-pd-name__title {\n flex: 1;\n margin: 0;\n color: var(--kombos-gray-900);\n}\n\n.kombos-pd-name__fav-btn {\n flex-shrink: 0;\n width: 1.5rem;\n height: 1.5rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n background: none;\n cursor: pointer;\n font-size: 1.5rem;\n color: var(--kombos-gray-700);\n padding: 0;\n}\n\n.kombos-pd-name__fav-btn:hover {\n color: var(--kombos-gray-900);\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd-name__row {\n gap: 1.5rem;\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n\n}\n"
17220
- },
17221
- {
17222
- "filename": "types.ts",
17223
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n hideFavoriteButton?: boolean;\n}\n"
17224
- }
17225
- ]
17226
- },
17227
- {
17228
- "id": "features-section",
17229
- "title": "Features Section",
17230
- "description": "Feature highlights grid using IkasComponentRenderer to render FeatureItem children with icon and text props.",
17231
- "code": "// import { IkasComponentRenderer } from \"@ikas/bp-storefront\";\n\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function Features(props: Props) {\n const { backgroundColor, components } = props;\n\n return (\n <section\n className=\"kombos-features\"\n style={backgroundColor ? { backgroundColor } : undefined}\n >\n <div className=\"kombos-container kombos-features__container\">\n <IkasComponentRenderer\n id=\"features\"\n components={components}\n parentProps={props}\n />\n </div>\n </section>\n );\n}\n\nexport default Features;\n",
17232
- "relatedFunctions": [
17233
- "getDefaultSrc"
17234
- ],
17235
- "categories": [
17236
- "Layout"
17237
- ],
17238
- "files": [
17239
- {
17240
- "filename": "children/FeatureItem/ikas-config-snippet.json",
17241
- "content": "{\n \"id\": \"{{PROJECT_ID}}-feature-item\",\n \"name\": \"FeatureItem\",\n \"type\": \"component\",\n \"entry\": \"./src/components/FeatureItem/index.tsx\",\n \"styles\": \"./src/components/FeatureItem/styles.css\",\n \"props\": [\n {\n \"name\": \"image\",\n \"displayName\": \"Image\",\n \"type\": \"IMAGE\",\n \"required\": false\n },\n {\n \"name\": \"text\",\n \"displayName\": \"Text\",\n \"type\": \"RICH_TEXT\",\n \"required\": false\n }\n ]\n}"
17242
- },
17243
- {
17244
- "filename": "children/FeatureItem/index.tsx",
17245
- "content": "import { getDefaultSrc } from \"@ikas/bp-storefront\";\n\nimport { Props } from \"./types\";\n\nexport function FeatureItem({ image, text }: Props) {\n const src = image ? getDefaultSrc(image) : undefined;\n\n return (\n <div className=\"kombos-feature-item\">\n {src && (\n <img\n className=\"kombos-feature-item__icon\"\n src={src}\n alt={image?.altText || \"\"}\n />\n )}\n {text && (\n <div\n className=\"kombos-feature-item__text text-sm-medium\"\n dangerouslySetInnerHTML={{ __html: text }}\n />\n )}\n </div>\n );\n}\n\nexport default FeatureItem;\n"
17246
- },
17247
- {
17248
- "filename": "children/FeatureItem/styles.css",
17249
- "content": ".kombos-feature-item {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1.5rem;\n}\n\n.kombos-feature-item__icon {\n width: 2rem;\n height: 2rem;\n overflow: hidden;\n object-fit: contain;\n flex-shrink: 0;\n}\n\n.kombos-feature-item__text {\n color: var(--kombos-gray-900);\n text-align: center;\n}\n"
17250
- },
17251
- {
17252
- "filename": "children/FeatureItem/types.ts",
17253
- "content": "import type { IkasImage } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n image?: IkasImage | null;\n text?: string;\n}\n"
17254
- },
17255
- {
17256
- "filename": "ikas-config-snippet.json",
17257
- "content": "{\n \"id\": \"{{PROJECT_ID}}-features\",\n \"name\": \"Features\",\n \"type\": \"section\",\n \"entry\": \"./src/components/Features/index.tsx\",\n \"styles\": \"./src/components/Features/styles.css\",\n \"props\": [\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"defaultValue\": \"#fafafa\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Features\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-feature-item\"\n ]\n }\n ]\n}"
17258
- },
17259
- {
17260
- "filename": "index.tsx",
17261
- "content": "// import { IkasComponentRenderer } from \"@ikas/bp-storefront\";\n\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function Features(props: Props) {\n const { backgroundColor, components } = props;\n\n return (\n <section\n className=\"kombos-features\"\n style={backgroundColor ? { backgroundColor } : undefined}\n >\n <div className=\"kombos-container kombos-features__container\">\n <IkasComponentRenderer\n id=\"features\"\n components={components}\n parentProps={props}\n />\n </div>\n </section>\n );\n}\n\nexport default Features;\n"
17262
- },
17263
- {
17264
- "filename": "styles.css",
17265
- "content": ".kombos-features {\n width: 100%;\n}\n\n.kombos-features__container {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 1.5rem;\n padding: 2rem 1rem;\n}\n\n.kombos-features__container > * > * > * {\n width: calc(50% - 0.75rem);\n flex-shrink: 0;\n}\n\n@media (min-width: 640px) {\n .kombos-features__container > * > * > * {\n width: calc(33.33% - 1rem);\n }\n}\n\n@media (min-width: 768px) {\n .kombos-features__container {\n padding: 3rem 2rem;\n }\n}\n\n@media (min-width: 1024px) {\n .kombos-features__container {\n padding: 3rem 4.5rem;\n }\n\n .kombos-features__container > * > * > * {\n width: auto;\n flex: 1 0 calc(16.66% - 1.25rem);\n }\n}\n"
17266
- },
17267
- {
17268
- "filename": "types.ts",
17269
- "content": "export interface Props {\n backgroundColor?: string;\n components?: any;\n}\n"
17270
- }
17271
- ]
17272
- },
17273
- {
17274
- "id": "footer-section",
17275
- "title": "Footer Section",
17276
- "description": "Complete footer with multiple link columns, social media icons via IkasComponentRenderer, newsletter signup, and copyright text. Configurable colors and layout.",
17277
- "code": "import {\n IkasNavigationLink,\n IkasComponentRenderer,\n createMediaSrcset,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport { IkasLogoSVG } from \"../../sub-components/icons\";\n\nexport function Footer(props: Props) {\n const {\n logo,\n description,\n copyrightText = \"© 2026 ikas Mağazası. Tüm Hakları Saklıdır\",\n linkColor,\n linkHoverColor,\n contactTitle = \"İletişim Bilgileri\",\n contactEmail,\n contactPhone,\n footerLinks,\n socialMediaIcons,\n } = props;\n\n const columns = footerLinks?.links ?? [];\n const hasContact = !!(contactEmail || contactPhone);\n const hasSocials = socialMediaIcons?.length > 0;\n\n return (\n <footer\n className=\"kombos-footer\"\n style={{\n...(linkColor ? { \"--footer-link-color\": linkColor } : {}),\n ...(linkHoverColor\n ? { \"--footer-link-hover-color\": linkHoverColor }\n : {}),\n }}\n >\n <div className=\"kombos-footer__wrapper kombos-container\">\n <div className=\"kombos-footer__top\">\n {/* Brand column */}\n <div className=\"kombos-footer__brand\">\n {logo && (\n <div className=\"kombos-footer__logo-wrap\">\n <img\n src={getDefaultSrc(logo)}\n srcSet={createMediaSrcset(logo)}\n sizes=\"96px\"\n alt={logo?.altText || \"Logo\"}\n className=\"kombos-footer__logo-img\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n </div>\n )}\n\n {description && (\n <div\n className=\"kombos-footer__desc text-sm-regular\"\n dangerouslySetInnerHTML={{ __html: description }}\n />\n )}\n\n {hasSocials && (\n <div className=\"kombos-footer__socials\">\n <IkasComponentRenderer\n id=\"footer-socials\"\n components={socialMediaIcons}\n parentProps={props}\n />\n </div>\n )}\n </div>\n\n {/* Link columns */}\n {(columns.length > 0 || hasContact) && (\n <div className=\"kombos-footer__columns\">\n {columns.map((column: IkasNavigationLink, i: number) => (\n <div key={i} className=\"kombos-footer__col\">\n <p className=\"kombos-footer__col-title text-sm-medium\">\n {column.label}\n </p>\n {column.subLinks?.length > 0 && (\n <nav className=\"kombos-footer__col-links\">\n {column.subLinks.map(\n (link: IkasNavigationLink, j: number) => (\n <a\n key={j}\n href={link.href}\n className=\"kombos-footer__col-link text-sm-regular\"\n target={link.openInNewTab ? \"_blank\" : undefined}\n rel={\n link.openInNewTab\n ? \"noopener noreferrer\"\n : undefined\n }\n >\n {link.label}\n </a>\n ),\n )}\n </nav>\n )}\n </div>\n ))}\n\n {/* Contact column */}\n {hasContact && (\n <div className=\"kombos-footer__col\">\n <p className=\"kombos-footer__col-title text-sm-medium\">\n {contactTitle}\n </p>\n <div className=\"kombos-footer__col-links\">\n {contactEmail && (\n <a\n href={`mailto:${contactEmail}`}\n className=\"kombos-footer__col-link text-sm-regular\"\n >\n {contactEmail}\n </a>\n )}\n {contactPhone && (\n <a\n href={`tel:${contactPhone.replace(/\\s/g, \"\")}`}\n className=\"kombos-footer__col-link text-sm-regular\"\n >\n {contactPhone}\n </a>\n )}\n </div>\n </div>\n )}\n </div>\n )}\n </div>\n\n {/* Bottom bar */}\n <div className=\"kombos-footer__bottom\">\n <div\n className=\"kombos-footer__copyright text-xs-regular\"\n dangerouslySetInnerHTML={{ __html: copyrightText }}\n />\n <div className=\"kombos-footer__badge\">\n <IkasLogoSVG className=\"kombos-footer__badge-logo\" />\n <span className=\"kombos-footer__badge-text text-xs-medium\">\n E-Ticaret Altyapısı ile Hazırlanmıştır.\n </span>\n </div>\n </div>\n </div>\n </footer>\n );\n}\n\nexport default Footer;\n",
17278
- "relatedFunctions": [
17279
- "createMediaSrcset",
17280
- "getDefaultSrc"
17281
- ],
17282
- "categories": [
17283
- "Navigation",
17284
- "Footer"
17285
- ],
17286
- "files": [
17287
- {
17288
- "filename": "children/SocialMediaIcon/ikas-config-snippet.json",
17289
- "content": "{\n \"id\": \"{{PROJECT_ID}}-social-media-icon\",\n \"name\": \"SocialMediaIcon\",\n \"type\": \"component\",\n \"entry\": \"./src/components/SocialMediaIcon/index.tsx\",\n \"styles\": \"./src/components/SocialMediaIcon/styles.css\",\n \"props\": [\n {\n \"name\": \"icon\",\n \"displayName\": \"Icon\",\n \"type\": \"IMAGE\",\n \"required\": false\n },\n {\n \"name\": \"link\",\n \"displayName\": \"Link\",\n \"type\": \"LINK\",\n \"required\": false\n }\n ]\n}"
17290
- },
17291
- {
17292
- "filename": "children/SocialMediaIcon/index.tsx",
17293
- "content": "import { getDefaultSrc, createMediaSrcset } from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function SocialMediaIcon({ icon, link }: Props) {\n if (!icon) return null;\n\n return (\n <a\n href={link?.href}\n target={link?.openInNewTab ? \"_blank\" : undefined}\n rel={link?.openInNewTab ? \"noopener noreferrer\" : undefined}\n aria-label={link?.label || undefined}\n className=\"kombos-social-media-icon\"\n >\n <img\n src={getDefaultSrc(icon)}\n srcSet={createMediaSrcset(icon)}\n sizes=\"20px\"\n alt={link?.label || \"\"}\n className=\"kombos-social-media-icon__img\"\n />\n </a>\n );\n}\n\nexport default SocialMediaIcon;\n"
17294
- },
17295
- {
17296
- "filename": "children/SocialMediaIcon/styles.css",
17297
- "content": ".kombos-social-media-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n text-decoration: none;\n transition: opacity 0.15s ease;\n}\n\n.kombos-social-media-icon:hover {\n opacity: 0.7;\n}\n\n.kombos-social-media-icon__img {\n width: 1.25rem;\n height: 1.25rem;\n object-fit: contain;\n}\n"
17298
- },
17299
- {
17300
- "filename": "children/SocialMediaIcon/types.ts",
17301
- "content": "import type { IkasImage, IkasNavigationLink } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n icon?: IkasImage | null;\n link?: IkasNavigationLink | null;\n}\n"
17302
- },
17303
- {
17304
- "filename": "ikas-config-snippet.json",
17305
- "content": "{\n \"id\": \"{{PROJECT_ID}}-footer\",\n \"name\": \"Footer\",\n \"type\": \"section\",\n \"entry\": \"./src/components/Footer/index.tsx\",\n \"styles\": \"./src/components/Footer/styles.css\",\n \"props\": [\n {\n \"name\": \"logo\",\n \"displayName\": \"Logo\",\n \"type\": \"IMAGE\",\n \"required\": false,\n \"groupId\": \"content\"\n },\n {\n \"name\": \"description\",\n \"displayName\": \"Description\",\n \"type\": \"RICH_TEXT\",\n \"required\": false,\n \"groupId\": \"content\"\n },\n {\n \"name\": \"copyrightText\",\n \"displayName\": \"Copyright Rights Text\",\n \"type\": \"RICH_TEXT\",\n \"required\": false,\n \"defaultValue\": \"<p>© 2026 ikas Mağazası. Tüm Hakları Saklıdır.</p>\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"footerLinks\",\n \"displayName\": \"Footer Links\",\n \"type\": \"LIST_OF_LINK\",\n \"required\": false,\n \"groupId\": \"links\"\n },\n {\n \"name\": \"linkColor\",\n \"displayName\": \"Link Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"linkHoverColor\",\n \"displayName\": \"Link Hover Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"contactTitle\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"İletişim Info\",\n \"groupId\": \"contact\"\n },\n {\n \"name\": \"contactEmail\",\n \"displayName\": \"Email\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"groupId\": \"contact\"\n },\n {\n \"name\": \"contactPhone\",\n \"displayName\": \"Phone\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"groupId\": \"contact\"\n },\n {\n \"name\": \"socialMediaIcons\",\n \"displayName\": \"Social Media Icons\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-social-media-icon\"\n ]\n }\n ],\n \"isFooter\": true,\n \"propGroups\": [\n {\n \"id\": \"links\",\n \"name\": \"Navigation\",\n \"description\": \"Site bağlantıları ve iletişim bilgileri\",\n \"children\": [\n {\n \"id\": \"contact\",\n \"name\": \"İletişim\",\n \"description\": \"İletişim başlığı, e-posta ve telefon bilgileri\"\n }\n ]\n },\n {\n \"id\": \"appearance\",\n \"name\": \"View\",\n \"description\": \"Link renkleri\"\n },\n {\n \"id\": \"content\",\n \"name\": \"Content\",\n \"description\": \"Logo, açıklama ve telif hakkı metni\"\n }\n ]\n}"
17306
- },
17307
- {
17308
- "filename": "index.tsx",
17309
- "content": "import {\n IkasNavigationLink,\n IkasComponentRenderer,\n createMediaSrcset,\n getDefaultSrc,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport { IkasLogoSVG } from \"../../sub-components/icons\";\n\nexport function Footer(props: Props) {\n const {\n logo,\n description,\n copyrightText = \"© 2026 ikas Mağazası. Tüm Hakları Saklıdır\",\n linkColor,\n linkHoverColor,\n contactTitle = \"İletişim Bilgileri\",\n contactEmail,\n contactPhone,\n footerLinks,\n socialMediaIcons,\n } = props;\n\n const columns = footerLinks?.links ?? [];\n const hasContact = !!(contactEmail || contactPhone);\n const hasSocials = socialMediaIcons?.length > 0;\n\n return (\n <footer\n className=\"kombos-footer\"\n style={{\n...(linkColor ? { \"--footer-link-color\": linkColor } : {}),\n ...(linkHoverColor\n ? { \"--footer-link-hover-color\": linkHoverColor }\n : {}),\n }}\n >\n <div className=\"kombos-footer__wrapper kombos-container\">\n <div className=\"kombos-footer__top\">\n {/* Brand column */}\n <div className=\"kombos-footer__brand\">\n {logo && (\n <div className=\"kombos-footer__logo-wrap\">\n <img\n src={getDefaultSrc(logo)}\n srcSet={createMediaSrcset(logo)}\n sizes=\"96px\"\n alt={logo?.altText || \"Logo\"}\n className=\"kombos-footer__logo-img\"\n loading=\"lazy\"\n decoding=\"async\"\n />\n </div>\n )}\n\n {description && (\n <div\n className=\"kombos-footer__desc text-sm-regular\"\n dangerouslySetInnerHTML={{ __html: description }}\n />\n )}\n\n {hasSocials && (\n <div className=\"kombos-footer__socials\">\n <IkasComponentRenderer\n id=\"footer-socials\"\n components={socialMediaIcons}\n parentProps={props}\n />\n </div>\n )}\n </div>\n\n {/* Link columns */}\n {(columns.length > 0 || hasContact) && (\n <div className=\"kombos-footer__columns\">\n {columns.map((column: IkasNavigationLink, i: number) => (\n <div key={i} className=\"kombos-footer__col\">\n <p className=\"kombos-footer__col-title text-sm-medium\">\n {column.label}\n </p>\n {column.subLinks?.length > 0 && (\n <nav className=\"kombos-footer__col-links\">\n {column.subLinks.map(\n (link: IkasNavigationLink, j: number) => (\n <a\n key={j}\n href={link.href}\n className=\"kombos-footer__col-link text-sm-regular\"\n target={link.openInNewTab ? \"_blank\" : undefined}\n rel={\n link.openInNewTab\n ? \"noopener noreferrer\"\n : undefined\n }\n >\n {link.label}\n </a>\n ),\n )}\n </nav>\n )}\n </div>\n ))}\n\n {/* Contact column */}\n {hasContact && (\n <div className=\"kombos-footer__col\">\n <p className=\"kombos-footer__col-title text-sm-medium\">\n {contactTitle}\n </p>\n <div className=\"kombos-footer__col-links\">\n {contactEmail && (\n <a\n href={`mailto:${contactEmail}`}\n className=\"kombos-footer__col-link text-sm-regular\"\n >\n {contactEmail}\n </a>\n )}\n {contactPhone && (\n <a\n href={`tel:${contactPhone.replace(/\\s/g, \"\")}`}\n className=\"kombos-footer__col-link text-sm-regular\"\n >\n {contactPhone}\n </a>\n )}\n </div>\n </div>\n )}\n </div>\n )}\n </div>\n\n {/* Bottom bar */}\n <div className=\"kombos-footer__bottom\">\n <div\n className=\"kombos-footer__copyright text-xs-regular\"\n dangerouslySetInnerHTML={{ __html: copyrightText }}\n />\n <div className=\"kombos-footer__badge\">\n <IkasLogoSVG className=\"kombos-footer__badge-logo\" />\n <span className=\"kombos-footer__badge-text text-xs-medium\">\n E-Ticaret Altyapısı ile Hazırlanmıştır.\n </span>\n </div>\n </div>\n </div>\n </footer>\n );\n}\n\nexport default Footer;\n"
17310
- },
17311
- {
17312
- "filename": "styles.css",
17313
- "content": ".kombos-footer {\n width: 100%;\n border-top: 1px solid var(--kombos-gray-200);\n}\n\n.kombos-footer__wrapper {\n padding-top: 1.5rem;\n padding-bottom: 1.5rem;\n display: flex;\n flex-direction: column;\n gap: 3rem;\n}\n\n/* ===== Top section ===== */\n\n.kombos-footer__top {\n display: flex;\n flex-direction: column;\n gap: 3rem;\n}\n\n/* Brand */\n\n.kombos-footer__brand {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.kombos-footer__logo-wrap {\n width: 6rem;\n height: 4rem;\n overflow: hidden;\n}\n\n.kombos-footer__logo-img {\n width: 100%;\n height: 100%;\n object-fit: contain;\n object-position: left center;\n}\n\n.kombos-footer__desc {\n margin: 0;\n color: var(--kombos-gray-700);\n max-width: 21.5rem;\n}\n\n/* Socials */\n\n.kombos-footer__socials {\n display: flex;\n gap: 1rem;\n align-items: center;\n font-size: 1.25rem;\n}\n\n/* Link columns */\n\n.kombos-footer__columns {\n display: grid;\n grid-template-columns: 1fr;\n gap: 2rem;\n}\n\n.kombos-footer__col {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n min-width: 0;\n}\n\n.kombos-footer__col-title {\n margin: 0;\n color: var(--kombos-gray-900);\n}\n\n.kombos-footer__col-links {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.kombos-footer__col-link {\n color: var(--footer-link-color, var(--kombos-gray-700));\n text-decoration: none;\n transition: color 0.15s ease;\n width: fit-content;\n}\n\n.kombos-footer__col-link:hover {\n color: var(--footer-link-hover-color, var(--kombos-gray-900));\n}\n\n/* ===== Bottom bar ===== */\n\n.kombos-footer__bottom {\n border-top: 1px solid var(--kombos-gray-200);\n padding-top: 1rem;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.kombos-footer__copyright {\n margin: 0;\n color: var(--kombos-gray-500);\n}\n\n.kombos-footer__badge {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.25rem 0.5rem;\n background: var(--kombos-gray-50);\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n width: fit-content;\n}\n\n.kombos-footer__badge-logo {\n width: 42px;\n height: auto;\n color: var(--kombos-gray-900);\n}\n\n.kombos-footer__badge-text {\n color: var(--kombos-gray-900);\n white-space: nowrap;\n}\n\n/* ===== 2 columns (660px+) ===== */\n\n@media (min-width: 660px) {\n .kombos-footer__columns {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n/* ===== Tablet (769px+) ===== */\n\n@media (min-width: 768px) {\n .kombos-footer__socials {\n gap: 0.75rem;\n }\n}\n\n/* ===== Desktop (1025px+) ===== */\n\n@media (min-width: 1024px) {\n .kombos-footer__wrapper {\n padding-top: 3rem;\n }\n\n .kombos-footer__top {\n flex-direction: row;\n gap: 7.75rem;\n }\n\n .kombos-footer__brand {\n width: 21.5rem;\n flex-shrink: 0;\n }\n\n .kombos-footer__columns {\n flex: 1;\n grid-template-columns: repeat(3, 1fr);\n }\n\n .kombos-footer__bottom {\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n }\n}\n"
17314
- },
17315
- {
17316
- "filename": "types.ts",
17317
- "content": "import type { IkasImage, IkasNavigationLinkList } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n logo?: IkasImage | null;\n description?: string;\n copyrightText?: string;\n footerLinks?: IkasNavigationLinkList;\n linkColor?: string;\n linkHoverColor?: string;\n contactTitle?: string;\n contactEmail?: string;\n contactPhone?: string;\n socialMediaIcons?: any;\n}\n"
17318
- }
17319
- ]
17320
- },
17321
- {
17322
- "id": "forgot-password-section",
17323
- "title": "Forgot Password Section",
17324
- "description": "Password recovery request form. Sends reset email via customerStore.sendRecoverPasswordEmail(). Includes email validation and success/error states.",
17325
- "code": "import {\n customerStore,\n getForgotPasswordForm,\n initForgotPasswordForm,\n} from \"@ikas/bp-storefront\";\nimport { useRedirectIfLoggedIn } from \"../../hooks/useRedirectIfLoggedIn\";\n\nimport { Props } from \"./types\";\nimport ForgotPasswordForm from \"./components/ForgotPasswordForm\";\nimport PageLoader from \"../../sub-components/PageLoader\";\n\nexport function ForgotPassword(props: Props) {\n const forgotForm = getForgotPasswordForm(customerStore);\n\n const isChecking = useRedirectIfLoggedIn(() => {\n initForgotPasswordForm(forgotForm);\n });\n\n if (isChecking) return <PageLoader />;\n\n return (\n <section className=\"forgot-password\">\n <div className=\"forgot-password__wrapper kombos-container\">\n <ForgotPasswordForm forgotForm={forgotForm} {...props} />\n </div>\n </section>\n );\n}\n\nexport default ForgotPassword;\n",
17326
- "relatedFunctions": [
17327
- "customerStore",
17328
- "getForgotPasswordForm",
17329
- "initForgotPasswordForm"
17330
- ],
17331
- "categories": [
17332
- "Customer",
17333
- "Auth",
17334
- "Form"
17335
- ],
17336
- "files": [
17337
- {
17338
- "filename": "components/ForgotPasswordForm/index.tsx",
17339
- "content": "import {\n getForgotPasswordForm,\n setForgotPasswordFormEmail,\n submitForgotPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\n\nimport { Props } from \"../../types\";\nimport FormItem from \"../../../../sub-components/FormItem\";\nimport Input from \"../../../../sub-components/Input\";\nimport Button from \"../../../../sub-components/Button\";\n\ninterface ForgotPasswordFormProps extends Props {\n forgotForm: ReturnType<typeof getForgotPasswordForm>;\n}\n\nconst ForgotPasswordForm = observer(function ForgotPasswordForm({\n forgotForm,\n title = \"Parolamı Unuttum\",\n subtitle = \"Şifrenizi sıfırlamanız için size bir e-posta göndereceğiz\",\n emailLabel = \"E-posta\",\n emailPlaceholder = \"yildo@ikas.com\",\n submitButtonText = \"Gönder\",\n submittingButtonText = \"Gönderiliyor...\",\n successTitle = \"Parolamı Unuttum\",\n successMessage = \"Size bir e-posta gönderdik.\",\n successButtonText = \"Alışverişe Başla\",\n backToLoginText = \"Giriş Sayfasına Dön\",\n}: ForgotPasswordFormProps) {\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n await submitForgotPasswordForm(forgotForm);\n };\n\n if (forgotForm.isSuccess) {\n return (\n <div className=\"forgot-password__container\">\n <div className=\"forgot-password__header\">\n <h1 className=\"forgot-password__title text-xl-medium md:display-xs-medium\">\n {successTitle}\n </h1>\n <p className=\"forgot-password__subtitle text-sm-regular\">\n {successMessage}\n </p>\n </div>\n\n <Button\n variant=\"primary\"\n size=\"s\"\n className=\"forgot-password__submit-btn\"\n onClick={() => Router.navigate(\"/\")}\n >\n {successButtonText}\n </Button>\n\n <div className=\"forgot-password__footer\">\n <button\n type=\"button\"\n className=\"forgot-password__back-link text-sm-medium\"\n onClick={() => Router.navigateToPage(\"LOGIN\")}\n >\n {backToLoginText}\n </button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"forgot-password__container\">\n <div className=\"forgot-password__header\">\n <h1 className=\"forgot-password__title text-xl-medium md:display-xs-medium\">{title}</h1>\n <p className=\"forgot-password__subtitle text-sm-regular\">{subtitle}</p>\n </div>\n\n {forgotForm.isFailure && forgotForm.responseMessage && (\n <div className=\"forgot-password__error-banner text-sm-regular\">\n {forgotForm.responseMessage}\n </div>\n )}\n\n <form className=\"forgot-password__form\" onSubmit={handleSubmit}>\n <FormItem\n label={emailLabel}\n htmlFor=\"forgot-email\"\n status={forgotForm.email?.hasError ? \"error\" : \"default\"}\n helper={\n forgotForm.email?.hasError ? forgotForm.email.message : undefined\n }\n >\n <Input\n id=\"forgot-email\"\n type=\"email\"\n placeholder={emailPlaceholder}\n value={forgotForm.email?.value ?? \"\"}\n onInput={(e: Event) =>\n setForgotPasswordFormEmail(\n forgotForm,\n (e.target as HTMLInputElement).value,\n )\n }\n />\n </FormItem>\n\n <Button\n variant=\"primary\"\n size=\"s\"\n className=\"forgot-password__submit-btn\"\n disabled={forgotForm.isSubmitting}\n >\n {forgotForm.isSubmitting ? submittingButtonText : submitButtonText}\n </Button>\n </form>\n\n <div className=\"forgot-password__footer\">\n <button\n type=\"button\"\n className=\"forgot-password__back-link text-sm-medium\"\n onClick={() => Router.navigateToPage(\"LOGIN\")}\n >\n {backToLoginText}\n </button>\n </div>\n </div>\n );\n});\n\nexport default ForgotPasswordForm;\n"
17340
- },
17341
- {
17342
- "filename": "components/ForgotPasswordForm/styles.css",
17343
- "content": ""
17344
- },
17345
- {
17346
- "filename": "ikas-config-snippet.json",
17347
- "content": "{\n \"id\": \"{{PROJECT_ID}}-forgot-password\",\n \"name\": \"ForgotPassword\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ForgotPassword/index.tsx\",\n \"styles\": \"./src/components/ForgotPassword/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Password Unuttum\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"subtitle\",\n \"displayName\": \"Description\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Şifrenizi sıfırlamanız için size bir e-posta göndereceğiz\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"emailLabel\",\n \"displayName\": \"Email Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Email\",\n \"groupId\": \"email\"\n },\n {\n \"name\": \"emailPlaceholder\",\n \"displayName\": \"Email Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"ikas@ikas.com\",\n \"groupId\": \"email\"\n },\n {\n \"name\": \"submitButtonText\",\n \"displayName\": \"Submit Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Submit\",\n \"groupId\": \"buttons\"\n },\n {\n \"name\": \"submittingButtonText\",\n \"displayName\": \"Submitting Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Gönderiliyor...\",\n \"groupId\": \"buttons\"\n },\n {\n \"name\": \"backToLoginText\",\n \"displayName\": \"Login Return Bağlantısı\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Login Sayfasına Return\",\n \"groupId\": \"buttons\"\n },\n {\n \"name\": \"successTitle\",\n \"displayName\": \"Success Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Password Unuttum\",\n \"groupId\": \"success\"\n },\n {\n \"name\": \"successMessage\",\n \"displayName\": \"Success Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Size bir e-posta gönderdik.\",\n \"groupId\": \"success\"\n },\n {\n \"name\": \"successButtonText\",\n \"displayName\": \"Success Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Start Shopping\",\n \"groupId\": \"success\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"success\",\n \"name\": \"Success Ekranı\",\n \"description\": \"Email gönderildikten sonra gösterilen başarı mesajları\"\n },\n {\n \"id\": \"content\",\n \"name\": \"Content\",\n \"description\": \"Page başlığı ve açıklama metni\"\n },\n {\n \"id\": \"formFields\",\n \"name\": \"Form Alanları\",\n \"description\": \"Email alanı etiket ve placeholder metinleri\",\n \"children\": [\n {\n \"id\": \"email\",\n \"name\": \"Email\"\n }\n ]\n },\n {\n \"id\": \"buttons\",\n \"name\": \"Buttons\",\n \"description\": \"Button ve link metinleri\"\n }\n ]\n}"
17348
- },
17349
- {
17350
- "filename": "index.tsx",
17351
- "content": "import {\n customerStore,\n getForgotPasswordForm,\n initForgotPasswordForm,\n} from \"@ikas/bp-storefront\";\nimport { useRedirectIfLoggedIn } from \"../../hooks/useRedirectIfLoggedIn\";\n\nimport { Props } from \"./types\";\nimport ForgotPasswordForm from \"./components/ForgotPasswordForm\";\nimport PageLoader from \"../../sub-components/PageLoader\";\n\nexport function ForgotPassword(props: Props) {\n const forgotForm = getForgotPasswordForm(customerStore);\n\n const isChecking = useRedirectIfLoggedIn(() => {\n initForgotPasswordForm(forgotForm);\n });\n\n if (isChecking) return <PageLoader />;\n\n return (\n <section className=\"forgot-password\">\n <div className=\"forgot-password__wrapper kombos-container\">\n <ForgotPasswordForm forgotForm={forgotForm} {...props} />\n </div>\n </section>\n );\n}\n\nexport default ForgotPassword;\n"
17352
- },
17353
- {
17354
- "filename": "styles.css",
17355
- "content": ".forgot-password {\n width: 100%;\n}\n\n.forgot-password__wrapper {\n display: flex;\n justify-content: center;\n align-items: flex-start;\n padding-top: 1rem;\n padding-bottom: 1rem;\n}\n\n.forgot-password__container {\n width: 100%;\n max-width: 29rem;\n display: flex;\n flex-direction: column;\n gap: 2rem;\n}\n\n.forgot-password__header {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n text-align: center;\n}\n\n.forgot-password__title {\n color: var(--kombos-gray-900);\n margin: 0;\n}\n\n.forgot-password__subtitle {\n color: var(--kombos-gray-700);\n margin: 0;\n}\n\n.forgot-password__error-banner {\n padding: 0.75rem 1rem;\n background: rgba(255, 60, 72, 0.08);\n border: 1px solid var(--kombos-error);\n border-radius: 6px;\n color: var(--kombos-error);\n}\n\n.forgot-password__form {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.forgot-password__submit-btn {\n width: 100%;\n}\n\n.forgot-password__footer {\n text-align: center;\n}\n\n.forgot-password__back-link {\n background: none;\n border: none;\n padding: 0;\n color: var(--kombos-gray-700);\n cursor: pointer;\n text-decoration: underline;\n}\n\n.forgot-password__back-link:hover {\n color: var(--kombos-gray-900);\n}\n\n@media (min-width: 768px) {\n .forgot-password__wrapper {\n padding-top: 1.5rem;\n padding-bottom: 1.5rem;\n }\n}\n\n@media (min-width: 1024px) {\n .forgot-password__wrapper {\n padding-top: 2rem;\n padding-bottom: 2rem;\n }\n}\n"
17356
- },
17357
- {
17358
- "filename": "types.ts",
17359
- "content": "export interface Props {\n title?: string;\n subtitle?: string;\n emailLabel?: string;\n emailPlaceholder?: string;\n submitButtonText?: string;\n submittingButtonText?: string;\n backToLoginText?: string;\n successTitle?: string;\n successMessage?: string;\n successButtonText?: string;\n}\n"
17360
- }
17361
- ]
17362
- },
17363
- {
17364
- "id": "header-section",
17365
- "title": "Header Section",
17366
- "description": "Complete header with announcement bar, responsive navigation (mega-menu), logo, customer icon with auth state, mini-cart sidebar with item management, search modal, cookie consent bar, and toast notifications. Uses IkasComponentRenderer for child component slots.",
17367
- "code": "// import { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport { useEffect, useCallback } from \"preact/hooks\";\nimport { Props } from \"./types\";\nimport { ToastContainer } from \"../../sub-components/Toast\";\nimport { useToast } from \"../../hooks/useToast\";\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\n\nexport function Header(props: Props) {\n const {\n backgroundColor = \"var(--kombos-white)\",\n borderColor = \"var(--kombos-gray-100)\",\n components,\n } = props;\n\n const toast = useToast();\n\n const handleShowToast = useCallback(\n (e: Event) => {\n const detail = (e as CustomEvent).detail as {\n message: string;\n variant: \"success\" | \"error\";\n };\n toast.show(detail.message, detail.variant);\n },\n [toast.show],\n );\n\n useEffect(() => {\n window.addEventListener(\"ikas:show-toast\", handleShowToast);\n return () => window.removeEventListener(\"ikas:show-toast\", handleShowToast);\n }, [handleShowToast]);\n\n return (\n <>\n <section\n className=\"kombos-header\"\n style={{\n backgroundColor,\n borderColor,\n }}\n >\n <IkasComponentRenderer\n id=\"header\"\n components={components}\n parentProps={props}\n />\n </section>\n <ToastContainer toasts={toast.toasts} onDismiss={toast.dismiss} />\n </>\n );\n}\n\nexport default Header;\n",
17368
- "relatedFunctions": [
17369
- "cartStore",
17370
- "customerStore",
17371
- "hasCustomer",
17372
- "getIkasOrderTotalItemCount",
17373
- "getDefaultSrc",
17374
- "getFormattedHeightSize",
17375
- "Router",
17376
- "createMediaSrcset",
17377
- "getCustomerConsentGranted",
17378
- "handleCustomerConsentGrant",
17379
- "waitForCustomerStoreInit"
17380
- ],
17381
- "categories": [
17382
- "Navigation",
17383
- "Header",
17384
- "Cart",
17385
- "Customer"
17386
- ],
17387
- "files": [
17388
- {
17389
- "filename": "children/Announcements/ikas-config-snippet.json",
17390
- "content": "{\n \"id\": \"{{PROJECT_ID}}-announcements\",\n \"name\": \"Announcements\",\n \"type\": \"component\",\n \"entry\": \"./src/components/Announcements/index.tsx\",\n \"styles\": \"./src/components/Announcements/styles.css\",\n \"props\": [\n {\n \"name\": \"bgColor\",\n \"displayName\": \"Background Color Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"defaultValue\": \"#14141a\",\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"textColor\",\n \"displayName\": \"Post Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"defaultValue\": \"#ffffff\",\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"items\",\n \"displayName\": \"Announcements\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-announcement\"\n ]\n },\n {\n \"name\": \"autoPlay\",\n \"displayName\": \"Auto Play\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"defaultValue\": true,\n \"groupId\": \"slider\"\n },\n {\n \"name\": \"autoPlayInterval\",\n \"displayName\": \"Play Interval (ms)\",\n \"type\": \"NUMBER\",\n \"required\": false,\n \"defaultValue\": 5000,\n \"groupId\": \"slider\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View\",\n \"description\": \"Background plan ve metin rengi ayarları\"\n },\n {\n \"id\": \"slider\",\n \"name\": \"Slider Settings\",\n \"description\": \"Auto oynatma ve zamanlama ayarları\"\n }\n ]\n}"
17391
- },
17392
- {
17393
- "filename": "children/Announcements/index.tsx",
17394
- "content": "import { useEffect, useCallback, useRef, useState } from \"preact/hooks\";\nimport { Props } from \"./types\";\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\n\nconst SWIPE_THRESHOLD = 50;\n\nexport function Announcements(props: Props) {\n const {\n bgColor,\n textColor,\n items,\n autoPlay = false,\n autoPlayInterval = 5000,\n } = props;\n const [current, setCurrent] = useState(0);\n const dragStartX = useRef(0);\n const isDragging = useRef(false);\n const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);\n\n const count = items?.length ?? 0;\n\n const resetAutoPlay = useCallback(() => {\n if (timerRef.current) clearInterval(timerRef.current);\n if (!autoPlay || count <= 1) return;\n const interval = autoPlayInterval > 0 ? autoPlayInterval : 7000;\n timerRef.current = setInterval(() => {\n setCurrent((prev) => (prev + 1) % count);\n }, interval);\n }, [autoPlay, autoPlayInterval, count]);\n\n useEffect(() => {\n resetAutoPlay();\n return () => {\n if (timerRef.current) clearInterval(timerRef.current);\n };\n }, [resetAutoPlay]);\n\n const handlePointerDown = useCallback((e: PointerEvent) => {\n isDragging.current = true;\n dragStartX.current = e.clientX;\n (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);\n }, []);\n\n const handlePointerUp = useCallback(\n (e: PointerEvent) => {\n if (!isDragging.current) return;\n isDragging.current = false;\n const diff = dragStartX.current - e.clientX;\n if (count <= 1) return;\n\n if (diff > SWIPE_THRESHOLD) {\n setCurrent((prev) => (prev + 1) % count);\n resetAutoPlay();\n } else if (diff < -SWIPE_THRESHOLD) {\n setCurrent((prev) => (prev - 1 + count) % count);\n resetAutoPlay();\n }\n },\n [count, resetAutoPlay],\n );\n\n return (\n <div\n className=\"kombos-announcements\"\n style={{\n backgroundColor: bgColor ?? \"var(--kombos-gray-900)\",\n color: textColor ?? \"var(--kombos-white)\",\n }}\n >\n <div\n className=\"kombos-announcements__slider\"\n onPointerDown={handlePointerDown}\n onPointerUp={handlePointerUp}\n onPointerCancel={handlePointerUp}\n >\n <div\n className=\"kombos-announcements__track\"\n style={{ transform: `translateX(-${current * 100}%)` }}\n >\n <IkasComponentRenderer\n id=\"announcements\"\n components={items}\n parentProps={props}\n />\n </div>\n </div>\n </div>\n );\n}\n\nexport default Announcements;\n"
17395
- },
17396
- {
17397
- "filename": "children/Announcements/styles.css",
17398
- "content": ".kombos-announcements {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0.5rem 1rem;\n overflow: hidden;\n}\n\n.kombos-announcements__slider {\n position: relative;\n width: 100%;\n overflow: hidden;\n cursor: grab;\n user-select: none;\n touch-action: pan-y;\n}\n\n.kombos-announcements__slider:active {\n cursor: grabbing;\n}\n\n.kombos-announcements__track {\n display: flex;\n transition: transform 0.4s ease-in-out;\n}\n\n.kombos-announcements__track > * > * > * {\n flex: 0 0 100%;\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n@media (min-width: 768px) {\n .kombos-announcements {\n padding: 0.75rem 2rem;\n }\n}\n\n@media (min-width: 1024px) {\n .kombos-announcements {\n padding: 0.75rem 4.5rem;\n }\n}\n"
17399
- },
17400
- {
17401
- "filename": "children/Announcements/types.ts",
17402
- "content": "export interface Props {\n bgColor?: string;\n textColor?: string;\n items?: any;\n autoPlay?: boolean;\n autoPlayInterval?: number;\n}\n"
17403
- },
17404
- {
17405
- "filename": "children/CookieBar/ikas-config-snippet.json",
17406
- "content": "{\n \"id\": \"{{PROJECT_ID}}-cookie-bar\",\n \"name\": \"CookieBar\",\n \"type\": \"component\",\n \"entry\": \"./src/components/CookieBar/index.tsx\",\n \"styles\": \"./src/components/CookieBar/styles.css\",\n \"props\": [\n {\n \"name\": \"cookieContent\",\n \"displayName\": \"Cookie Content\",\n \"type\": \"RICH_TEXT\",\n \"required\": true,\n \"defaultValue\": \"<p>Bu siteyi kullanırken çerezler kullanıyoruz. Daha fazla bilgi için <a target=\\\"_blank\\\" rel=\\\"noopener noreferrer nofollow\\\" class=\\\"editor-link\\\" tabindex=\\\"-1\\\" href=\\\"https://www.ikas.com\\\">gizlilik politikamızı</a> okuyabilirsiniz.</p>\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"buttonText\",\n \"displayName\": \"Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Kabul Et\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"position\",\n \"displayName\": \"Position Left\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"defaultValue\": false,\n \"groupId\": \"layout\"\n },\n {\n \"name\": \"showCloseIcon\",\n \"displayName\": \"Kapatma İkonunu Hide\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"defaultValue\": false,\n \"groupId\": \"layout\"\n },\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"textColor\",\n \"displayName\": \"Text Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"buttonTextColor\",\n \"displayName\": \"Button Text Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"appearance\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"content\",\n \"name\": \"Content\",\n \"description\": \"Cookie barı metin ve buton içerikleri\"\n },\n {\n \"id\": \"layout\",\n \"name\": \"Layout\",\n \"description\": \"Cookie barının konumu ve kapatma düğmesi ayarları\"\n },\n {\n \"id\": \"appearance\",\n \"name\": \"View\",\n \"description\": \"Cookie barı renk özelleştirmeleri\"\n }\n ]\n}"
17407
- },
17408
- {
17409
- "filename": "children/CookieBar/index.tsx",
17410
- "content": "import { useState, useEffect } from \"preact/hooks\";\nimport {\n customerStore,\n getCustomerConsentGranted,\n handleCustomerConsentGrant,\n waitForCustomerStoreInit,\n} from \"@ikas/bp-storefront\";\nimport { XSVG } from \"../../sub-components/icons\";\nimport { cx } from \"../../utils/cx\";\nimport { Props } from \"./types\";\n\nexport function CookieBar({\n cookieContent,\n buttonText,\n position = false,\n showCloseIcon = false,\n backgroundColor,\n textColor,\n buttonTextColor,\n}: Props) {\n const [dismissed, setDismissed] = useState(false);\n const [storeReady, setStoreReady] = useState(false);\n\n useEffect(() => {\n waitForCustomerStoreInit(customerStore).then(() => setStoreReady(true));\n }, []);\n\n if (!storeReady) return null;\n\n const hasConsent = getCustomerConsentGranted(customerStore);\n if (hasConsent || dismissed) return null;\n\n const positionClass = position\n ? \"kombos-cookie-bar--left\"\n : \"kombos-cookie-bar--right\";\n\n const cssVars = {\n ...(backgroundColor && { \"--cookie-bg-color\": backgroundColor }),\n ...(textColor && { \"--cookie-text-color\": textColor }),\n ...(buttonTextColor && { \"--cookie-button-text-color\": buttonTextColor }),\n } as Record<string, string>;\n\n return (\n <div className={cx(\"kombos-cookie-bar\", positionClass)} style={cssVars}>\n <div className=\"kombos-cookie-bar__wrapper\">\n <div\n className={cx(\n \"kombos-cookie-bar__top\",\n !showCloseIcon && \"kombos-cookie-bar__top--has-close\",\n )}\n >\n <div\n className=\"kombos-cookie-bar__content text-sm-regular\"\n dangerouslySetInnerHTML={{ __html: cookieContent || \"\" }}\n />\n\n {!showCloseIcon && (\n <button\n className=\"kombos-cookie-bar__close\"\n onClick={() => setDismissed(true)}\n aria-label=\"Close\"\n >\n <XSVG />\n </button>\n )}\n </div>\n <button\n className=\"kombos-cookie-bar__accept text-sm-semibold\"\n onClick={() => handleCustomerConsentGrant(customerStore)}\n >\n {buttonText}\n </button>\n </div>\n </div>\n );\n}\n\nexport default CookieBar;\n"
17411
- },
17412
- {
17413
- "filename": "children/CookieBar/styles.css",
17414
- "content": "/* ===== CookieBar Component ===== */\n\n.kombos-cookie-bar {\n position: fixed;\n bottom: 1.5rem;\n z-index: var(--kombos-z-overlay);\n width: calc(100% - 2rem);\n max-width: 22rem;\n color: var(--cookie-text-color, var(--kombos-white));\n}\n\n/* Position variants */\n.kombos-cookie-bar--right {\n right: 1rem;\n}\n\n.kombos-cookie-bar--left {\n left: 1rem;\n}\n\n/* Wrapper */\n.kombos-cookie-bar__wrapper {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n padding: 1rem;\n background-color: var(--cookie-bg-color, rgba(0, 0, 0, 0.7));\n border-radius: 8px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n word-break: break-word;\n}\n\n/* Top section (content + close) */\n.kombos-cookie-bar__top {\n position: relative;\n}\n\n.kombos-cookie-bar__top--has-close {\n padding-right: 1.5rem;\n}\n\n/* Rich text content */\n.kombos-cookie-bar__content {\n line-height: 1.5;\n}\n\n.kombos-cookie-bar__content a {\n text-decoration: underline;\n text-underline-offset: 2px;\n}\n\n/* Close button */\n.kombos-cookie-bar__close {\n position: absolute;\n top: -0.25rem;\n right: -0.5rem;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0.25rem;\n font-size: 1rem;\n color: var(--cookie-button-text-color, var(--cookie-text-color, var(--kombos-white)));\n background: none;\n border: none;\n cursor: pointer;\n opacity: 0.8;\n transition: opacity 0.15s ease;\n}\n\n.kombos-cookie-bar__close:hover {\n opacity: 1;\n}\n\n/* Accept button */\n.kombos-cookie-bar__accept {\n align-self: flex-end;\n padding: 0;\n color: var(--cookie-button-text-color, var(--cookie-text-color, var(--kombos-white)));\n background: none;\n border: none;\n text-decoration: underline;\n text-underline-offset: 2px;\n cursor: pointer;\n opacity: 0.9;\n transition: opacity 0.15s ease;\n}\n\n.kombos-cookie-bar__accept:hover {\n opacity: 1;\n}\n\n/* ===== Responsive ===== */\n\n@media (min-width: 768px) {\n .kombos-cookie-bar {\n bottom: 2rem;\n max-width: 24rem;\n }\n\n .kombos-cookie-bar--right {\n right: 2rem;\n }\n\n .kombos-cookie-bar--left {\n left: 2rem;\n }\n\n .kombos-cookie-bar__wrapper {\n padding: 1.5rem;\n }\n}\n"
17415
- },
17416
- {
17417
- "filename": "children/CookieBar/types.ts",
17418
- "content": "export interface Props {\n cookieContent: string;\n buttonText?: string;\n position?: boolean;\n showCloseIcon?: boolean;\n backgroundColor?: string;\n textColor?: string;\n buttonTextColor?: string;\n}\n"
17419
- },
17420
- {
17421
- "filename": "children/Navbar/components/CartSidebar/index.tsx",
17422
- "content": "import { useEffect, useState, useCallback } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport { useScrollLock } from \"../../../../hooks/useScrollLock\";\nimport { cx } from \"../../../../utils/cx\";\nimport {\n XSVG,\n Package1SVG,\n Handbag1SVG,\n} from \"../../../../sub-components/icons\";\nimport Button from \"../../../../sub-components/Button\";\nimport {\n cartStore,\n hasCart,\n getIkasOrderTotalItemCount,\n getIkasOrderFormattedTotalFinalPrice,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n removeItem,\n getCheckoutUrlFromCartStore,\n IkasOrderLineItem,\n Router,\n} from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../../../global-types\";\nimport CartItem from \"../../../../sub-components/CartItem\";\n\ninterface Props {\n cartTitle: string;\n emptyCartText: string;\n checkoutButtonText: string;\n viewCartButtonText: string;\n totalText: string;\n freeShippingText: string;\n emptyCartButtonText: string;\n imageAspectRatio?: AspectRatio;\n imageObjectFit?: ObjectFit;\n onClose: () => void;\n}\n\nconst CartSidebar = observer(function CartSidebar({\n cartTitle,\n emptyCartText,\n checkoutButtonText,\n viewCartButtonText,\n totalText,\n freeShippingText,\n emptyCartButtonText,\n imageAspectRatio,\n imageObjectFit,\n onClose,\n}: Props) {\n const [open, setOpen] = useState(false);\n\n useScrollLock();\n\n useEffect(() => {\n requestAnimationFrame(() => setOpen(true));\n }, []);\n\n const handleClose = useCallback(() => {\n setOpen(false);\n setTimeout(onClose, 300);\n }, [onClose]);\n\n const cart = cartStore.cart;\n const isCartLoading = cartStore.isCartLoading;\n const cartHasItems = hasCart(cartStore);\n const itemCount = cart ? getIkasOrderTotalItemCount(cart) : 0;\n const lineItems = cart?.orderLineItems ?? [];\n const adjustments = cart?.orderAdjustments ?? [];\n\n const handleRemove = async (item: IkasOrderLineItem) => {\n await removeItem(item);\n };\n\n return (\n <div\n className={cx(\"kombos-cart-overlay\", open && \"kombos-cart-overlay--open\")}\n >\n <div className=\"kombos-cart-backdrop\" onClick={handleClose} />\n <aside className=\"kombos-cart-sidebar\">\n {/* Header */}\n <div className=\"kombos-cart-sidebar__head\">\n <h3 className=\"kombos-cart-sidebar__title text-md-medium\">\n {cartTitle} ({itemCount})\n </h3>\n <button\n className=\"kombos-cart-sidebar__close\"\n onClick={handleClose}\n aria-label=\"Close\"\n >\n <XSVG />\n </button>\n </div>\n\n {/* Free Shipping Banner */}\n {freeShippingText && (\n <div className=\"kombos-cart-sidebar__banner\">\n <Package1SVG />\n <span className=\"text-xs-semibold\">{freeShippingText}</span>\n </div>\n )}\n\n {/* Loading */}\n {isCartLoading && (\n <div className=\"kombos-cart-sidebar__loading text-sm-regular\" />\n )}\n\n {/* Empty State */}\n {!cartHasItems && !isCartLoading && (\n <div className=\"kombos-cart-sidebar__empty\">\n <Handbag1SVG />\n <p className=\"kombos-cart-sidebar__empty-text text-md-medium\">\n {emptyCartText}\n </p>\n <Button\n variant=\"primary\"\n size=\"s\"\n className=\"kombos-cart-sidebar__empty-btn\"\n onClick={handleClose}\n >\n {emptyCartButtonText}\n </Button>\n </div>\n )}\n\n {/* Cart Items */}\n {cartHasItems && (\n <div className=\"kombos-cart-sidebar__items\">\n {lineItems.map((item) => (\n <CartItem\n key={item.id}\n item={item}\n onRemove={handleRemove}\n aspectRatio={imageAspectRatio}\n objectFit={imageObjectFit}\n />\n ))}\n </div>\n )}\n\n {/* Adjustments */}\n {adjustments.length > 0 && (\n <div className=\"kombos-cart-sidebar__adjustments\">\n {adjustments.map((adj: any, i: number) => (\n <div\n key={i}\n className=\"kombos-cart-sidebar__adj-row text-sm-regular\"\n >\n <span>{getOrderAdjustmentDisplayName(adj)}</span>\n <span className=\"kombos-cart-sidebar__adj-amount text-sm-medium\">\n {getOrderAdjustmentFormattedAmount(adj)}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Footer */}\n {cartHasItems && cart && (\n <div className=\"kombos-cart-sidebar__footer\">\n <div className=\"kombos-cart-sidebar__total text-md-semibold\">\n <span>{totalText}</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(cart)}</span>\n </div>\n <div className=\"kombos-cart-sidebar__footer-buttons\">\n <a\n href=\"/cart\"\n className=\"kombos-cart-sidebar__view-cart-link\"\n onClick={(e: Event) => {\n e.preventDefault();\n handleClose();\n Router.navigateToPage(\"CART\");\n }}\n >\n <Button\n variant=\"secondary\"\n size=\"s\"\n className=\"kombos-cart-sidebar__view-cart-btn\"\n >\n {viewCartButtonText}\n </Button>\n </a>\n <a\n href={getCheckoutUrlFromCartStore(cartStore)}\n className=\"kombos-cart-sidebar__checkout\"\n >\n <Button\n variant=\"primary\"\n size=\"s\"\n className=\"kombos-cart-sidebar__checkout-btn\"\n >\n {checkoutButtonText}\n </Button>\n </a>\n </div>\n </div>\n )}\n </aside>\n </div>\n );\n});\n\nexport default CartSidebar;\n"
17423
- },
17424
- {
17425
- "filename": "children/Navbar/components/CartSidebar/styles.css",
17426
- "content": "/* ===== Cart Sidebar ===== */\n.kombos-cart-overlay {\n position: fixed;\n inset: 0;\n z-index: var(--kombos-z-overlay);\n}\n\n.kombos-cart-backdrop {\n position: absolute;\n inset: 0;\n background: rgba(0, 0, 0, 0);\n transition: background 0.3s ease;\n}\n\n.kombos-cart-overlay--open .kombos-cart-backdrop {\n background: rgba(0, 0, 0, 0.35);\n}\n\n.kombos-cart-sidebar {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n max-width: 30.5rem;\n background: var(--kombos-white);\n display: flex;\n flex-direction: column;\n box-shadow: -4px 0 20px rgba(0, 0, 0, 0.08);\n transform: translateX(100%);\n transition: transform 0.3s ease;\n}\n\n.kombos-cart-overlay--open .kombos-cart-sidebar {\n transform: translateX(0);\n}\n\n/* Header */\n.kombos-cart-sidebar__head {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.25rem 1.5rem;\n border-bottom: 1px solid var(--kombos-gray-200);\n flex-shrink: 0;\n}\n\n.kombos-cart-sidebar__title {\n color: var(--kombos-gray-900);\n margin: 0;\n}\n\n.kombos-cart-sidebar__close {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--kombos-gray-700);\n padding: 0;\n display: flex;\n font-size: 1.25rem;\n}\n\n/* Free Shipping Banner */\n.kombos-cart-sidebar__banner {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 0.5rem;\n margin: 1.5rem 1.5rem 0;\n padding: 0.5rem 1.5rem;\n background: var(--kombos-success-bg);\n border-radius: 6px;\n color: var(--kombos-gray-900);\n flex-shrink: 0;\n}\n\n/* Loading */\n.kombos-cart-sidebar__loading {\n padding: 2rem 1.5rem;\n text-align: center;\n color: var(--kombos-gray-500);\n}\n\n/* Empty State */\n.kombos-cart-sidebar__empty {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1.5rem;\n padding: 3rem 1.5rem;\n color: var(--kombos-gray-500);\n font-size: 3rem;\n}\n\n.kombos-cart-sidebar__empty-text {\n margin: 0;\n color: var(--kombos-gray-900);\n}\n\n.kombos-cart-sidebar__empty-btn {\n min-width: 10rem;\n}\n\n/* Cart Items */\n.kombos-cart-sidebar__items {\n flex: 1;\n overflow-y: auto;\n padding: 0 1.5rem;\n}\n\n/* Adjustments */\n.kombos-cart-sidebar__adjustments {\n padding: 0.75rem 1.5rem;\n border-top: 1px solid var(--kombos-gray-200);\n flex-shrink: 0;\n}\n\n.kombos-cart-sidebar__adj-row {\n display: flex;\n justify-content: space-between;\n color: var(--kombos-gray-900);\n padding: 0.25rem 0;\n}\n\n.kombos-cart-sidebar__adj-amount {\n color: var(--kombos-error);\n}\n\n/* Footer */\n.kombos-cart-sidebar__footer {\n padding: 1.25rem 1.5rem;\n border-top: 1px solid var(--kombos-gray-200);\n flex-shrink: 0;\n}\n\n.kombos-cart-sidebar__total {\n display: flex;\n justify-content: space-between;\n color: var(--kombos-gray-900);\n margin-bottom: 1rem;\n}\n\n.kombos-cart-sidebar__footer-buttons {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.kombos-cart-sidebar__view-cart-link {\n display: block;\n text-decoration: none;\n flex: 1;\n}\n\n.kombos-cart-sidebar__view-cart-btn {\n width: 100%;\n}\n\n.kombos-cart-sidebar__checkout {\n display: block;\n text-decoration: none;\n flex: 1;\n}\n\n.kombos-cart-sidebar__checkout-btn {\n display: flex;\n width: 100%;\n}\n\n@media (min-width: 768px) {\n .kombos-cart-sidebar__footer-buttons {\n flex-direction: row;\n }\n}"
17427
- },
17428
- {
17429
- "filename": "children/Navbar/components/MobileMenu/index.tsx",
17430
- "content": "import { useEffect, useState, useCallback, useRef } from \"preact/hooks\";\nimport { useScrollLock } from \"../../../../hooks/useScrollLock\";\nimport {\n IkasNavigationLink,\n customerStore,\n hasCustomer,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport { cx } from \"../../../../utils/cx\";\nimport { XSVG, ShoppingBag1SVG, CaretRightSVG, CaretLeftSVG } from \"../../../../sub-components/icons\";\nimport Button from \"../../../../sub-components/Button\";\n\ninterface LinkGroup {\n links: IkasNavigationLink[];\n color?: string;\n}\n\ninterface Props {\n linkGroups: LinkGroup[];\n registerButtonText: string;\n loginButtonText: string;\n logoutButtonText: string;\n onClose: () => void;\n onCartOpen: () => void;\n}\n\ninterface DrillDown {\n parentLabel: string;\n subLinks: IkasNavigationLink[];\n}\n\nconst MobileMenu = observer(function MobileMenu({\n linkGroups,\n registerButtonText,\n loginButtonText,\n logoutButtonText,\n onClose,\n onCartOpen,\n}: Props) {\n const [open, setOpen] = useState(false);\n const [drillDown, setDrillDown] = useState<DrillDown | null>(null);\n const isLoggedIn = hasCustomer(customerStore);\n const skipCleanupRef = useRef(false);\n\n useScrollLock(true, skipCleanupRef);\n\n useEffect(() => {\n requestAnimationFrame(() => setOpen(true));\n }, []);\n\n const handleClose = useCallback(() => {\n setOpen(false);\n setTimeout(onClose, 300);\n }, [onClose]);\n\n const handleLinkClick = useCallback(\n (link: IkasNavigationLink) => {\n if (link.href) {\n handleClose();\n }\n },\n [handleClose],\n );\n\n const handleCartClick = useCallback(() => {\n skipCleanupRef.current = true;\n handleClose();\n onCartOpen();\n }, [handleClose, onCartOpen]);\n\n return (\n <div\n className={cx(\"kombos-mobile-overlay\", open && \"kombos-mobile-overlay--open\")}\n >\n <div className=\"kombos-mobile-backdrop\" onClick={handleClose} />\n <div className=\"kombos-mobile-menu\">\n {/* Header */}\n <div className=\"kombos-mobile-menu__head\">\n <button\n className=\"kombos-mobile-menu__icon-btn\"\n onClick={handleClose}\n aria-label=\"Close\"\n >\n <XSVG />\n </button>\n <button\n className=\"kombos-mobile-menu__icon-btn\"\n onClick={handleCartClick}\n aria-label=\"Cart\"\n >\n <ShoppingBag1SVG />\n </button>\n </div>\n\n {/* Content */}\n <div className=\"kombos-mobile-menu__body\">\n {drillDown ? (\n <>\n {/* Back button */}\n <button\n className=\"kombos-mobile-menu__back\"\n onClick={() => setDrillDown(null)}\n >\n <CaretLeftSVG />\n <span className=\"text-xs-medium\">{drillDown.parentLabel}</span>\n </button>\n {/* Sub-links */}\n <nav className=\"kombos-mobile-menu__nav\">\n {drillDown.subLinks.map(\n (sub: IkasNavigationLink, j: number) => (\n <a\n key={j}\n href={sub.href}\n className=\"kombos-mobile-menu__link text-sm-medium\"\n onClick={() => handleLinkClick(sub)}\n >\n {sub.label}\n </a>\n ),\n )}\n </nav>\n </>\n ) : (\n <nav className=\"kombos-mobile-menu__nav\">\n {linkGroups.map((group, gi) =>\n group.links.map((link: IkasNavigationLink, i: number) => {\n const hasSubLinks = link.subLinks && link.subLinks.length > 0;\n return (\n <div\n key={`${gi}-${i}`}\n className=\"kombos-mobile-menu__link-row\"\n >\n <a\n href={link.href}\n className=\"kombos-mobile-menu__link text-sm-medium\"\n style={group.color ? { color: group.color } : undefined}\n onClick={() => handleLinkClick(link)}\n >\n {link.label}\n </a>\n {hasSubLinks && (\n <button\n className=\"kombos-mobile-menu__drill-btn\"\n onClick={() =>\n setDrillDown({\n parentLabel: link.label,\n subLinks: link.subLinks!,\n })\n }\n aria-label={`${link.label} alt kategorileri`}\n >\n <CaretRightSVG />\n </button>\n )}\n </div>\n );\n }),\n )}\n </nav>\n )}\n </div>\n\n {/* Footer */}\n <div className=\"kombos-mobile-menu__footer\">\n {isLoggedIn ? (\n <Button\n variant=\"secondary\"\n className=\"kombos-mobile-menu__footer-btn\"\n onClick={() => Router.navigateToPage(\"ACCOUNT\")}\n >\n {logoutButtonText}\n </Button>\n ) : (\n <>\n <Button\n variant=\"secondary\"\n className=\"kombos-mobile-menu__footer-btn\"\n onClick={() => Router.navigateToPage(\"REGISTER\")}\n >\n {registerButtonText}\n </Button>\n <Button\n variant=\"primary\"\n className=\"kombos-mobile-menu__footer-btn\"\n onClick={() => Router.navigateToPage(\"LOGIN\")}\n >\n {loginButtonText}\n </Button>\n </>\n )}\n </div>\n </div>\n </div>\n );\n});\n\nexport default MobileMenu;\n"
17431
- },
17432
- {
17433
- "filename": "children/Navbar/components/MobileMenu/styles.css",
17434
- "content": "/* ===== Mobile Menu ===== */\n.kombos-mobile-overlay {\n position: fixed;\n inset: 0;\n z-index: var(--kombos-z-overlay);\n}\n\n.kombos-mobile-backdrop {\n position: absolute;\n inset: 0;\n background: rgba(0, 0, 0, 0);\n transition: background 0.3s ease;\n}\n\n.kombos-mobile-overlay--open .kombos-mobile-backdrop {\n background: rgba(0, 0, 0, 0.35);\n}\n\n.kombos-mobile-menu {\n position: absolute;\n inset: 0;\n width: 100%;\n max-width: 26rem;\n background: var(--kombos-white);\n display: flex;\n flex-direction: column;\n transform: translateX(-100%);\n transition: transform 0.3s ease;\n}\n\n.kombos-mobile-overlay--open .kombos-mobile-menu {\n transform: translateX(0);\n}\n\n/* Header */\n.kombos-mobile-menu__head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 1rem 0.75rem;\n height: 3.5rem;\n border-bottom: 1px solid var(--kombos-gray-100);\n flex-shrink: 0;\n}\n\n.kombos-mobile-menu__icon-btn {\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 1.5rem;\n height: 1.5rem;\n color: var(--kombos-gray-900);\n}\n\n/* Body */\n.kombos-mobile-menu__body {\n flex: 1;\n overflow-y: auto;\n}\n\n/* Back button */\n.kombos-mobile-menu__back {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n padding: 1rem 0.75rem;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--kombos-gray-900);\n width: 100%;\n}\n\n/* Nav */\n.kombos-mobile-menu__nav {\n padding: 0 0.75rem;\n}\n\n.kombos-mobile-menu__link-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n border-bottom: 1px solid var(--kombos-gray-100);\n}\n\n.kombos-mobile-menu__link {\n display: flex;\n align-items: center;\n flex: 1;\n color: var(--kombos-gray-900);\n text-decoration: none;\n padding: 1rem 0;\n}\n\n.kombos-mobile-menu__drill-btn {\n background: none;\n border: none;\n cursor: pointer;\n padding: 1rem 0 1rem 0.75rem;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--kombos-gray-900);\n}\n\n/* Footer */\n.kombos-mobile-menu__footer {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 1rem;\n padding: 1rem 1.5rem;\n border-top: 1px solid var(--kombos-gray-100);\n}\n\n.kombos-mobile-menu__footer-btn {\n flex: 1;\n}\n"
17435
- },
17436
- {
17437
- "filename": "children/Navbar/components/NavItem/index.tsx",
17438
- "content": "import { IkasNavigationLink } from \"@ikas/bp-storefront\";\nimport { CaretDownSVG, CaretRightSVG } from \"../../../../sub-components/icons\";\n\ninterface Props {\n link: IkasNavigationLink;\n linkColor?: string;\n}\n\nexport default function NavItem({ link, linkColor }: Props) {\n const hasSubLinks = link.subLinks && link.subLinks.length > 0;\n\n return (\n <div className=\"kombos-header__nav-item\">\n <a\n href={link.href}\n className=\"kombos-header__nav-link text-md-medium\"\n style={{ color: linkColor ?? \"var(--kombos-gray-700)\" }}\n target={link.openInNewTab ? \"_blank\" : undefined}\n >\n {link.label}\n {hasSubLinks && <CaretDownSVG className=\"kombos-header__chevron\" />}\n </a>\n {hasSubLinks && (\n <div className=\"kombos-header__dropdown\">\n {link.subLinks!.map((sub: IkasNavigationLink, j: number) => {\n const hasNestedLinks = sub.subLinks && sub.subLinks.length > 0;\n return (\n <div key={j} className=\"kombos-header__dropdown-item\">\n <a\n href={sub.href}\n className=\"kombos-header__dropdown-link text-md-medium\"\n target={sub.openInNewTab ? \"_blank\" : undefined}\n >\n <span>{sub.label}</span>\n {hasNestedLinks && (\n <CaretRightSVG className=\"kombos-header__caret-right\" />\n )}\n </a>\n {hasNestedLinks && (\n <div className=\"kombos-header__dropdown kombos-header__dropdown--nested\">\n {sub.subLinks!.map(\n (child: IkasNavigationLink, k: number) => (\n <div key={k} className=\"kombos-header__dropdown-item\">\n <a\n href={child.href}\n className=\"kombos-header__dropdown-link text-md-medium\"\n target={\n child.openInNewTab ? \"_blank\" : undefined\n }\n >\n <span>{child.label}</span>\n </a>\n </div>\n ),\n )}\n </div>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n}\n"
17439
- },
17440
- {
17441
- "filename": "children/Navbar/components/SearchModal/index.tsx",
17442
- "content": "import { useState, useEffect, useRef, useCallback } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport { useScrollLock } from \"../../../../hooks/useScrollLock\";\nimport {\n IkasProductList,\n IkasImage,\n searchProductList,\n getDefaultSrc,\n isEmpty,\n Router,\n createMediaSrcset,\n getProductOptionSet,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport { cx } from \"../../../../utils/cx\";\nimport { MagnifyingGlass1SVG, XSVG } from \"../../../../sub-components/icons\";\nimport SpinnerIcon from \"../../../../sub-components/SpinnerIcon\";\nimport ProductCard from \"../../../../sub-components/ProductCard\";\nimport Button from \"../../../../sub-components/Button\";\nimport Input from \"../../../../sub-components/Input\";\nimport type { AspectRatio, ObjectFit } from \"../../../../global-types\";\n\ninterface Props {\n productList: IkasProductList;\n logo?: IkasImage;\n\n logoSizeDesktop: string;\n logoSizeMobile: string;\n searchPlaceholder: string;\n searchingText: string;\n noResultsText: string;\n resultCountText: string;\n addToCartText: string;\n addedToCartText: string;\n outOfStockText: string;\n goToProductText: string;\n viewAllText: string;\n hideAddToCartButton?: boolean;\n components?: any;\n parentProps?: Record<string, any>;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n onClose: () => void;\n}\n\nconst SearchModal = observer(function SearchModal({\n productList,\n logo,\n logoSizeDesktop,\n logoSizeMobile,\n searchPlaceholder,\n searchingText,\n noResultsText,\n resultCountText,\n addToCartText,\n addedToCartText,\n outOfStockText,\n goToProductText,\n viewAllText,\n hideAddToCartButton,\n aspectRatio,\n objectFit,\n onClose,\n components,\n parentProps,\n}: Props) {\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [pendingSearch, setPendingSearch] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const products = productList.data ?? [];\n\n useEffect(() => {\n products.forEach((p) => {\n if (!p.productOptionSet) getProductOptionSet(p);\n });\n }, [products.length]);\n\n const isLoading = productList.isLoading;\n const totalCount = productList.count ?? 0;\n\n useEffect(() => {\n if (!isLoading) setPendingSearch(false);\n }, [isLoading]);\n\n useScrollLock();\n\n useEffect(() => {\n requestAnimationFrame(() => setOpen(true));\n setTimeout(() => inputRef.current?.focus(), 100);\n }, []);\n\n const handleClose = useCallback(() => {\n setOpen(false);\n setTimeout(onClose, 300);\n }, [onClose]);\n\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") handleClose();\n };\n document.addEventListener(\"keydown\", handleKeyDown);\n return () => document.removeEventListener(\"keydown\", handleKeyDown);\n }, [handleClose]);\n\n const handleInput = useCallback(\n (e: Event) => {\n const value = (e.target as HTMLInputElement).value;\n setQuery(value);\n setPendingSearch(true);\n searchProductList(productList, value);\n },\n [productList],\n );\n\n const showResults = !isLoading && !isEmpty(products);\n const showNoResults =\n !isLoading && isEmpty(products) && query.trim().length > 0;\n\n return (\n <div\n className={cx(\n \"kombos-search-overlay\",\n open && \"kombos-search-overlay--open\",\n )}\n onClick={handleClose}\n >\n {/* Panel */}\n <div\n className=\"kombos-search__panel\"\n onClick={(e) => e.stopPropagation()}\n >\n {/* Header */}\n <div className=\"kombos-search__header\">\n {/* Logo */}\n <div className=\"kombos-search__logo-area\">\n {logo && (\n <a\n href=\"/\"\n className=\"kombos-search__logo\"\n style={{\n \"--search-logo-h-desktop\": logoSizeDesktop,\n \"--search-logo-h-mobile\": logoSizeMobile,\n }}\n >\n <img\n src={getDefaultSrc(logo)}\n srcSet={createMediaSrcset(logo)}\n sizes=\"120px\"\n alt={logo?.altText || \"Logo\"}\n className=\"kombos-search__logo-img\"\n />\n </a>\n )}\n </div>\n\n {/* Search Input */}\n <Input\n className=\"kombos-search__input-wrap\"\n leftIcon={<MagnifyingGlass1SVG />}\n inputRef={inputRef}\n placeholder={searchPlaceholder}\n value={query}\n onInput={handleInput}\n />\n\n {/* Close */}\n <div className=\"kombos-search__close-area\">\n <button\n className=\"kombos-search__close\"\n onClick={handleClose}\n aria-label=\"Close\"\n >\n <XSVG />\n </button>\n </div>\n </div>\n\n {/* Body */}\n <div className=\"kombos-search__body\">\n {/* Searching */}\n {isLoading && (\n <div className=\"kombos-search__status\">\n <SpinnerIcon className=\"kombos-search__spinner\" />\n <span className=\"text-xl-medium md:display-xs-medium\">\n {searchingText}\n </span>\n </div>\n )}\n\n {/* No Results */}\n {showNoResults && (\n <div className=\"kombos-search__status\">\n <span className=\"text-xl-medium md:display-xs-medium\">\n {noResultsText}\n </span>\n </div>\n )}\n\n {/* Result Count */}\n {!isLoading &&\n !pendingSearch &&\n query.trim().length > 0 &&\n !isEmpty(products) && (\n <div className=\"kombos-search__result-count\">\n <span className=\"text-xl-medium md:display-xs-medium\">\n {totalCount} {resultCountText}\n </span>\n </div>\n )}\n\n {/* Results */}\n {showResults && (\n <>\n <div className=\"kombos-search__grid\">\n {products.map((product) => (\n <div key={product.id} className=\"kombos-search__card\">\n <ProductCard\n product={product}\n addToCartText={addToCartText}\n addedToCartText={addedToCartText}\n outOfStockText={outOfStockText}\n goToProductText={goToProductText}\n hideAddToCartButton={hideAddToCartButton}\n openCartOnAdd={false}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n sizes=\"(max-width: 767px) calc((100vw - 44px) / 2), (max-width: 1023px) calc((100vw - 48px) / 2), calc((100vw - 240px) / 5)\"\n />\n <IkasComponentRenderer\n id={`search-modal-product-${product.id}`}\n components={components}\n parentProps={parentProps}\n map={{\n product,\n }}\n className=\"kombos-search__card-content\"\n />\n </div>\n ))}\n </div>\n {query.trim().length > 0 && (\n <div className=\"kombos-search__view-all\">\n <Button\n variant=\"primary\"\n className=\"kombos-search__view-all-btn\"\n onClick={() => {\n Router.navigateToPage(\"SEARCH\", undefined, {\n q: query.trim(),\n });\n handleClose();\n }}\n >\n {viewAllText}\n </Button>\n </div>\n )}\n </>\n )}\n </div>\n </div>\n </div>\n );\n});\n\nexport default SearchModal;\n"
17443
- },
17444
- {
17445
- "filename": "children/Navbar/components/SearchModal/styles.css",
17446
- "content": "\n/* ===== Search Modal ===== */\n.kombos-search-overlay {\n position: fixed;\n inset: 0;\n z-index: var(--kombos-z-overlay);\n background: rgba(0, 0, 0, 0.35);\n opacity: 0;\n transition: opacity 0.3s ease;\n}\n\n.kombos-search-overlay--open {\n opacity: 1;\n}\n\n.kombos-search__panel {\n display: flex;\n flex-direction: column;\n max-height: 100%;\n background: var(--kombos-white);\n}\n\n/* Header */\n.kombos-search__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 1rem;\n border-bottom: 1px solid var(--kombos-gray-200);\n flex-shrink: 0;\n}\n\n.kombos-search__logo-area {\n flex-shrink: 0;\n width: 7.5rem;\n display: none;\n}\n\n.kombos-search__logo {\n display: flex;\n align-items: center;\n text-decoration: none;\n height: var(--search-logo-h-mobile, 3rem);\n overflow: hidden;\n}\n\n.kombos-search__logo-img {\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n\n.kombos-search__input-wrap {\n flex: 1;\n max-width: 59.5rem;\n}\n\n.kombos-search__close-area {\n flex-shrink: 0;\n width: auto;\n display: flex;\n justify-content: flex-end;\n}\n\n.kombos-search__close {\n background: var(--kombos-gray-50);\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n cursor: pointer;\n padding: 0.6875rem;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--kombos-gray-900);\n font-size: 1.25rem;\n transition: background-color 0.15s ease;\n}\n\n.kombos-search__close:hover {\n background: var(--kombos-gray-100);\n}\n\n/* Body */\n.kombos-search__body {\n overflow-y: auto;\n padding: 1.5rem 1rem 0;\n}\n\n/* Status (searching / no results) */\n.kombos-search__status {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: center;\n gap: 0.75rem;\n padding: 0 0 3rem;\n color: var(--kombos-gray-900);\n}\n\n.kombos-search__spinner {\n font-size: 2rem;\n}\n\n/* Result Count */\n.kombos-search__result-count {\n text-align: center;\n padding: 0 0 1.5rem;\n color: var(--kombos-gray-900);\n}\n\n/* Product Grid */\n.kombos-search__grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 0.75rem;\n padding-bottom: 3rem;\n}\n\n/* View All */\n.kombos-search__view-all {\n display: flex;\n justify-content: center;\n padding-bottom: 3rem;\n}\n\n.kombos-search__view-all-btn {\n min-width: 12.5rem;\n}\n\n.kombos-search__card {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n min-width: 0;\n}\n\n.kombos-search__card-content {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n/* ===== Responsive — Tablet ===== */\n@media (min-width: 768px) {\n .kombos-search__grid {\n gap: 1rem;\n }\n}\n\n/* ===== Responsive — Desktop ===== */\n@media (min-width: 1024px) {\n .kombos-search__header {\n padding: 1.25rem 3.5rem;\n gap: 1.5rem;\n }\n\n .kombos-search__logo-area {\n display: block;\n }\n\n .kombos-search__close-area {\n width: 7.5rem;\n }\n\n .kombos-search__body {\n padding: 3rem 4.5rem 0;\n }\n\n .kombos-search__result-count {\n padding-bottom: 3rem;\n }\n\n .kombos-search__grid {\n grid-template-columns: repeat(5, 1fr);\n gap: 1.5rem;\n }\n\n .kombos-search__logo {\n height: var(--search-logo-h-desktop, 3.75rem);\n }\n}\n"
17447
- },
17448
- {
17449
- "filename": "children/Navbar/ikas-config-snippet.json",
17450
- "content": "{\n \"id\": \"{{PROJECT_ID}}-navbar\",\n \"name\": \"Navbar\",\n \"type\": \"component\",\n \"entry\": \"./src/components/Navbar/index.tsx\",\n \"styles\": \"./src/components/Navbar/styles.css\",\n \"props\": [\n {\n \"name\": \"logo\",\n \"displayName\": \"Logo\",\n \"type\": \"IMAGE\",\n \"required\": false,\n \"groupId\": \"logo-settings\"\n },\n {\n \"name\": \"logoSizeDesktop\",\n \"displayName\": \"Logo Size Desktop\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"logo-settings\",\n \"typeId\": \"@ikas/bp-storefront-models-HeightStyleType\"\n },\n {\n \"name\": \"logoSizeMobile\",\n \"displayName\": \"Logo Size Mobile\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"logo-settings\",\n \"typeId\": \"@ikas/bp-storefront-models-HeightStyleType\"\n },\n {\n \"name\": \"navigationLinks\",\n \"displayName\": \"Navigation Links\",\n \"type\": \"LIST_OF_LINK\",\n \"required\": false,\n \"groupId\": \"navigation\"\n },\n {\n \"name\": \"navigationLinkColor\",\n \"displayName\": \"Link Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"navigation\"\n },\n {\n \"name\": \"coloredLinks\",\n \"displayName\": \"Links\",\n \"type\": \"LIST_OF_LINK\",\n \"required\": false,\n \"groupId\": \"colored-links\"\n },\n {\n \"name\": \"coloredLinkColor\",\n \"displayName\": \"Link Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"colored-links\"\n },\n {\n \"name\": \"cartTitle\",\n \"displayName\": \"Cart Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"My Shopping Cart\",\n \"groupId\": \"cart-sidebar\"\n },\n {\n \"name\": \"emptyCartText\",\n \"displayName\": \"Empty Cart Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Your cart is empty\",\n \"groupId\": \"cart-sidebar\"\n },\n {\n \"name\": \"checkoutButtonText\",\n \"displayName\": \"Payment Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Proceed to Checkout\",\n \"groupId\": \"cart-sidebar\"\n },\n {\n \"name\": \"totalText\",\n \"displayName\": \"Total Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Total\",\n \"groupId\": \"cart-sidebar\"\n },\n {\n \"name\": \"freeShippingText\",\n \"displayName\": \"Free Shipping Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Free shipping on orders over $150\",\n \"groupId\": \"cart-sidebar\"\n },\n {\n \"name\": \"emptyCartButtonText\",\n \"displayName\": \"Empty Cart Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Start Shopping\",\n \"groupId\": \"cart-sidebar\"\n },\n {\n \"name\": \"registerButtonText\",\n \"displayName\": \"Üye Ol Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sign Up\",\n \"groupId\": \"mobile-menu\"\n },\n {\n \"name\": \"loginButtonText\",\n \"displayName\": \"Login Yap Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sign In\",\n \"groupId\": \"mobile-menu\"\n },\n {\n \"name\": \"logoutButtonText\",\n \"displayName\": \"Çıkış Yap Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sign Out\",\n \"groupId\": \"mobile-menu\"\n },\n {\n \"name\": \"searchProductList\",\n \"displayName\": \"Search Product Listesi\",\n \"type\": \"PRODUCT_LIST\",\n \"required\": false,\n \"groupId\": \"search\"\n },\n {\n \"name\": \"hideAddToCartButton\",\n \"displayName\": \"Cart Add Butonunu Hide\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"search-card-settings\"\n },\n {\n \"name\": \"searchPlaceholder\",\n \"displayName\": \"Search Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"What are you looking for?\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"searchingText\",\n \"displayName\": \"Aranıyor Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Aranıyor...\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"noResultsText\",\n \"displayName\": \"Result Bulunamadı Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"No products found\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"resultCountText\",\n \"displayName\": \"Result Count Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Result\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"addToCartText\",\n \"displayName\": \"Cart Add Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Add to Cart\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"addedToCartText\",\n \"displayName\": \"Cart Added Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Added to Cart\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"outOfStockText\",\n \"displayName\": \"Sold Out Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sold Out\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"goToProductText\",\n \"displayName\": \"Product Git Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Go to Product\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"viewAllText\",\n \"displayName\": \"Tümünü Gör Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"View All\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Components\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"privateVarMap\": {\n \"product\": {\n \"id\": \"pvm_1772803767210_2\",\n \"typeId\": \"@ikas/bp-storefront-models-IkasProduct\"\n }\n },\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-card-product-price\",\n \"{{PROJECT_ID}}-card-product-variants\",\n \"{{PROJECT_ID}}-card-product-name\"\n ]\n },\n {\n \"name\": \"imageAspectRatio\",\n \"displayName\": \"Image Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"search-image\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"imageObjectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"search-image\",\n \"enumTypeId\": \"GrylMqHxui\"\n },\n {\n \"name\": \"viewCartButtonText\",\n \"displayName\": \"Sepeti Görüntüle Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"View Cart\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"logo-settings\",\n \"name\": \"Logo\",\n \"description\": \"Image ve boyut ayarları\"\n },\n {\n \"id\": \"navigation\",\n \"name\": \"Navigation\",\n \"description\": \"Links ve renkler\"\n },\n {\n \"id\": \"colored-links\",\n \"name\": \"Colored Links\",\n \"description\": \"Öne çıkan bağlantılar\"\n },\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Cart ve mobil menü metinleri\",\n \"children\": [\n {\n \"id\": \"cart-sidebar\",\n \"name\": \"Cart\",\n \"description\": \"Cart kenar çubuğu metinleri\"\n },\n {\n \"id\": \"mobile-menu\",\n \"name\": \"Mobile Menü\",\n \"description\": \"Giriş, kayıt ve çıkış butonları\"\n }\n ]\n },\n {\n \"id\": \"search\",\n \"name\": \"Search\",\n \"description\": \"Search modalı ayarları\",\n \"children\": [\n {\n \"id\": \"search-card-settings\",\n \"name\": \"Product Kartı Settings\",\n \"description\": \"Search sonuçlarındaki ürün kartı görünüm ayarları\"\n },\n {\n \"id\": \"search-texts\",\n \"name\": \"Texts\",\n \"description\": \"Search modalı metin ayarları\"\n }\n ]\n },\n {\n \"id\": \"search-image\",\n \"name\": \"Product Image Settings\",\n \"description\": \"Product görsellerinin oran ve sığdırma ayarları\"\n }\n ]\n}"
17451
- },
17452
- {
17453
- "filename": "children/Navbar/index.tsx",
17454
- "content": "import { useState, useEffect } from \"preact/hooks\";\nimport {\n cartStore,\n customerStore,\n hasCustomer,\n getIkasOrderTotalItemCount,\n getDefaultSrc,\n getFormattedHeightSize,\n Router,\n createMediaSrcset,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport CartSidebar from \"./components/CartSidebar\";\nimport MobileMenu from \"./components/MobileMenu\";\nimport SearchModal from \"./components/SearchModal\";\nimport NavItem from \"./components/NavItem\";\nimport {\n List1SVG,\n MagnifyingGlass1SVG,\n User1SVG,\n ShoppingBag1SVG,\n} from \"../../sub-components/icons\";\nexport function Navbar(props: Props) {\n const {\n logo,\n navigationLinks,\n logoSizeDesktop: rawLogoSizeDesktop,\n logoSizeMobile: rawLogoSizeMobile,\n cartTitle = \"Alışveriş Sepetim\",\n emptyCartText = \"Sepetinizde Ürün Bulunmamaktadır\",\n checkoutButtonText = \"Ödemeye Geç\",\n totalText = \"Toplam\",\n navigationLinkColor,\n coloredLinks,\n coloredLinkColor,\n registerButtonText = \"Üye Ol\",\n loginButtonText = \"Giriş Yap\",\n logoutButtonText = \"Çıkış Yap\",\n freeShippingText = \"150 TL ve üzeri siparişlerde kargo bedava\",\n emptyCartButtonText = \"Alışverişe Başla\",\n searchPlaceholder = \"Ne Aramıştınız?\",\n searchingText = \"Aranıyor...\",\n noResultsText = \"Aradığınız ürün bulunamadı\",\n resultCountText = \"Sonuç\",\n addToCartText = \"Sepete Ekle\",\n addedToCartText = \"Sepete Eklendi\",\n outOfStockText = \"Tükendi\",\n goToProductText = \"Ürüne Git\",\n viewAllText = \"Tümünü Gör\",\n viewCartButtonText = \"Sepeti Görüntüle\",\n searchProductList,\n hideAddToCartButton,\n imageAspectRatio,\n imageObjectFit,\n components,\n } = props;\n\n const logoSizeDesktop = getFormattedHeightSize(rawLogoSizeDesktop) || \"60px\";\n const logoSizeMobile = getFormattedHeightSize(rawLogoSizeMobile) || \"48px\";\n\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [cartOpen, setCartOpen] = useState(false);\n const [searchOpen, setSearchOpen] = useState(false);\n\n useEffect(() => {\n const openCart = () => setCartOpen(true);\n window.addEventListener(\"ikas:open-cart-sidebar\", openCart);\n return () => window.removeEventListener(\"ikas:open-cart-sidebar\", openCart);\n }, []);\n\n const cart = cartStore.cart;\n const itemCount = cart ? getIkasOrderTotalItemCount(cart) : 0;\n const isLoggedIn = hasCustomer(customerStore);\n\n const links = navigationLinks?.links ?? [];\n const coloredLinksList = coloredLinks?.links ?? [];\n\n return (\n <div\n className=\"kombos-navbar\"\n style={{\n \"--logo-h-desktop\": logoSizeDesktop,\n \"--logo-h-mobile\": logoSizeMobile,\n }}\n >\n <div className=\"kombos-navbar__inner kombos-container\">\n {/* Hamburger - mobile only */}\n <button\n className=\"kombos-navbar__hamburger\"\n onClick={() => setMobileMenuOpen(true)}\n aria-label=\"Menu\"\n >\n <List1SVG />\n </button>\n\n {/* Logo */}\n {logo && (\n <a\n className=\"kombos-navbar__logo\"\n href=\"/\"\n onClick={(e) => {\n e.preventDefault();\n Router.navigate(\"/\");\n }}\n style={{\n \"--logo-h-desktop\": logoSizeDesktop,\n \"--logo-h-mobile\": logoSizeMobile,\n }}\n >\n <img\n src={getDefaultSrc(logo)}\n srcSet={createMediaSrcset(logo)}\n sizes=\"200px\"\n alt={logo?.altText || \"Logo\"}\n className=\"kombos-navbar__logo-img\"\n width={200}\n height={60}\n fetchpriority=\"high\"\n loading=\"eager\"\n />\n </a>\n )}\n\n {/* Desktop Navigation */}\n <nav\n className={`kombos-navbar__nav${!logo ? \" kombos-navbar__nav--no-logo\" : \"\"}`}\n >\n {links.map((link, i) => (\n <NavItem key={i} link={link} linkColor={navigationLinkColor} />\n ))}\n {coloredLinksList.map((link, i) => (\n <NavItem key={`cl-${i}`} link={link} linkColor={coloredLinkColor} />\n ))}\n </nav>\n\n {/* Action Icons */}\n <div className=\"kombos-navbar__actions\">\n {/* Search */}\n <a\n className=\"kombos-navbar__icon-btn\"\n href=\"/search\"\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n if (searchProductList) {\n setSearchOpen(true);\n } else {\n Router.navigateToPage(\"SEARCH\");\n }\n }}\n aria-label=\"Search\"\n >\n <MagnifyingGlass1SVG />\n </a>\n\n {/* Account */}\n <a\n className=\"kombos-navbar__icon-btn\"\n href={isLoggedIn ? \"/account\" : \"/login\"}\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n Router.navigateToPage(isLoggedIn ? \"ACCOUNT\" : \"LOGIN\");\n }}\n aria-label=\"Account\"\n >\n <User1SVG />\n </a>\n\n {/* Cart */}\n <a\n className=\"kombos-navbar__icon-btn kombos-navbar__cart-trigger\"\n href=\"/cart\"\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n setCartOpen(true);\n }}\n aria-label=\"Cart\"\n >\n <ShoppingBag1SVG />\n {itemCount > 0 && (\n <span className=\"kombos-navbar__badge\">{itemCount}</span>\n )}\n </a>\n </div>\n </div>\n\n {/* Cart Sidebar */}\n {cartOpen && (\n <CartSidebar\n cartTitle={cartTitle}\n emptyCartText={emptyCartText}\n checkoutButtonText={checkoutButtonText}\n viewCartButtonText={viewCartButtonText}\n totalText={totalText}\n freeShippingText={freeShippingText}\n emptyCartButtonText={emptyCartButtonText}\n imageAspectRatio={imageAspectRatio}\n imageObjectFit={imageObjectFit}\n onClose={() => setCartOpen(false)}\n />\n )}\n\n {/* Search Modal */}\n {searchOpen && searchProductList && (\n <SearchModal\n productList={searchProductList}\n logo={logo ?? undefined}\n logoSizeDesktop={logoSizeDesktop}\n logoSizeMobile={logoSizeMobile}\n searchPlaceholder={searchPlaceholder}\n searchingText={searchingText}\n noResultsText={noResultsText}\n resultCountText={resultCountText}\n addToCartText={addToCartText}\n addedToCartText={addedToCartText}\n outOfStockText={outOfStockText}\n goToProductText={goToProductText}\n viewAllText={viewAllText}\n hideAddToCartButton={hideAddToCartButton}\n aspectRatio={imageAspectRatio}\n objectFit={imageObjectFit}\n onClose={() => setSearchOpen(false)}\n components={components}\n parentProps={props}\n />\n )}\n\n {/* Mobile Menu */}\n {mobileMenuOpen && (\n <MobileMenu\n linkGroups={[\n { links, color: navigationLinkColor },\n ...(coloredLinksList.length > 0\n ? [{ links: coloredLinksList, color: coloredLinkColor }]\n : []),\n ]}\n registerButtonText={registerButtonText}\n loginButtonText={loginButtonText}\n logoutButtonText={logoutButtonText}\n onClose={() => setMobileMenuOpen(false)}\n onCartOpen={() => setCartOpen(true)}\n />\n )}\n </div>\n );\n}\n\nexport default Navbar;\n"
17455
- },
17456
- {
17457
- "filename": "children/Navbar/styles.css",
17458
- "content": "/* ===== Navbar Component ===== */\n.kombos-navbar__inner {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding-block: 0.75rem;\n position: relative;\n justify-content: space-between;\n min-height: calc(var(--logo-h-mobile, 3rem) + 1.5rem);\n}\n\n/* Logo */\n.kombos-navbar__logo {\n flex-shrink: 0;\n text-decoration: none;\n display: flex;\n align-items: center;\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n height: var(--logo-h-mobile, 3rem);\n overflow: hidden;\n}\n\n.kombos-navbar__logo-img {\n width: 100%;\n height: 100%;\n object-fit: contain;\n padding: 0.5rem 0;\n}\n\n/* Navigation */\n.kombos-navbar__nav {\n flex: 1;\n display: none;\n align-items: center;\n justify-content: center;\n gap: 1.5rem;\n}\n\n/* NavItem sub-component uses kombos-header__* classes — keep those styles here */\n.kombos-header__nav-item {\n position: relative;\n}\n\n.kombos-header__nav-link {\n color: var(--kombos-gray-700);\n text-decoration: none;\n display: flex;\n align-items: center;\n gap: 0.25rem;\n white-space: nowrap;\n transition: opacity 0.2s ease;\n}\n\n.kombos-header__nav-link:hover {\n opacity: 0.7;\n}\n\n.kombos-header__chevron {\n flex-shrink: 0;\n transition: transform 0.2s ease;\n}\n\n.kombos-header__nav-item:hover .kombos-header__chevron {\n transform: rotate(180deg);\n}\n\n/* Dropdown */\n.kombos-header__dropdown {\n display: none;\n position: absolute;\n top: calc(100% + 1.125rem);\n left: 50%;\n transform: translateX(-50%);\n background: var(--kombos-white);\n border: 1px solid var(--kombos-gray-100);\n border-radius: 6px;\n padding: 0.375rem 0;\n width: 13rem;\n box-shadow: 0 2px 12px 0.4px rgba(0, 0, 0, 0.06);\n z-index: var(--kombos-z-dropdown);\n flex-direction: column;\n}\n\n/* Invisible bridge: covers the 18px gap between nav link and dropdown */\n.kombos-header__dropdown::before {\n content: \"\";\n position: absolute;\n bottom: 100%;\n left: -0.75rem;\n right: -0.75rem;\n height: 1.125rem;\n}\n\n.kombos-header__nav-item:hover > .kombos-header__dropdown {\n display: flex;\n}\n\n/* Nested dropdown (2nd level) */\n.kombos-header__dropdown--nested {\n top: -0.375rem;\n left: calc(100% + 0.75rem);\n transform: none;\n}\n\n/* Bridge: covers the 12px gap between 1st and 2nd level dropdowns */\n.kombos-header__dropdown--nested::before {\n content: \"\";\n position: absolute;\n top: -0.75rem;\n bottom: -0.75rem;\n right: 100%;\n left: auto;\n width: 0.75rem;\n height: auto;\n}\n\n.kombos-header__dropdown-item {\n position: relative;\n}\n\n.kombos-header__dropdown-item:hover > .kombos-header__dropdown--nested {\n display: flex;\n}\n\n.kombos-header__dropdown-link {\n display: flex;\n align-items: center;\n justify-content: space-between;\n color: var(--kombos-gray-900);\n text-decoration: none;\n padding: 0.75rem 1rem;\n transition: background-color 0.15s ease;\n}\n\n.kombos-header__dropdown-link:hover {\n background-color: var(--kombos-gray-50);\n}\n\n.kombos-header__caret-right {\n flex-shrink: 0;\n color: var(--kombos-gray-900);\n}\n\n/* Action Icons */\n.kombos-navbar__actions {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 0.75rem;\n}\n\n.kombos-navbar__icon-btn {\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 1.5rem;\n height: 1.5rem;\n position: relative;\n transition: opacity 0.2s ease;\n font-size: 1.25rem;\n}\n\n.kombos-navbar__icon-btn:hover {\n opacity: 0.6;\n}\n\n.kombos-navbar__badge {\n position: absolute;\n top: -0.375rem;\n right: -0.5rem;\n background: var(--kombos-badge-bg);\n color: var(--kombos-white);\n font-size: 0.625rem;\n font-weight: 600;\n min-width: 1.125rem;\n height: 1.125rem;\n border-radius: 9px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 0.25rem;\n}\n\n/* Hamburger (mobile only — visible by default) */\n.kombos-navbar__hamburger {\n display: flex;\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n width: 1.5rem;\n height: 1.5rem;\n align-items: center;\n justify-content: center;\n color: var(--kombos-gray-900);\n transition: opacity 0.2s ease;\n font-size: 1.375rem;\n}\n\n.kombos-navbar__hamburger:hover {\n opacity: 0.6;\n}\n\n/* ===== Responsive — Desktop ===== */\n@media (min-width: 1024px) {\n .kombos-navbar__inner {\n gap: 2rem;\n position: static;\n justify-content: initial;\n min-height: calc(var(--logo-h-desktop, 3.75rem) + 1.5rem);\n }\n\n .kombos-navbar__hamburger {\n display: none;\n }\n\n .kombos-navbar__nav {\n display: flex;\n }\n\n .kombos-navbar__nav--no-logo {\n justify-content: flex-start;\n }\n\n .kombos-navbar__logo {\n position: static;\n left: auto;\n top: auto;\n transform: none;\n height: var(--logo-h-desktop, 3.75rem);\n }\n\n .kombos-navbar__actions {\n gap: 1.25rem;\n }\n}\n"
17459
- },
17460
- {
17461
- "filename": "children/Navbar/types.ts",
17462
- "content": "import type { IkasImage, HeightStyleType, IkasNavigationLinkList, IkasProductList } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../global-types\";\n\nexport interface Props {\n logo?: IkasImage | null;\n logoSizeDesktop?: HeightStyleType;\n logoSizeMobile?: HeightStyleType;\n navigationLinks?: IkasNavigationLinkList;\n navigationLinkColor?: string;\n coloredLinks?: IkasNavigationLinkList;\n coloredLinkColor?: string;\n cartTitle?: string;\n emptyCartText?: string;\n checkoutButtonText?: string;\n totalText?: string;\n freeShippingText?: string;\n emptyCartButtonText?: string;\n registerButtonText?: string;\n loginButtonText?: string;\n logoutButtonText?: string;\n searchProductList?: IkasProductList;\n hideAddToCartButton?: boolean;\n searchPlaceholder?: string;\n searchingText?: string;\n noResultsText?: string;\n resultCountText?: string;\n addToCartText?: string;\n addedToCartText?: string;\n outOfStockText?: string;\n goToProductText?: string;\n viewAllText?: string;\n components?: any;\n imageAspectRatio?: AspectRatio;\n imageObjectFit?: ObjectFit;\n viewCartButtonText?: string;\n}\n"
17463
- },
17464
- {
17465
- "filename": "ikas-config-snippet.json",
17466
- "content": "{\n \"id\": \"{{PROJECT_ID}}-header\",\n \"name\": \"Header\",\n \"type\": \"section\",\n \"entry\": \"./src/components/Header/index.tsx\",\n \"styles\": \"./src/components/Header/styles.css\",\n \"props\": [\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"defaultValue\": \"#ffffff\",\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"borderColor\",\n \"displayName\": \"Border Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"defaultValue\": \"#f6f6f6\",\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Components\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-navbar\",\n \"{{PROJECT_ID}}-announcements\",\n \"{{PROJECT_ID}}-cookie-bar\"\n ]\n }\n ],\n \"isHeader\": true,\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View\",\n \"description\": \"Background ve kenarlık renkleri\"\n }\n ]\n}"
17467
- },
17468
- {
17469
- "filename": "index.tsx",
17470
- "content": "// import { IkasComponentRenderer } from \"@ikas/bp-storefront\";\nimport { useEffect, useCallback } from \"preact/hooks\";\nimport { Props } from \"./types\";\nimport { ToastContainer } from \"../../sub-components/Toast\";\nimport { useToast } from \"../../hooks/useToast\";\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\n\nexport function Header(props: Props) {\n const {\n backgroundColor = \"var(--kombos-white)\",\n borderColor = \"var(--kombos-gray-100)\",\n components,\n } = props;\n\n const toast = useToast();\n\n const handleShowToast = useCallback(\n (e: Event) => {\n const detail = (e as CustomEvent).detail as {\n message: string;\n variant: \"success\" | \"error\";\n };\n toast.show(detail.message, detail.variant);\n },\n [toast.show],\n );\n\n useEffect(() => {\n window.addEventListener(\"ikas:show-toast\", handleShowToast);\n return () => window.removeEventListener(\"ikas:show-toast\", handleShowToast);\n }, [handleShowToast]);\n\n return (\n <>\n <section\n className=\"kombos-header\"\n style={{\n backgroundColor,\n borderColor,\n }}\n >\n <IkasComponentRenderer\n id=\"header\"\n components={components}\n parentProps={props}\n />\n </section>\n <ToastContainer toasts={toast.toasts} onDismiss={toast.dismiss} />\n </>\n );\n}\n\nexport default Header;\n"
17471
- },
17472
- {
17473
- "filename": "styles.css",
17474
- "content": "/* ===== Header Section ===== */\n.kombos-header {\n width: 100%;\n background-color: var(--kombos-white);\n border-bottom: 1px solid var(--kombos-gray-200);\n}\n"
17475
- },
17476
- {
17477
- "filename": "types.ts",
17478
- "content": "export interface Props {\n backgroundColor?: string;\n borderColor?: string;\n components?: any;\n}\n"
17479
- }
17480
- ]
17481
- },
17482
- {
17483
- "id": "hero-slider-section",
17484
- "title": "Hero Slider Section",
17485
- "description": "Full-width hero slider with configurable slides containing image, title, subtitle, and CTA button. Uses IkasComponentRenderer to render child HeroSliderItem components. Touch/swipe support and auto-play.",
17486
- "code": "import { useState, useEffect, useCallback, useRef } from \"preact/hooks\";\nimport { Props } from \"./types\";\nimport SliderArrow from \"../../sub-components/SliderArrow\";\nimport { cx } from \"../../utils/cx\";\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\n\nexport function HeroSlider(props: Props) {\n const {\n backgroundColor,\n autoplay = false,\n autoplayDelay = 5000,\n fullWidth = false,\n showArrows = false,\n components,\n } = props;\n\n const count = components?.length ?? 0;\n\n const [current, setCurrent] = useState(0);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const resetAutoplay = useCallback(() => {\n if (timerRef.current) clearInterval(timerRef.current);\n if (!autoplay || count <= 1) return;\n timerRef.current = setInterval(() => {\n setCurrent((prev) => (prev + 1) % count);\n }, autoplayDelay);\n }, [autoplay, autoplayDelay, count]);\n\n const goTo = useCallback(\n (index: number) => {\n if (count === 0) return;\n setCurrent(((index % count) + count) % count);\n resetAutoplay();\n },\n [count, resetAutoplay],\n );\n\n useEffect(() => {\n resetAutoplay();\n return () => {\n if (timerRef.current) clearInterval(timerRef.current);\n };\n }, [resetAutoplay]);\n\n if (count === 0) return null;\n\n const viewportClass = cx(\n \"kombos-hero-slider__viewport\",\n !fullWidth && \"kombos-hero-slider__viewport--contained\",\n !fullWidth && \"kombos-container\",\n );\n\n return (\n <section\n className=\"kombos-hero-slider\"\n style={backgroundColor ? { backgroundColor } : undefined}\n >\n <div className={viewportClass}>\n <div className=\"kombos-hero-slider__viewport-inner\">\n <div\n className=\"kombos-hero-slider__track\"\n style={{ transform: `translateX(-${current * 100}%)` }}\n >\n <IkasComponentRenderer\n id=\"hero-slider\"\n components={components}\n parentProps={props}\n />\n </div>\n\n {/* Navigation arrows */}\n {showArrows && count > 1 && (\n <>\n <SliderArrow\n direction=\"left\"\n className=\"kombos-hero-slider__arrow kombos-hero-slider__arrow--prev\"\n onClick={() => goTo(current - 1)}\n />\n <SliderArrow\n direction=\"right\"\n className=\"kombos-hero-slider__arrow kombos-hero-slider__arrow--next\"\n onClick={() => goTo(current + 1)}\n />\n </>\n )}\n\n {/* Dot pagination */}\n {count > 1 && (\n <div className=\"kombos-hero-slider__dots\">\n {Array.from({ length: count }).map((_, i) => (\n <button\n key={i}\n className={cx(\n \"kombos-hero-slider__dot\",\n i === current && \"kombos-hero-slider__dot--active\",\n )}\n onClick={() => goTo(i)}\n aria-label={`Go to slide ${i + 1}`}\n />\n ))}\n </div>\n )}\n </div>\n </div>\n </section>\n );\n}\n\nexport default HeroSlider;\n",
17487
- "relatedFunctions": [
17488
- "getDefaultSrc",
17489
- "createMediaSrcset",
17490
- "getFormattedFontSize",
17491
- "Router"
17492
- ],
17493
- "categories": [
17494
- "Layout",
17495
- "Slider"
17496
- ],
17497
- "files": [
17498
- {
17499
- "filename": "children/HeroSliderItem/ikas-config-snippet.json",
17500
- "content": "{\n \"id\": \"{{PROJECT_ID}}-hero-slide\",\n \"name\": \"HeroSliderItem\",\n \"type\": \"component\",\n \"entry\": \"./src/components/HeroSliderItem/index.tsx\",\n \"styles\": \"./src/components/HeroSliderItem/styles.css\",\n \"props\": [\n {\n \"name\": \"image\",\n \"displayName\": \"Image\",\n \"type\": \"IMAGE\",\n \"required\": false,\n \"groupId\": \"desktop\"\n },\n {\n \"name\": \"aspectRatio\",\n \"displayName\": \"En Boy Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"objectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"enumTypeId\": \"GrylMqHxui\"\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"RICH_TEXT\",\n \"required\": false,\n \"groupId\": \"desktop\"\n },\n {\n \"name\": \"titleAlignment\",\n \"displayName\": \"Title Hizalama\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"enumTypeId\": \"08UWYbbs0P\"\n },\n {\n \"name\": \"textColor\",\n \"displayName\": \"Post Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"desktop\"\n },\n {\n \"name\": \"overlay\",\n \"displayName\": \"Overlay\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"desktop\"\n },\n {\n \"name\": \"roundedCorners\",\n \"displayName\": \"Rounded Corners\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"desktop\"\n },\n {\n \"name\": \"mobileImage\",\n \"displayName\": \"Image\",\n \"type\": \"IMAGE\",\n \"required\": false,\n \"groupId\": \"mobile\"\n },\n {\n \"name\": \"mobileAspectRatio\",\n \"displayName\": \"En Boy Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"mobileObjectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"enumTypeId\": \"GrylMqHxui\"\n },\n {\n \"name\": \"mobileTitle\",\n \"displayName\": \"Title\",\n \"type\": \"RICH_TEXT\",\n \"required\": false,\n \"groupId\": \"mobile\"\n },\n {\n \"name\": \"mobileTitleAlignment\",\n \"displayName\": \"Title Hizalama\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"enumTypeId\": \"08UWYbbs0P\"\n },\n {\n \"name\": \"mobileTextColor\",\n \"displayName\": \"Post Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"mobile\"\n },\n {\n \"name\": \"mobileOverlay\",\n \"displayName\": \"Overlay\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"mobile\"\n },\n {\n \"name\": \"mobileRoundedCorners\",\n \"displayName\": \"Rounded Corners\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"mobile\"\n },\n {\n \"name\": \"link\",\n \"displayName\": \"Link\",\n \"type\": \"LINK\",\n \"required\": false,\n \"groupId\": \"desktopButton\"\n },\n {\n \"name\": \"buttonFontSize\",\n \"displayName\": \"Post Size\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktopButton\",\n \"typeId\": \"@ikas/bp-storefront-models-FontSizeStyleType\"\n },\n {\n \"name\": \"buttonColor\",\n \"displayName\": \"Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"desktopButton\"\n },\n {\n \"name\": \"buttonBgColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"desktopButton\"\n },\n {\n \"name\": \"buttonHoverColor\",\n \"displayName\": \"Color (Hover)\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"desktopButton\"\n },\n {\n \"name\": \"buttonHoverBgColor\",\n \"displayName\": \"Background Color (Hover)\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"desktopButton\"\n },\n {\n \"name\": \"mobileLink\",\n \"displayName\": \"Link\",\n \"type\": \"LINK\",\n \"required\": false,\n \"groupId\": \"mobileButton\"\n },\n {\n \"name\": \"mobileButtonFontSize\",\n \"displayName\": \"Post Size\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobileButton\",\n \"typeId\": \"@ikas/bp-storefront-models-FontSizeStyleType\"\n },\n {\n \"name\": \"mobileButtonColor\",\n \"displayName\": \"Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"mobileButton\"\n },\n {\n \"name\": \"mobileButtonBgColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"mobileButton\"\n },\n {\n \"name\": \"mobileButtonHoverColor\",\n \"displayName\": \"Color (Hover)\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"mobileButton\"\n },\n {\n \"name\": \"mobileButtonHoverBgColor\",\n \"displayName\": \"Background Color (Hover)\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"mobileButton\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop Settings\",\n \"children\": [\n {\n \"id\": \"desktopButton\",\n \"name\": \"Button Settings\",\n \"description\": \"Desktop görünümündeki butonun link, renk ve hover ayarları\"\n }\n ],\n \"description\": \"Desktop cihazlarda gösterilecek görsel, başlık ve stil ayarları\"\n },\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile Settings\",\n \"children\": [\n {\n \"id\": \"mobileButton\",\n \"name\": \"Button Settings\",\n \"description\": \"Mobile görünümdeki butonun link, renk ve hover ayarları\"\n }\n ],\n \"description\": \"Mobile cihazlarda gösterilecek görsel, başlık ve stil ayarları\"\n }\n ]\n}"
17501
- },
17502
- {
17503
- "filename": "children/HeroSliderItem/index.tsx",
17504
- "content": "import {\n IkasImage,\n getDefaultSrc,\n createMediaSrcset,\n getFormattedFontSize,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport {\n resolveAspectRatio,\n resolveObjectFit,\n resolveVerticalAlignment,\n} from \"../../utils/media\";\nimport { cx } from \"../../utils/cx\";\n\nfunction renderImage(params: {\n img: IkasImage;\n cssClass?: string;\n objectFit?: string;\n aspectRatio?: string;\n alt?: string;\n sizes?: string;\n}) {\n const { img, cssClass, objectFit, aspectRatio, alt, sizes } = params;\n\n return (\n <div className={cssClass} style={aspectRatio ? { aspectRatio } : undefined}>\n <img\n className=\"kombos-hero-slider-item__img\"\n src={getDefaultSrc(img)}\n srcSet={createMediaSrcset(img)}\n sizes={sizes}\n alt={alt}\n style={{ objectFit }}\n loading=\"eager\"\n fetchpriority=\"high\"\n decoding=\"async\"\n />\n </div>\n );\n}\n\nexport function HeroSliderItem({\n image,\n aspectRatio,\n objectFit,\n title = \"\",\n titleAlignment,\n textColor = \"#FFFFFF\",\n overlay = false,\n roundedCorners = false,\n link = null,\n buttonFontSize,\n buttonColor,\n buttonBgColor,\n buttonHoverColor,\n buttonHoverBgColor,\n mobileImage,\n mobileAspectRatio,\n mobileObjectFit,\n mobileTitle,\n mobileTextColor = \"#FFFFFF\",\n mobileOverlay = false,\n mobileRoundedCorners = false,\n mobileLink,\n mobileButtonFontSize,\n mobileButtonColor,\n mobileButtonBgColor,\n mobileButtonHoverColor,\n mobileButtonHoverBgColor,\n mobileTitleAlignment,\n}: Props) {\n const desktopImg = image || mobileImage;\n const mobileImg = mobileImage || image;\n\n if (!desktopImg && !mobileImg) return null;\n\n const desktopFit = resolveObjectFit(objectFit);\n const mobileFit = mobileObjectFit\n ? resolveObjectFit(mobileObjectFit)\n : desktopFit;\n\n const desktopAR = aspectRatio ? resolveAspectRatio(aspectRatio) : undefined;\n const mobileAR = mobileAspectRatio\n ? resolveAspectRatio(mobileAspectRatio)\n : desktopAR;\n\n const desktopAlign = resolveVerticalAlignment(titleAlignment);\n const mobileAlign = mobileTitleAlignment\n ? resolveVerticalAlignment(mobileTitleAlignment)\n : \"flex-start\";\n\n const desktopTitle = title;\n const mobTitle = mobileTitle ?? title;\n\n const desktopTextColor = textColor;\n const mobTextColor = mobileTextColor ?? textColor;\n\n const desktopOverlay = overlay;\n const mobOverlay = mobileOverlay;\n\n // Mobile button falls back to desktop values\n const mobLink = mobileLink ?? link;\n const mobBtnFontSize = mobileButtonFontSize ?? buttonFontSize;\n const mobBtnColor = mobileButtonColor ?? buttonColor;\n const mobBtnBgColor = mobileButtonBgColor ?? buttonBgColor;\n const mobBtnHoverColor = mobileButtonHoverColor ?? buttonHoverColor;\n const mobBtnHoverBgColor = mobileButtonHoverBgColor ?? buttonHoverBgColor;\n\n const rootClass = cx(\n \"kombos-hero-slider-item\",\n roundedCorners && \"kombos-hero-slider-item--rounded\",\n mobileRoundedCorners && \"kombos-hero-slider-item--mobile-rounded\",\n );\n\n const handleDesktopClick = () => {\n if (!link) return;\n Router.navigate(link.href || \"/\", false, link.openInNewTab ?? false);\n };\n\n const handleMobileClick = () => {\n if (!mobLink) return;\n Router.navigate(mobLink.href || \"/\", false, mobLink.openInNewTab ?? false);\n };\n\n const formattedDesktopFontSize = getFormattedFontSize(buttonFontSize);\n const formattedMobileFontSize = getFormattedFontSize(mobBtnFontSize);\n\n const desktopBtnStyle = {\n ...(formattedDesktopFontSize ? { fontSize: formattedDesktopFontSize } : {}),\n ...(buttonColor ? { \"--btn-color\": buttonColor } : {}),\n ...(buttonBgColor ? { \"--btn-bg\": buttonBgColor } : {}),\n ...(buttonHoverColor ? { \"--btn-hover-color\": buttonHoverColor } : {}),\n ...(buttonHoverBgColor ? { \"--btn-hover-bg\": buttonHoverBgColor } : {}),\n };\n\n const mobileBtnStyle = {\n ...(formattedMobileFontSize ? { fontSize: formattedMobileFontSize } : {}),\n ...(mobBtnColor ? { \"--btn-color\": mobBtnColor } : {}),\n ...(mobBtnBgColor ? { \"--btn-bg\": mobBtnBgColor } : {}),\n ...(mobBtnHoverColor ? { \"--btn-hover-color\": mobBtnHoverColor } : {}),\n ...(mobBtnHoverBgColor ? { \"--btn-hover-bg\": mobBtnHoverBgColor } : {}),\n };\n\n return (\n <div className={rootClass}>\n {/* Desktop image */}\n {desktopImg &&\n renderImage({\n img: desktopImg,\n cssClass:\n \"kombos-hero-slider-item__img-wrap kombos-hero-slider-item__img-wrap--desktop\",\n objectFit: desktopFit,\n aspectRatio: desktopAR,\n alt: desktopImg?.altText ?? title,\n sizes: \"(min-width: 1024px) 100vw, 1px\",\n })}\n {/* Mobile image */}\n {mobileImg &&\n renderImage({\n img: mobileImg,\n cssClass:\n \"kombos-hero-slider-item__img-wrap kombos-hero-slider-item__img-wrap--mobile\",\n objectFit: mobileFit,\n aspectRatio: mobileAR,\n alt: mobileImg?.altText ?? mobTitle,\n sizes: \"(max-width: 1023px) 100vw, 1px\",\n })}\n {/* Desktop overlay */}\n {desktopOverlay && (\n <div className=\"kombos-hero-slider-item__overlay kombos-hero-slider-item__overlay--desktop\" />\n )}\n {/* Mobile overlay */}\n {mobOverlay && (\n <div className=\"kombos-hero-slider-item__overlay kombos-hero-slider-item__overlay--mobile\" />\n )}\n {/* Desktop content */}\n <div\n className=\"kombos-hero-slider-item__content kombos-hero-slider-item__content--desktop\"\n style={{\n \"--slide-justify\": desktopAlign,\n ...(desktopTextColor ? { color: desktopTextColor } : {}),\n }}\n >\n {desktopTitle && (\n <div\n className=\"kombos-hero-slider-item__title display-md-medium md:display-lg-medium\"\n dangerouslySetInnerHTML={{ __html: desktopTitle }}\n />\n )}\n {link && link.label && (\n <button\n type=\"button\"\n className=\"kombos-hero-slider-item__cta\"\n style={desktopBtnStyle}\n onClick={handleDesktopClick}\n >\n {link.label}\n </button>\n )}\n </div>\n {/* Mobile content */}\n <div\n className=\"kombos-hero-slider-item__content kombos-hero-slider-item__content--mobile\"\n style={{\n \"--slide-justify\": mobileAlign,\n ...(mobTextColor ? { color: mobTextColor } : {}),\n }}\n >\n {mobTitle && (\n <div\n className=\"kombos-hero-slider-item__title display-xs-semibold md:display-sm-semibold\"\n dangerouslySetInnerHTML={{ __html: mobTitle }}\n />\n )}\n {mobLink && mobLink.label && (\n <button\n type=\"button\"\n className=\"kombos-hero-slider-item__cta\"\n style={mobileBtnStyle}\n onClick={handleMobileClick}\n >\n {mobLink.label}\n </button>\n )}\n </div>\n </div>\n );\n}\n\nexport default HeroSliderItem;\n"
17505
- },
17506
- {
17507
- "filename": "children/HeroSliderItem/styles.css",
17508
- "content": ".kombos-hero-slider-item {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.kombos-hero-slider-item--mobile-rounded {\n border-radius: 12px;\n}\n\n/* Image wrapper */\n.kombos-hero-slider-item__img-wrap {\n width: 100%;\n}\n\n.kombos-hero-slider-item__img-wrap--desktop {\n display: none;\n}\n\n.kombos-hero-slider-item__img-wrap--mobile {\n display: block;\n}\n\n.kombos-hero-slider-item__img {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n/* Overlay */\n.kombos-hero-slider-item__overlay {\n position: absolute;\n inset: 0;\n background: rgba(0, 0, 0, 0.2);\n pointer-events: none;\n}\n\n.kombos-hero-slider-item__overlay--desktop {\n display: none;\n}\n\n.kombos-hero-slider-item__overlay--mobile {\n display: block;\n}\n\n/* Content */\n.kombos-hero-slider-item__content {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: var(--slide-justify, center);\n padding: 1.5rem 1rem;\n gap: 1.25rem;\n pointer-events: none;\n}\n\n.kombos-hero-slider-item__content--desktop {\n display: none;\n}\n\n.kombos-hero-slider-item__content--mobile {\n display: flex;\n justify-content: var(--slide-justify, flex-start);\n text-align: center;\n align-items: center;\n}\n\n.kombos-hero-slider-item__title {\n margin: 0;\n color: inherit;\n pointer-events: auto;\n width: 100%;\n text-wrap-style: pretty;\n}\n\n.kombos-hero-slider-item__content--desktop .kombos-hero-slider-item__title {\n max-width: 80%;\n}\n\n/* CTA button */\n.kombos-hero-slider-item__cta {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0.625rem 1.25rem;\n border: 1px solid transparent;\n border-radius: 6px;\n cursor: pointer;\n pointer-events: auto;\n background: var(--btn-bg, var(--kombos-white));\n color: var(--btn-color, var(--kombos-gray-900));\n border-color: var(--btn-bg, var(--kombos-gray-200));\n transition: background 0.2s ease, color 0.2s ease;\n}\n\n.kombos-hero-slider-item__cta:hover {\n background: var(--btn-hover-bg, var(--kombos-gray-50));\n color: var(--btn-hover-color, var(--btn-color, var(--kombos-gray-900)));\n border-color: var(--btn-hover-bg, var(--kombos-gray-300));\n}\n\n/* Tablet */\n@media (min-width: 768px) {\n .kombos-hero-slider-item__content {\n padding: 3rem;\n gap: 1.5rem;\n }\n}\n\n/* Desktop */\n@media (min-width: 1024px) {\n .kombos-hero-slider-item--rounded {\n border-radius: 12px;\n }\n\n .kombos-hero-slider-item--mobile-rounded {\n border-radius: 0;\n }\n\n /* Swap visibility: show desktop, hide mobile */\n .kombos-hero-slider-item__img-wrap--desktop {\n display: block;\n }\n\n .kombos-hero-slider-item__img-wrap--mobile {\n display: none;\n }\n\n .kombos-hero-slider-item__overlay--desktop {\n display: block;\n }\n\n .kombos-hero-slider-item__overlay--mobile {\n display: none;\n }\n\n .kombos-hero-slider-item__content--desktop {\n display: flex;\n align-items: center;\n text-align: center;\n }\n\n .kombos-hero-slider-item__content--mobile {\n display: none;\n }\n\n .kombos-hero-slider-item__content {\n padding: 2.5rem 4.5rem;\n }\n}\n"
17509
- },
17510
- {
17511
- "filename": "children/HeroSliderItem/types.ts",
17512
- "content": "import type { IkasImage, IkasNavigationLink, FontSizeStyleType } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit, VerticalAlignment } from \"../../global-types\";\n\nexport interface Props {\n image?: IkasImage | null;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n title?: string;\n titleAlignment?: VerticalAlignment;\n textColor?: string;\n overlay?: boolean;\n roundedCorners?: boolean;\n mobileImage?: IkasImage | null;\n mobileAspectRatio?: AspectRatio;\n mobileObjectFit?: ObjectFit;\n mobileTitle?: string;\n mobileTitleAlignment?: VerticalAlignment;\n mobileTextColor?: string;\n mobileOverlay?: boolean;\n mobileRoundedCorners?: boolean;\n link?: IkasNavigationLink | null;\n buttonFontSize?: FontSizeStyleType;\n buttonColor?: string;\n buttonBgColor?: string;\n buttonHoverColor?: string;\n buttonHoverBgColor?: string;\n mobileLink?: IkasNavigationLink | null;\n mobileButtonFontSize?: FontSizeStyleType;\n mobileButtonColor?: string;\n mobileButtonBgColor?: string;\n mobileButtonHoverColor?: string;\n mobileButtonHoverBgColor?: string;\n}\n"
17513
- },
17514
- {
17515
- "filename": "ikas-config-snippet.json",
17516
- "content": "{\n \"id\": \"{{PROJECT_ID}}-hero-slider\",\n \"name\": \"HeroSlider\",\n \"type\": \"section\",\n \"entry\": \"./src/components/HeroSlider/index.tsx\",\n \"styles\": \"./src/components/HeroSlider/styles.css\",\n \"props\": [\n {\n \"name\": \"backgroundColor\",\n \"displayName\": \"Background Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"defaultValue\": \"#ffffff\",\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"autoplay\",\n \"displayName\": \"Auto Play\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"settings\"\n },\n {\n \"name\": \"autoplayDelay\",\n \"displayName\": \"Play Interval (ms)\",\n \"type\": \"NUMBER\",\n \"required\": false,\n \"defaultValue\": 5000,\n \"groupId\": \"settings\"\n },\n {\n \"name\": \"fullWidth\",\n \"displayName\": \"Full Width\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"settings\"\n },\n {\n \"name\": \"showArrows\",\n \"displayName\": \"Arrows Show\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"settings\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Slides\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-hero-slide\"\n ]\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View\",\n \"description\": \"Background rengi ve stil ayarları\"\n },\n {\n \"id\": \"settings\",\n \"name\": \"Settings\",\n \"description\": \"Auto oynatma, genişlik ve navigasyon ayarları\"\n }\n ]\n}"
17517
- },
17518
- {
17519
- "filename": "index.tsx",
17520
- "content": "import { useState, useEffect, useCallback, useRef } from \"preact/hooks\";\nimport { Props } from \"./types\";\nimport SliderArrow from \"../../sub-components/SliderArrow\";\nimport { cx } from \"../../utils/cx\";\nimport { IkasComponentRenderer } from \"@ikas/bp-storefront\";\n\nexport function HeroSlider(props: Props) {\n const {\n backgroundColor,\n autoplay = false,\n autoplayDelay = 5000,\n fullWidth = false,\n showArrows = false,\n components,\n } = props;\n\n const count = components?.length ?? 0;\n\n const [current, setCurrent] = useState(0);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const resetAutoplay = useCallback(() => {\n if (timerRef.current) clearInterval(timerRef.current);\n if (!autoplay || count <= 1) return;\n timerRef.current = setInterval(() => {\n setCurrent((prev) => (prev + 1) % count);\n }, autoplayDelay);\n }, [autoplay, autoplayDelay, count]);\n\n const goTo = useCallback(\n (index: number) => {\n if (count === 0) return;\n setCurrent(((index % count) + count) % count);\n resetAutoplay();\n },\n [count, resetAutoplay],\n );\n\n useEffect(() => {\n resetAutoplay();\n return () => {\n if (timerRef.current) clearInterval(timerRef.current);\n };\n }, [resetAutoplay]);\n\n if (count === 0) return null;\n\n const viewportClass = cx(\n \"kombos-hero-slider__viewport\",\n !fullWidth && \"kombos-hero-slider__viewport--contained\",\n !fullWidth && \"kombos-container\",\n );\n\n return (\n <section\n className=\"kombos-hero-slider\"\n style={backgroundColor ? { backgroundColor } : undefined}\n >\n <div className={viewportClass}>\n <div className=\"kombos-hero-slider__viewport-inner\">\n <div\n className=\"kombos-hero-slider__track\"\n style={{ transform: `translateX(-${current * 100}%)` }}\n >\n <IkasComponentRenderer\n id=\"hero-slider\"\n components={components}\n parentProps={props}\n />\n </div>\n\n {/* Navigation arrows */}\n {showArrows && count > 1 && (\n <>\n <SliderArrow\n direction=\"left\"\n className=\"kombos-hero-slider__arrow kombos-hero-slider__arrow--prev\"\n onClick={() => goTo(current - 1)}\n />\n <SliderArrow\n direction=\"right\"\n className=\"kombos-hero-slider__arrow kombos-hero-slider__arrow--next\"\n onClick={() => goTo(current + 1)}\n />\n </>\n )}\n\n {/* Dot pagination */}\n {count > 1 && (\n <div className=\"kombos-hero-slider__dots\">\n {Array.from({ length: count }).map((_, i) => (\n <button\n key={i}\n className={cx(\n \"kombos-hero-slider__dot\",\n i === current && \"kombos-hero-slider__dot--active\",\n )}\n onClick={() => goTo(i)}\n aria-label={`Go to slide ${i + 1}`}\n />\n ))}\n </div>\n )}\n </div>\n </div>\n </section>\n );\n}\n\nexport default HeroSlider;\n"
17521
- },
17522
- {
17523
- "filename": "styles.css",
17524
- "content": ".kombos-hero-slider {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.kombos-hero-slider__viewport {\n position: relative;\n width: 100%;\n}\n\n.kombos-hero-slider__viewport--contained {\n max-width: var(--kombos-max-width);\n margin: 0 auto;\n}\n\n@media (max-width: 1499px) {\n .kombos-hero-slider__viewport--contained {\n padding-left: 0;\n padding-right: 0;\n }\n}\n\n.kombos-hero-slider__viewport-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.kombos-hero-slider__track {\n display: flex;\n transition: transform 0.5s ease-in-out;\n will-change: transform;\n}\n\n/* IkasComponentRenderer renders a display:contents wrapper,\n so the actual flex children are the grandchild ikas wrapper divs */\n.kombos-hero-slider__track > * > * > * {\n position: relative;\n flex: 0 0 100%;\n width: 100%;\n}\n\n/* Navigation arrows — positioned over the slider */\n.kombos-hero-slider__arrow {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n font-size: 1.125rem;\n}\n\n.kombos-hero-slider__arrow--prev {\n left: 0.75rem;\n}\n\n.kombos-hero-slider__arrow--next {\n right: 0.75rem;\n}\n\n/* Dot pagination */\n.kombos-hero-slider__dots {\n position: absolute;\n bottom: 1.25rem;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n gap: 0;\n align-items: center;\n}\n\n.kombos-hero-slider__dot {\n width: 0.5rem;\n height: 0.5rem;\n border-radius: 50%;\n border: none;\n padding: 1rem;\n box-sizing: content-box;\n cursor: pointer;\n background: rgba(255, 255, 255, 0.5);\n background-clip: content-box;\n transition: background 0.2s ease;\n}\n\n.kombos-hero-slider__dot--active {\n background: var(--kombos-white);\n background-clip: content-box;\n}\n\n.kombos-hero-slider__dot:hover {\n background: rgba(255, 255, 255, 0.8);\n background-clip: content-box;\n}\n\n/* Responsive — tablet */\n@media (min-width: 768px) {\n .kombos-hero-slider__arrow--prev {\n left: 1rem;\n }\n\n .kombos-hero-slider__arrow--next {\n right: 1rem;\n }\n\n .kombos-hero-slider__dots {\n bottom: 0.75rem;\n gap: 0;\n }\n\n .kombos-hero-slider__dot {\n width: 0.75rem;\n height: 0.75rem;\n padding: 0.75rem;\n }\n}\n\n/* Responsive — desktop */\n@media (min-width: 1024px) {\n .kombos-hero-slider__arrow--prev {\n left: 2rem;\n }\n\n .kombos-hero-slider__arrow--next {\n right: 2rem;\n }\n\n .kombos-hero-slider__dots {\n bottom: 2rem;\n }\n}\n"
17525
- },
17526
- {
17527
- "filename": "types.ts",
17528
- "content": "export interface Props {\n backgroundColor?: string;\n autoplay?: boolean;\n autoplayDelay?: number;\n fullWidth?: boolean;\n showArrows?: boolean;\n components?: any;\n}\n"
17529
- }
17530
- ]
17531
- },
17532
- {
17533
- "id": "image-handling",
17534
- "title": "Image Handling Pattern",
17535
- "description": "Product image gallery with responsive srcsets, aspect ratio control, zoom, and image preview modal. Shows getDefaultSrc, createMediaSrcset, getProductVariantMainImage, IkasImage handling, and image optimization patterns.",
17536
- "code": "import {\n getIkasCategoryPathItemHref,\n getProductCategoryPath,\n getProductVariantMainImage,\n getSelectedProductVariant,\n IkasImage,\n isNotEmpty,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport Breadcrumb from \"../../sub-components/Breadcrumb\";\nimport type { BreadcrumbItem } from \"../../sub-components/Breadcrumb\";\nimport ProductGallery from \"./components/ProductGallery\";\n\nexport function ProductDetail(props: Props) {\n const {\n product,\n components,\n aspectRatio,\n objectFit,\n bottomComponents,\n homepageText = \"Anasayfa\",\n } = props;\n if (!product) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\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\n const categoryPath = getProductCategoryPath(product);\n\n return (\n <section className=\"kombos-pd\">\n <div className=\"kombos-container kombos-pd__container\">\n <Breadcrumb\n items={[\n { label: homepageText, href: \"/\" } as BreadcrumbItem,\n ...(isNotEmpty(categoryPath)\n ? categoryPath.map(\n (pathItem: any) =>\n ({\n label: pathItem.name,\n href: getIkasCategoryPathItemHref(pathItem),\n }) as BreadcrumbItem,\n )\n : []),\n { label: product.name } as BreadcrumbItem,\n ]}\n size=\"xs\"\n className=\"kombos-pd__breadcrumb\"\n />\n\n <div className=\"kombos-pd__layout\">\n <ProductGallery\n images={images}\n productName={product.name}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n />\n\n <div className=\"kombos-pd__info\">\n <IkasComponentRenderer\n id=\"product-detail-info\"\n components={components}\n parentProps={props}\n />\n </div>\n </div>\n\n {bottomComponents && (\n <div className=\"kombos-pd__bottom\">\n <IkasComponentRenderer\n id=\"product-detail-bottom\"\n components={bottomComponents}\n parentProps={props}\n />\n </div>\n )}\n </div>\n </section>\n );\n}\n\nexport default ProductDetail;\n",
17537
- "relatedFunctions": [
17538
- "getIkasCategoryPathItemHref",
17539
- "getProductCategoryPath",
17540
- "getProductVariantMainImage",
17541
- "getSelectedProductVariant",
17542
- "isNotEmpty"
17543
- ],
17544
- "categories": [
17545
- "Product",
17546
- "Image"
17547
- ],
17548
- "files": [
17549
- {
17550
- "filename": "components/ProductGallery/index.tsx",
17551
- "content": "import {\n createMediaSrcset,\n getDefaultSrc,\n getThumbnailSrc,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { useRef, useState, useCallback, useEffect, useMemo } from \"preact/hooks\";\nimport { cx } from \"../../../../utils/cx\";\nimport { NoProductSVG, PlaySVG } from \"../../../../sub-components/icons\";\nimport SliderArrow from \"../../../../sub-components/SliderArrow\";\nimport { resolveAspectRatio, resolveObjectFit } from \"../../../../utils/media\";\nimport type { AspectRatio, ObjectFit } from \"../../../../global-types\";\n\nconst SETTLE_DELAY = 100;\nconst DRAG_THRESHOLD_RATIO = 0.15;\nconst SNAP_DURATION = 300;\n\nfunction easeInOutCubic(t: number): number {\n return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;\n}\n\ninterface Props {\n images: IkasImage[];\n productName: string;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n}\n\nexport default function ProductGallery({\n images,\n productName,\n aspectRatio,\n objectFit,\n}: Props) {\n const [selectedIndex, setSelectedIndex] = useState(0);\n const trackRef = useRef<HTMLDivElement>(null);\n const mainWrapRef = useRef<HTMLDivElement>(null);\n const thumbsRef = useRef<HTMLDivElement>(null);\n const isProgrammaticRef = useRef(false);\n const settleTimerRef = useRef<ReturnType<typeof setTimeout>>();\n const isDraggingRef = useRef(false);\n const dragStartXRef = useRef(0);\n const dragScrollLeftRef = useRef(0);\n const animFrameRef = useRef(0);\n\n const hasMultiple = images.length > 1;\n const resolvedAR = resolveAspectRatio(aspectRatio);\n const resolvedOF = resolveObjectFit(objectFit);\n\n const imageStyle = useMemo(() => ({ objectFit: resolvedOF as CSSStyleDeclaration[\"objectFit\"] }), [resolvedOF]);\n const thumbStyle = useMemo(() => ({ aspectRatio: resolvedAR }), [resolvedAR]);\n\n // Sync thumbnail column height with main image\n useEffect(() => {\n if (!hasMultiple) return;\n const syncHeight = () => {\n const wrap = mainWrapRef.current;\n const thumbs = thumbsRef.current;\n if (wrap && thumbs) {\n thumbs.style.maxHeight = `${wrap.offsetHeight}px`;\n }\n };\n syncHeight();\n window.addEventListener(\"resize\", syncHeight);\n return () => window.removeEventListener(\"resize\", syncHeight);\n }, [hasMultiple, aspectRatio]);\n\n // Auto-play/pause videos based on selected slide\n useEffect(() => {\n const track = trackRef.current;\n if (!track) return;\n const slides = track.children;\n for (let i = 0; i < slides.length; i++) {\n const video = slides[i].querySelector(\"video\");\n if (!video) continue;\n if (i === selectedIndex) {\n video.play().catch(() => {});\n } else {\n video.pause();\n video.currentTime = 0;\n }\n }\n }, [selectedIndex]);\n\n const scrollThumbIntoView = useCallback((index: number) => {\n const thumb = thumbsRef.current?.children[index] as HTMLElement | undefined;\n thumb?.scrollIntoView({ block: \"nearest\", behavior: \"smooth\" });\n }, []);\n\n const smoothScrollTo = useCallback((track: HTMLElement, target: number) => {\n cancelAnimationFrame(animFrameRef.current);\n const start = track.scrollLeft;\n const distance = target - start;\n if (Math.abs(distance) < 1) {\n track.scrollLeft = target;\n track.style.scrollSnapType = \"x mandatory\";\n isProgrammaticRef.current = false;\n return;\n }\n const startTime = performance.now();\n const step = (now: number) => {\n const elapsed = Math.min((now - startTime) / SNAP_DURATION, 1);\n track.scrollLeft = start + distance * easeInOutCubic(elapsed);\n if (elapsed < 1) {\n animFrameRef.current = requestAnimationFrame(step);\n } else {\n track.style.scrollSnapType = \"x mandatory\";\n isProgrammaticRef.current = false;\n }\n };\n animFrameRef.current = requestAnimationFrame(step);\n }, []);\n\n const goToSlide = useCallback((index: number) => {\n const track = trackRef.current;\n if (!track) return;\n isProgrammaticRef.current = true;\n clearTimeout(settleTimerRef.current);\n const slide = track.children[index] as HTMLElement | undefined;\n if (slide) {\n track.scrollTo({ left: slide.offsetLeft, behavior: \"smooth\" });\n }\n setSelectedIndex(index);\n scrollThumbIntoView(index);\n }, [scrollThumbIntoView]);\n\n const handlePrev = useCallback(() => {\n goToSlide(selectedIndex === 0 ? images.length - 1 : selectedIndex - 1);\n }, [goToSlide, selectedIndex, images.length]);\n\n const handleNext = useCallback(() => {\n goToSlide(selectedIndex === images.length - 1 ? 0 : selectedIndex + 1);\n }, [goToSlide, selectedIndex, images.length]);\n\n const handleScroll = useCallback(() => {\n clearTimeout(settleTimerRef.current);\n if (isProgrammaticRef.current) {\n settleTimerRef.current = setTimeout(() => {\n isProgrammaticRef.current = false;\n }, SETTLE_DELAY);\n return;\n }\n const track = trackRef.current;\n if (!track) return;\n const newIndex = Math.round(track.scrollLeft / track.clientWidth);\n if (newIndex >= 0 && newIndex < images.length) {\n setSelectedIndex(newIndex);\n }\n }, [images.length]);\n\n const handleMouseDown = useCallback((e: MouseEvent) => {\n const track = trackRef.current;\n if (!track) return;\n cancelAnimationFrame(animFrameRef.current);\n isDraggingRef.current = true;\n dragStartXRef.current = e.pageX;\n dragScrollLeftRef.current = track.scrollLeft;\n track.style.scrollSnapType = \"none\";\n track.style.cursor = \"grabbing\";\n }, []);\n\n const handleMouseMove = useCallback((e: MouseEvent) => {\n if (!isDraggingRef.current) return;\n e.preventDefault();\n const track = trackRef.current;\n if (!track) return;\n track.scrollLeft = dragScrollLeftRef.current - (e.pageX - dragStartXRef.current);\n }, []);\n\n const handleMouseEnd = useCallback((e: MouseEvent) => {\n if (!isDraggingRef.current) return;\n isDraggingRef.current = false;\n const track = trackRef.current;\n if (!track) return;\n track.style.cursor = \"\";\n\n const dragDelta = dragStartXRef.current - e.pageX;\n const threshold = track.clientWidth * DRAG_THRESHOLD_RATIO;\n let targetIndex = Math.round(dragScrollLeftRef.current / track.clientWidth);\n if (dragDelta > threshold) targetIndex++;\n else if (dragDelta < -threshold) targetIndex--;\n const clamped = Math.max(0, Math.min(targetIndex, images.length - 1));\n\n isProgrammaticRef.current = true;\n setSelectedIndex(clamped);\n smoothScrollTo(track, track.clientWidth * clamped);\n scrollThumbIntoView(clamped);\n }, [images.length, smoothScrollTo, scrollThumbIntoView]);\n\n return (\n <div className=\"kombos-pd__gallery\">\n {hasMultiple && (\n <div className=\"kombos-pd__thumbs\" ref={thumbsRef}>\n {images.map((img, i) => (\n <button\n key={img.id || i}\n type=\"button\"\n className={cx(\"kombos-pd__thumb\", i === selectedIndex && \"kombos-pd__thumb--active\")}\n onClick={() => goToSlide(i)}\n style={thumbStyle}\n >\n {img.isVideo ? (\n <div className=\"kombos-pd__thumb-video\">\n <video\n src={getDefaultSrc(img)}\n className=\"kombos-pd__thumb-img\"\n style={imageStyle}\n muted\n preload=\"metadata\"\n >\n <track kind=\"captions\" />\n </video>\n <span className=\"kombos-pd__thumb-play\">\n <PlaySVG />\n </span>\n </div>\n ) : (\n <img\n src={getThumbnailSrc(img)}\n srcSet={createMediaSrcset(img)}\n sizes=\"112px\"\n alt={`${productName} ${i + 1}`}\n className=\"kombos-pd__thumb-img\"\n style={imageStyle}\n />\n )}\n </button>\n ))}\n </div>\n )}\n\n <div className=\"kombos-pd__main-col\">\n <div\n className=\"kombos-pd__main-image-wrap\"\n ref={mainWrapRef}\n style={{ aspectRatio: resolvedAR }}\n >\n <div\n className=\"kombos-pd__track\"\n ref={trackRef}\n onScroll={handleScroll}\n onMouseDown={handleMouseDown}\n onMouseMove={handleMouseMove}\n onMouseUp={handleMouseEnd}\n onMouseLeave={handleMouseEnd}\n >\n {images.length > 0 ? (\n images.map((img, i) => (\n <div key={img.id || i} className=\"kombos-pd__slide\">\n {img.isVideo ? (\n <video\n className=\"kombos-pd__main-image kombos-pd__main-video\"\n src={getDefaultSrc(img)}\n loop\n muted\n playsInline\n style={imageStyle}\n >\n <track kind=\"captions\" />\n </video>\n ) : (\n <img\n className=\"kombos-pd__main-image\"\n src={getDefaultSrc(img)}\n srcSet={createMediaSrcset(img)}\n sizes=\"(max-width: 1023px) 100vw, min(calc((100vw - 440px) / 2), 530px)\"\n alt={img?.altText || `${productName} ${i + 1}`}\n style={imageStyle}\n loading={i === 0 ? \"eager\" : \"lazy\"}\n fetchpriority={i === 0 ? \"high\" : undefined}\n />\n )}\n </div>\n ))\n ) : (\n <div className=\"kombos-pd__slide\">\n <div className=\"kombos-pd__main-image kombos-pd__main-image--placeholder\">\n <NoProductSVG />\n </div>\n </div>\n )}\n </div>\n\n {hasMultiple && (\n <>\n <SliderArrow\n direction=\"left\"\n className=\"kombos-pd__arrow kombos-pd__arrow--prev\"\n onClick={handlePrev}\n />\n <SliderArrow\n direction=\"right\"\n className=\"kombos-pd__arrow kombos-pd__arrow--next\"\n onClick={handleNext}\n />\n </>\n )}\n\n {hasMultiple && (\n <div className=\"kombos-pd__dots\">\n {images.map((_, i) => (\n <button\n key={i}\n type=\"button\"\n className={cx(\"kombos-pd__dot\", i === selectedIndex && \"kombos-pd__dot--active\")}\n onClick={() => goToSlide(i)}\n aria-label={`${images[i]?.isVideo ? \"Video\" : \"Image\"} ${i + 1}`}\n />\n ))}\n </div>\n )}\n </div>\n </div>\n </div>\n );\n}\n"
17552
- },
17553
- {
17554
- "filename": "components/ProductGallery/styles.css",
17555
- "content": "/* ---- Gallery ---- */\n.kombos-pd__gallery {\n display: flex;\n flex-direction: column;\n}\n\n.kombos-pd__thumbs {\n display: none;\n}\n\n.kombos-pd__main-col {\n flex: 1;\n min-width: 0;\n}\n\n.kombos-pd__main-image-wrap {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.kombos-pd__track {\n display: flex;\n overflow-x: auto;\n scroll-snap-type: x mandatory;\n scrollbar-width: none;\n width: 100%;\n height: 100%;\n -webkit-overflow-scrolling: touch;\n cursor: grab;\n user-select: none;\n}\n\n.kombos-pd__track::-webkit-scrollbar {\n display: none;\n}\n\n.kombos-pd__slide {\n flex: 0 0 100%;\n width: 100%;\n scroll-snap-align: start;\n scroll-snap-stop: always;\n}\n\n.kombos-pd__main-image {\n width: 100%;\n height: 100%;\n display: block;\n -webkit-user-drag: none;\n user-drag: none;\n pointer-events: none;\n}\n\n.kombos-pd__main-video {\n pointer-events: auto;\n cursor: default;\n}\n\n.kombos-pd__thumb-video {\n position: relative;\n width: 100%;\n height: 100%;\n}\n\n.kombos-pd__thumb-play {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n display: flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.55);\n color: var(--kombos-white);\n font-size: 0.75rem;\n}\n\n.kombos-pd__main-image--placeholder {\n border: 1px solid var(--kombos-gray-100);\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n color: var(--kombos-gray-300);\n font-size: 5rem;\n}\n\n/* Arrow buttons */\n.kombos-pd__arrow {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n}\n\n.kombos-pd__arrow--prev {\n left: 1.25rem;\n}\n\n.kombos-pd__arrow--next {\n right: 1.25rem;\n}\n\n/* Dots */\n.kombos-pd__dots {\n position: absolute;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 0;\n}\n\n.kombos-pd__dot {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 2.25rem;\n height: 2.25rem;\n border: none;\n background: transparent;\n cursor: pointer;\n padding: 0;\n -webkit-tap-highlight-color: transparent;\n}\n\n.kombos-pd__dot::after {\n content: \"\";\n display: block;\n width: 0.625rem;\n height: 0.625rem;\n border-radius: 10px;\n border: 1px solid var(--kombos-gray-100);\n background: var(--kombos-white);\n transition: all 0.15s;\n}\n\n.kombos-pd__dot--active::after {\n width: 2rem;\n background: var(--kombos-gray-900);\n border-color: var(--kombos-gray-200);\n}\n\n/* ===== Tablet (>=768px) ===== */\n@media (min-width: 768px) {\n .kombos-pd__gallery {\n flex-direction: row;\n gap: 1.25rem;\n flex: 1;\n min-width: 0;\n }\n\n .kombos-pd__thumbs {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n flex-shrink: 0;\n width: 7rem;\n overflow-y: auto;\n scrollbar-width: none;\n }\n\n .kombos-pd__thumbs::-webkit-scrollbar {\n display: none;\n }\n\n .kombos-pd__thumb {\n width: 7rem;\n flex-shrink: 0;\n border-radius: 12px;\n border: 1px solid var(--kombos-gray-200);\n overflow: hidden;\n cursor: pointer;\n padding: 0.375rem;\n background: var(--kombos-white);\n transition: border-color 0.15s;\n }\n\n .kombos-pd__thumb:hover {\n border-color: var(--kombos-gray-500);\n }\n\n .kombos-pd__thumb--active {\n border-color: var(--kombos-gray-900);\n }\n\n .kombos-pd__thumb-img {\n width: 100%;\n height: 100%;\n display: block;\n border-radius: 8px;\n }\n\n .kombos-pd__main-col {\n align-self: flex-start;\n }\n\n .kombos-pd__main-image-wrap {\n border-radius: 6px;\n overflow: hidden;\n }\n\n .kombos-pd__main-image {\n max-height: 45.75rem;\n border-radius: 6px;\n }\n\n}\n"
17556
- },
17557
- {
17558
- "filename": "ikas-config-snippet.json",
17559
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail\",\n \"name\": \"ProductDetail\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ProductDetail/index.tsx\",\n \"styles\": \"./src/components/ProductDetail/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"aspectRatio\",\n \"displayName\": \"En Boy Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"gorsel-ayarlari\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"objectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"gorsel-ayarlari\",\n \"enumTypeId\": \"GrylMqHxui\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Info Components\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-product-detail-name-favorite\",\n \"{{PROJECT_ID}}-product-detail-sku\",\n \"{{PROJECT_ID}}-product-detail-prices\",\n \"{{PROJECT_ID}}-product-detail-product-group\",\n \"{{PROJECT_ID}}-product-detail-variant\",\n \"{{PROJECT_ID}}-product-detail-add-to-cart\",\n \"{{PROJECT_ID}}-product-detail-features\",\n \"{{PROJECT_ID}}-product-detail-description\",\n \"{{PROJECT_ID}}-product-detail-bundle-product\",\n \"{{PROJECT_ID}}-product-detail-option-set\",\n \"{{PROJECT_ID}}-product-detail-offer\"\n ]\n },\n {\n \"name\": \"bottomComponents\",\n \"displayName\": \"Sub Components\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-product-detail-bundle-furniture\",\n \"{{PROJECT_ID}}-product-detail-offer\"\n ]\n },\n {\n \"name\": \"homepageText\",\n \"displayName\": \"Home Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Home\",\n \"groupId\": \"metinler\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"gorsel-ayarlari\",\n \"name\": \"Image Settings\",\n \"description\": \"Product görselinin en boy oranı ve sığdırma modu\"\n },\n {\n \"id\": \"metinler\",\n \"name\": \"Texts\",\n \"description\": \"Breadcrumb ve sayfa metinleri\"\n }\n ]\n}"
17560
- },
17561
- {
17562
- "filename": "index.tsx",
17563
- "content": "import {\n getIkasCategoryPathItemHref,\n getProductCategoryPath,\n getProductVariantMainImage,\n getSelectedProductVariant,\n IkasImage,\n isNotEmpty,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport Breadcrumb from \"../../sub-components/Breadcrumb\";\nimport type { BreadcrumbItem } from \"../../sub-components/Breadcrumb\";\nimport ProductGallery from \"./components/ProductGallery\";\n\nexport function ProductDetail(props: Props) {\n const {\n product,\n components,\n aspectRatio,\n objectFit,\n bottomComponents,\n homepageText = \"Anasayfa\",\n } = props;\n if (!product) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\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\n const categoryPath = getProductCategoryPath(product);\n\n return (\n <section className=\"kombos-pd\">\n <div className=\"kombos-container kombos-pd__container\">\n <Breadcrumb\n items={[\n { label: homepageText, href: \"/\" } as BreadcrumbItem,\n ...(isNotEmpty(categoryPath)\n ? categoryPath.map(\n (pathItem: any) =>\n ({\n label: pathItem.name,\n href: getIkasCategoryPathItemHref(pathItem),\n }) as BreadcrumbItem,\n )\n : []),\n { label: product.name } as BreadcrumbItem,\n ]}\n size=\"xs\"\n className=\"kombos-pd__breadcrumb\"\n />\n\n <div className=\"kombos-pd__layout\">\n <ProductGallery\n images={images}\n productName={product.name}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n />\n\n <div className=\"kombos-pd__info\">\n <IkasComponentRenderer\n id=\"product-detail-info\"\n components={components}\n parentProps={props}\n />\n </div>\n </div>\n\n {bottomComponents && (\n <div className=\"kombos-pd__bottom\">\n <IkasComponentRenderer\n id=\"product-detail-bottom\"\n components={bottomComponents}\n parentProps={props}\n />\n </div>\n )}\n </div>\n </section>\n );\n}\n\nexport default ProductDetail;\n"
17564
- },
17565
- {
17566
- "filename": "styles.css",
17567
- "content": "/* ===== Product Detail Section ===== */\n\n.kombos-pd {\n width: 100%;\n}\n\n/* ---- Breadcrumb (mobile only, hidden on desktop) ---- */\n.kombos-pd__breadcrumb {\n padding-top: 1rem;\n}\n\n/* ---- Layout ---- */\n.kombos-pd__layout {\n display: flex;\n flex-direction: column;\n padding-top: 1rem;\n}\n\n/* ---- Product Info ---- */\n.kombos-pd__info {\n display: flex;\n flex-direction: column;\n padding-top: 1rem;\n padding-bottom: 1rem;\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd__container {\n padding-top: 2rem;\n padding-bottom: 2rem;\n }\n\n .kombos-pd__breadcrumb {\n padding-top: 0;\n padding-bottom: 1.5rem;\n }\n\n .kombos-pd__layout {\n display: grid;\n grid-template-columns: 7fr 5fr;\n gap: 2rem;\n padding-top: 0;\n }\n\n /* Gallery — sticky on desktop */\n .kombos-pd__gallery {\n position: sticky;\n top: 0.75rem;\n align-self: start;\n }\n\n /* Info — right side */\n .kombos-pd__info {\n min-width: 0;\n padding: 0;\n }\n}\n"
17568
- },
17569
- {
17570
- "filename": "types.ts",
17571
- "content": "import type { IkasProduct } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../global-types\";\n\nexport interface Props {\n product?: IkasProduct | null;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n components?: any;\n bottomComponents?: any;\n homepageText?: string;\n}\n"
17572
- }
17573
- ]
17574
- },
17575
- {
17576
- "id": "login-section",
17577
- "title": "Login Section",
17578
- "description": "Customer login form with email/password fields, form validation, error handling, social login support, and redirect to account on success. Uses customerStore.login() and useRedirectIfLoggedIn hook.",
17579
- "code": "import {\n customerStore,\n initLoginForm,\n Router,\n getLoginForm,\n handleSocialLogin,\n} from \"@ikas/bp-storefront\";\nimport { useRedirectIfLoggedIn } from \"../../hooks/useRedirectIfLoggedIn\";\n\nimport { Props } from \"./types\";\nimport LoginForm from \"./components/LoginForm\";\nimport PageLoader from \"../../sub-components/PageLoader\";\n\nexport function Login(props: Props) {\n const loginForm = getLoginForm(customerStore);\n\n const isChecking = useRedirectIfLoggedIn(() => {\n initLoginForm(loginForm);\n handleSocialLogin(customerStore).then((result) => {\n if (result.status === \"success\") {\n Router.navigateToPage(\"ACCOUNT\");\n }\n });\n });\n\n if (isChecking) return <PageLoader />;\n\n return (\n <section className=\"login\">\n <div className=\"login__wrapper kombos-container\">\n <LoginForm loginForm={loginForm} {...props} />\n </div>\n </section>\n );\n}\n\nexport default Login;\n",
17580
- "relatedFunctions": [
17581
- "customerStore",
17582
- "initLoginForm",
17583
- "Router",
17584
- "getLoginForm",
17585
- "handleSocialLogin"
17586
- ],
17587
- "categories": [
17588
- "Customer",
17589
- "Auth",
17590
- "Form"
17591
- ],
17592
- "files": [
17593
- {
17594
- "filename": "components/LoginForm/index.tsx",
17595
- "content": "import {\n customerStore,\n setLoginFormEmail,\n setLoginFormPassword,\n submitLoginForm,\n socialLogin,\n Router,\n getLoginForm,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\n\nimport { Props } from \"../../types\";\nimport Button from \"../../../../sub-components/Button\";\nimport Input from \"../../../../sub-components/Input\";\nimport FormItem from \"../../../../sub-components/FormItem\";\nimport SocialLoginButton from \"../../../../sub-components/SocialLoginButton\";\nimport { GoogleSVG, FacebookSVG } from \"../../../../sub-components/icons\";\n\ninterface LoginFormProps extends Props {\n loginForm: ReturnType<typeof getLoginForm>;\n}\n\nconst LoginForm = observer(function LoginForm({\n loginForm,\n title = \"Giriş Yap\",\n subtitle = \"Hesabınıza giriş yaparak siparişlerinizi\\ntakip edebilir ve alışverişe devam edebilirsiniz.\",\n emailLabel = \"E-posta\",\n emailPlaceholder = \"ornek@email.com\",\n passwordLabel = \"Şifre\",\n passwordPlaceholder = \"Şifrenizi girin\",\n forgotPasswordText = \"Parolamı unuttum\",\n submitButtonText = \"Giriş Yap\",\n submittingButtonText = \"Giriş yapılıyor...\",\n createAccountText = \"Yeni hesap oluştur\",\n showGoogleLogin = false,\n showFacebookLogin = false,\n googleButtonText = \"Google ile giriş yap\",\n facebookButtonText = \"Facebook ile giriş yap\",\n dividerText = \"veya\",\n}: LoginFormProps) {\n const showSocialLogin = showGoogleLogin || showFacebookLogin;\n\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitLoginForm(loginForm);\n if (success) {\n Router.navigateToPage(\"ACCOUNT\");\n }\n };\n\n return (\n <div className=\"login__container\">\n <div className=\"login__header\">\n <h1 className=\"login__title text-xl-medium md:display-xs-medium\">\n {title}\n </h1>\n <div\n className=\"login__subtitle text-sm-regular\"\n dangerouslySetInnerHTML={{ __html: subtitle }}\n />\n </div>\n\n {loginForm.isFailure && loginForm.responseMessage && (\n <div className=\"login__error-banner text-sm-regular\">\n {loginForm.responseMessage}\n </div>\n )}\n\n {showSocialLogin && (\n <div className=\"login__social\">\n {showGoogleLogin && (\n <SocialLoginButton\n icon={<GoogleSVG />}\n onClick={() => socialLogin(customerStore, \"google\")}\n >\n {googleButtonText}\n </SocialLoginButton>\n )}\n {showFacebookLogin && (\n <SocialLoginButton\n icon={<FacebookSVG />}\n onClick={() => socialLogin(customerStore, \"facebook\")}\n >\n {facebookButtonText}\n </SocialLoginButton>\n )}\n </div>\n )}\n\n {showSocialLogin && (\n <div className=\"login__divider\">\n <span className=\"login__divider-line\" />\n <span className=\"login__divider-text text-xs-regular\">\n {dividerText}\n </span>\n <span className=\"login__divider-line\" />\n </div>\n )}\n\n <form className=\"login__form\" onSubmit={handleSubmit}>\n <FormItem\n label={emailLabel}\n htmlFor=\"login-email\"\n status={loginForm.email?.hasError ? \"error\" : \"default\"}\n helper={\n loginForm.email?.hasError ? loginForm.email.message : undefined\n }\n >\n <Input\n id=\"login-email\"\n type=\"email\"\n name=\"email\"\n autoComplete=\"email\"\n placeholder={emailPlaceholder}\n value={loginForm.email?.value ?? \"\"}\n onInput={(e: Event) =>\n setLoginFormEmail(loginForm, (e.target as HTMLInputElement).value)\n }\n />\n </FormItem>\n\n <FormItem\n label={passwordLabel}\n htmlFor=\"login-password\"\n status={loginForm.password?.hasError ? \"error\" : \"default\"}\n helper={\n loginForm.password?.hasError\n ? loginForm.password.message\n : undefined\n }\n >\n <Input\n id=\"login-password\"\n password\n name=\"current-password\"\n autoComplete=\"current-password\"\n placeholder={passwordPlaceholder}\n value={loginForm.password?.value ?? \"\"}\n onInput={(e: Event) =>\n setLoginFormPassword(\n loginForm,\n (e.target as HTMLInputElement).value,\n )\n }\n />\n </FormItem>\n\n <div className=\"login__options\">\n <button\n type=\"button\"\n className=\"login__forgot text-sm-regular\"\n onClick={() => Router.navigateToPage(\"FORGOT_PASSWORD\")}\n >\n {forgotPasswordText}\n </button>\n </div>\n\n <Button\n variant=\"primary\"\n size=\"s\"\n className=\"login__submit-btn\"\n disabled={loginForm.isSubmitting}\n >\n {loginForm.isSubmitting ? submittingButtonText : submitButtonText}\n </Button>\n </form>\n\n <div className=\"login__footer\">\n <button\n type=\"button\"\n className=\"login__create-account text-sm-regular\"\n onClick={() => Router.navigateToPage(\"REGISTER\")}\n >\n {createAccountText}\n </button>\n </div>\n </div>\n );\n});\n\nexport default LoginForm;\n"
17596
- },
17597
- {
17598
- "filename": "components/LoginForm/styles.css",
17599
- "content": ""
17600
- },
17601
- {
17602
- "filename": "ikas-config-snippet.json",
17603
- "content": "{\n \"id\": \"{{PROJECT_ID}}-login\",\n \"name\": \"Login\",\n \"type\": \"section\",\n \"entry\": \"./src/components/Login/index.tsx\",\n \"styles\": \"./src/components/Login/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sign In\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"subtitle\",\n \"displayName\": \"Sub Title\",\n \"type\": \"RICH_TEXT\",\n \"required\": false,\n \"defaultValue\": \"<p>Hesabınıza giriş yaparak siparişlerinizi takip edebilir ve alışverişe devam edebilirsiniz.</p>\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"emailLabel\",\n \"displayName\": \"Email Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Email\",\n \"groupId\": \"email\"\n },\n {\n \"name\": \"emailPlaceholder\",\n \"displayName\": \"Email Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"ornek@email.com\",\n \"groupId\": \"email\"\n },\n {\n \"name\": \"passwordLabel\",\n \"displayName\": \"Password Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Password\",\n \"groupId\": \"password\"\n },\n {\n \"name\": \"passwordPlaceholder\",\n \"displayName\": \"Password Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Şifrenizi girin\",\n \"groupId\": \"password\"\n },\n {\n \"name\": \"submitButtonText\",\n \"displayName\": \"Login Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sign In\",\n \"groupId\": \"buttons\"\n },\n {\n \"name\": \"submittingButtonText\",\n \"displayName\": \"Login Yapılıyor Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Login yapılıyor...\",\n \"groupId\": \"buttons\"\n },\n {\n \"name\": \"forgotPasswordText\",\n \"displayName\": \"Password Unuttum Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Password unuttum\",\n \"groupId\": \"buttons\"\n },\n {\n \"name\": \"createAccountText\",\n \"displayName\": \"Account Oluştur Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Yeni hesap oluştur\",\n \"groupId\": \"buttons\"\n },\n {\n \"name\": \"showGoogleLogin\",\n \"displayName\": \"Sign in with Google\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"google\"\n },\n {\n \"name\": \"googleButtonText\",\n \"displayName\": \"Google Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Google ile giriş yap\",\n \"groupId\": \"google\"\n },\n {\n \"name\": \"showFacebookLogin\",\n \"displayName\": \"Sign in with Facebook\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"facebook\"\n },\n {\n \"name\": \"facebookButtonText\",\n \"displayName\": \"Facebook Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Facebook ile giriş yap\",\n \"groupId\": \"facebook\"\n },\n {\n \"name\": \"dividerText\",\n \"displayName\": \"Separator Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"veya\",\n \"groupId\": \"socialLogin\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"content\",\n \"name\": \"Content\",\n \"description\": \"Page başlığı ve açıklama\"\n },\n {\n \"id\": \"formFields\",\n \"name\": \"Form Alanları\",\n \"description\": \"Login formu etiket ve placeholder metinleri\",\n \"children\": [\n {\n \"id\": \"email\",\n \"name\": \"Email\"\n },\n {\n \"id\": \"password\",\n \"name\": \"Password\"\n }\n ]\n },\n {\n \"id\": \"buttons\",\n \"name\": \"Buttons\",\n \"description\": \"Button ve link metinleri\"\n },\n {\n \"id\": \"socialLogin\",\n \"name\": \"Social Login\",\n \"description\": \"Google ve Facebook ile giriş ayarları\",\n \"children\": [\n {\n \"id\": \"google\",\n \"name\": \"Google\"\n },\n {\n \"id\": \"facebook\",\n \"name\": \"Facebook\"\n }\n ]\n }\n ]\n}"
17604
- },
17605
- {
17606
- "filename": "index.tsx",
17607
- "content": "import {\n customerStore,\n initLoginForm,\n Router,\n getLoginForm,\n handleSocialLogin,\n} from \"@ikas/bp-storefront\";\nimport { useRedirectIfLoggedIn } from \"../../hooks/useRedirectIfLoggedIn\";\n\nimport { Props } from \"./types\";\nimport LoginForm from \"./components/LoginForm\";\nimport PageLoader from \"../../sub-components/PageLoader\";\n\nexport function Login(props: Props) {\n const loginForm = getLoginForm(customerStore);\n\n const isChecking = useRedirectIfLoggedIn(() => {\n initLoginForm(loginForm);\n handleSocialLogin(customerStore).then((result) => {\n if (result.status === \"success\") {\n Router.navigateToPage(\"ACCOUNT\");\n }\n });\n });\n\n if (isChecking) return <PageLoader />;\n\n return (\n <section className=\"login\">\n <div className=\"login__wrapper kombos-container\">\n <LoginForm loginForm={loginForm} {...props} />\n </div>\n </section>\n );\n}\n\nexport default Login;\n"
17608
- },
17609
- {
17610
- "filename": "styles.css",
17611
- "content": ".login {\n width: 100%;\n}\n\n.login__wrapper {\n display: flex;\n justify-content: center;\n align-items: flex-start;\n padding-top: 1rem;\n padding-bottom: 1rem;\n}\n\n.login__container {\n width: 100%;\n max-width: 29rem;\n display: flex;\n flex-direction: column;\n gap: 2rem;\n}\n\n.login__header {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n text-align: center;\n}\n\n.login__title {\n color: var(--kombos-gray-900);\n margin: 0;\n}\n\n.login__subtitle {\n color: var(--kombos-gray-700);\n margin: 0;\n white-space: pre-line;\n}\n\n.login__error-banner {\n padding: 0.75rem 1rem;\n background: rgba(255, 60, 72, 0.08);\n border: 1px solid var(--kombos-error);\n border-radius: 6px;\n color: var(--kombos-error);\n}\n\n/* Social Login */\n.login__social {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n/* Divider */\n.login__divider {\n display: flex;\n align-items: center;\n gap: 1rem;\n}\n\n.login__divider-line {\n flex: 1;\n height: 1px;\n background: var(--kombos-gray-200);\n}\n\n.login__divider-text {\n color: var(--kombos-gray-500);\n white-space: nowrap;\n}\n\n.login__form {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.login__options {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n}\n\n.login__forgot {\n background: none;\n border: none;\n padding: 0;\n color: var(--kombos-gray-900);\n cursor: pointer;\n text-decoration: underline;\n}\n\n.login__forgot:hover {\n text-decoration: underline;\n}\n\n.login__submit-btn {\n width: 100%;\n}\n\n.login__footer {\n text-align: center;\n}\n\n.login__create-account {\n background: none;\n border: none;\n padding: 0;\n color: var(--kombos-gray-900);\n cursor: pointer;\n}\n\n.login__create-account:hover {\n text-decoration: underline;\n}\n\n@media (min-width: 768px) {\n .login__wrapper {\n padding-top: 1.5rem;\n padding-bottom: 1.5rem;\n }\n}\n\n@media (min-width: 1024px) {\n .login__wrapper {\n padding-top: 2rem;\n padding-bottom: 2rem;\n }\n}\n"
17612
- },
17613
- {
17614
- "filename": "types.ts",
17615
- "content": "export interface Props {\n title?: string;\n subtitle?: string;\n emailLabel?: string;\n emailPlaceholder?: string;\n passwordLabel?: string;\n passwordPlaceholder?: string;\n submitButtonText?: string;\n submittingButtonText?: string;\n forgotPasswordText?: string;\n createAccountText?: string;\n showGoogleLogin?: boolean;\n googleButtonText?: string;\n showFacebookLogin?: boolean;\n facebookButtonText?: string;\n dividerText?: string;\n}\n"
17616
- }
17617
- ]
17618
- },
17619
- {
17620
- "id": "navigation",
17621
- "title": "Navigation Pattern",
17622
- "description": "Store navigation with Router API, customer state checks, cart state, page navigation, and responsive mobile menu. Shows Router.navigate, Router.navigateToPage, customerStore, cartStore, hasCustomer, and navigation link rendering.",
17623
- "code": "import { useState, useEffect } from \"preact/hooks\";\nimport {\n cartStore,\n customerStore,\n hasCustomer,\n getIkasOrderTotalItemCount,\n getDefaultSrc,\n getFormattedHeightSize,\n Router,\n createMediaSrcset,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport CartSidebar from \"./components/CartSidebar\";\nimport MobileMenu from \"./components/MobileMenu\";\nimport SearchModal from \"./components/SearchModal\";\nimport NavItem from \"./components/NavItem\";\nimport {\n List1SVG,\n MagnifyingGlass1SVG,\n User1SVG,\n ShoppingBag1SVG,\n} from \"../../sub-components/icons\";\nexport function Navbar(props: Props) {\n const {\n logo,\n navigationLinks,\n logoSizeDesktop: rawLogoSizeDesktop,\n logoSizeMobile: rawLogoSizeMobile,\n cartTitle = \"Alışveriş Sepetim\",\n emptyCartText = \"Sepetinizde Ürün Bulunmamaktadır\",\n checkoutButtonText = \"Ödemeye Geç\",\n totalText = \"Toplam\",\n navigationLinkColor,\n coloredLinks,\n coloredLinkColor,\n registerButtonText = \"Üye Ol\",\n loginButtonText = \"Giriş Yap\",\n logoutButtonText = \"Çıkış Yap\",\n freeShippingText = \"150 TL ve üzeri siparişlerde kargo bedava\",\n emptyCartButtonText = \"Alışverişe Başla\",\n searchPlaceholder = \"Ne Aramıştınız?\",\n searchingText = \"Aranıyor...\",\n noResultsText = \"Aradığınız ürün bulunamadı\",\n resultCountText = \"Sonuç\",\n addToCartText = \"Sepete Ekle\",\n addedToCartText = \"Sepete Eklendi\",\n outOfStockText = \"Tükendi\",\n goToProductText = \"Ürüne Git\",\n viewAllText = \"Tümünü Gör\",\n viewCartButtonText = \"Sepeti Görüntüle\",\n searchProductList,\n hideAddToCartButton,\n imageAspectRatio,\n imageObjectFit,\n components,\n } = props;\n\n const logoSizeDesktop = getFormattedHeightSize(rawLogoSizeDesktop) || \"60px\";\n const logoSizeMobile = getFormattedHeightSize(rawLogoSizeMobile) || \"48px\";\n\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [cartOpen, setCartOpen] = useState(false);\n const [searchOpen, setSearchOpen] = useState(false);\n\n useEffect(() => {\n const openCart = () => setCartOpen(true);\n window.addEventListener(\"ikas:open-cart-sidebar\", openCart);\n return () => window.removeEventListener(\"ikas:open-cart-sidebar\", openCart);\n }, []);\n\n const cart = cartStore.cart;\n const itemCount = cart ? getIkasOrderTotalItemCount(cart) : 0;\n const isLoggedIn = hasCustomer(customerStore);\n\n const links = navigationLinks?.links ?? [];\n const coloredLinksList = coloredLinks?.links ?? [];\n\n return (\n <div\n className=\"kombos-navbar\"\n style={{\n \"--logo-h-desktop\": logoSizeDesktop,\n \"--logo-h-mobile\": logoSizeMobile,\n }}\n >\n <div className=\"kombos-navbar__inner kombos-container\">\n {/* Hamburger - mobile only */}\n <button\n className=\"kombos-navbar__hamburger\"\n onClick={() => setMobileMenuOpen(true)}\n aria-label=\"Menu\"\n >\n <List1SVG />\n </button>\n\n {/* Logo */}\n {logo && (\n <a\n className=\"kombos-navbar__logo\"\n href=\"/\"\n onClick={(e) => {\n e.preventDefault();\n Router.navigate(\"/\");\n }}\n style={{\n \"--logo-h-desktop\": logoSizeDesktop,\n \"--logo-h-mobile\": logoSizeMobile,\n }}\n >\n <img\n src={getDefaultSrc(logo)}\n srcSet={createMediaSrcset(logo)}\n sizes=\"200px\"\n alt={logo?.altText || \"Logo\"}\n className=\"kombos-navbar__logo-img\"\n width={200}\n height={60}\n fetchpriority=\"high\"\n loading=\"eager\"\n />\n </a>\n )}\n\n {/* Desktop Navigation */}\n <nav\n className={`kombos-navbar__nav${!logo ? \" kombos-navbar__nav--no-logo\" : \"\"}`}\n >\n {links.map((link, i) => (\n <NavItem key={i} link={link} linkColor={navigationLinkColor} />\n ))}\n {coloredLinksList.map((link, i) => (\n <NavItem key={`cl-${i}`} link={link} linkColor={coloredLinkColor} />\n ))}\n </nav>\n\n {/* Action Icons */}\n <div className=\"kombos-navbar__actions\">\n {/* Search */}\n <a\n className=\"kombos-navbar__icon-btn\"\n href=\"/search\"\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n if (searchProductList) {\n setSearchOpen(true);\n } else {\n Router.navigateToPage(\"SEARCH\");\n }\n }}\n aria-label=\"Search\"\n >\n <MagnifyingGlass1SVG />\n </a>\n\n {/* Account */}\n <a\n className=\"kombos-navbar__icon-btn\"\n href={isLoggedIn ? \"/account\" : \"/login\"}\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n Router.navigateToPage(isLoggedIn ? \"ACCOUNT\" : \"LOGIN\");\n }}\n aria-label=\"Account\"\n >\n <User1SVG />\n </a>\n\n {/* Cart */}\n <a\n className=\"kombos-navbar__icon-btn kombos-navbar__cart-trigger\"\n href=\"/cart\"\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n setCartOpen(true);\n }}\n aria-label=\"Cart\"\n >\n <ShoppingBag1SVG />\n {itemCount > 0 && (\n <span className=\"kombos-navbar__badge\">{itemCount}</span>\n )}\n </a>\n </div>\n </div>\n\n {/* Cart Sidebar */}\n {cartOpen && (\n <CartSidebar\n cartTitle={cartTitle}\n emptyCartText={emptyCartText}\n checkoutButtonText={checkoutButtonText}\n viewCartButtonText={viewCartButtonText}\n totalText={totalText}\n freeShippingText={freeShippingText}\n emptyCartButtonText={emptyCartButtonText}\n imageAspectRatio={imageAspectRatio}\n imageObjectFit={imageObjectFit}\n onClose={() => setCartOpen(false)}\n />\n )}\n\n {/* Search Modal */}\n {searchOpen && searchProductList && (\n <SearchModal\n productList={searchProductList}\n logo={logo ?? undefined}\n logoSizeDesktop={logoSizeDesktop}\n logoSizeMobile={logoSizeMobile}\n searchPlaceholder={searchPlaceholder}\n searchingText={searchingText}\n noResultsText={noResultsText}\n resultCountText={resultCountText}\n addToCartText={addToCartText}\n addedToCartText={addedToCartText}\n outOfStockText={outOfStockText}\n goToProductText={goToProductText}\n viewAllText={viewAllText}\n hideAddToCartButton={hideAddToCartButton}\n aspectRatio={imageAspectRatio}\n objectFit={imageObjectFit}\n onClose={() => setSearchOpen(false)}\n components={components}\n parentProps={props}\n />\n )}\n\n {/* Mobile Menu */}\n {mobileMenuOpen && (\n <MobileMenu\n linkGroups={[\n { links, color: navigationLinkColor },\n ...(coloredLinksList.length > 0\n ? [{ links: coloredLinksList, color: coloredLinkColor }]\n : []),\n ]}\n registerButtonText={registerButtonText}\n loginButtonText={loginButtonText}\n logoutButtonText={logoutButtonText}\n onClose={() => setMobileMenuOpen(false)}\n onCartOpen={() => setCartOpen(true)}\n />\n )}\n </div>\n );\n}\n\nexport default Navbar;\n",
17624
- "relatedFunctions": [
17625
- "cartStore",
17626
- "customerStore",
17627
- "hasCustomer",
17628
- "getIkasOrderTotalItemCount",
17629
- "getDefaultSrc",
17630
- "getFormattedHeightSize",
17631
- "Router",
17632
- "createMediaSrcset"
17633
- ],
17634
- "categories": [
17635
- "Navigation"
17636
- ],
17637
- "files": [
17638
- {
17639
- "filename": "components/CartSidebar/index.tsx",
17640
- "content": "import { useEffect, useState, useCallback } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport { useScrollLock } from \"../../../../hooks/useScrollLock\";\nimport { cx } from \"../../../../utils/cx\";\nimport {\n XSVG,\n Package1SVG,\n Handbag1SVG,\n} from \"../../../../sub-components/icons\";\nimport Button from \"../../../../sub-components/Button\";\nimport {\n cartStore,\n hasCart,\n getIkasOrderTotalItemCount,\n getIkasOrderFormattedTotalFinalPrice,\n getOrderAdjustmentDisplayName,\n getOrderAdjustmentFormattedAmount,\n removeItem,\n getCheckoutUrlFromCartStore,\n IkasOrderLineItem,\n Router,\n} from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../../../global-types\";\nimport CartItem from \"../../../../sub-components/CartItem\";\n\ninterface Props {\n cartTitle: string;\n emptyCartText: string;\n checkoutButtonText: string;\n viewCartButtonText: string;\n totalText: string;\n freeShippingText: string;\n emptyCartButtonText: string;\n imageAspectRatio?: AspectRatio;\n imageObjectFit?: ObjectFit;\n onClose: () => void;\n}\n\nconst CartSidebar = observer(function CartSidebar({\n cartTitle,\n emptyCartText,\n checkoutButtonText,\n viewCartButtonText,\n totalText,\n freeShippingText,\n emptyCartButtonText,\n imageAspectRatio,\n imageObjectFit,\n onClose,\n}: Props) {\n const [open, setOpen] = useState(false);\n\n useScrollLock();\n\n useEffect(() => {\n requestAnimationFrame(() => setOpen(true));\n }, []);\n\n const handleClose = useCallback(() => {\n setOpen(false);\n setTimeout(onClose, 300);\n }, [onClose]);\n\n const cart = cartStore.cart;\n const isCartLoading = cartStore.isCartLoading;\n const cartHasItems = hasCart(cartStore);\n const itemCount = cart ? getIkasOrderTotalItemCount(cart) : 0;\n const lineItems = cart?.orderLineItems ?? [];\n const adjustments = cart?.orderAdjustments ?? [];\n\n const handleRemove = async (item: IkasOrderLineItem) => {\n await removeItem(item);\n };\n\n return (\n <div\n className={cx(\"kombos-cart-overlay\", open && \"kombos-cart-overlay--open\")}\n >\n <div className=\"kombos-cart-backdrop\" onClick={handleClose} />\n <aside className=\"kombos-cart-sidebar\">\n {/* Header */}\n <div className=\"kombos-cart-sidebar__head\">\n <h3 className=\"kombos-cart-sidebar__title text-md-medium\">\n {cartTitle} ({itemCount})\n </h3>\n <button\n className=\"kombos-cart-sidebar__close\"\n onClick={handleClose}\n aria-label=\"Close\"\n >\n <XSVG />\n </button>\n </div>\n\n {/* Free Shipping Banner */}\n {freeShippingText && (\n <div className=\"kombos-cart-sidebar__banner\">\n <Package1SVG />\n <span className=\"text-xs-semibold\">{freeShippingText}</span>\n </div>\n )}\n\n {/* Loading */}\n {isCartLoading && (\n <div className=\"kombos-cart-sidebar__loading text-sm-regular\" />\n )}\n\n {/* Empty State */}\n {!cartHasItems && !isCartLoading && (\n <div className=\"kombos-cart-sidebar__empty\">\n <Handbag1SVG />\n <p className=\"kombos-cart-sidebar__empty-text text-md-medium\">\n {emptyCartText}\n </p>\n <Button\n variant=\"primary\"\n size=\"s\"\n className=\"kombos-cart-sidebar__empty-btn\"\n onClick={handleClose}\n >\n {emptyCartButtonText}\n </Button>\n </div>\n )}\n\n {/* Cart Items */}\n {cartHasItems && (\n <div className=\"kombos-cart-sidebar__items\">\n {lineItems.map((item) => (\n <CartItem\n key={item.id}\n item={item}\n onRemove={handleRemove}\n aspectRatio={imageAspectRatio}\n objectFit={imageObjectFit}\n />\n ))}\n </div>\n )}\n\n {/* Adjustments */}\n {adjustments.length > 0 && (\n <div className=\"kombos-cart-sidebar__adjustments\">\n {adjustments.map((adj: any, i: number) => (\n <div\n key={i}\n className=\"kombos-cart-sidebar__adj-row text-sm-regular\"\n >\n <span>{getOrderAdjustmentDisplayName(adj)}</span>\n <span className=\"kombos-cart-sidebar__adj-amount text-sm-medium\">\n {getOrderAdjustmentFormattedAmount(adj)}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Footer */}\n {cartHasItems && cart && (\n <div className=\"kombos-cart-sidebar__footer\">\n <div className=\"kombos-cart-sidebar__total text-md-semibold\">\n <span>{totalText}</span>\n <span>{getIkasOrderFormattedTotalFinalPrice(cart)}</span>\n </div>\n <div className=\"kombos-cart-sidebar__footer-buttons\">\n <a\n href=\"/cart\"\n className=\"kombos-cart-sidebar__view-cart-link\"\n onClick={(e: Event) => {\n e.preventDefault();\n handleClose();\n Router.navigateToPage(\"CART\");\n }}\n >\n <Button\n variant=\"secondary\"\n size=\"s\"\n className=\"kombos-cart-sidebar__view-cart-btn\"\n >\n {viewCartButtonText}\n </Button>\n </a>\n <a\n href={getCheckoutUrlFromCartStore(cartStore)}\n className=\"kombos-cart-sidebar__checkout\"\n >\n <Button\n variant=\"primary\"\n size=\"s\"\n className=\"kombos-cart-sidebar__checkout-btn\"\n >\n {checkoutButtonText}\n </Button>\n </a>\n </div>\n </div>\n )}\n </aside>\n </div>\n );\n});\n\nexport default CartSidebar;\n"
17641
- },
17642
- {
17643
- "filename": "components/CartSidebar/styles.css",
17644
- "content": "/* ===== Cart Sidebar ===== */\n.kombos-cart-overlay {\n position: fixed;\n inset: 0;\n z-index: var(--kombos-z-overlay);\n}\n\n.kombos-cart-backdrop {\n position: absolute;\n inset: 0;\n background: rgba(0, 0, 0, 0);\n transition: background 0.3s ease;\n}\n\n.kombos-cart-overlay--open .kombos-cart-backdrop {\n background: rgba(0, 0, 0, 0.35);\n}\n\n.kombos-cart-sidebar {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 100%;\n max-width: 30.5rem;\n background: var(--kombos-white);\n display: flex;\n flex-direction: column;\n box-shadow: -4px 0 20px rgba(0, 0, 0, 0.08);\n transform: translateX(100%);\n transition: transform 0.3s ease;\n}\n\n.kombos-cart-overlay--open .kombos-cart-sidebar {\n transform: translateX(0);\n}\n\n/* Header */\n.kombos-cart-sidebar__head {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.25rem 1.5rem;\n border-bottom: 1px solid var(--kombos-gray-200);\n flex-shrink: 0;\n}\n\n.kombos-cart-sidebar__title {\n color: var(--kombos-gray-900);\n margin: 0;\n}\n\n.kombos-cart-sidebar__close {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--kombos-gray-700);\n padding: 0;\n display: flex;\n font-size: 1.25rem;\n}\n\n/* Free Shipping Banner */\n.kombos-cart-sidebar__banner {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 0.5rem;\n margin: 1.5rem 1.5rem 0;\n padding: 0.5rem 1.5rem;\n background: var(--kombos-success-bg);\n border-radius: 6px;\n color: var(--kombos-gray-900);\n flex-shrink: 0;\n}\n\n/* Loading */\n.kombos-cart-sidebar__loading {\n padding: 2rem 1.5rem;\n text-align: center;\n color: var(--kombos-gray-500);\n}\n\n/* Empty State */\n.kombos-cart-sidebar__empty {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1.5rem;\n padding: 3rem 1.5rem;\n color: var(--kombos-gray-500);\n font-size: 3rem;\n}\n\n.kombos-cart-sidebar__empty-text {\n margin: 0;\n color: var(--kombos-gray-900);\n}\n\n.kombos-cart-sidebar__empty-btn {\n min-width: 10rem;\n}\n\n/* Cart Items */\n.kombos-cart-sidebar__items {\n flex: 1;\n overflow-y: auto;\n padding: 0 1.5rem;\n}\n\n/* Adjustments */\n.kombos-cart-sidebar__adjustments {\n padding: 0.75rem 1.5rem;\n border-top: 1px solid var(--kombos-gray-200);\n flex-shrink: 0;\n}\n\n.kombos-cart-sidebar__adj-row {\n display: flex;\n justify-content: space-between;\n color: var(--kombos-gray-900);\n padding: 0.25rem 0;\n}\n\n.kombos-cart-sidebar__adj-amount {\n color: var(--kombos-error);\n}\n\n/* Footer */\n.kombos-cart-sidebar__footer {\n padding: 1.25rem 1.5rem;\n border-top: 1px solid var(--kombos-gray-200);\n flex-shrink: 0;\n}\n\n.kombos-cart-sidebar__total {\n display: flex;\n justify-content: space-between;\n color: var(--kombos-gray-900);\n margin-bottom: 1rem;\n}\n\n.kombos-cart-sidebar__footer-buttons {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.kombos-cart-sidebar__view-cart-link {\n display: block;\n text-decoration: none;\n flex: 1;\n}\n\n.kombos-cart-sidebar__view-cart-btn {\n width: 100%;\n}\n\n.kombos-cart-sidebar__checkout {\n display: block;\n text-decoration: none;\n flex: 1;\n}\n\n.kombos-cart-sidebar__checkout-btn {\n display: flex;\n width: 100%;\n}\n\n@media (min-width: 768px) {\n .kombos-cart-sidebar__footer-buttons {\n flex-direction: row;\n }\n}"
17645
- },
17646
- {
17647
- "filename": "components/MobileMenu/index.tsx",
17648
- "content": "import { useEffect, useState, useCallback, useRef } from \"preact/hooks\";\nimport { useScrollLock } from \"../../../../hooks/useScrollLock\";\nimport {\n IkasNavigationLink,\n customerStore,\n hasCustomer,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport { cx } from \"../../../../utils/cx\";\nimport { XSVG, ShoppingBag1SVG, CaretRightSVG, CaretLeftSVG } from \"../../../../sub-components/icons\";\nimport Button from \"../../../../sub-components/Button\";\n\ninterface LinkGroup {\n links: IkasNavigationLink[];\n color?: string;\n}\n\ninterface Props {\n linkGroups: LinkGroup[];\n registerButtonText: string;\n loginButtonText: string;\n logoutButtonText: string;\n onClose: () => void;\n onCartOpen: () => void;\n}\n\ninterface DrillDown {\n parentLabel: string;\n subLinks: IkasNavigationLink[];\n}\n\nconst MobileMenu = observer(function MobileMenu({\n linkGroups,\n registerButtonText,\n loginButtonText,\n logoutButtonText,\n onClose,\n onCartOpen,\n}: Props) {\n const [open, setOpen] = useState(false);\n const [drillDown, setDrillDown] = useState<DrillDown | null>(null);\n const isLoggedIn = hasCustomer(customerStore);\n const skipCleanupRef = useRef(false);\n\n useScrollLock(true, skipCleanupRef);\n\n useEffect(() => {\n requestAnimationFrame(() => setOpen(true));\n }, []);\n\n const handleClose = useCallback(() => {\n setOpen(false);\n setTimeout(onClose, 300);\n }, [onClose]);\n\n const handleLinkClick = useCallback(\n (link: IkasNavigationLink) => {\n if (link.href) {\n handleClose();\n }\n },\n [handleClose],\n );\n\n const handleCartClick = useCallback(() => {\n skipCleanupRef.current = true;\n handleClose();\n onCartOpen();\n }, [handleClose, onCartOpen]);\n\n return (\n <div\n className={cx(\"kombos-mobile-overlay\", open && \"kombos-mobile-overlay--open\")}\n >\n <div className=\"kombos-mobile-backdrop\" onClick={handleClose} />\n <div className=\"kombos-mobile-menu\">\n {/* Header */}\n <div className=\"kombos-mobile-menu__head\">\n <button\n className=\"kombos-mobile-menu__icon-btn\"\n onClick={handleClose}\n aria-label=\"Close\"\n >\n <XSVG />\n </button>\n <button\n className=\"kombos-mobile-menu__icon-btn\"\n onClick={handleCartClick}\n aria-label=\"Cart\"\n >\n <ShoppingBag1SVG />\n </button>\n </div>\n\n {/* Content */}\n <div className=\"kombos-mobile-menu__body\">\n {drillDown ? (\n <>\n {/* Back button */}\n <button\n className=\"kombos-mobile-menu__back\"\n onClick={() => setDrillDown(null)}\n >\n <CaretLeftSVG />\n <span className=\"text-xs-medium\">{drillDown.parentLabel}</span>\n </button>\n {/* Sub-links */}\n <nav className=\"kombos-mobile-menu__nav\">\n {drillDown.subLinks.map(\n (sub: IkasNavigationLink, j: number) => (\n <a\n key={j}\n href={sub.href}\n className=\"kombos-mobile-menu__link text-sm-medium\"\n onClick={() => handleLinkClick(sub)}\n >\n {sub.label}\n </a>\n ),\n )}\n </nav>\n </>\n ) : (\n <nav className=\"kombos-mobile-menu__nav\">\n {linkGroups.map((group, gi) =>\n group.links.map((link: IkasNavigationLink, i: number) => {\n const hasSubLinks = link.subLinks && link.subLinks.length > 0;\n return (\n <div\n key={`${gi}-${i}`}\n className=\"kombos-mobile-menu__link-row\"\n >\n <a\n href={link.href}\n className=\"kombos-mobile-menu__link text-sm-medium\"\n style={group.color ? { color: group.color } : undefined}\n onClick={() => handleLinkClick(link)}\n >\n {link.label}\n </a>\n {hasSubLinks && (\n <button\n className=\"kombos-mobile-menu__drill-btn\"\n onClick={() =>\n setDrillDown({\n parentLabel: link.label,\n subLinks: link.subLinks!,\n })\n }\n aria-label={`${link.label} alt kategorileri`}\n >\n <CaretRightSVG />\n </button>\n )}\n </div>\n );\n }),\n )}\n </nav>\n )}\n </div>\n\n {/* Footer */}\n <div className=\"kombos-mobile-menu__footer\">\n {isLoggedIn ? (\n <Button\n variant=\"secondary\"\n className=\"kombos-mobile-menu__footer-btn\"\n onClick={() => Router.navigateToPage(\"ACCOUNT\")}\n >\n {logoutButtonText}\n </Button>\n ) : (\n <>\n <Button\n variant=\"secondary\"\n className=\"kombos-mobile-menu__footer-btn\"\n onClick={() => Router.navigateToPage(\"REGISTER\")}\n >\n {registerButtonText}\n </Button>\n <Button\n variant=\"primary\"\n className=\"kombos-mobile-menu__footer-btn\"\n onClick={() => Router.navigateToPage(\"LOGIN\")}\n >\n {loginButtonText}\n </Button>\n </>\n )}\n </div>\n </div>\n </div>\n );\n});\n\nexport default MobileMenu;\n"
17649
- },
17650
- {
17651
- "filename": "components/MobileMenu/styles.css",
17652
- "content": "/* ===== Mobile Menu ===== */\n.kombos-mobile-overlay {\n position: fixed;\n inset: 0;\n z-index: var(--kombos-z-overlay);\n}\n\n.kombos-mobile-backdrop {\n position: absolute;\n inset: 0;\n background: rgba(0, 0, 0, 0);\n transition: background 0.3s ease;\n}\n\n.kombos-mobile-overlay--open .kombos-mobile-backdrop {\n background: rgba(0, 0, 0, 0.35);\n}\n\n.kombos-mobile-menu {\n position: absolute;\n inset: 0;\n width: 100%;\n max-width: 26rem;\n background: var(--kombos-white);\n display: flex;\n flex-direction: column;\n transform: translateX(-100%);\n transition: transform 0.3s ease;\n}\n\n.kombos-mobile-overlay--open .kombos-mobile-menu {\n transform: translateX(0);\n}\n\n/* Header */\n.kombos-mobile-menu__head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 1rem 0.75rem;\n height: 3.5rem;\n border-bottom: 1px solid var(--kombos-gray-100);\n flex-shrink: 0;\n}\n\n.kombos-mobile-menu__icon-btn {\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 1.5rem;\n height: 1.5rem;\n color: var(--kombos-gray-900);\n}\n\n/* Body */\n.kombos-mobile-menu__body {\n flex: 1;\n overflow-y: auto;\n}\n\n/* Back button */\n.kombos-mobile-menu__back {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n padding: 1rem 0.75rem;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--kombos-gray-900);\n width: 100%;\n}\n\n/* Nav */\n.kombos-mobile-menu__nav {\n padding: 0 0.75rem;\n}\n\n.kombos-mobile-menu__link-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n border-bottom: 1px solid var(--kombos-gray-100);\n}\n\n.kombos-mobile-menu__link {\n display: flex;\n align-items: center;\n flex: 1;\n color: var(--kombos-gray-900);\n text-decoration: none;\n padding: 1rem 0;\n}\n\n.kombos-mobile-menu__drill-btn {\n background: none;\n border: none;\n cursor: pointer;\n padding: 1rem 0 1rem 0.75rem;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--kombos-gray-900);\n}\n\n/* Footer */\n.kombos-mobile-menu__footer {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 1rem;\n padding: 1rem 1.5rem;\n border-top: 1px solid var(--kombos-gray-100);\n}\n\n.kombos-mobile-menu__footer-btn {\n flex: 1;\n}\n"
17653
- },
17654
- {
17655
- "filename": "components/NavItem/index.tsx",
17656
- "content": "import { IkasNavigationLink } from \"@ikas/bp-storefront\";\nimport { CaretDownSVG, CaretRightSVG } from \"../../../../sub-components/icons\";\n\ninterface Props {\n link: IkasNavigationLink;\n linkColor?: string;\n}\n\nexport default function NavItem({ link, linkColor }: Props) {\n const hasSubLinks = link.subLinks && link.subLinks.length > 0;\n\n return (\n <div className=\"kombos-header__nav-item\">\n <a\n href={link.href}\n className=\"kombos-header__nav-link text-md-medium\"\n style={{ color: linkColor ?? \"var(--kombos-gray-700)\" }}\n target={link.openInNewTab ? \"_blank\" : undefined}\n >\n {link.label}\n {hasSubLinks && <CaretDownSVG className=\"kombos-header__chevron\" />}\n </a>\n {hasSubLinks && (\n <div className=\"kombos-header__dropdown\">\n {link.subLinks!.map((sub: IkasNavigationLink, j: number) => {\n const hasNestedLinks = sub.subLinks && sub.subLinks.length > 0;\n return (\n <div key={j} className=\"kombos-header__dropdown-item\">\n <a\n href={sub.href}\n className=\"kombos-header__dropdown-link text-md-medium\"\n target={sub.openInNewTab ? \"_blank\" : undefined}\n >\n <span>{sub.label}</span>\n {hasNestedLinks && (\n <CaretRightSVG className=\"kombos-header__caret-right\" />\n )}\n </a>\n {hasNestedLinks && (\n <div className=\"kombos-header__dropdown kombos-header__dropdown--nested\">\n {sub.subLinks!.map(\n (child: IkasNavigationLink, k: number) => (\n <div key={k} className=\"kombos-header__dropdown-item\">\n <a\n href={child.href}\n className=\"kombos-header__dropdown-link text-md-medium\"\n target={\n child.openInNewTab ? \"_blank\" : undefined\n }\n >\n <span>{child.label}</span>\n </a>\n </div>\n ),\n )}\n </div>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n}\n"
17657
- },
17658
- {
17659
- "filename": "components/SearchModal/index.tsx",
17660
- "content": "import { useState, useEffect, useRef, useCallback } from \"preact/hooks\";\nimport { observer } from \"@ikas/component-utils\";\nimport { useScrollLock } from \"../../../../hooks/useScrollLock\";\nimport {\n IkasProductList,\n IkasImage,\n searchProductList,\n getDefaultSrc,\n isEmpty,\n Router,\n createMediaSrcset,\n getProductOptionSet,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport { cx } from \"../../../../utils/cx\";\nimport { MagnifyingGlass1SVG, XSVG } from \"../../../../sub-components/icons\";\nimport SpinnerIcon from \"../../../../sub-components/SpinnerIcon\";\nimport ProductCard from \"../../../../sub-components/ProductCard\";\nimport Button from \"../../../../sub-components/Button\";\nimport Input from \"../../../../sub-components/Input\";\nimport type { AspectRatio, ObjectFit } from \"../../../../global-types\";\n\ninterface Props {\n productList: IkasProductList;\n logo?: IkasImage;\n\n logoSizeDesktop: string;\n logoSizeMobile: string;\n searchPlaceholder: string;\n searchingText: string;\n noResultsText: string;\n resultCountText: string;\n addToCartText: string;\n addedToCartText: string;\n outOfStockText: string;\n goToProductText: string;\n viewAllText: string;\n hideAddToCartButton?: boolean;\n components?: any;\n parentProps?: Record<string, any>;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n onClose: () => void;\n}\n\nconst SearchModal = observer(function SearchModal({\n productList,\n logo,\n logoSizeDesktop,\n logoSizeMobile,\n searchPlaceholder,\n searchingText,\n noResultsText,\n resultCountText,\n addToCartText,\n addedToCartText,\n outOfStockText,\n goToProductText,\n viewAllText,\n hideAddToCartButton,\n aspectRatio,\n objectFit,\n onClose,\n components,\n parentProps,\n}: Props) {\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [pendingSearch, setPendingSearch] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const products = productList.data ?? [];\n\n useEffect(() => {\n products.forEach((p) => {\n if (!p.productOptionSet) getProductOptionSet(p);\n });\n }, [products.length]);\n\n const isLoading = productList.isLoading;\n const totalCount = productList.count ?? 0;\n\n useEffect(() => {\n if (!isLoading) setPendingSearch(false);\n }, [isLoading]);\n\n useScrollLock();\n\n useEffect(() => {\n requestAnimationFrame(() => setOpen(true));\n setTimeout(() => inputRef.current?.focus(), 100);\n }, []);\n\n const handleClose = useCallback(() => {\n setOpen(false);\n setTimeout(onClose, 300);\n }, [onClose]);\n\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") handleClose();\n };\n document.addEventListener(\"keydown\", handleKeyDown);\n return () => document.removeEventListener(\"keydown\", handleKeyDown);\n }, [handleClose]);\n\n const handleInput = useCallback(\n (e: Event) => {\n const value = (e.target as HTMLInputElement).value;\n setQuery(value);\n setPendingSearch(true);\n searchProductList(productList, value);\n },\n [productList],\n );\n\n const showResults = !isLoading && !isEmpty(products);\n const showNoResults =\n !isLoading && isEmpty(products) && query.trim().length > 0;\n\n return (\n <div\n className={cx(\n \"kombos-search-overlay\",\n open && \"kombos-search-overlay--open\",\n )}\n onClick={handleClose}\n >\n {/* Panel */}\n <div\n className=\"kombos-search__panel\"\n onClick={(e) => e.stopPropagation()}\n >\n {/* Header */}\n <div className=\"kombos-search__header\">\n {/* Logo */}\n <div className=\"kombos-search__logo-area\">\n {logo && (\n <a\n href=\"/\"\n className=\"kombos-search__logo\"\n style={{\n \"--search-logo-h-desktop\": logoSizeDesktop,\n \"--search-logo-h-mobile\": logoSizeMobile,\n }}\n >\n <img\n src={getDefaultSrc(logo)}\n srcSet={createMediaSrcset(logo)}\n sizes=\"120px\"\n alt={logo?.altText || \"Logo\"}\n className=\"kombos-search__logo-img\"\n />\n </a>\n )}\n </div>\n\n {/* Search Input */}\n <Input\n className=\"kombos-search__input-wrap\"\n leftIcon={<MagnifyingGlass1SVG />}\n inputRef={inputRef}\n placeholder={searchPlaceholder}\n value={query}\n onInput={handleInput}\n />\n\n {/* Close */}\n <div className=\"kombos-search__close-area\">\n <button\n className=\"kombos-search__close\"\n onClick={handleClose}\n aria-label=\"Close\"\n >\n <XSVG />\n </button>\n </div>\n </div>\n\n {/* Body */}\n <div className=\"kombos-search__body\">\n {/* Searching */}\n {isLoading && (\n <div className=\"kombos-search__status\">\n <SpinnerIcon className=\"kombos-search__spinner\" />\n <span className=\"text-xl-medium md:display-xs-medium\">\n {searchingText}\n </span>\n </div>\n )}\n\n {/* No Results */}\n {showNoResults && (\n <div className=\"kombos-search__status\">\n <span className=\"text-xl-medium md:display-xs-medium\">\n {noResultsText}\n </span>\n </div>\n )}\n\n {/* Result Count */}\n {!isLoading &&\n !pendingSearch &&\n query.trim().length > 0 &&\n !isEmpty(products) && (\n <div className=\"kombos-search__result-count\">\n <span className=\"text-xl-medium md:display-xs-medium\">\n {totalCount} {resultCountText}\n </span>\n </div>\n )}\n\n {/* Results */}\n {showResults && (\n <>\n <div className=\"kombos-search__grid\">\n {products.map((product) => (\n <div key={product.id} className=\"kombos-search__card\">\n <ProductCard\n product={product}\n addToCartText={addToCartText}\n addedToCartText={addedToCartText}\n outOfStockText={outOfStockText}\n goToProductText={goToProductText}\n hideAddToCartButton={hideAddToCartButton}\n openCartOnAdd={false}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n sizes=\"(max-width: 767px) calc((100vw - 44px) / 2), (max-width: 1023px) calc((100vw - 48px) / 2), calc((100vw - 240px) / 5)\"\n />\n <IkasComponentRenderer\n id={`search-modal-product-${product.id}`}\n components={components}\n parentProps={parentProps}\n map={{\n product,\n }}\n className=\"kombos-search__card-content\"\n />\n </div>\n ))}\n </div>\n {query.trim().length > 0 && (\n <div className=\"kombos-search__view-all\">\n <Button\n variant=\"primary\"\n className=\"kombos-search__view-all-btn\"\n onClick={() => {\n Router.navigateToPage(\"SEARCH\", undefined, {\n q: query.trim(),\n });\n handleClose();\n }}\n >\n {viewAllText}\n </Button>\n </div>\n )}\n </>\n )}\n </div>\n </div>\n </div>\n );\n});\n\nexport default SearchModal;\n"
17661
- },
17662
- {
17663
- "filename": "components/SearchModal/styles.css",
17664
- "content": "\n/* ===== Search Modal ===== */\n.kombos-search-overlay {\n position: fixed;\n inset: 0;\n z-index: var(--kombos-z-overlay);\n background: rgba(0, 0, 0, 0.35);\n opacity: 0;\n transition: opacity 0.3s ease;\n}\n\n.kombos-search-overlay--open {\n opacity: 1;\n}\n\n.kombos-search__panel {\n display: flex;\n flex-direction: column;\n max-height: 100%;\n background: var(--kombos-white);\n}\n\n/* Header */\n.kombos-search__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 1rem;\n border-bottom: 1px solid var(--kombos-gray-200);\n flex-shrink: 0;\n}\n\n.kombos-search__logo-area {\n flex-shrink: 0;\n width: 7.5rem;\n display: none;\n}\n\n.kombos-search__logo {\n display: flex;\n align-items: center;\n text-decoration: none;\n height: var(--search-logo-h-mobile, 3rem);\n overflow: hidden;\n}\n\n.kombos-search__logo-img {\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n\n.kombos-search__input-wrap {\n flex: 1;\n max-width: 59.5rem;\n}\n\n.kombos-search__close-area {\n flex-shrink: 0;\n width: auto;\n display: flex;\n justify-content: flex-end;\n}\n\n.kombos-search__close {\n background: var(--kombos-gray-50);\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n cursor: pointer;\n padding: 0.6875rem;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--kombos-gray-900);\n font-size: 1.25rem;\n transition: background-color 0.15s ease;\n}\n\n.kombos-search__close:hover {\n background: var(--kombos-gray-100);\n}\n\n/* Body */\n.kombos-search__body {\n overflow-y: auto;\n padding: 1.5rem 1rem 0;\n}\n\n/* Status (searching / no results) */\n.kombos-search__status {\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: center;\n gap: 0.75rem;\n padding: 0 0 3rem;\n color: var(--kombos-gray-900);\n}\n\n.kombos-search__spinner {\n font-size: 2rem;\n}\n\n/* Result Count */\n.kombos-search__result-count {\n text-align: center;\n padding: 0 0 1.5rem;\n color: var(--kombos-gray-900);\n}\n\n/* Product Grid */\n.kombos-search__grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 0.75rem;\n padding-bottom: 3rem;\n}\n\n/* View All */\n.kombos-search__view-all {\n display: flex;\n justify-content: center;\n padding-bottom: 3rem;\n}\n\n.kombos-search__view-all-btn {\n min-width: 12.5rem;\n}\n\n.kombos-search__card {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n min-width: 0;\n}\n\n.kombos-search__card-content {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n/* ===== Responsive — Tablet ===== */\n@media (min-width: 768px) {\n .kombos-search__grid {\n gap: 1rem;\n }\n}\n\n/* ===== Responsive — Desktop ===== */\n@media (min-width: 1024px) {\n .kombos-search__header {\n padding: 1.25rem 3.5rem;\n gap: 1.5rem;\n }\n\n .kombos-search__logo-area {\n display: block;\n }\n\n .kombos-search__close-area {\n width: 7.5rem;\n }\n\n .kombos-search__body {\n padding: 3rem 4.5rem 0;\n }\n\n .kombos-search__result-count {\n padding-bottom: 3rem;\n }\n\n .kombos-search__grid {\n grid-template-columns: repeat(5, 1fr);\n gap: 1.5rem;\n }\n\n .kombos-search__logo {\n height: var(--search-logo-h-desktop, 3.75rem);\n }\n}\n"
17665
- },
17666
- {
17667
- "filename": "ikas-config-snippet.json",
17668
- "content": "{\n \"id\": \"{{PROJECT_ID}}-navbar\",\n \"name\": \"Navbar\",\n \"type\": \"component\",\n \"entry\": \"./src/components/Navbar/index.tsx\",\n \"styles\": \"./src/components/Navbar/styles.css\",\n \"props\": [\n {\n \"name\": \"logo\",\n \"displayName\": \"Logo\",\n \"type\": \"IMAGE\",\n \"required\": false,\n \"groupId\": \"logo-settings\"\n },\n {\n \"name\": \"logoSizeDesktop\",\n \"displayName\": \"Logo Size Desktop\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"logo-settings\",\n \"typeId\": \"@ikas/bp-storefront-models-HeightStyleType\"\n },\n {\n \"name\": \"logoSizeMobile\",\n \"displayName\": \"Logo Size Mobile\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"logo-settings\",\n \"typeId\": \"@ikas/bp-storefront-models-HeightStyleType\"\n },\n {\n \"name\": \"navigationLinks\",\n \"displayName\": \"Navigation Links\",\n \"type\": \"LIST_OF_LINK\",\n \"required\": false,\n \"groupId\": \"navigation\"\n },\n {\n \"name\": \"navigationLinkColor\",\n \"displayName\": \"Link Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"navigation\"\n },\n {\n \"name\": \"coloredLinks\",\n \"displayName\": \"Links\",\n \"type\": \"LIST_OF_LINK\",\n \"required\": false,\n \"groupId\": \"colored-links\"\n },\n {\n \"name\": \"coloredLinkColor\",\n \"displayName\": \"Link Color\",\n \"type\": \"COLOR\",\n \"required\": false,\n \"groupId\": \"colored-links\"\n },\n {\n \"name\": \"cartTitle\",\n \"displayName\": \"Cart Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"My Shopping Cart\",\n \"groupId\": \"cart-sidebar\"\n },\n {\n \"name\": \"emptyCartText\",\n \"displayName\": \"Empty Cart Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Your cart is empty\",\n \"groupId\": \"cart-sidebar\"\n },\n {\n \"name\": \"checkoutButtonText\",\n \"displayName\": \"Payment Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Proceed to Checkout\",\n \"groupId\": \"cart-sidebar\"\n },\n {\n \"name\": \"totalText\",\n \"displayName\": \"Total Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Total\",\n \"groupId\": \"cart-sidebar\"\n },\n {\n \"name\": \"freeShippingText\",\n \"displayName\": \"Free Shipping Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Free shipping on orders over $150\",\n \"groupId\": \"cart-sidebar\"\n },\n {\n \"name\": \"emptyCartButtonText\",\n \"displayName\": \"Empty Cart Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Start Shopping\",\n \"groupId\": \"cart-sidebar\"\n },\n {\n \"name\": \"registerButtonText\",\n \"displayName\": \"Üye Ol Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sign Up\",\n \"groupId\": \"mobile-menu\"\n },\n {\n \"name\": \"loginButtonText\",\n \"displayName\": \"Login Yap Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sign In\",\n \"groupId\": \"mobile-menu\"\n },\n {\n \"name\": \"logoutButtonText\",\n \"displayName\": \"Çıkış Yap Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sign Out\",\n \"groupId\": \"mobile-menu\"\n },\n {\n \"name\": \"searchProductList\",\n \"displayName\": \"Search Product Listesi\",\n \"type\": \"PRODUCT_LIST\",\n \"required\": false,\n \"groupId\": \"search\"\n },\n {\n \"name\": \"hideAddToCartButton\",\n \"displayName\": \"Cart Add Butonunu Hide\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"search-card-settings\"\n },\n {\n \"name\": \"searchPlaceholder\",\n \"displayName\": \"Search Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"What are you looking for?\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"searchingText\",\n \"displayName\": \"Aranıyor Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Aranıyor...\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"noResultsText\",\n \"displayName\": \"Result Bulunamadı Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"No products found\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"resultCountText\",\n \"displayName\": \"Result Count Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Result\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"addToCartText\",\n \"displayName\": \"Cart Add Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Add to Cart\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"addedToCartText\",\n \"displayName\": \"Cart Added Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Added to Cart\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"outOfStockText\",\n \"displayName\": \"Sold Out Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sold Out\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"goToProductText\",\n \"displayName\": \"Product Git Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Go to Product\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"viewAllText\",\n \"displayName\": \"Tümünü Gör Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"View All\",\n \"groupId\": \"search-texts\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Components\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"privateVarMap\": {\n \"product\": {\n \"id\": \"pvm_1772803767210_2\",\n \"typeId\": \"@ikas/bp-storefront-models-IkasProduct\"\n }\n },\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-card-product-price\",\n \"{{PROJECT_ID}}-card-product-variants\",\n \"{{PROJECT_ID}}-card-product-name\"\n ]\n },\n {\n \"name\": \"imageAspectRatio\",\n \"displayName\": \"Image Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"search-image\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"imageObjectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"search-image\",\n \"enumTypeId\": \"GrylMqHxui\"\n },\n {\n \"name\": \"viewCartButtonText\",\n \"displayName\": \"Sepeti Görüntüle Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"View Cart\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"logo-settings\",\n \"name\": \"Logo\",\n \"description\": \"Image ve boyut ayarları\"\n },\n {\n \"id\": \"navigation\",\n \"name\": \"Navigation\",\n \"description\": \"Links ve renkler\"\n },\n {\n \"id\": \"colored-links\",\n \"name\": \"Colored Links\",\n \"description\": \"Öne çıkan bağlantılar\"\n },\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Cart ve mobil menü metinleri\",\n \"children\": [\n {\n \"id\": \"cart-sidebar\",\n \"name\": \"Cart\",\n \"description\": \"Cart kenar çubuğu metinleri\"\n },\n {\n \"id\": \"mobile-menu\",\n \"name\": \"Mobile Menü\",\n \"description\": \"Giriş, kayıt ve çıkış butonları\"\n }\n ]\n },\n {\n \"id\": \"search\",\n \"name\": \"Search\",\n \"description\": \"Search modalı ayarları\",\n \"children\": [\n {\n \"id\": \"search-card-settings\",\n \"name\": \"Product Kartı Settings\",\n \"description\": \"Search sonuçlarındaki ürün kartı görünüm ayarları\"\n },\n {\n \"id\": \"search-texts\",\n \"name\": \"Texts\",\n \"description\": \"Search modalı metin ayarları\"\n }\n ]\n },\n {\n \"id\": \"search-image\",\n \"name\": \"Product Image Settings\",\n \"description\": \"Product görsellerinin oran ve sığdırma ayarları\"\n }\n ]\n}"
17669
- },
17670
- {
17671
- "filename": "index.tsx",
17672
- "content": "import { useState, useEffect } from \"preact/hooks\";\nimport {\n cartStore,\n customerStore,\n hasCustomer,\n getIkasOrderTotalItemCount,\n getDefaultSrc,\n getFormattedHeightSize,\n Router,\n createMediaSrcset,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport CartSidebar from \"./components/CartSidebar\";\nimport MobileMenu from \"./components/MobileMenu\";\nimport SearchModal from \"./components/SearchModal\";\nimport NavItem from \"./components/NavItem\";\nimport {\n List1SVG,\n MagnifyingGlass1SVG,\n User1SVG,\n ShoppingBag1SVG,\n} from \"../../sub-components/icons\";\nexport function Navbar(props: Props) {\n const {\n logo,\n navigationLinks,\n logoSizeDesktop: rawLogoSizeDesktop,\n logoSizeMobile: rawLogoSizeMobile,\n cartTitle = \"Alışveriş Sepetim\",\n emptyCartText = \"Sepetinizde Ürün Bulunmamaktadır\",\n checkoutButtonText = \"Ödemeye Geç\",\n totalText = \"Toplam\",\n navigationLinkColor,\n coloredLinks,\n coloredLinkColor,\n registerButtonText = \"Üye Ol\",\n loginButtonText = \"Giriş Yap\",\n logoutButtonText = \"Çıkış Yap\",\n freeShippingText = \"150 TL ve üzeri siparişlerde kargo bedava\",\n emptyCartButtonText = \"Alışverişe Başla\",\n searchPlaceholder = \"Ne Aramıştınız?\",\n searchingText = \"Aranıyor...\",\n noResultsText = \"Aradığınız ürün bulunamadı\",\n resultCountText = \"Sonuç\",\n addToCartText = \"Sepete Ekle\",\n addedToCartText = \"Sepete Eklendi\",\n outOfStockText = \"Tükendi\",\n goToProductText = \"Ürüne Git\",\n viewAllText = \"Tümünü Gör\",\n viewCartButtonText = \"Sepeti Görüntüle\",\n searchProductList,\n hideAddToCartButton,\n imageAspectRatio,\n imageObjectFit,\n components,\n } = props;\n\n const logoSizeDesktop = getFormattedHeightSize(rawLogoSizeDesktop) || \"60px\";\n const logoSizeMobile = getFormattedHeightSize(rawLogoSizeMobile) || \"48px\";\n\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [cartOpen, setCartOpen] = useState(false);\n const [searchOpen, setSearchOpen] = useState(false);\n\n useEffect(() => {\n const openCart = () => setCartOpen(true);\n window.addEventListener(\"ikas:open-cart-sidebar\", openCart);\n return () => window.removeEventListener(\"ikas:open-cart-sidebar\", openCart);\n }, []);\n\n const cart = cartStore.cart;\n const itemCount = cart ? getIkasOrderTotalItemCount(cart) : 0;\n const isLoggedIn = hasCustomer(customerStore);\n\n const links = navigationLinks?.links ?? [];\n const coloredLinksList = coloredLinks?.links ?? [];\n\n return (\n <div\n className=\"kombos-navbar\"\n style={{\n \"--logo-h-desktop\": logoSizeDesktop,\n \"--logo-h-mobile\": logoSizeMobile,\n }}\n >\n <div className=\"kombos-navbar__inner kombos-container\">\n {/* Hamburger - mobile only */}\n <button\n className=\"kombos-navbar__hamburger\"\n onClick={() => setMobileMenuOpen(true)}\n aria-label=\"Menu\"\n >\n <List1SVG />\n </button>\n\n {/* Logo */}\n {logo && (\n <a\n className=\"kombos-navbar__logo\"\n href=\"/\"\n onClick={(e) => {\n e.preventDefault();\n Router.navigate(\"/\");\n }}\n style={{\n \"--logo-h-desktop\": logoSizeDesktop,\n \"--logo-h-mobile\": logoSizeMobile,\n }}\n >\n <img\n src={getDefaultSrc(logo)}\n srcSet={createMediaSrcset(logo)}\n sizes=\"200px\"\n alt={logo?.altText || \"Logo\"}\n className=\"kombos-navbar__logo-img\"\n width={200}\n height={60}\n fetchpriority=\"high\"\n loading=\"eager\"\n />\n </a>\n )}\n\n {/* Desktop Navigation */}\n <nav\n className={`kombos-navbar__nav${!logo ? \" kombos-navbar__nav--no-logo\" : \"\"}`}\n >\n {links.map((link, i) => (\n <NavItem key={i} link={link} linkColor={navigationLinkColor} />\n ))}\n {coloredLinksList.map((link, i) => (\n <NavItem key={`cl-${i}`} link={link} linkColor={coloredLinkColor} />\n ))}\n </nav>\n\n {/* Action Icons */}\n <div className=\"kombos-navbar__actions\">\n {/* Search */}\n <a\n className=\"kombos-navbar__icon-btn\"\n href=\"/search\"\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n if (searchProductList) {\n setSearchOpen(true);\n } else {\n Router.navigateToPage(\"SEARCH\");\n }\n }}\n aria-label=\"Search\"\n >\n <MagnifyingGlass1SVG />\n </a>\n\n {/* Account */}\n <a\n className=\"kombos-navbar__icon-btn\"\n href={isLoggedIn ? \"/account\" : \"/login\"}\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n Router.navigateToPage(isLoggedIn ? \"ACCOUNT\" : \"LOGIN\");\n }}\n aria-label=\"Account\"\n >\n <User1SVG />\n </a>\n\n {/* Cart */}\n <a\n className=\"kombos-navbar__icon-btn kombos-navbar__cart-trigger\"\n href=\"/cart\"\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n setCartOpen(true);\n }}\n aria-label=\"Cart\"\n >\n <ShoppingBag1SVG />\n {itemCount > 0 && (\n <span className=\"kombos-navbar__badge\">{itemCount}</span>\n )}\n </a>\n </div>\n </div>\n\n {/* Cart Sidebar */}\n {cartOpen && (\n <CartSidebar\n cartTitle={cartTitle}\n emptyCartText={emptyCartText}\n checkoutButtonText={checkoutButtonText}\n viewCartButtonText={viewCartButtonText}\n totalText={totalText}\n freeShippingText={freeShippingText}\n emptyCartButtonText={emptyCartButtonText}\n imageAspectRatio={imageAspectRatio}\n imageObjectFit={imageObjectFit}\n onClose={() => setCartOpen(false)}\n />\n )}\n\n {/* Search Modal */}\n {searchOpen && searchProductList && (\n <SearchModal\n productList={searchProductList}\n logo={logo ?? undefined}\n logoSizeDesktop={logoSizeDesktop}\n logoSizeMobile={logoSizeMobile}\n searchPlaceholder={searchPlaceholder}\n searchingText={searchingText}\n noResultsText={noResultsText}\n resultCountText={resultCountText}\n addToCartText={addToCartText}\n addedToCartText={addedToCartText}\n outOfStockText={outOfStockText}\n goToProductText={goToProductText}\n viewAllText={viewAllText}\n hideAddToCartButton={hideAddToCartButton}\n aspectRatio={imageAspectRatio}\n objectFit={imageObjectFit}\n onClose={() => setSearchOpen(false)}\n components={components}\n parentProps={props}\n />\n )}\n\n {/* Mobile Menu */}\n {mobileMenuOpen && (\n <MobileMenu\n linkGroups={[\n { links, color: navigationLinkColor },\n ...(coloredLinksList.length > 0\n ? [{ links: coloredLinksList, color: coloredLinkColor }]\n : []),\n ]}\n registerButtonText={registerButtonText}\n loginButtonText={loginButtonText}\n logoutButtonText={logoutButtonText}\n onClose={() => setMobileMenuOpen(false)}\n onCartOpen={() => setCartOpen(true)}\n />\n )}\n </div>\n );\n}\n\nexport default Navbar;\n"
17673
- },
17674
- {
17675
- "filename": "styles.css",
17676
- "content": "/* ===== Navbar Component ===== */\n.kombos-navbar__inner {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding-block: 0.75rem;\n position: relative;\n justify-content: space-between;\n min-height: calc(var(--logo-h-mobile, 3rem) + 1.5rem);\n}\n\n/* Logo */\n.kombos-navbar__logo {\n flex-shrink: 0;\n text-decoration: none;\n display: flex;\n align-items: center;\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n height: var(--logo-h-mobile, 3rem);\n overflow: hidden;\n}\n\n.kombos-navbar__logo-img {\n width: 100%;\n height: 100%;\n object-fit: contain;\n padding: 0.5rem 0;\n}\n\n/* Navigation */\n.kombos-navbar__nav {\n flex: 1;\n display: none;\n align-items: center;\n justify-content: center;\n gap: 1.5rem;\n}\n\n/* NavItem sub-component uses kombos-header__* classes — keep those styles here */\n.kombos-header__nav-item {\n position: relative;\n}\n\n.kombos-header__nav-link {\n color: var(--kombos-gray-700);\n text-decoration: none;\n display: flex;\n align-items: center;\n gap: 0.25rem;\n white-space: nowrap;\n transition: opacity 0.2s ease;\n}\n\n.kombos-header__nav-link:hover {\n opacity: 0.7;\n}\n\n.kombos-header__chevron {\n flex-shrink: 0;\n transition: transform 0.2s ease;\n}\n\n.kombos-header__nav-item:hover .kombos-header__chevron {\n transform: rotate(180deg);\n}\n\n/* Dropdown */\n.kombos-header__dropdown {\n display: none;\n position: absolute;\n top: calc(100% + 1.125rem);\n left: 50%;\n transform: translateX(-50%);\n background: var(--kombos-white);\n border: 1px solid var(--kombos-gray-100);\n border-radius: 6px;\n padding: 0.375rem 0;\n width: 13rem;\n box-shadow: 0 2px 12px 0.4px rgba(0, 0, 0, 0.06);\n z-index: var(--kombos-z-dropdown);\n flex-direction: column;\n}\n\n/* Invisible bridge: covers the 18px gap between nav link and dropdown */\n.kombos-header__dropdown::before {\n content: \"\";\n position: absolute;\n bottom: 100%;\n left: -0.75rem;\n right: -0.75rem;\n height: 1.125rem;\n}\n\n.kombos-header__nav-item:hover > .kombos-header__dropdown {\n display: flex;\n}\n\n/* Nested dropdown (2nd level) */\n.kombos-header__dropdown--nested {\n top: -0.375rem;\n left: calc(100% + 0.75rem);\n transform: none;\n}\n\n/* Bridge: covers the 12px gap between 1st and 2nd level dropdowns */\n.kombos-header__dropdown--nested::before {\n content: \"\";\n position: absolute;\n top: -0.75rem;\n bottom: -0.75rem;\n right: 100%;\n left: auto;\n width: 0.75rem;\n height: auto;\n}\n\n.kombos-header__dropdown-item {\n position: relative;\n}\n\n.kombos-header__dropdown-item:hover > .kombos-header__dropdown--nested {\n display: flex;\n}\n\n.kombos-header__dropdown-link {\n display: flex;\n align-items: center;\n justify-content: space-between;\n color: var(--kombos-gray-900);\n text-decoration: none;\n padding: 0.75rem 1rem;\n transition: background-color 0.15s ease;\n}\n\n.kombos-header__dropdown-link:hover {\n background-color: var(--kombos-gray-50);\n}\n\n.kombos-header__caret-right {\n flex-shrink: 0;\n color: var(--kombos-gray-900);\n}\n\n/* Action Icons */\n.kombos-navbar__actions {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 0.75rem;\n}\n\n.kombos-navbar__icon-btn {\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 1.5rem;\n height: 1.5rem;\n position: relative;\n transition: opacity 0.2s ease;\n font-size: 1.25rem;\n}\n\n.kombos-navbar__icon-btn:hover {\n opacity: 0.6;\n}\n\n.kombos-navbar__badge {\n position: absolute;\n top: -0.375rem;\n right: -0.5rem;\n background: var(--kombos-badge-bg);\n color: var(--kombos-white);\n font-size: 0.625rem;\n font-weight: 600;\n min-width: 1.125rem;\n height: 1.125rem;\n border-radius: 9px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0 0.25rem;\n}\n\n/* Hamburger (mobile only — visible by default) */\n.kombos-navbar__hamburger {\n display: flex;\n background: none;\n border: none;\n cursor: pointer;\n padding: 0;\n width: 1.5rem;\n height: 1.5rem;\n align-items: center;\n justify-content: center;\n color: var(--kombos-gray-900);\n transition: opacity 0.2s ease;\n font-size: 1.375rem;\n}\n\n.kombos-navbar__hamburger:hover {\n opacity: 0.6;\n}\n\n/* ===== Responsive — Desktop ===== */\n@media (min-width: 1024px) {\n .kombos-navbar__inner {\n gap: 2rem;\n position: static;\n justify-content: initial;\n min-height: calc(var(--logo-h-desktop, 3.75rem) + 1.5rem);\n }\n\n .kombos-navbar__hamburger {\n display: none;\n }\n\n .kombos-navbar__nav {\n display: flex;\n }\n\n .kombos-navbar__nav--no-logo {\n justify-content: flex-start;\n }\n\n .kombos-navbar__logo {\n position: static;\n left: auto;\n top: auto;\n transform: none;\n height: var(--logo-h-desktop, 3.75rem);\n }\n\n .kombos-navbar__actions {\n gap: 1.25rem;\n }\n}\n"
17677
- },
17678
- {
17679
- "filename": "types.ts",
17680
- "content": "import type { IkasImage, HeightStyleType, IkasNavigationLinkList, IkasProductList } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../global-types\";\n\nexport interface Props {\n logo?: IkasImage | null;\n logoSizeDesktop?: HeightStyleType;\n logoSizeMobile?: HeightStyleType;\n navigationLinks?: IkasNavigationLinkList;\n navigationLinkColor?: string;\n coloredLinks?: IkasNavigationLinkList;\n coloredLinkColor?: string;\n cartTitle?: string;\n emptyCartText?: string;\n checkoutButtonText?: string;\n totalText?: string;\n freeShippingText?: string;\n emptyCartButtonText?: string;\n registerButtonText?: string;\n loginButtonText?: string;\n logoutButtonText?: string;\n searchProductList?: IkasProductList;\n hideAddToCartButton?: boolean;\n searchPlaceholder?: string;\n searchingText?: string;\n noResultsText?: string;\n resultCountText?: string;\n addToCartText?: string;\n addedToCartText?: string;\n outOfStockText?: string;\n goToProductText?: string;\n viewAllText?: string;\n components?: any;\n imageAspectRatio?: AspectRatio;\n imageObjectFit?: ObjectFit;\n viewCartButtonText?: string;\n}\n"
17681
- }
17682
- ]
17683
- },
17684
- {
17685
- "id": "not-found-section",
17686
- "title": "404 Not Found Section",
17687
- "description": "Custom 404 page with configurable heading, message, and return-to-home button. Simple section with text props.",
17688
- "code": "import { Router } from \"@ikas/bp-storefront\";\n\nimport { Props } from \"./types\";\nimport Button from \"../../sub-components/Button\";\n\nexport function NotFound({ title, description, button }: Props) {\n return (\n <section className=\"not-found\">\n <div className=\"not-found__wrapper kombos-container\">\n <div className=\"not-found__content\">\n <h1 className=\"not-found__title display-sm-semibold\">{title}</h1>\n <p className=\"not-found__description text-md-regular\">\n {description}\n </p>\n {button?.href && button?.label && (\n <Button\n variant=\"primary\"\n size=\"s\"\n onClick={() =>\n Router.navigate(\n button.href,\n false,\n button.openInNewTab ?? false,\n )\n }\n style={{\n minWidth: 200,\n }}\n >\n {button.label}\n </Button>\n )}\n </div>\n </div>\n </section>\n );\n}\n\nexport default NotFound;\n",
17689
- "relatedFunctions": [
17690
- "Router"
17691
- ],
17692
- "categories": [
17693
- "Layout"
17694
- ],
17695
- "files": [
17696
- {
17697
- "filename": "ikas-config-snippet.json",
17698
- "content": "{\n \"id\": \"{{PROJECT_ID}}-not-found\",\n \"name\": \"NotFound\",\n \"type\": \"section\",\n \"entry\": \"./src/components/NotFound/index.tsx\",\n \"styles\": \"./src/components/NotFound/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Page Bulunamadı\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"description\",\n \"displayName\": \"Description\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Aradığınız sayfa mevcut değil veya taşınmış olabilir.\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"button\",\n \"displayName\": \"Button\",\n \"type\": \"LINK\",\n \"required\": false,\n \"groupId\": \"action\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"content\",\n \"name\": \"Content\",\n \"description\": \"Page başlığı ve açıklama metni ayarları\"\n },\n {\n \"id\": \"action\",\n \"name\": \"Redirect\",\n \"description\": \"Kullanıcıyı yönlendiren buton ayarları\"\n }\n ]\n}"
17699
- },
17700
- {
17701
- "filename": "index.tsx",
17702
- "content": "import { Router } from \"@ikas/bp-storefront\";\n\nimport { Props } from \"./types\";\nimport Button from \"../../sub-components/Button\";\n\nexport function NotFound({ title, description, button }: Props) {\n return (\n <section className=\"not-found\">\n <div className=\"not-found__wrapper kombos-container\">\n <div className=\"not-found__content\">\n <h1 className=\"not-found__title display-sm-semibold\">{title}</h1>\n <p className=\"not-found__description text-md-regular\">\n {description}\n </p>\n {button?.href && button?.label && (\n <Button\n variant=\"primary\"\n size=\"s\"\n onClick={() =>\n Router.navigate(\n button.href,\n false,\n button.openInNewTab ?? false,\n )\n }\n style={{\n minWidth: 200,\n }}\n >\n {button.label}\n </Button>\n )}\n </div>\n </div>\n </section>\n );\n}\n\nexport default NotFound;\n"
17703
- },
17704
- {
17705
- "filename": "styles.css",
17706
- "content": ".not-found {\n width: 100%;\n}\n\n.not-found__wrapper {\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 50vh;\n padding-top: 2rem;\n padding-bottom: 2rem;\n}\n\n.not-found__content {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n gap: 1rem;\n max-width: 29rem;\n text-wrap-style: pretty;\n}\n\n.not-found__title {\n color: var(--kombos-gray-900);\n margin: 0;\n}\n\n.not-found__description {\n color: var(--kombos-gray-500);\n margin: 0;\n}\n\n@media (min-width: 768px) {\n .not-found__wrapper {\n padding-top: 3rem;\n padding-bottom: 3rem;\n }\n}\n\n@media (min-width: 1024px) {\n .not-found__wrapper {\n padding-top: 4rem;\n padding-bottom: 4rem;\n }\n}\n"
17707
- },
17708
- {
17709
- "filename": "types.ts",
17710
- "content": "import type { IkasNavigationLink } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n title?: string;\n description?: string;\n button?: IkasNavigationLink | null;\n}\n"
17711
- }
17712
- ]
17713
- },
17714
- {
17715
- "id": "product-detail-section",
17716
- "title": "Product Detail Section",
17717
- "description": "Complete product detail page with image gallery, breadcrumb navigation, and IkasComponentRenderer slots for child components (name/favorite, SKU, prices, variant selection, add-to-cart, description, features, bundle products, option sets, offers).",
17718
- "code": "import {\n getIkasCategoryPathItemHref,\n getProductCategoryPath,\n getProductVariantMainImage,\n getSelectedProductVariant,\n IkasImage,\n isNotEmpty,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport Breadcrumb from \"../../sub-components/Breadcrumb\";\nimport type { BreadcrumbItem } from \"../../sub-components/Breadcrumb\";\nimport ProductGallery from \"./components/ProductGallery\";\n\nexport function ProductDetail(props: Props) {\n const {\n product,\n components,\n aspectRatio,\n objectFit,\n bottomComponents,\n homepageText = \"Anasayfa\",\n } = props;\n if (!product) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\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\n const categoryPath = getProductCategoryPath(product);\n\n return (\n <section className=\"kombos-pd\">\n <div className=\"kombos-container kombos-pd__container\">\n <Breadcrumb\n items={[\n { label: homepageText, href: \"/\" } as BreadcrumbItem,\n ...(isNotEmpty(categoryPath)\n ? categoryPath.map(\n (pathItem: any) =>\n ({\n label: pathItem.name,\n href: getIkasCategoryPathItemHref(pathItem),\n }) as BreadcrumbItem,\n )\n : []),\n { label: product.name } as BreadcrumbItem,\n ]}\n size=\"xs\"\n className=\"kombos-pd__breadcrumb\"\n />\n\n <div className=\"kombos-pd__layout\">\n <ProductGallery\n images={images}\n productName={product.name}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n />\n\n <div className=\"kombos-pd__info\">\n <IkasComponentRenderer\n id=\"product-detail-info\"\n components={components}\n parentProps={props}\n />\n </div>\n </div>\n\n {bottomComponents && (\n <div className=\"kombos-pd__bottom\">\n <IkasComponentRenderer\n id=\"product-detail-bottom\"\n components={bottomComponents}\n parentProps={props}\n />\n </div>\n )}\n </div>\n </section>\n );\n}\n\nexport default ProductDetail;\n",
17719
- "relatedFunctions": [
17720
- "getIkasCategoryPathItemHref",
17721
- "getProductCategoryPath",
17722
- "getProductVariantMainImage",
17723
- "getSelectedProductVariant",
17724
- "isNotEmpty",
17725
- "addIkasProductToFavorites",
17726
- "customerStore",
17727
- "getFormattedMarginTopSize",
17728
- "getFormattedMarginBottomSize",
17729
- "hasCustomer",
17730
- "isFavoriteIkasProduct",
17731
- "removeIkasProductFromFavorites",
17732
- "Router",
17733
- "getProductVariantDiscountPercentage",
17734
- "getProductVariantFormattedFinalPrice",
17735
- "getProductVariantFormattedSellPrice",
17736
- "hasProductVariantDiscount",
17737
- "getDisplayedProductGroups",
17738
- "addItemToCart",
17739
- "hasProductStock",
17740
- "hasProductVariantStock",
17741
- "isAddToCartEnabled",
17742
- "initProductOptionSetValues",
17743
- "getProductOptionSet",
17744
- "getDisplayedOptions",
17745
- "getDefaultSrc",
17746
- "acceptProductOffer",
17747
- "rejectProductOffer"
17748
- ],
17749
- "categories": [
17750
- "Product",
17751
- "ProductDetail"
17752
- ],
17753
- "files": [
17754
- {
17755
- "filename": "children/ProductDetailAddToCart/components/PayWithIkas/index.tsx",
17756
- "content": "import { IkasProduct } from \"@ikas/bp-storefront\";\nimport { usePayWithIkas } from \"../../../../hooks/usePayWithIkas\";\n\ninterface Props {\n product: IkasProduct;\n quantity: number;\n isEnabled: boolean;\n payWithIkasUrl: string;\n}\n\nexport default function PayWithIkas({\n product,\n quantity,\n isEnabled,\n payWithIkasUrl,\n}: Props) {\n const { iframeRef, iframeSrc, iframeHeight } = usePayWithIkas({\n product,\n quantity,\n isEnabled,\n payWithIkasUrl,\n });\n\n return (\n <iframe\n ref={iframeRef}\n className=\"kombos-pd-atc__ikas-pay\"\n title=\"Pay with ikas\"\n src={iframeSrc}\n width=\"100%\"\n height={iframeHeight}\n />\n );\n}\n"
17757
- },
17758
- {
17759
- "filename": "children/ProductDetailAddToCart/components/PayWithIkas/styles.css",
17760
- "content": ".kombos-pd-atc__ikas-pay {\n display: block;\n border: none;\n}\n"
17761
- },
17762
- {
17763
- "filename": "children/ProductDetailAddToCart/ikas-config-snippet.json",
17764
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-add-to-cart\",\n \"name\": \"ProductDetailAddToCart\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailAddToCart/index.tsx\",\n \"styles\": \"./src/components/ProductDetailAddToCart/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"addToCartButtonText\",\n \"displayName\": \"Cart Add Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Add to Cart\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"addingToCartText\",\n \"displayName\": \"Cart Adding Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Ekleniyor...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"outOfStockText\",\n \"displayName\": \"Sold Out Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sold Out\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"hideQuantityInput\",\n \"displayName\": \"Quantity Selector Hide\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"settings\"\n },\n {\n \"name\": \"errorMessage\",\n \"displayName\": \"Error Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Product sepete eklenemedi\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"optionSetErrorMessage\",\n \"displayName\": \"Option Set Error Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Lütfen gerekli seçenekleri doldurun\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"updateCartButtonText\",\n \"displayName\": \"Sepeti Update Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Update\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"updatingCartText\",\n \"displayName\": \"Cart Updating Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Güncelleniyor...\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n },\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Button ve durum metinleri\"\n },\n {\n \"id\": \"settings\",\n \"name\": \"Settings\",\n \"description\": \"Bileşen davranışını kontrol eden ayarlar\"\n }\n ]\n}"
17765
- },
17766
- {
17767
- "filename": "children/ProductDetailAddToCart/index.tsx",
17768
- "content": "import {\n addItemToCart,\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n getSelectedProductVariant,\n hasProductStock,\n hasProductVariantStock,\n isAddToCartEnabled,\n initProductOptionSetValues,\n IkasBundleSettings,\n IkasStorefrontConfig,\n} from \"@ikas/bp-storefront\";\nimport { useState } from \"preact/hooks\";\nimport { Props } from \"./types\";\nimport Button from \"../../sub-components/Button\";\nimport QuantitySelector from \"../../sub-components/QuantitySelector\";\nimport PayWithIkas from \"./components/PayWithIkas\";\nimport { isBundleOutOfStock } from \"../../utils/bundle\";\nimport { validateOptionSet } from \"../../utils/optionSet\";\nimport { showToast } from \"../../utils/toast\";\n\nexport function ProductDetailAddToCart({\n product,\n addToCartButtonText = \"Sepete Ekle\",\n addingToCartText = \"Ekleniyor...\",\n outOfStockText = \"Tükendi\",\n errorMessage = \"Ürün sepete eklenemedi\",\n optionSetErrorMessage = \"Lütfen gerekli seçenekleri doldurun\",\n updateCartButtonText = \"Güncelle\",\n updatingCartText = \"Güncelleniyor...\",\n hideQuantityInput = false,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n}: Props) {\n const [quantity, setQuantity] = useState(1);\n const [isAddingToCart, setIsAddingToCart] = useState(false);\n\n if (!product) return null;\n\n const editLineID =\n typeof window !== \"undefined\"\n ? new URLSearchParams(window.location.search).get(\"editLineID\")\n : null;\n const isEditMode = !!editLineID;\n\n const selectedVariant = getSelectedProductVariant(product);\n const bundleSettings = selectedVariant?.bundleSettings as\n | IkasBundleSettings\n | undefined;\n const isBundle = !!bundleSettings;\n\n const isEnabled = isAddToCartEnabled(product);\n\n const isOutOfStock = isBundle\n ? isBundleOutOfStock(bundleSettings) || !isEnabled\n : !hasProductStock(product) || !hasProductVariantStock(selectedVariant);\n\n const isDisabled = isOutOfStock || isAddingToCart;\n\n const payWithIkasUrl = IkasStorefrontConfig.getPayWithIkasUrl();\n const routing = IkasStorefrontConfig.getCurrentRouting();\n const showPayWithIkas =\n !isOutOfStock &&\n !!payWithIkasUrl &&\n routing?.locale === \"tr\" &&\n routing?.currencyCode === \"TRY\";\n\n const handleAddToCart = async () => {\n if (isDisabled) return;\n\n if (!validateOptionSet(product.productOptionSet, optionSetErrorMessage))\n return;\n\n if (!isAddToCartEnabled(product)) {\n showToast(errorMessage, \"error\");\n return;\n }\n\n setIsAddingToCart(true);\n try {\n const result = await addItemToCart(selectedVariant, product, quantity);\n\n if (result.success) {\n if (product.productOptionSet) {\n initProductOptionSetValues(product.productOptionSet);\n }\n window.dispatchEvent(new CustomEvent(\"ikas:reset-option-state\"));\n window.dispatchEvent(new CustomEvent(\"ikas:open-cart-sidebar\"));\n } else {\n showToast(errorMessage, \"error\");\n }\n } finally {\n setIsAddingToCart(false);\n }\n };\n\n const getButtonText = () => {\n if (isEditMode) {\n if (isAddingToCart) return updatingCartText;\n if (isOutOfStock) return outOfStockText;\n return updateCartButtonText;\n }\n if (isAddingToCart) return addingToCartText;\n if (isOutOfStock) return outOfStockText;\n return addToCartButtonText;\n };\n\n return (\n <div\n className=\"kombos-pd-atc\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n <div className=\"kombos-pd-atc__actions\">\n {!hideQuantityInput && !isOutOfStock && (\n <QuantitySelector value={quantity} onChange={setQuantity} />\n )}\n <Button\n variant=\"primary\"\n size=\"s\"\n className=\"kombos-pd-atc__btn\"\n disabled={isDisabled}\n onClick={handleAddToCart}\n >\n {getButtonText()}\n </Button>\n </div>\n {showPayWithIkas && (\n <PayWithIkas\n product={product}\n quantity={quantity}\n isEnabled={isEnabled}\n payWithIkasUrl={payWithIkasUrl!}\n />\n )}\n </div>\n );\n}\n\nexport default ProductDetailAddToCart;\n"
17769
- },
17770
- {
17771
- "filename": "children/ProductDetailAddToCart/styles.css",
17772
- "content": "/* ===== ProductDetailAddToCart ===== */\n\n.kombos-pd-atc {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n.kombos-pd-atc__actions {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n}\n\n.kombos-pd-atc__btn {\n flex: 1;\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd-atc {\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n}\n"
17773
- },
17774
- {
17775
- "filename": "children/ProductDetailAddToCart/types.ts",
17776
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n addToCartButtonText?: string;\n addingToCartText?: string;\n outOfStockText?: string;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n hideQuantityInput?: boolean;\n errorMessage?: string;\n optionSetErrorMessage?: string;\n updateCartButtonText?: string;\n updatingCartText?: string;\n}\n"
17777
- },
17778
- {
17779
- "filename": "children/ProductDetailBundleFurniture/components/BundleFurnitureRow/index.tsx",
17780
- "content": "import {\n IkasBundleProduct,\n getSelectedProductVariant,\n shouldDisplayBundleProductPrice,\n getBundleProductFormattedFinalPrice,\n getBundleProductFormattedFinalPriceWithQuantity,\n getBundleProductFormattedSellPrice,\n getBundleProductFormattedSellPriceWithQuantity,\n getBundleProductFinalPrice,\n getBundleProductSellPrice,\n getBundleProductFinalPriceWithQuantity,\n getBundleProductSellPriceWithQuantity,\n hasProductVariantStock,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport type { AspectRatio, ObjectFit } from \"../../../../global-types\";\nimport VariantBadge from \"../../../../sub-components/VariantBadge\";\nimport BundleQuantityBox from \"../../../../sub-components/BundleQuantityBox\";\nimport BundleMedia from \"../../../../sub-components/BundleMedia\";\nimport { adjustBundleProductQuantity } from \"../../../../utils/bundle\";\n\nexport interface BundleFurnitureRowTexts {\n outOfStockText: string;\n}\n\ninterface PriceCellProps {\n formattedFinalPrice: string;\n formattedSellPrice: string;\n hasDiscount: boolean;\n finalTypography: string;\n}\n\nfunction PriceCell({\n formattedFinalPrice,\n formattedSellPrice,\n hasDiscount,\n finalTypography,\n}: PriceCellProps) {\n return (\n <div className=\"kombos-bundle-furniture__price-col\">\n <span className={finalTypography}>{formattedFinalPrice}</span>\n {hasDiscount && (\n <span className=\"kombos-bundle-furniture__old-price text-sm-regular-strike\">\n {formattedSellPrice}\n </span>\n )}\n </div>\n );\n}\n\ninterface Props {\n bundleProduct: IkasBundleProduct;\n texts: BundleFurnitureRowTexts;\n showFeatures: boolean;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n}\n\nconst BundleFurnitureRow = observer(function BundleFurnitureRow({\n bundleProduct,\n texts,\n showFeatures,\n aspectRatio = \"Square\",\n objectFit = \"Cover\",\n}: Props) {\n const product = bundleProduct.product;\n if (!product) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\n const hasStock = selectedVariant\n ? hasProductVariantStock(selectedVariant)\n : false;\n const showPrice = shouldDisplayBundleProductPrice(bundleProduct);\n\n const unitFinalPrice = getBundleProductFinalPrice(bundleProduct);\n const unitSellPrice = getBundleProductSellPrice(bundleProduct);\n const hasUnitDiscount = showPrice && unitFinalPrice !== unitSellPrice;\n\n const totalFinalPrice = getBundleProductFinalPriceWithQuantity(bundleProduct);\n const totalSellPrice = getBundleProductSellPriceWithQuantity(bundleProduct);\n const hasTotalDiscount =\n showPrice &&\n totalFinalPrice !== totalSellPrice &&\n bundleProduct.quantity > 0;\n\n const showTotalPrice = showPrice && bundleProduct.quantity > 0;\n\n return (\n <tr className=\"kombos-bundle-furniture__row\">\n {/* Parts: image + name */}\n <td className=\"kombos-bundle-furniture__cell kombos-bundle-furniture__cell--parts\">\n <div className=\"kombos-bundle-furniture__part-info\">\n <BundleMedia\n variant={selectedVariant}\n alt=\"\"\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n wrapperClassName=\"kombos-bundle-furniture__thumb\"\n mediaClassName=\"kombos-bundle-furniture__thumb-img\"\n placeholderClassName=\"kombos-bundle-furniture__thumb-placeholder\"\n />\n <span className=\"kombos-bundle-furniture__part-name text-sm-medium\">\n {product.name}\n </span>\n </div>\n </td>\n\n {/* Unit Price */}\n <td className=\"kombos-bundle-furniture__cell kombos-bundle-furniture__cell--price\">\n {showPrice ? (\n <PriceCell\n formattedFinalPrice={getBundleProductFormattedFinalPrice(\n bundleProduct,\n )}\n formattedSellPrice={getBundleProductFormattedSellPrice(\n bundleProduct,\n )}\n hasDiscount={hasUnitDiscount}\n finalTypography=\"text-sm-medium\"\n />\n ) : (\n <span className=\"text-sm-regular\">-</span>\n )}\n </td>\n\n {/* Quantity */}\n <td className=\"kombos-bundle-furniture__cell kombos-bundle-furniture__cell--qty\">\n <BundleQuantityBox bundleProduct={bundleProduct} />\n {!hasStock && (\n <span className=\"kombos-bundle-furniture__no-stock text-xs-medium\">\n {texts.outOfStockText}\n </span>\n )}\n </td>\n\n {/* Features (variants) */}\n {showFeatures && (\n <td className=\"kombos-bundle-furniture__cell kombos-bundle-furniture__cell--features\">\n <VariantBadge product={product} disableRoute size=\"xs\" onSelect={() => adjustBundleProductQuantity(bundleProduct)} />\n </td>\n )}\n\n {/* Total Price */}\n <td className=\"kombos-bundle-furniture__cell kombos-bundle-furniture__cell--total\">\n {showTotalPrice ? (\n <PriceCell\n formattedFinalPrice={getBundleProductFormattedFinalPriceWithQuantity(\n bundleProduct,\n )}\n formattedSellPrice={getBundleProductFormattedSellPriceWithQuantity(\n bundleProduct,\n )}\n hasDiscount={hasTotalDiscount}\n finalTypography=\"text-sm-semibold\"\n />\n ) : (\n <span className=\"text-sm-regular\">-</span>\n )}\n </td>\n </tr>\n );\n});\n\nexport default BundleFurnitureRow;\n"
17781
- },
17782
- {
17783
- "filename": "children/ProductDetailBundleFurniture/components/BundleFurnitureSection/index.tsx",
17784
- "content": "import { useState } from \"preact/hooks\";\nimport {\n IkasBundleProduct,\n IkasBundleSettings,\n getDisplayedProductVariantTypes,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport type { AspectRatio, ObjectFit } from \"../../../../global-types\";\nimport { cx } from \"../../../../utils/cx\";\nimport BundleFurnitureRow from \"../BundleFurnitureRow\";\n\ninterface Texts {\n productContentTitle: string;\n partsLabel: string;\n unitPriceLabel: string;\n quantityLabel: string;\n featuresLabel: string;\n totalPriceLabel: string;\n outOfStockText: string;\n addProductItemText: string;\n}\n\ninterface Props {\n bundleSettings: IkasBundleSettings;\n isLoading: boolean;\n texts: Texts;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n}\n\nfunction hasAnyVariants(products: IkasBundleProduct[]): boolean {\n return products.some((bp) => {\n if (!bp.product) return false;\n return getDisplayedProductVariantTypes(bp.product).length > 0;\n });\n}\n\nconst COLUMN_HEADER_CLASS = \"kombos-bundle-furniture__th text-sm-semibold\";\n\nconst BundleFurnitureSection = observer(function BundleFurnitureSection({\n bundleSettings,\n isLoading,\n texts,\n aspectRatio,\n objectFit,\n}: Props) {\n const [isExpanded, setIsExpanded] = useState(true);\n\n const sortedProducts = [...bundleSettings.products].sort(\n (a, b) => a.order - b.order,\n );\n const showFeatures = hasAnyVariants(sortedProducts);\n\n return (\n <div className=\"kombos-bundle-furniture\" id=\"bundle-furniture-section\">\n {/* Collapsible header */}\n <button\n type=\"button\"\n className=\"kombos-bundle-furniture__toggle\"\n onClick={() => setIsExpanded((prev) => !prev)}\n aria-expanded={isExpanded}\n >\n <span className=\"kombos-bundle-furniture__toggle-text text-sm-semibold\">\n {texts.productContentTitle}\n </span>\n <span\n className={cx(\n \"kombos-bundle-furniture__toggle-icon\",\n isExpanded && \"kombos-bundle-furniture__toggle-icon--open\",\n )}\n />\n </button>\n\n {/* Table content */}\n {isExpanded && (\n <div className=\"kombos-bundle-furniture__body\">\n {isLoading ? (\n <div className=\"kombos-bundle-furniture__loading\">\n <div className=\"kombos-bundle-furniture__spinner\" />\n </div>\n ) : (\n <div className=\"kombos-bundle-furniture__table-wrap\">\n <table className=\"kombos-bundle-furniture__table\">\n <colgroup>\n <col style={{ width: showFeatures ? \"33.33%\" : \"40%\" }} />\n <col style={{ width: showFeatures ? \"16.67%\" : \"20%\" }} />\n <col style={{ width: showFeatures ? \"16.67%\" : \"20%\" }} />\n {showFeatures && <col style={{ width: \"16.67%\" }} />}\n <col style={{ width: showFeatures ? \"16.67%\" : \"20%\" }} />\n </colgroup>\n <thead>\n <tr className=\"kombos-bundle-furniture__head-row\">\n <th className={COLUMN_HEADER_CLASS}>{texts.partsLabel}</th>\n <th className={COLUMN_HEADER_CLASS}>\n {texts.unitPriceLabel}\n </th>\n <th className={COLUMN_HEADER_CLASS}>\n {texts.quantityLabel}\n </th>\n {showFeatures && (\n <th className={COLUMN_HEADER_CLASS}>\n {texts.featuresLabel}\n </th>\n )}\n <th\n className={COLUMN_HEADER_CLASS}\n style={{ textAlign: \"right\" }}\n >\n {texts.totalPriceLabel}\n </th>\n </tr>\n </thead>\n <tbody>\n {sortedProducts.map((bp) => (\n <BundleFurnitureRow\n key={bp.id}\n bundleProduct={bp}\n texts={texts}\n showFeatures={showFeatures}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n />\n ))}\n </tbody>\n </table>\n </div>\n )}\n </div>\n )}\n </div>\n );\n});\n\nexport default BundleFurnitureSection;\n"
17785
- },
17786
- {
17787
- "filename": "children/ProductDetailBundleFurniture/components/BundleFurnitureSection/styles.css",
17788
- "content": "/* ===== BundleFurnitureSection ===== */\n.kombos-bundle-furniture {\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n overflow: hidden;\n}\n\n/* Toggle header */\n.kombos-bundle-furniture__toggle {\n display: flex;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n padding: 0.75rem 1rem;\n border: none;\n background-color: var(--kombos-gray-900);\n color: var(--kombos-white);\n cursor: pointer;\n text-align: left;\n}\n\n.kombos-bundle-furniture__toggle-text {\n color: var(--kombos-white);\n}\n\n.kombos-bundle-furniture__toggle-icon {\n position: relative;\n width: 0.75rem;\n height: 0.75rem;\n}\n\n.kombos-bundle-furniture__toggle-icon::before,\n.kombos-bundle-furniture__toggle-icon::after {\n content: \"\";\n position: absolute;\n background-color: var(--kombos-white);\n transition: transform 0.25s ease;\n}\n\n.kombos-bundle-furniture__toggle-icon::before {\n top: 50%;\n left: 0;\n width: 100%;\n height: 0.125rem;\n transform: translateY(-50%);\n}\n\n.kombos-bundle-furniture__toggle-icon::after {\n top: 0;\n left: 50%;\n width: 0.125rem;\n height: 100%;\n transform: translateX(-50%);\n}\n\n.kombos-bundle-furniture__toggle-icon--open::after {\n transform: translateX(-50%) rotate(90deg);\n}\n\n/* Body */\n.kombos-bundle-furniture__body {\n}\n\n.kombos-bundle-furniture__loading {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 2rem;\n}\n\n.kombos-bundle-furniture__spinner {\n width: 1.5rem;\n height: 1.5rem;\n border: 2px solid var(--kombos-gray-200);\n border-top-color: var(--kombos-gray-700);\n border-radius: 50%;\n animation: kombos-furniture-spin 0.6s linear infinite;\n}\n\n@keyframes kombos-furniture-spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n/* Table */\n.kombos-bundle-furniture__table-wrap {\n overflow-x: auto;\n -webkit-overflow-scrolling: touch;\n}\n\n.kombos-bundle-furniture__table {\n width: 100%;\n min-width: 56.25rem;\n border-collapse: collapse;\n table-layout: fixed;\n}\n\n.kombos-bundle-furniture__th {\n text-align: left;\n padding: 0.75rem;\n color: var(--kombos-gray-500);\n border-bottom: 1px solid var(--kombos-gray-200);\n white-space: nowrap;\n}\n\n.kombos-bundle-furniture__row {\n border-bottom: 1px solid var(--kombos-gray-100);\n}\n\n.kombos-bundle-furniture__row:last-child {\n border-bottom: none;\n}\n\n.kombos-bundle-furniture__cell {\n padding: 0.75rem;\n vertical-align: middle;\n}\n\n.kombos-bundle-furniture__cell--parts {\n}\n\n.kombos-bundle-furniture__cell--price,\n.kombos-bundle-furniture__cell--total {\n white-space: nowrap;\n}\n\n.kombos-bundle-furniture__cell--total {\n text-align: right;\n}\n\n.kombos-bundle-furniture__cell--qty {\n}\n\n.kombos-bundle-furniture__cell--features {\n}\n\n/* Part info (image + name) */\n.kombos-bundle-furniture__part-info {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.kombos-bundle-furniture__thumb {\n width: 2.5rem;\n min-width: 2.5rem;\n border-radius: 4px;\n overflow: hidden;\n background-color: var(--kombos-gray-50);\n}\n\n.kombos-bundle-furniture__thumb-img {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.kombos-bundle-furniture__thumb-placeholder {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n font-size: 1rem;\n color: var(--kombos-gray-300);\n}\n\n.kombos-bundle-furniture__part-name {\n color: var(--kombos-gray-900);\n}\n\n/* Price columns */\n.kombos-bundle-furniture__price-col {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n}\n\n.kombos-bundle-furniture__old-price {\n color: var(--kombos-gray-500);\n}\n\n.kombos-bundle-furniture__no-stock {\n display: block;\n margin-top: 0.25rem;\n color: var(--kombos-error);\n}\n"
17789
- },
17790
- {
17791
- "filename": "children/ProductDetailBundleFurniture/ikas-config-snippet.json",
17792
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-bundle-furniture\",\n \"name\": \"ProductDetailBundleFurniture\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailBundleFurniture/index.tsx\",\n \"styles\": \"./src/components/ProductDetailBundleFurniture/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"productContentTitle\",\n \"displayName\": \"Product Content Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Product Content\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"partsLabel\",\n \"displayName\": \"Items Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Items\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"unitPriceLabel\",\n \"displayName\": \"Unit Price Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Unit Price\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"quantityLabel\",\n \"displayName\": \"Quantity Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Quantity\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"featuresLabel\",\n \"displayName\": \"Features Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Features\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"totalPriceLabel\",\n \"displayName\": \"Total Price Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Total Price\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"outOfStockText\",\n \"displayName\": \"Stokta Yok Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Stokta yok\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"addProductItemText\",\n \"displayName\": \"Product Add Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Product Add\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"aspectRatio\",\n \"displayName\": \"En Boy Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"appearance\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"objectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"appearance\",\n \"enumTypeId\": \"GrylMqHxui\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Bileşende görüntülenen metin etiketleri\"\n },\n {\n \"id\": \"margins\",\n \"name\": \"Border Spacing\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü margin ayarları\"\n },\n {\n \"id\": \"appearance\",\n \"name\": \"View\",\n \"description\": \"Image oran ve sığdırma ayarları\"\n }\n ]\n}"
17793
- },
17794
- {
17795
- "filename": "children/ProductDetailBundleFurniture/index.tsx",
17796
- "content": "import {\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport { useBundleProducts } from \"../../hooks/useBundleProducts\";\nimport BundleFurnitureSection from \"./components/BundleFurnitureSection\";\n\nexport function ProductDetailBundleFurniture({\n product,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n productContentTitle = \"Ürün İçeriği\",\n partsLabel = \"Parçalar\",\n unitPriceLabel = \"Birim Fiyat\",\n quantityLabel = \"Adet\",\n featuresLabel = \"Özellikler\",\n totalPriceLabel = \"Toplam Fiyat\",\n outOfStockText = \"Stokta yok\",\n addProductItemText = \"Ürün Ekle\",\n aspectRatio,\n objectFit,\n}: Props) {\n const { isLoading, selectedVariant, bundleSettings } =\n useBundleProducts(product);\n\n if (!product || !selectedVariant || !bundleSettings) return null;\n\n return (\n <div\n className=\"kombos-bundle-furniture-wrapper\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n <BundleFurnitureSection\n bundleSettings={bundleSettings}\n isLoading={isLoading}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n texts={{\n productContentTitle,\n partsLabel,\n unitPriceLabel,\n quantityLabel,\n featuresLabel,\n totalPriceLabel,\n outOfStockText,\n addProductItemText,\n }}\n />\n </div>\n );\n}\n\nexport default ProductDetailBundleFurniture;\n"
17797
- },
17798
- {
17799
- "filename": "children/ProductDetailBundleFurniture/styles.css",
17800
- "content": "/* ===== ProductDetailBundleFurniture ===== */\n.kombos-bundle-furniture-wrapper {\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n@media (min-width: 1024px) {\n .kombos-bundle-furniture-wrapper {\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n}\n"
17801
- },
17802
- {
17803
- "filename": "children/ProductDetailBundleFurniture/types.ts",
17804
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../global-types\";\n\nexport interface Props {\n product?: IkasProduct | null;\n productContentTitle?: string;\n partsLabel?: string;\n unitPriceLabel?: string;\n quantityLabel?: string;\n featuresLabel?: string;\n totalPriceLabel?: string;\n outOfStockText?: string;\n addProductItemText?: string;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n}\n"
17805
- },
17806
- {
17807
- "filename": "children/ProductDetailBundleProduct/components/BundleProductItem/index.tsx",
17808
- "content": "import {\n IkasBundleProduct,\n getSelectedProductVariant,\n getProductHref,\n shouldDisplayBundleProductPrice,\n getBundleProductFinalPrice,\n getBundleProductSellPrice,\n getBundleProductFormattedFinalPrice,\n getBundleProductFormattedSellPrice,\n getBundleProductFormattedFinalPriceWithQuantity,\n getBundleProductFormattedSellPriceWithQuantity,\n getBundleProductFinalPriceWithQuantity,\n getBundleProductSellPriceWithQuantity,\n hasProductVariantStock,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport VariantBadge from \"../../../../sub-components/VariantBadge\";\nimport BundleQuantityBox from \"../../../../sub-components/BundleQuantityBox\";\nimport BundleMedia from \"../../../../sub-components/BundleMedia\";\nimport { adjustBundleProductQuantity } from \"../../../../utils/bundle\";\nimport type { AspectRatio, ObjectFit } from \"../../../../global-types\";\n\ninterface Props {\n bundleProduct: IkasBundleProduct;\n quantityLabel: string;\n outOfStockText: string;\n bundleProductWithoutLink?: boolean;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n}\n\nconst BundleProductItem = observer(function BundleProductItem({\n bundleProduct,\n quantityLabel,\n outOfStockText,\n bundleProductWithoutLink,\n aspectRatio = \"Square\",\n objectFit = \"Cover\",\n}: Props) {\n const product = bundleProduct.product;\n if (!product) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\n const hasStock = selectedVariant\n ? hasProductVariantStock(selectedVariant)\n : false;\n\n const productHref = getProductHref(product);\n\n const showPrice = shouldDisplayBundleProductPrice(bundleProduct);\n const unitFinalPrice = getBundleProductFinalPrice(bundleProduct);\n const unitSellPrice = getBundleProductSellPrice(bundleProduct);\n const hasDiscount = showPrice && unitFinalPrice !== unitSellPrice;\n\n const hasQuantity = bundleProduct.quantity > 1;\n\n const totalFinalPrice = getBundleProductFinalPriceWithQuantity(bundleProduct);\n const totalSellPrice = getBundleProductSellPriceWithQuantity(bundleProduct);\n const hasTotalDiscount =\n showPrice &&\n totalFinalPrice !== totalSellPrice &&\n bundleProduct.quantity > 0;\n\n const ImageWrapper = bundleProductWithoutLink ? \"div\" : \"a\";\n const imageWrapperProps = bundleProductWithoutLink\n ? {}\n : { href: productHref, \"aria-label\": product.name };\n\n const NameWrapper = bundleProductWithoutLink ? \"span\" : \"a\";\n const nameWrapperProps = bundleProductWithoutLink\n ? {}\n : { href: productHref };\n\n return (\n <div className=\"kombos-bundle-item\">\n {/* Image */}\n <ImageWrapper\n className=\"kombos-bundle-item__image-wrap\"\n {...imageWrapperProps}\n >\n <BundleMedia\n variant={selectedVariant}\n alt={product.name}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n wrapperClassName=\"kombos-bundle-item__media-inner\"\n mediaClassName=\"kombos-bundle-item__image\"\n placeholderClassName=\"kombos-bundle-item__image-placeholder\"\n />\n </ImageWrapper>\n {/* Info */}\n <div className=\"kombos-bundle-item__info\">\n {/* Header: name + total price (quantity × unit) */}\n <div className=\"kombos-bundle-item__header\">\n <NameWrapper\n className=\"kombos-bundle-item__name text-sm-semibold\"\n {...nameWrapperProps}\n >\n {product.name}\n </NameWrapper>\n {showPrice && hasQuantity && (\n <div className=\"kombos-bundle-item__total-price\">\n {hasTotalDiscount && (\n <span className=\"kombos-bundle-item__total-sell text-sm-regular-strike\">\n {getBundleProductFormattedSellPriceWithQuantity(\n bundleProduct,\n )}\n </span>\n )}\n <span className=\"kombos-bundle-item__total-final text-sm-semibold\">\n {getBundleProductFormattedFinalPriceWithQuantity(bundleProduct)}\n </span>\n </div>\n )}\n {showPrice && !hasQuantity && (\n <div className=\"kombos-bundle-item__unit-price\">\n {hasDiscount && (\n <span className=\"kombos-bundle-item__sell-price text-sm-regular-strike\">\n {getBundleProductFormattedSellPrice(bundleProduct)}\n </span>\n )}\n <span className=\"kombos-bundle-item__final-price text-sm-semibold\">\n {getBundleProductFormattedFinalPrice(bundleProduct)}\n </span>\n </div>\n )}\n </div>\n\n {/* Variants */}\n <VariantBadge\n product={product}\n disableRoute\n size=\"xs\"\n onSelect={() => adjustBundleProductQuantity(bundleProduct)}\n />\n\n {/* Bottom: quantity + unit price + out of stock */}\n <div className=\"kombos-bundle-item__bottom\">\n <div className=\"kombos-bundle-item__qty-row\">\n <span className=\"kombos-bundle-item__qty-label text-xs-regular\">\n {quantityLabel}\n </span>\n <BundleQuantityBox bundleProduct={bundleProduct} />\n {showPrice && bundleProduct.quantity > 1 && (\n <div className=\"kombos-bundle-item__unit-price\">\n {hasDiscount && (\n <span className=\"kombos-bundle-item__sell-price text-sm-regular-strike\">\n {getBundleProductFormattedSellPrice(bundleProduct)}\n </span>\n )}\n <span className=\"kombos-bundle-item__final-price text-sm-semibold\">\n {getBundleProductFormattedFinalPrice(bundleProduct)}\n </span>\n </div>\n )}\n </div>\n\n {!hasStock && (\n <span className=\"kombos-bundle-item__out-of-stock text-xs-medium\">\n {outOfStockText}\n </span>\n )}\n </div>\n </div>\n </div>\n );\n});\n\nexport default BundleProductItem;\n"
17809
- },
17810
- {
17811
- "filename": "children/ProductDetailBundleProduct/components/BundleProductItem/styles.css",
17812
- "content": "/* ===== BundleProductItem ===== */\n.kombos-bundle-item {\n display: flex;\n gap: 0.75rem;\n padding: 0.75rem 0;\n border-bottom: 1px solid var(--kombos-gray-100);\n}\n\n.kombos-bundle-item__image-wrap {\n display: block;\n width: 6.25rem;\n min-width: 6.25rem;\n height: fit-content;\n border-radius: 6px;\n overflow: hidden;\n background-color: var(--kombos-gray-50);\n text-decoration: none;\n}\n\n.kombos-bundle-item__image {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.kombos-bundle-item__image-placeholder {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n font-size: 2rem;\n color: var(--kombos-gray-300);\n}\n\n.kombos-bundle-item__info {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n flex: 1;\n min-width: 0;\n}\n\n.kombos-bundle-item__header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 0.75rem;\n}\n\n.kombos-bundle-item__name {\n color: var(--kombos-gray-900);\n text-decoration: none;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n width: fit-content;\n}\n\na.kombos-bundle-item__name:hover {\n text-decoration: underline;\n}\n\n.kombos-bundle-item__unit-price {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n flex-shrink: 0;\n white-space: nowrap;\n margin-left: auto;\n}\n\n\n.kombos-bundle-item__final-price {\n color: var(--kombos-gray-900);\n}\n\n.kombos-bundle-item__sell-price {\n color: var(--kombos-gray-500);\n}\n\n.kombos-bundle-item__bottom {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n margin-top: auto;\n}\n\n.kombos-bundle-item__qty-row {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.kombos-bundle-item__qty-label {\n color: var(--kombos-gray-500);\n}\n\n.kombos-bundle-item__total-price {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n flex-shrink: 0;\n white-space: nowrap;\n}\n\n.kombos-bundle-item__total-final {\n color: var(--kombos-gray-900);\n}\n\n.kombos-bundle-item__total-sell {\n color: var(--kombos-gray-500);\n}\n\n.kombos-bundle-item__out-of-stock {\n color: var(--kombos-error);\n}\n\n/* ===== Mobile adjustments (< 768px) ===== */\n@media (max-width: 767px) {\n .kombos-bundle-item__image-wrap {\n width: 4.5rem;\n min-width: 4.5rem;\n }\n\n .kombos-bundle-item__header {\n flex-direction: column;\n gap: 0.25rem;\n }\n\n .kombos-bundle-item__unit-price {\n margin-left: 0;\n }\n\n .kombos-bundle-item__qty-row {\n flex-wrap: wrap;\n }\n}\n\n"
17813
- },
17814
- {
17815
- "filename": "children/ProductDetailBundleProduct/components/BundleSkeletonLoading/index.tsx",
17816
- "content": "interface Props {\n count?: number;\n variant?: \"card\" | \"table\";\n}\n\nexport default function BundleSkeletonLoading({ count = 3, variant = \"card\" }: Props) {\n if (variant === \"table\") {\n return (\n <div className=\"kombos-bundle-skeleton-table\">\n <div className=\"kombos-bundle-skeleton-table__header kombos-bundle-skeleton__shimmer\" />\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className=\"kombos-bundle-skeleton-table__row\">\n <div className=\"kombos-bundle-skeleton__bar kombos-bundle-skeleton__bar--wide kombos-bundle-skeleton__shimmer\" />\n <div className=\"kombos-bundle-skeleton__bar kombos-bundle-skeleton__bar--price kombos-bundle-skeleton__shimmer\" />\n </div>\n ))}\n </div>\n );\n }\n\n return (\n <div className=\"kombos-bundle-skeleton\">\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className=\"kombos-bundle-skeleton__item\">\n <div className=\"kombos-bundle-skeleton__image kombos-bundle-skeleton__shimmer\" />\n <div className=\"kombos-bundle-skeleton__info\">\n <div className=\"kombos-bundle-skeleton__bar kombos-bundle-skeleton__bar--wide kombos-bundle-skeleton__shimmer\" />\n <div className=\"kombos-bundle-skeleton__bar kombos-bundle-skeleton__bar--medium kombos-bundle-skeleton__shimmer\" />\n <div className=\"kombos-bundle-skeleton__bar kombos-bundle-skeleton__bar--narrow kombos-bundle-skeleton__shimmer\" />\n </div>\n </div>\n ))}\n </div>\n );\n}\n"
17817
- },
17818
- {
17819
- "filename": "children/ProductDetailBundleProduct/components/BundleSkeletonLoading/styles.css",
17820
- "content": "/* ===== BundleSkeletonLoading ===== */\n.kombos-bundle-skeleton {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.kombos-bundle-skeleton__item {\n display: flex;\n gap: 0.75rem;\n padding: 0.75rem 0;\n border-bottom: 1px solid var(--kombos-gray-100);\n}\n\n.kombos-bundle-skeleton__image {\n width: 6.25rem;\n min-width: 6.25rem;\n height: 4.75rem;\n border-radius: 6px;\n background-color: var(--kombos-gray-100);\n}\n\n.kombos-bundle-skeleton__info {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n flex: 1;\n}\n\n.kombos-bundle-skeleton__bar {\n height: 0.875rem;\n border-radius: 4px;\n background-color: var(--kombos-gray-100);\n}\n\n.kombos-bundle-skeleton__bar--wide {\n width: 70%;\n}\n\n.kombos-bundle-skeleton__bar--medium {\n width: 50%;\n}\n\n.kombos-bundle-skeleton__bar--narrow {\n width: 35%;\n}\n\n.kombos-bundle-skeleton__bar--price {\n width: 4.5rem;\n margin-left: auto;\n}\n\n/* ===== Table variant ===== */\n.kombos-bundle-skeleton-table {\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n overflow: hidden;\n}\n\n.kombos-bundle-skeleton-table__header {\n height: 2.75rem;\n background-color: var(--kombos-gray-200);\n}\n\n.kombos-bundle-skeleton-table__row {\n display: flex;\n align-items: center;\n padding: 0.75rem 1rem;\n border-bottom: 1px solid var(--kombos-gray-100);\n}\n\n.kombos-bundle-skeleton-table__row:last-child {\n border-bottom: none;\n}\n\n.kombos-bundle-skeleton__shimmer {\n background: linear-gradient(\n 90deg,\n var(--kombos-gray-100) 25%,\n var(--kombos-gray-50) 50%,\n var(--kombos-gray-100) 75%\n );\n background-size: 200% 100%;\n animation: kombos-shimmer 1.5s ease-in-out infinite;\n}\n"
17821
- },
17822
- {
17823
- "filename": "children/ProductDetailBundleProduct/components/FurnitureRow/index.tsx",
17824
- "content": "import {\n shouldDisplayBundleProductPrice,\n getBundleProductFormattedFinalPrice,\n getProductHref,\n IkasBundleProduct,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\n\ninterface Props {\n bundleProduct: IkasBundleProduct;\n withoutLink?: boolean;\n quantityLabel: string;\n}\n\nconst FurnitureRow = observer(function FurnitureRow({\n bundleProduct,\n withoutLink,\n quantityLabel,\n}: Props) {\n const product = bundleProduct.product;\n if (!product || bundleProduct.quantity === 0) return null;\n\n const showPrice = shouldDisplayBundleProductPrice(bundleProduct);\n const formattedPrice = showPrice\n ? getBundleProductFormattedFinalPrice(bundleProduct)\n : \"\";\n const productHref = getProductHref(product);\n\n const NameTag = withoutLink ? \"span\" : \"a\";\n const nameProps = withoutLink ? {} : { href: productHref };\n\n return (\n <tr className=\"kombos-bundle-table__row\">\n <td className=\"kombos-bundle-table__cell kombos-bundle-table__cell--name\">\n <NameTag\n className=\"kombos-bundle-table__link text-sm-regular\"\n {...nameProps}\n >\n {product.name}\n </NameTag>\n </td>\n <td className=\"kombos-bundle-table__cell kombos-bundle-table__cell--price text-sm-medium\">\n {showPrice && formattedPrice\n ? `${bundleProduct.quantity} x ${formattedPrice}`\n : `${quantityLabel} ${bundleProduct.quantity}`}\n </td>\n </tr>\n );\n});\n\nexport default FurnitureRow;\n"
17825
- },
17826
- {
17827
- "filename": "children/ProductDetailBundleProduct/components/FurnitureRow/styles.css",
17828
- "content": ".kombos-bundle-table__row {\n border-bottom: 1px solid var(--kombos-gray-200);\n}\n\n.kombos-bundle-table__row:last-child {\n border-bottom: none;\n}\n\n.kombos-bundle-table__cell {\n padding: 0.75rem 1rem;\n}\n\n.kombos-bundle-table__cell--name {\n color: var(--kombos-gray-900);\n}\n\n.kombos-bundle-table__link {\n color: inherit;\n text-decoration: none;\n}\n\na.kombos-bundle-table__link:hover {\n text-decoration: underline;\n}\n\n.kombos-bundle-table__cell--price {\n color: var(--kombos-gray-900);\n text-align: right;\n white-space: nowrap;\n}\n"
17829
- },
17830
- {
17831
- "filename": "children/ProductDetailBundleProduct/components/FurnitureView/index.tsx",
17832
- "content": "import { IkasBundleProduct } from \"@ikas/bp-storefront\";\nimport BundleSkeletonLoading from \"../BundleSkeletonLoading\";\nimport FurnitureRow from \"../FurnitureRow\";\n\ninterface Props {\n marginStyles: Record<string, string>;\n isLoading: boolean;\n skeletonCount: number;\n sortedProducts: IkasBundleProduct[];\n productContentTitle: string;\n bundleProductWithoutLink?: boolean;\n quantityLabel: string;\n}\n\nexport default function FurnitureView({\n marginStyles,\n isLoading,\n skeletonCount,\n sortedProducts,\n productContentTitle,\n bundleProductWithoutLink,\n quantityLabel,\n}: Props) {\n return (\n <div\n className=\"kombos-bundle\"\n style={{ ...marginStyles, \"--kombos-bundle-table-border\": \"var(--kombos-gray-900)\" }}\n >\n {isLoading ? (\n <BundleSkeletonLoading count={skeletonCount} variant=\"table\" />\n ) : (\n <div className=\"kombos-bundle-table\">\n <div className=\"kombos-bundle-table__header\">\n <span className=\"kombos-bundle-table__title text-md-semibold\">\n {productContentTitle}\n </span>\n </div>\n <table className=\"kombos-bundle-table__table\">\n <tbody>\n {sortedProducts.map((bp) => (\n <FurnitureRow\n key={bp.id}\n bundleProduct={bp}\n withoutLink={bundleProductWithoutLink}\n quantityLabel={quantityLabel}\n />\n ))}\n </tbody>\n </table>\n </div>\n )}\n </div>\n );\n}\n"
17833
- },
17834
- {
17835
- "filename": "children/ProductDetailBundleProduct/components/FurnitureView/styles.css",
17836
- "content": ".kombos-bundle-table {\n border: 1px solid var(--kombos-bundle-table-border, var(--kombos-gray-900));\n border-radius: 6px;\n overflow: hidden;\n}\n\n.kombos-bundle-table__header {\n display: flex;\n align-items: center;\n padding: 0.75rem 1rem;\n background-color: var(--kombos-gray-900);\n color: var(--kombos-white);\n}\n\n.kombos-bundle-table__title {\n color: inherit;\n}\n\n.kombos-bundle-table__table {\n width: 100%;\n border-collapse: collapse;\n}\n"
17837
- },
17838
- {
17839
- "filename": "children/ProductDetailBundleProduct/ikas-config-snippet.json",
17840
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-bundle-product\",\n \"name\": \"ProductDetailBundleProduct\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailBundleProduct/index.tsx\",\n \"styles\": \"./src/components/ProductDetailBundleProduct/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"isBundleFurniture\",\n \"displayName\": \"Furniture Bundle Mode\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"settings\"\n },\n {\n \"name\": \"bundleProductWithoutLink\",\n \"displayName\": \"Product Link Close\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"settings\"\n },\n {\n \"name\": \"quantityLabel\",\n \"displayName\": \"Quantity Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Quantity\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"outOfStockText\",\n \"displayName\": \"Stokta Yok Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Stokta yok\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"productContentTitle\",\n \"displayName\": \"Set Content Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Set içeriği\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"aspectRatio\",\n \"displayName\": \"En Boy Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"appearance\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"objectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"appearance\",\n \"enumTypeId\": \"GrylMqHxui\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n },\n {\n \"id\": \"settings\",\n \"name\": \"Settings\",\n \"description\": \"Bundle davranışını kontrol eden ayarlar\"\n },\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Bileşende görüntülenen metin etiketleri\"\n }\n ]\n}"
17841
- },
17842
- {
17843
- "filename": "children/ProductDetailBundleProduct/index.tsx",
17844
- "content": "import {\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport { useBundleProducts } from \"../../hooks/useBundleProducts\";\nimport BundleProductItem from \"./components/BundleProductItem\";\nimport BundleSkeletonLoading from \"./components/BundleSkeletonLoading\";\nimport FurnitureView from \"./components/FurnitureView\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction buildMarginStyles(\n props: Pick<\n Props,\n | \"mobileMarginTop\"\n | \"mobileMarginBottom\"\n | \"desktopMarginTop\"\n | \"desktopMarginBottom\"\n >,\n) {\n return {\n \"--mobile-mt\": getFormattedMarginTopSize(props.mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(props.mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(props.desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(props.desktopMarginBottom),\n } as Record<string, string>;\n}\n\n// ---------------------------------------------------------------------------\n// Main component\n// ---------------------------------------------------------------------------\n\nexport function ProductDetailBundleProduct({\n product,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n isBundleFurniture,\n bundleProductWithoutLink,\n quantityLabel = \"Adet\",\n outOfStockText = \"Stokta yok\",\n productContentTitle = \"Takım içeriği\",\n aspectRatio,\n objectFit,\n}: Props) {\n const { isLoading, selectedVariant, bundleSettings, sortedProducts } =\n useBundleProducts(product);\n\n if (!product || !selectedVariant || !bundleSettings) return null;\n\n const marginStyles = buildMarginStyles({\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n });\n\n const skeletonCount = sortedProducts.length || 3;\n\n if (isBundleFurniture) {\n return (\n <FurnitureView\n marginStyles={marginStyles}\n isLoading={isLoading}\n skeletonCount={skeletonCount}\n sortedProducts={sortedProducts}\n productContentTitle={productContentTitle}\n bundleProductWithoutLink={bundleProductWithoutLink}\n quantityLabel={quantityLabel}\n />\n );\n }\n\n return (\n <div className=\"kombos-bundle\" style={marginStyles}>\n <div className=\"kombos-bundle__list\">\n {isLoading ? (\n <BundleSkeletonLoading count={skeletonCount} />\n ) : (\n sortedProducts.map((bp) => (\n <BundleProductItem\n key={bp.id}\n bundleProduct={bp}\n quantityLabel={quantityLabel}\n outOfStockText={outOfStockText}\n bundleProductWithoutLink={bundleProductWithoutLink}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n />\n ))\n )}\n </div>\n </div>\n );\n}\n\nexport default ProductDetailBundleProduct;\n"
17845
- },
17846
- {
17847
- "filename": "children/ProductDetailBundleProduct/styles.css",
17848
- "content": "/* ===== ProductDetailBundleProduct ===== */\n.kombos-bundle {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n.kombos-bundle__list {\n display: flex;\n flex-direction: column;\n}\n\n@media (min-width: 1024px) {\n .kombos-bundle {\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n}\n"
17849
- },
17850
- {
17851
- "filename": "children/ProductDetailBundleProduct/types.ts",
17852
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../global-types\";\n\nexport interface Props {\n product?: IkasProduct | null;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n isBundleFurniture?: boolean;\n bundleProductWithoutLink?: boolean;\n quantityLabel?: string;\n outOfStockText?: string;\n productContentTitle?: string;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n}\n"
17853
- },
17854
- {
17855
- "filename": "children/ProductDetailDescription/ikas-config-snippet.json",
17856
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-description\",\n \"name\": \"ProductDetailDescription\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailDescription/index.tsx\",\n \"styles\": \"./src/components/ProductDetailDescription/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Product Features\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"defaultOpen\",\n \"displayName\": \"Start Description Expanded\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"settings\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Components\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-collapsible-content\"\n ]\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n },\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Bileşende görüntülenen metin ayarları\"\n },\n {\n \"id\": \"settings\",\n \"name\": \"Settings\",\n \"description\": \"Bileşenin davranış ayarları\"\n }\n ]\n}"
17857
- },
17858
- {
17859
- "filename": "children/ProductDetailDescription/index.tsx",
17860
- "content": "import {\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport CollapsibleGroup from \"../../sub-components/CollapsibleGroup\";\nimport { Props } from \"./types\";\n\nexport function ProductDetailDescription(props: Props) {\n const {\n product,\n title = \"Ürün Özellikleri\",\n defaultOpen = false,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n components,\n } = props;\n if (!product) return null;\n\n const hasDescription = !!product?.description;\n const hasComponents = Array.isArray(components) && components.length > 0;\n\n if (!hasDescription && !hasComponents) return null;\n\n return (\n <div\n className=\"kombos-pd-desc\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n {hasDescription && (\n <CollapsibleGroup title={title} defaultOpen={defaultOpen}>\n <div\n className=\"kombos-pd-desc__body text-sm-regular kombos-richtext\"\n dangerouslySetInnerHTML={{ __html: product.description }}\n />\n </CollapsibleGroup>\n )}\n {hasComponents && (\n <IkasComponentRenderer\n id=\"product-detail-description\"\n components={components}\n parentProps={props}\n />\n )}\n </div>\n );\n}\n\nexport default ProductDetailDescription;\n"
17861
- },
17862
- {
17863
- "filename": "children/ProductDetailDescription/styles.css",
17864
- "content": "/* ===== ProductDetailDescription ===== */\n\n.kombos-pd-desc {\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n border-top: 1px solid var(--kombos-gray-200);\n}\n\n.kombos-pd-desc__body {\n color: var(--kombos-gray-700);\n max-height: 500px;\n overflow: auto;\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd-desc {\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n}\n"
17865
- },
17866
- {
17867
- "filename": "children/ProductDetailDescription/types.ts",
17868
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n title?: string;\n defaultOpen?: boolean;\n components?: any;\n}\n"
17869
- },
17870
- {
17871
- "filename": "children/ProductDetailFeatures/ikas-config-snippet.json",
17872
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-features\",\n \"name\": \"ProductDetailFeatures\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailFeatures/index.tsx\",\n \"styles\": \"./src/components/ProductDetailFeatures/styles.css\",\n \"props\": [\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Features\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-product-detail-feature-item\"\n ]\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n }\n ]\n}"
17873
- },
17874
- {
17875
- "filename": "children/ProductDetailFeatures/index.tsx",
17876
- "content": "import {\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function ProductDetailFeatures(props: Props) {\n const {\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n components,\n } = props;\n\n const count = components?.length ?? 0;\n if (count === 0) return null;\n\n return (\n <div\n className=\"kombos-pd-features\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n \"--columns\": count,\n }}\n >\n <IkasComponentRenderer\n id=\"product-detail-features\"\n components={components}\n parentProps={props}\n />\n </div>\n );\n}\n\nexport default ProductDetailFeatures;\n"
17877
- },
17878
- {
17879
- "filename": "children/ProductDetailFeatures/styles.css",
17880
- "content": "/* ===== ProductDetailFeatures ===== */\n\n.kombos-pd-features {\n display: grid;\n grid-template-columns: repeat(var(--columns, 1), 1fr);\n gap: 1rem;\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd-features {\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n}\n"
17881
- },
17882
- {
17883
- "filename": "children/ProductDetailFeatures/types.ts",
17884
- "content": "import type { MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n components?: any;\n}\n"
17885
- },
17886
- {
17887
- "filename": "children/ProductDetailNameFavorite/ikas-config-snippet.json",
17888
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-name-favorite\",\n \"name\": \"ProductDetailNameFavorite\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailNameFavorite/index.tsx\",\n \"styles\": \"./src/components/ProductDetailNameFavorite/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"hideFavoriteButton\",\n \"displayName\": \"Favorite Butonunu Hide\",\n \"type\": \"BOOLEAN\",\n \"required\": false\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n }\n ]\n}"
17889
- },
17890
- {
17891
- "filename": "children/ProductDetailNameFavorite/index.tsx",
17892
- "content": "import {\n addIkasProductToFavorites,\n customerStore,\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n hasCustomer,\n isFavoriteIkasProduct,\n removeIkasProductFromFavorites,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport { Heart2SVG, HeartFilledSVG } from \"../../sub-components/icons\";\n\nexport function ProductDetailNameFavorite({\n product,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n hideFavoriteButton,\n}: Props) {\n if (!product) return null;\n\n const isFavorite = isFavoriteIkasProduct(product);\n\n const handleToggleFavorite = async () => {\n const isLoggedIn = hasCustomer(customerStore);\n\n if (!isLoggedIn) {\n Router.navigateToPage(\"LOGIN\");\n return;\n }\n\n if (isFavorite) {\n await removeIkasProductFromFavorites(product);\n } else {\n await addIkasProductToFavorites(product);\n }\n };\n\n return (\n <div\n className=\"kombos-pd-name__row\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n <h1 className=\"kombos-pd-name__title text-xl-medium lg:display-xs-medium\">{product.name}</h1>\n {!hideFavoriteButton && (\n <button\n type=\"button\"\n className=\"kombos-pd-name__fav-btn\"\n onClick={handleToggleFavorite}\n aria-label={isFavorite ? \"Remove from favorites\" : \"Add to favorites\"}\n >\n {isFavorite ? <HeartFilledSVG /> : <Heart2SVG />}\n </button>\n )}\n </div>\n );\n}\n\nexport default ProductDetailNameFavorite;\n"
17893
- },
17894
- {
17895
- "filename": "children/ProductDetailNameFavorite/styles.css",
17896
- "content": "/* ===== ProductDetailNameFavorite ===== */\n\n.kombos-pd-name__row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.5rem;\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n.kombos-pd-name__title {\n flex: 1;\n margin: 0;\n color: var(--kombos-gray-900);\n}\n\n.kombos-pd-name__fav-btn {\n flex-shrink: 0;\n width: 1.5rem;\n height: 1.5rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n background: none;\n cursor: pointer;\n font-size: 1.5rem;\n color: var(--kombos-gray-700);\n padding: 0;\n}\n\n.kombos-pd-name__fav-btn:hover {\n color: var(--kombos-gray-900);\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd-name__row {\n gap: 1.5rem;\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n\n}\n"
17897
- },
17898
- {
17899
- "filename": "children/ProductDetailNameFavorite/types.ts",
17900
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n hideFavoriteButton?: boolean;\n}\n"
17901
- },
17902
- {
17903
- "filename": "children/ProductDetailOffer/components/OfferCard/index.tsx",
17904
- "content": "import {\n IkasProductOffer,\n getSelectedProductVariant,\n getProductVariantMainImage,\n getDefaultSrc,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n hasProductVariantStock,\n isAcceptedProductOffer,\n acceptProductOffer,\n rejectProductOffer,\n getDisplayedProductVariantTypes,\n selectVariantValue,\n getProductHref,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport { cx } from \"../../../../utils/cx\";\nimport Toggle from \"../../../../sub-components/Toggle\";\nimport Select from \"../../../../sub-components/Select\";\nimport { CheckCircleSVG, NoProductSVG } from \"../../../../sub-components/icons\";\n\ninterface Props {\n offer: IkasProductOffer;\n outOfStockText?: string;\n addedToCartBannerText?: string;\n imageAspectRatio?: string;\n imageObjectFit?: string;\n}\n\nconst OfferCard = observer(function OfferCard({\n offer,\n outOfStockText = \"Tükendi\",\n addedToCartBannerText = \"Sepete eklenmiş\",\n imageAspectRatio,\n imageObjectFit,\n}: Props) {\n const product = offer.product;\n if (!product) return null;\n\n const variant = getSelectedProductVariant(product);\n const isSelected = offer.isSelected;\n const isAccepted = isAcceptedProductOffer(offer);\n const inStock = variant ? hasProductVariantStock(variant) : false;\n\n const productImage = variant\n ? getProductVariantMainImage(variant)\n : undefined;\n const image = productImage?.image;\n const imageSrc = image ? getDefaultSrc(image) : undefined;\n\n const price = getProductVariantFormattedFinalPrice(variant);\n const hasDiscount = hasProductVariantDiscount(variant);\n const originalPrice = getProductVariantFormattedSellPrice(variant);\n const variantTypes = getDisplayedProductVariantTypes(product);\n const productHref = getProductHref(product);\n\n function handleToggle(checked: boolean) {\n if (isAccepted) return;\n\n if (checked) {\n acceptProductOffer(offer);\n } else {\n rejectProductOffer(offer);\n }\n }\n\n const imageStyle =\n imageAspectRatio || imageObjectFit\n ? {\n ...(imageAspectRatio && { aspectRatio: imageAspectRatio }),\n ...(imageObjectFit && { objectFit: imageObjectFit }),\n }\n : undefined;\n\n return (\n <div\n className={cx(\n \"kombos-offer-card\",\n isSelected && \"kombos-offer-card--selected\",\n isAccepted && \"kombos-offer-card--accepted\",\n !inStock && \"kombos-offer-card--out-of-stock\",\n )}\n >\n <div className=\"kombos-offer-card__content\">\n <a href={productHref} className=\"kombos-offer-card__link\">\n {image?.isVideo ? (\n <video\n src={imageSrc}\n className=\"kombos-offer-card__image\"\n style={imageStyle}\n muted\n loop\n autoPlay\n playsInline\n >\n <track kind=\"captions\" />\n </video>\n ) : imageSrc ? (\n <img\n className=\"kombos-offer-card__image\"\n src={imageSrc}\n alt={product.name}\n loading=\"lazy\"\n style={imageStyle}\n />\n ) : (\n <div\n className=\"kombos-offer-card__image kombos-offer-card__image--placeholder\"\n style={imageStyle}\n >\n <NoProductSVG />\n </div>\n )}\n </a>\n\n <div className=\"kombos-offer-card__body\">\n <a\n href={productHref}\n className=\"kombos-offer-card__link kombos-offer-card__name text-sm-medium\"\n >\n {product.name}\n </a>\n\n {variantTypes.length > 0 && (\n <div className=\"kombos-offer-card__variants\">\n {variantTypes.map((dvt) => {\n const selected = dvt.displayedVariantValues.find(\n (dvv) => dvv.isSelected,\n );\n const options = dvt.displayedVariantValues.map((dvv) => ({\n label: dvv.variantValue.name,\n value: dvv.variantValue.id,\n }));\n\n return (\n <div\n key={dvt.variantType.id}\n className=\"kombos-offer-card__variant-group\"\n >\n <span className=\"kombos-offer-card__variant-label text-xs-medium\">\n {dvt.variantType.name}\n </span>\n <Select\n disabled={isAccepted}\n size=\"xs\"\n options={options}\n value={selected?.variantValue.id}\n aria-label={dvt.variantType.name}\n onChange={(e) => {\n const val = (e.target as HTMLSelectElement).value;\n const dvv = dvt.displayedVariantValues.find(\n (d) => d.variantValue.id === val,\n );\n if (dvv) {\n selectVariantValue(product, dvv.variantValue, true);\n }\n }}\n style={{\n maxWidth: 180,\n }}\n />\n </div>\n );\n })}\n </div>\n )}\n\n {inStock ? (\n <div className=\"kombos-offer-card__prices\">\n <span className=\"kombos-offer-card__price text-sm-semibold\">\n {price}\n </span>\n\n {hasDiscount && (\n <span className=\"kombos-offer-card__original-price text-sm-regular-strike\">\n {originalPrice}\n </span>\n )}\n </div>\n ) : (\n <span className=\"kombos-offer-card__out-of-stock text-xs-medium\">\n {outOfStockText}\n </span>\n )}\n </div>\n\n <div className=\"kombos-offer-card__toggle\">\n <Toggle\n checked={isSelected || isAccepted}\n onChange={inStock ? handleToggle : undefined}\n disabled={!inStock || isAccepted}\n />\n </div>\n </div>\n\n {isAccepted && (\n <div className=\"kombos-offer-card__banner\">\n <span className=\"kombos-offer-card__banner-icon\">\n <CheckCircleSVG />\n </span>\n <span className=\"text-xs-medium\">{addedToCartBannerText}</span>\n </div>\n )}\n </div>\n );\n});\n\nexport default OfferCard;\n"
17905
- },
17906
- {
17907
- "filename": "children/ProductDetailOffer/components/OfferCard/styles.css",
17908
- "content": "/* ===== Offer Card ===== */\n\n.kombos-offer-card {\n position: relative;\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n}\n\n.kombos-offer-card--selected {\n border-color: var(--kombos-gray-900);\n}\n\n.kombos-offer-card--accepted {\n padding-bottom: 0.75rem;\n}\n\n.kombos-offer-card--out-of-stock {\n opacity: 0.5;\n}\n\n.kombos-offer-card__content {\n display: flex;\n align-items: flex-start;\n gap: 0.75rem;\n padding: 0.75rem;\n}\n\n.kombos-offer-card__image {\n width: 4rem;\n height: auto;\n flex-shrink: 0;\n border-radius: 4px;\n object-fit: cover;\n aspect-ratio: 1 / 1;\n background: var(--kombos-gray-100);\n}\n\n.kombos-offer-card__image--placeholder {\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--kombos-gray-300);\n font-size: 1.5rem;\n}\n\n.kombos-offer-card__body {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n flex: 1;\n min-width: 0;\n}\n\n.kombos-offer-card__link {\n text-decoration: none;\n color: inherit;\n}\n\n.kombos-offer-card__name {\n display: block;\n color: var(--kombos-gray-900);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n width: fit-content;\n max-width: 100%;\n}\n\n.kombos-offer-card__name:hover {\n text-decoration: underline;\n}\n\n.kombos-offer-card__variants {\n display: flex;\n flex-direction: column;\n gap: 0.375rem;\n margin-top: 0.25rem;\n}\n\n.kombos-offer-card__variant-group {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n}\n\n.kombos-offer-card__variant-label {\n color: var(--kombos-gray-500);\n}\n\n.kombos-offer-card__prices {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n margin-top: 0.375rem;\n}\n\n.kombos-offer-card__original-price {\n color: var(--kombos-gray-500);\n}\n\n.kombos-offer-card__price {\n color: var(--kombos-gray-900);\n}\n\n.kombos-offer-card__out-of-stock {\n color: var(--kombos-gray-500);\n}\n\n.kombos-offer-card__toggle {\n flex-shrink: 0;\n margin-top: 0.125rem;\n}\n\n/* Added to cart banner */\n.kombos-offer-card__banner {\n position: absolute;\n bottom: 0;\n left: 50%;\n transform: translate(-50%, 50%);\n display: flex;\n align-items: center;\n gap: 0.25rem;\n padding: 0.375rem 0.5rem;\n background: var(--kombos-gray-900);\n color: var(--kombos-white);\n border-radius: 6px;\n white-space: nowrap;\n z-index: 1;\n}\n\n.kombos-offer-card__banner-icon {\n flex-shrink: 0;\n font-size: 0.875rem;\n display: flex;\n}\n\n@media (min-width: 768px) {\n .kombos-offer-card__content {\n gap: 1rem;\n padding: 1rem;\n }\n\n .kombos-offer-card__image {\n width: 5rem;\n }\n}\n"
17909
- },
17910
- {
17911
- "filename": "children/ProductDetailOffer/components/OfferSummary/index.tsx",
17912
- "content": "import {\n IkasProduct,\n isAcceptedProductOffer,\n getSelectedProductVariant,\n getProductVariantCampaignOffersDiscountPercentage,\n getProductVariantFormattedFinalPriceWithCampaignOffers,\n getProductVariantFormattedSellPriceWithCampaignOffers,\n hasProductVariantStock,\n isAddToCartEnabled,\n cartStore,\n findExistingCartItem,\n changeItemQuantity,\n addItemToCart,\n initProductOptionSetValues,\n} from \"@ikas/bp-storefront\";\nimport { validateOptionSet } from \"../../../../utils/optionSet\";\nimport { showToast } from \"../../../../utils/toast\";\nimport { observer } from \"@ikas/component-utils\";\nimport Button from \"../../../../sub-components/Button\";\nimport Tag from \"../../../../sub-components/Tag\";\nimport { InfoCircleSVG } from \"../../../../sub-components/icons\";\nimport { useState } from \"preact/hooks\";\n\ninterface Props {\n product: IkasProduct;\n offerInfoText: string;\n totalText: string;\n advantageousTotalText: string;\n isInline: boolean;\n addingToCartText: string;\n addToCartTogetherText: string;\n errorMessage: string;\n optionSetErrorMessage: string;\n}\n\nconst OfferSummary = observer(function OfferSummary({\n product,\n offerInfoText,\n totalText,\n advantageousTotalText,\n isInline,\n addingToCartText,\n addToCartTogetherText,\n errorMessage,\n optionSetErrorMessage,\n}: Props) {\n const [isAdding, setIsAdding] = useState(false);\n\n const selectedVariant = getSelectedProductVariant(product);\n\n const discountPercentage =\n getProductVariantCampaignOffersDiscountPercentage(selectedVariant);\n const finalPrice =\n getProductVariantFormattedFinalPriceWithCampaignOffers(selectedVariant);\n const sellPrice =\n getProductVariantFormattedSellPriceWithCampaignOffers(selectedVariant);\n const hasDiscount = discountPercentage > 0;\n const mainInStock = selectedVariant\n ? hasProductVariantStock(selectedVariant)\n : false;\n\n const acceptedCount =\n product.offers\n ?.filter((o) => !isAcceptedProductOffer(o))\n .reduce((acc, offer) => acc + (offer.isSelected ? 1 : 0), 0) || 0;\n\n async function handleAddToCartTogether() {\n if (isAdding || !mainInStock) return;\n\n if (!validateOptionSet(product.productOptionSet, optionSetErrorMessage))\n return;\n\n if (!isAddToCartEnabled(product!)) {\n showToast(errorMessage, \"error\");\n return;\n }\n\n setIsAdding(true);\n try {\n const cart = cartStore.cart;\n if (cart) {\n const existingItem = findExistingCartItem(\n cart,\n selectedVariant!,\n product!,\n );\n if (existingItem) {\n const result = await changeItemQuantity(\n existingItem,\n existingItem.quantity + 1,\n product!.offers,\n product!,\n );\n if (result.success) {\n if (product.productOptionSet) {\n initProductOptionSetValues(product.productOptionSet);\n }\n window.dispatchEvent(new CustomEvent(\"ikas:reset-option-state\"));\n window.dispatchEvent(new CustomEvent(\"ikas:open-cart-sidebar\"));\n } else {\n showToast(errorMessage, \"error\");\n }\n return;\n }\n }\n\n const result = await addItemToCart(selectedVariant!, product!, 1);\n if (result.success) {\n if (product.productOptionSet) {\n initProductOptionSetValues(product.productOptionSet);\n }\n window.dispatchEvent(new CustomEvent(\"ikas:reset-option-state\"));\n window.dispatchEvent(new CustomEvent(\"ikas:open-cart-sidebar\"));\n } else {\n showToast(errorMessage, \"error\");\n }\n } finally {\n setIsAdding(false);\n }\n }\n\n return (\n <div className=\"kombos-pd-offer__summary\">\n <div className=\"kombos-pd-offer__summary-info\">\n <span className=\"kombos-pd-offer__summary-info-icon\">\n <InfoCircleSVG />\n </span>\n <span className=\"text-xs-regular\">{offerInfoText}</span>\n </div>\n\n {hasDiscount && (\n <div className=\"kombos-pd-offer__total-row\">\n <span className=\"kombos-pd-offer__total-label text-sm-regular\">\n {totalText}\n </span>\n <span className=\"kombos-pd-offer__total-price--strike text-sm-regular\">\n {sellPrice}\n </span>\n </div>\n )}\n\n <div className=\"kombos-pd-offer__discount-row\">\n <span className=\"kombos-pd-offer__total-label text-sm-medium\">\n {hasDiscount ? advantageousTotalText : totalText}\n </span>\n <div className=\"kombos-pd-offer__discount-values\">\n {hasDiscount && (\n <Tag type=\"dark\" size=\"s\">\n -{Math.round(discountPercentage)}%\n </Tag>\n )}\n <span className=\"kombos-pd-offer__total-price text-md-semibold\">\n {finalPrice}\n </span>\n </div>\n </div>\n\n {!isInline && (\n <Button\n variant=\"primary\"\n size=\"s\"\n className=\"kombos-pd-offer__btn\"\n disabled={isAdding || !mainInStock}\n onClick={handleAddToCartTogether}\n >\n {isAdding\n ? addingToCartText\n : `${addToCartTogetherText} (${acceptedCount + 1})`}\n </Button>\n )}\n </div>\n );\n});\n\nexport default OfferSummary;\n"
17913
- },
17914
- {
17915
- "filename": "children/ProductDetailOffer/ikas-config-snippet.json",
17916
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-offer\",\n \"name\": \"ProductDetailOffer\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailOffer/index.tsx\",\n \"styles\": \"./src/components/ProductDetailOffer/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"preselectOffers\",\n \"displayName\": \"Offers Auto Seç\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"layout\"\n },\n {\n \"name\": \"addToCartTogetherText\",\n \"displayName\": \"Birlikte Cart Add Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Add Bundle to Cart\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"addingToCartText\",\n \"displayName\": \"Adding Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Ekleniyor...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"offerInfoText\",\n \"displayName\": \"Offer Info Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Seçtiğiniz ürünleri birlikte sepete ekleyerek indirimden yararlanın.\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"totalText\",\n \"displayName\": \"Total Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Total\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"advantageousTotalText\",\n \"displayName\": \"Discounted Total Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Discounted Total\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"outOfStockText\",\n \"displayName\": \"Sold Out Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sold Out\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"imageAspectRatio\",\n \"displayName\": \"Image En-Boy Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"image\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"imageObjectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"image\",\n \"enumTypeId\": \"GrylMqHxui\"\n },\n {\n \"name\": \"addedToCartBannerText\",\n \"displayName\": \"Cart Added Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Cart eklenmiş\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"compactView\",\n \"displayName\": \"Compact View\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"layout\"\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"errorMessage\",\n \"displayName\": \"Error Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Product sepete eklenemedi\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"optionSetErrorMessage\",\n \"displayName\": \"Option Set Error Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Lütfen gerekli seçenekleri doldurun\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Kullanıcıya görünen tüm metin içerikleri\"\n },\n {\n \"id\": \"layout\",\n \"name\": \"Layout\",\n \"description\": \"Bileşen düzen ve davranış ayarları\"\n },\n {\n \"id\": \"image\",\n \"name\": \"Image\",\n \"description\": \"Image boyut ve sığdırma ayarları\"\n },\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ]\n }\n ]\n}"
17917
- },
17918
- {
17919
- "filename": "children/ProductDetailOffer/index.tsx",
17920
- "content": "import {\n getDefaultSrc,\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n getProductVariantFormattedFinalPrice,\n getProductVariantMainImage,\n getSelectedProductVariant,\n acceptProductOffer,\n rejectProductOffer,\n} from \"@ikas/bp-storefront\";\nimport { useEffect } from \"preact/hooks\";\nimport { cx } from \"../../utils/cx\";\nimport { resolveAspectRatio, resolveObjectFit } from \"../../utils/media\";\nimport { Props } from \"./types\";\nimport OfferCard from \"./components/OfferCard\";\nimport OfferSummary from \"./components/OfferSummary\";\nimport { NoProductSVG } from \"../../sub-components/icons\";\n\nexport function ProductDetailOffer({\n product,\n compactView = false,\n preselectOffers = false,\n addToCartTogetherText = \"Birlikte Sepete Ekle\",\n addingToCartText = \"Ekleniyor...\",\n offerInfoText = \"Seçtiğiniz ürünleri birlikte sepete ekleyerek indirimden yararlanın.\",\n totalText = \"Toplam\",\n advantageousTotalText = \"Avantajlı Toplam\",\n outOfStockText = \"Tükendi\",\n imageAspectRatio,\n imageObjectFit,\n addedToCartBannerText = \"Sepete eklenmiş\",\n errorMessage = \"Ürün sepete eklenemedi\",\n optionSetErrorMessage = \"Lütfen gerekli seçenekleri doldurun\",\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n}: Props) {\n useEffect(() => {\n product?.offers?.forEach((offer) => {\n if (preselectOffers) {\n acceptProductOffer(offer);\n } else {\n rejectProductOffer(offer);\n }\n });\n }, [preselectOffers]);\n\n if (!product?.offers?.length) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\n\n const appliedCampaign = product.appliedCampaignOffer;\n\n const mainImage = getProductVariantMainImage(selectedVariant);\n const mainMedia = mainImage?.image;\n const mainImageSrc = mainMedia ? getDefaultSrc(mainMedia) : undefined;\n const mainPrice = getProductVariantFormattedFinalPrice(selectedVariant);\n\n // Resolve image styles\n const resolvedAspectRatio = resolveAspectRatio(imageAspectRatio);\n const resolvedObjectFit = resolveObjectFit(imageObjectFit);\n\n const isInline = compactView;\n\n const mainImageStyle = {\n aspectRatio: resolvedAspectRatio,\n objectFit: resolvedObjectFit,\n };\n\n const summaryPanel = (\n <OfferSummary\n product={product}\n offerInfoText={offerInfoText}\n totalText={totalText}\n advantageousTotalText={advantageousTotalText}\n isInline={isInline}\n addingToCartText={addingToCartText}\n addToCartTogetherText={addToCartTogetherText}\n errorMessage={errorMessage}\n optionSetErrorMessage={optionSetErrorMessage}\n />\n );\n\n return (\n <div\n className={cx(\"kombos-pd-offer\", isInline && \"kombos-pd-offer--inline\")}\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n {/* Campaign header */}\n {appliedCampaign &&\n (appliedCampaign.title || appliedCampaign.description) && (\n <div className=\"kombos-pd-offer__header\">\n {appliedCampaign.title && (\n <span className=\"kombos-pd-offer__title text-md-semibold md:text-lg-semibold\">\n {appliedCampaign.title}\n </span>\n )}\n {appliedCampaign.description && (\n <span className=\"kombos-pd-offer__desc text-sm-regular\">\n {appliedCampaign.description}\n </span>\n )}\n </div>\n )}\n\n {/* Grid wrapper for standalone mode */}\n {!isInline ? (\n <div className=\"kombos-pd-offer__grid\">\n {/* Cards area: main product + offers in 2-col grid */}\n <div className=\"kombos-pd-offer__cards\">\n {/* Main product card */}\n <div className=\"kombos-pd-offer__main-product\">\n <div className=\"kombos-pd-offer__main-content\">\n {mainMedia?.isVideo ? (\n <video\n src={mainImageSrc}\n className=\"kombos-pd-offer__main-image\"\n style={mainImageStyle}\n muted\n loop\n autoPlay\n playsInline\n >\n <track kind=\"captions\" />\n </video>\n ) : mainImageSrc ? (\n <img\n className=\"kombos-pd-offer__main-image\"\n src={mainImageSrc}\n alt={product.name}\n loading=\"lazy\"\n style={mainImageStyle}\n />\n ) : (\n <div\n className=\"kombos-pd-offer__main-image kombos-pd-offer__main-image--placeholder\"\n style={mainImageStyle}\n >\n <NoProductSVG />\n </div>\n )}\n <div className=\"kombos-pd-offer__main-body\">\n <span className=\"kombos-pd-offer__main-name text-sm-medium\">\n {product.name}\n </span>\n <span className=\"kombos-pd-offer__main-price text-sm-semibold\">\n {mainPrice}\n </span>\n </div>\n </div>\n </div>\n\n {/* Offer cards */}\n {product.offers.map((offer) => (\n <OfferCard\n key={offer.campaignOfferProductId}\n offer={offer}\n outOfStockText={outOfStockText}\n addedToCartBannerText={addedToCartBannerText}\n imageAspectRatio={resolvedAspectRatio}\n imageObjectFit={resolvedObjectFit}\n />\n ))}\n </div>\n\n {/* Summary panel */}\n {summaryPanel}\n </div>\n ) : (\n <>\n {/* Inline mode: vertical stack, no main product, no button */}\n <div className=\"kombos-pd-offer__list\">\n {product.offers.map((offer) => (\n <OfferCard\n key={offer.campaignOfferProductId}\n offer={offer}\n outOfStockText={outOfStockText}\n addedToCartBannerText={addedToCartBannerText}\n imageAspectRatio={resolvedAspectRatio}\n imageObjectFit={resolvedObjectFit}\n />\n ))}\n </div>\n\n {/* Summary (no button in inline mode) */}\n {summaryPanel}\n </>\n )}\n </div>\n );\n}\n\nexport default ProductDetailOffer;\n"
17921
- },
17922
- {
17923
- "filename": "children/ProductDetailOffer/styles.css",
17924
- "content": "/* ===== Product Detail Offer ===== */\n\n.kombos-pd-offer {\n display: flex;\n flex-direction: column;\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n/* Inline mode (compactView) */\n.kombos-pd-offer--inline {\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n padding: 1rem;\n gap: 1rem;\n}\n\n/* Header */\n.kombos-pd-offer__header {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n padding-block: 0.75rem;\n border-radius: 6px;\n}\n\n.kombos-pd-offer__title {\n color: inherit;\n}\n\n.kombos-pd-offer__desc {\n color: inherit;\n opacity: 0.8;\n}\n\n/* Grid layout (standalone mode): cards + summary side by side */\n.kombos-pd-offer__grid {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n/* Cards area: 2-col grid with main product + offer cards */\n.kombos-pd-offer__cards {\n display: grid;\n grid-template-columns: 1fr;\n gap: 0.75rem;\n}\n\n.kombos-pd-offer--inline .kombos-pd-offer__header {\n padding: 0;\n}\n\n.kombos-pd-offer--inline .kombos-pd-offer__summary {\n width: 100%;\n max-width: none;\n}\n\n/* Offer list (inline mode) */\n.kombos-pd-offer__list {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n/* Main product card (standalone mode) — same style as OfferCard */\n.kombos-pd-offer__main-product {\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n overflow: hidden;\n}\n\n.kombos-pd-offer__main-content {\n display: flex;\n align-items: flex-start;\n gap: 0.75rem;\n padding: 0.75rem;\n}\n\n.kombos-pd-offer__main-image {\n width: 4rem;\n flex-shrink: 0;\n border-radius: 4px;\n object-fit: cover;\n aspect-ratio: 1 / 1;\n background: var(--kombos-gray-100);\n}\n\n.kombos-pd-offer__main-image--placeholder {\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--kombos-gray-300);\n font-size: 1.5rem;\n}\n\n.kombos-pd-offer__main-body {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n flex: 1;\n min-width: 0;\n}\n\n.kombos-pd-offer__main-name {\n color: var(--kombos-gray-900);\n}\n\n.kombos-pd-offer__main-price {\n color: var(--kombos-gray-900);\n}\n\n/* Summary panel */\n.kombos-pd-offer__summary {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n padding: 1rem;\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n background: var(--kombos-gray-50);\n}\n\n.kombos-pd-offer__summary-info {\n display: flex;\n align-items: flex-start;\n gap: 0.5rem;\n color: var(--kombos-gray-600);\n}\n\n.kombos-pd-offer__summary-info-icon {\n flex-shrink: 0;\n font-size: 1rem;\n margin-top: 0.0625rem;\n}\n\n.kombos-pd-offer__total-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n.kombos-pd-offer__total-label {\n color: var(--kombos-gray-500);\n}\n\n.kombos-pd-offer__total-price {\n color: var(--kombos-gray-900);\n}\n\n.kombos-pd-offer__total-price--strike {\n color: var(--kombos-gray-500);\n text-decoration: line-through;\n}\n\n.kombos-pd-offer__discount-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n.kombos-pd-offer__discount-values {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.kombos-pd-offer__btn {\n width: 100%;\n margin-top: auto;\n}\n\n@media (min-width: 768px) {\n .kombos-pd-offer__header {\n padding-block: 1rem;\n }\n\n .kombos-pd-offer__cards {\n grid-template-columns: 1fr 1fr;\n }\n\n .kombos-pd-offer__main-content {\n gap: 1rem;\n padding: 1rem;\n }\n\n .kombos-pd-offer__main-image {\n width: 5rem;\n }\n}\n\n@media (min-width: 1024px) {\n .kombos-pd-offer {\n margin-top: var(--desktop-mt);\n margin-bottom: var(--desktop-mb);\n }\n\n .kombos-pd-offer__grid {\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 1rem;\n align-items: start;\n }\n\n .kombos-pd-offer__summary {\n min-width: 16rem;\n max-width: 20rem;\n justify-content: space-between;\n align-self: stretch;\n }\n}\n"
17925
- },
17926
- {
17927
- "filename": "children/ProductDetailOffer/types.ts",
17928
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../global-types\";\n\nexport interface Props {\n product?: IkasProduct | null;\n preselectOffers?: boolean;\n addToCartTogetherText?: string;\n addingToCartText?: string;\n offerInfoText?: string;\n totalText?: string;\n advantageousTotalText?: string;\n outOfStockText?: string;\n imageAspectRatio?: AspectRatio;\n imageObjectFit?: ObjectFit;\n addedToCartBannerText?: string;\n compactView?: boolean;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n errorMessage?: string;\n optionSetErrorMessage?: string;\n}\n"
17929
- },
17930
- {
17931
- "filename": "children/ProductDetailOptionSet/components/OptionCheckbox/index.tsx",
17932
- "content": "import {\n IkasProductOption,\n isChecked as isOptionChecked,\n setCheckboxValue,\n hasError as hasOptionError,\n getProductOptionFormattedLabel,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport Checkbox from \"../../../../sub-components/Checkbox\";\nimport FormItem from \"../../../../sub-components/FormItem\";\n\ninterface Props {\n option: IkasProductOption;\n showError: boolean;\n requiredErrorText: string;\n}\n\nconst OptionCheckbox = observer(function OptionCheckbox({\n option,\n showError,\n requiredErrorText,\n}: Props) {\n const checked = isOptionChecked(option);\n const errored = showError && hasOptionError(option);\n const label = getProductOptionFormattedLabel(option);\n\n return (\n <FormItem\n status={errored ? \"error\" : \"default\"}\n helper={errored ? requiredErrorText : undefined}\n >\n <div className=\"kombos-option-checkbox__row\">\n <Checkbox\n checked={checked}\n onChange={(val) => setCheckboxValue(option, val)}\n />\n <div className=\"kombos-option-checkbox__text\">\n <span className=\"kombos-option-checkbox__label text-sm-medium\">\n {label}\n </span>\n {option.optionalText && (\n <span className=\"kombos-option-checkbox__desc text-xs-regular\">\n {option.optionalText}\n </span>\n )}\n </div>\n </div>\n </FormItem>\n );\n});\n\nexport default OptionCheckbox;\n"
17933
- },
17934
- {
17935
- "filename": "children/ProductDetailOptionSet/components/OptionCheckbox/styles.css",
17936
- "content": ".kombos-option-checkbox__row {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.kombos-option-checkbox__text {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n}\n\n.kombos-option-checkbox__label {\n color: var(--kombos-gray-700);\n}\n\n.kombos-option-checkbox__desc {\n color: var(--kombos-gray-500);\n}\n"
17937
- },
17938
- {
17939
- "filename": "children/ProductDetailOptionSet/components/OptionChoice/components/ChoiceBox/index.tsx",
17940
- "content": "import {\n IkasProductOption,\n selectValue,\n isProductOptionSelectValueSelected,\n getProductOptionFormattedLabel,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport { cx } from \"../../../../../../utils/cx\";\nimport FormItem from \"../../../../../../sub-components/FormItem\";\nimport { getSelectValuePrice } from \"../../../../../../utils/optionPrice\";\n\ninterface Props {\n option: IkasProductOption;\n errored: boolean;\n requiredErrorText: string;\n constraintText?: string;\n}\n\nconst ChoiceBox = observer(function ChoiceBox({\n option,\n errored,\n requiredErrorText,\n constraintText,\n}: Props) {\n const settings = option.selectSettings!;\n\n return (\n <FormItem\n label={getProductOptionFormattedLabel(option)}\n status={errored ? \"error\" : \"default\"}\n description={option.optionalText}\n helper={errored ? requiredErrorText : constraintText}\n >\n <div className=\"kombos-option-choice__grid\">\n {settings.values.map((val) => {\n const isSelected = isProductOptionSelectValueSelected(option, val);\n const price = getSelectValuePrice(option, val);\n\n return (\n <button\n key={val.id}\n type=\"button\"\n className={cx(\"kombos-option-choice__box\", \"text-sm-medium\", isSelected && \"kombos-option-choice__box--selected\")}\n onClick={() => selectValue(option, val)}\n >\n <span>{val.value}</span>\n {price && (\n <span className=\"kombos-option-choice__box-price text-xs-regular\">\n {price}\n </span>\n )}\n </button>\n );\n })}\n </div>\n </FormItem>\n );\n});\n\nexport default ChoiceBox;\n"
17941
- },
17942
- {
17943
- "filename": "children/ProductDetailOptionSet/components/OptionChoice/components/ChoiceBox/styles.css",
17944
- "content": "/* Box grid */\n.kombos-option-choice__grid {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n\n.kombos-option-choice__box {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 0.125rem;\n padding: 0.5rem 1rem;\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n background: var(--kombos-white);\n color: var(--kombos-gray-700);\n cursor: pointer;\n transition: border-color 0.15s;\n}\n\n.kombos-option-choice__box:hover {\n border-color: var(--kombos-gray-400);\n}\n\n.kombos-option-choice__box--selected {\n border-color: var(--kombos-gray-900);\n background: var(--kombos-gray-900);\n color: var(--kombos-white);\n}\n\n.kombos-option-choice__box-price {\n color: var(--kombos-gray-500);\n}\n\n.kombos-option-choice__box--selected .kombos-option-choice__box-price {\n color: var(--kombos-white);\n}\n"
17945
- },
17946
- {
17947
- "filename": "children/ProductDetailOptionSet/components/OptionChoice/components/ChoiceSelect/index.tsx",
17948
- "content": "import {\n IkasProductOption,\n selectValue,\n clearValues,\n isProductOptionSelectValueSelected,\n getProductOptionFormattedLabel,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport { cx } from \"../../../../../../utils/cx\";\nimport Select from \"../../../../../../sub-components/Select\";\nimport FormItem from \"../../../../../../sub-components/FormItem\";\nimport Checkbox from \"../../../../../../sub-components/Checkbox\";\nimport { getSelectValuePrice } from \"../../../../../../utils/optionPrice\";\n\ninterface Props {\n option: IkasProductOption;\n errored: boolean;\n requiredErrorText: string;\n placeholderText: string;\n constraintText?: string;\n}\n\nconst ChoiceSelect = observer(function ChoiceSelect({\n option,\n errored,\n requiredErrorText,\n placeholderText,\n constraintText,\n}: Props) {\n const settings = option.selectSettings!;\n const selectedId = option.values?.[0] ?? \"\";\n const selectOptions = settings.values.map((v) => ({\n label: `${v.value} (${getSelectValuePrice(option, v)})`,\n value: v.id,\n }));\n\n const isMulti = (settings.maxSelect ?? 0) > 1;\n const selectId = !isMulti ? `option-choice-${option.id}` : undefined;\n\n return (\n <FormItem\n label={getProductOptionFormattedLabel(option)}\n htmlFor={selectId}\n status={errored ? \"error\" : \"default\"}\n description={option.optionalText}\n helper={errored ? requiredErrorText : constraintText}\n >\n {isMulti ? (\n <div\n className={cx(\n \"kombos-option-choice__checklist\",\n errored && \"kombos-option-choice__checklist--error\",\n )}\n >\n {settings.values.map((val) => {\n const isSelected = isProductOptionSelectValueSelected(option, val);\n const price = getSelectValuePrice(option, val);\n\n return (\n <div\n key={val.id}\n className=\"kombos-option-choice__checklist-row\"\n onClick={() => selectValue(option, val)}\n >\n <Checkbox\n checked={isSelected}\n onChange={() => selectValue(option, val)}\n />\n <span className=\"text-sm-medium\">{val.value}</span>\n {price && (\n <span className=\"kombos-option-choice__checklist-price text-xs-regular\">\n {price}\n </span>\n )}\n </div>\n );\n })}\n </div>\n ) : (\n <Select\n id={selectId}\n allowClear\n options={[{ label: placeholderText, value: \"\" }, ...selectOptions]}\n value={selectedId}\n onChange={(e: Event) => {\n const id = (e.target as HTMLSelectElement).value;\n if (!id) {\n clearValues(option, true);\n return;\n }\n const val = settings.values.find((v) => v.id === id);\n if (val) selectValue(option, val);\n }}\n onClear={() => clearValues(option, true)}\n size=\"xs\"\n />\n )}\n </FormItem>\n );\n});\n\nexport default ChoiceSelect;\n"
17949
- },
17950
- {
17951
- "filename": "children/ProductDetailOptionSet/components/OptionChoice/components/ChoiceSelect/styles.css",
17952
- "content": "/* Checklist (multi-select) */\n.kombos-option-choice__checklist {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n padding: 0.5rem;\n max-height: 15rem;\n overflow-y: auto;\n}\n\n.kombos-option-choice__checklist--error {\n border-color: var(--kombos-error);\n}\n\n.kombos-option-choice__checklist-row {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.375rem 0.5rem;\n border-radius: 6px;\n cursor: pointer;\n transition: background-color 0.15s;\n}\n\n.kombos-option-choice__checklist-row:hover {\n background-color: var(--kombos-gray-50);\n}\n\n.kombos-option-choice__checklist-price {\n color: var(--kombos-gray-500);\n margin-left: auto;\n}\n"
17953
- },
17954
- {
17955
- "filename": "children/ProductDetailOptionSet/components/OptionChoice/components/ChoiceSwatch/index.tsx",
17956
- "content": "import { useState } from \"preact/hooks\";\nimport {\n IkasProductOption,\n selectValue,\n isProductOptionSelectValueSelected,\n getProductOptionFormattedLabel,\n getSrc,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport { cx } from \"../../../../../../utils/cx\";\nimport FormItem from \"../../../../../../sub-components/FormItem\";\nimport Checkbox from \"../../../../../../sub-components/Checkbox\";\nimport ImagePreviewModal from \"../../../../../../sub-components/ImagePreviewModal\";\nimport { EyeSVG } from \"../../../../../../sub-components/icons\";\nimport { getSelectValuePrice } from \"../../../../../../utils/optionPrice\";\n\ninterface Props {\n option: IkasProductOption;\n errored: boolean;\n requiredErrorText: string;\n constraintText?: string;\n}\n\nconst ChoiceSwatch = observer(function ChoiceSwatch({\n option,\n errored,\n requiredErrorText,\n constraintText,\n}: Props) {\n const settings = option.selectSettings;\n const [preview, setPreview] = useState<{\n src: string;\n alt: string;\n } | null>(null);\n\n return (\n <FormItem\n label={getProductOptionFormattedLabel(option)}\n status={errored ? \"error\" : \"default\"}\n description={option.optionalText}\n helper={errored ? requiredErrorText : constraintText}\n >\n <div className=\"kombos-option-choice__swatch-grid\">\n {settings?.values?.map((val) => {\n const isSelected = isProductOptionSelectValueSelected(option, val);\n const thumbSrc = val.thumbnailImage\n ? getSrc(val.thumbnailImage, 180)\n : null;\n const price = getSelectValuePrice(option, val);\n\n return (\n <div key={val.id} className=\"kombos-option-choice__swatch-item\">\n <button\n type=\"button\"\n className={cx(\"kombos-option-choice__swatch-btn\", isSelected && \"kombos-option-choice__swatch-btn--selected\")}\n onClick={() => {\n if (thumbSrc) {\n const largeSrc = getSrc(val.thumbnailImage!, 800);\n setPreview({ src: largeSrc, alt: val.value });\n } else {\n selectValue(option, val);\n }\n }}\n title={val.value}\n >\n {thumbSrc ? (\n <>\n <img\n src={thumbSrc}\n alt={val.value}\n className=\"kombos-option-choice__swatch-img\"\n />\n <div className=\"kombos-option-choice__swatch-eye\">\n <EyeSVG />\n </div>\n </>\n ) : (\n <span\n className=\"kombos-option-choice__swatch-color\"\n style={{\n backgroundColor: val.colorCode ?? \"transparent\",\n }}\n />\n )}\n </button>\n {thumbSrc ? (\n <div className=\"kombos-option-choice__swatch-footer\">\n <Checkbox\n checked={isSelected}\n onChange={() => selectValue(option, val)}\n />\n {price && (\n <span className=\"kombos-option-choice__swatch-price text-xs-regular\">\n {price}\n </span>\n )}\n </div>\n ) : (\n price && (\n <span className=\"kombos-option-choice__swatch-price text-xs-regular\">\n {price}\n </span>\n )\n )}\n </div>\n );\n })}\n </div>\n\n {preview && (\n <ImagePreviewModal\n src={preview.src}\n alt={preview.alt}\n onClose={() => setPreview(null)}\n />\n )}\n </FormItem>\n );\n});\n\nexport default ChoiceSwatch;\n"
17957
- },
17958
- {
17959
- "filename": "children/ProductDetailOptionSet/components/OptionChoice/components/ChoiceSwatch/styles.css",
17960
- "content": "/* Swatch grid */\n.kombos-option-choice__swatch-grid {\n display: flex;\n flex-wrap: wrap;\n gap: 0.625rem;\n}\n\n/* Individual swatch item (button + price) */\n.kombos-option-choice__swatch-item {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 0.25rem;\n}\n\n/* Swatch button — square like Badge */\n.kombos-option-choice__swatch-btn {\n width: 2.25rem;\n height: 2.25rem;\n border: 1px solid var(--kombos-gray-200);\n border-radius: 6px;\n padding: 0.125rem;\n cursor: pointer;\n overflow: hidden;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--kombos-white);\n position: relative;\n transition: border-color 0.15s;\n}\n\n.kombos-option-choice__swatch-btn:hover:not(.kombos-option-choice__swatch-btn--selected) {\n border-color: var(--kombos-gray-400);\n}\n\n.kombos-option-choice__swatch-btn--selected {\n border-color: var(--kombos-gray-900);\n}\n\n/* Color fill */\n.kombos-option-choice__swatch-color {\n width: 100%;\n height: 100%;\n border-radius: 4px;\n}\n\n/* Image inside swatch */\n.kombos-option-choice__swatch-img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n border-radius: 4px;\n}\n\n/* Eye icon overlay on image swatches */\n.kombos-option-choice__swatch-eye {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.35);\n border-radius: 4px;\n opacity: 0;\n transition: opacity 0.15s;\n cursor: pointer;\n font-size: 1rem;\n color: var(--kombos-white);\n}\n\n.kombos-option-choice__swatch-btn:hover .kombos-option-choice__swatch-eye {\n opacity: 1;\n}\n\n/* Footer row: checkbox + price for image swatches */\n.kombos-option-choice__swatch-footer {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n/* Price below swatch */\n.kombos-option-choice__swatch-price {\n color: var(--kombos-gray-500);\n text-align: center;\n}\n"
17961
- },
17962
- {
17963
- "filename": "children/ProductDetailOptionSet/components/OptionChoice/index.tsx",
17964
- "content": "import {\n IkasProductOption,\n isChoiceOptionSelectType,\n isChoiceOptionSwatchType,\n hasError as hasOptionError,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport ChoiceSelect from \"./components/ChoiceSelect\";\nimport ChoiceSwatch from \"./components/ChoiceSwatch\";\nimport ChoiceBox from \"./components/ChoiceBox\";\n\ninterface Props {\n option: IkasProductOption;\n showError: boolean;\n requiredErrorText: string;\n placeholderText: string;\n minLabelText: string;\n maxLabelText: string;\n}\n\nconst OptionChoice = observer(function OptionChoice({\n option,\n showError,\n requiredErrorText,\n placeholderText,\n minLabelText,\n maxLabelText,\n}: Props) {\n const errored = showError && hasOptionError(option);\n const settings = option.selectSettings;\n if (!settings) return null;\n\n const isSelect = isChoiceOptionSelectType(option);\n const isSwatch = isChoiceOptionSwatchType(option);\n\n const constraintParts: string[] = [];\n if (settings.minSelect != null)\n constraintParts.push(`${minLabelText}${settings.minSelect}`);\n if (settings.maxSelect != null)\n constraintParts.push(`${maxLabelText}${settings.maxSelect}`);\n const constraintText =\n constraintParts.length > 0 ? constraintParts.join(\" / \") : undefined;\n\n const common = { option, errored, requiredErrorText, constraintText };\n\n if (isSelect) {\n return <ChoiceSelect {...common} placeholderText={placeholderText} />;\n }\n\n if (isSwatch) {\n return <ChoiceSwatch {...common} />;\n }\n\n return <ChoiceBox {...common} />;\n});\n\nexport default OptionChoice;\n"
17965
- },
17966
- {
17967
- "filename": "children/ProductDetailOptionSet/components/OptionColorPicker/index.tsx",
17968
- "content": "import {\n IkasProductOption,\n hasError as hasOptionError,\n getProductOptionFormattedLabel,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport FormItem from \"../../../../sub-components/FormItem\";\nimport ColorInput from \"../../../../sub-components/ColorInput\";\n\ninterface Props {\n option: IkasProductOption;\n showError: boolean;\n requiredErrorText: string;\n}\n\nconst OptionColorPicker = observer(function OptionColorPicker({\n option,\n showError,\n requiredErrorText,\n}: Props) {\n const errored = showError && hasOptionError(option);\n const hasValue = option.values && option.values.length > 0;\n const currentColor = option.values?.[0] ?? \"#FFFFFF\";\n\n const handleChange = (e: Event) => {\n const hex = (e.target as HTMLInputElement).value;\n option.values = [hex];\n };\n\n const handleClear = () => {\n option.values = [];\n };\n\n const inputId = `option-color-${option.id}`;\n\n return (\n <FormItem\n label={getProductOptionFormattedLabel(option)}\n htmlFor={inputId}\n status={errored ? \"error\" : \"default\"}\n description={option.optionalText}\n helper={errored ? requiredErrorText : undefined}\n >\n <ColorInput\n id={inputId}\n value={currentColor}\n onInput={handleChange}\n onClear={hasValue ? handleClear : undefined}\n />\n </FormItem>\n );\n});\n\nexport default OptionColorPicker;\n"
17969
- },
17970
- {
17971
- "filename": "children/ProductDetailOptionSet/components/OptionColorPicker/styles.css",
17972
- "content": ""
17973
- },
17974
- {
17975
- "filename": "children/ProductDetailOptionSet/components/OptionDatePicker/index.tsx",
17976
- "content": "import {\n IkasProductOption,\n hasError as hasOptionError,\n getProductOptionFormattedLabel,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport FormItem from \"../../../../sub-components/FormItem\";\nimport Input from \"../../../../sub-components/Input\";\n\ninterface Props {\n option: IkasProductOption;\n showError: boolean;\n requiredErrorText: string;\n minLabelText: string;\n maxLabelText: string;\n}\n\nfunction formatDateForInput(date: Date): string {\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, \"0\");\n const d = String(date.getDate()).padStart(2, \"0\");\n return `${y}-${m}-${d}`;\n}\n\nfunction formatDateForDisplay(dateStr: string): string {\n const [y, m, d] = dateStr.split(\"-\");\n return `${d}.${m}.${y}`;\n}\n\nfunction computeMinMaxDates(option: IkasProductOption) {\n const settings = option.dateSettings;\n let minDate: string | undefined;\n let maxDate: string | undefined;\n\n if (settings) {\n if (settings.min) {\n minDate = formatDateForInput(new Date(settings.min));\n } else if (settings.minRelativeNextDate != null) {\n const d = new Date();\n d.setDate(d.getDate() + settings.minRelativeNextDate);\n minDate = formatDateForInput(d);\n }\n\n if (settings.max) {\n maxDate = formatDateForInput(new Date(settings.max));\n } else if (settings.maxRelativeNextDate != null) {\n const d = new Date();\n d.setDate(d.getDate() + settings.maxRelativeNextDate);\n maxDate = formatDateForInput(d);\n }\n }\n\n return { minDate, maxDate };\n}\n\nconst OptionDatePicker = observer(function OptionDatePicker({\n option,\n showError,\n requiredErrorText,\n minLabelText,\n maxLabelText,\n}: Props) {\n const errored = showError && hasOptionError(option);\n const storedValue = option.values?.[0] ?? \"\";\n const currentValue = storedValue\n ? formatDateForInput(new Date(storedValue))\n : \"\";\n const { minDate, maxDate } = computeMinMaxDates(option);\n\n const applyDateValue = (val: string) => {\n if (!val) {\n option.values = [];\n return;\n }\n const date = new Date(val);\n if (isNaN(date.getTime()) || date.getFullYear() < 1900) return;\n date.setHours(23, 59, 0, 0);\n option.values = [date.toString()];\n };\n\n const handleChange = (e: Event) => {\n applyDateValue((e.target as HTMLInputElement).value);\n };\n\n const handleBlur = (e: Event) => {\n applyDateValue((e.target as HTMLInputElement).value);\n };\n\n const inputId = `option-date-${option.id}`;\n\n return (\n <FormItem\n label={getProductOptionFormattedLabel(option)}\n htmlFor={inputId}\n status={errored ? \"error\" : \"default\"}\n description={option.optionalText}\n helper={\n errored\n ? requiredErrorText\n : minDate || maxDate\n ? [\n minDate && `${minLabelText}${formatDateForDisplay(minDate)}`,\n maxDate && `${maxLabelText}${formatDateForDisplay(maxDate)}`,\n ]\n .filter(Boolean)\n .join(\" — \")\n : undefined\n }\n >\n <Input\n id={inputId}\n type=\"date\"\n value={currentValue}\n onChange={handleChange}\n onBlur={handleBlur}\n min={minDate}\n max={maxDate}\n size=\"xs\"\n />\n </FormItem>\n );\n});\n\nexport default OptionDatePicker;\n"
17977
- },
17978
- {
17979
- "filename": "children/ProductDetailOptionSet/components/OptionDatePicker/styles.css",
17980
- "content": "/* Styles handled by Input sub-component */\n"
17981
- },
17982
- {
17983
- "filename": "children/ProductDetailOptionSet/components/OptionFile/index.tsx",
17984
- "content": "import {\n IkasProductOption,\n productOptionFileUpload,\n hasError as hasOptionError,\n getProductOptionFormattedLabel,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport { useState, useRef } from \"preact/hooks\";\nimport FormItem from \"../../../../sub-components/FormItem\";\nimport { UploadSVG, TrashSVG } from \"../../../../sub-components/icons\";\n\ninterface Props {\n option: IkasProductOption;\n showError: boolean;\n requiredErrorText: string;\n minLabelText: string;\n maxLabelText: string;\n fileFallbackNameText: string;\n fileDropText: string;\n uploadingText: string;\n uploadFailedText: string;\n fileSizeErrorText: string;\n fileTypeErrorText: string;\n maxFilesErrorText: string;\n}\n\nconst MAX_FILE_SIZE_MB = 4;\n\nconst OptionFile = observer(function OptionFile({\n option,\n showError,\n requiredErrorText,\n minLabelText,\n maxLabelText,\n fileFallbackNameText,\n fileDropText,\n uploadingText,\n uploadFailedText,\n fileSizeErrorText,\n fileTypeErrorText,\n maxFilesErrorText,\n}: Props) {\n const [isUploading, setIsUploading] = useState(false);\n const [fileError, setFileError] = useState<string | null>(null);\n const [isDragOver, setIsDragOver] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const errored = showError && hasOptionError(option);\n const settings = option.fileSettings;\n const uploadedFiles = option.values ?? [];\n\n const validateFiles = (files: File[]): string | null => {\n for (const file of files) {\n if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) {\n return fileSizeErrorText\n .replace(\"{fileName}\", file.name)\n .replace(\"{maxSize}\", String(MAX_FILE_SIZE_MB));\n }\n\n if (settings?.allowedExtensions?.length) {\n const ext = file.name.split(\".\").pop()?.toLowerCase() ?? \"\";\n const allowed = settings.allowedExtensions.map((e) =>\n e.toLowerCase().replace(/^\\./, \"\"),\n );\n if (!allowed.includes(ext)) {\n return fileTypeErrorText\n .replace(\"{fileName}\", file.name)\n .replace(\"{ext}\", `.${ext}`);\n }\n }\n }\n\n const totalCount = uploadedFiles.length + files.length;\n if (settings?.maxQuantity != null && totalCount > settings.maxQuantity) {\n return maxFilesErrorText.replace(\"{max}\", String(settings.maxQuantity));\n }\n\n return null;\n };\n\n const handleUpload = async (fileList: FileList | null) => {\n if (!fileList?.length || isUploading) return;\n const files = Array.from(fileList);\n\n const error = validateFiles(files);\n if (error) {\n setFileError(error);\n return;\n }\n\n setFileError(null);\n setIsUploading(true);\n try {\n await productOptionFileUpload(option, files);\n } catch {\n setFileError(uploadFailedText);\n } finally {\n setIsUploading(false);\n if (inputRef.current) inputRef.current.value = \"\";\n }\n };\n\n const handleRemoveFile = (index: number) => {\n const newValues = [...uploadedFiles];\n newValues.splice(index, 1);\n option.values = newValues;\n };\n\n const handleDragOver = (e: DragEvent) => {\n e.preventDefault();\n if (!isUploading) setIsDragOver(true);\n };\n\n const handleDragLeave = () => {\n setIsDragOver(false);\n };\n\n const handleDrop = (e: DragEvent) => {\n e.preventDefault();\n setIsDragOver(false);\n if (isUploading) return;\n handleUpload(e.dataTransfer?.files ?? null);\n };\n\n const handleDropZoneClick = () => {\n if (isUploading) return;\n inputRef.current?.click();\n };\n\n const acceptExt = settings?.allowedExtensions?.length\n ? settings.allowedExtensions\n .map((e) => (e.startsWith(\".\") ? e : `.${e}`))\n .join(\",\")\n : undefined;\n\n const constraintParts: string[] = [];\n if (settings?.minQuantity != null)\n constraintParts.push(`${minLabelText}${settings.minQuantity}`);\n if (settings?.maxQuantity != null)\n constraintParts.push(`${maxLabelText}${settings.maxQuantity}`);\n if (settings?.allowedExtensions?.length) {\n constraintParts.push(settings.allowedExtensions.join(\", \"));\n }\n const constraintText =\n constraintParts.length > 0 ? constraintParts.join(\" / \") : undefined;\n\n const dropCls = [\n \"kombos-option-file__drop\",\n isDragOver && \"kombos-option-file__drop--active\",\n errored && \"kombos-option-file__drop--error\",\n isUploading && \"kombos-option-file__drop--disabled\",\n ]\n .filter(Boolean)\n .join(\" \");\n\n return (\n <FormItem\n label={getProductOptionFormattedLabel(option)}\n status={fileError || errored ? \"error\" : \"default\"}\n description={option.optionalText}\n helper={\n fileError || errored\n ? (fileError ?? requiredErrorText)\n : constraintText\n }\n >\n <div\n className={dropCls}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n onClick={handleDropZoneClick}\n >\n <span className=\"kombos-option-file__drop-icon\">\n <UploadSVG />\n </span>\n <span className=\"kombos-option-file__drop-text text-sm-regular\">\n {isUploading ? uploadingText : fileDropText}\n </span>\n <input\n ref={inputRef}\n type=\"file\"\n className=\"kombos-option-file__native\"\n accept={acceptExt}\n multiple={settings?.maxQuantity !== 1}\n onChange={(e: Event) =>\n handleUpload((e.target as HTMLInputElement).files)\n }\n />\n </div>\n\n {uploadedFiles.length > 0 && (\n <ul className=\"kombos-option-file__list\">\n {uploadedFiles.map((url, i) => {\n const name = url.split(\"/\").pop() ?? `${fileFallbackNameText} ${i + 1}`;\n return (\n <li key={url} className=\"kombos-option-file__item\">\n <span className=\"kombos-option-file__item-name text-xs-regular\">\n {name}\n </span>\n <button\n type=\"button\"\n className=\"kombos-option-file__item-remove\"\n onClick={() => handleRemoveFile(i)}\n >\n <TrashSVG />\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </FormItem>\n );\n});\n\nexport default OptionFile;\n"
17985
- },
17986
- {
17987
- "filename": "children/ProductDetailOptionSet/components/OptionFile/styles.css",
17988
- "content": ".kombos-option-file__drop {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n padding: 1.5rem;\n border: 1px dashed var(--kombos-gray-300);\n border-radius: 6px;\n cursor: pointer;\n transition: border-color 0.15s, background-color 0.15s;\n background: var(--kombos-white);\n}\n\n.kombos-option-file__drop:hover {\n border-color: var(--kombos-gray-400);\n background: var(--kombos-gray-50);\n}\n\n.kombos-option-file__drop--active {\n border-color: var(--kombos-gray-500);\n background: var(--kombos-gray-50);\n}\n\n.kombos-option-file__drop--error {\n border-color: var(--kombos-error) !important;\n}\n\n.kombos-option-file__drop--disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.kombos-option-file__drop-icon {\n font-size: 1.5rem;\n color: var(--kombos-gray-400);\n}\n\n.kombos-option-file__drop-text {\n color: var(--kombos-gray-500);\n}\n\n.kombos-option-file__native {\n display: none;\n}\n\n.kombos-option-file__list {\n list-style: none;\n padding: 0;\n margin: 0.5rem 0 0;\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n}\n\n.kombos-option-file__item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.375rem 0.5rem;\n border: 1px solid var(--kombos-gray-100);\n border-radius: 4px;\n background: var(--kombos-gray-50);\n}\n\n.kombos-option-file__item-name {\n color: var(--kombos-gray-700);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 80%;\n}\n\n.kombos-option-file__item-remove {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 0.875rem;\n color: var(--kombos-gray-400);\n padding: 0;\n display: flex;\n align-items: center;\n}\n\n.kombos-option-file__item-remove:hover {\n color: var(--kombos-error);\n}\n"
17989
- },
17990
- {
17991
- "filename": "children/ProductDetailOptionSet/components/OptionRenderer/index.tsx",
17992
- "content": "import {\n getDisplayedChildOptions,\n isTextOption,\n isTextAreaOption,\n isCheckboxOption,\n isChoiceOption,\n isColorPickerOption,\n isDatePickerOption,\n isFileOption,\n IkasProductOption,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\n\nimport OptionText from \"../OptionText\";\nimport OptionTextarea from \"../OptionTextarea\";\nimport OptionCheckbox from \"../OptionCheckbox\";\nimport OptionChoice from \"../OptionChoice\";\nimport OptionColorPicker from \"../OptionColorPicker\";\nimport OptionDatePicker from \"../OptionDatePicker\";\nimport OptionFile from \"../OptionFile\";\n\nexport interface TextProps {\n requiredErrorText: string;\n selectPlaceholderText: string;\n fileDropText: string;\n uploadingText: string;\n uploadFailedText: string;\n fileSizeErrorText: string;\n fileTypeErrorText: string;\n maxFilesErrorText: string;\n minLabelText: string;\n maxLabelText: string;\n fileFallbackNameText: string;\n}\n\ninterface Props {\n option: IkasProductOption;\n showError: boolean;\n texts: TextProps;\n}\n\nfunction renderOption(\n option: IkasProductOption,\n showError: boolean,\n texts: TextProps,\n) {\n const commonProps = {\n option,\n showError,\n requiredErrorText: texts.requiredErrorText,\n };\n\n const minMaxProps = {\n minLabelText: texts.minLabelText,\n maxLabelText: texts.maxLabelText,\n };\n\n if (isTextOption(option)) {\n return <OptionText {...commonProps} {...minMaxProps} />;\n }\n\n if (isTextAreaOption(option)) {\n return <OptionTextarea {...commonProps} {...minMaxProps} />;\n }\n\n if (isCheckboxOption(option)) {\n return <OptionCheckbox {...commonProps} />;\n }\n\n if (isChoiceOption(option)) {\n return (\n <OptionChoice\n {...commonProps}\n {...minMaxProps}\n placeholderText={texts.selectPlaceholderText}\n />\n );\n }\n\n if (isColorPickerOption(option)) {\n return <OptionColorPicker {...commonProps} />;\n }\n\n if (isDatePickerOption(option)) {\n return <OptionDatePicker {...commonProps} {...minMaxProps} />;\n }\n\n if (isFileOption(option)) {\n return (\n <OptionFile\n {...commonProps}\n {...minMaxProps}\n fileFallbackNameText={texts.fileFallbackNameText}\n fileDropText={texts.fileDropText}\n uploadingText={texts.uploadingText}\n uploadFailedText={texts.uploadFailedText}\n fileSizeErrorText={texts.fileSizeErrorText}\n fileTypeErrorText={texts.fileTypeErrorText}\n maxFilesErrorText={texts.maxFilesErrorText}\n />\n );\n }\n\n return null;\n}\n\nconst OptionRenderer = observer(function OptionRenderer({\n option,\n showError,\n texts,\n}: Props) {\n const childOptions = getDisplayedChildOptions(option) as IkasProductOption[];\n\n return (\n <div key={option.id} className=\"kombos-pd-optset__option\">\n {renderOption(option, showError, texts)}\n {childOptions.length > 0 && (\n <div className=\"kombos-pd-optset__children\">\n {childOptions.map((child) => (\n <OptionRenderer\n key={child.id}\n option={child}\n showError={showError}\n texts={texts}\n />\n ))}\n </div>\n )}\n </div>\n );\n});\n\nexport default OptionRenderer;\n"
17993
- },
17994
- {
17995
- "filename": "children/ProductDetailOptionSet/components/OptionText/index.tsx",
17996
- "content": "import {\n IkasProductOption,\n getTextValue,\n setTextValue,\n hasError as hasOptionError,\n getProductOptionFormattedLabel,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport Input from \"../../../../sub-components/Input\";\nimport FormItem from \"../../../../sub-components/FormItem\";\n\ninterface Props {\n option: IkasProductOption;\n showError: boolean;\n requiredErrorText: string;\n minLabelText: string;\n maxLabelText: string;\n}\n\nconst OptionText = observer(function OptionText({\n option,\n showError,\n requiredErrorText,\n minLabelText,\n maxLabelText,\n}: Props) {\n const value = getTextValue(option);\n const errored = showError && hasOptionError(option);\n const settings = option.textSettings;\n\n const helperParts: string[] = [];\n if (settings?.min != null) helperParts.push(`${minLabelText}${settings.min}`);\n if (settings?.max != null) helperParts.push(`${maxLabelText}${settings.max}`);\n const helperText =\n helperParts.length > 0 ? helperParts.join(\" / \") : undefined;\n\n const inputId = `option-text-${option.id}`;\n\n return (\n <FormItem\n label={getProductOptionFormattedLabel(option)}\n htmlFor={inputId}\n status={errored ? \"error\" : \"default\"}\n description={option.optionalText}\n helper={errored ? requiredErrorText : helperText}\n >\n <Input\n id={inputId}\n value={value}\n onInput={(e: Event) =>\n setTextValue(option, (e.target as HTMLInputElement).value)\n }\n maxLength={settings?.max ?? undefined}\n size=\"xs\"\n />\n </FormItem>\n );\n});\n\nexport default OptionText;\n"
17997
- },
17998
- {
17999
- "filename": "children/ProductDetailOptionSet/components/OptionText/styles.css",
18000
- "content": "/* FormItem handles all layout and styling */\n"
18001
- },
18002
- {
18003
- "filename": "children/ProductDetailOptionSet/components/OptionTextarea/index.tsx",
18004
- "content": "import {\n IkasProductOption,\n getTextValue,\n setTextValue,\n hasError as hasOptionError,\n getProductOptionFormattedLabel,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\nimport Textarea from \"../../../../sub-components/Textarea\";\nimport FormItem from \"../../../../sub-components/FormItem\";\n\ninterface Props {\n option: IkasProductOption;\n showError: boolean;\n requiredErrorText: string;\n minLabelText: string;\n maxLabelText: string;\n}\n\nconst OptionTextarea = observer(function OptionTextarea({\n option,\n showError,\n requiredErrorText,\n minLabelText,\n maxLabelText,\n}: Props) {\n const value = getTextValue(option);\n const errored = showError && hasOptionError(option);\n const settings = option.textSettings;\n\n const helperParts: string[] = [];\n if (settings?.min != null) helperParts.push(`${minLabelText}${settings.min}`);\n if (settings?.max != null) helperParts.push(`${maxLabelText}${settings.max}`);\n const helperText =\n helperParts.length > 0 ? helperParts.join(\" / \") : undefined;\n\n const textareaId = `option-textarea-${option.id}`;\n\n return (\n <FormItem\n label={getProductOptionFormattedLabel(option)}\n htmlFor={textareaId}\n status={errored ? \"error\" : \"default\"}\n helper={\n errored ? (\n requiredErrorText\n ) : (\n <div className=\"kombos-option-textarea__footer\">\n {helperText ? helperText : <span />}\n {settings?.max != null && (\n <span className=\"kombos-option-textarea__counter\">\n {(value ?? \"\").length} / {settings.max}\n </span>\n )}\n </div>\n )\n }\n description={option.optionalText}\n >\n <Textarea\n id={textareaId}\n value={value}\n onInput={(e: Event) =>\n setTextValue(option, (e.target as HTMLTextAreaElement).value)\n }\n maxLength={settings?.max ?? undefined}\n rows={4}\n size=\"xs\"\n />\n </FormItem>\n );\n});\n\nexport default OptionTextarea;\n"
18005
- },
18006
- {
18007
- "filename": "children/ProductDetailOptionSet/components/OptionTextarea/styles.css",
18008
- "content": ".kombos-option-textarea__footer {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n}\n\n.kombos-option-textarea__counter {\n color: var(--kombos-gray-400);\n margin-left: auto;\n}\n"
18009
- },
18010
- {
18011
- "filename": "children/ProductDetailOptionSet/ikas-config-snippet.json",
18012
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-option-set\",\n \"name\": \"ProductDetailOptionSet\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailOptionSet/index.tsx\",\n \"styles\": \"./src/components/ProductDetailOptionSet/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"selectPlaceholderText\",\n \"displayName\": \"Selection Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Seçiniz\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"fileDropText\",\n \"displayName\": \"File Drop Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"File seçin veya sürükleyin\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"uploadingText\",\n \"displayName\": \"Loading Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Yükleniyor...\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"uploadFailedText\",\n \"displayName\": \"Upload Failed Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Upload başarısız\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"fileSizeErrorText\",\n \"displayName\": \"File Size Error\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"{fileName}: max {maxSize}MB\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"fileTypeErrorText\",\n \"displayName\": \"File Type Error\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"{fileName}: {ext} uzantısına izin verilmiyor\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"maxFilesErrorText\",\n \"displayName\": \"Maximum File Error\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"En fazla {max} dosya yüklenebilir\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"requiredFieldErrorText\",\n \"displayName\": \"Required Field Error\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Bu alan zorunludur\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"minLabelText\",\n \"displayName\": \"Min Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Min: \",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"maxLabelText\",\n \"displayName\": \"Max Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Max: \",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"fileFallbackNameText\",\n \"displayName\": \"Default File Name\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"File\",\n \"groupId\": \"texts\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Bileşende görüntülenen metin ayarları\"\n },\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n }\n ]\n}"
18013
- },
18014
- {
18015
- "filename": "children/ProductDetailOptionSet/index.tsx",
18016
- "content": "import {\n getProductOptionSet,\n getDisplayedOptions,\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n IkasProductOption,\n} from \"@ikas/bp-storefront\";\nimport { useEffect, useState } from \"preact/hooks\";\nimport { Props } from \"./types\";\n\nimport OptionRenderer from \"./components/OptionRenderer\";\nimport type { TextProps } from \"./components/OptionRenderer\";\n\nexport function ProductDetailOptionSet({\n product,\n requiredFieldErrorText = \"Bu alan zorunludur\",\n selectPlaceholderText = \"Seçiniz\",\n fileDropText = \"Dosya seçin veya sürükleyin\",\n uploadingText = \"Yükleniyor...\",\n uploadFailedText = \"Yükleme başarısız\",\n fileSizeErrorText = \"{fileName}: max {maxSize}MB\",\n fileTypeErrorText = \"{fileName}: {ext} uzantısına izin verilmiyor\",\n maxFilesErrorText = \"En fazla {max} dosya yüklenebilir\",\n minLabelText = \"Min: \",\n maxLabelText = \"Max: \",\n fileFallbackNameText = \"File\",\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n}: Props) {\n const [loaded, setLoaded] = useState(false);\n const [showError, setShowError] = useState(false);\n\n useEffect(() => {\n if (!product) return;\n getProductOptionSet(product).then(() => setLoaded(true));\n }, [product]);\n\n useEffect(() => {\n const showHandler = () => setShowError(true);\n const resetHandler = () => setShowError(false);\n window.addEventListener(\"ikas:show-option-errors\", showHandler);\n window.addEventListener(\"ikas:reset-option-state\", resetHandler);\n return () => {\n window.removeEventListener(\"ikas:show-option-errors\", showHandler);\n window.removeEventListener(\"ikas:reset-option-state\", resetHandler);\n };\n }, []);\n\n if (!product || !loaded) return null;\n\n const optionSet = product.productOptionSet;\n if (!optionSet) return null;\n\n const displayedOptions = getDisplayedOptions(\n optionSet,\n ) as IkasProductOption[];\n if (!displayedOptions.length) return null;\n\n const texts: TextProps = {\n requiredErrorText: requiredFieldErrorText,\n selectPlaceholderText,\n fileDropText,\n uploadingText,\n uploadFailedText,\n fileSizeErrorText,\n fileTypeErrorText,\n maxFilesErrorText,\n minLabelText,\n maxLabelText,\n fileFallbackNameText,\n };\n\n return (\n <div\n className=\"kombos-pd-optset\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n <div className=\"kombos-pd-optset__list\">\n {displayedOptions.map((option) => (\n <OptionRenderer\n key={option.id}\n option={option as unknown as IkasProductOption}\n showError={showError}\n texts={texts}\n />\n ))}\n </div>\n </div>\n );\n}\n\nexport default ProductDetailOptionSet;\n"
18017
- },
18018
- {
18019
- "filename": "children/ProductDetailOptionSet/styles.css",
18020
- "content": ".kombos-pd-optset {\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n@media (min-width: 1024px) {\n .kombos-pd-optset {\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n}\n\n.kombos-pd-optset__list {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.kombos-pd-optset__option {\n display: flex;\n flex-direction: column;\n}\n\n.kombos-pd-optset__children {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n margin-top: 0.5rem;\n padding-left: 0.5rem;\n}\n"
18021
- },
18022
- {
18023
- "filename": "children/ProductDetailOptionSet/types.ts",
18024
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n selectPlaceholderText?: string;\n fileDropText?: string;\n uploadingText?: string;\n uploadFailedText?: string;\n fileSizeErrorText?: string;\n fileTypeErrorText?: string;\n maxFilesErrorText?: string;\n requiredFieldErrorText?: string;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n minLabelText?: string;\n maxLabelText?: string;\n fileFallbackNameText?: string;\n}\n"
18025
- },
18026
- {
18027
- "filename": "children/ProductDetailPrices/ikas-config-snippet.json",
18028
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-prices\",\n \"name\": \"ProductDetailPrices\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailPrices/index.tsx\",\n \"styles\": \"./src/components/ProductDetailPrices/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n }\n ]\n}"
18029
- },
18030
- {
18031
- "filename": "children/ProductDetailPrices/index.tsx",
18032
- "content": "import {\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n getProductVariantDiscountPercentage,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n getSelectedProductVariant,\n hasProductVariantDiscount,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport Tag from \"../../sub-components/Tag\";\n\nexport function ProductDetailPrices({\n product,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n}: Props) {\n if (!product) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\n\n const hasDiscount = hasProductVariantDiscount(selectedVariant);\n const finalPrice = getProductVariantFormattedFinalPrice(selectedVariant);\n const originalPrice = hasDiscount\n ? getProductVariantFormattedSellPrice(selectedVariant)\n : null;\n const discountPercentage = hasDiscount\n ? getProductVariantDiscountPercentage(selectedVariant)\n : null;\n\n return (\n <div\n className=\"kombos-pd-prices\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n <div className=\"kombos-pd-prices__prices\">\n <span className=\"kombos-pd-prices__final text-xl-medium md:display-xs-medium\">\n {finalPrice}\n </span>\n {originalPrice && (\n <span className=\"kombos-pd-prices__original text-lg-medium-strike md:text-xl-medium-strike\">\n {originalPrice}\n </span>\n )}\n </div>\n {discountPercentage != null && Number(discountPercentage) > 0 && (\n <Tag type=\"discounted\" size=\"m\">\n %{Math.round(Number(discountPercentage))}\n </Tag>\n )}\n </div>\n );\n}\n\nexport default ProductDetailPrices;\n"
18033
- },
18034
- {
18035
- "filename": "children/ProductDetailPrices/styles.css",
18036
- "content": "/* ===== ProductDetailPrices ===== */\n\n.kombos-pd-prices {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n flex-wrap: wrap;\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n.kombos-pd-prices__prices {\n display: flex;\n align-items: baseline;\n gap: 0.5rem;\n}\n\n.kombos-pd-prices__final {\n color: var(--kombos-gray-900);\n}\n\n.kombos-pd-prices__original {\n color: var(--kombos-gray-500);\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd-prices {\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n}\n"
18037
- },
18038
- {
18039
- "filename": "children/ProductDetailPrices/types.ts",
18040
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n}\n"
18041
- },
18042
- {
18043
- "filename": "children/ProductDetailProductGroup/ikas-config-snippet.json",
18044
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-product-group\",\n \"name\": \"ProductDetailProductGroup\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailProductGroup/index.tsx\",\n \"styles\": \"./src/components/ProductDetailProductGroup/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n }\n ]\n}"
18045
- },
18046
- {
18047
- "filename": "children/ProductDetailProductGroup/index.tsx",
18048
- "content": "import {\n getDisplayedProductGroups,\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport { BadgeImage, BadgeText } from \"../../sub-components/Badge\";\n\nexport function ProductDetailProductGroup({\n product,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n}: Props) {\n if (!product) return null;\n\n const groups = getDisplayedProductGroups(product);\n if (!groups.length) return null;\n\n return (\n <div\n className=\"kombos-pd-product-group\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n {groups.map((group) => {\n const selected = group.items.find((i) => i.isSelected);\n\n return (\n <div key={group.name} className=\"kombos-pd-product-group__group\">\n <span className=\"kombos-pd-product-group__label text-sm-medium\">\n {group.name}\n {selected && <span> - {selected.value}</span>}\n </span>\n <div className=\"kombos-pd-product-group__row\">\n {group.items.map((item) =>\n item.image ? (\n <BadgeImage\n key={item.value}\n image={item.image}\n alt={item.value}\n sizes=\"64px\"\n variantImg\n size=\"l\"\n selected={item.isSelected}\n href={item.href}\n title={item.value}\n />\n ) : (\n <BadgeText\n key={item.value}\n size=\"l\"\n selected={item.isSelected}\n href={item.href}\n title={item.value}\n >\n {item.value}\n </BadgeText>\n ),\n )}\n </div>\n </div>\n );\n })}\n </div>\n );\n}\n\nexport default ProductDetailProductGroup;\n"
18049
- },
18050
- {
18051
- "filename": "children/ProductDetailProductGroup/styles.css",
18052
- "content": "/* ===== ProductDetailProductGroup ===== */\n\n.kombos-pd-product-group {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n.kombos-pd-product-group__group {\n display: flex;\n flex-direction: column;\n}\n\n.kombos-pd-product-group__label {\n color: var(--kombos-gray-900);\n margin-bottom: 0.375rem;\n}\n\n.kombos-pd-product-group__row {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd-product-group {\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n}\n"
18053
- },
18054
- {
18055
- "filename": "children/ProductDetailProductGroup/types.ts",
18056
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n}\n"
18057
- },
18058
- {
18059
- "filename": "children/ProductDetailSku/ikas-config-snippet.json",
18060
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-sku\",\n \"name\": \"ProductDetailSku\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailSku/index.tsx\",\n \"styles\": \"./src/components/ProductDetailSku/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"productCodeLabel\",\n \"displayName\": \"Product Code Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Product Kodu:\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n },\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Bileşende görüntülenen metin ayarları\"\n }\n ]\n}"
18061
- },
18062
- {
18063
- "filename": "children/ProductDetailSku/index.tsx",
18064
- "content": "import {\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n getSelectedProductVariant,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function ProductDetailSku({\n product,\n productCodeLabel = \"Ürün Kodu:\",\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n}: Props) {\n if (!product) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\n const sku = selectedVariant?.sku;\n\n if (!sku) return null;\n\n return (\n <p\n className=\"kombos-pd-sku__text text-sm-medium\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n {productCodeLabel} {sku}\n </p>\n );\n}\n\nexport default ProductDetailSku;\n"
18065
- },
18066
- {
18067
- "filename": "children/ProductDetailSku/styles.css",
18068
- "content": "/* ===== ProductDetailSKU ===== */\n\n.kombos-pd-sku__text {\n margin: 0;\n color: var(--kombos-gray-600);\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd-sku__text {\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n}\n"
18069
- },
18070
- {
18071
- "filename": "children/ProductDetailSku/types.ts",
18072
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n productCodeLabel?: string;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n}\n"
18073
- },
18074
- {
18075
- "filename": "children/ProductDetailVariant/ikas-config-snippet.json",
18076
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-variant\",\n \"name\": \"ProductDetailVariant\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailVariant/index.tsx\",\n \"styles\": \"./src/components/ProductDetailVariant/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"useVariantImages\",\n \"displayName\": \"Image Variant\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"description\": \"Renk varyantı için ürün görsellerini kullan.\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n }\n ]\n}"
18077
- },
18078
- {
18079
- "filename": "children/ProductDetailVariant/index.tsx",
18080
- "content": "import {\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport VariantBadge from \"../../sub-components/VariantBadge\";\n\nexport function ProductDetailVariant({\n product,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n useVariantImages,\n}: Props) {\n if (!product) return null;\n\n return (\n <div\n className=\"kombos-pd-variant\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n <VariantBadge\n product={product}\n size=\"l\"\n showLabels\n useVariantImages={useVariantImages}\n />\n </div>\n );\n}\n\nexport default ProductDetailVariant;\n"
18081
- },
18082
- {
18083
- "filename": "children/ProductDetailVariant/styles.css",
18084
- "content": "/* ===== ProductDetailVariant ===== */\n\n.kombos-pd-variant {\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd-variant {\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n}\n"
18085
- },
18086
- {
18087
- "filename": "children/ProductDetailVariant/types.ts",
18088
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n /** Renk varyantı için ürün görsellerini kullan. */\n useVariantImages?: boolean;\n}\n"
18089
- },
18090
- {
18091
- "filename": "components/ProductGallery/index.tsx",
18092
- "content": "import {\n createMediaSrcset,\n getDefaultSrc,\n getThumbnailSrc,\n IkasImage,\n} from \"@ikas/bp-storefront\";\nimport { useRef, useState, useCallback, useEffect, useMemo } from \"preact/hooks\";\nimport { cx } from \"../../../../utils/cx\";\nimport { NoProductSVG, PlaySVG } from \"../../../../sub-components/icons\";\nimport SliderArrow from \"../../../../sub-components/SliderArrow\";\nimport { resolveAspectRatio, resolveObjectFit } from \"../../../../utils/media\";\nimport type { AspectRatio, ObjectFit } from \"../../../../global-types\";\n\nconst SETTLE_DELAY = 100;\nconst DRAG_THRESHOLD_RATIO = 0.15;\nconst SNAP_DURATION = 300;\n\nfunction easeInOutCubic(t: number): number {\n return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;\n}\n\ninterface Props {\n images: IkasImage[];\n productName: string;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n}\n\nexport default function ProductGallery({\n images,\n productName,\n aspectRatio,\n objectFit,\n}: Props) {\n const [selectedIndex, setSelectedIndex] = useState(0);\n const trackRef = useRef<HTMLDivElement>(null);\n const mainWrapRef = useRef<HTMLDivElement>(null);\n const thumbsRef = useRef<HTMLDivElement>(null);\n const isProgrammaticRef = useRef(false);\n const settleTimerRef = useRef<ReturnType<typeof setTimeout>>();\n const isDraggingRef = useRef(false);\n const dragStartXRef = useRef(0);\n const dragScrollLeftRef = useRef(0);\n const animFrameRef = useRef(0);\n\n const hasMultiple = images.length > 1;\n const resolvedAR = resolveAspectRatio(aspectRatio);\n const resolvedOF = resolveObjectFit(objectFit);\n\n const imageStyle = useMemo(() => ({ objectFit: resolvedOF as CSSStyleDeclaration[\"objectFit\"] }), [resolvedOF]);\n const thumbStyle = useMemo(() => ({ aspectRatio: resolvedAR }), [resolvedAR]);\n\n // Sync thumbnail column height with main image\n useEffect(() => {\n if (!hasMultiple) return;\n const syncHeight = () => {\n const wrap = mainWrapRef.current;\n const thumbs = thumbsRef.current;\n if (wrap && thumbs) {\n thumbs.style.maxHeight = `${wrap.offsetHeight}px`;\n }\n };\n syncHeight();\n window.addEventListener(\"resize\", syncHeight);\n return () => window.removeEventListener(\"resize\", syncHeight);\n }, [hasMultiple, aspectRatio]);\n\n // Auto-play/pause videos based on selected slide\n useEffect(() => {\n const track = trackRef.current;\n if (!track) return;\n const slides = track.children;\n for (let i = 0; i < slides.length; i++) {\n const video = slides[i].querySelector(\"video\");\n if (!video) continue;\n if (i === selectedIndex) {\n video.play().catch(() => {});\n } else {\n video.pause();\n video.currentTime = 0;\n }\n }\n }, [selectedIndex]);\n\n const scrollThumbIntoView = useCallback((index: number) => {\n const thumb = thumbsRef.current?.children[index] as HTMLElement | undefined;\n thumb?.scrollIntoView({ block: \"nearest\", behavior: \"smooth\" });\n }, []);\n\n const smoothScrollTo = useCallback((track: HTMLElement, target: number) => {\n cancelAnimationFrame(animFrameRef.current);\n const start = track.scrollLeft;\n const distance = target - start;\n if (Math.abs(distance) < 1) {\n track.scrollLeft = target;\n track.style.scrollSnapType = \"x mandatory\";\n isProgrammaticRef.current = false;\n return;\n }\n const startTime = performance.now();\n const step = (now: number) => {\n const elapsed = Math.min((now - startTime) / SNAP_DURATION, 1);\n track.scrollLeft = start + distance * easeInOutCubic(elapsed);\n if (elapsed < 1) {\n animFrameRef.current = requestAnimationFrame(step);\n } else {\n track.style.scrollSnapType = \"x mandatory\";\n isProgrammaticRef.current = false;\n }\n };\n animFrameRef.current = requestAnimationFrame(step);\n }, []);\n\n const goToSlide = useCallback((index: number) => {\n const track = trackRef.current;\n if (!track) return;\n isProgrammaticRef.current = true;\n clearTimeout(settleTimerRef.current);\n const slide = track.children[index] as HTMLElement | undefined;\n if (slide) {\n track.scrollTo({ left: slide.offsetLeft, behavior: \"smooth\" });\n }\n setSelectedIndex(index);\n scrollThumbIntoView(index);\n }, [scrollThumbIntoView]);\n\n const handlePrev = useCallback(() => {\n goToSlide(selectedIndex === 0 ? images.length - 1 : selectedIndex - 1);\n }, [goToSlide, selectedIndex, images.length]);\n\n const handleNext = useCallback(() => {\n goToSlide(selectedIndex === images.length - 1 ? 0 : selectedIndex + 1);\n }, [goToSlide, selectedIndex, images.length]);\n\n const handleScroll = useCallback(() => {\n clearTimeout(settleTimerRef.current);\n if (isProgrammaticRef.current) {\n settleTimerRef.current = setTimeout(() => {\n isProgrammaticRef.current = false;\n }, SETTLE_DELAY);\n return;\n }\n const track = trackRef.current;\n if (!track) return;\n const newIndex = Math.round(track.scrollLeft / track.clientWidth);\n if (newIndex >= 0 && newIndex < images.length) {\n setSelectedIndex(newIndex);\n }\n }, [images.length]);\n\n const handleMouseDown = useCallback((e: MouseEvent) => {\n const track = trackRef.current;\n if (!track) return;\n cancelAnimationFrame(animFrameRef.current);\n isDraggingRef.current = true;\n dragStartXRef.current = e.pageX;\n dragScrollLeftRef.current = track.scrollLeft;\n track.style.scrollSnapType = \"none\";\n track.style.cursor = \"grabbing\";\n }, []);\n\n const handleMouseMove = useCallback((e: MouseEvent) => {\n if (!isDraggingRef.current) return;\n e.preventDefault();\n const track = trackRef.current;\n if (!track) return;\n track.scrollLeft = dragScrollLeftRef.current - (e.pageX - dragStartXRef.current);\n }, []);\n\n const handleMouseEnd = useCallback((e: MouseEvent) => {\n if (!isDraggingRef.current) return;\n isDraggingRef.current = false;\n const track = trackRef.current;\n if (!track) return;\n track.style.cursor = \"\";\n\n const dragDelta = dragStartXRef.current - e.pageX;\n const threshold = track.clientWidth * DRAG_THRESHOLD_RATIO;\n let targetIndex = Math.round(dragScrollLeftRef.current / track.clientWidth);\n if (dragDelta > threshold) targetIndex++;\n else if (dragDelta < -threshold) targetIndex--;\n const clamped = Math.max(0, Math.min(targetIndex, images.length - 1));\n\n isProgrammaticRef.current = true;\n setSelectedIndex(clamped);\n smoothScrollTo(track, track.clientWidth * clamped);\n scrollThumbIntoView(clamped);\n }, [images.length, smoothScrollTo, scrollThumbIntoView]);\n\n return (\n <div className=\"kombos-pd__gallery\">\n {hasMultiple && (\n <div className=\"kombos-pd__thumbs\" ref={thumbsRef}>\n {images.map((img, i) => (\n <button\n key={img.id || i}\n type=\"button\"\n className={cx(\"kombos-pd__thumb\", i === selectedIndex && \"kombos-pd__thumb--active\")}\n onClick={() => goToSlide(i)}\n style={thumbStyle}\n >\n {img.isVideo ? (\n <div className=\"kombos-pd__thumb-video\">\n <video\n src={getDefaultSrc(img)}\n className=\"kombos-pd__thumb-img\"\n style={imageStyle}\n muted\n preload=\"metadata\"\n >\n <track kind=\"captions\" />\n </video>\n <span className=\"kombos-pd__thumb-play\">\n <PlaySVG />\n </span>\n </div>\n ) : (\n <img\n src={getThumbnailSrc(img)}\n srcSet={createMediaSrcset(img)}\n sizes=\"112px\"\n alt={`${productName} ${i + 1}`}\n className=\"kombos-pd__thumb-img\"\n style={imageStyle}\n />\n )}\n </button>\n ))}\n </div>\n )}\n\n <div className=\"kombos-pd__main-col\">\n <div\n className=\"kombos-pd__main-image-wrap\"\n ref={mainWrapRef}\n style={{ aspectRatio: resolvedAR }}\n >\n <div\n className=\"kombos-pd__track\"\n ref={trackRef}\n onScroll={handleScroll}\n onMouseDown={handleMouseDown}\n onMouseMove={handleMouseMove}\n onMouseUp={handleMouseEnd}\n onMouseLeave={handleMouseEnd}\n >\n {images.length > 0 ? (\n images.map((img, i) => (\n <div key={img.id || i} className=\"kombos-pd__slide\">\n {img.isVideo ? (\n <video\n className=\"kombos-pd__main-image kombos-pd__main-video\"\n src={getDefaultSrc(img)}\n loop\n muted\n playsInline\n style={imageStyle}\n >\n <track kind=\"captions\" />\n </video>\n ) : (\n <img\n className=\"kombos-pd__main-image\"\n src={getDefaultSrc(img)}\n srcSet={createMediaSrcset(img)}\n sizes=\"(max-width: 1023px) 100vw, min(calc((100vw - 440px) / 2), 530px)\"\n alt={img?.altText || `${productName} ${i + 1}`}\n style={imageStyle}\n loading={i === 0 ? \"eager\" : \"lazy\"}\n fetchpriority={i === 0 ? \"high\" : undefined}\n />\n )}\n </div>\n ))\n ) : (\n <div className=\"kombos-pd__slide\">\n <div className=\"kombos-pd__main-image kombos-pd__main-image--placeholder\">\n <NoProductSVG />\n </div>\n </div>\n )}\n </div>\n\n {hasMultiple && (\n <>\n <SliderArrow\n direction=\"left\"\n className=\"kombos-pd__arrow kombos-pd__arrow--prev\"\n onClick={handlePrev}\n />\n <SliderArrow\n direction=\"right\"\n className=\"kombos-pd__arrow kombos-pd__arrow--next\"\n onClick={handleNext}\n />\n </>\n )}\n\n {hasMultiple && (\n <div className=\"kombos-pd__dots\">\n {images.map((_, i) => (\n <button\n key={i}\n type=\"button\"\n className={cx(\"kombos-pd__dot\", i === selectedIndex && \"kombos-pd__dot--active\")}\n onClick={() => goToSlide(i)}\n aria-label={`${images[i]?.isVideo ? \"Video\" : \"Image\"} ${i + 1}`}\n />\n ))}\n </div>\n )}\n </div>\n </div>\n </div>\n );\n}\n"
18093
- },
18094
- {
18095
- "filename": "components/ProductGallery/styles.css",
18096
- "content": "/* ---- Gallery ---- */\n.kombos-pd__gallery {\n display: flex;\n flex-direction: column;\n}\n\n.kombos-pd__thumbs {\n display: none;\n}\n\n.kombos-pd__main-col {\n flex: 1;\n min-width: 0;\n}\n\n.kombos-pd__main-image-wrap {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n\n.kombos-pd__track {\n display: flex;\n overflow-x: auto;\n scroll-snap-type: x mandatory;\n scrollbar-width: none;\n width: 100%;\n height: 100%;\n -webkit-overflow-scrolling: touch;\n cursor: grab;\n user-select: none;\n}\n\n.kombos-pd__track::-webkit-scrollbar {\n display: none;\n}\n\n.kombos-pd__slide {\n flex: 0 0 100%;\n width: 100%;\n scroll-snap-align: start;\n scroll-snap-stop: always;\n}\n\n.kombos-pd__main-image {\n width: 100%;\n height: 100%;\n display: block;\n -webkit-user-drag: none;\n user-drag: none;\n pointer-events: none;\n}\n\n.kombos-pd__main-video {\n pointer-events: auto;\n cursor: default;\n}\n\n.kombos-pd__thumb-video {\n position: relative;\n width: 100%;\n height: 100%;\n}\n\n.kombos-pd__thumb-play {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n display: flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.55);\n color: var(--kombos-white);\n font-size: 0.75rem;\n}\n\n.kombos-pd__main-image--placeholder {\n border: 1px solid var(--kombos-gray-100);\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n color: var(--kombos-gray-300);\n font-size: 5rem;\n}\n\n/* Arrow buttons */\n.kombos-pd__arrow {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n}\n\n.kombos-pd__arrow--prev {\n left: 1.25rem;\n}\n\n.kombos-pd__arrow--next {\n right: 1.25rem;\n}\n\n/* Dots */\n.kombos-pd__dots {\n position: absolute;\n bottom: 0;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 0;\n}\n\n.kombos-pd__dot {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 2.25rem;\n height: 2.25rem;\n border: none;\n background: transparent;\n cursor: pointer;\n padding: 0;\n -webkit-tap-highlight-color: transparent;\n}\n\n.kombos-pd__dot::after {\n content: \"\";\n display: block;\n width: 0.625rem;\n height: 0.625rem;\n border-radius: 10px;\n border: 1px solid var(--kombos-gray-100);\n background: var(--kombos-white);\n transition: all 0.15s;\n}\n\n.kombos-pd__dot--active::after {\n width: 2rem;\n background: var(--kombos-gray-900);\n border-color: var(--kombos-gray-200);\n}\n\n/* ===== Tablet (>=768px) ===== */\n@media (min-width: 768px) {\n .kombos-pd__gallery {\n flex-direction: row;\n gap: 1.25rem;\n flex: 1;\n min-width: 0;\n }\n\n .kombos-pd__thumbs {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n flex-shrink: 0;\n width: 7rem;\n overflow-y: auto;\n scrollbar-width: none;\n }\n\n .kombos-pd__thumbs::-webkit-scrollbar {\n display: none;\n }\n\n .kombos-pd__thumb {\n width: 7rem;\n flex-shrink: 0;\n border-radius: 12px;\n border: 1px solid var(--kombos-gray-200);\n overflow: hidden;\n cursor: pointer;\n padding: 0.375rem;\n background: var(--kombos-white);\n transition: border-color 0.15s;\n }\n\n .kombos-pd__thumb:hover {\n border-color: var(--kombos-gray-500);\n }\n\n .kombos-pd__thumb--active {\n border-color: var(--kombos-gray-900);\n }\n\n .kombos-pd__thumb-img {\n width: 100%;\n height: 100%;\n display: block;\n border-radius: 8px;\n }\n\n .kombos-pd__main-col {\n align-self: flex-start;\n }\n\n .kombos-pd__main-image-wrap {\n border-radius: 6px;\n overflow: hidden;\n }\n\n .kombos-pd__main-image {\n max-height: 45.75rem;\n border-radius: 6px;\n }\n\n}\n"
18097
- },
18098
- {
18099
- "filename": "ikas-config-snippet.json",
18100
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail\",\n \"name\": \"ProductDetail\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ProductDetail/index.tsx\",\n \"styles\": \"./src/components/ProductDetail/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"aspectRatio\",\n \"displayName\": \"En Boy Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"gorsel-ayarlari\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"objectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"gorsel-ayarlari\",\n \"enumTypeId\": \"GrylMqHxui\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Info Components\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-product-detail-name-favorite\",\n \"{{PROJECT_ID}}-product-detail-sku\",\n \"{{PROJECT_ID}}-product-detail-prices\",\n \"{{PROJECT_ID}}-product-detail-product-group\",\n \"{{PROJECT_ID}}-product-detail-variant\",\n \"{{PROJECT_ID}}-product-detail-add-to-cart\",\n \"{{PROJECT_ID}}-product-detail-features\",\n \"{{PROJECT_ID}}-product-detail-description\",\n \"{{PROJECT_ID}}-product-detail-bundle-product\",\n \"{{PROJECT_ID}}-product-detail-option-set\",\n \"{{PROJECT_ID}}-product-detail-offer\"\n ]\n },\n {\n \"name\": \"bottomComponents\",\n \"displayName\": \"Sub Components\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-product-detail-bundle-furniture\",\n \"{{PROJECT_ID}}-product-detail-offer\"\n ]\n },\n {\n \"name\": \"homepageText\",\n \"displayName\": \"Home Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Home\",\n \"groupId\": \"metinler\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"gorsel-ayarlari\",\n \"name\": \"Image Settings\",\n \"description\": \"Product görselinin en boy oranı ve sığdırma modu\"\n },\n {\n \"id\": \"metinler\",\n \"name\": \"Texts\",\n \"description\": \"Breadcrumb ve sayfa metinleri\"\n }\n ]\n}"
18101
- },
18102
- {
18103
- "filename": "index.tsx",
18104
- "content": "import {\n getIkasCategoryPathItemHref,\n getProductCategoryPath,\n getProductVariantMainImage,\n getSelectedProductVariant,\n IkasImage,\n isNotEmpty,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport Breadcrumb from \"../../sub-components/Breadcrumb\";\nimport type { BreadcrumbItem } from \"../../sub-components/Breadcrumb\";\nimport ProductGallery from \"./components/ProductGallery\";\n\nexport function ProductDetail(props: Props) {\n const {\n product,\n components,\n aspectRatio,\n objectFit,\n bottomComponents,\n homepageText = \"Anasayfa\",\n } = props;\n if (!product) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\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\n const categoryPath = getProductCategoryPath(product);\n\n return (\n <section className=\"kombos-pd\">\n <div className=\"kombos-container kombos-pd__container\">\n <Breadcrumb\n items={[\n { label: homepageText, href: \"/\" } as BreadcrumbItem,\n ...(isNotEmpty(categoryPath)\n ? categoryPath.map(\n (pathItem: any) =>\n ({\n label: pathItem.name,\n href: getIkasCategoryPathItemHref(pathItem),\n }) as BreadcrumbItem,\n )\n : []),\n { label: product.name } as BreadcrumbItem,\n ]}\n size=\"xs\"\n className=\"kombos-pd__breadcrumb\"\n />\n\n <div className=\"kombos-pd__layout\">\n <ProductGallery\n images={images}\n productName={product.name}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n />\n\n <div className=\"kombos-pd__info\">\n <IkasComponentRenderer\n id=\"product-detail-info\"\n components={components}\n parentProps={props}\n />\n </div>\n </div>\n\n {bottomComponents && (\n <div className=\"kombos-pd__bottom\">\n <IkasComponentRenderer\n id=\"product-detail-bottom\"\n components={bottomComponents}\n parentProps={props}\n />\n </div>\n )}\n </div>\n </section>\n );\n}\n\nexport default ProductDetail;\n"
18105
- },
18106
- {
18107
- "filename": "styles.css",
18108
- "content": "/* ===== Product Detail Section ===== */\n\n.kombos-pd {\n width: 100%;\n}\n\n/* ---- Breadcrumb (mobile only, hidden on desktop) ---- */\n.kombos-pd__breadcrumb {\n padding-top: 1rem;\n}\n\n/* ---- Layout ---- */\n.kombos-pd__layout {\n display: flex;\n flex-direction: column;\n padding-top: 1rem;\n}\n\n/* ---- Product Info ---- */\n.kombos-pd__info {\n display: flex;\n flex-direction: column;\n padding-top: 1rem;\n padding-bottom: 1rem;\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd__container {\n padding-top: 2rem;\n padding-bottom: 2rem;\n }\n\n .kombos-pd__breadcrumb {\n padding-top: 0;\n padding-bottom: 1.5rem;\n }\n\n .kombos-pd__layout {\n display: grid;\n grid-template-columns: 7fr 5fr;\n gap: 2rem;\n padding-top: 0;\n }\n\n /* Gallery — sticky on desktop */\n .kombos-pd__gallery {\n position: sticky;\n top: 0.75rem;\n align-self: start;\n }\n\n /* Info — right side */\n .kombos-pd__info {\n min-width: 0;\n padding: 0;\n }\n}\n"
18109
- },
18110
- {
18111
- "filename": "types.ts",
18112
- "content": "import type { IkasProduct } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, ObjectFit } from \"../../global-types\";\n\nexport interface Props {\n product?: IkasProduct | null;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n components?: any;\n bottomComponents?: any;\n homepageText?: string;\n}\n"
18113
- }
18114
- ]
18115
- },
18116
- {
18117
- "id": "product-pricing",
18118
- "title": "Product Pricing Pattern",
18119
- "description": "Product price display with discount detection, formatted prices, campaign pricing, and currency handling. Shows getFormattedPrice, hasDiscount, getIkasProductPrices, and sellPrice/listPrice patterns.",
18120
- "code": "import {\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n getProductVariantDiscountPercentage,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n getSelectedProductVariant,\n hasProductVariantDiscount,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport Tag from \"../../sub-components/Tag\";\n\nexport function ProductDetailPrices({\n product,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n}: Props) {\n if (!product) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\n\n const hasDiscount = hasProductVariantDiscount(selectedVariant);\n const finalPrice = getProductVariantFormattedFinalPrice(selectedVariant);\n const originalPrice = hasDiscount\n ? getProductVariantFormattedSellPrice(selectedVariant)\n : null;\n const discountPercentage = hasDiscount\n ? getProductVariantDiscountPercentage(selectedVariant)\n : null;\n\n return (\n <div\n className=\"kombos-pd-prices\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n <div className=\"kombos-pd-prices__prices\">\n <span className=\"kombos-pd-prices__final text-xl-medium md:display-xs-medium\">\n {finalPrice}\n </span>\n {originalPrice && (\n <span className=\"kombos-pd-prices__original text-lg-medium-strike md:text-xl-medium-strike\">\n {originalPrice}\n </span>\n )}\n </div>\n {discountPercentage != null && Number(discountPercentage) > 0 && (\n <Tag type=\"discounted\" size=\"m\">\n %{Math.round(Number(discountPercentage))}\n </Tag>\n )}\n </div>\n );\n}\n\nexport default ProductDetailPrices;\n",
18121
- "relatedFunctions": [
18122
- "getFormattedMarginTopSize",
18123
- "getFormattedMarginBottomSize",
18124
- "getProductVariantDiscountPercentage",
18125
- "getProductVariantFormattedFinalPrice",
18126
- "getProductVariantFormattedSellPrice",
18127
- "getSelectedProductVariant",
18128
- "hasProductVariantDiscount"
18129
- ],
18130
- "categories": [
18131
- "Product",
18132
- "ProductDetail",
18133
- "Pricing"
18134
- ],
18135
- "files": [
18136
- {
18137
- "filename": "ikas-config-snippet.json",
18138
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-prices\",\n \"name\": \"ProductDetailPrices\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailPrices/index.tsx\",\n \"styles\": \"./src/components/ProductDetailPrices/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n }\n ]\n}"
18139
- },
18140
- {
18141
- "filename": "index.tsx",
18142
- "content": "import {\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n getProductVariantDiscountPercentage,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n getSelectedProductVariant,\n hasProductVariantDiscount,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport Tag from \"../../sub-components/Tag\";\n\nexport function ProductDetailPrices({\n product,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n}: Props) {\n if (!product) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\n\n const hasDiscount = hasProductVariantDiscount(selectedVariant);\n const finalPrice = getProductVariantFormattedFinalPrice(selectedVariant);\n const originalPrice = hasDiscount\n ? getProductVariantFormattedSellPrice(selectedVariant)\n : null;\n const discountPercentage = hasDiscount\n ? getProductVariantDiscountPercentage(selectedVariant)\n : null;\n\n return (\n <div\n className=\"kombos-pd-prices\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n <div className=\"kombos-pd-prices__prices\">\n <span className=\"kombos-pd-prices__final text-xl-medium md:display-xs-medium\">\n {finalPrice}\n </span>\n {originalPrice && (\n <span className=\"kombos-pd-prices__original text-lg-medium-strike md:text-xl-medium-strike\">\n {originalPrice}\n </span>\n )}\n </div>\n {discountPercentage != null && Number(discountPercentage) > 0 && (\n <Tag type=\"discounted\" size=\"m\">\n %{Math.round(Number(discountPercentage))}\n </Tag>\n )}\n </div>\n );\n}\n\nexport default ProductDetailPrices;\n"
18143
- },
18144
- {
18145
- "filename": "styles.css",
18146
- "content": "/* ===== ProductDetailPrices ===== */\n\n.kombos-pd-prices {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n flex-wrap: wrap;\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n.kombos-pd-prices__prices {\n display: flex;\n align-items: baseline;\n gap: 0.5rem;\n}\n\n.kombos-pd-prices__final {\n color: var(--kombos-gray-900);\n}\n\n.kombos-pd-prices__original {\n color: var(--kombos-gray-500);\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd-prices {\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n}\n"
18147
- },
18148
- {
18149
- "filename": "types.ts",
18150
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n}\n"
18151
- }
18152
- ]
18153
- },
18154
- {
18155
- "id": "product-reviews-section",
18156
- "title": "Product Reviews Section",
18157
- "description": "Product review display and submission. Shows review summary with star distribution, individual review cards with ratings, and a review form for authenticated customers. Uses ReviewSummary, ReviewCard, and ReviewForm sub-components.",
18158
- "code": "import { useState, useEffect, useRef } from \"preact/hooks\";\nimport type { IkasCustomerReviewList } from \"@ikas/bp-storefront\";\nimport {\n customerStore,\n getProductCustomerReviews,\n getCustomerReviewListPageCount,\n hasCustomerReviewListPrevPage,\n hasCustomerReviewListNextPage,\n getCustomerReviewListPage,\n isCustomerReviewEnabled,\n isCustomerReviewLoginRequired,\n hasCustomer,\n Router,\n getIkasProductCustomerReviewForm,\n clearIkasProductCustomerReviewForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport Pagination from \"../../sub-components/Pagination\";\nimport Button from \"../../sub-components/Button\";\nimport Modal from \"../../sub-components/Modal\";\nimport ImagePreviewModal from \"../../sub-components/ImagePreviewModal\";\nimport ReviewSummary from \"../../sub-components/ReviewSummary\";\nimport ReviewCard from \"../../sub-components/ReviewCard\";\nimport ReviewForm from \"../../sub-components/ReviewForm\";\nimport PageLoader from \"../../sub-components/PageLoader\";\n\nexport function ProductDetailReviews({\n product,\n reviewsPerPage = 5,\n sectionTitle = \"Customer Reviews\",\n writeReviewButtonText = \"Write a Review\",\n emptyStateText = \"No reviews yet. Be the first to review this product!\",\n reviewCountText = \"Based on {count} reviews\",\n submitButtonText = \"Submit Review\",\n submittingButtonText = \"Submitting...\",\n titlePlaceholder = \"Review title\",\n commentPlaceholder = \"Write your review...\",\n merchantReplyLabel = \"Store Reply\",\n starRequiredError = \"Please select a rating\",\n cancelButtonText = \"Cancel\",\n errorMessage = \"Something went wrong. Please try again.\",\n}: Props) {\n const listRef = useRef<HTMLDivElement>(null);\n const [reviewList, setReviewList] = useState<IkasCustomerReviewList | null>(\n null,\n );\n const [formVisible, setFormVisible] = useState(false);\n const [previewImage, setPreviewImage] = useState<{\n src: string;\n alt: string;\n } | null>(null);\n const [isLoading, setIsLoading] = useState(false);\n\n useEffect(() => {\n if (!product) return;\n\n setIsLoading(true);\n\n getProductCustomerReviews(product, reviewsPerPage)\n .then((result) => {\n setReviewList(result);\n })\n .finally(() => {\n setIsLoading(false);\n });\n }, [product, reviewsPerPage]);\n\n if (!product) return null;\n\n const reviewsEnabled = isCustomerReviewEnabled(product);\n const hasReviews = reviewList && reviewList.data.length > 0;\n const hasSummary =\n product.averageRating !== null && product.reviewCount !== null;\n\n const handleWriteReview = () => {\n if (isCustomerReviewLoginRequired(product) && !hasCustomer(customerStore)) {\n Router.navigateToPage(\"LOGIN\");\n return;\n }\n\n setFormVisible(true);\n };\n\n const handlePageChange = async (page: number) => {\n if (!reviewList) return;\n\n await getCustomerReviewListPage(reviewList, page);\n listRef.current?.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n };\n\n const reviewForm = getIkasProductCustomerReviewForm(product);\n\n const handleCloseForm = () => {\n clearIkasProductCustomerReviewForm(product);\n setFormVisible(false);\n };\n\n const handleReviewSuccess = () => {\n getProductCustomerReviews(product, reviewsPerPage).then(setReviewList);\n handleCloseForm();\n };\n\n return (\n <section className=\"product-detail-reviews\">\n <div className=\"kombos-container product-detail-reviews__inner\">\n <div className=\"product-detail-reviews__header\">\n <h2 className=\"product-detail-reviews__title display-xs-semibold\">\n {sectionTitle}\n </h2>\n {reviewsEnabled && !hasSummary && (\n <Button variant=\"secondary\" size=\"s\" onClick={handleWriteReview}>\n {writeReviewButtonText}\n </Button>\n )}\n </div>\n\n {hasSummary && (\n <ReviewSummary\n averageStar={product.averageRating!}\n totalReview={product.reviewCount!}\n stars={product.stars ?? []}\n reviewCountText={reviewCountText}\n action={\n reviewsEnabled ? (\n <Button\n variant=\"secondary\"\n size=\"s\"\n onClick={handleWriteReview}\n >\n {writeReviewButtonText}\n </Button>\n ) : undefined\n }\n />\n )}\n\n {isLoading && <PageLoader />}\n\n {!hasReviews && !isLoading && reviewList && (\n <p className=\"product-detail-reviews__empty text-md-regular\">\n {emptyStateText}\n </p>\n )}\n\n {hasReviews && !isLoading && (\n <>\n <div ref={listRef} className=\"product-detail-reviews__list\">\n {reviewList?.data.map((review) => (\n <ReviewCard\n key={review.id}\n review={review}\n merchantReplyLabel={merchantReplyLabel}\n onImageClick={(src, alt) => setPreviewImage({ src, alt })}\n />\n ))}\n </div>\n\n <Pagination\n currentPage={reviewList.page}\n totalPages={getCustomerReviewListPageCount(reviewList!)}\n hasPrev={hasCustomerReviewListPrevPage(reviewList!)}\n hasNext={hasCustomerReviewListNextPage(reviewList!)}\n onPageChange={handlePageChange}\n />\n </>\n )}\n </div>\n\n {formVisible && (\n <Modal\n title={writeReviewButtonText}\n onClose={handleCloseForm}\n okText={\n reviewForm.isSubmitting ? submittingButtonText : submitButtonText\n }\n cancelText={cancelButtonText}\n okButtonProps={{\n type: \"submit\",\n form: \"review-form\",\n disabled: reviewForm.isSubmitting,\n }}\n >\n <ReviewForm\n product={product}\n onSuccess={handleReviewSuccess}\n titlePlaceholder={titlePlaceholder}\n commentPlaceholder={commentPlaceholder}\n starRequiredError={starRequiredError}\n errorMessage={errorMessage}\n />\n </Modal>\n )}\n\n {previewImage && (\n <ImagePreviewModal\n src={previewImage.src}\n alt={previewImage.alt}\n onClose={() => setPreviewImage(null)}\n />\n )}\n </section>\n );\n}\n\nexport default ProductDetailReviews;\n",
18159
- "relatedFunctions": [
18160
- "customerStore",
18161
- "getProductCustomerReviews",
18162
- "getCustomerReviewListPageCount",
18163
- "hasCustomerReviewListPrevPage",
18164
- "hasCustomerReviewListNextPage",
18165
- "getCustomerReviewListPage",
18166
- "isCustomerReviewEnabled",
18167
- "isCustomerReviewLoginRequired",
18168
- "hasCustomer",
18169
- "Router",
18170
- "getIkasProductCustomerReviewForm",
18171
- "clearIkasProductCustomerReviewForm"
18172
- ],
18173
- "categories": [
18174
- "Product",
18175
- "ProductDetail",
18176
- "Review"
18177
- ],
18178
- "files": [
18179
- {
18180
- "filename": "ikas-config-snippet.json",
18181
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-reviews\",\n \"name\": \"ProductDetailReviews\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ProductDetailReviews/index.tsx\",\n \"styles\": \"./src/components/ProductDetailReviews/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"reviewsPerPage\",\n \"displayName\": \"Reviews Per Page\",\n \"type\": \"NUMBER\",\n \"required\": false,\n \"defaultValue\": 5,\n \"groupId\": \"settings\"\n },\n {\n \"name\": \"sectionTitle\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Customer Reviews\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"writeReviewButtonText\",\n \"displayName\": \"Write Review Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Write a Review\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"emptyStateText\",\n \"displayName\": \"Empty State Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Number reviews yet. Be the first to review this product!\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"reviewCountText\",\n \"displayName\": \"Review Count Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Based on {count} reviews\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"submitButtonText\",\n \"displayName\": \"Submit Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Submit Review\",\n \"groupId\": \"form\"\n },\n {\n \"name\": \"submittingButtonText\",\n \"displayName\": \"Submitting Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Submitting...\",\n \"groupId\": \"form\"\n },\n {\n \"name\": \"titlePlaceholder\",\n \"displayName\": \"Title Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Review title\",\n \"groupId\": \"form\"\n },\n {\n \"name\": \"commentPlaceholder\",\n \"displayName\": \"Comment Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Write your review...\",\n \"groupId\": \"form\"\n },\n {\n \"name\": \"merchantReplyLabel\",\n \"displayName\": \"Merchant Reply Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Store Reply\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"starRequiredError\",\n \"displayName\": \"Star Required Error\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Please select a rating\",\n \"groupId\": \"form\"\n },\n {\n \"name\": \"cancelButtonText\",\n \"displayName\": \"Cancel Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Cancel\",\n \"groupId\": \"form\"\n },\n {\n \"name\": \"errorMessage\",\n \"displayName\": \"Error Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Something went wrong. Please try again.\",\n \"groupId\": \"form\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Section başlığı, boş durum ve yorum sayısı metinleri\"\n },\n {\n \"id\": \"form\",\n \"name\": \"Form Texts\",\n \"description\": \"Review yazma formundaki alan ve buton metinleri\"\n },\n {\n \"id\": \"settings\",\n \"name\": \"Settings\",\n \"description\": \"Pagination ve görüntüleme ayarları\"\n }\n ]\n}"
18182
- },
18183
- {
18184
- "filename": "index.tsx",
18185
- "content": "import { useState, useEffect, useRef } from \"preact/hooks\";\nimport type { IkasCustomerReviewList } from \"@ikas/bp-storefront\";\nimport {\n customerStore,\n getProductCustomerReviews,\n getCustomerReviewListPageCount,\n hasCustomerReviewListPrevPage,\n hasCustomerReviewListNextPage,\n getCustomerReviewListPage,\n isCustomerReviewEnabled,\n isCustomerReviewLoginRequired,\n hasCustomer,\n Router,\n getIkasProductCustomerReviewForm,\n clearIkasProductCustomerReviewForm,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport Pagination from \"../../sub-components/Pagination\";\nimport Button from \"../../sub-components/Button\";\nimport Modal from \"../../sub-components/Modal\";\nimport ImagePreviewModal from \"../../sub-components/ImagePreviewModal\";\nimport ReviewSummary from \"../../sub-components/ReviewSummary\";\nimport ReviewCard from \"../../sub-components/ReviewCard\";\nimport ReviewForm from \"../../sub-components/ReviewForm\";\nimport PageLoader from \"../../sub-components/PageLoader\";\n\nexport function ProductDetailReviews({\n product,\n reviewsPerPage = 5,\n sectionTitle = \"Customer Reviews\",\n writeReviewButtonText = \"Write a Review\",\n emptyStateText = \"No reviews yet. Be the first to review this product!\",\n reviewCountText = \"Based on {count} reviews\",\n submitButtonText = \"Submit Review\",\n submittingButtonText = \"Submitting...\",\n titlePlaceholder = \"Review title\",\n commentPlaceholder = \"Write your review...\",\n merchantReplyLabel = \"Store Reply\",\n starRequiredError = \"Please select a rating\",\n cancelButtonText = \"Cancel\",\n errorMessage = \"Something went wrong. Please try again.\",\n}: Props) {\n const listRef = useRef<HTMLDivElement>(null);\n const [reviewList, setReviewList] = useState<IkasCustomerReviewList | null>(\n null,\n );\n const [formVisible, setFormVisible] = useState(false);\n const [previewImage, setPreviewImage] = useState<{\n src: string;\n alt: string;\n } | null>(null);\n const [isLoading, setIsLoading] = useState(false);\n\n useEffect(() => {\n if (!product) return;\n\n setIsLoading(true);\n\n getProductCustomerReviews(product, reviewsPerPage)\n .then((result) => {\n setReviewList(result);\n })\n .finally(() => {\n setIsLoading(false);\n });\n }, [product, reviewsPerPage]);\n\n if (!product) return null;\n\n const reviewsEnabled = isCustomerReviewEnabled(product);\n const hasReviews = reviewList && reviewList.data.length > 0;\n const hasSummary =\n product.averageRating !== null && product.reviewCount !== null;\n\n const handleWriteReview = () => {\n if (isCustomerReviewLoginRequired(product) && !hasCustomer(customerStore)) {\n Router.navigateToPage(\"LOGIN\");\n return;\n }\n\n setFormVisible(true);\n };\n\n const handlePageChange = async (page: number) => {\n if (!reviewList) return;\n\n await getCustomerReviewListPage(reviewList, page);\n listRef.current?.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n };\n\n const reviewForm = getIkasProductCustomerReviewForm(product);\n\n const handleCloseForm = () => {\n clearIkasProductCustomerReviewForm(product);\n setFormVisible(false);\n };\n\n const handleReviewSuccess = () => {\n getProductCustomerReviews(product, reviewsPerPage).then(setReviewList);\n handleCloseForm();\n };\n\n return (\n <section className=\"product-detail-reviews\">\n <div className=\"kombos-container product-detail-reviews__inner\">\n <div className=\"product-detail-reviews__header\">\n <h2 className=\"product-detail-reviews__title display-xs-semibold\">\n {sectionTitle}\n </h2>\n {reviewsEnabled && !hasSummary && (\n <Button variant=\"secondary\" size=\"s\" onClick={handleWriteReview}>\n {writeReviewButtonText}\n </Button>\n )}\n </div>\n\n {hasSummary && (\n <ReviewSummary\n averageStar={product.averageRating!}\n totalReview={product.reviewCount!}\n stars={product.stars ?? []}\n reviewCountText={reviewCountText}\n action={\n reviewsEnabled ? (\n <Button\n variant=\"secondary\"\n size=\"s\"\n onClick={handleWriteReview}\n >\n {writeReviewButtonText}\n </Button>\n ) : undefined\n }\n />\n )}\n\n {isLoading && <PageLoader />}\n\n {!hasReviews && !isLoading && reviewList && (\n <p className=\"product-detail-reviews__empty text-md-regular\">\n {emptyStateText}\n </p>\n )}\n\n {hasReviews && !isLoading && (\n <>\n <div ref={listRef} className=\"product-detail-reviews__list\">\n {reviewList?.data.map((review) => (\n <ReviewCard\n key={review.id}\n review={review}\n merchantReplyLabel={merchantReplyLabel}\n onImageClick={(src, alt) => setPreviewImage({ src, alt })}\n />\n ))}\n </div>\n\n <Pagination\n currentPage={reviewList.page}\n totalPages={getCustomerReviewListPageCount(reviewList!)}\n hasPrev={hasCustomerReviewListPrevPage(reviewList!)}\n hasNext={hasCustomerReviewListNextPage(reviewList!)}\n onPageChange={handlePageChange}\n />\n </>\n )}\n </div>\n\n {formVisible && (\n <Modal\n title={writeReviewButtonText}\n onClose={handleCloseForm}\n okText={\n reviewForm.isSubmitting ? submittingButtonText : submitButtonText\n }\n cancelText={cancelButtonText}\n okButtonProps={{\n type: \"submit\",\n form: \"review-form\",\n disabled: reviewForm.isSubmitting,\n }}\n >\n <ReviewForm\n product={product}\n onSuccess={handleReviewSuccess}\n titlePlaceholder={titlePlaceholder}\n commentPlaceholder={commentPlaceholder}\n starRequiredError={starRequiredError}\n errorMessage={errorMessage}\n />\n </Modal>\n )}\n\n {previewImage && (\n <ImagePreviewModal\n src={previewImage.src}\n alt={previewImage.alt}\n onClose={() => setPreviewImage(null)}\n />\n )}\n </section>\n );\n}\n\nexport default ProductDetailReviews;\n"
18186
- },
18187
- {
18188
- "filename": "styles.css",
18189
- "content": ".product-detail-reviews {\n width: 100%;\n}\n\n.product-detail-reviews__inner {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n padding-top: 2.5rem;\n padding-bottom: 2.5rem;\n}\n\n@media (min-width: 768px) {\n .product-detail-reviews__inner {\n padding-top: 3.5rem;\n padding-bottom: 3.5rem;\n }\n}\n\n.product-detail-reviews__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n flex-wrap: wrap;\n}\n\n.product-detail-reviews__title {\n margin: 0;\n color: var(--kombos-gray-900);\n}\n\n.product-detail-reviews__empty {\n color: var(--kombos-gray-500);\n text-align: center;\n padding: 3rem 1rem;\n margin: 0;\n}\n\n.product-detail-reviews__list {\n display: flex;\n flex-direction: column;\n}\n"
18190
- },
18191
- {
18192
- "filename": "types.ts",
18193
- "content": "import type { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n reviewsPerPage?: number;\n sectionTitle?: string;\n writeReviewButtonText?: string;\n emptyStateText?: string;\n reviewCountText?: string;\n submitButtonText?: string;\n submittingButtonText?: string;\n titlePlaceholder?: string;\n commentPlaceholder?: string;\n merchantReplyLabel?: string;\n starRequiredError?: string;\n cancelButtonText?: string;\n errorMessage?: string;\n}\n"
18194
- }
18195
- ]
18196
- },
18197
- {
18198
- "id": "product-slider-section",
18199
- "title": "Product Slider Section",
18200
- "description": "Horizontal product card carousel with configurable product list. Uses ProductCard sub-component with IkasComponentRenderer for customizable card content (name, price, variants). Supports privateVarMap to pass product data to children.",
18201
- "code": "import {\n useRef,\n useState,\n useEffect,\n useMemo,\n useCallback,\n} from \"preact/hooks\";\nimport {\n getProductOptionSet,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport ProductCard from \"../../sub-components/ProductCard\";\nimport SliderArrow from \"../../sub-components/SliderArrow\";\nimport { Props } from \"./types\";\n\nconst COLUMNS_MAP: Record<string, number> = {\n One: 1,\n Two: 2,\n Three: 3,\n Four: 4,\n Five: 5,\n};\n\nexport function ProductSlider(props: Props) {\n const {\n productList,\n title,\n addToCartText = \"Sepete Ekle\",\n addedToCartText = \"Sepete Eklendi\",\n outOfStockText = \"Tükendi\",\n goToProductText = \"Ürüne Git\",\n desktopColumns,\n aspectRatio,\n objectFit,\n hideAddToCartButton,\n components,\n } = props;\n\n const cols = COLUMNS_MAP[desktopColumns ?? \"\"] ?? 4;\n const trackRef = useRef<HTMLDivElement>(null);\n const [canScrollLeft, setCanScrollLeft] = useState(false);\n const [canScrollRight, setCanScrollRight] = useState(false);\n\n const updateScrollState = useCallback(() => {\n const track = trackRef.current;\n if (!track) return;\n\n setCanScrollLeft(track.scrollLeft > 1);\n setCanScrollRight(\n track.scrollLeft + track.clientWidth < track.scrollWidth - 1,\n );\n }, []);\n\n useEffect(() => {\n const track = trackRef.current;\n if (!track) return;\n track.addEventListener(\"scroll\", updateScrollState, { passive: true });\n window.addEventListener(\"resize\", updateScrollState);\n const ro = new ResizeObserver(updateScrollState);\n ro.observe(track);\n return () => {\n track.removeEventListener(\"scroll\", updateScrollState);\n window.removeEventListener(\"resize\", updateScrollState);\n ro.disconnect();\n };\n }, [updateScrollState]);\n\n const products = productList?.data ?? [];\n\n useEffect(() => {\n products.forEach((p) => {\n if (!p.productOptionSet) getProductOptionSet(p);\n });\n }, [products.length]);\n\n const productCardSizes = useMemo(() => {\n const desktopGaps = (cols - 1) * 24;\n return `(max-width: 767px) calc((100vw - 48px) / 2), (max-width: 1023px) calc((100vw - 80px) / 2), calc((100vw - ${144 + desktopGaps}px) / ${cols})`;\n }, [cols]);\n\n if (!productList) return null;\n\n const scroll = (direction: \"left\" | \"right\") => {\n const el = trackRef.current;\n if (!el) return;\n const amount = direction === \"left\" ? -el.clientWidth : el.clientWidth;\n el.scrollBy({ left: amount, behavior: \"smooth\" });\n };\n\n return (\n <section className=\"kombos-product-slider\">\n <div className=\"kombos-product-slider__wrapper kombos-container\">\n {title && (\n <h2 className=\"kombos-product-slider__title display-xs-medium md:display-sm-medium\">\n {title}\n </h2>\n )}\n\n <div className=\"kombos-product-slider__slider\">\n <SliderArrow\n direction=\"left\"\n className=\"kombos-product-slider__arrow kombos-product-slider__arrow--left\"\n onClick={() => scroll(\"left\")}\n disabled={!canScrollLeft}\n />\n\n <div\n className=\"kombos-product-slider__track\"\n ref={trackRef}\n style={{ \"--columns\": cols }}\n >\n {products.map((product, index) => (\n <div key={product.id} className=\"kombos-product-slider__card\">\n <ProductCard\n product={product}\n addToCartText={addToCartText}\n addedToCartText={addedToCartText}\n outOfStockText={outOfStockText}\n goToProductText={goToProductText}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n sizes={productCardSizes}\n hideAddToCartButton={hideAddToCartButton}\n priority={index < 4}\n />\n <IkasComponentRenderer\n id={`product-slider-product-${product.id}`}\n components={components}\n parentProps={props}\n map={{\n product,\n }}\n className=\"kombos-product-slider__card-content\"\n />\n </div>\n ))}\n </div>\n\n <SliderArrow\n direction=\"right\"\n className=\"kombos-product-slider__arrow kombos-product-slider__arrow--right\"\n onClick={() => scroll(\"right\")}\n disabled={!canScrollRight}\n />\n </div>\n </div>\n </section>\n );\n}\n\nexport default ProductSlider;\n",
18202
- "relatedFunctions": [
18203
- "getProductOptionSet",
18204
- "getSelectedProductVariant",
18205
- "getProductVariantFormattedFinalPrice",
18206
- "getProductVariantFormattedSellPrice",
18207
- "hasProductVariantDiscount",
18208
- "getProductHref"
18209
- ],
18210
- "categories": [
18211
- "Product",
18212
- "Slider",
18213
- "ProductList"
18214
- ],
18215
- "files": [
18216
- {
18217
- "filename": "children/CardProductName/ikas-config-snippet.json",
18218
- "content": "{\n \"id\": \"{{PROJECT_ID}}-card-product-name\",\n \"name\": \"CardProductName\",\n \"type\": \"component\",\n \"entry\": \"./src/components/CardProductName/index.tsx\",\n \"styles\": \"./src/components/CardProductName/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"singleLineName\",\n \"displayName\": \"Single Line Name\",\n \"type\": \"BOOLEAN\",\n \"required\": false\n }\n ]\n}"
18219
- },
18220
- {
18221
- "filename": "children/CardProductName/index.tsx",
18222
- "content": "import { getProductHref } from \"@ikas/bp-storefront\";\nimport { cx } from \"../../utils/cx\";\nimport { Props } from \"./types\";\n\nexport function CardProductName({ product, singleLineName }: Props) {\n if (!product) return null;\n\n const productHref = getProductHref(product);\n\n return (\n <span className=\"kombos-card-product-name\">\n <a\n href={productHref}\n className={cx(\n \"kombos-card-product-name__link text-sm-medium md:text-md-medium\",\n singleLineName && \"kombos-card-product-name__link--single-line\",\n )}\n >\n {product.name}\n </a>\n </span>\n );\n}\n\nexport default CardProductName;\n"
18223
- },
18224
- {
18225
- "filename": "children/CardProductName/styles.css",
18226
- "content": ".kombos-card-product-name {\n display: block;\n width: fit-content;\n max-width: 100%;\n}\n\n.kombos-card-product-name__link {\n display: block;\n color: var(--kombos-gray-900);\n text-decoration: none;\n max-width: 100%;\n}\n\n.kombos-card-product-name__link:hover {\n text-decoration: underline;\n}\n\n.kombos-card-product-name__link--single-line {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n"
18227
- },
18228
- {
18229
- "filename": "children/CardProductName/types.ts",
18230
- "content": "import type { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n singleLineName?: boolean;\n}\n"
18231
- },
18232
- {
18233
- "filename": "children/CardProductPrice/ikas-config-snippet.json",
18234
- "content": "{\n \"id\": \"{{PROJECT_ID}}-card-product-price\",\n \"name\": \"CardProductPrice\",\n \"type\": \"component\",\n \"entry\": \"./src/components/CardProductPrice/index.tsx\",\n \"styles\": \"./src/components/CardProductPrice/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n }\n ]\n}"
18235
- },
18236
- {
18237
- "filename": "children/CardProductPrice/index.tsx",
18238
- "content": "import {\n getSelectedProductVariant,\n getProductVariantFormattedFinalPrice,\n getProductVariantFormattedSellPrice,\n hasProductVariantDiscount,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\n\nexport function CardProductPrice({ product }: Props) {\n if (!product) return null;\n\n const selectedVariant = getSelectedProductVariant(product);\n\n const hasDiscount = hasProductVariantDiscount(selectedVariant);\n\n return (\n <div className=\"kombos-card-product-price\">\n <span className=\"kombos-card-product-price__current text-sm-medium sm:text-md-medium md:text-xl-medium\">\n {getProductVariantFormattedFinalPrice(selectedVariant)}\n </span>\n {hasDiscount && (\n <span className=\"kombos-card-product-price__old text-xs-regular-strike sm:text-sm-regular-strike md:text-md-regular-strike\">\n {getProductVariantFormattedSellPrice(selectedVariant)}\n </span>\n )}\n </div>\n );\n}\n\nexport default CardProductPrice;\n"
18239
- },
18240
- {
18241
- "filename": "children/CardProductPrice/styles.css",
18242
- "content": ".kombos-card-product-price {\n display: flex;\n align-items: baseline;\n gap: 0.5rem;\n}\n\n.kombos-card-product-price__current {\n color: var(--kombos-gray-900);\n}\n\n.kombos-card-product-price__old {\n color: var(--kombos-gray-500);\n}\n"
18243
- },
18244
- {
18245
- "filename": "children/CardProductPrice/types.ts",
18246
- "content": "import type { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n}\n"
18247
- },
18248
- {
18249
- "filename": "children/CardProductVariants/ikas-config-snippet.json",
18250
- "content": "{\n \"id\": \"{{PROJECT_ID}}-card-product-variants\",\n \"name\": \"CardProductVariants\",\n \"type\": \"component\",\n \"entry\": \"./src/components/CardProductVariants/index.tsx\",\n \"styles\": \"./src/components/CardProductVariants/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n }\n ]\n}"
18251
- },
18252
- {
18253
- "filename": "children/CardProductVariants/index.tsx",
18254
- "content": "import { Props } from \"./types\";\nimport VariantBadge from \"../../sub-components/VariantBadge\";\n\nexport function CardProductVariants({ product }: Props) {\n if (!product) return null;\n\n return <VariantBadge product={product} size=\"s\" scrollable disableRoute />;\n}\n\nexport default CardProductVariants;\n"
18255
- },
18256
- {
18257
- "filename": "children/CardProductVariants/styles.css",
18258
- "content": "/* CardProductVariants — styles handled by VariantBadge sub-component */\n"
18259
- },
18260
- {
18261
- "filename": "children/CardProductVariants/types.ts",
18262
- "content": "import type { IkasProduct } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n}\n"
18263
- },
18264
- {
18265
- "filename": "ikas-config-snippet.json",
18266
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-slider\",\n \"name\": \"ProductSlider\",\n \"type\": \"section\",\n \"entry\": \"./src/components/ProductSlider/index.tsx\",\n \"styles\": \"./src/components/ProductSlider/styles.css\",\n \"props\": [\n {\n \"name\": \"productList\",\n \"displayName\": \"Products\",\n \"type\": \"PRODUCT_LIST\",\n \"required\": true,\n \"groupId\": \"content\"\n },\n {\n \"name\": \"title\",\n \"displayName\": \"Section Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Yeni Gelenler\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"addToCartText\",\n \"displayName\": \"Cart Add Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Add to Cart\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"addedToCartText\",\n \"displayName\": \"Added Bildirimi\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Added to Cart\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"outOfStockText\",\n \"displayName\": \"Stokta Yok Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sold Out\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"goToProductText\",\n \"displayName\": \"Product Git Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Go to Product\",\n \"groupId\": \"texts\"\n },\n {\n \"name\": \"desktopColumns\",\n \"displayName\": \"Column Count\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"appearance\",\n \"enumTypeId\": \"Tkt3wfuJ5F\"\n },\n {\n \"name\": \"aspectRatio\",\n \"displayName\": \"En Boy Ratio\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"appearance\",\n \"enumTypeId\": \"s3OODUmZpJ\"\n },\n {\n \"name\": \"objectFit\",\n \"displayName\": \"Image Fit\",\n \"type\": \"ENUM\",\n \"required\": false,\n \"groupId\": \"appearance\",\n \"enumTypeId\": \"GrylMqHxui\"\n },\n {\n \"name\": \"hideAddToCartButton\",\n \"displayName\": \"Cart Add Butonunu Hide\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"appearance\"\n },\n {\n \"name\": \"components\",\n \"displayName\": \"Components\",\n \"type\": \"COMPONENT_LIST\",\n \"required\": false,\n \"privateVarMap\": {\n \"product\": {\n \"id\": \"pvm_1772803530689_1\",\n \"typeId\": \"@ikas/bp-storefront-models-IkasProduct\"\n }\n },\n \"filteredComponentIds\": [\n \"{{PROJECT_ID}}-card-product-price\",\n \"{{PROJECT_ID}}-card-product-variants\",\n \"{{PROJECT_ID}}-card-product-name\"\n ]\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"content\",\n \"name\": \"Content\",\n \"description\": \"Slider'da gösterilecek ürün listesi ve bölüm başlığı\"\n },\n {\n \"id\": \"texts\",\n \"name\": \"Texts\",\n \"description\": \"Product kartlarında görünen buton ve durum metinleri\"\n },\n {\n \"id\": \"appearance\",\n \"name\": \"View\",\n \"description\": \"Column sayısı, görsel ayarları ve kart görünüm tercihleri\"\n }\n ]\n}"
18267
- },
18268
- {
18269
- "filename": "index.tsx",
18270
- "content": "import {\n useRef,\n useState,\n useEffect,\n useMemo,\n useCallback,\n} from \"preact/hooks\";\nimport {\n getProductOptionSet,\n IkasComponentRenderer,\n} from \"@ikas/bp-storefront\";\nimport ProductCard from \"../../sub-components/ProductCard\";\nimport SliderArrow from \"../../sub-components/SliderArrow\";\nimport { Props } from \"./types\";\n\nconst COLUMNS_MAP: Record<string, number> = {\n One: 1,\n Two: 2,\n Three: 3,\n Four: 4,\n Five: 5,\n};\n\nexport function ProductSlider(props: Props) {\n const {\n productList,\n title,\n addToCartText = \"Sepete Ekle\",\n addedToCartText = \"Sepete Eklendi\",\n outOfStockText = \"Tükendi\",\n goToProductText = \"Ürüne Git\",\n desktopColumns,\n aspectRatio,\n objectFit,\n hideAddToCartButton,\n components,\n } = props;\n\n const cols = COLUMNS_MAP[desktopColumns ?? \"\"] ?? 4;\n const trackRef = useRef<HTMLDivElement>(null);\n const [canScrollLeft, setCanScrollLeft] = useState(false);\n const [canScrollRight, setCanScrollRight] = useState(false);\n\n const updateScrollState = useCallback(() => {\n const track = trackRef.current;\n if (!track) return;\n\n setCanScrollLeft(track.scrollLeft > 1);\n setCanScrollRight(\n track.scrollLeft + track.clientWidth < track.scrollWidth - 1,\n );\n }, []);\n\n useEffect(() => {\n const track = trackRef.current;\n if (!track) return;\n track.addEventListener(\"scroll\", updateScrollState, { passive: true });\n window.addEventListener(\"resize\", updateScrollState);\n const ro = new ResizeObserver(updateScrollState);\n ro.observe(track);\n return () => {\n track.removeEventListener(\"scroll\", updateScrollState);\n window.removeEventListener(\"resize\", updateScrollState);\n ro.disconnect();\n };\n }, [updateScrollState]);\n\n const products = productList?.data ?? [];\n\n useEffect(() => {\n products.forEach((p) => {\n if (!p.productOptionSet) getProductOptionSet(p);\n });\n }, [products.length]);\n\n const productCardSizes = useMemo(() => {\n const desktopGaps = (cols - 1) * 24;\n return `(max-width: 767px) calc((100vw - 48px) / 2), (max-width: 1023px) calc((100vw - 80px) / 2), calc((100vw - ${144 + desktopGaps}px) / ${cols})`;\n }, [cols]);\n\n if (!productList) return null;\n\n const scroll = (direction: \"left\" | \"right\") => {\n const el = trackRef.current;\n if (!el) return;\n const amount = direction === \"left\" ? -el.clientWidth : el.clientWidth;\n el.scrollBy({ left: amount, behavior: \"smooth\" });\n };\n\n return (\n <section className=\"kombos-product-slider\">\n <div className=\"kombos-product-slider__wrapper kombos-container\">\n {title && (\n <h2 className=\"kombos-product-slider__title display-xs-medium md:display-sm-medium\">\n {title}\n </h2>\n )}\n\n <div className=\"kombos-product-slider__slider\">\n <SliderArrow\n direction=\"left\"\n className=\"kombos-product-slider__arrow kombos-product-slider__arrow--left\"\n onClick={() => scroll(\"left\")}\n disabled={!canScrollLeft}\n />\n\n <div\n className=\"kombos-product-slider__track\"\n ref={trackRef}\n style={{ \"--columns\": cols }}\n >\n {products.map((product, index) => (\n <div key={product.id} className=\"kombos-product-slider__card\">\n <ProductCard\n product={product}\n addToCartText={addToCartText}\n addedToCartText={addedToCartText}\n outOfStockText={outOfStockText}\n goToProductText={goToProductText}\n aspectRatio={aspectRatio}\n objectFit={objectFit}\n sizes={productCardSizes}\n hideAddToCartButton={hideAddToCartButton}\n priority={index < 4}\n />\n <IkasComponentRenderer\n id={`product-slider-product-${product.id}`}\n components={components}\n parentProps={props}\n map={{\n product,\n }}\n className=\"kombos-product-slider__card-content\"\n />\n </div>\n ))}\n </div>\n\n <SliderArrow\n direction=\"right\"\n className=\"kombos-product-slider__arrow kombos-product-slider__arrow--right\"\n onClick={() => scroll(\"right\")}\n disabled={!canScrollRight}\n />\n </div>\n </div>\n </section>\n );\n}\n\nexport default ProductSlider;\n"
18271
- },
18272
- {
18273
- "filename": "styles.css",
18274
- "content": "/* ===== Product Slider Section ===== */\n.kombos-product-slider {\n box-sizing: border-box;\n width: 100%;\n max-width: 100%;\n overflow: hidden;\n}\n\n.kombos-product-slider__wrapper {\n padding-top: 1rem;\n padding-bottom: 1rem;\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.kombos-product-slider__title {\n color: var(--kombos-gray-900);\n}\n\n/* Slider wrapper */\n.kombos-product-slider__slider {\n position: relative;\n overflow: hidden;\n}\n\n/* Scrollable track */\n.kombos-product-slider__track {\n display: flex;\n gap: 1rem;\n width: 100%;\n overflow-x: auto;\n scroll-snap-type: x mandatory;\n -webkit-overflow-scrolling: touch;\n scrollbar-width: none;\n}\n\n.kombos-product-slider__track::-webkit-scrollbar {\n display: none;\n}\n\n/* Card sizing — mobile: 2 columns, desktop: driven by --columns */\n.kombos-product-slider__card {\n flex: 0 0 calc((100% - 1rem) / 2);\n min-width: 0;\n scroll-snap-align: start;\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.kombos-product-slider__card-content {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n/* Navigation arrows */\n.kombos-product-slider__arrow {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n z-index: var(--kombos-z-dropdown);\n}\n\n.kombos-product-slider__arrow--left {\n left: 0.5rem;\n}\n\n.kombos-product-slider__arrow--right {\n right: 0.5rem;\n}\n\n/* ===== Tablet (>=768px) ===== */\n@media (min-width: 768px) {\n .kombos-product-slider__wrapper {\n padding-top: 1.5rem;\n padding-bottom: 1.5rem;\n }\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-product-slider__wrapper {\n padding-top: 2rem;\n padding-bottom: 2rem;\n gap: 2rem;\n }\n\n .kombos-product-slider__track {\n gap: 1.5rem;\n }\n\n .kombos-product-slider__card {\n flex: 0 0 calc((100% - (var(--columns) - 1) * 1.5rem) / var(--columns));\n }\n\n .kombos-product-slider__arrow--left {\n left: 1rem;\n }\n\n .kombos-product-slider__arrow--right {\n right: 1rem;\n }\n}\n"
18275
- },
18276
- {
18277
- "filename": "types.ts",
18278
- "content": "import type { IkasProductList } from \"@ikas/bp-storefront\";\nimport type { AspectRatio, NumberOfColumns, ObjectFit } from \"../../global-types\";\n\nexport interface Props {\n productList: IkasProductList;\n title?: string;\n addToCartText?: string;\n addedToCartText?: string;\n outOfStockText?: string;\n goToProductText?: string;\n desktopColumns?: NumberOfColumns;\n aspectRatio?: AspectRatio;\n objectFit?: ObjectFit;\n hideAddToCartButton?: boolean;\n components?: any;\n}\n"
18279
- }
18280
- ]
18281
- },
18282
- {
18283
- "id": "recover-password-section",
18284
- "title": "Recover Password Section",
18285
- "description": "Password reset form for completing recovery. Reads token from URL, validates new password with confirmation, calls customerStore.recoverPassword(). Redirects to login on success.",
18286
- "code": "import {\n customerStore,\n getRecoverPasswordForm,\n initRecoverPasswordForm,\n} from \"@ikas/bp-storefront\";\nimport { useRedirectIfLoggedIn } from \"../../hooks/useRedirectIfLoggedIn\";\n\nimport { Props } from \"./types\";\nimport RecoverPasswordForm from \"./components/RecoverPasswordForm\";\nimport PageLoader from \"../../sub-components/PageLoader\";\n\nexport function RecoverPassword(props: Props) {\n const recoverForm = getRecoverPasswordForm(customerStore);\n\n const isChecking = useRedirectIfLoggedIn(() => {\n initRecoverPasswordForm(recoverForm);\n });\n\n if (isChecking) return <PageLoader />;\n\n return (\n <section className=\"recover-password\">\n <div className=\"recover-password__wrapper kombos-container\">\n <RecoverPasswordForm recoverForm={recoverForm} {...props} />\n </div>\n </section>\n );\n}\n\nexport default RecoverPassword;\n",
18287
- "relatedFunctions": [
18288
- "customerStore",
18289
- "getRecoverPasswordForm",
18290
- "initRecoverPasswordForm"
18291
- ],
18292
- "categories": [
18293
- "Customer",
18294
- "Auth",
18295
- "Form"
18296
- ],
18297
- "files": [
18298
- {
18299
- "filename": "components/RecoverPasswordForm/index.tsx",
18300
- "content": "import {\n getRecoverPasswordForm,\n setRecoverPasswordFormPassword,\n setRecoverPasswordFormPasswordAgain,\n submitRecoverPasswordForm,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\n\nimport { Props } from \"../../types\";\nimport Button from \"../../../../sub-components/Button\";\nimport Input from \"../../../../sub-components/Input\";\nimport FormItem from \"../../../../sub-components/FormItem\";\n\ninterface RecoverPasswordFormProps extends Omit<Props, \"backgroundColor\"> {\n recoverForm: ReturnType<typeof getRecoverPasswordForm>;\n}\n\nconst RecoverPasswordForm = observer(function RecoverPasswordForm({\n recoverForm,\n title = \"Parola Değiştirme\",\n subtitle = \"Lütfen yeni parolanızı belirleyiniz.\",\n passwordLabel = \"Yeni Şifre\",\n passwordPlaceholder = \"Parola\",\n passwordAgainLabel = \"Yeni Şifre Tekrar\",\n passwordAgainPlaceholder = \"Parola\",\n submitButtonText = \"Değiştir\",\n submittingButtonText = \"Değiştiriliyor...\",\n successMessage = \"Parolanız başarıyla değiştirildi.\",\n loginLinkText = \"Giriş Yap\",\n}: RecoverPasswordFormProps) {\n const handleSubmit = async (e: Event) => {\n e.preventDefault();\n const success = await submitRecoverPasswordForm(recoverForm);\n if (success) {\n Router.navigateToPage(\"LOGIN\");\n }\n };\n\n return (\n <div className=\"recover-password__container\">\n <div className=\"recover-password__header\">\n <h1 className=\"recover-password__title text-xl-medium md:display-xs-medium\">{title}</h1>\n <p className=\"recover-password__subtitle text-sm-regular\">{subtitle}</p>\n </div>\n\n {recoverForm.isSuccess && (\n <div className=\"recover-password__success-banner text-sm-regular\">\n {successMessage}\n </div>\n )}\n\n {recoverForm.isFailure && recoverForm.responseMessage && (\n <div className=\"recover-password__error-banner text-sm-regular\">\n {recoverForm.responseMessage}\n </div>\n )}\n\n {!recoverForm.isSuccess && (\n <form className=\"recover-password__form\" onSubmit={handleSubmit}>\n <FormItem\n label={passwordLabel}\n htmlFor=\"recover-password\"\n status={recoverForm.password?.hasError ? \"error\" : \"default\"}\n helper={\n recoverForm.password?.hasError\n ? recoverForm.password.message\n : undefined\n }\n >\n <Input\n id=\"recover-password\"\n password\n placeholder={passwordPlaceholder}\n value={recoverForm.password?.value ?? \"\"}\n onInput={(e: Event) =>\n setRecoverPasswordFormPassword(\n recoverForm,\n (e.target as HTMLInputElement).value,\n )\n }\n />\n </FormItem>\n\n <FormItem\n label={passwordAgainLabel}\n htmlFor=\"recover-password-again\"\n status={recoverForm.passwordAgain?.hasError ? \"error\" : \"default\"}\n helper={\n recoverForm.passwordAgain?.hasError\n ? recoverForm.passwordAgain.message\n : undefined\n }\n >\n <Input\n id=\"recover-password-again\"\n password\n placeholder={passwordAgainPlaceholder}\n value={recoverForm.passwordAgain?.value ?? \"\"}\n onInput={(e: Event) =>\n setRecoverPasswordFormPasswordAgain(\n recoverForm,\n (e.target as HTMLInputElement).value,\n )\n }\n />\n </FormItem>\n\n <Button\n variant=\"primary\"\n size=\"s\"\n className=\"recover-password__submit-btn\"\n disabled={recoverForm.isSubmitting}\n >\n {recoverForm.isSubmitting ? submittingButtonText : submitButtonText}\n </Button>\n </form>\n )}\n\n <div className=\"recover-password__footer\">\n <button\n type=\"button\"\n className=\"recover-password__login-link text-sm-medium\"\n onClick={() => Router.navigateToPage(\"LOGIN\")}\n >\n {loginLinkText}\n </button>\n </div>\n </div>\n );\n});\n\nexport default RecoverPasswordForm;\n"
18301
- },
18302
- {
18303
- "filename": "components/RecoverPasswordForm/styles.css",
18304
- "content": ""
18305
- },
18306
- {
18307
- "filename": "ikas-config-snippet.json",
18308
- "content": "{\n \"id\": \"{{PROJECT_ID}}-recover-password\",\n \"name\": \"RecoverPassword\",\n \"type\": \"section\",\n \"entry\": \"./src/components/RecoverPassword/index.tsx\",\n \"styles\": \"./src/components/RecoverPassword/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Parola Değiştirme\",\n \"groupId\": \"form-baslik\"\n },\n {\n \"name\": \"subtitle\",\n \"displayName\": \"Description\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Lütfen yeni parolanızı belirleyiniz.\",\n \"groupId\": \"form-baslik\"\n },\n {\n \"name\": \"passwordLabel\",\n \"displayName\": \"Password Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"New Password\",\n \"groupId\": \"form-alanlari\"\n },\n {\n \"name\": \"passwordPlaceholder\",\n \"displayName\": \"Password Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Parola\",\n \"groupId\": \"form-alanlari\"\n },\n {\n \"name\": \"passwordAgainLabel\",\n \"displayName\": \"Password Again Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Confirm New Password\",\n \"groupId\": \"form-alanlari\"\n },\n {\n \"name\": \"passwordAgainPlaceholder\",\n \"displayName\": \"Confirm Password Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Parola\",\n \"groupId\": \"form-alanlari\"\n },\n {\n \"name\": \"submitButtonText\",\n \"displayName\": \"Change Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Change\",\n \"groupId\": \"buton-metinleri\"\n },\n {\n \"name\": \"submittingButtonText\",\n \"displayName\": \"Changing Button\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Değiştiriliyor...\",\n \"groupId\": \"buton-metinleri\"\n },\n {\n \"name\": \"successMessage\",\n \"displayName\": \"Success Message\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Parolanız başarıyla değiştirildi.\",\n \"groupId\": \"basari-ekrani\"\n },\n {\n \"name\": \"loginLinkText\",\n \"displayName\": \"Login Return Bağlantısı\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Sign In\",\n \"groupId\": \"basari-ekrani\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"form-baslik\",\n \"name\": \"Form Title\",\n \"description\": \"Formun üst kısmında görünen başlık ve açıklama metinleri\"\n },\n {\n \"id\": \"form-alanlari\",\n \"name\": \"Form Alanları\",\n \"description\": \"Parola giriş alanlarının etiket ve placeholder metinleri\"\n },\n {\n \"id\": \"buton-metinleri\",\n \"name\": \"Button Texts\",\n \"description\": \"Submit butonu ve yükleniyor durumu metinleri\"\n },\n {\n \"id\": \"basari-ekrani\",\n \"name\": \"Success Ekranı\",\n \"description\": \"Parola başarıyla değiştirildikten sonra gösterilen mesaj ve bağlantılar\"\n }\n ]\n}"
18309
- },
18310
- {
18311
- "filename": "index.tsx",
18312
- "content": "import {\n customerStore,\n getRecoverPasswordForm,\n initRecoverPasswordForm,\n} from \"@ikas/bp-storefront\";\nimport { useRedirectIfLoggedIn } from \"../../hooks/useRedirectIfLoggedIn\";\n\nimport { Props } from \"./types\";\nimport RecoverPasswordForm from \"./components/RecoverPasswordForm\";\nimport PageLoader from \"../../sub-components/PageLoader\";\n\nexport function RecoverPassword(props: Props) {\n const recoverForm = getRecoverPasswordForm(customerStore);\n\n const isChecking = useRedirectIfLoggedIn(() => {\n initRecoverPasswordForm(recoverForm);\n });\n\n if (isChecking) return <PageLoader />;\n\n return (\n <section className=\"recover-password\">\n <div className=\"recover-password__wrapper kombos-container\">\n <RecoverPasswordForm recoverForm={recoverForm} {...props} />\n </div>\n </section>\n );\n}\n\nexport default RecoverPassword;\n"
18313
- },
18314
- {
18315
- "filename": "styles.css",
18316
- "content": ".recover-password {\n width: 100%;\n}\n\n.recover-password__wrapper {\n display: flex;\n justify-content: center;\n align-items: flex-start;\n padding-top: 1rem;\n padding-bottom: 1rem;\n}\n\n.recover-password__container {\n width: 100%;\n max-width: 29rem;\n display: flex;\n flex-direction: column;\n gap: 2rem;\n}\n\n.recover-password__header {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n text-align: center;\n}\n\n.recover-password__title {\n color: var(--kombos-gray-900);\n margin: 0;\n}\n\n.recover-password__subtitle {\n color: var(--kombos-gray-700);\n margin: 0;\n}\n\n.recover-password__success-banner {\n padding: 0.75rem 1rem;\n background: rgba(18, 183, 106, 0.08);\n border: 1px solid var(--kombos-success);\n border-radius: 6px;\n color: var(--kombos-success);\n}\n\n.recover-password__error-banner {\n padding: 0.75rem 1rem;\n background: rgba(255, 60, 72, 0.08);\n border: 1px solid var(--kombos-error);\n border-radius: 6px;\n color: var(--kombos-error);\n}\n\n.recover-password__form {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.recover-password__submit-btn {\n width: 100%;\n}\n\n.recover-password__footer {\n text-align: center;\n}\n\n.recover-password__login-link {\n background: none;\n border: none;\n padding: 0;\n color: var(--kombos-gray-700);\n cursor: pointer;\n text-decoration: underline;\n}\n\n.recover-password__login-link:hover {\n color: var(--kombos-gray-900);\n}\n\n@media (min-width: 768px) {\n .recover-password__wrapper {\n padding-top: 1.5rem;\n padding-bottom: 1.5rem;\n }\n}\n\n@media (min-width: 1024px) {\n .recover-password__wrapper {\n padding-top: 2rem;\n padding-bottom: 2rem;\n }\n}\n"
18317
- },
18318
- {
18319
- "filename": "types.ts",
18320
- "content": "export interface Props {\n title?: string;\n subtitle?: string;\n passwordLabel?: string;\n passwordPlaceholder?: string;\n passwordAgainLabel?: string;\n passwordAgainPlaceholder?: string;\n submitButtonText?: string;\n submittingButtonText?: string;\n successMessage?: string;\n loginLinkText?: string;\n}\n"
18321
- }
18322
- ]
18323
- },
18324
- {
18325
- "id": "register-section",
18326
- "title": "Register Section",
18327
- "description": "Customer registration form with name, email, password fields, validation, terms acceptance, and social login. Uses customerStore.register() with full error handling.",
18328
- "code": "import {\n customerStore,\n getRegisterForm,\n initRegisterForm,\n} from \"@ikas/bp-storefront\";\nimport { useRedirectIfLoggedIn } from \"../../hooks/useRedirectIfLoggedIn\";\n\nimport { Props } from \"./types\";\nimport RegisterForm from \"./components/RegisterForm\";\nimport PageLoader from \"../../sub-components/PageLoader\";\n\nexport function Register(props: Props) {\n const registerForm = getRegisterForm(customerStore);\n\n const isChecking = useRedirectIfLoggedIn(() => {\n initRegisterForm(registerForm);\n });\n\n if (isChecking) return <PageLoader />;\n\n return (\n <section className=\"register\">\n <div className=\"register__wrapper kombos-container\">\n <RegisterForm registerForm={registerForm} {...props} />\n </div>\n </section>\n );\n}\n\nexport default Register;\n",
18329
- "relatedFunctions": [
18330
- "customerStore",
18331
- "getRegisterForm",
18332
- "initRegisterForm"
18333
- ],
18334
- "categories": [
18335
- "Customer",
18336
- "Auth",
18337
- "Form"
18338
- ],
18339
- "files": [
18340
- {
18341
- "filename": "components/RegisterForm/index.tsx",
18342
- "content": "import {\n customerStore,\n getRegisterForm,\n setRegisterFormFirstName,\n setRegisterFormLastName,\n setRegisterFormEmail,\n setRegisterFormPassword,\n setRegisterFormIsMarketingAccepted,\n setRegisterFormIsMembershipAgreementAccepted,\n submitRegisterForm,\n socialLogin,\n Router,\n} from \"@ikas/bp-storefront\";\nimport { observer } from \"@ikas/component-utils\";\n\nimport { Props } from \"../../types\";\nimport Button from \"../../../../sub-components/Button\";\nimport Input from \"../../../../sub-components/Input\";\nimport FormItem from \"../../../../sub-components/FormItem\";\nimport Checkbox from \"../../../../sub-components/Checkbox\";\nimport SocialLoginButton from \"../../../../sub-components/SocialLoginButton\";\nimport { GoogleSVG, FacebookSVG } from \"../../../../sub-components/icons\";\n\ninterface RegisterFormProps extends Props {\n registerForm: ReturnType<typeof getRegisterForm>;\n}\n\nconst RegisterForm = observer(function RegisterForm({\n registerForm,\n title = \"Hesap Oluştur\",\n subtitle = \"Hemen ücretsiz hesap oluşturun ve alışverişe başlayın.\",\n firstNameLabel = \"İsim\",\n lastNameLabel = \"Soyisim\",\n emailLabel = \"E-posta\",\n passwordLabel = \"Şifre\",\n firstNamePlaceholder,\n lastNamePlaceholder,\n emailPlaceholder,\n passwordPlaceholder = \"Şifrenizi girin\",\n submitButtonText = \"Kayıt Ol\",\n submittingButtonText = \"Kayıt yapılıyor...\",\n loginLinkText = \"Zaten hesabınız var mı? Giriş yapın\",\n marketingConsentText = \"Kampanya ve promosyonlardan e-posta ile haberdar olmak istiyorum\",\n agreementConsentText = \"Üyelik sözleşmesini kabul ediyorum\",\n googleButtonText = \"Google ile kayıt ol\",\n facebookButtonText = \"Facebook ile kayıt ol\",\n dividerText = \"veya\",\n showGoogleLogin = false,\n showFacebookLogin = false,\n marketingConsentLink,\n agreementConsentLink,\n}: RegisterFormProps) {\n const resolvedFirstNamePlaceholder = firstNamePlaceholder || firstNameLabel;\n const resolvedLastNamePlaceholder = lastNamePlaceholder || lastNameLabel;\n const resolvedEmailPlaceholder = emailPlaceholder || emailLabel;\n\n const showSocialLogin = showGoogleLogin || showFacebookLogin;\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 return (\n <div className=\"register__container\">\n <div className=\"register__header\">\n <h1 className=\"register__title text-xl-medium md:display-xs-medium\">\n {title}\n </h1>\n <div\n className=\"register__subtitle text-sm-regular\"\n dangerouslySetInnerHTML={{ __html: subtitle }}\n />\n </div>\n\n {registerForm.isFailure && registerForm.responseMessage && (\n <div className=\"register__error-banner text-sm-regular\">\n {registerForm.responseMessage}\n </div>\n )}\n\n {showSocialLogin && (\n <div className=\"register__social\">\n {showGoogleLogin && (\n <SocialLoginButton\n icon={<GoogleSVG />}\n onClick={() => socialLogin(customerStore, \"google\")}\n >\n {googleButtonText}\n </SocialLoginButton>\n )}\n {showFacebookLogin && (\n <SocialLoginButton\n icon={<FacebookSVG />}\n onClick={() => socialLogin(customerStore, \"facebook\")}\n >\n {facebookButtonText}\n </SocialLoginButton>\n )}\n </div>\n )}\n\n {showSocialLogin && (\n <div className=\"register__divider\">\n <span className=\"register__divider-line\" />\n <span className=\"register__divider-text text-xs-regular\">\n {dividerText}\n </span>\n <span className=\"register__divider-line\" />\n </div>\n )}\n\n <form className=\"register__form\" onSubmit={handleSubmit}>\n <FormItem\n label={firstNameLabel}\n htmlFor=\"register-first-name\"\n status={registerForm.firstName?.hasError ? \"error\" : \"default\"}\n helper={\n registerForm.firstName?.hasError\n ? registerForm.firstName.message\n : undefined\n }\n >\n <Input\n id=\"register-first-name\"\n name=\"given-name\"\n autoComplete=\"given-name\"\n placeholder={resolvedFirstNamePlaceholder}\n value={registerForm.firstName?.value ?? \"\"}\n onInput={(e: Event) =>\n setRegisterFormFirstName(\n registerForm,\n (e.target as HTMLInputElement).value,\n )\n }\n />\n </FormItem>\n\n <FormItem\n label={lastNameLabel}\n htmlFor=\"register-last-name\"\n status={registerForm.lastName?.hasError ? \"error\" : \"default\"}\n helper={\n registerForm.lastName?.hasError\n ? registerForm.lastName.message\n : undefined\n }\n >\n <Input\n id=\"register-last-name\"\n name=\"family-name\"\n autoComplete=\"family-name\"\n placeholder={resolvedLastNamePlaceholder}\n value={registerForm.lastName?.value ?? \"\"}\n onInput={(e: Event) =>\n setRegisterFormLastName(\n registerForm,\n (e.target as HTMLInputElement).value,\n )\n }\n />\n </FormItem>\n\n <FormItem\n label={emailLabel}\n htmlFor=\"register-email\"\n status={registerForm.email?.hasError ? \"error\" : \"default\"}\n helper={\n registerForm.email?.hasError\n ? registerForm.email.message\n : undefined\n }\n >\n <Input\n id=\"register-email\"\n type=\"email\"\n name=\"email\"\n autoComplete=\"email\"\n placeholder={resolvedEmailPlaceholder}\n value={registerForm.email?.value ?? \"\"}\n onInput={(e: Event) =>\n setRegisterFormEmail(\n registerForm,\n (e.target as HTMLInputElement).value,\n )\n }\n />\n </FormItem>\n\n <FormItem\n label={passwordLabel}\n htmlFor=\"register-password\"\n status={registerForm.password?.hasError ? \"error\" : \"default\"}\n helper={\n registerForm.password?.hasError\n ? registerForm.password.message\n : undefined\n }\n >\n <Input\n id=\"register-password\"\n password\n name=\"new-password\"\n autoComplete=\"new-password\"\n placeholder={passwordPlaceholder}\n value={registerForm.password?.value ?? \"\"}\n onInput={(e: Event) =>\n setRegisterFormPassword(\n registerForm,\n (e.target as HTMLInputElement).value,\n )\n }\n />\n </FormItem>\n\n <div className=\"register__consents\">\n <div className=\"register__consent-field\">\n <div className=\"register__consent-row\">\n <Checkbox\n checked={registerForm.isMarketingAccepted?.value ?? false}\n onChange={(checked) =>\n setRegisterFormIsMarketingAccepted(registerForm, checked)\n }\n />\n {marketingConsentLink?.href ? (\n <a\n className=\"register__consent-label text-sm-regular\"\n href={marketingConsentLink.href}\n target={\n marketingConsentLink.openInNewTab ? \"_blank\" : undefined\n }\n rel={\n marketingConsentLink.openInNewTab\n ? \"noopener noreferrer\"\n : undefined\n }\n dangerouslySetInnerHTML={{ __html: marketingConsentText }}\n />\n ) : (\n <span\n className=\"register__consent-label text-sm-regular\"\n dangerouslySetInnerHTML={{ __html: marketingConsentText }}\n />\n )}\n </div>\n {registerForm.isMarketingAccepted?.hasError && (\n <span className=\"register__consent-error text-xs-regular\">\n {registerForm.isMarketingAccepted.message}\n </span>\n )}\n </div>\n\n <div className=\"register__consent-field\">\n <div className=\"register__consent-row\">\n <Checkbox\n checked={\n registerForm.isMembershipAgreementAccepted?.value ?? false\n }\n onChange={(checked) =>\n setRegisterFormIsMembershipAgreementAccepted(\n registerForm,\n checked,\n )\n }\n status={\n registerForm.isMembershipAgreementAccepted?.hasError\n ? \"error\"\n : \"default\"\n }\n />\n {agreementConsentLink?.href ? (\n <a\n className=\"register__consent-label text-sm-regular\"\n href={agreementConsentLink.href}\n target={\n agreementConsentLink.openInNewTab ? \"_blank\" : undefined\n }\n rel={\n agreementConsentLink.openInNewTab\n ? \"noopener noreferrer\"\n : undefined\n }\n dangerouslySetInnerHTML={{ __html: agreementConsentText }}\n />\n ) : (\n <span\n className=\"register__consent-label text-sm-regular\"\n dangerouslySetInnerHTML={{ __html: agreementConsentText }}\n />\n )}\n </div>\n {registerForm.isMembershipAgreementAccepted?.hasError && (\n <span className=\"register__consent-error text-xs-regular\">\n {registerForm.isMembershipAgreementAccepted.message}\n </span>\n )}\n </div>\n </div>\n\n <Button\n variant=\"primary\"\n size=\"s\"\n className=\"register__submit-btn\"\n disabled={registerForm.isSubmitting}\n >\n {registerForm.isSubmitting ? submittingButtonText : submitButtonText}\n </Button>\n </form>\n\n <div className=\"register__footer\">\n <button\n type=\"button\"\n className=\"register__login-link text-sm-medium\"\n onClick={() => Router.navigateToPage(\"LOGIN\")}\n >\n {loginLinkText}\n </button>\n </div>\n </div>\n );\n});\n\nexport default RegisterForm;\n"
18343
- },
18344
- {
18345
- "filename": "components/RegisterForm/styles.css",
18346
- "content": ""
18347
- },
18348
- {
18349
- "filename": "ikas-config-snippet.json",
18350
- "content": "{\n \"id\": \"{{PROJECT_ID}}-register\",\n \"name\": \"Register\",\n \"type\": \"section\",\n \"entry\": \"./src/components/Register/index.tsx\",\n \"styles\": \"./src/components/Register/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Account Oluştur\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"subtitle\",\n \"displayName\": \"Sub Title\",\n \"type\": \"RICH_TEXT\",\n \"required\": false,\n \"defaultValue\": \"<p>Hemen ücretsiz hesap oluşturun ve alışverişe başlayın.</p>\",\n \"groupId\": \"content\"\n },\n {\n \"name\": \"firstNameLabel\",\n \"displayName\": \"Name Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Name\",\n \"groupId\": \"firstName\"\n },\n {\n \"name\": \"firstNamePlaceholder\",\n \"displayName\": \"Name Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Name\",\n \"groupId\": \"firstName\"\n },\n {\n \"name\": \"lastNameLabel\",\n \"displayName\": \"Last Name Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Last Name\",\n \"groupId\": \"lastName\"\n },\n {\n \"name\": \"lastNamePlaceholder\",\n \"displayName\": \"Last Name Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Last Name\",\n \"groupId\": \"lastName\"\n },\n {\n \"name\": \"emailLabel\",\n \"displayName\": \"Email Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Email\",\n \"groupId\": \"email\"\n },\n {\n \"name\": \"emailPlaceholder\",\n \"displayName\": \"Email Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"ornek@email.com\",\n \"groupId\": \"email\"\n },\n {\n \"name\": \"passwordLabel\",\n \"displayName\": \"Password Label\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Password\",\n \"groupId\": \"password\"\n },\n {\n \"name\": \"passwordPlaceholder\",\n \"displayName\": \"Password Placeholder\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Şifrenizi girin\",\n \"groupId\": \"password\"\n },\n {\n \"name\": \"submitButtonText\",\n \"displayName\": \"Register Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Register\",\n \"groupId\": \"buttons\"\n },\n {\n \"name\": \"submittingButtonText\",\n \"displayName\": \"Register Yapılıyor Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Register yapılıyor...\",\n \"groupId\": \"buttons\"\n },\n {\n \"name\": \"loginLinkText\",\n \"displayName\": \"Login Yap Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Zaten hesabınız var mı? Login yapın\",\n \"groupId\": \"buttons\"\n },\n {\n \"name\": \"marketingConsentText\",\n \"displayName\": \"Marketing Confirmation Text\",\n \"type\": \"RICH_TEXT\",\n \"required\": false,\n \"groupId\": \"marketing-consent\"\n },\n {\n \"name\": \"marketingConsentLink\",\n \"displayName\": \"İletişim Permission Link\",\n \"type\": \"LINK\",\n \"required\": false,\n \"groupId\": \"marketing-consent\"\n },\n {\n \"name\": \"agreementConsentText\",\n \"displayName\": \"Membership Agreement Text\",\n \"type\": \"RICH_TEXT\",\n \"required\": false,\n \"groupId\": \"agreement-consent\"\n },\n {\n \"name\": \"agreementConsentLink\",\n \"displayName\": \"Membership Agreement Link\",\n \"type\": \"LINK\",\n \"required\": false,\n \"groupId\": \"agreement-consent\"\n },\n {\n \"name\": \"showGoogleLogin\",\n \"displayName\": \"Sign in with Google\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"google\"\n },\n {\n \"name\": \"googleButtonText\",\n \"displayName\": \"Google Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Google ile kayıt ol\",\n \"groupId\": \"google\"\n },\n {\n \"name\": \"showFacebookLogin\",\n \"displayName\": \"Sign in with Facebook\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"groupId\": \"facebook\"\n },\n {\n \"name\": \"facebookButtonText\",\n \"displayName\": \"Facebook Button Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Facebook ile kayıt ol\",\n \"groupId\": \"facebook\"\n },\n {\n \"name\": \"dividerText\",\n \"displayName\": \"Separator Text\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"veya\",\n \"groupId\": \"socialLogin\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"content\",\n \"name\": \"Content\",\n \"description\": \"Page başlığı ve açıklama\"\n },\n {\n \"id\": \"formFields\",\n \"name\": \"Form Alanları\",\n \"description\": \"Register formu etiket ve placeholder metinleri\",\n \"children\": [\n {\n \"id\": \"firstName\",\n \"name\": \"Name\"\n },\n {\n \"id\": \"lastName\",\n \"name\": \"Last Name\"\n },\n {\n \"id\": \"email\",\n \"name\": \"Email\"\n },\n {\n \"id\": \"password\",\n \"name\": \"Password\"\n }\n ]\n },\n {\n \"id\": \"buttons\",\n \"name\": \"Buttons\",\n \"description\": \"Button ve link metinleri\"\n },\n {\n \"id\": \"consents\",\n \"name\": \"Confirmation Texts\",\n \"description\": \"İletişim izni ve üyelik sözleşmesi\",\n \"children\": [\n {\n \"id\": \"marketing-consent\",\n \"name\": \"İletişim Permission\"\n },\n {\n \"id\": \"agreement-consent\",\n \"name\": \"Membership Agreement\"\n }\n ]\n },\n {\n \"id\": \"socialLogin\",\n \"name\": \"Social Login\",\n \"description\": \"Google ve Facebook ile giriş ayarları\",\n \"children\": [\n {\n \"id\": \"google\",\n \"name\": \"Google\"\n },\n {\n \"id\": \"facebook\",\n \"name\": \"Facebook\"\n }\n ]\n }\n ]\n}"
18351
- },
18352
- {
18353
- "filename": "index.tsx",
18354
- "content": "import {\n customerStore,\n getRegisterForm,\n initRegisterForm,\n} from \"@ikas/bp-storefront\";\nimport { useRedirectIfLoggedIn } from \"../../hooks/useRedirectIfLoggedIn\";\n\nimport { Props } from \"./types\";\nimport RegisterForm from \"./components/RegisterForm\";\nimport PageLoader from \"../../sub-components/PageLoader\";\n\nexport function Register(props: Props) {\n const registerForm = getRegisterForm(customerStore);\n\n const isChecking = useRedirectIfLoggedIn(() => {\n initRegisterForm(registerForm);\n });\n\n if (isChecking) return <PageLoader />;\n\n return (\n <section className=\"register\">\n <div className=\"register__wrapper kombos-container\">\n <RegisterForm registerForm={registerForm} {...props} />\n </div>\n </section>\n );\n}\n\nexport default Register;\n"
18355
- },
18356
- {
18357
- "filename": "styles.css",
18358
- "content": ".register {\n width: 100%;\n}\n\n.register__wrapper {\n display: flex;\n justify-content: center;\n align-items: flex-start;\n padding-top: 1rem;\n padding-bottom: 1rem;\n}\n\n.register__container {\n width: 100%;\n max-width: 29rem;\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n}\n\n.register__header {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n text-align: center;\n}\n\n.register__title {\n color: var(--kombos-gray-900);\n margin: 0;\n}\n\n.register__subtitle {\n color: var(--kombos-gray-500);\n margin: 0;\n}\n\n.register__error-banner {\n padding: 0.75rem 1rem;\n background: rgba(255, 60, 72, 0.08);\n border: 1px solid var(--kombos-error);\n border-radius: 6px;\n color: var(--kombos-error);\n}\n\n/* Social Login */\n.register__social {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n/* Divider */\n.register__divider {\n display: flex;\n align-items: center;\n gap: 1rem;\n}\n\n.register__divider-line {\n flex: 1;\n height: 1px;\n background: var(--kombos-gray-200);\n}\n\n.register__divider-text {\n color: var(--kombos-gray-500);\n white-space: nowrap;\n}\n\n/* Form */\n.register__form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n/* Consents */\n.register__consents {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.register__consent-field {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n}\n\n.register__consent-row {\n display: flex;\n align-items: flex-start;\n gap: 0.5rem;\n}\n\n.register__consent-row > .kombos-checkbox {\n margin-top: 3px;\n}\n\n.register__consent-error {\n color: var(--kombos-error);\n padding-left: 1.5rem;\n}\n\n.register__consent-label {\n color: var(--kombos-gray-700);\n margin-top: 1px;\n}\n\na.register__consent-label {\n text-decoration: none;\n}\n\na.register__consent-label:hover {\n color: var(--kombos-gray-900);\n}\n\n.register__submit-btn {\n width: 100%;\n}\n\n.register__footer {\n text-align: center;\n}\n\n.register__login-link {\n background: none;\n border: none;\n padding: 0;\n color: var(--kombos-gray-700);\n cursor: pointer;\n text-decoration: underline;\n}\n\n.register__login-link:hover {\n color: var(--kombos-gray-900);\n}\n\n@media (min-width: 768px) {\n .register__wrapper {\n padding-top: 1.5rem;\n padding-bottom: 1.5rem;\n }\n}\n\n@media (min-width: 1024px) {\n .register__wrapper {\n padding-top: 2rem;\n padding-bottom: 2rem;\n }\n}\n"
18359
- },
18360
- {
18361
- "filename": "types.ts",
18362
- "content": "import type { IkasNavigationLink } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n title?: string;\n subtitle?: string;\n firstNameLabel?: string;\n firstNamePlaceholder?: string;\n lastNameLabel?: string;\n lastNamePlaceholder?: string;\n emailLabel?: string;\n emailPlaceholder?: string;\n passwordLabel?: string;\n passwordPlaceholder?: string;\n submitButtonText?: string;\n submittingButtonText?: string;\n loginLinkText?: string;\n marketingConsentText?: string;\n marketingConsentLink?: IkasNavigationLink | null;\n agreementConsentText?: string;\n agreementConsentLink?: IkasNavigationLink | null;\n showGoogleLogin?: boolean;\n googleButtonText?: string;\n showFacebookLogin?: boolean;\n facebookButtonText?: string;\n dividerText?: string;\n}\n"
18363
- }
18364
- ]
18365
- },
18366
- {
18367
- "id": "rich-text-section",
18368
- "title": "Rich Text Section",
18369
- "description": "Simple section that renders rich text HTML content from a prop. Useful for custom content blocks, legal pages, and static content.",
18370
- "code": "import { Props } from \"./types\";\n\nexport function RichText({ title, content }: Props) {\n if (!title && !content) return null;\n\n return (\n <section className=\"kombos-rich-text\">\n <div className=\"kombos-container\">\n {title && (\n <h2 className=\"kombos-rich-text__title display-xs-semibold\">\n {title}\n </h2>\n )}\n {content && (\n <div\n className=\"kombos-rich-text__content text-md-regular kombos-richtext\"\n dangerouslySetInnerHTML={{ __html: content }}\n />\n )}\n </div>\n </section>\n );\n}\n\nexport default RichText;\n",
18371
- "relatedFunctions": [],
18372
- "categories": [
18373
- "Layout"
18374
- ],
18375
- "files": [
18376
- {
18377
- "filename": "ikas-config-snippet.json",
18378
- "content": "{\n \"id\": \"{{PROJECT_ID}}-rich-text\",\n \"name\": \"RichText\",\n \"type\": \"section\",\n \"entry\": \"./src/components/RichText/index.tsx\",\n \"styles\": \"./src/components/RichText/styles.css\",\n \"props\": [\n {\n \"name\": \"title\",\n \"displayName\": \"Title\",\n \"type\": \"TEXT\",\n \"required\": false,\n \"defaultValue\": \"Title\"\n },\n {\n \"name\": \"content\",\n \"displayName\": \"Content\",\n \"type\": \"RICH_TEXT\",\n \"required\": false\n }\n ]\n}"
18379
- },
18380
- {
18381
- "filename": "index.tsx",
18382
- "content": "import { Props } from \"./types\";\n\nexport function RichText({ title, content }: Props) {\n if (!title && !content) return null;\n\n return (\n <section className=\"kombos-rich-text\">\n <div className=\"kombos-container\">\n {title && (\n <h2 className=\"kombos-rich-text__title display-xs-semibold\">\n {title}\n </h2>\n )}\n {content && (\n <div\n className=\"kombos-rich-text__content text-md-regular kombos-richtext\"\n dangerouslySetInnerHTML={{ __html: content }}\n />\n )}\n </div>\n </section>\n );\n}\n\nexport default RichText;\n"
18383
- },
18384
- {
18385
- "filename": "styles.css",
18386
- "content": ".kombos-rich-text {\n width: 100%;\n}\n\n.kombos-rich-text .kombos-container {\n display: flex;\n flex-direction: column;\n gap: 1.5rem;\n max-width: 50rem;\n padding-top: 2.5rem;\n padding-bottom: 2.5rem;\n}\n\n.kombos-rich-text__title {\n color: var(--kombos-gray-900);\n margin: 0;\n text-align: center;\n}\n\n.kombos-rich-text__content {\n color: var(--kombos-gray-700);\n}\n\n.kombos-rich-text__content a {\n color: var(--kombos-gray-900);\n text-decoration: underline;\n}\n\n.kombos-rich-text__content a:hover {\n color: var(--kombos-gray-700);\n}\n\n.kombos-rich-text__content img {\n max-width: 100%;\n height: auto;\n border-radius: 6px;\n}\n\n@media (min-width: 768px) {\n .kombos-rich-text .kombos-container {\n padding-top: 3rem;\n padding-bottom: 3rem;\n }\n}\n\n@media (min-width: 1024px) {\n .kombos-rich-text .kombos-container {\n padding-top: 4rem;\n padding-bottom: 4rem;\n }\n}\n"
18387
- },
18388
- {
18389
- "filename": "types.ts",
18390
- "content": "export interface Props {\n title?: string;\n content?: string;\n}\n"
18391
- }
18392
- ]
18393
- },
18394
- {
18395
- "id": "variant-selection",
18396
- "title": "Variant Selection Pattern",
18397
- "description": "Product variant selection UI with color swatches, size dropdowns, and variant type detection. Shows selectVariantValue, getSelectedProductVariant, variant option types (COLOR, DROPDOWN, BUTTON), and variant availability checking.",
18398
- "code": "import {\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport VariantBadge from \"../../sub-components/VariantBadge\";\n\nexport function ProductDetailVariant({\n product,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n useVariantImages,\n}: Props) {\n if (!product) return null;\n\n return (\n <div\n className=\"kombos-pd-variant\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n <VariantBadge\n product={product}\n size=\"l\"\n showLabels\n useVariantImages={useVariantImages}\n />\n </div>\n );\n}\n\nexport default ProductDetailVariant;\n",
18399
- "relatedFunctions": [
18400
- "getFormattedMarginTopSize",
18401
- "getFormattedMarginBottomSize"
18402
- ],
18403
- "categories": [
18404
- "Product",
18405
- "ProductDetail"
18406
- ],
18407
- "files": [
18408
- {
18409
- "filename": "ikas-config-snippet.json",
18410
- "content": "{\n \"id\": \"{{PROJECT_ID}}-product-detail-variant\",\n \"name\": \"ProductDetailVariant\",\n \"type\": \"component\",\n \"entry\": \"./src/components/ProductDetailVariant/index.tsx\",\n \"styles\": \"./src/components/ProductDetailVariant/styles.css\",\n \"props\": [\n {\n \"name\": \"product\",\n \"displayName\": \"Product\",\n \"type\": \"PRODUCT\",\n \"required\": false\n },\n {\n \"name\": \"mobileMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"mobileMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"mobile\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"desktopMarginTop\",\n \"displayName\": \"Top Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginTopStyleType\"\n },\n {\n \"name\": \"desktopMarginBottom\",\n \"displayName\": \"Sub Spacing\",\n \"type\": \"TYPE\",\n \"required\": false,\n \"groupId\": \"desktop\",\n \"typeId\": \"@ikas/bp-storefront-models-MarginBottomStyleType\"\n },\n {\n \"name\": \"useVariantImages\",\n \"displayName\": \"Image Variant\",\n \"type\": \"BOOLEAN\",\n \"required\": false,\n \"description\": \"Renk varyantı için ürün görsellerini kullan.\"\n }\n ],\n \"propGroups\": [\n {\n \"id\": \"appearance\",\n \"name\": \"View Settings\",\n \"children\": [\n {\n \"id\": \"mobile\",\n \"name\": \"Mobile View\",\n \"description\": \"Mobile cihazlarda uygulanacak margin ayarları\"\n },\n {\n \"id\": \"desktop\",\n \"name\": \"Desktop View\",\n \"description\": \"Desktop cihazlarda uygulanacak margin ayarları\"\n }\n ],\n \"description\": \"Bileşenin mobil ve masaüstü görünüm ayarları\"\n }\n ]\n}"
18411
- },
18412
- {
18413
- "filename": "index.tsx",
18414
- "content": "import {\n getFormattedMarginTopSize,\n getFormattedMarginBottomSize,\n} from \"@ikas/bp-storefront\";\nimport { Props } from \"./types\";\nimport VariantBadge from \"../../sub-components/VariantBadge\";\n\nexport function ProductDetailVariant({\n product,\n mobileMarginTop,\n mobileMarginBottom,\n desktopMarginTop,\n desktopMarginBottom,\n useVariantImages,\n}: Props) {\n if (!product) return null;\n\n return (\n <div\n className=\"kombos-pd-variant\"\n style={{\n \"--mobile-mt\": getFormattedMarginTopSize(mobileMarginTop),\n \"--mobile-mb\": getFormattedMarginBottomSize(mobileMarginBottom),\n \"--desktop-mt\": getFormattedMarginTopSize(desktopMarginTop),\n \"--desktop-mb\": getFormattedMarginBottomSize(desktopMarginBottom),\n }}\n >\n <VariantBadge\n product={product}\n size=\"l\"\n showLabels\n useVariantImages={useVariantImages}\n />\n </div>\n );\n}\n\nexport default ProductDetailVariant;\n"
18415
- },
18416
- {
18417
- "filename": "styles.css",
18418
- "content": "/* ===== ProductDetailVariant ===== */\n\n.kombos-pd-variant {\n margin-top: var(--mobile-mt);\n margin-bottom: var(--mobile-mb);\n}\n\n/* ===== Desktop (>=1024px) ===== */\n@media (min-width: 1024px) {\n .kombos-pd-variant {\n margin-top: var(--desktop-mt, var(--mobile-mt));\n margin-bottom: var(--desktop-mb, var(--mobile-mb));\n }\n}\n"
18419
- },
18420
- {
18421
- "filename": "types.ts",
18422
- "content": "import type { IkasProduct, MarginTopStyleType, MarginBottomStyleType } from \"@ikas/bp-storefront\";\n\nexport interface Props {\n product?: IkasProduct | null;\n mobileMarginTop?: MarginTopStyleType;\n mobileMarginBottom?: MarginBottomStyleType;\n desktopMarginTop?: MarginTopStyleType;\n desktopMarginBottom?: MarginBottomStyleType;\n /** Renk varyantı için ürün görsellerini kullan. */\n useVariantImages?: boolean;\n}\n"
18423
- }
18424
- ]
18425
- }
18426
- ]
16404
+ "codeExamples": []
18427
16405
  }