forge/frontend/components/FileUpload.tsx
DJP 7a804e896d Initial commit - FORGE AI unified platform
Features:
- Image generation (OpenAI, Gemini, Leonardo, Bria, Stability, Flux)
- Nano Banana iterative editing
- Video generation and upscaling
- Audio TTS, STT, sound effects (ElevenLabs)
- Text prompt studio and alt text
- User authentication with JWT/cookies
- Admin panel with voice management
- Job queue with Celery
- PostgreSQL + Redis backend
- Next.js 15 + FastAPI architecture

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2025-12-09 20:39:00 -05:00

123 lines
3.3 KiB
TypeScript

'use client';
import { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { Upload, X, FileImage, FileVideo, FileAudio, File } from 'lucide-react';
import { clsx } from 'clsx';
interface FileUploadProps {
onUpload: (file: File) => void;
accept?: Record<string, string[]>;
maxSize?: number;
label?: string;
currentFile?: File | null;
onClear?: () => void;
}
const fileIcons: Record<string, any> = {
image: FileImage,
video: FileVideo,
audio: FileAudio,
};
export default function FileUpload({
onUpload,
accept,
maxSize = 100 * 1024 * 1024, // 100MB default
label = 'Upload a file',
currentFile,
onClear,
}: FileUploadProps) {
const [error, setError] = useState<string | null>(null);
const onDrop = useCallback(
(acceptedFiles: File[], rejectedFiles: any[]) => {
setError(null);
if (rejectedFiles.length > 0) {
const rejection = rejectedFiles[0];
if (rejection.errors[0]?.code === 'file-too-large') {
setError(`File too large. Max size is ${Math.round(maxSize / 1024 / 1024)}MB`);
} else if (rejection.errors[0]?.code === 'file-invalid-type') {
setError('Invalid file type');
} else {
setError('File rejected');
}
return;
}
if (acceptedFiles.length > 0) {
onUpload(acceptedFiles[0]);
}
},
[onUpload, maxSize]
);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept,
maxSize,
multiple: false,
});
const getFileIcon = (file: File) => {
const type = file.type.split('/')[0];
const Icon = fileIcons[type] || File;
return <Icon className="w-8 h-8 text-forge-yellow" />;
};
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
};
if (currentFile) {
return (
<div className="bg-forge-dark rounded-xl p-4 border border-gray-700">
<div className="flex items-center gap-4">
{getFileIcon(currentFile)}
<div className="flex-1 min-w-0">
<p className="text-white font-medium truncate">{currentFile.name}</p>
<p className="text-sm text-gray-500">{formatFileSize(currentFile.size)}</p>
</div>
{onClear && (
<button
onClick={onClear}
className="p-2 text-gray-400 hover:text-red-400 transition-colors"
>
<X className="w-5 h-5" />
</button>
)}
</div>
</div>
);
}
return (
<div>
<div
{...getRootProps()}
className={clsx(
'upload-zone',
isDragActive && 'active'
)}
>
<input {...getInputProps()} />
<Upload className="w-12 h-12 text-gray-500 mx-auto mb-4" />
<p className="text-white font-medium mb-2">{label}</p>
<p className="text-gray-500 text-sm">
Drag and drop or <span className="text-forge-yellow">browse</span>
</p>
{accept && (
<p className="text-gray-600 text-xs mt-2">
Accepted: {Object.keys(accept).join(', ')}
</p>
)}
</div>
{error && (
<p className="mt-2 text-sm text-red-400">{error}</p>
)}
</div>
);
}