@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,377 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, Suspense } from "react";
4
+ import { useRouter, useSearchParams } from "next/navigation";
5
+ import Breadcrumb from "@/app/components/reuseableUI/breadcrumb";
6
+ import EmptyState from "@/app/components/reuseableUI/emptyState";
7
+ import { ProductCard } from "@/app/components/reuseableUI/productCard";
8
+ import { FiltersCollapsible } from "@/app/components/filtersCollapsible";
9
+ import ItemsPerPageSelectClient from "@/app/components/shop/ItemsPerPageSelectClient";
10
+ import { shopApi, type PLSearchProduct } from "@/lib/api/shop";
11
+
12
+ type ItemsPerPage = 10 | 20 | 50 | 100;
13
+
14
+ interface SubcategoryItem {
15
+ id: string;
16
+ name: string;
17
+ slug: string;
18
+ count: number;
19
+ children?: SubcategoryItem[];
20
+ }
21
+
22
+ interface APICategory {
23
+ id: string;
24
+ name: string;
25
+ slug: string;
26
+ image?: string;
27
+ product_count?: number;
28
+ children?: APICategory[];
29
+ }
30
+
31
+ function CategoryPageClientInner(props: { slug: string }) {
32
+ const { slug } = props;
33
+ const router = useRouter();
34
+ const searchParams = useSearchParams();
35
+
36
+ const selectedSubcategory = searchParams?.get("subcategory") || "";
37
+
38
+ const [itemsPerPage, setItemsPerPage] = useState<ItemsPerPage>(10);
39
+ const [searchQuery, setSearchQuery] = useState("");
40
+ const [products, setProducts] = useState<PLSearchProduct[]>([]);
41
+ const [loading, setLoading] = useState(false);
42
+ const [loadingMore, setLoadingMore] = useState(false);
43
+ const [categoryName, setCategoryName] = useState<string>("");
44
+ const [subcategories, setSubcategories] = useState<SubcategoryItem[]>([]);
45
+ const [pagination, setPagination] = useState({
46
+ total: 0,
47
+ page: 1,
48
+ per_page: 10,
49
+ total_pages: 0,
50
+ });
51
+
52
+ // Helper to find a subcategory by slug in the tree
53
+ const findSubcategoryBySlug = (
54
+ items: SubcategoryItem[],
55
+ targetSlug: string
56
+ ): SubcategoryItem | null => {
57
+ for (const item of items) {
58
+ if (item.slug === targetSlug) return item;
59
+ if (item.children) {
60
+ const found = findSubcategoryBySlug(item.children, targetSlug);
61
+ if (found) return found;
62
+ }
63
+ }
64
+ return null;
65
+ };
66
+
67
+ // Get the selected subcategory object
68
+ const selectedSubcategoryObj = selectedSubcategory
69
+ ? findSubcategoryBySlug(subcategories, selectedSubcategory)
70
+ : null;
71
+
72
+ // Get the display name for the selected subcategory
73
+ const selectedSubcategoryName = selectedSubcategoryObj?.name;
74
+
75
+ // Determine which subcategories to display in the sidebar
76
+ // If selected subcategory has children, show those; otherwise show current level
77
+ const displayedSubcategories =
78
+ selectedSubcategoryObj?.children && selectedSubcategoryObj.children.length > 0
79
+ ? selectedSubcategoryObj.children
80
+ : subcategories;
81
+
82
+ // Update URL with subcategory parameter
83
+ const handleSubcategoryChange = (subcategorySlug: string) => {
84
+ const params = new URLSearchParams(searchParams?.toString() || "");
85
+
86
+ if (subcategorySlug === selectedSubcategory) {
87
+ // Deselect if clicking the same one
88
+ params.delete("subcategory");
89
+ } else {
90
+ params.set("subcategory", subcategorySlug);
91
+ }
92
+
93
+ const queryString = params.toString();
94
+ router.push(queryString ? `?${queryString}` : `/category/${slug}`, { scroll: false });
95
+ };
96
+
97
+ // Clear subcategory filter
98
+ const clearSubcategory = () => {
99
+ const params = new URLSearchParams(searchParams?.toString() || "");
100
+ params.delete("subcategory");
101
+ const queryString = params.toString();
102
+ router.push(queryString ? `?${queryString}` : `/category/${slug}`, { scroll: false });
103
+ };
104
+
105
+ // Fetch subcategories for the current category
106
+ useEffect(() => {
107
+ const fetchSubcategories = async () => {
108
+ try {
109
+ const partsLogicUrl = process.env.NEXT_PUBLIC_PARTSLOGIC_URL || "";
110
+ const res = await fetch(
111
+ `${partsLogicUrl}/api/categories?page=1&per_page=100`,
112
+ {
113
+ headers: {
114
+ "Content-Type": "application/json",
115
+ Accept: "application/json",
116
+ },
117
+ }
118
+ );
119
+ if (res.ok) {
120
+ const data = await res.json();
121
+ const categories: APICategory[] = data.categories || [];
122
+
123
+ // Find the current category and get its direct children only (flattened, no nested)
124
+ const findCategoryChildren = (cats: APICategory[], targetSlug: string): APICategory[] | null => {
125
+ for (const cat of cats) {
126
+ if (cat.slug === targetSlug) {
127
+ return cat.children || null;
128
+ }
129
+ if (cat.children) {
130
+ const found = findCategoryChildren(cat.children, targetSlug);
131
+ if (found) return found;
132
+ }
133
+ }
134
+ return null;
135
+ };
136
+
137
+ const children = findCategoryChildren(categories, slug);
138
+
139
+ if (children && children.length > 0) {
140
+ // Convert to SubcategoryItem format (preserve children for drill-down)
141
+ const convertToSubcategoryItem = (cat: APICategory): SubcategoryItem => ({
142
+ id: cat.id,
143
+ name: cat.name,
144
+ slug: cat.slug,
145
+ count: cat.product_count || 0,
146
+ children: cat.children?.map(convertToSubcategoryItem),
147
+ });
148
+ setSubcategories(children.map(convertToSubcategoryItem));
149
+ } else {
150
+ setSubcategories([]);
151
+ }
152
+ }
153
+ } catch (error) {
154
+ console.error("Error fetching subcategories:", error);
155
+ setSubcategories([]);
156
+ }
157
+ };
158
+
159
+ fetchSubcategories();
160
+ }, [slug]);
161
+
162
+ useEffect(() => {
163
+ const fetchProducts = async () => {
164
+ setLoading(true);
165
+ try {
166
+ // If subcategory is selected, filter by that; otherwise use the main category
167
+ const categorySlug = selectedSubcategory || slug;
168
+
169
+ const response = await shopApi.getProductsBySlug({
170
+ slug: categorySlug,
171
+ page: 1,
172
+ per_page: itemsPerPage,
173
+ search: searchQuery || undefined,
174
+ filterType: "category_slug",
175
+ });
176
+ setProducts(response.products || []);
177
+ setPagination(response.pagination);
178
+ if (response.products && response.products.length > 0) {
179
+ setCategoryName(response.products[0].category_name || slug);
180
+ } else {
181
+ setCategoryName(slug);
182
+ }
183
+ } catch (error) {
184
+ console.error("Error fetching products:", error);
185
+ setProducts([]);
186
+ } finally {
187
+ setLoading(false);
188
+ }
189
+ };
190
+
191
+ fetchProducts();
192
+ }, [itemsPerPage, searchQuery, slug, selectedSubcategory]);
193
+
194
+ const loadMore = async () => {
195
+ if (loadingMore || pagination.page >= pagination.total_pages) return;
196
+
197
+ setLoadingMore(true);
198
+ const nextPage = pagination.page + 1;
199
+ const categorySlug = selectedSubcategory || slug;
200
+
201
+ try {
202
+ const response = await shopApi.getProductsBySlug({
203
+ slug: categorySlug,
204
+ page: nextPage,
205
+ per_page: itemsPerPage,
206
+ search: searchQuery || undefined,
207
+ filterType: "category_slug",
208
+ });
209
+
210
+ const newProducts = response.products || [];
211
+ setProducts((prev) => [...prev, ...newProducts]);
212
+ setPagination(response.pagination);
213
+ } catch (error) {
214
+ console.error("Error loading more products:", error);
215
+ } finally {
216
+ setLoadingMore(false);
217
+ }
218
+ };
219
+
220
+ const displayName = selectedSubcategoryName || slug.replaceAll("-", " ");
221
+
222
+ const breadcrumbItems = [
223
+ { text: "HOME", link: "/" },
224
+ { text: "SHOP", link: "/products/all" },
225
+ { text: slug.replaceAll("-", " "), link: selectedSubcategory ? `/category/${slug}` : undefined },
226
+ ...(selectedSubcategoryName ? [{ text: selectedSubcategoryName }] : []),
227
+ ];
228
+
229
+ return (
230
+ <div className="container mx-auto min-h-[100dvh] py-12 px-4 md:px-6 md:py-16 lg:py-24 lg:px-0 relative">
231
+ <div className="space-y-5">
232
+ <Breadcrumb items={breadcrumbItems} />
233
+ <div className="flex flex-col lg:flex-row items-start lg:items-center justify-between w-full gap-4">
234
+ <div className="space-y-2">
235
+ <h2 className="font-normal uppercase text-[var(--color-secondary-800)] text-xl md:text-3xl lg:text-5xl font-primary">
236
+ {displayName}
237
+ </h2>
238
+ {searchQuery && (
239
+ <div className="flex items-center gap-2 text-sm text-[var(--color-secondary-600)]">
240
+ <div className="flex items-center gap-2">
241
+ {pagination.total > 0
242
+ ? `Found ${pagination.total} result${
243
+ pagination.total === 1 ? "" : "s"
244
+ } for "${searchQuery}" in ${categoryName || displayName}`
245
+ : `No results found for "${searchQuery}" in ${
246
+ categoryName || displayName
247
+ }`}
248
+ </div>
249
+ </div>
250
+ )}
251
+ {!searchQuery && (
252
+ <div className="flex items-center gap-2 text-sm text-[var(--color-secondary-600)]">
253
+ <div className="flex items-center gap-2">
254
+ {pagination.total} product{pagination.total === 1 ? "" : "s"}{" "}
255
+ in {displayName}
256
+ </div>
257
+ </div>
258
+ )}
259
+ </div>
260
+ <div className="ml-auto">
261
+ <ItemsPerPageSelectClient
262
+ value={itemsPerPage}
263
+ onChange={setItemsPerPage}
264
+ />
265
+ </div>
266
+ </div>
267
+ </div>
268
+
269
+ <div className="mt-10 flex gap-8">
270
+ {/* Sidebar - only show if there are subcategories to display */}
271
+ {displayedSubcategories.length > 0 && (
272
+ <aside className="hidden lg:block w-72 flex-shrink-0">
273
+ <div className="sticky top-32 space-y-6">
274
+ <FiltersCollapsible title="Subcategories" defaultOpen={true}>
275
+ <div className="space-y-3 px-1 overflow-y-auto max-h-96">
276
+ {selectedSubcategory && (
277
+ <button
278
+ type="button"
279
+ onClick={clearSubcategory}
280
+ className="text-sm text-[var(--color-primary-600)] hover:text-[var(--color-primary-700)] font-secondary"
281
+ >
282
+ Clear filter
283
+ </button>
284
+ )}
285
+ {displayedSubcategories.map((subcategory) => (
286
+ <label
287
+ key={subcategory.id}
288
+ className="flex items-center gap-2 font-secondary text-[var(--color-secondary-800)] cursor-pointer"
289
+ >
290
+ <input
291
+ type="radio"
292
+ name="subcategory"
293
+ className="h-5 w-5 md:h-4 md:w-4 accent-[var(--color-primary-600)] cursor-pointer"
294
+ checked={selectedSubcategory === subcategory.slug}
295
+ onChange={() => handleSubcategoryChange(subcategory.slug)}
296
+ />
297
+ <div className="flex justify-between w-full items-center">
298
+ <span className="truncate">{subcategory.name}</span>
299
+ <span className="text-xs font-secondary bg-[var(--color-secondary-200)] text-[var(--color-secondary-800)] px-2 py-0.5 rounded">
300
+ {subcategory.count}
301
+ </span>
302
+ </div>
303
+ </label>
304
+ ))}
305
+ </div>
306
+ </FiltersCollapsible>
307
+ </div>
308
+ </aside>
309
+ )}
310
+
311
+ <section className="flex-1 relative min-h-[400px]">
312
+ {loading && (
313
+ <div className="h-[60vh] bg-white/70 z-10 flex items-center justify-center rounded-lg">
314
+ <div className="size-12 border-t-2 border-black rounded-full animate-spin" />
315
+ </div>
316
+ )}
317
+
318
+ <div className={`grid grid-cols-2 sm:grid-cols-3 ${displayedSubcategories.length > 0 ? 'lg:grid-cols-3' : 'lg:grid-cols-4'} gap-4`}>
319
+ {loading ? null : products && products.length > 0 ? (
320
+ products.map((item) => (
321
+ <ProductCard
322
+ key={item.id}
323
+ id={item.id}
324
+ name={item.name}
325
+ image={item.primary_image || "/no-image-avail-large.png"}
326
+ href={`/product/${item.slug}`}
327
+ price={item.price_min || 0}
328
+ category_id={item.category_id || ""}
329
+ category={item.category_name || ""}
330
+ discount={
331
+ item.price_max &&
332
+ item.price_min &&
333
+ item.price_max > item.price_min
334
+ ? item.price_max - item.price_min
335
+ : null
336
+ }
337
+ isFeatured={
338
+ item.collection_names?.includes("Best Sellers") || false
339
+ }
340
+ onSale={(item.price_max || 0) > (item.price_min || 0)}
341
+ />
342
+ ))
343
+ ) : (
344
+ <EmptyState
345
+ text="No products found"
346
+ textParagraph="Try adjusting your search or filter to find what you're looking for."
347
+ className="col-span-full my-12"
348
+ />
349
+ )}
350
+ </div>
351
+
352
+ {!loading &&
353
+ pagination.page < pagination.total_pages &&
354
+ products.length > 0 && (
355
+ <div className="mt-10 flex justify-center">
356
+ <button
357
+ onClick={loadMore}
358
+ disabled={loadingMore}
359
+ className="px-4 py-2 bg-[var(--color-secondary-200)] text-gray-800 hover:opacity-80 disabled:opacity-60 transition-opacity"
360
+ >
361
+ {loadingMore ? "Loading..." : "Load More"}
362
+ </button>
363
+ </div>
364
+ )}
365
+ </section>
366
+ </div>
367
+ </div>
368
+ );
369
+ }
370
+
371
+ export default function CategoryPageClient(props: { slug: string }) {
372
+ return (
373
+ <Suspense>
374
+ <CategoryPageClientInner {...props} />
375
+ </Suspense>
376
+ );
377
+ }
@@ -0,0 +1,17 @@
1
+ import type { Metadata } from "next"
2
+ import { getStoreName } from "@/app/utils/branding"
3
+
4
+ export async function generateMetadata(): Promise<Metadata> {
5
+ return {
6
+ title: `Category - ${getStoreName()}`,
7
+ description: `Browse products in this category. Find high-quality parts and accessories for your needs.`,
8
+ }
9
+ }
10
+
11
+ export default function CategoryDetailLayout({
12
+ children,
13
+ }: {
14
+ children: React.ReactNode
15
+ }) {
16
+ return children
17
+ }
@@ -0,0 +1,224 @@
1
+ import CategoryPageClient from "./CategoryPageClient";
2
+ import {
3
+ generateProductCategoryPageSchema,
4
+ generateBreadcrumbSchema,
5
+ } from "@/lib/schema";
6
+ import { Metadata } from "next";
7
+ import { Suspense } from "react";
8
+ import { notFound } from "next/navigation";
9
+ import ServerProductGrid, {
10
+ type ServerGridProduct,
11
+ } from "@/app/components/seo/ServerProductGrid";
12
+ import { extractTextFromEditorJs } from "@/lib/seo/extractTextFromEditorJs";
13
+
14
+ export const revalidate = 300;
15
+
16
+ type SearchParams = Record<string, string | string[] | undefined>;
17
+
18
+ function toTitleCaseFromSlug(input: string): string {
19
+ return input
20
+ .replace(/-/g, " ")
21
+ .replace(/\b\w/g, (l) => l.toUpperCase());
22
+ }
23
+
24
+ function getFirstParam(v: string | string[] | undefined): string | undefined {
25
+ if (!v) return undefined;
26
+ return Array.isArray(v) ? v[0] : v;
27
+ }
28
+
29
+ function toInt(v: string | undefined): number | undefined {
30
+ if (!v) return undefined;
31
+ const n = Number.parseInt(v, 10);
32
+ return Number.isFinite(n) && n > 0 ? n : undefined;
33
+ }
34
+
35
+ async function fetchCategoryListing(input: {
36
+ categorySlug: string;
37
+ page: number;
38
+ perPage: number;
39
+ }): Promise<{
40
+ products: ServerGridProduct[];
41
+ pagination?: { total?: number; page?: number; per_page?: number; total_pages?: number };
42
+ categoryDescriptionJson?: string | null;
43
+ }> {
44
+ const base = process.env.NEXT_PUBLIC_PARTSLOGIC_URL;
45
+ if (!base) return { products: [] };
46
+
47
+ const url = new URL("/api/search/products", base);
48
+ url.searchParams.set("category_slug", input.categorySlug);
49
+ url.searchParams.set("page", String(input.page));
50
+ url.searchParams.set("per_page", String(input.perPage));
51
+
52
+ const res = await fetch(url.toString(), {
53
+ headers: { Accept: "application/json" },
54
+ next: { revalidate },
55
+ });
56
+
57
+ if (!res.ok) return { products: [] };
58
+ const json = (await res.json()) as {
59
+ products?: Array<ServerGridProduct & { category_description_json?: string | null }>;
60
+ pagination?: { total?: number; page?: number; per_page?: number; total_pages?: number };
61
+ };
62
+
63
+ const products = Array.isArray(json.products) ? json.products : [];
64
+ const categoryDescriptionJson = products?.[0]?.category_description_json ?? null;
65
+ return { products, pagination: json.pagination, categoryDescriptionJson };
66
+ }
67
+
68
+ export async function generateMetadata({
69
+ params,
70
+ searchParams,
71
+ }: {
72
+ params: Promise<{ slug: string }>;
73
+ searchParams?: Promise<SearchParams>;
74
+ }): Promise<Metadata> {
75
+ const { slug } = await params;
76
+ const decodedSlug = decodeURIComponent(slug);
77
+ const sp = searchParams ? await searchParams : undefined;
78
+ const subcategory = getFirstParam(sp?.subcategory);
79
+ const page = toInt(getFirstParam(sp?.page));
80
+
81
+ const basePath = `/category/${slug}`;
82
+ const canonical =
83
+ page && page > 1 ? `${basePath}?page=${page}` : basePath;
84
+
85
+ const categoryName = toTitleCaseFromSlug(decodedSlug);
86
+ const displayName = subcategory
87
+ ? toTitleCaseFromSlug(subcategory)
88
+ : categoryName;
89
+
90
+ const shouldNoindex = Boolean(subcategory);
91
+
92
+ return {
93
+ title: `${displayName} | Shop`,
94
+ description: `Browse our ${displayName} collection. Find the best products in ${displayName} category.`,
95
+ keywords: `${displayName}, shop, products, buy online`,
96
+ alternates: {
97
+ canonical,
98
+ },
99
+ robots: shouldNoindex
100
+ ? {
101
+ index: false,
102
+ follow: true,
103
+ }
104
+ : undefined,
105
+ openGraph: {
106
+ title: `${displayName} | Shop`,
107
+ description: `Browse our ${displayName} collection`,
108
+ type: "website",
109
+ url: canonical,
110
+ images: [{ url: "/Logo.png" }],
111
+ },
112
+ twitter: {
113
+ card: "summary_large_image",
114
+ title: `${displayName} | Shop`,
115
+ description: `Browse our ${displayName} collection`,
116
+ images: ["/Logo.png"],
117
+ },
118
+ };
119
+ }
120
+
121
+ export default async function CategoryPage({
122
+ params,
123
+ searchParams,
124
+ }: {
125
+ params: Promise<{ slug: string }>;
126
+ searchParams?: Promise<SearchParams>;
127
+ }) {
128
+ const { slug } = await params;
129
+ const decodedSlug = decodeURIComponent(slug);
130
+ const sp = searchParams ? await searchParams : undefined;
131
+ const subcategory = getFirstParam(sp?.subcategory);
132
+ const page = toInt(getFirstParam(sp?.page)) ?? 1;
133
+
134
+ const categoryName = toTitleCaseFromSlug(decodedSlug);
135
+ const displayName = subcategory
136
+ ? toTitleCaseFromSlug(subcategory)
137
+ : categoryName;
138
+
139
+ // Ensure this category slug is real (server-side 404 for invalid slugs).
140
+ try {
141
+ const listing = await fetchCategoryListing({
142
+ categorySlug: subcategory || decodedSlug,
143
+ page: 1,
144
+ perPage: 1,
145
+ });
146
+ if (!listing.pagination || !listing.pagination.total) {
147
+ notFound();
148
+ }
149
+ } catch {
150
+ notFound();
151
+ }
152
+
153
+ const listing = await fetchCategoryListing({
154
+ categorySlug: subcategory || decodedSlug,
155
+ page,
156
+ perPage: 12,
157
+ });
158
+
159
+ const introText = extractTextFromEditorJs(listing.categoryDescriptionJson);
160
+
161
+ // Generate schema.org structured data
162
+ const categoryPageSchema = generateProductCategoryPageSchema(
163
+ displayName,
164
+ `Browse our ${displayName} collection. Find the best products in ${displayName} category.`,
165
+ `/category/${slug}`
166
+ );
167
+
168
+ const breadcrumbSchema = generateBreadcrumbSchema([
169
+ { name: "Home", url: "/" },
170
+ { name: "Categories", url: "/category" },
171
+ { name: categoryName, url: `/category/${slug}` },
172
+ ...(subcategory
173
+ ? [{ name: displayName, url: `/category/${slug}?subcategory=${encodeURIComponent(subcategory)}` }]
174
+ : []),
175
+ ]);
176
+
177
+ return (
178
+ <main className="h-full w-full">
179
+ {/* Schema.org structured data */}
180
+ <script
181
+ type="application/ld+json"
182
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(categoryPageSchema) }}
183
+ />
184
+ <script
185
+ type="application/ld+json"
186
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
187
+ />
188
+
189
+ <div className="container mx-auto min-h-[100dvh] py-12 px-4 md:px-6 md:py-16 lg:py-24 lg:px-0">
190
+ <div className="space-y-5">
191
+ <div className="text-xs font-semibold text-[var(--color-secondary-600)]">
192
+ Home / Categories / {displayName}
193
+ </div>
194
+ <div className="space-y-2">
195
+ <h1 className="font-normal uppercase text-[var(--color-secondary-800)] text-xl md:text-3xl lg:text-5xl font-primary">
196
+ {displayName}
197
+ </h1>
198
+ {introText ? (
199
+ <p className="text-sm text-[var(--color-secondary-600)] max-w-3xl">
200
+ {introText}
201
+ </p>
202
+ ) : (
203
+ <p className="text-sm text-[var(--color-secondary-600)]">
204
+ Browse products in {displayName}.
205
+ </p>
206
+ )}
207
+ </div>
208
+
209
+ {/* SSR fallback (visible with JS disabled) */}
210
+ <div className="ssr-only">
211
+ <ServerProductGrid products={listing.products || []} />
212
+ </div>
213
+
214
+ {/* Enhanced client experience (visible with JS enabled) */}
215
+ <div className="js-only">
216
+ <Suspense>
217
+ <CategoryPageClient slug={slug} />
218
+ </Suspense>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ </main>
223
+ );
224
+ }