@salla.sa/twilight-theme-engine 1.0.85 → 1.0.87
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/README.md +4 -3
- package/bin/twilight-check-manifest.mjs +16 -0
- package/dist/{AddToCartForm-B5C2M4OZ.js → AddToCartForm-P644O2QL.js} +4 -4
- package/dist/{AddToCartForm-B5C2M4OZ.js.map → AddToCartForm-P644O2QL.js.map} +1 -1
- package/dist/{BlogAuthorPage-GTF4UGSS.js → BlogAuthorPage-VWSO6MPY.js} +13 -11
- package/dist/BlogAuthorPage-VWSO6MPY.js.map +1 -0
- package/dist/{BlogCategoryPage-TN7Y4NBP.js → BlogCategoryPage-K2KSQ6T6.js} +13 -11
- package/dist/BlogCategoryPage-K2KSQ6T6.js.map +1 -0
- package/dist/{BlogPage-TDB3M3PI.js → BlogPage-XRM7VWOZ.js} +12 -10
- package/dist/BlogPage-XRM7VWOZ.js.map +1 -0
- package/dist/{BlogSinglePage-4A52Q5QW.js → BlogSinglePage-XIAXT54H.js} +12 -10
- package/dist/BlogSinglePage-XIAXT54H.js.map +1 -0
- package/dist/{BlogTagPage-TOVPBETA.js → BlogTagPage-IHOA267V.js} +13 -11
- package/dist/BlogTagPage-IHOA267V.js.map +1 -0
- package/dist/{BrandsPage-VUDGYSQI.js → BrandsPage-ZFEILRT3.js} +12 -10
- package/dist/BrandsPage-ZFEILRT3.js.map +1 -0
- package/dist/{Breadcrumb-CI3SN5YR.js → Breadcrumb-CVSISL2K.js} +12 -10
- package/dist/Breadcrumb-CVSISL2K.js.map +1 -0
- package/dist/{CartItem-BHLEURAB.js → CartItem-EOI7P3JA.js} +12 -10
- package/dist/CartItem-EOI7P3JA.js.map +1 -0
- package/dist/{CartSummary-QERVESYN.js → CartSummary-RZGUBDKJ.js} +12 -10
- package/dist/CartSummary-RZGUBDKJ.js.map +1 -0
- package/dist/{Copyright-RL6ALKRC.js → Copyright-5BLYSP24.js} +4 -4
- package/dist/{Copyright-RL6ALKRC.js.map → Copyright-5BLYSP24.js.map} +1 -1
- package/dist/{CustomerLayout-VRZUWGQK.js → CustomerLayout-6F4BJAMU.js} +12 -10
- package/dist/CustomerLayout-6F4BJAMU.js.map +1 -0
- package/dist/{Footer-ZWBZCMDL.js → Footer-NF2NLDLR.js} +6 -6
- package/dist/{Footer-ZWBZCMDL.js.map → Footer-NF2NLDLR.js.map} +1 -1
- package/dist/{Header-WNK4O66B.js → Header-6DQJ5X7J.js} +9 -7
- package/dist/{Header-WNK4O66B.js.map → Header-6DQJ5X7J.js.map} +1 -1
- package/dist/{LoyaltyPage-CGTHWJ76.js → LoyaltyPage-YHWHIWCS.js} +12 -10
- package/dist/LoyaltyPage-YHWHIWCS.js.map +1 -0
- package/dist/{LoyaltyPointsPage-QSU6K3PW.js → LoyaltyPointsPage-T6K3H5A5.js} +10 -3
- package/dist/LoyaltyPointsPage-T6K3H5A5.js.map +1 -0
- package/dist/{MainMenu-V3T7F32R.js → MainMenu-Y2TAM7DK.js} +4 -4
- package/dist/{MainMenu-V3T7F32R.js.map → MainMenu-Y2TAM7DK.js.map} +1 -1
- package/dist/{MasterLayout-XQF644LD.js → MasterLayout-GBEEFFPG.js} +12 -10
- package/dist/{MasterLayout-XQF644LD.js.map → MasterLayout-GBEEFFPG.js.map} +1 -1
- package/dist/{MobileMenu-U4BVQYKH.js → MobileMenu-RE5XVGZL.js} +12 -10
- package/dist/MobileMenu-RE5XVGZL.js.map +1 -0
- package/dist/{NotificationsPage-ZRRQY7TF.js → NotificationsPage-QAFWZAS3.js} +12 -10
- package/dist/NotificationsPage-QAFWZAS3.js.map +1 -0
- package/dist/{OrderSinglePage-O3XYVMG5.js → OrderSinglePage-T4PRXTKG.js} +12 -10
- package/dist/OrderSinglePage-T4PRXTKG.js.map +1 -0
- package/dist/{OrdersPage-CMXZF5MI.js → OrdersPage-3S6ZS56E.js} +12 -10
- package/dist/OrdersPage-3S6ZS56E.js.map +1 -0
- package/dist/{PageSingle-IIG2US43.js → PageSingle-W3UTXDOW.js} +4 -4
- package/dist/{PageSingle-IIG2US43.js.map → PageSingle-W3UTXDOW.js.map} +1 -1
- package/dist/{ProductDetails-65GDABI7.js → ProductDetails-X5A7TG3F.js} +14 -7
- package/dist/ProductDetails-X5A7TG3F.js.map +1 -0
- package/dist/{ProfilePage-SNJ4WXU4.js → ProfilePage-DUH3AILJ.js} +12 -10
- package/dist/ProfilePage-DUH3AILJ.js.map +1 -0
- package/dist/{ReviewsPage-UT7UT55J.js → ReviewsPage-2C2P7VS3.js} +4 -4
- package/dist/{ReviewsPage-UT7UT55J.js.map → ReviewsPage-2C2P7VS3.js.map} +1 -1
- package/dist/{SeoCartWidget-DBJAXJKT.js → SeoCartWidget-VYLGEW2I.js} +12 -10
- package/dist/SeoCartWidget-VYLGEW2I.js.map +1 -0
- package/dist/{SettingsPage-FLTIOPU5.js → SettingsPage-OZJEW6CW.js} +12 -10
- package/dist/SettingsPage-OZJEW6CW.js.map +1 -0
- package/dist/{StoreClosedNotification-ZT3QKDKS.js → StoreClosedNotification-QHIZBNXO.js} +13 -11
- package/dist/{StoreClosedNotification-ZT3QKDKS.js.map → StoreClosedNotification-QHIZBNXO.js.map} +1 -1
- package/dist/{TestimonialsPage-CU7HKU4O.js → TestimonialsPage-K6WJKKME.js} +4 -4
- package/dist/{TestimonialsPage-CU7HKU4O.js.map → TestimonialsPage-K6WJKKME.js.map} +1 -1
- package/dist/{ThankYouPage-5X6B4X3T.js → ThankYouPage-H6QQEI5R.js} +12 -10
- package/dist/ThankYouPage-H6QQEI5R.js.map +1 -0
- package/dist/{WalletPage-H2BKIDPA.js → WalletPage-G5IBGN6B.js} +12 -10
- package/dist/WalletPage-G5IBGN6B.js.map +1 -0
- package/dist/{WishlistPage-RS5UCIQ3.js → WishlistPage-AIOTAB6L.js} +12 -10
- package/dist/WishlistPage-AIOTAB6L.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-4FJGZQD3.js +6 -0
- package/dist/chunk-4FJGZQD3.js.map +1 -0
- package/dist/{chunk-SDFDS4Q3.js → chunk-BCJTF3V7.js} +20 -4
- package/dist/chunk-BCJTF3V7.js.map +1 -0
- package/dist/chunk-DBW7XXNE.js +103 -0
- package/dist/chunk-DBW7XXNE.js.map +1 -0
- 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-SE5JYWOK.js → chunk-EZHXVHXY.js} +4 -4
- package/dist/{chunk-SE5JYWOK.js.map → chunk-EZHXVHXY.js.map} +1 -1
- package/dist/{chunk-JO7JWV34.js → chunk-FWYKOQV5.js} +5 -5
- package/dist/chunk-FWYKOQV5.js.map +1 -0
- package/dist/{chunk-JRNH3FUF.js → chunk-FYHF62KI.js} +4 -4
- package/dist/{chunk-JRNH3FUF.js.map → chunk-FYHF62KI.js.map} +1 -1
- package/dist/{chunk-UJKFBZFJ.js → chunk-GJ5IMPNY.js} +7 -7
- package/dist/{chunk-UJKFBZFJ.js.map → chunk-GJ5IMPNY.js.map} +1 -1
- package/dist/chunk-IVBOLREE.js +29 -0
- package/dist/chunk-IVBOLREE.js.map +1 -0
- package/dist/chunk-LWPKBVIE.js +14 -0
- package/dist/{chunk-7WAXN2PP.js.map → chunk-LWPKBVIE.js.map} +1 -1
- package/dist/{chunk-SX5MF2YA.js → chunk-N3BCZIGS.js} +4 -4
- package/dist/{chunk-SX5MF2YA.js.map → chunk-N3BCZIGS.js.map} +1 -1
- package/dist/{chunk-NZVR4OF7.js → chunk-NXQZ4RLM.js} +8 -8
- package/dist/chunk-NXQZ4RLM.js.map +1 -0
- package/dist/{chunk-NXQJA7D3.js → chunk-PCURRKIH.js} +3 -3
- package/dist/{chunk-NXQJA7D3.js.map → chunk-PCURRKIH.js.map} +1 -1
- package/dist/{chunk-4CZRUAZB.js → chunk-PU2LHFDQ.js} +10 -11
- package/dist/chunk-PU2LHFDQ.js.map +1 -0
- package/dist/{chunk-OWX2CPNQ.js → chunk-UQIVX4YA.js} +3 -3
- package/dist/{chunk-OWX2CPNQ.js.map → chunk-UQIVX4YA.js.map} +1 -1
- package/dist/chunk-X5NK4TZH.js +228 -0
- package/dist/chunk-X5NK4TZH.js.map +1 -0
- package/dist/{chunk-6SWDSRIL.js → chunk-XKNXYQ2K.js} +6 -6
- package/dist/{chunk-6SWDSRIL.js.map → chunk-XKNXYQ2K.js.map} +1 -1
- package/dist/{chunk-KHBSGD2D.js → chunk-XO7B3VWS.js} +3 -3
- package/dist/{chunk-KHBSGD2D.js.map → chunk-XO7B3VWS.js.map} +1 -1
- package/dist/cli/check-manifest.d.ts +10 -0
- package/dist/cli/check-manifest.d.ts.map +1 -0
- package/dist/cli/check-manifest.js +44 -0
- package/dist/cli/check-manifest.js.map +1 -0
- package/dist/components/cart/index.js +1 -1
- package/dist/components/common/index.js +2 -2
- package/dist/components/drawer/index.js +10 -8
- package/dist/components/home/index.js +12 -10
- package/dist/components/layout/Footer.d.ts.map +1 -1
- package/dist/components/layout/MasterLayout.d.ts.map +1 -1
- 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 +11 -9
- package/dist/components/store/index.js +12 -10
- package/dist/components/store/index.js.map +1 -1
- package/dist/{document-class-67HTP45G.js → document-class-FRL7JHWD.js} +6 -5
- package/dist/{document-class-67HTP45G.js.map → document-class-FRL7JHWD.js.map} +1 -1
- package/dist/hooks/index.js +10 -8
- package/dist/hooks/useGtm.js +10 -8
- package/dist/hooks/useMoney.js +10 -8
- package/dist/hooks/useStore.js +10 -8
- package/dist/index.js +10 -8
- package/dist/index.js.map +1 -1
- package/dist/{not-found-QTHBFPNT.js → not-found-E6AR4CZ4.js} +12 -10
- package/dist/not-found-E6AR4CZ4.js.map +1 -0
- package/dist/providers/index.js +10 -8
- 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 +14 -12
- package/dist/routes/cart.js.map +1 -1
- package/dist/routes/home.js +13 -11
- package/dist/routes/loyalty.js +2 -2
- package/dist/routes/page.js +1 -1
- package/dist/routes/product-listing.js +12 -10
- package/dist/routes/product-listing.js.map +1 -1
- package/dist/routes/product.js +12 -10
- 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 +13 -11
- 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 +33 -4
- package/dist/tanstack.js.map +1 -1
- package/dist/utils/index.js +2 -2
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +14 -25
- package/dist/vite/index.js.map +1 -1
- package/dist/vite/plugins/twilight-manifest.d.ts +23 -0
- package/dist/vite/plugins/twilight-manifest.d.ts.map +1 -1
- package/dist/vite/plugins/twilight-schema.plugin.d.ts.map +1 -1
- package/package.json +16 -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 +341 -0
- package/tailwind/components/layout/Header.tsx.txt +154 -0
- package/tailwind/components/layout/MasterLayout.tsx.txt +62 -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-4CZRUAZB.js.map +0 -1
- package/dist/chunk-7WAXN2PP.js +0 -14
- package/dist/chunk-JO7JWV34.js.map +0 -1
- 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,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
|
+
}
|