Nice onboarding flow & reset database function
This commit is contained in:
parent
db4408b953
commit
40c75d479a
@ -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'))
|
||||||
|
|||||||
@ -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);
|
||||||
@ -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' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -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.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<VStack spacing={4} align="stretch">
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -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();
|
||||||
Loading…
x
Reference in New Issue
Block a user