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.
Files changed (3) hide show
  1. package/README.md +17 -0
  2. package/jetclic-core.js +303 -0
  3. 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>
@@ -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
+ }