feat(popup): add email capture popup with Mautic integration
Some checks failed
Build and Deploy / build (push) Has been cancelled

- Email capture popup with scroll (10%) and exit intent triggers
- First name field and full tracking (UTM, device, time on page)
- Mautic API integration for contact creation
- GeoIP detection for country/region
- 4 locale support (sr, en, de, fr)
- Mautic tracking script in layout
This commit is contained in:
Unchained
2026-04-03 20:44:15 +02:00
parent 4e5481af1a
commit eb711fbf1a
16 changed files with 1019 additions and 1 deletions

View File

@@ -0,0 +1,101 @@
import { NextRequest, NextResponse } from "next/server";
import { createMauticContact } from "@/lib/mautic";
const requestCache = new Map<string, number>();
const DEBOUNCE_MS = 5000;
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const {
email,
locale,
country,
countryCode,
source,
trigger,
firstName,
lastName,
timeOnPage,
referrer,
pageUrl,
pageLanguage,
preferredLocale,
deviceName,
deviceOS,
userAgent,
utmSource,
utmMedium,
utmCampaign,
utmContent,
fbclid,
} = body;
if (!email || !email.includes("@")) {
return NextResponse.json(
{ error: "Invalid email" },
{ status: 400 }
);
}
const cacheKey = `${email}:${Date.now()}`;
const lastRequest = requestCache.get(cacheKey);
if (lastRequest && Date.now() - lastRequest < DEBOUNCE_MS) {
return NextResponse.json(
{ error: "Please wait before submitting again" },
{ status: 429 }
);
}
requestCache.set(cacheKey, Date.now());
const tags = [
"source:popup",
`locale:${locale || "en"}`,
`country:${countryCode || "XX"}`,
`popup_${trigger || "unknown"}`,
"lead:warm",
...(utmSource ? [`utm:${utmSource}`] : []),
...(deviceName ? [`device:${deviceName}`] : []),
];
const forwardedFor = request.headers.get("x-forwarded-for");
const realIP = request.headers.get("x-real-ip");
const ipAddress = forwardedFor?.split(",")[0]?.trim() || realIP || "unknown";
const result = await createMauticContact(email, tags, {
firstName: firstName || "",
lastName: lastName || "",
country: country || "",
preferredLocale: preferredLocale || locale || "en",
ipAddress,
utmSource: utmSource || "",
utmMedium: utmMedium || "",
utmCampaign: utmCampaign || "",
utmContent: utmContent || "",
pageUrl: pageUrl || request.headers.get("referer") || "",
});
console.log("Email capture success:", {
email,
firstName,
timeOnPage,
deviceName,
deviceOS,
utmSource,
utmMedium,
result
});
return NextResponse.json({
success: true,
alreadySubscribed: result.alreadyExists,
contactId: result.contactId,
});
} catch (error) {
console.error("Email capture error:", error);
return NextResponse.json(
{ error: "Failed to process subscription", details: error instanceof Error ? error.message : "Unknown error" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,67 @@
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
try {
// Check for Cloudflare's IP header first (production)
const cfConnectingIp = request.headers.get("cf-connecting-ip");
const forwardedFor = request.headers.get("x-forwarded-for");
const realIP = request.headers.get("x-real-ip");
// Use Cloudflare IP first, then fall back to other headers
let ip = cfConnectingIp || forwardedFor?.split(",")[0]?.trim() || realIP || "127.0.0.1";
// For local development, return XX as country code (Mautic accepts this)
if (ip === "127.0.0.1" || ip === "::1" || ip.startsWith("192.168.") || ip.startsWith("10.")) {
console.log("[GeoIP] Local/private IP detected:", ip);
return NextResponse.json({
country: "Unknown",
countryCode: "XX",
region: "",
city: "",
timezone: "",
});
}
const response = await fetch(`http://ip-api.com/json/${ip}?fields=status,message,country,countryCode,region,regionName,city,timezone`, {
headers: {
"Accept": "application/json",
},
});
if (!response.ok) {
throw new Error("GeoIP lookup failed");
}
const data = await response.json();
if (data.status !== "success") {
console.error("[GeoIP] API error:", data.message, "for IP:", ip);
return NextResponse.json({
country: "Unknown",
countryCode: "XX",
region: "",
city: "",
timezone: "",
});
}
console.log("[GeoIP] Success:", data.country, "(" + data.countryCode + ")");
return NextResponse.json({
country: data.country,
countryCode: data.countryCode,
region: data.regionName,
city: data.city,
timezone: data.timezone,
});
} catch (error) {
console.error("[GeoIP] Error:", error);
return NextResponse.json({
country: "Unknown",
countryCode: "XX",
region: "",
city: "",
timezone: "",
});
}
}