164 lines
5.0 KiB
TypeScript
164 lines
5.0 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import {
|
|
Modal,
|
|
ModalOverlay,
|
|
ModalContent,
|
|
ModalHeader,
|
|
ModalBody,
|
|
ModalFooter,
|
|
Button,
|
|
Input,
|
|
InputGroup,
|
|
InputLeftElement,
|
|
VStack,
|
|
HStack,
|
|
Text,
|
|
Icon,
|
|
useToast,
|
|
Box,
|
|
} from '@chakra-ui/react';
|
|
import { SearchIcon } from '@chakra-ui/icons';
|
|
import { 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<PlaylistSelectionModalProps> = ({
|
|
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 (
|
|
<Modal isOpen={isOpen} onClose={handleClose} size="md">
|
|
<ModalOverlay />
|
|
<ModalContent bg="gray.800" borderColor="gray.700" borderWidth="1px">
|
|
<ModalHeader color="white">
|
|
Add to Playlist
|
|
<Text fontSize="sm" color="gray.400" fontWeight="normal" mt={1}>
|
|
Select a playlist to add {selectedSongCount} song{selectedSongCount !== 1 ? 's' : ''}
|
|
</Text>
|
|
</ModalHeader>
|
|
|
|
<ModalBody>
|
|
<VStack spacing={4} align="stretch">
|
|
{/* Search Input */}
|
|
<InputGroup>
|
|
<InputLeftElement pointerEvents="none">
|
|
<SearchIcon color="gray.400" />
|
|
</InputLeftElement>
|
|
<Input
|
|
placeholder="Search playlists..."
|
|
value={searchQuery}
|
|
onChange={(e) => 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' }}
|
|
/>
|
|
</InputGroup>
|
|
|
|
{/* Playlist List */}
|
|
<Box maxH="300px" overflowY="auto">
|
|
{filteredPlaylists.length === 0 ? (
|
|
<Text color="gray.400" textAlign="center" py={4}>
|
|
{searchQuery ? 'No playlists found' : 'No playlists available'}
|
|
</Text>
|
|
) : (
|
|
<VStack spacing={1} align="stretch">
|
|
{filteredPlaylists.map((playlist) => (
|
|
<Box
|
|
key={playlist.id}
|
|
p={3}
|
|
borderRadius="md"
|
|
bg="gray.700"
|
|
cursor="pointer"
|
|
_hover={{ bg: 'gray.600' }}
|
|
onClick={() => handlePlaylistSelect(playlist.name)}
|
|
transition="background-color 0.2s"
|
|
>
|
|
<HStack spacing={3}>
|
|
<Icon as={FiMusic} color="blue.400" />
|
|
<VStack align="start" spacing={0} flex={1}>
|
|
<Text fontWeight="medium" color="white">
|
|
{playlist.name}
|
|
</Text>
|
|
<Text fontSize="sm" color="gray.400">
|
|
{playlist.trackCount || playlist.tracks?.length || 0} tracks
|
|
</Text>
|
|
</VStack>
|
|
</HStack>
|
|
</Box>
|
|
))}
|
|
</VStack>
|
|
)}
|
|
</Box>
|
|
</VStack>
|
|
</ModalBody>
|
|
|
|
<ModalFooter>
|
|
<Button variant="ghost" onClick={handleClose} color="gray.400">
|
|
Cancel
|
|
</Button>
|
|
</ModalFooter>
|
|
</ModalContent>
|
|
</Modal>
|
|
);
|
|
};
|