jam-tracks / frontend /src /components /FileUpload.jsx
Mina Emadi
Bundle demo catalog for immediate track selection
b6a817b
Raw History Blame Contribute Delete
10.4 kB
import React, { useState } from 'react'
import { listPresetNames } from '../utils/presetManifest'
/* Upload slots β€” hidden
const STEM_TYPES = [
{ name: 'guitar', label: 'Guitar', icon: '🎸' },
{ name: 'drums', label: 'Drums', icon: 'πŸ₯' },
{ name: 'bass', label: 'Bass', icon: '🎸' },
{ name: 'synth', label: 'Synth', icon: '🎹' },
{ name: 'click_record', label: 'Click Record', icon: '⏱️' }
]
*/
function getBundledPresets() {
try {
return { presets: listPresetNames(), catalogError: null }
} catch (err) {
return { presets: [], catalogError: err.message }
}
}
function FileUpload({ onLoadPreset, loading, error, onClearError }) {
const [{ presets, catalogError }] = useState(getBundledPresets)
const [selectedPreset, setSelectedPreset] = useState('')
/* Upload slots β€” hidden
const [stems, setStems] = useState({
guitar: null,
drums: null,
bass: null,
synth: null,
click_record: null
})
const [midiFiles, setMidiFiles] = useState([])
const [midiDragActive, setMidiDragActive] = useState(false)
const handleStemSelect = useCallback((stemType, file) => {
setStems(prev => ({ ...prev, [stemType]: file }))
}, [])
const handleStemRemove = useCallback((stemType) => {
setStems(prev => ({ ...prev, [stemType]: null }))
}, [])
const handleMidiSelect = useCallback((e) => {
const selectedFiles = Array.from(e.target.files).filter(f =>
f.name.toLowerCase().endsWith('.mid') || f.name.toLowerCase().endsWith('.midi')
)
setMidiFiles(prev => [...prev, ...selectedFiles])
}, [])
const handleMidiRemove = useCallback((index) => {
setMidiFiles(prev => prev.filter((_, i) => i !== index))
}, [])
const handleMidiDrag = useCallback((e) => {
e.preventDefault()
e.stopPropagation()
if (e.type === 'dragenter' || e.type === 'dragover') {
setMidiDragActive(true)
} else if (e.type === 'dragleave') {
setMidiDragActive(false)
}
}, [])
const handleMidiDrop = useCallback((e) => {
e.preventDefault()
e.stopPropagation()
setMidiDragActive(false)
const droppedFiles = Array.from(e.dataTransfer.files).filter(f =>
f.name.toLowerCase().endsWith('.mid') || f.name.toLowerCase().endsWith('.midi')
)
setMidiFiles(prev => [...prev, ...droppedFiles])
}, [])
const handleSubmit = useCallback(async () => {
const formData = new FormData()
Object.entries(stems).forEach(([name, file]) => {
if (file) formData.append(name, file)
})
midiFiles.forEach(file => {
formData.append('midi', file)
})
await onUpload(formData)
}, [stems, midiFiles, onUpload])
const stemCount = Object.values(stems).filter(f => f !== null).length
const hasFiles = stemCount > 0
*/
return (
<div className="card rounded-2xl p-6 animate-fade-in max-w-4xl mx-auto">
<h2 className="text-xl font-semibold mb-6 text-center">Load a Demo Track</h2>
{/* Demo presets dropdown */}
{presets.length > 0 && (
<div className="p-4 bg-surface-elevated rounded-xl border border-white/[0.08]">
<div className="flex gap-3">
<select
value={selectedPreset}
onChange={(e) => setSelectedPreset(e.target.value)}
disabled={loading}
className="flex-1 bg-surface border border-white/[0.08] rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-accent-500"
>
<option value="">Select a demo...</option>
{presets.map(p => <option key={p} value={p}>{p}</option>)}
</select>
<button
onClick={() => selectedPreset && onLoadPreset(selectedPreset)}
disabled={loading || !selectedPreset}
className="px-4 py-2 rounded-lg font-display text-base uppercase tracking-wider transition-all disabled:bg-white/[0.04] disabled:text-gray-500 disabled:cursor-not-allowed bg-accent-500 hover:bg-accent-400 text-surface"
>
{loading ? 'Loading...' : 'Load'}
</button>
</div>
</div>
)}
{/* Upload slots β€” hidden
<div className="grid grid-cols-2 gap-4 mb-6">
{STEM_TYPES.map(({ name, label, icon }) => (
<StemUploadCard
key={name}
name={name}
label={label}
icon={icon}
file={stems[name]}
onSelect={(file) => handleStemSelect(name, file)}
onRemove={() => handleStemRemove(name)}
disabled={loading}
/>
))}
</div>
<div
onDragEnter={handleMidiDrag}
onDragLeave={handleMidiDrag}
onDragOver={handleMidiDrag}
onDrop={handleMidiDrop}
className={`
mt-6 p-4 border-2 border-dashed rounded-xl transition-all
${midiDragActive
? 'border-blue-500 bg-blue-500/10'
: 'border-gray-600'
}
`}
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="text-2xl">🎹</span>
<span className="font-medium text-gray-300">MIDI Files (Optional)</span>
</div>
<label
htmlFor="midi-input"
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg cursor-pointer transition-colors text-sm"
>
+ Add MIDI
</label>
<input
type="file"
multiple
accept=".mid,.midi"
onChange={handleMidiSelect}
className="hidden"
id="midi-input"
disabled={loading}
/>
</div>
{midiFiles.length > 0 && (
<div className="space-y-2">
{midiFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between bg-gray-800/50 rounded-lg px-3 py-2"
>
<div className="flex items-center gap-2">
<span className="text-sm truncate">{file.name}</span>
<span className="text-xs text-gray-400">
{(file.size / 1024).toFixed(1)} KB
</span>
</div>
<button
onClick={() => handleMidiRemove(index)}
className="text-gray-400 hover:text-red-400 transition-colors"
disabled={loading}
>
βœ•
</button>
</div>
))}
</div>
)}
</div>
<button
onClick={handleSubmit}
disabled={loading || !hasFiles}
className={`
w-full mt-6 py-3 rounded-xl font-medium transition-all
${loading || !hasFiles
? 'bg-gray-600 cursor-not-allowed'
: 'bg-gradient-to-r from-primary-600 to-accent-600 hover:from-primary-500 hover:to-accent-500'
}
`}
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<span className="animate-spin">⏳</span>
Analyzing...
</span>
) : (
`Upload ${stemCount} Stem${stemCount !== 1 ? 's' : ''}${midiFiles.length > 0 ? ` + ${midiFiles.length} MIDI` : ''}`
)}
</button>
*/}
{/* Error display */}
{(error || catalogError) && (
<div className="mt-4 p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-300 text-sm flex items-center justify-between">
<span>{error || catalogError}</span>
{error && (
<button
onClick={onClearError}
className="text-red-400 hover:text-red-200"
>
βœ•
</button>
)}
</div>
)}
</div>
)
}
/* StemUploadCard β€” hidden
function StemUploadCard({ name, label, icon, file, onSelect, onRemove, disabled }) {
const [dragActive, setDragActive] = React.useState(false)
const handleFileChange = (e) => {
const selectedFile = e.target.files[0]
if (selectedFile && selectedFile.name.toLowerCase().endsWith('.wav')) {
onSelect(selectedFile)
}
}
const handleDrag = (e) => {
e.preventDefault()
e.stopPropagation()
if (e.type === 'dragenter' || e.type === 'dragover') {
setDragActive(true)
} else if (e.type === 'dragleave') {
setDragActive(false)
}
}
const handleDrop = (e) => {
e.preventDefault()
e.stopPropagation()
setDragActive(false)
const droppedFile = e.dataTransfer.files[0]
if (droppedFile && droppedFile.name.toLowerCase().endsWith('.wav')) {
onSelect(droppedFile)
}
}
return (
<div
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
className={`
p-4 rounded-xl border-2 transition-all
${file
? 'border-green-500 bg-green-500/10'
: dragActive
? 'border-blue-500 bg-blue-500/10'
: 'border-gray-600 hover:border-primary-500 bg-gray-800/30'
}
`}
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className="text-2xl">{icon}</span>
<span className="font-medium text-gray-300">{label}</span>
</div>
{file && (
<button
onClick={onRemove}
className="text-gray-400 hover:text-red-400 transition-colors"
disabled={disabled}
>
βœ•
</button>
)}
</div>
{file ? (
<div className="text-sm">
<p className="text-gray-300 truncate">{file.name}</p>
<p className="text-gray-400 text-xs mt-1">
{(file.size / 1024 / 1024).toFixed(1)} MB
</p>
</div>
) : (
<label
htmlFor={`${name}-input`}
className="block w-full py-2 text-center bg-gray-700 hover:bg-gray-600 rounded-lg cursor-pointer transition-colors text-sm"
>
Select File
</label>
)}
<input
type="file"
accept=".wav"
onChange={handleFileChange}
className="hidden"
id={`${name}-input`}
disabled={disabled}
/>
</div>
)
}
*/
export default FileUpload