2026-01-07 09:31:02 +00:00
|
|
|
import React, { useCallback, useState } from 'react';
|
|
|
|
|
import { UploadCloud } from 'lucide-react';
|
|
|
|
|
import { useToast } from '../../context/ToastContext';
|
|
|
|
|
|
|
|
|
|
interface FileUploadZoneProps {
|
|
|
|
|
onFilesSelected: (files: File[]) => void;
|
|
|
|
|
acceptedFormats?: string[];
|
|
|
|
|
maxSizeInMB?: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
export const FileUploadZone: React.FC<FileUploadZoneProps> = ({
|
|
|
|
|
onFilesSelected,
|
|
|
|
|
acceptedFormats = ['.wav', '.mp3', '.aiff', '.flac', '.zip'],
|
|
|
|
|
maxSizeInMB = 500,
|
2026-01-07 09:31:02 +00:00
|
|
|
}) => {
|
|
|
|
|
const { addToast } = useToast();
|
|
|
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
|
|
|
|
|
|
|
|
const validateFile = (file: File): boolean => {
|
|
|
|
|
// Check size
|
|
|
|
|
if (file.size > maxSizeInMB * 1024 * 1024) {
|
|
|
|
|
addToast(`File ${file.name} exceeds ${maxSizeInMB}MB limit`, 'error');
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
// Check extension (simple check)
|
2026-01-13 18:47:57 +00:00
|
|
|
const ext = `.${file.name.split('.').pop()?.toLowerCase()}`;
|
2026-01-07 09:31:02 +00:00
|
|
|
if (!acceptedFormats.includes(ext)) {
|
|
|
|
|
addToast(`Format ${ext} not supported for ${file.name}`, 'error');
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const handleDrop = useCallback(
|
|
|
|
|
(e: React.DragEvent) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setIsDragging(false);
|
|
|
|
|
|
|
|
|
|
const droppedFiles = Array.from(e.dataTransfer.files);
|
|
|
|
|
const validFiles = droppedFiles.filter(validateFile);
|
|
|
|
|
|
|
|
|
|
if (validFiles.length > 0) {
|
|
|
|
|
onFilesSelected(validFiles);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[onFilesSelected],
|
|
|
|
|
);
|
2026-01-07 09:31:02 +00:00
|
|
|
|
|
|
|
|
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
if (e.target.files) {
|
|
|
|
|
const selectedFiles = Array.from(e.target.files);
|
|
|
|
|
const validFiles = selectedFiles.filter(validateFile);
|
|
|
|
|
if (validFiles.length > 0) {
|
|
|
|
|
onFilesSelected(validFiles);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-13 18:47:57 +00:00
|
|
|
<div
|
|
|
|
|
onDragOver={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setIsDragging(true);
|
|
|
|
|
}}
|
2026-01-07 09:31:02 +00:00
|
|
|
onDragLeave={() => setIsDragging(false)}
|
|
|
|
|
onDrop={handleDrop}
|
|
|
|
|
className={`
|
aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 10:50:46 +00:00
|
|
|
border-2 border-dashed rounded-xl p-12 flex flex-col items-center justify-center text-center transition-all duration-300 group cursor-pointer
|
2026-01-13 18:47:57 +00:00
|
|
|
${
|
|
|
|
|
isDragging
|
|
|
|
|
? 'border-kodo-cyan bg-kodo-cyan/10 scale-[1.02]'
|
aesthetic-improvements: replace secondary cyan hover states with steel (batch 3)
- UI components: accordion hover text (1 file)
- Player components: AudioPlayer, MiniPlayer icon button hovers (2 files, 3 instances)
- Social components: CreatePostModal, SharePostModal, ProductDetailView (3 files, 3 instances)
- Settings: AccessibilitySettingsView hover text (1 file)
- Analytics: TrackAnalyticsView, AnalyticsView chart bar hovers (2 files, 2 instances)
- Gamification: LeaderboardView hover text (1 file)
- Upload: FileUploadZone hover border (1 file)
- Banner: BulkModeBanner close button hover (1 file)
- Total: ~12 files, ~15 instances replaced
- Preserved: Focus rings (cyan), active/selected states (cyan), primary actions (cyan)
- Note: ChatSidebar selected state hover kept cyan (primary state)
- Note: VirtualizedChatMessages scroll button kept cyan (primary action)
- Action 11.3.1.2 in progress (third batch complete)
2026-01-16 09:55:42 +00:00
|
|
|
: 'border-kodo-steel/50 bg-kodo-ink/30 hover:bg-kodo-ink/50 hover:border-kodo-steel/50'
|
2026-01-13 18:47:57 +00:00
|
|
|
}
|
2026-01-07 09:31:02 +00:00
|
|
|
`}
|
|
|
|
|
>
|
2026-01-13 18:47:57 +00:00
|
|
|
<input
|
|
|
|
|
type="file"
|
|
|
|
|
multiple
|
|
|
|
|
className="hidden"
|
2026-01-07 09:31:02 +00:00
|
|
|
id="file-upload-input"
|
|
|
|
|
onChange={handleFileInput}
|
|
|
|
|
accept={acceptedFormats.join(',')}
|
|
|
|
|
/>
|
2026-01-13 18:47:57 +00:00
|
|
|
<label
|
|
|
|
|
htmlFor="file-upload-input"
|
|
|
|
|
className="cursor-pointer w-full flex flex-col items-center"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className={`
|
2026-01-07 09:31:02 +00:00
|
|
|
w-20 h-20 rounded-full flex items-center justify-center mb-6 transition-all duration-300 shadow-lg
|
2026-01-16 11:06:00 +00:00
|
|
|
${isDragging ? 'bg-kodo-cyan text-black' : 'bg-kodo-slate text-kodo-cyan group-hover:bg-kodo-steel'}
|
2026-01-13 18:47:57 +00:00
|
|
|
`}
|
|
|
|
|
>
|
2026-01-07 09:31:02 +00:00
|
|
|
<UploadCloud className="w-10 h-10" />
|
|
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-15 22:54:05 +00:00
|
|
|
<h3 className="text-xl font-display font-bold text-white mb-2">
|
2026-01-07 09:31:02 +00:00
|
|
|
{isDragging ? 'Drop Files Here' : 'Drag & Drop or Click'}
|
|
|
|
|
</h3>
|
2026-01-16 00:56:44 +00:00
|
|
|
<p className="text-kodo-content-dim mb-6 max-w-sm">
|
2026-01-07 09:31:02 +00:00
|
|
|
Upload audio stems, project archives, or sample packs.
|
|
|
|
|
</p>
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2026-01-07 09:31:02 +00:00
|
|
|
<div className="flex flex-wrap justify-center gap-2 max-w-md">
|
2026-01-13 18:47:57 +00:00
|
|
|
{acceptedFormats.map((fmt) => (
|
|
|
|
|
<span
|
|
|
|
|
key={fmt}
|
2026-01-16 00:56:44 +00:00
|
|
|
className="px-2 py-1 bg-black/30 rounded text-[10px] font-mono text-kodo-content-dim border border-white/5 uppercase"
|
2026-01-13 18:47:57 +00:00
|
|
|
>
|
2026-01-07 09:31:02 +00:00
|
|
|
{fmt.replace('.', '')}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2026-01-16 00:56:44 +00:00
|
|
|
<p className="mt-4 text-[10px] text-kodo-content-dim">
|
2026-01-13 18:47:57 +00:00
|
|
|
Max file size: {maxSizeInMB}MB
|
|
|
|
|
</p>
|
2026-01-07 09:31:02 +00:00
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-01-13 18:47:57 +00:00
|
|
|
};
|