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>
56 lines
2.4 KiB
TypeScript
56 lines
2.4 KiB
TypeScript
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>
|
|
);
|
|
}
|