@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.
Files changed (441) hide show
  1. package/.env.example +57 -0
  2. package/APPLE_PAY_QUICK_START.md +165 -0
  3. package/APPLE_PAY_SETUP.md +331 -0
  4. package/README.md +46 -0
  5. package/SEO_AUDIT_CHECKLIST_STATUS.md +244 -0
  6. package/SEO_AUDIT_REPORT.md +66 -0
  7. package/eslint.config.mjs +16 -0
  8. package/next-env.d.ts +5 -0
  9. package/next.config.ts +109 -0
  10. package/package.json +47 -0
  11. package/postcss.config.mjs +5 -0
  12. package/public/.well-known/apple-developer-merchantid-domain-association +1 -0
  13. package/public/Logo.png +0 -0
  14. package/public/brand-video.mp4 +0 -0
  15. package/public/favicon.ico +0 -0
  16. package/public/file.svg +1 -0
  17. package/public/footer/facebook.tsx +34 -0
  18. package/public/footer/instagram.tsx +27 -0
  19. package/public/footer/mail.tsx +5 -0
  20. package/public/footer/x.tsx +35 -0
  21. package/public/globe.svg +1 -0
  22. package/public/icons/Authorize.net.webp +0 -0
  23. package/public/icons/amex.gif +0 -0
  24. package/public/icons/appIcon.png +0 -0
  25. package/public/icons/discover.gif +0 -0
  26. package/public/icons/master.gif +0 -0
  27. package/public/icons/paypal.png +0 -0
  28. package/public/icons/stripe.png +0 -0
  29. package/public/icons/visa.gif +0 -0
  30. package/public/images/BackgroundNoise.png +0 -0
  31. package/public/images/footer-background.png +0 -0
  32. package/public/next.svg +1 -0
  33. package/public/no-image-avail-large.png +0 -0
  34. package/public/random-car-1.jpeg +0 -0
  35. package/public/random-car-2.png +0 -0
  36. package/public/random-car-3.jpg +0 -0
  37. package/public/random-car-4.jpg +0 -0
  38. package/public/random-car-5.jpg +0 -0
  39. package/public/star.svg +3 -0
  40. package/public/vercel.svg +1 -0
  41. package/public/window.svg +1 -0
  42. package/scripts/seo-audit/generate-checklist.mjs +156 -0
  43. package/src/app/(auth)/account/forgot-password/layout.tsx +16 -0
  44. package/src/app/(auth)/account/forgot-password/page.tsx +135 -0
  45. package/src/app/(auth)/account/login/layout.tsx +16 -0
  46. package/src/app/(auth)/account/login/page.tsx +288 -0
  47. package/src/app/(auth)/account/otp/layout.tsx +16 -0
  48. package/src/app/(auth)/account/otp/page.tsx +108 -0
  49. package/src/app/(auth)/account/register/layout.tsx +16 -0
  50. package/src/app/(auth)/account/register/page.tsx +431 -0
  51. package/src/app/(auth)/account/reset-password/layout.tsx +16 -0
  52. package/src/app/(auth)/account/reset-password/page.tsx +222 -0
  53. package/src/app/[slug]/page.tsx +43 -0
  54. package/src/app/about/loading.tsx +17 -0
  55. package/src/app/about/page.tsx +61 -0
  56. package/src/app/account/address/layout.tsx +15 -0
  57. package/src/app/account/address/page.tsx +166 -0
  58. package/src/app/account/head.tsx +4 -0
  59. package/src/app/account/layout.tsx +62 -0
  60. package/src/app/account/orders/[id]/layout.tsx +17 -0
  61. package/src/app/account/orders/[id]/page.tsx +115 -0
  62. package/src/app/account/orders/components/orderDetailsModal.tsx +410 -0
  63. package/src/app/account/orders/layout.tsx +15 -0
  64. package/src/app/account/orders/page.tsx +146 -0
  65. package/src/app/account/page.tsx +39 -0
  66. package/src/app/account/settings/components/editProfileSuccessModal.tsx +28 -0
  67. package/src/app/account/settings/layout.tsx +15 -0
  68. package/src/app/account/settings/page.tsx +260 -0
  69. package/src/app/api/affirm/check-status/route.ts +94 -0
  70. package/src/app/api/affirm/create-checkout/route.ts +109 -0
  71. package/src/app/api/affirm/get-config/route.ts +108 -0
  72. package/src/app/api/affirm/process-payment/route.ts +244 -0
  73. package/src/app/api/affirm/test-connection/route.ts +45 -0
  74. package/src/app/api/auth/clear/route.ts +16 -0
  75. package/src/app/api/auth/clear-cookies/route.ts +42 -0
  76. package/src/app/api/auth/set/route.ts +47 -0
  77. package/src/app/api/configuration/route.ts +18 -0
  78. package/src/app/api/dynamic-page/[slug]/route.ts +24 -0
  79. package/src/app/api/form-submission/route.ts +237 -0
  80. package/src/app/api/paypal/capture-order/route.ts +303 -0
  81. package/src/app/api/paypal/create-order/route.ts +211 -0
  82. package/src/app/api/paypal/get-config/route.ts +240 -0
  83. package/src/app/api/search-proxy/route.ts +52 -0
  84. package/src/app/authorize-net-success/layout.tsx +19 -0
  85. package/src/app/authorize-net-success/page.tsx +12 -0
  86. package/src/app/authorize-net-success/summary.tsx +486 -0
  87. package/src/app/blog/[slug]/blogContentRenderer.tsx +369 -0
  88. package/src/app/blog/[slug]/layout.tsx +17 -0
  89. package/src/app/blog/[slug]/page.tsx +151 -0
  90. package/src/app/blog/constant.tsx +147 -0
  91. package/src/app/blog/layout.tsx +31 -0
  92. package/src/app/blog/page.tsx +81 -0
  93. package/src/app/brand/[id]/BrandPageClient.tsx +188 -0
  94. package/src/app/brand/[id]/layout.tsx +17 -0
  95. package/src/app/brand/[id]/page.tsx +176 -0
  96. package/src/app/brands/components/brandsListingClient.tsx +97 -0
  97. package/src/app/brands/layout.tsx +31 -0
  98. package/src/app/brands/page.tsx +40 -0
  99. package/src/app/cancellation-policy/page.tsx +53 -0
  100. package/src/app/cart/layout.tsx +19 -0
  101. package/src/app/cart/page.tsx +752 -0
  102. package/src/app/category/[slug]/CategoryPageClient.tsx +377 -0
  103. package/src/app/category/[slug]/layout.tsx +17 -0
  104. package/src/app/category/[slug]/page.tsx +224 -0
  105. package/src/app/category/page.tsx +114 -0
  106. package/src/app/checkout/components/addNewAddressModal.tsx +474 -0
  107. package/src/app/checkout/layout.tsx +19 -0
  108. package/src/app/checkout/page.tsx +3312 -0
  109. package/src/app/components/account/AccountTabs.tsx +40 -0
  110. package/src/app/components/ads/GoogleAdSense.tsx +74 -0
  111. package/src/app/components/analytics/AnalyticsScripts.tsx +78 -0
  112. package/src/app/components/analytics/ConditionalGTMNoscript.tsx +24 -0
  113. package/src/app/components/analytics/ConditionalGoogleAnalytics.tsx +16 -0
  114. package/src/app/components/ancillary/AncillaryContent.tsx +7 -0
  115. package/src/app/components/auth/TokenExpirationHandler.tsx +8 -0
  116. package/src/app/components/blog/BlogList.tsx +112 -0
  117. package/src/app/components/checkout/AddressInformationSection.tsx +34 -0
  118. package/src/app/components/checkout/AddressManagement.tsx +571 -0
  119. package/src/app/components/checkout/CheckoutHeader.tsx +51 -0
  120. package/src/app/components/checkout/CheckoutQuestions.tsx +454 -0
  121. package/src/app/components/checkout/CheckoutTermsModal.tsx +81 -0
  122. package/src/app/components/checkout/ContactDetailsSection.tsx +52 -0
  123. package/src/app/components/checkout/DealerShippingSection.tsx +359 -0
  124. package/src/app/components/checkout/DeliveryMethodSection.tsx +249 -0
  125. package/src/app/components/checkout/OrderSummary.tsx +386 -0
  126. package/src/app/components/checkout/TermsContentRenderer.tsx +147 -0
  127. package/src/app/components/checkout/WillCallSection.tsx +133 -0
  128. package/src/app/components/checkout/affirmPayment.tsx +383 -0
  129. package/src/app/components/checkout/checkoutProcessingModal.tsx +96 -0
  130. package/src/app/components/checkout/googlePayButton.tsx +334 -0
  131. package/src/app/components/checkout/paymentStep.tsx +180 -0
  132. package/src/app/components/checkout/paypalPayment.tsx +1083 -0
  133. package/src/app/components/checkout/saleorNativePayment.tsx +1758 -0
  134. package/src/app/components/dynamicPage/DynamicPageRenderer.tsx +13 -0
  135. package/src/app/components/dynamicPage/HtmlWidgetRenderer.tsx +144 -0
  136. package/src/app/components/filtersCollapsible/index.tsx +365 -0
  137. package/src/app/components/globalSearch/index.tsx +423 -0
  138. package/src/app/components/layout/cartDropDown.tsx +628 -0
  139. package/src/app/components/layout/components/FooterNewsletter.tsx +21 -0
  140. package/src/app/components/layout/footer.tsx +283 -0
  141. package/src/app/components/layout/header/accountMenuDropdown.tsx +53 -0
  142. package/src/app/components/layout/header/components/CartBadge.tsx +18 -0
  143. package/src/app/components/layout/header/components/LoadingState.tsx +17 -0
  144. package/src/app/components/layout/header/components/MenuItemDropdown.tsx +124 -0
  145. package/src/app/components/layout/header/components/MobileNavbar.tsx +123 -0
  146. package/src/app/components/layout/header/components/NavbarActions.tsx +125 -0
  147. package/src/app/components/layout/header/components/NavbarBrand.tsx +29 -0
  148. package/src/app/components/layout/header/components/NavigationLinks.tsx +131 -0
  149. package/src/app/components/layout/header/hamMenuSlide.tsx +318 -0
  150. package/src/app/components/layout/header/header.tsx +44 -0
  151. package/src/app/components/layout/header/hooks/useDropdown.ts +45 -0
  152. package/src/app/components/layout/header/hooks/useNavbarData.ts +138 -0
  153. package/src/app/components/layout/header/hooks/useNavbarState.ts +66 -0
  154. package/src/app/components/layout/header/megaMenuDropdown.tsx +116 -0
  155. package/src/app/components/layout/header/navBar.tsx +121 -0
  156. package/src/app/components/layout/header/search.tsx +418 -0
  157. package/src/app/components/layout/header/styles/navbarStyles.ts +27 -0
  158. package/src/app/components/layout/header/topBar.tsx +214 -0
  159. package/src/app/components/layout/joinNewsletterForm/index.tsx +72 -0
  160. package/src/app/components/layout/mobileAccordian/index.tsx +92 -0
  161. package/src/app/components/layout/paymentMethods.tsx +75 -0
  162. package/src/app/components/layout/rootLayout.tsx +23 -0
  163. package/src/app/components/layout/siteInfo.tsx +103 -0
  164. package/src/app/components/layout/socialLinks.tsx +65 -0
  165. package/src/app/components/newsletterSection/emailListSection.tsx +224 -0
  166. package/src/app/components/newsletterSection/emailSectionServer.tsx +8 -0
  167. package/src/app/components/providers/ApolloWrapper.tsx +12 -0
  168. package/src/app/components/providers/AppConfigurationProvider.tsx +108 -0
  169. package/src/app/components/providers/GoogleAnalyticsProvider.tsx +149 -0
  170. package/src/app/components/providers/GoogleTagManagerProvider.tsx +31 -0
  171. package/src/app/components/providers/RecaptchaProvider.tsx +18 -0
  172. package/src/app/components/providers/ServerAppConfigurationProvider.tsx +133 -0
  173. package/src/app/components/providers/YMMStatusProvider.tsx +15 -0
  174. package/src/app/components/reuseableUI/AboutUs.tsx +115 -0
  175. package/src/app/components/reuseableUI/AddToCartClient.tsx +125 -0
  176. package/src/app/components/reuseableUI/EditorJsRenderer.tsx +219 -0
  177. package/src/app/components/reuseableUI/HeroSectionsearchByVehicle.tsx +188 -0
  178. package/src/app/components/reuseableUI/ImageWithFallback.tsx +41 -0
  179. package/src/app/components/reuseableUI/Toast.tsx +101 -0
  180. package/src/app/components/reuseableUI/blogCard.tsx +52 -0
  181. package/src/app/components/reuseableUI/brandCard.tsx +68 -0
  182. package/src/app/components/reuseableUI/breadcrumb.tsx +38 -0
  183. package/src/app/components/reuseableUI/categoryCard.tsx +37 -0
  184. package/src/app/components/reuseableUI/categorySkeleton.tsx +31 -0
  185. package/src/app/components/reuseableUI/commonButton.tsx +48 -0
  186. package/src/app/components/reuseableUI/defaultInputField/index.tsx +84 -0
  187. package/src/app/components/reuseableUI/emptyState.tsx +29 -0
  188. package/src/app/components/reuseableUI/errorTag.tsx +15 -0
  189. package/src/app/components/reuseableUI/heading/index.tsx +20 -0
  190. package/src/app/components/reuseableUI/input.tsx +117 -0
  191. package/src/app/components/reuseableUI/listCard.tsx +137 -0
  192. package/src/app/components/reuseableUI/loadingUI.tsx +12 -0
  193. package/src/app/components/reuseableUI/modalLayout.tsx +76 -0
  194. package/src/app/components/reuseableUI/newsletter/newsletterClient.tsx +622 -0
  195. package/src/app/components/reuseableUI/newsletter/newslettersHomeModal.tsx +68 -0
  196. package/src/app/components/reuseableUI/offerCard.tsx +42 -0
  197. package/src/app/components/reuseableUI/passwordRules/passwordRules.tsx +56 -0
  198. package/src/app/components/reuseableUI/primaryButton/index.tsx +34 -0
  199. package/src/app/components/reuseableUI/productCard.tsx +118 -0
  200. package/src/app/components/reuseableUI/productSkeleton.tsx +34 -0
  201. package/src/app/components/reuseableUI/searchByVehicle.tsx +187 -0
  202. package/src/app/components/reuseableUI/secondaryButton/index.tsx +34 -0
  203. package/src/app/components/reuseableUI/section.tsx +20 -0
  204. package/src/app/components/reuseableUI/select/index.tsx +98 -0
  205. package/src/app/components/reuseableUI/skeletonLoader.tsx +117 -0
  206. package/src/app/components/reuseableUI/statusTag.tsx +24 -0
  207. package/src/app/components/reuseableUI/tags/saleTag.tsx +19 -0
  208. package/src/app/components/reuseableUI/testimonialCard.tsx +93 -0
  209. package/src/app/components/richText/EditorRenderer.tsx +318 -0
  210. package/src/app/components/search/HierarchicalCategoryFilter.tsx +155 -0
  211. package/src/app/components/search/SearchFilters.tsx +155 -0
  212. package/src/app/components/search/YMMSearchSidebar.tsx +187 -0
  213. package/src/app/components/seo/ServerProductCard.tsx +91 -0
  214. package/src/app/components/seo/ServerProductGrid.tsx +45 -0
  215. package/src/app/components/shop/CategoryFilter.tsx +184 -0
  216. package/src/app/components/shop/ItemsPerPageSelect.tsx +69 -0
  217. package/src/app/components/shop/ItemsPerPageSelectClient.tsx +58 -0
  218. package/src/app/components/shop/MobileFilters.tsx +103 -0
  219. package/src/app/components/shop/ProductGridSkeleton.tsx +16 -0
  220. package/src/app/components/shop/ProductsGrid.tsx +230 -0
  221. package/src/app/components/shop/SearchFilter.tsx +218 -0
  222. package/src/app/components/shop/SearchFilterClient.tsx +122 -0
  223. package/src/app/components/shop/SearchLoadingOverlay.tsx +32 -0
  224. package/src/app/components/shop/ShopMobileFilters.tsx +205 -0
  225. package/src/app/components/showroom/VehicleSearchDropdowns.tsx +187 -0
  226. package/src/app/components/showroom/brandsSwiper.tsx +49 -0
  227. package/src/app/components/showroom/brandsSwiperClient copy.tsx +93 -0
  228. package/src/app/components/showroom/brandsSwiperClient.tsx +122 -0
  229. package/src/app/components/showroom/brandsSwiperServer.tsx +42 -0
  230. package/src/app/components/showroom/bundleProducts.tsx +120 -0
  231. package/src/app/components/showroom/categoryGrid.tsx +51 -0
  232. package/src/app/components/showroom/categoryGridServer.tsx +45 -0
  233. package/src/app/components/showroom/categorySwiper.tsx +115 -0
  234. package/src/app/components/showroom/featureStrip.tsx +139 -0
  235. package/src/app/components/showroom/offersSwiper.tsx +181 -0
  236. package/src/app/components/showroom/productGrid.tsx +56 -0
  237. package/src/app/components/showroom/productSwiper.tsx +119 -0
  238. package/src/app/components/showroom/promotion-slider.tsx +138 -0
  239. package/src/app/components/showroom/promotion.tsx +207 -0
  240. package/src/app/components/showroom/promotionsSwiper.tsx +174 -0
  241. package/src/app/components/showroom/showroomHeroCarousel.tsx +141 -0
  242. package/src/app/components/showroom/testimonialsGrid.tsx +106 -0
  243. package/src/app/components/skeletons/ContentSkeleton.tsx +14 -0
  244. package/src/app/components/sortDropdown/index.tsx +116 -0
  245. package/src/app/components/tertiaryButton/index.tsx +25 -0
  246. package/src/app/components/theme/theme-provider.tsx +82 -0
  247. package/src/app/contact/layout.tsx +32 -0
  248. package/src/app/contact/page.tsx +591 -0
  249. package/src/app/content/[slug]/layout.tsx +17 -0
  250. package/src/app/content/[slug]/page.tsx +159 -0
  251. package/src/app/content/layout.tsx +31 -0
  252. package/src/app/content/page.tsx +88 -0
  253. package/src/app/core-policies/page.tsx +55 -0
  254. package/src/app/discounts/page.tsx +54 -0
  255. package/src/app/frequently-asked-questions/page.tsx +57 -0
  256. package/src/app/globals.css +440 -0
  257. package/src/app/hooks/useDealerLocations.ts +259 -0
  258. package/src/app/hooks/useGTMEngagement.ts +71 -0
  259. package/src/app/hooks/useGoogleAnalytics.ts +145 -0
  260. package/src/app/layout.tsx +149 -0
  261. package/src/app/not-found.tsx +31 -0
  262. package/src/app/order-confirmation/layout.tsx +19 -0
  263. package/src/app/order-confirmation/page.tsx +12 -0
  264. package/src/app/order-confirmation/summary.tsx +1775 -0
  265. package/src/app/page.tsx +194 -0
  266. package/src/app/privacy-policy/loading.tsx +17 -0
  267. package/src/app/privacy-policy/page.tsx +56 -0
  268. package/src/app/product/[id]/ProductDetailClient.tsx +2448 -0
  269. package/src/app/product/[id]/components/itemInquiryModal.tsx +461 -0
  270. package/src/app/product/[id]/layout.tsx +116 -0
  271. package/src/app/product/[id]/page.tsx +200 -0
  272. package/src/app/product/layout.tsx +15 -0
  273. package/src/app/products/all/AllProductsClient.tsx +743 -0
  274. package/src/app/products/all/page.tsx +176 -0
  275. package/src/app/products/components/shopEmptyState.tsx +29 -0
  276. package/src/app/request-return/layout.tsx +36 -0
  277. package/src/app/request-return/page.tsx +597 -0
  278. package/src/app/robots.txt/route.ts +27 -0
  279. package/src/app/search/layout.tsx +16 -0
  280. package/src/app/search/page.tsx +736 -0
  281. package/src/app/shipping-returns/page.tsx +60 -0
  282. package/src/app/site-map/layout.tsx +33 -0
  283. package/src/app/site-map/page.tsx +113 -0
  284. package/src/app/sitemap-index.xml/route.ts +20 -0
  285. package/src/app/sitemap.ts +10 -0
  286. package/src/app/terms-and-conditions/loading.tsx +17 -0
  287. package/src/app/terms-and-conditions/page.tsx +56 -0
  288. package/src/app/utils/appConfiguration.ts +327 -0
  289. package/src/app/utils/branding.ts +52 -0
  290. package/src/app/utils/configurationService.ts +202 -0
  291. package/src/app/utils/constant.tsx +242 -0
  292. package/src/app/utils/editorJsUtils.tsx +249 -0
  293. package/src/app/utils/functions.ts +146 -0
  294. package/src/app/utils/googleAnalytics.ts +168 -0
  295. package/src/app/utils/googleTagManager.ts +475 -0
  296. package/src/app/utils/ipDetection.ts +270 -0
  297. package/src/app/utils/serverConfigurationService.ts +209 -0
  298. package/src/app/utils/svgs/GridIcon.tsx +45 -0
  299. package/src/app/utils/svgs/account/myAccount/listDotIcon.tsx +3 -0
  300. package/src/app/utils/svgs/account/myAccount/tickIcon.tsx +10 -0
  301. package/src/app/utils/svgs/account/orderHistory/InfoIcon.tsx +49 -0
  302. package/src/app/utils/svgs/arrowDownIcon.tsx +17 -0
  303. package/src/app/utils/svgs/arrowIcon.tsx +25 -0
  304. package/src/app/utils/svgs/arrowUpIcon.tsx +16 -0
  305. package/src/app/utils/svgs/brandsSearchIcon.tsx +25 -0
  306. package/src/app/utils/svgs/cart/cartIcon.tsx +31 -0
  307. package/src/app/utils/svgs/cart/plusIcon.tsx +13 -0
  308. package/src/app/utils/svgs/cart/subtractIcon.tsx +13 -0
  309. package/src/app/utils/svgs/cart/successTickIcon.tsx +14 -0
  310. package/src/app/utils/svgs/chevronDownIcon.tsx +21 -0
  311. package/src/app/utils/svgs/closeEyeIcon.tsx +47 -0
  312. package/src/app/utils/svgs/crossIcon.tsx +25 -0
  313. package/src/app/utils/svgs/eyeIcon.tsx +29 -0
  314. package/src/app/utils/svgs/featureTag.tsx +20 -0
  315. package/src/app/utils/svgs/filterIcon.tsx +3 -0
  316. package/src/app/utils/svgs/globleIcon.tsx +41 -0
  317. package/src/app/utils/svgs/infoIcon.tsx +34 -0
  318. package/src/app/utils/svgs/listIcon.tsx +50 -0
  319. package/src/app/utils/svgs/logOutIcon.tsx +35 -0
  320. package/src/app/utils/svgs/menuIcon.tsx +8 -0
  321. package/src/app/utils/svgs/minusIcon.tsx +18 -0
  322. package/src/app/utils/svgs/newsletterIcon.tsx +19 -0
  323. package/src/app/utils/svgs/noDataFoundIcon-.tsx +26 -0
  324. package/src/app/utils/svgs/noProductFoundIcon.tsx +43 -0
  325. package/src/app/utils/svgs/passwordIcons/errorIcon.tsx +31 -0
  326. package/src/app/utils/svgs/passwordIcons/successIcon.tsx +24 -0
  327. package/src/app/utils/svgs/paymentProcessingIcons/hourglassIcon.tsx +43 -0
  328. package/src/app/utils/svgs/paymentProcessingIcons/modalCrossIcon.tsx +23 -0
  329. package/src/app/utils/svgs/paymentProcessingIcons/paymentFailedIcon.tsx +47 -0
  330. package/src/app/utils/svgs/pencilIcon.tsx +11 -0
  331. package/src/app/utils/svgs/plusIcon.tsx +25 -0
  332. package/src/app/utils/svgs/productInquiryIcon.tsx +40 -0
  333. package/src/app/utils/svgs/searchIcon.tsx +31 -0
  334. package/src/app/utils/svgs/shoppingCart.tsx +32 -0
  335. package/src/app/utils/svgs/spinnerIcon.tsx +22 -0
  336. package/src/app/utils/svgs/spinnerLoadingIcon.tsx +26 -0
  337. package/src/app/utils/svgs/successTickIcon.tsx +40 -0
  338. package/src/app/utils/svgs/swiperArrowIconLeft.tsx +18 -0
  339. package/src/app/utils/svgs/swiperArrowIconRight.tsx +18 -0
  340. package/src/app/utils/svgs/userProfileIcon.tsx +31 -0
  341. package/src/app/utils/svgs/warningCircleIcon.tsx +15 -0
  342. package/src/app/warranty/constant.tsx +63 -0
  343. package/src/app/warranty/loading.tsx +17 -0
  344. package/src/app/warranty/page.tsx +56 -0
  345. package/src/graphql/client.ts +288 -0
  346. package/src/graphql/mutations/accountAddressCreate.ts +56 -0
  347. package/src/graphql/mutations/accountAddressDelete.ts +23 -0
  348. package/src/graphql/mutations/accountAddressUpdate.ts +55 -0
  349. package/src/graphql/mutations/accountSetDefaultAddress.ts +32 -0
  350. package/src/graphql/mutations/accountUpdate.ts +34 -0
  351. package/src/graphql/mutations/changePassword.ts +25 -0
  352. package/src/graphql/mutations/checkout.ts +117 -0
  353. package/src/graphql/mutations/checkoutAddVoucher.ts +63 -0
  354. package/src/graphql/mutations/checkoutComplete.ts +79 -0
  355. package/src/graphql/mutations/checkoutCreate.ts +131 -0
  356. package/src/graphql/mutations/checkoutCustomerAttach.ts +50 -0
  357. package/src/graphql/mutations/checkoutEmailUpdate.ts +15 -0
  358. package/src/graphql/mutations/checkoutLineMetadataUpdate.ts +52 -0
  359. package/src/graphql/mutations/checkoutPaymentCreate.ts +82 -0
  360. package/src/graphql/mutations/paymentGatewayInitialize.ts +58 -0
  361. package/src/graphql/mutations/registerAccount.ts +65 -0
  362. package/src/graphql/mutations/requestPasswordReset.ts +32 -0
  363. package/src/graphql/mutations/setPassword.ts +49 -0
  364. package/src/graphql/mutations/signIn.ts +50 -0
  365. package/src/graphql/mutations/tokenRefresh.ts +19 -0
  366. package/src/graphql/mutations/updateCheckoutMetadata.ts +49 -0
  367. package/src/graphql/mutations/updateProfile.ts +18 -0
  368. package/src/graphql/mutations/willCallDeliveryMethod.ts +81 -0
  369. package/src/graphql/queries/checkout.ts +168 -0
  370. package/src/graphql/queries/findProductByOldSlug.ts +58 -0
  371. package/src/graphql/queries/getAboutPage.ts +24 -0
  372. package/src/graphql/queries/getAboutPageId.ts +9 -0
  373. package/src/graphql/queries/getAboutUs.ts +38 -0
  374. package/src/graphql/queries/getAddressInformation.ts +38 -0
  375. package/src/graphql/queries/getAllCategories.ts +41 -0
  376. package/src/graphql/queries/getAllCategoriesTree.ts +67 -0
  377. package/src/graphql/queries/getAllCategoriesWithProducts.ts +29 -0
  378. package/src/graphql/queries/getAllCollectionsWithProducts.ts +16 -0
  379. package/src/graphql/queries/getBlogs.ts +222 -0
  380. package/src/graphql/queries/getBrands.ts +17 -0
  381. package/src/graphql/queries/getBundles.ts +43 -0
  382. package/src/graphql/queries/getCategories.ts +20 -0
  383. package/src/graphql/queries/getChannels.ts +77 -0
  384. package/src/graphql/queries/getCheckoutQuestions.ts +115 -0
  385. package/src/graphql/queries/getCheckoutTermsAndConditions.ts +37 -0
  386. package/src/graphql/queries/getContactPage.ts +117 -0
  387. package/src/graphql/queries/getContentPage.ts +191 -0
  388. package/src/graphql/queries/getDiscountOffers.ts +18 -0
  389. package/src/graphql/queries/getDynamicPageBySlug.ts +251 -0
  390. package/src/graphql/queries/getFeaturedProducts.ts +48 -0
  391. package/src/graphql/queries/getHeroMetadata.ts +23 -0
  392. package/src/graphql/queries/getMenuBySlug.ts +84 -0
  393. package/src/graphql/queries/getMyProfile.ts +23 -0
  394. package/src/graphql/queries/getNewsletter.ts +122 -0
  395. package/src/graphql/queries/getNewsletterPage.ts +111 -0
  396. package/src/graphql/queries/getPageBySlug.ts +52 -0
  397. package/src/graphql/queries/getPageTypeId.ts +27 -0
  398. package/src/graphql/queries/getPaymentMethods.ts +61 -0
  399. package/src/graphql/queries/getProducts.ts +78 -0
  400. package/src/graphql/queries/getPromotions.ts +24 -0
  401. package/src/graphql/queries/getRequestReturnPage.ts +121 -0
  402. package/src/graphql/queries/getSiteInfo.ts +54 -0
  403. package/src/graphql/queries/getSocialLinks.ts +52 -0
  404. package/src/graphql/queries/getTestimonials.ts +25 -0
  405. package/src/graphql/queries/getUserWithCheckout.ts +27 -0
  406. package/src/graphql/queries/getVehicleMakes.ts +21 -0
  407. package/src/graphql/queries/getVehicleModels.ts +21 -0
  408. package/src/graphql/queries/getVehicleYears.ts +21 -0
  409. package/src/graphql/queries/meAddresses.ts +56 -0
  410. package/src/graphql/queries/myOrders.ts +37 -0
  411. package/src/graphql/queries/orderDetail.ts +231 -0
  412. package/src/graphql/queries/productDetailsById.ts +197 -0
  413. package/src/graphql/queries/productInquiry.ts +115 -0
  414. package/src/graphql/queries/productsByCategoriesAndCollections.ts +39 -0
  415. package/src/graphql/queries/willCallCollectionPoints.ts +55 -0
  416. package/src/graphql/server-client.ts +54 -0
  417. package/src/graphql/types/categories.ts +9 -0
  418. package/src/graphql/types/checkout.ts +168 -0
  419. package/src/graphql/types/offer.ts +12 -0
  420. package/src/graphql/types/product.ts +44 -0
  421. package/src/hooks/scrollPageTop.ts +9 -0
  422. package/src/hooks/serverNavbarData.ts +79 -0
  423. package/src/hooks/useCartSync.ts +24 -0
  424. package/src/hooks/useRecaptcha.ts +33 -0
  425. package/src/hooks/useTokenExpiration.ts +81 -0
  426. package/src/hooks/useVehicleData.ts +346 -0
  427. package/src/lib/api/kount.ts +165 -0
  428. package/src/lib/api/shop.ts +1445 -0
  429. package/src/lib/saleor/getSaleorApiUrl.ts +25 -0
  430. package/src/lib/schema.ts +303 -0
  431. package/src/lib/seo/extractTextFromEditorJs.ts +58 -0
  432. package/src/lib/seo/site.ts +10 -0
  433. package/src/lib/urls/normalizeInternalUrl.ts +53 -0
  434. package/src/middleware.ts +134 -0
  435. package/src/sitemaps/README.md +105 -0
  436. package/src/sitemaps/dynamic-pages-sitemap.ts +247 -0
  437. package/src/sitemaps/sitemap-index.ts +21 -0
  438. package/src/sitemaps/static-pages-sitemap.ts +36 -0
  439. package/src/store/useGlobalStore.tsx +1656 -0
  440. package/src/types/global.d.ts +148 -0
  441. 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
+ }