Compare commits
4 Commits
83e1281079
...
v0.1.6.28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17856ef09b | ||
|
|
fb833a7976 | ||
|
|
a4e61de53f | ||
|
|
73c1c1cf89 |
89
app/admin/specials/[id]/page.tsx
Normal file
89
app/admin/specials/[id]/page.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useParams, useRouter, usePathname } from 'next/navigation';
|
||||||
|
import CurateSpecialEditor, { CurateSpecial } from '@/components/CurateSpecialEditor';
|
||||||
|
|
||||||
|
export default function SpecialEditorPage() {
|
||||||
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const specialId = params.id as string;
|
||||||
|
|
||||||
|
// Locale aus dem Pfad ableiten (/en/..., /de/...)
|
||||||
|
const localeFromPath = pathname?.split('/')[1] as 'de' | 'en' | undefined;
|
||||||
|
const locale: 'de' | 'en' = localeFromPath === 'de' || localeFromPath === 'en' ? localeFromPath : 'en';
|
||||||
|
|
||||||
|
const [special, setSpecial] = useState<CurateSpecial | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const fetchSpecial = async (showLoading = true) => {
|
||||||
|
try {
|
||||||
|
if (showLoading) {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
|
const res = await fetch(`/api/specials/${specialId}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
setSpecial(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching special:', error);
|
||||||
|
} finally {
|
||||||
|
if (showLoading) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSpecial(true);
|
||||||
|
}, [specialId]);
|
||||||
|
|
||||||
|
const handleSaveStartTime = async (songId: number, startTime: number) => {
|
||||||
|
const res = await fetch(`/api/specials/${specialId}/songs`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ songId, startTime }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text().catch(() => res.statusText || 'Unknown error');
|
||||||
|
console.error('Error updating special song (admin):', res.status, errorText);
|
||||||
|
throw new Error(`Failed to save start time: ${errorText}`);
|
||||||
|
} else {
|
||||||
|
// Reload special data to update the start time in the song list
|
||||||
|
await fetchSpecial(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||||
|
<p>Loading...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!special) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||||
|
<p>Special not found</p>
|
||||||
|
<button onClick={() => router.push('/admin')}>Back to Admin</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CurateSpecialEditor
|
||||||
|
special={special}
|
||||||
|
locale={locale}
|
||||||
|
onBack={() => router.push('/admin')}
|
||||||
|
onSaveStartTime={handleSaveStartTime}
|
||||||
|
backLabel="← Back to Admin"
|
||||||
|
headerPrefix="Edit Special:"
|
||||||
|
noSongsSubHint="Go back to the admin dashboard to add songs to this special."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,14 @@ export async function GET(
|
|||||||
return NextResponse.json({ error: 'Special not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Special not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(special);
|
// Filtere Songs ohne vollständige Song-Daten (song, song.filename)
|
||||||
|
// Dies verhindert Fehler im Frontend, wenn Songs gelöscht wurden oder Daten fehlen
|
||||||
|
const filteredSongs = special.songs.filter(ss => ss.song && ss.song.filename);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
...special,
|
||||||
|
songs: filteredSongs,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -62,11 +62,14 @@ export default function CurateSpecialEditor({
|
|||||||
saveChangesLabel = '💾 Save Changes',
|
saveChangesLabel = '💾 Save Changes',
|
||||||
savedLabel = '✓ Saved',
|
savedLabel = '✓ Saved',
|
||||||
}: CurateSpecialEditorProps) {
|
}: CurateSpecialEditorProps) {
|
||||||
|
// Filtere Songs ohne vollständige Song-Daten (song, song.filename)
|
||||||
|
const validSongs = special.songs.filter(ss => ss.song && ss.song.filename);
|
||||||
|
|
||||||
const [selectedSongId, setSelectedSongId] = useState<number | null>(
|
const [selectedSongId, setSelectedSongId] = useState<number | null>(
|
||||||
special.songs.length > 0 ? special.songs[0].songId : null
|
validSongs.length > 0 ? validSongs[0].songId : null
|
||||||
);
|
);
|
||||||
const [pendingStartTime, setPendingStartTime] = useState<number | null>(
|
const [pendingStartTime, setPendingStartTime] = useState<number | null>(
|
||||||
special.songs.length > 0 ? special.songs[0].startTime : null
|
validSongs.length > 0 ? validSongs[0].startTime : null
|
||||||
);
|
);
|
||||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -77,7 +80,7 @@ export default function CurateSpecialEditor({
|
|||||||
const unlockSteps = JSON.parse(special.unlockSteps);
|
const unlockSteps = JSON.parse(special.unlockSteps);
|
||||||
const totalDuration = unlockSteps[unlockSteps.length - 1];
|
const totalDuration = unlockSteps[unlockSteps.length - 1];
|
||||||
|
|
||||||
const selectedSpecialSong = special.songs.find(ss => ss.songId === selectedSongId) ?? null;
|
const selectedSpecialSong = validSongs.find(ss => ss.songId === selectedSongId) ?? null;
|
||||||
|
|
||||||
const handleStartTimeChange = (newStartTime: number) => {
|
const handleStartTimeChange = (newStartTime: number) => {
|
||||||
setPendingStartTime(newStartTime);
|
setPendingStartTime(newStartTime);
|
||||||
@@ -111,7 +114,7 @@ export default function CurateSpecialEditor({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{special.songs.length === 0 ? (
|
{validSongs.length === 0 ? (
|
||||||
<div style={{ padding: '2rem', background: '#f3f4f6', borderRadius: '0.5rem', textAlign: 'center' }}>
|
<div style={{ padding: '2rem', background: '#f3f4f6', borderRadius: '0.5rem', textAlign: 'center' }}>
|
||||||
<p>{noSongsHint}</p>
|
<p>{noSongsHint}</p>
|
||||||
<p style={{ fontSize: '0.875rem', color: '#666', marginTop: '0.5rem' }}>
|
<p style={{ fontSize: '0.875rem', color: '#666', marginTop: '0.5rem' }}>
|
||||||
@@ -125,7 +128,7 @@ export default function CurateSpecialEditor({
|
|||||||
Select Song to Curate
|
Select Song to Curate
|
||||||
</h2>
|
</h2>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '1rem' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '1rem' }}>
|
||||||
{special.songs.map(ss => (
|
{validSongs.map(ss => (
|
||||||
<div
|
<div
|
||||||
key={ss.songId}
|
key={ss.songId}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -152,7 +155,7 @@ export default function CurateSpecialEditor({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedSpecialSong && (
|
{selectedSpecialSong && selectedSpecialSong.song && selectedSpecialSong.song.filename ? (
|
||||||
<div>
|
<div>
|
||||||
<h2 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1rem' }}>
|
<h2 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1rem' }}>
|
||||||
Curate: {selectedSpecialSong.song.title}
|
Curate: {selectedSpecialSong.song.title}
|
||||||
@@ -189,7 +192,13 @@ export default function CurateSpecialEditor({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : selectedSpecialSong ? (
|
||||||
|
<div style={{ padding: '2rem', background: '#fee2e2', borderRadius: '0.5rem', textAlign: 'center' }}>
|
||||||
|
<p style={{ color: '#991b1b', fontWeight: 'bold' }}>
|
||||||
|
Fehler: Song-Daten unvollständig. Bitte wählen Sie einen anderen Song.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoerdle",
|
"name": "hoerdle",
|
||||||
"version": "0.1.6.26",
|
"version": "0.1.6.28",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
Reference in New Issue
Block a user