From 50a486f6d86c152a6a9ad765b5a96ab86379af71 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:09:36 +0200 Subject: [PATCH] feat(playlist-reorder): enable intra-playlist row drag&drop with landing indicator; persist order via backend --- .../src/components/PaginatedSongList.tsx | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 5b2c169..40baf3f 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -51,7 +51,10 @@ const SongItem = memo<{ showCheckbox: boolean; onPlaySong?: (song: Song) => void; onDragStart: (e: React.DragEvent, songIdsFallback: string[]) => void; -}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong, onDragStart }) => { + onRowDragOver?: (e: React.DragEvent) => void; + onRowDrop?: (e: React.DragEvent) => void; + onRowDragStartCapture?: (e: React.DragEvent) => void; +}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong, onDragStart, onRowDragOver, onRowDrop, onRowDragStartCapture }) => { // Memoize the formatted duration to prevent recalculation const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]); const handleClick = useCallback(() => { @@ -89,6 +92,9 @@ const SongItem = memo<{ onDragStart={(e) => { onDragStart(e, [song.id]); }} + onDragOver={onRowDragOver} + onDrop={onRowDrop} + onDragStartCapture={onRowDragStartCapture} > {showCheckbox && ( = memo(({ const scrollContainerRef = useRef(null); const isTriggeringRef = useRef(false); const timeoutRef = useRef(null); - const dragIndexRef = useRef(null); + const [dragHoverIndex, setDragHoverIndex] = useState(null); // Store current values in refs to avoid stale closures const hasMoreRef = useRef(hasMore); @@ -267,11 +273,12 @@ export const PaginatedSongList: React.FC = memo(({ onPlaySong={onPlaySong} onDragStart={handleDragStart} // Simple playlist reordering within same list by dragging rows - onDragOver={(e: React.DragEvent) => { + onRowDragOver={(e: React.DragEvent) => { if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return; e.preventDefault(); + setDragHoverIndex(index); }} - onDrop={async (e: React.DragEvent) => { + onRowDrop={async (e: React.DragEvent) => { if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return; e.preventDefault(); const fromId = e.dataTransfer.getData('text/song-id'); @@ -283,12 +290,12 @@ export const PaginatedSongList: React.FC = memo(({ const [moved] = ordered.splice(fromIndex, 1); ordered.splice(toIndex, 0, moved); await onReorder(ordered.map(s => s.id)); + setDragHoverIndex(null); }} - onDragStartCapture={(e: React.DragEvent) => { + onRowDragStartCapture={(e: React.DragEvent) => { // Provide a simple id for intra-list reorder if (!currentPlaylist || selectedSongs.size > 0) return; e.dataTransfer.setData('text/song-id', song.id); - dragIndexRef.current = index; }} /> )); @@ -467,7 +474,16 @@ export const PaginatedSongList: React.FC = memo(({ id="song-list-container" > - {songItems} + + {songs.map((song, index) => ( + + {dragHoverIndex === index && ( + + )} + {songItems[index]} + + ))} + {/* Loading indicator for infinite scroll or playlist switching */} {(loading || isSwitchingPlaylist) && (