xpeditis2.0/apps/frontend/app/dashboard/docs/page.tsx
2026-03-31 16:19:35 +02:00

1114 lines
40 KiB
TypeScript

'use client';
import { useState, useEffect, Suspense } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import {
Search,
ChevronRight,
ArrowRight,
ExternalLink,
Zap,
Key,
Package,
TrendingUp,
Building2,
ShieldCheck,
AlertTriangle,
List,
Home,
Menu,
X,
CheckCircle2,
Clock,
Info,
} from 'lucide-react';
import { CodeBlock } from '@/components/docs/CodeBlock';
import { DOC_SECTIONS, ALL_NAV_ITEMS } from '@/components/docs/docsNav';
// ─── Shared sub-components ────────────────────────────────────────────────────
function H2({ children }: { children: React.ReactNode }) {
return <h2 className="text-xl font-semibold text-gray-900 mt-10 mb-3">{children}</h2>;
}
function H3({ children }: { children: React.ReactNode }) {
return <h3 className="text-base font-semibold text-gray-800 mt-6 mb-2">{children}</h3>;
}
function P({ children }: { children: React.ReactNode }) {
return <p className="text-gray-600 leading-relaxed mb-4">{children}</p>;
}
function Divider() {
return <hr className="border-gray-100 my-8" />;
}
function Callout({ type = 'info', children }: { type?: 'info' | 'warning' | 'success'; children: React.ReactNode }) {
const styles = {
info: 'bg-blue-50 border-blue-200 text-blue-800',
warning: 'bg-amber-50 border-amber-200 text-amber-800',
success: 'bg-green-50 border-green-200 text-green-800',
};
const icons = { info: Info, warning: AlertTriangle, success: CheckCircle2 };
const Icon = icons[type];
return (
<div className={`flex gap-3 p-4 rounded-lg border my-4 ${styles[type]}`}>
<Icon className="w-5 h-5 mt-0.5 flex-shrink-0" />
<div className="text-sm leading-relaxed">{children}</div>
</div>
);
}
function InlineCode({ children }: { children: React.ReactNode }) {
return (
<code className="px-1.5 py-0.5 rounded bg-gray-100 text-gray-800 text-sm font-mono">
{children}
</code>
);
}
function Table({ headers, rows }: { headers: string[]; rows: (string | React.ReactNode)[][] }) {
return (
<div className="overflow-x-auto my-4 rounded-lg border border-gray-200">
<table className="w-full text-sm">
<thead>
<tr className="bg-gray-50 border-b border-gray-200">
{headers.map(h => (
<th key={h} className="text-left px-4 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">
{h}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i} className={i % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'}>
{row.map((cell, j) => (
<td key={j} className="px-4 py-3 text-gray-700 border-b border-gray-100 last:border-0">
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
function SectionHeader({ title, description, badge }: { title: string; description: string; badge?: string }) {
return (
<div className="mb-8 pb-6 border-b border-gray-100">
{badge && (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-[#34CCCD]/10 text-[#0e9999] border border-[#34CCCD]/30 mb-4">
{badge}
</span>
)}
<h1 className="text-3xl font-bold text-gray-900 mb-2">{title}</h1>
<p className="text-lg text-gray-500">{description}</p>
</div>
);
}
// ─── Section: Home ────────────────────────────────────────────────────────────
function HomeSection({ onNavigate }: { onNavigate: (id: string) => void }) {
const cards = [
{
id: 'quickstart',
icon: Zap,
title: 'Guide de démarrage',
description: 'Faites votre première requête API en moins de 5 minutes.',
color: 'text-amber-500',
bg: 'bg-amber-50',
},
{
id: 'authentication',
icon: Key,
title: 'Authentification',
description: 'Créez et gérez vos clés API pour accéder à la plateforme.',
color: 'text-blue-500',
bg: 'bg-blue-50',
},
{
id: 'bookings',
icon: Package,
title: 'Bookings',
description: 'Créez, consultez et gérez des réservations de fret maritime.',
color: 'text-violet-500',
bg: 'bg-violet-50',
},
{
id: 'rates',
icon: TrendingUp,
title: 'Tarifs & Recherche',
description: 'Recherchez et comparez des tarifs en temps réel.',
color: 'text-emerald-500',
bg: 'bg-emerald-50',
},
{
id: 'organizations',
icon: Building2,
title: 'Organisations',
description: 'Accédez aux données de votre organisation.',
color: 'text-orange-500',
bg: 'bg-orange-50',
},
{
id: 'endpoints',
icon: List,
title: 'Référence complète',
description: 'Tous les endpoints, paramètres et réponses.',
color: 'text-gray-500',
bg: 'bg-gray-50',
},
];
return (
<div>
{/* Hero */}
<div className="relative rounded-2xl overflow-hidden mb-10 bg-gradient-to-br from-[#10183A] via-[#1a2a5e] to-[#0e3a3b]">
<div className="absolute inset-0 opacity-20"
style={{
backgroundImage: 'radial-gradient(circle at 70% 50%, #34CCCD 0%, transparent 60%)',
}}
/>
<div className="relative px-8 py-12">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/10 text-[#34CCCD] text-xs font-medium mb-4 border border-white/10">
<ShieldCheck className="w-3.5 h-3.5" />
Plans Gold &amp; Platinium uniquement
</div>
<h1 className="text-4xl font-bold text-white mb-3">
API Xpeditis
</h1>
<p className="text-gray-300 text-lg max-w-xl mb-6">
Intégrez la puissance de la freight logistics maritime dans vos applications.
Tarifs en temps réel, gestion de bookings, suivi d&apos;expéditions.
</p>
<div className="flex items-center gap-3">
<button
onClick={() => onNavigate('quickstart')}
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-[#34CCCD] text-[#10183A] font-semibold text-sm hover:bg-[#2bb8b9] transition-colors"
>
<Zap className="w-4 h-4" />
Commencer
</button>
<button
onClick={() => onNavigate('endpoints')}
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-white/10 text-white font-medium text-sm hover:bg-white/15 transition-colors border border-white/10"
>
Référence API
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Cards grid */}
<div className="mb-8">
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-4">Explorer la documentation</h2>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{cards.map(card => (
<button
key={card.id}
onClick={() => onNavigate(card.id)}
className="group text-left p-5 bg-white rounded-xl border border-gray-200 hover:border-gray-300 hover:shadow-md transition-all duration-200"
>
<div className={`w-9 h-9 rounded-lg ${card.bg} flex items-center justify-center mb-3`}>
<card.icon className={`w-4.5 h-4.5 ${card.color}`} style={{ width: 18, height: 18 }} />
</div>
<h3 className="font-semibold text-gray-900 mb-1 group-hover:text-[#10183A]">{card.title}</h3>
<p className="text-sm text-gray-500 leading-relaxed">{card.description}</p>
<div className="flex items-center gap-1 mt-3 text-xs font-medium text-gray-400 group-hover:text-[#34CCCD] transition-colors">
En savoir plus <ChevronRight className="w-3.5 h-3.5" />
</div>
</button>
))}
</div>
</div>
{/* Base URL */}
<Divider />
<H2>URL de base</H2>
<P>Toutes les requêtes API doivent être envoyées à l&apos;URL de base suivante :</P>
<CodeBlock language="bash" code="https://api.xpeditis.com" />
<P>En environnement de développement local :</P>
<CodeBlock language="bash" code="http://localhost:4000" />
</div>
);
}
// ─── Section: Quick Start ─────────────────────────────────────────────────────
function QuickStartSection({ onNavigate }: { onNavigate: (id: string) => void }) {
return (
<div>
<SectionHeader
title="Guide de démarrage"
description="Faites votre première requête API en 3 étapes."
badge="5 minutes"
/>
<Callout type="info">
L&apos;accès API est disponible uniquement sur les plans <strong>Gold</strong> et <strong>Platinium</strong>.
Rendez-vous dans <em>Paramètres Abonnement</em> pour upgrader.
</Callout>
{/* Step 1 */}
<div className="flex gap-4 mb-8">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-[#10183A] text-white flex items-center justify-center text-sm font-bold">1</div>
<div className="flex-1 pt-0.5">
<H3>Obtenir votre clé API</H3>
<P>
Dans le dashboard, rendez-vous dans <strong>Paramètres Clés API</strong>, puis cliquez sur <em>Créer une clé</em>.
La clé complète vous sera montrée <strong>une seule fois</strong> conservez-la immédiatement.
</P>
<P>Format de la clé :</P>
<CodeBlock language="bash" code="xped_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" />
</div>
</div>
{/* Step 2 */}
<div className="flex gap-4 mb-8">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-[#10183A] text-white flex items-center justify-center text-sm font-bold">2</div>
<div className="flex-1 pt-0.5">
<H3>Faire votre première requête</H3>
<P>Passez la clé dans l&apos;en-tête <InlineCode>X-API-Key</InlineCode> :</P>
<CodeBlock
language="bash"
code={`curl https://api.xpeditis.com/bookings \\
-H "X-API-Key: xped_live_votre_cle" \\
-H "Content-Type: application/json"`}
/>
<CodeBlock
language="typescript"
filename="example.ts"
code={`const response = await fetch('https://api.xpeditis.com/bookings', {
headers: {
'X-API-Key': process.env.XPEDITIS_API_KEY!,
'Content-Type': 'application/json',
},
});
const data = await response.json();
console.log(data);`}
/>
</div>
</div>
{/* Step 3 */}
<div className="flex gap-4 mb-8">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-[#10183A] text-white flex items-center justify-center text-sm font-bold">3</div>
<div className="flex-1 pt-0.5">
<H3>Lire la réponse</H3>
<P>Toutes les réponses sont en JSON. En cas de succès :</P>
<CodeBlock
language="json"
code={`{
"data": [ ... ],
"meta": {
"total": 42,
"page": 1,
"perPage": 20
}
}`}
/>
<P>En cas d&apos;erreur :</P>
<CodeBlock
language="json"
code={`{
"statusCode": 401,
"message": "Clé API invalide, expirée ou votre abonnement ne permet plus l'accès API.",
"error": "Unauthorized"
}`}
/>
</div>
</div>
<Divider />
<H2>Étapes suivantes</H2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{[
{ id: 'authentication', label: 'Gérer vos clés API', desc: 'Créer, lister et révoquer des clés' },
{ id: 'bookings', label: 'Créer un booking', desc: 'Réservez du fret maritime via l\'API' },
{ id: 'rates', label: 'Rechercher des tarifs', desc: 'Comparez les tarifs en temps réel' },
{ id: 'errors', label: 'Gestion des erreurs', desc: 'Tous les codes d\'erreur expliqués' },
].map(item => (
<button
key={item.id}
onClick={() => onNavigate(item.id)}
className="flex items-center justify-between p-4 text-left bg-white rounded-xl border border-gray-200 hover:border-[#34CCCD] hover:shadow-sm transition-all group"
>
<div>
<p className="font-medium text-gray-900 text-sm">{item.label}</p>
<p className="text-xs text-gray-500 mt-0.5">{item.desc}</p>
</div>
<ArrowRight className="w-4 h-4 text-gray-300 group-hover:text-[#34CCCD] transition-colors flex-shrink-0 ml-3" />
</button>
))}
</div>
</div>
);
}
// ─── Section: Authentication ──────────────────────────────────────────────────
function AuthenticationSection() {
return (
<div>
<SectionHeader
title="Clés API"
description="Authentifiez vos requêtes avec des clés API sécurisées."
/>
<H2>Vue d&apos;ensemble</H2>
<P>
L&apos;API Xpeditis utilise des clés API pour authentifier les requêtes.
Transmettez votre clé dans l&apos;en-tête <InlineCode>X-API-Key</InlineCode> de chaque requête.
</P>
<Callout type="warning">
Vos clés API sont confidentielles. Ne les partagez jamais dans du code public, des dépôts Git ou des forums.
En cas de compromission, révoquez immédiatement la clé depuis le dashboard.
</Callout>
<Divider />
<H2>Format de la clé</H2>
<P>Toutes les clés Xpeditis commencent par <InlineCode>xped_live_</InlineCode> suivi de 64 caractères hexadécimaux :</P>
<CodeBlock language="bash" code="xped_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" />
<Divider />
<H2>Utilisation</H2>
<H3>Header HTTP</H3>
<P>Passez votre clé dans l&apos;en-tête <InlineCode>X-API-Key</InlineCode> :</P>
<CodeBlock
language="bash"
code={`curl https://api.xpeditis.com/bookings \\
-H "X-API-Key: xped_live_votre_cle" \\
-H "Content-Type: application/json"`}
/>
<H3>Exemples par langage</H3>
<CodeBlock
language="typescript"
filename="xpeditis.ts"
code={`const XPEDITIS_API_KEY = process.env.XPEDITIS_API_KEY;
const response = await fetch('https://api.xpeditis.com/bookings', {
headers: {
'X-API-Key': XPEDITIS_API_KEY!,
'Content-Type': 'application/json',
},
});`}
/>
<CodeBlock
language="python"
filename="xpeditis.py"
code={`import os, requests
API_KEY = os.environ['XPEDITIS_API_KEY']
response = requests.get(
'https://api.xpeditis.com/bookings',
headers={
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
}
)
data = response.json()`}
/>
<CodeBlock
language="php"
filename="xpeditis.php"
code={`$apiKey = getenv('XPEDITIS_API_KEY');
$ch = curl_init('https://api.xpeditis.com/bookings');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-API-Key: $apiKey",
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);`}
/>
<Divider />
<H2>Endpoints de gestion</H2>
<P>Ces endpoints nécessitent une authentification par <strong>token JWT</strong> (connexion via le dashboard) et non une clé API.</P>
<Table
headers={['Méthode', 'Endpoint', 'Description']}
rows={[
['POST', '/api-keys', 'Créer une nouvelle clé API'],
['GET', '/api-keys', 'Lister toutes les clés de l\'organisation'],
['DELETE', '/api-keys/:id', 'Révoquer une clé API'],
]}
/>
<H3>Créer une clé</H3>
<CodeBlock
language="bash"
code={`curl -X POST https://api.xpeditis.com/api-keys \\
-H "Authorization: Bearer <access_token>" \\
-H "Content-Type: application/json" \\
-d '{
"name": "Intégration ERP Production",
"expiresAt": "2027-01-01T00:00:00.000Z"
}'`}
/>
<CodeBlock
language="json"
filename="response.json"
code={`{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Intégration ERP Production",
"keyPrefix": "xped_live_a1b2c3d4",
"isActive": true,
"lastUsedAt": null,
"expiresAt": "2027-01-01T00:00:00.000Z",
"createdAt": "2025-03-26T10:00:00.000Z",
"fullKey": "xped_live_a1b2c3d4e5f6..."
}`}
/>
<Callout type="warning">
Le champ <InlineCode>fullKey</InlineCode> est retourné <strong>une seule fois</strong>.
Stockez-le immédiatement dans un gestionnaire de secrets.
</Callout>
<Divider />
<H2>Sécurité</H2>
<H3>Stockage recommandé</H3>
<Table
headers={['Environnement', 'Méthode recommandée']}
rows={[
['Développement local', 'Fichier .env (jamais commité)'],
['CI/CD', 'Variables d\'environnement secrètes'],
['Production cloud', 'AWS Secrets Manager / HashiCorp Vault'],
['Docker / K8s', 'Kubernetes Secrets chiffrés'],
]}
/>
<H3>Rotation des clés</H3>
<P>
Effectuez une rotation régulière (tous les 90 jours recommandé) : créez une nouvelle clé, migrez votre système,
puis révoquez l&apos;ancienne.
</P>
</div>
);
}
// ─── Section: Bookings ────────────────────────────────────────────────────────
function BookingsSection() {
return (
<div>
<SectionHeader
title="Bookings"
description="Créez et gérez des réservations de fret maritime via l'API."
/>
<H2>Lister les bookings</H2>
<CodeBlock
language="bash"
code={`GET /bookings
# Avec filtres
GET /bookings?status=confirmed&page=1&limit=20`}
/>
<Table
headers={['Paramètre', 'Type', 'Description']}
rows={[
[<InlineCode key="s">status</InlineCode>, 'string', 'Filtrer par statut (draft, confirmed, in_transit, delivered, cancelled)'],
[<InlineCode key="p">page</InlineCode>, 'number', 'Numéro de page (défaut: 1)'],
[<InlineCode key="l">limit</InlineCode>, 'number', 'Résultats par page (défaut: 20, max: 100)'],
[<InlineCode key="o">origin</InlineCode>, 'string', 'Code port d\'origine (ex: FRLEH)'],
[<InlineCode key="d">destination</InlineCode>, 'string', 'Code port de destination (ex: CNSHA)'],
]}
/>
<CodeBlock
language="json"
filename="response.json"
code={`{
"data": [
{
"id": "uuid",
"bookingNumber": "WCM-2025-000123",
"status": "confirmed",
"origin": "FRLEH",
"destination": "CNSHA",
"carrier": "Maersk",
"departureDate": "2025-04-15",
"arrivalDate": "2025-05-20",
"totalAmount": { "amount": 2400.00, "currency": "USD" },
"createdAt": "2025-03-26T10:00:00.000Z"
}
],
"meta": { "total": 42, "page": 1, "perPage": 20 }
}`}
/>
<Divider />
<H2>Créer un booking</H2>
<CodeBlock
language="bash"
code={`POST /bookings
Content-Type: application/json
X-API-Key: xped_live_...`}
/>
<CodeBlock
language="json"
filename="request.json"
code={`{
"rateQuoteId": "uuid-du-rate-quote",
"shipper": {
"name": "Société Exportatrice SAS",
"contactName": "Jean Dupont",
"contactEmail": "j.dupont@exemple.fr",
"contactPhone": "+33612345678",
"address": {
"street": "15 rue du Commerce",
"city": "Le Havre",
"postalCode": "76600",
"country": "France"
}
},
"consignee": {
"name": "Shanghai Imports Co.",
"contactName": "Li Wei",
"contactEmail": "li@imports.cn",
"contactPhone": "+8613912345678",
"address": {
"street": "88 Pudong Ave",
"city": "Shanghai",
"postalCode": "200120",
"country": "China"
}
}
}`}
/>
<Divider />
<H2>Statuts d&apos;un booking</H2>
<Table
headers={['Statut', 'Description']}
rows={[
[<InlineCode key="1">draft</InlineCode>, 'Réservation créée, non confirmée'],
[<InlineCode key="2">pending_confirmation</InlineCode>, 'En attente de confirmation du transporteur'],
[<InlineCode key="3">confirmed</InlineCode>, 'Confirmée par le transporteur'],
[<InlineCode key="4">in_transit</InlineCode>, 'Expédition en cours'],
[<InlineCode key="5">delivered</InlineCode>, 'Livraison confirmée'],
[<InlineCode key="6">cancelled</InlineCode>, 'Annulée'],
]}
/>
</div>
);
}
// ─── Section: Rates ───────────────────────────────────────────────────────────
function RatesSection() {
return (
<div>
<SectionHeader
title="Tarifs & Recherche"
description="Recherchez et comparez des tarifs de fret maritime en temps réel."
/>
<H2>Rechercher des tarifs</H2>
<CodeBlock
language="bash"
code={`GET /rates/search?origin=FRLEH&destination=CNSHA&containerType=20GP&departureDate=2025-04-15`}
/>
<Table
headers={['Paramètre', 'Requis', 'Description']}
rows={[
[<InlineCode key="o">origin</InlineCode>, '✅', 'Code port d\'origine (UN/LOCODE, ex: FRLEH)'],
[<InlineCode key="d">destination</InlineCode>, '✅', 'Code port de destination (ex: CNSHA)'],
[<InlineCode key="c">containerType</InlineCode>, '✅', 'Type de conteneur: 20GP, 40GP, 40HC, 45HC, 20FR, 40FR'],
[<InlineCode key="dd">departureDate</InlineCode>, '❌', 'Date souhaitée de départ (YYYY-MM-DD)'],
[<InlineCode key="s">sortBy</InlineCode>, '❌', 'Tri: price_asc | price_desc | transit_time'],
]}
/>
<CodeBlock
language="json"
filename="response.json"
code={`{
"data": [
{
"id": "uuid",
"carrier": {
"name": "Maersk",
"scac": "MAEU",
"logoUrl": "..."
},
"origin": { "code": "FRLEH", "name": "Le Havre", "country": "France" },
"destination": { "code": "CNSHA", "name": "Shanghai", "country": "China" },
"containerType": "20GP",
"departureDate": "2025-04-15",
"arrivalDate": "2025-05-20",
"transitDays": 35,
"price": { "amount": 1850.00, "currency": "USD" },
"surcharges": [
{ "code": "BAF", "name": "Bunker Adjustment Factor", "amount": 150.00, "currency": "USD" }
],
"totalPrice": { "amount": 2000.00, "currency": "USD" },
"validUntil": "2025-04-01T00:00:00.000Z",
"directService": true
}
],
"meta": { "total": 8, "searchedAt": "2025-03-26T10:00:00.000Z" }
}`}
/>
<Callout type="info">
Les tarifs sont mis en cache pendant <strong>15 minutes</strong>. Au-delà, une nouvelle recherche est effectuée
auprès des transporteurs en temps réel.
</Callout>
<Divider />
<H2>Codes de ports (UN/LOCODE)</H2>
<P>Les ports sont identifiés par le code standard UN/LOCODE (5 caractères).</P>
<Table
headers={['Code', 'Port', 'Pays']}
rows={[
['FRLEH', 'Le Havre', 'France'],
['FRFOS', 'Fos-sur-Mer', 'France'],
['CNSHA', 'Shanghai', 'Chine'],
['CNNGB', 'Ningbo', 'Chine'],
['SGSIN', 'Singapore', 'Singapour'],
['USLAX', 'Los Angeles', 'États-Unis'],
['NLRTM', 'Rotterdam', 'Pays-Bas'],
['DEHAM', 'Hambourg', 'Allemagne'],
]}
/>
<CodeBlock
language="bash"
code="GET /ports?q=havre&limit=5"
/>
</div>
);
}
// ─── Section: Organizations ───────────────────────────────────────────────────
function OrganizationsSection() {
return (
<div>
<SectionHeader
title="Organisations"
description="Accédez aux informations de votre organisation."
/>
<H2>Profil de l&apos;organisation</H2>
<CodeBlock language="bash" code="GET /organizations/me" />
<CodeBlock
language="json"
filename="response.json"
code={`{
"id": "uuid",
"name": "Freight Forwarder SAS",
"type": "FREIGHT_FORWARDER",
"siret": "12345678901234",
"siren": "123456789",
"statusBadge": "gold",
"subscription": {
"plan": "GOLD",
"status": "ACTIVE",
"currentPeriodEnd": "2025-04-26T00:00:00.000Z",
"features": ["dashboard", "wiki", "user_management", "csv_export", "api_access"]
},
"address": {
"street": "15 rue du Commerce",
"city": "Le Havre",
"postalCode": "76600",
"country": "France"
}
}`}
/>
<Divider />
<H2>Membres de l&apos;organisation</H2>
<CodeBlock language="bash" code="GET /users?organizationId=uuid&role=MANAGER" />
</div>
);
}
// ─── Section: Endpoints ───────────────────────────────────────────────────────
function EndpointsSection() {
const endpoints = [
{ method: 'GET', path: '/bookings', desc: 'Lister les bookings' },
{ method: 'POST', path: '/bookings', desc: 'Créer un booking' },
{ method: 'GET', path: '/bookings/:id', desc: 'Détail d\'un booking' },
{ method: 'PATCH', path: '/bookings/:id/status', desc: 'Mettre à jour le statut' },
{ method: 'GET', path: '/rates/search', desc: 'Rechercher des tarifs' },
{ method: 'GET', path: '/rates/:id', desc: 'Détail d\'un tarif' },
{ method: 'GET', path: '/ports', desc: 'Lister les ports' },
{ method: 'GET', path: '/organizations/me', desc: 'Profil de l\'organisation' },
{ method: 'GET', path: '/users', desc: 'Membres de l\'organisation' },
{ method: 'POST', path: '/api-keys', desc: 'Créer une clé API (JWT requis)' },
{ method: 'GET', path: '/api-keys', desc: 'Lister les clés API (JWT requis)' },
{ method: 'DELETE', path: '/api-keys/:id', desc: 'Révoquer une clé (JWT requis)' },
];
const methodColor: Record<string, string> = {
GET: 'text-emerald-700 bg-emerald-50 border-emerald-200',
POST: 'text-blue-700 bg-blue-50 border-blue-200',
PATCH: 'text-amber-700 bg-amber-50 border-amber-200',
DELETE: 'text-red-700 bg-red-50 border-red-200',
};
return (
<div>
<SectionHeader
title="Tous les endpoints"
description="Référence complète de l'API Xpeditis."
/>
<H2>Endpoints disponibles</H2>
<div className="space-y-2">
{endpoints.map((ep, i) => (
<div key={i} className="flex items-center gap-3 p-3 rounded-lg border border-gray-100 hover:bg-gray-50 transition-colors">
<span className={`text-xs font-bold px-2 py-0.5 rounded border font-mono w-14 text-center flex-shrink-0 ${methodColor[ep.method]}`}>
{ep.method}
</span>
<code className="text-sm font-mono text-gray-700 flex-1">{ep.path}</code>
<span className="text-sm text-gray-500">{ep.desc}</span>
</div>
))}
</div>
<Divider />
<H2>Format de réponse standard</H2>
<CodeBlock
language="json"
filename="success.json"
code={`{
"data": { ... }, // ou "data": [ ... ] pour les listes
"meta": { // uniquement pour les listes paginées
"total": 100,
"page": 1,
"perPage": 20,
"totalPages": 5
}
}`}
/>
</div>
);
}
// ─── Section: Errors ─────────────────────────────────────────────────────────
function ErrorsSection() {
return (
<div>
<SectionHeader
title="Codes d'erreur"
description="Comprendre et gérer les erreurs de l'API."
/>
<H2>Format d&apos;erreur</H2>
<P>Toutes les erreurs retournent un JSON standardisé :</P>
<CodeBlock
language="json"
code={`{
"statusCode": 403,
"message": "L'accès API nécessite un abonnement Gold ou Platinium.",
"error": "Forbidden"
}`}
/>
<Divider />
<H2>Codes HTTP</H2>
<Table
headers={['Code', 'Signification', 'Cause probable']}
rows={[
['200', 'Succès', 'Requête traitée avec succès'],
['201', 'Créé', 'Ressource créée avec succès'],
['204', 'Pas de contenu', 'Suppression réussie'],
['400', 'Requête invalide', 'Corps de requête malformé ou paramètres invalides'],
['401', 'Non authentifié', 'Clé API manquante, invalide ou expirée'],
['403', 'Accès refusé', 'Plan insuffisant ou permissions manquantes'],
['404', 'Introuvable', 'Ressource inexistante ou appartenant à une autre organisation'],
['429', 'Trop de requêtes', 'Rate limit dépassé'],
['500', 'Erreur serveur', 'Erreur interne — contactez le support'],
]}
/>
<Divider />
<H2>Erreurs courantes</H2>
<H3>401 Clé API invalide</H3>
<CodeBlock
language="json"
code={`{
"statusCode": 401,
"message": "Clé API invalide, expirée ou votre abonnement ne permet plus l'accès API.",
"error": "Unauthorized"
}`}
/>
<P><strong>Solutions :</strong> Vérifiez que la clé commence par <InlineCode>xped_live_</InlineCode>, qu&apos;elle n&apos;est pas révoquée et que l&apos;abonnement est toujours Gold ou Platinium.</P>
<H3>403 Plan insuffisant</H3>
<CodeBlock
language="json"
code={`{
"statusCode": 403,
"message": "L'accès API nécessite un abonnement Gold ou Platinium. Mettez à niveau votre abonnement pour générer des clés API.",
"error": "Forbidden"
}`}
/>
<H3>429 Rate limit</H3>
<CodeBlock
language="json"
code={`{
"statusCode": 429,
"message": "Too many requests. Please wait before retrying.",
"error": "Too Many Requests",
"retryAfter": 60
}`}
/>
<P>En cas de 429, attendez la durée indiquée dans <InlineCode>retryAfter</InlineCode> avant de réessayer.</P>
</div>
);
}
// ─── Section: Rate Limiting ───────────────────────────────────────────────────
function RateLimitingSection() {
return (
<div>
<SectionHeader
title="Rate Limiting"
description="Limites de requêtes et bonnes pratiques."
/>
<H2>Limites par plan</H2>
<Table
headers={['Plan', 'Requêtes / minute', 'Requêtes / jour']}
rows={[
['Gold', '60', '10 000'],
['Platinium', '120', 'Illimité'],
]}
/>
<Callout type="info">
Le rate limiting est appliqué <strong>par utilisateur</strong> (ID de l&apos;utilisateur associé à la clé API).
</Callout>
<Divider />
<H2>En-têtes de réponse</H2>
<P>Chaque réponse inclut des en-têtes pour suivre votre consommation :</P>
<Table
headers={['En-tête', 'Description']}
rows={[
['X-RateLimit-Limit', 'Nombre maximum de requêtes par fenêtre'],
['X-RateLimit-Remaining', 'Requêtes restantes dans la fenêtre actuelle'],
['X-RateLimit-Reset', 'Timestamp UNIX de réinitialisation du compteur'],
]}
/>
<Divider />
<H2>Bonnes pratiques</H2>
<div className="space-y-3">
{[
{ icon: Clock, title: 'Exponential backoff', desc: 'En cas de 429, attendez avant de réessayer (1s, 2s, 4s, 8s…).' },
{ icon: CheckCircle2, title: 'Mise en cache', desc: 'Cachez les résultats côté client pour éviter les appels redondants. Les tarifs restent valides 15 minutes.' },
{ icon: ShieldCheck, title: 'Une clé par service', desc: 'Utilisez des clés séparées par service ou environnement pour un meilleur suivi et une révocation ciblée.' },
].map((item, i) => (
<div key={i} className="flex gap-3 p-4 rounded-lg bg-gray-50 border border-gray-100">
<item.icon className="w-5 h-5 text-[#34CCCD] flex-shrink-0 mt-0.5" />
<div>
<p className="font-medium text-gray-900 text-sm">{item.title}</p>
<p className="text-sm text-gray-500 mt-0.5">{item.desc}</p>
</div>
</div>
))}
</div>
</div>
);
}
// ─── Section map ──────────────────────────────────────────────────────────────
function SectionContent({
activeSection,
onNavigate,
}: {
activeSection: string;
onNavigate: (id: string) => void;
}) {
switch (activeSection) {
case 'home': return <HomeSection onNavigate={onNavigate} />;
case 'quickstart': return <QuickStartSection onNavigate={onNavigate} />;
case 'authentication': return <AuthenticationSection />;
case 'bookings': return <BookingsSection />;
case 'rates': return <RatesSection />;
case 'organizations': return <OrganizationsSection />;
case 'endpoints': return <EndpointsSection />;
case 'errors': return <ErrorsSection />;
case 'rate-limiting': return <RateLimitingSection />;
default: return <HomeSection onNavigate={onNavigate} />;
}
}
// ─── Main Page ────────────────────────────────────────────────────────────────
function DocsPageContent() {
const searchParams = useSearchParams();
const router = useRouter();
const [activeSection, setActiveSection] = useState(searchParams.get('section') ?? 'home');
const [searchQuery, setSearchQuery] = useState('');
const [sidebarOpen, setSidebarOpen] = useState(false);
const navigate = (id: string) => {
setActiveSection(id);
router.replace(`/dashboard/docs?section=${id}`, { scroll: false });
setSidebarOpen(false);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
// Filter nav based on search
const filteredSections = DOC_SECTIONS.map(section => ({
...section,
items: section.items.filter(item =>
item.label.toLowerCase().includes(searchQuery.toLowerCase())
),
})).filter(s => s.items.length > 0);
const Sidebar = () => (
<div className="flex flex-col h-full bg-white">
{/* Logo + title */}
<div className="px-4 pt-5 pb-4 border-b border-gray-100">
<div className="flex items-center gap-2 mb-1">
<div className="w-7 h-7 rounded-lg bg-[#10183A] flex items-center justify-center">
<span className="text-[#34CCCD] text-xs font-bold">X</span>
</div>
<span className="text-sm font-bold text-gray-900">Xpeditis</span>
<span className="text-xs text-gray-400 font-medium">/ API</span>
</div>
{/* Search */}
<div className="relative mt-3">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-400" />
<input
type="text"
placeholder="Rechercher..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
className="w-full pl-8 pr-3 py-1.5 text-sm bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#34CCCD]/30 focus:border-[#34CCCD] placeholder-gray-400"
/>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto py-3 px-3">
{filteredSections.map(section => (
<div key={section.title} className="mb-4">
<p className="px-3 mb-1 text-xs font-semibold text-gray-400 uppercase tracking-wider">
{section.title}
</p>
{section.items.map(item => (
<button
key={item.id}
onClick={() => navigate(item.id)}
className={`w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm transition-all mb-0.5 ${
activeSection === item.id
? 'bg-[#10183A] text-white font-medium'
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
}`}
>
<item.icon className="w-4 h-4 flex-shrink-0" />
<span>{item.label}</span>
{activeSection === item.id && (
<ChevronRight className="w-3.5 h-3.5 ml-auto opacity-70" />
)}
</button>
))}
</div>
))}
</nav>
{/* Footer */}
<div className="border-t border-gray-100 p-4">
<a
href="/dashboard/settings/api-keys"
className="flex items-center gap-2 text-xs text-gray-500 hover:text-[#10183A] transition-colors"
>
<Key className="w-3.5 h-3.5" />
Gérer mes clés API
<ExternalLink className="w-3 h-3 ml-auto" />
</a>
</div>
</div>
);
return (
/* Break out of dashboard's p-6 padding */
<div className="-m-6 flex" style={{ height: 'calc(100vh - 64px)' }}>
{/* Mobile sidebar overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 z-40 bg-gray-900/50 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Docs sidebar — desktop */}
<div className="hidden lg:flex flex-col w-64 xl:w-72 flex-shrink-0 border-r border-gray-100 overflow-hidden">
<Sidebar />
</div>
{/* Docs sidebar — mobile drawer */}
<div className={`fixed inset-y-0 left-0 z-50 w-72 transform transition-transform lg:hidden ${
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}>
<Sidebar />
</div>
{/* Main content */}
<div className="flex-1 overflow-y-auto min-w-0">
{/* Mobile top bar */}
<div className="lg:hidden flex items-center gap-3 px-4 py-3 border-b border-gray-100 bg-white sticky top-0 z-10">
<button
onClick={() => setSidebarOpen(true)}
className="p-1.5 rounded-lg hover:bg-gray-100"
>
<Menu className="w-5 h-5 text-gray-600" />
</button>
<span className="text-sm font-medium text-gray-700">
{ALL_NAV_ITEMS.find(i => i.id === activeSection)?.label ?? 'Documentation'}
</span>
</div>
<div className="max-w-3xl mx-auto px-6 xl:px-12 py-8 pb-16">
{/* Breadcrumb */}
<div className="flex items-center gap-1.5 text-xs text-gray-400 mb-6">
<button onClick={() => navigate('home')} className="hover:text-gray-600 transition-colors">
Documentation
</button>
{activeSection !== 'home' && (
<>
<ChevronRight className="w-3 h-3" />
<span className="text-gray-600">
{ALL_NAV_ITEMS.find(i => i.id === activeSection)?.label}
</span>
</>
)}
</div>
<SectionContent activeSection={activeSection} onNavigate={navigate} />
{/* Bottom nav */}
{activeSection !== 'home' && (
<div className="mt-12 pt-6 border-t border-gray-100">
<button
onClick={() => navigate('home')}
className="flex items-center gap-2 text-sm text-gray-400 hover:text-gray-700 transition-colors"
>
Retour à la vue d&apos;ensemble
</button>
</div>
)}
</div>
</div>
</div>
);
}
export default function DocsPage() {
return (
<Suspense fallback={<div className="-m-6 flex items-center justify-center" style={{ height: 'calc(100vh - 64px)' }}><div className="w-8 h-8 border-4 border-[#34CCCD] border-t-transparent rounded-full animate-spin" /></div>}>
<DocsPageContent />
</Suspense>
);
}