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:
55
packages/web/src/pages/Projects.tsx
Normal file
55
packages/web/src/pages/Projects.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user