Initial commit: URL Redirect Tracker application with comprehensive documentation

This commit is contained in:
Andrei
2025-08-18 06:37:57 +00:00
commit a5d19c8549
1580 changed files with 211670 additions and 0 deletions

94
public/api-docs.html Normal file
View File

@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>URL Redirect Tracker - API Documentation</title>
<link rel="stylesheet" href="styles.css">
</head>
<body class="dark-mode">
<div class="container">
<div class="header-container">
<h1>URL Redirect Tracker - API Documentation</h1>
<p>Programmatically track and analyze URL redirect chains with our public API</p>
<div class="back-link" style="margin-top: 15px;">
<a href="/" class="button">← Back to Tracker</a>
</div>
</div>
<div class="content">
<section>
<h2>Introduction</h2>
<p>The URL Redirect Tracker API allows you to programmatically track and analyze URL redirect chains. The API is rate-limited to 100 requests per hour per IP address.</p>
</section>
<section>
<h2>API Endpoints</h2>
<div class="card">
<h3>POST /api/v1/track</h3>
<p>Track a URL and get the full redirect chain using a POST request.</p>
<h4>Request Parameters (JSON Body)</h4>
<ul>
<li><strong>url</strong> (string, required): The URL to track</li>
<li><strong>method</strong> (string, optional): HTTP method (GET, HEAD, POST). Default: "GET"</li>
<li><strong>userAgent</strong> (string, optional): Custom User-Agent header</li>
</ul>
<h4>Example Request</h4>
<pre><code>curl -X POST https://urltrackertool.com/api/v1/track \
-H "Content-Type: application/json" \
-d '{
"url": "github.com",
"method": "GET"
}'</code></pre>
</div>
<div class="card">
<h3>GET /api/v1/track</h3>
<p>Track a URL and get the full redirect chain using a GET request with query parameters.</p>
<h4>Request Parameters (Query String)</h4>
<ul>
<li><strong>url</strong> (string, required): The URL to track</li>
<li><strong>method</strong> (string, optional): HTTP method (GET, HEAD, POST). Default: "GET"</li>
<li><strong>userAgent</strong> (string, optional): Custom User-Agent header</li>
</ul>
<h4>Example Request</h4>
<pre><code>curl "https://urltrackertool.com/api/v1/track?url=github.com&method=GET"</code></pre>
</div>
</section>
<section>
<h2>Response Format</h2>
<p>Both endpoints return the same JSON response structure:</p>
<pre><code>{
"success": true,
"status": 200,
"data": {
"url": "http://github.com",
"method": "GET",
"redirectCount": 1,
"finalUrl": "https://github.com/",
"finalStatusCode": 200,
"redirects": [
// Array of redirect objects with detailed information
]
}
}</code></pre>
</section>
<section>
<h2>Error Codes</h2>
<ul>
<li><strong>400</strong> - Bad Request (missing parameters)</li>
<li><strong>429</strong> - Too Many Requests (rate limit exceeded)</li>
<li><strong>500</strong> - Server Error</li>
</ul>
</section>
</div>
</div>
</body>
</html>

168
public/api-readme.md Normal file
View File

@@ -0,0 +1,168 @@
# URL Redirect Tracker API Documentation
This API allows you to programmatically track and analyze URL redirect chains, providing detailed information about each redirect, including HTTP status codes, headers, and response data.
## Rate Limiting
The API is limited to 100 requests per hour per IP address.
## API Endpoints
### POST /api/v1/track
Track a URL and get the full redirect chain using a POST request.
#### Request Parameters (JSON Body)
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| url | string | Yes | The URL to track (e.g., "example.com" or "https://example.com") |
| method | string | No | HTTP method to use (GET, HEAD, POST). Default: "GET" |
| userAgent | string | No | Custom User-Agent header to send with the request |
#### Example Request
```bash
curl -X POST https://urltrackertool.com/api/v1/track \
-H "Content-Type: application/json" \
-d '{
"url": "github.com",
"method": "GET",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome"
}'
```
### GET /api/v1/track
Track a URL and get the full redirect chain using a GET request with query parameters.
#### Request Parameters (Query String)
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| url | string | Yes | The URL to track (e.g., "example.com" or "https://example.com") |
| method | string | No | HTTP method to use (GET, HEAD, POST). Default: "GET" |
| userAgent | string | No | Custom User-Agent header to send with the request |
#### Example Request
```bash
curl "https://urltrackertool.com/api/v1/track?url=github.com&method=GET&userAgent=Mozilla%2F5.0%20Chrome"
```
## Response Format
Both endpoints return the same JSON structure:
```json
{
"success": true,
"status": 200,
"data": {
"url": "http://github.com",
"method": "GET",
"redirectCount": 1,
"finalUrl": "https://github.com/",
"finalStatusCode": 200,
"redirects": [
{
"url": "http://github.com",
"timestamp": 1684320000000,
"isSSL": false,
"duration": 220,
"statusCode": 301,
"statusText": "Moved Permanently",
"metadata": {
"status": 301,
"statusText": "Moved Permanently",
"headers": {
"location": "https://github.com/",
"content-length": "0",
"server": "GitHub.com"
},
"contentType": "text/html",
"contentLength": "0",
"server": "GitHub.com",
"date": "Wed, 17 May 2023 12:00:00 GMT",
"protocol": "http:",
"method": "GET"
}
},
{
"url": "https://github.com/",
"timestamp": 1684320000220,
"isSSL": true,
"duration": 350,
"statusCode": 200,
"statusText": "OK",
"metadata": {
"status": 200,
"statusText": "OK",
"headers": {
"content-type": "text/html; charset=utf-8",
"server": "GitHub.com"
},
"contentType": "text/html; charset=utf-8",
"server": "GitHub.com",
"date": "Wed, 17 May 2023 12:00:00 GMT",
"protocol": "https:",
"method": "GET"
},
"final": true
}
]
}
}
```
## Error Codes
| Status Code | Description |
|-------------|-------------|
| 400 | Bad Request - Required parameters are missing or invalid |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Server Error - An error occurred while processing the request |
## JavaScript Example
```javascript
const trackUrl = async (url) => {
const response = await fetch('https://urltrackertool.com/api/v1/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
url,
method: 'GET'
})
});
return await response.json();
};
// Usage
trackUrl('github.com')
.then(data => console.log(data))
.catch(err => console.error(err));
```
## Python Example
```python
import requests
def track_url(url):
response = requests.post(
'https://urltrackertool.com/api/v1/track',
json={
'url': url,
'method': 'GET'
}
)
return response.json()
# Usage
result = track_url('github.com')
print(result)
```

16
public/api.html Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>URL Redirect Tracker API</title>
</head>
<body>
<h1>URL Redirect Tracker API</h1>
<p>The API is available at:</p>
<ul>
<li><strong>POST /api/v1/track</strong> - Track redirects (JSON body with url, method, userAgent)</li>
<li><strong>GET /api/v1/track</strong> - Track redirects (Query params: url, method, userAgent)</li>
</ul>
<p>Rate limit: 100 requests per hour per IP</p>
<p><a href="/">Back to tracker</a></p>
</body>
</html>

164
public/index.html Normal file
View File

@@ -0,0 +1,164 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="URL Redirect Tracker - Track and visualize URL redirection chains, status codes, and response headers. Discover where your links ultimately lead.">
<meta name="keywords" content="URL tracker, redirect checker, HTTP redirection, URL chain, link redirect, SEO tools">
<meta name="author" content="URL Redirect Tracker">
<meta name="robots" content="index, follow">
<meta property="og:title" content="URL Redirect Tracker">
<meta property="og:description" content="Track and visualize URL redirection chains with detailed HTTP status codes and headers.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://urltrackertool.com">
<meta property="og:image" content="favicon.ico">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="URL Redirect Tracker">
<meta name="twitter:description" content="Track and visualize URL redirection chains with detailed HTTP status codes and headers.">
<title>URL Redirect Tracker</title>
<link rel="stylesheet" href="styles.css">
<!-- Use a reliable version of Mermaid -->
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js"></script>
<script>
// Simple Mermaid configuration
mermaid.initialize({
startOnLoad: false, // We'll manually trigger rendering
theme: 'dark', // Use dark theme by default
securityLevel: 'loose',
logLevel: 'error',
flowchart: {
useMaxWidth: true,
htmlLabels: true,
curve: 'linear'
}
});
</script>
<!-- JSON-LD structured data for SEO -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "URL Redirect Tracker",
"description": "A tool to track and visualize URL redirection chains, status codes, and response headers.",
"applicationCategory": "Utility",
"operatingSystem": "Any",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"featureList": "Track redirects, Visualize redirect chains, Display status codes, Show response headers"
}
</script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-ZDZ26XYN2P"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-ZDZ26XYN2P');
</script>
</head>
<body class="dark-mode">
<div class="container">
<div class="header-container">
<h1>URL Redirect Tracker</h1><br />
<p>Track and visualize URL redirection chains, status codes, and response headers.</p>
<button id="toggleMode" class="theme-toggle-button" aria-label="Toggle dark mode" aria-pressed="true">
<span class="theme-icon sun-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</span>
<span class="theme-icon moon-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</span>
</button>
</div>
<form id="urlForm">
<div class="input-group">
<input type="url" id="urlInput" placeholder="Enter URL (e.g., http://example.com)" required>
<div class="form-controls">
<div class="select-container">
<label for="httpMethod">Method:</label>
<select name="method" id="httpMethod">
<option value="GET">GET</option>
<option value="HEAD">HEAD</option>
<option value="POST">POST</option>
</select>
</div>
<button type="submit">Track Redirects</button>
</div>
</div>
<div class="user-agent-container">
<label for="userAgent">User-Agent:</label>
<select id="userAgent">
<option value="default">Default</option>
<option value="googlebot">Googlebot</option>
<option value="bingbot">Bingbot</option>
<option value="chrome">Chrome</option>
<option value="iphone">iPhone Safari</option>
</select>
</div>
</form>
<div id="loading" class="hidden">
<div class="spinner"></div>
<p>Tracking redirects...</p>
</div>
<div id="error" class="hidden">
<p class="error-message"></p>
</div>
<div id="warnings" class="hidden">
<div class="warning-container"></div>
</div>
<div id="results" class="hidden">
<div class="results-header">
<h2>Redirect Chain</h2>
<div class="summary-container"></div>
<button id="printResults" class="print-btn no-print">Print Results</button>
</div>
<div class="tabs-main">
<button class="tab-main active" data-tab="list-view">List View</button>
<button class="tab-main" data-tab="graph-view">Graph View</button>
</div>
<div class="tab-main-content" id="list-view">
<div class="results-container">
<ol id="redirectList"></ol>
</div>
</div>
<div class="tab-main-content hidden" id="graph-view">
<div class="graph-container">
<div id="mermaid-graph"></div>
</div>
</div>
</div>
<div class="footer" style="margin-top: 30px; padding: 15px 0; text-align: center; border-top: 1px solid var(--border-color);">
<a href="/api/docs" class="button">API Documentation</a>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

3
public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
User-agent: *
Allow: /
Sitemap: https://urltrackertool.com/sitemap.xml

838
public/script.js Normal file
View File

@@ -0,0 +1,838 @@
document.addEventListener('DOMContentLoaded', () => {
const urlForm = document.getElementById('urlForm');
const urlInput = document.getElementById('urlInput');
const httpMethodSelect = document.getElementById('httpMethod');
const userAgentSelect = document.getElementById('userAgent');
const loading = document.getElementById('loading');
const results = document.getElementById('results');
const redirectList = document.getElementById('redirectList');
const errorDiv = document.getElementById('error');
const errorMessage = document.querySelector('.error-message');
const warningsDiv = document.getElementById('warnings');
const warningContainer = document.querySelector('.warning-container');
const summaryContainer = document.querySelector('.summary-container');
const toggleModeBtn = document.getElementById('toggleMode');
const printResultsBtn = document.getElementById('printResults');
const mermaidGraph = document.getElementById('mermaid-graph');
// Check if all required elements exist
if (!urlForm || !urlInput || !redirectList || !errorMessage || !warningContainer || !summaryContainer) {
console.error('One or more required HTML elements not found. Check your HTML structure.');
return;
}
// Hide the graph view tab
const graphTabButton = document.querySelector('.tab-main[data-tab="graph-view"]');
if (graphTabButton) {
graphTabButton.style.display = 'none';
}
// Set the list view as the only visible tab
const listViewContent = document.getElementById('list-view');
if (listViewContent) {
listViewContent.classList.remove('hidden');
}
// User agent options
const userAgents = {
default: '',
googlebot: 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
bingbot: 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)',
chrome: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113 Safari/537.36)',
iphone: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Mobile/15E148 Safari/604.1'
};
// Set up tab switching
const tabButtons = document.querySelectorAll('.tab-main');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const tabId = button.getAttribute('data-tab');
// Skip processing if it's the graph-view tab
if (tabId === 'graph-view') {
return;
}
// Remove active class from all buttons and content
tabButtons.forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-main-content').forEach(content => {
content.classList.add('hidden');
});
// Add active class to clicked button and show content
button.classList.add('active');
document.getElementById(tabId).classList.remove('hidden');
// Re-render mermaid graph if graph view is selected
if (tabId === 'graph-view') {
console.log("Switched to graph view tab");
renderMermaidGraph();
}
});
});
// Toggle dark mode
if (toggleModeBtn) {
// Function to toggle dark mode
const toggleDarkMode = (isDark) => {
if (isDark) {
document.body.classList.add('dark-mode');
toggleModeBtn.setAttribute('aria-pressed', 'true');
localStorage.setItem('darkMode', 'enabled');
} else {
document.body.classList.remove('dark-mode');
toggleModeBtn.setAttribute('aria-pressed', 'false');
localStorage.setItem('darkMode', 'disabled');
}
};
// Check for system preference
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
// Check if dark mode was previously set
const storedTheme = localStorage.getItem('darkMode');
if (storedTheme === 'disabled') {
// User explicitly disabled dark mode
toggleDarkMode(false);
} else {
// Default to dark mode for all other cases:
// - When no stored preference
// - When stored preference is "enabled"
toggleDarkMode(true);
}
// Initialize ARIA attributes
toggleModeBtn.setAttribute('aria-pressed', document.body.classList.contains('dark-mode').toString());
// Add event listener for toggle button
toggleModeBtn.addEventListener('click', () => {
const isDarkMode = document.body.classList.contains('dark-mode');
toggleDarkMode(!isDarkMode);
});
// Also listen for system changes
prefersDarkScheme.addEventListener('change', (e) => {
if (localStorage.getItem('darkMode') === null) {
// Only auto-switch if user hasn't set a preference
toggleDarkMode(e.matches);
}
});
}
// Print results
printResultsBtn.addEventListener('click', () => {
window.print();
});
urlForm.addEventListener('submit', async (e) => {
e.preventDefault();
const url = urlInput.value.trim();
const method = httpMethodSelect.value;
const userAgent = userAgents[userAgentSelect.value] || '';
if (!url) {
showError('Please enter a valid URL');
return;
}
// Clear previous results
redirectList.innerHTML = '';
hideError();
hideWarnings();
showLoading();
hideResults();
try {
const response = await fetch('/api/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
url,
method,
userAgent
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to track redirects');
}
// Check for potential issues with the redirect chain
checkRedirectIssues(data.redirects);
// Generate and display redirect summary
generateRedirectSummary(data.redirects);
// Generate Mermaid graph
generateMermaidGraph(data.redirects);
// Display the redirects in list format
displayRedirects(data.redirects);
} catch (error) {
showError(error.message);
} finally {
hideLoading();
}
});
function checkRedirectIssues(redirects) {
// Clear previous warnings
warningContainer.innerHTML = '';
let warningCount = 0;
// Loop detection
const urlSet = new Set();
const loopUrl = redirects.find(redirect => {
if (urlSet.has(redirect.url)) {
return true;
}
urlSet.add(redirect.url);
return false;
});
if (loopUrl) {
addWarning(`⚠️ Redirect loop detected! URL "${loopUrl.url}" appears multiple times in the chain.`);
warningCount++;
}
// SSL downgrade detection
for (let i = 1; i < redirects.length; i++) {
const prevUrl = redirects[i-1].url;
const currUrl = redirects[i].url;
if (prevUrl.toLowerCase().startsWith('https://') && currUrl.toLowerCase().startsWith('http://')) {
addWarning(`⚠️ Mixed content warning: SSL downgrade from "${prevUrl}" to "${currUrl}"`);
warningCount++;
}
}
// Show warnings if any
if (warningCount > 0) {
showWarnings();
}
}
function generateRedirectSummary(redirects) {
// Create a summary of status codes
const statusCounts = {};
redirects.forEach(redirect => {
if (redirect.statusCode) {
const statusKey = `${redirect.statusCode} ${redirect.statusText || ''}`;
statusCounts[statusKey] = (statusCounts[statusKey] || 0) + 1;
}
});
// Create summary text
let summaryText = `${redirects.length} step${redirects.length === 1 ? '' : 's'} in chain: `;
const statusSummary = Object.entries(statusCounts)
.map(([status, count]) => `${count}× ${status.trim()}`)
.join(', ');
if (statusSummary) {
summaryText += statusSummary;
} else {
summaryText += 'No redirects';
}
// Add SSL info
const sslCount = redirects.filter(r => r.isSSL).length;
const nonSslCount = redirects.filter(r => !r.isSSL).length;
if (sslCount > 0 || nonSslCount > 0) {
summaryText += ` (${sslCount} SSL, ${nonSslCount} non-SSL)`;
}
summaryContainer.textContent = summaryText;
}
function generateMermaidGraph(redirects) {
console.log('Generating Mermaid graph from redirects:', redirects);
if (!redirects || redirects.length === 0) {
console.log('No redirects to display in graph');
return 'graph TD\n emptyNode["No redirects to display"]\n style emptyNode fill:#f9f9f9,stroke:#ccc,stroke-width:1px';
}
let definition = 'graph TD\n';
// Track visited URLs to avoid duplicates
const visitedUrls = new Set();
// First pass - add all nodes
redirects.forEach((redirect, index) => {
if (!redirect.url || !redirect.redirectUrl) return;
const fromUrl = sanitizeMermaidText(redirect.url);
const toUrl = sanitizeMermaidText(redirect.redirectUrl);
// Skip if we've already processed this exact redirect
const redirectKey = `${fromUrl}|${toUrl}`;
if (visitedUrls.has(redirectKey)) return;
visitedUrls.add(redirectKey);
// Create node labels with truncated URLs
const fromLabel = truncateUrl(redirect.url, 30);
const toLabel = truncateUrl(redirect.redirectUrl, 30);
// Add nodes with ID based on index to avoid duplicate IDs and label issues
definition += ` node${index}["${sanitizeMermaidText(fromLabel)}"]\n`;
definition += ` node${index}to["${sanitizeMermaidText(toLabel)}"]\n`;
// Add connection with status code if available
const statusText = redirect.statusCode ? ` (${redirect.statusCode})` : '';
definition += ` node${index} -->|"${statusText}"| node${index}to\n`;
});
console.log('Generated Mermaid definition:', definition);
return definition;
}
// Separate function to handle Mermaid rendering
function renderMermaidGraph() {
// First make sure Mermaid is properly loaded
if (typeof mermaid === 'undefined') {
console.error('Mermaid library not loaded');
displayGraphError('Graph rendering library not loaded properly.');
return;
}
// Reset and reinitialize Mermaid with current theme
try {
const isDarkMode = document.body.classList.contains('dark-mode');
console.log("Initializing Mermaid with theme:", isDarkMode ? 'dark' : 'default');
mermaid.initialize({
startOnLoad: false,
theme: isDarkMode ? 'dark' : 'default',
securityLevel: 'loose',
logLevel: 'warn', // Change to 'debug' for more detailed logs
fontFamily: 'sans-serif',
flowchart: {
useMaxWidth: true,
htmlLabels: true,
curve: 'basis'
}
});
// Wait a bit to ensure DOM is ready
setTimeout(() => {
try {
console.log("Running mermaid.run()");
mermaid.run({
querySelector: '.mermaid'
}).then(() => {
console.log("Mermaid graph rendered successfully");
}).catch(err => {
console.error("Mermaid rendering error:", err);
displayGraphError('Failed to render graph: ' + err.message);
});
} catch (err) {
console.error('Error during mermaid.run():', err);
// Fallback approach
try {
console.log("Falling back to contentLoaded()");
mermaid.contentLoaded();
} catch (innerErr) {
console.error('Error during fallback rendering:', innerErr);
displayGraphError('Failed to render graph. Technical details: ' + innerErr.message);
}
}
}, 200);
} catch (e) {
console.error('Error initializing Mermaid:', e);
displayGraphError('Failed to initialize graph rendering: ' + e.message);
}
}
// Sanitize text for Mermaid compatibility
function sanitizeMermaidText(text) {
if (!text) return '';
// Escape characters that can break Mermaid syntax
return text
.replace(/"/g, "'") // Replace double quotes with single quotes
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\(/g, '&#40;')
.replace(/\)/g, '&#41;')
.replace(/\[/g, '&#91;')
.replace(/\]/g, '&#93;')
.replace(/\{/g, '&#123;')
.replace(/\}/g, '&#125;');
}
// Helper function to truncate long URLs for display
function truncateUrl(url, maxLength) {
if (!url) return '';
try {
// Try to parse and display in a more readable format
const urlObj = new URL(url);
const displayUrl = urlObj.hostname + urlObj.pathname;
if (displayUrl.length <= maxLength) {
return displayUrl;
}
return displayUrl.substring(0, maxLength - 3) + '...';
} catch (e) {
// Fallback if URL parsing fails
return url.length > maxLength ? url.substring(0, maxLength - 3) + '...' : url;
}
}
// Display error message in the graph container
function displayGraphError(errorMessage) {
mermaidGraph.innerHTML = '';
const errorDiv = document.createElement('div');
errorDiv.className = 'mermaid-error';
errorDiv.innerHTML = `<p>Error rendering graph: ${errorMessage}</p>
<p>Please try a different URL or report this issue.</p>`;
mermaidGraph.appendChild(errorDiv);
}
function displayRedirects(redirects) {
if (!redirects || redirects.length === 0) {
showError('No redirect information received');
return;
}
redirects.forEach((redirect, index) => {
const li = document.createElement('li');
li.className = 'redirect-item';
if (!redirect.isSSL) {
li.classList.add('non-ssl');
}
if (redirect.final) {
li.classList.add('final');
}
// Create header with step info, status code, and duration
const header = document.createElement('div');
header.className = 'redirect-item-header';
// Step and status
const stepLabel = document.createElement('span');
const statusCode = redirect.statusCode ? ` (${redirect.statusCode} ${redirect.statusText || ''})` : '';
stepLabel.textContent = `Step ${index}:${statusCode}`;
stepLabel.className = 'step-label';
// Duration
const duration = document.createElement('span');
duration.className = 'redirect-duration';
const durationValue = redirect.duration !== undefined ? redirect.duration : 0;
duration.textContent = `${durationValue} ms`;
header.appendChild(stepLabel);
header.appendChild(duration);
// URL container with copy button
const urlContainer = document.createElement('div');
urlContainer.className = 'url-container';
// URL text
const urlDiv = document.createElement('div');
urlDiv.className = 'redirect-url';
urlDiv.textContent = redirect.url;
// Copy button
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-url-btn';
copyBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/>
<path d="M9.5 1h-3a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/>
</svg>`;
copyBtn.setAttribute('aria-label', 'Copy URL to clipboard');
copyBtn.setAttribute('title', 'Copy URL to clipboard');
// Add copy functionality
copyBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent toggling details when clicking the copy button
// Copy URL to clipboard
navigator.clipboard.writeText(redirect.url)
.then(() => {
// Show success feedback
const originalHTML = copyBtn.innerHTML;
copyBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
</svg>`;
copyBtn.classList.add('copied');
// Reset button after a delay
setTimeout(() => {
copyBtn.innerHTML = originalHTML;
copyBtn.classList.remove('copied');
}, 2000);
})
.catch(err => {
console.error('Could not copy URL: ', err);
alert('Failed to copy URL. Please try again.');
});
});
urlContainer.appendChild(urlDiv);
urlContainer.appendChild(copyBtn);
li.appendChild(header);
li.appendChild(urlContainer);
// Create collapsible section for details
const detailsContainer = document.createElement('div');
detailsContainer.className = 'details-container';
// Create toggle button
const toggleButton = document.createElement('button');
toggleButton.className = 'toggle-details-btn';
toggleButton.textContent = 'Show Details';
toggleButton.addEventListener('click', () => {
const detailsContent = detailsContainer.querySelector('.details-content');
const isHidden = detailsContent.classList.contains('hidden');
if (isHidden) {
detailsContent.classList.remove('hidden');
toggleButton.textContent = 'Hide Details';
} else {
detailsContent.classList.add('hidden');
toggleButton.textContent = 'Show Details';
}
});
// Create details content (hidden by default)
const detailsContent = document.createElement('div');
detailsContent.className = 'details-content hidden';
// Add tabs for different types of info
const tabsContainer = document.createElement('div');
tabsContainer.className = 'tabs-container';
const tabs = [
{ id: 'headers', name: 'Headers' },
{ id: 'body', name: 'Response Body' },
{ id: 'metadata', name: 'Metadata' },
{ id: 'tracking', name: 'Tracking Params' },
{ id: 'ssl', name: 'SSL Info' }
];
const tabButtons = document.createElement('div');
tabButtons.className = 'tab-buttons';
const tabContents = document.createElement('div');
tabContents.className = 'tab-contents';
tabs.forEach((tab, tabIndex) => {
// Create tab button
const tabButton = document.createElement('button');
tabButton.className = 'tab-button';
tabButton.textContent = tab.name;
if (tabIndex === 0) tabButton.classList.add('active');
// Create tab content
const tabContent = document.createElement('div');
tabContent.className = 'tab-content';
tabContent.id = `${tab.id}-${index}`;
if (tabIndex !== 0) tabContent.classList.add('hidden');
// Add content based on tab type
switch (tab.id) {
case 'headers':
if (redirect.metadata && redirect.metadata.headers) {
const headersContent = document.createElement('pre');
headersContent.className = 'headers-content';
// Format headers
let headersText = '';
const headers = redirect.metadata.headers;
for (const key in headers) {
headersText += `${key}: ${headers[key]}\n`;
}
headersContent.textContent = headersText || 'No headers available';
tabContent.appendChild(headersContent);
} else {
tabContent.textContent = 'No headers available';
}
break;
case 'body':
const bodyContent = document.createElement('pre');
bodyContent.className = 'body-content';
bodyContent.textContent = redirect.responseBody || 'No response body available';
tabContent.appendChild(bodyContent);
break;
case 'metadata':
if (redirect.metadata) {
const metadataContent = document.createElement('pre');
metadataContent.className = 'metadata-content';
metadataContent.textContent = JSON.stringify(redirect.metadata, null, 2);
tabContent.appendChild(metadataContent);
} else {
tabContent.textContent = 'No metadata available';
}
break;
case 'tracking':
// Extract tracking parameters from the URL
const trackingParams = extractTrackingParams(redirect.url);
if (Object.keys(trackingParams).length > 0) {
// Create a table for better presentation
const trackingTable = document.createElement('table');
trackingTable.className = 'tracking-table';
// Add table header
const tableHeader = document.createElement('thead');
const headerRow = document.createElement('tr');
const headerParam = document.createElement('th');
headerParam.textContent = 'Parameter';
headerRow.appendChild(headerParam);
const headerValue = document.createElement('th');
headerValue.textContent = 'Value';
headerRow.appendChild(headerValue);
tableHeader.appendChild(headerRow);
trackingTable.appendChild(tableHeader);
// Add table body
const tableBody = document.createElement('tbody');
Object.entries(trackingParams).forEach(([param, value]) => {
const row = document.createElement('tr');
const paramCell = document.createElement('td');
paramCell.className = 'param-name';
paramCell.textContent = param;
row.appendChild(paramCell);
const valueCell = document.createElement('td');
valueCell.className = 'param-value';
valueCell.textContent = value;
row.appendChild(valueCell);
tableBody.appendChild(row);
});
trackingTable.appendChild(tableBody);
tabContent.appendChild(trackingTable);
} else {
tabContent.textContent = 'No tracking parameters detected in this URL';
}
break;
case 'ssl':
if (redirect.sslInfo) {
const sslContent = document.createElement('pre');
sslContent.className = 'ssl-content';
sslContent.textContent = JSON.stringify(redirect.sslInfo, null, 2);
tabContent.appendChild(sslContent);
} else {
tabContent.textContent = redirect.isSSL
? 'SSL certificate information not available'
: 'Not an SSL connection';
}
break;
}
// Add click handler for tab button
tabButton.addEventListener('click', () => {
// Deactivate all buttons and hide all contents
tabButtons.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
tabContents.querySelectorAll('.tab-content').forEach(content => content.classList.add('hidden'));
// Activate this button and show its content
tabButton.classList.add('active');
tabContent.classList.remove('hidden');
});
tabButtons.appendChild(tabButton);
tabContents.appendChild(tabContent);
});
tabsContainer.appendChild(tabButtons);
tabsContainer.appendChild(tabContents);
// Add error message if present
if (redirect.error) {
const errorInfo = document.createElement('div');
errorInfo.className = 'redirect-error';
errorInfo.textContent = `Error: ${redirect.error}`;
detailsContent.appendChild(errorInfo);
}
detailsContent.appendChild(tabsContainer);
detailsContainer.appendChild(toggleButton);
detailsContainer.appendChild(detailsContent);
li.appendChild(detailsContainer);
redirectList.appendChild(li);
});
showResults();
}
function addWarning(message) {
const warningDiv = document.createElement('div');
warningDiv.textContent = message;
warningContainer.appendChild(warningDiv);
}
function showWarnings() {
warningsDiv.classList.remove('hidden');
}
function hideWarnings() {
warningsDiv.classList.add('hidden');
warningContainer.innerHTML = '';
}
function showLoading() {
loading.classList.remove('hidden');
}
function hideLoading() {
loading.classList.add('hidden');
}
function showResults() {
results.classList.remove('hidden');
}
function hideResults() {
results.classList.add('hidden');
}
function showError(message) {
errorMessage.textContent = message;
errorDiv.classList.remove('hidden');
}
function hideError() {
errorDiv.classList.add('hidden');
}
// Add a helper function to extract tracking parameters from URL
function extractTrackingParams(url) {
try {
const parsedUrl = new URL(url);
const searchParams = parsedUrl.searchParams;
const trackingParams = {};
// Common tracking parameters to look for
const knownParams = [
// UTM parameters
{ key: 'utm_source', label: 'Source' },
{ key: 'utm_medium', label: 'Medium' },
{ key: 'utm_campaign', label: 'Campaign' },
{ key: 'utm_term', label: 'Term' },
{ key: 'utm_content', label: 'Content' },
// Facebook parameters
{ key: 'fbclid', label: 'Facebook Click ID' },
// Google parameters
{ key: 'gclid', label: 'Google Click ID' },
{ key: 'gclsrc', label: 'Google Click Source' },
// Other common parameters
{ key: 'source', label: 'Source' },
{ key: 'medium', label: 'Medium' },
{ key: 'campaign', label: 'Campaign' },
{ key: 'term', label: 'Term' },
{ key: 'content', label: 'Content' },
{ key: 'referrer', label: 'Referrer' },
{ key: 'ref', label: 'Referral' },
{ key: 'affiliate', label: 'Affiliate' },
{ key: 'channel', label: 'Channel' },
// Microsoft Advertising
{ key: 'msclkid', label: 'Microsoft Click ID' },
// TikTok
{ key: 'ttclid', label: 'TikTok Click ID' }
];
// Check for known parameters
knownParams.forEach(param => {
if (searchParams.has(param.key)) {
trackingParams[param.label] = searchParams.get(param.key);
}
});
// Also collect any other parameters that look like tracking params
for (const [key, value] of searchParams.entries()) {
if (!Object.values(trackingParams).includes(value)) {
if (
key.includes('utm_') ||
key.includes('ref') ||
key.includes('source') ||
key.includes('medium') ||
key.includes('campaign') ||
key.includes('clid') ||
key.includes('tracking') ||
key.includes('ad')
) {
trackingParams[key] = value;
}
}
}
return trackingParams;
} catch (e) {
console.error('Error parsing URL tracking params:', e);
return {};
}
}
function updateMermaidGraph(redirects) {
const mermaidGraph = document.getElementById('mermaidGraph');
if (!mermaidGraph) {
console.error('Mermaid graph container not found');
return;
}
try {
// Clear existing graph
mermaidGraph.innerHTML = '';
// Get the graph definition
const graphDefinition = generateMermaidGraph(redirects);
// Create a new div element for Mermaid
const newDiv = document.createElement('div');
newDiv.className = 'mermaid';
newDiv.textContent = graphDefinition;
// Add the div to the container
mermaidGraph.appendChild(newDiv);
// Render the graph
renderMermaidGraph();
} catch (e) {
console.error('Error updating Mermaid graph:', e);
displayGraphError(e.message);
}
}
function displayGraphError(message) {
const mermaidGraph = document.getElementById('mermaidGraph');
if (mermaidGraph) {
mermaidGraph.innerHTML = `<div class="error-message">Graph error: ${sanitizeMermaidText(message)}</div>`;
}
}
});

9
public/sitemap.xml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://urltrackertool.com/</loc>
<lastmod>2023-11-15</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
</urlset>

670
public/styles.css Normal file
View File

@@ -0,0 +1,670 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg-color: #f5f5f5;
--container-bg: #fff;
--text-color: #333;
--heading-color: #2d3748;
--border-color: #e2e8f0;
--shadow-color: rgba(0, 0, 0, 0.1);
--primary-color: #4299e1;
--primary-hover: #3182ce;
--secondary-bg: #f8fafc;
--input-border: #ddd;
--error-color: #e53e3e;
--success-color: #48bb78;
--warning-color: #ed8936;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--bg-color);
transition: background-color 0.3s, color 0.3s;
}
.container {
max-width: 900px;
margin: 2rem auto;
padding: 2rem;
background-color: var(--container-bg);
border-radius: 8px;
box-shadow: 0 2px 10px var(--shadow-color);
transition: background-color 0.3s, box-shadow 0.3s;
position: relative;
}
h1 {
text-align: center;
margin-bottom: 2rem;
color: var(--heading-color);
}
h2 {
margin-top: 1.5rem;
margin-bottom: 1rem;
color: var(--heading-color);
}
.input-group {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 1rem;
}
.form-controls {
display: flex;
gap: 10px;
}
.select-container {
display: flex;
align-items: center;
gap: 5px;
}
.user-agent-container {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 10px;
}
input[type="url"] {
flex: 1;
padding: 10px 15px;
border: 1px solid var(--input-border);
border-radius: 4px;
font-size: 16px;
background-color: var(--container-bg);
color: var(--text-color);
}
select {
padding: 8px 12px;
border: 1px solid var(--input-border);
border-radius: 4px;
font-size: 14px;
background-color: var(--container-bg);
color: var(--text-color);
}
button {
padding: 10px 20px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
button:hover {
background-color: var(--primary-hover);
}
.toggle-mode-btn, .print-btn {
font-size: 0.85rem;
padding: 8px 12px;
background-color: var(--secondary-bg);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.toggle-mode-btn:hover, .print-btn:hover {
background-color: var(--primary-color);
color: white;
}
/* Theme toggle button styles */
.theme-toggle-button {
background: transparent;
border: none;
cursor: pointer;
padding: 8px;
display: none;
align-items: center;
justify-content: center;
border-radius: 50%;
color: var(--text-color);
transition: background-color 0.3s;
position: absolute;
top: 2rem;
right: 2rem;
}
.theme-toggle-button:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.theme-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
/* In light mode, show the moon icon */
.theme-icon.sun-icon {
display: none;
}
.theme-icon.moon-icon {
display: flex;
}
/* In dark mode, show the sun icon */
body.dark-mode .theme-icon.sun-icon {
display: flex;
}
body.dark-mode .theme-icon.moon-icon {
display: none;
}
.hidden {
display: none;
}
.results-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 1rem;
}
.summary-container {
padding: 10px;
background-color: var(--secondary-bg);
border-radius: 4px;
font-size: 0.9rem;
border: 1px solid var(--border-color);
}
.results-container {
margin-top: 1rem;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 1rem;
background-color: var(--secondary-bg);
}
#redirectList {
list-style-position: inside;
padding-left: 0.5rem;
}
.redirect-item {
padding: 15px;
margin-bottom: 15px;
border-radius: 6px;
background-color: var(--container-bg);
border-left: 4px solid var(--primary-color);
box-shadow: 0 1px 3px var(--shadow-color);
}
.redirect-item.non-ssl {
border-left-color: var(--error-color);
}
.redirect-item.final {
border-left-color: var(--success-color);
}
.redirect-item-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
align-items: center;
}
.step-label {
font-weight: 600;
color: var(--heading-color);
font-size: 1.05rem;
}
.redirect-url {
flex: 1;
word-break: break-all;
margin-right: 10px;
}
.redirect-duration {
font-weight: bold;
color: var(--heading-color);
padding: 3px 8px;
background-color: var(--border-color);
border-radius: 4px;
font-size: 0.9rem;
}
.error-message {
color: var(--error-color);
text-align: center;
font-weight: bold;
}
.redirect-error {
padding: 10px;
margin: 10px 0;
background-color: #FEF2F2;
border-left: 4px solid var(--error-color);
color: #b91c1c;
border-radius: 4px;
}
.warning-container {
padding: 10px;
margin: 10px 0;
background-color: #FEFCBF;
border-left: 4px solid var(--warning-color);
color: #975a16;
border-radius: 4px;
}
/* Spinner styling */
.spinner {
width: 40px;
height: 40px;
margin: 0 auto;
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#loading {
text-align: center;
margin: 2rem 0;
}
/* Details section styling */
.details-container {
margin-top: 10px;
}
.toggle-details-btn {
display: block;
width: 100%;
text-align: center;
font-size: 0.9rem;
padding: 8px;
background-color: var(--secondary-bg);
color: var(--heading-color);
border-radius: 4px;
transition: background-color 0.2s;
}
.toggle-details-btn:hover {
background-color: var(--border-color);
}
.details-content {
margin-top: 10px;
padding: 15px;
border-radius: 6px;
background-color: var(--secondary-bg);
border: 1px solid var(--border-color);
}
/* Tab styling */
.tabs-container {
margin-top: 10px;
}
.tab-buttons {
display: flex;
flex-wrap: wrap;
border-bottom: 1px solid var(--border-color);
margin-bottom: 15px;
}
.tab-button {
background-color: transparent;
color: var(--heading-color);
padding: 8px 15px;
border: none;
border-bottom: 2px solid transparent;
margin-right: 5px;
transition: all 0.3s;
cursor: pointer;
font-size: 0.9rem;
}
.tab-button:hover {
background-color: var(--secondary-bg);
color: var(--primary-color);
}
.tab-button.active {
color: var(--primary-color);
border-bottom: 2px solid var(--primary-color);
font-weight: 600;
}
.tab-content {
padding: 10px;
background-color: var(--container-bg);
border-radius: 4px;
border: 1px solid var(--border-color);
max-height: 300px;
overflow-y: auto;
}
/* Main tabs for list/graph view */
.tabs-main {
display: flex;
border-bottom: 1px solid var(--border-color);
margin-bottom: 15px;
}
.tab-main {
background-color: transparent;
color: var(--heading-color);
padding: 10px 20px;
border: none;
border-bottom: 2px solid transparent;
margin-right: 5px;
transition: all 0.3s;
cursor: pointer;
}
.tab-main:hover {
background-color: var(--secondary-bg);
}
.tab-main.active {
color: var(--primary-color);
border-bottom: 2px solid var(--primary-color);
font-weight: 600;
}
.tab-main-content {
display: block;
}
.tab-main-content.hidden {
display: none;
}
/* Graph styling */
.graph-container {
min-height: 400px;
padding: 20px;
background-color: var(--container-bg);
border-radius: 6px;
border: 1px solid var(--border-color);
margin-top: 10px;
overflow-x: auto;
}
/* Pre-formatted content styling */
pre {
white-space: pre-wrap;
word-wrap: break-word;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 0.85rem;
line-height: 1.5;
padding: 10px;
background-color: var(--secondary-bg);
border-radius: 4px;
color: var(--heading-color);
overflow-x: auto;
}
.headers-content,
.body-content,
.metadata-content,
.ssl-content {
max-height: 250px;
overflow-y: auto;
}
/* Dark mode */
body.dark-mode {
--bg-color: #121212;
--container-bg: #1e1e1e;
--text-color: #f5f5f5;
--heading-color: #e2e8f0;
--border-color: #2d3748;
--shadow-color: rgba(0, 0, 0, 0.4);
--secondary-bg: #2a2a2a;
--input-border: #4a5568;
}
body.dark-mode .spinner {
border-color: rgba(255, 255, 255, 0.1);
border-left-color: var(--primary-color);
}
body.dark-mode pre {
background-color: #2a2a2a;
color: #e2e8f0;
}
/* Print mode */
@media print {
body {
background-color: white;
color: black;
}
.container {
box-shadow: none;
margin: 0;
padding: 0;
}
.no-print {
display: none !important;
}
.redirect-item {
break-inside: avoid;
page-break-inside: avoid;
}
.toggle-details-btn {
display: none;
}
.details-content.hidden {
display: block !important;
}
.graph-container {
height: 100vh;
width: 100%;
}
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.container {
padding: 1rem;
margin: 1rem;
}
.redirect-item-header {
flex-direction: column;
align-items: flex-start;
}
.redirect-duration {
margin-top: 5px;
}
.tab-buttons {
overflow-x: auto;
white-space: nowrap;
display: flex;
flex-wrap: nowrap;
}
.tab-button {
flex: 1;
min-width: auto;
}
.input-group {
flex-direction: column;
}
.form-controls {
flex-direction: column;
}
input[type="url"], button, select {
width: 100%;
}
.results-header {
flex-direction: column;
align-items: flex-start;
}
.summary-container {
width: 100%;
}
}
.header-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.header-container h1 {
margin-bottom: 0;
width: 100%;
text-align: center;
}
.header-container p {
flex: 1;
width: 100%;
text-align: center;
margin-bottom: 1rem;
color: var(--text-color);
}
.header-container .toggle-mode-btn {
padding: 8px 12px;
font-size: 0.9rem;
background-color: var(--secondary-bg);
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.header-container .toggle-mode-btn:hover {
background-color: var(--primary-color);
color: white;
}
.mermaid-error {
padding: 15px;
margin: 10px 0;
background-color: #FEF2F2;
border-left: 4px solid var(--error-color);
color: #b91c1c;
border-radius: 4px;
font-weight: bold;
}
/* URL container with copy button */
.url-container {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.redirect-url {
flex: 1;
word-break: break-all;
margin-right: 10px;
}
.copy-url-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
min-width: 32px;
padding: 0;
background-color: var(--secondary-bg);
color: var(--heading-color);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.copy-url-btn:hover {
background-color: var(--primary-color);
color: white;
}
.copy-url-btn.copied {
background-color: var(--success-color);
color: white;
}
/* Tracking parameters table */
.tracking-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
font-size: 0.9rem;
}
.tracking-table th,
.tracking-table td {
padding: 8px 12px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.tracking-table th {
background-color: var(--secondary-bg);
font-weight: 600;
color: var(--heading-color);
}
.param-name {
font-weight: 600;
white-space: nowrap;
color: var(--heading-color);
width: 30%;
}
.param-value {
word-break: break-all;
}