@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,244 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ const TRANSACTION_PROCESS = `
4
+ mutation TransactionProcess(
5
+ $id: ID!
6
+ $data: JSON
7
+ ) {
8
+ transactionProcess(
9
+ id: $id
10
+ data: $data
11
+ ) {
12
+ transaction {
13
+ id
14
+ actions
15
+ chargedAmount {
16
+ amount
17
+ currency
18
+ }
19
+ checkout {
20
+ id
21
+ }
22
+ order {
23
+ id
24
+ number
25
+ total {
26
+ gross {
27
+ amount
28
+ currency
29
+ }
30
+ }
31
+ }
32
+ }
33
+ transactionEvent {
34
+ pspReference
35
+ message
36
+ type
37
+ }
38
+ data
39
+ errors {
40
+ field
41
+ message
42
+ code
43
+ }
44
+ }
45
+ }
46
+ `;
47
+
48
+ const CHECKOUT_COMPLETE = `
49
+ mutation CheckoutComplete($checkoutId: ID!) {
50
+ checkoutComplete(checkoutId: $checkoutId) {
51
+ order {
52
+ id
53
+ number
54
+ total {
55
+ gross {
56
+ amount
57
+ currency
58
+ }
59
+ }
60
+ }
61
+ errors {
62
+ field
63
+ message
64
+ code
65
+ }
66
+ }
67
+ }
68
+ `;
69
+
70
+ export async function POST(request: NextRequest) {
71
+ try {
72
+ const body = await request.json();
73
+ const { transactionId, affirmTransactionId, checkoutId } = body;
74
+
75
+ // Use transactionId if provided, otherwise fall back to checkoutId
76
+ const saleorTransactionId = transactionId || checkoutId;
77
+
78
+ if (!saleorTransactionId || !affirmTransactionId) {
79
+ return NextResponse.json(
80
+ { error: "Missing required fields: transactionId/checkoutId, affirmTransactionId" },
81
+ { status: 400 }
82
+ );
83
+ }
84
+
85
+ // Get Saleor API URL
86
+ const apiUrl = process.env.NEXT_PUBLIC_API_URL;
87
+ if (!apiUrl) {
88
+ throw new Error("NEXT_PUBLIC_API_URL is not configured");
89
+ }
90
+
91
+ // Get auth token from request cookies
92
+ const token = request.cookies.get("token")?.value;
93
+
94
+ const headers: Record<string, string> = {
95
+ "Content-Type": "application/json",
96
+ };
97
+
98
+ if (token) {
99
+ headers["Authorization"] = `Bearer ${token}`;
100
+ }
101
+
102
+ // Call Saleor's transaction process mutation
103
+ const response = await fetch(apiUrl, {
104
+ method: "POST",
105
+ headers,
106
+ body: JSON.stringify({
107
+ query: TRANSACTION_PROCESS,
108
+ variables: {
109
+ id: saleorTransactionId, // Use the correct transaction ID
110
+ data: {
111
+ affirmTransactionId: affirmTransactionId, // Send as object like PayPal
112
+ },
113
+ },
114
+ }),
115
+ });
116
+
117
+ if (!response.ok) {
118
+ const errorText = await response.text();
119
+ console.error("❌ Saleor request failed:", response.status, errorText);
120
+ throw new Error(`Saleor request failed: ${response.status} - ${errorText}`);
121
+ }
122
+
123
+ const result = await response.json();
124
+
125
+ const data = result.data;
126
+ const graphqlErrors = result.errors;
127
+
128
+ if (graphqlErrors || data?.transactionProcess?.errors?.length > 0) {
129
+ const errorMessage =
130
+ data?.transactionProcess?.errors?.[0]?.message ||
131
+ graphqlErrors?.[0]?.message ||
132
+ "Failed to process transaction";
133
+
134
+ console.error("❌ Transaction process error:", errorMessage);
135
+ return NextResponse.json({ error: errorMessage }, { status: 400 });
136
+ }
137
+
138
+ const transaction = data?.transactionProcess?.transaction;
139
+ const transactionEvent = data?.transactionProcess?.transactionEvent;
140
+ const order = transaction?.order;
141
+
142
+ // Avoid noisy logs (and accidental sensitive output) in templates/production.
143
+
144
+ // Check if payment was successful
145
+ if (transactionEvent?.type && !transactionEvent.type.includes("SUCCESS")) {
146
+ if (transactionEvent.type.includes("FAILURE")) {
147
+ return NextResponse.json(
148
+ { error: `Payment failed: ${transactionEvent?.message || "Unknown error"}` },
149
+ { status: 400 }
150
+ );
151
+ }
152
+ }
153
+
154
+ if (order) {
155
+ return NextResponse.json({
156
+ success: true,
157
+ order: {
158
+ id: order.id,
159
+ number: order.number,
160
+ total: order.total.gross.amount,
161
+ currency: order.total.gross.currency,
162
+ },
163
+ transactionId: transaction.id,
164
+ });
165
+ } else {
166
+ console.warn("⚠️ No order created by transactionProcess, attempting checkoutComplete...");
167
+
168
+ // Try to complete the checkout manually
169
+ const completeResponse = await fetch(apiUrl, {
170
+ method: "POST",
171
+ headers,
172
+ body: JSON.stringify({
173
+ query: CHECKOUT_COMPLETE,
174
+ variables: {
175
+ checkoutId: checkoutId || transaction.checkout?.id,
176
+ },
177
+ }),
178
+ });
179
+
180
+ if (!completeResponse.ok) {
181
+ console.error("❌ checkoutComplete request failed:", completeResponse.status);
182
+ return NextResponse.json(
183
+ {
184
+ success: false,
185
+ message: "Payment processed but order creation failed",
186
+ transactionId: transaction?.id,
187
+ },
188
+ { status: 202 }
189
+ );
190
+ }
191
+
192
+ const completeResult = await completeResponse.json();
193
+ const completeData = completeResult.data;
194
+ const completeErrors = completeResult.errors || completeData?.checkoutComplete?.errors;
195
+
196
+ if (completeErrors && completeErrors.length > 0) {
197
+ console.error("❌ checkoutComplete errors:", completeErrors);
198
+ return NextResponse.json(
199
+ {
200
+ success: false,
201
+ message: "Payment processed but order creation failed",
202
+ transactionId: transaction?.id,
203
+ details: completeErrors,
204
+ },
205
+ { status: 202 }
206
+ );
207
+ }
208
+
209
+ const completedOrder = completeData?.checkoutComplete?.order;
210
+
211
+ if (completedOrder) {
212
+ return NextResponse.json({
213
+ success: true,
214
+ order: {
215
+ id: completedOrder.id,
216
+ number: completedOrder.number,
217
+ total: completedOrder.total.gross.amount,
218
+ currency: completedOrder.total.gross.currency,
219
+ },
220
+ transactionId: transaction.id,
221
+ });
222
+ } else {
223
+ console.warn("⚠️ checkoutComplete did not create order");
224
+ return NextResponse.json(
225
+ {
226
+ success: false,
227
+ message: "Payment processed but order not created yet",
228
+ transactionId: transaction?.id,
229
+ },
230
+ { status: 202 }
231
+ );
232
+ }
233
+ }
234
+
235
+ } catch (error) {
236
+ console.error("❌ Error in process-payment API:", error);
237
+ return NextResponse.json(
238
+ {
239
+ error: error instanceof Error ? error.message : "Internal server error",
240
+ },
241
+ { status: 500 }
242
+ );
243
+ }
244
+ }
@@ -0,0 +1,45 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ export async function GET() {
4
+ try {
5
+ const response = NextResponse.json({
6
+ status: "success",
7
+ message: "Storefront connection test successful",
8
+ timestamp: new Date().toISOString(),
9
+ });
10
+
11
+ // Add CORS headers
12
+ response.headers.set('Access-Control-Allow-Origin', '*');
13
+ response.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
14
+ response.headers.set('Access-Control-Allow-Headers', 'Content-Type');
15
+
16
+ return response;
17
+ } catch (error) {
18
+ console.error("Test endpoint error:", error);
19
+ const errorResponse = NextResponse.json(
20
+ {
21
+ status: "error",
22
+ message: "Test endpoint failed"
23
+ },
24
+ { status: 500 }
25
+ );
26
+
27
+ // Add CORS headers to error response too
28
+ errorResponse.headers.set('Access-Control-Allow-Origin', '*');
29
+ errorResponse.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
30
+ errorResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type');
31
+
32
+ return errorResponse;
33
+ }
34
+ }
35
+
36
+ export async function OPTIONS() {
37
+ return new NextResponse(null, {
38
+ status: 200,
39
+ headers: {
40
+ 'Access-Control-Allow-Origin': '*',
41
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS',
42
+ 'Access-Control-Allow-Headers': 'Content-Type',
43
+ },
44
+ });
45
+ }
@@ -0,0 +1,16 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ export async function POST() {
4
+ const res = NextResponse.json({ success: true }, { headers: { 'Cache-Control': 'no-store' } });
5
+ const base = {
6
+ path: '/',
7
+ expires: new Date(0),
8
+ httpOnly: true,
9
+ sameSite: 'lax' as const,
10
+ secure: process.env.NODE_ENV === 'production',
11
+ };
12
+ res.cookies.set('token', '', base);
13
+ res.cookies.set('refreshToken', '', base);
14
+ res.cookies.set('isLoggedIn', '', { ...base, httpOnly: false });
15
+ return res;
16
+ }
@@ -0,0 +1,42 @@
1
+ import { NextResponse } from 'next/server';
2
+ import type { NextRequest } from 'next/server';
3
+
4
+ export async function GET(request: NextRequest) {
5
+ const { searchParams } = new URL(request.url);
6
+ const redirectTo = searchParams.get('redirect') || '/';
7
+ const reason = searchParams.get('reason');
8
+
9
+ // Create a response that will clear the HTTP-only cookies
10
+ const response = NextResponse.redirect(new URL(redirectTo, request.url));
11
+
12
+ // Clear the auth cookies by setting them to expire in the past
13
+ response.cookies.set({
14
+ name: 'token',
15
+ value: '',
16
+ path: '/',
17
+ expires: new Date(0), // Expire immediately
18
+ httpOnly: true,
19
+ sameSite: 'lax',
20
+ secure: process.env.NODE_ENV === 'production',
21
+ });
22
+
23
+ response.cookies.set({
24
+ name: 'refreshToken',
25
+ value: '',
26
+ path: '/',
27
+ expires: new Date(0), // Expire immediately
28
+ httpOnly: true,
29
+ sameSite: 'lax',
30
+ secure: process.env.NODE_ENV === 'production',
31
+ });
32
+
33
+ // Add debug headers in non-production
34
+ if (process.env.NODE_ENV !== 'production') {
35
+ response.headers.set('x-clear-cookies', '1');
36
+ if (reason) {
37
+ response.headers.set('x-clear-reason', reason);
38
+ }
39
+ }
40
+
41
+ return response;
42
+ }
@@ -0,0 +1,47 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ export async function POST(request: Request) {
4
+ try {
5
+ const { token, refreshToken, maxAgeSeconds = 60 * 60 * 24 * 7 } = await request.json();
6
+
7
+ if (!token) {
8
+ return NextResponse.json({ error: 'Missing token' }, { status: 400 });
9
+ }
10
+
11
+ const res = NextResponse.json({ ok: true });
12
+ const isProd = process.env.NODE_ENV === 'production';
13
+
14
+ // HttpOnly cookie for middleware
15
+ res.cookies.set('token', token, {
16
+ httpOnly: true, // 🔒 must be HttpOnly
17
+ secure: isProd,
18
+ sameSite: 'lax',
19
+ path: '/',
20
+ maxAge: maxAgeSeconds,
21
+ });
22
+
23
+ // Optional: refresh token
24
+ if (refreshToken) {
25
+ res.cookies.set('refreshToken', refreshToken, {
26
+ httpOnly: true, // 🔒 secure storage
27
+ secure: isProd,
28
+ sameSite: 'lax',
29
+ path: '/',
30
+ maxAge: maxAgeSeconds,
31
+ });
32
+ }
33
+
34
+ // Optional: frontend-friendly flag
35
+ res.cookies.set('isLoggedIn', '1', {
36
+ httpOnly: false,
37
+ secure: isProd,
38
+ sameSite: 'lax',
39
+ path: '/',
40
+ maxAge: maxAgeSeconds,
41
+ });
42
+
43
+ return res;
44
+ } catch {
45
+ return NextResponse.json({ error: 'Invalid request' }, { status: 400 });
46
+ }
47
+ }
@@ -0,0 +1,18 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ /**
4
+ * DEPRECATED: This API route is no longer used.
5
+ * Configuration is now handled server-side in the layout.tsx to prevent
6
+ * exposing configuration data to the client side.
7
+ *
8
+ * This endpoint is kept for backward compatibility but will return an error.
9
+ */
10
+ export async function GET() {
11
+ return NextResponse.json(
12
+ {
13
+ error: 'This endpoint is deprecated. Configuration is now handled server-side.',
14
+ message: 'Configuration data is no longer exposed to the client side for security reasons.'
15
+ },
16
+ { status: 410 } // 410 Gone - resource is no longer available
17
+ );
18
+ }
@@ -0,0 +1,24 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { fetchDynamicPageBySlug } from '@/graphql/queries/getDynamicPageBySlug';
3
+
4
+ export const dynamic = 'force-dynamic';
5
+
6
+ export async function GET(
7
+ request: NextRequest,
8
+ { params }: { params: Promise<{ slug: string }> }
9
+ ) {
10
+ try {
11
+ const { slug } = await params;
12
+
13
+ const pageData = await fetchDynamicPageBySlug(slug);
14
+
15
+ if (!pageData) {
16
+ return NextResponse.json({ error: 'Page not found' }, { status: 404 });
17
+ }
18
+
19
+ return NextResponse.json(pageData);
20
+ } catch (error) {
21
+ console.error('[API] Error fetching dynamic page:', error);
22
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
23
+ }
24
+ }
@@ -0,0 +1,237 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import nodemailer from "nodemailer";
3
+
4
+ export const runtime = "nodejs";
5
+
6
+ // Allowlist of permitted webhook domains for SSRF protection
7
+ // Add trusted webhook domains in env (e.g., Zapier, your CRM, etc.)
8
+ const ALLOWED_WEBHOOK_DOMAINS =
9
+ process.env.ALLOWED_WEBHOOK_DOMAINS?.split(",").map((d) => d.trim()) || [];
10
+
11
+ /**
12
+ * Validates a webhook URL against the allowlist to prevent SSRF attacks.
13
+ */
14
+ function isValidWebhookUrl(webhookUrl: string): boolean {
15
+ if (!webhookUrl || typeof webhookUrl !== "string") return false;
16
+
17
+ try {
18
+ const url = new URL(webhookUrl);
19
+
20
+ // Only allow HTTPS for security
21
+ if (url.protocol !== "https:") return false;
22
+
23
+ // Block private/internal hostnames
24
+ const hostname = url.hostname.toLowerCase();
25
+ const blockedPatterns = [
26
+ /^localhost$/i,
27
+ /^127\./,
28
+ /^10\./,
29
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
30
+ /^192\.168\./,
31
+ /^0\./,
32
+ /^169\.254\./,
33
+ /^\[::1\]$/,
34
+ /^\[fc/i,
35
+ /^\[fd/i,
36
+ /^\[fe80:/i,
37
+ ];
38
+ if (blockedPatterns.some((pattern) => pattern.test(hostname))) return false;
39
+
40
+ // Check against allowlist (if configured)
41
+ if (ALLOWED_WEBHOOK_DOMAINS.length > 0) {
42
+ const isAllowed = ALLOWED_WEBHOOK_DOMAINS.some(
43
+ (allowedDomain) =>
44
+ hostname === allowedDomain || hostname.endsWith(`.${allowedDomain}`)
45
+ );
46
+ if (!isAllowed) return false;
47
+ }
48
+
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ type SmtpConfig = {
56
+ host: string;
57
+ port: number;
58
+ secure: boolean;
59
+ user?: string;
60
+ pass?: string;
61
+ from: string;
62
+ to: string[];
63
+ replyTo?: string;
64
+ };
65
+
66
+ function getSmtpConfig(): SmtpConfig | null {
67
+ const host = process.env.SMTP_HOST?.trim();
68
+ const portRaw = process.env.SMTP_PORT?.trim();
69
+ const from = process.env.SMTP_FROM?.trim();
70
+ const toRaw = process.env.SMTP_TO?.trim();
71
+
72
+ if (!host || !portRaw || !from || !toRaw) return null;
73
+
74
+ const port = Number(portRaw);
75
+ if (!Number.isFinite(port) || port <= 0) return null;
76
+
77
+ const secure =
78
+ (process.env.SMTP_SECURE || "").trim().toLowerCase() === "true" || port === 465;
79
+
80
+ const user = process.env.SMTP_USER?.trim() || undefined;
81
+ const pass = process.env.SMTP_PASS?.trim() || undefined;
82
+ const replyTo = process.env.SMTP_REPLY_TO?.trim() || undefined;
83
+
84
+ const to = toRaw
85
+ .split(",")
86
+ .map((s) => s.trim())
87
+ .filter(Boolean);
88
+
89
+ if (!to.length) return null;
90
+
91
+ return { host, port, secure, user, pass, from, to, replyTo };
92
+ }
93
+
94
+ function subjectFor(formType: string, pageSlug?: string) {
95
+ const base = process.env.EMAIL_SUBJECT_PREFIX?.trim() || "";
96
+ const prefix = base ? `${base} ` : "";
97
+ const suffix = pageSlug ? ` (${pageSlug})` : "";
98
+ return `${prefix}New ${formType} submission${suffix}`;
99
+ }
100
+
101
+ function renderEmailText(params: {
102
+ formType: string;
103
+ pageSlug?: string;
104
+ data: unknown;
105
+ timestamp?: string;
106
+ ip?: string | null;
107
+ ua?: string | null;
108
+ }) {
109
+ const safeJson = JSON.stringify(params.data ?? {}, null, 2);
110
+ return [
111
+ `Form type: ${params.formType}`,
112
+ params.pageSlug ? `Page slug: ${params.pageSlug}` : null,
113
+ params.timestamp ? `Timestamp: ${params.timestamp}` : null,
114
+ params.ip ? `IP: ${params.ip}` : null,
115
+ params.ua ? `User-Agent: ${params.ua}` : null,
116
+ "",
117
+ "Data:",
118
+ safeJson,
119
+ "",
120
+ ]
121
+ .filter(Boolean)
122
+ .join("\n");
123
+ }
124
+
125
+ function escapeHtml(s: string) {
126
+ return s
127
+ .replaceAll("&", "&amp;")
128
+ .replaceAll("<", "&lt;")
129
+ .replaceAll(">", "&gt;")
130
+ .replaceAll('"', "&quot;")
131
+ .replaceAll("'", "&#39;");
132
+ }
133
+
134
+ function renderEmailHtml(text: string) {
135
+ return `<pre style="white-space:pre-wrap;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;">${escapeHtml(
136
+ text
137
+ )}</pre>`;
138
+ }
139
+
140
+ export async function POST(request: NextRequest) {
141
+ try {
142
+ const body = await request.json();
143
+ const { formType, pageSlug, data, metadata, timestamp } = body;
144
+
145
+ const results: {
146
+ smtp?: "sent" | "skipped" | "failed";
147
+ webhook?: "sent" | "skipped" | "failed";
148
+ } = {};
149
+
150
+ // Optional: Send email via SMTP (template-friendly; disabled unless SMTP_* vars provided)
151
+ const smtp = getSmtpConfig();
152
+ if (smtp) {
153
+ try {
154
+ const transporter = nodemailer.createTransport({
155
+ host: smtp.host,
156
+ port: smtp.port,
157
+ secure: smtp.secure,
158
+ auth: smtp.user && smtp.pass ? { user: smtp.user, pass: smtp.pass } : undefined,
159
+ });
160
+
161
+ const ip =
162
+ request.headers.get("x-forwarded-for") ||
163
+ request.headers.get("x-real-ip");
164
+ const ua = request.headers.get("user-agent");
165
+
166
+ const text = renderEmailText({
167
+ formType: String(formType || "form"),
168
+ pageSlug: typeof pageSlug === "string" ? pageSlug : undefined,
169
+ data,
170
+ timestamp: typeof timestamp === "string" ? timestamp : undefined,
171
+ ip,
172
+ ua,
173
+ });
174
+
175
+ await transporter.sendMail({
176
+ from: smtp.from,
177
+ to: smtp.to,
178
+ replyTo: smtp.replyTo,
179
+ subject: subjectFor(String(formType || "form"), typeof pageSlug === "string" ? pageSlug : undefined),
180
+ text,
181
+ html: renderEmailHtml(text),
182
+ });
183
+
184
+ results.smtp = "sent";
185
+ } catch (e) {
186
+ console.error("SMTP send failed:", e);
187
+ results.smtp = "failed";
188
+ }
189
+ } else {
190
+ results.smtp = "skipped";
191
+ }
192
+
193
+ // Optional: Send to webhook (with SSRF protection)
194
+ const webhookUrl = metadata?.webhookUrl;
195
+ if (typeof webhookUrl === "string" && webhookUrl.trim()) {
196
+ if (isValidWebhookUrl(webhookUrl)) {
197
+ try {
198
+ await fetch(webhookUrl, {
199
+ method: "POST",
200
+ headers: {
201
+ "Content-Type": "application/json",
202
+ },
203
+ body: JSON.stringify({
204
+ formType,
205
+ pageSlug,
206
+ data,
207
+ timestamp,
208
+ }),
209
+ });
210
+ results.webhook = "sent";
211
+ } catch (error) {
212
+ console.error("Webhook error:", error);
213
+ results.webhook = "failed";
214
+ }
215
+ } else {
216
+ results.webhook = "skipped";
217
+ }
218
+ }
219
+
220
+ return NextResponse.json({
221
+ success: true,
222
+ message: "Form submitted successfully",
223
+ results,
224
+ });
225
+
226
+ } catch (error) {
227
+ console.error("Form submission error:", error);
228
+
229
+ return NextResponse.json(
230
+ {
231
+ success: false,
232
+ message: "Failed to submit form",
233
+ },
234
+ { status: 500 }
235
+ );
236
+ }
237
+ }