jetclic-core 1.0.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/README.md +17 -0
- package/jetclic-core.js +303 -0
- package/package.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# JetClic Core 馃殌
|
|
2
|
+
Sistema centralizado de eventos y prospecci贸n para landing pages.
|
|
3
|
+
|
|
4
|
+
## Caracter铆sticas
|
|
5
|
+
- **Captura de UTMs**: Almacenamiento autom谩tico en `sessionStorage`.
|
|
6
|
+
- **GTM Ready**: Dispara eventos `jc_interaction` y `webform_contact`.
|
|
7
|
+
- **Validaci贸n de Formularios**: Manejo de 茅xito/error con atributos `data-feedback`.
|
|
8
|
+
- **Carga Inteligente**: reCAPTCHA Enterprise solo se carga al interactuar con el formulario.
|
|
9
|
+
|
|
10
|
+
## Uso
|
|
11
|
+
Inyecta tus IDs en el `window` antes de cargar el script:
|
|
12
|
+
```html
|
|
13
|
+
<script is:inline>
|
|
14
|
+
window.tagmanagerId = 'GTM-XXXX';
|
|
15
|
+
window.recaptchaProjectId = '6Lc...';
|
|
16
|
+
</script>
|
|
17
|
+
<script src="[https://cdn.jsdelivr.net/npm/jetclic-core@1/jetclic-core.js](https://cdn.jsdelivr.net/npm/jetclic-core@1/jetclic-core.js)" defer></script>
|
package/jetclic-core.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JetClic Core JS
|
|
3
|
+
* Unified logic for Forms, Analytics, and UI Interactions
|
|
4
|
+
* Version: 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
(function () {
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
// Configuration
|
|
11
|
+
const CONFIG = {
|
|
12
|
+
gtmBase: "https://www.googletagmanager.com/gtm.js",
|
|
13
|
+
whatsappBase: "https://api.whatsapp.com/send",
|
|
14
|
+
grecaptchaAction: "submit",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// State
|
|
18
|
+
const state = {
|
|
19
|
+
tagmanagerId: window.tagmanagerId || null,
|
|
20
|
+
recaptchaProjectId: window.recaptchaProjectId || null,
|
|
21
|
+
whatsappMessage: window.whatsappMessage || "Hola, 驴Me ayudar铆a con una cotizaci贸n?",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
//----------------------------------------------
|
|
25
|
+
// Information Collection & GTM Init
|
|
26
|
+
//----------------------------------------------
|
|
27
|
+
function init() {
|
|
28
|
+
captureUTMs();
|
|
29
|
+
initGTM();
|
|
30
|
+
initRecaptcha();
|
|
31
|
+
attachGlobalListeners();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function captureUTMs() {
|
|
35
|
+
const params = new URLSearchParams(window.location.search);
|
|
36
|
+
const keys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
|
|
37
|
+
|
|
38
|
+
keys.forEach(key => {
|
|
39
|
+
if (params.has(key)) {
|
|
40
|
+
sessionStorage.setItem(key, params.get(key));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function initRecaptcha() {
|
|
46
|
+
if (!state.recaptchaProjectId) return;
|
|
47
|
+
|
|
48
|
+
const loadScript = () => {
|
|
49
|
+
if (document.getElementById('recaptcha-script')) return;
|
|
50
|
+
const script = document.createElement('script');
|
|
51
|
+
script.id = 'recaptcha-script';
|
|
52
|
+
script.src = `https://www.google.com/recaptcha/enterprise.js?render=${state.recaptchaProjectId}`;
|
|
53
|
+
script.async = true;
|
|
54
|
+
script.defer = true;
|
|
55
|
+
document.head.appendChild(script);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Lazy load on interaction with forms
|
|
59
|
+
const forms = document.querySelectorAll('form[data-action="form_submit"]');
|
|
60
|
+
if (forms.length > 0) {
|
|
61
|
+
forms.forEach(form => {
|
|
62
|
+
form.addEventListener('focusin', loadScript, { once: true });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function initGTM() {
|
|
68
|
+
if (!state.tagmanagerId || state.tagmanagerId === "YOUR_GOOGLE_TAG_MANAGER_ID") return;
|
|
69
|
+
|
|
70
|
+
// GTM Script
|
|
71
|
+
(function (w, d, s, l, i) {
|
|
72
|
+
w[l] = w[l] || [];
|
|
73
|
+
w[l].push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
|
|
74
|
+
var f = d.getElementsByTagName(s)[0],
|
|
75
|
+
j = d.createElement(s),
|
|
76
|
+
dl = l != "dataLayer" ? "&l=" + l : "";
|
|
77
|
+
j.async = true;
|
|
78
|
+
j.src = CONFIG.gtmBase + "?id=" + i + dl;
|
|
79
|
+
f.parentNode.insertBefore(j, f);
|
|
80
|
+
})(window, document, "script", "dataLayer", state.tagmanagerId);
|
|
81
|
+
|
|
82
|
+
// GTM NoScript
|
|
83
|
+
const ns = document.createElement("noscript");
|
|
84
|
+
ns.innerHTML = `<iframe src="https://www.googletagmanager.com/ns.html?id=${state.tagmanagerId}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`;
|
|
85
|
+
document.body.appendChild(ns);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
//----------------------------------------------
|
|
89
|
+
// Global Event Delegation
|
|
90
|
+
//----------------------------------------------
|
|
91
|
+
function attachGlobalListeners() {
|
|
92
|
+
document.addEventListener("click", handleGlobalClick);
|
|
93
|
+
document.addEventListener("submit", handleGlobalSubmit);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function handleGlobalClick(e) {
|
|
97
|
+
const target = e.target.closest("[data-action]");
|
|
98
|
+
if (!target) return;
|
|
99
|
+
|
|
100
|
+
const action = target.getAttribute("data-action");
|
|
101
|
+
|
|
102
|
+
switch (action) {
|
|
103
|
+
case "whatsapp":
|
|
104
|
+
handleWhatsappClick(e, target);
|
|
105
|
+
break;
|
|
106
|
+
case "phone":
|
|
107
|
+
handlePhoneClick(e, target);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function handleGlobalSubmit(e) {
|
|
113
|
+
const target = e.target;
|
|
114
|
+
if (target.matches('form[data-action="form_submit"]')) {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
handleFormSubmit(target);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
//----------------------------------------------
|
|
121
|
+
// Action Handlers
|
|
122
|
+
//----------------------------------------------
|
|
123
|
+
function handleWhatsappClick(e, element) {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
const href = element.getAttribute("href") || "";
|
|
126
|
+
const phone = href.replace(/\D/g, "");
|
|
127
|
+
|
|
128
|
+
// Track Event
|
|
129
|
+
pushToDataLayer("jc_interaction", "whatsapp_contact", href);
|
|
130
|
+
localStorage.setItem("converted", "1");
|
|
131
|
+
|
|
132
|
+
// Construct URL
|
|
133
|
+
const finalUrl = `${CONFIG.whatsappBase}?phone=${phone}&text=${encodeURIComponent(
|
|
134
|
+
state.whatsappMessage
|
|
135
|
+
)}`;
|
|
136
|
+
|
|
137
|
+
// Open
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
window.open(finalUrl, "_blank");
|
|
140
|
+
}, 300);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function handlePhoneClick(e, element) {
|
|
144
|
+
// Let default action happen (tel: link) but track it first/parallel
|
|
145
|
+
const href = element.getAttribute("href");
|
|
146
|
+
pushToDataLayer("jc_interaction", "phone_contact", href);
|
|
147
|
+
localStorage.setItem("converted", "1");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//----------------------------------------------
|
|
151
|
+
// Form Handling
|
|
152
|
+
//----------------------------------------------
|
|
153
|
+
function handleFormSubmit(form) {
|
|
154
|
+
// 1. UI Updates (Optimistic)
|
|
155
|
+
const submitBtn = form.querySelector('[type="submit"]');
|
|
156
|
+
const originalBtnText = submitBtn.textContent;
|
|
157
|
+
const errorEl = form.querySelector('[data-feedback="error"]');
|
|
158
|
+
const successEl = form.querySelector('[data-feedback="success"]');
|
|
159
|
+
|
|
160
|
+
// Helper to reset UI
|
|
161
|
+
const resetUI = () => {
|
|
162
|
+
submitBtn.disabled = false;
|
|
163
|
+
submitBtn.textContent = originalBtnText;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const showMsg = (el, show) => {
|
|
167
|
+
if(el) {
|
|
168
|
+
if(show) el.classList.remove("d-none");
|
|
169
|
+
else el.classList.add("d-none");
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Hide messages
|
|
174
|
+
showMsg(errorEl, false);
|
|
175
|
+
showMsg(successEl, false);
|
|
176
|
+
|
|
177
|
+
// 2. Validation
|
|
178
|
+
form.classList.add("was-validated");
|
|
179
|
+
if (!form.checkValidity()) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
submitBtn.disabled = true;
|
|
184
|
+
submitBtn.textContent = "Enviando...";
|
|
185
|
+
|
|
186
|
+
// 3. Data Preparation
|
|
187
|
+
const formData = new FormData(form);
|
|
188
|
+
const rawData = Object.fromEntries(formData.entries());
|
|
189
|
+
|
|
190
|
+
// Normalize Data
|
|
191
|
+
const normalizedData = {
|
|
192
|
+
...rawData,
|
|
193
|
+
email: normalizeEmail(rawData.email),
|
|
194
|
+
phone: normalizePhone(rawData.phone),
|
|
195
|
+
name: (rawData.name || "").trim()
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Inject UTMs from sessionStorage
|
|
199
|
+
const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
|
|
200
|
+
utmKeys.forEach(key => {
|
|
201
|
+
const val = sessionStorage.getItem(key);
|
|
202
|
+
if (val) {
|
|
203
|
+
normalizedData[key] = val;
|
|
204
|
+
formData.set(key, val);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// 4. Submission Logic (with reCAPTCHA support)
|
|
209
|
+
const processSubmission = (token = null) => {
|
|
210
|
+
if (token) {
|
|
211
|
+
formData.set("recaptcha_token", token);
|
|
212
|
+
// Update hidden input if exists for legacy compatibility
|
|
213
|
+
const tokenInput = form.querySelector("#recaptcha-token");
|
|
214
|
+
if(tokenInput) tokenInput.value = token;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// We use formData here to preserve multipart/form-data if needed,
|
|
218
|
+
// but normalizedData contains the UTMs as well if we were sending JSON.
|
|
219
|
+
// The fetch uses formData, which we updated with UTMs above.
|
|
220
|
+
|
|
221
|
+
fetch(form.getAttribute("action"), {
|
|
222
|
+
method: "POST",
|
|
223
|
+
body: formData
|
|
224
|
+
})
|
|
225
|
+
.then(response => response.json())
|
|
226
|
+
.then(data => {
|
|
227
|
+
if (data.success) {
|
|
228
|
+
// Track Conversion
|
|
229
|
+
if (normalizedData.email || normalizedData.phone) {
|
|
230
|
+
window.dataLayer = window.dataLayer || [];
|
|
231
|
+
window.dataLayer.push({
|
|
232
|
+
event: "webform_contact",
|
|
233
|
+
user_data: {
|
|
234
|
+
email: normalizedData.email,
|
|
235
|
+
phone_number: normalizedData.phone,
|
|
236
|
+
name: normalizedData.name,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Success UI
|
|
242
|
+
form.reset();
|
|
243
|
+
form.classList.remove("was-validated");
|
|
244
|
+
showMsg(successEl, true);
|
|
245
|
+
setTimeout(() => showMsg(successEl, false), 6000);
|
|
246
|
+
} else {
|
|
247
|
+
throw new Error(data.message || "Server Error");
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
.catch(err => {
|
|
251
|
+
console.error("Form Submission Error:", err);
|
|
252
|
+
showMsg(errorEl, true);
|
|
253
|
+
})
|
|
254
|
+
.finally(() => {
|
|
255
|
+
resetUI();
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// 5. Execute reCAPTCHA if available
|
|
260
|
+
if (state.recaptchaProjectId && window.grecaptcha) {
|
|
261
|
+
grecaptcha.enterprise.ready(function() {
|
|
262
|
+
grecaptcha.enterprise.execute(state.recaptchaProjectId, {action: CONFIG.grecaptchaAction})
|
|
263
|
+
.then(processSubmission)
|
|
264
|
+
.catch(err => {
|
|
265
|
+
console.error("reCAPTCHA Error:", err);
|
|
266
|
+
showMsg(errorEl, true);
|
|
267
|
+
resetUI();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
} else {
|
|
271
|
+
processSubmission();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
//----------------------------------------------
|
|
276
|
+
// Utilities
|
|
277
|
+
//----------------------------------------------
|
|
278
|
+
function pushToDataLayer(event, action, label) {
|
|
279
|
+
window.dataLayer = window.dataLayer || [];
|
|
280
|
+
window.dataLayer.push({
|
|
281
|
+
event: event,
|
|
282
|
+
action: action,
|
|
283
|
+
label: label,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function normalizeEmail(email) {
|
|
288
|
+
return (email || "").toLowerCase().trim();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function normalizePhone(phone) {
|
|
292
|
+
let clean = (phone || "").replace(/\D/g, "");
|
|
293
|
+
// MX heuristic: if 10 digits, prepend 52
|
|
294
|
+
if (clean.length === 10) {
|
|
295
|
+
clean = "52" + clean;
|
|
296
|
+
}
|
|
297
|
+
return clean ? "+" + clean : "";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Initialize
|
|
301
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
302
|
+
|
|
303
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jetclic-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "N煤cleo l贸gico para landing pages de alta conversi贸n: UTMs, GTM y Formularios.",
|
|
5
|
+
"main": "jetclic-core.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"marketing",
|
|
11
|
+
"google-ads",
|
|
12
|
+
"conversion",
|
|
13
|
+
"gtm"
|
|
14
|
+
],
|
|
15
|
+
"author": "JetClic Architecture",
|
|
16
|
+
"license": "MIT"
|
|
17
|
+
}
|