forked from colin/resume
2
0
Fork 0
lucky-ddg/docker/resume/one-pager-tools/utm-tool.js

295 lines
9.5 KiB
JavaScript

/**
* 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 = `
<input type="text" placeholder="Parameter name" class="custom-key" aria-label="Custom parameter name">
<input type="text" placeholder="Value" class="custom-value" aria-label="Custom parameter value">
<button type="button" class="btn-remove-param" aria-label="Remove parameter">&times;</button>
`;
// 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 => `
<tr>
<th>${escapeHtml(row.param)}</th>
<td>${escapeHtml(row.value)}</td>
</tr>
`).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);
}
}
});