'use client'; import { useState, useEffect } from 'react'; interface Genre { id: number; name: string; _count?: { songs: number; }; } interface Song { id: number; title: string; artist: string; filename: string; createdAt: string; activations: number; genres: Genre[]; } type SortField = 'id' | 'title' | 'artist' | 'createdAt'; type SortDirection = 'asc' | 'desc'; export default function AdminPage() { const [password, setPassword] = useState(''); const [isAuthenticated, setIsAuthenticated] = useState(false); const [file, setFile] = useState(null); const [message, setMessage] = useState(''); const [songs, setSongs] = useState([]); const [genres, setGenres] = useState([]); const [newGenreName, setNewGenreName] = useState(''); // Edit state const [editingId, setEditingId] = useState(null); const [editTitle, setEditTitle] = useState(''); const [editArtist, setEditArtist] = useState(''); const [editGenreIds, setEditGenreIds] = useState([]); // Post-upload state const [uploadedSong, setUploadedSong] = useState(null); const [uploadGenreIds, setUploadGenreIds] = 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 [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 10; // Audio state const [playingSongId, setPlayingSongId] = useState(null); const [audioElement, setAudioElement] = useState(null); // Check for existing auth on mount useEffect(() => { const authToken = localStorage.getItem('hoerdle_admin_auth'); if (authToken === 'authenticated') { setIsAuthenticated(true); fetchSongs(); fetchGenres(); } }, []); const handleLogin = async () => { const res = await fetch('/api/admin/login', { method: 'POST', body: JSON.stringify({ password }), }); if (res.ok) { localStorage.setItem('hoerdle_admin_auth', 'authenticated'); setIsAuthenticated(true); fetchSongs(); fetchGenres(); } else { alert('Wrong password'); } }; const fetchSongs = async () => { const res = await fetch('/api/songs'); if (res.ok) { const data = await res.json(); setSongs(data); } }; const fetchGenres = async () => { const res = await fetch('/api/genres'); if (res.ok) { const data = await res.json(); setGenres(data); } }; const createGenre = async () => { if (!newGenreName.trim()) return; const res = await fetch('/api/genres', { method: 'POST', body: JSON.stringify({ name: newGenreName }), }); if (res.ok) { setNewGenreName(''); fetchGenres(); } else { alert('Failed to create genre'); } }; const deleteGenre = async (id: number) => { if (!confirm('Delete this genre?')) return; const res = await fetch('/api/genres', { method: 'DELETE', body: JSON.stringify({ id }), }); if (res.ok) { fetchGenres(); } else { alert('Failed to delete genre'); } }; const handleAICategorization = async () => { if (!confirm('This will categorize all songs without genres using AI. Continue?')) return; setIsCategorizing(true); setCategorizationResults(null); try { const res = await fetch('/api/categorize', { method: 'POST', }); if (res.ok) { const data = await res.json(); setCategorizationResults(data); fetchSongs(); // Refresh song list fetchGenres(); // Refresh genre counts } else { const error = await res.json(); alert(`Categorization failed: ${error.error || 'Unknown error'}`); } } catch (error) { alert('Failed to categorize songs'); console.error(error); } finally { setIsCategorizing(false); } }; const handleUpload = async (e: React.FormEvent) => { e.preventDefault(); if (!file) return; const formData = new FormData(); formData.append('file', file); setMessage('Uploading...'); const res = await fetch('/api/songs', { method: 'POST', body: formData, }); if (res.ok) { const data = await res.json(); const validation = data.validation; 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`; if (validation.warnings.length > 0) { statusMessage += `\n⚠️ Warnings:\n`; validation.warnings.forEach((warning: string) => { statusMessage += `β€’ ${warning}\n`; }); } setMessage(statusMessage); setFile(null); setUploadedSong(data.song); setUploadGenreIds([]); // Reset selection fetchSongs(); } else { setMessage('Upload failed.'); } }; const saveUploadedSongGenres = async () => { if (!uploadedSong) return; const res = await fetch('/api/songs', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: uploadedSong.id, title: uploadedSong.title, artist: uploadedSong.artist, genreIds: uploadGenreIds }), }); if (res.ok) { setUploadedSong(null); setUploadGenreIds([]); fetchSongs(); 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); setEditGenreIds(song.genres.map(g => g.id)); }; const cancelEditing = () => { setEditingId(null); setEditTitle(''); setEditArtist(''); setEditGenreIds([]); }; const saveEditing = async (id: number) => { const res = await fetch('/api/songs', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, title: editTitle, artist: editArtist, genreIds: editGenreIds }), }); if (res.ok) { setEditingId(null); fetchSongs(); } 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: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }), }); if (res.ok) { fetchSongs(); } 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(`/uploads/${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 => song.title.toLowerCase().includes(searchQuery.toLowerCase()) || song.artist.toLowerCase().includes(searchQuery.toLowerCase()) ); const sortedSongs = [...filteredSongs].sort((a, b) => { // Handle numeric sorting for ID if (sortField === 'id') { return sortDirection === 'asc' ? a.id - b.id : b.id - a.id; } // 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 (

Admin Login

setPassword(e.target.value)} className="form-input" style={{ marginBottom: '1rem', maxWidth: '300px' }} placeholder="Password" />
); } return (

Admin Dashboard

{/* Genre Management */}

Manage Genres

setNewGenreName(e.target.value)} placeholder="New Genre Name" className="form-input" style={{ maxWidth: '200px' }} />
{genres.map(genre => (
{genre.name} ({genre._count?.songs || 0})
))}
{/* AI Categorization */}
{genres.length === 0 && (

Please create at least one genre first.

)}
{/* Categorization Results */} {categorizationResults && (

βœ… Categorization Complete

{categorizationResults.message}

{categorizationResults.results && categorizationResults.results.length > 0 && (

Updated Songs:

{categorizationResults.results.map((result: any) => (
{result.title} by {result.artist}
{result.assignedGenres.map((genre: string) => ( {genre} ))}
))}
)}
)}

Upload New Song

setFile(e.target.files?.[0] || null)} className="form-input" required />
{message && (
{message}
)}
{/* Post-upload Genre Selection */} {uploadedSong && (

Assign Genres to "{uploadedSong.title}"

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

Song Library ({songs.length} songs)

{/* Search */}
setSearchQuery(e.target.value)} className="form-input" />
{paginatedSongs.map(song => ( {editingId === song.id ? ( <> ) : ( <> )} ))} {paginatedSongs.length === 0 && ( )}
handleSort('id')} > ID {sortField === 'id' && (sortDirection === 'asc' ? '↑' : '↓')} handleSort('title')} > Title {sortField === 'title' && (sortDirection === 'asc' ? '↑' : '↓')} handleSort('artist')} > Artist {sortField === 'artist' && (sortDirection === 'asc' ? '↑' : '↓')} Genres handleSort('createdAt')} > Added {sortField === 'createdAt' && (sortDirection === 'asc' ? '↑' : '↓')} Activations Actions
{song.id} setEditTitle(e.target.value)} className="form-input" style={{ padding: '0.25rem' }} /> setEditArtist(e.target.value)} className="form-input" style={{ padding: '0.25rem' }} />
{genres.map(genre => ( ))}
{new Date(song.createdAt).toLocaleDateString('de-DE')} {song.activations}
{song.title} {song.artist}
{song.genres?.map(g => ( {g.name} ))}
{new Date(song.createdAt).toLocaleDateString('de-DE')} {song.activations}
{searchQuery ? 'No songs found matching your search.' : 'No songs uploaded yet.'}
{/* Pagination */} {totalPages > 1 && (
Page {currentPage} of {totalPages}
)}
); }