feat: add place gate population scripts, map style assets, automated cron sync tasks, and expand dashboard and landing applications

This commit is contained in:
Hamza-Ayed
2026-09-19 21:22:05 +03:00
parent 82b1045f9c
commit ce31d79ba6
76 changed files with 118969 additions and 115428 deletions
+28 -69
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Routes, Route, useLocation } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import axios from 'axios';
import Sidebar from './components/Sidebar';
import Header from './components/Header';
@@ -8,82 +8,41 @@ import Analytics from './pages/Analytics';
import Billing from './pages/Billing';
import Documentation from './pages/Documentation';
import Playground from './pages/Playground';
import PublicHome from './pages/PublicHome';
import { LanguageProvider } from './i18n';
import { AuthProvider, useAuth } from './auth';
interface ApiKey {
id: string;
key: string;
name: string;
isActive: boolean;
rateLimit: number;
allowedOrigins: string[];
lastUsedAt: string | null;
}
export interface ApiKey { id: string; key: string; name: string; isActive: boolean; rateLimit: number; allowedOrigins: string[]; lastUsedAt: string | null; }
export interface Tenant { id: string; name: string; email: string; }
interface Tenant {
id: string;
name: string;
email: string;
}
const App = () => {
function ConsoleLayout() {
const { user, ready } = useAuth();
const [tenant, setTenant] = useState<Tenant | null>(null);
const [keys, setKeys] = useState<ApiKey[]>([]);
const [loading, setLoading] = useState(true);
const [connectionError, setConnectionError] = useState(false);
const location = useLocation();
const fetchData = async () => {
try {
setLoading(true);
setLoading(true); setConnectionError(false);
const tenantRes = await axios.get('/api/auth/management/me');
setTenant(tenantRes.data);
if (tenantRes.data && tenantRes.data.id) {
const keysRes = await axios.get(`/api/auth/management/keys/${tenantRes.data.id}`);
if (Array.isArray(keysRes.data)) {
setKeys(keysRes.data);
}
}
} catch (error: any) {
console.error('Failed to fetch dashboard data', error);
} finally {
setLoading(false);
}
const keysRes = await axios.get('/api/auth/management/keys');
setKeys(Array.isArray(keysRes.data) ? keysRes.data : []);
} catch (error) {
console.warn('Dashboard session is not available', error); setConnectionError(true);
} finally { setLoading(false); }
};
useEffect(() => { if (ready && user) void fetchData(); else if (ready) { setLoading(false); setConnectionError(true); } }, [ready, user]);
useEffect(() => { window.scrollTo(0, 0); }, [location.pathname]);
return <div className="console-layout"><Sidebar /><div className="console-main"><Header tenant={tenant} /><main className="console-content"><Routes>
<Route index element={<DashboardHome tenant={tenant} keys={keys} loading={loading} connectionError={connectionError} onRefresh={fetchData} />} />
<Route path="analytics" element={<Analytics />} /><Route path="billing" element={<Billing />} />
<Route path="documentation" element={<Documentation />} /><Route path="playground" element={<Playground apiKeys={keys} />} />
<Route path="*" element={<Navigate to="/console" replace />} />
</Routes></main></div></div>;
}
useEffect(() => {
fetchData();
}, []);
// Scroll to top on route change
useEffect(() => {
window.scrollTo(0, 0);
}, [location.pathname]);
return (
<div className="app-container">
<Sidebar />
<div className="flex-1 overflow-y-auto bg-gradient-to-br from-[#0a0a0b] via-[#0f1115] to-[#0a0a0b]">
<Header />
<main className="main-content">
<Routes>
<Route path="/" element={
<DashboardHome
tenant={tenant}
keys={keys}
loading={loading}
onRefresh={fetchData}
/>
} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/billing" element={<Billing />} />
<Route path="/documentation" element={<Documentation />} />
<Route path="/playground" element={<Playground apiKeys={keys} />} />
</Routes>
</main>
</div>
</div>
);
};
export default App;
export default function App() {
return <LanguageProvider><AuthProvider><Routes><Route path="/" element={<PublicHome />} /><Route path="/console/*" element={<ConsoleLayout />} /><Route path="*" element={<Navigate to="/" replace />} /></Routes></AuthProvider></LanguageProvider>;
}
+27
View File
@@ -0,0 +1,27 @@
/* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
import { getApp, getApps, initializeApp } from 'firebase/app';
import { GoogleAuthProvider, getAuth, onAuthStateChanged, signInWithPopup, signOut, type User } from 'firebase/auth';
import axios from 'axios';
const firebaseConfig = {
apiKey: 'AIzaSyAwybkM9FFkF2KRM0bFNAaEPNPbMZBWFn8',
authDomain: 'intaleq-map.firebaseapp.com',
projectId: 'intaleq-map',
storageBucket: 'intaleq-map.firebasestorage.app',
messagingSenderId: '1052695318500',
appId: '1:1052695318500:web:7913981536524a630401f1',
};
const firebaseApp = getApps().length ? getApp() : initializeApp(firebaseConfig);
const firebaseAuth = getAuth(firebaseApp);
type AuthValue = { user: User | null; ready: boolean; login: () => Promise<void>; logout: () => Promise<void> };
const AuthContext = createContext<AuthValue | null>(null);
export function AuthProvider({children}:{children:ReactNode}) {
const [user,setUser]=useState<User|null>(null); const [ready,setReady]=useState(false);
useEffect(()=>onAuthStateChanged(firebaseAuth,async next=>{setUser(next);if(next){axios.defaults.headers.common.Authorization=`Bearer ${await next.getIdToken()}`}else{delete axios.defaults.headers.common.Authorization}setReady(true)}),[]);
const value=useMemo<AuthValue>(()=>({user,ready,login:async()=>{await signInWithPopup(firebaseAuth,new GoogleAuthProvider())},logout:async()=>{await signOut(firebaseAuth)}}),[user,ready]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(){const value=useContext(AuthContext);if(!value)throw new Error('useAuth must be used inside AuthProvider');return value}
+9 -39
View File
@@ -1,40 +1,10 @@
import { Search, Bell, HelpCircle, ChevronDown } from 'lucide-react';
import { Languages, LogIn, LogOut, Search } from 'lucide-react';
import type { Tenant } from '../App';
import { useLanguage } from '../i18n';
import { useAuth } from '../auth';
const Header = () => {
return (
<header className="h-20 flex items-center justify-between px-8 bg-transparent sticky top-0 z-10">
<div className="relative w-96">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-500" size={18} />
<input
type="text"
placeholder="Search for keys, documentation, or settings..."
className="w-full bg-slate-900/50 border border-slate-800 rounded-xl py-2.5 pl-12 pr-4 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500/40 transition-all"
/>
</div>
<div className="flex items-center gap-6">
<div className="flex items-center gap-4 border-r border-slate-800 pr-6 mr-1">
<button className="text-slate-400 hover:text-white transition-colors relative">
<Bell size={20} />
<span className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full border-2 border-bg-deep"></span>
</button>
<button className="text-slate-400 hover:text-white transition-colors">
<HelpCircle size={20} />
</button>
</div>
<button className="flex items-center gap-2 group">
<div className="w-8 h-8 rounded-lg bg-gradient-to-tr from-slate-700 to-slate-600 border border-slate-700 overflow-hidden">
<div className="w-full h-full flex items-center justify-center text-[10px] font-bold text-slate-300">HA</div>
</div>
<div className="flex items-center gap-1">
<span className="text-sm font-medium text-slate-300 group-hover:text-white transition-colors">Hamza Admin</span>
<ChevronDown size={14} className="text-slate-500 group-hover:text-slate-300" />
</div>
</button>
</div>
</header>
);
};
export default Header;
export default function Header({ tenant }: { tenant: Tenant | null }) {
const { language, toggle, t } = useLanguage();
const { user, login, logout } = useAuth();
return <header className="console-header"><div className="console-search"><Search size={17}/><input placeholder={t('search')} /></div><div className="console-header-actions"><span className="system-state"><i/>{language === 'ar' ? 'الخدمة متاحة' : 'Service available'}</span><button onClick={toggle}><Languages size={17}/>{t('language')}</button><button onClick={()=>void(user?logout():login())}>{user?<LogOut size={16}/>:<LogIn size={16}/>} {user?(language==='ar'?'خروج':'Sign out'):(language==='ar'?'دخول':'Sign in')}</button><div className="console-user"><span>{tenant?.name || user?.displayName || t('developer')}<small>{tenant?.email || user?.email || (language === 'ar' ? 'سجّل الدخول لإدارة حسابك' : 'Sign in to manage your account')}</small></span><b>{(tenant?.name||user?.displayName)?.slice(0,2).toUpperCase() || 'DV'}</b></div></div></header>;
}
+7 -85
View File
@@ -1,87 +1,9 @@
import {
LayoutDashboard,
Key,
BarChart3,
CreditCard,
Settings,
LogOut,
Map as MapIcon,
Terminal
} from 'lucide-react';
import { NavLink } from 'react-router-dom';
import { BarChart3, BookOpen, CreditCard, FlaskConical, LayoutDashboard, Layers3, PanelLeftClose } from 'lucide-react';
import { Link, NavLink } from 'react-router-dom';
import { useLanguage } from '../i18n';
interface SidebarItemProps {
icon: any;
label: string;
to: string;
export default function Sidebar() {
const { t } = useLanguage();
const items = [[LayoutDashboard,t('overview'),'/console'],[BarChart3,t('analytics'),'/console/analytics'],[BookOpen,t('docs'),'/console/documentation'],[CreditCard,t('billing'),'/console/billing'],[FlaskConical,t('playground'),'/console/playground']] as const;
return <aside className="console-sidebar"><Link to="/" className="brand"><span className="brand-mark"><Layers3 size={20}/></span><span>SIRO<small>{t('brandSubtitle')}</small></span></Link><nav>{items.map(([Icon,label,to])=><NavLink key={to} to={to} end={to==='/console'} className={({isActive})=>isActive?'active':''}><Icon size={18}/><span>{label}</span></NavLink>)}</nav><div className="sidebar-bottom"><Link to="/"><PanelLeftClose size={17}/><span>{t('backWebsite')}</span></Link><small>SIRO Developer Platform<br/>v2.0</small></div></aside>;
}
const SidebarItem = ({ icon: Icon, label, to }: SidebarItemProps) => (
<NavLink
to={to}
className={({ isActive }) => `
w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300
${isActive
? 'bg-blue-600 text-white shadow-lg shadow-blue-500/20'
: 'text-slate-400 hover:text-white hover:bg-white/5'}
`}
>
<Icon size={18} />
{label}
</NavLink>
);
const Sidebar = () => {
return (
<aside className="w-72 h-screen glass border-r border-slate-800 flex flex-col p-6 sticky top-0">
<div className="flex items-center gap-3 mb-10 px-2">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-cyan-400 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/20">
<MapIcon className="text-white" size={24} />
</div>
<div>
<h2 className="text-xl font-bold tracking-tight flex items-center gap-2">
Intaleq <span className="text-blue-500">Map</span>
<span className="text-[8px] bg-blue-500/20 text-blue-500 px-1 rounded">v1.5</span>
</h2>
<p className="text-[10px] text-slate-500 uppercase tracking-widest font-black">Developer Portal</p>
</div>
</div>
<nav className="flex-1 space-y-2">
<SidebarItem icon={LayoutDashboard} label="Dashboard" to="/" />
<SidebarItem icon={BarChart3} label="Analytics" to="/analytics" />
<SidebarItem icon={Terminal} label="Documentation" to="/documentation" />
<SidebarItem icon={CreditCard} label="Billing" to="/billing" />
<SidebarItem icon={Key} label="Playground" to="/playground" />
</nav>
<div className="mt-auto pt-6 border-t border-slate-800/50">
<NavLink
to="/settings"
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-slate-400 hover:text-white hover:bg-white/5"
>
<Settings size={18} />
Project Settings
</NavLink>
<div className="mt-4 p-4 rounded-xl bg-slate-900/50 border border-slate-800">
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 rounded-full bg-slate-700"></div>
<div className="overflow-hidden">
<p className="text-sm font-semibold truncate">Hamza Al-Eghwairyeen</p>
<p className="text-xs text-slate-500 truncate">hamza@intaleq.xyz</p>
</div>
</div>
<button
className="w-full flex items-center justify-center gap-2 text-xs text-rose-500 hover:bg-rose-500/10 py-2 rounded-lg transition-colors"
>
<LogOut size={14} />
Sign Out
</button>
</div>
</div>
</aside>
);
};
export default Sidebar;
+56
View File
@@ -0,0 +1,56 @@
/* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
export type Language = 'ar' | 'en';
type Dictionary = Record<string, { ar: string; en: string }>;
const dictionary: Dictionary = {
brandSubtitle: { ar: 'منصة المطورين', en: 'Developer Platform' },
overview: { ar: 'نظرة عامة', en: 'Overview' },
analytics: { ar: 'التحليلات', en: 'Analytics' },
docs: { ar: 'التوثيق و SDKs', en: 'Docs & SDKs' },
billing: { ar: 'الاشتراك والفوترة', en: 'Plans & Billing' },
playground: { ar: 'مختبر الخرائط', en: 'Maps Playground' },
backWebsite: { ar: 'العودة للموقع', en: 'Back to website' },
language: { ar: 'English', en: 'العربية' },
console: { ar: 'لوحة المطور', en: 'Developer Console' },
search: { ar: 'ابحث في التوثيق أو المفاتيح...', en: 'Search docs or API keys...' },
developer: { ar: 'مطور', en: 'Developer' },
};
type LanguageContextValue = {
language: Language;
dir: 'rtl' | 'ltr';
toggle: () => void;
t: (key: keyof typeof dictionary) => string;
};
const LanguageContext = createContext<LanguageContextValue | null>(null);
export function LanguageProvider({ children }: { children: ReactNode }) {
const [language, setLanguage] = useState<Language>(() =>
localStorage.getItem('siro-language') === 'en' ? 'en' : 'ar'
);
useEffect(() => {
localStorage.setItem('siro-language', language);
document.documentElement.lang = language;
document.documentElement.dir = language === 'ar' ? 'rtl' : 'ltr';
}, [language]);
const value = useMemo<LanguageContextValue>(() => ({
language,
dir: language === 'ar' ? 'rtl' : 'ltr',
toggle: () => setLanguage(current => current === 'ar' ? 'en' : 'ar'),
t: key => dictionary[key][language],
}), [language]);
return <LanguageContext.Provider value={value}>{children}</LanguageContext.Provider>;
}
export function useLanguage() {
const value = useContext(LanguageContext);
if (!value) throw new Error('useLanguage must be used inside LanguageProvider');
return value;
}
File diff suppressed because one or more lines are too long
+6 -103
View File
@@ -1,104 +1,7 @@
import {
CartesianGrid,
Tooltip,
ResponsiveContainer,
AreaChart,
Area,
BarChart,
Bar,
XAxis,
YAxis
} from 'recharts';
import {
Activity,
Clock,
AlertCircle,
MousePointer2
} from 'lucide-react';
import { Activity, BarChart3, Clock, Database, Info } from 'lucide-react';
import { useLanguage } from '../i18n';
const mockData = [
{ name: 'Mon', requests: 4000, latency: 240, errors: 2 },
{ name: 'Tue', requests: 3000, latency: 198, errors: 1 },
{ name: 'Wed', requests: 2000, latency: 310, errors: 4 },
{ name: 'Thu', requests: 2780, latency: 208, errors: 1 },
{ name: 'Fri', requests: 1890, latency: 250, errors: 2 },
{ name: 'Sat', requests: 2390, latency: 210, errors: 1 },
{ name: 'Sun', requests: 3490, latency: 225, errors: 0 },
];
const Analytics = () => {
return (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-700">
<div className="mb-12">
<h1 className="text-4xl text-gradient mb-2">Analytics</h1>
<p className="text-slate-400">Deep insights into your API performance and usage patterns</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
{[
{ label: 'Avg Latency', value: '242ms', icon: Clock, color: 'text-blue-400' },
{ label: 'Error Rate', value: '0.04%', icon: AlertCircle, color: 'text-rose-400' },
{ label: 'P99 Latency', value: '410ms', icon: Activity, color: 'text-emerald-400' },
{ label: 'API Hits', value: '1.2M', icon: MousePointer2, color: 'text-violet-400' }
].map((stat, i) => (
<div key={i} className="glass p-6 rounded-2xl">
<div className="flex items-center gap-4 mb-4">
<div className={`w-10 h-10 rounded-xl bg-slate-900 flex items-center justify-center ${stat.color}`}>
<stat.icon size={20} />
</div>
<div>
<p className="text-sm text-slate-500 font-medium">{stat.label}</p>
<div className="text-2xl font-bold tracking-tight">{stat.value}</div>
</div>
</div>
</div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-12">
<div className="glass p-8 rounded-3xl">
<h3 className="text-xl font-bold mb-8">Request Volume</h3>
<div className="h-80 w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={mockData}>
<defs>
<linearGradient id="colorRequests" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis dataKey="name" stroke="#64748b" fontSize={12} tickLine={false} axisLine={false} />
<YAxis stroke="#64748b" fontSize={12} tickLine={false} axisLine={false} />
<Tooltip
contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: '12px' }}
itemStyle={{ color: '#f8fafc' }}
/>
<Area type="monotone" dataKey="requests" stroke="#3b82f6" fillOpacity={1} fill="url(#colorRequests)" strokeWidth={3} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
<div className="glass p-8 rounded-3xl">
<h3 className="text-xl font-bold mb-8">Service Latency (ms)</h3>
<div className="h-80 w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={mockData}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis dataKey="name" stroke="#64748b" fontSize={12} tickLine={false} axisLine={false} />
<YAxis stroke="#64748b" fontSize={12} tickLine={false} axisLine={false} />
<Tooltip
contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: '12px' }}
/>
<Bar dataKey="latency" fill="#8b5cf6" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
</div>
);
};
export default Analytics;
export default function Analytics() {
const { language }=useLanguage(); const ar=language==='ar';
return <div className="console-page"><div className="console-page-title"><div><span className="section-kicker">USAGE & HEALTH</span><h1>{ar?'التحليلات':'Analytics'}</h1><p>{ar?'راقب استخدام الواجهات وأداء المشروع بعد اتصال بيانات الاستخدام.':'Monitor API usage and project health once usage data is connected.'}</p></div></div><div className="console-stats">{[[Activity,ar?'طلبات API':'API requests'],[Clock,ar?'زمن الاستجابة':'Response time'],[Database,ar?'نقل البيانات':'Data transfer']].map(([Icon,label])=>{const I=Icon as typeof Activity;return <article key={String(label)}><I/><span><small>{String(label)}</small><b>—</b></span></article>})}</div><section className="console-panel analytics-empty"><BarChart3/><h2>{ar?'بانتظار بيانات الاستخدام الفعلية':'Waiting for real usage data'}</h2><p>{ar?'لن نعرض أرقامًا تجريبية على أنها أداء حقيقي. عند ربط endpoint التحليلات، ستظهر هنا الطلبات والأخطاء وزمن الاستجابة حسب المشروع والمفتاح.':'Demo numbers are not shown as real performance. Once the usage endpoint is connected, requests, errors and latency will appear here by project and key.'}</p><div><Info/><span>{ar?'مصدر البيانات المطلوب: Usage API المرتبط بالمستأجر الحالي.':'Required source: the current tenant Usage API.'}</span></div></section></div>;
}
+716 -91
View File
@@ -1,106 +1,731 @@
import {
CreditCard,
Check
import { useEffect, useState } from 'react';
import axios from 'axios';
import {
Check,
CircleDollarSign,
Loader2,
ShieldCheck,
Zap,
TrendingDown,
Server,
ArrowUpRight,
TrendingUp,
CreditCard,
Navigation
} from 'lucide-react';
import { useLanguage } from '../i18n';
const plans = [
type Subscription = {
plan?: string;
status?: string;
currentPeriodEnd?: string;
monthlyRequestLimit?: number;
};
type UsageSummary = {
monthlyUsage: number;
monthlyLimit: number;
totalToday: number;
avgLatency: number;
successRate: number;
};
const SIRO_INFRA_TIERS = [
{
name: 'Developer',
price: '$0',
description: 'Perfect for prototyping and small projects.',
features: ['50,000 monthly requests', 'Community support', 'Basic analytics', '1 API Key'],
current: true
id: 'STARTER',
price: '$350',
numericPrice: 350,
dailyRides: '10,000',
maxRides: 10000,
nameAr: 'باقة الانطلاق (Starter)',
nameEn: 'Sovereign Starter',
descAr: 'للشركات الناشئة والتطبيقات المحلية المتنامية.',
descEn: 'For growing mobility apps and early production deployments.',
qpsAr: 'حتى 2,100 طلب/دقيقة',
qpsEn: 'Up to 2,100 req/min',
sla: '99.9%',
featuresAr: [
'سعة حتى 10,000 رحلة يومياً',
'اتفاقية مستوى خدمة 99.9% SLA',
'زمن استجابة أقل من 10ms',
'سحابة سيادية مع عزل بيانات 100%',
'دعم فني وتحديثات مستمرة'
],
featuresEn: [
'Up to 10,000 rides / day capacity',
'99.9% Enterprise SLA',
'Sub-10ms edge latency',
'100% Sovereign Data Isolation',
'Standard technical support'
]
},
{
name: 'Startup',
price: '$49',
description: 'For growing apps with higher traffic needs.',
features: ['500,000 monthly requests', 'Priority support', 'Advanced analytics', '10 API Keys', 'SLA Guarantee'],
current: false,
popular: true
id: 'FLEET',
price: '$750',
numericPrice: 750,
dailyRides: '30,000',
maxRides: 30000,
nameAr: 'باقة الأساطيل (Fleet)',
nameEn: 'Advanced Fleet',
descAr: 'لأساطيل التوصيل والنقل الذكي متوسطة الحجم.',
descEn: 'For mid-size fleets and delivery networks demanding high resilience.',
qpsAr: 'حتى 5,400 طلب/دقيقة',
qpsEn: 'Up to 5,400 req/min',
sla: '99.95%',
popular: true,
featuresAr: [
'سعة حتى 30,000 رحلة يومياً',
'عنقود سيادي ثنائي متزامن ضد الانقطاع',
'توجيه ذكي متعدد الوسائط وأساطيل',
'اتفاقية مستوى خدمة 99.95% SLA',
'دعم متواصل 24/7 عبر القنوات المباشرة'
],
featuresEn: [
'Up to 30,000 rides / day capacity',
'Dual-zone redundant sovereign mesh',
'Multi-modal fleet routing engine',
'99.95% High-availability SLA',
'24/7 continuous enterprise support'
]
},
{
name: 'Business',
price: '$249',
description: 'Enterprise-grade performance and security.',
features: ['5 Million monthly requests', '24/7 Dedicated support', 'Real-time telemetry', 'Unlimited API Keys', 'Custom map styles'],
current: false
id: 'SCALE',
price: '$1,400',
numericPrice: 1400,
dailyRides: '75,000',
maxRides: 75000,
nameAr: 'التوسع الإقليمي (Scale)',
nameEn: 'Regional Scale',
descAr: 'لمنصات النقل السريع في عدة مدن ودول.',
descEn: 'For multi-city ride-hailing and regional logistics networks.',
qpsAr: 'حتى 15,000 طلب/دقيقة',
qpsEn: 'Up to 15,000 req/min',
sla: '99.99%',
featuresAr: [
'سعة حتى 75,000 رحلة يومياً',
'بنية موزعة إقليمياً مع موازنة أحمال ذكية',
'خوارزميات مطابقة الطرق (Map Matching)',
'تخصيص كامل لمعاملات الملاحة المحلية',
'مدير حساب تقني واستشاري مخصص'
],
featuresEn: [
'Up to 75,000 rides / day capacity',
'Regionally distributed load-balanced mesh',
'Advanced map-matching telemetry',
'Customized local routing models',
'Dedicated technical account strategist'
]
},
{
id: 'ENTERPRISE_FLEET',
price: '$2,800',
numericPrice: 2800,
dailyRides: '200,000',
maxRides: 200000,
nameAr: 'المؤسسات الكبرى (Enterprise Fleet)',
nameEn: 'Enterprise Mobility Fleet',
descAr: 'لشركات النقل واللوجستيات الكبرى على مستوى الشرق الأوسط.',
descEn: 'For national transport leaders and massive mobility operations.',
qpsAr: 'حتى 45,000 طلب/دقيقة',
qpsEn: 'Up to 45,000 req/min',
sla: '99.995%',
featuresAr: [
'سعة حتى 200,000 رحلة يومياً',
'توسع تلقائي فوري وفق ذروة الطلب اليومية',
'نشر سحابي خاص (Private Cloud / VPC)',
'تشفير سيادي عسكري للبيانات المكانية',
'اتفاقية خدمة 99.995% مع ضمانات تشغيل'
],
featuresEn: [
'Up to 200,000 rides / day capacity',
'Dynamic auto-scaling private sovereign VPC',
'Private Cloud / On-premise deployment',
'Military-grade geospatial data encryption',
'99.995% SLA with financial guarantees'
]
},
{
id: 'NATIONAL_GRID',
price: '$9,900',
numericPrice: 9900,
dailyRides: '1,000,000+',
maxRides: 1000000,
nameAr: 'الشبكة السيادية الوطنية (National Grid)',
nameEn: 'National Sovereign Grid',
descAr: 'للبنى التحتية الوطنية والمؤسسات الحكومية والأمنية.',
descEn: 'For sovereign government infrastructure, defense and national grids.',
qpsAr: 'حتى 270,000+ طلب/دقيقة',
qpsEn: 'Up to 270,000+ req/min',
sla: '99.999%',
featuresAr: [
'سعة تتجاوز 1,000,000 رحلة يومياً',
'نشر داخلي كامل معزول (Air-Gapped Ready)',
'سيادة وطنية مطلقة 100% على كافة البيانات',
'اتفاقية خدمة استراتيجية 99.999% SLA',
'فريق هندسي متخصص مقيم'
],
featuresEn: [
'1,000,000+ rides / day capacity',
'Full Air-gapped on-premise deployment',
'100% Absolute national data sovereignty',
'99.999% Strategic mission-critical SLA',
'Dedicated resident engineering team'
]
}
];
const Billing = () => {
const PAY_AS_YOU_GO_RATES = [
{
serviceAr: 'البحث والإكمال (Places Autocomplete)',
serviceEn: 'Places Autocomplete',
rate: '$0.25',
unitAr: 'لكل 1,000 طلب',
unitEn: 'per 1k requests',
googlePrice: '$2.83',
savings: '91%'
},
{
serviceAr: 'تفاصيل الأماكن والترميز الجغرافي (Places Details / Geocode)',
serviceEn: 'Places Details / Geocoding',
rate: '$0.75',
unitAr: 'لكل 1,000 طلب',
unitEn: 'per 1k requests',
googlePrice: '$17.00',
savings: '95%'
},
{
serviceAr: 'الترميز الجغرافي العكسي (Reverse Geocoding)',
serviceEn: 'Reverse Geocoding',
rate: '$0.25',
unitAr: 'لكل 1,000 طلب',
unitEn: 'per 1k requests',
googlePrice: '$5.00',
savings: '95%'
},
{
serviceAr: 'حساب المسارات والملاحة (Directions & Routing)',
serviceEn: 'Directions & Routing',
rate: '$0.50',
unitAr: 'لكل 1,000 طلب',
unitEn: 'per 1k requests',
googlePrice: '$5.00 - $10.00',
savings: '90%'
},
{
serviceAr: 'مصفوفة المسافات (Distance Matrix)',
serviceEn: 'Distance Matrix',
rate: '$1.00',
unitAr: 'لكل 1,000 طلب',
unitEn: 'per 1k requests',
googlePrice: '$10.00',
savings: '90%'
},
{
serviceAr: 'عرض الخرائط المتجهة (Dynamic Vector Maps)',
serviceEn: 'Dynamic Vector Maps',
rate: '$0.25',
unitAr: 'لكل 1,000 جلسة',
unitEn: 'per 1k sessions',
googlePrice: '$7.00',
savings: '96%'
}
];
export default function Billing() {
const { language } = useLanguage();
const ar = language === 'ar';
const [activeTab, setActiveTab] = useState<'payg' | 'fixed'>('payg');
const [subscription, setSubscription] = useState<Subscription | null>(null);
const [usage, setUsage] = useState<UsageSummary | null>(null);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState('');
const [notice, setNotice] = useState('');
// Interactive Calculator State
const [calcRides, setCalcRides] = useState(15000);
useEffect(() => {
Promise.all([
axios.get('/api/billing/subscription').then((r) => r.data).catch(() => null),
axios.get('/api/usage/summary').then((r) => r.data).catch(() => null)
])
.then(([subData, usageData]) => {
if (subData) setSubscription(subData);
if (usageData) setUsage(usageData);
})
.catch(() => {
setNotice(
ar
? 'سجّل الدخول لعرض خطتك الحالية وترقيتها.'
: 'Sign in to view and upgrade your current plan.'
);
})
.finally(() => setLoading(false));
}, [ar]);
const checkout = async (plan: string) => {
try {
setBusy(plan);
const response = await axios.post('/api/billing/checkout', {
plan,
provider: 'PAYMOB'
});
if (response.data?.checkoutUrl) {
window.location.assign(response.data.checkoutUrl);
}
} catch {
setNotice(
ar
? 'تعذر بدء الدفع. تحقق من الجلسة وإعدادات مزود الدفع.'
: 'Could not start checkout. Check your session and payment provider configuration.'
);
} finally {
setBusy('');
}
};
// Calculator calculations
const googleMonthly = Math.round(calcRides * 30 * 0.055);
const siroPaygMonthly = Math.round(calcRides * 30 * 0.0035);
// Determine matching fixed tier for this volume
const matchingTier =
SIRO_INFRA_TIERS.find((t) => calcRides <= t.maxRides) ||
SIRO_INFRA_TIERS[SIRO_INFRA_TIERS.length - 1];
const siroFixedMonthly = matchingTier.numericPrice;
// Best Siro price between PAYG and Fixed Tier
const siroBestMonthly = Math.min(siroPaygMonthly, siroFixedMonthly);
const monthlySavings = Math.max(0, googleMonthly - siroBestMonthly);
const savingsPercent = googleMonthly > 0 ? Math.round((monthlySavings / googleMonthly) * 100) : 93;
// Monthly requests calculation
const freeLimit = 10000;
const currentRequests = usage?.monthlyUsage || 0;
const freePercent = Math.min(100, Math.round((currentRequests / freeLimit) * 100));
// Current plan display helper
const planId = subscription?.plan || 'FREE';
const isFreePlan = planId === 'FREE';
const isPaygPlan = planId === 'PAY_AS_YOU_GO';
return (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-700">
<div className="mb-12">
<h1 className="text-4xl text-gradient mb-2">Billing & Plans</h1>
<p className="text-slate-400">Manage your subscription, usage limits, and billing history</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
{plans.map((plan, i) => (
<div key={i} className={`glass p-8 rounded-3xl flex flex-col relative transition-transform duration-300 hover:scale-[1.02] ${plan.popular ? 'border-blue-500/50' : ''}`}>
{plan.popular && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-blue-600 text-white text-[10px] font-black uppercase tracking-widest px-4 py-1.5 rounded-full shadow-lg shadow-blue-500/20">
Most Popular
</div>
)}
<div className="mb-8">
<h3 className="text-xl font-bold mb-1">{plan.name}</h3>
<div className="flex items-baseline gap-1 mb-4">
<span className="text-4xl font-black">{plan.price}</span>
<span className="text-slate-500 text-sm font-medium">/mo</span>
</div>
<p className="text-sm text-slate-500 leading-relaxed">{plan.description}</p>
</div>
<ul className="space-y-4 mb-10 flex-1">
{plan.features.map((feature, j) => (
<li key={j} className="flex items-center gap-3 text-sm text-slate-300">
<div className="w-5 h-5 rounded-full bg-blue-500/10 flex items-center justify-center">
<Check size={12} className="text-blue-500" />
</div>
{feature}
</li>
))}
</ul>
<button className={`w-full py-4 rounded-2xl text-sm font-bold transition-all duration-300 ${plan.current ? 'bg-slate-800 text-slate-400 cursor-default' : 'bg-blue-600 hover:bg-blue-500 text-white shadow-lg shadow-blue-500/20'}`}>
{plan.current ? 'Current Plan' : 'Upgrade Plan'}
</button>
</div>
))}
</div>
<div className="glass p-8 rounded-3xl mb-12 relative overflow-hidden">
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-500/5 blur-[100px]" />
<h3 className="text-xl font-bold mb-6">Current Usage</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<div>
<div className="flex justify-between items-end mb-3">
<span className="text-sm font-bold text-slate-400 uppercase tracking-widest">Monthly Requests</span>
<span className="text-sm font-mono text-blue-400">42,891 / 50,000</span>
</div>
<div className="w-full h-3 bg-slate-950 rounded-full border border-slate-800 overflow-hidden">
<div className="h-full bg-gradient-to-r from-blue-600 to-cyan-400 rounded-full" style={{ width: '85%' }} />
</div>
<p className="text-[10px] text-slate-500 mt-3 font-medium uppercase tracking-widest">Resetting in 14 days</p>
</div>
<div className="flex items-center gap-6 p-6 bg-slate-950/50 rounded-2xl border border-slate-800">
<div className="w-12 h-12 rounded-xl bg-blue-600/10 flex items-center justify-center text-blue-500">
<CreditCard size={24} />
</div>
<div>
<p className="text-sm font-bold">Payment Method</p>
<p className="text-xs text-slate-500">Visa ending in 4242</p>
</div>
<button className="ml-auto text-xs text-blue-500 font-bold hover:underline">Update</button>
</div>
<div className="console-page">
{/* Page Title */}
<div className="console-page-title">
<div>
<span className="section-kicker">PLANS & SOVEREIGN BILLING</span>
<h1>{ar ? 'الاشتراك والفوترة والجدوى' : 'Plans, Billing & Unit Economics'}</h1>
<p>
{ar
? 'اختر بين نموذج الدفع حسب الاستهلاك (Pay-As-You-Go) أو باقات البنية التحتية السحابية الثابتة. وفر حتى 95% مقارنة بـ Google Maps.'
: 'Choose between transparent Pay-As-You-Go or predictable fixed cloud tiers. Save up to 95% compared to Google Maps.'}
</p>
</div>
</div>
{notice && (
<div className="console-alert">
<CircleDollarSign />
<div>
<b>{notice}</b>
</div>
</div>
)}
{/* Subscription & Free Trial Status Dashboard */}
<section className="billing-status-dashboard">
{/* Active Plan Card */}
<div className="billing-card active-plan-card">
<div className="card-header">
<ShieldCheck size={22} className="icon-lime" />
<div>
<small>{ar ? 'الخطة الحالية' : 'CURRENT PLAN'}</small>
<h3>
{loading
? '…'
: isFreePlan
? ar
? 'فترة تجريبية مجانية (Free Sandbox)'
: 'Free Sandbox Trial'
: isPaygPlan
? ar
? 'الدفع حسب الاستهلاك (Pay-As-You-Go)'
: 'Pay-As-You-Go'
: planId}
</h3>
</div>
</div>
<p className="plan-summary-note">
{isFreePlan
? ar
? 'لديك 10,000 طلب مجاني شهرياً لتجربة المنصة وتطوير التطبيقات بدون أي رسوم.'
: 'Includes 10,000 free requests per month for testing and app development.'
: ar
? 'استهلاك مرن وغير محدود بنصف أسعار السوق مع حماية كاملة لهوامش الأرباح.'
: 'Flexible consumption at 50% below market rates with gross margin protection.'}
</p>
<div className="plan-meta-row">
<span>
<small>{ar ? 'حالة الحساب' : 'Status'}:</small>{' '}
<b className="text-lime">{subscription?.status || (ar ? 'نشط' : 'Active')}</b>
</span>
<span>
<small>{ar ? 'نهاية الفترة' : 'Period End'}:</small>{' '}
<b>
{subscription?.currentPeriodEnd
? new Date(subscription.currentPeriodEnd).toLocaleDateString(language)
: ar
? 'تتجدد تلقائياً'
: 'Auto-renews'}
</b>
</span>
</div>
</div>
{/* Free Quota / Usage Progress */}
<div className="billing-card quota-card">
<div className="card-header">
<Zap size={22} className="icon-lime" />
<div>
<small>{ar ? 'الحصة التجريبية الشهرية' : 'MONTHLY TRIAL QUOTA'}</small>
<h3>
{currentRequests.toLocaleString()} / {freeLimit.toLocaleString()}{' '}
<small>{ar ? 'طلب' : 'reqs'}</small>
</h3>
</div>
</div>
<div className="quota-bar-wrapper">
<div className="quota-bar-fill" style={{ width: `${freePercent}%` }} />
</div>
<div className="quota-footer">
<span>
{ar
? `المتبقي: ${(Math.max(0, freeLimit - currentRequests)).toLocaleString()} طلب مجاني`
: `${(Math.max(0, freeLimit - currentRequests)).toLocaleString()} free reqs left`}
</span>
<span>{freePercent}% {ar ? 'مستخدم' : 'used'}</span>
</div>
</div>
{/* Real-Time Estimated Savings */}
<div className="billing-card savings-card">
<div className="card-header">
<TrendingDown size={22} className="icon-lime" />
<div>
<small>{ar ? 'الوفر المالي مقابل Google Maps' : 'SAVINGS VS GOOGLE MAPS'}</small>
<h3 className="text-lime">
~${(Math.round(currentRequests * 0.0515)).toLocaleString()}{' '}
<span className="badge-pill">وفر 90%+</span>
</h3>
</div>
</div>
<p className="savings-detail">
{ar
? 'توفر سيرو أكثر من 0.30$ في كل رحلة مكتملة مقارنة بتكلفة Google Maps التراكمية.'
: 'Siro saves $0.30+ per completed ride compared to legacy Google Maps billing spikes.'}
</p>
</div>
</section>
{/* Pricing Models Tabs */}
<div className="pricing-model-tabs-box">
<div className="model-tabs">
<button
className={`model-tab-btn ${activeTab === 'payg' ? 'active' : ''}`}
onClick={() => setActiveTab('payg')}
>
<Zap size={18} />
<span>{ar ? '1. الدفع حسب الاستهلاك (Pay-As-You-Go)' : '1. Pay-As-You-Go (50% Off)'}</span>
<small>{ar ? '10,000 طلب مجاني شهرياً' : '10k free requests / mo'}</small>
</button>
<button
className={`model-tab-btn ${activeTab === 'fixed' ? 'active' : ''}`}
onClick={() => setActiveTab('fixed')}
>
<Server size={18} />
<span>{ar ? '2. باقات البنية التحتية السحابية الثابتة' : '2. Fixed Cloud Infrastructure'}</span>
<small>{ar ? 'فاتورة شهرية ثابتة بلا مفاجآت' : 'Predictable flat monthly fee'}</small>
</button>
</div>
</div>
{/* TAB 1: PAY AS YOU GO */}
{activeTab === 'payg' && (
<section className="payg-section">
<div className="section-headline">
<div>
<span className="section-kicker">01 / PAY-AS-YOU-GO RATES</span>
<h2>{ar ? 'تسعير واجهات البرمجة حسب الاستهلاك الفعلي' : 'Transparent Per-1,000 Requests Pricing'}</h2>
<p>
{ar
? 'احصل على 10,000 طلب مجاني شهرياً، وادفع فقط نصف أسعار أقل منافس في السوق دون أي عقود ملزمة أو رسوم خفية.'
: 'Get 10,000 free requests every month. Pay 50% below the lowest market competitor with no commitments.'}
</p>
</div>
<button
className="button lime"
onClick={() => void checkout('PAY_AS_YOU_GO')}
disabled={busy !== '' || isPaygPlan}
>
{busy === 'PAY_AS_YOU_GO' ? (
<Loader2 className="spin" size={16} />
) : isPaygPlan ? (
ar ? 'خطتك المفعلة حالياً' : 'Active Plan'
) : (
<>
<CreditCard size={16} />
{ar ? 'تفعيل الدفع حسب الاستهلاك' : 'Activate Pay-As-You-Go'}
</>
)}
</button>
</div>
<div className="table-scroll-container">
<table className="pricing-comp-table">
<thead>
<tr>
<th>{ar ? 'واجهة البرمجة (API Service)' : 'API Service'}</th>
<th>{ar ? 'سعر سيرو (Siro Rate)' : 'Siro Rate'}</th>
<th>{ar ? 'وحدة الاستهلاك' : 'Billing Unit'}</th>
<th>Google Maps</th>
<th>{ar ? 'نسبة التوفير المباشر' : 'Direct Savings'}</th>
</tr>
</thead>
<tbody>
{PAY_AS_YOU_GO_RATES.map((item) => (
<tr key={item.serviceEn}>
<td>
<strong>{ar ? item.serviceAr : item.serviceEn}</strong>
</td>
<td className="siro-price siro-col">
<strong style={{ fontSize: '15px' }}>{item.rate}</strong>
</td>
<td>{ar ? item.unitAr : item.unitEn}</td>
<td className="google-price">{item.googlePrice}</td>
<td>
<span className="savings-pill">{item.savings}</span>
</td>
</tr>
))}
<tr className="summary-highlight-row">
<td>
<strong>{ar ? 'تكلفة الـ 7 طلبات للرحلة المكتملة' : '7 Core API Calls / Single Trip'}</strong>
<br />
<small style={{ color: 'var(--muted)', fontSize: '11px' }}>
{ar ? '(2 إكمال + 1 تفاصيل + 1 ترميز عكسي + 1 مصفوفة + 1 مسار + 1 خريطة)' : '(2 Autocomplete + 1 Details + 1 Rev-Geo + 1 Matrix + 1 Route + 1 Map)'}
</small>
</td>
<td className="siro-price siro-col">
<strong style={{ fontSize: '17px' }}>~$0.0035</strong>
</td>
<td>{ar ? 'لكل رحلة مكتملة' : 'per completed ride'}</td>
<td className="google-price">~$0.055 - $0.28</td>
<td>
<span className="savings-pill">وفر 93%+</span>
</td>
</tr>
</tbody>
</table>
</div>
</section>
)}
{/* TAB 2: FIXED SOVEREIGN TIERS */}
{activeTab === 'fixed' && (
<section className="fixed-tiers-section">
<div className="section-headline">
<div>
<span className="section-kicker">02 / FIXED SOVEREIGN INFRASTRUCTURE</span>
<h2>{ar ? 'باقات البنية التحتية السحابية الثابتة للأساطيل' : 'Predictable Fixed Monthly Cloud Infrastructure'}</h2>
<p>
{ar
? 'اشتراك شهري مقطوع يزيل مفاجآت فواتير الاستهلاك لكل طلب ويمنحك سعة خوادم مخصصة واتفاقية مستوى خدمة SLA حتى 99.999%.'
: 'Predictable flat monthly cloud pricing eliminating per-call billing spikes, backed by enterprise SLA up to 99.999%.'}
</p>
</div>
</div>
<div className="fixed-plans-grid">
{SIRO_INFRA_TIERS.map((tier) => {
const isCurrent = subscription?.plan === tier.id;
return (
<article key={tier.id} className={`fixed-tier-card ${tier.popular ? 'popular' : ''}`}>
{tier.popular && (
<span className="plan-badge">{ar ? 'الأكثر طلباً للأساطيل' : 'MOST POPULAR'}</span>
)}
<div className="tier-header">
<small>{tier.id}</small>
<h3>{ar ? tier.nameAr : tier.nameEn}</h3>
<div className="tier-price-box">
<strong>{tier.price}</strong>
<em>/ {ar ? 'شهر' : 'month'}</em>
</div>
<p>{ar ? tier.descAr : tier.descEn}</p>
</div>
<div className="tier-capacity-pill">
<Navigation size={14} />
<span>{ar ? `سعة حتى ${tier.dailyRides} رحلة/يوم` : `Up to ${tier.dailyRides} rides/day`}</span>
</div>
<ul className="tier-features-list">
{(ar ? tier.featuresAr : tier.featuresEn).map((f) => (
<li key={f}>
<Check size={14} />
<span>{f}</span>
</li>
))}
</ul>
<div className="tier-action-box">
{tier.id === 'NATIONAL_GRID' ? (
<a className="button ghost" href="mailto:sales@intaleqapp.com">
{ar ? 'تواصل مع المبيعات' : 'Contact Sales'}
<ArrowUpRight size={14} />
</a>
) : (
<button
className={`button ${tier.popular ? 'lime' : 'ghost'}`}
onClick={() => void checkout(tier.id)}
disabled={busy !== '' || isCurrent}
>
{busy === tier.id ? (
<Loader2 className="spin" size={15} />
) : isCurrent ? (
ar ? 'خطتك الحالية' : 'Current Plan'
) : (
<>
{ar ? 'ترقية الباقة' : 'Upgrade Plan'}
<ArrowUpRight size={14} />
</>
)}
</button>
)}
</div>
</article>
);
})}
</div>
</section>
)}
{/* Interactive Developer ROI & Invoice Calculator */}
<section className="developer-calc-section">
<div className="calc-header">
<div>
<span className="section-kicker">INTERACTIVE INVOICE CALCULATOR</span>
<h2>{ar ? 'احسب فاتورتك الشهرية والوفر المحقق بدقة' : 'Calculate Your Monthly Bill & Direct Savings'}</h2>
<p>
{ar
? 'حرّك المؤشر وفق عدد الرحلات اليومية لأسطولك لمقارنة الفاتورة التقديرية بين Google Maps وسيرو.'
: 'Slide to your fleet’s daily completed rides to instantly compare monthly billing between Google Maps and Siro.'}
</p>
</div>
</div>
<div className="calc-grid">
{/* Slider Panel */}
<div className="calc-control-panel">
<div className="slider-label-row">
<span>
<Navigation size={18} /> {ar ? 'عدد الرحلات اليومية لأسطولك:' : 'Daily Completed Rides:'}
</span>
<strong className="calc-value-pill">
{calcRides.toLocaleString()} {ar ? 'رحلة / يوم' : 'rides / day'}
</strong>
</div>
<input
type="range"
min="1000"
max="500000"
step="1000"
value={calcRides}
onChange={(e) => setCalcRides(Number(e.target.value))}
className="roi-slider"
/>
<div className="preset-buttons">
{[5000, 15000, 30000, 75000, 150000, 300000].map((preset) => (
<button
key={preset}
className={`preset-btn ${calcRides === preset ? 'active' : ''}`}
onClick={() => setCalcRides(preset)}
>
{preset >= 1000000 ? `${preset / 1000000}M` : `${preset / 1000}K`}{' '}
{ar ? 'رحلة' : 'rides'}
</button>
))}
</div>
<div className="calc-spec-note">
<ShieldCheck size={18} />
<div>
<b>{ar ? 'حماية هوامش الأرباح (Gross Margin Protection)' : 'Gross Margin Protection'}</b>
<p>
{ar
? `توفير مباشر بمقدار ~$${((calcRides * 30 * (0.055 - 0.0035))).toLocaleString()} شهرياً ينعكس مباشرة كأرباح صافية لمشروعك.`
: `Direct savings of ~$${((calcRides * 30 * (0.055 - 0.0035))).toLocaleString()} monthly directly protects your gross margin.`}
</p>
</div>
</div>
</div>
{/* Results Comparison Grid */}
<div className="calc-results-panel">
<div className="result-stat-box google">
<span className="stat-kicker">GOOGLE MAPS</span>
<h3>${googleMonthly.toLocaleString()}</h3>
<small>{ar ? 'الفاتورة الشهرية المقدرة' : 'Estimated Monthly Bill'}</small>
<div className="mini-bar-bg">
<div className="mini-bar-fill google-bar" style={{ width: '100%' }} />
</div>
<p>{ar ? 'بناءً على $0.055 لكل رحلة' : 'Based on $0.055 / trip'}</p>
</div>
<div className="result-stat-box siro">
<span className="stat-kicker">SIRO MAPS</span>
<h3 className="text-lime">${siroBestMonthly.toLocaleString()}</h3>
<small>
{ar
? `أفضل خيار: ${matchingTier.nameAr} (${siroBestMonthly === siroFixedMonthly ? 'باقة ثابتة' : 'استهلاك مرن'})`
: `Best Option: ${matchingTier.nameEn}`}
</small>
<div className="mini-bar-bg">
<div
className="mini-bar-fill siro-bar"
style={{ width: `${Math.max(4, (siroBestMonthly / googleMonthly) * 100)}%` }}
/>
</div>
<p>
{ar
? `تكلفة الرحلة: $0.0035 فقط (وفر 93%)`
: `Unit cost: $0.0035 / ride (93% savings)`}
</p>
</div>
<div className="result-stat-box savings">
<div className="savings-tag">
<TrendingUp size={14} /> {ar ? `وفر ${savingsPercent}% شهرياً` : `${savingsPercent}% Net Savings`}
</div>
<span className="stat-kicker">{ar ? 'صافي التوفير الشهري' : 'MONTHLY NET SAVINGS'}</span>
<h2 className="savings-value">${monthlySavings.toLocaleString()}</h2>
<div className="annual-note">
<span>{ar ? 'الوفر السنوي الإجمالي' : 'Annual Savings'}:</span>
<strong>${(monthlySavings * 12).toLocaleString()}</strong>
</div>
</div>
</div>
</div>
</section>
<p className="billing-footnote">
{ar
? 'جميع الأسعار محسوبة بالدولار الأمريكي وتخضع لاتفاقيات مستوى الخدمة الرسمية. للدفع بالعملات المحلية أو عبر الحوالات البنكية المباشرة، يرجى التواصل مع فريق المبيعات.'
: 'All prices in USD. For enterprise invoicing, local currency or bank transfer settlements, contact sales@intaleqapp.com.'}
</p>
</div>
);
};
export default Billing;
}
+36 -287
View File
@@ -1,300 +1,49 @@
import { useState } from 'react';
import axios from 'axios';
import {
Plus,
Copy,
Eye,
EyeOff,
RefreshCw,
Trash2,
ShieldCheck,
Globe,
Zap,
TrendingUp,
Activity,
Loader2,
Terminal,
ArrowRight
} from 'lucide-react';
import { Activity, ArrowUpRight, CheckCircle2, Copy, CreditCard, Eye, EyeOff, FlaskConical, KeyRound, Loader2, Plus, RefreshCw, ShieldCheck, Trash2, TriangleAlert } from 'lucide-react';
import { Link } from 'react-router-dom';
import type { ApiKey, Tenant } from '../App';
import { useLanguage } from '../i18n';
interface ApiKey {
id: string;
key: string;
name: string;
isActive: boolean;
rateLimit: number;
allowedOrigins: string[];
lastUsedAt: string | null;
}
type Props = { tenant: Tenant | null; keys: ApiKey[]; loading: boolean; connectionError: boolean; onRefresh: () => void; };
interface Tenant {
id: string;
name: string;
email: string;
}
interface DashboardHomeProps {
tenant: Tenant | null;
keys: ApiKey[];
loading: boolean;
onRefresh: () => void;
}
const DashboardHome = ({ tenant, keys, loading, onRefresh }: DashboardHomeProps) => {
export default function DashboardHome({ tenant, keys, loading, connectionError, onRefresh }: Props) {
const { language } = useLanguage();
const ar = language === 'ar';
const [showKeys, setShowKeys] = useState<Record<string, boolean>>({});
const [isModalOpen, setIsModalOpen] = useState(false);
const [newKeyName, setNewKeyName] = useState('');
const [newKeyLimit, setNewKeyLimit] = useState(100);
const [modal, setModal] = useState(false);
const [name, setName] = useState('');
const [limit, setLimit] = useState(100);
const [busy, setBusy] = useState(false);
const [message, setMessage] = useState('');
const toggleKeyVisibility = (id: string) => {
setShowKeys(prev => ({ ...prev, [id]: !prev[id] }));
const createKey = async () => {
if (!name.trim()) return setMessage(ar ? 'أدخل اسمًا للمفتاح.' : 'Enter a key name.');
try { setBusy(true); await axios.post('/api/auth/management/keys',{name,rateLimit:limit}); setModal(false); setName(''); setMessage(''); onRefresh(); }
catch { setMessage(ar ? 'تعذر إنشاء المفتاح. تحقق من تسجيل الدخول.' : 'Could not create the key. Check your session.'); }
finally { setBusy(false); }
};
const revoke = async (key: ApiKey) => {
if (!window.confirm(ar ? `إلغاء المفتاح “${key.name}”؟` : `Revoke “${key.name}”?`)) return;
await axios.delete(`/api/auth/management/keys/${key.id}`); onRefresh();
};
const createKey = async (name: string, limit: number) => {
if (!tenant) return;
try {
await axios.post(`/api/auth/management/keys`, {
name,
rateLimit: limit
});
onRefresh();
setIsModalOpen(false);
setNewKeyName('');
} catch (e) {
alert('Failed to create key');
}
};
return <div className="console-page">
<div className="console-page-title"><div><span className="section-kicker">DEVELOPER CONSOLE</span><h1>{ar ? `مرحبًا${tenant?.name ? `، ${tenant.name}` : ''}` : `Welcome${tenant?.name ? `, ${tenant.name}` : ''}`}</h1><p>{ar ? 'المفاتيح، التجربة، التوثيق والاشتراك في مكان واحد.' : 'Keys, testing, documentation and plans in one place.'}</p></div><Link className="button lime" to="/console/playground">{ar ? 'افتح المختبر' : 'Open playground'}<ArrowUpRight size={16}/></Link></div>
const deleteKey = async (keyId: string, keyName: string) => {
if (!window.confirm(`Are you sure you want to revoke the API key "${keyName || 'Production Key'}"? This action cannot be undone.`)) {
return;
}
try {
await axios.delete(`/api/auth/management/keys/${keyId}`);
onRefresh();
} catch (e) {
alert('Failed to revoke key');
}
};
{connectionError && <div className="console-alert"><TriangleAlert size={19}/><div><b>{ar ? 'لم يتم العثور على جلسة مطور نشطة' : 'No active developer session found'}</b><p>{ar ? 'الواجهة جاهزة، لكن بيانات الحساب تحتاج إلى تسجيل الدخول وربط Firebase الحالي بالنسخة الموحدة.' : 'The interface is ready, but account data requires sign-in through the existing Firebase connection.'}</p></div><button onClick={onRefresh}><RefreshCw size={15}/>{ar ? 'إعادة المحاولة' : 'Retry'}</button></div>}
return (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-700">
{/* Hero Section */}
<div className="mb-12">
<h1 className="text-4xl text-gradient mb-2">
{loading ? 'Welcome back...' : `Welcome back, ${tenant?.name || 'Developer'}`}
</h1>
<p className="text-slate-400">Everything you need to build with premium Jordan Map Platform API</p>
</div>
{/* KPI Section */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
{[
{ label: 'Total Requests', value: '42,891', diff: '+12.5%', icon: Zap, color: 'text-blue-400' },
{ label: 'Success Rate', value: '99.98%', diff: '+0.01%', icon: ShieldCheck, color: 'text-emerald-400' },
{ label: 'Active Keys', value: loading ? '...' : keys.length.toString(), diff: 'Stable', icon: Activity, color: 'text-cyan-400' },
{ label: 'Map Load Speed', value: '342ms', diff: '-12ms', icon: TrendingUp, color: 'text-violet-400' }
].map((stat, i) => (
<div key={i} className="glass p-6 rounded-2xl group transition-all duration-300 hover:border-slate-700">
<div className="flex items-center justify-between mb-4">
<div className={`w-10 h-10 rounded-xl bg-slate-900 flex items-center justify-center ${stat.color} group-hover:scale-110 transition-transform`}>
<stat.icon size={20} />
</div>
<span className={`text-xs font-bold px-2 py-1 rounded-full bg-slate-900 ${stat.diff.startsWith('+') ? 'text-emerald-500' : 'text-slate-500'}`}>
{stat.diff}
</span>
</div>
<p className="text-sm text-slate-500 font-medium mb-1">{stat.label}</p>
<div className="text-2xl font-bold tracking-tight">{stat.value}</div>
</div>
))}
</div>
{/* Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center px-4">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setIsModalOpen(false)}></div>
<div className="glass w-full max-w-md p-8 rounded-3xl relative animate-in fade-in zoom-in duration-300">
<h2 className="text-2xl font-bold mb-2">Create API Key</h2>
<p className="text-sm text-slate-500 mb-8">Set up a new access point for your application.</p>
<div className="space-y-6">
<div>
<label className="block text-xs font-black uppercase tracking-widest text-slate-500 mb-2">Key Name</label>
<input
type="text"
placeholder="e.g. Production Web App"
className="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"
value={newKeyName}
onChange={e => setNewKeyName(e.target.value)}
/>
</div>
<div>
<label className="block text-xs font-black uppercase tracking-widest text-slate-500 mb-2">Rate Limit (Req/Min)</label>
<input
type="number"
className="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"
value={newKeyLimit}
onChange={e => setNewKeyLimit(parseInt(e.target.value))}
/>
</div>
<div className="flex gap-4 pt-4">
<button className="flex-1 btn btn-secondary" onClick={() => setIsModalOpen(false)}>Cancel</button>
<button className="flex-1 btn btn-primary" onClick={() => createKey(newKeyName, newKeyLimit)}>Create Key</button>
</div>
</div>
</div>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
{/* Chart Area */}
<div className="lg:col-span-2 glass rounded-2xl p-6 relative overflow-hidden">
<div className="flex items-center justify-between mb-8">
<div>
<h3 className="text-lg font-bold">Request Traffic</h3>
<p className="text-sm text-slate-500">Live traffic across all API endpoints</p>
</div>
<Link to="/analytics" className="text-xs text-blue-500 hover:text-blue-400 font-bold flex items-center gap-1 transition-colors">
Full Analytics <ArrowRight size={14} />
</Link>
</div>
<div className="h-64 flex items-end gap-2 px-2 relative">
{[40, 60, 55, 80, 70, 45, 90, 85, 60, 40, 30, 55, 75, 40, 60, 55, 80, 70, 45, 90].map((h, i) => (
<div key={i} className="flex-1 bg-gradient-to-t from-blue-600/20 to-blue-400/80 rounded-t-sm relative group" style={{ height: `${h}%` }}>
<div className="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>
</div>
))}
<div className="absolute inset-0 flex flex-col justify-between pointer-events-none pr-4">
{[1, 2, 3, 4].map(i => <div key={i} className="w-full border-t border-slate-800/50 h-0"></div>)}
</div>
</div>
</div>
{/* Quick Actions */}
<div className="glass rounded-2xl p-6 bg-gradient-to-br from-blue-600/10 to-transparent border-blue-500/10">
<h3 className="text-lg font-bold mb-2">Quick Start</h3>
<p className="text-sm text-slate-500 mb-6 font-medium">Get started with our lightweight React SDK in seconds.</p>
<div className="space-y-4">
<div className="bg-slate-950 rounded-xl p-4 border border-slate-800 font-mono text-xs">
<p className="text-slate-500 mb-2"># Install with npm</p>
<p className="text-blue-400">npm <span className="text-slate-200">install @intaleq/maps-gl</span></p>
</div>
<button
className="w-full btn btn-secondary text-sm group"
onClick={() => window.open('http://188.68.36.205:3200/api/docs', '_blank')}
>
<Terminal size={16} className="text-blue-400 group-hover:scale-110 transition-transform" />
View API Reference
</button>
<Link to="/playground" className="w-full btn btn-secondary text-sm group">
<Globe size={16} className="text-cyan-400 group-hover:scale-110 transition-transform" />
Try Maps Playground
</Link>
</div>
</div>
</div>
{/* API Keys Table */}
<div className="glass rounded-2xl overflow-hidden mb-12">
<div className="p-6 border-b border-slate-800 flex items-center justify-between bg-white/[0.02]">
<div>
<h3 className="text-lg font-bold">Your API Keys</h3>
<p className="text-sm text-slate-500">Manage keys for your web and mobile applications</p>
</div>
<button
className="btn btn-primary"
onClick={() => setIsModalOpen(true)}
disabled={loading || keys.length >= 1}
title={keys.length >= 1 ? "Limit of 1 API key per developer reached" : ""}
>
<Plus size={18} />
{keys.length >= 1 ? "Key Limit Reached" : "Create New Key"}
</button>
</div>
<div className="overflow-x-auto">
{loading ? (
<div className="flex flex-col items-center justify-center p-20 gap-4">
<Loader2 className="animate-spin text-blue-500" size={40} />
<p className="text-slate-500 animate-pulse">Fetching your secure keys...</p>
</div>
) : (
<table className="w-full text-left">
<thead>
<tr className="text-xs uppercase tracking-widest text-slate-500 bg-slate-900/40">
<th className="px-6 py-4 font-black">Name</th>
<th className="px-6 py-4 font-black">API Key</th>
<th className="px-6 py-4 font-black">Status</th>
<th className="px-6 py-4 font-black">Restrictions</th>
<th className="px-6 py-4 font-black text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{keys.map((apiKey) => (
<tr key={apiKey.id} className="hover:bg-white/[0.02] transition-colors">
<td className="px-6 py-6 font-medium">{apiKey.name}</td>
<td className="px-6 py-6">
<div className="flex items-center gap-2 bg-slate-900 rounded-lg px-3 py-1.5 w-fit border border-slate-800">
<code className="text-xs text-blue-400 font-mono">
{showKeys[apiKey.id] ? apiKey.key : "in_••••••••••••••••••••••••"}
</code>
<div className="flex items-center gap-1 ml-2 border-l border-slate-800 pl-2">
<button onClick={() => toggleKeyVisibility(apiKey.id)} className="text-slate-500 hover:text-white">
{showKeys[apiKey.id] ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
<button className="text-slate-500 hover:text-white" onClick={() => navigator.clipboard.writeText(apiKey.key)}>
<Copy size={14} />
</button>
</div>
</div>
</td>
<td className="px-6 py-6">
<span className={`flex items-center gap-1.5 text-xs font-bold ${apiKey.isActive ? 'text-emerald-400' : 'text-slate-500'}`}>
<div className={`w-1.5 h-1.5 rounded-full ${apiKey.isActive ? 'bg-emerald-400 animate-pulse' : 'bg-slate-500'}`} />
{apiKey.isActive ? 'Active' : 'Inactive'}
</span>
</td>
<td className="px-6 py-6">
<div className="flex gap-2">
{Array.isArray(apiKey?.allowedOrigins) && apiKey.allowedOrigins.length > 0 ? (
apiKey.allowedOrigins.map((org: string, idx: number) => (
<span key={idx} className="px-2 py-0.5 rounded-md bg-blue-500/10 text-[10px] uppercase font-black text-blue-400 flex items-center gap-1">
{Globe ? <Globe size={10} /> : '•'} {org}
</span>
))
) : (
<span className="px-2 py-0.5 rounded-md bg-slate-800 text-[10px] uppercase font-black text-slate-400 flex items-center gap-1">
{Globe ? <Globe size={10} /> : '•'} Universal
</span>
)}
</div>
</td>
<td className="px-6 py-6 text-right">
<div className="flex items-center justify-end gap-1">
<button className="p-2 text-slate-500 hover:text-white" title="Refresh" onClick={onRefresh}>
<RefreshCw size={16} />
</button>
<button className="p-2 text-slate-500 hover:text-red-400 transition-colors" title="Revoke Key" onClick={() => deleteKey(apiKey.id, apiKey.name)}>
<Trash2 size={16} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
<div className="console-stats">
<article><KeyRound/><span><small>{ar?'المفاتيح النشطة':'Active keys'}</small><b>{loading?'—':keys.filter(k=>k.isActive).length}</b></span></article>
<article><ShieldCheck/><span><small>{ar?'حماية النطاقات':'Origin restrictions'}</small><b>{keys.some(k=>k.allowedOrigins?.length)?(ar?'مفعّلة':'Enabled'):(ar?'غير مضبوطة':'Not set')}</b></span></article>
<article><Activity/><span><small>{ar?'آخر استخدام':'Last activity'}</small><b>{keys.find(k=>k.lastUsedAt)?.lastUsedAt ? new Date(keys.find(k=>k.lastUsedAt)!.lastUsedAt!).toLocaleDateString(language) : (ar?'لا يوجد':'No data')}</b></span></article>
<article style={{ borderLeft: '2px solid var(--lime)' }}><CreditCard style={{ color: 'var(--lime)' }}/><span><small>{ar?'الخطة والفوترة':'Plan & Quota'}</small><b>{ar?'10,000 مجاني / Pay-As-You-Go':'10k Free / PAYG'}</b><Link to="/console/billing" style={{ fontSize: '10px', color: 'var(--lime)', marginTop: '4px', display: 'flex', alignItems: 'center', gap: '4px' }}>{ar?'إدارة الاشتراك والوفر':'Manage & Savings'}<ArrowUpRight size={12}/></Link></span></article>
</div>
);
};
export default DashboardHome;
<div className="console-grid"><section className="console-panel quickstart"><div className="panel-heading"><div><span>01</span><h2>{ar?'ابدأ التكامل':'Start integrating'}</h2></div><Link to="/console/documentation">{ar?'كل التوثيق':'All docs'}<ArrowUpRight size={14}/></Link></div><pre dir="ltr"><code><i>npm</i> install intaleq-maps-gl</code></pre><div className="onboarding-steps">{[[CheckCircle2,ar?'اختر حزمة التطوير':'Choose an SDK'],[KeyRound,ar?'أنشئ مفتاح API':'Create an API key'],[FlaskConical,ar?'اختبر طلبًا في المختبر':'Test a request']].map(([Icon,text],i)=>{const I=Icon as typeof CheckCircle2;return <Link to={i===0?'/console/documentation':i===1?'/console':'/console/playground'} key={String(text)}><span>{i+1}</span><I size={18}/><b>{String(text)}</b><ArrowUpRight size={14}/></Link>})}</div></section><section className="console-panel sdk-summary"><div className="panel-heading"><div><span>02</span><h2>{ar?'حزم التطوير':'SDK coverage'}</h2></div></div><div className="sdk-mini-grid">{[['JS','JavaScript / TypeScript'],['FL','Flutter'],['SW','iOS / Swift'],['KT','Android / Kotlin']].map(([mark,label])=><Link to="/console/documentation" key={mark}><span>{mark}</span><b>{label}</b><small>{ar?'دليل البدء':'Quick start'}</small></Link>)}</div><a className="postman-link" href="/siro-maps.postman_collection.json" download><Copy size={16}/><span><b>Postman Collection</b><small>{ar?'ملف آمن بدون مفاتيح حقيقية':'Safe collection with no embedded secrets'}</small></span><ArrowUpRight size={15}/></a></section></div>
<section className="console-panel keys-panel"><div className="panel-heading"><div><span>03</span><div><h2>{ar?'مفاتيح API':'API keys'}</h2><p>{ar?'أدر وصول تطبيقات الويب والموبايل.':'Manage access for web and mobile apps.'}</p></div></div><button className="button lime" onClick={()=>setModal(true)} disabled={loading}><Plus size={16}/>{ar?'مفتاح جديد':'New key'}</button></div>{loading?<div className="panel-loading"><Loader2 className="spin"/>{ar?'جاري تحميل المفاتيح...':'Loading keys...'}</div>:keys.length===0?<div className="empty-state"><KeyRound/><h3>{ar?'لا توجد مفاتيح بعد':'No keys yet'}</h3><p>{ar?'أنشئ مفتاحًا لتبدأ أول طلب من تطبيقك.':'Create a key to send your first application request.'}</p></div>:<div className="key-list">{keys.map(key=><article key={key.id}><span className={key.isActive?'key-status active':'key-status'}>{key.isActive?(ar?'نشط':'Active'):(ar?'متوقف':'Inactive')}</span><div><b>{key.name}</b><code dir="ltr">{showKeys[key.id]?key.key:'in_••••••••••••••••••••'}</code></div><small>{key.rateLimit} req/min</small><div><button onClick={()=>setShowKeys(v=>({...v,[key.id]:!v[key.id]}))} aria-label="Toggle key">{showKeys[key.id]?<EyeOff/>:<Eye/>}</button><button onClick={()=>navigator.clipboard.writeText(key.key)} aria-label="Copy key"><Copy/></button><button onClick={()=>void revoke(key)} aria-label="Revoke key"><Trash2/></button></div></article>)}</div>}</section>
{modal&&<div className="modal-backdrop" onMouseDown={()=>setModal(false)}><div className="console-modal" onMouseDown={e=>e.stopPropagation()}><span className="section-kicker">NEW CREDENTIAL</span><h2>{ar?'إنشاء مفتاح API':'Create API key'}</h2><label>{ar?'اسم المفتاح':'Key name'}<input value={name} onChange={e=>setName(e.target.value)} placeholder={ar?'تطبيق الإنتاج':'Production app'}/></label><label>{ar?'الطلبات في الدقيقة':'Requests per minute'}<input type="number" min="1" value={limit} onChange={e=>setLimit(Number(e.target.value))}/></label>{message&&<p className="form-error">{message}</p>}<div><button className="button ghost" onClick={()=>setModal(false)}>{ar?'إلغاء':'Cancel'}</button><button className="button lime" onClick={()=>void createKey()} disabled={busy}>{busy?<Loader2 className="spin"/>:<Plus/>}{ar?'إنشاء':'Create'}</button></div></div></div>}
</div>;
}
+38 -130
View File
@@ -1,136 +1,44 @@
import {
Copy,
Code2,
ExternalLink
} from 'lucide-react';
import { useState } from 'react';
import { ArrowUpRight, BookOpen, Check, Copy, Download, ExternalLink, FileJson, Search } from 'lucide-react';
import { useLanguage } from '../i18n';
const endpoints = [
{
method: 'GET',
path: '/api/geocoding/search',
desc: 'Forward geocoding: Find coordinates from a place name.',
params: ['q (string)', 'lat (opt)', 'lng (opt)']
},
{
method: 'GET',
path: '/api/geocoding/reverse',
desc: 'Reverse geocoding: Find addresses from coordinates.',
params: ['lat (number)', 'lng (number)']
},
{
method: 'GET',
path: '/api/maps/tile/{z}/{x}/{y}',
desc: 'Vector tile delivery for map rendering.',
params: []
}
];
const sdks = {
js: { name:'JavaScript / TypeScript', install:'npm install intaleq-maps-gl', code:`import { IntaleqMap } from 'intaleq-maps-gl';
const Documentation = () => {
const copyCode = (code: string) => {
navigator.clipboard.writeText(code);
};
return (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-700">
<div className="mb-12">
<h1 className="text-4xl text-gradient mb-2">Documentation</h1>
<p className="text-slate-400">Guides, API reference, and examples to help you build faster</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
{/* Navigation Sidebar (Local) */}
<div className="lg:col-span-1 space-y-6">
<div>
<h4 className="text-[10px] font-black uppercase tracking-widest text-slate-500 mb-4 px-2">Getting Started</h4>
<div className="space-y-1">
{['Introduction', 'Authentication', 'SDK Installation', 'Best Practices'].map(item => (
<button key={item} className="w-full text-left px-3 py-2 rounded-lg text-sm transition-colors text-slate-300 hover:bg-white/5 hover:text-white">
{item}
</button>
))}
</div>
</div>
<div>
<h4 className="text-[10px] font-black uppercase tracking-widest text-slate-500 mb-4 px-2">Core APIs</h4>
<div className="space-y-1">
{['Geocoding', 'Routing', 'Tileserver', 'Telemetry'].map(item => (
<button key={item} className="w-full text-left px-3 py-2 rounded-lg text-sm transition-colors text-slate-300 hover:bg-white/5 hover:text-white">
{item}
</button>
))}
</div>
</div>
</div>
{/* Content Area */}
<div className="lg:col-span-3 space-y-12">
{/* Quick Start Card */}
<div className="glass p-8 rounded-3xl relative overflow-hidden">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-xl bg-blue-600/10 flex items-center justify-center text-blue-500">
<Code2 size={24} />
</div>
<h3 className="text-xl font-bold">Quick Integration</h3>
</div>
<p className="text-slate-300 text-sm leading-relaxed mb-6">
To start using the Intaleq Map API, initialize the MapLibre client with your secure API key.
Our vector tiles are delivered in MVT format and optimized for sub-100ms delivery.
</p>
<div className="bg-slate-950 rounded-2xl border border-slate-800 overflow-hidden relative group">
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-800 bg-slate-900/50">
<span className="text-xs font-mono text-slate-500">javascript</span>
<button onClick={() => copyCode(`const map = new maplibregl.Map({
const map = new IntaleqMap({
container: 'map',
style: 'https://map-saas.intaleqapp.com/api/maps/style.json?api_key=YOUR_KEY',
center: [35.91, 31.95],
zoom: 12
});`)} className="hover:text-blue-500 transition-colors">
<Copy size={14} />
</button>
</div>
<pre className="p-6 text-xs text-blue-400 font-mono leading-relaxed overflow-x-auto">
{`const map = new maplibregl.Map({
container: 'map',
style: 'https://map-saas.intaleqapp.com/api/maps/style.json?api_key=YOUR_KEY',
center: [35.91, 31.95],
zoom: 12
});`}
</pre>
</div>
</div>
apiKey: 'YOUR_API_KEY',
styleType: 'obsidian'
});` },
flutter: { name:'Flutter', install:'intaleq_maps: ^2.3.0', code:`IntaleqMap(
apiKey: 'YOUR_API_KEY',
initialCameraPosition: CameraPosition(
target: LatLng(31.9539, 35.9106),
zoom: 14,
),
)` },
swift: { name:'iOS / Swift', install:'import IntaleqMaps', code:`IntaleqMaps.shared.configure(
apiKey: "YOUR_API_KEY"
)
{/* API Reference List */}
<div>
<h3 className="text-xl font-bold mb-6">API Endpoints</h3>
<div className="space-y-4">
{endpoints.map((ep, i) => (
<div key={i} className="glass p-6 rounded-2xl border-l-4 border-blue-500/30">
<div className="flex items-center gap-3 mb-3">
<span className="text-[10px] font-black px-2 py-0.5 rounded bg-blue-500 text-white uppercase">{ep.method}</span>
<code className="text-sm font-bold text-slate-200">{ep.path}</code>
</div>
<p className="text-xs text-slate-500 mb-4">{ep.desc}</p>
<div className="flex gap-2">
{ep.params.map((p, j) => (
<span key={j} className="text-[10px] px-2 py-0.5 rounded-full bg-slate-900 border border-slate-800 text-slate-400 lowercase font-mono">
{p}
</span>
))}
</div>
</div>
))}
</div>
<button className="mt-8 flex items-center gap-2 text-sm text-blue-500 font-bold hover:underline" onClick={() => window.open('http://188.68.36.205:3200/api/docs', '_blank')}>
Open Full Swagger Documentation <ExternalLink size={14} />
</button>
</div>
</div>
</div>
</div>
);
let geocoding = IntaleqGeocodingService()
// Search and reverse geocoding services` },
kotlin: { name:'Android / Kotlin', install:'import com.intaleq.maps.*', code:`IntaleqMaps.initialize(
context = applicationContext,
apiKey = "YOUR_API_KEY"
)
// Use IntaleqMapController for map actions` },
};
type SdkKey = keyof typeof sdks;
export default Documentation;
export default function Documentation() {
const { language } = useLanguage(); const ar=language==='ar';
const [selected,setSelected]=useState<SdkKey>('js'); const [copied,setCopied]=useState('');
const copy = async (value:string,label:string)=>{await navigator.clipboard.writeText(value);setCopied(label);window.setTimeout(()=>setCopied(''),1400)};
return <div className="console-page docs-page"><div className="console-page-title"><div><span className="section-kicker">DOCUMENTATION</span><h1>{ar?'ابنِ على سيرو':'Build with Siro'}</h1><p>{ar?'اختر منصتك، أنشئ مفتاحًا، ثم جرّب البحث والخرائط والمسارات.':'Choose your platform, create a key, then test maps, search and routing.'}</p></div><a className="button ghost" href="/api/docs" target="_blank" rel="noreferrer">OpenAPI<ExternalLink size={15}/></a></div>
<div className="docs-search"><Search/><input placeholder={ar?'ابحث في التوثيق...':'Search documentation...'}/><kbd>⌘ K</kbd></div>
<section className="console-panel sdk-docs"><div className="panel-heading"><div><span>01</span><h2>{ar?'اختر حزمة التطوير':'Choose an SDK'}</h2></div></div><div className="sdk-doc-layout"><div className="sdk-tabs">{Object.entries(sdks).map(([key,value])=><button key={key} onClick={()=>setSelected(key as SdkKey)} className={selected===key?'active':''}><span>{key.slice(0,2).toUpperCase()}</span><b>{value.name}</b>{selected===key&&<Check/>}</button>)}</div><div className="code-card"><header><span>{sdks[selected].name}</span><button onClick={()=>void copy(sdks[selected].code,'code')}><Copy/>{copied==='code'?(ar?'تم النسخ':'Copied'):(ar?'نسخ':'Copy')}</button></header><div className="install-row"><code dir="ltr">{sdks[selected].install}</code><button onClick={()=>void copy(sdks[selected].install,'install')}><Copy/></button></div><pre dir="ltr"><code>{sdks[selected].code}</code></pre><small>{ar?'راجع ملفات الحزمة داخل المستودع قبل النشر العام، فمسارات التثبيت النهائية تعتمد على قناة توزيع كل حزمة.':'Review the package files before public release; final installation paths depend on each package distribution channel.'}</small></div></div></section>
<div className="console-grid docs-bottom"><section className="console-panel"><div className="panel-heading"><div><span>02</span><h2>REST API</h2></div></div><div className="endpoint-list"><article><b><em>GET</em>/api/geocoding/search</b><p>{ar?'البحث عن الأماكن والعناوين':'Search places and addresses'}</p></article><article><b><em>GET</em>/api/geocoding/reverse</b><p>{ar?'تحويل الإحداثيات إلى عنوان':'Convert coordinates to an address'}</p></article><article><b><em>GET</em>/api/maps/style.json</b><p>{ar?'تحميل نمط الخريطة':'Load a map style'}</p></article></div><a className="text-link" href="/api/docs" target="_blank" rel="noreferrer">{ar?'افتح المرجع الكامل':'Open full reference'}<ArrowUpRight/></a></section><section className="console-panel postman-card"><FileJson/><span className="section-kicker">POSTMAN COLLECTION</span><h2>{ar?'جرّب الواجهات بدون كتابة تطبيق':'Test the APIs before writing an app'}</h2><p>{ar?'حمّل الملف، استورده إلى Postman، واضبط baseUrl وapiKey في متغيرات المجموعة. الملف لا يحتوي أسرارًا حقيقية.':'Download, import into Postman, then set baseUrl and apiKey in collection variables. No real secrets are embedded.'}</p><a className="button lime" href="/siro-maps.postman_collection.json" download><Download/>{ar?'تحميل JSON':'Download JSON'}</a></section></div>
<section className="docs-note"><BookOpen/><div><b>{ar?'مسار المطور المقترح':'Recommended developer path'}</b><p>{ar?'التوثيق ← مفتاح API ← Postman أو Playground ← SDK ← مراقبة الاستخدام والاشتراك.':'Docs → API key → Postman or Playground → SDK → usage and plan monitoring.'}</p></div></section></div>;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff