With amazing details screen on the sidebar now!
This commit is contained in:
parent
ff371aa855
commit
35da4f83ce
2003
package-lock.json
generated
2003
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,5 +14,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^8.2.2"
|
"concurrently": "^8.2.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@chakra-ui/icons": "^2.1.1",
|
||||||
|
"@chakra-ui/input": "^2.1.2",
|
||||||
|
"@chakra-ui/react": "^2.8.2",
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.0",
|
||||||
|
"framer-motion": "^12.9.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,6 +5,14 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>Vite + React + TS</title>
|
||||||
|
<script>
|
||||||
|
// Insert this script in your index.html right after the <body> tag.
|
||||||
|
// This will help to prevent a flash if dark mode is the default.
|
||||||
|
(function() {
|
||||||
|
document.documentElement.style.backgroundColor = '#171923';
|
||||||
|
document.body.style.backgroundColor = '#171923';
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -10,8 +10,12 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@chakra-ui/checkbox": "^2.3.2",
|
||||||
|
"@chakra-ui/icons": "^2.1.1",
|
||||||
|
"@chakra-ui/input": "^2.1.2",
|
||||||
|
"@chakra-ui/menu": "^2.2.1",
|
||||||
"@chakra-ui/modal": "^2.3.1",
|
"@chakra-ui/modal": "^2.3.1",
|
||||||
"@chakra-ui/react": "^3.12.0",
|
"@chakra-ui/react": "^2.8.2",
|
||||||
"@chakra-ui/transition": "^2.1.0",
|
"@chakra-ui/transition": "^2.1.0",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
|
|||||||
@ -1,15 +1,69 @@
|
|||||||
import { Box, Button, Flex, Heading, Input, Spinner, Text } from "@chakra-ui/react";
|
import { Box, Button, Flex, Heading, Input, Spinner, Text, useStyleConfig } from "@chakra-ui/react";
|
||||||
import { useState } from "react";
|
import { useState, useRef } from "react";
|
||||||
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 { useXmlParser } from "./hooks/useXmlParser";
|
import { useXmlParser } from "./hooks/useXmlParser";
|
||||||
import { exportToXml } from "./services/xmlService";
|
import { exportToXml } from "./services/xmlService";
|
||||||
import { api } from "./services/api";
|
import { api } from "./services/api";
|
||||||
|
import { Song } from "./types/interfaces";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
|
const StyledFileInput = () => {
|
||||||
|
const { handleFileUpload } = useXmlParser();
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
inputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
position="relative"
|
||||||
|
width="auto"
|
||||||
|
maxW="300px"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
accept=".xml"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
height="40px"
|
||||||
|
padding="8px 12px"
|
||||||
|
opacity="0"
|
||||||
|
position="absolute"
|
||||||
|
top="0"
|
||||||
|
left="0"
|
||||||
|
width="100%"
|
||||||
|
cursor="pointer"
|
||||||
|
ref={inputRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
width="100%"
|
||||||
|
height="40px"
|
||||||
|
bg="gray.700"
|
||||||
|
color="gray.300"
|
||||||
|
border="2px dashed"
|
||||||
|
borderColor="gray.600"
|
||||||
|
_hover={{
|
||||||
|
bg: "gray.600",
|
||||||
|
borderColor: "gray.500"
|
||||||
|
}}
|
||||||
|
_active={{
|
||||||
|
bg: "gray.500"
|
||||||
|
}}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
Choose XML File
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function RekordboxReader() {
|
export default function RekordboxReader() {
|
||||||
const { songs, playlists, setPlaylists, handleFileUpload, loading } = useXmlParser();
|
const { songs, playlists, setPlaylists, loading } = useXmlParser();
|
||||||
const [selectedItem, setSelectedItem] = useState<string | null>("All Songs");
|
const [selectedItem, setSelectedItem] = useState<string | null>("All Songs");
|
||||||
|
const [selectedSong, setSelectedSong] = useState<Song | null>(null);
|
||||||
|
|
||||||
const handleCreatePlaylist = async (name: string) => {
|
const handleCreatePlaylist = async (name: string) => {
|
||||||
const newPlaylist = { name, tracks: [] };
|
const newPlaylist = { name, tracks: [] };
|
||||||
@ -18,10 +72,10 @@ export default function RekordboxReader() {
|
|||||||
setPlaylists(savedPlaylists);
|
setPlaylists(savedPlaylists);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddSongToPlaylist = async (songId: string, playlistName: string) => {
|
const handleAddSongsToPlaylist = async (songIds: string[], playlistName: string) => {
|
||||||
const updatedPlaylists = playlists.map((playlist) =>
|
const updatedPlaylists = playlists.map((playlist) =>
|
||||||
playlist.name === playlistName
|
playlist.name === playlistName
|
||||||
? { ...playlist, tracks: [...playlist.tracks, songId] }
|
? { ...playlist, tracks: [...new Set([...playlist.tracks, ...songIds])] }
|
||||||
: playlist
|
: playlist
|
||||||
);
|
);
|
||||||
const savedPlaylists = await api.savePlaylists(updatedPlaylists);
|
const savedPlaylists = await api.savePlaylists(updatedPlaylists);
|
||||||
@ -40,6 +94,10 @@ export default function RekordboxReader() {
|
|||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSongSelect = (song: Song) => {
|
||||||
|
setSelectedSong(song);
|
||||||
|
};
|
||||||
|
|
||||||
const displayedSongs = selectedItem === "All Songs"
|
const displayedSongs = selectedItem === "All Songs"
|
||||||
? songs
|
? songs
|
||||||
: songs.filter((song) =>
|
: songs.filter((song) =>
|
||||||
@ -57,21 +115,17 @@ export default function RekordboxReader() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Flex direction="column" gap={2} mb={4}>
|
<Flex direction="column" gap={4} mb={6}>
|
||||||
<Heading size="md">Rekordbox Reader</Heading>
|
<Heading size="md" mb={2}>Rekordbox Reader</Heading>
|
||||||
<Input
|
<Flex gap={4} align="center">
|
||||||
type="file"
|
<StyledFileInput />
|
||||||
accept=".xml"
|
|
||||||
onChange={handleFileUpload}
|
|
||||||
size="sm"
|
|
||||||
width="auto"
|
|
||||||
/>
|
|
||||||
{songs.length > 0 && (
|
{songs.length > 0 && (
|
||||||
<Button onClick={handleExport} size="sm" colorScheme="blue" width="auto">
|
<Button onClick={handleExport} size="sm" width="auto">
|
||||||
Export XML
|
Export XML
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Flex>
|
||||||
<Flex gap={4} alignItems="start">
|
<Flex gap={4} alignItems="start">
|
||||||
<Box w="200px">
|
<Box w="200px">
|
||||||
<PlaylistManager
|
<PlaylistManager
|
||||||
@ -84,10 +138,13 @@ export default function RekordboxReader() {
|
|||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
<SongList
|
<SongList
|
||||||
songs={displayedSongs}
|
songs={displayedSongs}
|
||||||
onAddToPlaylist={handleAddSongToPlaylist}
|
onAddToPlaylist={handleAddSongsToPlaylist}
|
||||||
playlists={playlists}
|
playlists={playlists}
|
||||||
|
onSongSelect={handleSongSelect}
|
||||||
|
selectedSongId={selectedSong?.id || null}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
<SongDetails song={selectedSong} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
119
packages/frontend/src/components/SongDetails.tsx
Normal file
119
packages/frontend/src/components/SongDetails.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { Box, VStack, Text, Divider } from "@chakra-ui/react";
|
||||||
|
import { Song } from "../types/interfaces";
|
||||||
|
|
||||||
|
interface SongDetailsProps {
|
||||||
|
song: Song | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SongDetails: React.FC<SongDetailsProps> = ({ song }) => {
|
||||||
|
if (!song) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
w="300px"
|
||||||
|
position="sticky"
|
||||||
|
top={4}
|
||||||
|
h="calc(100vh - 2rem)"
|
||||||
|
bg="gray.800"
|
||||||
|
p={4}
|
||||||
|
borderRadius="md"
|
||||||
|
borderLeft="1px"
|
||||||
|
borderColor="gray.700"
|
||||||
|
>
|
||||||
|
<Text color="gray.500">Select a song to view details</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const details = [
|
||||||
|
{ label: "Title", value: song.title },
|
||||||
|
{ label: "Artist", value: song.artist },
|
||||||
|
{ label: "Album", value: song.album },
|
||||||
|
{ label: "Genre", value: song.genre },
|
||||||
|
{ label: "BPM", value: song.averageBpm },
|
||||||
|
{ label: "Key", value: song.tonality },
|
||||||
|
{ label: "Year", value: song.year },
|
||||||
|
{ label: "Label", value: song.label },
|
||||||
|
{ label: "Mix", value: song.mix },
|
||||||
|
{ label: "Rating", value: song.rating },
|
||||||
|
{ label: "Comments", value: song.comments },
|
||||||
|
].filter(detail => detail.value); // Only show fields that have values
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
w="300px"
|
||||||
|
position="sticky"
|
||||||
|
top={4}
|
||||||
|
h="calc(100vh - 2rem)"
|
||||||
|
bg="gray.800"
|
||||||
|
borderRadius="md"
|
||||||
|
borderLeft="1px"
|
||||||
|
borderColor="gray.700"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
<Box p={4} flex="1" overflowY="auto" css={{
|
||||||
|
'&::-webkit-scrollbar': {
|
||||||
|
width: '4px',
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-track': {
|
||||||
|
background: 'transparent',
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-thumb': {
|
||||||
|
background: 'var(--chakra-colors-gray-600)',
|
||||||
|
borderRadius: '2px',
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-thumb:hover': {
|
||||||
|
background: 'var(--chakra-colors-gray-500)',
|
||||||
|
},
|
||||||
|
}}>
|
||||||
|
<VStack align="stretch" spacing={4}>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="lg" fontWeight="bold" color="white">
|
||||||
|
{song.title}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="md" color="gray.400">
|
||||||
|
{song.artist}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Divider borderColor="gray.700" />
|
||||||
|
<VStack align="stretch" spacing={3}>
|
||||||
|
{details.map(({ label, value }) => (
|
||||||
|
<Box key={label}>
|
||||||
|
<Text fontSize="xs" color="gray.500" mb={1}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" color="gray.300">
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
{song.tempo && (
|
||||||
|
<>
|
||||||
|
<Divider borderColor="gray.700" />
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="xs" color="gray.500" mb={2}>
|
||||||
|
Tempo Details
|
||||||
|
</Text>
|
||||||
|
<VStack align="stretch" spacing={2}>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="xs" color="gray.500">BPM</Text>
|
||||||
|
<Text fontSize="sm" color="gray.300">{song.tempo.bpm}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="xs" color="gray.500">Beat</Text>
|
||||||
|
<Text fontSize="sm" color="gray.300">{song.tempo.battito}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="xs" color="gray.500">Time Signature</Text>
|
||||||
|
<Text fontSize="sm" color="gray.300">{song.tempo.metro}</Text>
|
||||||
|
</Box>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,43 +1,204 @@
|
|||||||
import { Box, Flex, Text } from "@chakra-ui/react";
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Text,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
HStack,
|
||||||
|
} 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 { Search2Icon } from "@chakra-ui/icons";
|
||||||
import { Song } from "../types/interfaces";
|
import { Song } from "../types/interfaces";
|
||||||
import { ChangeEvent } from "react";
|
import { useState, useCallback, useMemo, forwardRef } from "react";
|
||||||
|
import { ChangeEvent, MouseEvent } from "react";
|
||||||
|
|
||||||
interface SongListProps {
|
interface SongListProps {
|
||||||
songs: Song[];
|
songs: Song[];
|
||||||
onAddToPlaylist: (songId: string, playlistName: string) => void;
|
onAddToPlaylist: (songIds: string[], playlistName: string) => void;
|
||||||
playlists: { name: string }[];
|
playlists: { name: string }[];
|
||||||
|
onSongSelect: (song: Song) => void;
|
||||||
|
selectedSongId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SongList: React.FC<SongListProps> = ({ songs, onAddToPlaylist, playlists }) => {
|
export const SongList: React.FC<SongListProps> = ({
|
||||||
|
songs,
|
||||||
|
onAddToPlaylist,
|
||||||
|
playlists,
|
||||||
|
onSongSelect,
|
||||||
|
selectedSongId
|
||||||
|
}) => {
|
||||||
|
const [selectedSongs, setSelectedSongs] = useState<Set<string>>(new Set());
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
const filteredSongs = useMemo(() => {
|
||||||
|
if (!searchQuery) return songs;
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
return songs.filter(
|
||||||
|
song =>
|
||||||
|
song.title.toLowerCase().includes(query) ||
|
||||||
|
song.artist.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}, [songs, searchQuery]);
|
||||||
|
|
||||||
|
const toggleSelection = useCallback((songId: string) => {
|
||||||
|
setSelectedSongs(prev => {
|
||||||
|
const newSelection = new Set(prev);
|
||||||
|
if (newSelection.has(songId)) {
|
||||||
|
newSelection.delete(songId);
|
||||||
|
} else {
|
||||||
|
newSelection.add(songId);
|
||||||
|
}
|
||||||
|
return newSelection;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleSelectAll = useCallback(() => {
|
||||||
|
setSelectedSongs(prev =>
|
||||||
|
prev.size === songs.length ? new Set() : new Set(songs.map(s => s.id))
|
||||||
|
);
|
||||||
|
}, [songs]);
|
||||||
|
|
||||||
|
const handleBulkAddToPlaylist = (playlistName: string) => {
|
||||||
|
if (selectedSongs.size > 0) {
|
||||||
|
onAddToPlaylist(Array.from(selectedSongs), playlistName);
|
||||||
|
setSelectedSongs(new Set()); // Clear selection after action
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSongClick = (e: MouseEvent, song: Song) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onSongSelect(song);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* Search Bar */}
|
||||||
|
<InputGroup mb={4}>
|
||||||
|
<InputLeftElement pointerEvents="none">
|
||||||
|
<Search2Icon color="gray.300" />
|
||||||
|
</InputLeftElement>
|
||||||
|
<Input
|
||||||
|
placeholder="Search songs by title or artist..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) => setSearchQuery(e.target.value)}
|
||||||
|
bg="gray.800"
|
||||||
|
borderColor="gray.600"
|
||||||
|
_hover={{ borderColor: "gray.500" }}
|
||||||
|
_focus={{ borderColor: "blue.300", boxShadow: "0 0 0 1px var(--chakra-colors-blue-300)" }}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
{/* Bulk Actions Toolbar */}
|
||||||
|
<Flex justify="space-between" align="center" mb={4} p={2} bg="gray.800" borderRadius="md">
|
||||||
|
<HStack>
|
||||||
|
<Checkbox
|
||||||
|
isChecked={selectedSongs.size === filteredSongs.length}
|
||||||
|
isIndeterminate={selectedSongs.size > 0 && selectedSongs.size < filteredSongs.length}
|
||||||
|
onChange={toggleSelectAll}
|
||||||
|
colorScheme="blue"
|
||||||
|
sx={{
|
||||||
|
'& > span:first-of-type': {
|
||||||
|
opacity: 1,
|
||||||
|
border: '2px solid',
|
||||||
|
borderColor: 'gray.500'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedSongs.size === 0
|
||||||
|
? "Select All"
|
||||||
|
: `Selected ${selectedSongs.size} song${selectedSongs.size === 1 ? '' : 's'}`}
|
||||||
|
</Checkbox>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{selectedSongs.size > 0 && (
|
||||||
|
<Menu>
|
||||||
|
<MenuButton as={Button} colorScheme="blue" size="sm">
|
||||||
|
Add to Playlist
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList>
|
||||||
|
{playlists.map((playlist) => (
|
||||||
|
<MenuItem
|
||||||
|
key={playlist.name}
|
||||||
|
value={playlist.name}
|
||||||
|
onClick={() => handleBulkAddToPlaylist(playlist.name)}
|
||||||
|
>
|
||||||
|
{playlist.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* Song List */}
|
||||||
<Flex direction="column" gap={2}>
|
<Flex direction="column" gap={2}>
|
||||||
{songs.map((song) => (
|
{filteredSongs.map((song) => (
|
||||||
<Box key={song.id} borderWidth="1px" borderRadius="md" p={2} shadow="sm">
|
<Box
|
||||||
|
key={song.id}
|
||||||
|
borderWidth="1px"
|
||||||
|
borderRadius="md"
|
||||||
|
p={2}
|
||||||
|
shadow="sm"
|
||||||
|
bg={selectedSongId === song.id ? "blue.800" : selectedSongs.has(song.id) ? "gray.700" : "transparent"}
|
||||||
|
_hover={{ bg: selectedSongId === song.id ? "blue.700" : selectedSongs.has(song.id) ? "gray.700" : "gray.900" }}
|
||||||
|
transition="background-color 0.2s"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={(e: MouseEvent) => handleSongClick(e, song)}
|
||||||
|
>
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
|
<HStack>
|
||||||
|
<Checkbox
|
||||||
|
isChecked={selectedSongs.has(song.id)}
|
||||||
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleSelection(song.id);
|
||||||
|
}}
|
||||||
|
colorScheme="blue"
|
||||||
|
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||||
|
sx={{
|
||||||
|
'& > span:first-of-type': {
|
||||||
|
opacity: 1,
|
||||||
|
border: '2px solid',
|
||||||
|
borderColor: 'gray.500'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Box>
|
<Box>
|
||||||
<Text fontWeight="bold" fontSize="sm">{song.title}</Text>
|
<Text fontWeight="bold" fontSize="sm">{song.title}</Text>
|
||||||
<Text color="gray.500" fontSize="xs">{song.artist}</Text>
|
<Text color="gray.500" fontSize="xs">{song.artist}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<select
|
</HStack>
|
||||||
onChange={(e: ChangeEvent<HTMLSelectElement>) => onAddToPlaylist(song.id, e.target.value)}
|
|
||||||
value=""
|
{!selectedSongs.has(song.id) && (
|
||||||
style={{
|
<Menu>
|
||||||
padding: '0.25rem',
|
<MenuButton
|
||||||
fontSize: '0.875rem',
|
as={IconButton}
|
||||||
borderRadius: '0.375rem',
|
aria-label="Add to playlist"
|
||||||
border: '1px solid #E2E8F0'
|
size="sm"
|
||||||
}}
|
variant="ghost"
|
||||||
|
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<option value="" disabled>Add to playlist</option>
|
•••
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList onClick={(e: MouseEvent) => e.stopPropagation()}>
|
||||||
{playlists.map((playlist) => (
|
{playlists.map((playlist) => (
|
||||||
<option key={playlist.name} value={playlist.name}>
|
<MenuItem
|
||||||
|
key={playlist.name}
|
||||||
|
value={playlist.name}
|
||||||
|
onClick={() => onAddToPlaylist([song.id], playlist.name)}
|
||||||
|
>
|
||||||
{playlist.name}
|
{playlist.name}
|
||||||
</option>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</select>
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,12 +1,108 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { ChakraProvider, defaultSystem } from '@chakra-ui/react';
|
import { ChakraProvider, extendTheme } from '@chakra-ui/react';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
const theme = extendTheme({
|
||||||
|
config: {
|
||||||
|
initialColorMode: 'dark',
|
||||||
|
useSystemColorMode: false,
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
global: {
|
||||||
|
body: {
|
||||||
|
bg: 'gray.900',
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
gray: {
|
||||||
|
700: '#2D3748',
|
||||||
|
800: '#1A202C',
|
||||||
|
900: '#171923',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
defaultProps: {
|
||||||
|
colorScheme: 'gray',
|
||||||
|
variant: 'outline',
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
solid: {
|
||||||
|
bg: 'gray.700',
|
||||||
|
color: 'white',
|
||||||
|
_hover: {
|
||||||
|
bg: 'gray.600',
|
||||||
|
},
|
||||||
|
_active: {
|
||||||
|
bg: 'gray.500',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
borderColor: 'gray.600',
|
||||||
|
color: 'gray.300',
|
||||||
|
_hover: {
|
||||||
|
bg: 'gray.700',
|
||||||
|
},
|
||||||
|
_active: {
|
||||||
|
bg: 'gray.600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IconButton: {
|
||||||
|
defaultProps: {
|
||||||
|
colorScheme: 'gray',
|
||||||
|
variant: 'outline',
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
ghost: {
|
||||||
|
color: 'gray.400',
|
||||||
|
_hover: {
|
||||||
|
bg: 'gray.700',
|
||||||
|
},
|
||||||
|
_active: {
|
||||||
|
bg: 'gray.600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
borderColor: 'gray.600',
|
||||||
|
color: 'gray.300',
|
||||||
|
_hover: {
|
||||||
|
bg: 'gray.700',
|
||||||
|
},
|
||||||
|
_active: {
|
||||||
|
bg: 'gray.600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Menu: {
|
||||||
|
baseStyle: {
|
||||||
|
list: {
|
||||||
|
bg: 'gray.800',
|
||||||
|
borderColor: 'gray.600',
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
bg: 'gray.800',
|
||||||
|
_hover: {
|
||||||
|
bg: 'gray.700',
|
||||||
|
},
|
||||||
|
_focus: {
|
||||||
|
bg: 'gray.700',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ChakraProvider value={defaultSystem}>
|
<ChakraProvider theme={theme}>
|
||||||
<App />
|
<App />
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user