feat: update interactive snippet
This commit is contained in:
parent
658f1420db
commit
7368d287e9
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue