Nice onboarding flow & reset database function

This commit is contained in:
Geert Rademakes 2025-04-25 10:42:45 +02:00
parent db4408b953
commit 40c75d479a
7 changed files with 209 additions and 37 deletions

View File

@ -4,6 +4,8 @@ import mongoose from 'mongoose';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import { songsRouter } from './routes/songs.js'; import { songsRouter } from './routes/songs.js';
import { playlistsRouter } from './routes/playlists.js'; import { playlistsRouter } from './routes/playlists.js';
import { Song } from './models/Song.js';
import { Playlist } from './models/Playlist.js';
dotenv.config(); dotenv.config();
@ -23,6 +25,23 @@ app.get('/api/health', (req, res) => {
}); });
}); });
// Reset endpoint
app.post('/api/reset', async (req, res) => {
try {
// Delete all documents from both collections
await Promise.all([
Song.deleteMany({}),
Playlist.deleteMany({})
]);
console.log('Database reset successful');
res.json({ message: 'Database reset successful' });
} catch (error) {
console.error('Error resetting database:', error);
res.status(500).json({ error: 'Failed to reset database' });
}
});
// Connect to MongoDB // Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/rekordbox') mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/rekordbox')
.then(() => console.log('Connected to MongoDB')) .then(() => console.log('Connected to MongoDB'))

View File

@ -1,22 +1,19 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
const playlistNodeSchema = new mongoose.Schema({ const playlistSchema = new mongoose.Schema({
name: { type: String, required: true }, id: String,
type: { type: String, enum: ['folder', 'playlist'], default: 'playlist' }, name: String,
tracks: [{ type: String, ref: 'Song', default: [] }], type: {
children: [{ type: mongoose.Schema.Types.Mixed }], // This allows recursive structure type: String,
enum: ['playlist', 'folder'],
required: true
},
tracks: [String],
children: [{
type: mongoose.Schema.Types.Mixed
}]
}, { }, {
_id: true, timestamps: true
id: true,
timestamps: true,
toJSON: {
transform: function(doc, ret) {
ret.id = ret._id.toString();
delete ret._id;
delete ret.__v;
return ret;
}
}
}); });
export const PlaylistNode = mongoose.model('PlaylistNode', playlistNodeSchema); export const Playlist = mongoose.model('Playlist', playlistSchema);

View File

@ -1,29 +1,28 @@
import express from 'express'; import express from 'express';
import { PlaylistNode } from '../models/Playlist.js'; import { Playlist } from '../models/Playlist.js';
const router = express.Router(); const router = express.Router();
// Get all playlists // Get all playlists
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
try { try {
const playlists = await PlaylistNode.find(); const playlists = await Playlist.find();
res.json(playlists); res.json(playlists);
} catch (error) { } catch (error) {
res.status(500).json({ message: 'Error fetching playlists', error }); console.error('Error fetching playlists:', error);
res.status(500).json({ error: 'Failed to fetch playlists' });
} }
}); });
// Create or update playlists // Save playlists in batch (replacing all existing ones)
router.post('/batch', async (req, res) => { router.post('/batch', async (req, res) => {
try { try {
const playlists = req.body; await Playlist.deleteMany({}); // Clear existing playlists
// Delete all existing playlists first const playlists = await Playlist.create(req.body);
await PlaylistNode.deleteMany({}); res.json(playlists);
// Insert new playlists
const result = await PlaylistNode.insertMany(playlists);
res.status(201).json(result);
} catch (error) { } catch (error) {
res.status(500).json({ message: 'Error creating playlists', error }); console.error('Error saving playlists:', error);
res.status(500).json({ error: 'Failed to save playlists' });
} }
}); });

View File

@ -1,4 +1,4 @@
import { Box, Button, Flex, Heading, Input, Spinner, Text, useStyleConfig, useBreakpointValue, IconButton, Drawer, DrawerBody, DrawerHeader, DrawerOverlay, DrawerContent, DrawerCloseButton, useDisclosure, Container, Menu, MenuButton, MenuList, MenuItem, useToken } 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, useToken, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, VStack } from "@chakra-ui/react";
import { ChevronLeftIcon, ChevronRightIcon, HamburgerIcon, ViewIcon, SettingsIcon, DragHandleIcon } from "@chakra-ui/icons"; import { ChevronLeftIcon, ChevronRightIcon, HamburgerIcon, ViewIcon, SettingsIcon, DragHandleIcon } from "@chakra-ui/icons";
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import { useNavigate, useLocation, Routes, Route } from "react-router-dom"; import { useNavigate, useLocation, Routes, Route } from "react-router-dom";
@ -123,6 +123,7 @@ export default function RekordboxReader() {
const initialLoadDone = useRef(false); const initialLoadDone = useRef(false);
const mobileFileInputRef = useRef<HTMLInputElement>(null); const mobileFileInputRef = useRef<HTMLInputElement>(null);
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen: isWelcomeOpen, onClose: onWelcomeClose } = useDisclosure({ defaultIsOpen: true });
const isMobile = useBreakpointValue({ base: true, md: false }); const isMobile = useBreakpointValue({ base: true, md: false });
const [sidebarWidth, setSidebarWidth] = useState(400); const [sidebarWidth, setSidebarWidth] = useState(400);
@ -408,6 +409,37 @@ export default function RekordboxReader() {
padding={0} padding={0}
userSelect={isResizing ? 'none' : 'auto'} userSelect={isResizing ? 'none' : 'auto'}
> >
{/* Welcome Modal */}
{!loading && songs.length === 0 && (
<Modal isOpen={isWelcomeOpen} onClose={onWelcomeClose} isCentered>
<ModalOverlay />
<ModalContent bg="gray.800" maxW="md">
<ModalHeader color="white">Welcome to Rekordbox Reader</ModalHeader>
<ModalBody>
<VStack spacing={4} align="stretch">
<Text color="gray.300">
It looks like your library is empty. To get started, you'll need to import your Rekordbox XML file.
</Text>
<Text color="gray.400">
Head over to the configuration page to learn how to export your library from Rekordbox and import it here.
</Text>
</VStack>
</ModalBody>
<ModalFooter>
<Button
colorScheme="blue"
onClick={() => {
onWelcomeClose();
navigate('/config');
}}
>
Go to Configuration
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)}
<Flex direction="column" h="100%" w="100%"> <Flex direction="column" h="100%" w="100%">
{/* Header */} {/* Header */}
<Flex <Flex

View File

@ -1,4 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import type { Song, PlaylistNode } from "../types/interfaces"; import type { Song, PlaylistNode } from "../types/interfaces";
import { parseXmlFile } from "../services/xmlService"; import { parseXmlFile } from "../services/xmlService";
import { api } from "../services/api"; import { api } from "../services/api";
@ -7,6 +8,7 @@ export const useXmlParser = () => {
const [songs, setSongs] = useState<Song[]>([]); const [songs, setSongs] = useState<Song[]>([]);
const [playlists, setPlaylists] = useState<PlaylistNode[]>([]); const [playlists, setPlaylists] = useState<PlaylistNode[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const navigate = useNavigate();
// Load data from the backend on mount // Load data from the backend on mount
useEffect(() => { useEffect(() => {
@ -44,7 +46,8 @@ export const useXmlParser = () => {
api.savePlaylists(parsedPlaylists) api.savePlaylists(parsedPlaylists)
]); ]);
// Refresh the page to ensure all data is reloaded // Navigate to home and refresh the page
navigate('/');
window.location.reload(); window.location.reload();
} catch (err) { } catch (err) {
console.error("Error processing XML:", err); console.error("Error processing XML:", err);
@ -53,12 +56,19 @@ export const useXmlParser = () => {
reader.readAsText(file); reader.readAsText(file);
}; };
const resetLibrary = async () => {
setSongs([]);
setPlaylists([]);
setLoading(false);
};
return { return {
songs, songs,
playlists, playlists,
setSongs, setSongs,
setPlaylists, setPlaylists,
handleFileUpload, handleFileUpload,
loading loading,
resetLibrary,
}; };
}; };

View File

@ -1,10 +1,13 @@
import { Box, Heading, VStack, Text, Button } from "@chakra-ui/react"; import { Box, Heading, VStack, Text, Button, OrderedList, ListItem, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, useToast } from "@chakra-ui/react";
import { useXmlParser } from "../hooks/useXmlParser"; import { useXmlParser } from "../hooks/useXmlParser";
import { exportToXml } from "../services/xmlService"; import { exportToXml } from "../services/xmlService";
import { StyledFileInput } from "../components/StyledFileInput"; import { StyledFileInput } from "../components/StyledFileInput";
import { api } from "../services/api";
export function Configuration() { export function Configuration() {
const { songs } = useXmlParser(); const { songs, resetLibrary } = useXmlParser();
const { isOpen, onOpen, onClose } = useDisclosure();
const toast = useToast();
const handleExport = () => { const handleExport = () => {
const xmlContent = exportToXml(songs, []); const xmlContent = exportToXml(songs, []);
@ -18,6 +21,29 @@ export function Configuration() {
document.body.removeChild(a); document.body.removeChild(a);
}; };
const handleResetDatabase = async () => {
try {
await api.resetDatabase();
await resetLibrary();
onClose();
toast({
title: "Database reset successful",
description: "Your library has been cleared.",
status: "success",
duration: 5000,
isClosable: true,
});
} catch (error) {
toast({
title: "Failed to reset database",
description: "An error occurred while resetting the database.",
status: "error",
duration: 5000,
isClosable: true,
});
}
};
return ( return (
<Box h="100%" bg="gray.900" p={8}> <Box h="100%" bg="gray.900" p={8}>
<VStack spacing={8} align="stretch" maxW="2xl" mx="auto"> <VStack spacing={8} align="stretch" maxW="2xl" mx="auto">
@ -25,11 +51,26 @@ export function Configuration() {
<Box bg="gray.800" p={6} borderRadius="lg" borderWidth="1px" borderColor="gray.700"> <Box bg="gray.800" p={6} borderRadius="lg" borderWidth="1px" borderColor="gray.700">
<Heading size="md" mb={4}>Library Management</Heading> <Heading size="md" mb={4}>Library Management</Heading>
<Text color="gray.400" mb={6}>
Import your Rekordbox XML library or export your current library to XML format. <VStack spacing={6} align="stretch">
<Box>
<Text color="gray.400" mb={4}>
To get started with Rekordbox Reader, you'll need to export your library from Rekordbox and import it here.
</Text> </Text>
<VStack spacing={4} align="stretch"> <OrderedList spacing={3} color="gray.400" mb={6}>
<ListItem>
Open Rekordbox and go to <Text as="span" color="gray.300" fontWeight="medium">File Export Collection in xml format</Text>
</ListItem>
<ListItem>
Choose a location to save your XML file
</ListItem>
<ListItem>
Click the button below to import your Rekordbox XML file
</ListItem>
</OrderedList>
</Box>
<Box> <Box>
<Text fontWeight="medium" mb={2}>Import Library</Text> <Text fontWeight="medium" mb={2}>Import Library</Text>
<StyledFileInput /> <StyledFileInput />
@ -38,14 +79,71 @@ export function Configuration() {
{songs.length > 0 && ( {songs.length > 0 && (
<Box> <Box>
<Text fontWeight="medium" mb={2}>Export Library</Text> <Text fontWeight="medium" mb={2}>Export Library</Text>
<Text color="gray.400" mb={4}>
Export your current library back to XML format to import into Rekordbox.
</Text>
<Button onClick={handleExport} width="full"> <Button onClick={handleExport} width="full">
Export XML Export XML
</Button> </Button>
</Box> </Box>
)} )}
<Box borderTopWidth="1px" borderColor="gray.700" pt={6} mt={4}>
<Text fontWeight="medium" mb={2}>Reset Database</Text>
<Text color="gray.400" mb={4}>
Clear all songs and playlists from the database. This action cannot be undone.
</Text>
<Button
onClick={onOpen}
width="full"
colorScheme="red"
variant="outline"
>
Reset Database
</Button>
</Box>
</VStack> </VStack>
</Box> </Box>
</VStack> </VStack>
{/* Reset Database Confirmation Modal */}
<Modal isOpen={isOpen} onClose={onClose} isCentered>
<ModalOverlay />
<ModalContent bg="gray.800">
<ModalHeader color="white">Confirm Database Reset</ModalHeader>
<ModalBody>
<VStack spacing={4} align="stretch">
<Text color="gray.300">
Are you sure you want to reset the database? This will:
</Text>
<OrderedList spacing={2} color="gray.400" pl={4}>
<ListItem>Delete all imported songs</ListItem>
<ListItem>Remove all playlists</ListItem>
<ListItem>Clear all custom configurations</ListItem>
</OrderedList>
<Text color="red.300" fontWeight="medium">
This action cannot be undone.
</Text>
</VStack>
</ModalBody>
<ModalFooter gap={3}>
<Button
variant="outline"
onClick={onClose}
color="gray.300"
_hover={{
bg: "whiteAlpha.200",
color: "white"
}}
>
Cancel
</Button>
<Button colorScheme="red" onClick={handleResetDatabase}>
Reset Database
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Box> </Box>
); );
} }

View File

@ -38,6 +38,23 @@ class Api {
if (!response.ok) throw new Error('Failed to save playlists'); if (!response.ok) throw new Error('Failed to save playlists');
return response.json(); return response.json();
} }
async resetDatabase(): Promise<boolean> {
try {
const response = await fetch(`${API_BASE_URL}/reset`, {
method: 'POST',
});
if (!response.ok) {
throw new Error('Failed to reset database');
}
return true;
} catch (error) {
console.error('Error resetting database:', error);
throw error;
}
}
} }
export const api = new Api(); export const api = new Api();