rekordbox-viewer/packages/frontend/src/components/PlaylistSelectionModal.tsx

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>
);
};