├── .babelrc
├── .gitignore..git
├── LICENSE
├── README.md
├── index.html
├── package.json
├── public
├── Consolation.mp3
├── illenium_cover.jpg
├── miro_cover.jpg
├── with_u_newVersion_4_FINAL.mp3
├── without_you.jpg
└── without_you.mp3
├── react-audio-component.sketch
├── server.js
├── src
├── actions
│ └── index.js
├── assets
│ └── default_cover.txt
├── audio
│ └── ReactAudio.js
├── components
│ ├── ArtistInfo.css
│ ├── ArtistInfo.js
│ ├── AudioControlButton.css
│ ├── AudioControlButton.js
│ ├── Utilities.css
│ ├── Utilities.js
│ └── svg
│ │ ├── HeartIcon.js
│ │ ├── NextIcon.js
│ │ ├── PauseIcon.js
│ │ ├── PlayIcon.js
│ │ ├── PreviousIcon.js
│ │ ├── RepeatIcon.js
│ │ ├── ShareIcon.js
│ │ └── SpeakerIcon.js
├── constants
│ └── ActionTypes.js
├── containers
│ ├── App.css
│ └── App.js
├── css
│ ├── ReactSlider.css
│ ├── index.css
│ └── variables.css
├── index.js
├── reducers
│ ├── audio.js
│ └── index.js
├── store
│ ├── configureStore.js
│ └── songs.json
└── utils
│ └── detection.js
├── webpack.hot.js
└── webpack.prod.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets" : ["es2015", "react", "stage-0"],
3 | "plugins": ["transform-decorators-legacy"]
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore..git:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 J.S. Leonard
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Audio Component Example Project
2 |
3 | A beautiful example project demonstrating how to build an audio player in React. Desktop optimized though it works on mobile down to iPhone 5 resolutions.
4 |
5 | ## The Player
6 |
7 | 
8 |
9 | Demo [here](http://leonardsouza.com/react-audio-example.html). Check it out!
10 |
11 | ### Quick Start
12 |
13 | `npm install`
14 |
15 | `npm start`
16 |
17 | `open localhost:3000`
18 |
19 |
20 | _Rest of README forthcoming_
21 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React Audio Player
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-audio-component",
3 | "version": "1.0.0",
4 | "description": "A beautiful audio player built with React",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "deploy": "webpack -p --config webpack.prod.js --output-file audio_player.js && mv ./public/audio_player.js ../portfolio/jslauthor.github.io/public",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/jslauthor/react-audio-component.git"
14 | },
15 | "keywords": [
16 | "audio",
17 | "html5",
18 | "react"
19 | ],
20 | "author": "J.S. Leonard",
21 | "license": "ISC",
22 | "bugs": {
23 | "url": "https://github.com/jslauthor/react-audio-component/issues"
24 | },
25 | "homepage": "https://github.com/jslauthor/react-audio-component#readme",
26 | "devDependencies": {
27 | "autoprefixer": "^6.3.6",
28 | "css-loader": "^0.23.1",
29 | "postcss-bem": "^0.4.1",
30 | "postcss-color-function": "^2.0.1",
31 | "postcss-custom-media": "^5.0.1",
32 | "postcss-import": "^8.1.2",
33 | "postcss-loader": "^0.9.1",
34 | "postcss-mixins": "^4.0.2",
35 | "postcss-nested": "^1.0.0",
36 | "postcss-sass-colors": "0.0.2",
37 | "postcss-simple-vars": "^2.0.0",
38 | "precss": "^1.4.0",
39 | "react-hot-loader": "^1.3.0",
40 | "style-loader": "^0.13.1",
41 | "webpack": "^1.13.1",
42 | "webpack-dev-server": "^1.14.1"
43 | },
44 | "dependencies": {
45 | "babel-loader": "^6.2.4",
46 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
47 | "babel-preset-es2015": "^6.9.0",
48 | "babel-preset-react": "^6.5.0",
49 | "babel-preset-stage-0": "^6.5.0",
50 | "imports-loader": "^0.6.5",
51 | "json-loader": "^0.5.4",
52 | "lodash": "^4.13.1",
53 | "marked": "^0.3.5",
54 | "raf": "^3.2.0",
55 | "raw-loader": "^0.5.1",
56 | "rc-progress": "^1.0.4",
57 | "rc-slider": "^3.7.1",
58 | "rc-tooltip": "^3.3.2",
59 | "react": "^15.1.0",
60 | "react-addons-css-transition-group": "^15.1.0",
61 | "react-dom": "^15.1.0",
62 | "react-marquee": "^0.1.0",
63 | "react-redux": "^4.4.5",
64 | "react-slider": "^0.6.1",
65 | "react-swipeable": "^3.5.0",
66 | "redux": "^3.5.2"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/public/Consolation.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jslauthor/react-audio-component/6d8589f573c9ccabfc91f1fcf100c758fedf8e9c/public/Consolation.mp3
--------------------------------------------------------------------------------
/public/illenium_cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jslauthor/react-audio-component/6d8589f573c9ccabfc91f1fcf100c758fedf8e9c/public/illenium_cover.jpg
--------------------------------------------------------------------------------
/public/miro_cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jslauthor/react-audio-component/6d8589f573c9ccabfc91f1fcf100c758fedf8e9c/public/miro_cover.jpg
--------------------------------------------------------------------------------
/public/with_u_newVersion_4_FINAL.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jslauthor/react-audio-component/6d8589f573c9ccabfc91f1fcf100c758fedf8e9c/public/with_u_newVersion_4_FINAL.mp3
--------------------------------------------------------------------------------
/public/without_you.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jslauthor/react-audio-component/6d8589f573c9ccabfc91f1fcf100c758fedf8e9c/public/without_you.jpg
--------------------------------------------------------------------------------
/public/without_you.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jslauthor/react-audio-component/6d8589f573c9ccabfc91f1fcf100c758fedf8e9c/public/without_you.mp3
--------------------------------------------------------------------------------
/react-audio-component.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jslauthor/react-audio-component/6d8589f573c9ccabfc91f1fcf100c758fedf8e9c/react-audio-component.sketch
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 |
2 | var webpack = require('webpack');
3 | var WebpackDevServer = require('webpack-dev-server');
4 | var config = require('./webpack.hot');
5 |
6 | new WebpackDevServer(webpack(config), {
7 | publicPath: config.output.publicPath,
8 | hot: true,
9 | historyApiFallback: true
10 | }).listen(3000, '0.0.0.0', function (err, result) {
11 | if (err) {
12 | return console.log(err);
13 | }
14 |
15 | console.log('Listening at http://0.0.0.0:3000/');
16 | });
17 |
--------------------------------------------------------------------------------
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/ActionTypes'
2 | import data from '../store/songs.json';
3 |
4 | export function retrieveSongs() {
5 | // This could be written as a thunk or saga
6 | return { type: types.INITIALIZE, songs: data.songs }
7 | }
8 |
9 | export function play(audio) {
10 | if (audio.paused)
11 | audio.play();
12 | else
13 | audio.pause();
14 |
15 | return { type: types.PLAY, audio }
16 | }
17 |
18 | export function pause(audio) {
19 | audio.pause();
20 | return { type: types.PAUSE, audio }
21 | }
22 |
23 | function resetAudio(audio) {
24 | // need to reset the song if it's the same file
25 | audio.currentTime = 0;
26 | const src = audio.src;
27 | audio.src = null;
28 | audio.src = src;
29 | }
30 |
31 | export function next(audio) {
32 | resetAudio(audio)
33 | return { type: types.NEXT, audio }
34 | }
35 |
36 | export function previous(audio) {
37 | resetAudio(audio)
38 | return { type: types.PREVIOUS, audio }
39 | }
40 |
41 | export function updateVolume(audio, volume) {
42 | audio.volume = volume/100;
43 | return { type: types.UPDATE_VOLUME, volume }
44 | }
45 |
46 | export function setTime(audio) {
47 | const percent = audio.currentTime / audio.duration;
48 | return { type: types.SET_TIME, audio }
49 | }
50 |
51 | export function setProgress(audio) {
52 | return { type: types.SET_PROGRESS, audio }
53 | }
54 |
55 | export function setError(audio) {
56 | return { type: types.ERROR, audio }
57 | }
58 |
59 | export function updatePosition(audio, percent) {
60 | audio.currentTime = percent * audio.duration;
61 | return { type: types.UPDATE_POSITION, audio }
62 | }
63 |
64 | export function toggleFavorite(id) {
65 | return { type: types.TOGGLE_FAVORITE, id }
66 | }
67 |
68 | export function toggleRepeat() {
69 | return { type: types.TOGGLE_REPEAT }
70 | }
71 |
72 | export function toggleLoop(audio) {
73 | audio.loop = !audio.loop;
74 | return { type: types.TOGGLE_LOOP, audio }
75 | }
76 |
--------------------------------------------------------------------------------
/src/assets/default_cover.txt:
--------------------------------------------------------------------------------
1 | data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBARXhpZgAATU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAB9KADAAQAAAABAAAB9AAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgB9AH0AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBQQEBAQEBQYFBQUFBQUGBgYGBgYGBgcHBwcHBwgICAgICQkJCQkJCQkJCf/bAEMBAQEBAgICBAICBAkGBQYJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCf/dAAQAIP/aAAwDAQACEQMRAD8A/l/ooorxznCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9D+X+iiivHOcKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/0f5f6KKK8c5wooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//S/l/ooorxznCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9P+X+iiivHOcKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/1P5f6KKK8c5wooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//V/l/ooorxznCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9b+X+iiivHOcKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/1/5f6KKK8c5wooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//Q/l/ooorxznCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9H+X+iiivHOcKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/0v5f6KKK8c5wooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAor720vwL/wAE9ZdNt5NU8aeIkuWiQyqIcAOQNwAFk4GD6M31PWuL+KXhL9izS/BF5e/CjxTreo68uz7Nb3UWIXy4D7ybWHAC5Od+c44PSuGOPTly8svuZ8Zh+NadSsqKw9ZXdrulJL1btovM+PaK2vDkOhXHiGwg8UTS2+mPcRLdywqGkSAsBIyKeCwXJA9a+6v+EC/4Jz/9Dt4j/wC/R/8AkCta+JVO1036K56WdcRQwMoxlSqTv/JCUrettj8+6K+m/jd4b/ZW0Xw9az/AnxDq2r6k9xtnivo9sSwbWywY28B3btoAG7IJ6Y58j+F9j8OdS8cWVl8WL2607QXL/abizUPMmEJTAKvwXwCQjHHaqp11KHPZ/dr9x0YPOo1sI8Wqc0lf3XFqWnaL1d+nc4Civ0E/4QL/AIJz/wDQ7eI/+/R/+QKofHn9lb4deF/g5Y/Hf4K65c6hocxVZE1LasziSTyleLEUROG4ZCmcfMDgGuaOZQ5lGSav3TR89R8QMG69OhWp1Kbm+WLnTlFOT2V2t30PgqiiivQPugooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP//T/l/ooorxznCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK+5/wBjjwz4d1/wr8T7jXLGC8e18OymFpo1cxkpKdybgdpyqnI5GBXwxX2j+yX8QvBngjwz8R7LxXqEVjLqmgSw2iyEjzpdki7E45YlxgdT+BrhzJSdF8u+n5o+M8QKdaWU1I4dNyvDa9/jjfbyPi6iiiu4+zCiiigAooooAK9J8Q6z8VbvwFo+l+Jjfnw5bsx00TxuttuOd3ksVCseucE9682r6f8AiR8bfDPjL9nvwZ8JNOtrqPUPDckr3EsioIXEhbHlkOWP3hnKisKzfNGyvr92h4uazqKtQVOkppy1f8i5Ze8vnZfM898D/A34jfEfwXrPjrwbZi+tNCKi5jRwZ8MCxKRfeYKBk459AcHHkVet/Bj4z+Mvgb4zh8YeEJfRLm2cnyrmLPMbgf8AjrdVPI9/qL9oX4cfDH4n/D2X9qT4JSxWcHmRrrmksQr29xMwTcijgFnYZA4YHev8QrCWJlCry1F7r2fn2f6Hj4nP8Rg8xVDGxXsajShNX0k9OSfq/hls9j4CoooruPsgooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//U/l/ooorxznCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKtW1je3qytZwvKIUMkhRS2xBwWbA4UZHJ4qrX6AfsRxo3hD4rMygn/hG5RyOxjmyPxrmxeI9lTc7f1c8DifO3l2CnjFHm5eXS9t5Jfqfn/RRRXSe+FFFFABRRRQAV96z/sgap4w/Z98H/ED4SaZc6nreqmVtQXz41jVFZ1UqshTHIHQmvgqvR/D/wAYfi14T0uPQ/C/ifVdOsoiSlvbXk0USljk7UVgoyTk4HWuXFU6srOk7NM+b4kwOYVo0pZdUUJRld813GSs1Z2ae7T+RR8BfDXxv8TvELeFPAlg2oagsbymFXRDsQgMcuyrwSO9L43+G/jj4b+JB4M8a2LWGosqP5DOjEiT7hyjMvP1rH8N+LvFXg7UzrfhLUrrS7wqUM9rM8Mm1sErvQg4OBkd6PEPi7xV4t1Ya/4p1K61K+AVRcXMzyy4T7o3uS2B254rT95z9OX8Tv5cb9avePsrdnzc3re1vlc961b9jn9ovQfDWoeK9Y8OvBa6bH5so86F5DGMlmVI3YsEAy2OcevOPmOv09/Yi+J3xI8c+IPF+neNNf1HV7eLw/cSJHeXMs6K+9BuCyMwBwSMjsa/MKubCV6jnOnVtdW28z57hfOMwq4vE4LMORyp8jTgmlaSb6t7W/EKKKK7z7YKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//9X+X+iiivHOcKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAr6t/Zp+MfhD4V6B480zxSZhJ4g0WSztPKTeDMVdVVuflB39TwMH8flKur8M+BvF3jKDULnwtp819HpVs13dtEuRFCn3nb2H54B44NYYmlGcHGex43EGX4bFYSVDFu0Ha7vbZprX1SOUooorc9kKKKKACiiigAooooAKKKKAO58C/Enxv8NLq9vfA9++ny6hayWVwyqjb4JMbl+dWweAQwww7EVw1fUP7LfwU8M/GzXfEOl+J57iBNL0ae9gNuVU+crKqFtytlRuJIGM8c+vy9WEKkHOUVurXPGweNws8ZXo0l+8jy8ztvdPl162V/QKKKK3PZCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/W/l/ooorxznCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK/QP9iH/kT/AIrf9i5L/wCi5q/Pyvor4EfHK0+Duj+MNKu9Pe9/4SbSpLCNkcJ5UjKyqzZByvzknHPArizClKdFxjvp+aPkeOstrYvK6mHw8bybjZek4t7+SZ860UUV2n1wUUUUAFFFFABRRRQAUUUUAfoF/wAE+CB4w8ZE/wDQuXH/AKMjr8/a1dJ17XNBkml0K8nsmuImt5TBI0Zkif78bFSNyN3U8HuKyq5qdDlqSqX3t+B8/l+SSoZhiMa5XVXk07cqa/G4UUUV0n0AUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQB/9f+X+iiivHOcKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAr074d/CLxn8ULHXNR8KRRvF4esnv7syOE/dICSE9WIBIHHTr0z5jX6B/sQ/8if8AFb/sXJf/AEXNXJjq7p0nOPl+Z8xxjnFXAZdPFULcycd9tZJP8Gfn5RRRXWfThRRRQAUUUUAFFFFABRRRQB9ifsb/AAw8E/FDxJ4osPG9mL2Ky0K4uIAXZPLmDIokG0j5lBOM5HtXx3X3p+wPqmmaX4t8Yy6lcR26nw3dEGRgoIV4ycZPYcmvguuGhKTr1E9tP1PjMmr1pZzjac2+VKnZdFdSvb9QoooruPswooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9D+X+iiivHOcKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAr2f4S/G3xB8IdM8S6XolrBcp4l059PmM27MYcMN6bSMkBm4PGceleMV7l8Hfgfqnxh0vxRqmn30VmvhnTX1BxIpYzbAzCMYI25Cn5ucccHPGGJ9nyP2mx43EDwSwknmFvZ6Xvfurbedjw2iiitz2QooooAKKKKACiiigAooooAKK+5/2FPDPh3xN4r8X2/iKxgvkj8O3LIs8ayBSXQFl3A4OCRkc4718MVz066lUlTttb8TxMFnUa+Nr4JRs6XLr35k3+FgoooroPbCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/0f5f6KKK8c5wooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACv0D/Yh/5E/wCK3/YuS/8Aouavz8r0TwJ8VPG3w2s9ZsPCN0LeLXrNrG8BRX3wvkHG4Ha2CQGHIya5MbQdSk4R8vzPmeMMmq5hl88JRaUm477aST/JHndFFFdZ9MFFFFABRRRQAUUUUAFFFFAH2j+xX8QfBvw98TeK73xnqEWnxXWgXMMLSnAkk3owRfViAcDqe1fF1WrWyvb4utlC8xiQyOEUttRerHHQDuegqrWFOgo1JTW7t+B42DyenRxlfGRfvVOW67cqaVvW4UUUVueyFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAf//S/l/ooorxznCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK+pv2Z/2erT4+xeKrdrmeK90fTjcWMUOwedctu8tHL8bSVwcFeudwxz8s10nhnxl4v8FXj6h4N1W80m4lTy3lsp5IHZM52lo2UkZGcE4rDEQnKDVN2Z5GfYXFVsJOngqnJUdrNq6Wqe3mtPmejfEX9nX4z/AAn0WPxF8QNCl0+yllECzGSGVfMYEhT5TuRkA4zgV554O8G+JviD4ltfCHg60a+1K9LCGBCoLbVLtyxCgBVJJJAAFaHib4m/EjxrZpp3jLxDqWrW8b+YkV7dzTor4I3BZHYA4JGRzzXN6PrWseHtTh1rw/dzWN5bndFPbyNFKhxjKuhDA4OODRTVXk963N+BOCjmCwrWIlD2utmlLl8rptv119D6W/4Yl/ah/wChUk/8CbX/AOPV5N8S/gv8T/g9LaQ/EjSZNLN+Ha3LPHIr+Xjfho2cZXcMgnPI4q3/AML++O//AEO2vf8Agyuv/jlcd4o8deN/HEsM/jTWb7WHtwVia9uJbgoGwSFMjNtBwM461lRWI5v3jVvJP/M8zK6eequnjZ0nDryxmn5Wbk1v5Gt8OfhZ4/8Ai1rMugfDzTX1O7giM8iIyIFjBC7i0jKo5IHXJ7V7T/wxL+1D/wBCpJ/4E2v/AMer538NeLfFXgy/Oq+D9Tu9JumQxmazmkgkKEglS0bKcEgZGccV3X/C/vjv/wBDtr3/AIMrr/45RWWI5v3bVvNP/MvNqedutfAzpKH96M27/KSX4HN/ED4b+N/hZr//AAi/j/T302/8tZhE5VsxvkKwZGZSCQRweoI6iuj+GnwL+K/xggurn4caNJqcdkyrM6yRRqrOCVGZXQE4HQZx361wPiHxP4l8XakdZ8V6jc6peMoUz3czzyFV6AvIWbA7DPFaXhf4gePPA4mHgrW7/RxcY837Fcy2/mbc7d3lsu7GTjPTNayVXk0tzfgehXjmH1NKlKHttNWpcl+ul7+mp73/AMMS/tQ/9CpJ/wCBNr/8er508T+GNe8GeILrwt4otms9QsnMc8L43Iw5xkEg8cggkEciu9/4X98d/wDodte/8GV1/wDHK8y1PVNT1vUJtW1m4lu7u4YvLNM7SSOx6szsSWJ7kmooKtf9618r/wCZy5LDN1N/2jKm4205FJO/nzSeh+kH7AfgHxTbw+K/iJe2LLol3ol3ZRXTFdryqyMyBc7iMA5ONvGM5r80a7HSfiJ8QNA0ObwzoWu6hZabc7vNtILqWOCTeNrb41YI24cHI5HBrjqVDDyjUnUk97fgY5Nklehj8Vja0k1V5bJJ6KKaV7t669AooorrPqQooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/0/5f6KKK8c5wooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//U/l/ooorxznCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9X+X+iiivHOcKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/1v5f6KKK8c5wooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//X/l/ooorxznCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9D+X+iiivHOcKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/0f5f6KKK8c5wooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//S/l/ooorxznCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9P+X+iiivHOcKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/1P5f6KKK8c5wooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//V/l/ooorxznCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigD/9b+X+iiivHOcKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/2Q==
2 |
--------------------------------------------------------------------------------
/src/audio/ReactAudio.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import isFunction from 'lodash/isFunction';
4 | import partialRight from 'lodash/partialRight';
5 | import forEach from 'lodash/forEach';
6 |
7 | class ReactAudio extends React.Component {
8 |
9 | static propTypes = {
10 | autoplay: React.PropTypes.bool,
11 | preload: React.PropTypes.bool,
12 | source: React.PropTypes.string,
13 | loop: React.PropTypes.bool,
14 | volume: React.PropTypes.number,
15 | onTimeupdate: React.PropTypes.func,
16 | onError: React.PropTypes.func,
17 | onProgress: React.PropTypes.func,
18 | onEnded: React.PropTypes.func
19 | };
20 |
21 | static defaultProps = {
22 | autoplay: false,
23 | preload: true,
24 | source: "",
25 | loop: false,
26 | volume: .8,
27 | onTimeupdate: null,
28 | onError: null,
29 | onProgress: null,
30 | onEnded: null
31 | };
32 |
33 | constructor(props) {
34 | super(props)
35 |
36 | this.state = {
37 | listeners: []
38 | };
39 | }
40 |
41 | get audio() {
42 | if (!this.refs)
43 | return {};
44 |
45 | return ReactDOM.findDOMNode(this.refs.audio);
46 | }
47 |
48 | set audio(a) {}
49 |
50 | handler(e, func) {
51 | if (isFunction(func)) {
52 | func(e);
53 | }
54 | }
55 |
56 | addListener = (event, func) => {
57 | var audio = ReactDOM.findDOMNode(this.refs.audio);
58 | audio.addEventListener(event, partialRight(this.handler, func));
59 | this.state.listeners.push({event: event, func: func});
60 | }
61 |
62 | removeAllListeners = () => {
63 | var audio = ReactDOM.findDOMNode(this.refs.audio);
64 | forEach(this.state.listeners, (obj) => {
65 | audio.removeEventListener(obj.event, obj.func);
66 | })
67 | this.state.listeners = [];
68 | }
69 |
70 | componentDidMount() {
71 | this.addListener('timeupdate', this.props.onTimeupdate);
72 | this.addListener('progress', this.props.onProgress);
73 | this.addListener('error', this.props.onError);
74 | this.addListener('ended', this.props.onEnded);
75 | this.addListener('loadeddata', this.props.onLoadedData);
76 | }
77 |
78 | componentWillUnmount() {
79 | this.removeAllListeners();
80 | }
81 |
82 | componentWillReceiveProps(nextProps) {
83 | if (nextProps.autoplay === true && this.props.autoplay === false) {
84 | this.audio.play();
85 | }
86 | }
87 |
88 | togglePlay = () => {
89 | if (this.audio.paused)
90 | this.audio.play();
91 | else
92 | this.audio.pause();
93 | }
94 |
95 | setPlaybackPercent(percent) {
96 | this.audio.currentTime = percent * this.audio.duration;
97 | }
98 |
99 | changeCurrentTimeBy = (amount) => {
100 | this.audio.currentTime += amount;
101 | }
102 |
103 | setVolume = (percent) => {
104 | this.audio.volume = percent;
105 | }
106 |
107 | render() {
108 | return(
109 |
118 | )
119 | }
120 |
121 | }
122 |
123 | export default ReactAudio;
124 |
--------------------------------------------------------------------------------
/src/components/ArtistInfo.css:
--------------------------------------------------------------------------------
1 | $maxCoverSize: 257px;
2 |
3 | @component artist-info {
4 |
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: flex-start;
8 | align-items: stretch;
9 | background-color: #3F413B;
10 | flex-grow: 1;
11 | overflow: hidden;
12 | max-width: $maxCoverSize;
13 | position: relative;
14 |
15 | @descendent cover-container {
16 | height: $maxCoverSize;
17 | width: $maxCoverSize;
18 | display: flex;
19 | /*user-select: none;*/
20 | flex-grow: 1;
21 | position: relative;
22 | }
23 |
24 | @descendent cover {
25 | width: 100%;
26 | height: auto;
27 | max-height: $maxCoverSize;
28 | max-width: $maxCoverSize;
29 | position: absolute;
30 | }
31 |
32 | @descendent song {
33 | height: 51px;
34 | padding: 0 15px;
35 | display: flex;
36 | flex-direction: column;
37 | justify-content: center;
38 | max-width: $maxCoverSize;
39 | }
40 |
41 | @modifier title {
42 | height: 22px;
43 | display: flex;
44 | flex-direction: column;
45 | justify-content: center;
46 | }
47 |
48 | @descendent error {
49 | position: absolute;
50 | width: 100%;
51 | height: 100%;
52 | background-color: rgba(0,0,0,.5);
53 | display: flex;
54 | justify-content: center;
55 | align-items: center;
56 | color: white;
57 | z-index: 40;
58 | }
59 |
60 | }
61 |
62 | .truncate {
63 | width: 100%;
64 | white-space: nowrap;
65 | overflow: hidden;
66 | text-overflow: ellipsis;
67 | }
68 |
69 | .cover-enter {
70 | transform: translate(100%);
71 | }
72 | .cover-enter.cover-enter-active {
73 | transform: translate(0%);
74 | transition: transform 250ms ease-in-out;
75 | }
76 | .cover-leave {
77 | transform: translate(0%);
78 | }
79 | .cover-leave.cover-leave-active {
80 | transform: translate(-100%);
81 | transition: transform 250ms ease-in-out;
82 | }
83 |
84 | @component marquee {
85 | width: 100%;
86 | height: 100%;
87 | overflow: hidden;
88 | position: relative;
89 |
90 | @modifier content {
91 | position: absolute;
92 | overflow: hidden;
93 | white-space: nowrap;
94 | top: -2px;
95 | padding: 0;
96 | margin: 0;
97 | h2 { display: inline; }
98 | }
99 |
100 | @modifier animate {
101 | animation: marquee 5s linear infinite;
102 | animation-delay: 1s;
103 | animation-direction: alternate;
104 | }
105 | }
106 |
107 | @keyframes marquee {
108 | 0% { transform: translateX(0%); }
109 | 10% { transform: translateX(0%); }
110 | 90% { transform: translateX(-90%); }
111 | 100% { transform: translateX(-90%); }
112 | }
113 |
--------------------------------------------------------------------------------
/src/components/ArtistInfo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
4 | import ReactSlider from 'rc-slider';
5 | import './ArtistInfo.css';
6 | import '../css/ReactSlider.css';
7 | import isEmpty from 'lodash/isEmpty';
8 | import isFunction from 'lodash/isFunction';
9 | import SpeakerIcon from './svg/SpeakerIcon.js';
10 | import Marquee from 'react-marquee';
11 | import cx from 'classnames';
12 | import Swipeable from 'react-swipeable';
13 | import partial from 'lodash/partial';
14 | import invoke from 'lodash/invoke';
15 | import detectMobile from '../utils/detection';
16 |
17 | const SpeakerHandle = props =>
18 |
19 |
20 |
21 |
22 | export default class ArtistInfo extends React.Component {
23 |
24 | constructor(props) {
25 | super(props);
26 |
27 | this.state = {
28 | showMarquee: false,
29 | marqueeDuration: 0,
30 | isMobile: detectMobile()
31 | }
32 | }
33 |
34 | static propTypes = {
35 | coverURL: React.PropTypes.string,
36 | title: React.PropTypes.string,
37 | artist: React.PropTypes.string,
38 | onVolumeChange: React.PropTypes.func,
39 | volume: React.PropTypes.number,
40 | hasError: React.PropTypes.bool
41 | };
42 |
43 | static defaultProps = {
44 | volume: 75,
45 | hasError: false
46 | };
47 |
48 | // This is dangerous. Never set state on update without a clear condition
49 | componentDidUpdate() {
50 | const title = ReactDOM.findDOMNode(this.refs.title);
51 | const node = ReactDOM.findDOMNode(this.refs.marquee);
52 | const shouldShow = (title.getBoundingClientRect().width - node.clientWidth) > 0;
53 | if (this.state.showMarquee != shouldShow) {
54 | this.setState({
55 | showMarquee: shouldShow,
56 | marqueeDuration: (node.scrollWidth / node.clientWidth) * 3
57 | });
58 | }
59 | }
60 |
61 | onVolumeChange = (value) => {
62 | if (isFunction(this.props.onVolumeChange)) {
63 | this.props.onVolumeChange(value);
64 | }
65 | }
66 |
67 | handleImageError = (e) => {
68 | e.currentTarget.src = require("raw!../assets/default_cover.txt");
69 | }
70 |
71 | handleSwiping = (e) => {
72 | if (typeof e === 'string') {
73 | switch (e) {
74 | case 'left':
75 | invoke(this.props, 'onNext');
76 | break;
77 | case 'right':
78 | invoke(this.props, 'onPrevious');
79 | break;
80 | }
81 | }
82 | }
83 |
84 | render() {
85 |
86 | const { title, artist, volume, songID, hasError } = this.props;
87 | const animate = {
88 | animation: `marquee ${this.state.marqueeDuration}s linear infinite`,
89 | animationDelay: '1s',
90 | animationDirection: 'alternate'
91 | };
92 |
93 | return (
94 |
95 |
96 | {
97 | hasError &&
98 |
99 | Error Playing Media
100 |
101 | }
102 |
103 |
106 |
107 |
111 |
113 |
114 |
115 |
116 |
117 | {
118 | !this.state.isMobile &&
119 |
} />
123 | }
124 |
125 |
126 |
127 |
128 |
130 |
{title}
131 |
132 |
133 |
134 |
{artist}
135 |
136 |
137 |
138 | );
139 | }
140 |
141 | }
142 |
--------------------------------------------------------------------------------
/src/components/AudioControlButton.css:
--------------------------------------------------------------------------------
1 | @import '../css/variables.css';
2 |
3 | $trackSize: 40px;
4 |
5 | @component audio-control {
6 |
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | position: relative;
11 | cursor: pointer;
12 |
13 | @modifier btn {
14 | transition: .25s all ease;
15 | width: 28px;
16 | height: 28px;
17 | background-color: $base_bg_color;
18 | border-radius: 50%;
19 | border: none;
20 | cursor: pointer;
21 | z-index: 10;
22 | &:focus {
23 | outline: none;
24 | }
25 | &:hover {
26 | background-color: color($base_bg_color tint(30%));
27 | }
28 | &:disabled {
29 | background-color: color($base_bg_color tint(82%));
30 | }
31 | }
32 |
33 | @modifier progress-container {
34 | position: absolute;
35 | top: 50%;
36 | left: 50%;
37 | }
38 |
39 | .rc-progress-circle {
40 | width: $trackSize;
41 | height: $trackSize;
42 | position: absolute;
43 | top: 50%;
44 | left: 50%;
45 | transform: translate(-50%, -50%);
46 | }
47 |
48 | @modifier play {
49 | margin-top: 3px;
50 | margin-left: 1px;
51 | }
52 |
53 | }
54 |
55 | .disabled {
56 | pointer-events: none;
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/AudioControlButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Circle } from 'rc-progress';
3 | import './AudioControlButton.css';
4 | import get from 'lodash/get';
5 | import PauseIcon from './svg/PauseIcon.js';
6 | import PlayIcon from './svg/PlayIcon.js';
7 | import NextIcon from './svg/NextIcon.js';
8 | import PreviousIcon from './svg/PreviousIcon.js';
9 | import range from 'lodash/range';
10 | import isFunction from 'lodash/isFunction';
11 |
12 | export default class AudioControlButton extends React.Component {
13 |
14 | static propTypes = {
15 | mode: React.PropTypes.oneOf(['play', 'pause', 'next', 'previous']),
16 | showProgress: React.PropTypes.bool,
17 | percent: React.PropTypes.number,
18 | duration: React.PropTypes.number,
19 | progress: React.PropTypes.object
20 | };
21 |
22 | static defaultProps = {
23 | mode: 'play',
24 | showProgress: false,
25 | percent: 0,
26 | duration: 0,
27 | progress: {}
28 | };
29 |
30 | handleTrackClick = (e) => {
31 | const mouseX = e.pageX - e.currentTarget.getBoundingClientRect().left;
32 | const mouseY = e.pageY - e.currentTarget.getBoundingClientRect().top;
33 | const circleCenterX = e.currentTarget.offsetWidth/2;
34 | const circleCenterY = e.currentTarget.offsetHeight/2;
35 | const angle = Math.atan2(mouseY - circleCenterY, mouseX - circleCenterX);
36 | const degree = (angle * 180/Math.PI + 360) % 360;
37 | const percent = ((degree/360) * 100) - 75;
38 |
39 | if (isFunction(this.props.onTrackClick)) {
40 | this.props.onTrackClick(percent >= 0 ? percent : 100 + percent);
41 | }
42 | }
43 |
44 | render() {
45 |
46 | let icon = ;
47 | switch (this.props.mode) {
48 | case 'play':
49 | icon = ;
50 | break;
51 | case 'next':
52 | icon = ;
53 | break;
54 | case 'previous':
55 | icon = ;
56 | break;
57 | }
58 |
59 | return (
60 |
61 | {
62 | this.props.showProgress && range(0, this.props.progress.length).map((i) => {
63 | const d = this.props.duration;
64 | const start = this.props.progress.start(i);
65 | const end = this.props.progress.end(i);
66 | const buffer = (end - start) / d;
67 |
68 | return
70 |
72 |
73 | })
74 | }
75 | { this.props.showProgress &&
}
76 | { this.props.showProgress &&
77 |
78 |
79 |
80 | }
81 |
82 |
85 |
86 |
87 |
88 | );
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/components/Utilities.css:
--------------------------------------------------------------------------------
1 | @import '../css/variables.css';
2 |
3 | @component utilities {
4 |
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: flex-start;
8 | align-items: center;
9 | width: 50px;
10 | padding: 15px 0;
11 |
12 | @modifier control {
13 | margin-bottom: 7.5px;
14 | }
15 |
16 | @modifier play {
17 | margin-bottom: 10px;
18 | }
19 |
20 | @modifier favorite {
21 | fill: $pink;
22 | }
23 |
24 | @modifier repeating {
25 | fill: $dark_green;
26 | }
27 |
28 | @modifier btn-green {
29 | transition: .5s all ease;
30 | &:hover {
31 | fill: color($dark_green saturation(10%));
32 | }
33 | }
34 |
35 | @modifier btn-pink {
36 | transition: .5s all ease;
37 | &:hover {
38 | fill: color($pink saturation(10%));
39 | }
40 | }
41 |
42 | @descendent social {
43 | flex-grow: 1;
44 | display: flex;
45 | flex-direction: column;
46 | align-items: center;
47 | justify-content: flex-end;
48 | * {
49 | margin-top: 15px;
50 | cursor: pointer;
51 | }
52 | }
53 |
54 | }
55 |
56 | .splash-anim {
57 | animation: splash .5s ease-in-out;
58 | }
59 |
60 | @keyframes splash {
61 | from {
62 | opacity: 0;
63 | transform: scale(0, 0);
64 | }
65 | 50% {
66 | opacity: 1;
67 | transform: scale(1.1, 1.1);
68 | }
69 | to {
70 | opacity: 1;
71 | transform: scale(1, 1);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/Utilities.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AudioControl from './AudioControlButton.js';
3 | import HeartIcon from './svg/HeartIcon.js';
4 | import ShareIcon from './svg/ShareIcon.js';
5 | import RepeatIcon from './svg/RepeatIcon.js';
6 | import cx from 'classnames';
7 | import './Utilities.css';
8 |
9 | export default class Utilities extends React.Component {
10 |
11 | static propTypes = {
12 | isPlaying: React.PropTypes.bool,
13 | percent: React.PropTypes.number,
14 | isFavorite: React.PropTypes.bool,
15 | onPlay: React.PropTypes.func,
16 | onNext: React.PropTypes.func,
17 | onPrevious: React.PropTypes.func,
18 | progress: React.PropTypes.object,
19 | onToggleFavorite: React.PropTypes.func,
20 | onToggleRepeat: React.PropTypes.func
21 | };
22 |
23 | static defaultProps = {
24 | isPlaying: false,
25 | percent: 0,
26 | isFavorite: false,
27 | progress: {},
28 | percent: 0
29 | }
30 |
31 | render() {
32 |
33 | const playMode = this.props.isPlaying ? "pause" : "play";
34 | const heartClasses = cx({
35 | 'utilities_btn-pink': !this.props.isFavorite,
36 | 'utilities_favorite': this.props.isFavorite
37 | });
38 | const repeatClasses = cx({
39 | 'utilities_btn-green': !this.props.isRepeating,
40 | 'utilities_repeating': this.props.isRepeating
41 | });
42 | const controlClasses = cx({
43 | 'utilities_control': true,
44 | 'disabled': this.props.disableChange,
45 | 'splash-anim': true
46 | })
47 |
48 | return (
49 |
50 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | );
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/svg/HeartIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import get from 'lodash/get';
3 |
4 | const HeartIcon = props => (
5 |
9 | );
10 |
11 | export default HeartIcon;
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/components/svg/NextIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const NextIcon = props => (
4 |
9 | );
10 |
11 | export default NextIcon;
12 |
--------------------------------------------------------------------------------
/src/components/svg/PauseIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const PauseIcon = props => (
4 |
10 | );
11 |
12 | export default PauseIcon;
13 |
--------------------------------------------------------------------------------
/src/components/svg/PlayIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const PlayIcon = props => (
4 |
8 | );
9 |
10 | export default PlayIcon;
11 |
--------------------------------------------------------------------------------
/src/components/svg/PreviousIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const PreviousIcon = props => (
4 |
9 | );
10 |
11 | export default PreviousIcon;
12 |
--------------------------------------------------------------------------------
/src/components/svg/RepeatIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import get from 'lodash/get';
3 |
4 | const RepeatIcon = props => (
5 |
9 | );
10 |
11 | export default RepeatIcon;
12 |
--------------------------------------------------------------------------------
/src/components/svg/ShareIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const ShareIcon = props => (
4 |
7 | );
8 |
9 | export default ShareIcon;
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/components/svg/SpeakerIcon.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const SpeakerIcon = props => (
4 |
10 | );
11 |
12 | export default SpeakerIcon;
13 |
--------------------------------------------------------------------------------
/src/constants/ActionTypes.js:
--------------------------------------------------------------------------------
1 | export const INITIALIZE = 'INITIALIZE'
2 | export const PLAY = 'PLAY'
3 | export const PAUSE = 'PAUSE'
4 | export const NEXT = 'NEXT'
5 | export const PREVIOUS = 'PREVIOUS'
6 | export const ERROR = 'ERROR'
7 | export const UPDATE_BUFFER = 'UPDATE_BUFFER'
8 | export const UPDATE_VOLUME = 'UPDATE_VOLUME'
9 | export const UPDATE_POSITION = 'UPDATE_POSITION'
10 | export const TOGGLE_FAVORITE = 'TOGGLE_FAVORITE'
11 | export const TOGGLE_REPEAT = 'TOGGLE_REPEAT'
12 | export const TOGGLE_LOOP = 'TOGGLE_LOOP'
13 | export const SET_TIME = 'SET_TIME'
14 | export const SET_PROGRESS = 'SET_PROGRESS'
15 |
--------------------------------------------------------------------------------
/src/containers/App.css:
--------------------------------------------------------------------------------
1 | @component app {
2 |
3 | min-width: 300px;
4 | min-height: 300px;
5 | background-image:linear-gradient(-165deg, #ebebeb 5%, #ebebeb 96%);
6 | border-radius:8px;
7 | box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, .17);
8 | display: flex;
9 | justify-content: center;
10 | align-items: stretch;
11 | overflow: hidden;
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/containers/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { bindActionCreators } from 'redux'
4 | import { connect } from 'react-redux'
5 | import * as AudioActions from '../actions'
6 | import Utilities from '../components/Utilities';
7 | import ArtistInfo from '../components/ArtistInfo';
8 | import ReactAudio from '../audio/ReactAudio';
9 | import find from 'lodash/find';
10 | import './App.css';
11 |
12 | @connect(
13 | state => ({audio: state.audio}),
14 | dispatch => bindActionCreators(AudioActions, dispatch)
15 | )
16 |
17 | export default class App extends React.Component {
18 |
19 | componentDidMount() {
20 | // Initialize DOM Audio and retrieve
21 | this.props.updateVolume(ReactDOM.findDOMNode(this.refs.audio), this.props.audio.volume);
22 | this.props.setProgress(ReactDOM.findDOMNode(this.refs.audio));
23 | this.props.setTime(ReactDOM.findDOMNode(this.refs.audio));
24 | this.props.retrieveSongs();
25 | }
26 |
27 | handleProgress = () => {
28 | this.props.setProgress(ReactDOM.findDOMNode(this.refs.audio));
29 | }
30 |
31 | handleTimeupdate = () => {
32 | this.props.setTime(ReactDOM.findDOMNode(this.refs.audio));
33 | }
34 |
35 | handleError = (e) => {
36 | this.props.setError(ReactDOM.findDOMNode(this.refs.audio));
37 | }
38 |
39 | handlePlay = () => {
40 | this.props.play(ReactDOM.findDOMNode(this.refs.audio));
41 | }
42 |
43 | handleNext = () => {
44 | const audio = ReactDOM.findDOMNode(this.refs.audio);
45 | this.props.next(audio);
46 | }
47 |
48 | handlePrevious = () => {
49 | const audio = ReactDOM.findDOMNode(this.refs.audio);
50 | this.props.previous(audio);
51 | }
52 |
53 | handleVolumeChange = (volume) => {
54 | this.props.updateVolume(ReactDOM.findDOMNode(this.refs.audio), volume);
55 | }
56 |
57 | handleToggleFavorite = () => {
58 | this.props.toggleFavorite();
59 | }
60 |
61 | handleToggleRepeat = () => {
62 | this.props.toggleRepeat();
63 | }
64 |
65 | handleTrackClick = (percent) => {
66 | this.props.updatePosition(ReactDOM.findDOMNode(this.refs.audio), percent/100);
67 | }
68 |
69 | handleEnd = () => {
70 | const audio = ReactDOM.findDOMNode(this.refs.audio);
71 | if (this.props.audio.isRepeating) {
72 | this.props.next(audio);
73 | } else {
74 | this.props.pause(audio);
75 | }
76 | }
77 |
78 | handleToggleLoop = () => {
79 | this.props.toggleLoop(ReactDOM.findDOMNode(this.refs.audio));
80 | }
81 |
82 | handleLoadedData = () => {
83 | const audio = ReactDOM.findDOMNode(this.refs.audio);
84 | if (this.props.audio.isRepeating) {
85 | this.props.play(audio);
86 | }
87 | }
88 |
89 | render() {
90 |
91 | const {
92 | volume, isPlaying, percent, isFavorite, progress, error,
93 | duration, isRepeating, songs, currentID, autoplay, isLooping
94 | } = this.props.audio;
95 |
96 | let song = find(songs, (o) => o.id === currentID);
97 | if (song === undefined) song = this.props.audio.defaultSong;
98 |
99 | return (
100 |
101 |
102 |
111 |
112 |
128 |
129 |
139 |
140 |
141 | );
142 | }
143 |
144 | }
145 |
--------------------------------------------------------------------------------
/src/css/ReactSlider.css:
--------------------------------------------------------------------------------
1 | @import './variables.css';
2 |
3 | @component rc-slider {
4 |
5 | position: relative;
6 | background-color: #536159;
7 |
8 | .rc-slider-handle {
9 | background-color: white;
10 | height: 20px;
11 | width: 20px;
12 | display: flex;
13 | justify-content: center;
14 | align-items: center;
15 | border-radius: 20px;
16 | cursor: pointer;
17 | position: absolute;
18 | transform: translate(-50%, -50%);
19 | z-index: 3;
20 | }
21 |
22 | .rc-slider-track {
23 | height: 3px;
24 | background-color: $dark_green;
25 | }
26 |
27 | .rc-slider-disabled.rc-slider-track {
28 | background-color: #999999;
29 | }
30 |
31 | .rc-slider-handle-icon {
32 | width: 12px;
33 | height: 11px;
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/src/css/index.css:
--------------------------------------------------------------------------------
1 | @import 'variables.css';
2 |
3 | * {
4 | box-sizing: border-box;
5 | }
6 |
7 | html, body {
8 | background-color: #D7D7D7;
9 | width: 100%;
10 | height: 100%;
11 | margin: 0;
12 | padding: 0;
13 | display: flex;
14 | justify-content: center;
15 | align-items: center;
16 | position: relative;
17 | font-family: "eurostile", sans-serif;
18 | color: white;
19 | }
20 |
21 | h1, h2, h3, h4, h5, h6 {
22 | margin: 0;
23 | padding: 0;
24 | text-overflow: ellipsis;
25 | }
26 |
27 | h2 {
28 | font-size: 18px;
29 | letter-spacing: 1.5px;
30 | font-weight: 700;
31 | color: rgba(255, 255, 255, .88);
32 | }
33 |
34 | h6 {
35 | font-size: 10px;
36 | font-weight: 400;
37 | font-style: italic;
38 | color: rgba(255, 255, 255, .64);
39 | }
40 |
--------------------------------------------------------------------------------
/src/css/variables.css:
--------------------------------------------------------------------------------
1 | $base_bg_color: #52554C;
2 | $dark_green: #5C8970;
3 | $pink: #EF4A9C;
4 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import { Provider } from 'react-redux'
4 | import App from './containers/App'
5 | import configureStore from './store/configureStore'
6 | import './css/index.css'
7 |
8 | const store = configureStore()
9 |
10 | render(
11 |
12 |
13 | ,
14 | document.getElementById('app')
15 | )
16 |
--------------------------------------------------------------------------------
/src/reducers/audio.js:
--------------------------------------------------------------------------------
1 | import {
2 | INITIALIZE, ERROR,
3 | UPDATE_VOLUME, NEXT, PREVIOUS,
4 | PLAY, SET_TIME, SET_PROGRESS,
5 | TOGGLE_FAVORITE, TOGGLE_REPEAT,
6 | UPDATE_POSITION, PAUSE, TOGGLE_LOOP
7 | } from '../constants/ActionTypes'
8 |
9 | import find from 'lodash/find';
10 | import findIndex from 'lodash/findIndex';
11 | import sortBy from 'lodash/sortBy';
12 | import indexOf from 'lodash/indexOf';
13 | import clone from 'lodash/clone';
14 |
15 | const initialState = {
16 | isPlaying: false,
17 | isFavorite: false,
18 | isRepeating: false,
19 | isLooping: false,
20 | percent: 0,
21 | volume: 65,
22 | progress: {},
23 | duration: 0,
24 | repeat: false,
25 | songs: [],
26 | currentID: null,
27 | defaultSong: {
28 | "id": -1,
29 | "coverURL": require("raw!../assets/default_cover.txt"),
30 | "audioFile": null,
31 | "title": "Waiting...",
32 | "artist": "No song loaded",
33 | "favorite": false
34 | }
35 | };
36 |
37 | function getSongIndex(songs, id) {
38 | return findIndex(songs, (o) => o.id === id);
39 | }
40 |
41 | function getAdjacentSong(songs, startIndex, direction) {
42 | let nextIndex = startIndex + direction;
43 | nextIndex = nextIndex < 0 ? songs.length-1 : nextIndex > songs.length-1 ? 0 : nextIndex;
44 | return songs[nextIndex].id;
45 | }
46 |
47 | function getAudioState(audio) {
48 | var test = {
49 | isPlaying: !audio.paused,
50 | percent: audio.currentTime / audio.duration,
51 | progress: audio.buffered,
52 | duration: audio.duration,
53 | isLooping: audio.loop,
54 | error: audio.error
55 | }
56 |
57 | return test;
58 | }
59 |
60 | export default function audio(state = initialState, action) {
61 | switch (action.type) {
62 | case INITIALIZE:
63 | const songsArray = sortBy(action.songs, ['id']);
64 | return {...state, songs: songsArray, currentID: songsArray[0].id };
65 | case PLAY:
66 | case PAUSE:
67 | case ERROR:
68 | return {...state, ...getAudioState(action.audio) };
69 | case NEXT:
70 | return {
71 | ...state,
72 | currentID: getAdjacentSong(state.songs, getSongIndex(state.songs, state.currentID), 1),
73 | ...getAudioState(action.audio)
74 | };
75 | case PREVIOUS:
76 | return {
77 | ...state,
78 | currentID: getAdjacentSong(state.songs, getSongIndex(state.songs, state.currentID), -1),
79 | ...getAudioState(action.audio)
80 | };
81 | case UPDATE_VOLUME:
82 | return {...state, volume: action.volume };
83 | case SET_TIME:
84 | return {...state, ...getAudioState(action.audio) };
85 | case UPDATE_POSITION:
86 | return {...state, ...getAudioState(action.audio) };
87 | case SET_PROGRESS:
88 | return {...state, ...getAudioState(action.audio) };
89 | case TOGGLE_FAVORITE:
90 | const songs = state.songs.map(clone);
91 | const song = find(songs, {id: state.currentID});
92 | song.favorite = !song.favorite;
93 | return {...state, songs };
94 | case TOGGLE_REPEAT:
95 | return {...state, isRepeating: !state.isRepeating };
96 | case TOGGLE_LOOP:
97 | return {...state, ...getAudioState(action.audio) };
98 | default:
99 | return state
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import audio from './audio'
3 |
4 | const rootReducer = combineReducers({
5 | audio
6 | })
7 |
8 | export default rootReducer
9 |
--------------------------------------------------------------------------------
/src/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux'
2 | import rootReducer from '../reducers'
3 |
4 | export default function configureStore(preloadedState) {
5 | const store = createStore(rootReducer, preloadedState)
6 |
7 | if (module.hot) {
8 | // Enable Webpack hot module replacement for reducers
9 | module.hot.accept('../reducers', () => {
10 | const nextReducer = require('../reducers').default
11 | store.replaceReducer(nextReducer)
12 | })
13 | }
14 |
15 | return store
16 | }
17 |
--------------------------------------------------------------------------------
/src/store/songs.json:
--------------------------------------------------------------------------------
1 | {
2 | "songs": [
3 | {
4 | "id": 1,
5 | "coverURL": "/public/miro_cover.jpg",
6 | "audioFile": "/public/Consolation.mp3",
7 | "title": "Miro",
8 | "artist": "Consolation",
9 | "favorite": true
10 | },
11 | {
12 | "id": 2,
13 | "coverURL": "/public/illenium_cover.jpg",
14 | "audioFile": "/public/with_u_newVersion_4_FINAL.mp3",
15 | "title": "Illenium - With You (ft. Quinn XCII) (Crystal Skies Remix)",
16 | "artist": "Crystal Skies",
17 | "favorite": false
18 | },
19 | {
20 | "id": 3,
21 | "coverURL": "/public/without_you.jpg",
22 | "audioFile": "/public/without_you.mp3",
23 | "title": "Illenium - Without You (Ft. SKYLR)",
24 | "artist": "Ryan Exley (Remix)",
25 | "favorite": true
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/src/utils/detection.js:
--------------------------------------------------------------------------------
1 | export default function() {
2 | var check = false;
3 | (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera);
4 | return check;
5 | }
6 |
--------------------------------------------------------------------------------
/webpack.hot.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const precss = require('precss');
4 | const autoprefixer = require('autoprefixer');
5 | const simpleVars = require('postcss-simple-vars');
6 | const postcssMixins = require('postcss-mixins');
7 | const postcssBEM = require('postcss-bem')({style: 'bem'});
8 | const postcssNested = require('postcss-nested');
9 | const colorFunctions = require('postcss-color-function');
10 | const postcssImport = require('postcss-import');
11 | const customMedia = require('postcss-custom-media');
12 |
13 | const BUILD_DIR = path.resolve(__dirname, 'public');
14 | const APP_DIR = path.resolve(__dirname, 'src');
15 |
16 | const config = {
17 | devtool: 'eval',
18 | entry: [
19 | 'webpack-dev-server/client?http://0.0.0.0:3000',
20 | 'webpack/hot/only-dev-server',
21 | APP_DIR + '/index.js'
22 | ],
23 | output: {
24 | path: BUILD_DIR,
25 | publicPath: '/public/',
26 | filename: 'bundle.js'
27 | },
28 | module : {
29 | loaders : [
30 | {
31 | test : /\.jsx?/,
32 | include : APP_DIR,
33 | exclude: /node_modules/,
34 | loaders: ['react-hot', 'babel'],
35 | plugins: [
36 | 'transform-runtime',
37 | 'add-module-exports',
38 | 'transform-decorators-legacy',
39 | ]
40 | },
41 | { test: /\.css$/, loader: "style-loader!css-loader!postcss-loader" },
42 | { test: /\.json$/, loaders: ['json-loader'] }
43 | ]
44 | },
45 | postcss: function(webpack) {
46 | return [
47 | postcssImport({addDependencyTo: webpack}),
48 | precss,
49 | autoprefixer,
50 | postcssMixins,
51 | simpleVars,
52 | colorFunctions,
53 | postcssBEM,
54 | postcssNested,
55 | customMedia
56 | ];
57 | },
58 | plugins: [
59 | new webpack.HotModuleReplacementPlugin()
60 | ]
61 | };
62 |
63 | module.exports = config;
64 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const precss = require('precss');
4 | const autoprefixer = require('autoprefixer');
5 | const simpleVars = require('postcss-simple-vars');
6 | const postcssMixins = require('postcss-mixins');
7 | const postcssBEM = require('postcss-bem')({style: 'bem'});
8 | const postcssNested = require('postcss-nested');
9 | const colorFunctions = require('postcss-color-function');
10 | const postcssImport = require('postcss-import');
11 | const customMedia = require('postcss-custom-media');
12 |
13 | const BUILD_DIR = path.resolve(__dirname, 'public');
14 | const APP_DIR = path.resolve(__dirname, 'src');
15 |
16 | const config = {
17 | entry: APP_DIR + '/index.js',
18 | output: {
19 | path: BUILD_DIR,
20 | filename: 'bundle.js'
21 | },
22 | module : {
23 | loaders : [
24 | {
25 | test : /\.jsx?/,
26 | include : APP_DIR,
27 | exclude: /node_modules/,
28 | loader : 'babel',
29 | plugins: [
30 | 'transform-runtime',
31 | 'add-module-exports',
32 | 'transform-decorators-legacy',
33 | ]
34 | },
35 | { test: /\.css$/, loader: "style-loader!css-loader!postcss-loader" },
36 | { test: /\.json$/, loaders: ['json-loader'] },
37 | {
38 | test: /masonry|imagesloaded|fizzy\-ui\-utils|desandro\-|outlayer|get\-size|doc\-ready|eventie|eventemitter/,
39 | loader: 'imports?define=>false&this=>window'
40 | }
41 | ]
42 | },
43 | postcss: function(webpack) {
44 | return [
45 | postcssImport({addDependencyTo: webpack}),
46 | precss,
47 | autoprefixer,
48 | postcssMixins,
49 | simpleVars,
50 | colorFunctions,
51 | postcssBEM,
52 | postcssNested,
53 | customMedia
54 | ];
55 | },
56 | plugins: [
57 | new webpack.DefinePlugin({
58 | 'process.env': {
59 | 'NODE_ENV': JSON.stringify('production')
60 | }
61 | })
62 | ]
63 | };
64 |
65 | module.exports = config;
66 |
--------------------------------------------------------------------------------