mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-05-03 00:00:54 +00:00
8379fdb1f8
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
506 lines
24 KiB
HTML
506 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>API Documentation</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet">
|
|
<style>
|
|
body { background-color: #f8f9fa; }
|
|
.sidebar { position: sticky; top: 0; height: 100vh; overflow-y: auto; background: white; box-shadow: 2px 0 5px rgba(0,0,0,0.1); }
|
|
.content { padding: 20px; }
|
|
.endpoint { margin-bottom: 40px; padding: 20px; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
.method { font-weight: bold; text-transform: uppercase; padding: 4px 8px; border-radius: 4px; }
|
|
.method.get { background: #d4edda; color: #155724; }
|
|
.method.post { background: #cce5ff; color: #004085; }
|
|
.method.put { background: #fff3cd; color: #856404; }
|
|
.method.delete { background: #f8d7da; color: #721c24; }
|
|
.param-table { font-size: 0.9em; }
|
|
.optional { color: #6c757d; font-style: italic; }
|
|
.example { background: #f8f9fa; border-left: 4px solid #007bff; }
|
|
pre { font-size: 0.85em; }
|
|
.copy-btn { opacity: 0.7; transition: opacity 0.2s ease; }
|
|
.copy-btn:hover { opacity: 1; }
|
|
.example:hover .copy-btn { opacity: 1; }
|
|
.nav-link.active { background-color: #007bff; color: white; font-weight: bold; }
|
|
.nav-link { transition: all 0.2s ease; }
|
|
.nav-link:hover:not(.active) { background-color: #e3f2fd; color: #0056b3; }
|
|
.group-header.active { font-weight: bold; color: #007bff; }
|
|
|
|
/* Custom scrollbar styling */
|
|
.sidebar::-webkit-scrollbar { width: 8px; }
|
|
.sidebar::-webkit-scrollbar-track { background: #f8f9fa; border-radius: 4px; }
|
|
.sidebar::-webkit-scrollbar-thumb { background: #dee2e6; border-radius: 4px; }
|
|
.sidebar::-webkit-scrollbar-thumb:hover { background: #adb5bd; }
|
|
|
|
/* Firefox scrollbar */
|
|
.sidebar { scrollbar-width: thin; scrollbar-color: #dee2e6 #f8f9fa; }
|
|
|
|
/* Mobile styles - disable sticky sidebar */
|
|
@media (max-width: 800px) {
|
|
.sidebar {
|
|
position: static !important;
|
|
height: auto !important;
|
|
overflow-y: visible !important;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<!-- Sidebar -->
|
|
<div class="col-md-3 sidebar">
|
|
<div class="p-3">
|
|
<a href="#introduction" class="text-decoration-none">
|
|
{{ sidebar_header_content|safe }}
|
|
</a>
|
|
<hr>
|
|
|
|
{% if introduction_content %}
|
|
<div class="mb-3">
|
|
<a href="#introduction" class="text-decoration-none">
|
|
<h6 class="text-muted">Introduction</h6>
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% for group, endpoints in grouped_endpoints.items() %}
|
|
<div class="mb-3">
|
|
<h6 class="text-muted group-header" data-group="{{ group }}">{{ group }}</h6>
|
|
{% for endpoint in endpoints %}
|
|
<div class="ms-2 mb-1">
|
|
<a href="#{{ group|replace(' ', '_')|replace('/', '')|replace('-', '')|lower }}_{{ endpoint.method|upper }}" class="nav-link py-1 px-2 rounded" data-endpoint="{{ group|replace(' ', '_')|replace('/', '')|replace('-', '')|lower }}_{{ endpoint.method|upper }}">
|
|
<span class="method {{ endpoint.method }}">{{ endpoint.method }}</span>
|
|
{{ endpoint.title or endpoint.name or endpoint.description }}
|
|
</a>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main content -->
|
|
<div class="col-md-9 content">
|
|
{% if introduction_content %}
|
|
<div id="introduction" class="mb-5">
|
|
{{ introduction_content|safe }}
|
|
</div>
|
|
{% endif %}
|
|
{% for group, endpoints in grouped_endpoints.items() %}
|
|
<h2 class="text-primary mb-4" id="group-{{ group|replace(' ', '_')|lower }}">{{ group }}</h2>
|
|
|
|
{% for endpoint in endpoints %}
|
|
<div class="endpoint" id="{{ group|replace(' ', '_')|replace('/', '')|replace('-', '')|lower }}_{{ endpoint.method|upper }}" data-group="{{ group }}">
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<h4>
|
|
<span class="method {{ endpoint.method }}">{{ endpoint.method }}</span>
|
|
<code>{{ endpoint.url|e }}</code>
|
|
</h4>
|
|
<h5 class="text-muted">{{ endpoint.name or endpoint.title }}</h5>
|
|
{% if endpoint.description %}
|
|
<p class="mt-3">{{ endpoint.description|safe }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if endpoint.params %}
|
|
<h6 class="mt-4">Parameters</h6>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm param-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Type</th>
|
|
<th>Required</th>
|
|
<th>Description</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for param in endpoint.params %}
|
|
<tr>
|
|
<td><code>{{ param.name }}</code></td>
|
|
<td><span class="badge bg-secondary">{{ param.type }}</span></td>
|
|
<td>{% if param.optional %}<span class="optional">Optional</span>{% else %}<span class="text-danger">Required</span>{% endif %}</td>
|
|
<td>{{ param.description }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if endpoint.query %}
|
|
<h6 class="mt-4">Query Parameters</h6>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm param-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Type</th>
|
|
<th>Required</th>
|
|
<th>Description</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for param in endpoint.query %}
|
|
<tr>
|
|
<td><code>{{ param.name }}</code></td>
|
|
<td><span class="badge bg-info">{{ param.type }}</span></td>
|
|
<td>{% if param.optional %}<span class="optional">Optional</span>{% else %}<span class="text-danger">Required</span>{% endif %}</td>
|
|
<td>{{ param.description }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if endpoint.success %}
|
|
<h6 class="mt-4">Success Responses</h6>
|
|
{% for success in endpoint.success %}
|
|
<div class="mb-2">
|
|
<span class="badge bg-success">{{ success.status }}</span>
|
|
<span class="badge bg-secondary ms-1">{{ success.type }}</span>
|
|
<strong class="ms-2">{{ success.name }}</strong>
|
|
<span class="ms-2 text-muted">{{ success.description }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
|
|
{% if endpoint.error %}
|
|
<h6 class="mt-4">Error Responses</h6>
|
|
{% for error in endpoint.error %}
|
|
<div class="mb-2">
|
|
<span class="badge bg-danger">{{ error.status }}</span>
|
|
<span class="badge bg-secondary ms-1">{{ error.type }}</span>
|
|
<span class="ms-2 text-muted">{{ error.description }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
|
|
{% if endpoint.example or endpoint.example_request or endpoint.example_response %}
|
|
<h6 class="mt-4">Example</h6>
|
|
|
|
{% if endpoint.example_request %}
|
|
<h7 class="mt-3 mb-2 text-muted">Request</h7>
|
|
<div class="example p-3 rounded position-relative mb-3">
|
|
<button class="btn btn-outline-secondary btn-sm position-absolute top-0 end-0 m-2 copy-btn"
|
|
data-bs-toggle="tooltip"
|
|
data-bs-placement="left"
|
|
title="Copy to clipboard"
|
|
onclick="copyToClipboard(this)">
|
|
<i class="bi bi-clipboard" aria-hidden="true"></i>
|
|
</button>
|
|
<pre><code class="language-bash">{{ endpoint.example_request }}</code></pre>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if endpoint.example_response %}
|
|
<h7 class="mt-3 mb-2 text-muted">Response</h7>
|
|
<div class="example p-3 rounded position-relative mb-3">
|
|
<button class="btn btn-outline-secondary btn-sm position-absolute top-0 end-0 m-2 copy-btn"
|
|
data-bs-toggle="tooltip"
|
|
data-bs-placement="left"
|
|
title="Copy to clipboard"
|
|
onclick="copyToClipboard(this)">
|
|
<i class="bi bi-clipboard" aria-hidden="true"></i>
|
|
</button>
|
|
<pre><code class="language-json">{{ endpoint.example_response }}</code></pre>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if endpoint.example and not endpoint.example_request and not endpoint.example_response %}
|
|
<div class="example p-3 rounded position-relative">
|
|
<button class="btn btn-outline-secondary btn-sm position-absolute top-0 end-0 m-2 copy-btn"
|
|
data-bs-toggle="tooltip"
|
|
data-bs-placement="left"
|
|
title="Copy to clipboard"
|
|
onclick="copyToClipboard(this)">
|
|
<i class="bi bi-clipboard" aria-hidden="true"></i>
|
|
</button>
|
|
<pre><code class="language-bash">{{ endpoint.example }}</code></pre>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
let isScrolling = false;
|
|
let isNavigating = false;
|
|
|
|
// Check if we should disable scroll handling on mobile
|
|
function isMobileWidth() {
|
|
return window.innerWidth < 800;
|
|
}
|
|
|
|
// Debounced scroll handler
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
// Function to scroll sidebar link into view if needed
|
|
function scrollIntoViewIfNeeded(element) {
|
|
if (!element) return;
|
|
|
|
const sidebar = $('.sidebar')[0];
|
|
const rect = element.getBoundingClientRect();
|
|
const sidebarRect = sidebar.getBoundingClientRect();
|
|
|
|
// Check if element is outside the sidebar viewport
|
|
const isAboveView = rect.top < sidebarRect.top;
|
|
const isBelowView = rect.bottom > sidebarRect.bottom;
|
|
|
|
if (isAboveView || isBelowView) {
|
|
element.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Intersection Observer for more efficient viewport detection
|
|
const observerOptions = {
|
|
root: null,
|
|
rootMargin: '-20% 0px -70% 0px', // Trigger when element is in top 30% of viewport
|
|
threshold: 0
|
|
};
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
// Don't update if user is actively navigating or on mobile
|
|
if (isNavigating || isMobileWidth()) return;
|
|
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const targetId = entry.target.id;
|
|
const targetGroup = entry.target.dataset.group;
|
|
|
|
// Update window location hash
|
|
if (window.location.hash !== '#' + targetId) {
|
|
history.replaceState(null, null, '#' + targetId);
|
|
}
|
|
|
|
// Remove all active states
|
|
$('.nav-link').removeClass('active');
|
|
$('.group-header').removeClass('active');
|
|
|
|
// Add active state to current item
|
|
const $activeLink = $(`.nav-link[data-endpoint="${targetId}"]`);
|
|
$activeLink.addClass('active');
|
|
$(`.group-header[data-group="${targetGroup}"]`).addClass('active');
|
|
|
|
// Handle introduction section
|
|
if (targetId === 'introduction') {
|
|
const $introLink = $('a[href="#introduction"]');
|
|
$introLink.addClass('active');
|
|
// Scroll intro link into view in sidebar
|
|
scrollIntoViewIfNeeded($introLink[0]);
|
|
} else {
|
|
// Scroll active link into view in sidebar
|
|
if ($activeLink.length) {
|
|
scrollIntoViewIfNeeded($activeLink[0]);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}, observerOptions);
|
|
|
|
// Observe all endpoints and introduction (only on desktop)
|
|
if (!isMobileWidth()) {
|
|
$('.endpoint').each(function() {
|
|
observer.observe(this);
|
|
});
|
|
|
|
if ($('#introduction').length) {
|
|
observer.observe($('#introduction')[0]);
|
|
}
|
|
}
|
|
|
|
// Smooth scrolling for navigation links
|
|
$('a[href^="#"]').on('click', function(e) {
|
|
e.preventDefault();
|
|
const targetHref = this.getAttribute('href');
|
|
const target = $(targetHref);
|
|
if (target.length) {
|
|
// Set navigation flag to prevent observer interference
|
|
isNavigating = true;
|
|
|
|
// Update window location hash immediately
|
|
history.pushState(null, null, targetHref);
|
|
|
|
$('html, body').animate({
|
|
scrollTop: target.offset().top - 20
|
|
}, 300, function() {
|
|
// Clear navigation flag after animation completes
|
|
setTimeout(() => {
|
|
isNavigating = false;
|
|
}, 100);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Fallback scroll handler with debouncing
|
|
const handleScroll = debounce(() => {
|
|
if (isScrolling || isNavigating || isMobileWidth()) return;
|
|
|
|
let current = '';
|
|
let currentGroup = '';
|
|
|
|
// Check which section is currently in view
|
|
$('.endpoint, #introduction').each(function() {
|
|
const element = $(this);
|
|
const elementTop = element.offset().top;
|
|
const elementBottom = elementTop + element.outerHeight();
|
|
const scrollTop = $(window).scrollTop() + 100; // Offset for better UX
|
|
|
|
if (scrollTop >= elementTop && scrollTop < elementBottom) {
|
|
current = this.id;
|
|
currentGroup = element.data('group');
|
|
return false; // Break loop
|
|
}
|
|
});
|
|
|
|
if (current) {
|
|
// Update window location hash
|
|
if (window.location.hash !== '#' + current) {
|
|
history.replaceState(null, null, '#' + current);
|
|
}
|
|
|
|
$('.nav-link').removeClass('active');
|
|
$('.group-header').removeClass('active');
|
|
|
|
const $activeLink = $(`.nav-link[data-endpoint="${current}"]`);
|
|
$activeLink.addClass('active');
|
|
if (currentGroup) {
|
|
$(`.group-header[data-group="${currentGroup}"]`).addClass('active');
|
|
}
|
|
|
|
if (current === 'introduction') {
|
|
const $introLink = $('a[href="#introduction"]');
|
|
$introLink.addClass('active');
|
|
scrollIntoViewIfNeeded($introLink[0]);
|
|
} else if ($activeLink.length) {
|
|
scrollIntoViewIfNeeded($activeLink[0]);
|
|
}
|
|
}
|
|
}, 50);
|
|
|
|
// Only bind scroll handler on desktop
|
|
if (!isMobileWidth()) {
|
|
$(window).on('scroll', handleScroll);
|
|
|
|
// Initial call
|
|
handleScroll();
|
|
}
|
|
|
|
// Initialize tooltips
|
|
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
|
const tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
|
});
|
|
});
|
|
|
|
// Copy to clipboard function
|
|
function copyToClipboard(button) {
|
|
const codeBlock = button.parentElement.querySelector('code');
|
|
const text = codeBlock.textContent;
|
|
|
|
// Use modern clipboard API
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
showCopyFeedback(button, true);
|
|
}).catch(() => {
|
|
fallbackCopyToClipboard(text, button);
|
|
});
|
|
} else {
|
|
fallbackCopyToClipboard(text, button);
|
|
}
|
|
}
|
|
|
|
// Fallback for older browsers
|
|
function fallbackCopyToClipboard(text, button) {
|
|
const textArea = document.createElement('textarea');
|
|
textArea.value = text;
|
|
textArea.style.position = 'fixed';
|
|
textArea.style.left = '-999999px';
|
|
textArea.style.top = '-999999px';
|
|
document.body.appendChild(textArea);
|
|
textArea.focus();
|
|
textArea.select();
|
|
|
|
try {
|
|
const successful = document.execCommand('copy');
|
|
showCopyFeedback(button, successful);
|
|
} catch (err) {
|
|
showCopyFeedback(button, false);
|
|
}
|
|
|
|
document.body.removeChild(textArea);
|
|
}
|
|
|
|
// Show copy feedback
|
|
function showCopyFeedback(button, success) {
|
|
const icon = button.querySelector('i');
|
|
const originalClass = icon.className;
|
|
const originalTitle = button.getAttribute('data-bs-original-title') || button.getAttribute('title');
|
|
|
|
if (success) {
|
|
icon.className = 'bi bi-check';
|
|
button.classList.remove('btn-outline-secondary');
|
|
button.classList.add('btn-success');
|
|
button.setAttribute('title', 'Copied!');
|
|
} else {
|
|
icon.className = 'bi bi-x';
|
|
button.classList.remove('btn-outline-secondary');
|
|
button.classList.add('btn-danger');
|
|
button.setAttribute('title', 'Failed to copy');
|
|
}
|
|
|
|
// Update tooltip
|
|
const tooltip = bootstrap.Tooltip.getInstance(button);
|
|
if (tooltip) {
|
|
tooltip.dispose();
|
|
new bootstrap.Tooltip(button);
|
|
}
|
|
|
|
// Reset after 2 seconds
|
|
setTimeout(() => {
|
|
icon.className = originalClass;
|
|
button.classList.remove('btn-success', 'btn-danger');
|
|
button.classList.add('btn-outline-secondary');
|
|
button.setAttribute('title', originalTitle);
|
|
|
|
// Update tooltip again
|
|
const tooltip = bootstrap.Tooltip.getInstance(button);
|
|
if (tooltip) {
|
|
tooltip.dispose();
|
|
new bootstrap.Tooltip(button);
|
|
}
|
|
}, 2000);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |