|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let servicesData = null;
|
|
|
let currentFilter = 'all';
|
|
|
let currentMethod = 'GET';
|
|
|
|
|
|
|
|
|
const svgIcons = {
|
|
|
chain: '<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>',
|
|
|
chart: '<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><line x1="12" y1="20" x2="12" y2="10"></line><line x1="18" y1="20" x2="18" y2="4"></line><line x1="6" y1="20" x2="6" y2="16"></line></svg>',
|
|
|
news: '<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"></path><path d="M18 14h-8"></path><path d="M15 18h-5"></path><path d="M10 6h8v4h-8V6Z"></path></svg>',
|
|
|
brain: '<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2Z"></path><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2Z"></path></svg>',
|
|
|
analytics: '<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M3 3v18h18"></path><path d="m19 9-5 5-4-4-3 3"></path></svg>'
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchServices() {
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/crypto-hub/services');
|
|
|
if (!response.ok) {
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
}
|
|
|
servicesData = await response.json();
|
|
|
return servicesData;
|
|
|
} catch (error) {
|
|
|
console.error('Error fetching services:', error);
|
|
|
showToast('❌', 'Failed to load services');
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function fetchStatistics() {
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/crypto-hub/stats');
|
|
|
if (!response.ok) {
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
}
|
|
|
return await response.json();
|
|
|
} catch (error) {
|
|
|
console.error('Error fetching statistics:', error);
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function testAPIEndpoint(url, method = 'GET', headers = null, body = null) {
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/crypto-hub/test', {
|
|
|
method: 'POST',
|
|
|
headers: {
|
|
|
'Content-Type': 'application/json'
|
|
|
},
|
|
|
body: JSON.stringify({
|
|
|
url: url,
|
|
|
method: method,
|
|
|
headers: headers,
|
|
|
body: body
|
|
|
})
|
|
|
});
|
|
|
|
|
|
if (!response.ok) {
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
}
|
|
|
|
|
|
return await response.json();
|
|
|
} catch (error) {
|
|
|
console.error('Error testing API:', error);
|
|
|
return {
|
|
|
success: false,
|
|
|
status_code: 0,
|
|
|
data: null,
|
|
|
error: error.message
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getIcon(category) {
|
|
|
|
|
|
const icons = {
|
|
|
explorer: svgIcons.chain,
|
|
|
market: svgIcons.chart,
|
|
|
news: svgIcons.news,
|
|
|
sentiment: svgIcons.brain,
|
|
|
analytics: svgIcons.analytics
|
|
|
};
|
|
|
return icons[category] || svgIcons.chain;
|
|
|
}
|
|
|
|
|
|
function renderServices() {
|
|
|
|
|
|
if (!servicesData) {
|
|
|
console.error('No services data available');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const grid = document.getElementById('servicesGrid');
|
|
|
if (!grid) {
|
|
|
console.error('Services grid element not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
let html = '';
|
|
|
const categories = servicesData.categories || {};
|
|
|
|
|
|
Object.entries(categories).forEach(([categoryId, categoryData]) => {
|
|
|
const services = categoryData.services || [];
|
|
|
|
|
|
services.forEach(service => {
|
|
|
|
|
|
if (currentFilter !== 'all' && categoryId !== currentFilter) return;
|
|
|
|
|
|
const hasKey = service.key ? `<span class="badge badge-key">🔑 Has Key</span>` : '';
|
|
|
const endpoints = service.endpoints || [];
|
|
|
const endpointsCount = endpoints.length;
|
|
|
|
|
|
html += `
|
|
|
<div class="service-card" data-category="${categoryId}" data-name="${service.name.toLowerCase()}">
|
|
|
<div class="service-header">
|
|
|
<div class="service-icon">${getIcon(categoryId)}</div>
|
|
|
<div class="service-info">
|
|
|
<div class="service-name">${escapeHtml(service.name)}</div>
|
|
|
<div class="service-url">${escapeHtml(service.url)}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="service-badges">
|
|
|
<span class="badge badge-category">${categoryId}</span>
|
|
|
${endpointsCount > 0 ? `<span class="badge badge-endpoints">${endpointsCount} endpoints</span>` : ''}
|
|
|
${hasKey}
|
|
|
</div>
|
|
|
${endpointsCount > 0 ? renderEndpoints(service, endpoints) : renderBaseEndpoint()}
|
|
|
</div>
|
|
|
`;
|
|
|
});
|
|
|
});
|
|
|
|
|
|
grid.innerHTML = html || '<div style="grid-column: 1/-1; text-align: center; padding: 4rem; color: var(--text-secondary);">No services found</div>';
|
|
|
}
|
|
|
|
|
|
function renderEndpoints(service, endpoints) {
|
|
|
|
|
|
const displayEndpoints = endpoints.slice(0, 2);
|
|
|
const remaining = endpoints.length - 2;
|
|
|
|
|
|
let html = '<div class="endpoints-list">';
|
|
|
|
|
|
displayEndpoints.forEach(endpoint => {
|
|
|
const endpointPath = endpoint.path || endpoint;
|
|
|
const fullUrl = service.url + endpointPath;
|
|
|
const description = endpoint.description || '';
|
|
|
|
|
|
html += `
|
|
|
<div class="endpoint-item">
|
|
|
<div class="endpoint-path" title="${escapeHtml(description)}">
|
|
|
${escapeHtml(endpointPath)}
|
|
|
</div>
|
|
|
<div class="endpoint-actions">
|
|
|
<button class="btn-sm" onclick='copyText("${escapeHtml(fullUrl, true)}")'>
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
|
</svg>
|
|
|
Copy
|
|
|
</button>
|
|
|
<button class="btn-sm" onclick='testEndpoint("${escapeHtml(fullUrl, true)}", "${escapeHtml(service.key || '', true)}")'>
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
|
|
|
</svg>
|
|
|
Test
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
});
|
|
|
|
|
|
if (remaining > 0) {
|
|
|
html += `<div style="text-align: center; color: var(--text-secondary); margin-top: 0.75rem; font-size: 0.875rem;">+${remaining} more endpoints</div>`;
|
|
|
}
|
|
|
|
|
|
html += '</div>';
|
|
|
return html;
|
|
|
}
|
|
|
|
|
|
function renderBaseEndpoint() {
|
|
|
|
|
|
return '<div style="color: var(--text-secondary); font-size: 0.875rem;">Base endpoint available</div>';
|
|
|
}
|
|
|
|
|
|
async function updateStatistics() {
|
|
|
|
|
|
const stats = await fetchStatistics();
|
|
|
if (!stats) return;
|
|
|
|
|
|
|
|
|
const statsElements = {
|
|
|
services: document.querySelector('.stat-value:nth-child(1)'),
|
|
|
endpoints: document.querySelector('.stat-value:nth-child(2)'),
|
|
|
keys: document.querySelector('.stat-value:nth-child(3)')
|
|
|
};
|
|
|
|
|
|
if (statsElements.services) {
|
|
|
document.querySelectorAll('.stat-value')[0].textContent = stats.total_services || 0;
|
|
|
}
|
|
|
if (statsElements.endpoints) {
|
|
|
document.querySelectorAll('.stat-value')[1].textContent = (stats.total_endpoints || 0) + '+';
|
|
|
}
|
|
|
if (statsElements.keys) {
|
|
|
document.querySelectorAll('.stat-value')[2].textContent = stats.api_keys_count || 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function setFilter(filter) {
|
|
|
|
|
|
currentFilter = filter;
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.filter-tab').forEach(tab => {
|
|
|
tab.classList.remove('active');
|
|
|
});
|
|
|
event.target.classList.add('active');
|
|
|
|
|
|
|
|
|
renderServices();
|
|
|
}
|
|
|
|
|
|
function filterServices() {
|
|
|
|
|
|
const search = document.getElementById('searchInput');
|
|
|
if (!search) return;
|
|
|
|
|
|
const searchTerm = search.value.toLowerCase();
|
|
|
const cards = document.querySelectorAll('.service-card');
|
|
|
|
|
|
cards.forEach(card => {
|
|
|
const text = card.textContent.toLowerCase();
|
|
|
card.style.display = text.includes(searchTerm) ? 'block' : 'none';
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function testEndpoint(url, key) {
|
|
|
|
|
|
openTester();
|
|
|
|
|
|
|
|
|
let finalUrl = url;
|
|
|
if (key) {
|
|
|
finalUrl = url.replace(/{KEY}/gi, key).replace(/{key}/gi, key);
|
|
|
}
|
|
|
|
|
|
const urlInput = document.getElementById('testUrl');
|
|
|
if (urlInput) {
|
|
|
urlInput.value = finalUrl;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function openTester() {
|
|
|
|
|
|
const modal = document.getElementById('testerModal');
|
|
|
if (modal) {
|
|
|
modal.classList.add('active');
|
|
|
|
|
|
setTimeout(() => {
|
|
|
const urlInput = document.getElementById('testUrl');
|
|
|
if (urlInput) urlInput.focus();
|
|
|
}, 100);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function closeTester() {
|
|
|
|
|
|
const modal = document.getElementById('testerModal');
|
|
|
if (modal) {
|
|
|
modal.classList.remove('active');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function setMethod(method, btn) {
|
|
|
|
|
|
currentMethod = method;
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.method-btn').forEach(b => {
|
|
|
b.classList.remove('active');
|
|
|
});
|
|
|
btn.classList.add('active');
|
|
|
|
|
|
|
|
|
const bodyGroup = document.getElementById('bodyGroup');
|
|
|
if (bodyGroup) {
|
|
|
bodyGroup.style.display = (method === 'POST' || method === 'PUT') ? 'block' : 'none';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function sendRequest() {
|
|
|
|
|
|
const urlInput = document.getElementById('testUrl');
|
|
|
const headersInput = document.getElementById('testHeaders');
|
|
|
const bodyInput = document.getElementById('testBody');
|
|
|
const responseBox = document.getElementById('responseBox');
|
|
|
const responseJson = document.getElementById('responseJson');
|
|
|
|
|
|
if (!urlInput || !responseBox || !responseJson) {
|
|
|
console.error('Required elements not found');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const url = urlInput.value.trim();
|
|
|
if (!url) {
|
|
|
showToast('⚠️', 'Please enter a URL');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
|
|
|
responseBox.style.display = 'block';
|
|
|
responseJson.textContent = '⏳ Sending request...';
|
|
|
|
|
|
try {
|
|
|
|
|
|
let headers = null;
|
|
|
if (headersInput && headersInput.value.trim()) {
|
|
|
try {
|
|
|
headers = JSON.parse(headersInput.value);
|
|
|
} catch (e) {
|
|
|
showToast('⚠️', 'Invalid JSON in headers');
|
|
|
responseJson.textContent = '❌ Error: Invalid headers JSON format';
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
let body = null;
|
|
|
if ((currentMethod === 'POST' || currentMethod === 'PUT') && bodyInput) {
|
|
|
body = bodyInput.value.trim();
|
|
|
}
|
|
|
|
|
|
|
|
|
const result = await testAPIEndpoint(url, currentMethod, headers, body);
|
|
|
|
|
|
if (result.success) {
|
|
|
responseJson.textContent = JSON.stringify(result.data, null, 2);
|
|
|
showToast('✅', `Success! Status: ${result.status_code}`);
|
|
|
} else {
|
|
|
responseJson.textContent = `❌ Error: ${result.error || 'Request failed'}\n\nStatus Code: ${result.status_code || 'N/A'}\n\nThis might be due to CORS policy, invalid API key, or network issues.`;
|
|
|
showToast('❌', 'Request failed');
|
|
|
}
|
|
|
} catch (error) {
|
|
|
responseJson.textContent = `❌ Error: ${error.message}`;
|
|
|
showToast('❌', 'Request failed');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function copyText(text) {
|
|
|
|
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
|
showToast('✅', 'Copied to clipboard!');
|
|
|
}).catch(() => {
|
|
|
showToast('❌', 'Failed to copy');
|
|
|
});
|
|
|
}
|
|
|
|
|
|
function exportJSON() {
|
|
|
|
|
|
if (!servicesData) {
|
|
|
showToast('⚠️', 'No data to export');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const data = {
|
|
|
exported_at: new Date().toISOString(),
|
|
|
...servicesData
|
|
|
};
|
|
|
|
|
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
const a = document.createElement('a');
|
|
|
a.href = url;
|
|
|
a.download = `crypto-api-hub-export-${Date.now()}.json`;
|
|
|
document.body.appendChild(a);
|
|
|
a.click();
|
|
|
document.body.removeChild(a);
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
showToast('✅', 'JSON exported successfully!');
|
|
|
}
|
|
|
|
|
|
function showToast(icon, message) {
|
|
|
|
|
|
const toast = document.getElementById('toast');
|
|
|
const toastIcon = document.getElementById('toastIcon');
|
|
|
const toastMessage = document.getElementById('toastMessage');
|
|
|
|
|
|
if (toast && toastIcon && toastMessage) {
|
|
|
toastIcon.textContent = icon;
|
|
|
toastMessage.textContent = message;
|
|
|
toast.classList.add('show');
|
|
|
setTimeout(() => toast.classList.remove('show'), 3000);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function escapeHtml(text, forAttribute = false) {
|
|
|
|
|
|
if (!text) return '';
|
|
|
|
|
|
const map = {
|
|
|
'&': '&',
|
|
|
'<': '<',
|
|
|
'>': '>',
|
|
|
'"': '"',
|
|
|
"'": '''
|
|
|
};
|
|
|
|
|
|
const escaped = String(text).replace(/[&<>"']/g, m => map[m]);
|
|
|
|
|
|
|
|
|
if (forAttribute) {
|
|
|
return escaped.replace(/"/g, '"');
|
|
|
}
|
|
|
|
|
|
return escaped;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function initializeDashboard() {
|
|
|
|
|
|
console.log('Initializing Crypto API Hub Dashboard...');
|
|
|
|
|
|
|
|
|
const data = await fetchServices();
|
|
|
if (!data) {
|
|
|
console.error('Failed to load services data');
|
|
|
showErrorState();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
|
|
|
renderServices();
|
|
|
|
|
|
|
|
|
await updateStatistics();
|
|
|
|
|
|
console.log('Dashboard initialized successfully!');
|
|
|
}
|
|
|
|
|
|
function showErrorState() {
|
|
|
|
|
|
const grid = document.getElementById('servicesGrid');
|
|
|
if (!grid) return;
|
|
|
|
|
|
grid.innerHTML = `
|
|
|
<div class="error-state" style="grid-column: 1/-1;">
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
|
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
|
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
|
</svg>
|
|
|
<h3>Failed to Load Services</h3>
|
|
|
<p>We couldn't load the API services. Please check your connection and try again.</p>
|
|
|
<button class="retry-btn" onclick="location.reload()">
|
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: inline; margin-right: 8px;">
|
|
|
<polyline points="23 4 23 10 17 10"></polyline>
|
|
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
|
|
</svg>
|
|
|
Retry
|
|
|
</button>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
|
|
|
if (document.readyState === 'loading') {
|
|
|
document.addEventListener('DOMContentLoaded', initializeDashboard);
|
|
|
} else {
|
|
|
initializeDashboard();
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
|
if (e.key === 'Escape') {
|
|
|
const modal = document.getElementById('testerModal');
|
|
|
if (modal && modal.classList.contains('active')) {
|
|
|
closeTester();
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
document.addEventListener('click', (e) => {
|
|
|
const modal = document.getElementById('testerModal');
|
|
|
if (modal && e.target === modal) {
|
|
|
closeTester();
|
|
|
}
|
|
|
});
|
|
|
|