├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── App.css ├── reportWebVitals.js ├── index.js ├── components │ ├── Credit.js │ ├── Song.js │ ├── Nav.js │ ├── Library.js │ ├── LibrarySong.js │ └── Player.js ├── data.js └── App.js ├── .gitignore ├── package.json ├── README.md ├── .eslintcache └── LICENSE /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WilsonLe/react-music-player/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WilsonLe/react-music-player/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WilsonLe/react-music-player/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | } 6 | body { 7 | font-family: "Lato", sans-serif; 8 | } 9 | 10 | h1, 11 | h2, 12 | h3 { 13 | color: rgb(54, 54, 54); 14 | } 15 | 16 | h3, 17 | h4 { 18 | font-weight: 400; 19 | color: rgb(100, 100, 100); 20 | } 21 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import reportWebVitals from "./reportWebVitals"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById("root") 11 | ); 12 | 13 | // If you want to start measuring performance in your app, pass a function 14 | // to log results (for example: reportWebVitals(console.log)) 15 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 16 | reportWebVitals(); 17 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Credit.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | const Credit = () => { 4 | return ( 5 | 6 | 7 | Made by Wilson 8 | 9 |

10 | 11 | Github repository 12 | 13 |
14 | ); 15 | }; 16 | 17 | const CreditContainer = styled.div` 18 | user-select: none; 19 | position: fixed; 20 | display: flex; 21 | flex-direction: column; 22 | align-items: flex-end; 23 | justify-content: flex-end; 24 | z-index: 12; 25 | bottom: 5px; 26 | right: 5px; 27 | color: rgb(155, 155, 155); 28 | font-size: 0.75rem; 29 | `; 30 | 31 | const Link = styled.a` 32 | color: rgb(155, 155, 155); 33 | `; 34 | export default Credit; 35 | -------------------------------------------------------------------------------- /src/components/Song.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Song = ({ currentSong }) => { 5 | return ( 6 | 7 | {currentSong.name} 8 |

{currentSong.name}

9 |

{currentSong.artist}

10 |
11 | ); 12 | }; 13 | 14 | const SongContainer = styled.div` 15 | margin-top: 10vh; 16 | min-height: 50vh; 17 | max-height: 60vh; 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | justify-content: center; 22 | `; 23 | 24 | const Img = styled.img` 25 | width: 20%; 26 | border-radius: 50%; 27 | @media screen and (max-width: 768px) { 28 | width: 50%; 29 | } 30 | `; 31 | 32 | const H1 = styled.h2` 33 | padding: 3rem 1rem 1rem 1rem; 34 | `; 35 | 36 | const H2 = styled.h3` 37 | font-size: 1rem; 38 | `; 39 | 40 | export default Song; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "music-player", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^1.2.32", 7 | "@fortawesome/free-solid-svg-icons": "^5.15.1", 8 | "@fortawesome/react-fontawesome": "^0.1.13", 9 | "@testing-library/jest-dom": "^5.11.4", 10 | "@testing-library/react": "^11.1.0", 11 | "@testing-library/user-event": "^12.1.10", 12 | "react": "^17.0.1", 13 | "react-dom": "^17.0.1", 14 | "react-scripts": "4.0.1", 15 | "styled-components": "^5.2.1", 16 | "uuid": "^8.3.1", 17 | "web-vitals": "^0.2.4" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A clean, minimalistic react music player web application. 2 | 3 | This project was made by following Dev Ed's react course, with some of my personal tweaks. In the course, he used scss modules to style, while I used styled components. I choose to use styled components because of the advantage of being able to style based on props, which is personally, is much more convenient than add class and scss modules. 4 | 5 | ![alt text](https://i.ibb.co/VtT4JPc/image.png "Vibes music web application") 6 | ![alt text](https://i.ibb.co/CtSvzvd/image.png "Vibes music web application with libraries") 7 | 8 | ## Test it out yourself! 9 | 10 | https://wilson-react-music-player.vercel.app/ 11 | 12 | Clone the repository and start testing out the application yourself! 13 | 14 | In the project directory, you can run: 15 | 16 | ``` 17 | // 1. If you have not installed Yarn: 18 | npm install yarn 19 | 20 | // 2. Install the dependencies: 21 | yarn install 22 | 23 | // 3. Kick start the project in development mode: 24 | yarn start 25 | ``` 26 | 27 | If you're ready to deploy the application to the internet, use: 28 | ``` 29 | yarn build 30 | ``` 31 | This will build the app for production to the "build" folder. The Built version will be optimized for best performance. 32 | 33 | -------------------------------------------------------------------------------- /src/components/Nav.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { faMusic } from "@fortawesome/free-solid-svg-icons"; 5 | 6 | const Nav = ({ libraryStatus, setLibraryStatus }) => { 7 | return ( 8 | 9 |

Vibes

10 | 14 |
15 | ); 16 | }; 17 | 18 | const NavContainer = styled.div` 19 | min-height: 10vh; 20 | display: flex; 21 | justify-content: space-around; 22 | align-items: center; 23 | @media screen and (max-width: 768px) { 24 | position: fixed; 25 | z-index: 10; 26 | top: 0; 27 | left: 0; 28 | width: 100%; 29 | } 30 | `; 31 | 32 | const H1 = styled.h1` 33 | transition: all 0.5s ease; 34 | 35 | @media screen and (max-width: 768px) { 36 | visibility: ${(p) => (p.libraryStatus ? "hidden" : "visible")}; 37 | opacity: ${(p) => (p.libraryStatus ? "0" : "100")}; 38 | transition: all 0.5s ease; 39 | } 40 | `; 41 | 42 | const Button = styled.button` 43 | background: transparent; 44 | border: none; 45 | cursor: pointer; 46 | border: 2px solid rgb(65, 65, 65); 47 | padding: 0.5rem; 48 | transition: all 0.3s ease; 49 | &:hover { 50 | background: rgb(65, 65, 65); 51 | color: white; 52 | } 53 | `; 54 | 55 | export default Nav; 56 | -------------------------------------------------------------------------------- /src/components/Library.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import LibrarySong from "./LibrarySong"; 3 | import styled from "styled-components"; 4 | 5 | const Library = ({ songs, currentSong, setCurrentSong, audioRef, isPlaying, setSongs, libraryStatus }) => { 6 | return ( 7 | 8 |

Library

9 | 10 | {songs.map((song) => ( 11 | 20 | ))} 21 | 22 |
23 | ); 24 | }; 25 | const LibraryContainer = styled.div` 26 | position: fixed; 27 | z-index: 9; 28 | top: 0; 29 | left: 0; 30 | width: 20rem; 31 | height: 100%; 32 | background-color: white; 33 | box-shadow: 2px 2px 50px rgb(204, 204, 204); 34 | user-select: none; 35 | overflow: scroll; 36 | transform: translateX(${(p) => (p.libraryStatus ? "0%" : "-100%")}); 37 | transition: all 0.5s ease; 38 | opacity: ${(p) => (p.libraryStatus ? "100" : "0")}; 39 | scrollbar-width: thin; 40 | scrollbar-color: rgba(155, 155, 155, 0.5) tranparent; 41 | &::-webkit-scrollbar { 42 | width: 5px; 43 | } 44 | &::-webkit-scrollbar-track { 45 | background: transparent; 46 | } 47 | &::-webkit-scrollbar-thumb { 48 | background-color: rgba(155, 155, 155, 0.5); 49 | border-radius: 20px; 50 | border: transparent; 51 | } 52 | @media screen and (max-width: 768px) { 53 | width: 100%; 54 | z-index: 9; 55 | } 56 | `; 57 | 58 | const SongContainer = styled.div` 59 | display: flex; 60 | flex-direction: column; 61 | background-color: white; 62 | `; 63 | 64 | const H1 = styled.h2` 65 | padding: 2rem; 66 | `; 67 | 68 | export default Library; 69 | -------------------------------------------------------------------------------- /src/components/LibrarySong.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const LibrarySong = ({ song, setCurrentSong, audioRef, isPlaying, songs, setSongs }) => { 5 | // Function 6 | const songSelectHandler = async () => { 7 | await setCurrentSong(song); 8 | const curSong = song; 9 | const songList = songs; 10 | 11 | const newSongs = songList.map((song) => { 12 | if (song.id === curSong.id) { 13 | return { 14 | ...song, 15 | active: true, 16 | }; 17 | } else { 18 | return { 19 | ...song, 20 | active: false, 21 | }; 22 | } 23 | }); 24 | setSongs(newSongs); 25 | 26 | // check if user is wanting to play a song. 27 | if (isPlaying) { 28 | audioRef.current.play(); 29 | } 30 | }; 31 | 32 | return ( 33 | 34 | {song.name} 35 | 36 |

{song.name}

37 |

{song.artist}

38 |
39 |
40 | ); 41 | }; 42 | const LibrarySongContainer = styled.div` 43 | padding: 0 2rem 0 2rem; 44 | height: 100px; 45 | width: 100%; 46 | display: flex; 47 | transition: all 0.3s ease; 48 | background-color: ${(p) => (p.isActive ? "pink" : "white")}; 49 | &:hover { 50 | background-color: lightblue; 51 | transition: all 0.3s ease; 52 | } 53 | &.active { 54 | background-color: pink; 55 | } 56 | `; 57 | 58 | const LibrarySongDescription = styled.div` 59 | width: 100%; 60 | height: 100%; 61 | display: flex; 62 | flex-direction: column; 63 | justify-content: center; 64 | `; 65 | 66 | const Img = styled.img` 67 | margin: 20px 0; 68 | height: 60px; 69 | `; 70 | 71 | const H1 = styled.h3` 72 | padding-left: 1rem; 73 | font-size: 1rem; 74 | `; 75 | 76 | const H2 = styled.h4` 77 | padding-left: 1rem; 78 | font-size: 0.7rem; 79 | `; 80 | 81 | export default LibrarySong; 82 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | 25 | 26 | Vibes 27 | 28 | 29 | 30 |
31 | 41 | 42 | -------------------------------------------------------------------------------- /src/data.js: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | function chillHop() { 3 | return [ 4 | { 5 | name: "Beaver Creek", 6 | cover: 7 | "https://chillhop.com/wp-content/uploads/2020/09/0255e8b8c74c90d4a27c594b3452b2daafae608d-1024x1024.jpg", 8 | artist: "Aso, Middle School, Aviino", 9 | audio: "https://mp3.chillhop.com/serve.php/?mp3=10075", 10 | color: ["#205950", "#2ab3bf"], 11 | id: uuidv4(), 12 | active: true, 13 | }, 14 | { 15 | name: "Daylight", 16 | cover: 17 | "https://chillhop.com/wp-content/uploads/2020/07/ef95e219a44869318b7806e9f0f794a1f9c451e4-1024x1024.jpg", 18 | artist: "Aiguille", 19 | audio: "https://mp3.chillhop.com/serve.php/?mp3=9272", 20 | color: ["#EF8EA9", "#ab417f"], 21 | id: uuidv4(), 22 | active: false, 23 | }, 24 | { 25 | name: "Keep Going", 26 | cover: 27 | "https://chillhop.com/wp-content/uploads/2020/07/ff35dede32321a8aa0953809812941bcf8a6bd35-1024x1024.jpg", 28 | artist: "Swørn", 29 | audio: "https://mp3.chillhop.com/serve.php/?mp3=9222", 30 | color: ["#CD607D", "#c94043"], 31 | id: uuidv4(), 32 | active: false, 33 | }, 34 | { 35 | name: "Nightfall", 36 | cover: 37 | "https://chillhop.com/wp-content/uploads/2020/07/ef95e219a44869318b7806e9f0f794a1f9c451e4-1024x1024.jpg", 38 | artist: "Aiguille", 39 | audio: "https://mp3.chillhop.com/serve.php/?mp3=9148", 40 | color: ["#EF8EA9", "#ab417f"], 41 | id: uuidv4(), 42 | active: false, 43 | }, 44 | { 45 | name: "Reflection", 46 | cover: 47 | "https://chillhop.com/wp-content/uploads/2020/07/ff35dede32321a8aa0953809812941bcf8a6bd35-1024x1024.jpg", 48 | artist: "Swørn", 49 | audio: "https://mp3.chillhop.com/serve.php/?mp3=9228", 50 | color: ["#CD607D", "#c94043"], 51 | id: uuidv4(), 52 | active: false, 53 | }, 54 | { 55 | name: "Under the City Stars", 56 | cover: 57 | "https://chillhop.com/wp-content/uploads/2020/09/0255e8b8c74c90d4a27c594b3452b2daafae608d-1024x1024.jpg", 58 | artist: "Aso, Middle School, Aviino", 59 | audio: "https://mp3.chillhop.com/serve.php/?mp3=10074", 60 | color: ["#205950", "#2ab3bf"], 61 | id: uuidv4(), 62 | active: false, 63 | }, 64 | //ADD MORE HERE 65 | ]; 66 | } 67 | 68 | export default chillHop; 69 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from "react"; 2 | import styled from "styled-components"; 3 | import "./App.css"; 4 | 5 | // Import components 6 | import Player from "./components/Player"; 7 | import Song from "./components/Song"; 8 | import Library from "./components/Library"; 9 | import Nav from "./components/Nav"; 10 | import Credit from "./components/Credit"; 11 | // Import data 12 | import data from "./data"; 13 | 14 | const App = () => { 15 | // Ref 16 | const audioRef = useRef(null); 17 | 18 | // State 19 | const [songs, setSongs] = useState(data()); 20 | const [currentSong, setCurrentSong] = useState(songs[0]); 21 | const [isPlaying, setIsPlaying] = useState(false); 22 | const [libraryStatus, setLibraryStatus] = useState(false); 23 | const [songInfo, setSongInfo] = useState({ 24 | currentTime: 0, 25 | duration: 0, 26 | }); 27 | 28 | // Functions 29 | const updateTimeHandler = (e) => { 30 | const currentTime = e.target.currentTime; 31 | const duration = e.target.duration; 32 | setSongInfo({ ...songInfo, currentTime, duration }); 33 | }; 34 | 35 | const songEndHandler = async () => { 36 | let currentIndex = songs.findIndex((song) => song.id === currentSong.id); 37 | let nextSong = songs[(currentIndex + 1) % songs.length]; 38 | await setCurrentSong(nextSong); 39 | 40 | const newSongs = songs.map((song) => { 41 | if (song.id === nextSong.id) { 42 | return { 43 | ...song, 44 | active: true, 45 | }; 46 | } else { 47 | return { 48 | ...song, 49 | active: false, 50 | }; 51 | } 52 | }); 53 | setSongs(newSongs); 54 | 55 | if (isPlaying) { 56 | audioRef.current.play(); 57 | } 58 | }; 59 | 60 | return ( 61 | 62 |