@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,323 @@
1
+ import type { ProductListLoaderData, ProductListLoaderContext, ProductListSource } from './types';
2
+ import type { ProductsListSource } from '../../api/product';
3
+ import { category } from '../../api/category';
4
+ import { product } from '../../api/product';
5
+ import { brand } from '../../api/brands';
6
+ import { notFound, orThrow, unauthorized } from '../../providers/NavigationProvider';
7
+ import { getAuthToken } from '../../api/client';
8
+ import { getTwilightContext } from '../../twilight/context';
9
+ import { RouteId } from '../../router/route-ids';
10
+
11
+ const DEFAULT_SORT = 'ourSuggest';
12
+
13
+ const SOURCE_TO_API: Record<ProductListSource, ProductsListSource> = {
14
+ categories: 'categories',
15
+ latest: 'latest',
16
+ sales: 'sales',
17
+ offers: 'offers',
18
+ search: 'search',
19
+ tags: 'tags',
20
+ brands: 'brands',
21
+ reorder: 'reorder',
22
+ };
23
+
24
+ type StaticSource = 'latest' | 'sales' | 'offers' | 'reorder';
25
+
26
+ const SOURCE_TITLE_KEY: Record<StaticSource, string> = {
27
+ latest: 'blocks.home.latest_products',
28
+ sales: 'common.titles.most_sales',
29
+ offers: 'common.titles.discounts',
30
+ reorder: 'pages.orders.repurchase',
31
+ };
32
+
33
+ const SOURCE_SLUG: Record<StaticSource, string> = {
34
+ latest: 'products?source=latest',
35
+ sales: 'products?source=sales',
36
+ offers: 'products?source=offers',
37
+ reorder: 'products?source=reorder',
38
+ };
39
+
40
+ export async function productListLoader(
41
+ ctx: ProductListLoaderContext
42
+ ): Promise<ProductListLoaderData> {
43
+ const { params, search } = ctx;
44
+ // A number is a page from an old Twig link; the API pages only by its own cursor.
45
+ const page = search?.page ? String(search.page) : undefined;
46
+ const cursor = page && Number.isNaN(Number(page)) ? page : undefined;
47
+ const sort = search?.sort ?? DEFAULT_SORT;
48
+
49
+ const sourceType = params.source ?? (params.id ? 'categories' : undefined);
50
+
51
+ if (!sourceType) {
52
+ throw new Error('ProductList loader requires source type');
53
+ }
54
+
55
+ if (sourceType === 'categories' && params.id) {
56
+ return loadCategoryProducts(params.id, cursor, sort);
57
+ }
58
+
59
+ if (sourceType === 'search') {
60
+ const keyword = (search?.q ?? params.keyword ?? '').trim();
61
+ return loadSearchProducts(keyword, cursor, sort);
62
+ }
63
+
64
+ if (sourceType === 'tags' && params.id) {
65
+ return loadTagProducts(params.id, cursor, sort);
66
+ }
67
+
68
+ if (sourceType === 'brands' && params.id) {
69
+ return loadBrandProducts(params.id, cursor, sort);
70
+ }
71
+
72
+ // Only a signed-in customer has products to order again; the API answers a guest 401.
73
+ if (sourceType === 'reorder' && !getAuthToken()) throw unauthorized();
74
+
75
+ if (sourceType in SOURCE_TITLE_KEY) {
76
+ return loadStaticProducts(
77
+ sourceType as StaticSource,
78
+ params.title ?? '',
79
+ params.slug ?? '',
80
+ cursor,
81
+ sort
82
+ );
83
+ }
84
+
85
+ throw new Error(`Unknown source type: ${sourceType}`);
86
+ }
87
+
88
+ // ============================================================================
89
+ // Source-Specific Loaders
90
+ // ============================================================================
91
+
92
+ async function loadCategoryProducts(
93
+ id: string,
94
+ cursor: string | undefined,
95
+ sort: string
96
+ ): Promise<ProductListLoaderData> {
97
+ const { queryClient, i18n } = getTwilightContext();
98
+ let categoryData, productsResult;
99
+ try {
100
+ [categoryData, productsResult] = await Promise.all([
101
+ queryClient.fetchQuery(category.queries.detail(id)),
102
+ queryClient.fetchQuery(
103
+ product.queries.list({
104
+ source: 'categories',
105
+ sourceValue: id,
106
+ cursor,
107
+ sort,
108
+ filterable: true,
109
+ })
110
+ ),
111
+ ]);
112
+ } catch {
113
+ throw notFound();
114
+ }
115
+
116
+ if (!categoryData) throw notFound();
117
+
118
+ return {
119
+ page: {
120
+ slug: RouteId.PRODUCT_INDEX,
121
+ id,
122
+ title: categoryData.name,
123
+ breadcrumbs: [
124
+ { name: i18n.t('common.titles.home'), url: '/' },
125
+ { name: categoryData.name, url: categoryData.url },
126
+ ],
127
+ },
128
+ source: { type: 'categories', value: id, entity: categoryData },
129
+ query: { sort, filters: true },
130
+ products: productsResult.items,
131
+ pagination: {
132
+ next: productsResult.next,
133
+ },
134
+ filters: productsResult.filters,
135
+ };
136
+ }
137
+
138
+ async function loadSearchProducts(
139
+ keyword: string,
140
+ cursor: string | undefined,
141
+ sort: string
142
+ ): Promise<ProductListLoaderData> {
143
+ if (!keyword) {
144
+ const { i18n } = getTwilightContext();
145
+ return {
146
+ page: {
147
+ slug: RouteId.SEARCH,
148
+ title: i18n.t('pages.products.search_result'),
149
+ breadcrumbs: [
150
+ { name: i18n.t('common.titles.home'), url: '/' },
151
+ { name: i18n.t('pages.products.search_result'), url: '/search' },
152
+ ],
153
+ },
154
+ source: { type: 'search', value: '' },
155
+ query: { sort, filters: true },
156
+ products: [],
157
+ pagination: { next: null },
158
+ };
159
+ }
160
+
161
+ const { queryClient, i18n } = getTwilightContext();
162
+ const productsResult = await queryClient.fetchQuery(
163
+ product.queries.list({
164
+ source: 'search',
165
+ sourceValue: keyword,
166
+ cursor,
167
+ sort,
168
+ filterable: true,
169
+ })
170
+ );
171
+
172
+ return {
173
+ page: {
174
+ slug: RouteId.SEARCH,
175
+ title: i18n.t('common.elements.search_about').replace(':word', keyword),
176
+ breadcrumbs: [
177
+ { name: i18n.t('common.titles.home'), url: '/' },
178
+ { name: keyword, url: '/search' },
179
+ ],
180
+ },
181
+ source: { type: 'search', value: keyword },
182
+ query: { sort, filters: true },
183
+ products: productsResult.items,
184
+ pagination: {
185
+ next: productsResult.next,
186
+ },
187
+ filters: productsResult.filters,
188
+ };
189
+ }
190
+
191
+ async function loadTagProducts(
192
+ id: string,
193
+ cursor: string | undefined,
194
+ sort: string
195
+ ): Promise<ProductListLoaderData> {
196
+ const { queryClient, i18n } = getTwilightContext();
197
+ const [tagData, productsResult] = await Promise.all([
198
+ orThrow(queryClient.fetchQuery(product.queries.tag(id))),
199
+ queryClient.fetchQuery(
200
+ product.queries.list({
201
+ source: 'tags',
202
+ sourceValue: id,
203
+ cursor,
204
+ sort,
205
+ })
206
+ ),
207
+ ]);
208
+
209
+ if (!tagData) throw notFound();
210
+
211
+ // List products carry their tags without an id, so the URL is there only sometimes.
212
+ const tagUrl = productsResult.items[0]?.tags?.find((t) => String(t.id) === id)?.url ?? '';
213
+ const tag = { id: Number(tagData.id), name: tagData.name, url: tagUrl };
214
+
215
+ return {
216
+ page: {
217
+ slug: RouteId.PRODUCT_INDEX_TAG,
218
+ id,
219
+ title: tag.name,
220
+ breadcrumbs: [
221
+ { name: i18n.t('common.titles.home'), url: '/' },
222
+ // Laravel's own `tags/{id}` route, for when the listed tags carry no URL.
223
+ { name: tag.name, url: tag.url || `/tags/${id}` },
224
+ ],
225
+ },
226
+ source: { type: 'tags', value: id, entity: tag },
227
+ query: { sort, filters: false },
228
+ products: productsResult.items,
229
+ pagination: {
230
+ next: productsResult.next,
231
+ },
232
+ };
233
+ }
234
+
235
+ async function loadBrandProducts(
236
+ id: string,
237
+ cursor: string | undefined,
238
+ sort: string
239
+ ): Promise<ProductListLoaderData> {
240
+ const { queryClient, i18n } = getTwilightContext();
241
+ let brandData, productsResult;
242
+ try {
243
+ [brandData, productsResult] = await Promise.all([
244
+ queryClient.fetchQuery(brand.queries.detail(id)),
245
+ queryClient.fetchQuery(
246
+ product.queries.list({
247
+ source: 'brands',
248
+ sourceValue: id,
249
+ cursor,
250
+ sort,
251
+ })
252
+ ),
253
+ ]);
254
+ } catch {
255
+ throw notFound();
256
+ }
257
+
258
+ if (!brandData) throw notFound();
259
+
260
+ return {
261
+ page: {
262
+ slug: RouteId.BRANDS_INDEX,
263
+ id,
264
+ title: brandData.name,
265
+ breadcrumbs: [
266
+ { name: i18n.t('common.titles.home'), url: '/' },
267
+ // The brand API sends no url.
268
+ { name: brandData.name, url: `/brands/${id}` },
269
+ ],
270
+ },
271
+ source: { type: 'brands', value: id, entity: brandData },
272
+ query: { sort, filters: false },
273
+ products: productsResult.items,
274
+ pagination: {
275
+ next: productsResult.next,
276
+ },
277
+ };
278
+ }
279
+
280
+ const SOURCE_ROUTE_ID: Record<StaticSource, string> = {
281
+ latest: RouteId.PRODUCT_INDEX_LATEST,
282
+ sales: RouteId.PRODUCT_INDEX_SALES,
283
+ offers: RouteId.PRODUCT_INDEX_OFFERS,
284
+ reorder: RouteId.PRODUCT_INDEX,
285
+ };
286
+
287
+ async function loadStaticProducts(
288
+ source: StaticSource,
289
+ title: string,
290
+ slug: string,
291
+ cursor: string | undefined,
292
+ sort: string
293
+ ): Promise<ProductListLoaderData> {
294
+ const { queryClient, i18n } = getTwilightContext();
295
+ const resolvedTitle = title || i18n.t(SOURCE_TITLE_KEY[source]);
296
+ const resolvedSlug = slug || SOURCE_SLUG[source];
297
+ const productsResult = await queryClient.fetchQuery({
298
+ ...product.queries.list({
299
+ source: SOURCE_TO_API[source],
300
+ cursor,
301
+ sort,
302
+ }),
303
+ // The reorder key names no customer, so a cached list could be another session's.
304
+ ...(source === 'reorder' && { staleTime: 0 }),
305
+ });
306
+
307
+ return {
308
+ page: {
309
+ slug: SOURCE_ROUTE_ID[source],
310
+ title: resolvedTitle,
311
+ breadcrumbs: [
312
+ { name: i18n.t('common.titles.home'), url: '/' },
313
+ { name: resolvedTitle, url: `/${resolvedSlug}` },
314
+ ],
315
+ },
316
+ source: { type: source },
317
+ query: { sort, filters: false },
318
+ products: productsResult.items,
319
+ pagination: {
320
+ next: productsResult.next,
321
+ },
322
+ };
323
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Product List Route Types
3
+ * Unified types for all product listing pages (categories, search, tags, brands, latest/sales/offers).
4
+ */
5
+
6
+ import type { Product, Category } from '../../types';
7
+ import type { Brand } from '../brands/types';
8
+ import type { RoutePagination } from '../types';
9
+ import type { Filter } from '../../api/product';
10
+
11
+ // Re-export for convenience
12
+ export type { Category } from '../../types';
13
+ export type { Brand } from '../brands/types';
14
+ export type { RoutePagination } from '../types';
15
+
16
+ // ============================================================================
17
+ // Source Types
18
+ // ============================================================================
19
+
20
+ /** Unified source type matching API sources directly */
21
+ export type ProductListSource =
22
+ 'categories' | 'latest' | 'sales' | 'offers' | 'search' | 'tags' | 'brands' | 'reorder';
23
+
24
+ /** Tag entity - products can be filtered by tags */
25
+ export interface ProductListTag {
26
+ id: number;
27
+ name: string;
28
+ url: string;
29
+ }
30
+
31
+ /** Entity union type for source metadata */
32
+ export type ProductListEntity = Category | ProductListTag | Brand;
33
+
34
+ // ============================================================================
35
+ // Loader Data
36
+ // ============================================================================
37
+
38
+ /** Static page metadata */
39
+ export interface ProductListPageMeta {
40
+ title: string;
41
+ slug: string;
42
+ id?: string;
43
+ breadcrumbs?: import('../../types').Breadcrumb[];
44
+ }
45
+
46
+ /** Source configuration */
47
+ export interface ProductListSourceConfig {
48
+ type: ProductListSource;
49
+ value?: string;
50
+ entity?: ProductListEntity;
51
+ }
52
+
53
+ /** URL query parameters — no page field, product list uses cursor-based pagination */
54
+ export interface ProductListQuery {
55
+ sort: string;
56
+ filters?: boolean;
57
+ }
58
+
59
+ /** Unified loader data for all product listing pages */
60
+ export interface ProductListLoaderData {
61
+ page: ProductListPageMeta;
62
+ source: ProductListSourceConfig;
63
+ query: ProductListQuery;
64
+ products: Product[];
65
+ pagination: RoutePagination;
66
+ filters?: Filter[];
67
+ }
68
+
69
+ // ============================================================================
70
+ // Loader Params & Context
71
+ // ============================================================================
72
+
73
+ export interface ProductListLoaderParams {
74
+ id?: string;
75
+ source?: ProductListSource;
76
+ title?: string;
77
+ slug?: string;
78
+ keyword?: string;
79
+ }
80
+
81
+ export interface ProductListLoaderContext {
82
+ params: ProductListLoaderParams;
83
+ locale?: string;
84
+ search?: { page?: string | number; sort?: string; q?: string };
85
+ }
86
+
87
+ // ============================================================================
88
+ // Props Types
89
+ // ============================================================================
90
+
91
+ /** Props for Category/Collection Page component */
92
+ export interface CategoryPageProps {
93
+ page: ProductListPageMeta;
94
+ category: Category;
95
+ products: Product[];
96
+ }
@@ -0,0 +1,130 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { product } from '../../api/product';
3
+ import type { ProductListFilters } from '../../api/product';
4
+ import type { Product } from '../../types';
5
+ import type { ProductListLoaderData } from './types';
6
+ import { onSallaEvent } from '../../utils/sdk-helpers';
7
+
8
+ type Source = ProductListLoaderData['source'];
9
+ type Node = Record<string, unknown>;
10
+
11
+ /** Path segments that would reach the shared prototype chain if used as object keys. */
12
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
13
+
14
+ /**
15
+ * Reads the `filters[...]` selection `<salla-filters>` writes to the address bar
16
+ * (`filters[k]=v`, `filters[k][]=v`, `filters[k][sub]=v`) back into an object, so a
17
+ * filtered link can be opened directly.
18
+ *
19
+ * The keys come straight from the URL, so nodes are null-prototype objects and any
20
+ * `__proto__` / `constructor` / `prototype` segment drops the whole entry — a crafted
21
+ * `?filters[__proto__][x]=1` must never reach `Object.prototype`.
22
+ */
23
+ export function parseFiltersFromSearch(search: string): ProductListFilters | null {
24
+ const out: Node = Object.create(null);
25
+ new URLSearchParams(search).forEach((value, rawKey) => {
26
+ if (!rawKey.startsWith('filters[')) return;
27
+ const path = [...rawKey.matchAll(/\[([^\]]*)\]/g)].map((m) => m[1]);
28
+ const isList = path[path.length - 1] === '';
29
+ const keys = isList ? path.slice(0, -1) : path;
30
+ if (!keys.length || keys.includes('') || keys.some((key) => UNSAFE_KEYS.has(key))) return;
31
+
32
+ let node = out;
33
+ keys.slice(0, -1).forEach((key) => {
34
+ const existing = node[key];
35
+ if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {
36
+ node[key] = Object.create(null);
37
+ }
38
+ node = node[key] as Node;
39
+ });
40
+ const leaf = keys[keys.length - 1];
41
+ if (isList) {
42
+ node[leaf] = [...(Array.isArray(node[leaf]) ? (node[leaf] as string[]) : []), value];
43
+ } else {
44
+ node[leaf] = value;
45
+ }
46
+ });
47
+ return Object.keys(out).length ? (out as ProductListFilters) : null;
48
+ }
49
+
50
+ interface FilteredPage {
51
+ items: Product[];
52
+ next: string | null;
53
+ }
54
+
55
+ export type AppliedFiltersStatus = 'idle' | 'loading' | 'ready' | 'error';
56
+
57
+ /**
58
+ * Applies the `<salla-filters>` selection to the product list.
59
+ *
60
+ * The filters component only rewrites the address bar (`history.pushState`, invisible
61
+ * to the router) and emits `salla-filters::changed`; nothing re-queries the list, so
62
+ * the route's loader output — fetched without filters — kept showing. This listens
63
+ * for that event (and for a filtered URL on first load) and fetches the first page
64
+ * with the selection.
65
+ *
66
+ * Each fetched page is stored with the selection that produced it, so `page` is only
67
+ * ever the page for the *current* selection. Until it arrives `status` is `loading`
68
+ * (and `lastPage` is the previous selection's page, for a dimmed placeholder); if the
69
+ * request fails `status` is `error` and `retry` re-runs it — the old products never
70
+ * pass for the new selection.
71
+ */
72
+ export function useAppliedFilters(
73
+ source: Source,
74
+ sort: string,
75
+ enabled: boolean
76
+ ): {
77
+ filters: ProductListFilters | null;
78
+ page: FilteredPage | null;
79
+ lastPage: FilteredPage | null;
80
+ status: AppliedFiltersStatus;
81
+ retry: () => void;
82
+ } {
83
+ const [filters, setFilters] = useState<ProductListFilters | null>(null);
84
+ const [result, setResult] = useState<{ key: string; page: FilteredPage } | null>(null);
85
+ const [failedKey, setFailedKey] = useState<string | null>(null);
86
+ const [attempt, setAttempt] = useState(0);
87
+
88
+ useEffect(() => {
89
+ if (!enabled) return;
90
+ setFilters(parseFiltersFromSearch(window.location.search));
91
+ const onChanged = (...args: unknown[]) => {
92
+ const next = args[0] as ProductListFilters | undefined;
93
+ setFilters(next && Object.keys(next).length ? { ...next } : null);
94
+ };
95
+ return onSallaEvent('salla-filters::changed', onChanged);
96
+ }, [enabled, source.type, source.value]);
97
+
98
+ const key = filters ? JSON.stringify([source.type, source.value, sort, filters]) : '';
99
+
100
+ useEffect(() => {
101
+ if (!filters) return;
102
+ const controller = new AbortController();
103
+ setFailedKey(null);
104
+ product
105
+ .list({
106
+ source: source.type,
107
+ sourceValue: source.value,
108
+ sort,
109
+ filterable: true,
110
+ filters,
111
+ signal: controller.signal,
112
+ })
113
+ .then((res) => setResult({ key, page: { items: res.items, next: res.next } }))
114
+ .catch(() => {
115
+ // Aborted by a newer selection (its own effect run owns the state now), or failed.
116
+ if (!controller.signal.aborted) setFailedKey(key);
117
+ });
118
+ return () => controller.abort();
119
+ // `key` already encodes filters + source + sort.
120
+ // eslint-disable-next-line react-hooks/exhaustive-deps
121
+ }, [key, attempt]);
122
+
123
+ const retry = useCallback(() => setAttempt((n) => n + 1), []);
124
+
125
+ if (!filters) return { filters: null, page: null, lastPage: null, status: 'idle', retry };
126
+
127
+ const page = result?.key === key ? result.page : null;
128
+ const status: AppliedFiltersStatus = page ? 'ready' : failedKey === key ? 'error' : 'loading';
129
+ return { filters, page, lastPage: result?.page ?? null, status, retry };
130
+ }
@@ -0,0 +1,104 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { product } from '../api/product';
3
+ import { isLocale } from '../providers/locale-constants';
4
+ import { useLocation, useNavigate } from '../providers/NavigationProvider';
5
+
6
+ /** Short codes are 5-10 letters/digits (e.g. `lvXvzln`). */
7
+ const SHORT_CODE = /^[A-Za-z0-9]{5,10}$/;
8
+
9
+ /**
10
+ * The product short code in a path shaped `/{locale}/{code}` or `/{code}` (the
11
+ * store base is already stripped from the router's location), plus the locale
12
+ * segment to keep when redirecting. `null` for any other path.
13
+ */
14
+ export function parseShortLinkPath(pathname: string): { code: string; locale?: string } | null {
15
+ const segments = pathname.split('/').filter(Boolean);
16
+ if (segments.length === 2 && isLocale(segments[0]) && SHORT_CODE.test(segments[1])) {
17
+ return { code: segments[1], locale: segments[0] };
18
+ }
19
+ if (segments.length === 1 && !isLocale(segments[0]) && SHORT_CODE.test(segments[0])) {
20
+ return { code: segments[0] };
21
+ }
22
+ return null;
23
+ }
24
+
25
+ /** A product URL in the stores' "new URLs" format: `{sku}-{name}`, or `p{id}-{name}` without a SKU. */
26
+ const PRODUCT_KEY = /^([0-9A-Za-z]+)-./;
27
+
28
+ /** The `{sku}` or `p{id}` key of `/{locale}/{sku}-{name}` or `/{sku}-{name}`, with that locale. */
29
+ export function parseProductKeyPath(pathname: string): { key: string; locale?: string } | null {
30
+ const segments = pathname.split('/').filter(Boolean);
31
+ const hasLocale = segments.length === 2 && isLocale(segments[0]);
32
+ if (!hasLocale && (segments.length !== 1 || isLocale(segments[0]))) return null;
33
+ const key = segments[segments.length - 1].match(PRODUCT_KEY)?.[1];
34
+ if (!key) return null;
35
+ return hasLocale ? { key, locale: segments[0] } : { key };
36
+ }
37
+
38
+ function productKeyOf(url: string | undefined): string | undefined {
39
+ try {
40
+ const last = new URL(url ?? '').pathname.split('/').filter(Boolean).pop() ?? '';
41
+ return last.match(PRODUCT_KEY)?.[1];
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ }
46
+
47
+ /** Like Laravel: `p{id}` is the id; anything else is a SKU, which only search can find. */
48
+ async function findByProductKey(key: string): Promise<number | null> {
49
+ const id = key.match(/^p(\d+)$/)?.[1];
50
+ if (id) return Number(id);
51
+ const { items } = await product.list({ source: 'search', keyword: key, perPage: 50 });
52
+ const found = items.find((item) => productKeyOf(item.url) === key);
53
+ return found ? Number(found.id) : null;
54
+ }
55
+
56
+ /** The SEO slug from a product URL (`…/{slug}/p{id}`), `-` when it has none. */
57
+ export function slugFromProductUrl(url: string | undefined): string {
58
+ try {
59
+ const segments = new URL(url ?? '').pathname.split('/').filter(Boolean);
60
+ const last = segments[segments.length - 1] ?? '';
61
+ return /^p\d+$/.test(last) && segments.length > 1 ? segments[segments.length - 2] : '-';
62
+ } catch {
63
+ return '-';
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Resolves a product URL that matched no route, a short link (`/ar/lvXvzln`) or
69
+ * a "new URLs" one (`/ar/1213asa-chocolate`), and replaces the URL with the
70
+ * product's page. Returns `true` while a lookup is in flight (so the 404 isn't
71
+ * flashed); settles to `false` when the path is neither or doesn't resolve.
72
+ */
73
+ export function useProductShortLink(): boolean {
74
+ const { pathname } = useLocation();
75
+ const navigate = useNavigate();
76
+ const shortLink = parseShortLinkPath(pathname);
77
+ const productKey = parseProductKeyPath(pathname);
78
+ const lookup = shortLink?.code ?? productKey?.key;
79
+ const locale = shortLink?.locale ?? productKey?.locale;
80
+ const isShortLink = !!shortLink;
81
+ const [failed, setFailed] = useState<string | null>(null);
82
+
83
+ useEffect(() => {
84
+ if (!lookup) return;
85
+ let cancelled = false;
86
+ const productPath = isShortLink
87
+ ? product
88
+ .find(lookup)
89
+ .then((found) => (found?.id ? `${slugFromProductUrl(found.url)}/p${found.id}` : null))
90
+ : findByProductKey(lookup).then((id) => (id ? `-/p${id}` : null));
91
+ productPath
92
+ .then((path) => {
93
+ if (cancelled) return;
94
+ if (!path) return setFailed(lookup);
95
+ navigate(`${locale ? `/${locale}` : ''}/${path}`, { replace: true });
96
+ })
97
+ .catch(() => !cancelled && setFailed(lookup));
98
+ return () => {
99
+ cancelled = true;
100
+ };
101
+ }, [lookup, isShortLink, locale, navigate]);
102
+
103
+ return !!lookup && failed !== lookup;
104
+ }