@alphasquad/saleor-template-advance 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +57 -0
- package/APPLE_PAY_QUICK_START.md +165 -0
- package/APPLE_PAY_SETUP.md +331 -0
- package/README.md +46 -0
- package/SEO_AUDIT_CHECKLIST_STATUS.md +244 -0
- package/SEO_AUDIT_REPORT.md +66 -0
- package/eslint.config.mjs +16 -0
- package/next-env.d.ts +5 -0
- package/next.config.ts +109 -0
- package/package.json +47 -0
- package/postcss.config.mjs +5 -0
- package/public/.well-known/apple-developer-merchantid-domain-association +1 -0
- package/public/Logo.png +0 -0
- package/public/brand-video.mp4 +0 -0
- package/public/favicon.ico +0 -0
- package/public/file.svg +1 -0
- package/public/footer/facebook.tsx +34 -0
- package/public/footer/instagram.tsx +27 -0
- package/public/footer/mail.tsx +5 -0
- package/public/footer/x.tsx +35 -0
- package/public/globe.svg +1 -0
- package/public/icons/Authorize.net.webp +0 -0
- package/public/icons/amex.gif +0 -0
- package/public/icons/appIcon.png +0 -0
- package/public/icons/discover.gif +0 -0
- package/public/icons/master.gif +0 -0
- package/public/icons/paypal.png +0 -0
- package/public/icons/stripe.png +0 -0
- package/public/icons/visa.gif +0 -0
- package/public/images/BackgroundNoise.png +0 -0
- package/public/images/footer-background.png +0 -0
- package/public/next.svg +1 -0
- package/public/no-image-avail-large.png +0 -0
- package/public/random-car-1.jpeg +0 -0
- package/public/random-car-2.png +0 -0
- package/public/random-car-3.jpg +0 -0
- package/public/random-car-4.jpg +0 -0
- package/public/random-car-5.jpg +0 -0
- package/public/star.svg +3 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/seo-audit/generate-checklist.mjs +156 -0
- package/src/app/(auth)/account/forgot-password/layout.tsx +16 -0
- package/src/app/(auth)/account/forgot-password/page.tsx +135 -0
- package/src/app/(auth)/account/login/layout.tsx +16 -0
- package/src/app/(auth)/account/login/page.tsx +288 -0
- package/src/app/(auth)/account/otp/layout.tsx +16 -0
- package/src/app/(auth)/account/otp/page.tsx +108 -0
- package/src/app/(auth)/account/register/layout.tsx +16 -0
- package/src/app/(auth)/account/register/page.tsx +431 -0
- package/src/app/(auth)/account/reset-password/layout.tsx +16 -0
- package/src/app/(auth)/account/reset-password/page.tsx +222 -0
- package/src/app/[slug]/page.tsx +43 -0
- package/src/app/about/loading.tsx +17 -0
- package/src/app/about/page.tsx +61 -0
- package/src/app/account/address/layout.tsx +15 -0
- package/src/app/account/address/page.tsx +166 -0
- package/src/app/account/head.tsx +4 -0
- package/src/app/account/layout.tsx +62 -0
- package/src/app/account/orders/[id]/layout.tsx +17 -0
- package/src/app/account/orders/[id]/page.tsx +115 -0
- package/src/app/account/orders/components/orderDetailsModal.tsx +410 -0
- package/src/app/account/orders/layout.tsx +15 -0
- package/src/app/account/orders/page.tsx +146 -0
- package/src/app/account/page.tsx +39 -0
- package/src/app/account/settings/components/editProfileSuccessModal.tsx +28 -0
- package/src/app/account/settings/layout.tsx +15 -0
- package/src/app/account/settings/page.tsx +260 -0
- package/src/app/api/affirm/check-status/route.ts +94 -0
- package/src/app/api/affirm/create-checkout/route.ts +109 -0
- package/src/app/api/affirm/get-config/route.ts +108 -0
- package/src/app/api/affirm/process-payment/route.ts +244 -0
- package/src/app/api/affirm/test-connection/route.ts +45 -0
- package/src/app/api/auth/clear/route.ts +16 -0
- package/src/app/api/auth/clear-cookies/route.ts +42 -0
- package/src/app/api/auth/set/route.ts +47 -0
- package/src/app/api/configuration/route.ts +18 -0
- package/src/app/api/dynamic-page/[slug]/route.ts +24 -0
- package/src/app/api/form-submission/route.ts +237 -0
- package/src/app/api/paypal/capture-order/route.ts +303 -0
- package/src/app/api/paypal/create-order/route.ts +211 -0
- package/src/app/api/paypal/get-config/route.ts +240 -0
- package/src/app/api/search-proxy/route.ts +52 -0
- package/src/app/authorize-net-success/layout.tsx +19 -0
- package/src/app/authorize-net-success/page.tsx +12 -0
- package/src/app/authorize-net-success/summary.tsx +486 -0
- package/src/app/blog/[slug]/blogContentRenderer.tsx +369 -0
- package/src/app/blog/[slug]/layout.tsx +17 -0
- package/src/app/blog/[slug]/page.tsx +151 -0
- package/src/app/blog/constant.tsx +147 -0
- package/src/app/blog/layout.tsx +31 -0
- package/src/app/blog/page.tsx +81 -0
- package/src/app/brand/[id]/BrandPageClient.tsx +188 -0
- package/src/app/brand/[id]/layout.tsx +17 -0
- package/src/app/brand/[id]/page.tsx +176 -0
- package/src/app/brands/components/brandsListingClient.tsx +97 -0
- package/src/app/brands/layout.tsx +31 -0
- package/src/app/brands/page.tsx +40 -0
- package/src/app/cancellation-policy/page.tsx +53 -0
- package/src/app/cart/layout.tsx +19 -0
- package/src/app/cart/page.tsx +752 -0
- package/src/app/category/[slug]/CategoryPageClient.tsx +377 -0
- package/src/app/category/[slug]/layout.tsx +17 -0
- package/src/app/category/[slug]/page.tsx +224 -0
- package/src/app/category/page.tsx +114 -0
- package/src/app/checkout/components/addNewAddressModal.tsx +474 -0
- package/src/app/checkout/layout.tsx +19 -0
- package/src/app/checkout/page.tsx +3312 -0
- package/src/app/components/account/AccountTabs.tsx +40 -0
- package/src/app/components/ads/GoogleAdSense.tsx +74 -0
- package/src/app/components/analytics/AnalyticsScripts.tsx +78 -0
- package/src/app/components/analytics/ConditionalGTMNoscript.tsx +24 -0
- package/src/app/components/analytics/ConditionalGoogleAnalytics.tsx +16 -0
- package/src/app/components/ancillary/AncillaryContent.tsx +7 -0
- package/src/app/components/auth/TokenExpirationHandler.tsx +8 -0
- package/src/app/components/blog/BlogList.tsx +112 -0
- package/src/app/components/checkout/AddressInformationSection.tsx +34 -0
- package/src/app/components/checkout/AddressManagement.tsx +571 -0
- package/src/app/components/checkout/CheckoutHeader.tsx +51 -0
- package/src/app/components/checkout/CheckoutQuestions.tsx +454 -0
- package/src/app/components/checkout/CheckoutTermsModal.tsx +81 -0
- package/src/app/components/checkout/ContactDetailsSection.tsx +52 -0
- package/src/app/components/checkout/DealerShippingSection.tsx +359 -0
- package/src/app/components/checkout/DeliveryMethodSection.tsx +249 -0
- package/src/app/components/checkout/OrderSummary.tsx +386 -0
- package/src/app/components/checkout/TermsContentRenderer.tsx +147 -0
- package/src/app/components/checkout/WillCallSection.tsx +133 -0
- package/src/app/components/checkout/affirmPayment.tsx +383 -0
- package/src/app/components/checkout/checkoutProcessingModal.tsx +96 -0
- package/src/app/components/checkout/googlePayButton.tsx +334 -0
- package/src/app/components/checkout/paymentStep.tsx +180 -0
- package/src/app/components/checkout/paypalPayment.tsx +1083 -0
- package/src/app/components/checkout/saleorNativePayment.tsx +1758 -0
- package/src/app/components/dynamicPage/DynamicPageRenderer.tsx +13 -0
- package/src/app/components/dynamicPage/HtmlWidgetRenderer.tsx +144 -0
- package/src/app/components/filtersCollapsible/index.tsx +365 -0
- package/src/app/components/globalSearch/index.tsx +423 -0
- package/src/app/components/layout/cartDropDown.tsx +628 -0
- package/src/app/components/layout/components/FooterNewsletter.tsx +21 -0
- package/src/app/components/layout/footer.tsx +283 -0
- package/src/app/components/layout/header/accountMenuDropdown.tsx +53 -0
- package/src/app/components/layout/header/components/CartBadge.tsx +18 -0
- package/src/app/components/layout/header/components/LoadingState.tsx +17 -0
- package/src/app/components/layout/header/components/MenuItemDropdown.tsx +124 -0
- package/src/app/components/layout/header/components/MobileNavbar.tsx +123 -0
- package/src/app/components/layout/header/components/NavbarActions.tsx +125 -0
- package/src/app/components/layout/header/components/NavbarBrand.tsx +29 -0
- package/src/app/components/layout/header/components/NavigationLinks.tsx +131 -0
- package/src/app/components/layout/header/hamMenuSlide.tsx +318 -0
- package/src/app/components/layout/header/header.tsx +44 -0
- package/src/app/components/layout/header/hooks/useDropdown.ts +45 -0
- package/src/app/components/layout/header/hooks/useNavbarData.ts +138 -0
- package/src/app/components/layout/header/hooks/useNavbarState.ts +66 -0
- package/src/app/components/layout/header/megaMenuDropdown.tsx +116 -0
- package/src/app/components/layout/header/navBar.tsx +121 -0
- package/src/app/components/layout/header/search.tsx +418 -0
- package/src/app/components/layout/header/styles/navbarStyles.ts +27 -0
- package/src/app/components/layout/header/topBar.tsx +214 -0
- package/src/app/components/layout/joinNewsletterForm/index.tsx +72 -0
- package/src/app/components/layout/mobileAccordian/index.tsx +92 -0
- package/src/app/components/layout/paymentMethods.tsx +75 -0
- package/src/app/components/layout/rootLayout.tsx +23 -0
- package/src/app/components/layout/siteInfo.tsx +103 -0
- package/src/app/components/layout/socialLinks.tsx +65 -0
- package/src/app/components/newsletterSection/emailListSection.tsx +224 -0
- package/src/app/components/newsletterSection/emailSectionServer.tsx +8 -0
- package/src/app/components/providers/ApolloWrapper.tsx +12 -0
- package/src/app/components/providers/AppConfigurationProvider.tsx +108 -0
- package/src/app/components/providers/GoogleAnalyticsProvider.tsx +149 -0
- package/src/app/components/providers/GoogleTagManagerProvider.tsx +31 -0
- package/src/app/components/providers/RecaptchaProvider.tsx +18 -0
- package/src/app/components/providers/ServerAppConfigurationProvider.tsx +133 -0
- package/src/app/components/providers/YMMStatusProvider.tsx +15 -0
- package/src/app/components/reuseableUI/AboutUs.tsx +115 -0
- package/src/app/components/reuseableUI/AddToCartClient.tsx +125 -0
- package/src/app/components/reuseableUI/EditorJsRenderer.tsx +219 -0
- package/src/app/components/reuseableUI/HeroSectionsearchByVehicle.tsx +188 -0
- package/src/app/components/reuseableUI/ImageWithFallback.tsx +41 -0
- package/src/app/components/reuseableUI/Toast.tsx +101 -0
- package/src/app/components/reuseableUI/blogCard.tsx +52 -0
- package/src/app/components/reuseableUI/brandCard.tsx +68 -0
- package/src/app/components/reuseableUI/breadcrumb.tsx +38 -0
- package/src/app/components/reuseableUI/categoryCard.tsx +37 -0
- package/src/app/components/reuseableUI/categorySkeleton.tsx +31 -0
- package/src/app/components/reuseableUI/commonButton.tsx +48 -0
- package/src/app/components/reuseableUI/defaultInputField/index.tsx +84 -0
- package/src/app/components/reuseableUI/emptyState.tsx +29 -0
- package/src/app/components/reuseableUI/errorTag.tsx +15 -0
- package/src/app/components/reuseableUI/heading/index.tsx +20 -0
- package/src/app/components/reuseableUI/input.tsx +117 -0
- package/src/app/components/reuseableUI/listCard.tsx +137 -0
- package/src/app/components/reuseableUI/loadingUI.tsx +12 -0
- package/src/app/components/reuseableUI/modalLayout.tsx +76 -0
- package/src/app/components/reuseableUI/newsletter/newsletterClient.tsx +622 -0
- package/src/app/components/reuseableUI/newsletter/newslettersHomeModal.tsx +68 -0
- package/src/app/components/reuseableUI/offerCard.tsx +42 -0
- package/src/app/components/reuseableUI/passwordRules/passwordRules.tsx +56 -0
- package/src/app/components/reuseableUI/primaryButton/index.tsx +34 -0
- package/src/app/components/reuseableUI/productCard.tsx +118 -0
- package/src/app/components/reuseableUI/productSkeleton.tsx +34 -0
- package/src/app/components/reuseableUI/searchByVehicle.tsx +187 -0
- package/src/app/components/reuseableUI/secondaryButton/index.tsx +34 -0
- package/src/app/components/reuseableUI/section.tsx +20 -0
- package/src/app/components/reuseableUI/select/index.tsx +98 -0
- package/src/app/components/reuseableUI/skeletonLoader.tsx +117 -0
- package/src/app/components/reuseableUI/statusTag.tsx +24 -0
- package/src/app/components/reuseableUI/tags/saleTag.tsx +19 -0
- package/src/app/components/reuseableUI/testimonialCard.tsx +93 -0
- package/src/app/components/richText/EditorRenderer.tsx +318 -0
- package/src/app/components/search/HierarchicalCategoryFilter.tsx +155 -0
- package/src/app/components/search/SearchFilters.tsx +155 -0
- package/src/app/components/search/YMMSearchSidebar.tsx +187 -0
- package/src/app/components/seo/ServerProductCard.tsx +91 -0
- package/src/app/components/seo/ServerProductGrid.tsx +45 -0
- package/src/app/components/shop/CategoryFilter.tsx +184 -0
- package/src/app/components/shop/ItemsPerPageSelect.tsx +69 -0
- package/src/app/components/shop/ItemsPerPageSelectClient.tsx +58 -0
- package/src/app/components/shop/MobileFilters.tsx +103 -0
- package/src/app/components/shop/ProductGridSkeleton.tsx +16 -0
- package/src/app/components/shop/ProductsGrid.tsx +230 -0
- package/src/app/components/shop/SearchFilter.tsx +218 -0
- package/src/app/components/shop/SearchFilterClient.tsx +122 -0
- package/src/app/components/shop/SearchLoadingOverlay.tsx +32 -0
- package/src/app/components/shop/ShopMobileFilters.tsx +205 -0
- package/src/app/components/showroom/VehicleSearchDropdowns.tsx +187 -0
- package/src/app/components/showroom/brandsSwiper.tsx +49 -0
- package/src/app/components/showroom/brandsSwiperClient copy.tsx +93 -0
- package/src/app/components/showroom/brandsSwiperClient.tsx +122 -0
- package/src/app/components/showroom/brandsSwiperServer.tsx +42 -0
- package/src/app/components/showroom/bundleProducts.tsx +120 -0
- package/src/app/components/showroom/categoryGrid.tsx +51 -0
- package/src/app/components/showroom/categoryGridServer.tsx +45 -0
- package/src/app/components/showroom/categorySwiper.tsx +115 -0
- package/src/app/components/showroom/featureStrip.tsx +139 -0
- package/src/app/components/showroom/offersSwiper.tsx +181 -0
- package/src/app/components/showroom/productGrid.tsx +56 -0
- package/src/app/components/showroom/productSwiper.tsx +119 -0
- package/src/app/components/showroom/promotion-slider.tsx +138 -0
- package/src/app/components/showroom/promotion.tsx +207 -0
- package/src/app/components/showroom/promotionsSwiper.tsx +174 -0
- package/src/app/components/showroom/showroomHeroCarousel.tsx +141 -0
- package/src/app/components/showroom/testimonialsGrid.tsx +106 -0
- package/src/app/components/skeletons/ContentSkeleton.tsx +14 -0
- package/src/app/components/sortDropdown/index.tsx +116 -0
- package/src/app/components/tertiaryButton/index.tsx +25 -0
- package/src/app/components/theme/theme-provider.tsx +82 -0
- package/src/app/contact/layout.tsx +32 -0
- package/src/app/contact/page.tsx +591 -0
- package/src/app/content/[slug]/layout.tsx +17 -0
- package/src/app/content/[slug]/page.tsx +159 -0
- package/src/app/content/layout.tsx +31 -0
- package/src/app/content/page.tsx +88 -0
- package/src/app/core-policies/page.tsx +55 -0
- package/src/app/discounts/page.tsx +54 -0
- package/src/app/frequently-asked-questions/page.tsx +57 -0
- package/src/app/globals.css +440 -0
- package/src/app/hooks/useDealerLocations.ts +259 -0
- package/src/app/hooks/useGTMEngagement.ts +71 -0
- package/src/app/hooks/useGoogleAnalytics.ts +145 -0
- package/src/app/layout.tsx +149 -0
- package/src/app/not-found.tsx +31 -0
- package/src/app/order-confirmation/layout.tsx +19 -0
- package/src/app/order-confirmation/page.tsx +12 -0
- package/src/app/order-confirmation/summary.tsx +1775 -0
- package/src/app/page.tsx +194 -0
- package/src/app/privacy-policy/loading.tsx +17 -0
- package/src/app/privacy-policy/page.tsx +56 -0
- package/src/app/product/[id]/ProductDetailClient.tsx +2448 -0
- package/src/app/product/[id]/components/itemInquiryModal.tsx +461 -0
- package/src/app/product/[id]/layout.tsx +116 -0
- package/src/app/product/[id]/page.tsx +200 -0
- package/src/app/product/layout.tsx +15 -0
- package/src/app/products/all/AllProductsClient.tsx +743 -0
- package/src/app/products/all/page.tsx +176 -0
- package/src/app/products/components/shopEmptyState.tsx +29 -0
- package/src/app/request-return/layout.tsx +36 -0
- package/src/app/request-return/page.tsx +597 -0
- package/src/app/robots.txt/route.ts +27 -0
- package/src/app/search/layout.tsx +16 -0
- package/src/app/search/page.tsx +736 -0
- package/src/app/shipping-returns/page.tsx +60 -0
- package/src/app/site-map/layout.tsx +33 -0
- package/src/app/site-map/page.tsx +113 -0
- package/src/app/sitemap-index.xml/route.ts +20 -0
- package/src/app/sitemap.ts +10 -0
- package/src/app/terms-and-conditions/loading.tsx +17 -0
- package/src/app/terms-and-conditions/page.tsx +56 -0
- package/src/app/utils/appConfiguration.ts +327 -0
- package/src/app/utils/branding.ts +52 -0
- package/src/app/utils/configurationService.ts +202 -0
- package/src/app/utils/constant.tsx +242 -0
- package/src/app/utils/editorJsUtils.tsx +249 -0
- package/src/app/utils/functions.ts +146 -0
- package/src/app/utils/googleAnalytics.ts +168 -0
- package/src/app/utils/googleTagManager.ts +475 -0
- package/src/app/utils/ipDetection.ts +270 -0
- package/src/app/utils/serverConfigurationService.ts +209 -0
- package/src/app/utils/svgs/GridIcon.tsx +45 -0
- package/src/app/utils/svgs/account/myAccount/listDotIcon.tsx +3 -0
- package/src/app/utils/svgs/account/myAccount/tickIcon.tsx +10 -0
- package/src/app/utils/svgs/account/orderHistory/InfoIcon.tsx +49 -0
- package/src/app/utils/svgs/arrowDownIcon.tsx +17 -0
- package/src/app/utils/svgs/arrowIcon.tsx +25 -0
- package/src/app/utils/svgs/arrowUpIcon.tsx +16 -0
- package/src/app/utils/svgs/brandsSearchIcon.tsx +25 -0
- package/src/app/utils/svgs/cart/cartIcon.tsx +31 -0
- package/src/app/utils/svgs/cart/plusIcon.tsx +13 -0
- package/src/app/utils/svgs/cart/subtractIcon.tsx +13 -0
- package/src/app/utils/svgs/cart/successTickIcon.tsx +14 -0
- package/src/app/utils/svgs/chevronDownIcon.tsx +21 -0
- package/src/app/utils/svgs/closeEyeIcon.tsx +47 -0
- package/src/app/utils/svgs/crossIcon.tsx +25 -0
- package/src/app/utils/svgs/eyeIcon.tsx +29 -0
- package/src/app/utils/svgs/featureTag.tsx +20 -0
- package/src/app/utils/svgs/filterIcon.tsx +3 -0
- package/src/app/utils/svgs/globleIcon.tsx +41 -0
- package/src/app/utils/svgs/infoIcon.tsx +34 -0
- package/src/app/utils/svgs/listIcon.tsx +50 -0
- package/src/app/utils/svgs/logOutIcon.tsx +35 -0
- package/src/app/utils/svgs/menuIcon.tsx +8 -0
- package/src/app/utils/svgs/minusIcon.tsx +18 -0
- package/src/app/utils/svgs/newsletterIcon.tsx +19 -0
- package/src/app/utils/svgs/noDataFoundIcon-.tsx +26 -0
- package/src/app/utils/svgs/noProductFoundIcon.tsx +43 -0
- package/src/app/utils/svgs/passwordIcons/errorIcon.tsx +31 -0
- package/src/app/utils/svgs/passwordIcons/successIcon.tsx +24 -0
- package/src/app/utils/svgs/paymentProcessingIcons/hourglassIcon.tsx +43 -0
- package/src/app/utils/svgs/paymentProcessingIcons/modalCrossIcon.tsx +23 -0
- package/src/app/utils/svgs/paymentProcessingIcons/paymentFailedIcon.tsx +47 -0
- package/src/app/utils/svgs/pencilIcon.tsx +11 -0
- package/src/app/utils/svgs/plusIcon.tsx +25 -0
- package/src/app/utils/svgs/productInquiryIcon.tsx +40 -0
- package/src/app/utils/svgs/searchIcon.tsx +31 -0
- package/src/app/utils/svgs/shoppingCart.tsx +32 -0
- package/src/app/utils/svgs/spinnerIcon.tsx +22 -0
- package/src/app/utils/svgs/spinnerLoadingIcon.tsx +26 -0
- package/src/app/utils/svgs/successTickIcon.tsx +40 -0
- package/src/app/utils/svgs/swiperArrowIconLeft.tsx +18 -0
- package/src/app/utils/svgs/swiperArrowIconRight.tsx +18 -0
- package/src/app/utils/svgs/userProfileIcon.tsx +31 -0
- package/src/app/utils/svgs/warningCircleIcon.tsx +15 -0
- package/src/app/warranty/constant.tsx +63 -0
- package/src/app/warranty/loading.tsx +17 -0
- package/src/app/warranty/page.tsx +56 -0
- package/src/graphql/client.ts +288 -0
- package/src/graphql/mutations/accountAddressCreate.ts +56 -0
- package/src/graphql/mutations/accountAddressDelete.ts +23 -0
- package/src/graphql/mutations/accountAddressUpdate.ts +55 -0
- package/src/graphql/mutations/accountSetDefaultAddress.ts +32 -0
- package/src/graphql/mutations/accountUpdate.ts +34 -0
- package/src/graphql/mutations/changePassword.ts +25 -0
- package/src/graphql/mutations/checkout.ts +117 -0
- package/src/graphql/mutations/checkoutAddVoucher.ts +63 -0
- package/src/graphql/mutations/checkoutComplete.ts +79 -0
- package/src/graphql/mutations/checkoutCreate.ts +131 -0
- package/src/graphql/mutations/checkoutCustomerAttach.ts +50 -0
- package/src/graphql/mutations/checkoutEmailUpdate.ts +15 -0
- package/src/graphql/mutations/checkoutLineMetadataUpdate.ts +52 -0
- package/src/graphql/mutations/checkoutPaymentCreate.ts +82 -0
- package/src/graphql/mutations/paymentGatewayInitialize.ts +58 -0
- package/src/graphql/mutations/registerAccount.ts +65 -0
- package/src/graphql/mutations/requestPasswordReset.ts +32 -0
- package/src/graphql/mutations/setPassword.ts +49 -0
- package/src/graphql/mutations/signIn.ts +50 -0
- package/src/graphql/mutations/tokenRefresh.ts +19 -0
- package/src/graphql/mutations/updateCheckoutMetadata.ts +49 -0
- package/src/graphql/mutations/updateProfile.ts +18 -0
- package/src/graphql/mutations/willCallDeliveryMethod.ts +81 -0
- package/src/graphql/queries/checkout.ts +168 -0
- package/src/graphql/queries/findProductByOldSlug.ts +58 -0
- package/src/graphql/queries/getAboutPage.ts +24 -0
- package/src/graphql/queries/getAboutPageId.ts +9 -0
- package/src/graphql/queries/getAboutUs.ts +38 -0
- package/src/graphql/queries/getAddressInformation.ts +38 -0
- package/src/graphql/queries/getAllCategories.ts +41 -0
- package/src/graphql/queries/getAllCategoriesTree.ts +67 -0
- package/src/graphql/queries/getAllCategoriesWithProducts.ts +29 -0
- package/src/graphql/queries/getAllCollectionsWithProducts.ts +16 -0
- package/src/graphql/queries/getBlogs.ts +222 -0
- package/src/graphql/queries/getBrands.ts +17 -0
- package/src/graphql/queries/getBundles.ts +43 -0
- package/src/graphql/queries/getCategories.ts +20 -0
- package/src/graphql/queries/getChannels.ts +77 -0
- package/src/graphql/queries/getCheckoutQuestions.ts +115 -0
- package/src/graphql/queries/getCheckoutTermsAndConditions.ts +37 -0
- package/src/graphql/queries/getContactPage.ts +117 -0
- package/src/graphql/queries/getContentPage.ts +191 -0
- package/src/graphql/queries/getDiscountOffers.ts +18 -0
- package/src/graphql/queries/getDynamicPageBySlug.ts +251 -0
- package/src/graphql/queries/getFeaturedProducts.ts +48 -0
- package/src/graphql/queries/getHeroMetadata.ts +23 -0
- package/src/graphql/queries/getMenuBySlug.ts +84 -0
- package/src/graphql/queries/getMyProfile.ts +23 -0
- package/src/graphql/queries/getNewsletter.ts +122 -0
- package/src/graphql/queries/getNewsletterPage.ts +111 -0
- package/src/graphql/queries/getPageBySlug.ts +52 -0
- package/src/graphql/queries/getPageTypeId.ts +27 -0
- package/src/graphql/queries/getPaymentMethods.ts +61 -0
- package/src/graphql/queries/getProducts.ts +78 -0
- package/src/graphql/queries/getPromotions.ts +24 -0
- package/src/graphql/queries/getRequestReturnPage.ts +121 -0
- package/src/graphql/queries/getSiteInfo.ts +54 -0
- package/src/graphql/queries/getSocialLinks.ts +52 -0
- package/src/graphql/queries/getTestimonials.ts +25 -0
- package/src/graphql/queries/getUserWithCheckout.ts +27 -0
- package/src/graphql/queries/getVehicleMakes.ts +21 -0
- package/src/graphql/queries/getVehicleModels.ts +21 -0
- package/src/graphql/queries/getVehicleYears.ts +21 -0
- package/src/graphql/queries/meAddresses.ts +56 -0
- package/src/graphql/queries/myOrders.ts +37 -0
- package/src/graphql/queries/orderDetail.ts +231 -0
- package/src/graphql/queries/productDetailsById.ts +197 -0
- package/src/graphql/queries/productInquiry.ts +115 -0
- package/src/graphql/queries/productsByCategoriesAndCollections.ts +39 -0
- package/src/graphql/queries/willCallCollectionPoints.ts +55 -0
- package/src/graphql/server-client.ts +54 -0
- package/src/graphql/types/categories.ts +9 -0
- package/src/graphql/types/checkout.ts +168 -0
- package/src/graphql/types/offer.ts +12 -0
- package/src/graphql/types/product.ts +44 -0
- package/src/hooks/scrollPageTop.ts +9 -0
- package/src/hooks/serverNavbarData.ts +79 -0
- package/src/hooks/useCartSync.ts +24 -0
- package/src/hooks/useRecaptcha.ts +33 -0
- package/src/hooks/useTokenExpiration.ts +81 -0
- package/src/hooks/useVehicleData.ts +346 -0
- package/src/lib/api/kount.ts +165 -0
- package/src/lib/api/shop.ts +1445 -0
- package/src/lib/saleor/getSaleorApiUrl.ts +25 -0
- package/src/lib/schema.ts +303 -0
- package/src/lib/seo/extractTextFromEditorJs.ts +58 -0
- package/src/lib/seo/site.ts +10 -0
- package/src/lib/urls/normalizeInternalUrl.ts +53 -0
- package/src/middleware.ts +134 -0
- package/src/sitemaps/README.md +105 -0
- package/src/sitemaps/dynamic-pages-sitemap.ts +247 -0
- package/src/sitemaps/sitemap-index.ts +21 -0
- package/src/sitemaps/static-pages-sitemap.ts +36 -0
- package/src/store/useGlobalStore.tsx +1656 -0
- package/src/types/global.d.ts +148 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,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("&", "&")
|
|
128
|
+
.replaceAll("<", "<")
|
|
129
|
+
.replaceAll(">", ">")
|
|
130
|
+
.replaceAll('"', """)
|
|
131
|
+
.replaceAll("'", "'");
|
|
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
|
+
}
|