Initial commit: URL Redirect Tracker application with comprehensive documentation
This commit is contained in:
94
public/api-docs.html
Normal file
94
public/api-docs.html
Normal 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
168
public/api-readme.md
Normal 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
16
public/api.html
Normal 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
164
public/index.html
Normal 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
3
public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Sitemap: https://urltrackertool.com/sitemap.xml
|
||||
838
public/script.js
Normal file
838
public/script.js
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\(/g, '(')
|
||||
.replace(/\)/g, ')')
|
||||
.replace(/\[/g, '[')
|
||||
.replace(/\]/g, ']')
|
||||
.replace(/\{/g, '{')
|
||||
.replace(/\}/g, '}');
|
||||
}
|
||||
|
||||
// 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
9
public/sitemap.xml
Normal 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
670
public/styles.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user