diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 4965a83..75d08f1 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -26,8 +26,12 @@ type SortDirection = 'asc' | 'desc'; export default function AdminPage() { const [password, setPassword] = useState(''); const [isAuthenticated, setIsAuthenticated] = useState(false); - const [file, setFile] = useState(null); + const [files, setFiles] = useState([]); const [message, setMessage] = useState(''); + const [isUploading, setIsUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState<{ current: number, total: number }>({ current: 0, total: 0 }); + const [uploadResults, setUploadResults] = useState([]); + const [isDragging, setIsDragging] = useState(false); const [songs, setSongs] = useState([]); const [genres, setGenres] = useState([]); const [newGenreName, setNewGenreName] = useState(''); @@ -52,6 +56,7 @@ export default function AdminPage() { // Search and pagination state const [searchQuery, setSearchQuery] = useState(''); + const [selectedGenreFilter, setSelectedGenreFilter] = useState(null); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 10; @@ -192,45 +197,99 @@ export default function AdminPage() { } }; - const handleUpload = async (e: React.FormEvent) => { + const handleBatchUpload = async (e: React.FormEvent) => { e.preventDefault(); - if (!file) return; + if (files.length === 0) return; - const formData = new FormData(); - formData.append('file', file); + setIsUploading(true); + setUploadResults([]); + setUploadProgress({ current: 0, total: files.length }); + setMessage(''); - setMessage('Uploading...'); - const res = await fetch('/api/songs', { - method: 'POST', - body: formData, - }); + const results = []; - if (res.ok) { - const data = await res.json(); - const validation = data.validation; + // Upload files sequentially to avoid timeout + for (let i = 0; i < files.length; i++) { + const file = files[i]; + setUploadProgress({ current: i + 1, total: files.length }); - let statusMessage = 'āœ… Song uploaded successfully!\n\n'; - statusMessage += `šŸ“Š Audio Info:\n`; - statusMessage += `• Format: ${validation.codec || 'unknown'}\n`; - statusMessage += `• Bitrate: ${Math.round(validation.bitrate / 1000)} kbps\n`; - statusMessage += `• Sample Rate: ${validation.sampleRate} Hz\n`; - statusMessage += `• Duration: ${Math.round(validation.duration)} seconds\n`; - statusMessage += `• Cover Art: ${validation.hasCover ? 'āœ… Yes' : 'āŒ No'}\n`; + try { + const formData = new FormData(); + formData.append('file', file); - if (validation.warnings.length > 0) { - statusMessage += `\nāš ļø Warnings:\n`; - validation.warnings.forEach((warning: string) => { - statusMessage += `• ${warning}\n`; + const res = await fetch('/api/songs', { + method: 'POST', + body: formData, + }); + + if (res.ok) { + const data = await res.json(); + results.push({ + filename: file.name, + success: true, + song: data.song, + validation: data.validation + }); + } else { + results.push({ + filename: file.name, + success: false, + error: 'Upload failed' + }); + } + } catch (error) { + results.push({ + filename: file.name, + success: false, + error: 'Network error' }); } + } - setMessage(statusMessage); - setFile(null); - setUploadedSong(data.song); - setUploadGenreIds([]); // Reset selection - fetchSongs(); + setUploadResults(results); + setFiles([]); + setIsUploading(false); + fetchSongs(); + + // Auto-trigger categorization after uploads + const successCount = results.filter(r => r.success).length; + if (successCount > 0) { + setMessage(`āœ… Uploaded ${successCount}/${files.length} songs successfully!\n\nšŸ¤– Starting auto-categorization...`); + // Small delay to let user see the message + setTimeout(() => { + handleAICategorization(); + }, 1000); } else { - setMessage('Upload failed.'); + setMessage(`āŒ All uploads failed.`); + } + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + + const droppedFiles = Array.from(e.dataTransfer.files).filter( + file => file.type === 'audio/mpeg' || file.name.endsWith('.mp3') + ); + + if (droppedFiles.length > 0) { + setFiles(droppedFiles); + } + }; + + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files) { + setFiles(Array.from(e.target.files)); } }; @@ -359,10 +418,17 @@ export default function AdminPage() { }; // Filter and sort songs - const filteredSongs = songs.filter(song => - song.title.toLowerCase().includes(searchQuery.toLowerCase()) || - song.artist.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const filteredSongs = songs.filter(song => { + // Text search filter + const matchesSearch = song.title.toLowerCase().includes(searchQuery.toLowerCase()) || + song.artist.toLowerCase().includes(searchQuery.toLowerCase()); + + // Genre filter + const matchesGenre = selectedGenreFilter === null || + song.genres.some(g => g.id === selectedGenreFilter); + + return matchesSearch && matchesGenre; + }); const sortedSongs = [...filteredSongs].sort((a, b) => { // Handle numeric sorting for ID @@ -529,27 +595,88 @@ export default function AdminPage() {
-

Upload New Song

-
-
- +

Upload Songs

+ + {/* Drag & Drop Zone */} +
document.getElementById('file-input')?.click()} + > +
šŸ“
+

+ {files.length > 0 ? `${files.length} file(s) selected` : 'Drag & drop MP3 files here'} +

+

+ or click to browse +

setFile(e.target.files?.[0] || null)} - className="form-input" - required + multiple + onChange={handleFileChange} + style={{ display: 'none' }} />
- + {message && (
)} - - {/* Post-upload Genre Selection */} - {uploadedSong && ( -
-

Assign Genres to "{uploadedSong.title}"

-
- {genres.map(genre => ( - - ))} -
- -
- )}
@@ -601,15 +693,47 @@ export default function AdminPage() { Song Library ({songs.length} songs) - {/* Search */} -
+ {/* Search and Filter */} +
setSearchQuery(e.target.value)} className="form-input" + style={{ flex: '1', minWidth: '200px' }} /> + + {(searchQuery || selectedGenreFilter) && ( + + )}