@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,752 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CHECKOUT_CREATE } from '@/graphql/mutations/checkoutCreate';
|
|
4
|
+
import { ME_ADDRESSES_QUERY, type MeAddressesData } from '@/graphql/queries/meAddresses';
|
|
5
|
+
import { useGlobalStore } from '@/store/useGlobalStore';
|
|
6
|
+
import { useQuery } from '@apollo/client';
|
|
7
|
+
import Image from 'next/image';
|
|
8
|
+
import { useRouter } from 'next/navigation';
|
|
9
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
10
|
+
import Breadcrumb from '../components/reuseableUI/breadcrumb';
|
|
11
|
+
import CommonButton from '../components/reuseableUI/commonButton';
|
|
12
|
+
import EmptyState from '../components/reuseableUI/emptyState';
|
|
13
|
+
import { ArrowIcon } from '../utils/svgs/arrowIcon';
|
|
14
|
+
import { CartIcon } from '../utils/svgs/cart/cartIcon';
|
|
15
|
+
import { PlusIcon } from '../utils/svgs/cart/plusIcon';
|
|
16
|
+
import { SubtractIcon } from '../utils/svgs/cart/subtractIcon';
|
|
17
|
+
import { gtmRemoveFromCart, gtmViewCart, gtmAddToCart, gtmBeginCheckout, Product } from '../utils/googleTagManager';
|
|
18
|
+
import { useAppConfiguration } from '../components/providers/ServerAppConfigurationProvider';
|
|
19
|
+
import { getSaleorApiUrl } from '@/lib/saleor/getSaleorApiUrl';
|
|
20
|
+
|
|
21
|
+
type CartItemOption = {
|
|
22
|
+
variantId: string;
|
|
23
|
+
name: string;
|
|
24
|
+
price: number;
|
|
25
|
+
optionSetName: string;
|
|
26
|
+
optionSetLabel: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type CartItem = {
|
|
30
|
+
key: string;
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
price: number;
|
|
34
|
+
image: string;
|
|
35
|
+
quantity: number;
|
|
36
|
+
category?: string;
|
|
37
|
+
sku?: string;
|
|
38
|
+
selectedOptions?: CartItemOption[];
|
|
39
|
+
customInputs?: Record<string, string>;
|
|
40
|
+
skipBaseProduct?: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Helper to calculate item total including options
|
|
44
|
+
const getItemTotal = (item: CartItem): number => {
|
|
45
|
+
let total = item.price;
|
|
46
|
+
if (item.selectedOptions?.length) {
|
|
47
|
+
total += item.selectedOptions.reduce((sum, opt) => sum + opt.price, 0);
|
|
48
|
+
}
|
|
49
|
+
return total;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default function CartPage() {
|
|
53
|
+
const { cartItems: items, totalAmount, removeFromCart, updateQuantity, addToCart, checkoutId, setCheckoutId, setCheckoutToken, isLoggedIn, user, guestEmail, guestShippingInfo } = useGlobalStore();
|
|
54
|
+
const { getGoogleTagManagerConfig } = useAppConfiguration();
|
|
55
|
+
const [refreshedItems, setRefreshedItems] = useState<CartItem[]>([]);
|
|
56
|
+
const [refreshedTotals, setRefreshedTotals] = useState({ totalItems: 0, totalAmount: 0 });
|
|
57
|
+
const [pricesRefreshed, setPricesRefreshed] = useState(false);
|
|
58
|
+
const [loadingItems, setLoadingItems] = useState<{ [key: string]: { plus: boolean; minus: boolean; remove: boolean } }>({});
|
|
59
|
+
|
|
60
|
+
const gtmConfig = getGoogleTagManagerConfig();
|
|
61
|
+
|
|
62
|
+
const router = useRouter();
|
|
63
|
+
const [creating, setCreating] = useState(false);
|
|
64
|
+
const [error, setError] = useState<string | null>(null);
|
|
65
|
+
|
|
66
|
+
// When logged in, fetch account addresses
|
|
67
|
+
const { data: meData, loading: meLoading } = useQuery<MeAddressesData>(ME_ADDRESSES_QUERY, { skip: !isLoggedIn });
|
|
68
|
+
const accountShipping = useMemo(() => {
|
|
69
|
+
const me = meData?.me;
|
|
70
|
+
if (!me || !me.addresses?.length) return null;
|
|
71
|
+
const defId = me.defaultShippingAddress?.id;
|
|
72
|
+
return (defId ? me.addresses.find((a: { id: string }) => a.id === defId) : me.addresses[0]) || null;
|
|
73
|
+
}, [meData]);
|
|
74
|
+
const accountBilling = useMemo(() => {
|
|
75
|
+
const me = meData?.me;
|
|
76
|
+
if (!me || !me.addresses?.length) return null;
|
|
77
|
+
const defId = me.defaultBillingAddress?.id;
|
|
78
|
+
return (defId ? me.addresses.find((a: { id: string }) => a.id === defId) : accountShipping || me.addresses[0]) || null;
|
|
79
|
+
}, [meData, accountShipping]);
|
|
80
|
+
|
|
81
|
+
const endpoint = getSaleorApiUrl();
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
// Refresh prices from checkout if available to ensure we show discounted prices
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const refreshPricesFromCheckout = async () => {
|
|
87
|
+
if (!checkoutId || pricesRefreshed || items.length === 0) return;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const response = await fetch(endpoint, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
query: `
|
|
95
|
+
query GetCheckoutDetails($id: ID!) {
|
|
96
|
+
checkout(id: $id) {
|
|
97
|
+
id
|
|
98
|
+
totalPrice { gross { amount currency } }
|
|
99
|
+
subtotalPrice { gross { amount currency } }
|
|
100
|
+
lines {
|
|
101
|
+
id
|
|
102
|
+
quantity
|
|
103
|
+
totalPrice { gross { amount currency } }
|
|
104
|
+
variant {
|
|
105
|
+
id
|
|
106
|
+
name
|
|
107
|
+
product {
|
|
108
|
+
name
|
|
109
|
+
thumbnail { url }
|
|
110
|
+
pricing {
|
|
111
|
+
discount { gross { amount currency } }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
pricing {
|
|
115
|
+
price { gross { amount currency } }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
`,
|
|
122
|
+
variables: { id: checkoutId }
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (response.ok) {
|
|
127
|
+
const data = await response.json();
|
|
128
|
+
const checkout = data?.data?.checkout;
|
|
129
|
+
|
|
130
|
+
if (checkout?.lines?.length > 0) {
|
|
131
|
+
|
|
132
|
+
// Calculate unit prices - use variant pricing (discounted) instead of line total
|
|
133
|
+
type CheckoutLine = {
|
|
134
|
+
id: string;
|
|
135
|
+
quantity: number;
|
|
136
|
+
variant: {
|
|
137
|
+
id: string;
|
|
138
|
+
product: {
|
|
139
|
+
name: string;
|
|
140
|
+
pricing?: {
|
|
141
|
+
discount?: {
|
|
142
|
+
gross?: {
|
|
143
|
+
amount: number;
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
pricing?: {
|
|
149
|
+
price?: {
|
|
150
|
+
gross?: {
|
|
151
|
+
amount: number;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
name: string;
|
|
156
|
+
};
|
|
157
|
+
totalPrice: {
|
|
158
|
+
gross: {
|
|
159
|
+
amount: number;
|
|
160
|
+
currency: string;
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
name: string;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Build a map of checkout lines by variant ID for quick lookup
|
|
167
|
+
const checkoutLinesByVariantId = new Map<string, CheckoutLine>();
|
|
168
|
+
for (const line of checkout.lines) {
|
|
169
|
+
checkoutLinesByVariantId.set(line.variant.id, line);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const updatedItems: CartItem[] = items
|
|
173
|
+
.map((item: CartItem) => {
|
|
174
|
+
if (item.skipBaseProduct && item.selectedOptions?.length) {
|
|
175
|
+
// For items with skipBaseProduct, match using the first selectedOption's variant
|
|
176
|
+
const firstOptionLine = checkoutLinesByVariantId.get(item.selectedOptions[0].variantId);
|
|
177
|
+
if (!firstOptionLine) return null;
|
|
178
|
+
|
|
179
|
+
// Calculate total price from all option lines
|
|
180
|
+
let optionsTotalPrice = 0;
|
|
181
|
+
const updatedOptions = item.selectedOptions.map(opt => {
|
|
182
|
+
const optLine = checkoutLinesByVariantId.get(opt.variantId);
|
|
183
|
+
if (optLine) {
|
|
184
|
+
const lineTotal = optLine.totalPrice?.gross?.amount ?? 0;
|
|
185
|
+
const qty = Math.max(1, optLine.quantity);
|
|
186
|
+
const unitPrice = lineTotal / qty;
|
|
187
|
+
optionsTotalPrice += unitPrice;
|
|
188
|
+
return { ...opt, price: unitPrice };
|
|
189
|
+
}
|
|
190
|
+
return opt;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
...item,
|
|
195
|
+
price: 0, // Base product not included
|
|
196
|
+
// Don't update quantity from Saleor - use the grouped quantity from store
|
|
197
|
+
// Saleor combines lines, so line.quantity would be wrong for grouped items
|
|
198
|
+
selectedOptions: updatedOptions,
|
|
199
|
+
};
|
|
200
|
+
} else {
|
|
201
|
+
// Normal item - match by item id (base product variant)
|
|
202
|
+
const line = checkoutLinesByVariantId.get(item.id);
|
|
203
|
+
if (!line) return null;
|
|
204
|
+
|
|
205
|
+
// Use line totalPrice divided by quantity to get the actual unit price
|
|
206
|
+
const lineTotal = line?.totalPrice?.gross?.amount ?? 0;
|
|
207
|
+
const qty = Math.max(1, line.quantity);
|
|
208
|
+
const unitPrice = lineTotal / qty;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
...item,
|
|
212
|
+
price: unitPrice,
|
|
213
|
+
// Don't update quantity from Saleor - use the grouped quantity from store
|
|
214
|
+
// Saleor combines lines, so line.quantity would be wrong for grouped items
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
.filter(Boolean) as CartItem[];
|
|
219
|
+
|
|
220
|
+
const totalItems = updatedItems.reduce((sum, item) => sum + item.quantity, 0);
|
|
221
|
+
const totalAmount = updatedItems.reduce((sum, item) => sum + (getItemTotal(item) * item.quantity), 0);
|
|
222
|
+
|
|
223
|
+
setRefreshedItems(updatedItems);
|
|
224
|
+
setRefreshedTotals({ totalItems, totalAmount });
|
|
225
|
+
setPricesRefreshed(true);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error('[Cart] Failed to refresh prices from checkout:', error);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
refreshPricesFromCheckout();
|
|
234
|
+
}, [checkoutId, endpoint, items, pricesRefreshed]);
|
|
235
|
+
|
|
236
|
+
// Reset prices refreshed state when global cart items change (e.g., when items are removed)
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
if (pricesRefreshed && refreshedItems.length > 0) {
|
|
239
|
+
// Check if any items in refreshedItems no longer exist in the global store (by key)
|
|
240
|
+
const hasRemovedItems = refreshedItems.some(refreshedItem =>
|
|
241
|
+
!items.find(globalItem => globalItem.key === refreshedItem.key)
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Check if global items count differs from refreshed items count
|
|
245
|
+
const itemCountDiffers = items.length !== refreshedItems.length;
|
|
246
|
+
|
|
247
|
+
if (hasRemovedItems || itemCountDiffers) {
|
|
248
|
+
setPricesRefreshed(false);
|
|
249
|
+
setRefreshedItems([]);
|
|
250
|
+
setRefreshedTotals({ totalItems: 0, totalAmount: 0 });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}, [items, refreshedItems, pricesRefreshed]);
|
|
254
|
+
|
|
255
|
+
// Force refresh when cart becomes empty but still has cached items
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (items.length === 0 && (refreshedItems.length > 0 || pricesRefreshed)) {
|
|
258
|
+
setPricesRefreshed(false);
|
|
259
|
+
setRefreshedItems([]);
|
|
260
|
+
setRefreshedTotals({ totalItems: 0, totalAmount: 0 });
|
|
261
|
+
}
|
|
262
|
+
}, [items.length, refreshedItems.length, pricesRefreshed]);
|
|
263
|
+
|
|
264
|
+
// Track cart view with GTM
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
const currentItems = pricesRefreshed ? refreshedItems : items;
|
|
267
|
+
if (currentItems.length > 0) {
|
|
268
|
+
const products: Product[] = currentItems.map((item, index) => ({
|
|
269
|
+
item_id: item.id,
|
|
270
|
+
item_name: item.name,
|
|
271
|
+
item_category: item.category || 'Products',
|
|
272
|
+
price: item.price,
|
|
273
|
+
quantity: item.quantity,
|
|
274
|
+
currency: 'USD',
|
|
275
|
+
index: index
|
|
276
|
+
}));
|
|
277
|
+
|
|
278
|
+
const totalValue = currentItems.reduce((sum, item) => sum + (getItemTotal(item) * item.quantity), 0);
|
|
279
|
+
gtmViewCart(products, 'USD', totalValue, gtmConfig?.container_id);
|
|
280
|
+
}
|
|
281
|
+
}, [items, refreshedItems, pricesRefreshed]);
|
|
282
|
+
|
|
283
|
+
const handleProceed = useCallback(async () => {
|
|
284
|
+
setError(null);
|
|
285
|
+
if (isLoggedIn && meLoading) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Track begin_checkout GTM event
|
|
290
|
+
const gtmConfig = getGoogleTagManagerConfig();
|
|
291
|
+
if (displayItems.length > 0) {
|
|
292
|
+
const products: Product[] = displayItems.map((item, index) => ({
|
|
293
|
+
item_id: item.id,
|
|
294
|
+
item_name: item.name,
|
|
295
|
+
item_category: item.category || 'Products',
|
|
296
|
+
price: item.price,
|
|
297
|
+
quantity: item.quantity,
|
|
298
|
+
currency: 'USD',
|
|
299
|
+
index: index
|
|
300
|
+
}));
|
|
301
|
+
const totalValue = displayItems.reduce((sum, item) => sum + (getItemTotal(item) * item.quantity), 0);
|
|
302
|
+
gtmBeginCheckout(products, 'USD', totalValue, undefined, gtmConfig?.container_id);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// If we already have a checkout, use it instead of creating a new one
|
|
306
|
+
if (checkoutId) {
|
|
307
|
+
router.push(`/checkout?checkoutId=${encodeURIComponent(checkoutId)}`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Otherwise, create a new checkout
|
|
312
|
+
setCreating(true);
|
|
313
|
+
try {
|
|
314
|
+
// Build lines from cart using the item id as variantId directly
|
|
315
|
+
const lines = items.map((it) => ({ quantity: it.quantity, variantId: it.id }));
|
|
316
|
+
if (lines.length === 0) {
|
|
317
|
+
setCreating(false);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const email = (isLoggedIn ? (user?.email || meData?.me?.email || '') : guestEmail) || 'guest@example.com';
|
|
321
|
+
|
|
322
|
+
const mutation = CHECKOUT_CREATE;
|
|
323
|
+
type CheckoutLineInputTS = { variantId: string; quantity: number };
|
|
324
|
+
type CheckoutCreateInputTS = {
|
|
325
|
+
channel: string;
|
|
326
|
+
email: string;
|
|
327
|
+
lines: CheckoutLineInputTS[];
|
|
328
|
+
};
|
|
329
|
+
const input: CheckoutCreateInputTS = {
|
|
330
|
+
channel: process.env.NEXT_PUBLIC_SALEOR_CHANNEL || 'default-channel',
|
|
331
|
+
email,
|
|
332
|
+
lines,
|
|
333
|
+
};
|
|
334
|
+
const variables = { input };
|
|
335
|
+
const res = await fetch(endpoint, {
|
|
336
|
+
method: 'POST',
|
|
337
|
+
headers: { 'Content-Type': 'application/json' },
|
|
338
|
+
body: JSON.stringify({ query: mutation, variables }),
|
|
339
|
+
});
|
|
340
|
+
if (!res.ok) throw new Error('Failed to create checkout');
|
|
341
|
+
const json = await res.json();
|
|
342
|
+
const errs = json.data?.checkoutCreate?.errors;
|
|
343
|
+
if (errs && errs.length) throw new Error(errs[0]?.message || 'Checkout creation error');
|
|
344
|
+
const createdId = json.data?.checkoutCreate?.checkout?.id as string | undefined;
|
|
345
|
+
const createdToken = json.data?.checkoutCreate?.checkout?.token as string | undefined;
|
|
346
|
+
if (!createdId) throw new Error('No checkout id returned');
|
|
347
|
+
setCheckoutId(createdId);
|
|
348
|
+
if (createdToken) {
|
|
349
|
+
setCheckoutToken(createdToken);
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
localStorage.setItem('checkoutId', createdId);
|
|
353
|
+
if (createdToken) localStorage.setItem('checkoutToken', createdToken);
|
|
354
|
+
} catch { }
|
|
355
|
+
router.push(`/checkout?checkoutId=${encodeURIComponent(createdId)}`);
|
|
356
|
+
} catch (e) {
|
|
357
|
+
const msg = e instanceof Error ? e.message : 'Unable to proceed to checkout';
|
|
358
|
+
console.error('[Cart] handleProceed error:', e);
|
|
359
|
+
setError(msg);
|
|
360
|
+
} finally {
|
|
361
|
+
setCreating(false);
|
|
362
|
+
}
|
|
363
|
+
}, [checkoutId, endpoint, guestEmail, isLoggedIn, items, meData, meLoading, router, setCheckoutId, setCheckoutToken, user?.email]);
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
// Use refreshed items and totals if available, otherwise fall back to store values
|
|
367
|
+
const displayItems = pricesRefreshed ? refreshedItems : items;
|
|
368
|
+
const displayTotalAmount = pricesRefreshed ? refreshedTotals.totalAmount : totalAmount;
|
|
369
|
+
|
|
370
|
+
// Enhanced remove and update functions that work with refreshed prices
|
|
371
|
+
// Now uses item.key for all identity operations to support different option sets
|
|
372
|
+
const handleRemoveFromCart = useCallback(async (itemKey: string) => {
|
|
373
|
+
setLoadingItems(prev => ({ ...prev, [itemKey]: { ...prev[itemKey], remove: true } }));
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
// Get item before removing for GTM tracking
|
|
377
|
+
const currentItems = pricesRefreshed ? refreshedItems : items;
|
|
378
|
+
const itemToRemove = currentItems.find(item => item.key === itemKey);
|
|
379
|
+
|
|
380
|
+
if (itemToRemove) {
|
|
381
|
+
// GTM tracking for remove from cart
|
|
382
|
+
const product: Product = {
|
|
383
|
+
item_id: itemToRemove.id,
|
|
384
|
+
item_name: itemToRemove.name,
|
|
385
|
+
item_category: itemToRemove.category || 'Products',
|
|
386
|
+
price: itemToRemove.price,
|
|
387
|
+
quantity: itemToRemove.quantity,
|
|
388
|
+
currency: 'USD'
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
gtmRemoveFromCart([product], 'USD', getItemTotal(itemToRemove) * itemToRemove.quantity, gtmConfig?.container_id);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (pricesRefreshed) {
|
|
395
|
+
// Update both store and local refreshed state
|
|
396
|
+
setRefreshedItems(prev => prev.filter(item => item.key !== itemKey));
|
|
397
|
+
setRefreshedTotals(() => {
|
|
398
|
+
const filteredItems = refreshedItems.filter(item => item.key !== itemKey);
|
|
399
|
+
const totalItems = filteredItems.reduce((sum, item) => sum + item.quantity, 0);
|
|
400
|
+
const totalAmount = filteredItems.reduce((sum, item) => sum + (getItemTotal(item) * item.quantity), 0);
|
|
401
|
+
return { totalItems, totalAmount };
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
removeFromCart(itemKey);
|
|
405
|
+
|
|
406
|
+
// Small delay to ensure backend sync completes
|
|
407
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
408
|
+
} finally {
|
|
409
|
+
setLoadingItems(prev => ({ ...prev, [itemKey]: { ...prev[itemKey], remove: false } }));
|
|
410
|
+
}
|
|
411
|
+
}, [pricesRefreshed, refreshedItems, removeFromCart, items]);
|
|
412
|
+
|
|
413
|
+
const handleUpdateQuantity = useCallback(async (itemKey: string, newQuantity: number) => {
|
|
414
|
+
setLoadingItems(prev => ({ ...prev, [itemKey]: { ...prev[itemKey], minus: true } }));
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
if (pricesRefreshed) {
|
|
418
|
+
// Update both store and local refreshed state
|
|
419
|
+
setRefreshedItems(prev => {
|
|
420
|
+
const updated = prev.map(item =>
|
|
421
|
+
item.key === itemKey
|
|
422
|
+
? { ...item, quantity: Math.max(0, newQuantity) }
|
|
423
|
+
: item
|
|
424
|
+
).filter(item => item.quantity > 0);
|
|
425
|
+
|
|
426
|
+
// Update totals
|
|
427
|
+
const totalItems = updated.reduce((sum, item) => sum + item.quantity, 0);
|
|
428
|
+
const totalAmount = updated.reduce((sum, item) => sum + (getItemTotal(item) * item.quantity), 0);
|
|
429
|
+
setRefreshedTotals({ totalItems, totalAmount });
|
|
430
|
+
|
|
431
|
+
return updated;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
updateQuantity(itemKey, newQuantity);
|
|
435
|
+
|
|
436
|
+
// Small delay to ensure backend sync completes
|
|
437
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
438
|
+
} finally {
|
|
439
|
+
setLoadingItems(prev => ({ ...prev, [itemKey]: { ...prev[itemKey], minus: false } }));
|
|
440
|
+
}
|
|
441
|
+
}, [pricesRefreshed, updateQuantity]);
|
|
442
|
+
|
|
443
|
+
const handleAddToCart = useCallback(async (item: CartItem) => {
|
|
444
|
+
setLoadingItems(prev => ({ ...prev, [item.key]: { ...prev[item.key], plus: true } }));
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
await addToCart({
|
|
448
|
+
key: item.key,
|
|
449
|
+
id: item.id,
|
|
450
|
+
name: item.name,
|
|
451
|
+
price: item.price,
|
|
452
|
+
image: item.image,
|
|
453
|
+
quantity: 1,
|
|
454
|
+
sku: item.sku,
|
|
455
|
+
category: item.category,
|
|
456
|
+
selectedOptions: item.selectedOptions,
|
|
457
|
+
customInputs: item.customInputs,
|
|
458
|
+
skipBaseProduct: item.skipBaseProduct
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// GTM tracking for add to cart - quantity 1 is correct here as we're adding 1 more item
|
|
462
|
+
const product: Product = {
|
|
463
|
+
item_id: item.id,
|
|
464
|
+
item_name: item.name,
|
|
465
|
+
item_category: item.category || 'Products',
|
|
466
|
+
price: item.price,
|
|
467
|
+
quantity: 1, // This is correct - we're adding 1 more item
|
|
468
|
+
currency: 'USD'
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
gtmAddToCart([product], 'USD', item.price, gtmConfig?.container_id);
|
|
472
|
+
|
|
473
|
+
// Reset price refresh state to force refresh with new data
|
|
474
|
+
setPricesRefreshed(false);
|
|
475
|
+
} catch (error) {
|
|
476
|
+
console.error('[Cart] Failed to add item to cart:', error);
|
|
477
|
+
} finally {
|
|
478
|
+
setLoadingItems(prev => ({ ...prev, [item.key]: { ...prev[item.key], plus: false } }));
|
|
479
|
+
}
|
|
480
|
+
}, [addToCart]);
|
|
481
|
+
|
|
482
|
+
if (items.length === 0) {
|
|
483
|
+
return (
|
|
484
|
+
<div className="max-w-7xl mx-auto p-6 lg:px-4 lg:py-8">
|
|
485
|
+
<div className='flex lg:hidden justify-between items-center w-full'>
|
|
486
|
+
<p className='font-semibold text-xl font-secondary text-[var(--color-secondary-800)]'>
|
|
487
|
+
MY CART
|
|
488
|
+
</p>
|
|
489
|
+
<div className="flex items-center gap-1 cursor-pointer">
|
|
490
|
+
{
|
|
491
|
+
!isLoggedIn ?
|
|
492
|
+
<CommonButton
|
|
493
|
+
onClick={() => router.push("/account/login")}
|
|
494
|
+
className="p-0 text-sm"
|
|
495
|
+
content="LOG IN"
|
|
496
|
+
variant="tertiary"
|
|
497
|
+
/> : <CommonButton
|
|
498
|
+
onClick={() => router.push("/")}
|
|
499
|
+
className="p-0 text-sm"
|
|
500
|
+
content="CONTINUE SHOPPING"
|
|
501
|
+
variant="tertiary"
|
|
502
|
+
/>
|
|
503
|
+
}
|
|
504
|
+
<span className="size-5 text-[var(--color-primary-600)]">
|
|
505
|
+
{ArrowIcon}
|
|
506
|
+
</span>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
<div className='hidden lg:block space-y-10'>
|
|
510
|
+
<div className='flex items-center justify-between w-full'>
|
|
511
|
+
<Breadcrumb
|
|
512
|
+
items={[
|
|
513
|
+
{ text: 'Home', link: '/' },
|
|
514
|
+
{ text: 'Cart' },
|
|
515
|
+
]}
|
|
516
|
+
/>
|
|
517
|
+
<div className="flex items-center gap-1 cursor-pointer">
|
|
518
|
+
{
|
|
519
|
+
!isLoggedIn ?
|
|
520
|
+
<CommonButton
|
|
521
|
+
onClick={() => router.push("/account/login")}
|
|
522
|
+
className="p-0"
|
|
523
|
+
content="LOG IN"
|
|
524
|
+
variant="tertiary"
|
|
525
|
+
/> : <CommonButton
|
|
526
|
+
onClick={() => router.push("/")}
|
|
527
|
+
className="p-0"
|
|
528
|
+
content="CONTINUE SHOPPING"
|
|
529
|
+
variant="tertiary"
|
|
530
|
+
/>
|
|
531
|
+
}
|
|
532
|
+
<span className="size-5 text-[var(--color-primary-600)]">
|
|
533
|
+
{ArrowIcon}
|
|
534
|
+
</span>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
<p className='font-primary font-normal text-4xl text-[var(--color-secondary-800)]'>
|
|
538
|
+
CART
|
|
539
|
+
</p>
|
|
540
|
+
</div>
|
|
541
|
+
<EmptyState
|
|
542
|
+
icon={CartIcon}
|
|
543
|
+
iconContainer="p-5"
|
|
544
|
+
text="YOUR CART IS EMPTY"
|
|
545
|
+
textParagraph="Browse parts and accessories to get started."
|
|
546
|
+
className='h-[70vh]'
|
|
547
|
+
buttonLabel="BACK TO HOME"
|
|
548
|
+
buttonVariant="secondary"
|
|
549
|
+
onClick={() => router.push("/")}
|
|
550
|
+
/>
|
|
551
|
+
</div>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return (
|
|
556
|
+
<div className="lg:container lg:mx-auto p-6 lg:px-6 lg:py-24">
|
|
557
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 md:gap-10 lg:gap-14">
|
|
558
|
+
<div className="lg:col-span-2 space-y-5 lg:space-y-10 lg:border-r lg:border-[var(--color-secondary-200)] lg:pr-14">
|
|
559
|
+
<div className='flex lg:hidden justify-between items-center w-full border-b border-[var(--color-secondary-200)] pb-3'>
|
|
560
|
+
<p className='font-semibold text-xl font-secondary text-[var(--color-secondary-800)]'>
|
|
561
|
+
MY CART
|
|
562
|
+
</p>
|
|
563
|
+
<div className="flex items-center gap-1 cursor-pointer">
|
|
564
|
+
{
|
|
565
|
+
!isLoggedIn ?
|
|
566
|
+
<CommonButton
|
|
567
|
+
onClick={() => router.push("/account/login")}
|
|
568
|
+
className="p-0 text-sm"
|
|
569
|
+
content="LOG IN"
|
|
570
|
+
variant="tertiary"
|
|
571
|
+
/> : <CommonButton
|
|
572
|
+
onClick={() => router.push("/")}
|
|
573
|
+
className="p-0 text-sm"
|
|
574
|
+
content="CONTINUE SHOPPING"
|
|
575
|
+
variant="tertiary"
|
|
576
|
+
/>
|
|
577
|
+
}
|
|
578
|
+
<span className="size-5 text-[var(--color-primary-600)]">
|
|
579
|
+
{ArrowIcon}
|
|
580
|
+
</span>
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
<div className='hidden lg:block space-y-10'>
|
|
584
|
+
<div className='flex items-center justify-between w-full'>
|
|
585
|
+
<Breadcrumb
|
|
586
|
+
items={[
|
|
587
|
+
{ text: 'Home', link: '/' },
|
|
588
|
+
{ text: 'Cart' },
|
|
589
|
+
]}
|
|
590
|
+
/>
|
|
591
|
+
<div className="flex items-center gap-1 cursor-pointer">
|
|
592
|
+
{
|
|
593
|
+
!isLoggedIn ?
|
|
594
|
+
<CommonButton
|
|
595
|
+
onClick={() => router.push("/account/login")}
|
|
596
|
+
className="p-0"
|
|
597
|
+
content="LOG IN"
|
|
598
|
+
variant="tertiary"
|
|
599
|
+
/> : <CommonButton
|
|
600
|
+
onClick={() => router.push("/")}
|
|
601
|
+
className="p-0"
|
|
602
|
+
content="CONTINUE SHOPPING"
|
|
603
|
+
variant="tertiary"
|
|
604
|
+
/>
|
|
605
|
+
}
|
|
606
|
+
<span className="size-5 text-[var(--color-primary-600)]">
|
|
607
|
+
{ArrowIcon}
|
|
608
|
+
</span>
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
611
|
+
<p className='font-primary font-normal text-4xl text-[var(--color-secondary-800)]'>
|
|
612
|
+
CART
|
|
613
|
+
</p>
|
|
614
|
+
</div>
|
|
615
|
+
<div className='space-y-4'>
|
|
616
|
+
{displayItems.map((item: CartItem) => (
|
|
617
|
+
<div key={item.key} className="flex flex-col md:flex-row items-center gap-4 border-b border-[var(--color-secondary-200)] last:border-b-0 pb-6 last:pb-0">
|
|
618
|
+
<div className='w-full items-center gap-2 lg:gap-5 flex'>
|
|
619
|
+
<div className="relative size-[50px] md:size-[100px] flex-shrink-0">
|
|
620
|
+
<Image
|
|
621
|
+
src={item.image}
|
|
622
|
+
alt={item.name}
|
|
623
|
+
fill
|
|
624
|
+
className="object-contain"
|
|
625
|
+
/>
|
|
626
|
+
</div>
|
|
627
|
+
<div className="flex flex-row md:flex-col items-start lg:items-start gap-4 w-full justify-between">
|
|
628
|
+
<div className='space-y-1'>
|
|
629
|
+
<p className="text-xs lg:text-sm font-normal text-[var(--color-secondary-800)]">{item.category ?? "N/A"}</p>
|
|
630
|
+
<h3 className="font-medium font-secondary text-xs md:text-sm lg:text-xl/7 text-[var(--color-secondary-800)]">{item.name}</h3>
|
|
631
|
+
{/* Display selected options */}
|
|
632
|
+
{item.selectedOptions && item.selectedOptions.length > 0 && (
|
|
633
|
+
<div className="mt-2 space-y-1">
|
|
634
|
+
{item.selectedOptions.map((option, idx) => (
|
|
635
|
+
<div key={idx} className="text-xs lg:text-sm text-[var(--color-secondary-600)] flex items-center gap-2">
|
|
636
|
+
<span className="font-medium">{option.optionSetLabel}:</span>
|
|
637
|
+
<span>{option.name}</span>
|
|
638
|
+
{option.price > 0 && (
|
|
639
|
+
<span className="text-[var(--color-primary-600)]">(+${option.price.toFixed(2)})</span>
|
|
640
|
+
)}
|
|
641
|
+
</div>
|
|
642
|
+
))}
|
|
643
|
+
</div>
|
|
644
|
+
)}
|
|
645
|
+
{/* Display custom inputs */}
|
|
646
|
+
{item.customInputs && Object.keys(item.customInputs).length > 0 && (
|
|
647
|
+
<div className="mt-2 space-y-1">
|
|
648
|
+
{Object.entries(item.customInputs).map(([key, value]) => (
|
|
649
|
+
<div key={key} className="text-xs lg:text-sm text-[var(--color-secondary-600)]">
|
|
650
|
+
<span className="font-medium">{key}:</span> {value}
|
|
651
|
+
</div>
|
|
652
|
+
))}
|
|
653
|
+
</div>
|
|
654
|
+
)}
|
|
655
|
+
</div>
|
|
656
|
+
<CommonButton
|
|
657
|
+
onClick={() => handleRemoveFromCart(item.key)}
|
|
658
|
+
disabled={loadingItems[item.key]?.remove || loadingItems[item.key]?.plus || loadingItems[item.key]?.minus}
|
|
659
|
+
variant="tertiary"
|
|
660
|
+
className="p-0 text-sm lg:text-base"
|
|
661
|
+
>
|
|
662
|
+
{loadingItems[item.key]?.remove ? 'Removing...' : 'Remove'}
|
|
663
|
+
</CommonButton>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
<div className='flex flex-row justify-between w-full md:w-auto md:justify-normal items-center gap-6 lg:gap-10 pl-[58px] md:pl-0'>
|
|
667
|
+
<div className="flex items-center border border-[var(--color-secondary-200)] bg-[var(--color-secondary-50)] py-2 px-1 md:py-2.5 md:px-2 lg:py-3 gap-2 min-w-32 justify-between">
|
|
668
|
+
<button
|
|
669
|
+
onClick={() => handleUpdateQuantity(item.key, item.quantity - 1)}
|
|
670
|
+
disabled={loadingItems[item.key]?.minus || loadingItems[item.key]?.plus || loadingItems[item.key]?.remove}
|
|
671
|
+
className={`border-r border-[var(--color-secondary-200)] [&>svg]:size-6 pr-2 transition-opacity ${
|
|
672
|
+
loadingItems[item.key]?.minus || loadingItems[item.key]?.plus || loadingItems[item.key]?.remove
|
|
673
|
+
? 'cursor-not-allowed opacity-50'
|
|
674
|
+
: 'cursor-pointer hover:opacity-75'
|
|
675
|
+
}`}
|
|
676
|
+
>
|
|
677
|
+
{loadingItems[item.key]?.minus ? (
|
|
678
|
+
<svg className="animate-spin size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
679
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
680
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
681
|
+
</svg>
|
|
682
|
+
) : (
|
|
683
|
+
SubtractIcon
|
|
684
|
+
)}
|
|
685
|
+
</button>
|
|
686
|
+
<span className='font-normal text-base font-secondary text-[var(--color-secondary-800)]'>{item.quantity}</span>
|
|
687
|
+
<button
|
|
688
|
+
onClick={() => handleAddToCart(item)}
|
|
689
|
+
disabled={loadingItems[item.key]?.plus || loadingItems[item.key]?.minus || loadingItems[item.key]?.remove}
|
|
690
|
+
className={`border-l border-[var(--color-secondary-200)] [&>svg]:size-6 pl-2 transition-opacity ${
|
|
691
|
+
loadingItems[item.key]?.plus || loadingItems[item.key]?.minus || loadingItems[item.key]?.remove
|
|
692
|
+
? 'cursor-not-allowed opacity-50'
|
|
693
|
+
: 'cursor-pointer hover:opacity-75'
|
|
694
|
+
}`}
|
|
695
|
+
>
|
|
696
|
+
{loadingItems[item.key]?.plus ? (
|
|
697
|
+
<svg className="animate-spin size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
698
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
699
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
700
|
+
</svg>
|
|
701
|
+
) : (
|
|
702
|
+
PlusIcon
|
|
703
|
+
)}
|
|
704
|
+
</button>
|
|
705
|
+
</div>
|
|
706
|
+
<div className="font-semibold text-base lg:text-2xl text-[var(--color-primary-600)] font-secondary">
|
|
707
|
+
${(getItemTotal(item) * item.quantity).toFixed(2)}
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
</div>
|
|
711
|
+
))}
|
|
712
|
+
</div>
|
|
713
|
+
</div>
|
|
714
|
+
|
|
715
|
+
<div className="lg:col-span-1">
|
|
716
|
+
<h2 className="text-base font-secondary text-[var(--color-secondary-800)] font-medium mb-4 uppercase">Summary</h2>
|
|
717
|
+
|
|
718
|
+
<div className="gap-2 flex flex-col mb-4 font-normal text-sm md:text-base font-secondary text-[var(--color-secondary-600)]">
|
|
719
|
+
<div className="flex justify-between">
|
|
720
|
+
<span>Subtotal</span>
|
|
721
|
+
<span className='font-medium'>${displayTotalAmount.toFixed(2)}</span>
|
|
722
|
+
</div>
|
|
723
|
+
{/* <div className="flex justify-between pb-2">
|
|
724
|
+
<span>Tax</span>
|
|
725
|
+
<span className='font-medium'>N/A</span>
|
|
726
|
+
</div> */}
|
|
727
|
+
<div className="border-t border-gray-200 pt-4 flex justify-between text-base md:text-xl text-[var(--color-secondary-600)] font-medium ">
|
|
728
|
+
<span>TOTAL</span>
|
|
729
|
+
<span className='font-semibold text-[var(--color-secondary-800)]'>${displayTotalAmount.toFixed(2)}</span>
|
|
730
|
+
</div>
|
|
731
|
+
</div>
|
|
732
|
+
{error && (
|
|
733
|
+
<div className="mb-3 text-sm text-red-600">{error}</div>
|
|
734
|
+
)}
|
|
735
|
+
<CommonButton
|
|
736
|
+
onClick={handleProceed}
|
|
737
|
+
disabled={creating || (isLoggedIn && meLoading)}
|
|
738
|
+
variant="primary"
|
|
739
|
+
className='mt-2 py-2.5 md:py-3.5 w-full flex items-center gap-2 justify-center text-sm lg:text-base'
|
|
740
|
+
>
|
|
741
|
+
{creating ? 'Preparing checkout...' : (isLoggedIn && meLoading ? 'Loading your address...' : 'Proceed to Checkout')}
|
|
742
|
+
{
|
|
743
|
+
!creating && <span className="size-5 ">
|
|
744
|
+
{ArrowIcon}
|
|
745
|
+
</span>
|
|
746
|
+
}
|
|
747
|
+
</CommonButton>
|
|
748
|
+
</div>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
);
|
|
752
|
+
}
|