Add curator special curation flow and shared editor
This commit is contained in:
@@ -1,103 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import WaveformEditor from '@/components/WaveformEditor';
|
||||
|
||||
interface Song {
|
||||
id: number;
|
||||
title: string;
|
||||
artist: string;
|
||||
filename: string;
|
||||
}
|
||||
|
||||
interface SpecialSong {
|
||||
id: number;
|
||||
songId: number;
|
||||
startTime: number;
|
||||
order: number | null;
|
||||
song: Song;
|
||||
}
|
||||
|
||||
interface Special {
|
||||
id: number;
|
||||
name: string;
|
||||
subtitle?: string;
|
||||
maxAttempts: number;
|
||||
unlockSteps: string;
|
||||
songs: SpecialSong[];
|
||||
}
|
||||
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;
|
||||
|
||||
const [special, setSpecial] = useState<Special | null>(null);
|
||||
const [selectedSongId, setSelectedSongId] = useState<number | null>(null);
|
||||
// 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 [saving, setSaving] = useState(false);
|
||||
const [pendingStartTime, setPendingStartTime] = useState<number | null>(null);
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSpecial = async () => {
|
||||
try {
|
||||
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 {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSpecial();
|
||||
}, [specialId]);
|
||||
|
||||
const fetchSpecial = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/specials/${specialId}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setSpecial(data);
|
||||
if (data.songs.length > 0) {
|
||||
setSelectedSongId(data.songs[0].songId);
|
||||
// Initialize pendingStartTime with the current startTime of the first song
|
||||
setPendingStartTime(data.songs[0].startTime);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching special:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartTimeChange = (newStartTime: number) => {
|
||||
setPendingStartTime(newStartTime);
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!special || !selectedSongId || pendingStartTime === null) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch(`/api/specials/${specialId}/songs`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ songId: selectedSongId, startTime: pendingStartTime })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
// Update local state
|
||||
setSpecial(prev => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
songs: prev.songs.map(ss =>
|
||||
ss.songId === selectedSongId ? { ...ss, startTime: pendingStartTime } : ss
|
||||
)
|
||||
};
|
||||
});
|
||||
setHasUnsavedChanges(false);
|
||||
setPendingStartTime(null); // Reset pending state after saving
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating start time:', error);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
const handleSaveStartTime = async (songId: number, startTime: number) => {
|
||||
await fetch(`/api/specials/${specialId}/songs`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ songId, startTime }),
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -117,116 +60,16 @@ export default function SpecialEditorPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const selectedSpecialSong = special.songs.find(ss => ss.songId === selectedSongId);
|
||||
const unlockSteps = JSON.parse(special.unlockSteps);
|
||||
const totalDuration = unlockSteps[unlockSteps.length - 1];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '2rem', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<div style={{ marginBottom: '2rem' }}>
|
||||
<button
|
||||
onClick={() => router.push('/admin')}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
background: '#e5e7eb',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '1rem'
|
||||
}}
|
||||
>
|
||||
← Back to Admin
|
||||
</button>
|
||||
<h1 style={{ fontSize: '2rem', fontWeight: 'bold' }}>
|
||||
Edit Special: {special.name}
|
||||
</h1>
|
||||
{special.subtitle && (
|
||||
<p style={{ fontSize: '1.125rem', color: '#4b5563', marginTop: '0.25rem' }}>
|
||||
{special.subtitle}
|
||||
</p>
|
||||
)}
|
||||
<p style={{ color: '#666', marginTop: '0.5rem' }}>
|
||||
Max Attempts: {special.maxAttempts} | Puzzle Duration: {totalDuration}s
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{special.songs.length === 0 ? (
|
||||
<div style={{ padding: '2rem', background: '#f3f4f6', borderRadius: '0.5rem', textAlign: 'center' }}>
|
||||
<p>No songs assigned to this special yet.</p>
|
||||
<p style={{ fontSize: '0.875rem', color: '#666', marginTop: '0.5rem' }}>
|
||||
Go back to the admin dashboard to add songs to this special.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div style={{ marginBottom: '2rem' }}>
|
||||
<h2 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1rem' }}>
|
||||
Select Song to Curate
|
||||
</h2>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '1rem' }}>
|
||||
{special.songs.map(ss => (
|
||||
<div
|
||||
key={ss.songId}
|
||||
onClick={() => setSelectedSongId(ss.songId)}
|
||||
style={{
|
||||
padding: '1rem',
|
||||
background: selectedSongId === ss.songId ? '#4f46e5' : '#f3f4f6',
|
||||
color: selectedSongId === ss.songId ? 'white' : 'black',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: 'pointer',
|
||||
border: selectedSongId === ss.songId ? '2px solid #4f46e5' : '2px solid transparent'
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 'bold' }}>{ss.song.title}</div>
|
||||
<div style={{ fontSize: '0.875rem', opacity: 0.8 }}>{ss.song.artist}</div>
|
||||
<div style={{ fontSize: '0.75rem', marginTop: '0.5rem', opacity: 0.7 }}>
|
||||
Start: {ss.startTime}s
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedSpecialSong && (
|
||||
<div>
|
||||
<h2 style={{ fontSize: '1.25rem', fontWeight: 'bold', marginBottom: '1rem' }}>
|
||||
Curate: {selectedSpecialSong.song.title}
|
||||
</h2>
|
||||
<div style={{ background: '#f9fafb', padding: '1.5rem', borderRadius: '0.5rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||
<p style={{ fontSize: '0.875rem', color: '#666', margin: 0 }}>
|
||||
Click on the waveform to select where the puzzle should start. The highlighted region shows what players will hear.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!hasUnsavedChanges || saving}
|
||||
style={{
|
||||
padding: '0.5rem 1.5rem',
|
||||
background: hasUnsavedChanges ? '#10b981' : '#e5e7eb',
|
||||
color: hasUnsavedChanges ? 'white' : '#9ca3af',
|
||||
border: 'none',
|
||||
borderRadius: '0.5rem',
|
||||
cursor: hasUnsavedChanges && !saving ? 'pointer' : 'not-allowed',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '0.875rem',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{saving ? '💾 Saving...' : hasUnsavedChanges ? '💾 Save Changes' : '✓ Saved'}
|
||||
</button>
|
||||
</div>
|
||||
<WaveformEditor
|
||||
audioUrl={`/uploads/${selectedSpecialSong.song.filename}`}
|
||||
startTime={pendingStartTime ?? selectedSpecialSong.startTime}
|
||||
duration={totalDuration}
|
||||
unlockSteps={unlockSteps}
|
||||
onStartTimeChange={handleStartTimeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<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."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user