diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 0b5b0ab..7b09520 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -70,10 +70,14 @@ const SongItem = memo<{ // Memoize the formatted duration to prevent recalculation const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]); const handleClick = useCallback(() => { - console.log('SongItem clicked:', song.title); onSelect(song); }, [onSelect, song]); + const hasMusicFile = useMemo(() => + song.s3File?.hasS3File || song.hasMusicFile || false, + [song.s3File?.hasS3File, song.hasMusicFile] + ); + const handleCheckboxClick = useCallback((e: React.ChangeEvent) => { e.stopPropagation(); if (onCheckboxToggle) { @@ -85,14 +89,10 @@ const SongItem = memo<{ const handlePlayClick = useCallback((e: React.MouseEvent) => { e.stopPropagation(); - if (onPlaySong && (song.s3File?.hasS3File || song.hasMusicFile)) { + if (onPlaySong && hasMusicFile) { onPlaySong(song); } - }, [onPlaySong, song]); - - const hasMusicFile = (song: Song): boolean => { - return song.s3File?.hasS3File || song.hasMusicFile || false; - }; + }, [onPlaySong, hasMusicFile, song]); return ( - {hasMusicFile(song) && onPlaySong && ( + {hasMusicFile && onPlaySong && ( } @@ -156,6 +156,28 @@ const SongItem = memo<{ SongItem.displayName = 'SongItem'; +// Custom comparison function to prevent unnecessary re-renders +const areEqual = (prevProps: any, nextProps: any) => { + return ( + prevProps.song.id === nextProps.song.id && + prevProps.isSelected === nextProps.isSelected && + prevProps.isHighlighted === nextProps.isHighlighted && + prevProps.showCheckbox === nextProps.showCheckbox && + prevProps.showDropIndicatorTop === nextProps.showDropIndicatorTop && + prevProps.index === nextProps.index && + prevProps.song.title === nextProps.song.title && + prevProps.song.artist === nextProps.song.artist && + prevProps.song.totalTime === nextProps.song.totalTime && + prevProps.song.tonality === nextProps.song.tonality && + prevProps.song.averageBpm === nextProps.song.averageBpm && + prevProps.song.s3File?.hasS3File === nextProps.song.s3File?.hasS3File && + prevProps.song.hasMusicFile === nextProps.song.hasMusicFile + ); +}; + +// Apply custom comparison to SongItem +const OptimizedSongItem = memo(SongItem, areEqual); + export const PaginatedSongList: React.FC = memo(({ songs, onAddToPlaylist, @@ -303,6 +325,54 @@ export const PaginatedSongList: React.FC = memo(({ onSongSelect(song); }, [onSongSelect]); + // Memoized drag handlers to prevent re-renders + const createRowDragOver = useCallback((index: number) => { + if (!onReorder || !currentPlaylist) return undefined; + return (e: React.DragEvent) => { + e.preventDefault(); + setDragHoverIndex(index); + }; + }, [onReorder, currentPlaylist]); + + const createRowDrop = useCallback((index: number) => { + if (!onReorder || !currentPlaylist) return undefined; + return async (e: React.DragEvent) => { + e.preventDefault(); + const fromId = e.dataTransfer.getData('text/song-id'); + const multiJson = e.dataTransfer.getData('application/json'); + let multiIds: string[] | null = null; + if (multiJson) { + try { + const parsed = JSON.parse(multiJson); + if (parsed && parsed.type === 'songs' && Array.isArray(parsed.songIds)) { + multiIds = parsed.songIds as string[]; + } + } catch {} + } + if (!fromId && !multiIds) return; + const toId = songs[index]?.id; + if (!toId) return; + if (multiIds && multiIds.length > 0) { + await api.moveTracksInPlaylist(currentPlaylist, multiIds, toId); + } else { + await api.moveTrackInPlaylist(currentPlaylist, fromId!, toId); + } + await onReorder(songs.map(s => s.id)); + setDragHoverIndex(null); + setIsReorderDragging(false); + }; + }, [onReorder, currentPlaylist, songs]); + + const createRowDragStartCapture = useCallback((song: Song) => { + if (!currentPlaylist) return undefined; + return (e: React.DragEvent) => { + e.dataTransfer.setData('text/song-id', song.id); + try { e.dataTransfer.effectAllowed = 'move'; } catch {} + try { e.dataTransfer.dropEffect = 'move'; } catch {} + setIsReorderDragging(true); + }; + }, [currentPlaylist]); + // Memoized search handler with debouncing // Search handled inline via localSearchQuery effect @@ -591,7 +661,7 @@ export const PaginatedSongList: React.FC = memo(({ {songs.map((song, index) => { const allowReorder = Boolean(onReorder && currentPlaylist); return ( - = memo(({ onPlaySong={onPlaySong} showDropIndicatorTop={dragHoverIndex === index} onDragStart={handleDragStart} - onRowDragOver={allowReorder ? ((e: React.DragEvent) => { - if (!onReorder || !currentPlaylist) return; - e.preventDefault(); - setDragHoverIndex(index); - }) : undefined} - onRowDrop={allowReorder ? (async (e: React.DragEvent) => { - if (!onReorder || !currentPlaylist) return; - e.preventDefault(); - const fromId = e.dataTransfer.getData('text/song-id'); - const multiJson = e.dataTransfer.getData('application/json'); - let multiIds: string[] | null = null; - if (multiJson) { - try { - const parsed = JSON.parse(multiJson); - if (parsed && parsed.type === 'songs' && Array.isArray(parsed.songIds)) { - multiIds = parsed.songIds as string[]; - } - } catch {} - } - if (!fromId && !multiIds) return; - const toId = songs[index]?.id; - if (!toId) return; - if (multiIds && multiIds.length > 0) { - await api.moveTracksInPlaylist(currentPlaylist, multiIds, toId); - } else { - await api.moveTrackInPlaylist(currentPlaylist, fromId!, toId); - } - await onReorder(songs.map(s => s.id)); - setDragHoverIndex(null); - setIsReorderDragging(false); - }) : undefined} - onRowDragStartCapture={allowReorder ? ((e: React.DragEvent) => { - if (!currentPlaylist) return; - e.dataTransfer.setData('text/song-id', song.id); - try { e.dataTransfer.effectAllowed = 'move'; } catch {} - try { e.dataTransfer.dropEffect = 'move'; } catch {} - setIsReorderDragging(true); - }) : undefined} + onRowDragOver={allowReorder ? createRowDragOver(index) : undefined} + onRowDrop={allowReorder ? createRowDrop(index) : undefined} + onRowDragStartCapture={allowReorder ? createRowDragStartCapture(song) : undefined} /> ); })}