From 7368d287e9edf9bf0a30928f9f6fb3cd791bc55b Mon Sep 17 00:00:00 2001 From: Oleg Proskurin Date: Wed, 15 Oct 2025 00:31:49 +0700 Subject: [PATCH] feat: update interactive snippet --- .../app/docs/final/api/text-to-image/page.tsx | 5 +- .../docs/final/InteractiveAPIWidgetFinal.tsx | 506 ++++++++++-------- .../src/components/shared/SubsectionNav.tsx | 261 ++++++++- 3 files changed, 535 insertions(+), 237 deletions(-) diff --git a/apps/landing/src/app/docs/final/api/text-to-image/page.tsx b/apps/landing/src/app/docs/final/api/text-to-image/page.tsx index 7c1ef9e..51df4be 100644 --- a/apps/landing/src/app/docs/final/api/text-to-image/page.tsx +++ b/apps/landing/src/app/docs/final/api/text-to-image/page.tsx @@ -47,12 +47,11 @@ const parameters = [ export default function TextToImageAPIPage() { return ( <> - {/* Subsection Navigation */} + {/* Subsection Navigation with API Key Input */} { + 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(null); const [error, setError] = useState(null); const [isExpanded, setIsExpanded] = useState(false); - - // Parameter values const [paramValues, setParamValues] = useState>({}); - // 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 = { 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 => { - 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 ( <> -
- {/* Header with API Key Input */} -
-
-
-

Try it out

-

{description}

-
-
- 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 */} +
+ {/* Header */} +
+
+
+ {method}
+ {endpoint}
-
- - {/* Language Tabs with Expand Button */} -
-
- {(['curl', 'javascript', 'python', 'go'] as Language[]).map((lang) => ( - - ))} -
-
- - -
-
- - {/* Code Display */} -
-
-            {generateCode()}
-          
-
- - {/* Try It Button */} -
- {/* Response Section - Enhanced with success/error styling */} + {/* Code Section - Full Width */} +
+ {/* Language Tabs */} +
+ {(['curl', 'javascript', 'python', 'go'] as Language[]).map((lang, idx) => { + const colors = ['purple', 'cyan', 'amber', 'purple']; + const color = colors[idx]; + return ( + + ); + })} +
+ + {/* Code Display */} +
+
+ Code Example + +
+
+
+                {generateCode()}
+              
+
+
+
+ + {/* Footer */} +
+ + +
+ + {/* Inline Response (if any) */} {(response || error) && ( -
-
-

Response

- {error ? ( - // Error Response -
-
- - ✗ Error - +
+

+ 📦 + Response +

+ {error ? ( +
+
+ ⚠️ +
+

Error

+

{error}

-

{error}

- ) : isSuccess ? ( - // Success Response -
-
- +
+ ) : ( +
+ {isSuccess && ( +
+ ✓ 200 Success
-
-                    {renderResponse(response)}
-                  
-
- ) : ( - // Normal Response -
-                  {renderResponse(response)}
+                )}
+                
+                  {JSON.stringify(response, null, 2)}
                 
- )} -
+
+ )}
)}
- {/* Expanded Code Modal */} - setIsExpanded(false)} - codes={generateAllCodes()} - initialLanguage={language} - /> + {/* Expanded Modal */} + {isExpanded && ( +
+
+ {/* Modal Header */} +
+
+
+ {method} +
+ {endpoint} +
+ +
+ + {/* Two-Panel Layout */} +
+ {/* Left: Code */} +
+
+ {(['curl', 'javascript', 'python', 'go'] as Language[]).map((lang, idx) => { + const colors = ['purple', 'cyan', 'amber', 'purple']; + const color = colors[idx]; + return ( + + ); + })} +
+ +
+
+ Code Example + +
+
+
+                      {generateCode()}
+                    
+
+
+
+ + {/* Right: Response */} +
+

+ 📦 + Response +

+ +
+
+ {response && ( + + {response.success ? '✓ Success' : '✕ Error'} + + )} +
+
+ {error ? ( +
+
+ ⚠️ +
+

Error

+

{error}

+
+
+
+ ) : response ? ( +
+                        {JSON.stringify(response, null, 2)}
+                      
+ ) : ( +
+
+

🚀

+

+ Click "Try It" below to see the response +

+
+
+ )} +
+
+ + {/* Try It Button in Expanded View */} + +
+
+ + {/* Footer hint */} +
+

{description}

+
+
+
+ )} ); }; diff --git a/apps/landing/src/components/shared/SubsectionNav.tsx b/apps/landing/src/components/shared/SubsectionNav.tsx index edb1485..f3800a9 100644 --- a/apps/landing/src/components/shared/SubsectionNav.tsx +++ b/apps/landing/src/components/shared/SubsectionNav.tsx @@ -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 @@ * */ -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(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 (