feat: add code examples to upload page
This commit is contained in:
parent
f080063746
commit
5aef778fc5
|
|
@ -5,6 +5,7 @@ import { MinimizedApiKey } from '@/components/demo/MinimizedApiKey';
|
||||||
import { GenerationTimer } from '@/components/demo/GenerationTimer';
|
import { GenerationTimer } from '@/components/demo/GenerationTimer';
|
||||||
import { ResultCard } from '@/components/demo/ResultCard';
|
import { ResultCard } from '@/components/demo/ResultCard';
|
||||||
import { AdvancedOptionsModal, AdvancedOptionsData } from '@/components/demo/AdvancedOptionsModal';
|
import { AdvancedOptionsModal, AdvancedOptionsData } from '@/components/demo/AdvancedOptionsModal';
|
||||||
|
import { ImageZoomModal } from '@/components/demo/ImageZoomModal';
|
||||||
|
|
||||||
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_KEY = 'banatie_demo_api_key';
|
const API_KEY_STORAGE_KEY = 'banatie_demo_api_key';
|
||||||
|
|
@ -621,29 +622,7 @@ export default function DemoTTIPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Zoom Modal */}
|
{/* Zoom Modal */}
|
||||||
{zoomedImage && (
|
<ImageZoomModal imageUrl={zoomedImage} onClose={() => setZoomedImage(null)} />
|
||||||
<div
|
|
||||||
className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4"
|
|
||||||
onClick={() => setZoomedImage(null)}
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-label="Zoomed image view"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => setZoomedImage(null)}
|
|
||||||
className="absolute top-4 right-4 w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 text-white flex items-center justify-center transition-colors focus:ring-2 focus:ring-white"
|
|
||||||
aria-label="Close zoomed image"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
<img
|
|
||||||
src={zoomedImage}
|
|
||||||
alt="Zoomed"
|
|
||||||
className="max-w-full max-h-full object-contain"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
import { useState, useEffect, useRef, DragEvent, ChangeEvent } from 'react';
|
import { useState, useEffect, useRef, DragEvent, ChangeEvent } from 'react';
|
||||||
import { MinimizedApiKey } from '@/components/demo/MinimizedApiKey';
|
import { MinimizedApiKey } from '@/components/demo/MinimizedApiKey';
|
||||||
|
import { CodeExamplesWidget } from '@/components/demo/CodeExamplesWidget';
|
||||||
|
import { ImageZoomModal } from '@/components/demo/ImageZoomModal';
|
||||||
|
|
||||||
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_KEY = 'banatie_demo_api_key';
|
const API_KEY_STORAGE_KEY = 'banatie_demo_api_key';
|
||||||
|
|
@ -61,6 +63,12 @@ export default function DemoUploadPage() {
|
||||||
// History State
|
// History State
|
||||||
const [uploadHistory, setUploadHistory] = useState<UploadHistoryItem[]>([]);
|
const [uploadHistory, setUploadHistory] = useState<UploadHistoryItem[]>([]);
|
||||||
|
|
||||||
|
// Zoom Modal State
|
||||||
|
const [zoomedImageUrl, setZoomedImageUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Copy Feedback State
|
||||||
|
const [codeCopied, setCodeCopied] = useState(false);
|
||||||
|
|
||||||
// Refs
|
// Refs
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
|
@ -321,6 +329,42 @@ export default function DemoUploadPage() {
|
||||||
return `${(ms / 1000).toFixed(2)}s`;
|
return `${(ms / 1000).toFixed(2)}s`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateUploadCodeExamples = (item: UploadHistoryItem, key: string, baseUrl: string) => {
|
||||||
|
const localPath = `./${item.originalName}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
curl: `curl -X POST "${baseUrl}/api/upload" \\
|
||||||
|
-H "X-API-Key: ${key}" \\
|
||||||
|
-F "file=@${localPath}"`,
|
||||||
|
fetch: `const formData = new FormData();
|
||||||
|
formData.append('file', fileInput.files[0]);
|
||||||
|
|
||||||
|
fetch('${baseUrl}/api/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': '${key}'
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => console.log(data))
|
||||||
|
.catch(error => console.error('Error:', error));`,
|
||||||
|
rest: `POST ${baseUrl}/api/upload
|
||||||
|
Headers:
|
||||||
|
X-API-Key: ${key}
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
|
||||||
|
Body (form-data):
|
||||||
|
file: @${localPath}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyCode = (code: string) => {
|
||||||
|
navigator.clipboard.writeText(code);
|
||||||
|
setCodeCopied(true);
|
||||||
|
setTimeout(() => setCodeCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 max-w-7xl mx-auto px-6 py-12 md:py-16 min-h-screen">
|
<div className="relative z-10 max-w-7xl mx-auto px-6 py-12 md:py-16 min-h-screen">
|
||||||
{apiKeyValidated && apiKeyInfo && (
|
{apiKeyValidated && apiKeyInfo && (
|
||||||
|
|
@ -522,52 +566,94 @@ export default function DemoUploadPage() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{uploadHistory.length > 0 && (
|
{uploadHistory.length > 0 && (
|
||||||
<section className="space-y-4" aria-label="Upload History">
|
<section className="space-y-6" aria-label="Upload History">
|
||||||
<h2 className="text-xl md:text-2xl font-bold text-white">Upload History</h2>
|
<h2 className="text-xl md:text-2xl font-bold text-white">Upload History</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="space-y-6">
|
||||||
{uploadHistory.map((item) => (
|
{uploadHistory.map((item) => (
|
||||||
<div
|
<div key={item.id} className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||||
key={item.id}
|
{/* Column 1: Image Card */}
|
||||||
className="p-4 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-xl hover:border-slate-600 transition-all"
|
<div className="lg:col-span-1">
|
||||||
>
|
<div className="p-4 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-xl hover:border-slate-600 transition-all h-full">
|
||||||
<div className="aspect-video bg-slate-800 rounded-lg mb-3 overflow-hidden">
|
<div
|
||||||
<img
|
className="aspect-video bg-slate-800 rounded-lg mb-3 overflow-hidden cursor-pointer group relative"
|
||||||
src={item.url}
|
onClick={() => setZoomedImageUrl(item.url)}
|
||||||
alt={item.originalName}
|
role="button"
|
||||||
className="w-full h-full object-cover"
|
tabIndex={0}
|
||||||
|
aria-label="View full size image"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
setZoomedImageUrl(item.url);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={item.url}
|
||||||
|
alt={item.originalName}
|
||||||
|
className="w-full h-full object-cover transition-transform group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
className="w-12 h-12 text-white"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-white text-sm font-medium truncate" title={item.originalName}>
|
||||||
|
{item.originalName}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-2 text-xs">
|
||||||
|
<span className="px-2 py-1 bg-slate-700/50 text-gray-300 rounded">
|
||||||
|
{formatFileSize(item.size)}
|
||||||
|
</span>
|
||||||
|
<span className="px-2 py-1 bg-green-600/20 text-green-400 rounded border border-green-600/30">
|
||||||
|
{formatDuration(item.durationMs)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
{item.timestamp.toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setZoomedImageUrl(item.url)}
|
||||||
|
className="mt-2 w-full text-center px-3 py-1.5 text-xs text-amber-400 hover:text-amber-300 border border-amber-600/30 hover:border-amber-500/50 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
View Full Size
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Columns 2-3: API Code Examples Widget */}
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<CodeExamplesWidget
|
||||||
|
codeExamples={generateUploadCodeExamples(item, apiKey, API_BASE_URL)}
|
||||||
|
onCopy={handleCopyCode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="text-white text-sm font-medium truncate" title={item.originalName}>
|
|
||||||
{item.originalName}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center justify-between text-xs text-gray-400">
|
|
||||||
<span>{formatFileSize(item.size)}</span>
|
|
||||||
<span>{formatDuration(item.durationMs)}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
{item.timestamp.toLocaleString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href={item.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="mt-3 block text-center px-3 py-1.5 text-xs text-amber-400 hover:text-amber-300 border border-amber-600/30 hover:border-amber-500/50 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
View Full Image
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Image Zoom Modal */}
|
||||||
|
<ImageZoomModal imageUrl={zoomedImageUrl} onClose={() => setZoomedImageUrl(null)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
type CodeTab = 'curl' | 'fetch' | 'rest';
|
||||||
|
|
||||||
|
interface CodeExamplesWidgetProps {
|
||||||
|
codeExamples: {
|
||||||
|
curl: string;
|
||||||
|
fetch: string;
|
||||||
|
rest: string;
|
||||||
|
};
|
||||||
|
onCopy: (text: string) => void;
|
||||||
|
defaultTab?: CodeTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CodeExamplesWidget = ({
|
||||||
|
codeExamples,
|
||||||
|
onCopy,
|
||||||
|
defaultTab = 'curl',
|
||||||
|
}: CodeExamplesWidgetProps) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<CodeTab>(defaultTab);
|
||||||
|
|
||||||
|
const getCodeForTab = () => {
|
||||||
|
switch (activeTab) {
|
||||||
|
case 'curl':
|
||||||
|
return codeExamples.curl;
|
||||||
|
case 'fetch':
|
||||||
|
return codeExamples.fetch;
|
||||||
|
case 'rest':
|
||||||
|
return codeExamples.rest;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-950/50 rounded-xl border border-slate-700 overflow-hidden">
|
||||||
|
<div className="flex items-center gap-2 bg-slate-900/50 px-4 py-2 border-b border-slate-700">
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-red-500/50"></div>
|
||||||
|
<div className="w-3 h-3 rounded-full bg-yellow-500/50"></div>
|
||||||
|
<div className="w-3 h-3 rounded-full bg-green-500/50"></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex gap-2 ml-4">
|
||||||
|
<TabButton
|
||||||
|
active={activeTab === 'curl'}
|
||||||
|
onClick={() => setActiveTab('curl')}
|
||||||
|
label="cURL"
|
||||||
|
/>
|
||||||
|
<TabButton
|
||||||
|
active={activeTab === 'fetch'}
|
||||||
|
onClick={() => setActiveTab('fetch')}
|
||||||
|
label="JS Fetch"
|
||||||
|
/>
|
||||||
|
<TabButton
|
||||||
|
active={activeTab === 'rest'}
|
||||||
|
onClick={() => setActiveTab('rest')}
|
||||||
|
label="REST"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => onCopy(getCodeForTab())}
|
||||||
|
className="px-3 py-1 text-xs bg-amber-600/20 hover:bg-amber-600/30 text-amber-400 rounded transition-colors"
|
||||||
|
aria-label="Copy code"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre className="p-4 text-xs md:text-sm text-gray-300 overflow-x-auto">
|
||||||
|
<code>{getCodeForTab()}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TabButton = ({
|
||||||
|
active,
|
||||||
|
onClick,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
active: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
label: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={`px-3 py-1 text-xs rounded transition-colors ${
|
||||||
|
active ? 'bg-slate-700 text-white' : 'text-gray-400 hover:text-white'
|
||||||
|
}`}
|
||||||
|
aria-pressed={active}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
interface ImageZoomModalProps {
|
||||||
|
imageUrl: string | null;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImageZoomModal = ({ imageUrl, onClose }: ImageZoomModalProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (imageUrl) {
|
||||||
|
document.addEventListener('keydown', handleEscape);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleEscape);
|
||||||
|
};
|
||||||
|
}, [imageUrl, onClose]);
|
||||||
|
|
||||||
|
if (!imageUrl) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4"
|
||||||
|
onClick={onClose}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Zoomed image view"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-4 right-4 w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 text-white flex items-center justify-center transition-colors focus:ring-2 focus:ring-white"
|
||||||
|
aria-label="Close zoomed image"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
alt="Zoomed"
|
||||||
|
className="max-w-full max-h-full object-contain"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||||
import { InspectMode } from './InspectMode';
|
import { InspectMode } from './InspectMode';
|
||||||
import { PromptReuseButton } from './PromptReuseButton';
|
import { PromptReuseButton } from './PromptReuseButton';
|
||||||
import { CompletedTimerBadge } from './GenerationTimer';
|
import { CompletedTimerBadge } from './GenerationTimer';
|
||||||
|
import { CodeExamplesWidget } from './CodeExamplesWidget';
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
|
|
@ -57,7 +58,6 @@ interface ResultCardProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ViewMode = 'preview' | 'inspect';
|
type ViewMode = 'preview' | 'inspect';
|
||||||
type CodeTab = 'curl' | 'fetch' | 'rest';
|
|
||||||
|
|
||||||
export function ResultCard({
|
export function ResultCard({
|
||||||
result,
|
result,
|
||||||
|
|
@ -68,7 +68,6 @@ export function ResultCard({
|
||||||
onReusePrompt,
|
onReusePrompt,
|
||||||
}: ResultCardProps) {
|
}: ResultCardProps) {
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('preview');
|
const [viewMode, setViewMode] = useState<ViewMode>('preview');
|
||||||
const [activeTab, setActiveTab] = useState<CodeTab>('curl');
|
|
||||||
|
|
||||||
// Build enhancement options JSON for code examples
|
// Build enhancement options JSON for code examples
|
||||||
const buildEnhancementOptionsJson = () => {
|
const buildEnhancementOptionsJson = () => {
|
||||||
|
|
@ -165,15 +164,10 @@ X-API-Key: ${apiKey}
|
||||||
"filename": "generated_image"
|
"filename": "generated_image"
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const getCodeForTab = () => {
|
const codeExamples = {
|
||||||
switch (activeTab) {
|
curl: curlCode,
|
||||||
case 'curl':
|
fetch: fetchCode,
|
||||||
return curlCode;
|
rest: restCode,
|
||||||
case 'fetch':
|
|
||||||
return fetchCode;
|
|
||||||
case 'rest':
|
|
||||||
return restCode;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -240,12 +234,7 @@ X-API-Key: ${apiKey}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* API Code Examples */}
|
{/* API Code Examples */}
|
||||||
<CodeExamples
|
<CodeExamplesWidget codeExamples={codeExamples} onCopy={onCopy} />
|
||||||
activeTab={activeTab}
|
|
||||||
setActiveTab={setActiveTab}
|
|
||||||
code={getCodeForTab()}
|
|
||||||
onCopy={onCopy}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<InspectMode
|
<InspectMode
|
||||||
|
|
@ -385,76 +374,3 @@ function ImagePreview({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Code Examples Component
|
|
||||||
function CodeExamples({
|
|
||||||
activeTab,
|
|
||||||
setActiveTab,
|
|
||||||
code,
|
|
||||||
onCopy,
|
|
||||||
}: {
|
|
||||||
activeTab: CodeTab;
|
|
||||||
setActiveTab: (tab: CodeTab) => void;
|
|
||||||
code: string;
|
|
||||||
onCopy: (text: string) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="bg-slate-950/50 rounded-xl border border-slate-700 overflow-hidden">
|
|
||||||
<div className="flex items-center gap-2 bg-slate-900/50 px-4 py-2 border-b border-slate-700">
|
|
||||||
<div className="flex gap-1.5">
|
|
||||||
<div className="w-3 h-3 rounded-full bg-red-500/50"></div>
|
|
||||||
<div className="w-3 h-3 rounded-full bg-yellow-500/50"></div>
|
|
||||||
<div className="w-3 h-3 rounded-full bg-green-500/50"></div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 flex gap-2 ml-4">
|
|
||||||
<TabButton
|
|
||||||
active={activeTab === 'curl'}
|
|
||||||
onClick={() => setActiveTab('curl')}
|
|
||||||
label="cURL"
|
|
||||||
/>
|
|
||||||
<TabButton
|
|
||||||
active={activeTab === 'fetch'}
|
|
||||||
onClick={() => setActiveTab('fetch')}
|
|
||||||
label="JS Fetch"
|
|
||||||
/>
|
|
||||||
<TabButton
|
|
||||||
active={activeTab === 'rest'}
|
|
||||||
onClick={() => setActiveTab('rest')}
|
|
||||||
label="REST"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => onCopy(code)}
|
|
||||||
className="px-3 py-1 text-xs bg-amber-600/20 hover:bg-amber-600/30 text-amber-400 rounded transition-colors"
|
|
||||||
aria-label="Copy code"
|
|
||||||
>
|
|
||||||
Copy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-xs md:text-sm text-gray-300 overflow-x-auto">
|
|
||||||
<code>{code}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabButton({
|
|
||||||
active,
|
|
||||||
onClick,
|
|
||||||
label,
|
|
||||||
}: {
|
|
||||||
active: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
label: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
className={`px-3 py-1 text-xs rounded transition-colors ${
|
|
||||||
active ? 'bg-slate-700 text-white' : 'text-gray-400 hover:text-white'
|
|
||||||
}`}
|
|
||||||
aria-pressed={active}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue