Finalize scoring system, release year integration, and fix song deletion
This commit is contained in:
@@ -3,13 +3,14 @@
|
||||
const GOTIFY_URL = process.env.GOTIFY_URL;
|
||||
const GOTIFY_APP_TOKEN = process.env.GOTIFY_APP_TOKEN;
|
||||
|
||||
export async function sendGotifyNotification(attempts: number, status: 'won' | 'lost', puzzleId: number, genre?: string | null) {
|
||||
export async function sendGotifyNotification(attempts: number, status: 'won' | 'lost', puzzleId: number, genre?: string | null, score?: number) {
|
||||
try {
|
||||
const genreText = genre ? `[${genre}] ` : '';
|
||||
const title = `Hördle ${genreText}#${puzzleId} ${status === 'won' ? 'Solved!' : 'Failed'}`;
|
||||
const scoreText = score !== undefined ? ` with a score of ${score}` : '';
|
||||
const message = status === 'won'
|
||||
? `Puzzle #${puzzleId} ${genre ? `(${genre}) ` : ''}was solved in ${attempts} attempt(s).`
|
||||
: `Puzzle #${puzzleId} ${genre ? `(${genre}) ` : ''}was failed after ${attempts} attempt(s).`;
|
||||
? `Puzzle #${puzzleId} ${genre ? `(${genre}) ` : ''}was solved in ${attempts} attempt(s)${scoreText}.`
|
||||
: `Puzzle #${puzzleId} ${genre ? `(${genre}) ` : ''}was failed after ${attempts} attempt(s)${scoreText}.`;
|
||||
|
||||
const response = await fetch(`${GOTIFY_URL}/message?token=${GOTIFY_APP_TOKEN}`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -40,6 +40,7 @@ interface Song {
|
||||
artist: string;
|
||||
filename: string;
|
||||
createdAt: string;
|
||||
releaseYear: number | null;
|
||||
activations: number;
|
||||
puzzles: DailyPuzzle[];
|
||||
genres: Genre[];
|
||||
@@ -48,7 +49,7 @@ interface Song {
|
||||
ratingCount: number;
|
||||
}
|
||||
|
||||
type SortField = 'id' | 'title' | 'artist' | 'createdAt';
|
||||
type SortField = 'id' | 'title' | 'artist' | 'createdAt' | 'releaseYear';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
export default function AdminPage() {
|
||||
@@ -91,6 +92,7 @@ export default function AdminPage() {
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [editTitle, setEditTitle] = useState('');
|
||||
const [editArtist, setEditArtist] = useState('');
|
||||
const [editReleaseYear, setEditReleaseYear] = useState<number | ''>('');
|
||||
const [editGenreIds, setEditGenreIds] = useState<number[]>([]);
|
||||
const [editSpecialIds, setEditSpecialIds] = useState<number[]>([]);
|
||||
|
||||
@@ -577,6 +579,7 @@ export default function AdminPage() {
|
||||
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) : []);
|
||||
};
|
||||
@@ -585,6 +588,7 @@ export default function AdminPage() {
|
||||
setEditingId(null);
|
||||
setEditTitle('');
|
||||
setEditArtist('');
|
||||
setEditReleaseYear('');
|
||||
setEditGenreIds([]);
|
||||
setEditSpecialIds([]);
|
||||
};
|
||||
@@ -597,6 +601,7 @@ export default function AdminPage() {
|
||||
id,
|
||||
title: editTitle,
|
||||
artist: editArtist,
|
||||
releaseYear: editReleaseYear === '' ? null : Number(editReleaseYear),
|
||||
genreIds: editGenreIds,
|
||||
specialIds: editSpecialIds
|
||||
}),
|
||||
@@ -706,10 +711,15 @@ export default function AdminPage() {
|
||||
});
|
||||
|
||||
const sortedSongs = [...filteredSongs].sort((a, b) => {
|
||||
// Handle numeric sorting for ID
|
||||
// Handle numeric sorting for ID and Release Year
|
||||
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;
|
||||
}
|
||||
|
||||
// String sorting for other fields
|
||||
const valA = String(a[sortField]).toLowerCase();
|
||||
@@ -1223,10 +1233,15 @@ export default function AdminPage() {
|
||||
style={{ padding: '0.75rem', cursor: 'pointer', userSelect: 'none' }}
|
||||
onClick={() => handleSort('title')}
|
||||
>
|
||||
Title {sortField === 'title' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
Song {sortField === 'title' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
|
||||
<th style={{ padding: '0.75rem' }}>Genres</th>
|
||||
<th
|
||||
style={{ padding: '0.75rem', cursor: 'pointer', userSelect: 'none' }}
|
||||
onClick={() => handleSort('releaseYear')}
|
||||
>
|
||||
Year {sortField === 'releaseYear' && (sortDirection === 'asc' ? '↑' : '↓')}
|
||||
</th>
|
||||
<th style={{ padding: '0.75rem' }}>Genres / Specials</th>
|
||||
<th
|
||||
style={{ padding: '0.75rem', cursor: 'pointer', userSelect: 'none' }}
|
||||
onClick={() => handleSort('createdAt')}
|
||||
@@ -1263,6 +1278,16 @@ export default function AdminPage() {
|
||||
placeholder="Artist"
|
||||
/>
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem' }}>
|
||||
<input
|
||||
type="number"
|
||||
value={editReleaseYear}
|
||||
onChange={e => setEditReleaseYear(e.target.value === '' ? '' : Number(e.target.value))}
|
||||
className="form-input"
|
||||
style={{ padding: '0.25rem', width: '80px' }}
|
||||
placeholder="Year"
|
||||
/>
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem' }}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
|
||||
{genres.map(genre => (
|
||||
@@ -1369,6 +1394,9 @@ export default function AdminPage() {
|
||||
})}
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem', color: '#666' }}>
|
||||
{song.releaseYear || '-'}
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem' }}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem' }}>
|
||||
{song.genres?.map(g => (
|
||||
|
||||
@@ -29,6 +29,7 @@ export async function GET() {
|
||||
filename: song.filename,
|
||||
createdAt: song.createdAt,
|
||||
coverImage: song.coverImage,
|
||||
releaseYear: song.releaseYear,
|
||||
activations: song.puzzles.length,
|
||||
puzzles: song.puzzles,
|
||||
genres: song.genres,
|
||||
@@ -179,12 +180,25 @@ export async function POST(request: Request) {
|
||||
console.error('Failed to extract cover image:', e);
|
||||
}
|
||||
|
||||
// Fetch release year from MusicBrainz
|
||||
let releaseYear = null;
|
||||
try {
|
||||
const { getReleaseYear } = await import('@/lib/musicbrainz');
|
||||
releaseYear = await getReleaseYear(artist, title);
|
||||
if (releaseYear) {
|
||||
console.log(`Fetched release year ${releaseYear} for "${title}" by "${artist}"`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch release year from MusicBrainz:', e);
|
||||
}
|
||||
|
||||
const song = await prisma.song.create({
|
||||
data: {
|
||||
title,
|
||||
artist,
|
||||
filename,
|
||||
coverImage,
|
||||
releaseYear,
|
||||
},
|
||||
include: { genres: true, specials: true }
|
||||
});
|
||||
@@ -201,7 +215,7 @@ export async function POST(request: Request) {
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
try {
|
||||
const { id, title, artist, genreIds, specialIds } = await request.json();
|
||||
const { id, title, artist, releaseYear, genreIds, specialIds } = await request.json();
|
||||
|
||||
if (!id || !title || !artist) {
|
||||
return NextResponse.json({ error: 'Missing fields' }, { status: 400 });
|
||||
@@ -209,6 +223,11 @@ export async function PUT(request: Request) {
|
||||
|
||||
const data: any = { title, artist };
|
||||
|
||||
// Update releaseYear if provided (can be null to clear it)
|
||||
if (releaseYear !== undefined) {
|
||||
data.releaseYear = releaseYear;
|
||||
}
|
||||
|
||||
if (genreIds) {
|
||||
data.genres = {
|
||||
set: genreIds.map((gId: number) => ({ id: gId }))
|
||||
|
||||
Reference in New Issue
Block a user