feat: update interactive snippet
This commit is contained in:
parent
658f1420db
commit
7368d287e9
|
|
@ -47,12 +47,11 @@ const parameters = [
|
|||
export default function TextToImageAPIPage() {
|
||||
return (
|
||||
<>
|
||||
{/* Subsection Navigation */}
|
||||
{/* Subsection Navigation with API Key Input */}
|
||||
<SubsectionNav
|
||||
items={navItems}
|
||||
currentPath="/docs/final/api/text-to-image"
|
||||
ctaText="Join Beta"
|
||||
ctaHref="/signup"
|
||||
showApiKeyInput={true}
|
||||
/>
|
||||
|
||||
<DocsLayoutFinal
|
||||
|
|
|
|||
|
|
@ -1,25 +1,17 @@
|
|||
'use client';
|
||||
|
||||
/**
|
||||
* Interactive API Widget - Final Variant
|
||||
* Interactive API Widget - Final Variant (Redesigned)
|
||||
*
|
||||
* Enhanced version of Variant A with:
|
||||
* 1. Expand button for full-screen code view
|
||||
* 2. Success response styling (green accent)
|
||||
* 3. Error response styling (red accent)
|
||||
* 4. Clickable image URLs in response
|
||||
* 5. Status badges (200 Success, Error)
|
||||
*
|
||||
* Features:
|
||||
* - Multi-language code tabs (curl, JavaScript, Python, Go)
|
||||
* - API key input field (persists via localStorage)
|
||||
* - "Try It" button to execute live requests
|
||||
* - Response viewer with enhanced styling
|
||||
* - Clean, focused design
|
||||
* 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';
|
||||
import { CodeBlockExpanded } from '@/components/docs/shared/CodeBlockExpanded';
|
||||
|
||||
interface InteractiveAPIWidgetFinalProps {
|
||||
endpoint: string;
|
||||
|
|
@ -39,6 +31,21 @@ 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,
|
||||
|
|
@ -51,23 +58,31 @@ export const InteractiveAPIWidgetFinal = ({
|
|||
const [response, setResponse] = useState<any>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// Parameter values
|
||||
const [paramValues, setParamValues] = useState<Record<string, string>>({});
|
||||
|
||||
// Load API key from localStorage
|
||||
// Load API key from localStorage and listen for changes
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem(API_KEY_STORAGE);
|
||||
if (stored) setApiKey(stored);
|
||||
const loadApiKey = () => {
|
||||
const stored = localStorage.getItem(API_KEY_STORAGE);
|
||||
if (stored) setApiKey(stored);
|
||||
};
|
||||
|
||||
// Initialize with proper defaults
|
||||
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',
|
||||
};
|
||||
|
||||
// Override with parameter defaults if provided
|
||||
parameters.forEach((param) => {
|
||||
if (param.defaultValue) {
|
||||
defaults[param.name] = param.defaultValue;
|
||||
|
|
@ -75,17 +90,11 @@ export const InteractiveAPIWidgetFinal = ({
|
|||
});
|
||||
|
||||
setParamValues(defaults);
|
||||
}, [parameters]);
|
||||
|
||||
// Save API key to localStorage
|
||||
const handleApiKeyChange = (value: string) => {
|
||||
setApiKey(value);
|
||||
if (value) {
|
||||
localStorage.setItem(API_KEY_STORAGE, value);
|
||||
} else {
|
||||
localStorage.removeItem(API_KEY_STORAGE);
|
||||
}
|
||||
};
|
||||
return () => {
|
||||
window.removeEventListener('apiKeyChanged', handleApiKeyChange as EventListener);
|
||||
};
|
||||
}, [parameters]);
|
||||
|
||||
// Generate code examples
|
||||
const generateCode = (): string => {
|
||||
|
|
@ -142,7 +151,6 @@ print(response.json())`;
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
|
|
@ -169,84 +177,10 @@ func main() {
|
|||
}
|
||||
};
|
||||
|
||||
// Generate all language codes at once
|
||||
const generateAllCodes = (): Record<Language, string> => {
|
||||
const url = `${API_BASE_URL}${endpoint}`;
|
||||
|
||||
return {
|
||||
curl: `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"
|
||||
}'`,
|
||||
|
||||
javascript: `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);`,
|
||||
|
||||
python: `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())`,
|
||||
|
||||
go: `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()
|
||||
}`,
|
||||
};
|
||||
};
|
||||
|
||||
// Execute API request
|
||||
const executeRequest = async () => {
|
||||
if (!apiKey) {
|
||||
setError('Please enter your API key');
|
||||
setError('Please enter your API key in the top-right corner');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -255,7 +189,6 @@ func main() {
|
|||
setResponse(null);
|
||||
|
||||
try {
|
||||
// Build proper request body
|
||||
const body = {
|
||||
prompt: paramValues.prompt || 'a futuristic city at sunset',
|
||||
filename: paramValues.filename || 'demo',
|
||||
|
|
@ -280,7 +213,6 @@ func main() {
|
|||
|
||||
console.log('🟢 Response Status:', res.status, res.statusText);
|
||||
|
||||
// Handle non-OK responses
|
||||
if (!res.ok) {
|
||||
let errorMessage = `HTTP ${res.status}: ${res.statusText}`;
|
||||
try {
|
||||
|
|
@ -309,145 +241,269 @@ func main() {
|
|||
navigator.clipboard.writeText(generateCode());
|
||||
};
|
||||
|
||||
// Render response with clickable URLs
|
||||
const renderResponse = (data: any): string => {
|
||||
return JSON.stringify(data, null, 2);
|
||||
// Expand API key input in navigation
|
||||
const expandApiKey = () => {
|
||||
window.dispatchEvent(new CustomEvent('expandApiKeyInput'));
|
||||
};
|
||||
|
||||
// Check if response is success
|
||||
const isSuccess = response && response.success === true;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="my-8 bg-slate-900/50 backdrop-blur-sm border border-slate-700 rounded-2xl overflow-hidden">
|
||||
{/* Header with API Key Input */}
|
||||
<div className="p-4 border-b border-slate-700 bg-slate-900/80">
|
||||
<div className="flex flex-col md:flex-row gap-3 items-start md:items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white mb-1">Try it out</h3>
|
||||
<p className="text-xs text-gray-500">{description}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 w-full md:w-64">
|
||||
<input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => handleApiKeyChange(e.target.value)}
|
||||
placeholder="Enter your API key"
|
||||
className="w-full px-3 py-2 text-xs bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
{/* 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>
|
||||
</div>
|
||||
|
||||
{/* Language Tabs with Expand Button */}
|
||||
<div className="flex items-center justify-between bg-slate-950/50 px-4 py-2 border-b border-slate-700">
|
||||
<div className="flex gap-2">
|
||||
{(['curl', 'javascript', 'python', 'go'] as Language[]).map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
onClick={() => setLanguage(lang)}
|
||||
className={`px-3 py-1 text-xs rounded transition-colors ${
|
||||
language === lang
|
||||
? 'bg-slate-700 text-white'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{lang === 'javascript' ? 'JavaScript' : lang.charAt(0).toUpperCase() + lang.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setIsExpanded(true)}
|
||||
className="px-3 py-1 text-xs text-gray-400 hover:text-white transition-colors"
|
||||
aria-label="Expand code view"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={copyCode}
|
||||
className="px-3 py-1 text-xs bg-purple-600/20 hover:bg-purple-600/30 text-purple-400 rounded transition-colors"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code Display */}
|
||||
<div className="p-4 bg-slate-950/50">
|
||||
<pre className="text-xs text-gray-300 overflow-x-auto">
|
||||
<code>{generateCode()}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Try It Button */}
|
||||
<div className="p-4 border-t border-slate-700 bg-slate-900/50">
|
||||
<button
|
||||
onClick={executeRequest}
|
||||
disabled={!apiKey || isExecuting}
|
||||
className="w-full 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"
|
||||
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"
|
||||
>
|
||||
{isExecuting ? 'Executing...' : 'Try It'}
|
||||
⛶ Expand
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Response Section - Enhanced with success/error styling */}
|
||||
{/* 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-slate-700">
|
||||
<div className="p-4 bg-slate-900/80">
|
||||
<h4 className="text-sm font-semibold text-white mb-3">Response</h4>
|
||||
{error ? (
|
||||
// Error Response
|
||||
<div className="relative p-4 bg-red-500/5 border border-red-500/30 rounded-xl">
|
||||
<div className="absolute top-3 right-3">
|
||||
<span className="px-2 py-1 text-xs bg-red-500/20 text-red-400 rounded-full">
|
||||
✗ Error
|
||||
</span>
|
||||
<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>
|
||||
<p className="text-sm text-red-400 mt-6">{error}</p>
|
||||
</div>
|
||||
) : isSuccess ? (
|
||||
// Success Response
|
||||
<div className="relative p-4 bg-green-500/5 border border-green-500/30 rounded-xl shadow-lg shadow-green-500/10">
|
||||
<div className="absolute top-3 right-3">
|
||||
<span className="px-2 py-1 text-xs bg-green-500/20 text-green-400 rounded-full">
|
||||
</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 mt-6">
|
||||
<code>{renderResponse(response)}</code>
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
// Normal Response
|
||||
<pre className="p-3 bg-slate-950/50 rounded-lg text-xs text-gray-300 overflow-x-auto">
|
||||
<code>{renderResponse(response)}</code>
|
||||
)}
|
||||
<pre className="text-xs text-gray-300 overflow-x-auto max-h-60">
|
||||
<code>{JSON.stringify(response, null, 2)}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Expanded Code Modal */}
|
||||
<CodeBlockExpanded
|
||||
isOpen={isExpanded}
|
||||
onClose={() => setIsExpanded(false)}
|
||||
codes={generateAllCodes()}
|
||||
initialLanguage={language}
|
||||
/>
|
||||
{/* 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
* Reusable navigation bar for documentation and other subsections
|
||||
* Features:
|
||||
* - Dark nav bar with decorative wave line
|
||||
* - Active state indicator (purple underline/highlight)
|
||||
* - "Join Beta" CTA button on the right
|
||||
* - Active state indicator (purple color)
|
||||
* - Optional API Key input on the right
|
||||
* - Responsive (hamburger menu on mobile)
|
||||
* - Can be reused across landing app sections
|
||||
*
|
||||
|
|
@ -15,12 +15,11 @@
|
|||
* <SubsectionNav
|
||||
* items={[...]}
|
||||
* currentPath="/docs/final"
|
||||
* ctaText="Join Beta"
|
||||
* ctaHref="/signup"
|
||||
* showApiKeyInput={true}
|
||||
* />
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
interface NavItem {
|
||||
label: string;
|
||||
|
|
@ -33,24 +32,89 @@ interface SubsectionNavProps {
|
|||
ctaText?: string;
|
||||
ctaHref?: string;
|
||||
onCtaClick?: () => void;
|
||||
showApiKeyInput?: boolean;
|
||||
}
|
||||
|
||||
const API_KEY_STORAGE = 'banatie_docs_api_key';
|
||||
|
||||
export const SubsectionNav = ({
|
||||
items,
|
||||
currentPath,
|
||||
ctaText = 'Join Beta',
|
||||
ctaHref = '/signup',
|
||||
onCtaClick,
|
||||
showApiKeyInput = false,
|
||||
}: SubsectionNavProps) => {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [isApiKeyExpanded, setIsApiKeyExpanded] = useState(false);
|
||||
const [keyVisible, setKeyVisible] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isActive = (href: string) => currentPath.startsWith(href);
|
||||
|
||||
// Load API key from localStorage
|
||||
useEffect(() => {
|
||||
if (showApiKeyInput) {
|
||||
const stored = localStorage.getItem(API_KEY_STORAGE);
|
||||
if (stored) setApiKey(stored);
|
||||
}
|
||||
}, [showApiKeyInput]);
|
||||
|
||||
// Handle API key changes
|
||||
const handleApiKeyChange = (value: string) => {
|
||||
setApiKey(value);
|
||||
if (value) {
|
||||
localStorage.setItem(API_KEY_STORAGE, value);
|
||||
} else {
|
||||
localStorage.removeItem(API_KEY_STORAGE);
|
||||
}
|
||||
|
||||
// Dispatch event to notify widgets
|
||||
window.dispatchEvent(new CustomEvent('apiKeyChanged', { detail: value }));
|
||||
};
|
||||
|
||||
// Handle clear API key
|
||||
const handleClear = () => {
|
||||
setApiKey('');
|
||||
localStorage.removeItem(API_KEY_STORAGE);
|
||||
window.dispatchEvent(new CustomEvent('apiKeyChanged', { detail: '' }));
|
||||
};
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsApiKeyExpanded(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isApiKeyExpanded) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isApiKeyExpanded]);
|
||||
|
||||
// Listen for custom event to expand API key from widgets
|
||||
useEffect(() => {
|
||||
const handleExpandApiKey = () => {
|
||||
setIsApiKeyExpanded(true);
|
||||
// Scroll to top to show nav
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
window.addEventListener('expandApiKeyInput', handleExpandApiKey);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('expandApiKeyInput', handleExpandApiKey);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<nav className="sticky top-0 z-40 bg-slate-950/80 backdrop-blur-sm border-b border-white/10">
|
||||
{/* Main Nav Container */}
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="flex items-center h-12">
|
||||
<div className="flex items-center justify-between h-12">
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center gap-8">
|
||||
{items.map((item) => {
|
||||
|
|
@ -70,6 +134,126 @@ export const SubsectionNav = ({
|
|||
})}
|
||||
</div>
|
||||
|
||||
{/* Right Side: API Key Input (Desktop) */}
|
||||
{showApiKeyInput && (
|
||||
<div className="hidden md:block relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsApiKeyExpanded(!isApiKeyExpanded)}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-slate-800/50 hover:bg-slate-800 border border-purple-500/30 hover:border-purple-500/50 rounded-lg transition-all"
|
||||
aria-label="Toggle API key input"
|
||||
>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full transition-colors ${
|
||||
apiKey ? 'bg-green-400' : 'bg-amber-400'
|
||||
}`}
|
||||
></div>
|
||||
<span className="text-xs text-gray-300 font-medium">
|
||||
🔑 {apiKey ? 'API Key Set' : 'API Key'}
|
||||
</span>
|
||||
<svg
|
||||
className={`w-3 h-3 text-gray-400 transition-transform ${
|
||||
isApiKeyExpanded ? 'rotate-180' : ''
|
||||
}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Card */}
|
||||
{isApiKeyExpanded && (
|
||||
<div className="absolute top-full right-0 mt-2 w-80 p-4 bg-slate-900/95 backdrop-blur-sm border border-purple-500/30 rounded-xl shadow-2xl z-50 animate-fade-in">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white mb-1">API Key</h3>
|
||||
<p className="text-xs text-gray-400">
|
||||
Enter once, use across all examples
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsApiKeyExpanded(false)}
|
||||
className="w-8 h-8 rounded-lg bg-slate-800 hover:bg-slate-700 text-gray-400 hover:text-white flex items-center justify-center transition-colors"
|
||||
aria-label="Close API key input"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label className="text-xs text-gray-500 mb-1 block">Your API Key</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type={keyVisible ? 'text' : 'password'}
|
||||
value={apiKey}
|
||||
onChange={(e) => handleApiKeyChange(e.target.value)}
|
||||
placeholder="Enter your API key"
|
||||
className="flex-1 px-3 py-2 bg-slate-800 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 rounded-lg text-sm text-gray-300 font-mono placeholder-gray-500 focus:outline-none transition-colors"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setKeyVisible(!keyVisible)}
|
||||
className="px-3 py-2 bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded-lg text-gray-400 hover:text-white transition-colors"
|
||||
aria-label={keyVisible ? 'Hide API key' : 'Show API key'}
|
||||
>
|
||||
{keyVisible ? (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{apiKey && (
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="w-full px-3 py-2 bg-red-900/20 hover:bg-red-900/30 border border-red-700/50 hover:border-red-600 rounded-lg text-red-400 text-xs font-medium transition-colors"
|
||||
>
|
||||
Clear API Key
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500">
|
||||
<p>Stored locally in your browser</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<div className="md:hidden flex items-center ml-auto">
|
||||
<button
|
||||
|
|
@ -149,6 +333,65 @@ export const SubsectionNav = ({
|
|||
</a>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* API Key Input in Mobile Menu */}
|
||||
{showApiKeyInput && (
|
||||
<div className="pt-4 mt-4 border-t border-white/10">
|
||||
<h4 className="text-xs font-semibold text-white mb-2">API Key</h4>
|
||||
<p className="text-xs text-gray-400 mb-3">
|
||||
Enter once, use across all examples
|
||||
</p>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type={keyVisible ? 'text' : 'password'}
|
||||
value={apiKey}
|
||||
onChange={(e) => handleApiKeyChange(e.target.value)}
|
||||
placeholder="Enter your API key"
|
||||
className="flex-1 px-3 py-2 bg-slate-800 border border-slate-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 rounded-lg text-sm text-gray-300 font-mono placeholder-gray-500 focus:outline-none transition-colors"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setKeyVisible(!keyVisible)}
|
||||
className="px-3 py-2 bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded-lg text-gray-400 hover:text-white transition-colors"
|
||||
aria-label={keyVisible ? 'Hide API key' : 'Show API key'}
|
||||
>
|
||||
{keyVisible ? (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{apiKey && (
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="w-full px-3 py-2 bg-red-900/20 hover:bg-red-900/30 border border-red-700/50 hover:border-red-600 rounded-lg text-red-400 text-xs font-medium transition-colors"
|
||||
>
|
||||
Clear API Key
|
||||
</button>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 mt-2">Stored locally in your browser</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue