2026-04-15-2 dashboard convert to html without vite and react
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Analytics Logic (Simple CSS Charts)
|
||||
*/
|
||||
|
||||
const analytics = {
|
||||
mockData: [
|
||||
{ name: 'Mon', requests: 4000, latency: 240 },
|
||||
{ name: 'Tue', requests: 3000, latency: 198 },
|
||||
{ name: 'Wed', requests: 2000, latency: 310 },
|
||||
{ name: 'Thu', requests: 2780, latency: 208 },
|
||||
{ name: 'Fri', requests: 1890, latency: 250 },
|
||||
{ name: 'Sat', requests: 2390, latency: 210 },
|
||||
{ name: 'Sun', requests: 3490, latency: 225 },
|
||||
],
|
||||
|
||||
init: () => {
|
||||
console.log('📈 Initializing Analytics...');
|
||||
analytics.renderVolumeChart();
|
||||
analytics.renderLatencyChart();
|
||||
},
|
||||
|
||||
renderVolumeChart: () => {
|
||||
const container = document.getElementById('v-chart');
|
||||
if (!container) return;
|
||||
|
||||
const max = Math.max(...analytics.mockData.map(d => d.requests));
|
||||
|
||||
container.innerHTML = analytics.mockData.map(d => `
|
||||
<div class="flex-1 flex flex-col items-center gap-2 group">
|
||||
<div class="w-full bg-blue-600/20 rounded-t-lg relative overflow-hidden flex items-end" style="height: 100%">
|
||||
<div class="w-full bg-gradient-to-t from-blue-600 to-blue-400 rounded-t-lg transition-all duration-700 hover:brightness-125"
|
||||
style="height: ${(d.requests / max) * 100}%">
|
||||
</div>
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span class="text-[10px] font-bold bg-slate-900 px-2 py-1 rounded border border-slate-800">${d.requests}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-[10px] font-bold text-slate-500">${d.name}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
},
|
||||
|
||||
renderLatencyChart: () => {
|
||||
const container = document.getElementById('l-chart');
|
||||
if (!container) return;
|
||||
|
||||
const max = Math.max(...analytics.mockData.map(d => d.latency));
|
||||
|
||||
container.innerHTML = analytics.mockData.map(d => `
|
||||
<div class="flex-1 flex flex-col items-center gap-2 group">
|
||||
<div class="w-full bg-violet-600/10 rounded-t-lg relative overflow-hidden flex items-end" style="height: 100%">
|
||||
<div class="w-full bg-gradient-to-t from-violet-600 to-violet-400 rounded-t-lg transition-all duration-700 hover:brightness-125"
|
||||
style="height: ${(d.latency / max) * 100}%">
|
||||
</div>
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<span class="text-[10px] font-bold bg-slate-900 px-2 py-1 rounded border border-slate-800">${d.latency}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-[10px] font-bold text-slate-500">${d.name}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Main App Logic for Intaleq Dashboard (Vanilla Version)
|
||||
*/
|
||||
|
||||
const app = {
|
||||
state: {
|
||||
tenant: null,
|
||||
keys: [],
|
||||
showKeys: {}, // { id: boolean }
|
||||
activePage: 'home'
|
||||
},
|
||||
|
||||
init: async () => {
|
||||
console.log('🚀 Dashboard Initializing...');
|
||||
app.bindEvents();
|
||||
app.handleRouting();
|
||||
await app.fetchData();
|
||||
lucide.createIcons();
|
||||
},
|
||||
|
||||
bindEvents: () => {
|
||||
window.addEventListener('hashchange', app.handleRouting);
|
||||
|
||||
// Form submission for new key
|
||||
const createKeyForm = document.getElementById('create-key-form');
|
||||
if (createKeyForm) {
|
||||
createKeyForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
await app.createKey();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleRouting: () => {
|
||||
const hash = window.location.hash.replace('#', '') || 'home';
|
||||
app.state.activePage = hash;
|
||||
|
||||
// Update UI
|
||||
document.querySelectorAll('.page-section').forEach(s => s.classList.remove('active'));
|
||||
const activeSection = document.getElementById(hash);
|
||||
if (activeSection) activeSection.classList.add('active');
|
||||
|
||||
// Update Nav
|
||||
document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
|
||||
const activeLink = document.getElementById(`nav-${hash}`);
|
||||
if (activeLink) activeLink.classList.add('active');
|
||||
|
||||
// Specialized page logic
|
||||
if (hash === 'playground') {
|
||||
playground.init(app.state.keys);
|
||||
} else if (hash === 'analytics') {
|
||||
analytics.init();
|
||||
}
|
||||
},
|
||||
|
||||
fetchData: async () => {
|
||||
try {
|
||||
// Fetch Tenant
|
||||
const tenantRes = await fetch('/api/auth/management/me');
|
||||
if (tenantRes.ok) {
|
||||
app.state.tenant = await tenantRes.data || await tenantRes.json();
|
||||
app.updateHeader();
|
||||
}
|
||||
|
||||
// Fetch Keys
|
||||
if (app.state.tenant && app.state.tenant.id) {
|
||||
const keysRes = await fetch(`/api/auth/management/keys/${app.state.tenant.id}`);
|
||||
if (keysRes.ok) {
|
||||
app.state.keys = await keysRes.json();
|
||||
app.renderKeysTable();
|
||||
app.updateStats();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch dashboard data', error);
|
||||
}
|
||||
},
|
||||
|
||||
updateHeader: () => {
|
||||
if (!app.state.tenant) return;
|
||||
document.getElementById('tenant-name').textContent = app.state.tenant.name;
|
||||
document.getElementById('tenant-email').textContent = app.state.tenant.email;
|
||||
document.getElementById('welcome-msg').textContent = `Welcome back, ${app.state.tenant.name}`;
|
||||
},
|
||||
|
||||
updateStats: () => {
|
||||
document.getElementById('active-keys-count').textContent = app.state.keys.length;
|
||||
|
||||
// Inject random bars for traffic
|
||||
const container = document.getElementById('traffic-bars');
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
const values = [40, 60, 55, 80, 70, 45, 90, 85, 60, 40, 30, 55, 75, 40, 60, 55, 80, 70, 45, 90];
|
||||
values.forEach(h => {
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'flex-1 bg-gradient-to-t from-blue-600/20 to-blue-400/80 rounded-t-sm relative group hover:to-blue-300 transition-all';
|
||||
bar.style.height = `${h}%`;
|
||||
bar.innerHTML = `<div class="absolute -top-10 left-1/2 -translate-x-1/2 glass px-2 py-1 rounded text-[10px] font-bold opacity-0 group-hover:opacity-100 transition-opacity z-10">${h}k</div>`;
|
||||
container.appendChild(bar);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
renderKeysTable: () => {
|
||||
const tbody = document.getElementById('keys-table-body');
|
||||
const createBtn = document.getElementById('create-key-btn');
|
||||
|
||||
if (!tbody) return;
|
||||
|
||||
if (app.state.keys.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="5" class="py-20 text-center text-slate-500 font-medium">No API keys found. Create one to get started.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable create button if limit reached (per React logic)
|
||||
if (app.state.keys.length >= 1) {
|
||||
createBtn.disabled = true;
|
||||
createBtn.title = "Limit of 1 API key per developer reached";
|
||||
createBtn.innerHTML = '<i data-lucide="shield-alert" class="w-4 h-4"></i> Limit Reached';
|
||||
}
|
||||
|
||||
tbody.innerHTML = app.state.keys.map(key => {
|
||||
const isVisible = app.state.showKeys[key.id];
|
||||
const maskedKey = isVisible ? key.key : "in_••••••••••••••••••••••••";
|
||||
const eyeIcon = isVisible ? 'eye-off' : 'eye';
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-white/[0.02] transition-colors">
|
||||
<td class="px-6 py-6 font-medium">${key.name}</td>
|
||||
<td class="px-6 py-6">
|
||||
<div class="flex items-center gap-2 bg-slate-900 rounded-lg px-3 py-1.5 w-fit border border-slate-800">
|
||||
<code class="text-xs text-blue-400 font-mono">${maskedKey}</code>
|
||||
<div class="flex items-center gap-1 ml-2 border-l border-slate-800 pl-2">
|
||||
<button onclick="app.toggleKeyVisibility('${key.id}')" class="text-slate-500 hover:text-white">
|
||||
<i data-lucide="${eyeIcon}" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
<button class="text-slate-500 hover:text-white" onclick="app.copyToClipboard('${key.key}')">
|
||||
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<span class="flex items-center gap-1.5 text-xs font-bold ${key.isActive ? 'text-emerald-400' : 'text-slate-500'}">
|
||||
<div class="w-1.5 h-1.5 rounded-full ${key.isActive ? 'bg-emerald-400 animate-pulse' : 'bg-slate-500'}"></div>
|
||||
${key.isActive ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<div class="flex gap-2">
|
||||
${(key.allowedOrigins && key.allowedOrigins.length > 0) ?
|
||||
key.allowedOrigins.map(org => `<span class="px-2 py-0.5 rounded-md bg-blue-500/10 text-[10px] uppercase font-black text-blue-400 flex items-center gap-1"><i data-lucide="globe" class="w-2.5 h-2.5"></i> ${org}</span>`).join('') :
|
||||
`<span class="px-2 py-0.5 rounded-md bg-slate-800 text-[10px] uppercase font-black text-slate-400 flex items-center gap-1"><i data-lucide="globe" class="w-2.5 h-2.5"></i> Universal</span>`
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right">
|
||||
<button class="p-2 text-slate-500 hover:text-white" onclick="app.fetchData()">
|
||||
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
lucide.createIcons();
|
||||
},
|
||||
|
||||
toggleKeyVisibility: (id) => {
|
||||
app.state.showKeys[id] = !app.state.showKeys[id];
|
||||
app.renderKeysTable();
|
||||
},
|
||||
|
||||
copyToClipboard: (text) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
// Simple toast or just feedback
|
||||
console.log('Copied to clipboard');
|
||||
},
|
||||
|
||||
toggleModal: (id, show) => {
|
||||
const modal = document.getElementById(id);
|
||||
if (modal) {
|
||||
if (show) modal.classList.remove('hidden');
|
||||
else modal.classList.add('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
createKey: async () => {
|
||||
const name = document.getElementById('new-key-name').value;
|
||||
const btn = document.getElementById('btn-submit-key');
|
||||
|
||||
if (!app.state.tenant) return;
|
||||
|
||||
try {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Creating...';
|
||||
|
||||
const res = await fetch(`/api/auth/management/keys/${app.state.tenant.id}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, rateLimit: 100 })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
app.toggleModal('create-key-modal', false);
|
||||
await app.fetchData();
|
||||
document.getElementById('new-key-name').value = '';
|
||||
} else {
|
||||
alert('Failed to create key');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Create Key';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Start app
|
||||
document.addEventListener('DOMContentLoaded', app.init);
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Playground Logic (Map Integration)
|
||||
*/
|
||||
|
||||
const playground = {
|
||||
map: null,
|
||||
currentStyle: 'obsidian',
|
||||
selectedKey: '',
|
||||
|
||||
init: (keys) => {
|
||||
if (playground.map) return;
|
||||
console.log('🗺️ Initializing Playground...');
|
||||
|
||||
playground.renderKeySelect(keys);
|
||||
playground.bindEvents();
|
||||
|
||||
if (keys.length > 0) {
|
||||
playground.selectedKey = keys[0].key;
|
||||
playground.loadMap();
|
||||
}
|
||||
},
|
||||
|
||||
renderKeySelect: (keys) => {
|
||||
const select = document.getElementById('pg-key-select');
|
||||
if (!select) return;
|
||||
select.innerHTML = keys.map(k => `<option value="${k.key}">${k.name}</option>`).join('');
|
||||
if (keys.length === 0) select.innerHTML = '<option value="">No keys available</option>';
|
||||
},
|
||||
|
||||
bindEvents: () => {
|
||||
const select = document.getElementById('pg-key-select');
|
||||
if (select) {
|
||||
select.addEventListener('change', (e) => {
|
||||
playground.selectedKey = e.target.value;
|
||||
playground.updateMap();
|
||||
});
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById('pg-search');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('keydown', async (e) => {
|
||||
if (e.key === 'Enter' && searchInput.value) {
|
||||
await playground.handleSearch(searchInput.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
loadMap: async () => {
|
||||
const container = document.getElementById('map');
|
||||
if (!container || !playground.selectedKey) return;
|
||||
|
||||
try {
|
||||
// Set RTL Text Plugin
|
||||
if (maplibregl.getRTLTextPluginStatus() === 'unavailable') {
|
||||
maplibregl.setRTLTextPlugin(
|
||||
'https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.js',
|
||||
null,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const styleUrl = `/api/maps/style.json?api_key=${playground.selectedKey}&theme=${playground.currentStyle}`;
|
||||
const res = await fetch(styleUrl);
|
||||
const styleData = await res.json();
|
||||
|
||||
playground.map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
style: styleData,
|
||||
center: [35.91, 31.95], // Amman, Jordan
|
||||
zoom: 12,
|
||||
attributionControl: false
|
||||
});
|
||||
|
||||
playground.map.addControl(new maplibregl.NavigationControl(), 'top-right');
|
||||
|
||||
playground.map.on('load', () => {
|
||||
console.log('Map loaded!');
|
||||
playground.map.resize();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to init map', error);
|
||||
}
|
||||
},
|
||||
|
||||
updateMap: async () => {
|
||||
if (!playground.map) return;
|
||||
try {
|
||||
const styleUrl = `/api/maps/style.json?api_key=${playground.selectedKey}&theme=${playground.currentStyle}`;
|
||||
const res = await fetch(styleUrl);
|
||||
const styleData = await res.json();
|
||||
playground.map.setStyle(styleData);
|
||||
} catch (error) {
|
||||
console.error('Failed to update style', error);
|
||||
}
|
||||
},
|
||||
|
||||
setStyle: (style) => {
|
||||
playground.currentStyle = style;
|
||||
// Update UI buttons
|
||||
document.getElementById('style-obsidian').className = (style === 'obsidian') ? 'flex-1 py-3 text-xs font-bold rounded-xl bg-blue-600 text-white shadow-lg shadow-blue-500/20' : 'flex-1 py-3 text-xs font-bold rounded-xl bg-slate-900 text-slate-500';
|
||||
document.getElementById('style-light').className = (style === 'light') ? 'flex-1 py-3 text-xs font-bold rounded-xl bg-blue-600 text-white shadow-lg shadow-blue-500/20' : 'flex-1 py-3 text-xs font-bold rounded-xl bg-slate-900 text-slate-500';
|
||||
|
||||
playground.updateMap();
|
||||
},
|
||||
|
||||
handleSearch: async (query) => {
|
||||
try {
|
||||
const res = await fetch(`/api/geocoding/search?q=${encodeURIComponent(query)}`, {
|
||||
headers: { 'x-api-key': playground.selectedKey }
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data && data.length > 0) {
|
||||
playground.map.flyTo({ center: [data[0].longitude, data[0].latitude], zoom: 14 });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Search failed", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user