├── .editorconfig ├── .gitignore ├── .jscsrc ├── .jshintrc ├── README.md ├── app ├── img │ ├── audio.svg │ ├── check.svg │ ├── close.svg │ ├── facebook.svg │ ├── fail.svg │ ├── favicon.png │ ├── ipad.png │ ├── ipad@2x.png │ ├── iphone.png │ ├── iphone@2x.png │ ├── meta-logo.jpg │ ├── pause.svg │ ├── press-cnet.jpg │ ├── press-dnet.jpg │ ├── press-lifehack.jpg │ ├── press-ph.jpg │ ├── press-spotify.jpg │ ├── reload.svg │ ├── remove.svg │ ├── search.svg │ ├── spotify-logo.png │ ├── tail-spin.svg │ ├── title-github.jpg │ ├── twitter.svg │ └── volume.svg ├── index.html ├── js │ ├── actions │ │ ├── AlertActions.js │ │ ├── ModalActions.js │ │ ├── PlaylistActions.js │ │ ├── SearchActions.js │ │ └── UserActions.js │ ├── app.js │ ├── components │ │ ├── Alert.js │ │ ├── Footer.js │ │ ├── Loading.js │ │ ├── Modal.js │ │ ├── Player.js │ │ ├── Playlist.js │ │ ├── SearchBox.js │ │ ├── Tip.js │ │ ├── Title.js │ │ ├── Top.js │ │ └── Track.js │ ├── constants │ │ └── constants.js │ ├── core │ │ ├── Magic.js │ │ └── Spotify.js │ ├── dispatcher.js │ └── stores │ │ ├── AlertStore.js │ │ ├── ModalStore.js │ │ ├── PlaylistStore.js │ │ ├── SearchStore.js │ │ └── UserStore.js ├── login │ └── index.html └── styles │ └── style.css ├── gulpfile.js └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = LF 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .DS_Store 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "disallowSpacesInNamedFunctionExpression": { 4 | "beforeOpeningRoundBrace": true 5 | }, 6 | "disallowSpacesInFunctionExpression": { 7 | "beforeOpeningRoundBrace": true 8 | }, 9 | "disallowSpacesInAnonymousFunctionExpression": { 10 | "beforeOpeningRoundBrace": true 11 | }, 12 | "disallowSpacesInFunctionDeclaration": { 13 | "beforeOpeningRoundBrace": true 14 | }, 15 | "disallowEmptyBlocks": true, 16 | "disallowSpacesInCallExpression": true, 17 | "disallowSpacesInsideArrayBrackets": true, 18 | "disallowSpacesInsideParentheses": true, 19 | "disallowQuotedKeysInObjects": true, 20 | "disallowSpaceAfterObjectKeys": true, 21 | "disallowSpaceAfterPrefixUnaryOperators": true, 22 | "disallowSpaceBeforePostfixUnaryOperators": true, 23 | "disallowSpaceBeforeBinaryOperators": [ 24 | "," 25 | ], 26 | "disallowMixedSpacesAndTabs": true, 27 | "disallowTrailingWhitespace": true, 28 | "requireTrailingComma": false, 29 | "disallowYodaConditions": true, 30 | "disallowKeywords": [ "with" ], 31 | "disallowKeywordsOnNewLine": ["else"], 32 | "disallowMultipleLineBreaks": true, 33 | "disallowMultipleLineStrings": true, 34 | "disallowMultipleVarDecl": true, 35 | "disallowSpaceBeforeComma": true, 36 | "disallowSpaceBeforeSemicolon": true, 37 | "requireSpaceBeforeBlockStatements": true, 38 | "requireParenthesesAroundIIFE": true, 39 | "requireSpacesInConditionalExpression": true, 40 | "requireBlocksOnNewline": 1, 41 | "requireCommaBeforeLineBreak": true, 42 | "requireSpaceBeforeBinaryOperators": true, 43 | "requireSpaceAfterBinaryOperators": true, 44 | "requireLineFeedAtFileEnd": true, 45 | "requireCapitalizedConstructors": true, 46 | "requireDotNotation": true, 47 | "requireSpacesInForStatement": true, 48 | "requireSpaceBetweenArguments": true, 49 | "requireCurlyBraces": [ 50 | "do" 51 | ], 52 | "requireSpaceAfterKeywords": [ 53 | "if", 54 | "else", 55 | "for", 56 | "while", 57 | "do", 58 | "switch", 59 | "case", 60 | "return", 61 | "try", 62 | "catch", 63 | "typeof" 64 | ], 65 | "requireSemicolons": true, 66 | "safeContextKeyword": "_this", 67 | "validateLineBreaks": "LF", 68 | "validateQuoteMarks": "'", 69 | "validateIndentation": 2, 70 | "maximumLineLength": 100 71 | } 72 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": false, 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": false, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "angular": false, 23 | "spyOn" : false, 24 | "describe" : false, 25 | "beforeEach" : false, 26 | "afterEach" : false, 27 | "inject" : false, 28 | "it" : false, 29 | "expect" : false, 30 | "$httpBackend" : false, 31 | "runs" : false, 32 | "$" : false, 33 | "jasmine": false, 34 | "saveAs": false, 35 | "d3": false, 36 | "nv": false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Magic Playlist / 2 | 3 | > Get the playlist of your dreams based on a song 4 | 5 | [![title](https://raw.githubusercontent.com/loverajoel/magicplaylist/master/app/img/title-github.jpg)](http://magicplaylist.co/) 6 | 7 | Magic Playlist is an intelligent algorithm developed under Spotify's API that enables users to create a playlist based on a song. 8 | 9 | The algorithm detects the main artists and creates a playlist based on their high rated tracks. You can preview each song, remove it and add security attributions such as public or private. Give it a name, save it into your Spotify's account and enjoy! 10 | 11 | Go to [MagicPlaylist /](http://magicplaylist.co/) 12 | 13 | #Features 14 | - Create an Awesome playlist based on a song 15 | - Play audio preview (30 seconds) 16 | - Edit playlist 17 | - Make new playlist based in a track of the list 18 | - Save playlist in Spotify 19 | - Share playlist 20 | 21 | #Algorithm Overview 22 | 1. Given a Track extract his popularity 23 | 2. Get related Artists form that Track 24 | 3. Get top tracks from each related Artist 25 | 4. Sort all Tracks from popularity(ASC) 26 | 5. Alternate by Artist 27 | 6. Select a batch of 30 Tracks most closest to the first Track popularity 28 | 7. Sort by popularity 29 | 8. Alternate by Artist 30 | 9. Enjoy the playlist 31 | 32 | [The Algorithm](https://github.com/loverajoel/magicplaylist/blob/master/app/js/core/Magic.js) :star2: 33 | 34 | #Stack 35 | - ES6 36 | - Flux 37 | - React 38 | - [Spotify-SDK](https://github.com/loverajoel/spotify-sdk) 39 | 40 | #Spotify API 41 | 42 | This entire app is based on [Spotify API](https://developer.spotify.com/web-api/):heart: 43 | 44 | #Stay In Touch 45 | 46 | Follow us for news [@magicplaylistco](https://twitter.com/magicplaylistco) 47 | 48 | #Press 49 | 50 | [![MagicPlaylist](https://raw.githubusercontent.com/loverajoel/magicplaylist/master/app/img/press-spotify.jpg)](https://developer.spotify.com/showcase/item/magic-playlist/) 51 | 52 | [![MagicPlaylist Creates Spotify Playlists Based on a Single Song](https://raw.githubusercontent.com/loverajoel/magicplaylist/master/app/img/press-lifehack.jpg)](http://lifehacker.com/magicplaylist-creates-spotify-playlists-based-on-a-sing-1739415795) 53 | 54 | [![CNET - Create Spotify playlists based on one song with MagicPlaylist](https://raw.githubusercontent.com/loverajoel/magicplaylist/master/app/img/press-cnet.jpg)](http://www.cnet.com/how-to/create-spotify-playlists-based-on-one-song-with-magicplaylist/) 55 | 56 | [![MagicPlaylist - Create Spotify playlists based on one song](https://raw.githubusercontent.com/loverajoel/magicplaylist/master/app/img/press-ph.jpg)](https://www.producthunt.com/tech/magic-playlist/) 57 | 58 | [![MagicPlaylist trenger bare én sang](https://raw.githubusercontent.com/loverajoel/magicplaylist/master/app/img/press-dnet.jpg)](http://www.dinside.no/935094/magicplaylist-trenger-bare-n-sang) 59 | 60 | #Contributing 61 | 62 | 63 | ``` 64 | npm install 65 | npm run dev 66 | 67 | ``` 68 | 69 | Add polyfill to build and works with all browsers 70 | ``` 71 | npm run dev --production 72 | ``` 73 | # Authors 74 | 75 | Code by Lovera Joel ([@loverajoel](https://twitter.com/loverajoel)) 76 | 77 | Design by Agustín Schelstraete ([@aschelstraete](https://twitter.com/aschelstraete)) 78 | 79 | 80 | Made with :heart: from Córdoba, Argentina. 81 | -------------------------------------------------------------------------------- /app/img/audio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 15 | 16 | 17 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/img/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/img/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/img/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/img/fail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /app/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loverajoel/magicplaylist/340118adc297dbb2d36e8524b463f5df9387da87/app/img/favicon.png -------------------------------------------------------------------------------- /app/img/ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loverajoel/magicplaylist/340118adc297dbb2d36e8524b463f5df9387da87/app/img/ipad.png -------------------------------------------------------------------------------- /app/img/ipad@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loverajoel/magicplaylist/340118adc297dbb2d36e8524b463f5df9387da87/app/img/ipad@2x.png -------------------------------------------------------------------------------- /app/img/iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loverajoel/magicplaylist/340118adc297dbb2d36e8524b463f5df9387da87/app/img/iphone.png -------------------------------------------------------------------------------- /app/img/iphone@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loverajoel/magicplaylist/340118adc297dbb2d36e8524b463f5df9387da87/app/img/iphone@2x.png -------------------------------------------------------------------------------- /app/img/meta-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loverajoel/magicplaylist/340118adc297dbb2d36e8524b463f5df9387da87/app/img/meta-logo.jpg -------------------------------------------------------------------------------- /app/img/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/img/press-cnet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loverajoel/magicplaylist/340118adc297dbb2d36e8524b463f5df9387da87/app/img/press-cnet.jpg -------------------------------------------------------------------------------- /app/img/press-dnet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loverajoel/magicplaylist/340118adc297dbb2d36e8524b463f5df9387da87/app/img/press-dnet.jpg -------------------------------------------------------------------------------- /app/img/press-lifehack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loverajoel/magicplaylist/340118adc297dbb2d36e8524b463f5df9387da87/app/img/press-lifehack.jpg -------------------------------------------------------------------------------- /app/img/press-ph.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loverajoel/magicplaylist/340118adc297dbb2d36e8524b463f5df9387da87/app/img/press-ph.jpg -------------------------------------------------------------------------------- /app/img/press-spotify.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loverajoel/magicplaylist/340118adc297dbb2d36e8524b463f5df9387da87/app/img/press-spotify.jpg -------------------------------------------------------------------------------- /app/img/reload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/img/remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /app/img/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /app/img/spotify-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loverajoel/magicplaylist/340118adc297dbb2d36e8524b463f5df9387da87/app/img/spotify-logo.png -------------------------------------------------------------------------------- /app/img/tail-spin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 20 | 21 | 22 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/img/title-github.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loverajoel/magicplaylist/340118adc297dbb2d36e8524b463f5df9387da87/app/img/title-github.jpg -------------------------------------------------------------------------------- /app/img/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/img/volume.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | MagicPlaylist 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /app/js/actions/AlertActions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Dispatcher from '../dispatcher'; 4 | import {ALERT_OPEN, ALERT_CLOSE} from '../constants/constants'; 5 | 6 | let AlertActions = { 7 | 8 | open: () => { 9 | Dispatcher.dispatch({ 10 | type: ALERT_OPEN 11 | }); 12 | }, 13 | 14 | close: () => { 15 | Dispatcher.dispatch({ 16 | type: ALERT_CLOSE 17 | }); 18 | } 19 | 20 | }; 21 | 22 | export default AlertActions; 23 | -------------------------------------------------------------------------------- /app/js/actions/ModalActions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Dispatcher from '../dispatcher'; 4 | import {MODAL_OPEN, MODAL_CLOSE} from '../constants/constants'; 5 | 6 | let ModalActions = { 7 | 8 | open: () => { 9 | Dispatcher.dispatch({ 10 | type: MODAL_OPEN 11 | }); 12 | }, 13 | 14 | close: () => { 15 | Dispatcher.dispatch({ 16 | type: MODAL_CLOSE 17 | }); 18 | } 19 | 20 | }; 21 | 22 | export default ModalActions; 23 | -------------------------------------------------------------------------------- /app/js/actions/PlaylistActions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Dispatcher from '../dispatcher'; 4 | import { 5 | PLAYLIST_ADD_TRACKS, 6 | PLAYLIST_REMOVE_TRACK, 7 | PLAYLIST_LOADING, 8 | PLAYLIST_REMOVE_TRACKS, 9 | PLAYLIST_CREATED, 10 | PLAYLIST_SAVING, 11 | USER_TOKEN_ERROR, 12 | PLAYLIST_TRACK_NOT_FOUND, 13 | PLAYLIST_SAVE_FAIL, 14 | PLAYLIST_LIMIT_429, 15 | SEARCH_RESET, 16 | PLAYLIST_FAILED 17 | } from '../constants/constants'; 18 | import Spotify from '../core/Spotify'; 19 | import {login} from './UserActions'; 20 | 21 | let PlaylistActions = { 22 | 23 | search: (text, country) => { 24 | Dispatcher.dispatch({ 25 | type: PLAYLIST_LOADING 26 | }); 27 | Spotify.search(text, country, (tracks, mainTrack) => { 28 | if (tracks.length) { 29 | Dispatcher.dispatch({ 30 | type: PLAYLIST_ADD_TRACKS, 31 | tracks: tracks, 32 | mainTrack: mainTrack 33 | }); 34 | ga('send', 'event', 'event', 'new-playlist', 'new'); 35 | } else { 36 | Dispatcher.dispatch({ 37 | type: PLAYLIST_TRACK_NOT_FOUND, 38 | tracks: [] 39 | }); 40 | ga('send', 'event', 'event', 'playlist-search', 'no-result'); 41 | } 42 | }, (error) => { 43 | ga('send', 'event', 'event', 'new-api-error', error.response.status); 44 | if (error.response.status === 429) { 45 | Dispatcher.dispatch({ 46 | type: PLAYLIST_LIMIT_429, 47 | tracks: [] 48 | }); 49 | ga('send', 'event', 'event', 'playlist-search', '429'); 50 | } else if (error.response.status === 401) { 51 | Dispatcher.dispatch({ 52 | type: PLAYLIST_LIMIT_429, 53 | tracks: [] 54 | }); 55 | ga('send', 'event', 'event', 'playlist-search', '401'); 56 | } else { 57 | Dispatcher.dispatch({ 58 | type: PLAYLIST_FAILED 59 | }); 60 | ga('send', 'event', 'event', 'playlist-search', 'error'); 61 | } 62 | Dispatcher.dispatch({ 63 | type: SEARCH_RESET 64 | }); 65 | }); 66 | }, 67 | 68 | removeTracks: () => { 69 | Dispatcher.dispatch({ 70 | type: PLAYLIST_REMOVE_TRACKS 71 | }); 72 | }, 73 | 74 | removeTrack: (index) => { 75 | Dispatcher.dispatch({ 76 | type: PLAYLIST_REMOVE_TRACK, 77 | index: index 78 | }); 79 | }, 80 | 81 | save: (userId, name, isPublic, tracks) => { 82 | Dispatcher.dispatch({ 83 | type: PLAYLIST_SAVING 84 | }); 85 | Spotify.savePlaylist(userId, name, isPublic, tracks).then((response) => { 86 | Dispatcher.dispatch({ 87 | type: PLAYLIST_CREATED, 88 | response: response 89 | }); 90 | ga('send', 'event', 'event', 'playlist-save', 'saved'); 91 | }).catch((error) => { 92 | if (error.response.status === 401) { 93 | ga('send', 'event', 'event', 'playlist-save', 'token error'); 94 | login().then(() => { 95 | PlaylistActions.save(userId, name, isPublic, tracks); 96 | }); 97 | } else { 98 | Dispatcher.dispatch({ 99 | type: PLAYLIST_SAVE_FAIL 100 | }); 101 | ga('send', 'event', 'event', 'playlist-save', 'error'); 102 | } 103 | }); 104 | } 105 | 106 | }; 107 | 108 | export default PlaylistActions; 109 | -------------------------------------------------------------------------------- /app/js/actions/SearchActions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Dispatcher from '../dispatcher'; 4 | import {SEARCH_ADD, SEARCH_RESET} from '../constants/constants'; 5 | 6 | let SearchActions = { 7 | 8 | newSearch: (text) => { 9 | Dispatcher.dispatch({ 10 | type: SEARCH_ADD, 11 | text: text 12 | }); 13 | }, 14 | 15 | resetSearch: () => { 16 | Dispatcher.dispatch({ 17 | type: SEARCH_RESET 18 | }); 19 | } 20 | 21 | }; 22 | 23 | export default SearchActions; 24 | -------------------------------------------------------------------------------- /app/js/actions/UserActions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Dispatcher from '../dispatcher'; 4 | import {USER_LOGED, USER_TOKEN, USER_LOGOUT, USER_COUNTRY} from '../constants/constants'; 5 | import Spotify from '../core/Spotify'; 6 | 7 | let UserActions = { 8 | 9 | login: () => { 10 | ga('send', 'event', 'event', 'login', 'init'); 11 | return new Promise((resolve, reject) => { 12 | Spotify.login().then((data) => { 13 | Dispatcher.dispatch({ 14 | type: USER_TOKEN, 15 | data: data 16 | }); 17 | ga('send', 'event', 'event', 'login', 'fin'); 18 | Spotify.getUser().then((data) => { 19 | Dispatcher.dispatch({ 20 | type: USER_LOGED, 21 | data: data 22 | }); 23 | resolve(); 24 | }); 25 | }); 26 | }); 27 | }, 28 | 29 | getCountry: () => { 30 | if (localStorage.magic_country) { 31 | Dispatcher.dispatch({ 32 | type: USER_COUNTRY, 33 | data: localStorage.magic_country 34 | }); 35 | } else { 36 | let checkStatus = (response) => { 37 | if (response.status >= 200 && response.status < 300) { 38 | return response; 39 | } else { 40 | var error = new Error(response.statusText); 41 | error.response = response; 42 | throw error; 43 | } 44 | }; 45 | 46 | let parseJSON = (response) => { 47 | return response.json(); 48 | }; 49 | 50 | fetch('http://ip-api.com/json', { 51 | method: 'GET' 52 | }).then(checkStatus) 53 | .then(parseJSON) 54 | .then((response) => { 55 | 56 | let markets = ['AD','AR','AT','AU','BE','BG','BO','BR','CA','CH','CL','CO','CR','CY','CZ', 57 | 'DE','DK','DO','EC','EE','ES','FI','FR','GB','GR','GT','HK','HN','HU','IE','IS','IT','LI', 58 | 'LT','LU','LV','MC','MT','MX','MY','NI','NL','NO','NZ','PA','PE','PH','PL','PT','PY','RO', 59 | 'SE','SG','SI','SK','SV','TR','TW','US','UY']; 60 | 61 | if (markets.indexOf(response.countryCode) > -1) { 62 | localStorage.magic_country = response.countryCode; 63 | Dispatcher.dispatch({ 64 | type: USER_COUNTRY, 65 | data: response.countryCode 66 | }); 67 | } else { 68 | localStorage.magic_country = 'US'; 69 | Dispatcher.dispatch({ 70 | type: USER_COUNTRY, 71 | data: 'US' 72 | }); 73 | } 74 | }).catch((error) => { 75 | ga('send', 'event', 'event', 'error-country', 'catch'); 76 | }); 77 | } 78 | } 79 | }; 80 | 81 | export default UserActions; 82 | -------------------------------------------------------------------------------- /app/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 7 | 8 | import SearchBox from './components/SearchBox'; 9 | import Playlist from './components/Playlist'; 10 | import Top from './components/Top'; 11 | import Footer from './components/Footer'; 12 | import Title from './components/Title'; 13 | import Modal from './components/Modal'; 14 | import Loading from './components/Loading'; 15 | import Alert from './components/Alert'; 16 | import Tip from './components/Tip'; 17 | 18 | import SearchStore from './stores/SearchStore'; 19 | import PlaylistStore from './stores/PlaylistStore'; 20 | import ModalStore from './stores/ModalStore'; 21 | import UserStore from './stores/UserStore'; 22 | import AlertStore from './stores/AlertStore'; 23 | 24 | import {getCountry} from './actions/UserActions'; 25 | 26 | let getAppState = () => { 27 | return { 28 | text: SearchStore.getSearch(), 29 | tracks: PlaylistStore.getTracks(), 30 | mainTrack: PlaylistStore.getMainTrack(), 31 | searching: SearchStore.getSearch() !== '', 32 | loading: PlaylistStore.getLoading(), 33 | user: UserStore.getUser(), 34 | token: UserStore.getToken(), 35 | modalOpen: ModalStore.isOpen(), 36 | alertOpen: AlertStore.isOpen(), 37 | alert: AlertStore.status(), 38 | country: UserStore.getCountry(), 39 | lastPlaylist: PlaylistStore.getLastPlaylist() 40 | }; 41 | }; 42 | 43 | class App extends Component { 44 | 45 | constructor(props) { 46 | super(props); 47 | this.state = getAppState(); 48 | } 49 | 50 | componentDidMount() { 51 | getCountry(); 52 | SearchStore.addChangeListener(this._onChange.bind(this)); 53 | PlaylistStore.addChangeListener(this._onChange.bind(this)); 54 | ModalStore.addChangeListener(this._onChange.bind(this)); 55 | UserStore.addChangeListener(this._onChange.bind(this)); 56 | AlertStore.addChangeListener(this._onChange.bind(this)); 57 | } 58 | 59 | componentWillUnmount() { 60 | SearchStore.removeChangeListener(this._onChange.bind(this)); 61 | TrackStore.removeChangeListener(this._onChange.bind(this)); 62 | ModalStore.removeChangeListener(this._onChange.bind(this)); 63 | UserStore.removeChangeListener(this._onChange.bind(this)); 64 | AlertStore.removeChangeListener(this._onChange.bind(this)); 65 | } 66 | 67 | _onChange() { 68 | this.setState(getAppState()); 69 | } 70 | 71 | render() { 72 | return
73 | 74 | { this.state.searching ? this.renderTop() : null } 75 | 76 | 77 | { !this.state.searching ? this.renderSearch() : null } 78 | 79 | 80 | { this.state.searching && !this.state.loading ? this.renderPlaylist() : null } 81 | 82 | 83 | { this.state.loading ? : null } 84 | 85 | 86 | { this.state.modalOpen ? this.renderModal() : null } 87 | 88 | 89 | { this.state.alertOpen ? this.renderAlert() : null } 90 | 91 |
; 93 | } 94 | 95 | renderTop() { 96 | return ; 97 | } 98 | 99 | renderSearch() { 100 | return
101 | 102 | <SearchBox country={this.state.country}/> 103 | <Tip/> 104 | </div>; 105 | } 106 | 107 | renderPlaylist() { 108 | return <Playlist 109 | mainTrack={this.state.mainTrack} 110 | tracks={this.state.tracks} 111 | country={this.state.country} 112 | />; 113 | } 114 | 115 | renderModal() { 116 | return <Modal user={this.state.user} token={this.state.token}/>; 117 | } 118 | 119 | renderAlert() { 120 | return <Alert 121 | username={this.state.user ? this.state.user._display_name : null} 122 | status={this.state.alert} 123 | lastPlaylist={this.state.lastPlaylist} 124 | />; 125 | } 126 | } 127 | 128 | ReactDOM.render( 129 | <App/>, 130 | document.getElementById('container') 131 | ); 132 | -------------------------------------------------------------------------------- /app/js/components/Alert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | import {close} from '../actions/AlertActions'; 5 | import {login} from '../actions/UserActions'; 6 | 7 | class Alert extends Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | if (this.props.status.fail) { 12 | setTimeout(() => { 13 | close(); 14 | }, 3000); 15 | } 16 | } 17 | 18 | _handleDone() { 19 | close(); 20 | ga('send', 'event', 'button', 'click', 'alert-close'); 21 | } 22 | 23 | _handleLogin() { 24 | login(); 25 | close(); 26 | } 27 | 28 | _hanbleShareFB() { 29 | let url = `http://facebook.com/sharer.php?s=100&p[url]=http://www.magicplaylist.co`; 30 | open( 31 | url, 32 | 'fbshare', 33 | 'height=380,width=660,resizable=0,toolbar=0,menubar=0,status=0,location=0,scrollbars=0' 34 | ); 35 | ga('send', 'event', 'button', 'click', 'share-fb'); 36 | } 37 | 38 | _hanbleShareTW() { 39 | let url = `https://twitter.com/intent/tweet?text=Checkout my new playlist on Spotify! 40 | ${this.props.lastPlaylist}. Create yours on http://www.magicplaylist.co&hashtags=MagicPlaylist`; 41 | open( 42 | url, 43 | 'tshare', 44 | 'height=400,width=550,resizable=1,toolbar=0,menubar=0,status=0,location=0' 45 | ); 46 | ga('send', 'event', 'button', 'click', 'share-tw'); 47 | } 48 | 49 | render() { 50 | return <div className='alert-shadow'> 51 | <div className='alert-modal'> 52 | { this.props.status.loading ? this.renderLoading() : null } 53 | { this.props.status.fail ? this.renderFail() : null } 54 | { this.props.status.share ? this.renderShare() : null } 55 | { this.props.status.limit ? this.renderLimit() : null } 56 | </div> 57 | </div>; 58 | } 59 | 60 | renderShare() { 61 | return <div className='alert-share'> 62 | <span className='share-title'>High five {this.props.username}!</span> 63 | <span className='share-subtitle'>Your playlist is now on Spotify.</span> 64 | <span className='share-cta'> 65 | Go to your playlists lists or <a 66 | href={this.props.lastPlaylist} 67 | target='_blank' 68 | >play it online</a>. 69 | </span> 70 | <span className='share-message'>Share it with your friends</span> 71 | <div className='share'> 72 | <div className='facebook source' onClick={this._hanbleShareFB.bind(this)}> 73 | <img src='img/facebook.svg'/> 74 | </div> 75 | <div className='twitter source' onClick={this._hanbleShareTW.bind(this)}> 76 | <img src='img/twitter.svg'/> 77 | </div> 78 | </div> 79 | <div className='btn-done' onClick={this._handleDone}>Close window</div> 80 | </div>; 81 | } 82 | 83 | renderLimit() { 84 | return <div className='alert-limit'> 85 | <span className='limit-title'>Ohh to many people here!</span> 86 | <span className='limit-subtitle'> 87 | Try again in a seconds or login for ensure your search. 88 | </span> 89 | <div className='btn-close' onClick={this._handleDone}>Close</div> 90 | <div className='btn-done' onClick={this._handleLogin}>Login</div> 91 | </div>; 92 | } 93 | 94 | renderFail() { 95 | return <div className='alert-fail'> 96 | <span>Huston, we have a problem here! :(</span> 97 | <img src='img/fail.svg'/> 98 | </div>; 99 | } 100 | 101 | renderLoading() { 102 | return <div className='alert-loading'><img src='img/audio.svg'/></div>; 103 | } 104 | 105 | } 106 | 107 | export default Alert; 108 | -------------------------------------------------------------------------------- /app/js/components/Footer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | 5 | class Footer extends Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | _handleClick(text) { 12 | ga('send', 'event', 'button', 'footer', text); 13 | } 14 | 15 | render() { 16 | let style = !this.props.tracks ? 'footer fixed' : 'footer'; 17 | return <div className={style}> 18 | <div className='copy'> 19 | Developed by <a 20 | href='https://github.com/loverajoel' 21 | target='_blank' 22 | onClick={this._handleClick.bind(this, '@loverajoel')} 23 | >@loverajoel 24 | </a> / Designed by <a 25 | href='https://dribbble.com/curva' 26 | target='_blank' 27 | onClick={this._handleClick.bind(this, '@aschelstraete')}>@aschelstraete 28 | </a> / <a 29 | href='https://github.com/loverajoel/magicplaylist' 30 | onClick={this._handleClick.bind(this, 'github')} 31 | target='_blank' 32 | >Source Code 33 | </a> / <a 34 | href='https://twitter.com/magicplaylistco' 35 | onClick={this._handleClick.bind(this, 'twitter')} 36 | target='_blank'>@magicplaylistco</a> 37 | </div> 38 | <div className='spotify-api'>Created using the API of <a 39 | href='https://developer.spotify.com/web-api/' 40 | target='_blank'><img src='img/spotify-logo.png' 41 | /></a> 42 | </div> 43 | </div>; 44 | } 45 | } 46 | 47 | export default Footer; 48 | -------------------------------------------------------------------------------- /app/js/components/Loading.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | let Loading = () => { 6 | return <img className='loading' src='img/audio.svg'/>; 7 | }; 8 | 9 | export default Loading; 10 | -------------------------------------------------------------------------------- /app/js/components/Modal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | import {close} from '../actions/ModalActions'; 7 | import {login} from '../actions/UserActions'; 8 | import {save} from '../actions/PlaylistActions'; 9 | 10 | import PlaylistStore from '../stores/PlaylistStore'; 11 | import UserStore from '../stores/UserStore'; 12 | 13 | class Modal extends Component { 14 | 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | inputError: false, 19 | playlistName: '', 20 | playlistPublic: true 21 | }; 22 | } 23 | 24 | componentDidMount() { 25 | ReactDOM.findDOMNode(this.refs.playlistName).focus(); 26 | } 27 | 28 | _handleClose() { 29 | close(); 30 | ga('send', 'event', 'button', 'click', 'close-modal'); 31 | } 32 | 33 | _savePlaylist() { 34 | const playlistName = ReactDOM.findDOMNode(this.refs.playlistName).value; 35 | close(); 36 | save( 37 | UserStore.getUser()._id, 38 | playlistName, 39 | this.state.playlistPublic, PlaylistStore.getTracks() 40 | ); 41 | } 42 | 43 | _validateForm() { 44 | const playlistName = ReactDOM.findDOMNode(this.refs.playlistName).value; 45 | let isValid = playlistName.length > 3; 46 | this.setState({ 47 | inputError: !isValid 48 | }); 49 | 50 | return isValid; 51 | } 52 | 53 | _handleSave() { 54 | if (!this._validateForm()) { 55 | return; 56 | } 57 | if (this.props.token && 58 | this.props.user && 59 | Number(localStorage.magic_token_expires) > Date.now()) 60 | { 61 | this._savePlaylist(); 62 | } else { 63 | login().then(() => { 64 | this._savePlaylist(); 65 | }); 66 | } 67 | ga('send', 'event', 'button', 'click', 'playlist-save'); 68 | } 69 | 70 | _handlePublic(status) { 71 | this.setState({ 72 | playlistPublic: status 73 | }); 74 | } 75 | 76 | render() { 77 | let inputPlaceholder = this.state.inputError ? 'Please enter a valid name!' : 'Name'; 78 | let inputClass = this.state.inputError ? 'playlist-name error' : 'playlist-name'; 79 | return <div className='modal'> 80 | <div className='modal-container'> 81 | <div className='close-modal'> 82 | <img src='img/close.svg' onClick={this._handleClose}/> 83 | </div> 84 | <div> 85 | <input 86 | type='text' 87 | placeholder={inputPlaceholder} 88 | className={inputClass} 89 | ref='playlistName' 90 | /> 91 | </div> 92 | <span className='status'>Playlist Status</span> 93 | <div className='radio-container'> 94 | <input id='true' 95 | type='radio' 96 | name='public' 97 | value='true' 98 | onChange={this._handlePublic.bind(this, true)} 99 | defaultChecked={true} 100 | /> 101 | <label htmlFor='true'>Public</label> 102 | <input 103 | id='false' 104 | type='radio' 105 | name='public' 106 | value='false' 107 | onChange={this._handlePublic.bind(this, true)} 108 | /> 109 | <label htmlFor='false'>Private</label> 110 | </div> 111 | <div> 112 | <button 113 | className='save' 114 | type='button' 115 | onClick={this._handleSave.bind(this)} 116 | >Save playlist</button> 117 | </div> 118 | </div> 119 | </div>; 120 | } 121 | } 122 | 123 | export default Modal; 124 | -------------------------------------------------------------------------------- /app/js/components/Player.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | class Track extends Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | src: this.props.source, 12 | elem: null, 13 | isPlaying: false, 14 | isLoading: false 15 | }; 16 | this.playerEvent = {}; 17 | } 18 | 19 | componentDidMount() { 20 | this.audioTag = ReactDOM.findDOMNode(this.refs.audio); 21 | this.props.ptag(this.audioTag); 22 | 23 | this.playerEvent.loadStart = () => { 24 | this.setState({ 25 | isLoading: true 26 | }); 27 | }; 28 | 29 | this.playerEvent.loadEnd = () => { 30 | this.setState({ 31 | isLoading: false 32 | }); 33 | }; 34 | 35 | this.playerEvent.isPlaying = () => { 36 | this.setState({ 37 | isPlaying: false 38 | }); 39 | }; 40 | 41 | this.audioTag.addEventListener('ended', this.playerEvent.isPlaying); 42 | this.audioTag.addEventListener('pause', this.playerEvent.isPlaying); 43 | this.audioTag.addEventListener('loadeddata', this.playerEvent.loadEnd); 44 | this.audioTag.addEventListener('loadstart', this.playerEvent.loadStart); 45 | this.audioTag.addEventListener('suspend', this.playerEvent.loadEnd); 46 | } 47 | 48 | componentWillUnmount() { 49 | this.audioTag.removeEventListener('ended', this.playerEvent.isPlaying); 50 | this.audioTag.removeEventListener('pause', this.playerEvent.isPlaying); 51 | this.audioTag.removeEventListener('loadeddata', this.playerEvent.loadEnd); 52 | this.audioTag.removeEventListener('loadstart', this.playerEvent.loadStart); 53 | this.audioTag.removeEventListener('suspend', this.playerEvent.loadStart); 54 | } 55 | 56 | _play() { 57 | this.props.stopAll.apply(); 58 | this.audioTag.play(); 59 | this.setState({ 60 | isPlaying: true 61 | }); 62 | ga('send', 'event', 'button', 'click', 'playlist-play'); 63 | } 64 | 65 | _stop() { 66 | this.audioTag.pause(); 67 | this.setState({ 68 | isPlaying: false 69 | }); 70 | ga('send', 'event', 'button', 'click', 'playlist-stop'); 71 | } 72 | 73 | render() { 74 | return <div> 75 | { !this.state.isPlaying && !this.state.isLoading ? 76 | <img src='img/volume.svg' onClick={this._play.bind(this)}/> : null 77 | } 78 | { this.state.isPlaying && !this.state.isLoading ? 79 | <img src='img/pause.svg' onClick={this._stop.bind(this)}/> : null 80 | } 81 | { this.state.isLoading ? 82 | <img src='img/tail-spin.svg' className='player-loading'/> : null 83 | } 84 | <audio ref='audio' src={this.props.source} preload='none'/> 85 | </div>; 86 | } 87 | } 88 | 89 | export default Track; 90 | -------------------------------------------------------------------------------- /app/js/components/Playlist.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | import Track from './track'; 5 | import {open} from '../actions/ModalActions'; 6 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 7 | 8 | class Playlist extends Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | audios: [] 14 | }; 15 | } 16 | 17 | _handleSave() { 18 | open(); 19 | ga('send', 'event', 'button', 'click', 'open-modal-save-playlist'); 20 | } 21 | 22 | _add(elem) { 23 | this.state.audios.push(elem); 24 | } 25 | 26 | _stopAll() { 27 | this.state.audios.map((item) => { 28 | item.pause(); 29 | }); 30 | } 31 | 32 | render() { 33 | var tracks = this.props.tracks.map((track, i) => { 34 | return <Track 35 | track={track} 36 | key={'track_' + track._id} 37 | index={i} 38 | ptag={this._add.bind(this)} 39 | stopAll={this._stopAll.bind(this)} 40 | country={this.props.country} 41 | />; 42 | }); 43 | return <div className='playlist'> 44 | <div className='info'> 45 | { !this.props.tracks.length ? 46 | <div className='track-name'>Hey! The track doesn't exist! :(</div> : null 47 | } 48 | { this.props.mainTrack ? 49 | <div className='track-name'> 50 | <strong> 51 | {this.props.mainTrack.name} 52 | </strong>, {this.props.mainTrack.artists.first().name} 53 | </div> : null 54 | } 55 | { this.props.tracks.length ? 56 | <div className='save-playlist' onClick={this._handleSave}> 57 | Save playlist on Spotify 58 | </div> : null 59 | } 60 | </div> 61 | <ul className='trackList'> 62 | <ReactCSSTransitionGroup 63 | transitionName='fadeList' 64 | transitionEnterTimeout={0} 65 | transitionLeaveTimeout={0} 66 | > 67 | {tracks} 68 | </ReactCSSTransitionGroup> 69 | </ul> 70 | </div>; 71 | } 72 | } 73 | 74 | export default Playlist; 75 | -------------------------------------------------------------------------------- /app/js/components/SearchBox.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import Autosuggest from 'react-autosuggest'; 6 | 7 | import {newSearch} from '../actions/SearchActions'; 8 | import PlaylistActions from '../actions/PlaylistActions'; 9 | 10 | import Spotify from '../core/Spotify'; 11 | 12 | class SearchBox extends Component { 13 | 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | initialValue: this.props.value 18 | }; 19 | } 20 | 21 | _search(text) { 22 | newSearch(text); 23 | PlaylistActions.search(text, this.props.country); 24 | ga('send', 'event', 'event', 'new-search', text); 25 | } 26 | 27 | _handleSearch() { 28 | // I know that's ugly :( 29 | const text = document.querySelector('#search-input').value; 30 | if (text.length > 3) { 31 | this._search(text); 32 | ga('send', 'event', 'button', 'click', 'search-box-input'); 33 | } 34 | } 35 | 36 | _handleKeyPress(event) { 37 | if (event.key === 'Enter') { 38 | const text = event.target.value; 39 | if (text.length > 3) { 40 | this._search(text); 41 | ga('send', 'event', 'key', 'press', 'search-box-enter'); 42 | } 43 | } 44 | } 45 | 46 | render() { 47 | let country = this.props.country; 48 | let time; 49 | let getSuggestions = (input, callback) => { 50 | if (time) { 51 | clearTimeout(time); 52 | } 53 | time = setTimeout(() => { 54 | Spotify.autocomplete(input, country).then((tracks) => { 55 | ga('send', 'event', 'load', 'suggestion', 'show'); 56 | callback(null, tracks); 57 | }); 58 | }, 500); 59 | }; 60 | 61 | let suggestionRenderer = (track) => { 62 | return <span>{track.name}, {track.artists.first().name}</span>; 63 | }; 64 | 65 | let getSuggestionValue = (track) => { 66 | return `${track.name}, ${track.artists.first().name}`; 67 | }; 68 | 69 | let showWhen = (input) => { 70 | return input.trim().length > 3; 71 | }; 72 | 73 | let onSuggestionSelected = (suggestion) => { 74 | ga('send', 'event', 'click', 'suggestion', 'click-suggestion'); 75 | this._search(suggestion); 76 | }; 77 | 78 | const inputAttributes = { 79 | id: 'search-input', 80 | type: 'text', 81 | ref: 'searchInput', 82 | className: 'input-search', 83 | placeholder: 'What is your favorite song?', 84 | onKeyPress: this._handleKeyPress.bind(this) 85 | }; 86 | 87 | return <div className='search-box'> 88 | <div className='search-group'> 89 | <span className='input-group-btn'> 90 | <div className='btn-search' onClick={this._handleSearch.bind(this)}> 91 | <img src='img/search.svg'/> 92 | </div> 93 | </span> 94 | <Autosuggest 95 | suggestions={getSuggestions} 96 | onSuggestionSelected={onSuggestionSelected.bind(this)} 97 | inputAttributes={inputAttributes} 98 | defaultValue={this.state.initialValue} 99 | suggestionRenderer={suggestionRenderer} 100 | suggestionValue={getSuggestionValue} 101 | showWhen={showWhen} 102 | cache={true} 103 | /> 104 | </div> 105 | </div>; 106 | } 107 | 108 | } 109 | 110 | export default SearchBox; 111 | -------------------------------------------------------------------------------- /app/js/components/Tip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | 5 | let Tip = () => { 6 | return <div className='tip'> 7 | <span>Tip: Type a song + artist for better results.</span> 8 | <span>(ex: Billie Jean, Michael Jackson)</span> 9 | </div>; 10 | }; 11 | 12 | export default Tip; 13 | -------------------------------------------------------------------------------- /app/js/components/Title.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | let Title = () => { 6 | return <div className='title auto-hidden'> 7 | <h1><strong>Magic</strong>Playlist /</h1> 8 | <h3>Get the playlist of your dreams based on a song.</h3> 9 | </div>; 10 | }; 11 | 12 | export default Title; 13 | -------------------------------------------------------------------------------- /app/js/components/Top.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | import {resetSearch} from '../actions/SearchActions'; 5 | import SearchBox from './SearchBox'; 6 | 7 | class Top extends Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | _handleTitle() { 14 | resetSearch(); 15 | } 16 | 17 | render() { 18 | return <div className='top'> 19 | <div className='title' onClick={this._handleTitle}> 20 | <span><strong>Magic</strong>Playlist /</span> 21 | </div> 22 | <div className='search'> 23 | <SearchBox value={this.props.search} country={this.props.country}/> 24 | </div> 25 | </div>; 26 | } 27 | } 28 | 29 | export default Top; 30 | -------------------------------------------------------------------------------- /app/js/components/Track.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React, {Component} from 'react'; 4 | import PlaylistActions from '../actions/PlaylistActions'; 5 | import Player from './Player'; 6 | 7 | class Track extends Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | } 12 | 13 | _remove() { 14 | PlaylistActions.removeTrack(this.props.index); 15 | ga('send', 'event', 'button', 'click', 'playlist-remove-track'); 16 | } 17 | 18 | _handleReSearch() { 19 | PlaylistActions.search(this.props.track, this.props.country); 20 | ga('send', 'event', 'event', 'new-re-search', this.props.track.name); 21 | } 22 | 23 | render() { 24 | let track = this.props.track; 25 | return <li> 26 | <div className='track-name'>{track.name}, {track.artists.first().name}</div> 27 | <div 28 | className='remove tooltip center' 29 | onClick={this._remove.bind(this)} 30 | data-tooltip='Remove this track' 31 | > 32 | <img src='img/remove.svg'/> 33 | </div> 34 | <div className='play tooltip center' data-tooltip='Preview this track'> 35 | <Player 36 | source={track.preview_url} 37 | ptag={this.props.ptag.bind(this)} 38 | stopAll={this.props.stopAll.bind(this)}/> 39 | </div> 40 | <div 41 | className='re-search tooltip center' 42 | onClick={this._handleReSearch.bind(this)} 43 | data-tooltip='Make a new playlist!' 44 | > 45 | <img src='img/reload.svg'/> 46 | </div> 47 | </li>; 48 | } 49 | } 50 | 51 | export default Track; 52 | -------------------------------------------------------------------------------- /app/js/constants/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default { 4 | SEARCH_ADD: 'search-add', 5 | SEARCH_UPDATE: 'search-update', 6 | SEARCH_RESET: 'search-reset', 7 | PLAYLIST_ADD_TRACKS: 'playlist-add-tracks', 8 | PLAYLIST_REMOVE_TRACK: 'playlist-remove-track', 9 | PLAYLIST_REMOVE_TRACKS: 'playlist-remove-tracks', 10 | PLAYLIST_LOADING: 'playlist-loading', 11 | PLAYLIST_CREATED: 'paylist-created', 12 | PLAYLIST_SAVING: 'paylist-saving', 13 | PLAYLIST_FAILED: 'paylist-failded', 14 | PLAYLIST_TRACK_NOT_FOUND: 'paylist-track-not-found', 15 | PLAYLIST_SAVE_FAIL: 'paylist-save-fail', 16 | PLAYLIST_LIMIT_429: 'playlist-limit', 17 | MODAL_OPEN: 'modal-open', 18 | MODAL_CLOSE: 'modal-close', 19 | USER_LOGED: 'user-loged', 20 | USER_TOKEN: 'user-token', 21 | USER_LOGOUT: 'user-logout', 22 | USER_COUNTRY: 'user-country', 23 | ALERT_OPEN: 'alert-open', 24 | ALERT_CLOSE: 'alert-close' 25 | }; 26 | -------------------------------------------------------------------------------- /app/js/core/Magic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Magic algorithm 3 | 4 | let closest = function(list, x, cant) { 5 | let final_list = []; 6 | let final_cant = list.length > cant ? cant : list.length; 7 | 8 | let search_closest = function(x) { 9 | return list.sort(function(prev, next) { 10 | return Math.abs(x - prev.popularity) - Math.abs(x - next.popularity); 11 | }).splice(0, 1)[0]; 12 | }; 13 | 14 | let get = function() { 15 | if (final_list.length !== final_cant) { 16 | final_list.push(search_closest(x)); 17 | return get(); 18 | } else { 19 | return final_list; 20 | } 21 | }; 22 | return get(x); 23 | }; 24 | 25 | let alternate = function(list) { 26 | let index = 0; 27 | let list_size = list.length; 28 | let process = function(list_process) { 29 | // Search the next item different, remove and return this. 30 | let serchNextDifferent = function(number) { 31 | for (let i = index + 1; i <= list_size; i++) { 32 | if (list_process[i] && list_process[i].artists.first().id !== number) { 33 | return list_process.splice(i, 1)[0]; 34 | } 35 | }; 36 | }; 37 | // Search the next item different, remove and return this. 38 | let serchPrevDifferent = function(number, index) { 39 | for (let i = index - 1; i >= 0; i--) { 40 | if (list_process[i] && 41 | list_process[i].artists.first().id !== number && 42 | list_process[i].artists.first().id !== list_process[index].artists.first().id && 43 | number !== list_process[i - 1].artists.first().id && 44 | i) 45 | { 46 | return list_process.splice(i, 1)[0]; 47 | } 48 | }; 49 | }; 50 | // Check if the current item and the prev are equals 51 | if (list_process[index - 1] && 52 | list_process[index - 1].artists.first().id === list_process[index].artists.first().id) 53 | { 54 | let next = serchNextDifferent(list_process[index].artists.first().id); 55 | if (next) { 56 | list_process.splice(index, 0, next); 57 | } else { 58 | let prev = serchPrevDifferent(list_process[index].artists.first().id, index); 59 | if (prev) { 60 | list_process.splice(index - 1, 0, prev); 61 | } else { 62 | list_process.push(list_process.splice(index, 1)[0]); 63 | } 64 | } 65 | } 66 | // next 67 | if (list_size - 1 !== index) { 68 | index++; 69 | return process(list_process); 70 | } else { 71 | return list_process; 72 | } 73 | }; 74 | return process(list); 75 | }; 76 | 77 | let orderByPopularity = (list) => { 78 | return list.sort((a, b) => { 79 | return a.popularity - b.popularity; 80 | }).reverse(); 81 | }; 82 | 83 | let magic = (list, points) => { 84 | return alternate(orderByPopularity(closest(alternate(orderByPopularity(list)), points, 30))); 85 | }; 86 | 87 | export default { 88 | closest, 89 | alternate, 90 | magic 91 | }; 92 | -------------------------------------------------------------------------------- /app/js/core/Spotify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {Client, TrackHandler, PlaylistHandler, ArtistHandler, UserHandler} from 'spotify-sdk'; 4 | import {magic} from './Magic'; 5 | 6 | let client = Client.instance; 7 | 8 | client.settings = { 9 | clientId: '', 10 | secretId: '', 11 | scopes: 'playlist-modify-public playlist-modify-private', 12 | redirect_uri: 'http://localhost:3000/app/login/index.html' 13 | }; 14 | 15 | let settings = { 16 | tracks: 20, 17 | artists: 20 18 | }; 19 | 20 | let track = new TrackHandler(); 21 | let user = new UserHandler(); 22 | let playlist = new PlaylistHandler(); 23 | 24 | let total = 0; 25 | 26 | let Spotify = { 27 | trackList: [], 28 | 29 | autocomplete: (text, country) => { 30 | return track.search(text, {limit: 5, market: country}); 31 | }, 32 | search: (text, country, callback, fail) => { 33 | if (text.id) { 34 | return Spotify.getTracks(text, country, callback, fail); 35 | } else { 36 | track.search(text, {limit: 1, market: country}).then((trackCollection) => { 37 | if (trackCollection.length) { 38 | Spotify.getTracks(trackCollection.first(), country, callback, fail); 39 | } else { 40 | callback([]); 41 | } 42 | }).catch(fail); 43 | } 44 | }, 45 | 46 | getTracks: (track, country, callback, fail) => { 47 | Spotify.trackList = []; 48 | track.artists.first().relatedArtists().then((relatedArtists) => { 49 | relatedArtists = relatedArtists.slice(0, settings.artists - 1); 50 | if (relatedArtists.length) { 51 | relatedArtists.push(track.artists.first()); 52 | for (var i = relatedArtists.length - 1; i >= 0; i--) { 53 | total = relatedArtists.length - 1; 54 | relatedArtists[i].topTracks({country: country}).then((tracks) => { 55 | if (tracks.length) { 56 | for (var e = tracks.length - 1; e >= 0; e--) { 57 | Spotify.trackList.push(tracks[e]); 58 | if (e === 0) { 59 | total -= 1; 60 | if (total === 0) { 61 | callback( 62 | magic( 63 | Spotify.trackList, 64 | track.popularity 65 | ), track 66 | ); 67 | } 68 | } 69 | }; 70 | } else { 71 | total -= 1; 72 | } 73 | }).catch(fail); 74 | }; 75 | } else { 76 | callback([]); 77 | } 78 | }).catch(fail); 79 | }, 80 | 81 | login: () => { 82 | return new Promise((resolve, reject) => { 83 | client.login((url) => { 84 | window.open( 85 | url, 86 | 'Spotify', 87 | 'menubar=no,location=no,resizable=yes,scrollbars=yes,status=no,width=400,height=500' 88 | ); 89 | // :D 90 | window.addEventListener('storage', (data) => { 91 | if (data.key === 'magic_token') { 92 | resolve(data.newValue); 93 | } 94 | }); 95 | }); 96 | }); 97 | }, 98 | 99 | getUser: () => { 100 | client.token = localStorage.magic_token; 101 | return new Promise((resolve, reject) => { 102 | user.me().then((userEntity) => { 103 | localStorage.magic_user = JSON.stringify(userEntity); 104 | resolve(userEntity); 105 | }).catch((error) => { 106 | reject(error); 107 | }); 108 | }); 109 | }, 110 | 111 | savePlaylist: (userId, name, isPublic, tracks) => { 112 | client.token = localStorage.magic_token; 113 | return new Promise((resolve, reject) => { 114 | playlist.create(userId, name + ' by magicplaylist.co', isPublic).then((myPlaylist) => { 115 | myPlaylist.addTrack(tracks).then((snapshot) => { 116 | resolve(myPlaylist); 117 | }); 118 | }).catch((error) => { 119 | reject(error); 120 | }); 121 | }); 122 | } 123 | }; 124 | 125 | export default Spotify; 126 | -------------------------------------------------------------------------------- /app/js/dispatcher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {Dispatcher} from 'flux'; 4 | 5 | export default new Dispatcher(); 6 | -------------------------------------------------------------------------------- /app/js/stores/AlertStore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {EventEmitter} from 'events'; 4 | import Dispatcher from '../dispatcher'; 5 | import { 6 | ALERT_OPEN, 7 | ALERT_CLOSE, 8 | PLAYLIST_SAVING, 9 | PLAYLIST_CREATED, 10 | PLAYLIST_FAILED, 11 | USER_TOKEN_ERROR, 12 | PLAYLIST_LIMIT_429 13 | } from '../constants/constants'; 14 | 15 | let CHANGE_EVENT = 'change'; 16 | 17 | let _isOpen = false; 18 | let _status = { 19 | loading: false, 20 | fail: false, 21 | share: false, 22 | limit: false 23 | }; 24 | 25 | class AlertStore extends EventEmitter { 26 | constructor() { 27 | super(); 28 | this.registerAtDispatcher(); 29 | } 30 | 31 | isOpen() { 32 | return _isOpen; 33 | } 34 | 35 | status() { 36 | return _status; 37 | } 38 | 39 | emitChange() { 40 | this.emit(CHANGE_EVENT); 41 | } 42 | 43 | addChangeListener(callback) { 44 | this.on(CHANGE_EVENT, callback); 45 | } 46 | 47 | removeChangeListener(callback) { 48 | this.removeListener(CHANGE_EVENT, callback); 49 | } 50 | 51 | registerAtDispatcher() { 52 | Dispatcher.register((action) => { 53 | 54 | switch (action.type) { 55 | 56 | case ALERT_OPEN: { 57 | _isOpen = true; 58 | this.emitChange(); 59 | break; 60 | } 61 | 62 | case ALERT_CLOSE: { 63 | _isOpen = false; 64 | this.emitChange(); 65 | break; 66 | } 67 | 68 | case PLAYLIST_SAVING: { 69 | _isOpen = true; 70 | _status.loading = true; 71 | _status.share = false; 72 | this.emitChange(); 73 | break; 74 | } 75 | 76 | case PLAYLIST_CREATED: { 77 | _isOpen = true; 78 | _status.loading = false; 79 | _status.share = true; 80 | this.emitChange(); 81 | break; 82 | } 83 | 84 | case PLAYLIST_FAILED: { 85 | _isOpen = true; 86 | _status.loading = false; 87 | _status.share = false; 88 | _status.fail = true; 89 | this.emitChange(); 90 | break; 91 | } 92 | 93 | case USER_TOKEN_ERROR: { 94 | _isOpen = true; 95 | _status.loading = false; 96 | _status.share = false; 97 | _status.fail = true; 98 | this.emitChange(); 99 | break; 100 | } 101 | 102 | case PLAYLIST_LIMIT_429: { 103 | _isOpen = true; 104 | _status.loading = false; 105 | _status.share = false; 106 | _status.fail = false; 107 | _status.limit = true; 108 | this.emitChange(); 109 | break; 110 | } 111 | 112 | default: { 113 | break; 114 | } 115 | } 116 | }); 117 | } 118 | 119 | } 120 | 121 | export default new AlertStore(); 122 | -------------------------------------------------------------------------------- /app/js/stores/ModalStore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {EventEmitter} from 'events'; 4 | import Dispatcher from '../dispatcher'; 5 | import {MODAL_OPEN, MODAL_CLOSE} from '../constants/constants'; 6 | 7 | let CHANGE_EVENT = 'change'; 8 | 9 | let _isOpen = false; 10 | 11 | class SearchStore extends EventEmitter { 12 | constructor() { 13 | super(); 14 | this.registerAtDispatcher(); 15 | } 16 | 17 | isOpen() { 18 | return _isOpen; 19 | } 20 | 21 | emitChange() { 22 | this.emit(CHANGE_EVENT); 23 | } 24 | 25 | addChangeListener(callback) { 26 | this.on(CHANGE_EVENT, callback); 27 | } 28 | 29 | removeChangeListener(callback) { 30 | this.removeListener(CHANGE_EVENT, callback); 31 | } 32 | 33 | registerAtDispatcher() { 34 | Dispatcher.register((action) => { 35 | 36 | switch (action.type) { 37 | 38 | case MODAL_OPEN: { 39 | _isOpen = true; 40 | this.emitChange(); 41 | break; 42 | } 43 | 44 | case MODAL_CLOSE: { 45 | _isOpen = false; 46 | this.emitChange(); 47 | break; 48 | } 49 | 50 | default: { 51 | break; 52 | } 53 | } 54 | }); 55 | } 56 | 57 | } 58 | 59 | export default new SearchStore(); 60 | -------------------------------------------------------------------------------- /app/js/stores/PlaylistStore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {EventEmitter} from 'events'; 4 | import Dispatcher from '../dispatcher'; 5 | import { 6 | PLAYLIST_ADD_TRACKS, 7 | PLAYLIST_REMOVE_TRACK, 8 | PLAYLIST_LOADING, 9 | PLAYLIST_REMOVE_TRACKS, 10 | PLAYLIST_CREATED, 11 | PLAYLIST_SAVING, 12 | PLAYLIST_TRACK_NOT_FOUND, 13 | SEARCH_RESET 14 | } from '../constants/constants'; 15 | 16 | let CHANGE_EVENT = 'change'; 17 | 18 | let _tracks = []; 19 | let _mainTrack; 20 | let _loading = false; 21 | let _lastPlaylist; 22 | 23 | class PlaylistStore extends EventEmitter { 24 | constructor() { 25 | super(); 26 | this.registerAtDispatcher(); 27 | } 28 | 29 | getTracks() { 30 | return _tracks; 31 | } 32 | 33 | getMainTrack() { 34 | return _mainTrack; 35 | } 36 | 37 | getLoading() { 38 | return _loading; 39 | } 40 | 41 | getLastPlaylist() { 42 | return _lastPlaylist; 43 | } 44 | 45 | emitChange() { 46 | this.emit(CHANGE_EVENT); 47 | } 48 | 49 | addChangeListener(callback) { 50 | this.on(CHANGE_EVENT, callback); 51 | } 52 | 53 | removeChangeListener(callback) { 54 | this.removeListener(CHANGE_EVENT, callback); 55 | } 56 | 57 | registerAtDispatcher() { 58 | Dispatcher.register((action) => { 59 | const {type, tracks} = action; 60 | 61 | switch (type) { 62 | 63 | case PLAYLIST_ADD_TRACKS: { 64 | _tracks = tracks; 65 | _mainTrack = action.mainTrack; 66 | _loading = false; 67 | this.emitChange(); 68 | break; 69 | } 70 | 71 | case PLAYLIST_REMOVE_TRACK: { 72 | _tracks.splice(action.index, 1); 73 | this.emitChange(); 74 | break; 75 | } 76 | 77 | case PLAYLIST_LOADING: { 78 | _loading = true; 79 | this.emitChange(); 80 | break; 81 | } 82 | 83 | case PLAYLIST_REMOVE_TRACKS: { 84 | _tracks = []; 85 | this.emitChange(); 86 | break; 87 | } 88 | 89 | case PLAYLIST_CREATED: { 90 | _lastPlaylist = action.response.external_urls.spotify; 91 | this.emitChange(); 92 | break; 93 | } 94 | 95 | case PLAYLIST_TRACK_NOT_FOUND: { 96 | _tracks = []; 97 | _mainTrack = null; 98 | _loading = false; 99 | this.emitChange(); 100 | break; 101 | } 102 | 103 | case SEARCH_RESET: { 104 | _tracks = []; 105 | _mainTrack = null; 106 | _loading = false; 107 | this.emitChange(); 108 | break; 109 | } 110 | 111 | default: { 112 | break; 113 | } 114 | 115 | } 116 | }); 117 | } 118 | } 119 | 120 | export default new PlaylistStore(); 121 | -------------------------------------------------------------------------------- /app/js/stores/SearchStore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {EventEmitter} from 'events'; 4 | import Dispatcher from '../dispatcher'; 5 | import {SEARCH_ADD, SEARCH_RESET} from '../constants/constants'; 6 | 7 | let CHANGE_EVENT = 'change'; 8 | 9 | let _currentSearch = ''; 10 | 11 | class SearchStore extends EventEmitter { 12 | constructor() { 13 | super(); 14 | this.registerAtDispatcher(); 15 | } 16 | 17 | getSearch() { 18 | return _currentSearch; 19 | } 20 | 21 | emitChange() { 22 | this.emit(CHANGE_EVENT); 23 | } 24 | 25 | addChangeListener(callback) { 26 | this.on(CHANGE_EVENT, callback); 27 | } 28 | 29 | removeChangeListener(callback) { 30 | this.removeListener(CHANGE_EVENT, callback); 31 | } 32 | 33 | registerAtDispatcher() { 34 | Dispatcher.register((action) => { 35 | const {type, text} = action; 36 | switch (type) { 37 | 38 | case SEARCH_ADD: { 39 | _currentSearch = text.id ? `${text.name}, ${text.artists.first().name}` : text; 40 | this.emitChange(); 41 | break; 42 | } 43 | 44 | case SEARCH_RESET: { 45 | _currentSearch = ''; 46 | this.emitChange(); 47 | break; 48 | } 49 | 50 | default: { 51 | break; 52 | } 53 | } 54 | }); 55 | } 56 | 57 | } 58 | 59 | export default new SearchStore(); 60 | -------------------------------------------------------------------------------- /app/js/stores/UserStore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {EventEmitter} from 'events'; 4 | import Dispatcher from '../dispatcher'; 5 | import {USER_LOGED, USER_TOKEN, USER_LOGOUT, USER_COUNTRY} from '../constants/constants'; 6 | 7 | let CHANGE_EVENT = 'change'; 8 | 9 | let _user = JSON.parse(localStorage.getItem('magic_user')) || null; 10 | let _token = localStorage.getItem('magic_token') || null; 11 | let _country = localStorage.getItem('magic_country') || 'US'; 12 | 13 | class SearchStore extends EventEmitter { 14 | constructor() { 15 | super(); 16 | this.registerAtDispatcher(); 17 | } 18 | 19 | getUser() { 20 | return _user; 21 | } 22 | 23 | getToken() { 24 | return _token; 25 | } 26 | 27 | getCountry() { 28 | return _country; 29 | } 30 | 31 | emitChange() { 32 | this.emit(CHANGE_EVENT); 33 | } 34 | 35 | addChangeListener(callback) { 36 | this.on(CHANGE_EVENT, callback); 37 | } 38 | 39 | removeChangeListener(callback) { 40 | this.removeListener(CHANGE_EVENT, callback); 41 | } 42 | 43 | registerAtDispatcher() { 44 | Dispatcher.register((action) => { 45 | 46 | switch (action.type) { 47 | 48 | case USER_LOGED: { 49 | _user = action.data; 50 | this.emitChange(); 51 | break; 52 | } 53 | 54 | case USER_TOKEN: { 55 | _token = action.data; 56 | this.emitChange(); 57 | break; 58 | } 59 | 60 | case USER_LOGOUT: { 61 | _user = null; 62 | this.emitChange(); 63 | break; 64 | } 65 | 66 | case USER_COUNTRY: { 67 | _country = action.data; 68 | this.emitChange(); 69 | break; 70 | } 71 | 72 | default: { 73 | break; 74 | } 75 | } 76 | }); 77 | } 78 | 79 | } 80 | 81 | export default new SearchStore(); 82 | -------------------------------------------------------------------------------- /app/login/index.html: -------------------------------------------------------------------------------- 1 | <html> 2 | <head> 3 | <title>MagicPlaylist/ 4 | 5 | 6 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/styles/style.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Montserrat:400+700); 2 | @import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro); 3 | 4 | body { 5 | margin: 0px; 6 | padding: 0px; 7 | height: 100%; 8 | font-family: 'Montserrat', sans-serif; 9 | background: #5e04c2; 10 | } 11 | 12 | div { 13 | box-sizing: border-box; 14 | } 15 | 16 | *:focus { 17 | outline: 0; 18 | } 19 | 20 | .auto-hidden { 21 | overflow: hidden; 22 | } 23 | 24 | .hidden { 25 | display: none 26 | } 27 | 28 | .container { 29 | width: 100%; 30 | float: left; 31 | } 32 | 33 | strong { 34 | font-weight: 700; 35 | } 36 | 37 | a { 38 | text-decoration: underline; 39 | } 40 | 41 | /* Search Containter Component */ 42 | 43 | .search-container { 44 | width: 600px; 45 | margin: -100px 0 0 -300px; 46 | position: absolute; 47 | top: 40%; 48 | left: 50%; 49 | } 50 | 51 | .search-container h1 { 52 | font-family: 'Montserrat', sans-serif; 53 | font-size: 26px; 54 | color: #fff; 55 | font-weight: 400; 56 | text-align: center; 57 | margin: 0px; 58 | } 59 | 60 | .search-container h3 { 61 | font-family: 'Montserrat', sans-serif; 62 | font-size: 18px; 63 | color: #fff; 64 | font-weight: 400; 65 | text-align: center; 66 | width: 100%; 67 | float: left; 68 | margin: 10px 0 0 0; 69 | } 70 | 71 | @media screen and (max-width: 700px) { 72 | .search-container { 73 | width: 90%; 74 | margin: -100px 0 0 -45%; 75 | position: absolute; 76 | top: 40%; 77 | left: 50%; 78 | } 79 | } 80 | 81 | /* Search Box Component */ 82 | 83 | .search-box { 84 | margin: 40px 0 0 0; 85 | } 86 | 87 | .search-group { 88 | width: 100%; 89 | position: relative; 90 | } 91 | 92 | .search-group .input-group-btn { 93 | position: absolute; 94 | left: 0px 95 | } 96 | 97 | .search-group .input-group-btn .btn-search { 98 | width: 52px; 99 | height: 63px; 100 | cursor: pointer; 101 | } 102 | 103 | .search-group .input-group-btn .btn-search img { 104 | width: 24px; 105 | margin: 20px 0 0 11px; 106 | } 107 | 108 | .search-group .input-search { 109 | width: 100%; 110 | font-family: 'Source Sans Pro', sans-serif; 111 | font-size: 18px; 112 | padding: 20px 46px 20px 46px; 113 | border: 0px; 114 | border-radius: 5px; 115 | background: #410287; 116 | color: #fff; 117 | -webkit-transition: 0.2s ease-in-out; 118 | -moz-transition: 0.2s ease-in-out; 119 | -o-transition: 0.2s ease-in-out; 120 | transition: 0.2s ease-in-out; 121 | } 122 | 123 | .search-group .input-search:focus { 124 | background: #773CB5; 125 | } 126 | 127 | .search-group .input-search:focus::-webkit-input-placeholder { 128 | text-align: left; 129 | opacity: 0.5; 130 | } 131 | 132 | .search-group .input-search::-webkit-input-placeholder { 133 | color: #fff; 134 | text-align: center; 135 | } 136 | 137 | .search-group .input-search:-moz-placeholder { /* Firefox 18- */ 138 | color: #fff; 139 | text-align: center; 140 | } 141 | 142 | .search-group .input-search::-moz-placeholder { /* Firefox 19+ */ 143 | color: #fff; 144 | text-align: center; 145 | } 146 | 147 | .search-group .input-search:-ms-input-placeholder { 148 | color: #fff; 149 | text-align: center; 150 | } 151 | 152 | .search-container .tip { 153 | margin: 30px 0 0 0; 154 | } 155 | 156 | .search-container .tip span { 157 | font-size: 14px; 158 | font-family: sans-serif; 159 | color: #C698FA; 160 | float: left; 161 | clear: both; 162 | text-align: center; 163 | width: 100%; 164 | margin: 10px 0 0 0; 165 | } 166 | 167 | .top { 168 | width: 100%; 169 | height: 90px; 170 | float: left; 171 | background: #4f03a5; 172 | position: fixed; 173 | top: 0px; 174 | left: 0px; 175 | z-index: 1 176 | } 177 | 178 | .top .title { 179 | width: 20%; 180 | float: left; 181 | font-family: 'Montserrat', sans-serif; 182 | font-size: 18px; 183 | color: #fff; 184 | font-weight: 400; 185 | text-align: center; 186 | margin: 33px 0 0 0; 187 | cursor: pointer; 188 | } 189 | 190 | .top .search { 191 | width: 60%; 192 | height: 70px; 193 | float: left; 194 | } 195 | 196 | .top .search .search-box { 197 | margin: 20px auto 0 auto; 198 | width: 600px; 199 | } 200 | 201 | .top .search .search-group .input-group-btn .btn-search { 202 | width: 38px; 203 | height: 49px; 204 | cursor: pointer; 205 | } 206 | 207 | .top .search .search-group .input-group-btn .btn-search img { 208 | width: 20px; 209 | margin: 15px 0 0 11px; 210 | } 211 | 212 | .top .search .search-group .input-search { 213 | width: 100%; 214 | font-size: 18px; 215 | padding: 13px 13px 13px 39px; 216 | border: 0px; 217 | } 218 | 219 | .top .close { 220 | width: 15%; 221 | height: 70px; 222 | float: left; 223 | color: #fff; 224 | font-size: 60px; 225 | font-weight: lighter; 226 | padding: 0 0 0 14px; 227 | cursor: pointer; 228 | } 229 | 230 | @media screen and (max-width: 700px) { 231 | .top { 232 | height: 120px; 233 | } 234 | 235 | .top .title { 236 | width: 100%; 237 | margin: 10px 0 0 0; 238 | } 239 | 240 | .top .search { 241 | width: 100%; 242 | height: 70px; 243 | margin: 0 auto; 244 | } 245 | 246 | .top .search .search-box { 247 | width: 90%; 248 | } 249 | } 250 | 251 | /* Playlist Component */ 252 | 253 | .playlist .info { 254 | width: 70%; 255 | margin: 133px auto 0; 256 | text-align: center; 257 | } 258 | 259 | .info .track-name { 260 | font-family: 'Montserrat', sans-serif; 261 | font-size: 40px; 262 | color: #fff; 263 | text-align: center; 264 | width: 100%; 265 | } 266 | 267 | .info .save-playlist { 268 | display: inline-block; 269 | height: 54px; 270 | background: #12FFCC; 271 | font-family: 'Source Sans Pro', sans-serif; 272 | font-size: 18px; 273 | color: #125E4D; 274 | text-align: center; 275 | border-radius: 40px; 276 | padding: 15px 30px; 277 | margin: 45px auto 0 auto; 278 | cursor: pointer; 279 | } 280 | 281 | .trackList { 282 | width: 70%; 283 | margin: 80px auto 100px; 284 | list-style: none; 285 | padding: 0px; 286 | } 287 | 288 | .trackList li { 289 | width: 100%; 290 | float: left; 291 | font-family: 'Source Sans Pro', sans-serif; 292 | font-size: 20px; 293 | color: #fff; 294 | border-bottom: 1px solid #410287; 295 | padding: 13px 10px 296 | } 297 | 298 | .trackList li .remove { 299 | width: 24px; 300 | height: 24px; 301 | float: right; 302 | margin: 0 0 0 15px; 303 | cursor: pointer; 304 | } 305 | 306 | .trackList li .play { 307 | width: 24px; 308 | height: 24px; 309 | float: right; 310 | margin: 0 0 0 15px; 311 | cursor: pointer; 312 | } 313 | 314 | .player-loading { 315 | width: 100%; 316 | } 317 | 318 | .trackList li .track-name { 319 | float: left; 320 | } 321 | 322 | .trackList li .re-search { 323 | width: 24px; 324 | height: 24px; 325 | float: right; 326 | margin: 0 0 0 15px; 327 | cursor: pointer; 328 | } 329 | 330 | .trackList li .re-search img { 331 | transform: rotate(0deg); 332 | -webkit-transform: rotate(0deg); 333 | -ms-transform: rotate(0deg); 334 | transition: all .2s ease-in; 335 | } 336 | 337 | .trackList li .re-search:hover img { 338 | transform: rotate(-360deg); 339 | -webkit-transform: rotate(-360deg); 340 | -ms-transform: rotate(-360deg); 341 | transition: all .5s ease-in; 342 | } 343 | 344 | @media screen and (max-width: 700px) { 345 | .info .save-playlist { 346 | width: 100%; 347 | } 348 | 349 | .trackList { 350 | width: 90%; 351 | } 352 | 353 | .trackList li { 354 | overflow: hidden; 355 | } 356 | 357 | .trackList li .track-name { 358 | width: 85%; 359 | float: left; 360 | } 361 | 362 | .trackList li .play { 363 | margin: 10px 0 0 15px; 364 | } 365 | 366 | .trackList li .re-search { 367 | display: none; 368 | } 369 | } 370 | 371 | /* Modal */ 372 | 373 | .modal { 374 | height: 100%; 375 | width: 100%; 376 | top: 0px; 377 | float: left; 378 | background: #fff; 379 | position: fixed; 380 | overflow: scroll; 381 | z-index: 2 382 | } 383 | 384 | .modal .modal-container { 385 | width: 90%; 386 | margin: 30px auto 0; 387 | } 388 | 389 | .modal-container h2 { 390 | 391 | } 392 | 393 | .modal-container .playlist-name { 394 | width: 100%; 395 | font-family: 'Source Sans Pro', sans-serif; 396 | font-size: 30px; 397 | padding: 9px; 398 | border: 0px; 399 | background: #fff; 400 | color: #000; 401 | border-bottom: 2px solid #ccc; 402 | } 403 | 404 | .modal-container .playlist-name.error::-webkit-input-placeholder { 405 | color: red; 406 | } 407 | 408 | .modal-container .playlist-name.error:-moz-placeholder { /* Firefox 18- */ 409 | color: red; 410 | } 411 | 412 | .modal-container .playlist-name.error::-moz-placeholder { /* Firefox 19+ */ 413 | color: red; 414 | } 415 | 416 | .modal-container .playlist-name.error:-ms-input-placeholder { 417 | color: red; 418 | } 419 | 420 | .modal-container .save { 421 | width: 184px; 422 | height: 54px; 423 | background: #12FFCC; 424 | font-family: 'Source Sans Pro', sans-serif; 425 | font-size: 18px; 426 | color: #125E4D; 427 | text-align: center; 428 | border-radius: 40px; 429 | padding: 16px; 430 | margin: 40px auto 0 auto; 431 | cursor: pointer; 432 | border: 0px; 433 | clear: both; 434 | float: left; 435 | } 436 | 437 | .modal-container .close-modal { 438 | width: 100%; 439 | height: 32px; 440 | float: left; 441 | cursor: pointer; 442 | } 443 | 444 | .modal-container .close-modal img { 445 | width: 32px; 446 | height: 32px; 447 | float: right; 448 | } 449 | 450 | .modal-container .status { 451 | font-family: 'Montserrat', sans-serif; 452 | font-size: 18px; 453 | color: #4C2EE6; 454 | margin: 30px 0 0 0; 455 | float: left; 456 | } 457 | 458 | .modal-container .radio-container { 459 | float: left; 460 | margin: 20px 0 0 0; 461 | clear: both; 462 | } 463 | 464 | .radio-container input[type="radio"] { 465 | display:none; 466 | } 467 | 468 | .radio-container label { 469 | display: inline-block; 470 | background-color: #5e04c2; 471 | padding: 7px 0; 472 | font-family: Arial; 473 | font-size: 16px; 474 | width: 30px; 475 | margin: 10px 0; 476 | height: 17px; 477 | text-indent: 40px; 478 | color: #5e04c2; 479 | border-radius: 5px; 480 | float: left; 481 | clear: both; 482 | cursor: pointer; 483 | } 484 | 485 | .radio-container input[type="radio"]:checked + label { 486 | background-image: url('../img/check.svg'); 487 | background-size: 20px 20px; 488 | background-repeat: no-repeat; 489 | background-position: 5px; 490 | } 491 | 492 | /* Loading Component */ 493 | 494 | .loading { 495 | top: 50%; 496 | left: 50%; 497 | position: absolute; 498 | margin: -40px 0 0 -27px; 499 | } 500 | 501 | /* Footer Component */ 502 | 503 | .footer { 504 | width: 100%; 505 | float: left; 506 | padding: 20px; 507 | } 508 | 509 | .footer.fixed { 510 | position: fixed; 511 | bottom: 0px 512 | } 513 | 514 | .footer .copy { 515 | font-family: 'Source Sans Pro', sans-serif; 516 | font-size: 12px; 517 | color: #fff; 518 | float: left; 519 | padding: 11px 0 0 0; 520 | } 521 | 522 | .footer .copy a { 523 | color: #12FFCC; 524 | text-decoration: none; 525 | } 526 | 527 | .footer .spotify-api { 528 | font-family: 'Source Sans Pro', sans-serif; 529 | font-size: 12px; 530 | color: #fff; 531 | float: right; 532 | } 533 | 534 | .footer .spotify-api span { 535 | float: left; 536 | margin: 11px 10px 0 0; 537 | } 538 | 539 | .footer .spotify-api img { 540 | width: 100px 541 | } 542 | 543 | @media screen and (max-width: 700px) { 544 | .footer { 545 | display: none; 546 | } 547 | } 548 | 549 | /* Alert Component */ 550 | 551 | .alert-shadow { 552 | width: 100%; 553 | height: 100%; 554 | position: fixed; 555 | z-index: 9000; 556 | top: 0px; 557 | background: rgba(65, 2, 135, 0.65); 558 | z-index: 2 559 | } 560 | 561 | .alert-modal { 562 | width: 400px; 563 | min-height: 400px; 564 | position: absolute; 565 | text-align: center; 566 | top: 50%; 567 | left: 50%; 568 | margin: -200px 0 0 -200px; 569 | padding: 15px 0 30px; 570 | box-sizing: border-box; 571 | background: #fff; 572 | border-radius: 5px; 573 | overflow: hidden; 574 | font-family: 'Montserrat', sans-serif; 575 | z-index: 3 576 | } 577 | 578 | .alert-loading { 579 | width: 100%; 580 | height: 100%; 581 | background: #fff; 582 | } 583 | 584 | .alert-loading img { 585 | width: 80px; 586 | position: absolute; 587 | top: 50%; 588 | right: 50%; 589 | margin: -40px -40px 0 0; 590 | } 591 | 592 | .alert-modal .alert-loading img { 593 | width: 48px; 594 | margin: -24px -24px 0 0; 595 | } 596 | 597 | .alert-fail { 598 | width: 100%; 599 | height: 407px; 600 | background: red; 601 | position: absolute; 602 | top: 0px; 603 | } 604 | 605 | .alert-fail img { 606 | width: 80px; 607 | position: absolute; 608 | top: 50%; 609 | right: 50%; 610 | margin: -40px -40px 0 0; 611 | } 612 | 613 | .alert-fail span { 614 | font-size: 20px; 615 | width: 100%; 616 | float: left; 617 | margin: 30% 0 0 0; 618 | text-align: center; 619 | color: #fff; 620 | 621 | } 622 | 623 | .alert-share { 624 | width: 100%; 625 | height: 100%; 626 | } 627 | 628 | .alert-share .share-title { 629 | font-size: 32px; 630 | color: #4C2EE6; 631 | text-align: center; 632 | padding: 15px 0; 633 | width: 100%; 634 | float: left; 635 | } 636 | 637 | .alert-share .share-subtitle, 638 | .alert-share .share-cta { 639 | font-size: 18px; 640 | color: #370273; 641 | text-align: center; 642 | width: 100%; 643 | float: left; 644 | } 645 | .alert-share .share-cta { 646 | font-size: 14px; 647 | margin-top: 10px; 648 | } 649 | 650 | .alert-share .share-subtitle span { 651 | display: block; 652 | color: #370273; 653 | } 654 | 655 | .alert-share .share-message { 656 | font-size: 14px; 657 | color: #4C2EE6; 658 | text-align: center; 659 | width: 100%; 660 | float: left; 661 | margin: 40px 0 0 0; 662 | } 663 | 664 | .alert-share .share { 665 | width: 160px; 666 | margin: 0px 120px; 667 | padding: 10px 0px 0px; 668 | overflow: hidden; 669 | float: left; 670 | } 671 | 672 | .alert-share .share .source { 673 | width: 50px; 674 | height: 50px; 675 | margin: 15px; 676 | float: left; 677 | cursor: pointer; 678 | } 679 | 680 | .alert-share .share .source img { 681 | width: 100%; 682 | float: left; 683 | } 684 | 685 | .alert-share .btn-done { 686 | height: 54px; 687 | background: #12FFCC; 688 | font-family: 'Source Sans Pro', sans-serif; 689 | font-size: 18px; 690 | color: #125E4D; 691 | text-align: center; 692 | display: inline-block; 693 | border-radius: 40px; 694 | padding: 15px 30px; 695 | margin: 40px 0 0; 696 | cursor: pointer; 697 | } 698 | 699 | .alert-limit { 700 | width: 100%; 701 | height: 100%; 702 | } 703 | 704 | .alert-limit .limit-title { 705 | font-size: 32px; 706 | color: #4C2EE6; 707 | text-align: center; 708 | padding: 15px 0; 709 | width: 100%; 710 | float: left; 711 | } 712 | 713 | .alert-limit .limit-subtitle { 714 | font-size: 18px; 715 | color: #370273; 716 | text-align: center; 717 | width: 100%; 718 | float: left; 719 | } 720 | 721 | .alert-limit .btn-done { 722 | height: 54px; 723 | background: #12FFCC; 724 | font-family: 'Source Sans Pro', sans-serif; 725 | font-size: 18px; 726 | color: #125E4D; 727 | text-align: center; 728 | display: inline-block; 729 | border-radius: 40px; 730 | padding: 15px 30px; 731 | margin: 130px 10px 0; 732 | cursor: pointer; 733 | } 734 | 735 | .alert-limit .btn-close { 736 | height: 54px; 737 | background: #F5958B; 738 | font-family: 'Source Sans Pro', sans-serif; 739 | font-size: 18px; 740 | color: #125E4D; 741 | text-align: center; 742 | display: inline-block; 743 | border-radius: 40px; 744 | padding: 15px 30px; 745 | margin: 130px 1px 0; 746 | cursor: pointer; 747 | } 748 | 749 | /**/ 750 | 751 | .fade-enter { 752 | opacity: 0.01; 753 | transition: opacity .2s ease-in; 754 | -webkit-transform: opacity .2s ease-in; 755 | -ms-transform: opacity .2s ease-in; 756 | } 757 | 758 | .fade-enter.fade-enter-active { 759 | opacity: 1; 760 | } 761 | 762 | .fade-leave { 763 | opacity: 1; 764 | transition: opacity .2s ease-in; 765 | -webkit-transform: opacity .2s ease-in; 766 | -ms-transform: opacity .2s ease-in; 767 | } 768 | 769 | .fade-leave.fade-leave-active { 770 | opacity: 0.01; 771 | } 772 | 773 | /* Fade Up */ 774 | .fadeOut-enter { 775 | opacity: 0.00; 776 | transform: translateY(280px); 777 | -webkit-transform: translateY(280px); 778 | -ms-transform: translateY(280px); 779 | transition: all .5s ease-in; 780 | } 781 | 782 | .fadeOut-enter.fadeOut-enter-active { 783 | opacity: 1; 784 | transform: translateY(0px); 785 | -webkit-transform: translateY(0px); 786 | -ms-transform: translateY(0px); 787 | } 788 | 789 | .fadeOut-leave { 790 | opacity: 0; 791 | -webkit-transform: scale(0.6); 792 | -moz-transform: scale(0.6); 793 | -o-transform: scale(0.6); 794 | -ms-transform: scale(0.6); 795 | transform: scale(0.6); 796 | -webkit-transition: 0.5s ease-in-out; 797 | -moz-transition: 0.5s ease-in-out; 798 | -o-transition: 0.5s ease-in-out; 799 | transition: 0.5s ease-in-out; 800 | } 801 | 802 | .fadeOut-leave.example-leave-active { 803 | opacity: 1; 804 | -webkit-transform: scale(1); 805 | -moz-transform: scale(1); 806 | -o-transform: scale(1); 807 | -ms-transform: scale(1); 808 | transform: scale(1); 809 | } 810 | /**/ 811 | 812 | /* Fade Down */ 813 | .fadeDown-enter { 814 | opacity: 0.00; 815 | transform: translateY(-100px); 816 | -webkit-transform: translateY(-100px); 817 | -ms-transform: translateY(-100px); 818 | transition: all .5s ease-in; 819 | } 820 | 821 | .fadeDown-enter.fadeDown-enter-active { 822 | opacity: 1; 823 | transform: translateY(0px); 824 | -webkit-transform: translateY(0px); 825 | -ms-transform: translateY(0px); 826 | } 827 | 828 | .fadeDown-leave { 829 | opacity: 1; 830 | transform: translateY(-280px); 831 | -webkit-transform: translateY(-280px); 832 | -ms-transform: translateY(-280px); 833 | transition: all .3s ease-out; 834 | } 835 | 836 | .fadeDown-leave.example-leave-active { 837 | opacity: 0.01; 838 | } 839 | /**/ 840 | 841 | /* fade list*/ 842 | 843 | .fadeList-enter { 844 | opacity: 0.01; 845 | transition: opacity .2s ease-in; 846 | } 847 | 848 | .fadeList-enter.fadeList-enter-active { 849 | opacity: 1; 850 | } 851 | 852 | .fadeList-leave { 853 | opacity: 1; 854 | transition: opacity .2s ease-in; 855 | } 856 | 857 | .fadeList-leave.fadeList-leave-active { 858 | opacity: 0.01; 859 | } 860 | 861 | 862 | .react-autosuggest__suggestions { 863 | width: 100%; 864 | margin: 10px 0 0 0; 865 | padding: 0px; 866 | list-style: none; 867 | position: absolute; 868 | z-index: 2 869 | } 870 | 871 | .react-autosuggest__suggestion { 872 | color: #410287; 873 | padding: 20px; 874 | background: #fff; 875 | cursor: pointer; 876 | } 877 | 878 | .react-autosuggest__suggestion--focused { 879 | background: #773CB5; 880 | color: #fff; 881 | } 882 | 883 | .top .react-autosuggest__suggestion { 884 | padding: 15px 885 | } 886 | 887 | /* Tooltip */ 888 | .tooltip.center { 889 | position: relative; 890 | } 891 | 892 | .tooltip.center:before, .tooltip.center:after { 893 | opacity: 0; 894 | z-index: 98; 895 | -moz-transition: opacity 300ms, visibility 0ms linear 300ms; 896 | -o-transition: opacity 300ms, visibility 0ms linear 300ms; 897 | -webkit-transition: opacity 300ms, visibility 0ms linear; 898 | -webkit-transition-delay: 0s, 300ms; 899 | transition: opacity 300ms, visibility 0ms linear 300ms; 900 | pointer-events: none; 901 | } 902 | 903 | @media screen and (max-width: 700px) { 904 | .tooltip.center:before, .tooltip.center:after { 905 | display: none !important; 906 | } 907 | } 908 | 909 | .tooltip.center:before { 910 | content: attr(data-tooltip); 911 | position: absolute; 912 | width: 114px; 913 | padding: 10px 15px; 914 | line-height: 18px; 915 | text-align: center; 916 | font-size: 13px; 917 | font-weight: normal; 918 | white-space: normal; 919 | border-radius: 3px; 920 | background-color: #fff; 921 | color: #5e04c2; 922 | } 923 | 924 | .tooltip.center:after { 925 | content: ''; 926 | } 927 | 928 | .tooltip.center:before { 929 | top: -9px; 930 | bottom: auto; 931 | -moz-transform: translateY(-100%); 932 | -ms-transform: translateY(-100%); 933 | -webkit-transform: translateY(-100%); 934 | transform: translateY(-100%); 935 | } 936 | 937 | .tooltip.center:after { 938 | content: ''; 939 | display: block; 940 | position: absolute; 941 | height: 0; 942 | width: 0; 943 | border-top: 6px solid #fff; 944 | border-left: 6px solid transparent; 945 | border-right: 6px solid transparent; 946 | border-bottom: none; 947 | top: -9px; 948 | bottom: auto; 949 | } 950 | 951 | .tooltip.center:before, .tooltip.center:after { 952 | left: 50%; 953 | right: auto; 954 | margin-left: -6px; 955 | } 956 | 957 | .tooltip.center:before { 958 | margin-left: -70px; 959 | } 960 | 961 | .tooltip.center:hover { 962 | /* putting comment here so :hover is declared first by itself to fix ie10 bug with transitions on psuedo elements- http://css-tricks.com/pseudo-element-animationstransitions-bug-fixed-in-webkit/ */ 963 | } 964 | 965 | .tooltip.center:hover:before, .tooltip.center:hover:after { 966 | opacity: 1; 967 | pointer-events: all; 968 | -moz-transition-delay: 0ms; 969 | -o-transition-delay: 0ms; 970 | -webkit-transition-delay: 0ms; 971 | transition-delay: 0ms; 972 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var fs = require('fs'); 3 | var browserify = require('browserify'); 4 | var watchify = require('watchify'); 5 | var babelify = require('babelify'); 6 | var rimraf = require('rimraf'); 7 | var source = require('vinyl-source-stream'); 8 | var _ = require('lodash'); 9 | var browserSync = require('browser-sync'); 10 | var reload = browserSync.reload; 11 | var streamify = require('gulp-streamify'); 12 | var uglify = require('gulp-uglify'); 13 | var args = require('yargs').argv; 14 | var gulpif = require('gulp-if'); 15 | var jshint = require('gulp-jshint'); 16 | var stylish = require('jshint-stylish'); 17 | var minifyCss = require('gulp-minify-css'); 18 | 19 | var isProduction = args.production === true; 20 | 21 | var config = { 22 | entryFile: './app/js/app.js', 23 | outputDir: './dist/js', 24 | outputFile: 'build.js' 25 | }; 26 | 27 | // clean the output directory 28 | gulp.task('clean', function(cb) { 29 | rimraf(config.outputDir, cb); 30 | }); 31 | 32 | var bundler; 33 | function getBundler() { 34 | if (!bundler) { 35 | if (isProduction) { 36 | bundler = watchify( 37 | browserify([ 38 | require.resolve('whatwg-fetch/fetch'), 39 | require.resolve('core-js/fn/symbol'), 40 | require.resolve('core-js/fn/promise'), 41 | config.entryFile], _.extend({ debug: true }, watchify.args))); 42 | } else { 43 | bundler = watchify( 44 | browserify(config.entryFile, _.extend({ debug: !isProduction }, watchify.args)) 45 | ); 46 | } 47 | } 48 | return bundler; 49 | }; 50 | 51 | function bundle() { 52 | return getBundler() 53 | .transform(babelify) 54 | .bundle() 55 | .on('error', function(err) { console.log('Error: ' + err.message); }) 56 | .pipe(source(config.outputFile)) 57 | .pipe(gulpif(isProduction, streamify(uglify()))) 58 | .pipe(gulp.dest(config.outputDir)) 59 | .pipe(reload({ stream: true })); 60 | } 61 | 62 | gulp.task('css', function() { 63 | gulp.src('./app/styles/style.css') 64 | .pipe(minifyCss()) 65 | .pipe(gulp.dest('./dist/styles/')); 66 | }); 67 | 68 | gulp.task('img', function() { 69 | gulp.src('./app/img/*') 70 | .pipe(gulp.dest('./dist/img')); 71 | }); 72 | 73 | gulp.task('html', function() { 74 | gulp.src('./app/index.html') 75 | .pipe(gulp.dest('./dist/')); 76 | gulp.src('./app/login/index.html') 77 | .pipe(gulp.dest('./dist/login')); 78 | }); 79 | 80 | gulp.task('build-persistent-deploy', ['clean', 'css', 'img', 'html'], function() { 81 | return bundle(); 82 | }); 83 | 84 | gulp.task('build-persistent', ['clean'], function() { 85 | return bundle(); 86 | }); 87 | 88 | gulp.task('deploy', ['build-persistent-deploy'], function() { 89 | process.exit(0); 90 | }); 91 | 92 | gulp.task('build', ['build-persistent'], function() { 93 | process.exit(0); 94 | }); 95 | 96 | gulp.task('watch', ['build-persistent'], function() { 97 | 98 | browserSync({ 99 | server: { 100 | baseDir: './' 101 | } 102 | }); 103 | 104 | getBundler().on('update', function() { 105 | gulp.start('build-persistent'); 106 | }); 107 | }); 108 | 109 | // WEB SERVER 110 | gulp.task('serve', function() { 111 | browserSync({ 112 | server: { 113 | baseDir: './' 114 | } 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MagicPlaylist", 3 | "version": "1.0.0", 4 | "description": "Get the playlist of your dreams based on a song", 5 | "devDependencies": { 6 | "babel-jest": "5.3.0", 7 | "babelify": "6.4.0", 8 | "browser-sync": "2.9.11", 9 | "browserify": "11.2.0", 10 | "gulp": "3.9.0", 11 | "gulp-if": "2.0.0", 12 | "gulp-jshint": "1.11.2", 13 | "gulp-minify-css": "^1.2.1", 14 | "gulp-rename": "1.2.2", 15 | "gulp-streamify": "1.0.2", 16 | "gulp-uglify": "1.4.2", 17 | "jest-cli": "0.6.1", 18 | "jshint-stylish": "2.0.1", 19 | "lodash": "3.10.1", 20 | "rimraf": "latest", 21 | "vinyl-source-stream": "1.1.0", 22 | "watchify": "3.5.0", 23 | "yargs": "3.29.0", 24 | "core-js": "^1.2.0", 25 | "whatwg-fetch": "^0.9.0" 26 | }, 27 | "dependencies": { 28 | "flux": "2.1.1", 29 | "react": "0.14.0", 30 | "react-addons-css-transition-group": "0.14.0", 31 | "react-autosuggest": "^2.1.1", 32 | "react-dom": "0.14.0", 33 | "spotify-sdk": "0.0.28" 34 | }, 35 | "scripts": { 36 | "dev": "gulp watch", 37 | "build": "gulp build", 38 | "deploy": "NODE_ENV=production gulp deploy --production", 39 | "test": "jest" 40 | }, 41 | "author": "loverajoel", 42 | "license": "MIT" 43 | } 44 | --------------------------------------------------------------------------------