url based navigation
This commit is contained in:
parent
35da4f83ce
commit
ea2942edc1
@ -23,6 +23,7 @@
|
|||||||
"framer-motion": "^12.5.0",
|
"framer-motion": "^12.5.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.5.2",
|
||||||
"sax": "^1.4.1",
|
"sax": "^1.4.1",
|
||||||
"xml2js": "^0.6.2",
|
"xml2js": "^0.6.2",
|
||||||
"xmlbuilder": "^15.1.1"
|
"xmlbuilder": "^15.1.1"
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Box, Button, Flex, Heading, Input, Spinner, Text, useStyleConfig } from "@chakra-ui/react";
|
import { Box, Button, Flex, Heading, Input, Spinner, Text, useStyleConfig } from "@chakra-ui/react";
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
import { SongList } from "./components/SongList";
|
import { SongList } from "./components/SongList";
|
||||||
import { PlaylistManager } from "./components/PlaylistManager";
|
import { PlaylistManager } from "./components/PlaylistManager";
|
||||||
import { SongDetails } from "./components/SongDetails";
|
import { SongDetails } from "./components/SongDetails";
|
||||||
@ -62,14 +63,47 @@ const StyledFileInput = () => {
|
|||||||
|
|
||||||
export default function RekordboxReader() {
|
export default function RekordboxReader() {
|
||||||
const { songs, playlists, setPlaylists, loading } = useXmlParser();
|
const { songs, playlists, setPlaylists, loading } = useXmlParser();
|
||||||
const [selectedItem, setSelectedItem] = useState<string | null>("All Songs");
|
|
||||||
const [selectedSong, setSelectedSong] = useState<Song | null>(null);
|
const [selectedSong, setSelectedSong] = useState<Song | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const initialLoadDone = useRef(false);
|
||||||
|
|
||||||
|
// Get the current playlist from URL or default to "All Songs"
|
||||||
|
const currentPlaylist = location.pathname === "/"
|
||||||
|
? "All Songs"
|
||||||
|
: decodeURIComponent(location.pathname.slice(1));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Only run this check after the initial data load
|
||||||
|
if (!loading && playlists.length > 0) {
|
||||||
|
initialLoadDone.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've loaded the data and the playlist doesn't exist
|
||||||
|
if (initialLoadDone.current &&
|
||||||
|
currentPlaylist !== "All Songs" &&
|
||||||
|
!playlists.some(p => p.name === currentPlaylist)) {
|
||||||
|
navigate("/", { replace: true });
|
||||||
|
}
|
||||||
|
}, [currentPlaylist, playlists, navigate, loading]);
|
||||||
|
|
||||||
|
const handlePlaylistSelect = (name: string) => {
|
||||||
|
setSelectedSong(null); // Clear selected song when changing playlists
|
||||||
|
if (name === "All Songs") {
|
||||||
|
navigate("/");
|
||||||
|
} else {
|
||||||
|
// Use encodeURIComponent to properly handle spaces and special characters
|
||||||
|
const encodedName = encodeURIComponent(name);
|
||||||
|
navigate(`/${encodedName}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreatePlaylist = async (name: string) => {
|
const handleCreatePlaylist = async (name: string) => {
|
||||||
const newPlaylist = { name, tracks: [] };
|
const newPlaylist = { name, tracks: [] };
|
||||||
const updatedPlaylists = [...playlists, newPlaylist];
|
const updatedPlaylists = [...playlists, newPlaylist];
|
||||||
const savedPlaylists = await api.savePlaylists(updatedPlaylists);
|
const savedPlaylists = await api.savePlaylists(updatedPlaylists);
|
||||||
setPlaylists(savedPlaylists);
|
setPlaylists(savedPlaylists);
|
||||||
|
handlePlaylistSelect(name); // Navigate to the new playlist
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddSongsToPlaylist = async (songIds: string[], playlistName: string) => {
|
const handleAddSongsToPlaylist = async (songIds: string[], playlistName: string) => {
|
||||||
@ -82,6 +116,18 @@ export default function RekordboxReader() {
|
|||||||
setPlaylists(savedPlaylists);
|
setPlaylists(savedPlaylists);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromPlaylist = async (songIds: string[]) => {
|
||||||
|
if (currentPlaylist === "All Songs") return;
|
||||||
|
|
||||||
|
const updatedPlaylists = playlists.map((playlist) =>
|
||||||
|
playlist.name === currentPlaylist
|
||||||
|
? { ...playlist, tracks: playlist.tracks.filter(id => !songIds.includes(id)) }
|
||||||
|
: playlist
|
||||||
|
);
|
||||||
|
const savedPlaylists = await api.savePlaylists(updatedPlaylists);
|
||||||
|
setPlaylists(savedPlaylists);
|
||||||
|
};
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
const xmlContent = exportToXml(songs, playlists);
|
const xmlContent = exportToXml(songs, playlists);
|
||||||
const blob = new Blob([xmlContent], { type: "application/xml" });
|
const blob = new Blob([xmlContent], { type: "application/xml" });
|
||||||
@ -94,14 +140,10 @@ export default function RekordboxReader() {
|
|||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSongSelect = (song: Song) => {
|
const displayedSongs = currentPlaylist === "All Songs"
|
||||||
setSelectedSong(song);
|
|
||||||
};
|
|
||||||
|
|
||||||
const displayedSongs = selectedItem === "All Songs"
|
|
||||||
? songs
|
? songs
|
||||||
: songs.filter((song) =>
|
: songs.filter((song) =>
|
||||||
playlists.find((p) => p.name === selectedItem)?.tracks.includes(song.id)
|
playlists.find((p) => p.name === currentPlaylist)?.tracks.includes(song.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -109,6 +151,11 @@ export default function RekordboxReader() {
|
|||||||
<Flex height="100vh" align="center" justify="center" direction="column" gap={4}>
|
<Flex height="100vh" align="center" justify="center" direction="column" gap={4}>
|
||||||
<Spinner size="xl" />
|
<Spinner size="xl" />
|
||||||
<Text>Loading your library...</Text>
|
<Text>Loading your library...</Text>
|
||||||
|
{currentPlaylist !== "All Songs" && (
|
||||||
|
<Text fontSize="sm" color="gray.500">
|
||||||
|
Navigating to playlist: {currentPlaylist}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -130,18 +177,20 @@ export default function RekordboxReader() {
|
|||||||
<Box w="200px">
|
<Box w="200px">
|
||||||
<PlaylistManager
|
<PlaylistManager
|
||||||
playlists={playlists}
|
playlists={playlists}
|
||||||
selectedItem={selectedItem}
|
selectedItem={currentPlaylist}
|
||||||
onPlaylistCreate={handleCreatePlaylist}
|
onPlaylistCreate={handleCreatePlaylist}
|
||||||
onPlaylistSelect={setSelectedItem}
|
onPlaylistSelect={handlePlaylistSelect}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
<SongList
|
<SongList
|
||||||
songs={displayedSongs}
|
songs={displayedSongs}
|
||||||
onAddToPlaylist={handleAddSongsToPlaylist}
|
onAddToPlaylist={handleAddSongsToPlaylist}
|
||||||
|
onRemoveFromPlaylist={handleRemoveFromPlaylist}
|
||||||
playlists={playlists}
|
playlists={playlists}
|
||||||
onSongSelect={handleSongSelect}
|
onSongSelect={setSelectedSong}
|
||||||
selectedSongId={selectedSong?.id || null}
|
selectedSongId={selectedSong?.id || null}
|
||||||
|
currentPlaylist={currentPlaylist}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<SongDetails song={selectedSong} />
|
<SongDetails song={selectedSong} />
|
||||||
|
|||||||
@ -1,35 +1,55 @@
|
|||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Flex,
|
||||||
Input,
|
Input,
|
||||||
Stack,
|
|
||||||
useDisclosure,
|
|
||||||
CloseButton,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import {
|
|
||||||
Modal,
|
Modal,
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalFooter,
|
|
||||||
ModalBody,
|
ModalBody,
|
||||||
} from "@chakra-ui/modal";
|
ModalCloseButton,
|
||||||
import { useState } from "react";
|
ModalContent,
|
||||||
import { Playlist } from "../types/interfaces";
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Text,
|
||||||
|
useDisclosure,
|
||||||
|
VStack,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Playlist } from "../types/Playlist";
|
||||||
|
|
||||||
interface PlaylistManagerProps {
|
interface PlaylistManagerProps {
|
||||||
playlists: Playlist[];
|
playlists: Playlist[];
|
||||||
selectedItem: string | null;
|
selectedItem: string | null;
|
||||||
onPlaylistCreate: (name: string) => void;
|
onPlaylistCreate: (name: string) => void;
|
||||||
onPlaylistSelect: (name: string) => void;
|
onPlaylistSelect: (id: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getButtonStyles = (isSelected: boolean) => ({
|
||||||
|
width: "100%",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
bg: isSelected ? "blue.800" : "transparent",
|
||||||
|
color: isSelected ? "white" : "gray.100",
|
||||||
|
fontWeight: isSelected ? "600" : "normal",
|
||||||
|
borderRadius: "md",
|
||||||
|
px: 4,
|
||||||
|
py: 2,
|
||||||
|
transition: "all 0.2s",
|
||||||
|
_hover: {
|
||||||
|
bg: isSelected ? "blue.600" : "whiteAlpha.200",
|
||||||
|
transform: "translateX(2px)",
|
||||||
|
},
|
||||||
|
_active: {
|
||||||
|
bg: isSelected ? "blue.700" : "whiteAlpha.300",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
|
export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
|
||||||
playlists,
|
playlists,
|
||||||
selectedItem,
|
selectedItem,
|
||||||
onPlaylistCreate,
|
onPlaylistCreate,
|
||||||
onPlaylistSelect,
|
onPlaylistSelect,
|
||||||
}) => {
|
}) => {
|
||||||
const { open, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const [newPlaylistName, setNewPlaylistName] = useState("");
|
const [newPlaylistName, setNewPlaylistName] = useState("");
|
||||||
|
|
||||||
const handleCreatePlaylist = () => {
|
const handleCreatePlaylist = () => {
|
||||||
@ -41,88 +61,97 @@ export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box>
|
||||||
<Stack gap={2}>
|
<VStack spacing={2} align="stretch" mb={4}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onPlaylistSelect("All Songs")}
|
{...getButtonStyles(selectedItem === null)}
|
||||||
colorScheme={selectedItem === "All Songs" ? "blue" : "gray"}
|
onClick={() => onPlaylistSelect(null)}
|
||||||
size="sm"
|
|
||||||
>
|
>
|
||||||
All Songs
|
All Songs
|
||||||
</Button>
|
</Button>
|
||||||
{playlists.map((playlist) => (
|
{playlists.map((playlist) => (
|
||||||
<Button
|
<Button
|
||||||
key={playlist.name}
|
key={playlist._id}
|
||||||
|
{...getButtonStyles(selectedItem === playlist.name)}
|
||||||
onClick={() => onPlaylistSelect(playlist.name)}
|
onClick={() => onPlaylistSelect(playlist.name)}
|
||||||
colorScheme={selectedItem === playlist.name ? "blue" : "gray"}
|
|
||||||
size="sm"
|
|
||||||
>
|
>
|
||||||
{playlist.name}
|
{playlist.name}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
<Button onClick={onOpen} colorScheme="green" size="sm">
|
</VStack>
|
||||||
Create New Playlist
|
|
||||||
</Button>
|
<Button
|
||||||
</Stack>
|
onClick={onOpen}
|
||||||
|
colorScheme="blue"
|
||||||
|
size="sm"
|
||||||
|
width="100%"
|
||||||
|
borderRadius="md"
|
||||||
|
_hover={{
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
boxShadow: "sm",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create New Playlist
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={open}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
isCentered
|
isCentered
|
||||||
motionPreset="slideInBottom"
|
motionPreset="slideInBottom"
|
||||||
>
|
>
|
||||||
<ModalOverlay
|
<ModalOverlay bg="blackAlpha.700" backdropFilter="blur(5px)" />
|
||||||
style={{
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|
||||||
backdropFilter: 'blur(2px)'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
padding={"1rem"}
|
bg="gray.800"
|
||||||
bg="gray.20"
|
borderRadius="xl"
|
||||||
bgColor={"rgba(0, 0, 0, 1)"}
|
boxShadow="xl"
|
||||||
maxW="500px"
|
border="1px"
|
||||||
w="90%"
|
borderColor="gray.700"
|
||||||
borderRadius={25}
|
|
||||||
boxShadow={100}
|
|
||||||
margin="0 auto"
|
|
||||||
marginTop="10%"
|
|
||||||
>
|
>
|
||||||
<ModalHeader color="gray.800" fontSize="lg" fontWeight="bold" pt={6} px={6}>
|
<ModalHeader color="white">Create New Playlist</ModalHeader>
|
||||||
Create New Playlist
|
<ModalCloseButton color="white" />
|
||||||
</ModalHeader>
|
<ModalBody pb={6}>
|
||||||
<CloseButton
|
|
||||||
position="absolute"
|
|
||||||
right={'1rem'}
|
|
||||||
top={'1rem'}
|
|
||||||
onClick={onClose}
|
|
||||||
color="gray.600"
|
|
||||||
/>
|
|
||||||
<ModalBody px={6} py={8} paddingTop="2rem">
|
|
||||||
<Input
|
<Input
|
||||||
value={newPlaylistName}
|
value={newPlaylistName}
|
||||||
onChange={(e) => setNewPlaylistName(e.target.value)}
|
onChange={(e) => setNewPlaylistName(e.target.value)}
|
||||||
placeholder="Enter playlist name"
|
placeholder="Enter playlist name"
|
||||||
autoFocus
|
bg="gray.700"
|
||||||
size="lg"
|
border="none"
|
||||||
_placeholder={{ color: 'gray.400' }}
|
color="white"
|
||||||
onKeyDown={(e) => {
|
_placeholder={{ color: "gray.400" }}
|
||||||
if (e.key === 'Enter') {
|
_focus={{
|
||||||
handleCreatePlaylist();
|
boxShadow: "0 0 0 1px blue.500",
|
||||||
}
|
borderColor: "blue.500",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter px={6} py={4} borderTop="1px" borderColor="gray.100">
|
|
||||||
<Button colorScheme="blue" mr={3} onClick={handleCreatePlaylist} size="md" px={8}>
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
mr={3}
|
||||||
|
onClick={handleCreatePlaylist}
|
||||||
|
isDisabled={!newPlaylistName.trim()}
|
||||||
|
_hover={{
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
boxShadow: "sm",
|
||||||
|
}}
|
||||||
|
>
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onClose} size="md" variant="ghost" color="gray.600">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onClose}
|
||||||
|
color="gray.300"
|
||||||
|
_hover={{
|
||||||
|
bg: "whiteAlpha.200",
|
||||||
|
}}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -5,11 +5,16 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
IconButton,
|
IconButton,
|
||||||
HStack,
|
HStack,
|
||||||
|
Menu as ChakraMenu,
|
||||||
|
MenuButton as ChakraMenuButton,
|
||||||
|
MenuList as ChakraMenuList,
|
||||||
|
MenuItem,
|
||||||
|
MenuDivider,
|
||||||
|
MenuGroup,
|
||||||
|
Checkbox as ChakraCheckbox
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Menu, MenuButton, MenuList, MenuItem } from "@chakra-ui/menu";
|
|
||||||
import { Checkbox } from "@chakra-ui/checkbox";
|
|
||||||
import { Input, InputGroup, InputLeftElement } from "@chakra-ui/input";
|
import { Input, InputGroup, InputLeftElement } from "@chakra-ui/input";
|
||||||
import { Search2Icon } from "@chakra-ui/icons";
|
import { Search2Icon, ChevronDownIcon } from "@chakra-ui/icons";
|
||||||
import { Song } from "../types/interfaces";
|
import { Song } from "../types/interfaces";
|
||||||
import { useState, useCallback, useMemo, forwardRef } from "react";
|
import { useState, useCallback, useMemo, forwardRef } from "react";
|
||||||
import { ChangeEvent, MouseEvent } from "react";
|
import { ChangeEvent, MouseEvent } from "react";
|
||||||
@ -17,17 +22,21 @@ import { ChangeEvent, MouseEvent } from "react";
|
|||||||
interface SongListProps {
|
interface SongListProps {
|
||||||
songs: Song[];
|
songs: Song[];
|
||||||
onAddToPlaylist: (songIds: string[], playlistName: string) => void;
|
onAddToPlaylist: (songIds: string[], playlistName: string) => void;
|
||||||
|
onRemoveFromPlaylist?: (songIds: string[]) => void;
|
||||||
playlists: { name: string }[];
|
playlists: { name: string }[];
|
||||||
onSongSelect: (song: Song) => void;
|
onSongSelect: (song: Song) => void;
|
||||||
selectedSongId: string | null;
|
selectedSongId: string | null;
|
||||||
|
currentPlaylist: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SongList: React.FC<SongListProps> = ({
|
export const SongList: React.FC<SongListProps> = ({
|
||||||
songs,
|
songs,
|
||||||
onAddToPlaylist,
|
onAddToPlaylist,
|
||||||
|
onRemoveFromPlaylist,
|
||||||
playlists,
|
playlists,
|
||||||
onSongSelect,
|
onSongSelect,
|
||||||
selectedSongId
|
selectedSongId,
|
||||||
|
currentPlaylist
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedSongs, setSelectedSongs] = useState<Set<string>>(new Set());
|
const [selectedSongs, setSelectedSongs] = useState<Set<string>>(new Set());
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
@ -93,7 +102,7 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
{/* Bulk Actions Toolbar */}
|
{/* Bulk Actions Toolbar */}
|
||||||
<Flex justify="space-between" align="center" mb={4} p={2} bg="gray.800" borderRadius="md">
|
<Flex justify="space-between" align="center" mb={4} p={2} bg="gray.800" borderRadius="md">
|
||||||
<HStack>
|
<HStack>
|
||||||
<Checkbox
|
<ChakraCheckbox
|
||||||
isChecked={selectedSongs.size === filteredSongs.length}
|
isChecked={selectedSongs.size === filteredSongs.length}
|
||||||
isIndeterminate={selectedSongs.size > 0 && selectedSongs.size < filteredSongs.length}
|
isIndeterminate={selectedSongs.size > 0 && selectedSongs.size < filteredSongs.length}
|
||||||
onChange={toggleSelectAll}
|
onChange={toggleSelectAll}
|
||||||
@ -109,26 +118,51 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
{selectedSongs.size === 0
|
{selectedSongs.size === 0
|
||||||
? "Select All"
|
? "Select All"
|
||||||
: `Selected ${selectedSongs.size} song${selectedSongs.size === 1 ? '' : 's'}`}
|
: `Selected ${selectedSongs.size} song${selectedSongs.size === 1 ? '' : 's'}`}
|
||||||
</Checkbox>
|
</ChakraCheckbox>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{selectedSongs.size > 0 && (
|
{selectedSongs.size > 0 && (
|
||||||
<Menu>
|
<ChakraMenu>
|
||||||
<MenuButton as={Button} colorScheme="blue" size="sm">
|
<ChakraMenuButton
|
||||||
Add to Playlist
|
as={Button}
|
||||||
</MenuButton>
|
rightIcon={<ChevronDownIcon />}
|
||||||
<MenuList>
|
size="sm"
|
||||||
{playlists.map((playlist) => (
|
colorScheme="blue"
|
||||||
<MenuItem
|
>
|
||||||
key={playlist.name}
|
Actions
|
||||||
value={playlist.name}
|
</ChakraMenuButton>
|
||||||
onClick={() => handleBulkAddToPlaylist(playlist.name)}
|
<ChakraMenuList>
|
||||||
>
|
<MenuGroup title="Add to Playlist">
|
||||||
{playlist.name}
|
{playlists
|
||||||
</MenuItem>
|
.filter(p => p.name !== currentPlaylist)
|
||||||
))}
|
.map((playlist) => (
|
||||||
</MenuList>
|
<MenuItem
|
||||||
</Menu>
|
key={playlist.name}
|
||||||
|
onClick={() => {
|
||||||
|
handleBulkAddToPlaylist(playlist.name);
|
||||||
|
setSelectedSongs(new Set());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{playlist.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</MenuGroup>
|
||||||
|
{currentPlaylist !== "All Songs" && onRemoveFromPlaylist && (
|
||||||
|
<>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem
|
||||||
|
color="red.300"
|
||||||
|
onClick={() => {
|
||||||
|
onRemoveFromPlaylist(Array.from(selectedSongs));
|
||||||
|
setSelectedSongs(new Set());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove from {currentPlaylist}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ChakraMenuList>
|
||||||
|
</ChakraMenu>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
@ -149,7 +183,7 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
>
|
>
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<HStack>
|
<HStack>
|
||||||
<Checkbox
|
<ChakraCheckbox
|
||||||
isChecked={selectedSongs.has(song.id)}
|
isChecked={selectedSongs.has(song.id)}
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -172,8 +206,8 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{!selectedSongs.has(song.id) && (
|
{!selectedSongs.has(song.id) && (
|
||||||
<Menu>
|
<ChakraMenu>
|
||||||
<MenuButton
|
<ChakraMenuButton
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
aria-label="Add to playlist"
|
aria-label="Add to playlist"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -181,8 +215,8 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
•••
|
•••
|
||||||
</MenuButton>
|
</ChakraMenuButton>
|
||||||
<MenuList onClick={(e: MouseEvent) => e.stopPropagation()}>
|
<ChakraMenuList onClick={(e: MouseEvent) => e.stopPropagation()}>
|
||||||
{playlists.map((playlist) => (
|
{playlists.map((playlist) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={playlist.name}
|
key={playlist.name}
|
||||||
@ -192,8 +226,8 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
{playlist.name}
|
{playlist.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</MenuList>
|
</ChakraMenuList>
|
||||||
</Menu>
|
</ChakraMenu>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { ChakraProvider, extendTheme } from '@chakra-ui/react';
|
import { ChakraProvider, extendTheme } from '@chakra-ui/react';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
|
|
||||||
@ -102,8 +103,10 @@ const theme = extendTheme({
|
|||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ChakraProvider theme={theme}>
|
<BrowserRouter>
|
||||||
<App />
|
<ChakraProvider theme={theme}>
|
||||||
</ChakraProvider>
|
<App />
|
||||||
|
</ChakraProvider>
|
||||||
|
</BrowserRouter>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -33,6 +33,19 @@ export const api = {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async deleteSongs(songIds: string[]): Promise<void> {
|
||||||
|
console.log(`Deleting ${songIds.length} songs from API...`);
|
||||||
|
const response = await fetch(`${API_URL}/songs/batch`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ ids: songIds }),
|
||||||
|
});
|
||||||
|
await handleResponse<void>(response);
|
||||||
|
console.log(`Successfully deleted ${songIds.length} songs`);
|
||||||
|
},
|
||||||
|
|
||||||
async getPlaylists(): Promise<Playlist[]> {
|
async getPlaylists(): Promise<Playlist[]> {
|
||||||
console.log('Fetching playlists from API...');
|
console.log('Fetching playlists from API...');
|
||||||
const response = await fetch(`${API_URL}/playlists`);
|
const response = await fetch(`${API_URL}/playlists`);
|
||||||
|
|||||||
7
packages/frontend/src/types/Playlist.ts
Normal file
7
packages/frontend/src/types/Playlist.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface Playlist {
|
||||||
|
_id: string;
|
||||||
|
name: string;
|
||||||
|
songs: string[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user