feat: add React frontend with auth, project list, import, and project detail pages

Converts packages/web from vanilla TypeScript Vite scaffold to React with:
- React 19, react-router-dom v7, @tanstack/react-query v5, Tailwind CSS v4
- JWT auth context with auto-refresh token support
- Login/Register pages, protected Layout with auth guard
- Projects list with grid cards and delete action
- ImportDialog supporting URL or file upload with API key display
- ProjectDetail with 4 tabs: Documentation, Modules, MCP Integration, Settings
- All TypeScript compiles cleanly (noEmit check passes)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 13:36:45 +08:00
parent ac60f0bb49
commit c3f8b598af
26 changed files with 1143 additions and 389 deletions

View File

@@ -0,0 +1,58 @@
import { useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '../lib/api';
import DocPreview from './tabs/DocPreview';
import ModuleManagement from './tabs/ModuleManagement';
import McpIntegration from './tabs/McpIntegration';
import ProjectSettings from './tabs/ProjectSettings';
type ProjectData = {
id: string; name: string; description: string | null; baseUrl: string | null;
openApiVersion: string;
modules: Array<{ id: string; name: string; description: string | null; _count: { endpoints: number } }>;
_count: { endpoints: number };
};
const tabs = ['Documentation', 'Modules', 'MCP Integration', 'Settings'] as const;
type Tab = (typeof tabs)[number];
export default function ProjectDetail() {
const { id } = useParams<{ id: string }>();
const [activeTab, setActiveTab] = useState<Tab>('Documentation');
const { data: project, isLoading } = useQuery({
queryKey: ['project', id],
queryFn: () => apiFetch<ProjectData>(`/projects/${id}`),
});
if (isLoading) return <div>Loading...</div>;
if (!project) return <div>Project not found</div>;
return (
<div>
<div className="mb-4"><Link to="/" className="text-sm text-blue-600 hover:underline">&larr; Back to projects</Link></div>
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-semibold">{project.name}</h2>
{project.description && <p className="text-sm text-gray-500 mt-1">{project.description}</p>}
</div>
<div className="text-sm text-gray-400">OpenAPI {project.openApiVersion} &middot; {project._count.endpoints} endpoints</div>
</div>
<div className="border-b mb-6">
<div className="flex gap-6">
{tabs.map((tab) => (
<button key={tab} onClick={() => setActiveTab(tab)}
className={`pb-2 text-sm font-medium border-b-2 transition-colors ${activeTab === tab ? 'border-blue-600 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
{tab}
</button>
))}
</div>
</div>
{activeTab === 'Documentation' && <DocPreview projectId={project.id} />}
{activeTab === 'Modules' && <ModuleManagement projectId={project.id} />}
{activeTab === 'MCP Integration' && <McpIntegration project={project} />}
{activeTab === 'Settings' && <ProjectSettings project={project} />}
</div>
);
}