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:
96
packages/web/src/pages/ImportDialog.tsx
Normal file
96
packages/web/src/pages/ImportDialog.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { apiFetch } from '../lib/api';
|
||||
|
||||
type ImportResult = {
|
||||
project: { id: string; name: string };
|
||||
apiKey: string;
|
||||
stats: { modules: number; endpoints: number };
|
||||
};
|
||||
|
||||
export default function ImportDialog({ onClose }: { onClose: () => void }) {
|
||||
const [mode, setMode] = useState<'url' | 'file'>('url');
|
||||
const [url, setUrl] = useState('');
|
||||
const [fileContent, setFileContent] = useState<string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [result, setResult] = useState<ImportResult | null>(null);
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => setFileContent(reader.result as string);
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
let body: Record<string, unknown>;
|
||||
if (mode === 'url') {
|
||||
body = { specUrl: url };
|
||||
} else {
|
||||
try { body = { spec: JSON.parse(fileContent) }; } catch { body = { spec: fileContent }; }
|
||||
}
|
||||
const data = await apiFetch<ImportResult>('/projects', {
|
||||
method: 'POST', body: JSON.stringify(body),
|
||||
});
|
||||
setResult(data);
|
||||
queryClient.invalidateQueries({ queryKey: ['projects'] });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Import failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-lg">
|
||||
{!result ? (
|
||||
<>
|
||||
<h2 className="text-lg font-semibold mb-4">Import OpenAPI Document</h2>
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button onClick={() => setMode('url')} className={`px-3 py-1 rounded text-sm ${mode === 'url' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100'}`}>From URL</button>
|
||||
<button onClick={() => setMode('file')} className={`px-3 py-1 rounded text-sm ${mode === 'file' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100'}`}>Upload File</button>
|
||||
</div>
|
||||
{mode === 'url' ? (
|
||||
<input type="url" placeholder="https://api.example.com/openapi.json" value={url} onChange={(e) => setUrl(e.target.value)} className="w-full px-3 py-2 border rounded-md mb-4" />
|
||||
) : (
|
||||
<input type="file" accept=".json,.yaml,.yml" onChange={handleFileChange} className="w-full mb-4" />
|
||||
)}
|
||||
{error && <p className="text-red-500 text-sm mb-4">{error}</p>}
|
||||
<div className="flex justify-end gap-2">
|
||||
<button onClick={onClose} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-md">Cancel</button>
|
||||
<button onClick={handleImport} disabled={loading || (mode === 'url' ? !url : !fileContent)}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50">
|
||||
{loading ? 'Importing...' : 'Import'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h2 className="text-lg font-semibold mb-4 text-green-600">Import Successful!</h2>
|
||||
<div className="space-y-3 text-sm">
|
||||
<p><strong>Project:</strong> {result.project.name}</p>
|
||||
<p><strong>Modules:</strong> {result.stats.modules}</p>
|
||||
<p><strong>Endpoints:</strong> {result.stats.endpoints}</p>
|
||||
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded">
|
||||
<p className="font-medium text-yellow-800 mb-1">API Key (save it now):</p>
|
||||
<code className="text-xs break-all">{result.apiKey}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end mt-4">
|
||||
<button onClick={() => navigate(`/projects/${result.project.id}`)} className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">Go to Project</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user