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 ( - <> -
- - Vite logo - - - React logo - -
-

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 ( +
+
+ +
+ +
+
+ {song} +
+
+ +
+ + + +
+
+ ); + }; + + 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); }); }