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,55 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Link } from 'react-router-dom';
import { apiFetch } from '../lib/api';
import ImportDialog from './ImportDialog';
type ProjectSummary = {
id: string; name: string; description: string | null; openApiVersion: string;
updatedAt: string; _count: { endpoints: number; modules: number };
};
export default function Projects() {
const [showImport, setShowImport] = useState(false);
const queryClient = useQueryClient();
const { data: projects, isLoading } = useQuery({
queryKey: ['projects'],
queryFn: () => apiFetch<ProjectSummary[]>('/projects'),
});
const deleteMutation = useMutation({
mutationFn: (id: string) => apiFetch(`/projects/${id}`, { method: 'DELETE' }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['projects'] }),
});
if (isLoading) return <div>Loading projects...</div>;
return (
<div>
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold">Projects</h2>
<button onClick={() => setShowImport(true)} className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">Import API Doc</button>
</div>
{projects?.length === 0 && <p className="text-gray-500 text-center py-12">No projects yet. Import an OpenAPI document to get started.</p>}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{projects?.map((p) => (
<div key={p.id} className="p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow">
<Link to={`/projects/${p.id}`}>
<h3 className="font-medium text-lg">{p.name}</h3>
{p.description && <p className="text-sm text-gray-500 mt-1 line-clamp-2">{p.description}</p>}
<div className="mt-3 flex items-center gap-4 text-xs text-gray-400">
<span>OpenAPI {p.openApiVersion}</span>
<span>{p._count.modules} modules</span>
<span>{p._count.endpoints} endpoints</span>
</div>
</Link>
<button onClick={() => { if (confirm('Delete this project?')) deleteMutation.mutate(p.id); }}
className="mt-2 text-xs text-red-500 hover:underline">Delete</button>
</div>
))}
</div>
{showImport && <ImportDialog onClose={() => setShowImport(false)} />}
</div>
);
}