@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.
- package/dist/{AddToCartForm-B5C2M4OZ.js → AddToCartForm-5X2WZDWR.js} +3 -3
- package/dist/{AddToCartForm-B5C2M4OZ.js.map → AddToCartForm-5X2WZDWR.js.map} +1 -1
- package/dist/{BlogAuthorPage-GTF4UGSS.js → BlogAuthorPage-LO7FSSJR.js} +12 -10
- package/dist/BlogAuthorPage-LO7FSSJR.js.map +1 -0
- package/dist/{BlogCategoryPage-TN7Y4NBP.js → BlogCategoryPage-KKFQ3PGR.js} +12 -10
- package/dist/BlogCategoryPage-KKFQ3PGR.js.map +1 -0
- package/dist/{BlogPage-TDB3M3PI.js → BlogPage-VLF3NSGE.js} +11 -9
- package/dist/BlogPage-VLF3NSGE.js.map +1 -0
- package/dist/{BlogSinglePage-4A52Q5QW.js → BlogSinglePage-AVK535IW.js} +11 -9
- package/dist/BlogSinglePage-AVK535IW.js.map +1 -0
- package/dist/{BlogTagPage-TOVPBETA.js → BlogTagPage-F3T4D7RH.js} +12 -10
- package/dist/BlogTagPage-F3T4D7RH.js.map +1 -0
- package/dist/{BrandsPage-VUDGYSQI.js → BrandsPage-LVSBZSVC.js} +11 -9
- package/dist/BrandsPage-LVSBZSVC.js.map +1 -0
- package/dist/{Breadcrumb-CI3SN5YR.js → Breadcrumb-VLFTXRFC.js} +11 -9
- package/dist/Breadcrumb-VLFTXRFC.js.map +1 -0
- package/dist/{CartItem-BHLEURAB.js → CartItem-G3H34GW6.js} +11 -9
- package/dist/CartItem-G3H34GW6.js.map +1 -0
- package/dist/{CartSummary-QERVESYN.js → CartSummary-VH37RPUI.js} +11 -9
- package/dist/CartSummary-VH37RPUI.js.map +1 -0
- package/dist/{Copyright-RL6ALKRC.js → Copyright-HE5ZWINZ.js} +3 -3
- package/dist/{Copyright-RL6ALKRC.js.map → Copyright-HE5ZWINZ.js.map} +1 -1
- package/dist/{CustomerLayout-VRZUWGQK.js → CustomerLayout-LKBLOAVW.js} +11 -9
- package/dist/CustomerLayout-LKBLOAVW.js.map +1 -0
- package/dist/{Footer-ZWBZCMDL.js → Footer-HHXAST72.js} +5 -5
- package/dist/{Footer-ZWBZCMDL.js.map → Footer-HHXAST72.js.map} +1 -1
- package/dist/{Header-WNK4O66B.js → Header-GEVZM2YG.js} +8 -6
- package/dist/{Header-WNK4O66B.js.map → Header-GEVZM2YG.js.map} +1 -1
- package/dist/{LoyaltyPage-CGTHWJ76.js → LoyaltyPage-M5CT3CFD.js} +11 -9
- package/dist/LoyaltyPage-M5CT3CFD.js.map +1 -0
- package/dist/{LoyaltyPointsPage-QSU6K3PW.js → LoyaltyPointsPage-SDJB52U6.js} +10 -3
- package/dist/LoyaltyPointsPage-SDJB52U6.js.map +1 -0
- package/dist/{MainMenu-V3T7F32R.js → MainMenu-I7EYS752.js} +3 -3
- package/dist/{MainMenu-V3T7F32R.js.map → MainMenu-I7EYS752.js.map} +1 -1
- package/dist/{MasterLayout-XQF644LD.js → MasterLayout-QDK4T3NE.js} +11 -9
- package/dist/{MasterLayout-XQF644LD.js.map → MasterLayout-QDK4T3NE.js.map} +1 -1
- package/dist/{MobileMenu-U4BVQYKH.js → MobileMenu-7VTCWAVJ.js} +11 -9
- package/dist/MobileMenu-7VTCWAVJ.js.map +1 -0
- package/dist/{NotificationsPage-ZRRQY7TF.js → NotificationsPage-QCABE6QD.js} +11 -9
- package/dist/NotificationsPage-QCABE6QD.js.map +1 -0
- package/dist/{OrderSinglePage-O3XYVMG5.js → OrderSinglePage-2HVBKBG4.js} +11 -9
- package/dist/OrderSinglePage-2HVBKBG4.js.map +1 -0
- package/dist/{OrdersPage-CMXZF5MI.js → OrdersPage-5MNCPCM3.js} +11 -9
- package/dist/OrdersPage-5MNCPCM3.js.map +1 -0
- package/dist/{PageSingle-IIG2US43.js → PageSingle-EN5XIM2B.js} +3 -3
- package/dist/{PageSingle-IIG2US43.js.map → PageSingle-EN5XIM2B.js.map} +1 -1
- package/dist/{ProductDetails-65GDABI7.js → ProductDetails-UEWQPPDZ.js} +13 -6
- package/dist/ProductDetails-UEWQPPDZ.js.map +1 -0
- package/dist/{ProfilePage-SNJ4WXU4.js → ProfilePage-TZSU3GHZ.js} +11 -9
- package/dist/ProfilePage-TZSU3GHZ.js.map +1 -0
- package/dist/{ReviewsPage-UT7UT55J.js → ReviewsPage-WYZ6CHF2.js} +3 -3
- package/dist/{ReviewsPage-UT7UT55J.js.map → ReviewsPage-WYZ6CHF2.js.map} +1 -1
- package/dist/{SeoCartWidget-DBJAXJKT.js → SeoCartWidget-5JR5RXQM.js} +11 -9
- package/dist/SeoCartWidget-5JR5RXQM.js.map +1 -0
- package/dist/{SettingsPage-FLTIOPU5.js → SettingsPage-DIDRQXMM.js} +11 -9
- package/dist/SettingsPage-DIDRQXMM.js.map +1 -0
- package/dist/{StoreClosedNotification-ZT3QKDKS.js → StoreClosedNotification-C2G32E34.js} +12 -10
- package/dist/{StoreClosedNotification-ZT3QKDKS.js.map → StoreClosedNotification-C2G32E34.js.map} +1 -1
- package/dist/{TestimonialsPage-CU7HKU4O.js → TestimonialsPage-XPHFRBZ7.js} +3 -3
- package/dist/{TestimonialsPage-CU7HKU4O.js.map → TestimonialsPage-XPHFRBZ7.js.map} +1 -1
- package/dist/{ThankYouPage-5X6B4X3T.js → ThankYouPage-UA6YYFB2.js} +11 -9
- package/dist/ThankYouPage-UA6YYFB2.js.map +1 -0
- package/dist/{WalletPage-H2BKIDPA.js → WalletPage-AJVV7LVL.js} +11 -9
- package/dist/WalletPage-AJVV7LVL.js.map +1 -0
- package/dist/{WishlistPage-RS5UCIQ3.js → WishlistPage-UW6LJLSW.js} +11 -9
- package/dist/WishlistPage-UW6LJLSW.js.map +1 -0
- package/dist/api/advertisement.d.ts +33 -0
- package/dist/api/advertisement.d.ts.map +1 -0
- package/dist/api/advertisement.js +8 -0
- package/dist/api/advertisement.js.map +1 -0
- package/dist/boot/critical-style.d.ts +3 -0
- package/dist/boot/critical-style.d.ts.map +1 -0
- package/dist/boot/index.d.ts +15 -0
- package/dist/boot/index.d.ts.map +1 -0
- package/dist/boot.d.ts +1 -0
- package/dist/boot.js +69 -0
- package/dist/boot.js.map +1 -0
- package/dist/{chunk-SE5JYWOK.js → chunk-2HCCIHU7.js} +4 -4
- package/dist/{chunk-SE5JYWOK.js.map → chunk-2HCCIHU7.js.map} +1 -1
- package/dist/chunk-4FJGZQD3.js +6 -0
- package/dist/chunk-4FJGZQD3.js.map +1 -0
- package/dist/{chunk-SDFDS4Q3.js → chunk-4WVJJ7SN.js} +20 -4
- package/dist/chunk-4WVJJ7SN.js.map +1 -0
- package/dist/{chunk-NXQJA7D3.js → chunk-BAYGWTNM.js} +3 -3
- package/dist/{chunk-NXQJA7D3.js.map → chunk-BAYGWTNM.js.map} +1 -1
- package/dist/{chunk-OWX2CPNQ.js → chunk-CLBFF2CY.js} +3 -3
- package/dist/{chunk-OWX2CPNQ.js.map → chunk-CLBFF2CY.js.map} +1 -1
- package/dist/chunk-DBW7XXNE.js +103 -0
- package/dist/chunk-DBW7XXNE.js.map +1 -0
- package/dist/{chunk-KHBSGD2D.js → chunk-DBXH3EMJ.js} +3 -3
- package/dist/{chunk-KHBSGD2D.js.map → chunk-DBXH3EMJ.js.map} +1 -1
- package/dist/{chunk-JRNH3FUF.js → chunk-E26YG3AO.js} +4 -4
- package/dist/{chunk-JRNH3FUF.js.map → chunk-E26YG3AO.js.map} +1 -1
- package/dist/{chunk-22SH7OVF.js → chunk-E7WWO3V3.js} +3 -3
- package/dist/{chunk-22SH7OVF.js.map → chunk-E7WWO3V3.js.map} +1 -1
- package/dist/{chunk-UJKFBZFJ.js → chunk-HQ7WA37L.js} +7 -7
- package/dist/{chunk-UJKFBZFJ.js.map → chunk-HQ7WA37L.js.map} +1 -1
- package/dist/chunk-IVBOLREE.js +29 -0
- package/dist/chunk-IVBOLREE.js.map +1 -0
- package/dist/{chunk-6SWDSRIL.js → chunk-KP3VYG4F.js} +6 -6
- package/dist/{chunk-6SWDSRIL.js.map → chunk-KP3VYG4F.js.map} +1 -1
- package/dist/{chunk-4CZRUAZB.js → chunk-PWTXR4RR.js} +10 -10
- package/dist/{chunk-4CZRUAZB.js.map → chunk-PWTXR4RR.js.map} +1 -1
- package/dist/chunk-QJYUTRAQ.js +14 -0
- package/dist/{chunk-7WAXN2PP.js.map → chunk-QJYUTRAQ.js.map} +1 -1
- package/dist/{chunk-JO7JWV34.js → chunk-U55ZJC2X.js} +4 -4
- package/dist/{chunk-JO7JWV34.js.map → chunk-U55ZJC2X.js.map} +1 -1
- package/dist/{chunk-SX5MF2YA.js → chunk-WSDG4ZRI.js} +4 -4
- package/dist/{chunk-SX5MF2YA.js.map → chunk-WSDG4ZRI.js.map} +1 -1
- package/dist/{chunk-NZVR4OF7.js → chunk-X54QP332.js} +8 -8
- package/dist/chunk-X54QP332.js.map +1 -0
- package/dist/components/cart/index.js +1 -1
- package/dist/components/common/index.js +1 -1
- package/dist/components/drawer/index.js +9 -7
- package/dist/components/home/index.js +11 -9
- package/dist/components/layout/PageAdvertisements.d.ts +7 -0
- package/dist/components/layout/PageAdvertisements.d.ts.map +1 -0
- package/dist/components/layout/index.d.ts +1 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/components/layout/index.js +8 -1
- package/dist/components/product/index.js +10 -8
- package/dist/components/store/index.js +11 -9
- package/dist/components/store/index.js.map +1 -1
- package/dist/{document-class-67HTP45G.js → document-class-7AXAAWGZ.js} +6 -5
- package/dist/{document-class-67HTP45G.js.map → document-class-7AXAAWGZ.js.map} +1 -1
- package/dist/hooks/index.js +9 -7
- package/dist/hooks/useGtm.js +9 -7
- package/dist/hooks/useMoney.js +9 -7
- package/dist/hooks/useStore.js +9 -7
- package/dist/index.js +9 -7
- package/dist/index.js.map +1 -1
- package/dist/{not-found-QTHBFPNT.js → not-found-3EEMQZMU.js} +11 -9
- package/dist/not-found-3EEMQZMU.js.map +1 -0
- package/dist/providers/index.js +9 -7
- package/dist/routes/account/orders.js +1 -1
- package/dist/routes/account.js +6 -6
- package/dist/routes/blog.js +5 -5
- package/dist/routes/brands.js +1 -1
- package/dist/routes/cart.js +13 -11
- package/dist/routes/cart.js.map +1 -1
- package/dist/routes/home.js +12 -10
- package/dist/routes/loyalty.js +2 -2
- package/dist/routes/page.js +1 -1
- package/dist/routes/product-listing.js +11 -9
- package/dist/routes/product-listing.js.map +1 -1
- package/dist/routes/product.js +11 -9
- package/dist/routes/product.js.map +1 -1
- package/dist/routes/root/beforeLoad.d.ts +2 -2
- package/dist/routes/root/beforeLoad.d.ts.map +1 -1
- package/dist/routes/root/head.d.ts.map +1 -1
- package/dist/routes/testimonials.js +2 -2
- package/dist/routes/thank-you.js +1 -1
- package/dist/skeleton.js +12 -10
- package/dist/tanstack/document-class.d.ts.map +1 -1
- package/dist/tanstack/index.d.ts +1 -0
- package/dist/tanstack/index.d.ts.map +1 -1
- package/dist/tanstack/stylesheet.d.ts +15 -0
- package/dist/tanstack/stylesheet.d.ts.map +1 -0
- package/dist/tanstack.js +34 -5
- package/dist/tanstack.js.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +12 -3
- package/tailwind/components/Component.tsx.txt +21 -0
- package/tailwind/components/ComponentRegistry.ts.txt +66 -0
- package/tailwind/components/cart/CartItem.tsx.txt +211 -0
- package/tailwind/components/cart/CartSummary.tsx.txt +269 -0
- package/tailwind/components/cart/PreOrderNotification.tsx.txt +16 -0
- package/tailwind/components/cart/PriceQuoteButton.tsx.txt +21 -0
- package/tailwind/components/cart/SeoCartWidget.tsx.txt +99 -0
- package/tailwind/components/cart/index.ts.txt +11 -0
- package/tailwind/components/cart/lazy.ts.txt +15 -0
- package/tailwind/components/collapse/Collapse.tsx.txt +95 -0
- package/tailwind/components/collapse/index.ts.txt +41 -0
- package/tailwind/components/common/Breadcrumb.tsx.txt +27 -0
- package/tailwind/components/common/BreadcrumbJsonLd.tsx.txt +40 -0
- package/tailwind/components/common/BreadcrumbTrail.tsx.txt +97 -0
- package/tailwind/components/common/CurrencySymbol.tsx.txt +8 -0
- package/tailwind/components/common/DeferredData.tsx.txt +122 -0
- package/tailwind/components/common/ErrorPage.tsx.txt +69 -0
- package/tailwind/components/common/Image.tsx.txt +119 -0
- package/tailwind/components/common/Link.tsx.txt +36 -0
- package/tailwind/components/common/Link.types.ts.txt +85 -0
- package/tailwind/components/common/NoContent.tsx.txt +27 -0
- package/tailwind/components/common/RenderWhenVisible.tsx.txt +92 -0
- package/tailwind/components/common/index.ts.txt +42 -0
- package/tailwind/components/common/lazy.ts.txt +28 -0
- package/tailwind/components/defineComponent.ts.txt +32 -0
- package/tailwind/components/drawer/Drawer.tsx.txt +219 -0
- package/tailwind/components/drawer/index.ts.txt +38 -0
- package/tailwind/components/dropdown/Dropdown.tsx.txt +166 -0
- package/tailwind/components/dropdown/index.ts.txt +40 -0
- package/tailwind/components/home/BundleComponent.tsx.txt +35 -0
- package/tailwind/components/home/ComponentErrorBoundary.tsx.txt +98 -0
- package/tailwind/components/home/FeaturedProductsStyle1.tsx.txt +125 -0
- package/tailwind/components/home/FeaturedProductsStyle2.tsx.txt +127 -0
- package/tailwind/components/home/FeaturedProductsStyle3.tsx.txt +71 -0
- package/tailwind/components/home/FixedBanner.tsx.txt +68 -0
- package/tailwind/components/home/FixedProducts.tsx.txt +72 -0
- package/tailwind/components/home/HomePageRenderer.tsx.txt +284 -0
- package/tailwind/components/home/PageBlocks.tsx.txt +24 -0
- package/tailwind/components/home/ParallaxBackground.tsx.txt +50 -0
- package/tailwind/components/home/PhotosSlider.tsx.txt +61 -0
- package/tailwind/components/home/ProductsSlider.tsx.txt +87 -0
- package/tailwind/components/home/ReorderProducts.tsx.txt +38 -0
- package/tailwind/components/home/SquarePhotos.tsx.txt +68 -0
- package/tailwind/components/home/StoreFeatures.tsx.txt +34 -0
- package/tailwind/components/home/Testimonials.tsx.txt +36 -0
- package/tailwind/components/home/Youtube.tsx.txt +48 -0
- package/tailwind/components/home/index.ts.txt +50 -0
- package/tailwind/components/home/lazy.ts.txt +17 -0
- package/tailwind/components/home/register.ts.txt +44 -0
- package/tailwind/components/home/reorder-hooks.ts.txt +52 -0
- package/tailwind/components/home/types.ts.txt +4 -0
- package/tailwind/components/layout/Copyright.tsx.txt +23 -0
- package/tailwind/components/layout/CustomerLayout.tsx.txt +91 -0
- package/tailwind/components/layout/Footer.tsx.txt +343 -0
- package/tailwind/components/layout/Header.tsx.txt +154 -0
- package/tailwind/components/layout/MasterLayout.tsx.txt +63 -0
- package/tailwind/components/layout/PageAdvertisements.tsx.txt +131 -0
- package/tailwind/components/layout/WidgetHead.tsx.txt +53 -0
- package/tailwind/components/layout/index.ts.txt +27 -0
- package/tailwind/components/layout/lazy.ts.txt +25 -0
- package/tailwind/components/layout/types.ts.txt +11 -0
- package/tailwind/components/modal/ImageModal.tsx.txt +70 -0
- package/tailwind/components/modal/Modal.tsx.txt +245 -0
- package/tailwind/components/modal/index.ts.txt +39 -0
- package/tailwind/components/modal/lazy.ts.txt +10 -0
- package/tailwind/components/navigation/MainMenu.tsx.txt +94 -0
- package/tailwind/components/navigation/MobileMenu.tsx.txt +160 -0
- package/tailwind/components/navigation/index.ts.txt +4 -0
- package/tailwind/components/navigation/lazy.ts.txt +11 -0
- package/tailwind/components/product/AddToCartForm.tsx.txt +365 -0
- package/tailwind/components/product/ProductCard.tsx.txt +305 -0
- package/tailwind/components/product/ProductDetails.tsx.txt +267 -0
- package/tailwind/components/product/ProductGallery.tsx.txt +234 -0
- package/tailwind/components/product/index.ts.txt +15 -0
- package/tailwind/components/product/lazy.ts.txt +9 -0
- package/tailwind/components/snippets/AppsSnippets.tsx.txt +225 -0
- package/tailwind/components/snippets/SallaSdkScripts.tsx.txt +14 -0
- package/tailwind/components/snippets/index.ts.txt +4 -0
- package/tailwind/components/store/StoreClosedNotification.tsx.txt +73 -0
- package/tailwind/components/store/index.ts.txt +8 -0
- package/tailwind/components/store/lazy.ts.txt +8 -0
- package/tailwind/components/toast/Toaster.tsx.txt +177 -0
- package/tailwind/components/toast/index.ts.txt +10 -0
- package/tailwind/components/toast/types.ts.txt +38 -0
- package/tailwind/components/toast/useToast.ts.txt +61 -0
- package/tailwind/components/tracking/GoogleTag.tsx.txt +77 -0
- package/tailwind/components/tracking/SiftSnippet.tsx.txt +49 -0
- package/tailwind/components/tracking/TwilightBundles.tsx.txt +67 -0
- package/tailwind/components/tracking/index.ts.txt +11 -0
- package/tailwind/components/tracking/lazy.ts.txt +11 -0
- package/tailwind/components/useComponent.ts.txt +12 -0
- package/tailwind/routes/account/index.tsx.txt +16 -0
- package/tailwind/routes/account/notifications/NotificationsPage.tsx.txt +54 -0
- package/tailwind/routes/account/notifications/index.tsx.txt +39 -0
- package/tailwind/routes/account/notifications/loader.ts.txt +22 -0
- package/tailwind/routes/account/notifications/types.ts.txt +18 -0
- package/tailwind/routes/account/orders/OrderSinglePage.tsx.txt +181 -0
- package/tailwind/routes/account/orders/OrdersPage.tsx.txt +28 -0
- package/tailwind/routes/account/orders/index.tsx.txt +86 -0
- package/tailwind/routes/account/orders/loader.ts.txt +66 -0
- package/tailwind/routes/account/orders/single/OrderActions.tsx.txt +197 -0
- package/tailwind/routes/account/orders/single/OrderHeader.tsx.txt +143 -0
- package/tailwind/routes/account/orders/single/OrderItemRow.tsx.txt +209 -0
- package/tailwind/routes/account/orders/single/OrderSections.tsx.txt +215 -0
- package/tailwind/routes/account/orders/single/types.ts.txt +38 -0
- package/tailwind/routes/account/orders/types.ts.txt +29 -0
- package/tailwind/routes/account/profile/ProfilePage.tsx.txt +30 -0
- package/tailwind/routes/account/profile/index.tsx.txt +39 -0
- package/tailwind/routes/account/profile/loader.ts.txt +16 -0
- package/tailwind/routes/account/profile/types.ts.txt +5 -0
- package/tailwind/routes/account/settings/SettingsPage.tsx.txt +13 -0
- package/tailwind/routes/account/settings/index.tsx.txt +39 -0
- package/tailwind/routes/account/settings/loader.ts.txt +13 -0
- package/tailwind/routes/account/settings/types.ts.txt +5 -0
- package/tailwind/routes/account/types.ts.txt +320 -0
- package/tailwind/routes/account/wallet/WalletPage.tsx.txt +195 -0
- package/tailwind/routes/account/wallet/index.tsx.txt +38 -0
- package/tailwind/routes/account/wallet/loader.ts.txt +27 -0
- package/tailwind/routes/account/wallet/types.ts.txt +11 -0
- package/tailwind/routes/account/wishlist/WishlistPage.tsx.txt +314 -0
- package/tailwind/routes/account/wishlist/WishlistShare.tsx.txt +248 -0
- package/tailwind/routes/account/wishlist/index.tsx.txt +40 -0
- package/tailwind/routes/account/wishlist/loader.ts.txt +26 -0
- package/tailwind/routes/account/wishlist/types.ts.txt +9 -0
- package/tailwind/routes/blog/BlogAuthorPage.tsx.txt +82 -0
- package/tailwind/routes/blog/BlogCategoryPage.tsx.txt +84 -0
- package/tailwind/routes/blog/BlogPage.tsx.txt +191 -0
- package/tailwind/routes/blog/BlogSinglePage.tsx.txt +168 -0
- package/tailwind/routes/blog/BlogTagPage.tsx.txt +84 -0
- package/tailwind/routes/blog/BlogTags.tsx.txt +54 -0
- package/tailwind/routes/blog/components.tsx.txt +87 -0
- package/tailwind/routes/blog/index.tsx.txt +238 -0
- package/tailwind/routes/blog/loader.ts.txt +173 -0
- package/tailwind/routes/blog/types.ts.txt +106 -0
- package/tailwind/routes/brands/BrandsPage.tsx.txt +179 -0
- package/tailwind/routes/brands/index.tsx.txt +47 -0
- package/tailwind/routes/brands/loader.ts.txt +14 -0
- package/tailwind/routes/brands/types.ts.txt +53 -0
- package/tailwind/routes/cart/CartPage.tsx.txt +112 -0
- package/tailwind/routes/cart/index.tsx.txt +39 -0
- package/tailwind/routes/cart/loader.ts.txt +15 -0
- package/tailwind/routes/cart/types.ts.txt +6 -0
- package/tailwind/routes/home/HomePage.tsx.txt +53 -0
- package/tailwind/routes/home/index.tsx.txt +56 -0
- package/tailwind/routes/home/loader.ts.txt +24 -0
- package/tailwind/routes/home/types.ts.txt +11 -0
- package/tailwind/routes/index.ts.txt +94 -0
- package/tailwind/routes/lazy.ts.txt +105 -0
- package/tailwind/routes/loyalty/LoyaltyPage.tsx.txt +17 -0
- package/tailwind/routes/loyalty/LoyaltyPointsPage.tsx.txt +11 -0
- package/tailwind/routes/loyalty/index.tsx.txt +81 -0
- package/tailwind/routes/loyalty/loader.ts.txt +53 -0
- package/tailwind/routes/loyalty/types.ts.txt +109 -0
- package/tailwind/routes/not-found.tsx.txt +50 -0
- package/tailwind/routes/page/PageSingle.tsx.txt +34 -0
- package/tailwind/routes/page/index.tsx.txt +60 -0
- package/tailwind/routes/page/loader.ts.txt +19 -0
- package/tailwind/routes/page/types.ts.txt +34 -0
- package/tailwind/routes/product/ProductPage.tsx.txt +77 -0
- package/tailwind/routes/product/index.tsx.txt +157 -0
- package/tailwind/routes/product/loader.ts.txt +35 -0
- package/tailwind/routes/product/types.ts.txt +6 -0
- package/tailwind/routes/product-listing/ProductListPage.tsx.txt +321 -0
- package/tailwind/routes/product-listing/index.tsx.txt +95 -0
- package/tailwind/routes/product-listing/loader.ts.txt +323 -0
- package/tailwind/routes/product-listing/types.ts.txt +96 -0
- package/tailwind/routes/product-listing/useAppliedFilters.ts.txt +130 -0
- package/tailwind/routes/product-short-link.ts.txt +104 -0
- package/tailwind/routes/root/beforeLoad.ts.txt +354 -0
- package/tailwind/routes/root/head.ts.txt +424 -0
- package/tailwind/routes/root/index.ts.txt +2 -0
- package/tailwind/routes/seo-redirects.ts.txt +82 -0
- package/tailwind/routes/testimonials/ReviewsPage.tsx.txt +12 -0
- package/tailwind/routes/testimonials/TestimonialsPage.tsx.txt +80 -0
- package/tailwind/routes/testimonials/index.tsx.txt +80 -0
- package/tailwind/routes/testimonials/loader.ts.txt +29 -0
- package/tailwind/routes/testimonials/types.ts.txt +9 -0
- package/tailwind/routes/thank-you/ThankYouPage.tsx.txt +241 -0
- package/tailwind/routes/thank-you/index.tsx.txt +49 -0
- package/tailwind/routes/thank-you/loader.ts.txt +40 -0
- package/tailwind/routes/thank-you/types.ts.txt +16 -0
- package/tailwind/routes/types.ts.txt +41 -0
- package/dist/BlogAuthorPage-GTF4UGSS.js.map +0 -1
- package/dist/BlogCategoryPage-TN7Y4NBP.js.map +0 -1
- package/dist/BlogPage-TDB3M3PI.js.map +0 -1
- package/dist/BlogSinglePage-4A52Q5QW.js.map +0 -1
- package/dist/BlogTagPage-TOVPBETA.js.map +0 -1
- package/dist/BrandsPage-VUDGYSQI.js.map +0 -1
- package/dist/Breadcrumb-CI3SN5YR.js.map +0 -1
- package/dist/CartItem-BHLEURAB.js.map +0 -1
- package/dist/CartSummary-QERVESYN.js.map +0 -1
- package/dist/CustomerLayout-VRZUWGQK.js.map +0 -1
- package/dist/LoyaltyPage-CGTHWJ76.js.map +0 -1
- package/dist/LoyaltyPointsPage-QSU6K3PW.js.map +0 -1
- package/dist/MobileMenu-U4BVQYKH.js.map +0 -1
- package/dist/NotificationsPage-ZRRQY7TF.js.map +0 -1
- package/dist/OrderSinglePage-O3XYVMG5.js.map +0 -1
- package/dist/OrdersPage-CMXZF5MI.js.map +0 -1
- package/dist/ProductDetails-65GDABI7.js.map +0 -1
- package/dist/ProfilePage-SNJ4WXU4.js.map +0 -1
- package/dist/SeoCartWidget-DBJAXJKT.js.map +0 -1
- package/dist/SettingsPage-FLTIOPU5.js.map +0 -1
- package/dist/ThankYouPage-5X6B4X3T.js.map +0 -1
- package/dist/WalletPage-H2BKIDPA.js.map +0 -1
- package/dist/WishlistPage-RS5UCIQ3.js.map +0 -1
- package/dist/chunk-7WAXN2PP.js +0 -14
- package/dist/chunk-NZVR4OF7.js.map +0 -1
- package/dist/chunk-SDFDS4Q3.js.map +0 -1
- 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,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';
|