Files
hoerdle/app/admin/page.tsx
2025-11-21 12:25:19 +01:00

282 lines
12 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
interface Song {
id: number;
title: string;
artist: string;
filename: string;
createdAt: string;
}
type SortField = 'title' | 'artist';
type SortDirection = 'asc' | 'desc';
export default function AdminPage() {
const [password, setPassword] = useState('');
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [file, setFile] = useState<File | null>(null);
const [title, setTitle] = useState('');
const [artist, setArtist] = useState('');
const [message, setMessage] = useState('');
const [songs, setSongs] = useState<Song[]>([]);
// Edit state
const [editingId, setEditingId] = useState<number | null>(null);
const [editTitle, setEditTitle] = useState('');
const [editArtist, setEditArtist] = useState('');
// Sort state
const [sortField, setSortField] = useState<SortField>('artist');
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
const handleLogin = async () => {
const res = await fetch('/api/admin/login', {
method: 'POST',
body: JSON.stringify({ password }),
});
if (res.ok) {
setIsAuthenticated(true);
fetchSongs();
} else {
alert('Wrong password');
}
};
const fetchSongs = async () => {
const res = await fetch('/api/songs');
if (res.ok) {
const data = await res.json();
setSongs(data);
}
};
const handleUpload = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) return;
const formData = new FormData();
formData.append('file', file);
if (title) formData.append('title', title);
if (artist) formData.append('artist', artist);
setMessage('Uploading...');
const res = await fetch('/api/songs', {
method: 'POST',
body: formData,
});
if (res.ok) {
setMessage('Song uploaded successfully!');
setTitle('');
setArtist('');
setFile(null);
fetchSongs();
} else {
setMessage('Upload failed.');
}
};
const startEditing = (song: Song) => {
setEditingId(song.id);
setEditTitle(song.title);
setEditArtist(song.artist);
};
const cancelEditing = () => {
setEditingId(null);
setEditTitle('');
setEditArtist('');
};
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 }),
});
if (res.ok) {
setEditingId(null);
fetchSongs();
} else {
alert('Failed to update song');
}
};
const handleSort = (field: SortField) => {
if (sortField === field) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection('asc');
}
};
const sortedSongs = [...songs].sort((a, b) => {
const valA = a[sortField].toLowerCase();
const valB = b[sortField].toLowerCase();
if (valA < valB) return sortDirection === 'asc' ? -1 : 1;
if (valA > valB) return sortDirection === 'asc' ? 1 : -1;
return 0;
});
if (!isAuthenticated) {
return (
<div className="container" style={{ justifyContent: 'center' }}>
<h1 className="title" style={{ marginBottom: '1rem', fontSize: '1.5rem' }}>Admin Login</h1>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
className="form-input"
style={{ marginBottom: '1rem', maxWidth: '300px' }}
placeholder="Password"
/>
<button onClick={handleLogin} className="btn-primary">Login</button>
</div>
);
}
return (
<div className="admin-container">
<h1 className="title" style={{ marginBottom: '2rem' }}>Admin Dashboard</h1>
<div className="admin-card" style={{ marginBottom: '2rem' }}>
<h2 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1rem' }}>Upload New Song</h2>
<form onSubmit={handleUpload}>
<div className="form-group">
<label className="form-label">MP3 File (Required)</label>
<input
type="file"
accept="audio/mpeg"
onChange={e => setFile(e.target.files?.[0] || null)}
className="form-input"
required
/>
</div>
<div className="form-group">
<label className="form-label">Title (Optional - extracted from file if empty)</label>
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
className="form-input"
/>
</div>
<div className="form-group">
<label className="form-label">Artist (Optional - extracted from file if empty)</label>
<input
type="text"
value={artist}
onChange={e => setArtist(e.target.value)}
className="form-input"
/>
</div>
<button type="submit" className="btn-primary">
Upload Song
</button>
{message && <p style={{ textAlign: 'center', marginTop: '0.5rem' }}>{message}</p>}
</form>
</div>
<div className="admin-card">
<h2 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1rem' }}>Song Library</h2>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
<thead>
<tr style={{ borderBottom: '2px solid #e5e7eb', textAlign: 'left' }}>
<th style={{ padding: '0.75rem' }}>ID</th>
<th
style={{ padding: '0.75rem', cursor: 'pointer', userSelect: 'none' }}
onClick={() => handleSort('title')}
>
Title {sortField === 'title' && (sortDirection === 'asc' ? '↑' : '↓')}
</th>
<th
style={{ padding: '0.75rem', cursor: 'pointer', userSelect: 'none' }}
onClick={() => handleSort('artist')}
>
Artist {sortField === 'artist' && (sortDirection === 'asc' ? '↑' : '↓')}
</th>
<th style={{ padding: '0.75rem' }}>Filename</th>
<th style={{ padding: '0.75rem' }}>Actions</th>
</tr>
</thead>
<tbody>
{sortedSongs.map(song => (
<tr key={song.id} style={{ borderBottom: '1px solid #e5e7eb' }}>
<td style={{ padding: '0.75rem' }}>{song.id}</td>
{editingId === song.id ? (
<>
<td style={{ padding: '0.75rem' }}>
<input
type="text"
value={editTitle}
onChange={e => setEditTitle(e.target.value)}
className="form-input"
style={{ padding: '0.25rem' }}
/>
</td>
<td style={{ padding: '0.75rem' }}>
<input
type="text"
value={editArtist}
onChange={e => setEditArtist(e.target.value)}
className="form-input"
style={{ padding: '0.25rem' }}
/>
</td>
<td style={{ padding: '0.75rem', color: '#666' }}>{song.filename}</td>
<td style={{ padding: '0.75rem' }}>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<button
onClick={() => saveEditing(song.id)}
style={{ color: 'green', cursor: 'pointer', border: 'none', background: 'none', fontWeight: 'bold' }}
>
Save
</button>
<button
onClick={cancelEditing}
style={{ color: 'red', cursor: 'pointer', border: 'none', background: 'none' }}
>
Cancel
</button>
</div>
</td>
</>
) : (
<>
<td style={{ padding: '0.75rem', fontWeight: 'bold' }}>{song.title}</td>
<td style={{ padding: '0.75rem' }}>{song.artist}</td>
<td style={{ padding: '0.75rem', color: '#666' }}>{song.filename}</td>
<td style={{ padding: '0.75rem' }}>
<button
onClick={() => startEditing(song)}
style={{ color: 'blue', cursor: 'pointer', border: 'none', background: 'none', textDecoration: 'underline' }}
>
Edit
</button>
</td>
</>
)}
</tr>
))}
{songs.length === 0 && (
<tr>
<td colSpan={5} style={{ padding: '1rem', textAlign: 'center', color: '#666' }}>
No songs uploaded yet.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
);
}