Introduce an admin portal (React + Nginx), WebSocket routing, and API versioning middleware with `/api/v1/` prefix deprecation. Add master API key authentication, new Prometheus metrics for AI token consumption and active WebSockets, and extend S3 config with a public endpoint URL. Update test paths and fixtures to align with the new routing structure.
121 lines
6.3 KiB
TypeScript
121 lines
6.3 KiB
TypeScript
import { useState } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { FileText, Search, Loader2, AlertCircle, BookOpen } from 'lucide-react';
|
|
import { MarkdownViewer } from '@/components/docs/MarkdownViewer';
|
|
import { getDocsList, getDocContent } from '@/api/docs';
|
|
import { clsx } from 'clsx';
|
|
|
|
export default function DocsPage() {
|
|
const [selectedDoc, setSelectedDoc] = useState<string>('README.md');
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
const { data: docsList, isLoading: docsListLoading, error: docsListError } = useQuery({
|
|
queryKey: ['docsList'],
|
|
queryFn: getDocsList,
|
|
});
|
|
|
|
const { data: docContent, isLoading: docContentLoading, error: docContentError } = useQuery({
|
|
queryKey: ['docContent', selectedDoc],
|
|
queryFn: () => getDocContent(selectedDoc),
|
|
enabled: !!selectedDoc,
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-content-primary flex items-center gap-2">
|
|
<BookOpen size={28} className="text-brand-500" />
|
|
Aides & Documentation
|
|
</h1>
|
|
<p className="text-content-secondary mt-1">
|
|
Consultez la documentation technique et les guides du projet Imago.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-surface-raised border border-border rounded-xl flex overflow-hidden min-h-[600px] shadow-sm">
|
|
|
|
{/* Sidebar with document list */}
|
|
<div className="w-64 border-r border-border bg-surface flex flex-col shrink-0">
|
|
<div className="p-4 border-b border-border">
|
|
<h2 className="font-semibold text-content-primary text-sm uppercase tracking-wider">
|
|
Sommaire
|
|
</h2>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto p-2 space-y-1">
|
|
{docsListLoading && (
|
|
<div className="flex justify-center p-4">
|
|
<Loader2 className="animate-spin text-brand-500" size={20} />
|
|
</div>
|
|
)}
|
|
{docsListError && (
|
|
<div className="p-4 text-sm text-status-error flex items-start gap-2 bg-status-error/10 rounded-lg m-2">
|
|
<AlertCircle size={16} className="shrink-0 mt-0.5" />
|
|
<span>Erreur lors du chargement de la liste.</span>
|
|
</div>
|
|
)}
|
|
{docsList?.map((doc) => (
|
|
<button
|
|
key={doc.name}
|
|
onClick={() => setSelectedDoc(doc.name)}
|
|
className={clsx(
|
|
'w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-left transition-colors',
|
|
selectedDoc === doc.name
|
|
? 'bg-brand-500/10 text-brand-400 font-medium'
|
|
: 'text-content-secondary hover:bg-surface-overlay hover:text-content-primary'
|
|
)}
|
|
>
|
|
<FileText size={16} className={selectedDoc === doc.name ? 'text-brand-400' : 'text-content-muted'} />
|
|
<span className="truncate">{doc.title}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content Area */}
|
|
<div className="flex-1 flex flex-col bg-surface min-w-0">
|
|
{/* Toolbar with Search */}
|
|
<div className="h-14 border-b border-border flex items-center px-6 justify-between shrink-0 bg-surface-raised/50">
|
|
<div className="font-medium text-content-primary flex items-center gap-2">
|
|
<FileText size={18} className="text-content-muted" />
|
|
{docsList?.find(d => d.name === selectedDoc)?.title || selectedDoc}
|
|
</div>
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-content-muted" size={16} />
|
|
<input
|
|
type="text"
|
|
placeholder="Rechercher..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-9 pr-4 py-1.5 bg-surface border border-border rounded-lg text-sm text-content-primary placeholder-content-muted focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 w-64 transition-all"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Document Viewer */}
|
|
<div className="flex-1 overflow-y-auto p-8 relative">
|
|
{docContentLoading && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-surface/80 backdrop-blur-sm z-10">
|
|
<Loader2 className="animate-spin text-brand-500" size={32} />
|
|
</div>
|
|
)}
|
|
{docContentError && (
|
|
<div className="flex items-center gap-3 p-4 bg-status-error/10 text-status-error rounded-xl max-w-lg mx-auto mt-10">
|
|
<AlertCircle size={24} />
|
|
<p>Impossible de charger le document. Le fichier est peut-être manquant.</p>
|
|
</div>
|
|
)}
|
|
{!docContentError && docContent && (
|
|
<div className="mx-auto max-w-4xl">
|
|
<MarkdownViewer content={docContent.content} searchTerm={searchTerm} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|