Better UI!

This commit is contained in:
Geert Rademakes 2025-04-24 23:11:07 +02:00
parent 7e08b0e567
commit 83fe6994b8
3 changed files with 265 additions and 123 deletions

View File

@ -1,7 +1,19 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #root {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#root { #root {
max-width: 1280px; max-width: 1280px;
margin: 0;
padding: 1rem;
} }
.logo { .logo {

View File

@ -1,4 +1,5 @@
import { Box, Button, Flex, Heading, Input, Spinner, Text, useStyleConfig } from "@chakra-ui/react"; import { Box, Button, Flex, Heading, Input, Spinner, Text, useStyleConfig, useBreakpointValue, IconButton, Drawer, DrawerBody, DrawerHeader, DrawerOverlay, DrawerContent, DrawerCloseButton, useDisclosure, Container, Menu, MenuButton, MenuList, MenuItem } from "@chakra-ui/react";
import { ChevronLeftIcon, ChevronRightIcon, HamburgerIcon, ViewIcon, SettingsIcon } from "@chakra-ui/icons";
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import { useNavigate, useLocation } from "react-router-dom"; import { useNavigate, useLocation } from "react-router-dom";
import { SongList } from "./components/SongList"; import { SongList } from "./components/SongList";
@ -10,7 +11,7 @@ import { api } from "./services/api";
import { Song } from "./types/interfaces"; import { Song } from "./types/interfaces";
import "./App.css"; import "./App.css";
const StyledFileInput = () => { const StyledFileInput = ({ isMobile = false }) => {
const { handleFileUpload } = useXmlParser(); const { handleFileUpload } = useXmlParser();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -22,7 +23,7 @@ const StyledFileInput = () => {
<Box <Box
position="relative" position="relative"
width="auto" width="auto"
maxW="300px" maxW={isMobile ? "100%" : "300px"}
> >
<Input <Input
type="file" type="file"
@ -54,6 +55,7 @@ const StyledFileInput = () => {
bg: "gray.500" bg: "gray.500"
}} }}
onClick={handleClick} onClick={handleClick}
size={isMobile ? "sm" : "md"}
> >
Choose XML File Choose XML File
</Button> </Button>
@ -67,6 +69,10 @@ export default function RekordboxReader() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const initialLoadDone = useRef(false); const initialLoadDone = useRef(false);
const mobileFileInputRef = useRef<HTMLInputElement>(null);
const { isOpen, onOpen, onClose } = useDisclosure();
const isMobile = useBreakpointValue({ base: true, md: false });
// Get the current playlist from URL or default to "All Songs" // Get the current playlist from URL or default to "All Songs"
const currentPlaylist = location.pathname === "/" const currentPlaylist = location.pathname === "/"
@ -169,41 +175,202 @@ export default function RekordboxReader() {
); );
} }
const playlistManager = (
<PlaylistManager
playlists={playlists}
selectedItem={currentPlaylist}
onPlaylistCreate={handleCreatePlaylist}
onPlaylistSelect={(name) => {
handlePlaylistSelect(name || "All Songs");
if (isMobile) onClose();
}}
onPlaylistDelete={handlePlaylistDelete}
/>
);
return ( return (
<Box> <Box
<Flex direction="column" gap={4} mb={6}> position="fixed"
<Heading size="md" mb={2}>Rekordbox Reader</Heading> top={0}
<Flex gap={4} align="center"> left={0}
<StyledFileInput /> right={0}
{songs.length > 0 && ( bottom={0}
<Button onClick={handleExport} size="sm" width="auto"> overflow="hidden"
Export XML margin={0}
</Button> padding={0}
>
<Flex direction="column" h="100%" w="100%">
{/* Header */}
<Flex
px={isMobile ? 2 : 4}
py={2}
bg="gray.800"
borderBottom="1px"
borderColor="gray.700"
align="center"
gap={2}
w="full"
>
{isMobile && (
<IconButton
aria-label="Open menu"
icon={<ChevronRightIcon />}
onClick={onOpen}
variant="solid"
colorScheme="blue"
size="md"
fontSize="20px"
/>
)}
<Heading size={isMobile ? "sm" : "md"}>Rekordbox Reader</Heading>
{/* Desktop Actions */}
{!isMobile && (
<Flex gap={2} align="center" ml="auto">
<StyledFileInput />
{songs.length > 0 && (
<Button onClick={handleExport} size="sm" width="auto">
Export XML
</Button>
)}
</Flex>
)}
{/* Mobile Actions */}
{isMobile && (
<Menu>
<MenuButton
as={IconButton}
icon={<SettingsIcon />}
variant="ghost"
ml="auto"
size="md"
color="gray.300"
_hover={{ color: "white", bg: "whiteAlpha.200" }}
/>
<MenuList bg="gray.800" borderColor="gray.700">
<MenuItem
bg="gray.800"
_hover={{ bg: "gray.700" }}
>
<StyledFileInput isMobile />
</MenuItem>
{songs.length > 0 && (
<MenuItem
bg="gray.800"
_hover={{ bg: "gray.700" }}
onClick={handleExport}
color="gray.100"
>
Export XML
</MenuItem>
)}
</MenuList>
</Menu>
)} )}
</Flex> </Flex>
</Flex>
<Flex gap={4} alignItems="start"> {/* Main Content */}
<Box w="200px"> <Flex flex={1} overflow="hidden" w="full">
<PlaylistManager {/* Sidebar - Desktop */}
playlists={playlists} {!isMobile && (
selectedItem={currentPlaylist} <Box
onPlaylistCreate={handleCreatePlaylist} w="300px"
onPlaylistSelect={handlePlaylistSelect} minW="300px"
onPlaylistDelete={handlePlaylistDelete} p={4}
/> borderRight="1px"
</Box> borderColor="gray.700"
<Box flex={1}> overflowY="auto"
<SongList bg="gray.900"
songs={displayedSongs} >
onAddToPlaylist={handleAddSongsToPlaylist} {playlistManager}
onRemoveFromPlaylist={handleRemoveFromPlaylist} </Box>
playlists={playlists} )}
onSongSelect={setSelectedSong}
selectedSongId={selectedSong?.id || null} {/* Sidebar - Mobile */}
currentPlaylist={currentPlaylist} <Drawer
/> isOpen={isOpen}
</Box> placement="left"
<SongDetails song={selectedSong} /> onClose={onClose}
size="full"
>
<DrawerOverlay />
<DrawerContent bg="gray.900" p={0}>
<DrawerHeader
borderBottomWidth="1px"
bg="gray.800"
display="flex"
alignItems="center"
px={2}
py={2}
>
<Text>Playlists</Text>
<IconButton
aria-label="Close menu"
icon={<ChevronLeftIcon />}
onClick={onClose}
variant="ghost"
ml="auto"
color="blue.400"
_hover={{ color: "blue.300", bg: "whiteAlpha.200" }}
/>
</DrawerHeader>
<DrawerBody p={2}>
{playlistManager}
</DrawerBody>
</DrawerContent>
</Drawer>
{/* Main Content Area */}
<Flex
flex={1}
gap={4}
p={isMobile ? 2 : 4}
overflowY="hidden"
w="full"
>
{/* Song List */}
<Box flex={1} overflowY="auto" w="full">
<SongList
songs={displayedSongs}
onAddToPlaylist={handleAddSongsToPlaylist}
onRemoveFromPlaylist={handleRemoveFromPlaylist}
playlists={playlists}
onSongSelect={setSelectedSong}
selectedSongId={selectedSong?.id || null}
currentPlaylist={currentPlaylist}
/>
</Box>
{/* Details Panel */}
{!isMobile && (
<Box
w="350px"
minW="350px"
p={4}
borderLeft="1px"
borderColor="gray.700"
overflowY="auto"
bg="gray.900"
sx={{
'&::-webkit-scrollbar': {
width: '8px',
borderRadius: '8px',
backgroundColor: 'gray.900',
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: 'gray.700',
borderRadius: '8px',
},
overflowY: 'auto',
overflowX: 'hidden',
}}
>
<SongDetails song={selectedSong} />
</Box>
)}
</Flex>
</Flex>
</Flex> </Flex>
</Box> </Box>
); );

View File

@ -8,17 +8,7 @@ interface SongDetailsProps {
export const SongDetails: React.FC<SongDetailsProps> = ({ song }) => { export const SongDetails: React.FC<SongDetailsProps> = ({ song }) => {
if (!song) { if (!song) {
return ( return (
<Box <Box h="full" p={4}>
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> <Text color="gray.500">Select a song to view details</Text>
</Box> </Box>
); );
@ -36,84 +26,57 @@ export const SongDetails: React.FC<SongDetailsProps> = ({ song }) => {
{ label: "Mix", value: song.mix }, { label: "Mix", value: song.mix },
{ label: "Rating", value: song.rating }, { label: "Rating", value: song.rating },
{ label: "Comments", value: song.comments }, { label: "Comments", value: song.comments },
].filter(detail => detail.value); // Only show fields that have values ].filter(detail => detail.value);
return ( return (
<Box <Box h="full">
w="300px" <VStack align="stretch" spacing={4} p={4}>
position="sticky" <Box>
top={4} <Text fontSize="lg" fontWeight="bold" color="white">
h="calc(100vh - 2rem)" {song.title}
bg="gray.800" </Text>
borderRadius="md" <Text fontSize="md" color="gray.400">
borderLeft="1px" {song.artist}
borderColor="gray.700" </Text>
display="flex" </Box>
flexDirection="column" <Divider borderColor="gray.700" />
> <VStack align="stretch" spacing={3}>
<Box p={4} flex="1" overflowY="auto" css={{ {details.map(({ label, value }) => (
'&::-webkit-scrollbar': { <Box key={label}>
width: '4px', <Text fontSize="xs" color="gray.500" mb={1}>
}, {label}
'&::-webkit-scrollbar-track': { </Text>
background: 'transparent', <Text fontSize="sm" color="gray.300">
}, {value}
'&::-webkit-scrollbar-thumb': { </Text>
background: 'var(--chakra-colors-gray-600)', </Box>
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> </VStack>
</Box> {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>
); );
}; };