diff --git a/frontend/index.html b/frontend/index.html
index e4b78ea..e65d3fc 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2,9 +2,8 @@
-
- Vite + React + TS
+ Music Control
diff --git a/frontend/package.json b/frontend/package.json
index 8462518..e9b6929 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,8 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
+ "@tailwindcss/vite": "^4.0.6",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "tailwindcss": "^4.0.6"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg
deleted file mode 100644
index e7b8dfb..0000000
--- a/frontend/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/frontend/src/App.css b/frontend/src/App.css
deleted file mode 100644
index b9d355d..0000000
--- a/frontend/src/App.css
+++ /dev/null
@@ -1,42 +0,0 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
deleted file mode 100644
index 2ae45de..0000000
--- a/frontend/src/App.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
-import './App.css'
-
-function App() {
- const [count, setCount] = useState(0)
-
- return (
- <>
-
- MPV Queue
-
-
-
- Edit src/App.tsx and save to test HMR
-
-
-
- Click on the Vite and React logos to learn more
-
- >
- )
-}
-
-export default App
diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg
deleted file mode 100644
index 6c87de9..0000000
--- a/frontend/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/frontend/src/components/App.tsx b/frontend/src/components/App.tsx
new file mode 100644
index 0000000..7b4e595
--- /dev/null
+++ b/frontend/src/components/App.tsx
@@ -0,0 +1,132 @@
+import React, { HTMLAttributes, useState, useRef, useEffect } from 'react';
+import classNames from 'classnames';
+import { FaPlay, FaPause, FaStepForward, FaStepBackward } from 'react-icons/fa';
+
+interface NowPlayingProps extends HTMLAttributes {
+ songName: string;
+ fileName: string;
+ isPlaying: boolean;
+ onPlayPause: () => void;
+ onStepForward: () => void;
+ onStepBackward: () => void;
+}
+
+const NowPlaying: React.FC = (props) => {
+ return (
+
+
+
+
+
{props.songName}
+
{props.fileName}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const SongTable: React.FC = () => {
+ const songs = Array.from({ length: 20 }, (_, index) => `Song ${index + 1}`);
+
+ const SongRow: React.FC<{ song: string }> = ({ song }) => {
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
+ const buttonRef = useRef(null);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (buttonRef.current && !buttonRef.current.contains(event.target as Node)) {
+ setShowDeleteConfirm(false);
+ }
+ };
+
+ if (showDeleteConfirm) {
+ document.addEventListener('click', handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener('click', handleClickOutside);
+ };
+ }, [showDeleteConfirm]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ };
+
+ return (
+
+ {songs.map((song, index) => (
+
+ ))}
+
+ );
+};
+
+const App: React.FC = () => {
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [songName, setSongName] = useState('Song Name');
+ const [fileName, setFileName] = useState('Song Name.mp3');
+
+ return (
+
+
+ setIsPlaying(!isPlaying)}
+ onStepForward={() => {}}
+ onStepBackward={() => {}}
+ />
+
+
+
+ );
+};
+
+export default App;
\ No newline at end of file
diff --git a/frontend/src/components/index.tsx b/frontend/src/components/index.tsx
new file mode 100644
index 0000000..c3b6627
--- /dev/null
+++ b/frontend/src/components/index.tsx
@@ -0,0 +1 @@
+export { default as App } from './App';
\ No newline at end of file
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 6119ad9..f1d8c73 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1,68 +1 @@
-:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
+@import "tailwindcss";
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index bef5202..0ed30c1 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,7 +1,7 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
-import App from './App.tsx'
+import { App } from './components'
createRoot(document.getElementById('root')!).render(
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 9f1ae35..13e8cc5 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -1,10 +1,12 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
- plugins: [react()],
+ plugins: [react(), tailwindcss()],
server: {
+ // For development only: proxy /api to backend running on separate port.
proxy: {
'/api': {
target: 'http://localhost:3000',
diff --git a/package.json b/package.json
index 6f85787..04a5fd8 100644
--- a/package.json
+++ b/package.json
@@ -20,8 +20,10 @@
"typescript": "^5.7.3"
},
"dependencies": {
+ "classnames": "^2.5.1",
"express": "^4.21.2",
"express-ws": "^5.0.2",
+ "react-icons": "^5.4.0",
"ws": "^8.18.0"
}
}
diff --git a/src/MediaPlayer.ts b/src/MediaPlayer.ts
index 0d9ac75..5a3de57 100644
--- a/src/MediaPlayer.ts
+++ b/src/MediaPlayer.ts
@@ -34,7 +34,7 @@ export class MediaPlayer {
console.log(`Player process spawned, opening socket @ ${socketPath}`);
setTimeout(() => {
this.connectToSocket(socketPath);
- }, 200);
+ }, 500);
});
}