feat(special-editor): add manual save button with unsaved changes indicator

This commit is contained in:
Hördle Bot
2025-11-23 01:07:53 +01:00
parent 23c2697424
commit e06e0d2919

View File

@@ -36,6 +36,8 @@ export default function SpecialEditorPage() {
const [selectedSongId, setSelectedSongId] = useState<number | null>(null); const [selectedSongId, setSelectedSongId] = useState<number | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [pendingStartTime, setPendingStartTime] = useState<number | null>(null);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
useEffect(() => { useEffect(() => {
fetchSpecial(); fetchSpecial();
@@ -49,6 +51,8 @@ export default function SpecialEditorPage() {
setSpecial(data); setSpecial(data);
if (data.songs.length > 0) { if (data.songs.length > 0) {
setSelectedSongId(data.songs[0].songId); setSelectedSongId(data.songs[0].songId);
// Initialize pendingStartTime with the current startTime of the first song
setPendingStartTime(data.songs[0].startTime);
} }
} }
} catch (error) { } catch (error) {
@@ -58,15 +62,20 @@ export default function SpecialEditorPage() {
} }
}; };
const handleStartTimeChange = async (songId: number, newStartTime: number) => { const handleStartTimeChange = (newStartTime: number) => {
if (!special) return; setPendingStartTime(newStartTime);
setHasUnsavedChanges(true);
};
const handleSave = async () => {
if (!special || !selectedSongId || pendingStartTime === null) return;
setSaving(true); setSaving(true);
try { try {
const res = await fetch(`/api/specials/${specialId}/songs`, { const res = await fetch(`/api/specials/${specialId}/songs`, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ songId, startTime: newStartTime }) body: JSON.stringify({ songId: selectedSongId, startTime: pendingStartTime })
}); });
if (res.ok) { if (res.ok) {
@@ -76,10 +85,12 @@ export default function SpecialEditorPage() {
return { return {
...prev, ...prev,
songs: prev.songs.map(ss => songs: prev.songs.map(ss =>
ss.songId === songId ? { ...ss, startTime: newStartTime } : ss ss.songId === selectedSongId ? { ...ss, startTime: pendingStartTime } : ss
) )
}; };
}); });
setHasUnsavedChanges(false);
setPendingStartTime(null); // Reset pending state after saving
} }
} catch (error) { } catch (error) {
console.error('Error updating start time:', error); console.error('Error updating start time:', error);
@@ -176,21 +187,35 @@ export default function SpecialEditorPage() {
Curate: {selectedSpecialSong.song.title} Curate: {selectedSpecialSong.song.title}
</h2> </h2>
<div style={{ background: '#f9fafb', padding: '1.5rem', borderRadius: '0.5rem' }}> <div style={{ background: '#f9fafb', padding: '1.5rem', borderRadius: '0.5rem' }}>
<p style={{ fontSize: '0.875rem', color: '#666', marginBottom: '1rem' }}> <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. Click on the waveform to select where the puzzle should start. The highlighted region shows what players will hear.
</p> </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 <WaveformEditor
audioUrl={`/uploads/${selectedSpecialSong.song.filename}`} audioUrl={`/uploads/${selectedSpecialSong.song.filename}`}
startTime={selectedSpecialSong.startTime} startTime={pendingStartTime ?? selectedSpecialSong.startTime}
duration={totalDuration} duration={totalDuration}
unlockSteps={unlockSteps} unlockSteps={unlockSteps}
onStartTimeChange={(newStartTime) => handleStartTimeChange(selectedSpecialSong.songId, newStartTime)} onStartTimeChange={handleStartTimeChange}
/> />
{saving && (
<div style={{ marginTop: '1rem', color: '#4f46e5', fontSize: '0.875rem' }}>
Saving...
</div>
)}
</div> </div>
</div> </div>
)} )}