/** * UTM URL Generator functionality * Builds tracking URLs with UTM parameters * Client-side only - no data sent to server */ document.addEventListener('DOMContentLoaded', function() { // DOM Elements const baseUrlInput = document.getElementById('baseUrl'); const utmSourceInput = document.getElementById('utmSource'); const utmMediumInput = document.getElementById('utmMedium'); const utmCampaignInput = document.getElementById('utmCampaign'); const utmTermInput = document.getElementById('utmTerm'); const utmContentInput = document.getElementById('utmContent'); const customParamsContainer = document.getElementById('customParams'); const addParamBtn = document.getElementById('addParamBtn'); const generatedUrlDiv = document.getElementById('generatedUrl'); const validationMessage = document.getElementById('validationMessage'); const copyBtn = document.getElementById('copyBtn'); const copyFeedback = document.getElementById('copyFeedback'); const urlBreakdown = document.getElementById('urlBreakdown'); const breakdownBody = document.getElementById('breakdownBody'); // Current generated URL let currentUrl = ''; let customParamCount = 0; // Debounce timer let debounceTimer; // Add input listeners to all UTM fields const utmInputs = [ baseUrlInput, utmSourceInput, utmMediumInput, utmCampaignInput, utmTermInput, utmContentInput ]; utmInputs.forEach(input => { input.addEventListener('input', debounceGenerate); }); // Add custom parameter button addParamBtn.addEventListener('click', addCustomParam); // Copy button copyBtn.addEventListener('click', copyToClipboard); /** * Debounced URL generation */ function debounceGenerate() { clearTimeout(debounceTimer); debounceTimer = setTimeout(generateUrl, 150); } /** * Add a custom parameter row */ function addCustomParam() { customParamCount++; const row = document.createElement('div'); row.className = 'custom-param-row'; row.id = `customParam${customParamCount}`; row.innerHTML = ` `; // Add input listeners const keyInput = row.querySelector('.custom-key'); const valueInput = row.querySelector('.custom-value'); keyInput.addEventListener('input', debounceGenerate); valueInput.addEventListener('input', debounceGenerate); // Add remove listener const removeBtn = row.querySelector('.btn-remove-param'); removeBtn.addEventListener('click', function() { row.remove(); debounceGenerate(); }); customParamsContainer.appendChild(row); keyInput.focus(); } /** * Validate URL format */ function isValidUrl(string) { try { const url = new URL(string); return url.protocol === 'http:' || url.protocol === 'https:'; } catch (_) { return false; } } /** * Get all custom parameters */ function getCustomParams() { const params = []; const rows = customParamsContainer.querySelectorAll('.custom-param-row'); rows.forEach(row => { const key = row.querySelector('.custom-key').value.trim(); const value = row.querySelector('.custom-value').value.trim(); if (key && value) { params.push({ key, value }); } }); return params; } /** * Generate the UTM URL */ function generateUrl() { const baseUrl = baseUrlInput.value.trim(); const source = utmSourceInput.value.trim(); const medium = utmMediumInput.value.trim(); const campaign = utmCampaignInput.value.trim(); const term = utmTermInput.value.trim(); const content = utmContentInput.value.trim(); const customParams = getCustomParams(); // Clear previous state validationMessage.textContent = ''; validationMessage.className = 'validation-message'; generatedUrlDiv.className = 'generated-url'; // Check if we have any input at all if (!baseUrl) { generatedUrlDiv.textContent = 'Enter a URL to start generating your tracking URL'; generatedUrlDiv.classList.add('empty'); copyBtn.disabled = true; urlBreakdown.style.display = 'none'; currentUrl = ''; return; } // Validate base URL format if (!isValidUrl(baseUrl)) { generatedUrlDiv.textContent = baseUrl; generatedUrlDiv.classList.add('invalid'); showValidation('Enter a valid URL (starting with http:// or https://)', 'error'); copyBtn.disabled = true; urlBreakdown.style.display = 'none'; currentUrl = ''; return; } // Build the URL with whatever parameters are filled in try { const url = new URL(baseUrl); // Add UTM parameters only if they have values if (source) { url.searchParams.set('utm_source', source); } if (medium) { url.searchParams.set('utm_medium', medium); } if (campaign) { url.searchParams.set('utm_campaign', campaign); } if (term) { url.searchParams.set('utm_term', term); } if (content) { url.searchParams.set('utm_content', content); } // Add custom parameters customParams.forEach(param => { url.searchParams.set(param.key, param.value); }); currentUrl = url.toString(); generatedUrlDiv.textContent = currentUrl; generatedUrlDiv.classList.add('valid'); copyBtn.disabled = false; // Show hint about missing recommended fields const missing = []; if (!source) missing.push('source'); if (!medium) missing.push('medium'); if (!campaign) missing.push('campaign'); if (missing.length > 0) { showValidation(`Tip: Add ${missing.join(', ')} for complete tracking`, 'info'); } else { showValidation('URL ready to copy', 'success'); } // Update breakdown updateBreakdown(url, customParams); } catch (error) { showValidation('Error generating URL: ' + error.message, 'error'); generatedUrlDiv.classList.add('invalid'); copyBtn.disabled = true; urlBreakdown.style.display = 'none'; currentUrl = ''; } } /** * Show validation message */ function showValidation(message, type) { validationMessage.textContent = message; validationMessage.className = `validation-message ${type}`; } /** * Update the URL breakdown table */ function updateBreakdown(url, customParams) { urlBreakdown.style.display = 'block'; const rows = []; // Base URL rows.push({ param: 'Base URL', value: url.origin + url.pathname }); // UTM parameters const utmParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; utmParams.forEach(param => { const value = url.searchParams.get(param); if (value) { rows.push({ param, value }); } }); // Custom parameters customParams.forEach(param => { rows.push({ param: param.key, value: param.value }); }); breakdownBody.innerHTML = rows.map(row => ` ${escapeHtml(row.param)} ${escapeHtml(row.value)} `).join(''); } /** * Escape HTML to prevent XSS */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Copy URL to clipboard */ async function copyToClipboard() { if (!currentUrl) return; try { await navigator.clipboard.writeText(currentUrl); copyFeedback.classList.add('show'); setTimeout(() => { copyFeedback.classList.remove('show'); }, 2000); } catch (err) { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = currentUrl; textArea.style.position = 'fixed'; textArea.style.left = '-9999px'; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); copyFeedback.classList.add('show'); setTimeout(() => { copyFeedback.classList.remove('show'); }, 2000); } catch (e) { console.error('Failed to copy:', e); } document.body.removeChild(textArea); } } });