'use client'; import { useState, useEffect, useRef } from 'react'; import { getLocalizedValue } from '@/lib/i18n'; interface Special { id: number; name: any; subtitle?: any; maxAttempts: number; unlockSteps: string; launchDate?: string; endDate?: string; curator?: string; _count?: { songs: number; }; } interface Genre { id: number; name: any; subtitle?: any; active: boolean; _count?: { songs: number; }; } interface DailyPuzzle { id: number; date: string; songId: number; genreId: number | null; specialId: number | null; } interface Song { id: number; title: string; artist: string; filename: string; createdAt: string; releaseYear: number | null; activations: number; puzzles: DailyPuzzle[]; genres: Genre[]; specials: Special[]; averageRating: number; ratingCount: number; excludeFromGlobal: boolean; } interface News { id: number; title: any; content: any; author: string | null; publishedAt: string; featured: boolean; specialId: number | null; special: { id: number; name: any; } | null; } type SortField = 'id' | 'title' | 'artist' | 'createdAt' | 'releaseYear' | 'activations' | 'averageRating'; type SortDirection = 'asc' | 'desc'; export default function AdminPage({ params }: { params: { locale: string } }) { const [activeTab, setActiveTab] = useState<'de' | 'en'>('de'); 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({ de: '', en: '' }); const [newGenreSubtitle, setNewGenreSubtitle] = useState({ de: '', en: '' }); const [newGenreActive, setNewGenreActive] = useState(true); const [editingGenreId, setEditingGenreId] = useState(null); const [editGenreName, setEditGenreName] = useState({ de: '', en: '' }); const [editGenreSubtitle, setEditGenreSubtitle] = useState({ de: '', en: '' }); const [editGenreActive, setEditGenreActive] = useState(true); // Specials state const [specials, setSpecials] = useState([]); const [newSpecialName, setNewSpecialName] = useState({ de: '', en: '' }); const [newSpecialSubtitle, setNewSpecialSubtitle] = useState({ de: '', en: '' }); const [newSpecialMaxAttempts, setNewSpecialMaxAttempts] = useState(7); const [newSpecialUnlockSteps, setNewSpecialUnlockSteps] = useState('[2,4,7,11,16,30,60]'); const [newSpecialLaunchDate, setNewSpecialLaunchDate] = useState(''); const [newSpecialEndDate, setNewSpecialEndDate] = useState(''); const [newSpecialCurator, setNewSpecialCurator] = useState(''); const [editingSpecialId, setEditingSpecialId] = useState(null); const [editSpecialName, setEditSpecialName] = useState({ de: '', en: '' }); const [editSpecialSubtitle, setEditSpecialSubtitle] = useState({ de: '', en: '' }); const [editSpecialMaxAttempts, setEditSpecialMaxAttempts] = useState(7); const [editSpecialUnlockSteps, setEditSpecialUnlockSteps] = useState('[2,4,7,11,16,30,60]'); const [editSpecialLaunchDate, setEditSpecialLaunchDate] = useState(''); const [editSpecialEndDate, setEditSpecialEndDate] = useState(''); const [editSpecialCurator, setEditSpecialCurator] = useState(''); // News state const [news, setNews] = useState([]); const [newNewsTitle, setNewNewsTitle] = useState({ de: '', en: '' }); const [newNewsContent, setNewNewsContent] = useState({ de: '', en: '' }); const [newNewsAuthor, setNewNewsAuthor] = useState(''); const [newNewsFeatured, setNewNewsFeatured] = useState(false); const [newNewsSpecialId, setNewNewsSpecialId] = useState(null); const [editingNewsId, setEditingNewsId] = useState(null); const [editNewsTitle, setEditNewsTitle] = useState({ de: '', en: '' }); const [editNewsContent, setEditNewsContent] = useState({ de: '', en: '' }); const [editNewsAuthor, setEditNewsAuthor] = useState(''); 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 const [dailyPuzzles, setDailyPuzzles] = useState([]); const [playingPuzzleId, setPlayingPuzzleId] = useState(null); const [showDailyPuzzles, setShowDailyPuzzles] = useState(false); const fileInputRef = useRef(null); // Check for existing auth on mount useEffect(() => { const authToken = localStorage.getItem('hoerdle_admin_auth'); if (authToken === 'authenticated') { setIsAuthenticated(true); fetchSongs(); fetchGenres(); fetchDailyPuzzles(); fetchSpecials(); fetchNews(); } }, []); 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(); fetchDailyPuzzles(); fetchSpecials(); fetchNews(); } else { alert('Wrong password'); } }; const handleLogout = () => { localStorage.removeItem('hoerdle_admin_auth'); setIsAuthenticated(false); setPassword(''); // Reset all state setSongs([]); setGenres([]); setSpecials([]); setDailyPuzzles([]); }; // Helper function to add auth headers to requests const getAuthHeaders = () => { const authToken = localStorage.getItem('hoerdle_admin_auth'); return { 'Content-Type': 'application/json', 'x-admin-auth': authToken || '' }; }; const fetchSongs = async () => { const res = await fetch('/api/songs', { headers: getAuthHeaders() }); if (res.ok) { const data = await res.json(); setSongs(data); } }; const fetchGenres = async () => { const res = await fetch('/api/genres', { headers: getAuthHeaders() }); if (res.ok) { const data = await res.json(); setGenres(data); } }; const createGenre = async () => { if (!newGenreName.de.trim() && !newGenreName.en.trim()) return; const res = await fetch('/api/genres', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ name: newGenreName, subtitle: newGenreSubtitle, active: newGenreActive }), }); if (res.ok) { setNewGenreName({ de: '', en: '' }); setNewGenreSubtitle({ de: '', en: '' }); setNewGenreActive(true); fetchGenres(); } else { alert('Failed to create genre'); } }; const startEditGenre = (genre: Genre) => { setEditingGenreId(genre.id); setEditGenreName(typeof genre.name === 'string' ? { de: genre.name, en: genre.name } : genre.name); setEditGenreSubtitle(genre.subtitle ? (typeof genre.subtitle === 'string' ? { de: genre.subtitle, en: genre.subtitle } : genre.subtitle) : { de: '', en: '' }); setEditGenreActive(genre.active !== undefined ? genre.active : true); }; const saveEditedGenre = async () => { if (editingGenreId === null) return; const res = await fetch('/api/genres', { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify({ id: editingGenreId, name: editGenreName, subtitle: editGenreSubtitle, active: editGenreActive }), }); if (res.ok) { setEditingGenreId(null); fetchGenres(); } else { alert('Failed to update genre'); } }; // Specials functions const fetchSpecials = async () => { const res = await fetch('/api/specials', { headers: getAuthHeaders() }); if (res.ok) { const data = await res.json(); setSpecials(data); } }; const handleCreateSpecial = async (e: React.FormEvent) => { e.preventDefault(); if (!newSpecialName.de.trim() && !newSpecialName.en.trim()) return; const res = await fetch('/api/specials', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ name: newSpecialName, subtitle: newSpecialSubtitle, maxAttempts: newSpecialMaxAttempts, unlockSteps: newSpecialUnlockSteps, launchDate: newSpecialLaunchDate || null, endDate: newSpecialEndDate || null, curator: newSpecialCurator || null, }), }); if (res.ok) { setNewSpecialName({ de: '', en: '' }); setNewSpecialSubtitle({ de: '', en: '' }); setNewSpecialMaxAttempts(7); setNewSpecialUnlockSteps('[2,4,7,11,16,30,60]'); setNewSpecialLaunchDate(''); setNewSpecialEndDate(''); setNewSpecialCurator(''); fetchSpecials(); } else { alert('Failed to create special'); } }; const handleDeleteSpecial = async (id: number) => { if (!confirm('Delete this special?')) return; const res = await fetch('/api/specials', { method: 'DELETE', headers: getAuthHeaders(), body: JSON.stringify({ id }), }); if (res.ok) fetchSpecials(); else alert('Failed to delete special'); }; // Daily Puzzles functions const fetchDailyPuzzles = async () => { const res = await fetch('/api/admin/daily-puzzles', { headers: getAuthHeaders() }); if (res.ok) { const data = await res.json(); setDailyPuzzles(data); } }; const handleDeletePuzzle = async (puzzleId: number) => { if (!confirm('Delete this daily puzzle? A new one will be generated automatically.')) return; const res = await fetch('/api/admin/daily-puzzles', { method: 'DELETE', headers: getAuthHeaders(), body: JSON.stringify({ puzzleId }), }); if (res.ok) { fetchDailyPuzzles(); alert('Puzzle deleted and regenerated successfully'); } else { alert('Failed to delete puzzle'); } }; const handlePlayPuzzle = (puzzle: any) => { if (playingPuzzleId === puzzle.id) { // Pause audioElement?.pause(); setPlayingPuzzleId(null); setAudioElement(null); } else { // Stop any currently playing audio if (audioElement) { audioElement.pause(); setAudioElement(null); } const audio = new Audio(puzzle.song.audioUrl); audio.play() .then(() => { setAudioElement(audio); setPlayingPuzzleId(puzzle.id); }) .catch((error) => { console.error('Playback error:', error); alert(`Failed to play audio: ${error.message}`); setPlayingPuzzleId(null); setAudioElement(null); }); audio.onended = () => { setPlayingPuzzleId(null); setAudioElement(null); }; } }; const startEditSpecial = (special: Special) => { setEditingSpecialId(special.id); setEditSpecialName(typeof special.name === 'string' ? { de: special.name, en: special.name } : special.name); setEditSpecialSubtitle(special.subtitle ? (typeof special.subtitle === 'string' ? { de: special.subtitle, en: special.subtitle } : special.subtitle) : { de: '', en: '' }); setEditSpecialMaxAttempts(special.maxAttempts); setEditSpecialUnlockSteps(special.unlockSteps); setEditSpecialLaunchDate(special.launchDate ? new Date(special.launchDate).toISOString().split('T')[0] : ''); setEditSpecialEndDate(special.endDate ? new Date(special.endDate).toISOString().split('T')[0] : ''); setEditSpecialCurator(special.curator || ''); }; const saveEditedSpecial = async () => { if (editingSpecialId === null) return; const res = await fetch('/api/specials', { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify({ id: editingSpecialId, name: editSpecialName, subtitle: editSpecialSubtitle, maxAttempts: editSpecialMaxAttempts, unlockSteps: editSpecialUnlockSteps, launchDate: editSpecialLaunchDate || null, endDate: editSpecialEndDate || null, curator: editSpecialCurator || null, }), }); if (res.ok) { setEditingSpecialId(null); fetchSpecials(); } else { alert('Failed to update special'); } }; // News functions const fetchNews = async () => { const res = await fetch('/api/news', { headers: getAuthHeaders() }); if (res.ok) { const data = await res.json(); setNews(data); } }; const handleCreateNews = async (e: React.FormEvent) => { e.preventDefault(); if ((!newNewsTitle.de.trim() && !newNewsTitle.en.trim()) || (!newNewsContent.de.trim() && !newNewsContent.en.trim())) return; const res = await fetch('/api/news', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ title: newNewsTitle, content: newNewsContent, author: newNewsAuthor || null, featured: newNewsFeatured, specialId: newNewsSpecialId }), }); if (res.ok) { setNewNewsTitle({ de: '', en: '' }); setNewNewsContent({ de: '', en: '' }); setNewNewsAuthor(''); setNewNewsFeatured(false); setNewNewsSpecialId(null); fetchNews(); } else { alert('Failed to create news'); } }; const startEditNews = (newsItem: News) => { setEditingNewsId(newsItem.id); setEditNewsTitle(typeof newsItem.title === 'string' ? { de: newsItem.title, en: newsItem.title } : newsItem.title); setEditNewsContent(typeof newsItem.content === 'string' ? { de: newsItem.content, en: newsItem.content } : newsItem.content); setEditNewsAuthor(newsItem.author || ''); setEditNewsFeatured(newsItem.featured); setEditNewsSpecialId(newsItem.specialId); }; const saveEditedNews = async () => { if (editingNewsId === null) return; const res = await fetch('/api/news', { method: 'PUT', headers: getAuthHeaders(), body: JSON.stringify({ id: editingNewsId, title: editNewsTitle, content: editNewsContent, author: editNewsAuthor || null, featured: editNewsFeatured, specialId: editNewsSpecialId }), }); if (res.ok) { setEditingNewsId(null); fetchNews(); } else { alert('Failed to update news'); } }; const handleDeleteNews = async (id: number) => { if (!confirm('Delete this news item?')) return; const res = await fetch('/api/news', { method: 'DELETE', headers: getAuthHeaders(), body: JSON.stringify({ id }), }); if (res.ok) { fetchNews(); } else { alert('Failed to delete news'); } }; // Load specials after auth useEffect(() => { if (isAuthenticated) fetchSpecials(); }, [isAuthenticated]); const deleteGenre = async (id: number) => { if (!confirm('Delete this genre?')) return; const res = await fetch('/api/genres', { method: 'DELETE', headers: getAuthHeaders(), 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 { let offset = 0; let hasMore = true; let allResults: any[] = []; let totalUncategorized = 0; let totalProcessed = 0; // Process in batches while (hasMore) { const res = await fetch('/api/categorize', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ offset }) }); if (!res.ok) { const error = await res.json(); alert(`Categorization failed: ${error.error || 'Unknown error'}`); break; } const data = await res.json(); totalUncategorized = data.totalUncategorized; totalProcessed = data.processed; hasMore = data.hasMore; offset = data.nextOffset || 0; // Accumulate results allResults = [...allResults, ...data.results]; // Update UI with progress setCategorizationResults({ message: `Processing: ${totalProcessed} / ${totalUncategorized} songs...`, totalProcessed: totalUncategorized, totalCategorized: allResults.length, results: allResults, inProgress: hasMore }); } // Final update setCategorizationResults({ message: `Completed! Processed ${totalUncategorized} songs, categorized ${allResults.length}`, totalProcessed: totalUncategorized, totalCategorized: allResults.length, results: allResults, inProgress: false }); fetchSongs(); // Refresh song list fetchGenres(); // Refresh genre counts } catch (error) { alert('Failed to categorize songs'); console.error(error); } finally { setIsCategorizing(false); } }; 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 => getLocalizedValue(g.name, activeTab)) .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 (

Admin Login

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

HΓΆrdle Admin Dashboard

{/* Special Management */}

Manage Specials

setNewSpecialName({ ...newSpecialName, [activeTab]: e.target.value })} className="form-input" required />
setNewSpecialSubtitle({ ...newSpecialSubtitle, [activeTab]: e.target.value })} className="form-input" />
setNewSpecialMaxAttempts(Number(e.target.value))} className="form-input" min={1} style={{ width: '80px' }} />
setNewSpecialUnlockSteps(e.target.value)} className="form-input" style={{ width: '200px' }} />
setNewSpecialLaunchDate(e.target.value)} className="form-input" />
setNewSpecialEndDate(e.target.value)} className="form-input" />
setNewSpecialCurator(e.target.value)} className="form-input" />
{specials.map(special => (
{getLocalizedValue(special.name, activeTab)} ({special._count?.songs || 0}) {special.subtitle && - {getLocalizedValue(special.subtitle, activeTab)}} Curate
))}
{editingSpecialId !== null && (

Edit Special

setEditSpecialName({ ...editSpecialName, [activeTab]: e.target.value })} className="form-input" />
setEditSpecialSubtitle({ ...editSpecialSubtitle, [activeTab]: e.target.value })} className="form-input" />
setEditSpecialMaxAttempts(Number(e.target.value))} className="form-input" min={1} style={{ width: '80px' }} />
setEditSpecialUnlockSteps(e.target.value)} className="form-input" style={{ width: '200px' }} />
setEditSpecialLaunchDate(e.target.value)} className="form-input" />
setEditSpecialEndDate(e.target.value)} className="form-input" />
setEditSpecialCurator(e.target.value)} className="form-input" />
)}
{/* Genre Management */}

Manage Genres

setNewGenreName({ ...newGenreName, [activeTab]: e.target.value })} placeholder="New Genre Name" className="form-input" style={{ maxWidth: '200px' }} /> setNewGenreSubtitle({ ...newGenreSubtitle, [activeTab]: e.target.value })} placeholder="Subtitle" className="form-input" style={{ maxWidth: '300px' }} />
{genres.map(genre => (
{getLocalizedValue(genre.name, activeTab)} ({genre._count?.songs || 0}) {genre.subtitle && - {getLocalizedValue(genre.subtitle, activeTab)}}
))}
{editingGenreId !== null && (

Edit Genre

setEditGenreName({ ...editGenreName, [activeTab]: e.target.value })} className="form-input" />
setEditGenreSubtitle({ ...editGenreSubtitle, [activeTab]: e.target.value })} className="form-input" style={{ width: '300px' }} />
)} {/* 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} ))}
))}
)}
)}
{/* News Management */}

Manage News & Announcements

setNewNewsTitle({ ...newNewsTitle, [activeTab]: e.target.value })} placeholder="News Title" className="form-input" required />