diff --git a/src/components/test-ppc-leads/HeroSection.tsx b/src/components/test-ppc-leads/HeroSection.tsx new file mode 100644 index 000000000..256e761d2 --- /dev/null +++ b/src/components/test-ppc-leads/HeroSection.tsx @@ -0,0 +1,63 @@ +import axios from 'axios'; +import Image from 'next/image'; + +import { getImage } from '@/lib/getImage'; +import { HighlightText } from '@/components/components/HighlightText'; +import { HeroSection as HeroSectionText } from '@/assets/types/test-ppc-leads'; +import SignUpForm from '@/components/shared/SignUpForm'; + +interface HeroSectionProps { + textContent: HeroSectionText; + lang?: string; +} + +const LANDING_NAME = 'test-ppc-leads'; +const LEAD_REQUEST_TIMEOUT = 3000; + +export const HeroSection = ({ textContent, lang }: HeroSectionProps): JSX.Element => { + const registerLead = async (email: string) => { + await axios.post( + '/api/ppc-leads', + { email, locale: lang, landing: LANDING_NAME }, + { timeout: LEAD_REQUEST_TIMEOUT }, + ); + }; + + return ( +
+
+
+

+ +

+ +
+ +
+ +
+
+ +
+ Internxt Drive web app +
+
+ ); +}; + +export default HeroSection; diff --git a/src/pages/api/ppc-leads.ts b/src/pages/api/ppc-leads.ts new file mode 100644 index 000000000..02128afb2 --- /dev/null +++ b/src/pages/api/ppc-leads.ts @@ -0,0 +1,112 @@ +import axios, { AxiosError } from 'axios'; +import { NextApiRequest, NextApiResponse } from 'next'; + +import { getClientIp } from '@/utils/get-client-ip'; + +const KLAVIYO_PRIVATE_API_KEY = process.env.KLAVIYO_PRIVATE_API_KEY; +const KLAVIYO_PPC_LEADS_LIST_ID = process.env.KLAVIYO_PPC_LEADS_LIST_ID; +const KLAVIYO_API_URL = 'https://a.klaviyo.com/api'; +const KLAVIYO_API_REVISION = '2024-10-15'; + +const THROTTLE_TIME = 2 * 1000; +const MAX_EMAIL_LENGTH = 254; +const MAX_LANDING_LENGTH = 100; +const requestTimestamps = new Map(); + +interface KlaviyoErrorResponse { + errors?: { + meta?: { + duplicate_profile_id?: string; + }; + }[]; +} + +const klaviyoAxios = axios.create({ + baseURL: KLAVIYO_API_URL, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `Klaviyo-API-Key ${KLAVIYO_PRIVATE_API_KEY}`, + revision: KLAVIYO_API_REVISION, + }, +}); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') { + return res.status(405).json({ status: 'Error' }); + } + + if (!KLAVIYO_PRIVATE_API_KEY || !KLAVIYO_PPC_LEADS_LIST_ID) { + return res.status(500).json({ status: 'Error', message: 'Klaviyo is not configured' }); + } + + const ip = getClientIp(req); + const now = Date.now(); + const lastRequest = requestTimestamps.get(ip) || 0; + + if (now - lastRequest < THROTTLE_TIME) { + return res.status(429).json({ status: 'Error' }); + } + + requestTimestamps.set(ip, now); + + const { email, locale, landing } = req.body; + + if (!email?.includes('@') || email.length > MAX_EMAIL_LENGTH) { + return res.status(400).json({ status: 'Error', message: 'Invalid email' }); + } + + if (landing && landing.length > MAX_LANDING_LENGTH) { + return res.status(400).json({ status: 'Error', message: 'Field too long' }); + } + + try { + await createPpcLead({ email, locale, landing: landing || '' }); + res.status(200).json({ status: 'Success' }); + } catch (error) { + const axiosError = error as AxiosError; + res.status(500).json({ status: 'Error', message: axiosError.message }); + } +} + +const profileAttributes = (email: string, locale: string, landing: string) => ({ + email, + locale, + properties: { landing, origin_contact: 'PPC' }, +}); + +async function getOrCreateProfile(email: string, locale: string, landing: string): Promise { + try { + const response = await klaviyoAxios.post<{ data: { id: string } }>('/profiles/', { + data: { + type: 'profile', + attributes: profileAttributes(email, locale, landing), + }, + }); + return response.data.data.id; + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.response?.status === 409) { + const duplicateId = axiosError.response.data?.errors?.[0]?.meta?.duplicate_profile_id; + if (duplicateId) { + await klaviyoAxios.patch(`/profiles/${duplicateId}/`, { + data: { + type: 'profile', + id: duplicateId, + attributes: profileAttributes(email, locale, landing), + }, + }); + return duplicateId; + } + } + throw error; + } +} + +async function createPpcLead({ email, locale, landing }: { email: string; locale: string; landing: string }) { + const profileId = await getOrCreateProfile(email, locale, landing); + + await klaviyoAxios.post(`/lists/${KLAVIYO_PPC_LEADS_LIST_ID}/relationships/profiles/`, { + data: [{ type: 'profile', id: profileId }], + }); +} diff --git a/src/pages/test-ppc-leads.tsx b/src/pages/test-ppc-leads.tsx new file mode 100644 index 000000000..579e6f37d --- /dev/null +++ b/src/pages/test-ppc-leads.tsx @@ -0,0 +1,124 @@ +import { GetStaticPropsContext } from 'next'; + +import Layout from '@/components/layout/Layout'; +import Navbar from '@/components/layout/navbars/Navbar'; +import { MinimalFooter } from '@/components/layout/footers/MinimalFooter'; +import ReviewsSection from '@/components/home/ReviewsSection'; +import TrustedSection from '@/components/home/TrustedSection'; +import FAQSection from '@/components/shared/sections/FaqSection'; +import CardsGridSection from '@/components/shared/sections/CardsGridSection'; +import FloatingCtaSectionv2 from '@/components/shared/FloatingCtaSectionV2'; +import HeroSection from '@/components/test-ppc-leads/HeroSection'; +import ChoosePlanSection from '@/components/test-ppc-leads/ChoosePlanSection'; +import usePricing from '@/hooks/usePricing'; +import { getMinimumPrice } from '@/utils/priceHelper'; +import { PromoCodeName } from '@/lib/types'; +import { TestPpcLeadsText } from '@/assets/types/test-ppc-leads'; +import { FooterText, MetatagsDescription, NavigationBarText } from '@/assets/types/layout/types'; + +interface TestPpcLeadsProps { + metatagsDescriptions: MetatagsDescription[]; + navbarLang: NavigationBarText; + textContent: TestPpcLeadsText; + footerLang: FooterText; + lang: GetStaticPropsContext['locale']; +} + +const TestPpcLeads = ({ + metatagsDescriptions, + navbarLang, + textContent, + footerLang, + lang, +}: TestPpcLeadsProps): JSX.Element => { + const metatags = metatagsDescriptions.find((desc) => desc.id === 'test-ppc-leads'); + const locale = lang as string; + + const { + products, + loadingCards, + currency, + coupon: individualCoupon, + } = usePricing({ + couponCode: PromoCodeName.META85, + }); + + const decimalDiscount = individualCoupon?.percentOff && 100 - individualCoupon.percentOff; + const minimumPrice = getMinimumPrice(products, decimalDiscount); + + return ( + + + + + + + + + + + + + + + + +

+ {textContent.CtaSection.title} +

+

+ {textContent.CtaSection.description} +

+ + } + bgGradientContainerColor="linear-gradient(279.29deg, #0c4499 3.25%, #041733 78.35%)" + containerDetails="px-6 lg:flex-row lg:justify-between lg:px-10 lg:text-left" + bgPadding="py-10 lg:py-20" + /> + + +
+ ); +}; + +export function getStaticProps(ctx: GetStaticPropsContext) { + const lang = ctx.locale; + + const metatagsDescriptions = require(`@/assets/lang/${lang}/metatags-descriptions.json`); + const textContent = require(`@/assets/lang/${lang}/test-ppc-leads.json`); + const navbarLang = require(`@/assets/lang/${lang}/navbar.json`); + const footerLang = require(`@/assets/lang/${lang}/footer.json`); + + return { + props: { + lang, + metatagsDescriptions, + textContent, + navbarLang, + footerLang, + }, + }; +} + +export default TestPpcLeads;