feat: add place gate population scripts, map style assets, automated cron sync tasks, and expand dashboard and landing applications
This commit is contained in:
+28
-69
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+758
-100
File diff suppressed because one or more lines are too long
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user