feat: add code examples to upload page

This commit is contained in:
Oleg Proskurin 2025-10-11 16:08:08 +07:00
parent f080063746
commit 5aef778fc5
5 changed files with 277 additions and 149 deletions

View File

@ -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>
); );
} }

View File

@ -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>
); );
} }

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
}