├── .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 | [](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 | [](https://developer.spotify.com/showcase/item/magic-playlist/)
51 |
52 | [](http://lifehacker.com/magicplaylist-creates-spotify-playlists-based-on-a-sing-1739415795)
53 |
54 | [](http://www.cnet.com/how-to/create-spotify-playlists-based-on-one-song-with-magicplaylist/)
55 |
56 | [](https://www.producthunt.com/tech/magic-playlist/)
57 |
58 | [](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 |
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 |
103 |
104 | ;
105 | }
106 |
107 | renderPlaylist() {
108 | return ;
113 | }
114 |
115 | renderModal() {
116 | return ;
117 | }
118 |
119 | renderAlert() {
120 | return ;
125 | }
126 | }
127 |
128 | ReactDOM.render(
129 | ,
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
51 |
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 |
57 |
;
58 | }
59 |
60 | renderShare() {
61 | return
62 |
High five {this.props.username}!
63 |
Your playlist is now on Spotify.
64 |
65 | Go to your playlists lists or play it online .
69 |
70 |
Share it with your friends
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
Close window
80 |
;
81 | }
82 |
83 | renderLimit() {
84 | return
85 |
Ohh to many people here!
86 |
87 | Try again in a seconds or login for ensure your search.
88 |
89 |
Close
90 |
Login
91 |
;
92 | }
93 |
94 | renderFail() {
95 | return
96 |
Huston, we have a problem here! :(
97 |
98 |
;
99 | }
100 |
101 | renderLoading() {
102 | return ;
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
18 |
38 |
Created using the API of
42 |
43 |
;
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 ;
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
80 |
81 |
82 |
83 |
84 |
85 |
91 |
92 |
Playlist Status
93 |
94 |
101 | Public
102 |
109 | Private
110 |
111 |
112 | Save playlist
117 |
118 |
119 |
;
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
75 | { !this.state.isPlaying && !this.state.isLoading ?
76 |
: null
77 | }
78 | { this.state.isPlaying && !this.state.isLoading ?
79 |
: null
80 | }
81 | { this.state.isLoading ?
82 |
: null
83 | }
84 |
85 |
;
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 ;
42 | });
43 | return
44 |
45 | { !this.props.tracks.length ?
46 |
Hey! The track doesn't exist! :(
: null
47 | }
48 | { this.props.mainTrack ?
49 |
50 |
51 | {this.props.mainTrack.name}
52 | , {this.props.mainTrack.artists.first().name}
53 |
: null
54 | }
55 | { this.props.tracks.length ?
56 |
57 | Save playlist on Spotify
58 |
: null
59 | }
60 |
61 |
62 |
67 | {tracks}
68 |
69 |
70 |
;
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 {track.name}, {track.artists.first().name} ;
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
88 |
89 |
90 |
91 |
92 |
93 |
94 |
104 |
105 |
;
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
7 | Tip: Type a song + artist for better results.
8 | (ex: Billie Jean, Michael Jackson)
9 |
;
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
7 |
Magic Playlist /
8 | Get the playlist of your dreams based on a song.
9 | ;
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
19 |
20 | Magic Playlist /
21 |
22 |
23 |
24 |
25 |
;
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
26 | {track.name}, {track.artists.first().name}
27 |
32 |
33 |
34 |
40 |
45 |
46 |
47 | ;
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 |
2 |
3 | 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 |
--------------------------------------------------------------------------------