feat: optimize web ui
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { apiFetch } from '../lib/api';
|
||||
import Modal from '../components/Modal';
|
||||
|
||||
type ImportResult = {
|
||||
project: { id: string; name: string };
|
||||
@@ -13,20 +14,30 @@ export default function ImportDialog({ onClose }: { onClose: () => void }) {
|
||||
const [mode, setMode] = useState<'url' | 'file'>('url');
|
||||
const [url, setUrl] = useState('');
|
||||
const [fileContent, setFileContent] = useState<string>('');
|
||||
const [fileName, setFileName] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [result, setResult] = useState<ImportResult | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const handleFile = (file: File) => {
|
||||
setFileName(file.name);
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => setFileContent(reader.result as string);
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragging(false);
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) handleFile(file);
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
@@ -49,48 +60,113 @@ export default function ImportDialog({ onClose }: { onClose: () => void }) {
|
||||
}
|
||||
};
|
||||
|
||||
const copyKey = () => {
|
||||
if (result?.apiKey) {
|
||||
navigator.clipboard.writeText(result.apiKey);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
<Modal open onClose={onClose} size="md">
|
||||
{!result ? (
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<h2 className="text-[15px] font-semibold text-text-primary">Import OpenAPI Document</h2>
|
||||
<p className="section-desc">Import a Swagger 2.0 or OpenAPI 3.x document to create a new project.</p>
|
||||
</div>
|
||||
|
||||
{/* Mode toggle */}
|
||||
<div className="flex gap-0.5 p-0.5 rounded-lg bg-bg-tertiary max-w-fit border border-border-muted">
|
||||
<button onClick={() => setMode('url')} className={`px-4 py-[6px] rounded-md text-[13px] font-medium transition-all ${mode === 'url' ? 'bg-bg-elevated text-text-primary shadow-sm' : 'text-text-muted hover:text-text-secondary'}`}>From URL</button>
|
||||
<button onClick={() => setMode('file')} className={`px-4 py-[6px] rounded-md text-[13px] font-medium transition-all ${mode === 'file' ? 'bg-bg-elevated text-text-primary shadow-sm' : 'text-text-muted hover:text-text-secondary'}`}>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="input-base" />
|
||||
) : (
|
||||
<div
|
||||
onDragOver={(e) => { e.preventDefault(); setDragging(true); }}
|
||||
onDragLeave={() => setDragging(false)}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all ${
|
||||
dragging ? 'border-accent bg-accent-muted' : 'border-border-default hover:border-border-strong'
|
||||
}`}
|
||||
>
|
||||
<input ref={fileInputRef} type="file" accept=".json,.yaml,.yml" onChange={(e) => e.target.files?.[0] && handleFile(e.target.files[0])} className="hidden" />
|
||||
<svg className="w-8 h-8 mx-auto text-text-muted mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||
</svg>
|
||||
{fileName ? (
|
||||
<p className="text-[13px] text-text-primary font-medium">{fileName}</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-[13px] text-text-secondary">Drop your OpenAPI file here</p>
|
||||
<p className="text-[11px] text-text-muted mt-1">JSON or YAML</p>
|
||||
</>
|
||||
)}
|
||||
</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'}
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="p-3 rounded-lg bg-danger-muted flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-danger shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><circle cx="12" cy="12" r="10" /><path d="M15 9l-6 6m0-6l6 6" /></svg>
|
||||
<span className="text-danger text-[13px]">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2.5">
|
||||
<button onClick={onClose} className="btn-ghost">Cancel</button>
|
||||
<button onClick={handleImport} disabled={loading || (mode === 'url' ? !url : !fileContent)} className="btn-primary">
|
||||
{loading ? (
|
||||
<><svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /></svg> Importing...</>
|
||||
) : 'Import'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-success-muted flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}><path d="M5 13l4 4L19 7" /></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-[15px] font-semibold text-text-primary">Import Successful</h2>
|
||||
<p className="text-[13px] text-text-muted">{result.project.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="card p-4 text-center">
|
||||
<div className="text-2xl font-bold text-text-primary tabular-nums">{result.stats.modules}</div>
|
||||
<div className="text-[11px] text-text-muted mt-0.5 uppercase tracking-wide">Modules</div>
|
||||
</div>
|
||||
<div className="card p-4 text-center">
|
||||
<div className="text-2xl font-bold text-text-primary tabular-nums">{result.stats.endpoints}</div>
|
||||
<div className="text-[11px] text-text-muted mt-0.5 uppercase tracking-wide">Endpoints</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-warning-muted border border-warning/20">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /></svg>
|
||||
<p className="text-[13px] font-medium text-warning">API Key — save it now</p>
|
||||
</div>
|
||||
<button onClick={copyKey} className="text-[11px] font-medium text-warning hover:underline">
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</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>
|
||||
<code className="text-xs break-all text-text-primary font-mono bg-bg-primary/30 rounded p-2 block">{result.apiKey}</code>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button onClick={() => navigate(`/projects/${result.project.id}`)} className="btn-primary">Go to Project</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user