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() { export default function TextToImageAPIPage() {
return ( return (
<> <>
{/* Subsection Navigation */} {/* Subsection Navigation with API Key Input */}
<SubsectionNav <SubsectionNav
items={navItems} items={navItems}
currentPath="/docs/final/api/text-to-image" currentPath="/docs/final/api/text-to-image"
ctaText="Join Beta" showApiKeyInput={true}
ctaHref="/signup"
/> />
<DocsLayoutFinal <DocsLayoutFinal

View File

@ -1,25 +1,17 @@
'use client'; 'use client';
/** /**
* Interactive API Widget - Final Variant * Interactive API Widget - Final Variant (Redesigned)
* *
* Enhanced version of Variant A with: * Minimized layout inspired by Variant C with Final variant design system:
* 1. Expand button for full-screen code view * - No inline API key input (uses DocsApiKeyInput component)
* 2. Success response styling (green accent) * - Clean header with method badge, endpoint, and expand button
* 3. Error response styling (red accent) * - Full-width code snippet area
* 4. Clickable image URLs in response * - Compact footer with API key anchor link and Try It button
* 5. Status badges (200 Success, Error) * - Expanded view opens full-screen modal with code + response side-by-side
*
* 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
*/ */
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { CodeBlockExpanded } from '@/components/docs/shared/CodeBlockExpanded';
interface InteractiveAPIWidgetFinalProps { interface InteractiveAPIWidgetFinalProps {
endpoint: string; 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_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
const API_KEY_STORAGE = 'banatie_docs_api_key'; 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 = ({ export const InteractiveAPIWidgetFinal = ({
endpoint, endpoint,
method, method,
@ -51,23 +58,31 @@ export const InteractiveAPIWidgetFinal = ({
const [response, setResponse] = useState<any>(null); const [response, setResponse] = useState<any>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
// Parameter values
const [paramValues, setParamValues] = useState<Record<string, string>>({}); const [paramValues, setParamValues] = useState<Record<string, string>>({});
// Load API key from localStorage // Load API key from localStorage and listen for changes
useEffect(() => { useEffect(() => {
const stored = localStorage.getItem(API_KEY_STORAGE); const loadApiKey = () => {
if (stored) setApiKey(stored); 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> = { const defaults: Record<string, string> = {
prompt: 'a futuristic city at sunset', prompt: 'a futuristic city at sunset',
filename: 'demo', filename: 'demo',
aspectRatio: '16:9', aspectRatio: '16:9',
}; };
// Override with parameter defaults if provided
parameters.forEach((param) => { parameters.forEach((param) => {
if (param.defaultValue) { if (param.defaultValue) {
defaults[param.name] = param.defaultValue; defaults[param.name] = param.defaultValue;
@ -75,17 +90,11 @@ export const InteractiveAPIWidgetFinal = ({
}); });
setParamValues(defaults); setParamValues(defaults);
}, [parameters]);
// Save API key to localStorage return () => {
const handleApiKeyChange = (value: string) => { window.removeEventListener('apiKeyChanged', handleApiKeyChange as EventListener);
setApiKey(value); };
if (value) { }, [parameters]);
localStorage.setItem(API_KEY_STORAGE, value);
} else {
localStorage.removeItem(API_KEY_STORAGE);
}
};
// Generate code examples // Generate code examples
const generateCode = (): string => { const generateCode = (): string => {
@ -142,7 +151,6 @@ print(response.json())`;
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"net/http" "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 // Execute API request
const executeRequest = async () => { const executeRequest = async () => {
if (!apiKey) { if (!apiKey) {
setError('Please enter your API key'); setError('Please enter your API key in the top-right corner');
return; return;
} }
@ -255,7 +189,6 @@ func main() {
setResponse(null); setResponse(null);
try { try {
// Build proper request body
const body = { const body = {
prompt: paramValues.prompt || 'a futuristic city at sunset', prompt: paramValues.prompt || 'a futuristic city at sunset',
filename: paramValues.filename || 'demo', filename: paramValues.filename || 'demo',
@ -280,7 +213,6 @@ func main() {
console.log('🟢 Response Status:', res.status, res.statusText); console.log('🟢 Response Status:', res.status, res.statusText);
// Handle non-OK responses
if (!res.ok) { if (!res.ok) {
let errorMessage = `HTTP ${res.status}: ${res.statusText}`; let errorMessage = `HTTP ${res.status}: ${res.statusText}`;
try { try {
@ -309,145 +241,269 @@ func main() {
navigator.clipboard.writeText(generateCode()); navigator.clipboard.writeText(generateCode());
}; };
// Render response with clickable URLs // Expand API key input in navigation
const renderResponse = (data: any): string => { const expandApiKey = () => {
return JSON.stringify(data, null, 2); window.dispatchEvent(new CustomEvent('expandApiKeyInput'));
}; };
// Check if response is success
const isSuccess = response && response.success === true; const isSuccess = response && response.success === true;
return ( return (
<> <>
<div className="my-8 bg-slate-900/50 backdrop-blur-sm border border-slate-700 rounded-2xl overflow-hidden"> {/* Minimized Widget */}
{/* Header with API Key Input */} <div className="my-8 bg-slate-900/50 backdrop-blur-sm border border-purple-500/30 rounded-2xl overflow-hidden shadow-xl">
<div className="p-4 border-b border-slate-700 bg-slate-900/80"> {/* Header */}
<div className="flex flex-col md:flex-row gap-3 items-start md:items-center justify-between"> <div className="flex items-center justify-between bg-slate-950/80 px-6 py-4 border-b border-purple-500/30">
<div> <div className="flex items-center gap-3">
<h3 className="text-sm font-semibold text-white mb-1">Try it out</h3> <div className={`px-3 py-1.5 rounded-lg bg-gradient-to-r ${getMethodColor(method)} text-white text-sm font-bold shadow-lg`}>
<p className="text-xs text-gray-500">{description}</p> {method}
</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"
/>
</div> </div>
<code className="text-sm text-gray-400">{endpoint}</code>
</div> </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 <button
onClick={executeRequest} onClick={() => setIsExpanded(true)}
disabled={!apiKey || isExecuting} 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"
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"
> >
{isExecuting ? 'Executing...' : 'Try It'} Expand
</button> </button>
</div> </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) && ( {(response || error) && (
<div className="border-t border-slate-700"> <div className="border-t border-purple-500/30 p-6 bg-slate-950/50">
<div className="p-4 bg-slate-900/80"> <h4 className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
<h4 className="text-sm font-semibold text-white mb-3">Response</h4> <span>📦</span>
{error ? ( Response
// Error Response </h4>
<div className="relative p-4 bg-red-500/5 border border-red-500/30 rounded-xl"> {error ? (
<div className="absolute top-3 right-3"> <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">
<span className="px-2 py-1 text-xs bg-red-500/20 text-red-400 rounded-full"> <div className="flex items-start gap-2">
Error <span></span>
</span> <div>
<p className="font-semibold mb-1">Error</p>
<p className="text-xs">{error}</p>
</div> </div>
<p className="text-sm text-red-400 mt-6">{error}</p>
</div> </div>
) : isSuccess ? ( </div>
// 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={`p-4 rounded-xl border ${
<div className="absolute top-3 right-3"> isSuccess
<span className="px-2 py-1 text-xs bg-green-500/20 text-green-400 rounded-full"> ? '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 200 Success
</span> </span>
</div> </div>
<pre className="text-xs text-gray-300 overflow-x-auto mt-6"> )}
<code>{renderResponse(response)}</code> <pre className="text-xs text-gray-300 overflow-x-auto max-h-60">
</pre> <code>{JSON.stringify(response, null, 2)}</code>
</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> </pre>
)} </div>
</div> )}
</div> </div>
)} )}
</div> </div>
{/* Expanded Code Modal */} {/* Expanded Modal */}
<CodeBlockExpanded {isExpanded && (
isOpen={isExpanded} <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm">
onClose={() => setIsExpanded(false)} <div className="relative w-[90vw] h-[85vh] bg-slate-900 border border-purple-500/30 rounded-2xl shadow-2xl overflow-hidden flex flex-col">
codes={generateAllCodes()} {/* Modal Header */}
initialLanguage={language} <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 * Reusable navigation bar for documentation and other subsections
* Features: * Features:
* - Dark nav bar with decorative wave line * - Dark nav bar with decorative wave line
* - Active state indicator (purple underline/highlight) * - Active state indicator (purple color)
* - "Join Beta" CTA button on the right * - Optional API Key input on the right
* - Responsive (hamburger menu on mobile) * - Responsive (hamburger menu on mobile)
* - Can be reused across landing app sections * - Can be reused across landing app sections
* *
@ -15,12 +15,11 @@
* <SubsectionNav * <SubsectionNav
* items={[...]} * items={[...]}
* currentPath="/docs/final" * currentPath="/docs/final"
* ctaText="Join Beta" * showApiKeyInput={true}
* ctaHref="/signup"
* /> * />
*/ */
import { useState } from 'react'; import { useState, useEffect, useRef } from 'react';
interface NavItem { interface NavItem {
label: string; label: string;
@ -33,24 +32,89 @@ interface SubsectionNavProps {
ctaText?: string; ctaText?: string;
ctaHref?: string; ctaHref?: string;
onCtaClick?: () => void; onCtaClick?: () => void;
showApiKeyInput?: boolean;
} }
const API_KEY_STORAGE = 'banatie_docs_api_key';
export const SubsectionNav = ({ export const SubsectionNav = ({
items, items,
currentPath, currentPath,
ctaText = 'Join Beta', showApiKeyInput = false,
ctaHref = '/signup',
onCtaClick,
}: SubsectionNavProps) => { }: SubsectionNavProps) => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); 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); 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 ( return (
<nav className="sticky top-0 z-40 bg-slate-950/80 backdrop-blur-sm border-b border-white/10"> <nav className="sticky top-0 z-40 bg-slate-950/80 backdrop-blur-sm border-b border-white/10">
{/* Main Nav Container */} {/* Main Nav Container */}
<div className="max-w-7xl mx-auto px-6"> <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 */} {/* Desktop Navigation */}
<div className="hidden md:flex items-center gap-8"> <div className="hidden md:flex items-center gap-8">
{items.map((item) => { {items.map((item) => {
@ -70,6 +134,126 @@ export const SubsectionNav = ({
})} })}
</div> </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 */} {/* Mobile Menu Button */}
<div className="md:hidden flex items-center ml-auto"> <div className="md:hidden flex items-center ml-auto">
<button <button
@ -149,6 +333,65 @@ export const SubsectionNav = ({
</a> </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>
</div> </div>
)} )}