215 lines
10 KiB
TypeScript
215 lines
10 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
|
import { Card } from '../ui/card';
|
|
import { Button } from '../ui/button';
|
|
import { Check, ChevronRight, Layers } from 'lucide-react';
|
|
import { useToast } from '../../context/ToastContext';
|
|
import { FileUploadZone } from '../upload/FileUploadZone';
|
|
import { FilePreviewCard, UploadFile } from '../upload/FilePreviewCard';
|
|
import { BulkUploadModal } from '../upload/BulkUploadModal';
|
|
import { MetadataEditor } from '../upload/metadata/MetadataEditor';
|
|
import { uploadService } from '../../services/uploadService';
|
|
|
|
export const UploadView: React.FC = () => {
|
|
const { addToast } = useToast();
|
|
const [step, setStep] = useState(1);
|
|
|
|
// File State
|
|
const [files, setFiles] = useState<UploadFile[]>([]);
|
|
const [showBulkModal, setShowBulkModal] = useState(false);
|
|
|
|
// --- STEP 1 LOGIC: UPLOAD HANDLING ---
|
|
|
|
const handleFilesSelected = (newFiles: File[]) => {
|
|
const uploadFiles: UploadFile[] = newFiles.map(f => ({
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
file: f,
|
|
progress: 0,
|
|
status: 'paused',
|
|
previewUrl: f.type.startsWith('image/') ? URL.createObjectURL(f) : undefined
|
|
}));
|
|
|
|
setFiles(prev => [...prev, ...uploadFiles]);
|
|
if (uploadFiles.length > 1 || files.length > 0) {
|
|
// Optional: Auto open bulk modal if many files
|
|
// setShowBulkModal(true);
|
|
}
|
|
addToast(`${newFiles.length} files selected`, 'info');
|
|
|
|
// Auto-start upload
|
|
uploadFiles.forEach(uf => triggerUpload(uf));
|
|
};
|
|
|
|
const triggerUpload = async (uploadFile: UploadFile) => {
|
|
setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'uploading' } : f));
|
|
|
|
try {
|
|
await uploadService.uploadFile(uploadFile.file, (progress) => {
|
|
setFiles(prev => {
|
|
// Check if cancelled/paused
|
|
const current = prev.find(f => f.id === uploadFile.id);
|
|
if (!current || current.status === 'paused' || current.status === 'error') return prev;
|
|
return prev.map(f => f.id === uploadFile.id ? { ...f, progress } : f);
|
|
});
|
|
});
|
|
|
|
setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, progress: 100, status: 'completed' } : f));
|
|
} catch (error) {
|
|
setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'error' } : f));
|
|
addToast(`Failed to upload ${uploadFile.file.name}`, 'error');
|
|
}
|
|
};
|
|
|
|
const handlePause = (id: string) => {
|
|
// In real app, abort controller would be used
|
|
setFiles(prev => prev.map(f => f.id === id ? { ...f, status: 'paused' } : f));
|
|
};
|
|
|
|
const handleResume = (id: string) => {
|
|
const file = files.find(f => f.id === id);
|
|
if (file) triggerUpload(file);
|
|
};
|
|
|
|
const handleCancel = (id: string) => {
|
|
setFiles(prev => prev.filter(f => f.id !== id));
|
|
};
|
|
|
|
const handleStartBulkUpload = () => {
|
|
files.filter(f => f.status !== 'completed' && f.status !== 'uploading').forEach(f => triggerUpload(f));
|
|
};
|
|
|
|
const allCompleted = files.length > 0 && files.every(f => f.status === 'completed');
|
|
|
|
const handleMetadataComplete = (_metadata: any) => {
|
|
// Here we would sync metadata with backend for the uploaded files
|
|
// await trackService.updateMetadata(metadata);
|
|
setStep(3);
|
|
};
|
|
|
|
// --- RENDER ---
|
|
|
|
return (
|
|
<div className="animate-fadeIn max-w-5xl mx-auto pb-20">
|
|
<h2 className="text-3xl font-display font-bold text-white mb-2">UPLOAD STUDIO</h2>
|
|
<p className="text-gray-400 font-mono text-sm mb-8">Publish your sounds to the Veza Network.</p>
|
|
|
|
{/* Stepper */}
|
|
<div className="flex items-center justify-between mb-8 px-4">
|
|
{[
|
|
{ num: 1, label: 'Upload' },
|
|
{ num: 2, label: 'Metadata' },
|
|
{ num: 3, label: 'Review' },
|
|
{ num: 4, label: 'Publish' }
|
|
].map((s, i) => (
|
|
<div key={s.num} className="flex items-center gap-3">
|
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm transition-all duration-300 ${step >= s.num ? 'bg-kodo-cyan text-black' : 'bg-kodo-slate text-gray-500'}`}>
|
|
{step > s.num ? <Check className="w-5 h-5" /> : s.num}
|
|
</div>
|
|
<span className={`${step >= s.num ? 'text-white' : 'text-gray-600'} font-medium hidden md:block`}>{s.label}</span>
|
|
{i < 3 && <div className="w-12 h-px bg-kodo-steel mx-4 hidden md:block opacity-50"></div>}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<Card variant="default" className="min-h-[600px] flex flex-col relative overflow-hidden">
|
|
|
|
{/* STEP 1: UPLOAD CORE */}
|
|
{step === 1 && (
|
|
<div className="flex-1 p-8 animate-fadeIn flex flex-col gap-8">
|
|
{files.length === 0 ? (
|
|
<div className="flex-1 flex flex-col justify-center">
|
|
<FileUploadZone onFilesSelected={handleFilesSelected} />
|
|
</div>
|
|
) : (
|
|
<div className="flex-1 flex flex-col min-h-0">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="font-bold text-white">Files ({files.length})</h3>
|
|
<div className="flex gap-2">
|
|
<Button variant="ghost" size="sm" icon={<Layers className="w-4 h-4" />} onClick={() => setShowBulkModal(true)}>
|
|
Bulk View
|
|
</Button>
|
|
<div className="relative overflow-hidden">
|
|
<Button variant="secondary" size="sm">Add More</Button>
|
|
<input type="file" multiple className="absolute inset-0 opacity-0 cursor-pointer" onChange={(e) => { if (e.target.files) handleFilesSelected(Array.from(e.target.files)); }} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 overflow-y-auto max-h-[400px] custom-scrollbar pr-2 mb-4">
|
|
{files.map(file => (
|
|
<FilePreviewCard
|
|
key={file.id}
|
|
fileData={file}
|
|
onPause={() => handlePause(file.id)}
|
|
onResume={() => handleResume(file.id)}
|
|
onCancel={() => handleCancel(file.id)}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-auto">
|
|
<div className="bg-kodo-ink p-4 rounded-lg border border-kodo-steel flex justify-between items-center">
|
|
<div className="text-sm text-gray-400">
|
|
{allCompleted
|
|
? <span className="text-kodo-lime flex items-center gap-2"><Check className="w-4 h-4" /> All files uploaded successfully</span>
|
|
: <span>Processing uploads... please wait.</span>
|
|
}
|
|
</div>
|
|
<Button
|
|
variant="primary"
|
|
disabled={!allCompleted}
|
|
onClick={() => setStep(2)}
|
|
icon={<ChevronRight className="w-4 h-4" />}
|
|
>
|
|
Continue to Metadata
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* STEP 2: METADATA EDITOR */}
|
|
{step === 2 && (
|
|
<div className="flex-1 p-6 animate-fadeIn overflow-y-auto">
|
|
<MetadataEditor
|
|
files={files}
|
|
onBack={() => setStep(1)}
|
|
onNext={handleMetadataComplete}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* STEP 3: SUCCESS PLACEHOLDER */}
|
|
{step >= 3 && (
|
|
<div className="flex-1 flex flex-col items-center justify-center p-8 text-center animate-fadeIn">
|
|
<div className="w-16 h-16 bg-kodo-lime/20 rounded-full flex items-center justify-center text-kodo-lime mb-4">
|
|
<Check className="w-8 h-8" />
|
|
</div>
|
|
<h3 className="text-2xl font-bold text-white mb-4">Ready to Publish</h3>
|
|
<p className="text-gray-400 mb-8 max-w-md">
|
|
Your tracks have been metadata tagged and are ready for distribution on the Veza Network.
|
|
</p>
|
|
<div className="flex gap-4">
|
|
<Button variant="ghost" onClick={() => setStep(2)}>Back</Button>
|
|
<Button variant="primary" onClick={() => { setStep(1); setFiles([]); addToast("Tracks Published", "success"); }}>Publish Now</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
{/* MODALS */}
|
|
{showBulkModal && (
|
|
<BulkUploadModal
|
|
files={files}
|
|
onClose={() => setShowBulkModal(false)}
|
|
onStartUpload={handleStartBulkUpload}
|
|
onCancelFile={handleCancel}
|
|
onPauseFile={handlePause}
|
|
onResumeFile={handleResume}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|