2026-04-15-4

This commit is contained in:
Hamza-Ayed
2026-04-15 19:56:49 +03:00
parent 9cd1ac4c1d
commit 3d61362602
54 changed files with 12659 additions and 436 deletions
+110 -22
View File
@@ -13,50 +13,138 @@ const analytics = {
{ name: 'Sun', requests: 3490, latency: 225 },
],
init: () => {
console.log('📈 Initializing Analytics...');
analytics.renderVolumeChart();
analytics.renderLatencyChart();
init: async () => {
console.log('📈 Initializing Live Analytics...');
const headers = auth.getAuthHeader();
// 1. Fetch Summary for Success Ratio
try {
const summaryRes = await fetch('/api/usage/summary', { headers });
if (summaryRes.ok) {
const summary = await summaryRes.json();
analytics.renderSuccessRatio(summary.successRate);
}
} catch (e) { console.error(e); }
// 2. Fetch History for Volume Chart
const history = await analytics.fetchHistory();
if (history && history.length > 0) {
analytics.renderVolumeChart(history);
} else {
analytics.renderPlaceholder();
}
analytics.renderLatencyChart();
},
renderVolumeChart: () => {
renderSuccessRatio: (percent) => {
const circle = document.getElementById('success-circle');
const display = document.getElementById('success-percent-display');
if (!circle || !display) return;
display.textContent = `${percent}%`;
// Circumference is 552.9 (2 * PI * 88)
const offset = 552.9 - (percent / 100) * 552.9;
circle.style.strokeDashoffset = offset;
circle.classList.toggle('text-emerald-500', percent >= 95);
circle.classList.toggle('text-blue-600', percent < 95);
},
fetchHistory: async () => {
try {
const headers = auth.getAuthHeader();
const res = await fetch('/api/usage/history?days=7', { headers });
if (res.ok) {
const raw = await res.json();
// Map raw database records to chart data
return raw.map(item => {
const date = new Date(item.date);
return {
name: date.toLocaleDateString('en-US', { weekday: 'short' }),
requests: parseInt(item.count, 10)
};
});
}
} catch (error) {
console.error('Failed to fetch analytics history', error);
}
return null;
},
renderVolumeChart: (data) => {
const container = document.getElementById('v-chart');
if (!container) return;
const max = Math.max(...analytics.mockData.map(d => d.requests));
const max = Math.max(...data.map(d => d.requests), 1);
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}%">
container.innerHTML = data.map((d, i) => `
<div class="flex-1 flex flex-col items-center gap-4 group h-full">
<div class="flex-1 w-full bg-slate-900/40 rounded-2xl relative overflow-hidden flex items-end p-1 border border-white/[0.03] backdrop-blur-sm">
<div class="w-full bg-gradient-to-t from-blue-600 via-blue-500 to-cyan-400 rounded-xl transition-all duration-1000 ease-out hover:brightness-125 shadow-[0_0_30px_rgba(37,99,235,0.2)]"
style="height: 0%; transition-delay: ${i * 50}ms">
<script>
setTimeout(() => {
document.querySelectorAll('.group h-full div[style*="height: 0%"]')[0].style.height = "${(d.requests / max) * 100}%";
}, 100);
</script>
</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 class="absolute inset-0 flex flex-col items-center justify-center opacity-0 group-hover:opacity-100 transition-all transform translate-y-2 group-hover:translate-y-0 z-10">
<span class="text-[10px] font-black bg-blue-600 text-white px-3 py-1.5 rounded-lg shadow-2xl border border-blue-400/30 mb-2">${d.requests.toLocaleString()}</span>
</div>
</div>
<span class="text-[10px] font-bold text-slate-500">${d.name}</span>
<span class="text-[10px] font-black text-slate-500 uppercase tracking-widest">${d.name}</span>
</div>
`).join('');
// Trigger animations after render
setTimeout(() => {
container.querySelectorAll('.w-full.bg-gradient-to-t').forEach((el, index) => {
const height = el.parentElement.parentElement.dataset.height;
el.style.height = el.getAttribute('data-target-height');
});
}, 100);
},
renderPlaceholder: () => {
const container = document.getElementById('v-chart');
if (container) container.innerHTML = `
<div class="w-full h-full flex flex-col items-center justify-center text-slate-500 gap-4">
<div class="w-16 h-16 rounded-full bg-slate-900 flex items-center justify-center opacity-50">
<i data-lucide="bar-chart" class="w-8 h-8"></i>
</div>
<p class="text-xs italic font-bold tracking-widest uppercase opacity-40">No activity recorded for this period</p>
</div>`;
if (window.lucide) lucide.createIcons();
},
renderLatencyChart: () => {
const container = document.getElementById('l-chart');
if (!container) return;
const max = Math.max(...analytics.mockData.map(d => d.latency));
const mockLatency = [
{ name: 'Mon', latency: 240 },
{ name: 'Tue', latency: 198 },
{ name: 'Wed', latency: 310 },
{ name: 'Thu', latency: 208 },
{ name: 'Fri', latency: 250 },
{ name: 'Sat', latency: 210 },
{ name: 'Sun', latency: 225 },
];
const max = Math.max(...mockLatency.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"
container.innerHTML = mockLatency.map((d, i) => `
<div class="flex-1 flex flex-col items-center gap-4 group h-full">
<div class="flex-1 w-full bg-slate-900/40 rounded-2xl relative overflow-hidden flex items-end p-1 border border-white/[0.03] backdrop-blur-sm">
<div class="w-full bg-gradient-to-t from-violet-600 via-violet-500 to-fuchsia-400 rounded-xl transition-all duration-1000 ease-out hover:brightness-125 shadow-[0_0_30px_rgba(139,92,246,0.2)]"
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 class="absolute inset-0 flex flex-col items-center justify-center opacity-0 group-hover:opacity-100 transition-all transform translate-y-2 group-hover:translate-y-0 z-10">
<span class="text-[10px] font-black bg-violet-600 text-white px-3 py-1.5 rounded-lg shadow-2xl border border-violet-400/30 mb-2">${d.latency}ms</span>
</div>
</div>
<span class="text-[10px] font-bold text-slate-500">${d.name}</span>
<span class="text-[10px] font-black text-slate-500 uppercase tracking-widest">${d.name}</span>
</div>
`).join('');
}
+87 -23
View File
@@ -11,13 +11,18 @@ const app = {
},
init: async () => {
console.log('🚀 Dashboard Initializing...');
console.log('🚀 Dashboard Initializing Components...');
app.bindEvents();
app.handleRouting();
await app.fetchData();
lucide.createIcons();
},
onAuthenticated: async () => {
console.log('🔑 User Authenticated, Fetching Data...');
await app.fetchData();
app.updateStats();
},
bindEvents: () => {
window.addEventListener('hashchange', app.handleRouting);
@@ -50,25 +55,31 @@ const app = {
playground.init(app.state.keys);
} else if (hash === 'analytics') {
analytics.init();
} else if (hash === 'billing') {
billing.init();
} else if (hash === 'refinement') {
refinement.init();
} else if (hash === 'docs') {
docs.init();
}
},
fetchData: async () => {
const headers = auth.getAuthHeader();
try {
// Fetch Tenant
const tenantRes = await fetch('/api/auth/management/me');
const tenantRes = await fetch('/api/auth/management/me', { headers });
if (tenantRes.ok) {
app.state.tenant = await tenantRes.data || await tenantRes.json();
app.state.tenant = 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 (app.state.tenant) {
const keysRes = await fetch('/api/auth/management/keys', { headers });
if (keysRes.ok) {
app.state.keys = await keysRes.json();
app.renderKeysTable();
app.updateStats();
}
}
} catch (error) {
@@ -80,21 +91,73 @@ const app = {
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}`;
document.getElementById('welcome-msg').textContent = `Welcome back, ${app.state.tenant.name.split(' ')[0]}`;
// Role-based UI visibility
if (app.state.tenant.role === 'ADMIN') {
const auditLink = document.getElementById('nav-refinement');
if (auditLink) auditLink.classList.remove('hidden');
}
// Update user avatar if photoUrl exists
if (app.state.tenant.photoUrl) {
const avatarContainer = document.querySelector('.w-10.h-10.rounded-full.bg-slate-900');
if (avatarContainer) {
avatarContainer.innerHTML = `<img src="${app.state.tenant.photoUrl}" class="w-full h-full rounded-full object-cover border border-blue-500/20" alt="Profile">`;
}
}
},
updateStats: () => {
document.getElementById('active-keys-count').textContent = app.state.keys.length;
updateStats: async () => {
try {
const headers = auth.getAuthHeader();
const res = await fetch('/api/usage/summary', { headers });
if (res.ok) {
const data = await res.json();
// Update KPI Cards
document.getElementById('active-keys-count').textContent = app.state.keys.length;
// Detailed Stats from getUsageSummary
if (document.getElementById('stat-total-req')) {
document.getElementById('stat-total-req').textContent = data.monthlyUsage.toLocaleString();
}
if (document.getElementById('stat-success-percent')) {
document.getElementById('stat-success-percent').textContent = `${data.successRate}%`;
document.getElementById('stat-success-rate').textContent = data.successRate >= 95 ? 'Excellent' : 'Stable';
}
if (document.getElementById('stat-avg-latency')) {
document.getElementById('stat-avg-latency').textContent = `${data.avgLatency}ms`;
document.getElementById('stat-latency-val').textContent = `-${Math.round(data.avgLatency * 0.1)}ms`;
}
// Update Progress bar & Quota card
const bar = document.getElementById('usage-progress-bar');
const label = document.getElementById('usage-percentage-label');
const limitText = document.getElementById('usage-limit-text');
if (bar) bar.style.width = `${data.percentage}%`;
if (label) label.textContent = `${data.percentage}% USED`;
if (limitText) {
const remaining = Math.max(0, data.limit - data.monthlyUsage);
limitText.textContent = `${remaining.toLocaleString()} requests left`;
}
}
} catch (error) {
console.error('Failed to update stats', error);
}
// Inject random bars for traffic
// Inject sample bars for visual flair if chart not ready
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 => {
// High-density bars for a tech/premium feel
const values = [40, 60, 55, 80, 70, 45, 90, 85, 60, 40, 30, 55, 75, 40, 60, 55, 80, 70, 45, 90, 85, 60, 40, 30, 55, 75, 50, 65, 80, 70, 30];
values.forEach((h, index) => {
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.className = 'flex-1 bg-gradient-to-t from-blue-600/10 to-blue-400/60 rounded-t-[2px] relative group hover:to-blue-300 transition-all duration-500';
bar.style.height = `${h}%`;
bar.style.transitionDelay = `${index * 20}ms`;
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);
});
@@ -112,12 +175,10 @@ const app = {
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';
}
// Enable create button (removed previous limit restrictiveness)
createBtn.disabled = false;
createBtn.title = "Create a new API key";
createBtn.innerHTML = '<i data-lucide="plus" class="w-4 h-4"></i> Create Key';
tbody.innerHTML = app.state.keys.map(key => {
const isVisible = app.state.showKeys[key.id];
@@ -195,9 +256,12 @@ const app = {
btn.disabled = true;
btn.textContent = 'Creating...';
const res = await fetch(`/api/auth/management/keys/${app.state.tenant.id}`, {
const res = await fetch('/api/auth/management/keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...auth.getAuthHeader()
},
body: JSON.stringify({ name, rateLimit: 100 })
});
@@ -217,5 +281,5 @@ const app = {
}
};
// Start app
// Start app components
document.addEventListener('DOMContentLoaded', app.init);
+80
View File
@@ -0,0 +1,80 @@
/**
* Firebase Authentication Logic for Intaleq Dashboard
*/
const auth = {
firebaseAuth: null,
currentUser: null,
idToken: null,
init: () => {
console.log('🔐 Initializing Auth Module...');
// 1. Initialize Firebase
firebase.initializeApp(firebaseConfig);
auth.firebaseAuth = firebase.auth();
// 2. Listen for Auth Changes
auth.firebaseAuth.onAuthStateChanged(async (user) => {
if (user) {
console.log('✅ User logged in:', user.email);
auth.currentUser = user;
auth.idToken = await user.getIdToken();
// Show Dashboard, Hide Login
auth.toggleUI(true);
// Initialize main app data
app.onAuthenticated();
} else {
console.log('❌ No active session.');
auth.currentUser = null;
auth.idToken = null;
// Show Login, Hide Dashboard
auth.toggleUI(false);
}
});
},
signInWithGoogle: async () => {
const provider = new firebase.auth.GoogleAuthProvider();
try {
await auth.firebaseAuth.signInWithPopup(provider);
} catch (error) {
console.error('Sign-in error:', error);
alert('Failed to sign in. Please try again.');
}
},
signOut: async () => {
try {
await auth.firebaseAuth.signOut();
} catch (error) {
console.error('Sign-out error:', error);
}
},
toggleUI: (isAuthenticated) => {
const loginSection = document.getElementById('login-section');
const mainSidebar = document.getElementById('main-sidebar');
const mainContent = document.getElementById('main-content');
if (isAuthenticated) {
if (loginSection) loginSection.classList.add('hidden');
if (mainSidebar) mainSidebar.classList.remove('hidden');
if (mainContent) mainContent.classList.remove('hidden');
} else {
if (loginSection) loginSection.classList.remove('hidden');
if (mainSidebar) mainSidebar.classList.add('hidden');
if (mainContent) mainContent.classList.add('hidden');
}
},
getAuthHeader: () => {
return auth.idToken ? { 'Authorization': `Bearer ${auth.idToken}` } : {};
}
};
// Start Auth on Load
document.addEventListener('DOMContentLoaded', auth.init);
+129
View File
@@ -0,0 +1,129 @@
/**
* Billing & Subscription Logic
*/
const billing = {
state: {
subscription: null,
invoices: []
},
init: async () => {
console.log('💳 Initializing Billing...');
await billing.fetchSubscription();
await billing.fetchInvoices();
billing.renderUI();
},
fetchSubscription: async () => {
try {
const headers = auth.getAuthHeader();
const res = await fetch('/api/billing/subscription', { headers });
if (res.ok) {
billing.state.subscription = await res.json();
}
} catch (error) {
console.error('Failed to fetch subscription', error);
}
},
fetchInvoices: async () => {
try {
const headers = auth.getAuthHeader();
const res = await fetch('/api/billing/invoices', { headers });
if (res.ok) {
billing.state.invoices = await res.json();
}
} catch (error) {
console.error('Failed to fetch invoices', error);
}
},
renderUI: () => {
const sub = billing.state.subscription;
if (!sub) return;
// 1. Update Current Plan Badges
document.querySelectorAll('.current-plan-name').forEach(el => el.textContent = sub.plan);
// 2. Render Plan Cards logic
const plans = ['FREE', 'PRO', 'ENTERPRISE'];
plans.forEach(p => {
const card = document.getElementById(`plan-card-${p.toLowerCase()}`);
if (card) {
const btn = card.querySelector('.plan-btn');
if (p === sub.plan) {
card.classList.add('border-blue-500/50');
if (btn) {
btn.textContent = 'Current Plan';
btn.disabled = true;
btn.classList.add('opacity-50');
}
} else {
card.classList.remove('border-blue-500/50');
if (btn) {
btn.textContent = p === 'ENTERPRISE' ? 'Contact Sales' : 'Upgrade Now';
btn.disabled = false;
btn.classList.remove('opacity-50');
}
}
}
});
// 3. Render Invoice History
const tbody = document.getElementById('invoice-table-body');
if (tbody) {
if (billing.state.invoices.length === 0) {
tbody.innerHTML = `<tr><td colspan="4" class="py-10 text-center text-slate-500 text-xs">No transactions yet.</td></tr>`;
} else {
tbody.innerHTML = billing.state.invoices.map(inv => `
<tr class="border-b border-white/[0.02] hover:bg-white/[0.01]">
<td class="py-4 text-xs font-medium">${new Date(inv.createdAt).toLocaleDateString()}</td>
<td class="py-4 text-xs font-bold">$${inv.amount}</td>
<td class="py-4 text-xs">
<span class="px-2 py-0.5 rounded bg-slate-800 text-[10px] uppercase font-black">${inv.provider}</span>
</td>
<td class="py-4 text-xs">
<span class="status-badge ${inv.status.toLowerCase()}">${inv.status}</span>
</td>
</tr>
`).join('');
}
}
},
startCheckout: async (plan, provider) => {
try {
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'Processing...';
btn.disabled = true;
const headers = {
'Content-Type': 'application/json',
...auth.getAuthHeader()
};
const res = await fetch('/api/billing/checkout', {
method: 'POST',
headers,
body: JSON.stringify({ plan, provider })
});
if (res.ok) {
const data = await res.json();
if (data.checkoutUrl) {
window.location.href = data.checkoutUrl;
}
} else {
alert('Checkout failed. Please try again.');
}
btn.textContent = originalText;
btn.disabled = false;
} catch (error) {
console.error('Checkout error', error);
alert('An error occurred during checkout.');
}
}
};
+146
View File
@@ -0,0 +1,146 @@
/**
* Documentation Engine for Intaleq Dashboard
*/
const docs = {
init: () => {
console.log('📚 Initializing Documentation...');
docs.bindEvents();
docs.renderSection('getting-started');
},
bindEvents: () => {
// Handle side-nav clicks
document.querySelectorAll('.docs-nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const section = e.currentTarget.getAttribute('data-section');
docs.renderSection(section);
// Active state
document.querySelectorAll('.docs-nav-link').forEach(l => l.classList.remove('active', 'bg-blue-500/10', 'text-blue-400'));
e.currentTarget.classList.add('active', 'bg-blue-500/10', 'text-blue-400');
});
});
},
renderSection: (id) => {
const container = document.getElementById('docs-content');
if (!container) return;
// Content repository
const content = {
'getting-started': `
<div class="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div>
<h3 class="text-3xl font-black mb-4">Getting Started</h3>
<p class="text-slate-400 leading-relaxed">Welcome to the Intaleq Map Platform. Our APIs allow you to integrate high-quality vector maps, geocoding, and routing into your web and mobile applications with ease.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="glass p-6 rounded-2xl border-white/5">
<h4 class="font-bold mb-2 flex items-center gap-2">
<i data-lucide="key" class="w-4 h-4 text-blue-400"></i>
1. Get an API Key
</h4>
<p class="text-xs text-slate-500">Go to the Credentials page and create your first API key.</p>
</div>
<div class="glass p-6 rounded-2xl border-white/5">
<h4 class="font-bold mb-2 flex items-center gap-2">
<i data-lucide="code" class="w-4 h-4 text-emerald-400"></i>
2. Install SDK
</h4>
<p class="text-xs text-slate-500">Use our MapLibre wrappers for JavaScript or Flutter.</p>
</div>
</div>
<div class="space-y-4">
<h4 class="text-lg font-bold">Base URL</h4>
<div class="bg-slate-900 rounded-xl p-4 font-mono text-sm border border-slate-800 text-blue-400">
https://map-dashbord.intaleqapp.com/api
</div>
</div>
</div>
`,
'tiles-api': `
<div class="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div>
<h3 class="text-3xl font-black mb-2">Vector Tiles API</h3>
<p class="text-slate-400">Render high-performance vector maps from our global database.</p>
</div>
<div class="space-y-6">
<div class="endpoint-card glass rounded-2xl border-white/5 overflow-hidden">
<div class="p-4 bg-white/[0.02] border-b border-white/5 flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="px-2 py-1 bg-green-500/20 text-green-400 text-[10px] font-black rounded uppercase">GET</span>
<code class="text-xs font-bold">/maps/style.json</code>
</div>
</div>
<div class="p-6">
<p class="text-sm text-slate-400 mb-6">Returns the MapLibre-compatible style configuration. Use the <code>theme</code> parameter to switch between 'light' and 'obsidian'.</p>
<h5 class="text-xs font-black uppercase tracking-widest text-slate-500 mb-4">Code Example</h5>
<div class="relative group">
<pre class="bg-slate-950 p-4 rounded-xl text-xs font-mono text-slate-300 leading-relaxed overflow-x-auto">
// Initialize MapLibre with Intaleq Style
const map = new maplibregl.Map({
container: 'map',
style: 'https://map-dashbord.intaleqapp.com/api/maps/style.json?theme=obsidian',
center: [35.9106, 31.9539], // Amman
zoom: 12
});</pre>
</div>
</div>
</div>
</div>
</div>
`,
'geocoding-api': `
<div class="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div>
<h3 class="text-3xl font-black mb-2">Geocoding API</h3>
<p class="text-slate-400">Convert addresses to coordinates (Forward) or coordinates to addresses (Reverse).</p>
</div>
<div class="endpoint-card glass rounded-2xl border-white/5 overflow-hidden">
<div class="p-4 bg-white/[0.02] border-b border-white/5 flex items-center gap-3">
<span class="px-2 py-1 bg-green-500/20 text-green-400 text-[10px] font-black rounded uppercase">GET</span>
<code class="text-xs font-bold">/geocoding/search</code>
</div>
<div class="p-6">
<table class="w-full text-left text-xs mb-6">
<thead>
<tr class="text-slate-500 uppercase tracking-widest font-black border-b border-white/5">
<th class="pb-3">Parameter</th>
<th class="pb-3">Type</th>
<th class="pb-3">Description</th>
</tr>
</thead>
<tbody class="divide-y divide-white/[0.02]">
<tr>
<td class="py-3 font-bold text-blue-400 font-mono">q</td>
<td class="py-3 text-slate-500">string</td>
<td class="py-3">Search query (address, place, coordinates)</td>
</tr>
<tr>
<td class="py-3 font-bold text-blue-400 font-mono">limit</td>
<td class="py-3 text-slate-500">number</td>
<td class="py-3">Max results (default: 5)</td>
</tr>
</tbody>
</table>
<h5 class="text-xs font-black uppercase tracking-widest text-slate-500 mb-4">Request</h5>
<pre class="bg-slate-950 p-4 rounded-xl text-xs font-mono text-blue-400 mb-4">curl "https://map-dashbord.intaleqapp.com/api/geocoding/search?q=Amman&limit=1" \\
-H "x-api-key: YOUR_API_KEY"</pre>
</div>
</div>
</div>
`
};
container.innerHTML = content[id] || '<p class="text-slate-500">Documentation section coming soon...</p>';
lucide.createIcons();
}
};
+9
View File
@@ -0,0 +1,9 @@
const firebaseConfig = {
apiKey: "AIzaSyAwybkM9FFkF2KRM0bFNAaEPNPbMZBWFn8",
authDomain: "intaleq-map.firebaseapp.com",
projectId: "intaleq-map",
storageBucket: "intaleq-map.firebasestorage.app",
messagingSenderId: "1052695318500",
appId: "1:1052695318500:web:7913981536524a630401f1",
measurementId: "G-P5Z2YXH481"
};
+157
View File
@@ -0,0 +1,157 @@
/**
* i18n Translation Engine
*/
const i18n = {
currentLang: localStorage.getItem('intaleq_lang') || 'en',
translations: {
en: {
// Navbar
'nav-features': 'Features',
'nav-why': 'Why Us?',
'nav-pricing': 'Pricing',
'nav-launch': 'Launch Dashboard',
'lang-toggle': 'العربية',
// Hero
'hero-badge': 'Now with 3D Buildings in Jordan & Syria',
'hero-title': 'The Map API That <br> <span class="text-blue-500">Doesn\'t Break</span> The Bank.',
'hero-desc': 'Build premium location-based apps with high-fidelity vector tiles, optimized routing, and 3D buildings. 85% cheaper than Google Maps.',
'hero-cta-start': 'Start Building Free',
'hero-cta-view': 'View Comparison',
// Features
'feat-latency-title': 'Zero Latency',
'feat-latency-desc': 'Our infrastructure is optimized for MENA region, ensuring map tiles load in under 200ms.',
'feat-routing-title': 'Smart Routing',
'feat-routing-desc': 'Enterprise-grade routing engine with support for alternative paths and traffic awareness.',
'feat-geocoding-title': 'Local Geocoding',
'feat-geocoding-desc': 'Highly accurate search for local landmarks and neighborhoods in Jordan & Syria.',
// Dashboard Sidebar
'side-dashboard': 'Dashboard',
'side-playground': 'Playground',
'side-analytics': 'Analytics',
'side-billing': 'Billing',
'side-audit': 'Place Audit',
'side-docs': 'Documentation',
'side-upgrade': 'Upgrade Plan',
// Dashboard General
'welcome': 'Welcome back',
'system-status': 'System Operational',
'quota-card-title': 'Monthly Quota',
'requests-left': 'requests left',
'used-label': 'USED',
// KPI Labels
'kpi-total-req': 'Total Requests',
'kpi-success-rate': 'Success Rate',
'kpi-active-keys': 'Active Keys',
'kpi-latency': 'Avg Latency',
},
ar: {
// Navbar
'nav-features': 'المميزات',
'nav-why': 'لماذا نحن؟',
'nav-pricing': 'الأسعار',
'nav-launch': 'لوحة التحكم',
'lang-toggle': 'English',
// Hero
'hero-badge': 'الآن مع المباني ثلاثية الأبعاد في الأردن وسوريا',
'hero-title': 'واجهة خرائط برمجية <br> <span class="text-blue-500">لا ترهق</span> ميزانيتك.',
'hero-desc': 'ابنِ تطبيقات خرائط فاخرة مع خرائط مجهزة، توجيه ذكي، ومباني ثلاثية الأبعاد. أوفر بنسبة 85% من خرائط جوجل.',
'hero-cta-start': 'ابدأ مجاناً',
'hero-cta-view': 'قارن الأسعار',
// Features
'feat-latency-title': 'سرعة فائقة',
'feat-latency-desc': 'بنيتنا التحتية محسنة لمنطقة الشرق الأوسط، مما يضمن تحميل الخرائط في أقل من 200 مللي ثانية.',
'feat-routing-title': 'توجيه ذكي',
'feat-routing-desc': 'محرك توجيه من الفئة المؤسسية يدعم المسارات البديلة والوعي بحركة المرور.',
'feat-geocoding-title': 'بحث مكاني محلي',
'feat-geocoding-desc': 'دقة عالية جداً في البحث عن المعالم والأحياء في الأردن وسوريا.',
// Dashboard Sidebar
'side-dashboard': 'لوحة التحكم',
'side-playground': 'ساحة الاختبار',
'side-analytics': 'التحليلات',
'side-billing': 'الفواتير',
'side-audit': 'تدقيق الأماكن',
'side-docs': 'التوثيق',
'side-upgrade': 'ترقية الخطة',
// Dashboard General
'welcome': 'مرحباً بك مجدداً',
'system-status': 'النظام يعمل بكفاءة',
'quota-card-title': 'رصيد الاستهلاك',
'requests-left': 'طلب متبقي',
'used-label': 'مستهلك',
// KPI Labels
'kpi-total-req': 'إجمالي الطلبات',
'kpi-success-rate': 'نسبة النجاح',
'kpi-active-keys': 'المفاتيح النشطة',
'kpi-latency': 'متوسط سرعة الاستجابة',
}
},
init: () => {
i18n.apply(i18n.currentLang);
},
toggle: () => {
const nextLang = i18n.currentLang === 'en' ? 'ar' : 'en';
i18n.currentLang = nextLang;
localStorage.setItem('intaleq_lang', nextLang);
i18n.apply(nextLang);
},
apply: (lang) => {
const rtl = lang === 'ar';
document.documentElement.dir = rtl ? 'rtl' : 'ltr';
document.documentElement.lang = lang;
// Apply font classes
if (rtl) {
document.body.style.fontFamily = "'Cairo', sans-serif";
document.body.classList.add('rtl-mode');
} else {
document.body.style.fontFamily = "'Plus Jakarta Sans', sans-serif";
document.body.classList.remove('rtl-mode');
}
// Mirror Sidebar if in dashboard
const sidebar = document.getElementById('main-sidebar');
if (sidebar) {
if (rtl) {
sidebar.classList.add('order-last', 'border-l', 'border-r-0');
sidebar.classList.remove('border-r');
} else {
sidebar.classList.remove('order-last', 'border-l');
sidebar.classList.add('border-r');
}
}
// Translate elements with data-i18n attribute
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
const translation = i18n.translations[lang][key];
if (translation) {
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
el.placeholder = translation;
} else {
el.innerHTML = translation;
}
}
});
// Trigger icon re-render if lucide is present
if (window.lucide) lucide.createIcons();
}
};
// Initialize on load
document.addEventListener('DOMContentLoaded', i18n.init);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+36 -4
View File
@@ -14,6 +14,13 @@ const playground = {
playground.renderKeySelect(keys);
playground.bindEvents();
// Watch for section changes to trigger resize
window.addEventListener('hashchange', () => {
if (window.location.hash === '#playground' && playground.map) {
setTimeout(() => playground.map.resize(), 100);
}
});
if (keys.length > 0) {
playground.selectedKey = keys[0].key;
playground.loadMap();
@@ -53,30 +60,55 @@ const playground = {
try {
// Set RTL Text Plugin
if (maplibregl.getRTLTextPluginStatus() === 'unavailable') {
console.log('🌐 Loading MapLibre RTL Plugin...');
maplibregl.setRTLTextPlugin(
'https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.js',
'js/plugins/mapbox-gl-rtl-text.js',
null,
true
);
}
console.log(`🛰️ Fetching Map Style for theme: ${playground.currentStyle}...`);
const styleUrl = `/api/maps/style.json?api_key=${playground.selectedKey}&theme=${playground.currentStyle}`;
const res = await fetch(styleUrl);
if (!res.ok) {
throw new Error(`Style fetch failed with status: ${res.status}`);
}
const styleData = await res.json();
console.log('🎨 Style fetched successfully, initializing MapLibre...');
playground.map = new maplibregl.Map({
container: 'map',
style: styleData,
center: [35.91, 31.95], // Amman, Jordan
zoom: 12,
attributionControl: false
attributionControl: false,
trackResize: true
});
playground.map.addControl(new maplibregl.NavigationControl(), 'top-right');
playground.map.on('load', () => {
console.log('Map loaded!');
playground.map.resize();
console.log('✅ Map engine ready and tiles loading!');
// Wait slightly for container animation to finish
setTimeout(() => {
playground.map.resize();
const container = document.getElementById('map');
if (container && container.offsetWidth > 0) {
console.log('📏 Map container size verified:', container.offsetWidth, 'x', container.offsetHeight);
} else {
console.warn('⚠️ Map container has 0 width. Potential visibility issue.');
}
}, 500);
});
playground.map.on('error', (e) => {
console.error('❌ MapLibre Error Detail:', e.error || e);
if (e.error && e.error.message.includes('Style')) {
alert('Map Style Error: Please verify your API Key and Network connection.');
}
});
} catch (error) {
File diff suppressed because one or more lines are too long
+118
View File
@@ -0,0 +1,118 @@
/**
* Map Refinement (Place Audit) logic
*/
const refinement = {
state: {
candidates: []
},
init: async () => {
console.log('📍 Initializing Map Refinement Logic...');
await refinement.fetchCandidates();
},
fetchCandidates: async () => {
const headers = auth.getAuthHeader();
try {
const res = await fetch('/api/map-refinement/candidates?status=PENDING', { headers });
if (res.ok) {
refinement.state.candidates = await res.json();
refinement.renderTable();
}
} catch (error) {
console.error('Failed to fetch candidates', error);
}
},
renderTable: () => {
const tbody = document.getElementById('refinement-table-body');
if (!tbody) return;
if (refinement.state.candidates.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="5" class="py-20 text-center text-slate-500">
<div class="flex flex-col items-center gap-4">
<i data-lucide="map-pin" class="w-8 h-8 opacity-20"></i>
<p>No pending location suggestions.</p>
</div>
</td>
</tr>
`;
lucide.createIcons();
return;
}
tbody.innerHTML = refinement.state.candidates.map(c => `
<tr class="hover:bg-white/[0.02] transition-colors border-b border-white/[0.03]">
<td class="px-8 py-6">
<div class="font-bold text-white">${c.name_ar || c.name}</div>
<div class="text-[10px] text-slate-500 uppercase font-bold mt-1">${c.country}</div>
</td>
<td class="px-8 py-6">
<span class="px-2 py-1 rounded-lg bg-blue-500/10 text-blue-400 text-[10px] font-black uppercase tracking-wider">${c.category || 'General'}</span>
</td>
<td class="px-8 py-6 font-mono text-xs text-slate-400">
${parseFloat(c.latitude).toFixed(5)}, ${parseFloat(c.longitude).toFixed(5)}
</td>
<td class="px-8 py-6 text-sm text-slate-400 font-medium">
${c.submittedBy || 'System User'}
</td>
<td class="px-8 py-6 text-right space-x-2">
<button onclick="refinement.reject('${c.id}')" class="p-2.5 rounded-xl bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-all" title="Reject">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
<button onclick="refinement.approve('${c.id}')" class="p-2.5 rounded-xl bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20 transition-all border border-emerald-500/20 shadow-lg shadow-emerald-500/10" title="Approve">
<i data-lucide="check" class="w-4 h-4"></i>
</button>
</td>
</tr>
`).join('');
lucide.createIcons();
},
approve: async (id) => {
if (!confirm('Are you sure you want to approve this location and add it to the production map?')) return;
const headers = auth.getAuthHeader();
try {
const res = await fetch(`/api/map-refinement/candidates/${id}/approve`, {
method: 'PATCH',
headers
});
if (res.ok) {
await refinement.fetchCandidates();
} else {
alert('Approval failed.');
}
} catch (error) {
console.error(error);
}
},
reject: async (id) => {
const reason = prompt('Please enter a reason for rejection:');
if (reason === null) return;
const headers = auth.getAuthHeader();
try {
const res = await fetch(`/api/map-refinement/candidates/${id}/reject`, {
method: 'PATCH',
headers: {
...headers,
'Content-Type': 'application/json'
},
body: JSON.stringify({ reason })
});
if (res.ok) {
await refinement.fetchCandidates();
} else {
alert('Rejection failed.');
}
} catch (error) {
console.error(error);
}
}
};