@salla.sa/twilight-theme-engine 1.0.85 → 1.0.86

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 (372) hide show
  1. package/dist/{AddToCartForm-B5C2M4OZ.js → AddToCartForm-5X2WZDWR.js} +3 -3
  2. package/dist/{AddToCartForm-B5C2M4OZ.js.map → AddToCartForm-5X2WZDWR.js.map} +1 -1
  3. package/dist/{BlogAuthorPage-GTF4UGSS.js → BlogAuthorPage-LO7FSSJR.js} +12 -10
  4. package/dist/BlogAuthorPage-LO7FSSJR.js.map +1 -0
  5. package/dist/{BlogCategoryPage-TN7Y4NBP.js → BlogCategoryPage-KKFQ3PGR.js} +12 -10
  6. package/dist/BlogCategoryPage-KKFQ3PGR.js.map +1 -0
  7. package/dist/{BlogPage-TDB3M3PI.js → BlogPage-VLF3NSGE.js} +11 -9
  8. package/dist/BlogPage-VLF3NSGE.js.map +1 -0
  9. package/dist/{BlogSinglePage-4A52Q5QW.js → BlogSinglePage-AVK535IW.js} +11 -9
  10. package/dist/BlogSinglePage-AVK535IW.js.map +1 -0
  11. package/dist/{BlogTagPage-TOVPBETA.js → BlogTagPage-F3T4D7RH.js} +12 -10
  12. package/dist/BlogTagPage-F3T4D7RH.js.map +1 -0
  13. package/dist/{BrandsPage-VUDGYSQI.js → BrandsPage-LVSBZSVC.js} +11 -9
  14. package/dist/BrandsPage-LVSBZSVC.js.map +1 -0
  15. package/dist/{Breadcrumb-CI3SN5YR.js → Breadcrumb-VLFTXRFC.js} +11 -9
  16. package/dist/Breadcrumb-VLFTXRFC.js.map +1 -0
  17. package/dist/{CartItem-BHLEURAB.js → CartItem-G3H34GW6.js} +11 -9
  18. package/dist/CartItem-G3H34GW6.js.map +1 -0
  19. package/dist/{CartSummary-QERVESYN.js → CartSummary-VH37RPUI.js} +11 -9
  20. package/dist/CartSummary-VH37RPUI.js.map +1 -0
  21. package/dist/{Copyright-RL6ALKRC.js → Copyright-HE5ZWINZ.js} +3 -3
  22. package/dist/{Copyright-RL6ALKRC.js.map → Copyright-HE5ZWINZ.js.map} +1 -1
  23. package/dist/{CustomerLayout-VRZUWGQK.js → CustomerLayout-LKBLOAVW.js} +11 -9
  24. package/dist/CustomerLayout-LKBLOAVW.js.map +1 -0
  25. package/dist/{Footer-ZWBZCMDL.js → Footer-HHXAST72.js} +5 -5
  26. package/dist/{Footer-ZWBZCMDL.js.map → Footer-HHXAST72.js.map} +1 -1
  27. package/dist/{Header-WNK4O66B.js → Header-GEVZM2YG.js} +8 -6
  28. package/dist/{Header-WNK4O66B.js.map → Header-GEVZM2YG.js.map} +1 -1
  29. package/dist/{LoyaltyPage-CGTHWJ76.js → LoyaltyPage-M5CT3CFD.js} +11 -9
  30. package/dist/LoyaltyPage-M5CT3CFD.js.map +1 -0
  31. package/dist/{LoyaltyPointsPage-QSU6K3PW.js → LoyaltyPointsPage-SDJB52U6.js} +10 -3
  32. package/dist/LoyaltyPointsPage-SDJB52U6.js.map +1 -0
  33. package/dist/{MainMenu-V3T7F32R.js → MainMenu-I7EYS752.js} +3 -3
  34. package/dist/{MainMenu-V3T7F32R.js.map → MainMenu-I7EYS752.js.map} +1 -1
  35. package/dist/{MasterLayout-XQF644LD.js → MasterLayout-QDK4T3NE.js} +11 -9
  36. package/dist/{MasterLayout-XQF644LD.js.map → MasterLayout-QDK4T3NE.js.map} +1 -1
  37. package/dist/{MobileMenu-U4BVQYKH.js → MobileMenu-7VTCWAVJ.js} +11 -9
  38. package/dist/MobileMenu-7VTCWAVJ.js.map +1 -0
  39. package/dist/{NotificationsPage-ZRRQY7TF.js → NotificationsPage-QCABE6QD.js} +11 -9
  40. package/dist/NotificationsPage-QCABE6QD.js.map +1 -0
  41. package/dist/{OrderSinglePage-O3XYVMG5.js → OrderSinglePage-2HVBKBG4.js} +11 -9
  42. package/dist/OrderSinglePage-2HVBKBG4.js.map +1 -0
  43. package/dist/{OrdersPage-CMXZF5MI.js → OrdersPage-5MNCPCM3.js} +11 -9
  44. package/dist/OrdersPage-5MNCPCM3.js.map +1 -0
  45. package/dist/{PageSingle-IIG2US43.js → PageSingle-EN5XIM2B.js} +3 -3
  46. package/dist/{PageSingle-IIG2US43.js.map → PageSingle-EN5XIM2B.js.map} +1 -1
  47. package/dist/{ProductDetails-65GDABI7.js → ProductDetails-UEWQPPDZ.js} +13 -6
  48. package/dist/ProductDetails-UEWQPPDZ.js.map +1 -0
  49. package/dist/{ProfilePage-SNJ4WXU4.js → ProfilePage-TZSU3GHZ.js} +11 -9
  50. package/dist/ProfilePage-TZSU3GHZ.js.map +1 -0
  51. package/dist/{ReviewsPage-UT7UT55J.js → ReviewsPage-WYZ6CHF2.js} +3 -3
  52. package/dist/{ReviewsPage-UT7UT55J.js.map → ReviewsPage-WYZ6CHF2.js.map} +1 -1
  53. package/dist/{SeoCartWidget-DBJAXJKT.js → SeoCartWidget-5JR5RXQM.js} +11 -9
  54. package/dist/SeoCartWidget-5JR5RXQM.js.map +1 -0
  55. package/dist/{SettingsPage-FLTIOPU5.js → SettingsPage-DIDRQXMM.js} +11 -9
  56. package/dist/SettingsPage-DIDRQXMM.js.map +1 -0
  57. package/dist/{StoreClosedNotification-ZT3QKDKS.js → StoreClosedNotification-C2G32E34.js} +12 -10
  58. package/dist/{StoreClosedNotification-ZT3QKDKS.js.map → StoreClosedNotification-C2G32E34.js.map} +1 -1
  59. package/dist/{TestimonialsPage-CU7HKU4O.js → TestimonialsPage-XPHFRBZ7.js} +3 -3
  60. package/dist/{TestimonialsPage-CU7HKU4O.js.map → TestimonialsPage-XPHFRBZ7.js.map} +1 -1
  61. package/dist/{ThankYouPage-5X6B4X3T.js → ThankYouPage-UA6YYFB2.js} +11 -9
  62. package/dist/ThankYouPage-UA6YYFB2.js.map +1 -0
  63. package/dist/{WalletPage-H2BKIDPA.js → WalletPage-AJVV7LVL.js} +11 -9
  64. package/dist/WalletPage-AJVV7LVL.js.map +1 -0
  65. package/dist/{WishlistPage-RS5UCIQ3.js → WishlistPage-UW6LJLSW.js} +11 -9
  66. package/dist/WishlistPage-UW6LJLSW.js.map +1 -0
  67. package/dist/api/advertisement.d.ts +33 -0
  68. package/dist/api/advertisement.d.ts.map +1 -0
  69. package/dist/api/advertisement.js +8 -0
  70. package/dist/api/advertisement.js.map +1 -0
  71. package/dist/boot/critical-style.d.ts +3 -0
  72. package/dist/boot/critical-style.d.ts.map +1 -0
  73. package/dist/boot/index.d.ts +15 -0
  74. package/dist/boot/index.d.ts.map +1 -0
  75. package/dist/boot.d.ts +1 -0
  76. package/dist/boot.js +69 -0
  77. package/dist/boot.js.map +1 -0
  78. package/dist/{chunk-SE5JYWOK.js → chunk-2HCCIHU7.js} +4 -4
  79. package/dist/{chunk-SE5JYWOK.js.map → chunk-2HCCIHU7.js.map} +1 -1
  80. package/dist/chunk-4FJGZQD3.js +6 -0
  81. package/dist/chunk-4FJGZQD3.js.map +1 -0
  82. package/dist/{chunk-SDFDS4Q3.js → chunk-4WVJJ7SN.js} +20 -4
  83. package/dist/chunk-4WVJJ7SN.js.map +1 -0
  84. package/dist/{chunk-NXQJA7D3.js → chunk-BAYGWTNM.js} +3 -3
  85. package/dist/{chunk-NXQJA7D3.js.map → chunk-BAYGWTNM.js.map} +1 -1
  86. package/dist/{chunk-OWX2CPNQ.js → chunk-CLBFF2CY.js} +3 -3
  87. package/dist/{chunk-OWX2CPNQ.js.map → chunk-CLBFF2CY.js.map} +1 -1
  88. package/dist/chunk-DBW7XXNE.js +103 -0
  89. package/dist/chunk-DBW7XXNE.js.map +1 -0
  90. package/dist/{chunk-KHBSGD2D.js → chunk-DBXH3EMJ.js} +3 -3
  91. package/dist/{chunk-KHBSGD2D.js.map → chunk-DBXH3EMJ.js.map} +1 -1
  92. package/dist/{chunk-JRNH3FUF.js → chunk-E26YG3AO.js} +4 -4
  93. package/dist/{chunk-JRNH3FUF.js.map → chunk-E26YG3AO.js.map} +1 -1
  94. package/dist/{chunk-22SH7OVF.js → chunk-E7WWO3V3.js} +3 -3
  95. package/dist/{chunk-22SH7OVF.js.map → chunk-E7WWO3V3.js.map} +1 -1
  96. package/dist/{chunk-UJKFBZFJ.js → chunk-HQ7WA37L.js} +7 -7
  97. package/dist/{chunk-UJKFBZFJ.js.map → chunk-HQ7WA37L.js.map} +1 -1
  98. package/dist/chunk-IVBOLREE.js +29 -0
  99. package/dist/chunk-IVBOLREE.js.map +1 -0
  100. package/dist/{chunk-6SWDSRIL.js → chunk-KP3VYG4F.js} +6 -6
  101. package/dist/{chunk-6SWDSRIL.js.map → chunk-KP3VYG4F.js.map} +1 -1
  102. package/dist/{chunk-4CZRUAZB.js → chunk-PWTXR4RR.js} +10 -10
  103. package/dist/{chunk-4CZRUAZB.js.map → chunk-PWTXR4RR.js.map} +1 -1
  104. package/dist/chunk-QJYUTRAQ.js +14 -0
  105. package/dist/{chunk-7WAXN2PP.js.map → chunk-QJYUTRAQ.js.map} +1 -1
  106. package/dist/{chunk-JO7JWV34.js → chunk-U55ZJC2X.js} +4 -4
  107. package/dist/{chunk-JO7JWV34.js.map → chunk-U55ZJC2X.js.map} +1 -1
  108. package/dist/{chunk-SX5MF2YA.js → chunk-WSDG4ZRI.js} +4 -4
  109. package/dist/{chunk-SX5MF2YA.js.map → chunk-WSDG4ZRI.js.map} +1 -1
  110. package/dist/{chunk-NZVR4OF7.js → chunk-X54QP332.js} +8 -8
  111. package/dist/chunk-X54QP332.js.map +1 -0
  112. package/dist/components/cart/index.js +1 -1
  113. package/dist/components/common/index.js +1 -1
  114. package/dist/components/drawer/index.js +9 -7
  115. package/dist/components/home/index.js +11 -9
  116. package/dist/components/layout/PageAdvertisements.d.ts +7 -0
  117. package/dist/components/layout/PageAdvertisements.d.ts.map +1 -0
  118. package/dist/components/layout/index.d.ts +1 -0
  119. package/dist/components/layout/index.d.ts.map +1 -1
  120. package/dist/components/layout/index.js +8 -1
  121. package/dist/components/product/index.js +10 -8
  122. package/dist/components/store/index.js +11 -9
  123. package/dist/components/store/index.js.map +1 -1
  124. package/dist/{document-class-67HTP45G.js → document-class-7AXAAWGZ.js} +6 -5
  125. package/dist/{document-class-67HTP45G.js.map → document-class-7AXAAWGZ.js.map} +1 -1
  126. package/dist/hooks/index.js +9 -7
  127. package/dist/hooks/useGtm.js +9 -7
  128. package/dist/hooks/useMoney.js +9 -7
  129. package/dist/hooks/useStore.js +9 -7
  130. package/dist/index.js +9 -7
  131. package/dist/index.js.map +1 -1
  132. package/dist/{not-found-QTHBFPNT.js → not-found-3EEMQZMU.js} +11 -9
  133. package/dist/not-found-3EEMQZMU.js.map +1 -0
  134. package/dist/providers/index.js +9 -7
  135. package/dist/routes/account/orders.js +1 -1
  136. package/dist/routes/account.js +6 -6
  137. package/dist/routes/blog.js +5 -5
  138. package/dist/routes/brands.js +1 -1
  139. package/dist/routes/cart.js +13 -11
  140. package/dist/routes/cart.js.map +1 -1
  141. package/dist/routes/home.js +12 -10
  142. package/dist/routes/loyalty.js +2 -2
  143. package/dist/routes/page.js +1 -1
  144. package/dist/routes/product-listing.js +11 -9
  145. package/dist/routes/product-listing.js.map +1 -1
  146. package/dist/routes/product.js +11 -9
  147. package/dist/routes/product.js.map +1 -1
  148. package/dist/routes/root/beforeLoad.d.ts +2 -2
  149. package/dist/routes/root/beforeLoad.d.ts.map +1 -1
  150. package/dist/routes/root/head.d.ts.map +1 -1
  151. package/dist/routes/testimonials.js +2 -2
  152. package/dist/routes/thank-you.js +1 -1
  153. package/dist/skeleton.js +12 -10
  154. package/dist/tanstack/document-class.d.ts.map +1 -1
  155. package/dist/tanstack/index.d.ts +1 -0
  156. package/dist/tanstack/index.d.ts.map +1 -1
  157. package/dist/tanstack/stylesheet.d.ts +15 -0
  158. package/dist/tanstack/stylesheet.d.ts.map +1 -0
  159. package/dist/tanstack.js +34 -5
  160. package/dist/tanstack.js.map +1 -1
  161. package/dist/utils/index.js +1 -1
  162. package/dist/vite/index.js +1 -1
  163. package/package.json +12 -3
  164. package/tailwind/components/Component.tsx.txt +21 -0
  165. package/tailwind/components/ComponentRegistry.ts.txt +66 -0
  166. package/tailwind/components/cart/CartItem.tsx.txt +211 -0
  167. package/tailwind/components/cart/CartSummary.tsx.txt +269 -0
  168. package/tailwind/components/cart/PreOrderNotification.tsx.txt +16 -0
  169. package/tailwind/components/cart/PriceQuoteButton.tsx.txt +21 -0
  170. package/tailwind/components/cart/SeoCartWidget.tsx.txt +99 -0
  171. package/tailwind/components/cart/index.ts.txt +11 -0
  172. package/tailwind/components/cart/lazy.ts.txt +15 -0
  173. package/tailwind/components/collapse/Collapse.tsx.txt +95 -0
  174. package/tailwind/components/collapse/index.ts.txt +41 -0
  175. package/tailwind/components/common/Breadcrumb.tsx.txt +27 -0
  176. package/tailwind/components/common/BreadcrumbJsonLd.tsx.txt +40 -0
  177. package/tailwind/components/common/BreadcrumbTrail.tsx.txt +97 -0
  178. package/tailwind/components/common/CurrencySymbol.tsx.txt +8 -0
  179. package/tailwind/components/common/DeferredData.tsx.txt +122 -0
  180. package/tailwind/components/common/ErrorPage.tsx.txt +69 -0
  181. package/tailwind/components/common/Image.tsx.txt +119 -0
  182. package/tailwind/components/common/Link.tsx.txt +36 -0
  183. package/tailwind/components/common/Link.types.ts.txt +85 -0
  184. package/tailwind/components/common/NoContent.tsx.txt +27 -0
  185. package/tailwind/components/common/RenderWhenVisible.tsx.txt +92 -0
  186. package/tailwind/components/common/index.ts.txt +42 -0
  187. package/tailwind/components/common/lazy.ts.txt +28 -0
  188. package/tailwind/components/defineComponent.ts.txt +32 -0
  189. package/tailwind/components/drawer/Drawer.tsx.txt +219 -0
  190. package/tailwind/components/drawer/index.ts.txt +38 -0
  191. package/tailwind/components/dropdown/Dropdown.tsx.txt +166 -0
  192. package/tailwind/components/dropdown/index.ts.txt +40 -0
  193. package/tailwind/components/home/BundleComponent.tsx.txt +35 -0
  194. package/tailwind/components/home/ComponentErrorBoundary.tsx.txt +98 -0
  195. package/tailwind/components/home/FeaturedProductsStyle1.tsx.txt +125 -0
  196. package/tailwind/components/home/FeaturedProductsStyle2.tsx.txt +127 -0
  197. package/tailwind/components/home/FeaturedProductsStyle3.tsx.txt +71 -0
  198. package/tailwind/components/home/FixedBanner.tsx.txt +68 -0
  199. package/tailwind/components/home/FixedProducts.tsx.txt +72 -0
  200. package/tailwind/components/home/HomePageRenderer.tsx.txt +284 -0
  201. package/tailwind/components/home/PageBlocks.tsx.txt +24 -0
  202. package/tailwind/components/home/ParallaxBackground.tsx.txt +50 -0
  203. package/tailwind/components/home/PhotosSlider.tsx.txt +61 -0
  204. package/tailwind/components/home/ProductsSlider.tsx.txt +87 -0
  205. package/tailwind/components/home/ReorderProducts.tsx.txt +38 -0
  206. package/tailwind/components/home/SquarePhotos.tsx.txt +68 -0
  207. package/tailwind/components/home/StoreFeatures.tsx.txt +34 -0
  208. package/tailwind/components/home/Testimonials.tsx.txt +36 -0
  209. package/tailwind/components/home/Youtube.tsx.txt +48 -0
  210. package/tailwind/components/home/index.ts.txt +50 -0
  211. package/tailwind/components/home/lazy.ts.txt +17 -0
  212. package/tailwind/components/home/register.ts.txt +44 -0
  213. package/tailwind/components/home/reorder-hooks.ts.txt +52 -0
  214. package/tailwind/components/home/types.ts.txt +4 -0
  215. package/tailwind/components/layout/Copyright.tsx.txt +23 -0
  216. package/tailwind/components/layout/CustomerLayout.tsx.txt +91 -0
  217. package/tailwind/components/layout/Footer.tsx.txt +343 -0
  218. package/tailwind/components/layout/Header.tsx.txt +154 -0
  219. package/tailwind/components/layout/MasterLayout.tsx.txt +63 -0
  220. package/tailwind/components/layout/PageAdvertisements.tsx.txt +131 -0
  221. package/tailwind/components/layout/WidgetHead.tsx.txt +53 -0
  222. package/tailwind/components/layout/index.ts.txt +27 -0
  223. package/tailwind/components/layout/lazy.ts.txt +25 -0
  224. package/tailwind/components/layout/types.ts.txt +11 -0
  225. package/tailwind/components/modal/ImageModal.tsx.txt +70 -0
  226. package/tailwind/components/modal/Modal.tsx.txt +245 -0
  227. package/tailwind/components/modal/index.ts.txt +39 -0
  228. package/tailwind/components/modal/lazy.ts.txt +10 -0
  229. package/tailwind/components/navigation/MainMenu.tsx.txt +94 -0
  230. package/tailwind/components/navigation/MobileMenu.tsx.txt +160 -0
  231. package/tailwind/components/navigation/index.ts.txt +4 -0
  232. package/tailwind/components/navigation/lazy.ts.txt +11 -0
  233. package/tailwind/components/product/AddToCartForm.tsx.txt +365 -0
  234. package/tailwind/components/product/ProductCard.tsx.txt +305 -0
  235. package/tailwind/components/product/ProductDetails.tsx.txt +267 -0
  236. package/tailwind/components/product/ProductGallery.tsx.txt +234 -0
  237. package/tailwind/components/product/index.ts.txt +15 -0
  238. package/tailwind/components/product/lazy.ts.txt +9 -0
  239. package/tailwind/components/snippets/AppsSnippets.tsx.txt +225 -0
  240. package/tailwind/components/snippets/SallaSdkScripts.tsx.txt +14 -0
  241. package/tailwind/components/snippets/index.ts.txt +4 -0
  242. package/tailwind/components/store/StoreClosedNotification.tsx.txt +73 -0
  243. package/tailwind/components/store/index.ts.txt +8 -0
  244. package/tailwind/components/store/lazy.ts.txt +8 -0
  245. package/tailwind/components/toast/Toaster.tsx.txt +177 -0
  246. package/tailwind/components/toast/index.ts.txt +10 -0
  247. package/tailwind/components/toast/types.ts.txt +38 -0
  248. package/tailwind/components/toast/useToast.ts.txt +61 -0
  249. package/tailwind/components/tracking/GoogleTag.tsx.txt +77 -0
  250. package/tailwind/components/tracking/SiftSnippet.tsx.txt +49 -0
  251. package/tailwind/components/tracking/TwilightBundles.tsx.txt +67 -0
  252. package/tailwind/components/tracking/index.ts.txt +11 -0
  253. package/tailwind/components/tracking/lazy.ts.txt +11 -0
  254. package/tailwind/components/useComponent.ts.txt +12 -0
  255. package/tailwind/routes/account/index.tsx.txt +16 -0
  256. package/tailwind/routes/account/notifications/NotificationsPage.tsx.txt +54 -0
  257. package/tailwind/routes/account/notifications/index.tsx.txt +39 -0
  258. package/tailwind/routes/account/notifications/loader.ts.txt +22 -0
  259. package/tailwind/routes/account/notifications/types.ts.txt +18 -0
  260. package/tailwind/routes/account/orders/OrderSinglePage.tsx.txt +181 -0
  261. package/tailwind/routes/account/orders/OrdersPage.tsx.txt +28 -0
  262. package/tailwind/routes/account/orders/index.tsx.txt +86 -0
  263. package/tailwind/routes/account/orders/loader.ts.txt +66 -0
  264. package/tailwind/routes/account/orders/single/OrderActions.tsx.txt +197 -0
  265. package/tailwind/routes/account/orders/single/OrderHeader.tsx.txt +143 -0
  266. package/tailwind/routes/account/orders/single/OrderItemRow.tsx.txt +209 -0
  267. package/tailwind/routes/account/orders/single/OrderSections.tsx.txt +215 -0
  268. package/tailwind/routes/account/orders/single/types.ts.txt +38 -0
  269. package/tailwind/routes/account/orders/types.ts.txt +29 -0
  270. package/tailwind/routes/account/profile/ProfilePage.tsx.txt +30 -0
  271. package/tailwind/routes/account/profile/index.tsx.txt +39 -0
  272. package/tailwind/routes/account/profile/loader.ts.txt +16 -0
  273. package/tailwind/routes/account/profile/types.ts.txt +5 -0
  274. package/tailwind/routes/account/settings/SettingsPage.tsx.txt +13 -0
  275. package/tailwind/routes/account/settings/index.tsx.txt +39 -0
  276. package/tailwind/routes/account/settings/loader.ts.txt +13 -0
  277. package/tailwind/routes/account/settings/types.ts.txt +5 -0
  278. package/tailwind/routes/account/types.ts.txt +320 -0
  279. package/tailwind/routes/account/wallet/WalletPage.tsx.txt +195 -0
  280. package/tailwind/routes/account/wallet/index.tsx.txt +38 -0
  281. package/tailwind/routes/account/wallet/loader.ts.txt +27 -0
  282. package/tailwind/routes/account/wallet/types.ts.txt +11 -0
  283. package/tailwind/routes/account/wishlist/WishlistPage.tsx.txt +314 -0
  284. package/tailwind/routes/account/wishlist/WishlistShare.tsx.txt +248 -0
  285. package/tailwind/routes/account/wishlist/index.tsx.txt +40 -0
  286. package/tailwind/routes/account/wishlist/loader.ts.txt +26 -0
  287. package/tailwind/routes/account/wishlist/types.ts.txt +9 -0
  288. package/tailwind/routes/blog/BlogAuthorPage.tsx.txt +82 -0
  289. package/tailwind/routes/blog/BlogCategoryPage.tsx.txt +84 -0
  290. package/tailwind/routes/blog/BlogPage.tsx.txt +191 -0
  291. package/tailwind/routes/blog/BlogSinglePage.tsx.txt +168 -0
  292. package/tailwind/routes/blog/BlogTagPage.tsx.txt +84 -0
  293. package/tailwind/routes/blog/BlogTags.tsx.txt +54 -0
  294. package/tailwind/routes/blog/components.tsx.txt +87 -0
  295. package/tailwind/routes/blog/index.tsx.txt +238 -0
  296. package/tailwind/routes/blog/loader.ts.txt +173 -0
  297. package/tailwind/routes/blog/types.ts.txt +106 -0
  298. package/tailwind/routes/brands/BrandsPage.tsx.txt +179 -0
  299. package/tailwind/routes/brands/index.tsx.txt +47 -0
  300. package/tailwind/routes/brands/loader.ts.txt +14 -0
  301. package/tailwind/routes/brands/types.ts.txt +53 -0
  302. package/tailwind/routes/cart/CartPage.tsx.txt +112 -0
  303. package/tailwind/routes/cart/index.tsx.txt +39 -0
  304. package/tailwind/routes/cart/loader.ts.txt +15 -0
  305. package/tailwind/routes/cart/types.ts.txt +6 -0
  306. package/tailwind/routes/home/HomePage.tsx.txt +53 -0
  307. package/tailwind/routes/home/index.tsx.txt +56 -0
  308. package/tailwind/routes/home/loader.ts.txt +24 -0
  309. package/tailwind/routes/home/types.ts.txt +11 -0
  310. package/tailwind/routes/index.ts.txt +94 -0
  311. package/tailwind/routes/lazy.ts.txt +105 -0
  312. package/tailwind/routes/loyalty/LoyaltyPage.tsx.txt +17 -0
  313. package/tailwind/routes/loyalty/LoyaltyPointsPage.tsx.txt +11 -0
  314. package/tailwind/routes/loyalty/index.tsx.txt +81 -0
  315. package/tailwind/routes/loyalty/loader.ts.txt +53 -0
  316. package/tailwind/routes/loyalty/types.ts.txt +109 -0
  317. package/tailwind/routes/not-found.tsx.txt +50 -0
  318. package/tailwind/routes/page/PageSingle.tsx.txt +34 -0
  319. package/tailwind/routes/page/index.tsx.txt +60 -0
  320. package/tailwind/routes/page/loader.ts.txt +19 -0
  321. package/tailwind/routes/page/types.ts.txt +34 -0
  322. package/tailwind/routes/product/ProductPage.tsx.txt +77 -0
  323. package/tailwind/routes/product/index.tsx.txt +157 -0
  324. package/tailwind/routes/product/loader.ts.txt +35 -0
  325. package/tailwind/routes/product/types.ts.txt +6 -0
  326. package/tailwind/routes/product-listing/ProductListPage.tsx.txt +321 -0
  327. package/tailwind/routes/product-listing/index.tsx.txt +95 -0
  328. package/tailwind/routes/product-listing/loader.ts.txt +323 -0
  329. package/tailwind/routes/product-listing/types.ts.txt +96 -0
  330. package/tailwind/routes/product-listing/useAppliedFilters.ts.txt +130 -0
  331. package/tailwind/routes/product-short-link.ts.txt +104 -0
  332. package/tailwind/routes/root/beforeLoad.ts.txt +354 -0
  333. package/tailwind/routes/root/head.ts.txt +424 -0
  334. package/tailwind/routes/root/index.ts.txt +2 -0
  335. package/tailwind/routes/seo-redirects.ts.txt +82 -0
  336. package/tailwind/routes/testimonials/ReviewsPage.tsx.txt +12 -0
  337. package/tailwind/routes/testimonials/TestimonialsPage.tsx.txt +80 -0
  338. package/tailwind/routes/testimonials/index.tsx.txt +80 -0
  339. package/tailwind/routes/testimonials/loader.ts.txt +29 -0
  340. package/tailwind/routes/testimonials/types.ts.txt +9 -0
  341. package/tailwind/routes/thank-you/ThankYouPage.tsx.txt +241 -0
  342. package/tailwind/routes/thank-you/index.tsx.txt +49 -0
  343. package/tailwind/routes/thank-you/loader.ts.txt +40 -0
  344. package/tailwind/routes/thank-you/types.ts.txt +16 -0
  345. package/tailwind/routes/types.ts.txt +41 -0
  346. package/dist/BlogAuthorPage-GTF4UGSS.js.map +0 -1
  347. package/dist/BlogCategoryPage-TN7Y4NBP.js.map +0 -1
  348. package/dist/BlogPage-TDB3M3PI.js.map +0 -1
  349. package/dist/BlogSinglePage-4A52Q5QW.js.map +0 -1
  350. package/dist/BlogTagPage-TOVPBETA.js.map +0 -1
  351. package/dist/BrandsPage-VUDGYSQI.js.map +0 -1
  352. package/dist/Breadcrumb-CI3SN5YR.js.map +0 -1
  353. package/dist/CartItem-BHLEURAB.js.map +0 -1
  354. package/dist/CartSummary-QERVESYN.js.map +0 -1
  355. package/dist/CustomerLayout-VRZUWGQK.js.map +0 -1
  356. package/dist/LoyaltyPage-CGTHWJ76.js.map +0 -1
  357. package/dist/LoyaltyPointsPage-QSU6K3PW.js.map +0 -1
  358. package/dist/MobileMenu-U4BVQYKH.js.map +0 -1
  359. package/dist/NotificationsPage-ZRRQY7TF.js.map +0 -1
  360. package/dist/OrderSinglePage-O3XYVMG5.js.map +0 -1
  361. package/dist/OrdersPage-CMXZF5MI.js.map +0 -1
  362. package/dist/ProductDetails-65GDABI7.js.map +0 -1
  363. package/dist/ProfilePage-SNJ4WXU4.js.map +0 -1
  364. package/dist/SeoCartWidget-DBJAXJKT.js.map +0 -1
  365. package/dist/SettingsPage-FLTIOPU5.js.map +0 -1
  366. package/dist/ThankYouPage-5X6B4X3T.js.map +0 -1
  367. package/dist/WalletPage-H2BKIDPA.js.map +0 -1
  368. package/dist/WishlistPage-RS5UCIQ3.js.map +0 -1
  369. package/dist/chunk-7WAXN2PP.js +0 -14
  370. package/dist/chunk-NZVR4OF7.js.map +0 -1
  371. package/dist/chunk-SDFDS4Q3.js.map +0 -1
  372. package/dist/not-found-QTHBFPNT.js.map +0 -1
@@ -0,0 +1,77 @@
1
+ import { SallaQuickOrder } from '@salla.sa/twilight-components-react/quick-order';
2
+ import { SallaOffer } from '@salla.sa/twilight-components-react/offer';
3
+ import { SallaComments } from '@salla.sa/twilight-components-react/comments';
4
+ import { HookSlot } from '../../hooks';
5
+ import { useTranslation } from '../../providers/I18nProvider';
6
+ import { useProduct } from '../../hooks/useProduct';
7
+ import { useComments } from '../../hooks/useComments';
8
+ import type { ProductPageProps } from './types';
9
+ import { Breadcrumb, RenderWhenVisible } from '../../components/common';
10
+ import { ProductDetails, AddToCartForm, ProductGallery } from '../../components/product';
11
+ import { ProductsSlider, Testimonials } from '../../components/home';
12
+ import { useTwilight } from '../../providers/twilight-context';
13
+
14
+ export function ProductPage({ product: initialProduct, page }: ProductPageProps) {
15
+ const { theme, store } = useTwilight();
16
+ const { t } = useTranslation();
17
+ const { product } = useProduct(initialProduct);
18
+ const { commentsKey } = useComments();
19
+
20
+ return (
21
+ <div key={product.id}>
22
+ <div className="container">
23
+ {theme?.settings?.product_show_breadcrumbs !== false && <Breadcrumb page={page} />}
24
+
25
+ <HookSlot name="product:start" />
26
+
27
+ <div className="flex flex-col items-start md:flex-row" id={`product-${product.id}`}>
28
+ <ProductGallery product={product} />
29
+
30
+ <div className="main-content md:sticky md:overflow-hidden top-24 w-full md:w-2/4 md:pb-16">
31
+ <HookSlot name="product:details.start" />
32
+ <ProductDetails product={product} showTags={theme.settings.show_tags} />
33
+ <HookSlot name="product:details.end" />
34
+
35
+ <HookSlot name="product:single.form.start" />
36
+ <AddToCartForm product={product} stickyAddToCart={theme.settings.sticky_add_to_cart} />
37
+ <HookSlot name="product:single.form.end" />
38
+ {/* After the form, as in Twig; `.start` and `description` are in ProductDetails */}
39
+ <HookSlot name="product:single.description.end" context={{ product }} />
40
+
41
+ <SallaQuickOrder className="mt-5 md:-mb-2 block" />
42
+ </div>
43
+ </div>
44
+
45
+ <SallaOffer />
46
+ </div>
47
+
48
+ {store.settings?.rating?.show_on_product && (
49
+ <RenderWhenVisible>
50
+ <SallaComments key={commentsKey} itemId={Number(product.id)} type={'product' as any} />
51
+ </RenderWhenVisible>
52
+ )}
53
+
54
+ <HookSlot name="product:related.start" />
55
+ <RenderWhenVisible>
56
+ <div className="container">
57
+ <ProductsSlider
58
+ data={{
59
+ products: { source: 'related', source_value: Number(product.id) },
60
+ title: t('pages.products.similar_products'),
61
+ display_all_url: '',
62
+ }}
63
+ />
64
+ </div>
65
+ </RenderWhenVisible>
66
+ <HookSlot name="product:related.end" />
67
+
68
+ <RenderWhenVisible>
69
+ <div className="container">
70
+ <Testimonials data={{}} />
71
+ </div>
72
+ </RenderWhenVisible>
73
+
74
+ <HookSlot name="product:end" />
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,157 @@
1
+ import type { HeadDescriptor } from '../../utils/head';
2
+ import { buildHreflangAlternates } from '../../utils/baseHead';
3
+ import type { ProductPageProps } from './types';
4
+ import type { IdParams } from '../types';
5
+ import type { TwilightContext } from '../../twilight/context';
6
+ import { productLoader } from './loader';
7
+ import { RouteId } from '../../router/route-ids';
8
+ import { ProductPage } from './ProductPage';
9
+ import { fastCheckoutScript } from '../../utils/fast-checkout';
10
+ import { getActiveCurrency } from '../../api/client';
11
+ export { productLoader } from './loader';
12
+ export type { ProductPageProps } from './types';
13
+
14
+ async function loader(
15
+ ctx: { params: IdParams; locale?: string },
16
+ extend?: (
17
+ data: ProductPageProps,
18
+ ctx: { params: IdParams }
19
+ ) => Promise<Record<string, unknown>> | Record<string, unknown>
20
+ ): Promise<ProductPageProps> {
21
+ const data = await productLoader({ params: ctx.params });
22
+
23
+ if (extend) {
24
+ const extra = await extend(data, ctx);
25
+ return { ...data, ...extra } as ProductPageProps;
26
+ }
27
+ return data;
28
+ }
29
+
30
+ function head(ctx: TwilightContext, data: ProductPageProps): HeadDescriptor {
31
+ if (!data?.product) {
32
+ return {
33
+ title: 'Products',
34
+ description: 'Browse our products',
35
+ };
36
+ }
37
+ const product = data.product;
38
+ const storeName = ctx.settings?.store?.name ?? '';
39
+
40
+ // Strip HTML and limit description to 160 chars
41
+ const description = product.description?.replace(/<[^>]*>/g, '').slice(0, 160) ?? '';
42
+
43
+ // Price valid until: 1 month from now
44
+ const priceValidUntil = new Date();
45
+ priceValidUntil.setMonth(priceValidUntil.getMonth() + 1);
46
+
47
+ // Collect all product images
48
+ const images = product.images?.map((img) => img.url).filter(Boolean) ?? [];
49
+ if (product.image?.url && !images.includes(product.image.url)) {
50
+ images.unshift(product.image.url);
51
+ }
52
+
53
+ // Build JSON-LD schema matching SchemaProductWidget.php
54
+ const jsonLd: Record<string, unknown> = {
55
+ '@context': 'https://schema.org',
56
+ '@type': 'Product',
57
+ name: product.name,
58
+ description,
59
+ sku: product.sku,
60
+ image: images.length > 0 ? images : undefined,
61
+ offers: {
62
+ '@type': 'Offer',
63
+ url: product.url,
64
+ availability: product.is_available
65
+ ? 'https://schema.org/InStock'
66
+ : 'https://schema.org/OutOfStock',
67
+ price: product.sale_price,
68
+ priceCurrency: getActiveCurrency(product.currency),
69
+ priceValidUntil: priceValidUntil.toISOString().split('T')[0],
70
+ seller: storeName
71
+ ? {
72
+ '@type': 'Organization',
73
+ name: storeName,
74
+ }
75
+ : undefined,
76
+ },
77
+ };
78
+
79
+ // Add brand if exists
80
+ if (product.brand) {
81
+ jsonLd.brand = {
82
+ '@type': 'Brand',
83
+ name: product.brand.name,
84
+ description: product.brand.description,
85
+ logo: product.brand.logo,
86
+ };
87
+ }
88
+
89
+ // Add aggregateRating and review if rating exists
90
+ if (product.rating && product.rating.count > 0) {
91
+ const ratingValue = product.rating.stars;
92
+ jsonLd.aggregateRating = {
93
+ '@type': 'AggregateRating',
94
+ ratingCount: product.rating.count,
95
+ ratingValue,
96
+ };
97
+ jsonLd.review = {
98
+ '@type': 'Review',
99
+ author: storeName
100
+ ? {
101
+ '@type': 'Organization',
102
+ name: storeName,
103
+ }
104
+ : undefined,
105
+ reviewRating: {
106
+ '@type': 'Rating',
107
+ bestRating: 5,
108
+ ratingValue,
109
+ },
110
+ };
111
+ }
112
+
113
+ if (product.gtin && product.gtin.length === 8) {
114
+ jsonLd.gtin8 = product.gtin;
115
+ }
116
+
117
+ if (product.mpn) {
118
+ jsonLd.mpn = product.mpn;
119
+ }
120
+
121
+ // Twig: "<product> | <store name>", without the store meta title.
122
+ const title = storeName ? `${product.name} | ${storeName}` : product.name;
123
+ const base: HeadDescriptor = {
124
+ title,
125
+ appendStoreTitle: false,
126
+ description,
127
+ canonical: product.url,
128
+ openGraph: {
129
+ type: 'product',
130
+ title,
131
+ description,
132
+ images: product.image?.url,
133
+ url: product.url,
134
+ },
135
+ twitter: {
136
+ card: 'summary_large_image',
137
+ title,
138
+ description,
139
+ images: product.image?.url,
140
+ },
141
+ jsonLd,
142
+ alternateLanguages: buildHreflangAlternates(ctx.settings, ctx.location.pathname),
143
+ // Quick-buy widget code, eager only here: the Buy Now button is above the fold on this page.
144
+ // Other routes load it on idle from the body:end default hook, which also owns the
145
+ // `#fast-checkout-js` marker `salla-add-product-button` checks for.
146
+ scripts: ctx.settings?.store?.settings?.buy_now ? [fastCheckoutScript()] : [],
147
+ };
148
+
149
+ return base;
150
+ }
151
+
152
+ export const Product = {
153
+ id: RouteId.PRODUCT_SINGLE,
154
+ loader,
155
+ head,
156
+ Component: ProductPage,
157
+ } as const;
@@ -0,0 +1,35 @@
1
+ import type { ProductPageProps } from './types';
2
+ import type { IdParams } from '../types';
3
+ import { product } from '../../api/product';
4
+ import { getTwilightContext } from '../../twilight/context';
5
+ import { RouteId } from '../../router/route-ids';
6
+ import type { Breadcrumb } from '../../types';
7
+
8
+ export async function productLoader({ params }: { params: IdParams }): Promise<ProductPageProps> {
9
+ const data = await product.findOrThrow(params.id);
10
+ const i18n = getTwilightContext().i18n;
11
+ const home: Breadcrumb = { name: i18n.t('common.titles.home'), url: '/' };
12
+
13
+ const breadcrumbs: Breadcrumb[] = [home];
14
+ if (data.category) {
15
+ breadcrumbs.push({ name: data.category.name, url: data.category.url });
16
+ }
17
+ breadcrumbs.push({ name: data.name, url: data.url });
18
+
19
+ const page = {
20
+ slug: RouteId.PRODUCT_SINGLE,
21
+ id: data.id,
22
+ title: data.name,
23
+ url: data.url,
24
+ parent:
25
+ data.category && data.category.id
26
+ ? { id: data.category.id, name: data.category.name, url: data.category.url }
27
+ : null,
28
+ breadcrumbs,
29
+ };
30
+
31
+ return {
32
+ page,
33
+ product: data,
34
+ };
35
+ }
@@ -0,0 +1,6 @@
1
+ import type { Page, Product } from '../../types';
2
+
3
+ export interface ProductPageProps {
4
+ page: Page;
5
+ product: Product;
6
+ }
@@ -0,0 +1,321 @@
1
+ import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
2
+ import { HookSlot, useStore } from '../../hooks';
3
+ import { useTranslation } from '../../providers/I18nProvider';
4
+ import { useNavigate, useLocation } from '../../providers/NavigationProvider';
5
+ import { useTwilight } from '../../providers/TwilightProvider';
6
+ import { sortOptions } from '../../utils/sort-options';
7
+ import { Drawer } from '../../components/drawer';
8
+ import { Breadcrumb } from '../../components/common';
9
+ import { SallaFilters } from '@salla.sa/twilight-components-react/filters';
10
+ import { ItemsList, ReviewItemSkeleton } from '@salla.sa/twilight-components-react';
11
+ import { Testimonials, useReorderButtons } from '../../components/home';
12
+ import { ProductCard } from '../../components/product';
13
+ import { NoContent, ProductCardSkeleton, Image, RenderWhenVisible } from '../../components/common';
14
+ import { product, parseNextPageToken } from '../../api/product';
15
+ import { useAppliedFilters } from './useAppliedFilters';
16
+ import type { ProductListLoaderData } from './types';
17
+ import type { Brand } from '../brands/types';
18
+ import type { Product } from '../../types';
19
+ import { onSallaEvent } from '../../utils/sdk-helpers';
20
+
21
+ /** 3-col + 288px sidebar from md; 3-col of the full container below md. */
22
+ const FILTERED_PRODUCT_IMAGE_SIZES = '(min-width: 768px) 25vw, 33vw';
23
+
24
+ /**
25
+ * Product List Page - Unified component for all product listing pages
26
+ *
27
+ * Handles:
28
+ * - Category pages (with filters, category entity, testimonials)
29
+ * - Search pages (keyword display, product count)
30
+ * - Tag pages (tag entity)
31
+ * - Brand pages (brand entity with banner/logo)
32
+ * - Static listings (latest, sales, offers)
33
+ */
34
+ export function ProductListPage({
35
+ page,
36
+ source,
37
+ query,
38
+ products,
39
+ pagination,
40
+ filters,
41
+ }: ProductListLoaderData) {
42
+ const { t } = useTranslation();
43
+ const store = useStore();
44
+ const { theme } = useTwilight();
45
+ const navigate = useNavigate();
46
+ // Sort is now SSR-driven, no local state needed
47
+ const selectedSort = query.sort;
48
+ const [filtersOpen, setFiltersOpen] = useState(false);
49
+
50
+ // Determine filters visibility
51
+ const showFilters = query.filters && store?.settings?.product?.filters;
52
+ // const isSearch = source.type === 'search';
53
+ const isBrand = source.type === 'brands';
54
+ const brand = isBrand ? (source.entity as Brand | undefined) : null;
55
+ const isReorder = source.type === 'reorder';
56
+ useReorderButtons(isReorder);
57
+
58
+ // Generate sort options with translations
59
+ const sortOptionsWithTrans = useMemo(() => sortOptions(selectedSort, t), [selectedSort, t]);
60
+
61
+ // Get location from hook for consistent URL handling
62
+ const location = useLocation();
63
+
64
+ // Re-query the list with the selection made in `<salla-filters>`: the component only
65
+ // rewrites the address bar and emits `salla-filters::changed`, and the route's loader
66
+ // output was fetched without it.
67
+ const {
68
+ filters: appliedFilters,
69
+ page: filteredPage,
70
+ lastPage,
71
+ status: filterStatus,
72
+ retry: retryFilters,
73
+ } = useAppliedFilters(source, selectedSort, !!showFilters);
74
+ // While the new selection loads, the previous page stays up (dimmed, `aria-busy`); if
75
+ // the request fails nothing stale passes for the new selection. Infinite scroll is only
76
+ // live for the loader's own page or a fully loaded filtered one.
77
+ const shownPage = filterStatus === 'ready' ? filteredPage : lastPage;
78
+ const shownProducts =
79
+ filterStatus === 'error'
80
+ ? []
81
+ : filterStatus === 'idle'
82
+ ? products
83
+ : (shownPage?.items ?? products);
84
+ const shownNext =
85
+ filterStatus === 'idle'
86
+ ? pagination?.next
87
+ : filterStatus === 'ready'
88
+ ? filteredPage?.next
89
+ : null;
90
+ const filteredKey =
91
+ filterStatus === 'idle'
92
+ ? ''
93
+ : `-${filterStatus}${filterStatus === 'ready' ? JSON.stringify(appliedFilters) : ''}`;
94
+
95
+ // Listen for filters changed event to close drawer
96
+ useEffect(() => {
97
+ const handleFiltersChanged = () => setFiltersOpen(false);
98
+ return onSallaEvent('salla-filters::changed', handleFiltersChanged);
99
+ }, []);
100
+
101
+ // Handle sort change - use client-side navigation to trigger route loader re-run
102
+ const handleSortChange = useCallback(
103
+ (event: React.ChangeEvent<HTMLSelectElement>) => {
104
+ const newSort = event.target.value;
105
+
106
+ const params = new URLSearchParams(location.searchStr);
107
+ params.set('sort', newSort);
108
+ params.delete('page'); // clear any cursor token from URL on sort change
109
+
110
+ // Use client-side navigation to trigger route loader re-execution (SSR-like).
111
+ // Built from the router's location rather than `window.location`: on a host
112
+ // where the store is the root, the address bar carries the store segment and
113
+ // `navigate` would add it a second time.
114
+ navigate(`${location.pathname}?${params.toString()}`);
115
+ },
116
+ [navigate, location.pathname, location.searchStr]
117
+ );
118
+
119
+ // Key for remounting products list
120
+ const listKey = source.value ?? source.type;
121
+
122
+ // Track cursor for the next page — initialized from SSR pagination response.
123
+ // On sort change the key prop remounts ItemsList and this effect resets the cursor.
124
+ const nextCursorRef = useRef<string | null>(shownNext ?? null);
125
+ useEffect(() => {
126
+ nextCursorRef.current = shownNext ?? null;
127
+ }, [shownNext]);
128
+
129
+ const loader = useCallback(async () => {
130
+ const { cursor, page } = parseNextPageToken(nextCursorRef.current);
131
+
132
+ const result = await product.list({
133
+ source: source.type,
134
+ sourceValue: source.value,
135
+ ...(cursor && { cursor }),
136
+ ...(page !== undefined && { page }),
137
+ sort: selectedSort,
138
+ ...(appliedFilters && { filterable: true, filters: appliedFilters }),
139
+ });
140
+
141
+ nextCursorRef.current = result.next;
142
+
143
+ return result;
144
+ }, [source.type, source.value, selectedSort, appliedFilters]);
145
+
146
+ return (
147
+ <div
148
+ key={listKey}
149
+ className="container px-2.5 ms:px-5 mb-10"
150
+ data-source={isReorder ? 'reorder' : undefined}
151
+ >
152
+ {theme?.settings?.product_index_show_breadcrumbs !== false && <Breadcrumb page={page} />}
153
+
154
+ {isBrand && brand && (
155
+ <>
156
+ <Image
157
+ width={1200}
158
+ height={300}
159
+ className="w-full max-h-[300px] object-cover rounded-md bg-gray-100"
160
+ src={brand.banner || undefined}
161
+ alt={brand.name}
162
+ priority
163
+ />
164
+ <header className="flex flex-col md:flex-row items-center md:items-start text-center rtl:md:text-right ltr:md:text-left mt-2 mb-8">
165
+ <Image
166
+ className="rounded-md w-40 h-24 object-contain shadow-md p-4 bg-white shrink-0"
167
+ src={brand.logo || undefined}
168
+ alt={brand.name}
169
+ aspectRatio="5/3"
170
+ />
171
+ <div className="px-5 pt-3 rtl:text-right">
172
+ <h1 className="text-2xl font-bold mb-1">{brand.name}</h1>
173
+ {brand.description && (
174
+ <p
175
+ className="text-base text-gray-500 font-medium"
176
+ dangerouslySetInnerHTML={{ __html: brand.description }}
177
+ />
178
+ )}
179
+ </div>
180
+ </header>
181
+ </>
182
+ )}
183
+
184
+ <HookSlot name="product:list.start" />
185
+
186
+ <div className={showFilters ? 'md:grid md:grid-cols-[288px_1fr] gap-8' : ''}>
187
+ {/* <FiltersSkeleton></FiltersSkeleton> */}
188
+ {showFilters && (
189
+ <SallaFilters
190
+ id="filters-menu"
191
+ className="hidden md:block lg:sticky lg:top-20"
192
+ filters={filters as any}
193
+ />
194
+ )}
195
+
196
+ <div className="min-w-0">
197
+ <div className="mb-4 sm:mb-6 flex justify-between items-center">
198
+ <h1 className="font-bold text-xl rtl:pl-3 ltr:pr-3" id="page-main-title">
199
+ {isBrand ? t('common.titles.products') : page.title}
200
+ </h1>
201
+
202
+ <div className="flex items-center space-x-2 rtl:space-x-reverse">
203
+ {showFilters && (
204
+ <button
205
+ type="button"
206
+ onClick={() => setFiltersOpen(true)}
207
+ className="filters-trigger bg-border-color text-primary lg:hidden p-2 rounded"
208
+ >
209
+ <i className="sicon-filter text-gray-text text-2xl" />
210
+ </button>
211
+ )}
212
+
213
+ {sortOptionsWithTrans.length > 0 && (
214
+ <div className="flex items-center">
215
+ <label
216
+ className="hidden sm:block rtl:ml-3 ltr:mr-3 whitespace-nowrap"
217
+ htmlFor="product-sort"
218
+ >
219
+ {t('pages.categories.sorting', 'Sort By')}
220
+ </label>
221
+ <select
222
+ id="product-sort"
223
+ className="form-input pt-0 pb-1 rtl:md:pl-10 ltr:md:pr-10"
224
+ value={selectedSort}
225
+ onChange={handleSortChange}
226
+ >
227
+ {sortOptionsWithTrans.map((option) => (
228
+ <option key={option.id} value={option.id}>
229
+ {option.name}
230
+ </option>
231
+ ))}
232
+ </select>
233
+ </div>
234
+ )}
235
+ </div>
236
+ </div>
237
+
238
+ <HookSlot name="product:list.items.start" />
239
+
240
+ <div
241
+ className={`flex min-h-screen${filterStatus === 'loading' ? ' opacity-60' : ''}`}
242
+ aria-busy={filterStatus === 'loading'}
243
+ >
244
+ {filterStatus === 'error' ? (
245
+ <div className="flex-1 min-w-0 flex flex-col items-center">
246
+ <NoContent
247
+ icon="sicon-shopping-bag"
248
+ message={t('common.errors.error_occurred', 'Something went wrong')}
249
+ />
250
+ <button type="button" className="link--primary mt-4" onClick={retryFilters}>
251
+ {t('common.elements.try_again', 'Try again')}
252
+ </button>
253
+ </div>
254
+ ) : (
255
+ <ItemsList<Product>
256
+ key={`${listKey}-${selectedSort}${filteredKey}`}
257
+ resetKey={`${listKey}-${selectedSort}${filteredKey}`}
258
+ items={shownProducts}
259
+ loader={loader}
260
+ mode={shownNext ? 'infinite' : 'static'}
261
+ pageUrl={location.pathname + location.searchStr}
262
+ className="s-products-list flex-1 min-w-0"
263
+ itemsClassName={`s-products-list-wrapper s-products-list-vertical-cards${showFilters ? ' s-products-list-with-filters' : ''}`}
264
+ skeleton={<ProductCardSkeleton />}
265
+ skeletonType="item"
266
+ skeletonCount={12}
267
+ empty={
268
+ <NoContent
269
+ icon="sicon-shopping-bag"
270
+ message={t(
271
+ 'pages.categories.no_products',
272
+ 'No products found in this category'
273
+ )}
274
+ />
275
+ }
276
+ t={(key, fallback) => t(key, fallback || key)}
277
+ >
278
+ {(items) =>
279
+ items.map((prod, idx) => (
280
+ <ProductCard
281
+ key={prod.id}
282
+ product={prod}
283
+ index={idx}
284
+ sizes={showFilters ? FILTERED_PRODUCT_IMAGE_SIZES : undefined}
285
+ />
286
+ ))
287
+ }
288
+ </ItemsList>
289
+ )}
290
+ </div>
291
+
292
+ <HookSlot name="product:list.items.end" />
293
+ </div>
294
+ </div>
295
+
296
+ {showFilters && (
297
+ <Drawer isOpen={filtersOpen} onClose={() => setFiltersOpen(false)} size="md">
298
+ <div className="p-4">
299
+ <div className="flex justify-between items-center mb-4">
300
+ <h3 className="font-bold text-lg">{t('pages.categories.filters', 'Filters')}</h3>
301
+ <button
302
+ type="button"
303
+ onClick={() => setFiltersOpen(false)}
304
+ className="btn--close-sm sicon-cancel text-gray-500"
305
+ />
306
+ </div>
307
+ <SallaFilters id="filters-menu-mobile" filters={filters as any} />
308
+ </div>
309
+ </Drawer>
310
+ )}
311
+
312
+ {source.type === 'categories' && store?.settings?.category?.testimonial_enabled && (
313
+ <RenderWhenVisible placeholder={<ReviewItemSkeleton />}>
314
+ <Testimonials data={{}} />
315
+ </RenderWhenVisible>
316
+ )}
317
+
318
+ <HookSlot name="product:list.end" />
319
+ </div>
320
+ );
321
+ }
@@ -0,0 +1,95 @@
1
+ import { PageSkeleton } from '@salla.sa/twilight-components-react/skeletons';
2
+ import type { HeadDescriptor } from '../../utils/head';
3
+ import { buildHreflangAlternates } from '../../utils/baseHead';
4
+ import { productListLoader } from './loader';
5
+ import type { ProductListLoaderData, ProductListTag, Category } from './types';
6
+ import type { TwilightContext } from '../../twilight/context';
7
+ import { RouteId } from '../../router/route-ids';
8
+ import { registry, COMPONENT_KEYS } from '../../components/ComponentRegistry';
9
+ import { ProductListPage } from './ProductListPage';
10
+
11
+ const PRODUCTS_SOURCES = new Set(['latest', 'sales', 'offers', 'reorder']);
12
+
13
+ export function head(ctx: TwilightContext, data: ProductListLoaderData): HeadDescriptor {
14
+ const entity = data.source.entity;
15
+ const category = data.source.type === 'categories' ? (entity as Category | undefined) : undefined;
16
+ const tag = data.source.type === 'tags' ? (entity as ProductListTag | undefined) : undefined;
17
+ const productsPath = PRODUCTS_SOURCES.has(data.source.type)
18
+ ? `/products?source=${data.source.type}`
19
+ : undefined;
20
+
21
+ const alternateLanguages = buildHreflangAlternates(
22
+ ctx.settings,
23
+ category?.url ?? (tag?.url || productsPath || ctx.location.pathname)
24
+ );
25
+ const canonical = category?.url ?? (tag?.url || undefined);
26
+ const title = category?.name ?? tag?.name ?? data.page.title;
27
+ const image = category?.image ?? undefined;
28
+ const storeName = ctx.settings?.store?.name;
29
+ // Twig: categories read "<category> | <store name>", other listings get the default suffix.
30
+ const pageTitle = category && storeName ? `${title} | ${storeName}` : title;
31
+
32
+ const base: HeadDescriptor = {
33
+ title: pageTitle,
34
+ appendStoreTitle: !category,
35
+ description: `Browse ${title}`,
36
+ canonical,
37
+ openGraph: {
38
+ type: 'website',
39
+ title: pageTitle,
40
+ description: `Browse ${title}`,
41
+ images: image,
42
+ url: canonical,
43
+ },
44
+ twitter: {
45
+ card: 'summary_large_image',
46
+ title: pageTitle,
47
+ description: `Browse ${title}`,
48
+ images: image,
49
+ },
50
+ alternateLanguages,
51
+ };
52
+
53
+ return base;
54
+ }
55
+
56
+ export interface ProductListSkeletonProps {
57
+ hideFilters?: boolean;
58
+ }
59
+
60
+ function ProductListingPage(props: ProductListLoaderData) {
61
+ const Page =
62
+ registry.resolve<ProductListLoaderData>(COMPONENT_KEYS.PRODUCT_LIST_PAGE) ?? ProductListPage;
63
+ return <Page {...props} />;
64
+ }
65
+
66
+ function ProductListingPending({ hideFilters }: ProductListSkeletonProps) {
67
+ const Skeleton = registry.resolve<ProductListSkeletonProps>(COMPONENT_KEYS.PRODUCT_LIST_SKELETON);
68
+ return Skeleton ? <Skeleton hideFilters={hideFilters} /> : <PageSkeleton />;
69
+ }
70
+
71
+ export const ProductListing = {
72
+ id: RouteId.PRODUCT_INDEX,
73
+ loader: productListLoader,
74
+ head,
75
+ Component: ProductListingPage,
76
+ Pending: ProductListingPending,
77
+ } as const;
78
+
79
+ export { ProductListPage };
80
+ export { productListLoader };
81
+ export { useAppliedFilters, parseFiltersFromSearch } from './useAppliedFilters';
82
+ export type { AppliedFiltersStatus } from './useAppliedFilters';
83
+
84
+ export type {
85
+ ProductListLoaderData,
86
+ ProductListLoaderParams,
87
+ ProductListLoaderContext,
88
+ ProductListSource,
89
+ ProductListTag,
90
+ ProductListEntity,
91
+ ProductListPageMeta,
92
+ ProductListSourceConfig,
93
+ ProductListQuery,
94
+ CategoryPageProps,
95
+ } from './types';