feat: update interactive snippet

This commit is contained in:
Oleg Proskurin 2025-10-15 00:31:49 +07:00
parent 658f1420db
commit 7368d287e9
3 changed files with 535 additions and 237 deletions

View File

@ -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

View File

@ -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>
)}
</>
);
};

View File

@ -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>
)}