@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,184 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useMemo } from "react";
4
+ import type { CategoryNode } from "@/graphql/queries/getAllCategoriesTree";
5
+ import { FiltersCollapsible } from "@/app/components/filtersCollapsible";
6
+
7
+ export interface CategoryFilterProps {
8
+ categories: { edges: { node: CategoryNode }[] };
9
+ selectedSlugs: string[];
10
+ // Callback with the updated selection
11
+ onChangeSelected?: (next: string[]) => void;
12
+ }
13
+
14
+ export default function CategoryFilter({ categories, selectedSlugs, onChangeSelected }: CategoryFilterProps) {
15
+
16
+ const [selected, setSelected] = useState<Set<string>>(() => new Set(selectedSlugs));
17
+ const [expanded, setExpanded] = useState<Set<string>>(() => {
18
+ const open = new Set<string>();
19
+ categories.edges.forEach(({ node }) => {
20
+ if (selectedSlugs.includes(node.slug)) open.add(node.id);
21
+ const hasSelectedChild = node.children?.edges?.some(({ node: c }) => selectedSlugs.includes(c.slug));
22
+ if (hasSelectedChild) open.add(node.id);
23
+ });
24
+ return open;
25
+ });
26
+
27
+ // Stable key that changes only when the category tree structure changes
28
+ const categoriesKey = useMemo(() => {
29
+ try {
30
+ return JSON.stringify(
31
+ categories.edges.map(({ node }) => ({
32
+ id: node.id,
33
+ slug: node.slug,
34
+ children: node.children?.edges?.map(({ node: c }) => c.id) ?? [],
35
+ }))
36
+ );
37
+ } catch {
38
+ // Fallback to length if serialization fails for any reason
39
+ return String(categories.edges.length);
40
+ }
41
+ }, [categories]);
42
+
43
+ // Build a lookup of slug -> all descendant slugs (one level deep based on current query)
44
+ const slugToDescSlugs = useMemo(() => {
45
+ const map = new Map<string, string[]>();
46
+ const walkChildren = (n: CategoryNode | undefined | null): string[] => {
47
+ if (!n?.children?.edges?.length) return [];
48
+ const direct = n.children.edges.map(({ node }) => node.slug);
49
+ // If future depth is added, recurse here
50
+ return direct;
51
+ };
52
+ categories.edges.forEach(({ node }) => {
53
+ map.set(node.slug, walkChildren(node));
54
+ node.children?.edges?.forEach(({ node: child }) => {
55
+ // Children in current query do not include nested children; treat as leaf
56
+ map.set(child.slug, []);
57
+ });
58
+ });
59
+ return map;
60
+ }, [categoriesKey, categories]);
61
+
62
+
63
+ useEffect(() => {
64
+ // keep local state in sync with props when SSR re-renders
65
+ setSelected(new Set(selectedSlugs));
66
+ // re-compute expanded to ensure parents with selections are open
67
+ const open = new Set<string>();
68
+ categories.edges.forEach(({ node }) => {
69
+ if (selectedSlugs.includes(node.slug)) open.add(node.id);
70
+ const hasSelectedChild = node.children?.edges?.some(({ node: c }) => selectedSlugs.includes(c.slug));
71
+ if (hasSelectedChild) open.add(node.id);
72
+ });
73
+ setExpanded(open);
74
+ }, [selectedSlugs, categoriesKey]);
75
+
76
+ const toggle = (slug: string) => {
77
+ const base = new Set(selected);
78
+ const descendants = slugToDescSlugs.get(slug) || [];
79
+ if (base.has(slug)) {
80
+ base.delete(slug);
81
+ descendants.forEach((s) => base.delete(s));
82
+ } else {
83
+ base.add(slug);
84
+ descendants.forEach((s) => base.add(s));
85
+ }
86
+
87
+ // Update state only, don't modify URL
88
+ setSelected(base);
89
+ // Notify parent component about the change (if callback provided)
90
+ onChangeSelected?.(Array.from(base));
91
+ };
92
+
93
+ const clearAll = () => {
94
+ // Update state only, don't modify URL
95
+ setSelected(new Set());
96
+ // Notify parent component about the change
97
+ onChangeSelected?.([]);
98
+ };
99
+
100
+ const isChecked = (slug: string) => selected.has(slug);
101
+
102
+ const toggleParent = (id: string) => {
103
+ const next = new Set(expanded);
104
+ if (next.has(id)) next.delete(id); else next.add(id);
105
+ setExpanded(next);
106
+ };
107
+
108
+ return (
109
+ <FiltersCollapsible title="Categories" defaultOpen>
110
+ {/* <div className="mb-2 flex items-center justify-end px-2">
111
+ <button
112
+ type="button"
113
+ onClick={clearAll}
114
+ className="text-sm text-[var(--color-secondary-700)] hover:text-[var(--color-secondary-900)] font-secondary"
115
+ >
116
+ Clear all
117
+ </button>
118
+ </div> */}
119
+ <div className="px-1">
120
+ <ul className="space-y-4 md:space-y-3">
121
+ {categories.edges.map(({ node }) => (
122
+ <li key={node.id} >
123
+ <div className="flex items-center justify-between gap-2 cursor-pointer">
124
+ <label className="flex flex-1 items-center gap-2 font-secondary text-[var(--color-secondary-800)] w-full cursor-pointer">
125
+ <input
126
+ type="checkbox"
127
+ className="h-5 w-5 md:h-4 md:w-4 accent-[var(--color-primary-600)] cursor-pointer"
128
+ checked={isChecked(node.slug)}
129
+ onChange={() => toggle(node.slug)}
130
+ />
131
+ <span className="flex items-center gap-2">
132
+ <span className="truncate">{node.name}</span>
133
+
134
+ </span>
135
+ </label>
136
+ {node.children?.edges?.length ? (
137
+ <button
138
+ type="button"
139
+ aria-label={expanded.has(node.id) ? "Collapse" : "Expand"}
140
+ className="p-2 md:p-1 text-[var(--color-secondary-700)] hover:text-[var(--color-secondary-900)] transition-transform cursor-pointer"
141
+ onClick={() => toggleParent(node.id)}
142
+ >
143
+ <svg
144
+ className={`h-5 w-5 md:h-4 md:w-4 transition-transform ${expanded.has(node.id) ? "rotate-180" : "rotate-0"}`}
145
+ viewBox="0 0 20 20"
146
+ fill="currentColor"
147
+ xmlns="http://www.w3.org/2000/svg"
148
+ >
149
+ <path fillRule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 10.94l3.71-3.71a.75.75 0 111.06 1.06l-4.24 4.24a.75.75 0 01-1.06 0L5.21 8.29a.75.75 0 01.02-1.08z" clipRule="evenodd" />
150
+ </svg>
151
+ </button>
152
+ ) : <span className="w-5" />}
153
+ </div>
154
+ {node.children?.edges?.length && expanded.has(node.id) ? (
155
+ <ul className="mt-2 ml-6 text-sm text-[var(--color-secondary-800)] space-y-2 md:space-y-1">
156
+ {node.children.edges.map(({ node: child }) => (
157
+ <li key={child.id} >
158
+ <label className="flex flex-1 items-center gap-2 font-secondary w-full cursor-pointer">
159
+ <input
160
+ type="checkbox"
161
+ className="h-5 w-5 md:h-4 md:w-4 accent-[var(--color-primary-600)] cursor-pointer"
162
+ checked={isChecked(child.slug)}
163
+ onChange={() => toggle(child.slug)}
164
+ />
165
+ <div className="flex justify-between w-full items-center cursor-pointer">
166
+ <span className="truncate">{child.name}</span>
167
+ {typeof child.products?.totalCount === "number" && (
168
+ <span className="text-xs font-secondary bg-[var(--color-secondary-200)] text-[var(--color-secondary-800)] size-fit px-0.5 text-center leading-4">
169
+ {child.products.totalCount}
170
+ </span>
171
+ )}
172
+ </div>
173
+ </label>
174
+ </li>
175
+ ))}
176
+ </ul>
177
+ ) : null}
178
+ </li>
179
+ ))}
180
+ </ul>
181
+ </div>
182
+ </FiltersCollapsible>
183
+ );
184
+ }
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { useRouter, useSearchParams, usePathname } from "next/navigation";
5
+ // import { ChevronDownIcon } from "@/app/utils/svgs/chevronDownIcon";
6
+
7
+ const OPTIONS = [10, 20, 50, 100] as const;
8
+
9
+ type Option = (typeof OPTIONS)[number];
10
+
11
+ export default function ItemsPerPageSelect() {
12
+ const router = useRouter();
13
+ const searchParams = useSearchParams();
14
+ const pathname = usePathname();
15
+
16
+ const value: Option = useMemo(() => {
17
+ const raw = searchParams?.get("first");
18
+ const n = raw ? parseInt(raw, 10) : NaN;
19
+ return (OPTIONS as readonly number[]).includes(n) ? (n as Option) : 20;
20
+ }, [searchParams]);
21
+
22
+ const onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
23
+ const next = parseInt(e.target.value, 10) as Option;
24
+ const params = new URLSearchParams(searchParams?.toString() || "");
25
+ params.set("first", String(next));
26
+ // reset pagination cursor when page size changes
27
+ params.delete("after");
28
+ const href = `${pathname}?${params.toString()}`;
29
+ router.push(href, { scroll: false });
30
+ };
31
+
32
+ return (
33
+ <label className="flex items-center justify-end gap-2 text-sm font-secondary">
34
+ <span className="text-[var(--color-secondary-700)] flex-shrink-0">Items per page:</span>
35
+ <div className="relative inline-block">
36
+ <select
37
+ aria-label="Items per page"
38
+ value={value}
39
+ onChange={onChange}
40
+ className="h-9 pl-2 pr-8 rounded border bg-[var(--color-secondary-50)] text-[var(--color-secondary-900)] border-[var(--color-secondary-300)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-600)] cursor-pointer appearance-none"
41
+ >
42
+ {OPTIONS.map((opt) => (
43
+ <option key={opt} value={opt} className="bg-white text-[var(--color-secondary-900)]">
44
+ {opt}
45
+ </option>
46
+ ))}
47
+ </select>
48
+ <span className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2">
49
+ <svg
50
+ xmlns="http://www.w3.org/2000/svg"
51
+ viewBox="0 0 16 16"
52
+ width="16"
53
+ height="16"
54
+ fill="none"
55
+ aria-hidden
56
+ >
57
+ <path
58
+ d="M13 6L8 11L3 6"
59
+ stroke="var(--color-secondary-700)"
60
+ strokeLinecap="round"
61
+ strokeLinejoin="round"
62
+ strokeWidth="1.5"
63
+ />
64
+ </svg>
65
+ </span>
66
+ </div>
67
+ </label>
68
+ );
69
+ }
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ const OPTIONS = [10, 20, 50, 100] as const;
4
+
5
+ type Option = (typeof OPTIONS)[number];
6
+
7
+ interface ItemsPerPageSelectClientProps {
8
+ value: Option;
9
+ onChange: (value: Option) => void;
10
+ }
11
+
12
+ export default function ItemsPerPageSelectClient({
13
+ value,
14
+ onChange,
15
+ }: ItemsPerPageSelectClientProps) {
16
+ const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
17
+ const next = parseInt(e.target.value, 10) as Option;
18
+ onChange(next);
19
+ };
20
+
21
+ return (
22
+ <label className="flex items-center justify-end gap-2 text-sm font-secondary ml-1">
23
+ {/* <span className="text-[var(--color-secondary-700)] flex-shrink-0">Items per page:</span> */}
24
+ <div className="relative inline-block">
25
+ <select
26
+ aria-label="Items per page"
27
+ value={value}
28
+ onChange={handleChange}
29
+ className="h-12 pl-3 pr-8 border bg-[var(--color-secondary-110)] text-[var(--color-secondary-900)] border-[var(--color-secondary-300)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-600)] cursor-pointer appearance-none"
30
+ >
31
+ {OPTIONS.map((opt) => (
32
+ <option key={opt} value={opt} className="bg-white text-[var(--color-secondary-900)]">
33
+ {opt}
34
+ </option>
35
+ ))}
36
+ </select>
37
+ <span className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2">
38
+ <svg
39
+ xmlns="http://www.w3.org/2000/svg"
40
+ viewBox="0 0 16 16"
41
+ width="16"
42
+ height="16"
43
+ fill="none"
44
+ aria-hidden
45
+ >
46
+ <path
47
+ d="M13 6L8 11L3 6"
48
+ stroke="var(--color-secondary-700)"
49
+ strokeLinecap="round"
50
+ strokeLinejoin="round"
51
+ strokeWidth="1.5"
52
+ />
53
+ </svg>
54
+ </span>
55
+ </div>
56
+ </label>
57
+ );
58
+ }
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import ModalLayout from "@/app/components/reuseableUI/modalLayout";
5
+ import CategoryFilter from "@/app/components/shop/CategoryFilter";
6
+ import type { CategoryNode } from "@/graphql/queries/getAllCategoriesTree";
7
+ import { FilterIcon } from "@/app/utils/svgs/filterIcon";
8
+ import { useRouter, useSearchParams } from "next/navigation";
9
+
10
+ export default function MobileFilters({
11
+ categories,
12
+ selectedSlugs,
13
+ basePath = "/products/all",
14
+ }: {
15
+ categories: { edges: { node: CategoryNode }[] };
16
+ selectedSlugs: string[];
17
+ basePath?: string;
18
+ }) {
19
+ const router = useRouter();
20
+ const searchParams = useSearchParams();
21
+ const [open, setOpen] = useState(false);
22
+ const activeCount = selectedSlugs.length;
23
+ const [tempSelected, setTempSelected] = useState<string[]>(selectedSlugs);
24
+
25
+ // Sync temp selection with current when opening
26
+ useEffect(() => {
27
+ if (open) setTempSelected(selectedSlugs);
28
+ // eslint-disable-next-line react-hooks/exhaustive-deps
29
+ }, [open]);
30
+
31
+ const applyFilters = () => {
32
+ const params = new URLSearchParams(searchParams?.toString() || "");
33
+ params.delete("category");
34
+ params.delete("after");
35
+ for (const s of tempSelected) params.append("category", s);
36
+ const href = `${basePath}?${params.toString()}`;
37
+ router.push(href, { scroll: false });
38
+ setOpen(false);
39
+ };
40
+
41
+ return (
42
+ <div className="lg:hidden w-full pt-6 z-50">
43
+ <div className="flex justify-between items-center w-full">
44
+ <p className="font-semibold text-sm px-4 py-3.5">FILTER BY</p>
45
+ <button
46
+ type="button"
47
+ aria-label={activeCount > 0 ? `Open filters, ${activeCount} active` : "Open filters"}
48
+ className="p-3.5 cursor-pointer relative"
49
+ onClick={() => setOpen(true)}
50
+ >
51
+ {FilterIcon}
52
+ {activeCount > 0 && (
53
+ <span
54
+ aria-hidden
55
+ className="absolute -top-0.5 -right-0.5 min-w-[18px] h-[18px] px-1 rounded-full bg-[var(--color-primary)] text-white text-[10px] leading-[18px] text-center font-semibold"
56
+ >
57
+ {activeCount}
58
+ </span>
59
+ )}
60
+ </button>
61
+ </div>
62
+
63
+ <ModalLayout
64
+ isModalOpen={open}
65
+ onClose={() => setOpen(false)}
66
+ heading="Filters"
67
+ className="lg:max-w-lg overflow-hidden"
68
+ >
69
+ <div className="flex h-full flex-col">
70
+ {/* Scrollable content */}
71
+ <div className="flex-1 overflow-y-auto mt-4 md:mt-6 pt-1 md:pt-2 space-y-4 px-1">
72
+ <CategoryFilter
73
+ categories={categories}
74
+ selectedSlugs={tempSelected}
75
+ onChangeSelected={setTempSelected}
76
+ />
77
+ </div>
78
+
79
+ {/* Fixed footer inside modal panel (non-scrolling) */}
80
+ <div className="shrink-0 border-t border-[var(--color-secondary-200)] bg-[var(--color-secondary-50)] px-4 py-3">
81
+ <div className="space-y-2">
82
+ <button
83
+ type="button"
84
+ onClick={applyFilters}
85
+ className="w-full h-11 rounded bg-[var(--color-primary)] text-white font-secondary font-semibold"
86
+ >
87
+ Apply Filters
88
+ </button>
89
+ <button
90
+ type="button"
91
+ onClick={() => setTempSelected([])}
92
+ className="w-full h-11 rounded border border-[var(--color-secondary-300)] text-[var(--color-secondary-800)] bg-white font-secondary"
93
+ >
94
+ Clear
95
+ </button>
96
+ </div>
97
+ <div className="pt-[env(safe-area-inset-bottom)]" />
98
+ </div>
99
+ </div>
100
+ </ModalLayout>
101
+ </div>
102
+ );
103
+ }
@@ -0,0 +1,16 @@
1
+ export default function ProductGridSkeleton() {
2
+ return (
3
+ <div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
4
+ {[...Array(6)].map((_, i) => (
5
+ <div key={i} className="bg-white rounded-lg border border-[var(--color-secondary-200)] overflow-hidden animate-pulse">
6
+ <div className="aspect-square bg-[var(--color-secondary-100)]"></div>
7
+ <div className="p-4 space-y-2">
8
+ <div className="h-4 bg-[var(--color-secondary-100)] rounded w-3/4"></div>
9
+ <div className="h-4 bg-[var(--color-secondary-100)] rounded w-1/2"></div>
10
+ <div className="h-4 bg-[var(--color-secondary-100)] rounded w-1/4"></div>
11
+ </div>
12
+ </div>
13
+ ))}
14
+ </div>
15
+ );
16
+ }
@@ -0,0 +1,230 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { shopApi } from "@/lib/api/shop";
5
+ import { ProductCard } from "@/app/components/reuseableUI/productCard";
6
+ import EmptyState from "@/app/components/reuseableUI/emptyState";
7
+ import { gtmViewItemList, gtmSearchWithResults, Product } from "@/app/utils/googleTagManager";
8
+ import { useAppConfiguration } from "../providers/ServerAppConfigurationProvider";
9
+
10
+ export interface ProductsGridProps {
11
+ initialEdges: Array<{ node: { id: string; name: string; slug: string; media?: Array<{ id: string; url: string; alt: string | null }>; category: { id: string; name: string } | null; pricing: { onSale: boolean | null; priceRange: { start: { gross: { amount: number; currency: string } } | null; stop: { gross: { amount: number; currency: string } } | null } | null } | null } }>;
12
+ initialPageInfo: { hasNextPage: boolean; hasPreviousPage: boolean; startCursor: string | null; endCursor: string | null };
13
+ channel: string;
14
+ first: number;
15
+ categoryIds?: string[];
16
+ brandIds?: string[];
17
+ search?: string;
18
+ fitment?: string;
19
+ layout?: 'shop' | 'category';
20
+ }
21
+
22
+ export default function ProductsGrid({
23
+ initialEdges,
24
+ initialPageInfo,
25
+ channel,
26
+ first,
27
+ categoryIds,
28
+ brandIds,
29
+ search,
30
+ fitment,
31
+ layout = 'shop',
32
+ }: ProductsGridProps) {
33
+ const { getGoogleTagManagerConfig } = useAppConfiguration();
34
+ const gtmConfig = getGoogleTagManagerConfig();
35
+ const [edges, setEdges] = useState(initialEdges);
36
+ const [pageInfo, setPageInfo] = useState(initialPageInfo);
37
+ const [loadingMore, setLoadingMore] = useState(false);
38
+ const [error, setError] = useState<string | null>(null);
39
+
40
+ // Reset grid when any upstream input changes (e.g., filters via URL)
41
+ const contextKey = useMemo(
42
+ () => JSON.stringify({ c: channel, f: first, cats: categoryIds ?? [], brands: brandIds ?? [], search: search ?? "" }),
43
+ [channel, first, categoryIds, brandIds, search]
44
+ );
45
+ const currentKeyRef = useRef(contextKey);
46
+ useEffect(() => {
47
+ setEdges(initialEdges);
48
+ setPageInfo(initialPageInfo);
49
+ setError(null);
50
+ setLoadingMore(false);
51
+ currentKeyRef.current = contextKey;
52
+ }, [initialEdges, initialPageInfo, contextKey]);
53
+
54
+ // Track product impressions
55
+ useEffect(() => {
56
+ if (edges.length > 0) {
57
+ const products: Product[] = edges.map(({ node }, index) => ({
58
+ item_id: node.id,
59
+ item_name: node.name,
60
+ item_category: node.category?.name || 'Products',
61
+ price: node.pricing?.priceRange?.start?.gross?.amount || 0,
62
+ currency: node.pricing?.priceRange?.start?.gross?.currency || 'USD',
63
+ index: index,
64
+ item_list_name: layout === 'category' ? 'Category Products' : search ? 'Search Results' : 'Shop Products',
65
+ item_list_id: layout === 'category' ? 'category_list' : search ? 'search_results' : 'shop_list',
66
+ }));
67
+
68
+ gtmViewItemList(
69
+ products,
70
+ layout === 'category' ? 'Category Products' : search ? 'Search Results' : 'Shop Products',
71
+ layout === 'category' ? 'category_list' : search ? 'search_results' : 'shop_list',
72
+ gtmConfig?.container_id
73
+ );
74
+
75
+ // Track search results if this is a search
76
+ if (search && search.trim()) {
77
+ gtmSearchWithResults(search.trim(), edges.length, 'Products', gtmConfig?.container_id);
78
+ }
79
+ }
80
+ }, [edges, layout, search]);
81
+
82
+ const loadMore = async () => {
83
+ if (!pageInfo?.hasNextPage || loadingMore) return;
84
+ setLoadingMore(true);
85
+ setError(null);
86
+ try {
87
+ const requestKey = currentKeyRef.current;
88
+
89
+ // Use cursor-based pagination for GraphQL
90
+ const after = pageInfo?.endCursor;
91
+
92
+ let response;
93
+
94
+ if (categoryIds && categoryIds.length > 0) {
95
+ // Category-specific products
96
+ response = await shopApi.getProductsByCategory({
97
+ categoryIds,
98
+ channel,
99
+ first,
100
+ after: after || undefined,
101
+ search: search || undefined
102
+ });
103
+ } else if (brandIds && brandIds.length > 0) {
104
+ // Brand-specific products (product types)
105
+ response = await shopApi.getProductsByProductType({
106
+ productTypeIds: brandIds,
107
+ channel,
108
+ first,
109
+ after: after || undefined,
110
+ search: search || undefined
111
+ });
112
+ } else {
113
+ // General product search using existing function
114
+ response = await shopApi.getProductsByCategoriesAndProductTypes({
115
+ categoryIds: categoryIds || undefined,
116
+ productTypeIds: brandIds || undefined,
117
+ channel,
118
+ first,
119
+ sortField: "DATE",
120
+ sortDirection: "ASC"
121
+ });
122
+ }
123
+
124
+ // Ignore stale responses if inputs changed during the request
125
+ if (requestKey !== currentKeyRef.current) return;
126
+
127
+ // Products are already in the correct GraphQL format, but need to add onSale field
128
+ type GraphQLEdgeWithOnSale = {
129
+ cursor: string;
130
+ node: {
131
+ id: string;
132
+ name: string;
133
+ slug: string;
134
+ description: string;
135
+ category: { id: string; name: string } | null;
136
+ productType: { id: string; name: string } | null;
137
+ media: Array<{ id: string; url: string; alt: string | null }>;
138
+ pricing: {
139
+ onSale: boolean | null;
140
+ priceRange: {
141
+ start: { gross: { amount: number; currency: string } } | null;
142
+ stop: { gross: { amount: number; currency: string } } | null;
143
+ } | null;
144
+ } | null;
145
+ };
146
+ };
147
+
148
+ const newEdges: GraphQLEdgeWithOnSale[] = response.products.edges.map((edge) => ({
149
+ ...edge,
150
+ node: {
151
+ ...edge.node,
152
+ pricing: edge.node.pricing ? {
153
+ ...edge.node.pricing,
154
+ onSale: null // GraphQL doesn't provide onSale info
155
+ } : null
156
+ }
157
+ }));
158
+
159
+ // Dedupe by product ID
160
+ setEdges((prev) => {
161
+ const seen = new Set(prev.map((e) => e.node.id));
162
+ const fresh = newEdges.filter((e) => !seen.has(e.node.id));
163
+ return [...prev, ...fresh];
164
+ });
165
+
166
+ // Update page info from GraphQL response
167
+ setPageInfo(response.products.pageInfo);
168
+
169
+ } catch (e) {
170
+ console.error('Load more error:', e);
171
+ setError(e instanceof Error ? e.message : "Failed to load more products");
172
+ } finally {
173
+ setLoadingMore(false);
174
+ }
175
+ };
176
+
177
+ return (
178
+ <>
179
+ {edges.length > 0 ? (
180
+ <div className={`grid gap-4 ${
181
+ layout === 'category'
182
+ ? 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'
183
+ : 'grid-cols-2 lg:grid-cols-3'
184
+ }`}>
185
+ {edges.map(({ node }) => {
186
+ const img = node.media?.[0]?.url || "/no-image-avail-large.png";
187
+ const price = node.pricing?.priceRange?.start?.gross?.amount ?? 0;
188
+ // Use the original Saleor slug as-is for the URL
189
+ const href = `/product/${encodeURIComponent(node.slug)}`;
190
+ return (
191
+ <ProductCard
192
+ key={node.id}
193
+ id={node.id}
194
+ name={node.name}
195
+ image={img}
196
+ href={href}
197
+ price={price}
198
+ category_id={node.category?.id || ""}
199
+ category={node.category?.name || ""}
200
+ onSale={Boolean(node.pricing?.onSale)}
201
+ />
202
+ );
203
+ })}
204
+ </div>
205
+ ) : (
206
+ <EmptyState
207
+ text="No products found"
208
+ textParagraph="Try adjusting your search or filter to find what you're looking for."
209
+ className="col-span-full my-12"
210
+ />
211
+ )}
212
+
213
+ {error ? (
214
+ <div className="mt-4 text-sm text-red-600">{error}</div>
215
+ ) : null}
216
+
217
+ {pageInfo?.hasNextPage && (
218
+ <div className="mt-10 flex justify-center">
219
+ <button
220
+ onClick={loadMore}
221
+ disabled={loadingMore}
222
+ className="px-4 py-2 bg-[var(--color-secondary-200)] text-gray-800 hover:opacity-80 disabled:opacity-60"
223
+ >
224
+ {loadingMore ? "Loading..." : "Load More"}
225
+ </button>
226
+ </div>
227
+ )}
228
+ </>
229
+ );
230
+ }