diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 49bc6d1..6aab3b8 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -65,18 +65,10 @@ interface News { } | null; } -type SortField = 'id' | 'title' | 'artist' | 'createdAt' | 'releaseYear' | 'activations' | 'averageRating'; -type SortDirection = 'asc' | 'desc'; - export default function AdminPage() { const [password, setPassword] = useState(''); const [isAuthenticated, setIsAuthenticated] = useState(false); - 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(''); @@ -120,40 +112,11 @@ export default function AdminPage() { const [editNewsFeatured, setEditNewsFeatured] = useState(false); const [editNewsSpecialId, setEditNewsSpecialId] = useState(null); - // Edit state - const [editingId, setEditingId] = useState(null); - const [editTitle, setEditTitle] = useState(''); - const [editArtist, setEditArtist] = useState(''); - const [editReleaseYear, setEditReleaseYear] = useState(''); - const [editGenreIds, setEditGenreIds] = useState([]); - const [editSpecialIds, setEditSpecialIds] = useState([]); - const [editExcludeFromGlobal, setEditExcludeFromGlobal] = useState(false); - - // Post-upload state - const [uploadedSong, setUploadedSong] = useState(null); - const [uploadGenreIds, setUploadGenreIds] = useState([]); - const [uploadExcludeFromGlobal, setUploadExcludeFromGlobal] = useState(false); - - // Batch upload genre selection - const [batchUploadGenreIds, setBatchUploadGenreIds] = useState([]); - // AI Categorization state const [isCategorizing, setIsCategorizing] = useState(false); const [categorizationResults, setCategorizationResults] = useState(null); - // Sort state - const [sortField, setSortField] = useState('artist'); - const [sortDirection, setSortDirection] = useState('asc'); - - // Search and pagination state - const [searchQuery, setSearchQuery] = useState(''); - const [selectedGenreFilter, setSelectedGenreFilter] = useState(''); - const [selectedSpecialFilter, setSelectedSpecialFilter] = useState(null); - const [currentPage, setCurrentPage] = useState(1); - const itemsPerPage = 10; - // Audio state - const [playingSongId, setPlayingSongId] = useState(null); const [audioElement, setAudioElement] = useState(null); // Daily Puzzles state @@ -598,418 +561,6 @@ export default function AdminPage() { } }; - const handleBatchUpload = async (e: React.FormEvent) => { - e.preventDefault(); - if (files.length === 0) return; - - setIsUploading(true); - setUploadResults([]); - setUploadProgress({ current: 0, total: files.length }); - setMessage(''); - - const results = []; - - // 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 }); - - try { - console.log(`Uploading file ${i + 1}/${files.length}: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)}MB)`); - - const formData = new FormData(); - formData.append('file', file); - formData.append('excludeFromGlobal', String(uploadExcludeFromGlobal)); - - const res = await fetch('/api/songs', { - method: 'POST', - headers: { 'x-admin-auth': localStorage.getItem('hoerdle_admin_auth') || '' }, - body: formData, - }); - - console.log(`Response status for ${file.name}: ${res.status}`); - - if (res.ok) { - const data = await res.json(); - console.log(`Upload successful for ${file.name}:`, data); - results.push({ - filename: file.name, - success: true, - song: data.song, - validation: data.validation - }); - } else if (res.status === 409) { - // Duplicate detected - const data = await res.json(); - console.log(`Duplicate detected for ${file.name}:`, data); - results.push({ - filename: file.name, - success: false, - isDuplicate: true, - duplicate: data.duplicate, - error: `Duplicate: Already exists as "${data.duplicate.title}" by "${data.duplicate.artist}"` - }); - } else { - const errorText = await res.text(); - console.error(`Upload failed for ${file.name} (${res.status}):`, errorText); - results.push({ - filename: file.name, - success: false, - error: `Upload failed (${res.status}): ${errorText.substring(0, 100)}` - }); - } - } catch (error) { - console.error(`Network error for ${file.name}:`, error); - results.push({ - filename: file.name, - success: false, - error: `Network error: ${error instanceof Error ? error.message : 'Unknown error'}` - }); - } - } - - setUploadResults(results); - setFiles([]); - setIsUploading(false); - - // Assign genres to successfully uploaded songs - if (batchUploadGenreIds.length > 0) { - const successfulUploads = results.filter(r => r.success && r.song); - for (const result of successfulUploads) { - try { - await fetch('/api/songs', { - method: 'PUT', - headers: getAuthHeaders(), - body: JSON.stringify({ - id: result.song.id, - title: result.song.title, - artist: result.song.artist, - genreIds: batchUploadGenreIds - }), - }); - } catch (error) { - console.error(`Failed to assign genres to ${result.song.title}:`, error); - } - } - } - - fetchSongs(); - fetchGenres(); - fetchSpecials(); // Update special counts - - // Auto-trigger categorization after uploads - const successCount = results.filter(r => r.success).length; - const duplicateCount = results.filter(r => r.isDuplicate).length; - const failedCount = results.filter(r => !r.success && !r.isDuplicate).length; - if (successCount > 0) { - let msg = `βœ… Uploaded ${successCount}/${files.length} songs successfully!`; - if (duplicateCount > 0) { - msg += `\n⚠️ Skipped ${duplicateCount} duplicate(s)`; - } - if (failedCount > 0) { - msg += `\n❌ ${failedCount} failed`; - } - if (batchUploadGenreIds.length > 0) { - const selectedGenreNames = genres - .filter(g => batchUploadGenreIds.includes(g.id)) - .map(g => g.name) - .join(', '); - msg += `\n🏷️ Assigned genres: ${selectedGenreNames}`; - } - msg += '\n\nπŸ€– Starting auto-categorization...'; - setMessage(msg); - // Small delay to let user see the message - setTimeout(() => { - handleAICategorization(); - }, 1000); - } else if (duplicateCount > 0 && failedCount === 0) { - setMessage(`⚠️ All ${duplicateCount} file(s) were duplicates - nothing uploaded.`); - } else { - setMessage(`❌ All uploads failed.`); - } - }; - - const handleDragEnter = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(true); - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - e.dataTransfer.dropEffect = 'copy'; - if (!isDragging) setIsDragging(true); - }; - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - - // Prevent flickering when dragging over children - if (e.currentTarget.contains(e.relatedTarget as Node)) { - return; - } - setIsDragging(false); - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragging(false); - - const droppedFiles = Array.from(e.dataTransfer.files); - - // Validate file types - const validFiles: File[] = []; - const invalidFiles: string[] = []; - - droppedFiles.forEach(file => { - if (file.type === 'audio/mpeg' || file.name.toLowerCase().endsWith('.mp3')) { - validFiles.push(file); - } else { - invalidFiles.push(`${file.name} (${file.type || 'unknown type'})`); - } - }); - - if (invalidFiles.length > 0) { - alert(`⚠️ The following files are not supported:\n\n${invalidFiles.join('\n')}\n\nOnly MP3 files are allowed.`); - } - - if (validFiles.length > 0) { - setFiles(validFiles); - } - }; - - const handleFileChange = (e: React.ChangeEvent) => { - if (e.target.files) { - const selectedFiles = Array.from(e.target.files); - - // Validate file types - const validFiles: File[] = []; - const invalidFiles: string[] = []; - - selectedFiles.forEach(file => { - if (file.type === 'audio/mpeg' || file.name.toLowerCase().endsWith('.mp3')) { - validFiles.push(file); - } else { - invalidFiles.push(`${file.name} (${file.type || 'unknown type'})`); - } - }); - - if (invalidFiles.length > 0) { - alert(`⚠️ The following files are not supported:\n\n${invalidFiles.join('\n')}\n\nOnly MP3 files are allowed.`); - } - - if (validFiles.length > 0) { - setFiles(validFiles); - } - } - }; - - const saveUploadedSongGenres = async () => { - if (!uploadedSong) return; - - const res = await fetch('/api/songs', { - method: 'PUT', - headers: getAuthHeaders(), - body: JSON.stringify({ - id: uploadedSong.id, - title: uploadedSong.title, - artist: uploadedSong.artist, - genreIds: uploadGenreIds - }), - }); - - if (res.ok) { - setUploadedSong(null); - setUploadGenreIds([]); - fetchSongs(); - fetchGenres(); - fetchSpecials(); // Update special counts if song was assigned to specials - setMessage(prev => prev + '\nβœ… Genres assigned successfully!'); - } else { - alert('Failed to assign genres'); - } - }; - - const startEditing = (song: Song) => { - setEditingId(song.id); - setEditTitle(song.title); - setEditArtist(song.artist); - setEditReleaseYear(song.releaseYear || ''); - setEditGenreIds(song.genres.map(g => g.id)); - setEditSpecialIds(song.specials ? song.specials.map(s => s.id) : []); - setEditExcludeFromGlobal(song.excludeFromGlobal || false); - }; - - const cancelEditing = () => { - setEditingId(null); - setEditTitle(''); - setEditArtist(''); - setEditReleaseYear(''); - setEditGenreIds([]); - setEditSpecialIds([]); - setEditExcludeFromGlobal(false); - }; - - const saveEditing = async (id: number) => { - const res = await fetch('/api/songs', { - method: 'PUT', - headers: getAuthHeaders(), - body: JSON.stringify({ - id, - title: editTitle, - artist: editArtist, - releaseYear: editReleaseYear === '' ? null : Number(editReleaseYear), - genreIds: editGenreIds, - specialIds: editSpecialIds, - excludeFromGlobal: editExcludeFromGlobal - }), - }); - - if (res.ok) { - setEditingId(null); - fetchSongs(); - fetchGenres(); - fetchSpecials(); // Update special counts - } else { - alert('Failed to update song'); - } - }; - - const handleDelete = async (id: number, title: string) => { - if (!confirm(`Are you sure you want to delete "${title}"? This will also delete the file.`)) { - return; - } - - const res = await fetch('/api/songs', { - method: 'DELETE', - headers: getAuthHeaders(), - body: JSON.stringify({ id }), - }); - - if (res.ok) { - fetchSongs(); - fetchGenres(); - fetchSpecials(); // Update special counts - } else { - alert('Failed to delete song'); - } - }; - - const handleSort = (field: SortField) => { - if (sortField === field) { - setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); - } else { - setSortField(field); - setSortDirection('asc'); - } - }; - - const handlePlayPause = (song: Song) => { - if (playingSongId === song.id) { - // Pause current song - audioElement?.pause(); - setPlayingSongId(null); - } else { - // Stop any currently playing song - audioElement?.pause(); - - // Play new song - const audio = new Audio(`/api/audio/${song.filename}`); - - // Handle playback errors - audio.onerror = () => { - alert(`Failed to load audio file: ${song.filename}\nThe file may be corrupted or missing.`); - setPlayingSongId(null); - setAudioElement(null); - }; - - audio.play() - .then(() => { - setAudioElement(audio); - setPlayingSongId(song.id); - }) - .catch((error) => { - console.error('Playback error:', error); - alert(`Failed to play audio: ${error.message}`); - setPlayingSongId(null); - setAudioElement(null); - }); - - // Reset when song ends - audio.onended = () => { - setPlayingSongId(null); - setAudioElement(null); - }; - } - }; - - // Filter and sort songs - const filteredSongs = songs.filter(song => { - // Text search filter - const matchesSearch = song.title.toLowerCase().includes(searchQuery.toLowerCase()) || - song.artist.toLowerCase().includes(searchQuery.toLowerCase()); - - // Genre filter - // Unified Filter - let matchesFilter = true; - if (selectedGenreFilter) { - if (selectedGenreFilter.startsWith('genre:')) { - const genreId = Number(selectedGenreFilter.split(':')[1]); - matchesFilter = genreId === -1 - ? song.genres.length === 0 - : song.genres.some(g => g.id === genreId); - } else if (selectedGenreFilter.startsWith('special:')) { - const specialId = Number(selectedGenreFilter.split(':')[1]); - matchesFilter = song.specials?.some(s => s.id === specialId) || false; - } else if (selectedGenreFilter === 'daily') { - const today = new Date().toISOString().split('T')[0]; - matchesFilter = song.puzzles?.some(p => p.date === today) || false; - } else if (selectedGenreFilter === 'no-global') { - matchesFilter = song.excludeFromGlobal === true; - } - } - - return matchesSearch && matchesFilter; - }); - - const sortedSongs = [...filteredSongs].sort((a, b) => { - // Handle numeric sorting for ID, Release Year, Activations, and Rating - if (sortField === 'id') { - return sortDirection === 'asc' ? a.id - b.id : b.id - a.id; - } - if (sortField === 'releaseYear') { - const yearA = a.releaseYear || 0; - const yearB = b.releaseYear || 0; - return sortDirection === 'asc' ? yearA - yearB : yearB - yearA; - } - if (sortField === 'activations') { - return sortDirection === 'asc' ? a.activations - b.activations : b.activations - a.activations; - } - if (sortField === 'averageRating') { - return sortDirection === 'asc' ? a.averageRating - b.averageRating : b.averageRating - a.averageRating; - } - - // String sorting for other fields - const valA = String(a[sortField]).toLowerCase(); - const valB = String(b[sortField]).toLowerCase(); - - if (valA < valB) return sortDirection === 'asc' ? -1 : 1; - if (valA > valB) return sortDirection === 'asc' ? 1 : -1; - return 0; - }); - - // Pagination - const totalPages = Math.ceil(sortedSongs.length / itemsPerPage); - const startIndex = (currentPage - 1) * itemsPerPage; - const paginatedSongs = sortedSongs.slice(startIndex, startIndex + itemsPerPage); - - // Reset to page 1 when search changes - useEffect(() => { - setCurrentPage(1); - }, [searchQuery]); if (!isAuthenticated) { return ( @@ -1459,154 +1010,7 @@ export default function AdminPage() { -
-

Upload Songs

-
- {/* Drag & Drop Zone */} -
fileInputRef.current?.click()} - > -
πŸ“
-

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

-

- or click to browse -

- -
- - {/* File List */} - {files.length > 0 && ( -
-

Selected Files:

-
- {files.map((file, index) => ( -
- πŸ“„ {file.name} -
- ))} -
-
- )} - - {/* Upload Progress */} - {isUploading && ( -
-

- Uploading: {uploadProgress.current} / {uploadProgress.total} -

-
-
-
-
- )} - -
- -
- {genres.map(genre => ( - - ))} -
-

- Selected genres will be assigned to all uploaded songs. -

-
- -
- -

- If checked, these songs will only appear in Genre or Special puzzles. -

-
- - - - {message && ( -
- {message} -
- )} - -
+ {/* Upload Songs wurde in das Kuratoren-Dashboard verlagert */} {/* Today's Daily Puzzles */}
@@ -1682,397 +1086,7 @@ export default function AdminPage() { ))}
-
-

- Song Library ({songs.length} songs) -

- - {/* Search and Filter */} -
- setSearchQuery(e.target.value)} - className="form-input" - style={{ flex: '1', minWidth: '200px' }} - /> - - {(searchQuery || selectedGenreFilter) && ( - - )} -
- -
- - - - - - - - - - - - - - - {paginatedSongs.map(song => ( - - - - {editingId === song.id ? ( - <> - - - - - - - - - ) : ( - <> - - - - - - - - - )} - - ))} - {paginatedSongs.length === 0 && ( - - - - )} - -
handleSort('id')} - > - ID {sortField === 'id' && (sortDirection === 'asc' ? '↑' : '↓')} - handleSort('title')} - > - Song {sortField === 'title' && (sortDirection === 'asc' ? '↑' : '↓')} - handleSort('releaseYear')} - > - Year {sortField === 'releaseYear' && (sortDirection === 'asc' ? '↑' : '↓')} - Genres / Specials handleSort('createdAt')} - > - Added {sortField === 'createdAt' && (sortDirection === 'asc' ? '↑' : '↓')} - handleSort('activations')} - > - Activations {sortField === 'activations' && (sortDirection === 'asc' ? '↑' : '↓')} - handleSort('averageRating')} - > - Rating {sortField === 'averageRating' && (sortDirection === 'asc' ? '↑' : '↓')} - Actions
{song.id} - setEditTitle(e.target.value)} - className="form-input" - style={{ padding: '0.25rem', marginBottom: '0.5rem', width: '100%' }} - placeholder="Title" - /> - setEditArtist(e.target.value)} - className="form-input" - style={{ padding: '0.25rem', width: '100%' }} - placeholder="Artist" - /> - - setEditReleaseYear(e.target.value === '' ? '' : Number(e.target.value))} - className="form-input" - style={{ padding: '0.25rem', width: '80px' }} - placeholder="Year" - /> - -
- {genres.map(genre => ( - - ))} -
-
- {specials.map(special => ( - - ))} -
-
- -
-
- {new Date(song.createdAt).toLocaleDateString('de-DE')} - {song.activations} - {song.averageRating > 0 ? ( - - {song.averageRating.toFixed(1)} β˜… ({song.ratingCount}) - - ) : ( - - - )} - -
- - -
-
-
{song.title}
-
{song.artist}
- - {song.excludeFromGlobal && ( -
- - 🚫 No Global - -
- )} - - {/* Daily Puzzle Badges */} -
- {song.puzzles?.filter(p => p.date === new Date().toISOString().split('T')[0]).map(p => { - if (!p.genreId && !p.specialId) { - return ( - - 🌍 Global Daily - - ); - } - if (p.genreId) { - const genreName = genres.find(g => g.id === p.genreId)?.name; - return ( - - 🏷️ {genreName} Daily - - ); - } - if (p.specialId) { - const specialName = specials.find(s => s.id === p.specialId)?.name; - return ( - - β˜… {specialName} Daily - - ); - } - return null; - })} -
-
- {song.releaseYear || '-'} - -
- {song.genres?.map(g => ( - - {g.name} - - ))} -
-
- {song.specials?.map(s => ( - - {s.name} - - ))} -
-
- {new Date(song.createdAt).toLocaleDateString('de-DE')} - {song.activations} - {song.averageRating > 0 ? ( - - {song.averageRating.toFixed(1)} β˜… ({song.ratingCount}) - - ) : ( - - - )} - -
- - - -
-
- {searchQuery ? 'No songs found matching your search.' : 'No songs uploaded yet.'} -
-
- - {/* Pagination */} - {totalPages > 1 && ( -
- - - Page {currentPage} of {totalPages} - - -
- )} -
+ {/* Song Library wurde in das Kuratoren-Dashboard verlagert */}