├── src
├── index.css
├── index.js
├── App.test.js
├── components
│ ├── SongsList
│ │ ├── SongsList.css
│ │ └── SongsList.js
│ └── Player
│ │ ├── Player.css
│ │ └── Player.js
├── App.css
├── fetchSongs.js
└── App.js
├── .gitignore
├── package.json
├── specs.txt
├── README.md
└── public
└── index.html
/src/index.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import './index.css';
5 |
6 | ReactDOM.render(
7 | ,
8 | document.getElementById('root')
9 | );
10 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | });
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # testing
7 | coverage
8 |
9 | # production
10 | build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | npm-debug.log
16 |
--------------------------------------------------------------------------------
/src/components/SongsList/SongsList.css:
--------------------------------------------------------------------------------
1 | .songs-list {
2 | padding-bottom: 9em;
3 | }
4 |
5 | .song-list-item {
6 | cursor: pointer;
7 | font-size: 1.4rem;
8 | font-weight: 300;
9 | margin: 1em 0;
10 | padding: .75em 1em;
11 | border: 1px solid #14B7F4;
12 | border-radius: 2px;
13 | color: #FFF;
14 | background-color: #171D25;
15 | }
16 |
17 | .song-list-item:hover {
18 | border-color: #9ecaed;
19 | box-shadow: 0 0 10px #9ecaed;
20 | }
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | color: #FFF;
5 | font-family: 'Open Sans', sans-serif;
6 | background-color: #1D242E;
7 | }
8 |
9 | li {
10 | list-style: none;
11 | font-size: 1.1rem;
12 | }
13 |
14 | a {
15 | color: #FFF;
16 | text-decoration: none;
17 | }
18 |
19 | .container {
20 | width: 600px;
21 | margin: 0 auto;
22 | }
23 |
24 | .title {
25 | font-size: 3.2rem;
26 | font-weight: 300;
27 | text-align: center;
28 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jukebox",
3 | "version": "0.1.0",
4 | "private": true,
5 | "devDependencies": {
6 | "react-scripts": "0.7.0"
7 | },
8 | "dependencies": {
9 | "axios": "^0.15.2",
10 | "react": "^15.3.2",
11 | "react-dom": "^15.3.2"
12 | },
13 | "scripts": {
14 | "start": "react-scripts start",
15 | "build": "react-scripts build",
16 | "test": "react-scripts test --env=jsdom",
17 | "eject": "react-scripts eject"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/specs.txt:
--------------------------------------------------------------------------------
1 | v1:
2 | ==
3 | Player fixed at the bottom of the screen
4 | - prev, next buttons
5 | - play-pause toggle buttons
6 | - loop song
7 | - slider
8 | - current time on left side of slider and total time on right
9 | - volume control - mute and unmute
10 |
11 | List all the songs available in a SINGLE folder for now.
12 | - clicking on a song should play it
13 | - list current song being played
14 | - play next song automatically after first finishes
15 |
16 |
17 | v2:
18 | ==
19 | - search bar
20 | - deep searching inside folders?
--------------------------------------------------------------------------------
/src/components/SongsList/SongsList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import './SongsList.css';
4 |
5 | class SongsList extends Component {
6 | render() {
7 | return (
8 |
9 | {
10 | this.props.songs.map((song, i) => {
11 | return
this.props.handleSongClick(e.target.id)}>
16 | {song.song_title.slice(0, -4)}
17 |
18 | })
19 | }
20 |
21 | );
22 | }
23 | }
24 |
25 | export default SongsList;
--------------------------------------------------------------------------------
/src/fetchSongs.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | function filterSongs(html) {
3 | let songsList = [];
4 |
5 | let htmlDoc = document.createElement('div');
6 | htmlDoc.innerHTML = html;
7 |
8 | let listOfLinks = htmlDoc.querySelectorAll('a');
9 | listOfLinks = Array.from(listOfLinks);
10 |
11 | songsList = listOfLinks.map(link => {
12 | return {
13 | song_title: link.innerHTML,
14 | song_url: link.href.replace('http://localhost:3000', 'http://localhost:9000')
15 | }
16 | });
17 |
18 | songsList = songsList.filter(link => link.song_title.includes('.mp3'));
19 |
20 | return songsList;
21 | }
22 |
23 | function fetchSongs() {
24 | return new Promise((resolve, reject) => {
25 | axios.get('http://localhost:9000')
26 | .then(response => {
27 | const songs = filterSongs(response.data);
28 | return resolve(songs);
29 | })
30 | .catch(err => reject(err));
31 | });
32 | }
33 |
34 | export default fetchSongs;
--------------------------------------------------------------------------------
/src/components/Player/Player.css:
--------------------------------------------------------------------------------
1 | .player-controls {
2 | position: fixed;
3 | text-align: center;
4 | bottom: 0;
5 | width: 100%;
6 | background-color: #171D25;
7 | opacity: 0.90;
8 | padding: .75em 0 1em;
9 | }
10 |
11 | .icons-container {
12 | width: 500px;
13 | height: 4rem;
14 | margin: 0 auto;
15 | }
16 |
17 | .material-icons {
18 | font-size: 2.2rem;
19 | cursor: pointer;
20 | vertical-align: middle;
21 | user-select: none;
22 | }
23 |
24 | .play, .pause {
25 | font-size: 4rem;
26 | }
27 |
28 | .repeat {
29 | float: left;
30 | margin-top: 1rem;
31 | }
32 |
33 | .glow {
34 | outline: none;
35 | border-color: #9ecaed;
36 | box-shadow: 0 0 10px #9ecaed;
37 | }
38 |
39 | .volume {
40 | float: right;
41 | margin-top: 1rem;
42 | }
43 |
44 | .slider {
45 | -webkit-appearance: none;
46 | width: 500px;
47 | height: 2px;
48 | outline: none;
49 | cursor: pointer;
50 | }
51 |
52 | .time {
53 | display: inline-block;
54 | margin: 0 .5em;
55 | vertical-align: sub;
56 | }
57 | .song-title {
58 | padding-top: .5em;
59 | }
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Jukebox
2 | ####Music player built in React
3 |
4 | 
5 |
6 | ## Features
7 |
8 | * Displays list of `.mp3` files in your folder
9 | * Will play all the songs in the order displayed
10 | * You can jump to any song by clicking on it
11 | * Displays current time and total duration of the file being played
12 | * Play previous or next song
13 | * Loop through a song infinitely
14 | * Mute/unmute the volume
15 | * Highlights the song currently being played. Also lists the same at the bottom of the screen
16 |
17 |
18 | ## Setting it up on your machine
19 |
20 | ### Server
21 | - Serve files from your local folder at `http://localhost:9000/`. (I know I know! I will fix it. Have to add many more things)
22 | - You might get CORS error if you are using Python's SimpleHTTPServer. I suggest you install [http-server](https://github.com/indexzero/http-server) via npm. Then `cd` into your folder with `.mp3` files and run `http-server -a localhost -p 9000 --cors`
23 | - Right now just it will only run `.mp3` files. So just keep those in your folder.
24 |
25 | ### Client
26 | - It is bootstrapped with [create-react-app](https://github.com/facebookincubator/create-react-app)
27 | - Once your local server is up, `cd` into the project folder. Now first run `npm install` to install dependencies and then run `npm start`
28 |
29 |
30 | #### Enjoy!
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
17 | Jukebox
18 |
19 |
20 |
21 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import SongsList from './components/SongsList/SongsList';
3 | import Player from './components/Player/Player';
4 | import fetchSongs from './fetchSongs.js';
5 |
6 | import './App.css';
7 |
8 | class App extends Component {
9 | constructor() {
10 | super();
11 | this.state = {
12 | songs: [],
13 | currentSong: 0
14 | }
15 | this.handlePrev = this.handlePrev.bind(this);
16 | this.handleNext = this.handleNext.bind(this);
17 | this.handleSongClick = this.handleSongClick.bind(this);
18 | }
19 |
20 | componentWillMount(){
21 | fetchSongs()
22 | .then(songs => this.setState({songs: songs}))
23 | .catch(err => console.log(err));
24 | }
25 |
26 | handlePrev() {
27 | // console.log("prev clicked");
28 | const nextSong = parseInt(this.state.currentSong, 10) - 1;
29 | if(this.state.currentSong > 0) {
30 | this.setState({currentSong: nextSong});
31 | }
32 | }
33 |
34 | handleNext() {
35 | const nextSong = parseInt(this.state.currentSong, 10) + 1;
36 | if(this.state.currentSong < this.state.songs.length - 1) {
37 | this.setState({currentSong: nextSong});
38 | }
39 | }
40 |
41 | handleSongClick(id) {
42 | this.setState({currentSong: id});
43 | }
44 |
45 | render() {
46 | return (
47 |
48 |
49 |
Jukebox
50 |
55 |
56 | {
57 | this.state.songs.length
58 | ?
64 | : null
65 | }
66 |
67 | );
68 | }
69 | }
70 |
71 | export default App;
--------------------------------------------------------------------------------
/src/components/Player/Player.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import './Player.css'
4 |
5 | class Player extends Component {
6 |
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | currentSongUrl: "",
11 | currentSongTitle: "",
12 | isPlaying: false,
13 | isMute: false,
14 | currentTime: 0,
15 | duration: 0,
16 | isLooping: false
17 | }
18 | this.handlePlay = this.handlePlay.bind(this);
19 | this.handleVolume = this.handleVolume.bind(this);
20 | this.handleSlider = this.handleSlider.bind(this);
21 | this.handleLooping = this.handleLooping.bind(this);
22 | }
23 |
24 | handlePlay() {
25 | this.state.isPlaying ? this.refs.player.pause() : this.refs.player.play();
26 | this.setState({isPlaying: !this.state.isPlaying});
27 | }
28 |
29 | handleVolume() {
30 | this.setState({isMute: !this.state.isMute})
31 | }
32 |
33 | handleTime() {
34 | const audio = this.refs.player;
35 | let timer;
36 | if(timer){window.cancelRequestAnimFrame(timer)};
37 | if(audio) {
38 | timer = window.requestAnimationFrame(() => {
39 | this.setState({
40 | currentTime: audio.currentTime,
41 | duration: audio.duration
42 | });
43 | if(Math.floor(audio.currentTime) === Math.floor(audio.duration)) {
44 | if(this.state.isLooping) {
45 | this.refs.player.currentTime = 0;
46 | this.refs.player.play();
47 | } else {
48 | this.props.playNext();
49 | }
50 | }
51 | });
52 | }
53 | }
54 |
55 | handleSlider(event) {
56 | this.refs.player.currentTime = event.target.value;
57 | this.setState({currentTime: event.target.value});
58 | }
59 |
60 | handleLooping() {
61 | this.setState({isLooping: !this.state.isLooping});
62 | }
63 |
64 | componentWillMount() {
65 | this.setState({
66 | currentSongUrl: this.props.currentSongUrl,
67 | currentSongTitle: this.props.currentSongTitle
68 | });
69 | }
70 |
71 | componentDidMount() {
72 | this.handleTime();
73 | }
74 |
75 | componentDidUpdate() {
76 | this.handleTime();
77 | }
78 |
79 | componentWillReceiveProps(nextProps) {
80 | // console.log("receiving ", nextProps.currentSongUrl);
81 | this.setState({
82 | currentSongUrl: nextProps.currentSongUrl,
83 | currentSongTitle: nextProps.currentSongTitle,
84 | isPlaying: true
85 | });
86 | }
87 |
88 | render() {
89 | return (
90 |
91 |
92 |
93 |
94 | repeat_one
97 |
98 |
99 | skip_previous
100 |
101 |
105 | {this.state.isPlaying ? "pause_circle_outline" : "play_circle_outline"}
106 |
107 |
108 | skip_next
109 |
110 |
114 | {this.state.isMute ? "volume_off" : "volume_up"}
115 |
116 |
117 |
118 |
124 |
125 |
126 |
{convertSecondsToMinsSecs(this.state.currentTime)}
127 |
136 |
{convertSecondsToMinsSecs(this.state.duration)}
137 |
{this.state.currentSongTitle.slice(0, -4)}
138 |
139 |
140 | );
141 | }
142 | }
143 |
144 | export default Player;
145 |
146 | function convertSecondsToMinsSecs(totalSec) {
147 | const minutes = parseInt( totalSec / 60, 10) % 60;
148 | const seconds = parseInt(totalSec % 60, 10);
149 | const result = (minutes < 10 ? "0" + minutes : minutes) + ":" + (seconds < 10 ? "0" + seconds : seconds);
150 | return result;
151 | }
--------------------------------------------------------------------------------