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 { ResultCard } from '@/components/demo/ResultCard';
|
||||
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_KEY_STORAGE_KEY = 'banatie_demo_api_key';
|
||||
|
|
@ -621,29 +622,7 @@ export default function DemoTTIPage() {
|
|||
)}
|
||||
|
||||
{/* Zoom Modal */}
|
||||
{zoomedImage && (
|
||||
<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>
|
||||
)}
|
||||
<ImageZoomModal imageUrl={zoomedImage} onClose={() => setZoomedImage(null)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
import { useState, useEffect, useRef, DragEvent, ChangeEvent } from 'react';
|
||||
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_KEY_STORAGE_KEY = 'banatie_demo_api_key';
|
||||
|
|
@ -61,6 +63,12 @@ export default function DemoUploadPage() {
|
|||
// History State
|
||||
const [uploadHistory, setUploadHistory] = useState<UploadHistoryItem[]>([]);
|
||||
|
||||
// Zoom Modal State
|
||||
const [zoomedImageUrl, setZoomedImageUrl] = useState<string | null>(null);
|
||||
|
||||
// Copy Feedback State
|
||||
const [codeCopied, setCodeCopied] = useState(false);
|
||||
|
||||
// Refs
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
|
|
@ -321,6 +329,42 @@ export default function DemoUploadPage() {
|
|||
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 (
|
||||
<div className="relative z-10 max-w-7xl mx-auto px-6 py-12 md:py-16 min-h-screen">
|
||||
{apiKeyValidated && apiKeyInfo && (
|
||||
|
|
@ -522,29 +566,60 @@ export default function DemoUploadPage() {
|
|||
</section>
|
||||
|
||||
{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>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="space-y-6">
|
||||
{uploadHistory.map((item) => (
|
||||
<div key={item.id} className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
{/* Column 1: Image Card */}
|
||||
<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
|
||||
key={item.id}
|
||||
className="p-4 bg-slate-900/80 backdrop-blur-sm border border-slate-700 rounded-xl hover:border-slate-600 transition-all"
|
||||
className="aspect-video bg-slate-800 rounded-lg mb-3 overflow-hidden cursor-pointer group relative"
|
||||
onClick={() => setZoomedImageUrl(item.url)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="View full size image"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setZoomedImageUrl(item.url);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="aspect-video bg-slate-800 rounded-lg mb-3 overflow-hidden">
|
||||
<img
|
||||
src={item.url}
|
||||
alt={item.originalName}
|
||||
className="w-full h-full object-cover"
|
||||
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 className="space-y-1">
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<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 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', {
|
||||
|
|
@ -554,20 +629,31 @@ export default function DemoUploadPage() {
|
|||
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"
|
||||
<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 Image
|
||||
</a>
|
||||
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>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Image Zoom Modal */}
|
||||
<ImageZoomModal imageUrl={zoomedImageUrl} onClose={() => setZoomedImageUrl(null)} />
|
||||
</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 { PromptReuseButton } from './PromptReuseButton';
|
||||
import { CompletedTimerBadge } from './GenerationTimer';
|
||||
import { CodeExamplesWidget } from './CodeExamplesWidget';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
|
||||
|
|
@ -57,7 +58,6 @@ interface ResultCardProps {
|
|||
}
|
||||
|
||||
type ViewMode = 'preview' | 'inspect';
|
||||
type CodeTab = 'curl' | 'fetch' | 'rest';
|
||||
|
||||
export function ResultCard({
|
||||
result,
|
||||
|
|
@ -68,7 +68,6 @@ export function ResultCard({
|
|||
onReusePrompt,
|
||||
}: ResultCardProps) {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('preview');
|
||||
const [activeTab, setActiveTab] = useState<CodeTab>('curl');
|
||||
|
||||
// Build enhancement options JSON for code examples
|
||||
const buildEnhancementOptionsJson = () => {
|
||||
|
|
@ -165,15 +164,10 @@ X-API-Key: ${apiKey}
|
|||
"filename": "generated_image"
|
||||
}`;
|
||||
|
||||
const getCodeForTab = () => {
|
||||
switch (activeTab) {
|
||||
case 'curl':
|
||||
return curlCode;
|
||||
case 'fetch':
|
||||
return fetchCode;
|
||||
case 'rest':
|
||||
return restCode;
|
||||
}
|
||||
const codeExamples = {
|
||||
curl: curlCode,
|
||||
fetch: fetchCode,
|
||||
rest: restCode,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -240,12 +234,7 @@ X-API-Key: ${apiKey}
|
|||
</div>
|
||||
|
||||
{/* API Code Examples */}
|
||||
<CodeExamples
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
code={getCodeForTab()}
|
||||
onCopy={onCopy}
|
||||
/>
|
||||
<CodeExamplesWidget codeExamples={codeExamples} onCopy={onCopy} />
|
||||
</>
|
||||
) : (
|
||||
<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