From 6d2eae9c7b03d0fa9551dd211aedfdd8b8be5004 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Wed, 13 Aug 2025 16:21:51 +0200 Subject: [PATCH] feat(frontend): custom drag preview with selection count badge when dragging multiple songs --- .../src/components/PaginatedSongList.tsx | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 0274ff7..3cf68f5 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -189,6 +189,7 @@ export const PaginatedSongList: React.FC = memo(({ const [endDropHover, setEndDropHover] = useState(false); const [isReorderDragging, setIsReorderDragging] = useState(false); const lastSelectedIndexRef = useRef(null); + const dragPreviewRef = useRef(null); // Store current values in refs to avoid stale closures const hasMoreRef = useRef(hasMore); @@ -313,6 +314,52 @@ export const PaginatedSongList: React.FC = memo(({ e.dataTransfer.setData('application/json', JSON.stringify(payload)); e.dataTransfer.setData('text/plain', JSON.stringify(payload)); e.dataTransfer.effectAllowed = 'copyMove'; + + // Create a custom drag image with count badge when dragging multiple + try { + const count = ids.length; + if (count > 1) { + // Build a lightweight preview element + const preview = document.createElement('div'); + preview.style.position = 'fixed'; + preview.style.top = '-1000px'; + preview.style.left = '-1000px'; + preview.style.pointerEvents = 'none'; + preview.style.padding = '6px 10px'; + preview.style.borderRadius = '8px'; + preview.style.background = 'rgba(26, 32, 44, 0.95)'; // gray.900 + preview.style.color = '#E2E8F0'; // gray.200 + preview.style.fontSize = '12px'; + preview.style.fontWeight = '600'; + preview.style.display = 'inline-flex'; + preview.style.alignItems = 'center'; + preview.style.gap = '8px'; + preview.style.boxShadow = '0 4px 12px rgba(0,0,0,0.5)'; + + const dot = document.createElement('div'); + dot.style.background = '#3182CE'; // blue.500 + dot.style.color = 'white'; + dot.style.minWidth = '20px'; + dot.style.height = '20px'; + dot.style.borderRadius = '10px'; + dot.style.display = 'flex'; + dot.style.alignItems = 'center'; + dot.style.justifyContent = 'center'; + dot.style.fontSize = '12px'; + dot.style.fontWeight = '700'; + dot.textContent = String(count); + + const label = document.createElement('div'); + label.textContent = count === 1 ? 'song' : 'songs'; + + preview.appendChild(dot); + preview.appendChild(label); + document.body.appendChild(preview); + dragPreviewRef.current = preview; + // Offset so the cursor isn't on top of the preview + e.dataTransfer.setDragImage(preview, 10, 10); + } + } catch {} }, [selectedSongs]); const handleDragEnd = useCallback(() => { @@ -321,6 +368,11 @@ export const PaginatedSongList: React.FC = memo(({ setEndDropHover(false); setDragHoverIndex(null); dragSelectionRef.current = null; + // Cleanup drag preview element + if (dragPreviewRef.current && dragPreviewRef.current.parentNode) { + try { dragPreviewRef.current.parentNode.removeChild(dragPreviewRef.current); } catch {} + } + dragPreviewRef.current = null; }, []); // Virtualizer for large lists