veza/apps/web/src/components/views/UploadView.tsx
2026-01-07 19:39:21 +01:00

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