@salla.sa/twilight-theme-engine 1.0.84 → 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-JUMFLSKN.js → AddToCartForm-5X2WZDWR.js} +3 -3
- package/dist/{AddToCartForm-JUMFLSKN.js.map → AddToCartForm-5X2WZDWR.js.map} +1 -1
- package/dist/{BlogAuthorPage-LFAUMKC7.js → BlogAuthorPage-LO7FSSJR.js} +12 -10
- package/dist/BlogAuthorPage-LO7FSSJR.js.map +1 -0
- package/dist/{BlogCategoryPage-NWYFFPCD.js → BlogCategoryPage-KKFQ3PGR.js} +12 -10
- package/dist/BlogCategoryPage-KKFQ3PGR.js.map +1 -0
- package/dist/{BlogPage-BOHXFV7E.js → BlogPage-VLF3NSGE.js} +11 -9
- package/dist/BlogPage-VLF3NSGE.js.map +1 -0
- package/dist/{BlogSinglePage-FYEXQPMB.js → BlogSinglePage-AVK535IW.js} +11 -9
- package/dist/BlogSinglePage-AVK535IW.js.map +1 -0
- package/dist/{BlogTagPage-DAM3RNVA.js → BlogTagPage-F3T4D7RH.js} +12 -10
- package/dist/BlogTagPage-F3T4D7RH.js.map +1 -0
- package/dist/{BrandsPage-GAKQCE6O.js → BrandsPage-LVSBZSVC.js} +11 -9
- package/dist/BrandsPage-LVSBZSVC.js.map +1 -0
- package/dist/{Breadcrumb-NAYSZNRV.js → Breadcrumb-VLFTXRFC.js} +11 -9
- package/dist/Breadcrumb-VLFTXRFC.js.map +1 -0
- package/dist/{CartItem-ZRVLIZT3.js → CartItem-G3H34GW6.js} +11 -9
- package/dist/CartItem-G3H34GW6.js.map +1 -0
- package/dist/{CartSummary-U2TVLG35.js → CartSummary-VH37RPUI.js} +11 -9
- package/dist/CartSummary-VH37RPUI.js.map +1 -0
- package/dist/{Copyright-2LSBADZD.js → Copyright-HE5ZWINZ.js} +3 -3
- package/dist/{Copyright-2LSBADZD.js.map → Copyright-HE5ZWINZ.js.map} +1 -1
- package/dist/{CustomerLayout-WSKXWI7D.js → CustomerLayout-LKBLOAVW.js} +11 -9
- package/dist/CustomerLayout-LKBLOAVW.js.map +1 -0
- package/dist/{Footer-ONNRJU4G.js → Footer-HHXAST72.js} +5 -5
- package/dist/{Footer-ONNRJU4G.js.map → Footer-HHXAST72.js.map} +1 -1
- package/dist/{Header-C6VXLLUC.js → Header-GEVZM2YG.js} +8 -6
- package/dist/{Header-C6VXLLUC.js.map → Header-GEVZM2YG.js.map} +1 -1
- package/dist/{LoyaltyPage-IG5KSYNO.js → LoyaltyPage-M5CT3CFD.js} +11 -9
- package/dist/LoyaltyPage-M5CT3CFD.js.map +1 -0
- package/dist/{LoyaltyPointsPage-RKO56ZER.js → LoyaltyPointsPage-SDJB52U6.js} +10 -3
- package/dist/LoyaltyPointsPage-SDJB52U6.js.map +1 -0
- package/dist/{MainMenu-USOYTKPU.js → MainMenu-I7EYS752.js} +3 -3
- package/dist/{MainMenu-USOYTKPU.js.map → MainMenu-I7EYS752.js.map} +1 -1
- package/dist/{MasterLayout-2W24ACEK.js → MasterLayout-QDK4T3NE.js} +11 -9
- package/dist/{MasterLayout-2W24ACEK.js.map → MasterLayout-QDK4T3NE.js.map} +1 -1
- package/dist/{MobileMenu-E4X2GOZ3.js → MobileMenu-7VTCWAVJ.js} +11 -9
- package/dist/MobileMenu-7VTCWAVJ.js.map +1 -0
- package/dist/{NotificationsPage-BJ5XVWEG.js → NotificationsPage-QCABE6QD.js} +11 -9
- package/dist/NotificationsPage-QCABE6QD.js.map +1 -0
- package/dist/{OrderSinglePage-544E36FJ.js → OrderSinglePage-2HVBKBG4.js} +11 -9
- package/dist/OrderSinglePage-2HVBKBG4.js.map +1 -0
- package/dist/{OrdersPage-AFBWVTZU.js → OrdersPage-5MNCPCM3.js} +11 -9
- package/dist/OrdersPage-5MNCPCM3.js.map +1 -0
- package/dist/{PageSingle-H2EJIL74.js → PageSingle-EN5XIM2B.js} +3 -3
- package/dist/{PageSingle-H2EJIL74.js.map → PageSingle-EN5XIM2B.js.map} +1 -1
- package/dist/{ProductDetails-SDZHALU6.js → ProductDetails-UEWQPPDZ.js} +13 -6
- package/dist/ProductDetails-UEWQPPDZ.js.map +1 -0
- package/dist/{ProfilePage-V5KV6GQK.js → ProfilePage-TZSU3GHZ.js} +11 -9
- package/dist/ProfilePage-TZSU3GHZ.js.map +1 -0
- package/dist/{ReviewsPage-CXKJABXP.js → ReviewsPage-WYZ6CHF2.js} +3 -3
- package/dist/{ReviewsPage-CXKJABXP.js.map → ReviewsPage-WYZ6CHF2.js.map} +1 -1
- package/dist/{SeoCartWidget-JAV6M3X5.js → SeoCartWidget-5JR5RXQM.js} +11 -9
- package/dist/SeoCartWidget-5JR5RXQM.js.map +1 -0
- package/dist/{SettingsPage-43SUCLQR.js → SettingsPage-DIDRQXMM.js} +11 -9
- package/dist/SettingsPage-DIDRQXMM.js.map +1 -0
- package/dist/{StoreClosedNotification-MINBJOGZ.js → StoreClosedNotification-C2G32E34.js} +12 -10
- package/dist/{StoreClosedNotification-MINBJOGZ.js.map → StoreClosedNotification-C2G32E34.js.map} +1 -1
- package/dist/{TestimonialsPage-RLLIDLOA.js → TestimonialsPage-XPHFRBZ7.js} +3 -3
- package/dist/{TestimonialsPage-RLLIDLOA.js.map → TestimonialsPage-XPHFRBZ7.js.map} +1 -1
- package/dist/{ThankYouPage-52SSZ7FV.js → ThankYouPage-UA6YYFB2.js} +11 -9
- package/dist/ThankYouPage-UA6YYFB2.js.map +1 -0
- package/dist/{WalletPage-4AN4UJ52.js → WalletPage-AJVV7LVL.js} +11 -9
- package/dist/WalletPage-AJVV7LVL.js.map +1 -0
- package/dist/{WishlistPage-L6IYJUJZ.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-R5BQDCDK.js → chunk-2HCCIHU7.js} +4 -4
- package/dist/{chunk-R5BQDCDK.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-UNEZZMQA.js → chunk-4WVJJ7SN.js} +20 -4
- package/dist/chunk-4WVJJ7SN.js.map +1 -0
- package/dist/{chunk-FFWSQJLV.js → chunk-BAYGWTNM.js} +3 -3
- package/dist/{chunk-FFWSQJLV.js.map → chunk-BAYGWTNM.js.map} +1 -1
- package/dist/{chunk-WSRVPR7V.js → chunk-CLBFF2CY.js} +3 -3
- package/dist/{chunk-WSRVPR7V.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-CCUE2FMU.js → chunk-DBXH3EMJ.js} +3 -3
- package/dist/{chunk-CCUE2FMU.js.map → chunk-DBXH3EMJ.js.map} +1 -1
- package/dist/{chunk-MECSOKS7.js → chunk-E26YG3AO.js} +4 -4
- package/dist/{chunk-MECSOKS7.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-IC6XW3PD.js → chunk-HQ7WA37L.js} +7 -7
- package/dist/{chunk-IC6XW3PD.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-RRGEASAL.js → chunk-KP3VYG4F.js} +6 -6
- package/dist/{chunk-RRGEASAL.js.map → chunk-KP3VYG4F.js.map} +1 -1
- package/dist/{chunk-EFF2WO6W.js → chunk-PWTXR4RR.js} +10 -10
- package/dist/{chunk-EFF2WO6W.js.map → chunk-PWTXR4RR.js.map} +1 -1
- package/dist/chunk-QJYUTRAQ.js +14 -0
- package/dist/{chunk-4EA56R6I.js.map → chunk-QJYUTRAQ.js.map} +1 -1
- package/dist/{chunk-YFQUSAET.js → chunk-U55ZJC2X.js} +4 -4
- package/dist/{chunk-YFQUSAET.js.map → chunk-U55ZJC2X.js.map} +1 -1
- package/dist/{chunk-R54GSKJS.js → chunk-WSDG4ZRI.js} +30 -16
- package/dist/chunk-WSDG4ZRI.js.map +1 -0
- package/dist/{chunk-3PWARKZX.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/HomePageRenderer.d.ts +6 -0
- package/dist/components/home/HomePageRenderer.d.ts.map +1 -1
- 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-SD6BUMZN.js → document-class-7AXAAWGZ.js} +6 -5
- package/dist/{document-class-SD6BUMZN.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-TUU7WQLL.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/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.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-LFAUMKC7.js.map +0 -1
- package/dist/BlogCategoryPage-NWYFFPCD.js.map +0 -1
- package/dist/BlogPage-BOHXFV7E.js.map +0 -1
- package/dist/BlogSinglePage-FYEXQPMB.js.map +0 -1
- package/dist/BlogTagPage-DAM3RNVA.js.map +0 -1
- package/dist/BrandsPage-GAKQCE6O.js.map +0 -1
- package/dist/Breadcrumb-NAYSZNRV.js.map +0 -1
- package/dist/CartItem-ZRVLIZT3.js.map +0 -1
- package/dist/CartSummary-U2TVLG35.js.map +0 -1
- package/dist/CustomerLayout-WSKXWI7D.js.map +0 -1
- package/dist/LoyaltyPage-IG5KSYNO.js.map +0 -1
- package/dist/LoyaltyPointsPage-RKO56ZER.js.map +0 -1
- package/dist/MobileMenu-E4X2GOZ3.js.map +0 -1
- package/dist/NotificationsPage-BJ5XVWEG.js.map +0 -1
- package/dist/OrderSinglePage-544E36FJ.js.map +0 -1
- package/dist/OrdersPage-AFBWVTZU.js.map +0 -1
- package/dist/ProductDetails-SDZHALU6.js.map +0 -1
- package/dist/ProfilePage-V5KV6GQK.js.map +0 -1
- package/dist/SeoCartWidget-JAV6M3X5.js.map +0 -1
- package/dist/SettingsPage-43SUCLQR.js.map +0 -1
- package/dist/ThankYouPage-52SSZ7FV.js.map +0 -1
- package/dist/WalletPage-4AN4UJ52.js.map +0 -1
- package/dist/WishlistPage-L6IYJUJZ.js.map +0 -1
- package/dist/chunk-3PWARKZX.js.map +0 -1
- package/dist/chunk-4EA56R6I.js +0 -14
- package/dist/chunk-R54GSKJS.js.map +0 -1
- package/dist/chunk-UNEZZMQA.js.map +0 -1
- package/dist/not-found-TUU7WQLL.js.map +0 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { SallaSlider } from '@salla.sa/twilight-components-react/slider';
|
|
2
|
+
import { Suspense, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { registry } from '../ComponentRegistry';
|
|
4
|
+
import { useTwilight } from '../../providers/TwilightProvider';
|
|
5
|
+
import { useTranslation } from '../../providers/I18nProvider';
|
|
6
|
+
import type { Product, ProductImage } from '../../types';
|
|
7
|
+
import { Image } from '../common/Image';
|
|
8
|
+
import { ImageModal } from '../modal';
|
|
9
|
+
import { isNativeApp } from '../../native-app';
|
|
10
|
+
|
|
11
|
+
const PRODUCT_GALLERY_KEY = 'product:gallery';
|
|
12
|
+
|
|
13
|
+
export interface ProductGalleryProps {
|
|
14
|
+
product: Product;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type ImagePreview = { src: string; alt: string };
|
|
18
|
+
type GalleryMedia = { url: string; type: 'image' | 'video' | '3d-image' };
|
|
19
|
+
|
|
20
|
+
function ProductGalleryContent({ product }: ProductGalleryProps) {
|
|
21
|
+
const { t } = useTranslation();
|
|
22
|
+
const { theme } = useTwilight();
|
|
23
|
+
const sliderId = `details-slider-${product.id}`;
|
|
24
|
+
const objectFit = theme.settings.slider_background_size ?? 'cover';
|
|
25
|
+
|
|
26
|
+
// Clicking an image opens an in-app lightbox instead of navigating to the CDN URL.
|
|
27
|
+
const [preview, setPreview] = useState<ImagePreview | null>(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const raf = requestAnimationFrame(() => {
|
|
31
|
+
const el = document.getElementById(sliderId);
|
|
32
|
+
if (!el?.parentElement) return;
|
|
33
|
+
const { parentElement, nextSibling } = el;
|
|
34
|
+
parentElement.removeChild(el);
|
|
35
|
+
parentElement.insertBefore(el, nextSibling);
|
|
36
|
+
});
|
|
37
|
+
return () => cancelAnimationFrame(raf);
|
|
38
|
+
}, [sliderId]);
|
|
39
|
+
|
|
40
|
+
// The product-details API only returns `image` (singular).
|
|
41
|
+
// Fall back so the main image always renders even without the full gallery array.
|
|
42
|
+
const images: ProductImage[] =
|
|
43
|
+
product.images && product.images.length > 0
|
|
44
|
+
? product.images
|
|
45
|
+
: product.image?.url
|
|
46
|
+
? [product.image]
|
|
47
|
+
: [];
|
|
48
|
+
|
|
49
|
+
const hasManyImages = images.length > 1;
|
|
50
|
+
const nativeGallery = images.flatMap<GalleryMedia>((image) => {
|
|
51
|
+
if (image.three_d_image_url) return [{ url: image.three_d_image_url, type: '3d-image' }];
|
|
52
|
+
if (image.video_url) return [{ url: image.video_url, type: 'video' }];
|
|
53
|
+
return image.url ? [{ url: image.url, type: 'image' }] : [];
|
|
54
|
+
});
|
|
55
|
+
const openNativeGallery = () => {
|
|
56
|
+
if (!isNativeApp()) return false;
|
|
57
|
+
window.salla?.event.dispatch('mobile::gallery.open', { images: nativeGallery });
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="sidebar md:sticky top-24 w-full md:!w-2/4 rtl:ml-8 ltr:mr-8 pb-8 md:pb-16 overflow-hidden shrink-0">
|
|
63
|
+
<SallaSlider
|
|
64
|
+
id={sliderId}
|
|
65
|
+
className="details-slider rounded-md image-slider"
|
|
66
|
+
type="thumbs"
|
|
67
|
+
loop={false}
|
|
68
|
+
autoHeight
|
|
69
|
+
listenToThumbnailsOption
|
|
70
|
+
showThumbsControls={false}
|
|
71
|
+
sliderConfig={{ watchOverflow: true, autoHeight: true }}
|
|
72
|
+
>
|
|
73
|
+
{product.promotion_title && (
|
|
74
|
+
<div className="promotion-title">{product.promotion_title}</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{product.calories && (
|
|
78
|
+
<div className="absolute z-[2] top-4 rtl:left-4 ltr:right-4 bg-white shadow-sm flex flex-col justify-center items-center rounded-full w-20 h-20 md:w-24 md:h-24">
|
|
79
|
+
<span className="text-red-500 text-xl leading-none font-bold">{product.calories}</span>
|
|
80
|
+
<span className="text-xs text-gray-500">
|
|
81
|
+
{t('pages.products.calories', 'Calories')}
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
<div slot="items">
|
|
87
|
+
{images.map((image, index) => (
|
|
88
|
+
<ImageSlide
|
|
89
|
+
key={image.id ?? image.three_d_image_url ?? image.video_url ?? image.url}
|
|
90
|
+
image={image}
|
|
91
|
+
product={product}
|
|
92
|
+
index={index}
|
|
93
|
+
objectFit={objectFit}
|
|
94
|
+
onPreview={setPreview}
|
|
95
|
+
onNativeOpen={openNativeGallery}
|
|
96
|
+
/>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{hasManyImages && (
|
|
101
|
+
<div slot="thumbs">
|
|
102
|
+
{images.map((image, index) => (
|
|
103
|
+
<ThumbSlide
|
|
104
|
+
key={image.id ?? image.three_d_image_url ?? image.video_url ?? image.url}
|
|
105
|
+
image={image}
|
|
106
|
+
index={index}
|
|
107
|
+
/>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</SallaSlider>
|
|
112
|
+
|
|
113
|
+
<Suspense fallback={null}>
|
|
114
|
+
<ImageModal
|
|
115
|
+
isOpen={!!preview}
|
|
116
|
+
onClose={() => setPreview(null)}
|
|
117
|
+
src={preview?.src ?? ''}
|
|
118
|
+
alt={preview?.alt}
|
|
119
|
+
/>
|
|
120
|
+
</Suspense>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function ProductGallery(props: ProductGalleryProps) {
|
|
126
|
+
const Component = useMemo(() => {
|
|
127
|
+
const resolved = registry.resolve<ProductGalleryProps>(PRODUCT_GALLERY_KEY);
|
|
128
|
+
// A theme that seeds the key with this component (register + override) would
|
|
129
|
+
// otherwise recurse into itself when it has not actually overridden anything.
|
|
130
|
+
return resolved === ProductGallery ? null : resolved;
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
if (Component) {
|
|
134
|
+
return <Component {...props} />;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return <ProductGalleryContent {...props} />;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function ImageSlide({
|
|
141
|
+
image,
|
|
142
|
+
product,
|
|
143
|
+
index,
|
|
144
|
+
objectFit,
|
|
145
|
+
onPreview,
|
|
146
|
+
onNativeOpen,
|
|
147
|
+
}: {
|
|
148
|
+
image: ProductImage;
|
|
149
|
+
product: Product;
|
|
150
|
+
index: number;
|
|
151
|
+
objectFit: 'cover' | 'contain';
|
|
152
|
+
onPreview: (preview: ImagePreview) => void;
|
|
153
|
+
onNativeOpen: () => boolean;
|
|
154
|
+
}) {
|
|
155
|
+
const [loaded, setLoaded] = useState(false);
|
|
156
|
+
const isVideo = Boolean(image.video_url);
|
|
157
|
+
|
|
158
|
+
if (image.three_d_image_url) {
|
|
159
|
+
return (
|
|
160
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: model-viewer is an interactive web component.
|
|
161
|
+
<model-viewer
|
|
162
|
+
style={{ minHeight: '500px' }}
|
|
163
|
+
className="swiper-slide model-entry w-full h-full"
|
|
164
|
+
loading="lazy"
|
|
165
|
+
camera-controls
|
|
166
|
+
touch-action="pan-y"
|
|
167
|
+
auto-rotate
|
|
168
|
+
poster={image.url || undefined}
|
|
169
|
+
src={image.three_d_image_url}
|
|
170
|
+
shadow-intensity="1"
|
|
171
|
+
alt={image.alt}
|
|
172
|
+
onClick={onNativeOpen}
|
|
173
|
+
></model-viewer>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<a
|
|
179
|
+
data-fslightbox={`product_${product.id}`}
|
|
180
|
+
data-img-id={image.id}
|
|
181
|
+
data-slid-index={index}
|
|
182
|
+
{...(image.video_url ? { 'data-video-src': image.video_url } : {})}
|
|
183
|
+
data-caption={image.alt}
|
|
184
|
+
data-infinite="false"
|
|
185
|
+
data-type={image.video_url ? 'youtube' : 'image'}
|
|
186
|
+
href={image.video_url || image.url}
|
|
187
|
+
// FsLightbox isn't loaded in this React theme, so the bare href would leave the SPA.
|
|
188
|
+
// Open images in an in-app lightbox; let videos open in a new tab.
|
|
189
|
+
{...(isVideo ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
190
|
+
onClick={(event) => {
|
|
191
|
+
if (onNativeOpen()) {
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (isVideo || !image.url) return;
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
onPreview({ src: image.url, alt: image.alt || product.name });
|
|
198
|
+
}}
|
|
199
|
+
aria-label={product.name}
|
|
200
|
+
className={`swiper-slide magnify-wrapper homeslider__slide ${image.video_url ? 'video-entry' : ''}`}
|
|
201
|
+
>
|
|
202
|
+
<div className={`w-full ${loaded ? '' : 'aspect-square bg-gray-100 animate-pulse'}`}>
|
|
203
|
+
<Image
|
|
204
|
+
id={String(image.id)}
|
|
205
|
+
src={image.url}
|
|
206
|
+
alt={image.alt || product.name}
|
|
207
|
+
className={`h-full w-full object-${objectFit}`}
|
|
208
|
+
aspectRatio="1/1"
|
|
209
|
+
priority={index === 0}
|
|
210
|
+
noWrapper
|
|
211
|
+
onLoad={() => setLoaded(true)}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
</a>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function ThumbSlide({ image, index }: { image: ProductImage; index: number }) {
|
|
219
|
+
return (
|
|
220
|
+
<div
|
|
221
|
+
className={`slide--one-fourth ${image.video_url ? 'video-entry' : ''} ${image.three_d_image_url ? 'model-entry' : ''}`}
|
|
222
|
+
data-caption={image.alt}
|
|
223
|
+
>
|
|
224
|
+
<Image
|
|
225
|
+
src={image.url}
|
|
226
|
+
alt={image.alt || ''}
|
|
227
|
+
className="object-cover w-full h-full bg-gray-100 rounded-md overflow-hidden"
|
|
228
|
+
title={image.alt}
|
|
229
|
+
priority={index < 5}
|
|
230
|
+
noWrapper
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LazyProductDetails, LazyAddToCartForm } from './lazy';
|
|
2
|
+
import { ProductCard } from './ProductCard';
|
|
3
|
+
import { ProductGallery } from './ProductGallery';
|
|
4
|
+
|
|
5
|
+
export { ProductGallery };
|
|
6
|
+
export type { ProductGalleryProps } from './ProductGallery';
|
|
7
|
+
|
|
8
|
+
// Lazy-loaded product components (code-split)
|
|
9
|
+
export { LazyProductDetails as ProductDetails, LazyAddToCartForm as AddToCartForm };
|
|
10
|
+
|
|
11
|
+
// Eager components (lightweight, used frequently)
|
|
12
|
+
export { ProductCard };
|
|
13
|
+
export type { ProductCardProps, ProductCardLayout } from './ProductCard';
|
|
14
|
+
|
|
15
|
+
// Types are co-located with components (not in types.ts)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { lazy } from 'react';
|
|
2
|
+
|
|
3
|
+
export const LazyProductDetails = lazy(() =>
|
|
4
|
+
import('./ProductDetails').then((m) => ({ default: m.ProductDetails }))
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
export const LazyAddToCartForm = lazy(() =>
|
|
8
|
+
import('./AddToCartForm').then((m) => ({ default: m.AddToCartForm }))
|
|
9
|
+
);
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { useEffect, useMemo } from 'react';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
import { apps, type AppSnippetRow } from '../../api/apps';
|
|
4
|
+
import { installAppScope } from '../../utils/app-scope';
|
|
5
|
+
import { applyThemeHookSlots } from '../../utils/theme-hook-slots';
|
|
6
|
+
import { getTwilightContext } from '../../twilight/context';
|
|
7
|
+
|
|
8
|
+
/** How long to wait on the SDK / settings before injecting anyway. */
|
|
9
|
+
const READY_TIMEOUT_MS = 2500;
|
|
10
|
+
|
|
11
|
+
// Module-level so StrictMode double-effects and remounts can't inject twice per page load.
|
|
12
|
+
let injected = false;
|
|
13
|
+
|
|
14
|
+
/** A snippet plus its app's scope id — the key that resolves its config bucket. */
|
|
15
|
+
type OwnedSnippet = AppSnippetRow & { appScopeId: string };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Appends raw snippet HTML to `target`, recreating each `<script>` — innerHTML-parsed
|
|
19
|
+
* scripts are flagged "already started" and never run.
|
|
20
|
+
*/
|
|
21
|
+
export function injectHtml(html: string, target: HTMLElement) {
|
|
22
|
+
const tpl = document.createElement('template');
|
|
23
|
+
tpl.innerHTML = html;
|
|
24
|
+
tpl.content.querySelectorAll('script').forEach((inert) => {
|
|
25
|
+
// importmap scripts can't be added post-init; the engine already injects its own.
|
|
26
|
+
if (inert.type === 'importmap') {
|
|
27
|
+
inert.remove();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const script = document.createElement('script');
|
|
31
|
+
for (const attr of Array.from(inert.attributes)) {
|
|
32
|
+
script.setAttribute(attr.name, attr.value);
|
|
33
|
+
}
|
|
34
|
+
script.text = inert.text;
|
|
35
|
+
inert.replaceWith(script);
|
|
36
|
+
});
|
|
37
|
+
target.appendChild(tpl.content);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Replaces `{{customer.x}}` placeholders with SDK config `user.*` values. Non-`customer.`
|
|
42
|
+
* params are left untouched, matching the classic themes.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveCustomerParams(content: string, params?: string[]): string {
|
|
45
|
+
if (!params?.length) return content;
|
|
46
|
+
for (const param of params) {
|
|
47
|
+
if (!param.startsWith('customer.')) continue;
|
|
48
|
+
const value = String(window.salla?.config?.get(param.replace(/^customer\./, 'user.')) ?? '');
|
|
49
|
+
// Tolerate inner whitespace (`{{ customer.email }}`); function replacement keeps `value`
|
|
50
|
+
// literal so a `$&` in customer data isn't expanded.
|
|
51
|
+
const escaped = param.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
52
|
+
const placeholder = new RegExp(`\\{\\{\\s*${escaped}\\s*\\}\\}`, 'g');
|
|
53
|
+
content = content.replace(placeholder, () => value);
|
|
54
|
+
}
|
|
55
|
+
return content;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Resolves after `ms`, so a promise that may never settle can't block injection. */
|
|
59
|
+
const timeout = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `onReady` hangs forever if the SDK never initializes; race a timeout so a broken SDK
|
|
63
|
+
* degrades to unsubstituted placeholders instead of dropping every snippet.
|
|
64
|
+
*/
|
|
65
|
+
const waitForSdk = (): Promise<unknown> => {
|
|
66
|
+
const ready = window.salla?.onReady();
|
|
67
|
+
return ready ? Promise.race([ready, timeout(READY_TIMEOUT_MS)]) : Promise.resolve();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Holds the snippets' onReady callbacks until all are injected, as on Twig, where they parse before ready. */
|
|
71
|
+
export function withDeferredOnReady(inject: () => void): void {
|
|
72
|
+
const sdk = window.salla;
|
|
73
|
+
const onReady = sdk?.onReady;
|
|
74
|
+
if (!sdk || typeof onReady !== 'function') return inject();
|
|
75
|
+
|
|
76
|
+
const queued: Array<() => void> = [];
|
|
77
|
+
sdk.onReady = (callback) =>
|
|
78
|
+
new Promise<void>((resolve, reject) => {
|
|
79
|
+
queued.push(() => onReady.call(sdk, callback).then(resolve, reject));
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
inject();
|
|
83
|
+
} finally {
|
|
84
|
+
sdk.onReady = onReady;
|
|
85
|
+
}
|
|
86
|
+
queued.forEach((run) => run());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whitelists hook slots, installs the config sandbox, then injects the inline snippets.
|
|
91
|
+
* Runs once per page load, after the SDK is ready.
|
|
92
|
+
*/
|
|
93
|
+
function mountSnippets(
|
|
94
|
+
owned: OwnedSnippet[],
|
|
95
|
+
clientSnippets: OwnedSnippet[],
|
|
96
|
+
buckets: Record<string, Record<string, unknown>>
|
|
97
|
+
): void {
|
|
98
|
+
// Backstop for the inline head copy (see renderThemeHookSlotsScript) — that one runs at
|
|
99
|
+
// parse time, ahead of the snippet scripts; this only covers a head that didn't render.
|
|
100
|
+
applyThemeHookSlots();
|
|
101
|
+
|
|
102
|
+
// Install before any snippet resolves config. The inline SSR-head install normally got there
|
|
103
|
+
// first (with these buckets, or with none when its prefetch failed), so this either installs
|
|
104
|
+
// the factory or seeds the buckets into it. Pass the complete map once.
|
|
105
|
+
installAppScope({ apps: buckets });
|
|
106
|
+
|
|
107
|
+
// Flag every CDN scope with a missing bucket (SSR-rendered ones included).
|
|
108
|
+
const unresolved = new Set<string>();
|
|
109
|
+
for (const s of owned) {
|
|
110
|
+
if (s.url && !(s.appScopeId in buckets)) unresolved.add(s.appScopeId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
withDeferredOnReady(() => {
|
|
114
|
+
for (const snippet of clientSnippets) {
|
|
115
|
+
// Honour `place`: a head-placed snippet is often a <meta>/<link>/<style> (site
|
|
116
|
+
// verification, canonical, preload) that crawlers only read inside <head>.
|
|
117
|
+
const target = snippet.place?.includes('head') ? document.head : document.body;
|
|
118
|
+
try {
|
|
119
|
+
injectHtml(resolveCustomerParams(snippet.content, snippet.customer_parameters), target);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(`[salla] failed to inject snippet for app ${snippet.appScopeId}`, error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (unresolved.size) {
|
|
127
|
+
// One line per page, not per snippet; expected during settings-endpoint rollout.
|
|
128
|
+
console.warn(
|
|
129
|
+
`[salla] app settings missing for ${unresolved.size} app(s); their snippets ran without app.* config`,
|
|
130
|
+
[...unresolved]
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A CDN snippet reduced to what its SSR `<script>` tag needs. */
|
|
136
|
+
interface SsrCdnScript {
|
|
137
|
+
key: string;
|
|
138
|
+
url: string;
|
|
139
|
+
snippetId?: number;
|
|
140
|
+
appScopeId: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Mounts installed apps' snippets once per page load:
|
|
145
|
+
* - **CDN** — rendered as SSR `<script>` tags in <body> (see return), so they exist on first
|
|
146
|
+
* paint. `place` doesn't apply: they're deferred scripts, which run in order either way.
|
|
147
|
+
* - **inline** — injected client-side in the effect, into `place`'s target (head or body).
|
|
148
|
+
*/
|
|
149
|
+
export function AppsSnippets() {
|
|
150
|
+
// From twilight context, not useQuery: React Query isn't dehydrated here, so useQuery has no
|
|
151
|
+
// SSR data — but the CDN <script> tags must render server-side.
|
|
152
|
+
const ssrSnippets = getTwilightContext().appsSnippets;
|
|
153
|
+
|
|
154
|
+
// Client fallback for when the SSR prefetch failed or timed out (it fails open, non-fatal).
|
|
155
|
+
// Without it a single blip on `apps/snippets` would drop every installed app's snippets for
|
|
156
|
+
// the whole page load — no CDN tags, no inline scripts, no hook slots. Retries here because
|
|
157
|
+
// the query's own `retry: 0` exists only to bound the SSR wait.
|
|
158
|
+
const { data: fallbackSnippets } = useQuery({
|
|
159
|
+
...apps.queries.snippets(),
|
|
160
|
+
enabled: !ssrSnippets,
|
|
161
|
+
retry: 2,
|
|
162
|
+
});
|
|
163
|
+
const data = ssrSnippets ?? fallbackSnippets;
|
|
164
|
+
|
|
165
|
+
const { data: settings, isSuccess, isError } = useQuery({ ...apps.queries.settings(), retry: 2 });
|
|
166
|
+
|
|
167
|
+
// Fail open: inject once settings settle either way, so snippets that never read `app.*`
|
|
168
|
+
// aren't dropped when the bucket is empty.
|
|
169
|
+
const settingsSettled = isSuccess || isError;
|
|
170
|
+
|
|
171
|
+
// Each enabled app's snippets, tagged with its scope id (the encoded `id`). Older payloads
|
|
172
|
+
// lacking `id` get a deterministic per-app fallback sharing one empty bucket.
|
|
173
|
+
const owned = useMemo<OwnedSnippet[]>(
|
|
174
|
+
() =>
|
|
175
|
+
(data ?? [])
|
|
176
|
+
.filter((app) => app.status === 'enabled')
|
|
177
|
+
.flatMap((app) =>
|
|
178
|
+
app.snippets.map((s) => ({ ...s, appScopeId: app.id ?? `unresolved-${app.app_id}` }))
|
|
179
|
+
),
|
|
180
|
+
[data]
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Every CDN snippet → a real `<script>` tag in the SSR body (rendered below).
|
|
184
|
+
const ssrCdnScripts = useMemo<SsrCdnScript[]>(
|
|
185
|
+
() =>
|
|
186
|
+
owned
|
|
187
|
+
.filter((s) => s.url)
|
|
188
|
+
.map((s, i) => ({
|
|
189
|
+
key: `${s.appScopeId}:${s.id ?? i}`,
|
|
190
|
+
url: s.url as string,
|
|
191
|
+
snippetId: s.id,
|
|
192
|
+
appScopeId: s.appScopeId,
|
|
193
|
+
})),
|
|
194
|
+
[owned]
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (injected || !data || !settingsSettled) return;
|
|
199
|
+
injected = true;
|
|
200
|
+
|
|
201
|
+
// Client-injected: inline snippets only. Every CDN snippet is SSR-rendered (below).
|
|
202
|
+
const clientSnippets = owned.filter((s) => !s.url);
|
|
203
|
+
|
|
204
|
+
// Always wait for the SDK: a snippet body calls the global `salla`/`Salla` (config, hooks,
|
|
205
|
+
// event bus) whether or not it declares customer parameters, and those are only populated
|
|
206
|
+
// after init. The wait is timeout-raced, so a broken SDK still can't strand injection.
|
|
207
|
+
void waitForSdk().then(() => mountSnippets(owned, clientSnippets, settings?.apps ?? {}));
|
|
208
|
+
}, [data, owned, settings, settingsSettled]);
|
|
209
|
+
|
|
210
|
+
// CDN snippets as classic `defer` scripts: non-blocking, in-order, kept in <body> (React hoists
|
|
211
|
+
// only `async` to <head>), and still exposing `document.currentScript` for the wrapper.
|
|
212
|
+
return (
|
|
213
|
+
<>
|
|
214
|
+
{ssrCdnScripts.map((s) => (
|
|
215
|
+
<script
|
|
216
|
+
key={s.key}
|
|
217
|
+
src={s.url}
|
|
218
|
+
defer
|
|
219
|
+
data-scope-id={s.appScopeId}
|
|
220
|
+
data-snippet-id={s.snippetId != null ? String(s.snippetId) : ''}
|
|
221
|
+
/>
|
|
222
|
+
))}
|
|
223
|
+
</>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StoreContext } from '../../api/store';
|
|
2
|
+
import { resolveTwilightSdkUrl, TICKETING_SCRIPT_URL } from '../../routes/root/head';
|
|
3
|
+
|
|
4
|
+
/** SDK modules at the end of <body>, ahead of the snippets: same run order as from <head>, later download. */
|
|
5
|
+
export function SallaSdkScripts({ settings }: { settings?: StoreContext }) {
|
|
6
|
+
return (
|
|
7
|
+
<>
|
|
8
|
+
<script id="salla-sdk" type="module" src={resolveTwilightSdkUrl(settings)} />
|
|
9
|
+
{settings?.store?.settings?.ticketing_system_enabled && (
|
|
10
|
+
<script id="salla-ticketing-script" type="module" src={TICKETING_SCRIPT_URL} />
|
|
11
|
+
)}
|
|
12
|
+
</>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Component - injects installed marketplace apps' storefront snippets (inline + CDN).
|
|
2
|
+
// `injectHtml` / `resolveCustomerParams` stay internal to the component (tests import them
|
|
3
|
+
// from the module directly) — themes only ever render <AppsSnippets />.
|
|
4
|
+
export { AppsSnippets } from './AppsSnippets';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useIsProduct } from '../../providers/NavigationProvider';
|
|
3
|
+
import { useTwilight } from '../../providers/TwilightProvider';
|
|
4
|
+
import { useOpeningHours } from '../../hooks/useOpeningHours';
|
|
5
|
+
import { useTranslation } from '../../providers/I18nProvider';
|
|
6
|
+
|
|
7
|
+
export interface StoreClosedNotificationProps {
|
|
8
|
+
/** Whether to show the notification (defaults to true when store is closed on product page) */
|
|
9
|
+
show?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* StoreClosedNotification - Shows a notification bar when the store is closed.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors PHP ReceivingOrdersTimesWidget:
|
|
16
|
+
* - Only shows on product pages
|
|
17
|
+
* - Only shows when store is closed for receiving orders
|
|
18
|
+
* - Displays when the store will open next
|
|
19
|
+
* - Has a close button to dismiss
|
|
20
|
+
*
|
|
21
|
+
* Injected via hook registry at body:start.
|
|
22
|
+
*/
|
|
23
|
+
export function StoreClosedNotification(_props: StoreClosedNotificationProps) {
|
|
24
|
+
const { store } = useTwilight();
|
|
25
|
+
const { t } = useTranslation();
|
|
26
|
+
const { isOpen, isEnabled, nextOpenFormatted } = useOpeningHours(store.settings?.opening_hours);
|
|
27
|
+
const [dismissed, setDismissed] = useState(false);
|
|
28
|
+
|
|
29
|
+
// Check if we're on a product page
|
|
30
|
+
const isProductPage = useIsProduct();
|
|
31
|
+
|
|
32
|
+
// Conditions: product page, opening hours enabled, store closed, not dismissed
|
|
33
|
+
if (!isProductPage || !isEnabled || isOpen || dismissed || !nextOpenFormatted) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const handleClose = (e: React.MouseEvent) => {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
setDismissed(true);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className="store-notify"
|
|
45
|
+
id="store-closed-notification"
|
|
46
|
+
data-testid="store-closed-notification"
|
|
47
|
+
style={{
|
|
48
|
+
backgroundColor: '#d63031',
|
|
49
|
+
color: '#fff',
|
|
50
|
+
padding: '10px 15px',
|
|
51
|
+
display: 'flex',
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
justifyContent: 'center',
|
|
54
|
+
position: 'relative',
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<a
|
|
58
|
+
href="#"
|
|
59
|
+
id="notify-close"
|
|
60
|
+
onClick={handleClose}
|
|
61
|
+
style={{ position: 'absolute', left: 15, color: '#fff', textDecoration: 'none' }}
|
|
62
|
+
>
|
|
63
|
+
<i className="sicon-cancel" />
|
|
64
|
+
</a>
|
|
65
|
+
<p style={{ margin: 0, textAlign: 'center', flex: 1 }}>
|
|
66
|
+
{t('store.closed_notification', {
|
|
67
|
+
defaultValue: `المتجر حاليا مغلق لاستقبال الطلبات وسيتم فتحه (${nextOpenFormatted})`,
|
|
68
|
+
nextOpen: nextOpenFormatted,
|
|
69
|
+
})}
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|