2026-02-06 23:01:30 -05:00

260 lines
9.3 KiB
TypeScript

'use client';
import { useState, useRef, useCallback } from 'react';
import { Link2, Upload, LayoutGrid, FileText, X, Loader2 } from 'lucide-react';
type Tab = 'url' | 'file' | 'template';
interface SpecUploaderProps {
onAnalyze: (input: { type: 'url' | 'raw'; value: string }) => void;
loading?: boolean;
}
export function SpecUploader({ onAnalyze, loading = false }: SpecUploaderProps) {
const [activeTab, setActiveTab] = useState<Tab>('url');
const [urlInput, setUrlInput] = useState('');
const [dragActive, setDragActive] = useState(false);
const [fileName, setFileName] = useState<string | null>(null);
const [fileContent, setFileContent] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [
{ id: 'url', label: 'Paste URL', icon: <Link2 className="h-4 w-4" /> },
{ id: 'file', label: 'Upload File', icon: <Upload className="h-4 w-4" /> },
{ id: 'template', label: 'Pick Template', icon: <LayoutGrid className="h-4 w-4" /> },
];
const handleFileRead = useCallback((file: File) => {
const validExts = ['.json', '.yaml', '.yml'];
const ext = '.' + file.name.split('.').pop()?.toLowerCase();
if (!validExts.includes(ext)) {
alert('Please upload a .json, .yaml, or .yml file');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result as string;
setFileName(file.name);
setFileContent(content);
};
reader.readAsText(file);
}, []);
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
const file = e.dataTransfer.files[0];
if (file) handleFileRead(file);
},
[handleFileRead],
);
const handleSubmitUrl = () => {
if (!urlInput.trim()) return;
// Auto-detect: if it starts with http(s), it's a URL; otherwise raw spec content
const isUrl = /^https?:\/\//i.test(urlInput.trim());
onAnalyze({
type: isUrl ? 'url' : 'raw',
value: urlInput.trim(),
});
};
const handleSubmitFile = () => {
if (!fileContent) return;
onAnalyze({ type: 'raw', value: fileContent });
};
const clearFile = () => {
setFileName(null);
setFileContent(null);
if (fileInputRef.current) fileInputRef.current.value = '';
};
return (
<div className="w-full">
{/* Tab header */}
<div className="flex border-b border-gray-800 mb-6">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
flex items-center gap-2 px-5 py-3 text-sm font-medium
border-b-2 transition-all -mb-px
${
activeTab === tab.id
? 'border-indigo-500 text-indigo-400'
: 'border-transparent text-gray-500 hover:text-gray-300'
}
`}
>
{tab.icon}
{tab.label}
</button>
))}
</div>
{/* Tab content */}
<div className="min-h-[200px]">
{/* URL Tab */}
{activeTab === 'url' && (
<div className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-2">
OpenAPI/Swagger spec URL or paste raw content
</label>
<textarea
value={urlInput}
onChange={(e) => setUrlInput(e.target.value)}
placeholder="https://api.example.com/openapi.json or paste spec content..."
rows={4}
className="w-full px-4 py-3 rounded-xl bg-gray-800 border border-gray-700
text-gray-100 placeholder-gray-500 text-sm
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent
resize-none transition-all"
/>
</div>
<button
onClick={handleSubmitUrl}
disabled={!urlInput.trim() || loading}
className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg
font-medium text-sm bg-indigo-600 text-white
hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed
transition-colors"
>
{loading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Analyzing...
</>
) : (
'Analyze'
)}
</button>
</div>
)}
{/* File Tab */}
{activeTab === 'file' && (
<div className="space-y-4">
<div
onDragOver={(e) => { e.preventDefault(); setDragActive(true); }}
onDragLeave={() => setDragActive(false)}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={`
flex flex-col items-center justify-center
border-2 border-dashed rounded-xl p-10 cursor-pointer
transition-all duration-200
${
dragActive
? 'border-indigo-500 bg-indigo-500/5'
: fileName
? 'border-emerald-500/50 bg-emerald-500/5'
: 'border-gray-700 hover:border-gray-500 bg-gray-800/30'
}
`}
>
<input
ref={fileInputRef}
type="file"
accept=".json,.yaml,.yml"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleFileRead(file);
}}
/>
{fileName ? (
<div className="flex items-center gap-3">
<FileText className="h-8 w-8 text-emerald-400" />
<div>
<p className="text-sm font-medium text-gray-200">{fileName}</p>
<p className="text-xs text-gray-500">Click to replace or drag a new file</p>
</div>
<button
onClick={(e) => { e.stopPropagation(); clearFile(); }}
className="p-1 rounded-full hover:bg-gray-700 text-gray-500 hover:text-gray-300"
>
<X className="h-4 w-4" />
</button>
</div>
) : (
<>
<Upload className="h-10 w-10 text-gray-600 mb-3" />
<p className="text-sm text-gray-400 mb-1">
Drop your API spec here or click to browse
</p>
<p className="text-xs text-gray-600">
Supports .json, .yaml, .yml
</p>
</>
)}
</div>
{fileName && (
<button
onClick={handleSubmitFile}
disabled={loading}
className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg
font-medium text-sm bg-indigo-600 text-white
hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed
transition-colors"
>
{loading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Analyzing...
</>
) : (
'Analyze'
)}
</button>
)}
</div>
)}
{/* Template Tab */}
{activeTab === 'template' && (
<div className="space-y-4">
<p className="text-sm text-gray-400">
Start from a community template instead of an API spec.
</p>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{[
{ name: 'Stripe Payments', category: 'Finance', tools: 24 },
{ name: 'HubSpot CRM', category: 'CRM', tools: 32 },
{ name: 'Shopify Store', category: 'eCommerce', tools: 28 },
{ name: 'GitHub Projects', category: 'DevTools', tools: 18 },
{ name: 'Slack Bot', category: 'Communication', tools: 15 },
{ name: 'Notion Workspace', category: 'ProjectMgmt', tools: 20 },
].map((tpl) => (
<a
key={tpl.name}
href="/marketplace"
className="p-3 rounded-lg bg-gray-800/50 border border-gray-800
hover:border-gray-600 transition-all group"
>
<p className="text-sm font-medium text-gray-200 group-hover:text-white">
{tpl.name}
</p>
<p className="text-xs text-gray-500 mt-1">
{tpl.category} · {tpl.tools} tools
</p>
</a>
))}
</div>
<a
href="/marketplace"
className="inline-block text-sm text-indigo-400 hover:text-indigo-300 transition-colors"
>
Browse all templates
</a>
</div>
)}
</div>
</div>
);
}