diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 69d050a..826cdd6 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -1,27 +1,30 @@ -import React, { useState, useCallback, useMemo, useRef, useEffect, memo } from 'react'; -import { useDebounce } from '../hooks/useDebounce'; +import React, { useState, useRef, useEffect, useCallback, useMemo, memo } from 'react'; import { Box, Flex, Text, - Input, - InputGroup, - InputLeftElement, - Checkbox, Button, + IconButton, HStack, Menu, MenuButton, MenuList, MenuItem, MenuDivider, - IconButton, + Checkbox, + Tooltip, Spinner, + useDisclosure, + Input, + InputGroup, + InputLeftElement, } from '@chakra-ui/react'; import { Search2Icon, ChevronDownIcon } from '@chakra-ui/icons'; +import { FiPlay } from 'react-icons/fi'; import type { Song, PlaylistNode } from '../types/interfaces'; import { formatDuration, formatTotalDuration } from '../utils/formatters'; -import { FiPlay } from 'react-icons/fi'; +import { useDebounce } from '../hooks/useDebounce'; +import { PlaylistSelectionModal } from './PlaylistSelectionModal'; interface PaginatedSongListProps { songs: Song[]; @@ -150,6 +153,7 @@ export const PaginatedSongList: React.FC = memo(({ }) => { const [selectedSongs, setSelectedSongs] = useState>(new Set()); const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery); + const { isOpen: isPlaylistModalOpen, onOpen: onPlaylistModalOpen, onClose: onPlaylistModalClose } = useDisclosure(); const observerRef = useRef(null); const loadingRef = useRef(null); const scrollContainerRef = useRef(null); @@ -386,41 +390,27 @@ export const PaginatedSongList: React.FC = memo(({ {selectedSongs.size > 0 && ( - - } + + + Add to Playlist... + + {currentPlaylist !== "All Songs" && onRemoveFromPlaylist && ( + + )} + )} @@ -477,6 +467,15 @@ export const PaginatedSongList: React.FC = memo(({ )} + + {/* Playlist Selection Modal */} + ); }); \ No newline at end of file diff --git a/packages/frontend/src/components/PlaylistSelectionModal.tsx b/packages/frontend/src/components/PlaylistSelectionModal.tsx new file mode 100644 index 0000000..eae0e2b --- /dev/null +++ b/packages/frontend/src/components/PlaylistSelectionModal.tsx @@ -0,0 +1,165 @@ +import React, { useState, useMemo } from 'react'; +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, + Button, + Input, + InputGroup, + InputLeftElement, + VStack, + HStack, + Text, + Icon, + useToast, + Box, + Divider, +} from '@chakra-ui/react'; +import { SearchIcon, FolderIcon } from '@chakra-ui/icons'; +import { FiFolder, FiMusic } from 'react-icons/fi'; +import type { PlaylistNode } from '../types/interfaces'; + +interface PlaylistSelectionModalProps { + isOpen: boolean; + onClose: () => void; + playlists: PlaylistNode[]; + onPlaylistSelect: (playlistName: string) => void; + selectedSongCount: number; +} + +export const PlaylistSelectionModal: React.FC = ({ + isOpen, + onClose, + playlists, + onPlaylistSelect, + selectedSongCount, +}) => { + const [searchQuery, setSearchQuery] = useState(''); + const toast = useToast(); + + // Flatten all playlists (including nested ones) for search + const allPlaylists = useMemo(() => { + const flattenPlaylists = (nodes: PlaylistNode[]): PlaylistNode[] => { + const result: PlaylistNode[] = []; + for (const node of nodes) { + if (node.type === 'playlist') { + result.push(node); + } else if (node.type === 'folder' && node.children) { + result.push(...flattenPlaylists(node.children)); + } + } + return result; + }; + return flattenPlaylists(playlists); + }, [playlists]); + + // Filter playlists based on search query + const filteredPlaylists = useMemo(() => { + if (!searchQuery.trim()) return allPlaylists; + + const query = searchQuery.toLowerCase(); + return allPlaylists.filter(playlist => + playlist.name.toLowerCase().includes(query) + ); + }, [allPlaylists, searchQuery]); + + const handlePlaylistSelect = (playlistName: string) => { + onPlaylistSelect(playlistName); + onClose(); + setSearchQuery(''); + + toast({ + title: 'Songs Added', + description: `${selectedSongCount} song${selectedSongCount !== 1 ? 's' : ''} added to "${playlistName}"`, + status: 'success', + duration: 3000, + isClosable: true, + }); + }; + + const handleClose = () => { + onClose(); + setSearchQuery(''); + }; + + return ( + + + + + Add to Playlist + + Select a playlist to add {selectedSongCount} song{selectedSongCount !== 1 ? 's' : ''} + + + + + + {/* Search Input */} + + + + + setSearchQuery(e.target.value)} + bg="gray.700" + borderColor="gray.600" + color="white" + _placeholder={{ color: 'gray.400' }} + _focus={{ borderColor: 'blue.400', boxShadow: 'none' }} + _hover={{ borderColor: 'gray.500' }} + /> + + + {/* Playlist List */} + + {filteredPlaylists.length === 0 ? ( + + {searchQuery ? 'No playlists found' : 'No playlists available'} + + ) : ( + + {filteredPlaylists.map((playlist) => ( + handlePlaylistSelect(playlist.name)} + transition="background-color 0.2s" + > + + + + + {playlist.name} + + + {playlist.tracks?.length || 0} tracks + + + + + ))} + + )} + + + + + + + + + + ); +}; \ No newline at end of file