diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 20ffcf3..639b403 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -4,6 +4,8 @@ import mongoose from 'mongoose'; import dotenv from 'dotenv'; import { songsRouter } from './routes/songs.js'; import { playlistsRouter } from './routes/playlists.js'; +import { Song } from './models/Song.js'; +import { Playlist } from './models/Playlist.js'; 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 mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/rekordbox') .then(() => console.log('Connected to MongoDB')) diff --git a/packages/backend/src/models/Playlist.ts b/packages/backend/src/models/Playlist.ts index 5cbd820..d9661d7 100644 --- a/packages/backend/src/models/Playlist.ts +++ b/packages/backend/src/models/Playlist.ts @@ -1,22 +1,19 @@ import mongoose from 'mongoose'; -const playlistNodeSchema = new mongoose.Schema({ - name: { type: String, required: true }, - type: { type: String, enum: ['folder', 'playlist'], default: 'playlist' }, - tracks: [{ type: String, ref: 'Song', default: [] }], - children: [{ type: mongoose.Schema.Types.Mixed }], // This allows recursive structure +const playlistSchema = new mongoose.Schema({ + id: String, + name: String, + type: { + type: String, + enum: ['playlist', 'folder'], + required: true + }, + tracks: [String], + children: [{ + type: mongoose.Schema.Types.Mixed + }] }, { - _id: true, - id: true, - timestamps: true, - toJSON: { - transform: function(doc, ret) { - ret.id = ret._id.toString(); - delete ret._id; - delete ret.__v; - return ret; - } - } + timestamps: true }); -export const PlaylistNode = mongoose.model('PlaylistNode', playlistNodeSchema); \ No newline at end of file +export const Playlist = mongoose.model('Playlist', playlistSchema); \ No newline at end of file diff --git a/packages/backend/src/routes/playlists.ts b/packages/backend/src/routes/playlists.ts index a25b37c..3a01ac3 100644 --- a/packages/backend/src/routes/playlists.ts +++ b/packages/backend/src/routes/playlists.ts @@ -1,29 +1,28 @@ import express from 'express'; -import { PlaylistNode } from '../models/Playlist.js'; +import { Playlist } from '../models/Playlist.js'; const router = express.Router(); // Get all playlists router.get('/', async (req, res) => { try { - const playlists = await PlaylistNode.find(); + const playlists = await Playlist.find(); res.json(playlists); } 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) => { try { - const playlists = req.body; - // Delete all existing playlists first - await PlaylistNode.deleteMany({}); - // Insert new playlists - const result = await PlaylistNode.insertMany(playlists); - res.status(201).json(result); + await Playlist.deleteMany({}); // Clear existing playlists + const playlists = await Playlist.create(req.body); + res.json(playlists); } 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' }); } }); diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 2d02c4d..8243f1d 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -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 { useState, useRef, useEffect } from "react"; import { useNavigate, useLocation, Routes, Route } from "react-router-dom"; @@ -123,6 +123,7 @@ export default function RekordboxReader() { const initialLoadDone = useRef(false); const mobileFileInputRef = useRef(null); const { isOpen, onOpen, onClose } = useDisclosure(); + const { isOpen: isWelcomeOpen, onClose: onWelcomeClose } = useDisclosure({ defaultIsOpen: true }); const isMobile = useBreakpointValue({ base: true, md: false }); const [sidebarWidth, setSidebarWidth] = useState(400); @@ -408,6 +409,37 @@ export default function RekordboxReader() { padding={0} userSelect={isResizing ? 'none' : 'auto'} > + {/* Welcome Modal */} + {!loading && songs.length === 0 && ( + + + + Welcome to Rekordbox Reader + + + + It looks like your library is empty. To get started, you'll need to import your Rekordbox XML file. + + + Head over to the configuration page to learn how to export your library from Rekordbox and import it here. + + + + + + + + + )} + {/* Header */} { const [songs, setSongs] = useState([]); const [playlists, setPlaylists] = useState([]); const [loading, setLoading] = useState(true); + const navigate = useNavigate(); // Load data from the backend on mount useEffect(() => { @@ -44,7 +46,8 @@ export const useXmlParser = () => { api.savePlaylists(parsedPlaylists) ]); - // Refresh the page to ensure all data is reloaded + // Navigate to home and refresh the page + navigate('/'); window.location.reload(); } catch (err) { console.error("Error processing XML:", err); @@ -53,12 +56,19 @@ export const useXmlParser = () => { reader.readAsText(file); }; + const resetLibrary = async () => { + setSongs([]); + setPlaylists([]); + setLoading(false); + }; + return { songs, playlists, setSongs, setPlaylists, handleFileUpload, - loading + loading, + resetLibrary, }; }; \ No newline at end of file diff --git a/packages/frontend/src/pages/Configuration.tsx b/packages/frontend/src/pages/Configuration.tsx index 3df2d9c..b582b09 100644 --- a/packages/frontend/src/pages/Configuration.tsx +++ b/packages/frontend/src/pages/Configuration.tsx @@ -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 { exportToXml } from "../services/xmlService"; import { StyledFileInput } from "../components/StyledFileInput"; +import { api } from "../services/api"; export function Configuration() { - const { songs } = useXmlParser(); + const { songs, resetLibrary } = useXmlParser(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const toast = useToast(); const handleExport = () => { const xmlContent = exportToXml(songs, []); @@ -18,6 +21,29 @@ export function Configuration() { 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 ( @@ -25,11 +51,26 @@ export function Configuration() { Library Management - - Import your Rekordbox XML library or export your current library to XML format. - - + + + + To get started with Rekordbox Reader, you'll need to export your library from Rekordbox and import it here. + + + + + Open Rekordbox and go to File → Export Collection in xml format + + + Choose a location to save your XML file + + + Click the button below to import your Rekordbox XML file + + + + Import Library @@ -38,14 +79,71 @@ export function Configuration() { {songs.length > 0 && ( Export Library + + Export your current library back to XML format to import into Rekordbox. + )} + + + Reset Database + + Clear all songs and playlists from the database. This action cannot be undone. + + + + + {/* Reset Database Confirmation Modal */} + + + + Confirm Database Reset + + + + Are you sure you want to reset the database? This will: + + + Delete all imported songs + Remove all playlists + Clear all custom configurations + + + This action cannot be undone. + + + + + + + + + ); } \ No newline at end of file diff --git a/packages/frontend/src/services/api.ts b/packages/frontend/src/services/api.ts index 80ff1af..0a65157 100644 --- a/packages/frontend/src/services/api.ts +++ b/packages/frontend/src/services/api.ts @@ -38,6 +38,23 @@ class Api { if (!response.ok) throw new Error('Failed to save playlists'); return response.json(); } + + async resetDatabase(): Promise { + 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(); \ No newline at end of file