feat(special-editor): add manual save button with unsaved changes indicator
This commit is contained in:
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user