@alphasquad/saleor-template-advance 0.1.0
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/.env.example +57 -0
- package/APPLE_PAY_QUICK_START.md +165 -0
- package/APPLE_PAY_SETUP.md +331 -0
- package/README.md +46 -0
- package/SEO_AUDIT_CHECKLIST_STATUS.md +244 -0
- package/SEO_AUDIT_REPORT.md +66 -0
- package/eslint.config.mjs +16 -0
- package/next-env.d.ts +5 -0
- package/next.config.ts +109 -0
- package/package.json +47 -0
- package/postcss.config.mjs +5 -0
- package/public/.well-known/apple-developer-merchantid-domain-association +1 -0
- package/public/Logo.png +0 -0
- package/public/brand-video.mp4 +0 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/footer/facebook.tsx +34 -0
- package/public/footer/instagram.tsx +27 -0
- package/public/footer/mail.tsx +5 -0
- package/public/footer/x.tsx +35 -0
- package/public/globe.svg +1 -0
- package/public/icons/Authorize.net.webp +0 -0
- package/public/icons/amex.gif +0 -0
- package/public/icons/appIcon.png +0 -0
- package/public/icons/discover.gif +0 -0
- package/public/icons/master.gif +0 -0
- package/public/icons/paypal.png +0 -0
- package/public/icons/stripe.png +0 -0
- package/public/icons/visa.gif +0 -0
- package/public/images/BackgroundNoise.png +0 -0
- package/public/images/footer-background.png +0 -0
- package/public/next.svg +1 -0
- package/public/no-image-avail-large.png +0 -0
- package/public/random-car-1.jpeg +0 -0
- package/public/random-car-2.png +0 -0
- package/public/random-car-3.jpg +0 -0
- package/public/random-car-4.jpg +0 -0
- package/public/random-car-5.jpg +0 -0
- package/public/star.svg +3 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/seo-audit/generate-checklist.mjs +156 -0
- package/src/app/(auth)/account/forgot-password/layout.tsx +16 -0
- package/src/app/(auth)/account/forgot-password/page.tsx +135 -0
- package/src/app/(auth)/account/login/layout.tsx +16 -0
- package/src/app/(auth)/account/login/page.tsx +288 -0
- package/src/app/(auth)/account/otp/layout.tsx +16 -0
- package/src/app/(auth)/account/otp/page.tsx +108 -0
- package/src/app/(auth)/account/register/layout.tsx +16 -0
- package/src/app/(auth)/account/register/page.tsx +431 -0
- package/src/app/(auth)/account/reset-password/layout.tsx +16 -0
- package/src/app/(auth)/account/reset-password/page.tsx +222 -0
- package/src/app/[slug]/page.tsx +43 -0
- package/src/app/about/loading.tsx +17 -0
- package/src/app/about/page.tsx +61 -0
- package/src/app/account/address/layout.tsx +15 -0
- package/src/app/account/address/page.tsx +166 -0
- package/src/app/account/head.tsx +4 -0
- package/src/app/account/layout.tsx +62 -0
- package/src/app/account/orders/[id]/layout.tsx +17 -0
- package/src/app/account/orders/[id]/page.tsx +115 -0
- package/src/app/account/orders/components/orderDetailsModal.tsx +410 -0
- package/src/app/account/orders/layout.tsx +15 -0
- package/src/app/account/orders/page.tsx +146 -0
- package/src/app/account/page.tsx +39 -0
- package/src/app/account/settings/components/editProfileSuccessModal.tsx +28 -0
- package/src/app/account/settings/layout.tsx +15 -0
- package/src/app/account/settings/page.tsx +260 -0
- package/src/app/api/affirm/check-status/route.ts +94 -0
- package/src/app/api/affirm/create-checkout/route.ts +109 -0
- package/src/app/api/affirm/get-config/route.ts +108 -0
- package/src/app/api/affirm/process-payment/route.ts +244 -0
- package/src/app/api/affirm/test-connection/route.ts +45 -0
- package/src/app/api/auth/clear/route.ts +16 -0
- package/src/app/api/auth/clear-cookies/route.ts +42 -0
- package/src/app/api/auth/set/route.ts +47 -0
- package/src/app/api/configuration/route.ts +18 -0
- package/src/app/api/dynamic-page/[slug]/route.ts +24 -0
- package/src/app/api/form-submission/route.ts +237 -0
- package/src/app/api/paypal/capture-order/route.ts +303 -0
- package/src/app/api/paypal/create-order/route.ts +211 -0
- package/src/app/api/paypal/get-config/route.ts +240 -0
- package/src/app/api/search-proxy/route.ts +52 -0
- package/src/app/authorize-net-success/layout.tsx +19 -0
- package/src/app/authorize-net-success/page.tsx +12 -0
- package/src/app/authorize-net-success/summary.tsx +486 -0
- package/src/app/blog/[slug]/blogContentRenderer.tsx +369 -0
- package/src/app/blog/[slug]/layout.tsx +17 -0
- package/src/app/blog/[slug]/page.tsx +151 -0
- package/src/app/blog/constant.tsx +147 -0
- package/src/app/blog/layout.tsx +31 -0
- package/src/app/blog/page.tsx +81 -0
- package/src/app/brand/[id]/BrandPageClient.tsx +188 -0
- package/src/app/brand/[id]/layout.tsx +17 -0
- package/src/app/brand/[id]/page.tsx +176 -0
- package/src/app/brands/components/brandsListingClient.tsx +97 -0
- package/src/app/brands/layout.tsx +31 -0
- package/src/app/brands/page.tsx +40 -0
- package/src/app/cancellation-policy/page.tsx +53 -0
- package/src/app/cart/layout.tsx +19 -0
- package/src/app/cart/page.tsx +752 -0
- package/src/app/category/[slug]/CategoryPageClient.tsx +377 -0
- package/src/app/category/[slug]/layout.tsx +17 -0
- package/src/app/category/[slug]/page.tsx +224 -0
- package/src/app/category/page.tsx +114 -0
- package/src/app/checkout/components/addNewAddressModal.tsx +474 -0
- package/src/app/checkout/layout.tsx +19 -0
- package/src/app/checkout/page.tsx +3312 -0
- package/src/app/components/account/AccountTabs.tsx +40 -0
- package/src/app/components/ads/GoogleAdSense.tsx +74 -0
- package/src/app/components/analytics/AnalyticsScripts.tsx +78 -0
- package/src/app/components/analytics/ConditionalGTMNoscript.tsx +24 -0
- package/src/app/components/analytics/ConditionalGoogleAnalytics.tsx +16 -0
- package/src/app/components/ancillary/AncillaryContent.tsx +7 -0
- package/src/app/components/auth/TokenExpirationHandler.tsx +8 -0
- package/src/app/components/blog/BlogList.tsx +112 -0
- package/src/app/components/checkout/AddressInformationSection.tsx +34 -0
- package/src/app/components/checkout/AddressManagement.tsx +571 -0
- package/src/app/components/checkout/CheckoutHeader.tsx +51 -0
- package/src/app/components/checkout/CheckoutQuestions.tsx +454 -0
- package/src/app/components/checkout/CheckoutTermsModal.tsx +81 -0
- package/src/app/components/checkout/ContactDetailsSection.tsx +52 -0
- package/src/app/components/checkout/DealerShippingSection.tsx +359 -0
- package/src/app/components/checkout/DeliveryMethodSection.tsx +249 -0
- package/src/app/components/checkout/OrderSummary.tsx +386 -0
- package/src/app/components/checkout/TermsContentRenderer.tsx +147 -0
- package/src/app/components/checkout/WillCallSection.tsx +133 -0
- package/src/app/components/checkout/affirmPayment.tsx +383 -0
- package/src/app/components/checkout/checkoutProcessingModal.tsx +96 -0
- package/src/app/components/checkout/googlePayButton.tsx +334 -0
- package/src/app/components/checkout/paymentStep.tsx +180 -0
- package/src/app/components/checkout/paypalPayment.tsx +1083 -0
- package/src/app/components/checkout/saleorNativePayment.tsx +1758 -0
- package/src/app/components/dynamicPage/DynamicPageRenderer.tsx +13 -0
- package/src/app/components/dynamicPage/HtmlWidgetRenderer.tsx +144 -0
- package/src/app/components/filtersCollapsible/index.tsx +365 -0
- package/src/app/components/globalSearch/index.tsx +423 -0
- package/src/app/components/layout/cartDropDown.tsx +628 -0
- package/src/app/components/layout/components/FooterNewsletter.tsx +21 -0
- package/src/app/components/layout/footer.tsx +283 -0
- package/src/app/components/layout/header/accountMenuDropdown.tsx +53 -0
- package/src/app/components/layout/header/components/CartBadge.tsx +18 -0
- package/src/app/components/layout/header/components/LoadingState.tsx +17 -0
- package/src/app/components/layout/header/components/MenuItemDropdown.tsx +124 -0
- package/src/app/components/layout/header/components/MobileNavbar.tsx +123 -0
- package/src/app/components/layout/header/components/NavbarActions.tsx +125 -0
- package/src/app/components/layout/header/components/NavbarBrand.tsx +29 -0
- package/src/app/components/layout/header/components/NavigationLinks.tsx +131 -0
- package/src/app/components/layout/header/hamMenuSlide.tsx +318 -0
- package/src/app/components/layout/header/header.tsx +44 -0
- package/src/app/components/layout/header/hooks/useDropdown.ts +45 -0
- package/src/app/components/layout/header/hooks/useNavbarData.ts +138 -0
- package/src/app/components/layout/header/hooks/useNavbarState.ts +66 -0
- package/src/app/components/layout/header/megaMenuDropdown.tsx +116 -0
- package/src/app/components/layout/header/navBar.tsx +121 -0
- package/src/app/components/layout/header/search.tsx +418 -0
- package/src/app/components/layout/header/styles/navbarStyles.ts +27 -0
- package/src/app/components/layout/header/topBar.tsx +214 -0
- package/src/app/components/layout/joinNewsletterForm/index.tsx +72 -0
- package/src/app/components/layout/mobileAccordian/index.tsx +92 -0
- package/src/app/components/layout/paymentMethods.tsx +75 -0
- package/src/app/components/layout/rootLayout.tsx +23 -0
- package/src/app/components/layout/siteInfo.tsx +103 -0
- package/src/app/components/layout/socialLinks.tsx +65 -0
- package/src/app/components/newsletterSection/emailListSection.tsx +224 -0
- package/src/app/components/newsletterSection/emailSectionServer.tsx +8 -0
- package/src/app/components/providers/ApolloWrapper.tsx +12 -0
- package/src/app/components/providers/AppConfigurationProvider.tsx +108 -0
- package/src/app/components/providers/GoogleAnalyticsProvider.tsx +149 -0
- package/src/app/components/providers/GoogleTagManagerProvider.tsx +31 -0
- package/src/app/components/providers/RecaptchaProvider.tsx +18 -0
- package/src/app/components/providers/ServerAppConfigurationProvider.tsx +133 -0
- package/src/app/components/providers/YMMStatusProvider.tsx +15 -0
- package/src/app/components/reuseableUI/AboutUs.tsx +115 -0
- package/src/app/components/reuseableUI/AddToCartClient.tsx +125 -0
- package/src/app/components/reuseableUI/EditorJsRenderer.tsx +219 -0
- package/src/app/components/reuseableUI/HeroSectionsearchByVehicle.tsx +188 -0
- package/src/app/components/reuseableUI/ImageWithFallback.tsx +41 -0
- package/src/app/components/reuseableUI/Toast.tsx +101 -0
- package/src/app/components/reuseableUI/blogCard.tsx +52 -0
- package/src/app/components/reuseableUI/brandCard.tsx +68 -0
- package/src/app/components/reuseableUI/breadcrumb.tsx +38 -0
- package/src/app/components/reuseableUI/categoryCard.tsx +37 -0
- package/src/app/components/reuseableUI/categorySkeleton.tsx +31 -0
- package/src/app/components/reuseableUI/commonButton.tsx +48 -0
- package/src/app/components/reuseableUI/defaultInputField/index.tsx +84 -0
- package/src/app/components/reuseableUI/emptyState.tsx +29 -0
- package/src/app/components/reuseableUI/errorTag.tsx +15 -0
- package/src/app/components/reuseableUI/heading/index.tsx +20 -0
- package/src/app/components/reuseableUI/input.tsx +117 -0
- package/src/app/components/reuseableUI/listCard.tsx +137 -0
- package/src/app/components/reuseableUI/loadingUI.tsx +12 -0
- package/src/app/components/reuseableUI/modalLayout.tsx +76 -0
- package/src/app/components/reuseableUI/newsletter/newsletterClient.tsx +622 -0
- package/src/app/components/reuseableUI/newsletter/newslettersHomeModal.tsx +68 -0
- package/src/app/components/reuseableUI/offerCard.tsx +42 -0
- package/src/app/components/reuseableUI/passwordRules/passwordRules.tsx +56 -0
- package/src/app/components/reuseableUI/primaryButton/index.tsx +34 -0
- package/src/app/components/reuseableUI/productCard.tsx +118 -0
- package/src/app/components/reuseableUI/productSkeleton.tsx +34 -0
- package/src/app/components/reuseableUI/searchByVehicle.tsx +187 -0
- package/src/app/components/reuseableUI/secondaryButton/index.tsx +34 -0
- package/src/app/components/reuseableUI/section.tsx +20 -0
- package/src/app/components/reuseableUI/select/index.tsx +98 -0
- package/src/app/components/reuseableUI/skeletonLoader.tsx +117 -0
- package/src/app/components/reuseableUI/statusTag.tsx +24 -0
- package/src/app/components/reuseableUI/tags/saleTag.tsx +19 -0
- package/src/app/components/reuseableUI/testimonialCard.tsx +93 -0
- package/src/app/components/richText/EditorRenderer.tsx +318 -0
- package/src/app/components/search/HierarchicalCategoryFilter.tsx +155 -0
- package/src/app/components/search/SearchFilters.tsx +155 -0
- package/src/app/components/search/YMMSearchSidebar.tsx +187 -0
- package/src/app/components/seo/ServerProductCard.tsx +91 -0
- package/src/app/components/seo/ServerProductGrid.tsx +45 -0
- package/src/app/components/shop/CategoryFilter.tsx +184 -0
- package/src/app/components/shop/ItemsPerPageSelect.tsx +69 -0
- package/src/app/components/shop/ItemsPerPageSelectClient.tsx +58 -0
- package/src/app/components/shop/MobileFilters.tsx +103 -0
- package/src/app/components/shop/ProductGridSkeleton.tsx +16 -0
- package/src/app/components/shop/ProductsGrid.tsx +230 -0
- package/src/app/components/shop/SearchFilter.tsx +218 -0
- package/src/app/components/shop/SearchFilterClient.tsx +122 -0
- package/src/app/components/shop/SearchLoadingOverlay.tsx +32 -0
- package/src/app/components/shop/ShopMobileFilters.tsx +205 -0
- package/src/app/components/showroom/VehicleSearchDropdowns.tsx +187 -0
- package/src/app/components/showroom/brandsSwiper.tsx +49 -0
- package/src/app/components/showroom/brandsSwiperClient copy.tsx +93 -0
- package/src/app/components/showroom/brandsSwiperClient.tsx +122 -0
- package/src/app/components/showroom/brandsSwiperServer.tsx +42 -0
- package/src/app/components/showroom/bundleProducts.tsx +120 -0
- package/src/app/components/showroom/categoryGrid.tsx +51 -0
- package/src/app/components/showroom/categoryGridServer.tsx +45 -0
- package/src/app/components/showroom/categorySwiper.tsx +115 -0
- package/src/app/components/showroom/featureStrip.tsx +139 -0
- package/src/app/components/showroom/offersSwiper.tsx +181 -0
- package/src/app/components/showroom/productGrid.tsx +56 -0
- package/src/app/components/showroom/productSwiper.tsx +119 -0
- package/src/app/components/showroom/promotion-slider.tsx +138 -0
- package/src/app/components/showroom/promotion.tsx +207 -0
- package/src/app/components/showroom/promotionsSwiper.tsx +174 -0
- package/src/app/components/showroom/showroomHeroCarousel.tsx +141 -0
- package/src/app/components/showroom/testimonialsGrid.tsx +106 -0
- package/src/app/components/skeletons/ContentSkeleton.tsx +14 -0
- package/src/app/components/sortDropdown/index.tsx +116 -0
- package/src/app/components/tertiaryButton/index.tsx +25 -0
- package/src/app/components/theme/theme-provider.tsx +82 -0
- package/src/app/contact/layout.tsx +32 -0
- package/src/app/contact/page.tsx +591 -0
- package/src/app/content/[slug]/layout.tsx +17 -0
- package/src/app/content/[slug]/page.tsx +159 -0
- package/src/app/content/layout.tsx +31 -0
- package/src/app/content/page.tsx +88 -0
- package/src/app/core-policies/page.tsx +55 -0
- package/src/app/discounts/page.tsx +54 -0
- package/src/app/frequently-asked-questions/page.tsx +57 -0
- package/src/app/globals.css +440 -0
- package/src/app/hooks/useDealerLocations.ts +259 -0
- package/src/app/hooks/useGTMEngagement.ts +71 -0
- package/src/app/hooks/useGoogleAnalytics.ts +145 -0
- package/src/app/layout.tsx +149 -0
- package/src/app/not-found.tsx +31 -0
- package/src/app/order-confirmation/layout.tsx +19 -0
- package/src/app/order-confirmation/page.tsx +12 -0
- package/src/app/order-confirmation/summary.tsx +1775 -0
- package/src/app/page.tsx +194 -0
- package/src/app/privacy-policy/loading.tsx +17 -0
- package/src/app/privacy-policy/page.tsx +56 -0
- package/src/app/product/[id]/ProductDetailClient.tsx +2448 -0
- package/src/app/product/[id]/components/itemInquiryModal.tsx +461 -0
- package/src/app/product/[id]/layout.tsx +116 -0
- package/src/app/product/[id]/page.tsx +200 -0
- package/src/app/product/layout.tsx +15 -0
- package/src/app/products/all/AllProductsClient.tsx +743 -0
- package/src/app/products/all/page.tsx +176 -0
- package/src/app/products/components/shopEmptyState.tsx +29 -0
- package/src/app/request-return/layout.tsx +36 -0
- package/src/app/request-return/page.tsx +597 -0
- package/src/app/robots.txt/route.ts +27 -0
- package/src/app/search/layout.tsx +16 -0
- package/src/app/search/page.tsx +736 -0
- package/src/app/shipping-returns/page.tsx +60 -0
- package/src/app/site-map/layout.tsx +33 -0
- package/src/app/site-map/page.tsx +113 -0
- package/src/app/sitemap-index.xml/route.ts +20 -0
- package/src/app/sitemap.ts +10 -0
- package/src/app/terms-and-conditions/loading.tsx +17 -0
- package/src/app/terms-and-conditions/page.tsx +56 -0
- package/src/app/utils/appConfiguration.ts +327 -0
- package/src/app/utils/branding.ts +52 -0
- package/src/app/utils/configurationService.ts +202 -0
- package/src/app/utils/constant.tsx +242 -0
- package/src/app/utils/editorJsUtils.tsx +249 -0
- package/src/app/utils/functions.ts +146 -0
- package/src/app/utils/googleAnalytics.ts +168 -0
- package/src/app/utils/googleTagManager.ts +475 -0
- package/src/app/utils/ipDetection.ts +270 -0
- package/src/app/utils/serverConfigurationService.ts +209 -0
- package/src/app/utils/svgs/GridIcon.tsx +45 -0
- package/src/app/utils/svgs/account/myAccount/listDotIcon.tsx +3 -0
- package/src/app/utils/svgs/account/myAccount/tickIcon.tsx +10 -0
- package/src/app/utils/svgs/account/orderHistory/InfoIcon.tsx +49 -0
- package/src/app/utils/svgs/arrowDownIcon.tsx +17 -0
- package/src/app/utils/svgs/arrowIcon.tsx +25 -0
- package/src/app/utils/svgs/arrowUpIcon.tsx +16 -0
- package/src/app/utils/svgs/brandsSearchIcon.tsx +25 -0
- package/src/app/utils/svgs/cart/cartIcon.tsx +31 -0
- package/src/app/utils/svgs/cart/plusIcon.tsx +13 -0
- package/src/app/utils/svgs/cart/subtractIcon.tsx +13 -0
- package/src/app/utils/svgs/cart/successTickIcon.tsx +14 -0
- package/src/app/utils/svgs/chevronDownIcon.tsx +21 -0
- package/src/app/utils/svgs/closeEyeIcon.tsx +47 -0
- package/src/app/utils/svgs/crossIcon.tsx +25 -0
- package/src/app/utils/svgs/eyeIcon.tsx +29 -0
- package/src/app/utils/svgs/featureTag.tsx +20 -0
- package/src/app/utils/svgs/filterIcon.tsx +3 -0
- package/src/app/utils/svgs/globleIcon.tsx +41 -0
- package/src/app/utils/svgs/infoIcon.tsx +34 -0
- package/src/app/utils/svgs/listIcon.tsx +50 -0
- package/src/app/utils/svgs/logOutIcon.tsx +35 -0
- package/src/app/utils/svgs/menuIcon.tsx +8 -0
- package/src/app/utils/svgs/minusIcon.tsx +18 -0
- package/src/app/utils/svgs/newsletterIcon.tsx +19 -0
- package/src/app/utils/svgs/noDataFoundIcon-.tsx +26 -0
- package/src/app/utils/svgs/noProductFoundIcon.tsx +43 -0
- package/src/app/utils/svgs/passwordIcons/errorIcon.tsx +31 -0
- package/src/app/utils/svgs/passwordIcons/successIcon.tsx +24 -0
- package/src/app/utils/svgs/paymentProcessingIcons/hourglassIcon.tsx +43 -0
- package/src/app/utils/svgs/paymentProcessingIcons/modalCrossIcon.tsx +23 -0
- package/src/app/utils/svgs/paymentProcessingIcons/paymentFailedIcon.tsx +47 -0
- package/src/app/utils/svgs/pencilIcon.tsx +11 -0
- package/src/app/utils/svgs/plusIcon.tsx +25 -0
- package/src/app/utils/svgs/productInquiryIcon.tsx +40 -0
- package/src/app/utils/svgs/searchIcon.tsx +31 -0
- package/src/app/utils/svgs/shoppingCart.tsx +32 -0
- package/src/app/utils/svgs/spinnerIcon.tsx +22 -0
- package/src/app/utils/svgs/spinnerLoadingIcon.tsx +26 -0
- package/src/app/utils/svgs/successTickIcon.tsx +40 -0
- package/src/app/utils/svgs/swiperArrowIconLeft.tsx +18 -0
- package/src/app/utils/svgs/swiperArrowIconRight.tsx +18 -0
- package/src/app/utils/svgs/userProfileIcon.tsx +31 -0
- package/src/app/utils/svgs/warningCircleIcon.tsx +15 -0
- package/src/app/warranty/constant.tsx +63 -0
- package/src/app/warranty/loading.tsx +17 -0
- package/src/app/warranty/page.tsx +56 -0
- package/src/graphql/client.ts +288 -0
- package/src/graphql/mutations/accountAddressCreate.ts +56 -0
- package/src/graphql/mutations/accountAddressDelete.ts +23 -0
- package/src/graphql/mutations/accountAddressUpdate.ts +55 -0
- package/src/graphql/mutations/accountSetDefaultAddress.ts +32 -0
- package/src/graphql/mutations/accountUpdate.ts +34 -0
- package/src/graphql/mutations/changePassword.ts +25 -0
- package/src/graphql/mutations/checkout.ts +117 -0
- package/src/graphql/mutations/checkoutAddVoucher.ts +63 -0
- package/src/graphql/mutations/checkoutComplete.ts +79 -0
- package/src/graphql/mutations/checkoutCreate.ts +131 -0
- package/src/graphql/mutations/checkoutCustomerAttach.ts +50 -0
- package/src/graphql/mutations/checkoutEmailUpdate.ts +15 -0
- package/src/graphql/mutations/checkoutLineMetadataUpdate.ts +52 -0
- package/src/graphql/mutations/checkoutPaymentCreate.ts +82 -0
- package/src/graphql/mutations/paymentGatewayInitialize.ts +58 -0
- package/src/graphql/mutations/registerAccount.ts +65 -0
- package/src/graphql/mutations/requestPasswordReset.ts +32 -0
- package/src/graphql/mutations/setPassword.ts +49 -0
- package/src/graphql/mutations/signIn.ts +50 -0
- package/src/graphql/mutations/tokenRefresh.ts +19 -0
- package/src/graphql/mutations/updateCheckoutMetadata.ts +49 -0
- package/src/graphql/mutations/updateProfile.ts +18 -0
- package/src/graphql/mutations/willCallDeliveryMethod.ts +81 -0
- package/src/graphql/queries/checkout.ts +168 -0
- package/src/graphql/queries/findProductByOldSlug.ts +58 -0
- package/src/graphql/queries/getAboutPage.ts +24 -0
- package/src/graphql/queries/getAboutPageId.ts +9 -0
- package/src/graphql/queries/getAboutUs.ts +38 -0
- package/src/graphql/queries/getAddressInformation.ts +38 -0
- package/src/graphql/queries/getAllCategories.ts +41 -0
- package/src/graphql/queries/getAllCategoriesTree.ts +67 -0
- package/src/graphql/queries/getAllCategoriesWithProducts.ts +29 -0
- package/src/graphql/queries/getAllCollectionsWithProducts.ts +16 -0
- package/src/graphql/queries/getBlogs.ts +222 -0
- package/src/graphql/queries/getBrands.ts +17 -0
- package/src/graphql/queries/getBundles.ts +43 -0
- package/src/graphql/queries/getCategories.ts +20 -0
- package/src/graphql/queries/getChannels.ts +77 -0
- package/src/graphql/queries/getCheckoutQuestions.ts +115 -0
- package/src/graphql/queries/getCheckoutTermsAndConditions.ts +37 -0
- package/src/graphql/queries/getContactPage.ts +117 -0
- package/src/graphql/queries/getContentPage.ts +191 -0
- package/src/graphql/queries/getDiscountOffers.ts +18 -0
- package/src/graphql/queries/getDynamicPageBySlug.ts +251 -0
- package/src/graphql/queries/getFeaturedProducts.ts +48 -0
- package/src/graphql/queries/getHeroMetadata.ts +23 -0
- package/src/graphql/queries/getMenuBySlug.ts +84 -0
- package/src/graphql/queries/getMyProfile.ts +23 -0
- package/src/graphql/queries/getNewsletter.ts +122 -0
- package/src/graphql/queries/getNewsletterPage.ts +111 -0
- package/src/graphql/queries/getPageBySlug.ts +52 -0
- package/src/graphql/queries/getPageTypeId.ts +27 -0
- package/src/graphql/queries/getPaymentMethods.ts +61 -0
- package/src/graphql/queries/getProducts.ts +78 -0
- package/src/graphql/queries/getPromotions.ts +24 -0
- package/src/graphql/queries/getRequestReturnPage.ts +121 -0
- package/src/graphql/queries/getSiteInfo.ts +54 -0
- package/src/graphql/queries/getSocialLinks.ts +52 -0
- package/src/graphql/queries/getTestimonials.ts +25 -0
- package/src/graphql/queries/getUserWithCheckout.ts +27 -0
- package/src/graphql/queries/getVehicleMakes.ts +21 -0
- package/src/graphql/queries/getVehicleModels.ts +21 -0
- package/src/graphql/queries/getVehicleYears.ts +21 -0
- package/src/graphql/queries/meAddresses.ts +56 -0
- package/src/graphql/queries/myOrders.ts +37 -0
- package/src/graphql/queries/orderDetail.ts +231 -0
- package/src/graphql/queries/productDetailsById.ts +197 -0
- package/src/graphql/queries/productInquiry.ts +115 -0
- package/src/graphql/queries/productsByCategoriesAndCollections.ts +39 -0
- package/src/graphql/queries/willCallCollectionPoints.ts +55 -0
- package/src/graphql/server-client.ts +54 -0
- package/src/graphql/types/categories.ts +9 -0
- package/src/graphql/types/checkout.ts +168 -0
- package/src/graphql/types/offer.ts +12 -0
- package/src/graphql/types/product.ts +44 -0
- package/src/hooks/scrollPageTop.ts +9 -0
- package/src/hooks/serverNavbarData.ts +79 -0
- package/src/hooks/useCartSync.ts +24 -0
- package/src/hooks/useRecaptcha.ts +33 -0
- package/src/hooks/useTokenExpiration.ts +81 -0
- package/src/hooks/useVehicleData.ts +346 -0
- package/src/lib/api/kount.ts +165 -0
- package/src/lib/api/shop.ts +1445 -0
- package/src/lib/saleor/getSaleorApiUrl.ts +25 -0
- package/src/lib/schema.ts +303 -0
- package/src/lib/seo/extractTextFromEditorJs.ts +58 -0
- package/src/lib/seo/site.ts +10 -0
- package/src/lib/urls/normalizeInternalUrl.ts +53 -0
- package/src/middleware.ts +134 -0
- package/src/sitemaps/README.md +105 -0
- package/src/sitemaps/dynamic-pages-sitemap.ts +247 -0
- package/src/sitemaps/sitemap-index.ts +21 -0
- package/src/sitemaps/static-pages-sitemap.ts +36 -0
- package/src/store/useGlobalStore.tsx +1656 -0
- package/src/types/global.d.ts +148 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export type EditorJsParagraphBlock = { id?: string; type: "paragraph"; data: { text?: string } };
|
|
4
|
+
export type EditorJsHeaderBlock = { id?: string; type: "header"; data: { text?: string; level?: number } };
|
|
5
|
+
export type EditorJsListBlock = { id?: string; type: "list"; data: { style?: "ordered" | "unordered"; items?: string[] } };
|
|
6
|
+
export type EditorJsQuoteBlock = { id?: string; type: "quote"; data: { text?: string; caption?: string; alignment?: string } };
|
|
7
|
+
export type EditorJsLinkBlock = { id?: string; type: "linkTool"; data: { link?: string; meta?: { title?: string; description?: string; image?: { url?: string } } } };
|
|
8
|
+
export type EditorJsImageBlock = { id?: string; type: "image"; data: { file?: { url?: string }; caption?: string; withBorder?: boolean; withBackground?: boolean; stretched?: boolean } };
|
|
9
|
+
export type EditorJsDelimiterBlock = { id?: string; type: "delimiter"; data?: Record<string, unknown> };
|
|
10
|
+
export type EditorJsTableBlock = { id?: string; type: "table"; data: { withHeadings?: boolean; content?: string[][] } };
|
|
11
|
+
export type EditorJsCodeBlock = { id?: string; type: "code"; data: { code?: string } };
|
|
12
|
+
export type EditorJsGenericBlock = { id?: string; type: string; data?: Record<string, unknown> | null };
|
|
13
|
+
export type EditorJsBlock =
|
|
14
|
+
| EditorJsParagraphBlock
|
|
15
|
+
| EditorJsHeaderBlock
|
|
16
|
+
| EditorJsListBlock
|
|
17
|
+
| EditorJsQuoteBlock
|
|
18
|
+
| EditorJsLinkBlock
|
|
19
|
+
| EditorJsImageBlock
|
|
20
|
+
| EditorJsDelimiterBlock
|
|
21
|
+
| EditorJsTableBlock
|
|
22
|
+
| EditorJsCodeBlock
|
|
23
|
+
| EditorJsGenericBlock;
|
|
24
|
+
export type EditorJsDoc = { time?: number; blocks?: EditorJsBlock[]; version?: string };
|
|
25
|
+
|
|
26
|
+
export function parseEditorJsToSections(content: string | null | undefined) {
|
|
27
|
+
if (!content) return null;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(content) as EditorJsDoc;
|
|
30
|
+
const blocks = parsed.blocks || [];
|
|
31
|
+
if (!blocks.length) return null;
|
|
32
|
+
|
|
33
|
+
const sections: Array<{ title: string; description: React.ReactNode }> = [];
|
|
34
|
+
let currentTitle = "";
|
|
35
|
+
let currentContent: React.ReactNode[] = [];
|
|
36
|
+
|
|
37
|
+
const flushCurrentSection = () => {
|
|
38
|
+
if (currentTitle && currentContent.length > 0) {
|
|
39
|
+
sections.push({
|
|
40
|
+
title: currentTitle,
|
|
41
|
+
description: (
|
|
42
|
+
<div className="space-y-4 [&_a]:text-blue-600 [&_a]:underline [&_a]:font-medium [&_a:hover]:text-blue-800 [&_a:hover]:no-underline [&_a]:transition-colors [&_a]:duration-200">
|
|
43
|
+
{currentContent.map((content, idx) => (
|
|
44
|
+
<div key={idx}>{content}</div>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
});
|
|
49
|
+
currentContent = [];
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
blocks.forEach((b: EditorJsBlock, idx: number) => {
|
|
54
|
+
const key = b.id || String(idx);
|
|
55
|
+
|
|
56
|
+
if (b.type === "header") {
|
|
57
|
+
const headerData = (b as EditorJsHeaderBlock).data;
|
|
58
|
+
const level = Math.min(6, Math.max(1, Number(headerData?.level) || 2));
|
|
59
|
+
const html = String(headerData?.text || "");
|
|
60
|
+
|
|
61
|
+
// H3 starts a new section
|
|
62
|
+
if (level === 3) {
|
|
63
|
+
flushCurrentSection();
|
|
64
|
+
// Strip HTML tags for the title, but keep the HTML for rendering
|
|
65
|
+
currentTitle = html.replace(/<[^>]*>/g, "");
|
|
66
|
+
} else if (level === 1) {
|
|
67
|
+
// H1 is the main title, skip it as it's already in the page heading
|
|
68
|
+
return;
|
|
69
|
+
} else {
|
|
70
|
+
// Other headers become content
|
|
71
|
+
if (level === 2) {
|
|
72
|
+
currentContent.push(<h2 key={key} dangerouslySetInnerHTML={{ __html: html }} />);
|
|
73
|
+
} else if (level === 4) {
|
|
74
|
+
currentContent.push(<h4 key={key} dangerouslySetInnerHTML={{ __html: html }} />);
|
|
75
|
+
} else if (level === 5) {
|
|
76
|
+
currentContent.push(<h5 key={key} dangerouslySetInnerHTML={{ __html: html }} />);
|
|
77
|
+
} else {
|
|
78
|
+
currentContent.push(<h6 key={key} dangerouslySetInnerHTML={{ __html: html }} />);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} else if (b.type === "paragraph") {
|
|
82
|
+
const paragraphData = (b as EditorJsParagraphBlock).data;
|
|
83
|
+
const html = String(paragraphData?.text || "");
|
|
84
|
+
currentContent.push(<p key={key} dangerouslySetInnerHTML={{ __html: html }} />);
|
|
85
|
+
} else if (b.type === "list") {
|
|
86
|
+
const listData = (b as EditorJsListBlock).data || {};
|
|
87
|
+
const style = listData.style === "ordered" ? "ol" : "ul";
|
|
88
|
+
const items: string[] = Array.isArray(listData.items) ? listData.items : [];
|
|
89
|
+
if (style === "ol") {
|
|
90
|
+
currentContent.push(
|
|
91
|
+
<ol key={key} className="list-inside list-decimal">
|
|
92
|
+
{items.map((it, i) => (
|
|
93
|
+
<li key={i} dangerouslySetInnerHTML={{ __html: String(it) }} />
|
|
94
|
+
))}
|
|
95
|
+
</ol>
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
currentContent.push(
|
|
99
|
+
<ul key={key} className="list-inside list-disc">
|
|
100
|
+
{items.map((it, i) => (
|
|
101
|
+
<li key={i} dangerouslySetInnerHTML={{ __html: String(it) }} />
|
|
102
|
+
))}
|
|
103
|
+
</ul>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
} else if (b.type === "quote") {
|
|
107
|
+
const quoteData = (b as EditorJsQuoteBlock).data || {};
|
|
108
|
+
const text = String(quoteData.text || "");
|
|
109
|
+
const caption = String(quoteData.caption || "");
|
|
110
|
+
const alignment = quoteData.alignment || "left";
|
|
111
|
+
currentContent.push(
|
|
112
|
+
<blockquote
|
|
113
|
+
key={key}
|
|
114
|
+
className={`border-l-4 border-gray-300 pl-4 py-2 ${alignment === "center" ? "text-center" : ""} italic text-gray-700`}
|
|
115
|
+
>
|
|
116
|
+
<div dangerouslySetInnerHTML={{ __html: text }} />
|
|
117
|
+
{caption ? (
|
|
118
|
+
<cite
|
|
119
|
+
className="block mt-2 text-sm text-gray-500 not-italic"
|
|
120
|
+
dangerouslySetInnerHTML={{ __html: caption }}
|
|
121
|
+
/>
|
|
122
|
+
) : null}
|
|
123
|
+
</blockquote>
|
|
124
|
+
);
|
|
125
|
+
} else if (b.type === "linkTool") {
|
|
126
|
+
const linkData = (b as EditorJsLinkBlock).data || {};
|
|
127
|
+
const link = linkData.link || "";
|
|
128
|
+
const title = linkData.meta?.title || link;
|
|
129
|
+
const description = linkData.meta?.description || "";
|
|
130
|
+
const image = linkData.meta?.image?.url || "";
|
|
131
|
+
currentContent.push(
|
|
132
|
+
<div key={key} className="border-2 border-blue-200 bg-blue-50 rounded-lg p-4 hover:border-blue-300 hover:bg-blue-100 transition-all duration-200">
|
|
133
|
+
<a href={link} target="_blank" rel="noopener noreferrer" className="block group">
|
|
134
|
+
<div className="flex gap-4">
|
|
135
|
+
{image && (
|
|
136
|
+
<img
|
|
137
|
+
src={image}
|
|
138
|
+
alt={title}
|
|
139
|
+
className="w-16 h-16 object-cover rounded flex-shrink-0 border border-blue-200"
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
142
|
+
<div className="flex-1">
|
|
143
|
+
<h4 className="font-bold text-blue-700 group-hover:text-blue-900 line-clamp-2 text-lg">{title}</h4>
|
|
144
|
+
{description && (
|
|
145
|
+
<p className="text-sm text-gray-700 mt-1 line-clamp-2 font-medium">{description}</p>
|
|
146
|
+
)}
|
|
147
|
+
<div className="flex items-center mt-2">
|
|
148
|
+
<svg className="w-4 h-4 text-blue-500 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
149
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
150
|
+
</svg>
|
|
151
|
+
<p className="text-xs text-blue-600 font-medium">{new URL(link).hostname}</p>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</a>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
} else if (b.type === "image") {
|
|
159
|
+
const imageData = (b as EditorJsImageBlock).data || {};
|
|
160
|
+
const url = imageData.file?.url || "";
|
|
161
|
+
const caption = imageData.caption || "";
|
|
162
|
+
const withBorder = imageData.withBorder;
|
|
163
|
+
const withBackground = imageData.withBackground;
|
|
164
|
+
const stretched = imageData.stretched;
|
|
165
|
+
if (url) {
|
|
166
|
+
currentContent.push(
|
|
167
|
+
<figure key={key} className={`${stretched ? "w-full" : "max-w-2xl mx-auto"}`}>
|
|
168
|
+
<img
|
|
169
|
+
src={url}
|
|
170
|
+
alt={caption || "Image"}
|
|
171
|
+
className={`w-full h-auto rounded-lg ${withBorder ? "border border-gray-200" : ""} ${withBackground ? "bg-gray-50 p-4" : ""}`}
|
|
172
|
+
/>
|
|
173
|
+
{caption && (
|
|
174
|
+
<figcaption className="text-sm text-gray-600 text-center mt-2 italic">
|
|
175
|
+
{caption}
|
|
176
|
+
</figcaption>
|
|
177
|
+
)}
|
|
178
|
+
</figure>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
} else if (b.type === "delimiter") {
|
|
182
|
+
currentContent.push(
|
|
183
|
+
<div key={key} className="flex justify-center py-6">
|
|
184
|
+
<div className="flex space-x-1">
|
|
185
|
+
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
|
|
186
|
+
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
|
|
187
|
+
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
} else if (b.type === "table") {
|
|
192
|
+
const tableData = (b as EditorJsTableBlock).data || {};
|
|
193
|
+
const withHeadings = tableData.withHeadings || false;
|
|
194
|
+
const content = tableData.content || [];
|
|
195
|
+
if (content.length > 0) {
|
|
196
|
+
currentContent.push(
|
|
197
|
+
<div key={key} className="overflow-x-auto">
|
|
198
|
+
<table className="min-w-full border-collapse border border-gray-200">
|
|
199
|
+
{withHeadings && content[0] && (
|
|
200
|
+
<thead>
|
|
201
|
+
<tr className="bg-gray-50">
|
|
202
|
+
{content[0].map((cell, cellIdx) => (
|
|
203
|
+
<th
|
|
204
|
+
key={cellIdx}
|
|
205
|
+
className="border border-gray-200 px-4 py-2 text-left font-semibold text-gray-900"
|
|
206
|
+
>
|
|
207
|
+
{cell}
|
|
208
|
+
</th>
|
|
209
|
+
))}
|
|
210
|
+
</tr>
|
|
211
|
+
</thead>
|
|
212
|
+
)}
|
|
213
|
+
<tbody>
|
|
214
|
+
{content.slice(withHeadings ? 1 : 0).map((row, rowIdx) => (
|
|
215
|
+
<tr key={rowIdx} className={rowIdx % 2 === 0 ? "bg-white" : "bg-gray-50"}>
|
|
216
|
+
{row.map((cell, cellIdx) => (
|
|
217
|
+
<td
|
|
218
|
+
key={cellIdx}
|
|
219
|
+
className="border border-gray-200 px-4 py-2 text-gray-700"
|
|
220
|
+
>
|
|
221
|
+
{cell}
|
|
222
|
+
</td>
|
|
223
|
+
))}
|
|
224
|
+
</tr>
|
|
225
|
+
))}
|
|
226
|
+
</tbody>
|
|
227
|
+
</table>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
} else if (b.type === "code") {
|
|
232
|
+
const codeData = (b as EditorJsCodeBlock).data || {};
|
|
233
|
+
const code = codeData.code || "";
|
|
234
|
+
currentContent.push(
|
|
235
|
+
<pre key={key} className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
|
|
236
|
+
<code className="text-sm">{code}</code>
|
|
237
|
+
</pre>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Flush the last section
|
|
243
|
+
flushCurrentSection();
|
|
244
|
+
|
|
245
|
+
return sections;
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
export type Theme = "asphalt";
|
|
5
|
+
|
|
6
|
+
export function cn(...inputs: ClassValue[]) {
|
|
7
|
+
return twMerge(clsx(inputs));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// utils/function.ts
|
|
11
|
+
|
|
12
|
+
export const EXTENSIONS = ["webp", "png", "jpeg", "jpg"] as const;
|
|
13
|
+
|
|
14
|
+
/** Remove trailing slashes */
|
|
15
|
+
export function trimSlash(s?: string | null): string {
|
|
16
|
+
return (s || "").replace(/\/+$/, "");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Strip known image extensions from a URL */
|
|
20
|
+
export function stripImageExt(url: string): string {
|
|
21
|
+
return url.replace(/\.(webp|png|jpe?g)$/i, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SrcRoot =
|
|
25
|
+
| string
|
|
26
|
+
| {
|
|
27
|
+
basePath: string;
|
|
28
|
+
fileName: string; // without extension
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build candidate URLs in preferred extension order.
|
|
33
|
+
*/
|
|
34
|
+
export function getImageCandidates(
|
|
35
|
+
input: SrcRoot,
|
|
36
|
+
extensions: readonly string[] = EXTENSIONS
|
|
37
|
+
): string[] {
|
|
38
|
+
if (typeof input === "string") {
|
|
39
|
+
const root = stripImageExt(input);
|
|
40
|
+
return extensions.map((ext) => `${root}.${ext}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const basePath = trimSlash(input.basePath);
|
|
44
|
+
const fileRoot = `${basePath}/${input.fileName}`;
|
|
45
|
+
return extensions.map((ext) => `${fileRoot}.${ext}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Normalize a Saleor product slug for URL display by replacing multiple consecutive dashes with single dash.
|
|
50
|
+
*
|
|
51
|
+
* Saleor often generates slugs with multiple dashes (e.g., --- or --) that should be
|
|
52
|
+
* displayed as single dashes in URLs for better readability.
|
|
53
|
+
*
|
|
54
|
+
* @example normalizeSlugForUrl("aero-exhaust---blue-flame-dual-tip---30")
|
|
55
|
+
* // Returns: "aero-exhaust-blue-flame-dual-tip-30"
|
|
56
|
+
*/
|
|
57
|
+
export function normalizeSlugForUrl(slug: string): string {
|
|
58
|
+
if (!slug) return "";
|
|
59
|
+
|
|
60
|
+
// Replace 2 or more consecutive dashes with a single dash
|
|
61
|
+
return slug.replace(/-{2,}/g, "-");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Denormalize a URL slug back to Saleor's format for API queries.
|
|
66
|
+
*
|
|
67
|
+
* Since we normalized the slug by replacing multiple consecutive dashes with single dashes,
|
|
68
|
+
* we simply return the slug as-is because Saleor should be able to match it.
|
|
69
|
+
*
|
|
70
|
+
* If Saleor can't match the normalized slug, consider using product ID for queries instead.
|
|
71
|
+
*
|
|
72
|
+
* @example denormalizeSlugForApi("aero-exhaust-blue-flame-dual-tip-30-outlet-30497695")
|
|
73
|
+
* // Returns: "aero-exhaust-blue-flame-dual-tip-30-outlet-30497695" (as-is)
|
|
74
|
+
*/
|
|
75
|
+
export function denormalizeSlugForApi(urlSlug: string): string {
|
|
76
|
+
if (!urlSlug) return "";
|
|
77
|
+
|
|
78
|
+
// Return the slug as-is. Saleor's GraphQL API should handle slug matching
|
|
79
|
+
// flexibly enough to match both normalized and non-normalized formats.
|
|
80
|
+
return urlSlug;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create an SEO-friendly product URL with normalized slug + product ID.
|
|
85
|
+
* Format: /product/{normalized-slug}-{product-id}
|
|
86
|
+
* @example createProductUrl("aero-exhaust---turbine", "UHJvZHVjdDozMDQ5NzYwNg==")
|
|
87
|
+
* Returns: "aero-exhaust-turbine-UHJvZHVjdDozMDQ5NzYwNg"
|
|
88
|
+
*/
|
|
89
|
+
export function createProductUrl(slug: string, productId: string): string {
|
|
90
|
+
const normalizedSlug = normalizeSlugForUrl(slug);
|
|
91
|
+
// Append product ID to the end for reliable querying
|
|
92
|
+
return `${normalizedSlug}-${productId}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extract product ID from a Saleor slug that has numeric ID at the end.
|
|
97
|
+
* Saleor slugs often end with the product ID number.
|
|
98
|
+
* @example extractProductIdFromSlug("aero-exhaust---turbine-30497612")
|
|
99
|
+
* Returns: "30497612"
|
|
100
|
+
*/
|
|
101
|
+
export function extractProductIdFromSlug(slug: string): string | null {
|
|
102
|
+
if (!slug) return null;
|
|
103
|
+
|
|
104
|
+
// Match numeric ID at the end of the slug
|
|
105
|
+
const match = slug.match(/-(\d+)$/);
|
|
106
|
+
|
|
107
|
+
if (match && match[1]) {
|
|
108
|
+
return match[1];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Remove the product ID from the end of a Saleor slug.
|
|
116
|
+
* @example removeProductIdFromSlug("aero-exhaust---turbine-30497612")
|
|
117
|
+
* Returns: "aero-exhaust---turbine"
|
|
118
|
+
*/
|
|
119
|
+
export function removeProductIdFromSlug(slug: string): string {
|
|
120
|
+
if (!slug) return "";
|
|
121
|
+
|
|
122
|
+
// Remove numeric ID from the end
|
|
123
|
+
return slug.replace(/-\d+$/, "");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Ensures image URL has the S3 base URL if it's a relative path
|
|
128
|
+
* @param imageUrl - The image URL (can be relative or absolute)
|
|
129
|
+
* @returns Full image URL with S3 base if needed
|
|
130
|
+
*/
|
|
131
|
+
export function getFullImageUrl(imageUrl: string | null | undefined): string {
|
|
132
|
+
if (!imageUrl) return "";
|
|
133
|
+
|
|
134
|
+
// If already has protocol (http:// or https://), return as is
|
|
135
|
+
if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) {
|
|
136
|
+
return imageUrl;
|
|
137
|
+
}
|
|
138
|
+
if (imageUrl.endsWith("/no-image-avail-large.png")) {
|
|
139
|
+
return imageUrl;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// If it's a relative path, prepend S3 base URL
|
|
143
|
+
const S3_BASE_URL = "https://wsm-saleor-assets.s3.us-west-2.amazonaws.com";
|
|
144
|
+
const cleanPath = imageUrl.startsWith("/") ? imageUrl.slice(1) : imageUrl;
|
|
145
|
+
return `${S3_BASE_URL}/${cleanPath}`;
|
|
146
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { sendGAEvent } from '@next/third-parties/google';
|
|
4
|
+
|
|
5
|
+
// GA_MEASUREMENT_ID is now obtained from app configuration service, not process.env
|
|
6
|
+
let GA_MEASUREMENT_ID: string | null = null;
|
|
7
|
+
|
|
8
|
+
// Function to get GA Measurement ID from app configuration
|
|
9
|
+
export const getGAMeasurementId = async (): Promise<string | null> => {
|
|
10
|
+
if (GA_MEASUREMENT_ID) return GA_MEASUREMENT_ID;
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// Import app config service dynamically to avoid circular dependencies
|
|
14
|
+
const { appConfigService } = await import('./appConfiguration');
|
|
15
|
+
await appConfigService.fetchConfiguration();
|
|
16
|
+
const gaConfig = appConfigService.getGoogleAnalyticsConfig();
|
|
17
|
+
GA_MEASUREMENT_ID = gaConfig?.measurement_id || null;
|
|
18
|
+
return GA_MEASUREMENT_ID;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error('Failed to get GA configuration:', error);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// For client-side components that use useAppConfiguration hook
|
|
26
|
+
export const getGAMeasurementIdFromContext = (gaConfig: { measurement_id: string } | null): string | null => {
|
|
27
|
+
return gaConfig?.measurement_id || null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Legacy support - will be deprecated
|
|
31
|
+
export { GA_MEASUREMENT_ID };
|
|
32
|
+
|
|
33
|
+
export const gtag = (...args: unknown[]) => {
|
|
34
|
+
if (typeof window !== "undefined" && window.gtag) {
|
|
35
|
+
window.gtag(...args);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
export const trackPageView = (url: string, title?: string, measurementId?: string | null) => {
|
|
41
|
+
// Use provided measurementId or fall back to legacy GA_MEASUREMENT_ID
|
|
42
|
+
const activeMeasurementId = measurementId !== undefined ? measurementId : GA_MEASUREMENT_ID;
|
|
43
|
+
if (!activeMeasurementId) return;
|
|
44
|
+
|
|
45
|
+
sendGAEvent({
|
|
46
|
+
event: 'page_view',
|
|
47
|
+
page_title: title || document.title,
|
|
48
|
+
page_location: url,
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const trackEvent = (action: string, parameters?: Record<string, unknown>, measurementId?: string | null) => {
|
|
53
|
+
const activeMeasurementId = measurementId !== undefined ? measurementId : GA_MEASUREMENT_ID;
|
|
54
|
+
if (!activeMeasurementId) return;
|
|
55
|
+
|
|
56
|
+
sendGAEvent({
|
|
57
|
+
event: action,
|
|
58
|
+
...parameters,
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const trackCustomEvent = (eventName: string, parameters?: Record<string, unknown>, measurementId?: string | null) => {
|
|
63
|
+
const activeMeasurementId = measurementId !== undefined ? measurementId : GA_MEASUREMENT_ID;
|
|
64
|
+
if (!activeMeasurementId) return;
|
|
65
|
+
|
|
66
|
+
sendGAEvent({
|
|
67
|
+
event: eventName,
|
|
68
|
+
custom_parameter: true,
|
|
69
|
+
...parameters,
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const trackUserEngagement = (engagementTimeMs: number, measurementId?: string | null) => {
|
|
74
|
+
const activeMeasurementId = measurementId !== undefined ? measurementId : GA_MEASUREMENT_ID;
|
|
75
|
+
if (!activeMeasurementId) return;
|
|
76
|
+
|
|
77
|
+
sendGAEvent({
|
|
78
|
+
event: 'user_engagement',
|
|
79
|
+
engagement_time_msec: engagementTimeMs,
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const trackScrollDepth = (scrollPercent: number, measurementId?: string | null) => {
|
|
84
|
+
const activeMeasurementId = measurementId !== undefined ? measurementId : GA_MEASUREMENT_ID;
|
|
85
|
+
if (!activeMeasurementId) return;
|
|
86
|
+
|
|
87
|
+
sendGAEvent({
|
|
88
|
+
event: 'scroll',
|
|
89
|
+
percent_scrolled: scrollPercent,
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const trackOutboundClick = (url: string, linkText?: string, measurementId?: string | null) => {
|
|
94
|
+
const activeMeasurementId = measurementId !== undefined ? measurementId : GA_MEASUREMENT_ID;
|
|
95
|
+
if (!activeMeasurementId) return;
|
|
96
|
+
|
|
97
|
+
sendGAEvent({
|
|
98
|
+
event: 'click',
|
|
99
|
+
event_category: 'outbound',
|
|
100
|
+
event_label: url,
|
|
101
|
+
link_text: linkText,
|
|
102
|
+
transport_type: 'beacon',
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const trackFileDownload = (fileName: string, fileType?: string, measurementId?: string | null) => {
|
|
107
|
+
const activeMeasurementId = measurementId !== undefined ? measurementId : GA_MEASUREMENT_ID;
|
|
108
|
+
if (!activeMeasurementId) return;
|
|
109
|
+
|
|
110
|
+
sendGAEvent({
|
|
111
|
+
event: 'file_download',
|
|
112
|
+
file_name: fileName,
|
|
113
|
+
file_extension: fileType,
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const trackSiteSearch = (searchTerm: string, resultCount?: number, measurementId?: string | null) => {
|
|
118
|
+
const activeMeasurementId = measurementId !== undefined ? measurementId : GA_MEASUREMENT_ID;
|
|
119
|
+
if (!activeMeasurementId) return;
|
|
120
|
+
|
|
121
|
+
sendGAEvent({
|
|
122
|
+
event: 'search',
|
|
123
|
+
search_term: searchTerm,
|
|
124
|
+
number_of_results: resultCount,
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const trackTimingEvent = (name: string, value: number, category?: string, measurementId?: string | null) => {
|
|
129
|
+
const activeMeasurementId = measurementId !== undefined ? measurementId : GA_MEASUREMENT_ID;
|
|
130
|
+
if (!activeMeasurementId) return;
|
|
131
|
+
|
|
132
|
+
sendGAEvent({
|
|
133
|
+
event: 'timing_complete',
|
|
134
|
+
name: name,
|
|
135
|
+
value: value,
|
|
136
|
+
event_category: category || 'performance',
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const trackEcommerce = (action: string, parameters: Record<string, unknown>, measurementId?: string | null) => {
|
|
141
|
+
const activeMeasurementId = measurementId !== undefined ? measurementId : GA_MEASUREMENT_ID;
|
|
142
|
+
if (!activeMeasurementId) return;
|
|
143
|
+
|
|
144
|
+
sendGAEvent({
|
|
145
|
+
event: action,
|
|
146
|
+
...parameters,
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const setUserProperties = (properties: Record<string, unknown>, measurementId?: string | null) => {
|
|
151
|
+
const activeMeasurementId = measurementId !== undefined ? measurementId : GA_MEASUREMENT_ID;
|
|
152
|
+
if (!activeMeasurementId) return;
|
|
153
|
+
|
|
154
|
+
gtag("config", activeMeasurementId, {
|
|
155
|
+
user_properties: properties,
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const trackException = (description: string, fatal = false, measurementId?: string | null) => {
|
|
160
|
+
const activeMeasurementId = measurementId !== undefined ? measurementId : GA_MEASUREMENT_ID;
|
|
161
|
+
if (!activeMeasurementId) return;
|
|
162
|
+
|
|
163
|
+
sendGAEvent({
|
|
164
|
+
event: 'exception',
|
|
165
|
+
description: description,
|
|
166
|
+
fatal: fatal,
|
|
167
|
+
});
|
|
168
|
+
};
|