diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 25578fe..8646dd0 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -251,6 +251,74 @@ export default function RekordboxReader() { } }; + const handleCreateFolder = async (name: string) => { + const newFolder: PlaylistNode = { + id: uuidv4(), + name, + type: 'folder', + children: [], + }; + const updatedPlaylists = [...playlists, newFolder]; + const savedPlaylists = await api.savePlaylists(updatedPlaylists); + setPlaylists(savedPlaylists); + }; + + const handleMovePlaylist = async (playlistName: string, targetFolderName: string | null) => { + let updatedPlaylists = [...playlists]; + let playlistToMove: PlaylistNode | null = null; + + // Helper function to remove playlist from its current location + const removePlaylist = (nodes: PlaylistNode[]): PlaylistNode[] => { + return nodes.reduce((acc: PlaylistNode[], node) => { + if (node.name === playlistName) { + playlistToMove = node; + return acc; + } + if (node.type === 'folder' && node.children) { + return [...acc, { + ...node, + children: removePlaylist(node.children) + }]; + } + return [...acc, node]; + }, []); + }; + + // First, remove the playlist from its current location + updatedPlaylists = removePlaylist(updatedPlaylists); + + if (!playlistToMove) return; // Playlist not found + + if (targetFolderName === null) { + // Move to root level + updatedPlaylists.push(playlistToMove); + } else { + // Move to target folder + const addToFolder = (nodes: PlaylistNode[]): PlaylistNode[] => { + return nodes.map(node => { + if (node.name === targetFolderName && node.type === 'folder') { + return { + ...node, + children: [...(node.children || []), playlistToMove!] + }; + } + if (node.type === 'folder' && node.children) { + return { + ...node, + children: addToFolder(node.children) + }; + } + return node; + }); + }; + + updatedPlaylists = addToFolder(updatedPlaylists); + } + + const savedPlaylists = await api.savePlaylists(updatedPlaylists); + setPlaylists(savedPlaylists); + }; + const displayedSongs = currentPlaylist === "All Songs" ? songs : songs.filter((song: Song) => { @@ -322,6 +390,8 @@ export default function RekordboxReader() { if (isMobile) onClose(); }} onPlaylistDelete={handlePlaylistDelete} + onFolderCreate={handleCreateFolder} + onPlaylistMove={handleMovePlaylist} /> ); diff --git a/packages/frontend/src/components/PlaylistManager.tsx b/packages/frontend/src/components/PlaylistManager.tsx index 3578c50..9670229 100644 --- a/packages/frontend/src/components/PlaylistManager.tsx +++ b/packages/frontend/src/components/PlaylistManager.tsx @@ -19,9 +19,12 @@ import { MenuItem, IconButton, Collapse, + MenuDivider, + MenuGroup, + useToast } from "@chakra-ui/react"; -import { ChevronDownIcon, DeleteIcon, ChevronRightIcon } from "@chakra-ui/icons"; -import React, { useState } from "react"; +import { ChevronDownIcon, DeleteIcon, ChevronRightIcon, AddIcon, ViewIcon } from "@chakra-ui/icons"; +import React, { useState, useCallback } from "react"; import { PlaylistNode } from "../types/interfaces"; interface PlaylistManagerProps { @@ -30,6 +33,8 @@ interface PlaylistManagerProps { onPlaylistCreate: (name: string) => void; onPlaylistSelect: (name: string | null) => void; onPlaylistDelete: (name: string) => void; + onFolderCreate: (name: string) => void; + onPlaylistMove: (playlistName: string, targetFolderName: string | null) => void; } const getButtonStyles = (isSelected: boolean) => ({ @@ -57,6 +62,8 @@ interface PlaylistItemProps { selectedItem: string | null; onPlaylistSelect: (name: string | null) => void; onPlaylistDelete: (name: string) => void; + onPlaylistMove: (playlistName: string, targetFolderName: string | null) => void; + allFolders: { name: string }[]; } const PlaylistItem: React.FC = ({ @@ -65,9 +72,11 @@ const PlaylistItem: React.FC = ({ selectedItem, onPlaylistSelect, onPlaylistDelete, + onPlaylistMove, + allFolders, }) => { const [isOpen, setIsOpen] = useState(true); - const indent = level * 16; // 16px indent per level + const indent = level * 20; // Increased indent per level if (node.type === 'folder') { return ( @@ -98,6 +107,8 @@ const PlaylistItem: React.FC = ({ selectedItem={selectedItem} onPlaylistSelect={onPlaylistSelect} onPlaylistDelete={onPlaylistDelete} + onPlaylistMove={onPlaylistMove} + allFolders={allFolders} /> ))} @@ -127,6 +138,27 @@ const PlaylistItem: React.FC = ({ _hover={{ color: "white", bg: "whiteAlpha.200" }} /> + + onPlaylistMove(node.name, null)} + > + Root level + + + {allFolders.map((folder) => ( + onPlaylistMove(node.name, folder.name)} + > + {folder.name} + + ))} + + = ({ onPlaylistCreate, onPlaylistSelect, onPlaylistDelete, + onFolderCreate, + onPlaylistMove, }) => { - const { isOpen, onOpen, onClose } = useDisclosure(); + const { isOpen: isPlaylistModalOpen, onOpen: onPlaylistModalOpen, onClose: onPlaylistModalClose } = useDisclosure(); + const { isOpen: isFolderModalOpen, onOpen: onFolderModalOpen, onClose: onFolderModalClose } = useDisclosure(); const [newPlaylistName, setNewPlaylistName] = useState(""); + const [newFolderName, setNewFolderName] = useState(""); + + // Helper function to get all folders from the playlist tree + const getAllFolders = useCallback((nodes: PlaylistNode[]): { name: string }[] => { + let result: { name: string }[] = []; + for (const node of nodes) { + if (node.type === 'folder') { + result.push({ name: node.name }); + if (node.children) { + result = result.concat(getAllFolders(node.children)); + } + } + } + return result; + }, []); + + const allFolders = getAllFolders(playlists); const handleCreatePlaylist = () => { if (newPlaylistName.trim()) { onPlaylistCreate(newPlaylistName); setNewPlaylistName(""); - onClose(); + onPlaylistModalClose(); + } + }; + + const handleCreateFolder = () => { + if (newFolderName.trim()) { + onFolderCreate(newFolderName); + setNewFolderName(""); + onFolderModalClose(); } }; @@ -177,27 +237,47 @@ export const PlaylistManager: React.FC = ({ selectedItem={selectedItem} onPlaylistSelect={onPlaylistSelect} onPlaylistDelete={onPlaylistDelete} + onPlaylistMove={onPlaylistMove} + allFolders={allFolders} /> ))} - + + + + + {/* New Playlist Modal */} @@ -242,7 +322,67 @@ export const PlaylistManager: React.FC = ({ + + + + + {/* New Folder Modal */} + + + + Create New Folder + + + setNewFolderName(e.target.value)} + placeholder="Enter folder name" + bg="gray.700" + border="none" + color="white" + _placeholder={{ color: "gray.400" }} + _focus={{ + boxShadow: "0 0 0 1px teal.500", + borderColor: "teal.500", + }} + /> + + + + +