Geert Rademakes 7e1f4e1cd4 UI fixes!
2025-04-25 09:21:38 +02:00

435 lines
12 KiB
TypeScript

import {
Box,
Button,
Flex,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
useDisclosure,
VStack,
Menu,
MenuButton,
MenuList,
MenuItem,
IconButton,
Collapse,
MenuDivider,
MenuGroup,
Icon
} from "@chakra-ui/react";
import { ChevronDownIcon, DeleteIcon, ChevronRightIcon, AddIcon, ViewIcon } from "@chakra-ui/icons";
import React, { useState, useCallback } from "react";
import { PlaylistNode } from "../types/interfaces";
interface PlaylistManagerProps {
playlists: PlaylistNode[];
selectedItem: string | null;
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) => ({
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
bg: isSelected ? "blue.800" : "transparent",
color: isSelected ? "white" : "gray.100",
fontWeight: isSelected ? "600" : "normal",
borderRadius: "md",
px: 4,
py: 2,
cursor: "pointer",
transition: "all 0.2s",
_hover: {
bg: isSelected ? "blue.600" : "whiteAlpha.200",
transform: "translateX(2px)",
},
_active: {
bg: isSelected ? "blue.700" : "whiteAlpha.300",
},
});
interface PlaylistItemProps {
node: PlaylistNode;
level: number;
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<PlaylistItemProps> = ({
node,
level,
selectedItem,
onPlaylistSelect,
onPlaylistDelete,
onPlaylistMove,
allFolders,
}) => {
const [isOpen, setIsOpen] = useState(true);
const indent = level * 10;
if (node.type === 'folder') {
return (
<Box>
<Flex align="center" gap={1}>
<Button
flex={1}
{...getButtonStyles(false)}
onClick={() => setIsOpen(!isOpen)}
ml={indent}
justifyContent="flex-start"
leftIcon={
<Box position="relative" display="flex" alignItems="center">
<ChevronRightIcon
position="absolute"
right="-6px"
transform={isOpen ? 'rotate(90deg)' : 'none'}
transition="transform 0.2s"
color="gray.400"
/>
</Box>
}
>
<Icon
viewBox="0 0 24 24"
color="blue.300"
ml={1}
mr={2}
>
<path
fill="currentColor"
d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10z"
/>
</Icon>
{node.name}
</Button>
</Flex>
<Collapse in={isOpen}>
<VStack spacing={1} mt={2} align="stretch">
{node.children?.map((child, index) => (
<PlaylistItem
key={child.id || index}
node={child}
level={level + 1}
selectedItem={selectedItem}
onPlaylistSelect={onPlaylistSelect}
onPlaylistDelete={onPlaylistDelete}
onPlaylistMove={onPlaylistMove}
allFolders={allFolders}
/>
))}
</VStack>
</Collapse>
</Box>
);
}
return (
<Flex align="center" gap={0}>
<Button
flex="1 1 auto"
{...getButtonStyles(selectedItem === node.name)}
onClick={() => onPlaylistSelect(node.name)}
ml={indent}
borderRightRadius={0}
borderRight="1px solid"
borderRightColor="whiteAlpha.200"
>
{node.name}
</Button>
<Menu>
<MenuButton
as={Button}
width="32px"
height="40px"
borderLeftRadius={0}
bg={selectedItem === node.name ? "blue.800" : "transparent"}
color={selectedItem === node.name ? "white" : "gray.100"}
fontWeight={selectedItem === node.name ? "600" : "normal"}
transition="all 0.2s"
_hover={{
bg: selectedItem === node.name ? "blue.600" : "whiteAlpha.200",
transform: "none"
}}
_active={{
bg: selectedItem === node.name ? "blue.700" : "whiteAlpha.300",
}}
aria-label="Playlist options"
p={0}
>
<ChevronDownIcon
color={selectedItem === node.name ? "white" : "gray.400"}
_hover={{ color: "white" }}
/>
</MenuButton>
<MenuList bg="gray.800" borderColor="gray.700">
<MenuGroup title="Move to folder">
<MenuItem
bg="gray.800"
_hover={{ bg: "gray.700" }}
onClick={() => onPlaylistMove(node.name, null)}
>
Root level
</MenuItem>
<MenuDivider />
{allFolders.map((folder) => (
<MenuItem
key={folder.name}
bg="gray.800"
_hover={{ bg: "gray.700" }}
onClick={() => onPlaylistMove(node.name, folder.name)}
>
{folder.name}
</MenuItem>
))}
</MenuGroup>
<MenuDivider />
<MenuItem
bg="gray.800"
color="red.300"
_hover={{ bg: "gray.700" }}
icon={<DeleteIcon />}
onClick={() => onPlaylistDelete(node.name)}
>
Delete Playlist
</MenuItem>
</MenuList>
</Menu>
</Flex>
);
};
export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
playlists,
selectedItem,
onPlaylistCreate,
onPlaylistSelect,
onPlaylistDelete,
onFolderCreate,
onPlaylistMove,
}) => {
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("");
onPlaylistModalClose();
}
};
const handleCreateFolder = () => {
if (newFolderName.trim()) {
onFolderCreate(newFolderName);
setNewFolderName("");
onFolderModalClose();
}
};
return (
<Box>
<VStack spacing={2} align="stretch" mb={4}>
<Button
{...getButtonStyles(selectedItem === null)}
onClick={() => onPlaylistSelect(null)}
>
All Songs
</Button>
{playlists.map((node, index) => (
<PlaylistItem
key={node.id || index}
node={node}
level={0}
selectedItem={selectedItem}
onPlaylistSelect={onPlaylistSelect}
onPlaylistDelete={onPlaylistDelete}
onPlaylistMove={onPlaylistMove}
allFolders={allFolders}
/>
))}
</VStack>
<Flex gap={2}>
<Button
onClick={onPlaylistModalOpen}
colorScheme="blue"
size="sm"
flex={1}
leftIcon={<AddIcon />}
borderRadius="md"
_hover={{
transform: "translateY(-1px)",
boxShadow: "sm",
}}
>
New Playlist
</Button>
<Button
onClick={onFolderModalOpen}
colorScheme="teal"
size="sm"
flex={1}
leftIcon={<ViewIcon />}
borderRadius="md"
_hover={{
transform: "translateY(-1px)",
boxShadow: "sm",
}}
>
New Folder
</Button>
</Flex>
{/* New Playlist Modal */}
<Modal
isOpen={isPlaylistModalOpen}
onClose={onPlaylistModalClose}
isCentered
motionPreset="slideInBottom"
>
<ModalOverlay bg="blackAlpha.700" backdropFilter="blur(5px)" />
<ModalContent
bg="gray.800"
borderRadius="xl"
boxShadow="xl"
border="1px"
borderColor="gray.700"
>
<ModalHeader color="white">Create New Playlist</ModalHeader>
<ModalCloseButton color="white" />
<ModalBody pb={6}>
<Input
value={newPlaylistName}
onChange={(e) => setNewPlaylistName(e.target.value)}
placeholder="Enter playlist name"
bg="gray.700"
border="none"
color="white"
_placeholder={{ color: "gray.400" }}
_focus={{
boxShadow: "0 0 0 1px blue.500",
borderColor: "blue.500",
}}
/>
</ModalBody>
<ModalFooter>
<Button
colorScheme="blue"
mr={3}
onClick={handleCreatePlaylist}
isDisabled={!newPlaylistName.trim()}
_hover={{
transform: "translateY(-1px)",
boxShadow: "sm",
}}
>
Create
</Button>
<Button
variant="ghost"
onClick={onPlaylistModalClose}
color="gray.300"
_hover={{
bg: "whiteAlpha.200",
}}
>
Cancel
</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* New Folder Modal */}
<Modal
isOpen={isFolderModalOpen}
onClose={onFolderModalClose}
isCentered
motionPreset="slideInBottom"
>
<ModalOverlay bg="blackAlpha.700" backdropFilter="blur(5px)" />
<ModalContent
bg="gray.800"
borderRadius="xl"
boxShadow="xl"
border="1px"
borderColor="gray.700"
>
<ModalHeader color="white">Create New Folder</ModalHeader>
<ModalCloseButton color="white" />
<ModalBody pb={6}>
<Input
value={newFolderName}
onChange={(e) => 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",
}}
/>
</ModalBody>
<ModalFooter>
<Button
colorScheme="teal"
mr={3}
onClick={handleCreateFolder}
isDisabled={!newFolderName.trim()}
_hover={{
transform: "translateY(-1px)",
boxShadow: "sm",
}}
>
Create
</Button>
<Button
variant="ghost"
onClick={onFolderModalClose}
color="gray.300"
_hover={{
bg: "whiteAlpha.200",
}}
>
Cancel
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Box>
);
};