Compare commits
No commits in common. "8daf0cd526da004b828c32d711283a3bfe4d124d" and "35da4f83ce94daee49797f6aad1c4ecf4471e668" have entirely different histories.
8daf0cd526
...
35da4f83ce
56
package-lock.json
generated
56
package-lock.json
generated
@ -4945,51 +4945,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
|
||||||
"version": "7.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.2.tgz",
|
|
||||||
"integrity": "sha512-9Rw8r199klMnlGZ8VAsV/I8WrIF6IyJ90JQUdboupx1cdkgYqwnrYjH+I/nY/7cA1X5zia4mDJqH36npP7sxGQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"cookie": "^1.0.1",
|
|
||||||
"set-cookie-parser": "^2.6.0",
|
|
||||||
"turbo-stream": "2.4.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=18",
|
|
||||||
"react-dom": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-router-dom": {
|
|
||||||
"version": "7.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.2.tgz",
|
|
||||||
"integrity": "sha512-yk1XW8Fj7gK7flpYBXF3yzd2NbX6P7Kxjvs2b5nu1M04rb5pg/Zc4fGdBNTeT4eDYL2bvzWNyKaIMJX/RKHTTg==",
|
|
||||||
"dependencies": {
|
|
||||||
"react-router": "7.5.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=18",
|
|
||||||
"react-dom": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-router/node_modules/cookie": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-style-singleton": {
|
"node_modules/react-style-singleton": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||||
@ -5254,11 +5209,6 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/set-cookie-parser": {
|
|
||||||
"version": "2.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
|
||||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
|
|
||||||
},
|
|
||||||
"node_modules/setprototypeof": {
|
"node_modules/setprototypeof": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
@ -5579,11 +5529,6 @@
|
|||||||
"fsevents": "~2.3.3"
|
"fsevents": "~2.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/turbo-stream": {
|
|
||||||
"version": "2.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
|
||||||
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="
|
|
||||||
},
|
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
@ -6042,7 +5987,6 @@
|
|||||||
"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"
|
||||||
|
|||||||
@ -23,7 +23,6 @@
|
|||||||
"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,6 +1,5 @@
|
|||||||
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, useEffect } from "react";
|
import { useState, useRef } 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";
|
||||||
@ -63,47 +62,14 @@ 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) => {
|
||||||
@ -116,18 +82,6 @@ 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" });
|
||||||
@ -140,10 +94,14 @@ export default function RekordboxReader() {
|
|||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayedSongs = currentPlaylist === "All Songs"
|
const handleSongSelect = (song: Song) => {
|
||||||
|
setSelectedSong(song);
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayedSongs = selectedItem === "All Songs"
|
||||||
? songs
|
? songs
|
||||||
: songs.filter((song) =>
|
: songs.filter((song) =>
|
||||||
playlists.find((p) => p.name === currentPlaylist)?.tracks.includes(song.id)
|
playlists.find((p) => p.name === selectedItem)?.tracks.includes(song.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -151,11 +109,6 @@ 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -177,20 +130,18 @@ export default function RekordboxReader() {
|
|||||||
<Box w="200px">
|
<Box w="200px">
|
||||||
<PlaylistManager
|
<PlaylistManager
|
||||||
playlists={playlists}
|
playlists={playlists}
|
||||||
selectedItem={currentPlaylist}
|
selectedItem={selectedItem}
|
||||||
onPlaylistCreate={handleCreatePlaylist}
|
onPlaylistCreate={handleCreatePlaylist}
|
||||||
onPlaylistSelect={handlePlaylistSelect}
|
onPlaylistSelect={setSelectedItem}
|
||||||
/>
|
/>
|
||||||
</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={setSelectedSong}
|
onSongSelect={handleSongSelect}
|
||||||
selectedSongId={selectedSong?.id || null}
|
selectedSongId={selectedSong?.id || null}
|
||||||
currentPlaylist={currentPlaylist}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<SongDetails song={selectedSong} />
|
<SongDetails song={selectedSong} />
|
||||||
|
|||||||
@ -1,55 +1,35 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Stack,
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalContent,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalOverlay,
|
|
||||||
Text,
|
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
VStack,
|
CloseButton,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import React, { useState } from "react";
|
import {
|
||||||
import { Playlist } from "../types/Playlist";
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalFooter,
|
||||||
|
ModalBody,
|
||||||
|
} from "@chakra-ui/modal";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Playlist } from "../types/interfaces";
|
||||||
|
|
||||||
interface PlaylistManagerProps {
|
interface PlaylistManagerProps {
|
||||||
playlists: Playlist[];
|
playlists: Playlist[];
|
||||||
selectedItem: string | null;
|
selectedItem: string | null;
|
||||||
onPlaylistCreate: (name: string) => void;
|
onPlaylistCreate: (name: string) => void;
|
||||||
onPlaylistSelect: (id: string | null) => void;
|
onPlaylistSelect: (name: string) => 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 { isOpen, onOpen, onClose } = useDisclosure();
|
const { open, onOpen, onClose } = useDisclosure();
|
||||||
const [newPlaylistName, setNewPlaylistName] = useState("");
|
const [newPlaylistName, setNewPlaylistName] = useState("");
|
||||||
|
|
||||||
const handleCreatePlaylist = () => {
|
const handleCreatePlaylist = () => {
|
||||||
@ -61,97 +41,88 @@ export const PlaylistManager: React.FC<PlaylistManagerProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<>
|
||||||
<VStack spacing={2} align="stretch" mb={4}>
|
<Stack gap={2}>
|
||||||
<Button
|
<Button
|
||||||
{...getButtonStyles(selectedItem === null)}
|
onClick={() => onPlaylistSelect("All Songs")}
|
||||||
onClick={() => onPlaylistSelect(null)}
|
colorScheme={selectedItem === "All Songs" ? "blue" : "gray"}
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
All Songs
|
All Songs
|
||||||
</Button>
|
</Button>
|
||||||
{playlists.map((playlist) => (
|
{playlists.map((playlist) => (
|
||||||
<Button
|
<Button
|
||||||
key={playlist._id}
|
key={playlist.name}
|
||||||
{...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>
|
||||||
))}
|
))}
|
||||||
</VStack>
|
<Button onClick={onOpen} colorScheme="green" size="sm">
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={onOpen}
|
|
||||||
colorScheme="blue"
|
|
||||||
size="sm"
|
|
||||||
width="100%"
|
|
||||||
borderRadius="md"
|
|
||||||
_hover={{
|
|
||||||
transform: "translateY(-1px)",
|
|
||||||
boxShadow: "sm",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create New Playlist
|
Create New Playlist
|
||||||
</Button>
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
isCentered
|
isCentered
|
||||||
motionPreset="slideInBottom"
|
motionPreset="slideInBottom"
|
||||||
>
|
>
|
||||||
<ModalOverlay bg="blackAlpha.700" backdropFilter="blur(5px)" />
|
<ModalOverlay
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||||
|
backdropFilter: 'blur(2px)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
bg="gray.800"
|
padding={"1rem"}
|
||||||
borderRadius="xl"
|
bg="gray.20"
|
||||||
boxShadow="xl"
|
bgColor={"rgba(0, 0, 0, 1)"}
|
||||||
border="1px"
|
maxW="500px"
|
||||||
borderColor="gray.700"
|
w="90%"
|
||||||
|
borderRadius={25}
|
||||||
|
boxShadow={100}
|
||||||
|
margin="0 auto"
|
||||||
|
marginTop="10%"
|
||||||
>
|
>
|
||||||
<ModalHeader color="white">Create New Playlist</ModalHeader>
|
<ModalHeader color="gray.800" fontSize="lg" fontWeight="bold" pt={6} px={6}>
|
||||||
<ModalCloseButton color="white" />
|
Create New Playlist
|
||||||
<ModalBody pb={6}>
|
</ModalHeader>
|
||||||
|
<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"
|
||||||
bg="gray.700"
|
autoFocus
|
||||||
border="none"
|
size="lg"
|
||||||
color="white"
|
_placeholder={{ color: 'gray.400' }}
|
||||||
_placeholder={{ color: "gray.400" }}
|
onKeyDown={(e) => {
|
||||||
_focus={{
|
if (e.key === 'Enter') {
|
||||||
boxShadow: "0 0 0 1px blue.500",
|
handleCreatePlaylist();
|
||||||
borderColor: "blue.500",
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
<ModalFooter px={6} py={4} borderTop="1px" borderColor="gray.100">
|
||||||
<ModalFooter>
|
<Button colorScheme="blue" mr={3} onClick={handleCreatePlaylist} size="md" px={8}>
|
||||||
<Button
|
|
||||||
colorScheme="blue"
|
|
||||||
mr={3}
|
|
||||||
onClick={handleCreatePlaylist}
|
|
||||||
isDisabled={!newPlaylistName.trim()}
|
|
||||||
_hover={{
|
|
||||||
transform: "translateY(-1px)",
|
|
||||||
boxShadow: "sm",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={onClose} size="md" variant="ghost" color="gray.600">
|
||||||
variant="ghost"
|
|
||||||
onClick={onClose}
|
|
||||||
color="gray.300"
|
|
||||||
_hover={{
|
|
||||||
bg: "whiteAlpha.200",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -5,16 +5,11 @@ 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, ChevronDownIcon } from "@chakra-ui/icons";
|
import { Search2Icon } 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";
|
||||||
@ -22,21 +17,17 @@ 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("");
|
||||||
@ -102,7 +93,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>
|
||||||
<ChakraCheckbox
|
<Checkbox
|
||||||
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}
|
||||||
@ -118,51 +109,26 @@ 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'}`}
|
||||||
</ChakraCheckbox>
|
</Checkbox>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{selectedSongs.size > 0 && (
|
{selectedSongs.size > 0 && (
|
||||||
<ChakraMenu>
|
<Menu>
|
||||||
<ChakraMenuButton
|
<MenuButton as={Button} colorScheme="blue" size="sm">
|
||||||
as={Button}
|
Add to Playlist
|
||||||
rightIcon={<ChevronDownIcon />}
|
</MenuButton>
|
||||||
size="sm"
|
<MenuList>
|
||||||
colorScheme="blue"
|
{playlists.map((playlist) => (
|
||||||
>
|
|
||||||
Actions
|
|
||||||
</ChakraMenuButton>
|
|
||||||
<ChakraMenuList>
|
|
||||||
<MenuGroup title="Add to Playlist">
|
|
||||||
{playlists
|
|
||||||
.filter(p => p.name !== currentPlaylist)
|
|
||||||
.map((playlist) => (
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={playlist.name}
|
key={playlist.name}
|
||||||
onClick={() => {
|
value={playlist.name}
|
||||||
handleBulkAddToPlaylist(playlist.name);
|
onClick={() => handleBulkAddToPlaylist(playlist.name)}
|
||||||
setSelectedSongs(new Set());
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{playlist.name}
|
{playlist.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</MenuGroup>
|
</MenuList>
|
||||||
{currentPlaylist !== "All Songs" && onRemoveFromPlaylist && (
|
</Menu>
|
||||||
<>
|
|
||||||
<MenuDivider />
|
|
||||||
<MenuItem
|
|
||||||
color="red.300"
|
|
||||||
onClick={() => {
|
|
||||||
onRemoveFromPlaylist(Array.from(selectedSongs));
|
|
||||||
setSelectedSongs(new Set());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove from {currentPlaylist}
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ChakraMenuList>
|
|
||||||
</ChakraMenu>
|
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
@ -183,7 +149,7 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
>
|
>
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<HStack>
|
<HStack>
|
||||||
<ChakraCheckbox
|
<Checkbox
|
||||||
isChecked={selectedSongs.has(song.id)}
|
isChecked={selectedSongs.has(song.id)}
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -206,8 +172,8 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{!selectedSongs.has(song.id) && (
|
{!selectedSongs.has(song.id) && (
|
||||||
<ChakraMenu>
|
<Menu>
|
||||||
<ChakraMenuButton
|
<MenuButton
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
aria-label="Add to playlist"
|
aria-label="Add to playlist"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -215,8 +181,8 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
•••
|
•••
|
||||||
</ChakraMenuButton>
|
</MenuButton>
|
||||||
<ChakraMenuList onClick={(e: MouseEvent) => e.stopPropagation()}>
|
<MenuList onClick={(e: MouseEvent) => e.stopPropagation()}>
|
||||||
{playlists.map((playlist) => (
|
{playlists.map((playlist) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={playlist.name}
|
key={playlist.name}
|
||||||
@ -226,8 +192,8 @@ export const SongList: React.FC<SongListProps> = ({
|
|||||||
{playlist.name}
|
{playlist.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</ChakraMenuList>
|
</MenuList>
|
||||||
</ChakraMenu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
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';
|
||||||
|
|
||||||
@ -103,10 +102,8 @@ const theme = extendTheme({
|
|||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<BrowserRouter>
|
|
||||||
<ChakraProvider theme={theme}>
|
<ChakraProvider theme={theme}>
|
||||||
<App />
|
<App />
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
</BrowserRouter>
|
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -33,19 +33,6 @@ 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`);
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
export interface Playlist {
|
|
||||||
_id: string;
|
|
||||||
name: string;
|
|
||||||
songs: string[];
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user