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
File diff suppressed because one or more lines are too long
+647
View File
@@ -0,0 +1,647 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jordan Map Platform | Developer Dashboard</title>
<!-- Tailwind CSS Play CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- MapLibre GL JS -->
<script src="https://unpkg.com/maplibre-gl@5.1.1/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@5.1.1/dist/maplibre-gl.css" rel="stylesheet" />
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<!-- Firebase SDK (v10.12.0) -->
<script src="https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/10.12.0/firebase-auth-compat.js"></script>
<script src="js/firebase-config.js"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
slate: {
950: '#0a0a0b',
}
}
}
}
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Cairo:wght@400;600;700;900&display=swap');
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: #0a0a0b;
color: #f8fafc;
}
/* RTL Specifics */
.rtl-mode #main-content {
text-align: right;
}
.rtl-mode .nav-link {
flex-direction: row-reverse;
}
.glass {
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.text-gradient {
background: linear-gradient(135deg, #fff 0%, #94a3b8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.btn {
@apply flex items-center gap-2 px-4 py-2.5 rounded-xl font-bold transition-all duration-300 active:scale-95 text-sm;
}
.btn-primary {
@apply bg-blue-600 text-white hover:bg-blue-500 shadow-lg shadow-blue-500/20 disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-secondary {
@apply bg-slate-900 text-slate-400 hover:text-white border border-slate-800 hover:border-slate-700;
}
/* Nav logic */
.page-section {
display: none;
}
.page-section.active {
display: block;
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Sidebar active state */
.nav-link.active {
@apply bg-blue-600/10 text-blue-400 border-r-2 border-blue-600;
}
</style>
</head>
<body class="overflow-hidden h-screen flex bg-[#0a0a0b]">
<!-- Login Section -->
<section id="login-section" class="fixed inset-0 z-[200] flex items-center justify-center bg-gradient-to-br from-slate-950 via-[#0a0a0b] to-blue-950/20 overflow-hidden">
<div class="absolute inset-0 opacity-20 pointer-events-none">
<div class="absolute top-1/4 left-1/4 w-96 h-96 bg-blue-600 rounded-full blur-[128px] animate-pulse"></div>
<div class="absolute bottom-1/4 right-1/4 w-96 h-96 bg-cyan-600 rounded-full blur-[128px] animate-pulse delay-700"></div>
</div>
<div class="glass max-w-md w-full p-10 rounded-[2.5rem] relative z-10 text-center border-white/10 shadow-2xl">
<div class="w-20 h-20 rounded-2xl bg-gradient-to-tr from-blue-600 to-cyan-400 flex items-center justify-center text-white shadow-2xl shadow-blue-500/30 mx-auto mb-8 rotate-3">
<i data-lucide="layers" class="w-10 h-10"></i>
</div>
<h1 class="text-4xl font-black tracking-tight mb-3 text-gradient">Intaleq Maps</h1>
<p class="text-slate-400 font-medium mb-10 leading-relaxed">The premium developer platform for mapping services in Jordan & Syria.</p>
<button onclick="auth.signInWithGoogle()" class="w-full btn bg-white text-slate-950 hover:bg-slate-100 py-4 text-base font-black shadow-xl flex items-center justify-center gap-3 active:scale-[0.98] transition-all rounded-2xl">
<svg class="w-5 h-5" viewBox="0 0 24 24">
<path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
Continue with Google
</button>
<p class="mt-8 text-[11px] text-slate-500 font-bold uppercase tracking-widest">Enterprise Ready · Secure Access</p>
</div>
</section>
<!-- Sidebar -->
<aside id="main-sidebar" class="hidden w-64 border-r border-slate-800 bg-slate-950/50 backdrop-blur-xl flex flex-col z-50">
<div class="p-8">
<div class="flex items-center gap-3 group cursor-pointer">
<div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-blue-600 to-cyan-400 flex items-center justify-center text-white shadow-lg shadow-blue-500/20 group-hover:rotate-12 transition-transform">
<i data-lucide="layers" class="w-6 h-6"></i>
</div>
<div>
<h1 class="font-black text-lg tracking-tight leading-none">Intaleq</h1>
<span class="text-[10px] uppercase tracking-[0.2em] font-bold text-slate-500">Maps SaaS</span>
</div>
</div>
</div>
<nav class="flex-1 px-4 space-y-2 mt-4">
<a href="#home" class="nav-link flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:bg-white/5 text-slate-400 font-bold group" id="nav-home">
<i data-lucide="layout-dashboard" class="w-5 h-5 group-hover:text-white transition-colors"></i>
<span data-i18n="side-dashboard">Dashboard</span>
</a>
<a href="#playground" class="nav-link flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:bg-white/5 text-slate-400 font-bold group" id="nav-playground">
<i data-lucide="terminal" class="w-5 h-5 group-hover:text-white transition-colors"></i>
<span data-i18n="side-playground">Playground</span>
</a>
<a href="#analytics" class="nav-link flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:bg-white/5 text-slate-400 font-bold group" id="nav-analytics">
<i data-lucide="bar-chart-3" class="w-5 h-5 group-hover:text-white transition-colors"></i>
<span data-i18n="side-analytics">Analytics</span>
</a>
<a href="#billing" class="nav-link flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:bg-white/5 text-slate-400 font-bold group" id="nav-billing">
<i data-lucide="credit-card" class="w-5 h-5 group-hover:text-white transition-colors"></i>
<span data-i18n="side-billing">Billing</span>
</a>
<a href="#refinement" class="nav-link flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:bg-white/5 text-slate-400 font-bold group hidden" id="nav-refinement">
<i data-lucide="map-pin" class="w-5 h-5 group-hover:text-white transition-colors"></i>
<span data-i18n="side-audit">Place Audit</span>
</a>
<a href="#docs" class="nav-link flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:bg-white/5 text-slate-400 font-bold group" id="nav-docs">
<i data-lucide="book-open" class="w-5 h-5 group-hover:text-white transition-colors"></i>
<span data-i18n="side-docs">Documentation</span>
</a>
</nav>
<div class="p-6">
<div class="glass p-5 rounded-2xl border border-blue-500/10 bg-gradient-to-br from-blue-600/5 to-transparent">
<p class="text-[10px] font-black uppercase tracking-wider text-blue-400 mb-2">Beta Access</p>
<p class="text-xs text-slate-400 leading-relaxed font-medium mb-4">You're currently on the free sandbox tier.</p>
<a href="#billing" class="w-full btn btn-primary !py-2 !text-xs text-center flex items-center justify-center" data-i18n="side-upgrade">Upgrade Plan</a>
</div>
</div>
</aside>
<!-- Main Content -->
<div id="main-content" class="hidden flex-1 flex flex-col min-w-0 bg-gradient-to-br from-slate-950 via-[#0f1115] to-slate-950 overflow-y-auto">
<!-- Header -->
<header class="h-20 border-b border-white/[0.03] flex items-center justify-between px-8 sticky top-0 bg-slate-950/50 backdrop-blur-md z-40">
<div class="flex items-center gap-4">
<div class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
<span class="text-xs font-black uppercase tracking-widest text-slate-500" data-i18n="system-status">System Operational</span>
</div>
<div class="flex items-center gap-6">
<!-- Language Toggle -->
<button onclick="i18n.toggle()" class="px-3 py-1.5 rounded-lg border border-white/10 hover:bg-white/5 transition-all text-xs font-bold text-slate-400 flex items-center gap-2" data-i18n="lang-toggle">
العربية
</button>
<div class="flex flex-col items-end">
<p class="text-sm font-bold" id="tenant-name">Loading...</p>
<p class="text-[10px] text-slate-500 font-medium" id="tenant-email">developer@intaleq.com</p>
</div>
<div class="w-10 h-10 rounded-full bg-slate-900 border border-slate-800 flex items-center justify-center text-blue-400">
<i data-lucide="user" class="w-5 h-5"></i>
</div>
</div>
</header>
<main class="p-12 max-w-7xl mx-auto w-full">
<!-- Home Section -->
<section id="home" class="page-section">
<div class="mb-12">
<h2 class="text-4xl text-gradient mb-2" id="welcome-msg">Welcome back...</h2>
<p class="text-slate-400">Everything you need to build with premium Jordan Map Platform API</p>
</div>
<!-- KPI Stats -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-12">
<div class="glass p-6 rounded-2xl group border-[#1e293b]">
<div class="flex items-center justify-between mb-4">
<div class="w-10 h-10 rounded-xl bg-slate-900 flex items-center justify-center text-blue-400">
<i data-lucide="zap" class="w-5 h-5"></i>
</div>
<span class="text-xs font-bold px-2 py-1 rounded-full bg-slate-900 text-emerald-500">+12.5%</span>
</div>
<p class="text-sm text-slate-500 font-medium mb-1" data-i18n="kpi-total-req">Total Requests</p>
<div class="text-2xl font-bold tracking-tight" id="stat-total-req">...</div>
</div>
<div class="glass p-6 rounded-2xl group">
<div class="flex items-center justify-between mb-4">
<div class="w-10 h-10 rounded-xl bg-slate-900 flex items-center justify-center text-emerald-400">
<i data-lucide="shield-check" class="w-5 h-5"></i>
</div>
<span class="text-xs font-bold px-2 py-1 rounded-full bg-slate-900 text-emerald-500" id="stat-success-rate">99.9%</span>
</div>
<p class="text-sm text-slate-500 font-medium mb-1" data-i18n="kpi-success-rate">Success Rate</p>
<div class="text-2xl font-bold tracking-tight" id="stat-success-percent">...</div>
</div>
<div class="glass p-6 rounded-2xl group">
<div class="flex items-center justify-between mb-4">
<div class="w-10 h-10 rounded-xl bg-slate-900 flex items-center justify-center text-cyan-400">
<i data-lucide="activity" class="w-5 h-5"></i>
</div>
<span class="text-xs font-bold px-2 py-1 rounded-full bg-slate-900 text-slate-500">Live</span>
</div>
<p class="text-sm text-slate-500 font-medium mb-1" data-i18n="kpi-active-keys">Active Keys</p>
<div class="text-2xl font-bold tracking-tight" id="active-keys-count">...</div>
</div>
<div class="glass p-6 rounded-2xl group">
<div class="flex items-center justify-between mb-4">
<div class="w-10 h-10 rounded-xl bg-slate-900 flex items-center justify-center text-violet-400">
<i data-lucide="trending-up" class="w-5 h-5"></i>
</div>
<span class="text-xs font-bold px-2 py-1 rounded-full bg-slate-900 text-slate-500" id="stat-latency-val">-ms</span>
</div>
<p class="text-sm text-slate-500 font-medium mb-1" data-i18n="kpi-latency">Avg Latency</p>
<div class="text-2xl font-bold tracking-tight" id="stat-avg-latency">...</div>
</div>
<!-- Usage Battery Card -->
<div class="glass p-6 rounded-2xl group bg-gradient-to-br from-blue-600/5 to-transparent border-blue-500/10">
<div class="flex items-center justify-between mb-4">
<div class="w-10 h-10 rounded-xl bg-slate-900 flex items-center justify-center text-blue-500">
<i data-lucide="battery-charging" class="w-5 h-5"></i>
</div>
<span class="text-[10px] font-black uppercase text-blue-500 tracking-widest" id="usage-percentage-label">...</span>
</div>
<p class="text-sm text-slate-500 font-medium mb-3" data-i18n="quota-card-title">Monthly Quota</p>
<div class="w-full h-2 bg-slate-900 rounded-full overflow-hidden mb-2">
<div id="usage-progress-bar" class="h-full bg-blue-500 transition-all duration-1000" style="width: 0%"></div>
</div>
<p class="text-[10px] text-slate-500 font-bold" id="usage-limit-text">... requests left</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
<!-- Traffic Mockup -->
<div class="lg:col-span-2 glass rounded-2xl p-6 relative overflow-hidden">
<div class="flex items-center justify-between mb-8">
<div>
<h3 class="text-lg font-bold">Request Traffic</h3>
<p class="text-sm text-slate-500">Live traffic across all API endpoints</p>
</div>
<a href="#analytics" class="text-xs text-blue-500 hover:text-blue-400 font-bold flex items-center gap-1 transition-colors">
Full Analytics <i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</a>
</div>
<div class="h-64 flex items-end gap-1.5 px-2 relative" id="traffic-bars">
<!-- Premium Animated Bars via JS -->
</div>
</div>
<!-- Quick Actions -->
<div class="glass rounded-2xl p-6 bg-gradient-to-br from-blue-600/10 to-transparent border-blue-500/10">
<h3 class="text-lg font-bold mb-2">Quick Start</h3>
<p class="text-sm text-slate-500 mb-6 font-medium">Get started with our lightweight SDK in seconds.</p>
<div class="space-y-4">
<div class="bg-slate-950 rounded-xl p-4 border border-slate-800 font-mono text-xs">
<p class="text-slate-500 mb-2"># Install with npm</p>
<p class="text-blue-400">npm <span class="text-slate-200">install @intaleq/maps-gl</span></p>
</div>
<button class="w-full btn btn-secondary text-sm group" onclick="window.open('/api/docs', '_blank')">
<i data-lucide="terminal" class="w-4 h-4 text-blue-400 group-hover:scale-110 transition-transform"></i>
View API Reference
</button>
<a href="#playground" class="w-full btn btn-secondary text-sm group">
<i data-lucide="globe" class="w-4 h-4 text-cyan-400 group-hover:scale-110 transition-transform"></i>
Try Maps Playground
</a>
</div>
</div>
</div>
<!-- API Keys Table -->
<div class="glass rounded-2xl overflow-hidden mb-12">
<div class="p-6 border-b border-slate-800 flex items-center justify-between bg-white/[0.02]">
<div>
<h3 class="text-lg font-bold">Your API Keys</h3>
<p class="text-sm text-slate-500">Manage keys for your applications</p>
</div>
<button class="btn btn-primary" onclick="app.toggleModal('create-key-modal', true)" id="create-key-btn">
<i data-lucide="plus" class="w-4 h-4"></i>
Create New Key
</button>
</div>
<div class="overflow-x-auto min-h-[200px]">
<table class="w-full text-left">
<thead>
<tr class="text-xs uppercase tracking-widest text-slate-500 bg-slate-900/40">
<th class="px-6 py-4 font-black">Name</th>
<th class="px-6 py-4 font-black">API Key</th>
<th class="px-6 py-4 font-black">Status</th>
<th class="px-6 py-4 font-black">Restrictions</th>
<th class="px-6 py-4 font-black text-right">Actions</th>
</tr>
</thead>
<tbody id="keys-table-body" class="divide-y divide-slate-800/50">
<!-- Injected by JS -->
<tr>
<td colspan="5" class="py-20 text-center text-slate-500">
<div class="flex flex-col items-center gap-4">
<i data-lucide="loader-2" class="w-8 h-8 animate-spin text-blue-500"></i>
<p>Fetching your secure keys...</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- Playground Section -->
<section id="playground" class="page-section">
<div class="mb-12">
<h2 class="text-4xl text-gradient mb-2">Maps Playground</h2>
<p class="text-slate-400">Test your API keys and visualize vector tiles in real-time</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Sidebar Controls -->
<div class="lg:col-span-1 space-y-6">
<div class="glass p-6 rounded-2xl">
<h4 class="text-[10px] uppercase font-black tracking-widest text-slate-500 mb-4">Configuration</h4>
<div class="space-y-4">
<div>
<label class="text-xs text-slate-400 mb-2 block">Active API Key</label>
<select id="pg-key-select" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20"></select>
</div>
<div>
<label class="text-xs text-slate-400 mb-2 block">Map Style</label>
<div class="flex gap-2">
<button onclick="playground.setStyle('obsidian')" id="style-obsidian" class="flex-1 py-3 text-xs font-bold rounded-xl bg-blue-600 text-white shadow-lg shadow-blue-500/20">Obsidian</button>
<button onclick="playground.setStyle('light')" id="style-light" class="flex-1 py-3 text-xs font-bold rounded-xl bg-slate-900 text-slate-500">Light</button>
</div>
</div>
</div>
</div>
</div>
<!-- Map Container -->
<div class="lg:col-span-3 glass rounded-3xl overflow-hidden relative" style="min-height: 600px;">
<div id="map" class="absolute inset-0 w-full h-full bg-slate-900"></div>
<!-- Search Overlay -->
<div class="absolute top-6 left-6 w-full max-w-sm">
<div class="relative">
<i data-lucide="search" class="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500 w-4 h-4"></i>
<input type="text" id="pg-search" placeholder="Search Amman, Jordan..."
class="w-full bg-slate-950/80 backdrop-blur-md border border-slate-800 rounded-2xl px-12 py-4 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 shadow-2xl">
</div>
</div>
</div>
</section>
<!-- Analytics Section -->
<section id="analytics" class="page-section">
<div class="mb-12">
<h2 class="text-4xl text-gradient mb-2">Analytics</h2>
<p class="text-slate-400">Deep insights into your API performance</p>
</div>
<!-- Simple Chart Mockups -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-12">
<div class="glass p-8 rounded-3xl">
<h3 class="text-xl font-bold mb-8">Request Volume (Overall)</h3>
<div class="h-80 w-full flex items-end justify-between gap-4 px-4" id="v-chart">
<!-- Injected by analytics.js -->
</div>
</div>
<div class="glass p-8 rounded-3xl">
<h3 class="text-xl font-bold mb-8">Success Ratio</h3>
<div class="flex items-center justify-center h-80">
<div class="relative w-48 h-48 rounded-full border-[12px] border-slate-900 flex items-center justify-center">
<svg class="absolute inset-0 w-full h-full -rotate-90">
<circle cx="96" cy="96" r="88" fill="transparent" stroke="currentColor" stroke-width="12" class="text-blue-600" id="success-circle" stroke-dasharray="552.9" stroke-dashoffset="0" />
</svg>
<span class="text-3xl font-black" id="success-percent-display">100%</span>
</div>
</div>
</div>
</div>
<div class="glass rounded-3xl overflow-hidden">
<div class="p-8 border-b border-white/5">
<h3 class="text-xl font-bold">Latency Breakdown</h3>
</div>
<div class="p-8 h-80 flex items-end justify-around gap-2" id="l-chart"></div>
</div>
</section>
<!-- Map Refinement Section -->
<section id="refinement" class="page-section">
<div class="mb-12">
<h2 class="text-4xl text-gradient mb-2">Place Audit</h2>
<p class="text-slate-400">Review and approve user-suggested map locations</p>
</div>
<div class="glass rounded-3xl overflow-hidden">
<div class="overflow-x-auto min-h-[400px]">
<table class="w-full text-left">
<thead>
<tr class="text-xs uppercase tracking-widest text-slate-500 bg-slate-900/40">
<th class="px-8 py-5 font-black">Location Name</th>
<th class="px-8 py-5 font-black">Category</th>
<th class="px-8 py-5 font-black">Coordinates</th>
<th class="px-8 py-5 font-black">Submitter</th>
<th class="px-8 py-5 font-black text-right">Actions</th>
</tr>
</thead>
<tbody id="refinement-table-body" class="divide-y divide-slate-800/50">
<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>
</tbody>
</table>
</div>
</div>
</section>
<!-- Billing Section -->
<section id="billing" class="page-section p-10">
<div class="mb-12">
<h2 class="text-3xl font-black mb-2 text-gradient">Subscription & Billing</h2>
<p class="text-slate-400 font-medium">Manage your plan, payment methods, and invoice history.</p>
</div>
<!-- Current Plan Summary -->
<div class="glass p-8 rounded-[2rem] border-white/5 mb-12 flex items-center justify-between">
<div class="flex items-center gap-6">
<div class="w-16 h-16 rounded-2xl bg-blue-500/10 flex items-center justify-center text-blue-400">
<i data-lucide="crown" class="w-8 h-8"></i>
</div>
<div>
<h3 class="text-xl font-bold mb-1">Current Plan: <span class="current-plan-name text-blue-400">FREE</span></h3>
<p class="text-slate-500 text-sm font-medium">Your monthly usage resets on the 1st of each month.</p>
</div>
</div>
<div class="flex gap-3">
<div class="px-4 py-2 rounded-xl bg-slate-900 border border-slate-800 text-xs font-bold text-slate-400 flex items-center gap-2">
<i data-lucide="shield-check" class="w-4 h-4 text-emerald-400"></i> Active
</div>
</div>
</div>
<!-- Pricing Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
<!-- Free -->
<div id="plan-card-free" class="glass p-8 rounded-[2.5rem] border-white/5 flex flex-col hover:border-white/10 transition-all">
<div class="mb-8">
<span class="text-xs font-black tracking-widest text-slate-500 uppercase">Starter</span>
<h4 class="text-4xl font-black mt-2">$0 <span class="text-sm font-medium text-slate-500">/mo</span></h4>
</div>
<ul class="space-y-4 mb-10 flex-1">
<li class="flex items-center gap-3 text-sm text-slate-300 font-medium">
<i data-lucide="check" class="w-4 h-4 text-blue-400"></i> 8,000 requests /mo
</li>
<li class="flex items-center gap-3 text-sm text-slate-300 font-medium">
<i data-lucide="check" class="w-4 h-4 text-blue-400"></i> Standard Map Tiles
</li>
<li class="flex items-center gap-3 text-sm text-slate-300 font-medium">
<i data-lucide="check" class="w-4 h-4 text-blue-400"></i> Basic Geocoding
</li>
</ul>
<button class="plan-btn w-full btn bg-slate-800 text-white py-4 rounded-2xl font-black text-sm active:scale-95 transition-all">Current Plan</button>
</div>
<!-- Pro -->
<div id="plan-card-pro" class="glass p-8 rounded-[2.5rem] border-blue-500/20 bg-blue-500/[0.02] flex flex-col relative overflow-hidden group">
<div class="absolute top-4 right-4 bg-blue-500 text-white text-[10px] font-black px-3 py-1 rounded-full uppercase tracking-widest shadow-lg shadow-blue-500/20">Popular</div>
<div class="mb-8">
<span class="text-xs font-black tracking-widest text-blue-400 uppercase">Professional</span>
<h4 class="text-4xl font-black mt-2 text-gradient">$40 <span class="text-sm font-medium text-slate-500">/mo</span></h4>
</div>
<ul class="space-y-4 mb-10 flex-1">
<li class="flex items-center gap-3 text-sm text-slate-300 font-medium">
<i data-lucide="check" class="w-4 h-4 text-blue-400"></i> 50,000 requests /mo
</li>
<li class="flex items-center gap-3 text-sm text-slate-300 font-medium">
<i data-lucide="check" class="w-4 h-4 text-blue-400"></i> 3D Building Extrusion
</li>
<li class="flex items-center gap-3 text-sm text-slate-300 font-medium">
<i data-lucide="check" class="w-4 h-4 text-blue-400"></i> Advanced Routing API
</li>
<li class="flex items-center gap-3 text-sm text-slate-300 font-medium">
<i data-lucide="check" class="w-4 h-4 text-blue-400"></i> Priority Support
</li>
</ul>
<div class="flex gap-2">
<button onclick="billing.startCheckout('PRO', 'PAYMOB')" class="plan-btn flex-1 btn bg-blue-600 hover:bg-blue-500 text-white py-4 rounded-2xl font-black text-sm shadow-xl shadow-blue-500/20 active:scale-95 transition-all">Pay with Card</button>
<button onclick="billing.startCheckout('PRO', 'BINANCE')" class="p-4 rounded-2xl bg-white/5 border border-white/5 hover:bg-white/10 transition-all text-yellow-500" title="Pay with Crypto">
<i data-lucide="bitcoin" class="w-5 h-5"></i>
</button>
</div>
</div>
<!-- Enterprise -->
<div id="plan-card-enterprise" class="glass p-8 rounded-[2.5rem] border-white/5 flex flex-col hover:border-white/10 transition-all">
<div class="mb-8">
<span class="text-xs font-black tracking-widest text-violet-400 uppercase">Enterprise</span>
<h4 class="text-4xl font-black mt-2">Custom</h4>
</div>
<ul class="space-y-4 mb-10 flex-1">
<li class="flex items-center gap-3 text-sm text-slate-300 font-medium">
<i data-lucide="check" class="w-4 h-4 text-violet-400"></i> Unlimited Requests
</li>
<li class="flex items-center gap-3 text-sm text-slate-300 font-medium">
<i data-lucide="check" class="w-4 h-4 text-violet-400"></i> Custom Data Layers
</li>
<li class="flex items-center gap-3 text-sm text-slate-300 font-medium">
<i data-lucide="check" class="w-4 h-4 text-violet-400"></i> SLA Guarantees
</li>
<li class="flex items-center gap-3 text-sm text-slate-300 font-medium">
<i data-lucide="check" class="w-4 h-4 text-violet-400"></i> Dedicated Architect
</li>
</ul>
<button class="plan-btn w-full btn border border-violet-500/30 text-violet-300 hover:bg-violet-500/10 py-4 rounded-2xl font-black text-sm active:scale-95 transition-all">Contact Sales</button>
</div>
</div>
<!-- Invoice History -->
<div class="glass rounded-[2rem] border-white/5 overflow-hidden">
<div class="p-8 border-b border-white/5 flex items-center justify-between">
<h3 class="text-lg font-bold">Transaction History</h3>
<i data-lucide="receipt" class="w-5 h-5 text-slate-500"></i>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left">
<thead>
<tr class="bg-white/[0.01]">
<th class="px-8 py-5 text-[10px] font-black uppercase tracking-widest text-slate-500">Date</th>
<th class="px-8 py-5 text-[10px] font-black uppercase tracking-widest text-slate-500">Amount</th>
<th class="px-8 py-5 text-[10px] font-black uppercase tracking-widest text-slate-500">Provider</th>
<th class="px-8 py-5 text-[10px] font-black uppercase tracking-widest text-slate-500">Status</th>
</tr>
</thead>
<tbody id="invoice-table-body">
<!-- Loaded via JS -->
</tbody>
</table>
</div>
</div>
<!-- Documentation Section -->
<section id="docs" class="page-section h-full">
<div class="flex h-full gap-8">
<!-- Docs Nav -->
<div class="w-64 flex flex-col gap-2 shrink-0">
<div class="mb-4">
<h2 class="text-xl font-black text-gradient">Guides</h2>
</div>
<a href="javascript:void(0)" data-section="getting-started" class="docs-nav-link active bg-blue-500/10 text-blue-400 p-4 rounded-2xl text-sm font-bold flex items-center gap-3 transition-all hover:bg-blue-500/5">
<i data-lucide="rocket" class="w-4 h-4"></i> Getting Started
</a>
<a href="javascript:void(0)" data-section="tiles-api" class="docs-nav-link p-4 rounded-2xl text-sm font-bold text-slate-500 flex items-center gap-3 transition-all hover:bg-white/5 hover:text-white">
<i data-lucide="map" class="w-4 h-4"></i> Map Tiles API
</a>
<a href="javascript:void(0)" data-section="geocoding-api" class="docs-nav-link p-4 rounded-2xl text-sm font-bold text-slate-500 flex items-center gap-3 transition-all hover:bg-white/5 hover:text-white">
<i data-lucide="search" class="w-4 h-4"></i> Geocoding API
</a>
<a href="javascript:void(0)" data-section="routing-api" class="docs-nav-link p-4 rounded-2xl text-sm font-bold text-slate-500 flex items-center gap-3 transition-all hover:bg-white/5 hover:text-white">
<i data-lucide="navigation" class="w-4 h-4"></i> Routing API
</a>
</div>
<!-- Docs Content Area -->
<div class="flex-1 glass rounded-[2.5rem] border-white/5 p-10 overflow-y-auto" id="docs-content">
<!-- Loaded via docs.js -->
</div>
</div>
</section>
</main>
</div>
<!-- Modals -->
<div id="create-key-modal" class="fixed inset-0 z-[100] hidden">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="app.toggleModal('create-key-modal', false)"></div>
<div class="absolute inset-0 flex items-center justify-center p-4">
<div class="glass w-full max-w-md p-8 rounded-3xl animate-in fade-in zoom-in duration-300">
<h2 class="text-2xl font-bold mb-2">Create API Key</h2>
<p class="text-sm text-slate-500 mb-8">Set up a new access point for your application.</p>
<form id="create-key-form" class="space-y-6">
<div>
<label class="block text-xs font-black uppercase tracking-widest text-slate-500 mb-2">Key Name</label>
<input type="text" id="new-key-name" placeholder="e.g. Production Web App" required
class="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20">
</div>
<div class="flex items-center gap-3 pt-4 border-t border-slate-800">
<button type="button" onclick="app.toggleModal('create-key-modal', false)" class="flex-1 btn btn-secondary">
Cancel
</button>
<button type="submit" id="btn-submit-key" class="flex-1 btn btn-primary">
Create Key
</button>
</div>
</form>
</div>
</div>
</div>
<!-- core script -->
<script src="js/i18n.js"></script>
<script src="js/auth.js"></script>
<script src="js/app.js"></script>
<script src="js/playground.js"></script>
<script src="js/analytics.js"></script>
<script src="js/billing.js"></script>
<script src="js/docs.js"></script>
</body>
</html>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+239 -348
View File
@@ -3,372 +3,263 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jordan Map Platform | Developer Dashboard</title>
<!-- Tailwind CSS Play CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- MapLibre GL JS -->
<script src="https://unpkg.com/maplibre-gl@5.1.1/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@5.1.1/dist/maplibre-gl.css" rel="stylesheet" />
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
slate: {
950: '#0a0a0b',
}
}
}
}
}
</script>
<title>Intaleq Maps | The Premium Google Maps Alternative</title>
<script src="js/lib/tailwind.min.js"></script>
<script src="js/lib/lucide.min.js"></script>
<script src="js/lib/maplibre-gl.js"></script>
<link href="css/lib/maplibre-gl.css" rel="stylesheet" />
<style>
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: #0a0a0b;
color: #f8fafc;
/* Local Font Definitions */
@font-face {
font-family: 'Plus Jakarta Sans';
src: url('fonts/pjs-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Plus Jakarta Sans';
src: url('fonts/pjs-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Cairo';
src: url('fonts/cairo-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Cairo';
src: url('fonts/cairo-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
}
.glass {
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.text-gradient {
background: linear-gradient(135deg, #fff 0%, #94a3b8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.btn {
@apply flex items-center gap-2 px-4 py-2.5 rounded-xl font-bold transition-all duration-300 active:scale-95 text-sm;
}
.btn-primary {
@apply bg-blue-600 text-white hover:bg-blue-500 shadow-lg shadow-blue-500/20 disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-secondary {
@apply bg-slate-900 text-slate-400 hover:text-white border border-slate-800 hover:border-slate-700;
}
/* Nav logic */
.page-section {
display: none;
}
.page-section.active {
display: block;
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Sidebar active state */
.nav-link.active {
@apply bg-blue-600/10 text-blue-400 border-r-2 border-blue-600;
}
body { font-family: 'Plus Jakarta Sans', sans-serif; background-color: #050505; color: #fff; }
.glass { background: rgba(20, 20, 25, 0.7); backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.05); }
.text-gradient { background: linear-gradient(135deg, #fff 0%, #94a3b8 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.blue-gradient { background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%); }
.feature-card:hover { transform: translateY(-5px); border-color: rgba(37, 99, 235, 0.3); }
.glow { box-shadow: 0 0 50px -10px rgba(37, 99, 235, 0.2); }
</style>
</head>
<body class="overflow-hidden h-screen flex">
<body class="overflow-x-hidden selection:bg-blue-500/30">
<!-- Sidebar -->
<aside class="w-64 border-r border-slate-800 bg-slate-950/50 backdrop-blur-xl flex flex-col z-50">
<div class="p-8">
<div class="flex items-center gap-3 group cursor-pointer">
<div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-blue-600 to-cyan-400 flex items-center justify-center text-white shadow-lg shadow-blue-500/20 group-hover:rotate-12 transition-transform">
<i data-lucide="layers" class="w-6 h-6"></i>
<!-- Nav -->
<nav class="fixed top-0 w-full z-50 border-b border-white/5 bg-black/50 backdrop-blur-xl">
<div class="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl blue-gradient flex items-center justify-center shadow-lg shadow-blue-500/20">
<i data-lucide="layers" class="w-6 h-6 text-white text-white"></i>
</div>
<div>
<h1 class="font-black text-lg tracking-tight leading-none">Intaleq</h1>
<span class="text-[10px] uppercase tracking-[0.2em] font-bold text-slate-500">Maps SaaS</span>
<span class="font-black text-xl tracking-tight">Intaleq <span class="text-blue-500">Maps</span></span>
</div>
<div class="hidden md:flex items-center gap-8 text-sm font-bold text-slate-400">
<a href="#features" class="hover:text-white transition-colors" data-i18n="nav-features">Features</a>
<a href="#comparison" class="hover:text-white transition-colors" data-i18n="nav-why">Why Us?</a>
<a href="#pricing" class="hover:text-white transition-colors" data-i18n="nav-pricing">Pricing</a>
<button onclick="i18n.toggle()" class="flex items-center gap-2 px-3 py-1.5 rounded-lg border border-white/10 hover:bg-white/5 transition-all" data-i18n="lang-toggle">
العربية
</button>
</div>
<a href="dashboard.html" class="px-6 py-2.5 rounded-xl bg-white text-black font-black text-sm hover:scale-105 transition-all" data-i18n="nav-launch">Launch Dashboard</a>
</div>
</nav>
<!-- Hero -->
<section class="relative pt-40 pb-20 px-6 overflow-hidden">
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-[1000px] h-[600px] bg-blue-600/10 blur-[120px] rounded-full pointer-events-none"></div>
<div class="max-w-5xl mx-auto text-center relative z-10">
<div class="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-blue-500/10 border border-blue-500/20 text-blue-400 text-xs font-black uppercase tracking-widest mb-8" data-i18n="hero-badge">
<i data-lucide="sparkles" class="w-3.5 h-3.5"></i> Now with 3D Buildings in Jordan & Syria
</div>
<h1 class="text-6xl md:text-8xl font-black tracking-tighter mb-8 leading-[0.9] text-gradient" data-i18n="hero-title">
The Map API That <br> <span class="text-blue-500">Doesn't Break</span> The Bank.
</h1>
<p class="text-xl text-slate-400 max-w-2xl mx-auto mb-12 font-medium leading-relaxed" data-i18n="hero-desc">
Build premium location-based apps with high-fidelity vector tiles, optimized routing, and 3D buildings. 85% cheaper than Google Maps.
</p>
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
<a href="dashboard.html" class="w-full sm:w-auto px-8 py-4 rounded-2xl blue-gradient text-white font-black text-lg shadow-2xl shadow-blue-500/30 hover:scale-105 transition-all flex items-center justify-center gap-3" data-i18n="hero-cta-start">
Start Building Free <i data-lucide="arrow-right" class="w-5 h-5"></i>
</a>
<a href="#comparison" class="w-full sm:w-auto px-8 py-4 rounded-2xl glass text-white font-black text-lg hover:bg-white/5 transition-all text-center" data-i18n="hero-cta-view">
View Comparison
</a>
</div>
</div>
</section>
<section class="px-6 pb-40">
<div class="max-w-6xl mx-auto glass rounded-[3rem] p-4 glow relative overflow-hidden group">
<div id="hero-map" class="rounded-[2rem] overflow-hidden bg-slate-900 aspect-video relative">
<!-- Live Map Context -->
</div>
<!-- Overlay Info -->
<div class="absolute bottom-12 left-12 max-w-md pointer-events-none">
<div class="glass p-6 rounded-3xl border-blue-500/20 backdrop-blur-md">
<h3 class="text-xl font-black mb-2 flex items-center gap-2 italic">
<i data-lucide="box" class="text-blue-400 w-5 h-5"></i> REAL-TIME 3D
</h3>
<p class="text-sm text-slate-400 font-medium">Render every skyscraper in Amman and every street in Damascus with sub-meter precision and beautiful 3D extrusions.</p>
</div>
</div>
</div>
</section>
<nav class="flex-1 px-4 space-y-2 mt-4">
<a href="#home" class="nav-link flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:bg-white/5 text-slate-400 font-bold group" id="nav-home">
<i data-lucide="layout-dashboard" class="w-5 h-5 group-hover:text-white transition-colors"></i>
Dashboard
</a>
<a href="#playground" class="nav-link flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:bg-white/5 text-slate-400 font-bold group" id="nav-playground">
<i data-lucide="terminal" class="w-5 h-5 group-hover:text-white transition-colors"></i>
Playground
</a>
<a href="#analytics" class="nav-link flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:bg-white/5 text-slate-400 font-bold group" id="nav-analytics">
<i data-lucide="bar-chart-3" class="w-5 h-5 group-hover:text-white transition-colors"></i>
Analytics
</a>
<a href="#billing" class="nav-link flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:bg-white/5 text-slate-400 font-bold group" id="nav-billing">
<i data-lucide="credit-card" class="w-5 h-5 group-hover:text-white transition-colors"></i>
Billing
</a>
<a href="#docs" class="nav-link flex items-center gap-3 px-4 py-3 rounded-xl transition-all hover:bg-white/5 text-slate-400 font-bold group" id="nav-docs">
<i data-lucide="book-open" class="w-5 h-5 group-hover:text-white transition-colors"></i>
Documentation
</a>
</nav>
<div class="p-6">
<div class="glass p-5 rounded-2xl border border-blue-500/10 bg-gradient-to-br from-blue-600/5 to-transparent">
<p class="text-[10px] font-black uppercase tracking-wider text-blue-400 mb-2">Beta Access</p>
<p class="text-xs text-slate-400 leading-relaxed font-medium mb-4">You're currently on the free sandbox tier.</p>
<button class="w-full btn btn-primary !py-2 !text-xs">Upgrade Plan</button>
<!-- Comparison Table -->
<section id="comparison" class="py-24 px-6 bg-white/[0.01]">
<div class="max-w-5xl mx-auto">
<div class="text-center mb-16">
<h2 class="text-4xl font-black mb-4">Numbers Don't Lie.</h2>
<p class="text-slate-400 font-medium">Switch from Google Maps and save thousands every month.</p>
</div>
</div>
</aside>
<!-- Main Content -->
<div class="flex-1 flex flex-col min-w-0 bg-gradient-to-br from-slate-950 via-[#0f1115] to-slate-950 overflow-y-auto">
<!-- Header -->
<header class="h-20 border-b border-white/[0.03] flex items-center justify-between px-8 sticky top-0 bg-slate-950/50 backdrop-blur-md z-40">
<div class="flex items-center gap-4">
<div class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
<span class="text-xs font-black uppercase tracking-widest text-slate-500">System Operational</span>
</div>
<div class="flex items-center gap-6">
<div class="flex flex-col items-end">
<p class="text-sm font-bold" id="tenant-name">Loading...</p>
<p class="text-[10px] text-slate-500 font-medium" id="tenant-email">developer@intaleq.com</p>
</div>
<div class="w-10 h-10 rounded-full bg-slate-900 border border-slate-800 flex items-center justify-center text-blue-400">
<i data-lucide="user" class="w-5 h-5"></i>
</div>
</div>
</header>
<main class="p-12 max-w-7xl mx-auto w-full">
<!-- Home Section -->
<section id="home" class="page-section">
<div class="mb-12">
<h2 class="text-4xl text-gradient mb-2" id="welcome-msg">Welcome back...</h2>
<p class="text-slate-400">Everything you need to build with premium Jordan Map Platform API</p>
<div class="glass rounded-[2.5rem] overflow-hidden">
<table class="w-full text-left">
<thead>
<tr class="border-b border-white/5 bg-blue-600/5">
<th class="px-8 py-6 text-sm font-black uppercase tracking-widest text-slate-400">Feature</th>
<th class="px-8 py-6 text-sm font-black uppercase tracking-widest text-slate-400 text-center">Google Maps</th>
<th class="px-8 py-6 text-sm font-black uppercase tracking-widest text-blue-400 text-center bg-blue-500/10">Intaleq Maps</th>
</tr>
</thead>
<tbody class="divide-y divide-white/5">
<tr>
<td class="px-8 py-6 font-bold">50,000 API Requests</td>
<td class="px-8 py-6 text-center text-red-400 font-medium">$350 / mo</td>
<td class="px-8 py-6 text-center text-emerald-400 font-black bg-blue-500/5">$40 / mo</td>
</tr>
<tr>
<td class="px-8 py-6 font-bold">Map Tiles (Vector)</td>
<td class="px-8 py-6 text-center text-slate-400">Charged per load</td>
<td class="px-8 py-6 text-center text-white font-black bg-blue-500/5">UNLIMITED / FREE</td>
</tr>
<tr>
<td class="px-8 py-6 font-bold">3D Buildings Data</td>
<td class="px-8 py-6 text-center text-slate-400">Limited in Levant</td>
<td class="px-8 py-6 text-center text-white font-black bg-blue-500/5">Full Coverage (Jordan/Syria)</td>
</tr>
<tr>
<td class="px-8 py-6 font-bold">Bill Transparency</td>
<td class="px-8 py-6 text-center text-slate-400">Hidden Costs</td>
<td class="px-8 py-6 text-center text-white font-black bg-blue-500/5">Fixed Pricing</td>
</tr>
</tbody>
</table>
</div>
<p class="mt-8 text-center text-xs text-slate-500 font-bold uppercase tracking-widest leading-loose">
* Based on public pricing as of April 2026. Savings calculated on equivalent request volume.
</p>
</div>
</section>
<!-- Features -->
<section id="features" class="py-40 px-6">
<div class="max-w-7xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="glass p-10 rounded-[2.5rem] feature-card transition-all duration-300">
<div class="w-14 h-14 rounded-2xl bg-blue-500/10 flex items-center justify-center text-blue-400 mb-8">
<i data-lucide="zap" class="w-7 h-7"></i>
</div>
<!-- KPI Stats -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
<div class="glass p-6 rounded-2xl group border-[#1e293b]">
<div class="flex items-center justify-between mb-4">
<div class="w-10 h-10 rounded-xl bg-slate-900 flex items-center justify-center text-blue-400">
<i data-lucide="zap" class="w-5 h-5"></i>
</div>
<span class="text-xs font-bold px-2 py-1 rounded-full bg-slate-900 text-emerald-500">+12.5%</span>
</div>
<p class="text-sm text-slate-500 font-medium mb-1">Total Requests</p>
<div class="text-2xl font-bold tracking-tight">42,891</div>
</div>
<div class="glass p-6 rounded-2xl group">
<div class="flex items-center justify-between mb-4">
<div class="w-10 h-10 rounded-xl bg-slate-900 flex items-center justify-center text-emerald-400">
<i data-lucide="shield-check" class="w-5 h-5"></i>
</div>
<span class="text-xs font-bold px-2 py-1 rounded-full bg-slate-900 text-emerald-500">+0.01%</span>
</div>
<p class="text-sm text-slate-500 font-medium mb-1">Success Rate</p>
<div class="text-2xl font-bold tracking-tight">99.98%</div>
</div>
<div class="glass p-6 rounded-2xl group">
<div class="flex items-center justify-between mb-4">
<div class="w-10 h-10 rounded-xl bg-slate-900 flex items-center justify-center text-cyan-400">
<i data-lucide="activity" class="w-5 h-5"></i>
</div>
<span class="text-xs font-bold px-2 py-1 rounded-full bg-slate-900 text-slate-500">Stable</span>
</div>
<p class="text-sm text-slate-500 font-medium mb-1">Active Keys</p>
<div class="text-2xl font-bold tracking-tight" id="active-keys-count">...</div>
</div>
<div class="glass p-6 rounded-2xl group">
<div class="flex items-center justify-between mb-4">
<div class="w-10 h-10 rounded-xl bg-slate-900 flex items-center justify-center text-violet-400">
<i data-lucide="trending-up" class="w-5 h-5"></i>
</div>
<span class="text-xs font-bold px-2 py-1 rounded-full bg-slate-900 text-slate-500">-12ms</span>
</div>
<p class="text-sm text-slate-500 font-medium mb-1">Map Load Speed</p>
<div class="text-2xl font-bold tracking-tight">342ms</div>
</div>
<h3 class="text-2xl font-black mb-4" data-i18n="feat-latency-title">Zero Latency</h3>
<p class="text-slate-400 font-medium leading-relaxed" data-i18n="feat-latency-desc">Our infrastructure is optimized for MENA region, ensuring map tiles load in under 200ms anywhere in Amman or Damascus.</p>
</div>
<div class="glass p-10 rounded-[2.5rem] feature-card transition-all duration-300">
<div class="w-14 h-14 rounded-2xl bg-cyan-500/10 flex items-center justify-center text-cyan-400 mb-8">
<i data-lucide="navigation" class="w-7 h-7"></i>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
<!-- Traffic Mockup -->
<div class="lg:col-span-2 glass rounded-2xl p-6 relative overflow-hidden">
<div class="flex items-center justify-between mb-8">
<div>
<h3 class="text-lg font-bold">Request Traffic</h3>
<p class="text-sm text-slate-500">Live traffic across all API endpoints</p>
</div>
<a href="#analytics" class="text-xs text-blue-500 hover:text-blue-400 font-bold flex items-center gap-1 transition-colors">
Full Analytics <i data-lucide="arrow-right" class="w-3.5 h-3.5"></i>
</a>
</div>
<div class="h-64 flex items-end gap-2 px-2 relative" id="traffic-bars">
<!-- Bars injected by JS -->
</div>
</div>
<!-- Quick Actions -->
<div class="glass rounded-2xl p-6 bg-gradient-to-br from-blue-600/10 to-transparent border-blue-500/10">
<h3 class="text-lg font-bold mb-2">Quick Start</h3>
<p class="text-sm text-slate-500 mb-6 font-medium">Get started with our lightweight SDK in seconds.</p>
<div class="space-y-4">
<div class="bg-slate-950 rounded-xl p-4 border border-slate-800 font-mono text-xs">
<p class="text-slate-500 mb-2"># Install with npm</p>
<p class="text-blue-400">npm <span class="text-slate-200">install @intaleq/maps-gl</span></p>
</div>
<button class="w-full btn btn-secondary text-sm group" onclick="window.open('/api/docs', '_blank')">
<i data-lucide="terminal" class="w-4 h-4 text-blue-400 group-hover:scale-110 transition-transform"></i>
View API Reference
</button>
<a href="#playground" class="w-full btn btn-secondary text-sm group">
<i data-lucide="globe" class="w-4 h-4 text-cyan-400 group-hover:scale-110 transition-transform"></i>
Try Maps Playground
</a>
</div>
</div>
<h3 class="text-2xl font-black mb-4" data-i18n="feat-routing-title">Smart Routing</h3>
<p class="text-slate-400 font-medium leading-relaxed" data-i18n="feat-routing-desc">Enterprise-grade routing engine with support for alternative paths, traffic awareness, and custom road constraints.</p>
</div>
<div class="glass p-10 rounded-[2.5rem] feature-card transition-all duration-300">
<div class="w-14 h-14 rounded-2xl bg-violet-500/10 flex items-center justify-center text-violet-400 mb-8">
<i data-lucide="shield-check" class="w-7 h-7"></i>
</div>
<!-- API Keys Table -->
<div class="glass rounded-2xl overflow-hidden mb-12">
<div class="p-6 border-b border-slate-800 flex items-center justify-between bg-white/[0.02]">
<div>
<h3 class="text-lg font-bold">Your API Keys</h3>
<p class="text-sm text-slate-500">Manage keys for your applications</p>
</div>
<button class="btn btn-primary" onclick="app.toggleModal('create-key-modal', true)" id="create-key-btn">
<i data-lucide="plus" class="w-4 h-4"></i>
Create New Key
</button>
</div>
<div class="overflow-x-auto min-h-[200px]">
<table class="w-full text-left">
<thead>
<tr class="text-xs uppercase tracking-widest text-slate-500 bg-slate-900/40">
<th class="px-6 py-4 font-black">Name</th>
<th class="px-6 py-4 font-black">API Key</th>
<th class="px-6 py-4 font-black">Status</th>
<th class="px-6 py-4 font-black">Restrictions</th>
<th class="px-6 py-4 font-black text-right">Actions</th>
</tr>
</thead>
<tbody id="keys-table-body" class="divide-y divide-slate-800/50">
<!-- Injected by JS -->
<tr>
<td colspan="5" class="py-20 text-center text-slate-500">
<div class="flex flex-col items-center gap-4">
<i data-lucide="loader-2" class="w-8 h-8 animate-spin text-blue-500"></i>
<p>Fetching your secure keys...</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- Playground Section -->
<section id="playground" class="page-section">
<div class="mb-12">
<h2 class="text-4xl text-gradient mb-2">Maps Playground</h2>
<p class="text-slate-400">Test your API keys and visualize vector tiles in real-time</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Sidebar Controls -->
<div class="lg:col-span-1 space-y-6">
<div class="glass p-6 rounded-2xl">
<h4 class="text-[10px] uppercase font-black tracking-widest text-slate-500 mb-4">Configuration</h4>
<div class="space-y-4">
<div>
<label class="text-xs text-slate-400 mb-2 block">Active API Key</label>
<select id="pg-key-select" class="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20"></select>
</div>
<div>
<label class="text-xs text-slate-400 mb-2 block">Map Style</label>
<div class="flex gap-2">
<button onclick="playground.setStyle('obsidian')" id="style-obsidian" class="flex-1 py-3 text-xs font-bold rounded-xl bg-blue-600 text-white shadow-lg shadow-blue-500/20">Obsidian</button>
<button onclick="playground.setStyle('light')" id="style-light" class="flex-1 py-3 text-xs font-bold rounded-xl bg-slate-900 text-slate-500">Light</button>
</div>
</div>
</div>
</div>
</div>
<!-- Map Container -->
<div class="lg:col-span-3 glass rounded-3xl overflow-hidden relative" style="height: 600px;">
<div id="map" class="absolute inset-0"></div>
<!-- Search Overlay -->
<div class="absolute top-6 left-6 w-full max-w-sm">
<div class="relative">
<i data-lucide="search" class="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500 w-4 h-4"></i>
<input type="text" id="pg-search" placeholder="Search Amman, Jordan..."
class="w-full bg-slate-950/80 backdrop-blur-md border border-slate-800 rounded-2xl px-12 py-4 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 shadow-2xl">
</div>
</div>
</div>
</div>
</section>
<!-- Analytics Section -->
<section id="analytics" class="page-section">
<div class="mb-12">
<h2 class="text-4xl text-gradient mb-2">Analytics</h2>
<p class="text-slate-400">Deep insights into your API performance</p>
</div>
<!-- Simple Chart Mockups -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="glass p-8 rounded-3xl">
<h3 class="text-xl font-bold mb-8">Request Volume (Last 7 Days)</h3>
<div class="h-80 w-full flex items-end justify-between gap-4 px-4" id="v-chart"></div>
</div>
<div class="glass p-8 rounded-3xl">
<h3 class="text-xl font-bold mb-8">Service Latency (ms)</h3>
<div class="h-80 w-full flex items-end justify-between gap-4 px-4" id="l-chart"></div>
</div>
</div>
</section>
</main>
</div>
<!-- Modals -->
<div id="create-key-modal" class="fixed inset-0 z-[100] hidden">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="app.toggleModal('create-key-modal', false)"></div>
<div class="absolute inset-0 flex items-center justify-center p-4">
<div class="glass w-full max-w-md p-8 rounded-3xl animate-in fade-in zoom-in duration-300">
<h2 class="text-2xl font-bold mb-2">Create API Key</h2>
<p class="text-sm text-slate-500 mb-8">Set up a new access point for your application.</p>
<form id="create-key-form" class="space-y-6">
<div>
<label class="block text-xs font-black uppercase tracking-widest text-slate-500 mb-2">Key Name</label>
<input type="text" id="new-key-name" placeholder="e.g. Production Web App" required
class="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20">
</div>
<div class="flex gap-4 pt-4">
<button type="button" class="flex-1 btn btn-secondary" onclick="app.toggleModal('create-key-modal', false)">Cancel</button>
<button type="submit" class="flex-1 btn btn-primary" id="btn-submit-key">Create Key</button>
</div>
</form>
<h3 class="text-2xl font-black mb-4" data-i18n="feat-geocoding-title">Local Geocoding</h3>
<p class="text-slate-400 font-medium leading-relaxed" data-i18n="feat-geocoding-desc">Highly accurate search for local landmarks, neighborhoods, and buildings often missing from global providers.</p>
</div>
</div>
</div>
</section>
<!-- core script -->
<script src="js/app.js"></script>
<script src="js/playground.js"></script>
<script src="js/analytics.js"></script>
<!-- Pricing (Simplified) -->
<section id="pricing" class="py-24 px-6 relative">
<div class="max-w-3xl mx-auto glass p-12 rounded-[3rem] text-center border-blue-500/30 overflow-hidden">
<div class="absolute -top-24 -right-24 w-64 h-64 bg-blue-500/10 blur-[80px] rounded-full"></div>
<h2 class="text-4xl font-black mb-4">Simple, Transparent Pricing.</h2>
<div class="my-10">
<h4 class="text-6xl font-black text-blue-500">$40<span class="text-xl text-slate-500 font-bold tracking-normal italic"> / 50k requests</span></h4>
</div>
<ul class="space-y-4 mb-10 text-slate-400 font-bold">
<li>Everything in Free Tier included</li>
<li>Commercial License for Fleet Tracking</li>
<li>Premium 3D Map Tiles (Unlimited)</li>
<li>24/7 Priority Support</li>
</ul>
<a href="dashboard.html#billing" class="inline-flex btn px-12 py-4 rounded-2xl blue-gradient text-white font-black text-lg shadow-xl shadow-blue-500/20 hover:scale-105 transition-all">Get Professional Access</a>
</div>
</section>
<!-- Footer -->
<footer class="py-20 border-t border-white/5 text-center">
<div class="flex items-center justify-center gap-3 mb-8">
<div class="w-8 h-8 rounded-lg blue-gradient flex items-center justify-center text-white">
<i data-lucide="layers" class="w-5 h-5 text-white"></i>
</div>
<span class="font-black text-lg">Intaleq <span class="text-blue-500">Maps</span></span>
</div>
<p class="text-slate-500 text-sm font-bold tracking-widest uppercase">Intaleq Software Solutions &copy; 2026</p>
</footer>
<script src="js/i18n.js"></script>
<script>
lucide.createIcons();
// Initialize Cinematic Hero Map
try {
const map = new maplibregl.Map({
container: 'hero-map',
style: '/api/maps/styles/obsidian', // Using our local premium style
center: [35.9285, 31.9454], // Amman
zoom: 14,
pitch: 60,
bearing: -20,
interactive: false,
antialias: true
});
// Smooth rotation for cinematic feel
let angle = -20;
function rotate() {
angle += 0.05;
map.setBearing(angle % 360);
requestAnimationFrame(rotate);
}
map.on('load', () => {
rotate();
// Add 3D building layer if available in style
if (map.getSource('openmaptiles')) {
const layers = map.getStyle().layers;
const labelLayerId = layers.find(l => l.type === 'symbol' && l.layout['text-field'])?.id;
map.addLayer({
'id': '3d-buildings',
'source': 'openmaptiles',
'source-layer': 'building',
'type': 'fill-extrusion',
'minzoom': 15,
'paint': {
'fill-extrusion-color': '#334155',
'fill-extrusion-height': ['get', 'render_height'],
'fill-extrusion-base': ['get', 'render_min_height'],
'fill-extrusion-opacity': 0.6
}
}, labelLayerId);
}
});
} catch (e) {
console.warn('Hero map initialization skipped (offline/style error)');
}
</script>
</body>
</html>
+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);
}
}
};