forked from colin/resume
295 lines
9.5 KiB
JavaScript
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">×</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);
|
|
}
|
|
}
|
|
});
|
|
|