394 lines
14 KiB
TypeScript
394 lines
14 KiB
TypeScript
'use client';
|
||
|
||
/**
|
||
* Interactive API Widget - Variant C: Modern & Visual (Shopify-inspired)
|
||
*
|
||
* Design Philosophy: Floating, expandable card with colorful visual feedback
|
||
*
|
||
* Features:
|
||
* - Full-screen expandable mode toggle button
|
||
* - Large gradient-bordered card with shadow glow
|
||
* - Prominent "Test Now ⚡" button (larger, more visual)
|
||
* - Response in colorful gradient-bordered card
|
||
* - Success/error states with vibrant colored badges
|
||
* - More visual feedback and animations
|
||
* - Large emoji icons throughout
|
||
* - Colorful code syntax highlighting hints
|
||
* - Floating shadow effects
|
||
*
|
||
* Layout:
|
||
* - Top: Large colorful API key input card
|
||
* - Main: Expandable code editor with rainbow gradient border
|
||
* - Language tabs as colorful pills
|
||
* - Large prominent "Test Now" button with gradient
|
||
* - Response card with colored status indicators
|
||
*
|
||
* Visual Elements:
|
||
* - Purple/cyan/amber gradients everywhere
|
||
* - Large shadows with colored glows
|
||
* - Scale and hover animations
|
||
* - Success: Green gradient with glow
|
||
* - Error: Red gradient with glow
|
||
*/
|
||
|
||
import { useState, useEffect } from 'react';
|
||
|
||
interface InteractiveAPIWidgetCProps {
|
||
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 InteractiveAPIWidgetC = ({
|
||
endpoint,
|
||
method,
|
||
description,
|
||
parameters = [],
|
||
}: InteractiveAPIWidgetCProps) => {
|
||
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 [paramValues, setParamValues] = useState<Record<string, string>>({});
|
||
const [isExpanded, setIsExpanded] = useState(false);
|
||
|
||
useEffect(() => {
|
||
const stored = localStorage.getItem(API_KEY_STORAGE);
|
||
if (stored) setApiKey(stored);
|
||
|
||
const defaults: Record<string, string> = {};
|
||
parameters.forEach((param) => {
|
||
if (param.defaultValue) {
|
||
defaults[param.name] = param.defaultValue;
|
||
}
|
||
});
|
||
setParamValues(defaults);
|
||
}, [parameters]);
|
||
|
||
const handleApiKeyChange = (value: string) => {
|
||
setApiKey(value);
|
||
if (value) {
|
||
localStorage.setItem(API_KEY_STORAGE, value);
|
||
} else {
|
||
localStorage.removeItem(API_KEY_STORAGE);
|
||
}
|
||
};
|
||
|
||
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();`;
|
||
|
||
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)`;
|
||
|
||
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)
|
||
}`;
|
||
|
||
default:
|
||
return '';
|
||
}
|
||
};
|
||
|
||
const executeRequest = async () => {
|
||
if (!apiKey) {
|
||
setError('Please enter your API key above ✨');
|
||
return;
|
||
}
|
||
|
||
setIsExecuting(true);
|
||
setError(null);
|
||
|
||
try {
|
||
const body: Record<string, any> = {};
|
||
parameters.forEach((param) => {
|
||
if (paramValues[param.name]) {
|
||
body[param.name] = paramValues[param.name];
|
||
}
|
||
});
|
||
|
||
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,
|
||
});
|
||
|
||
const data = await res.json();
|
||
setResponse(data);
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : 'Failed to execute request 😔');
|
||
} finally {
|
||
setIsExecuting(false);
|
||
}
|
||
};
|
||
|
||
const copyCode = () => {
|
||
navigator.clipboard.writeText(generateCode());
|
||
};
|
||
|
||
return (
|
||
<div className="my-12">
|
||
{/* API Key Card - Large and Colorful */}
|
||
<div className="mb-6 p-6 bg-gradient-to-br from-purple-500/10 to-cyan-500/10 backdrop-blur-sm border-2 border-purple-500/40 rounded-2xl shadow-xl shadow-purple-500/20">
|
||
<div className="flex items-center gap-4 mb-3">
|
||
<span className="text-2xl">🔑</span>
|
||
<label className="text-sm font-bold text-white">Your API Key</label>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<input
|
||
type="password"
|
||
value={apiKey}
|
||
onChange={(e) => handleApiKeyChange(e.target.value)}
|
||
placeholder="Enter your API key (saved locally)"
|
||
className="flex-1 px-4 py-3 text-sm bg-slate-900/80 border-2 border-purple-500/30 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500/50 transition-all"
|
||
/>
|
||
<div
|
||
className={`px-4 py-3 rounded-xl font-semibold text-sm transition-all ${
|
||
apiKey
|
||
? 'bg-green-500/20 text-green-400 border-2 border-green-500/40 shadow-lg shadow-green-500/20'
|
||
: 'bg-gray-500/20 text-gray-400 border-2 border-gray-500/20'
|
||
}`}
|
||
>
|
||
{apiKey ? '✓ Ready' : '⚠️ Required'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Main Widget Card - Large Gradient Border */}
|
||
<div
|
||
className={`bg-gradient-to-br from-slate-900/80 to-slate-950/80 backdrop-blur-sm border-2 border-purple-500/40 rounded-2xl overflow-hidden shadow-2xl shadow-purple-500/30 transition-all duration-500 ${
|
||
isExpanded ? 'fixed inset-4 z-50' : ''
|
||
}`}
|
||
>
|
||
{/* Header with Expand Button */}
|
||
<div className="flex items-center justify-between bg-slate-950/80 px-6 py-4 border-b-2 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>
|
||
<span className="text-sm text-gray-400">{endpoint}</span>
|
||
</div>
|
||
<button
|
||
onClick={() => setIsExpanded(!isExpanded)}
|
||
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-2 border-cyan-500/40 hover:border-cyan-500/60"
|
||
>
|
||
{isExpanded ? '✕ Close' : '⛶ Expand'}
|
||
</button>
|
||
</div>
|
||
|
||
<div className="grid md:grid-cols-2 gap-6 p-6">
|
||
{/* Left: Code Editor */}
|
||
<div>
|
||
{/* Language Tabs - Colorful Pills */}
|
||
<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-2 ${
|
||
language === lang
|
||
? color === 'purple'
|
||
? 'bg-purple-500/30 text-white border-purple-500/60 shadow-lg shadow-purple-500/30'
|
||
: color === 'cyan'
|
||
? 'bg-cyan-500/30 text-white border-cyan-500/60 shadow-lg shadow-cyan-500/30'
|
||
: 'bg-amber-500/30 text-white border-amber-500/60 shadow-lg shadow-amber-500/30'
|
||
: '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 - Gradient Border */}
|
||
<div className="border-2 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-2 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 h-80 overflow-auto">
|
||
<pre className="text-xs text-gray-300 leading-relaxed">
|
||
<code>{generateCode()}</code>
|
||
</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right: Response */}
|
||
<div>
|
||
<h4 className="text-sm font-bold text-white mb-4 flex items-center gap-2">
|
||
<span className="text-xl">📦</span>
|
||
Response
|
||
</h4>
|
||
|
||
<div className="border-2 border-purple-500/30 rounded-xl overflow-hidden shadow-lg">
|
||
<div className="bg-slate-950/80 px-4 py-3 border-b-2 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-2 border-green-500/40 shadow-lg shadow-green-500/20'
|
||
: 'bg-red-500/20 text-red-400 border-2 border-red-500/40 shadow-lg shadow-red-500/20'
|
||
}`}
|
||
>
|
||
{response.success ? '✓ Success' : '✕ Error'}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="p-4 bg-slate-950/50 h-80 overflow-auto">
|
||
{error ? (
|
||
<div className="p-4 bg-red-500/10 border-2 border-red-500/40 rounded-xl text-red-400 text-sm shadow-lg shadow-red-500/20">
|
||
<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-xs 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-2xl mb-3">🚀</p>
|
||
<p className="text-sm text-gray-500">
|
||
Click <strong className="text-purple-400">"Test Now"</strong> below to see the response
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Large Test Now Button */}
|
||
<div className="px-6 pb-6">
|
||
<button
|
||
onClick={executeRequest}
|
||
disabled={!apiKey || isExecuting}
|
||
className="w-full px-8 py-4 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 hover:scale-105 flex items-center justify-center gap-3 border-2 border-purple-500/50"
|
||
>
|
||
{isExecuting ? (
|
||
<>
|
||
<span className="animate-spin">⚡</span>
|
||
<span>Executing...</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<span>⚡</span>
|
||
<span>Test Now</span>
|
||
</>
|
||
)}
|
||
</button>
|
||
<p className="mt-3 text-xs text-gray-500 text-center">{description}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|