veza/apps/web/src/components/views/UploadView.tsx

302 lines
10 KiB
TypeScript
Raw Normal View History

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 {
// Using the new chunked upload from uploadService
// Here we provide basic metadata from the filename
const metadata = {
title:
uploadFile.file.name.split('.').slice(0, -1).join('.') ||
uploadFile.file.name,
artist: 'Unknown Artist', // Placeholder, would be updated in Step 2
genre: 'None',
};
await uploadService.uploadTrack(uploadFile.file, metadata, (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-kodo-content-dim 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-4">
<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-kodo-content-dim'}`}
>
{step > s.num ? <Check className="w-5 h-5" /> : s.num}
</div>
<span
className={`${step >= s.num ? 'text-white' : 'text-kodo-content-dim'} 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-kodo-content-dim">
{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-xl font-bold text-white mb-4">
Ready to Publish
</h3>
<p className="text-kodo-content-dim 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>
);
};