banatie-service/apps/landing/src/components/docs/final/InteractiveAPIWidgetFinal.tsx

510 lines
19 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
/**
* Interactive API Widget - Final Variant (Redesigned)
*
* Minimized layout inspired by Variant C with Final variant design system:
* - No inline API key input (uses DocsApiKeyInput component)
* - Clean header with method badge, endpoint, and expand button
* - Full-width code snippet area
* - Compact footer with API key anchor link and Try It button
* - Expanded view opens full-screen modal with code + response side-by-side
*/
import { useState, useEffect } from 'react';
interface InteractiveAPIWidgetFinalProps {
endpoint: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
description: string;
parameters?: Array<{
name: string;
type: string;
required: boolean;
description: string;
defaultValue?: string;
}>;
}
type Language = 'curl' | 'javascript' | 'python' | 'go';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
const API_KEY_STORAGE = 'banatie_docs_api_key';
const getMethodColor = (method: string): string => {
switch (method) {
case 'GET':
return 'from-cyan-500 to-cyan-600';
case 'POST':
return 'from-purple-500 to-purple-600';
case 'PUT':
return 'from-amber-500 to-amber-600';
case 'DELETE':
return 'from-red-500 to-red-600';
default:
return 'from-purple-500 to-cyan-500';
}
};
export const InteractiveAPIWidgetFinal = ({
endpoint,
method,
description,
parameters = [],
}: InteractiveAPIWidgetFinalProps) => {
const [language, setLanguage] = useState<Language>('curl');
const [apiKey, setApiKey] = useState('');
const [isExecuting, setIsExecuting] = useState(false);
const [response, setResponse] = useState<any>(null);
const [error, setError] = useState<string | null>(null);
const [isExpanded, setIsExpanded] = useState(false);
const [paramValues, setParamValues] = useState<Record<string, string>>({});
// Load API key from localStorage and listen for changes
useEffect(() => {
const loadApiKey = () => {
const stored = localStorage.getItem(API_KEY_STORAGE);
if (stored) setApiKey(stored);
};
loadApiKey();
// Listen for API key changes from DocsApiKeyInput
const handleApiKeyChange = (e: CustomEvent) => {
setApiKey(e.detail);
};
window.addEventListener('apiKeyChanged', handleApiKeyChange as EventListener);
// Initialize parameter defaults
const defaults: Record<string, string> = {
prompt: 'a futuristic city at sunset',
filename: 'demo',
aspectRatio: '16:9',
};
parameters.forEach((param) => {
if (param.defaultValue) {
defaults[param.name] = param.defaultValue;
}
});
setParamValues(defaults);
return () => {
window.removeEventListener('apiKeyChanged', handleApiKeyChange as EventListener);
};
}, [parameters]);
// Generate code examples
const generateCode = (): string => {
const url = `${API_BASE_URL}${endpoint}`;
switch (language) {
case 'curl':
return `curl -X ${method} "${url}" \\
-H "X-API-Key: ${apiKey || 'YOUR_API_KEY'}" \\
-H "Content-Type: application/json" \\
-d '{
"prompt": "a futuristic city at sunset",
"filename": "city",
"aspectRatio": "16:9"
}'`;
case 'javascript':
return `const response = await fetch('${url}', {
method: '${method}',
headers: {
'X-API-Key': '${apiKey || 'YOUR_API_KEY'}',
'Content-Type': 'application/json'
},
body: JSON.stringify({
prompt: 'a futuristic city at sunset',
filename: 'city',
aspectRatio: '16:9'
})
});
const data = await response.json();
console.log(data);`;
case 'python':
return `import requests
url = '${url}'
headers = {
'X-API-Key': '${apiKey || 'YOUR_API_KEY'}',
'Content-Type': 'application/json'
}
data = {
'prompt': 'a futuristic city at sunset',
'filename': 'city',
'aspectRatio': '16:9'
}
response = requests.${method.toLowerCase()}(url, headers=headers, json=data)
print(response.json())`;
case 'go':
return `package main
import (
"bytes"
"encoding/json"
"net/http"
)
func main() {
url := "${url}"
data := map[string]interface{}{
"prompt": "a futuristic city at sunset",
"filename": "city",
"aspectRatio": "16:9",
}
jsonData, _ := json.Marshal(data)
req, _ := http.NewRequest("${method}", url, bytes.NewBuffer(jsonData))
req.Header.Set("X-API-Key", "${apiKey || 'YOUR_API_KEY'}")
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
}`;
default:
return '';
}
};
// Execute API request
const executeRequest = async () => {
if (!apiKey) {
setError('Please enter your API key in the top-right corner');
return;
}
setIsExecuting(true);
setError(null);
setResponse(null);
try {
const body = {
prompt: paramValues.prompt || 'a futuristic city at sunset',
filename: paramValues.filename || 'demo',
aspectRatio: paramValues.aspectRatio || '16:9',
};
console.log('🔵 API Request:', {
url: `${API_BASE_URL}${endpoint}`,
method,
headers: { 'X-API-Key': apiKey.substring(0, 10) + '...' },
body,
});
const res = await fetch(`${API_BASE_URL}${endpoint}`, {
method,
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json',
},
body: method !== 'GET' ? JSON.stringify(body) : undefined,
});
console.log('🟢 Response Status:', res.status, res.statusText);
if (!res.ok) {
let errorMessage = `HTTP ${res.status}: ${res.statusText}`;
try {
const errorData = await res.json();
errorMessage = errorData.message || errorData.error || errorMessage;
} catch {
// Response is not JSON
}
throw new Error(errorMessage);
}
const data = await res.json();
console.log('✅ Response Data:', data);
setResponse(data);
} catch (err) {
console.error('❌ API Error:', err);
const errorMsg = err instanceof Error ? err.message : 'Failed to execute request';
setError(`Request failed: ${errorMsg}`);
} finally {
setIsExecuting(false);
}
};
// Copy code to clipboard
const copyCode = () => {
navigator.clipboard.writeText(generateCode());
};
// Expand API key input in navigation
const expandApiKey = () => {
window.dispatchEvent(new CustomEvent('expandApiKeyInput'));
};
const isSuccess = response && response.success === true;
return (
<>
{/* Minimized Widget */}
<div className="my-8 bg-slate-900/50 backdrop-blur-sm border border-purple-500/30 rounded-2xl overflow-hidden shadow-xl">
{/* Header */}
<div className="flex items-center justify-between bg-slate-950/80 px-6 py-4 border-b border-purple-500/30">
<div className="flex items-center gap-3">
<div className={`px-3 py-1.5 rounded-lg bg-gradient-to-r ${getMethodColor(method)} text-white text-sm font-bold shadow-lg`}>
{method}
</div>
<code className="text-sm text-gray-400">{endpoint}</code>
</div>
<button
onClick={() => setIsExpanded(true)}
className="px-4 py-2 bg-cyan-600/20 hover:bg-cyan-600/30 text-cyan-400 rounded-lg transition-all text-sm font-semibold border border-cyan-500/40 hover:border-cyan-500/60"
>
Expand
</button>
</div>
{/* Code Section - Full Width */}
<div className="p-6">
{/* Language Tabs */}
<div className="flex items-center gap-2 mb-4">
{(['curl', 'javascript', 'python', 'go'] as Language[]).map((lang, idx) => {
const colors = ['purple', 'cyan', 'amber', 'purple'];
const color = colors[idx];
return (
<button
key={lang}
onClick={() => setLanguage(lang)}
className={`px-4 py-2 text-sm rounded-xl font-semibold transition-all border ${
language === lang
? color === 'purple'
? 'bg-purple-500/30 text-white border-purple-500/60 shadow-lg shadow-purple-500/20'
: color === 'cyan'
? 'bg-cyan-500/30 text-white border-cyan-500/60 shadow-lg shadow-cyan-500/20'
: 'bg-amber-500/30 text-white border-amber-500/60 shadow-lg shadow-amber-500/20'
: 'bg-slate-800/30 text-gray-400 border-slate-700 hover:border-slate-600 hover:text-white'
}`}
>
{lang === 'javascript' ? 'JS' : lang.toUpperCase()}
</button>
);
})}
</div>
{/* Code Display */}
<div className="border border-cyan-500/30 rounded-xl overflow-hidden shadow-lg">
<div className="flex items-center justify-between bg-slate-950/80 px-4 py-3 border-b border-cyan-500/20">
<span className="text-xs text-cyan-400 font-semibold">Code Example</span>
<button
onClick={copyCode}
className="px-3 py-1.5 text-xs bg-purple-600/30 hover:bg-purple-600/50 text-purple-300 rounded-lg transition-all font-semibold"
>
📋 Copy
</button>
</div>
<div className="p-4 bg-slate-950/50 max-h-80 overflow-auto">
<pre className="text-sm text-gray-300 leading-relaxed">
<code>{generateCode()}</code>
</pre>
</div>
</div>
</div>
{/* Footer */}
<div className="grid grid-cols-2 gap-4 px-6 pb-6">
<button
onClick={expandApiKey}
className="flex items-center gap-2 px-4 py-2.5 bg-slate-800/50 hover:bg-slate-800 border border-slate-700 rounded-lg text-gray-400 hover:text-white text-sm transition-colors"
>
<span>🔑</span>
<span>{apiKey ? 'API Key Set' : 'Enter API Key'}</span>
</button>
<button
onClick={executeRequest}
disabled={!apiKey || isExecuting}
className="px-4 py-2.5 rounded-lg bg-gradient-to-r from-purple-600 to-cyan-600 text-white text-sm font-semibold hover:from-purple-500 hover:to-cyan-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg hover:shadow-purple-500/30"
>
{isExecuting ? '⚡ Executing...' : '⚡ Try It'}
</button>
</div>
{/* Inline Response (if any) */}
{(response || error) && (
<div className="border-t border-purple-500/30 p-6 bg-slate-950/50">
<h4 className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
<span>📦</span>
Response
</h4>
{error ? (
<div className="p-4 bg-red-500/10 border border-red-500/40 rounded-xl text-red-400 text-sm shadow-lg shadow-red-500/10">
<div className="flex items-start gap-2">
<span></span>
<div>
<p className="font-semibold mb-1">Error</p>
<p className="text-xs">{error}</p>
</div>
</div>
</div>
) : (
<div className={`p-4 rounded-xl border ${
isSuccess
? 'bg-green-500/5 border-green-500/40 shadow-lg shadow-green-500/10'
: 'bg-slate-800/50 border-slate-700'
}`}>
{isSuccess && (
<div className="flex items-center gap-2 mb-3">
<span className="px-2 py-1 text-xs bg-green-500/20 text-green-400 rounded-full border border-green-500/40">
200 Success
</span>
</div>
)}
<pre className="text-xs text-gray-300 overflow-x-auto max-h-60">
<code>{JSON.stringify(response, null, 2)}</code>
</pre>
</div>
)}
</div>
)}
</div>
{/* Expanded Modal */}
{isExpanded && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm">
<div className="relative w-[90vw] h-[85vh] bg-slate-900 border border-purple-500/30 rounded-2xl shadow-2xl overflow-hidden flex flex-col">
{/* Modal Header */}
<div className="flex items-center justify-between bg-slate-950/80 px-6 py-4 border-b border-purple-500/30">
<div className="flex items-center gap-3">
<div className={`px-3 py-1.5 rounded-lg bg-gradient-to-r ${getMethodColor(method)} text-white text-sm font-bold shadow-lg`}>
{method}
</div>
<code className="text-sm text-gray-400">{endpoint}</code>
</div>
<button
onClick={() => setIsExpanded(false)}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-gray-400 hover:text-white rounded-lg transition-all text-sm font-semibold"
>
Close
</button>
</div>
{/* Two-Panel Layout */}
<div className="grid grid-cols-2 gap-6 p-6 flex-1 overflow-hidden">
{/* Left: Code */}
<div className="flex flex-col overflow-hidden">
<div className="flex items-center gap-2 mb-4">
{(['curl', 'javascript', 'python', 'go'] as Language[]).map((lang, idx) => {
const colors = ['purple', 'cyan', 'amber', 'purple'];
const color = colors[idx];
return (
<button
key={lang}
onClick={() => setLanguage(lang)}
className={`px-4 py-2 text-sm rounded-xl font-semibold transition-all border ${
language === lang
? color === 'purple'
? 'bg-purple-500/30 text-white border-purple-500/60 shadow-lg shadow-purple-500/20'
: color === 'cyan'
? 'bg-cyan-500/30 text-white border-cyan-500/60 shadow-lg shadow-cyan-500/20'
: 'bg-amber-500/30 text-white border-amber-500/60 shadow-lg shadow-amber-500/20'
: 'bg-slate-800/30 text-gray-400 border-slate-700 hover:border-slate-600 hover:text-white'
}`}
>
{lang === 'javascript' ? 'JS' : lang.toUpperCase()}
</button>
);
})}
</div>
<div className="border border-cyan-500/30 rounded-xl overflow-hidden shadow-lg flex-1 flex flex-col">
<div className="flex items-center justify-between bg-slate-950/80 px-4 py-3 border-b border-cyan-500/20">
<span className="text-xs text-cyan-400 font-semibold">Code Example</span>
<button
onClick={copyCode}
className="px-3 py-1.5 text-xs bg-purple-600/30 hover:bg-purple-600/50 text-purple-300 rounded-lg transition-all font-semibold"
>
📋 Copy
</button>
</div>
<div className="p-4 bg-slate-950/50 overflow-auto flex-1">
<pre className="text-base text-gray-300 leading-relaxed">
<code>{generateCode()}</code>
</pre>
</div>
</div>
</div>
{/* Right: Response */}
<div className="flex flex-col overflow-hidden">
<h4 className="text-sm font-semibold text-white mb-4 flex items-center gap-2">
<span>📦</span>
Response
</h4>
<div className="border border-purple-500/30 rounded-xl overflow-hidden shadow-lg flex-1 flex flex-col">
<div className="bg-slate-950/80 px-4 py-3 border-b border-purple-500/20">
{response && (
<span
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-bold ${
response.success
? 'bg-green-500/20 text-green-400 border border-green-500/40 shadow-lg shadow-green-500/10'
: 'bg-red-500/20 text-red-400 border border-red-500/40 shadow-lg shadow-red-500/10'
}`}
>
{response.success ? '✓ Success' : '✕ Error'}
</span>
)}
</div>
<div className="p-4 bg-slate-950/50 overflow-auto flex-1">
{error ? (
<div className="p-4 bg-red-500/10 border border-red-500/40 rounded-xl text-red-400 text-sm shadow-lg shadow-red-500/10">
<div className="flex items-start gap-2">
<span className="text-xl"></span>
<div>
<p className="font-semibold mb-1">Error</p>
<p className="text-xs">{error}</p>
</div>
</div>
</div>
) : response ? (
<pre className="text-sm text-gray-300 leading-relaxed">
<code>{JSON.stringify(response, null, 2)}</code>
</pre>
) : (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<p className="text-4xl mb-3">🚀</p>
<p className="text-sm text-gray-500">
Click <strong className="text-purple-400">"Try It"</strong> below to see the response
</p>
</div>
</div>
)}
</div>
</div>
{/* Try It Button in Expanded View */}
<button
onClick={executeRequest}
disabled={!apiKey || isExecuting}
className="mt-4 px-6 py-3 rounded-xl bg-gradient-to-r from-purple-600 to-cyan-600 text-white text-base font-bold hover:from-purple-500 hover:to-cyan-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-2xl hover:shadow-purple-500/50"
>
{isExecuting ? '⚡ Executing...' : '⚡ Try It Now'}
</button>
</div>
</div>
{/* Footer hint */}
<div className="px-6 pb-4 text-center">
<p className="text-xs text-gray-500">{description}</p>
</div>
</div>
</div>
)}
</>
);
};