feat(popup): add email capture popup with Mautic integration
Some checks failed
Build and Deploy / build (push) Has been cancelled
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:
@@ -15,6 +15,7 @@ import {
|
||||
trackRybbitUserLogin,
|
||||
trackRybbitUserRegister,
|
||||
trackRybbitNewsletterSignup,
|
||||
trackRybbitEvent,
|
||||
} from "@/lib/services/RybbitService";
|
||||
|
||||
export function useAnalytics() {
|
||||
@@ -178,6 +179,23 @@ export function useAnalytics() {
|
||||
trackRybbitNewsletterSignup(email, source);
|
||||
}, []);
|
||||
|
||||
// Popup tracking functions
|
||||
const trackPopupView = useCallback((data: { trigger: string; locale: string; country?: string }) => {
|
||||
trackRybbitEvent("popup_view", data);
|
||||
}, []);
|
||||
|
||||
const trackPopupSubmit = useCallback((data: { trigger: string; locale: string; country?: string }) => {
|
||||
trackRybbitEvent("popup_submit", data);
|
||||
}, []);
|
||||
|
||||
const trackPopupCtaClick = useCallback((data: { locale: string }) => {
|
||||
trackRybbitEvent("popup_cta_click", data);
|
||||
}, []);
|
||||
|
||||
const trackPopupDismiss = useCallback((data: { trigger: string; locale: string }) => {
|
||||
trackRybbitEvent("popup_dismiss", data);
|
||||
}, []);
|
||||
|
||||
// No-op placeholder for identifyUser (OpenPanel removed)
|
||||
const identifyUser = useCallback((_user: {
|
||||
profileId: string;
|
||||
@@ -203,6 +221,10 @@ export function useAnalytics() {
|
||||
trackUserLogin,
|
||||
trackUserRegister,
|
||||
trackNewsletterSignup,
|
||||
trackPopupView,
|
||||
trackPopupSubmit,
|
||||
trackPopupCtaClick,
|
||||
trackPopupDismiss,
|
||||
identifyUser,
|
||||
};
|
||||
}
|
||||
|
||||
19
src/lib/geoip.ts
Normal file
19
src/lib/geoip.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
interface GeoIPResponse {
|
||||
country: string;
|
||||
countryCode: string;
|
||||
}
|
||||
|
||||
export async function getCountryFromIP(): Promise<GeoIPResponse> {
|
||||
try {
|
||||
const response = await fetch("/api/geoip");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to get country");
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return {
|
||||
country: "Unknown",
|
||||
countryCode: "XX",
|
||||
};
|
||||
}
|
||||
}
|
||||
120
src/lib/mautic.ts
Normal file
120
src/lib/mautic.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
interface MauticToken {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
token_type: string;
|
||||
}
|
||||
|
||||
let cachedToken: MauticToken | null = null;
|
||||
let tokenExpiresAt: number = 0;
|
||||
|
||||
async function getMauticToken(): Promise<string> {
|
||||
if (cachedToken && Date.now() < tokenExpiresAt - 60000) {
|
||||
return cachedToken.access_token;
|
||||
}
|
||||
|
||||
const clientId = process.env.MAUTIC_CLIENT_ID;
|
||||
const clientSecret = process.env.MAUTIC_CLIENT_SECRET;
|
||||
const apiUrl = process.env.MAUTIC_API_URL || "https://mautic.nodecrew.me";
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
throw new Error("Mautic credentials not configured");
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiUrl}/oauth/v2/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("Mautic token error:", response.status, errorText);
|
||||
throw new Error(`Failed to get Mautic token: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const token: MauticToken = await response.json();
|
||||
cachedToken = token;
|
||||
tokenExpiresAt = Date.now() + token.expires_in * 1000;
|
||||
|
||||
return token.access_token;
|
||||
}
|
||||
|
||||
export async function createMauticContact(
|
||||
email: string,
|
||||
tags: string[],
|
||||
additionalData?: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
country?: string;
|
||||
city?: string;
|
||||
phone?: string;
|
||||
website?: string;
|
||||
preferredLocale?: string;
|
||||
ipAddress?: string;
|
||||
utmSource?: string;
|
||||
utmMedium?: string;
|
||||
utmCampaign?: string;
|
||||
utmContent?: string;
|
||||
pageUrl?: string;
|
||||
}
|
||||
): Promise<{ success: boolean; alreadyExists?: boolean; contactId?: number }> {
|
||||
try {
|
||||
const token = await getMauticToken();
|
||||
const apiUrl = process.env.MAUTIC_API_URL || "https://mautic.nodecrew.me";
|
||||
|
||||
const payload: any = {
|
||||
email,
|
||||
tags: tags.join(","),
|
||||
};
|
||||
|
||||
if (additionalData) {
|
||||
if (additionalData.firstName) payload.firstname = additionalData.firstName;
|
||||
if (additionalData.lastName) payload.lastname = additionalData.lastName;
|
||||
if (additionalData.country) payload.country = additionalData.country;
|
||||
if (additionalData.city) payload.city = additionalData.city;
|
||||
if (additionalData.phone) payload.phone = additionalData.phone;
|
||||
if (additionalData.preferredLocale) payload.preferred_locale = additionalData.preferredLocale;
|
||||
if (additionalData.utmSource) payload.utm_source = additionalData.utmSource;
|
||||
if (additionalData.utmMedium) payload.utm_medium = additionalData.utmMedium;
|
||||
if (additionalData.utmCampaign) payload.utm_campaign = additionalData.utmCampaign;
|
||||
if (additionalData.utmContent) payload.utm_content = additionalData.utmContent;
|
||||
if (additionalData.pageUrl) payload.page_url = additionalData.pageUrl;
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiUrl}/api/contacts/new`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (response.status === 409) {
|
||||
return { success: true, alreadyExists: true };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error("Mautic API error:", response.status, errorText);
|
||||
throw new Error(`Mautic API error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
console.log("Mautic API success:", responseData);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
contactId: responseData.contact?.id
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Mautic contact creation failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user