├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── LICENSE
├── README.md
├── bin
├── build.js
└── gh-deploy.js
├── package.json
├── public
├── CNAME
├── browserconfig.xml
├── favicon.ico
├── icons
│ ├── android-icon-144x144.png
│ ├── android-icon-192x192.png
│ ├── android-icon-36x36.png
│ ├── android-icon-48x48.png
│ ├── android-icon-72x72.png
│ ├── android-icon-96x96.png
│ ├── apple-icon-114x114.png
│ ├── apple-icon-120x120.png
│ ├── apple-icon-144x144.png
│ ├── apple-icon-152x152.png
│ ├── apple-icon-180x180.png
│ ├── apple-icon-57x57.png
│ ├── apple-icon-60x60.png
│ ├── apple-icon-72x72.png
│ ├── apple-icon-76x76.png
│ ├── apple-icon-precomposed.png
│ ├── apple-icon.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon-96x96.png
│ ├── ms-icon-144x144.png
│ ├── ms-icon-150x150.png
│ ├── ms-icon-310x310.png
│ └── ms-icon-70x70.png
├── images
│ ├── 20v_error.gif
│ ├── 20v_loading.gif
│ ├── facebook.svg
│ ├── logo.svg
│ ├── meta-logo.jpg
│ ├── search.svg
│ └── twitter.svg
├── index.html
└── manifest.json
├── sass
├── _helpers.scss
├── _reset.scss
└── style.scss
├── server.js
├── src
├── App.js
├── actions
│ ├── app.js
│ ├── player.js
│ └── search.js
├── components
│ ├── Footer
│ │ ├── _index.scss
│ │ └── index.js
│ ├── Header
│ │ ├── _index.scss
│ │ └── index.js
│ ├── PlayerManager
│ │ ├── CGBand.js
│ │ ├── Player.js
│ │ ├── PlayerControls.js
│ │ ├── _index.scss
│ │ └── index.js
│ ├── Search
│ │ ├── _index.scss
│ │ └── index.js
│ ├── Share
│ │ ├── _index.scss
│ │ └── index.js
│ └── player
│ │ └── Player.js
├── constants
│ ├── app.js
│ ├── player.js
│ └── search.js
├── core
│ ├── Magic.js
│ └── Spotify.js
├── devel-config.js
├── index.js
├── reducers
│ ├── app.js
│ ├── index.js
│ ├── player.js
│ └── search.js
└── store
│ └── StoreManager.js
├── webpack.config.js
└── webpack.config.production.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 4
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 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/**
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true
4 | },
5 | ecmaFeatures: {
6 | "modules": true,
7 | "jsx": true
8 | },
9 | "globals": {
10 | },
11 | "plugins": [
12 | "react"
13 | ],
14 | "extends": "rackt",
15 | "parser": "babel-eslint",
16 | "rules": {
17 | // Possible Errors
18 | "comma-dangle": [2, "never"],
19 | "no-cond-assign": 2,
20 | "no-console": 2,
21 | "no-constant-condition": 2,
22 | "no-control-regex": 2,
23 | "no-debugger": 2,
24 | "no-dupe-keys": 2,
25 | "no-empty": 2,
26 | "no-empty-character-class": 2,
27 | "no-ex-assign": 2,
28 | "no-extra-boolean-cast": 2,
29 | "no-extra-parens": 0,
30 | "no-extra-semi": 2,
31 | "no-func-assign": 2,
32 | "no-inner-declarations": 2,
33 | "no-invalid-regexp": 2,
34 | "no-irregular-whitespace": 2,
35 | "no-negated-in-lhs": 2,
36 | "no-obj-calls": 2,
37 | "no-regex-spaces": 2,
38 | "no-reserved-keys": 0,
39 | "no-sparse-arrays": 2,
40 | "no-unreachable": 2,
41 | "use-isnan": 2,
42 | "valid-jsdoc": 0,
43 | "valid-typeof": 2,
44 | // Best Practices
45 | "block-scoped-var": 2,
46 | "complexity": 0,
47 | "consistent-return": 2,
48 | "curly": 2,
49 | "default-case": 2,
50 | "dot-notation": 2,
51 | "eqeqeq": 2,
52 | "guard-for-in": 2,
53 | "no-alert": 2,
54 | "no-caller": 2,
55 | "no-div-regex": 2,
56 | "no-else-return": 2,
57 | "no-empty-label": 2,
58 | "no-eq-null": 2,
59 | "no-eval": 2,
60 | "no-extend-native": 2,
61 | "no-extra-bind": 2,
62 | "no-fallthrough": 2,
63 | "no-floating-decimal": 2,
64 | "no-implied-eval": 2,
65 | "no-iterator": 2,
66 | "no-labels": 2,
67 | "no-lone-blocks": 2,
68 | "no-loop-func": 2,
69 | "no-multi-spaces": 2,
70 | "no-multi-str": 0,
71 | "no-native-reassign": 2,
72 | "no-new": 2,
73 | "no-new-func": 2,
74 | "no-new-wrappers": 2,
75 | "no-octal": 2,
76 | "no-octal-escape": 2,
77 | "no-process-env": 2,
78 | "no-proto": 2,
79 | "no-redeclare": 2,
80 | "no-return-assign": 2,
81 | "no-script-url": 2,
82 | "no-self-compare": 2,
83 | "no-sequences": 2,
84 | "no-unused-expressions": 2,
85 | "no-void": 0,
86 | "no-warning-comments": 2,
87 | "no-with": 2,
88 | "radix": 2,
89 | "vars-on-top": 0,
90 | "wrap-iife": 2,
91 | "yoda": 2,
92 | // Strict Mode
93 | "strict": [2, "function"],
94 | // Variables
95 | "no-catch-shadow": 2,
96 | "no-delete-var": 2,
97 | "no-label-var": 2,
98 | "no-shadow": 2,
99 | "no-shadow-restricted-names": 2,
100 | "no-undef": 2,
101 | "no-undef-init": 2,
102 | "no-undefined": 2,
103 | "no-unused-vars": 2,
104 | "no-use-before-define": 2,
105 | // Stylistic Issues
106 | "indent": [2, 4, {
107 | "SwitchCase": 1
108 | }],
109 | "brace-style": 2,
110 | "camelcase": 0,
111 | "comma-spacing": 2,
112 | "comma-style": 2,
113 | "consistent-this": 0,
114 | "eol-last": 2,
115 | "func-names": 0,
116 | "func-style": 0,
117 | "key-spacing": [2, {
118 | "beforeColon": false,
119 | "afterColon": true
120 | }],
121 | "max-nested-callbacks": 0,
122 | "new-cap": 2,
123 | "new-parens": 2,
124 | "no-array-constructor": 2,
125 | "no-inline-comments": 0,
126 | "no-lonely-if": 2,
127 | "no-mixed-spaces-and-tabs": 2,
128 | "no-nested-ternary": 2,
129 | "no-new-object": 2,
130 | "semi-spacing": [2, {
131 | "before": false,
132 | "after": true
133 | }],
134 | "no-spaced-func": 2,
135 | "no-ternary": 0,
136 | "no-trailing-spaces": 2,
137 | "no-multiple-empty-lines": 2,
138 | "no-underscore-dangle": 0,
139 | "one-var": 0,
140 | "operator-assignment": [2, "always"],
141 | "padded-blocks": 0,
142 | "quotes": [2, "single"],
143 | "quote-props": [2, "as-needed"],
144 | "semi": [2, "always"],
145 | "sort-vars": [2, {"ignoreCase": true}],
146 | "space-after-keywords": 2,
147 | "space-before-blocks": 2,
148 | "object-curly-spacing": [2, "always", {
149 | "objectsInObjects": false,
150 | "arraysInObjects": false
151 | }],
152 | "array-bracket-spacing": [2, "never"],
153 | "space-in-parens": 2,
154 | "space-infix-ops": 2,
155 | "space-return-throw-case": 2,
156 | "space-unary-ops": 2,
157 | "spaced-comment": 2,
158 | "wrap-regex": 0,
159 | // Legacy
160 | "max-depth": 0,
161 | "max-len": [2, 120],
162 | "max-params": 0,
163 | "max-statements": 0,
164 | "no-plusplus": 0,
165 | /////// REACT ///////
166 | "react/display-name": 0,
167 | "react/forbid-prop-types": 0,
168 | "react/jsx-boolean-value": 1,
169 | "react/jsx-closing-bracket-location": 1,
170 | "react/jsx-curly-spacing": 0,
171 | "react/jsx-handler-names": 0,
172 | "react/jsx-indent-props": 0,
173 | "react/jsx-indent": 0,
174 | "react/jsx-key": 0,
175 | "react/jsx-max-props-per-line": 0,
176 | "react/jsx-no-bind": 1,
177 | "react/jsx-no-duplicate-props": 0,
178 | "react/jsx-no-literals": 0,
179 | "react/jsx-no-undef": 0,
180 | "react/jsx-pascal-case": 1,
181 | "react/jsx-quotes": 0,
182 | "react/jsx-sort-prop-types": 0,
183 | "react/jsx-sort-props": 0,
184 | "react/jsx-uses-react": 0,
185 | "react/jsx-uses-vars": 0,
186 | "react/no-danger": 0,
187 | "react/no-deprecated": 0,
188 | "react/no-did-mount-set-state": 0,
189 | "react/no-did-update-set-state": 0,
190 | "react/no-direct-mutation-state": 0,
191 | "react/no-is-mounted": 1,
192 | "react/no-multi-comp": 0,
193 | "react/no-set-state": 0,
194 | "react/no-string-refs": 0,
195 | "react/no-unknown-property": 0,
196 | "react/prefer-es6-class": 1,
197 | "react/prop-types": 0,
198 | "react/react-in-jsx-scope": 0,
199 | "react/require-extension": 0,
200 | "react/self-closing-comp": 1,
201 | "react/sort-comp": 0,
202 | "react/wrap-multilines": 1
203 | }
204 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | node_modules
3 | *.sublime-*
4 | page
5 | dist
6 | src/prod-config.js
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, hAPPckathon
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any
4 | purpose with or without fee is hereby granted, provided that the above
5 | copyright notice and this permission notice appear in all copies.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 |
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #20v /
2 |
3 | > Create and enjoy a custom music channel based on a single song
4 |
5 | [](http://20v.co/)
6 |
7 | Go to [20v](http://20v.co/)
8 |
9 | #Features
10 | - Create an awesome video channel based on a song.
11 |
12 | #Stack
13 | - ES6
14 | - [Redux](https://github.com/reactjs/redux)
15 | - [React](https://facebook.github.io/react/)
16 | - [Spotify-SDK](https://github.com/loverajoel/spotify-sdk)
17 | - [react-youtube](https://github.com/troybetz/react-youtube)
18 | - Youtube API v3
19 |
20 | #APIs
21 |
22 | This app is based on [Spotify API](https://developer.spotify.com/web-api/) and [YouTube API v3](https://developers.google.com/youtube/v3/)
23 |
24 | #Stay In Touch
25 |
26 | Follow us for news [@20v](https://twitter.com/_20v_)
27 |
28 |
29 | #Contributing
30 |
31 | clone this repo
32 | ```
33 | npm install
34 | npm start
35 | ```
36 | browse ```http://localhost:3000```
37 |
38 | If you're considering an active development please get a [YouTube API key](https://console.developers.google.com/apis/) and place it on ```src/devel-config.js```. Current development API key could change without warning.
39 |
40 | # Authors
41 |
42 | Marcos Herrera ([@cosmitar](https://twitter.com/cosmitar))
43 |
44 | Lucas Di Mattia ([@untallucas](https://twitter.com/untallucas))
45 |
46 | Romi Viola ([@romikid](https://twitter.com/romikid))
47 |
48 | Lovera Joel ([@loverajoel](https://twitter.com/loverajoel))
49 |
50 |
51 | Made with :heart: from Córdoba, Argentina.
52 |
--------------------------------------------------------------------------------
/bin/build.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | var spawn = require('child_process').spawn;
4 | var command = './node_modules/.bin/webpack --config webpack.config.production.js';
5 | var build = spawn('/bin/sh', ['-c', command], { stdio: [0,1,2] });
6 |
--------------------------------------------------------------------------------
/bin/gh-deploy.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | var exec = require('child_process').exec;
4 | function puts(error, stdout, stderr) {
5 | if (stderr) {
6 | console.error(stderr);
7 | return;
8 | }
9 | console.log(stdout);
10 | }
11 |
12 | var copyFilesCommands = [
13 | 'cp -R ./public/* ./page/',
14 | 'rm -rf ./page/static',
15 | 'cp -r ./dist/ ./page/static/',
16 | 'rm page/.git/index.lock'
17 | ].join(' && ');
18 |
19 | var gitCommands = [
20 | 'cd ./page',
21 | 'git add -A',
22 | 'git commit -a -m "gh-pages update"',
23 | 'git push origin gh-pages --force'
24 | ].join(' && ');
25 |
26 | exec(copyFilesCommands, puts);
27 | exec(gitCommands, puts);
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "20v",
3 | "version": "0.1.0",
4 | "description": "An app to enjoy a music channel with your favorites videos",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/fusenlabs/20v"
9 | },
10 | "devDependencies": {
11 | "babel-core": "^5.4.5",
12 | "babel-eslint": "^5.0.0-beta6",
13 | "babel-loader": "^5.0.0",
14 | "core-js": "^2.0.3",
15 | "eslint": "^1.10.3",
16 | "eslint-config-rackt": "^1.1.1",
17 | "eslint-plugin-react": "^3.15.0",
18 | "extract-text-webpack-plugin": "^1.0.1",
19 | "react-hot-loader": "^1.1.5",
20 | "webpack": "^1.8.4",
21 | "webpack-dev-server": "^1.8.0"
22 | },
23 | "dependencies": {
24 | "babel-runtime": "^5.1.10",
25 | "css-loader": "^0.23.0",
26 | "exports-loader": "latest",
27 | "imports-loader": "latest",
28 | "node-sass": "^3.4.2",
29 | "react": "^0.14.2",
30 | "react-addons-css-transition-group": "^0.14.3",
31 | "react-autosuggest": "^2.2.5",
32 | "react-dom": "^0.14.2",
33 | "react-redux": "^4.0.0",
34 | "react-youtube": "^4.1.2",
35 | "redux": "^3.0.4",
36 | "redux-thunk": "^1.0.0",
37 | "sass-loader": "^3.1.2",
38 | "spotify-sdk": "0.0.29",
39 | "style-loader": "^0.13.0",
40 | "youtube-client-wrapper": "git://github.com/Cosmitar/youtube-client-wrapper",
41 | "whatwg-fetch": "latest"
42 | },
43 | "scripts": {
44 | "test": "echo \"Error: no test specified\" && exit 1",
45 | "start": "node server.js",
46 | "build": "node ./bin/build.js",
47 | "gh-deploy": "node ./bin/gh-deploy.js",
48 | "deploy": "npm run gh-deploy",
49 | "predeploy": "npm run build",
50 | "lint": "./node_modules/.bin/eslint ."
51 | },
52 | "author": "@Fūsenlabs",
53 | "license": "ISC",
54 | "bugs": {
55 | "url": "https://github.com/fusenlabs/20v/issues"
56 | },
57 | "homepage": "https://20v.co"
58 | }
59 |
--------------------------------------------------------------------------------
/public/CNAME:
--------------------------------------------------------------------------------
1 | www.20v.co
2 |
--------------------------------------------------------------------------------
/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 | #ffffff
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/favicon.ico
--------------------------------------------------------------------------------
/public/icons/android-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/android-icon-144x144.png
--------------------------------------------------------------------------------
/public/icons/android-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/android-icon-192x192.png
--------------------------------------------------------------------------------
/public/icons/android-icon-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/android-icon-36x36.png
--------------------------------------------------------------------------------
/public/icons/android-icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/android-icon-48x48.png
--------------------------------------------------------------------------------
/public/icons/android-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/android-icon-72x72.png
--------------------------------------------------------------------------------
/public/icons/android-icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/android-icon-96x96.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/apple-icon-114x114.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/apple-icon-120x120.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/apple-icon-144x144.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/apple-icon-152x152.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/apple-icon-180x180.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/apple-icon-57x57.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/apple-icon-60x60.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/apple-icon-72x72.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/apple-icon-76x76.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/apple-icon-precomposed.png
--------------------------------------------------------------------------------
/public/icons/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/apple-icon.png
--------------------------------------------------------------------------------
/public/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/icons/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/favicon-96x96.png
--------------------------------------------------------------------------------
/public/icons/ms-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/ms-icon-144x144.png
--------------------------------------------------------------------------------
/public/icons/ms-icon-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/ms-icon-150x150.png
--------------------------------------------------------------------------------
/public/icons/ms-icon-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/ms-icon-310x310.png
--------------------------------------------------------------------------------
/public/icons/ms-icon-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/icons/ms-icon-70x70.png
--------------------------------------------------------------------------------
/public/images/20v_error.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/images/20v_error.gif
--------------------------------------------------------------------------------
/public/images/20v_loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/images/20v_loading.gif
--------------------------------------------------------------------------------
/public/images/facebook.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/images/meta-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/images/meta-logo.jpg
--------------------------------------------------------------------------------
/public/images/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/images/twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 20v - Music for your eyes
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 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
61 |
62 |
63 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "App",
3 | "icons": [
4 | {
5 | "src": "\/android-icon-36x36.png",
6 | "sizes": "36x36",
7 | "type": "image\/png",
8 | "density": "0.75"
9 | },
10 | {
11 | "src": "\/android-icon-48x48.png",
12 | "sizes": "48x48",
13 | "type": "image\/png",
14 | "density": "1.0"
15 | },
16 | {
17 | "src": "\/android-icon-72x72.png",
18 | "sizes": "72x72",
19 | "type": "image\/png",
20 | "density": "1.5"
21 | },
22 | {
23 | "src": "\/android-icon-96x96.png",
24 | "sizes": "96x96",
25 | "type": "image\/png",
26 | "density": "2.0"
27 | },
28 | {
29 | "src": "\/android-icon-144x144.png",
30 | "sizes": "144x144",
31 | "type": "image\/png",
32 | "density": "3.0"
33 | },
34 | {
35 | "src": "\/android-icon-192x192.png",
36 | "sizes": "192x192",
37 | "type": "image\/png",
38 | "density": "4.0"
39 | }
40 | ]
41 | }
--------------------------------------------------------------------------------
/sass/_helpers.scss:
--------------------------------------------------------------------------------
1 | /* 0.2 Helpers */
2 |
3 | @mixin border-radius($border-radius-radius: 50%) {
4 | -webkit-border-radius: $border-radius-radius;
5 | -moz-border-radius: $border-radius-radius;
6 | -ms-border-radius: $border-radius-radius;
7 | -o-border-radius: $border-radius-radius;
8 | border-radius: $border-radius-radius;
9 | }
10 |
11 | @mixin transition($transition-speed: 0.5s, $transition-property: all, $transition-ease: ease-in-out) {
12 | -webkit-transition: $transition-property $transition-speed;
13 | -moz-transition: $transition-property $transition-ease $transition-speed;
14 | -ms-transition: $transition-property $transition-ease $transition-speed;
15 | -o-transition: $transition-property $transition-ease $transition-speed;
16 | transition: $transition-property $transition-ease $transition-speed;
17 | }
18 |
19 | @mixin transition-delay($transition-delay-time: 0.2s) {
20 | -webkit-transition-delay: $transition-delay-time;
21 | -moz-transition-delay: $transition-delay-time;
22 | -ms-transition-delay: $transition-delay-time;
23 | -o-transition-delay: $transition-delay-time;
24 | transition-delay: $transition-delay-time;
25 | }
26 |
27 | @mixin single-filter($filter-name, $filter-amount) {
28 | -webkit-filter: $filter-name+unquote('(#{$filter-amount})');
29 | -moz-filter: $filter-name+unquote('(#{$filter-amount})');
30 | -o-filter: $filter-name+unquote('(#{$filter-amount})');
31 | -ms-filter: $filter-name+unquote('(#{$filter-amount})');
32 | filter: $filter-name+unquote('(#{$filter-amount})');
33 | }
34 |
35 | @mixin complex-filter($filters-group) {
36 | -webkit-filter: $filters-group;
37 | -moz-filter: $filters-group;
38 | -o-filter: $filters-group;
39 | -ms-filter: $filters-group;
40 | filter: $filters-group;
41 | }
42 |
43 | @mixin transform-scale($scale-amount) {
44 | -webkit-transform: scale($scale-amount);
45 | -ms-transform: scale($scale-amount);
46 | transform: scale($scale-amount);
47 | }
48 |
49 | @mixin animation($name, $time) {
50 | -webkit-animation: $name $time ease-in-out;
51 | -moz-animation: $name $time ease-in-out;
52 | -ms-animation: $name $time ease-in-out;
53 | -o-animation: $name $time ease-in-out;
54 | animation: $name $time ease-in-out;
55 | }
56 |
57 | @mixin backface-visibility-fix() {
58 | -webkit-backface-visibility: hidden;
59 | -moz-backface-visibility: hidden;
60 | backface-visibility: hidden;
61 | -webkit-transform: translate3d(0, 0, 0);
62 | -moz-transform: translate3d(0, 0, 0);
63 | transform: translate3d(0, 0, 0);
64 | }
65 |
66 | @mixin transform-3d($transform-3d-perspective, $transform-3d-3dtransformation) {
67 | -webkit-transform: perspective( $transform-3d-perspective ) translate3d( unquote($transform-3d-3dtransformation) );
68 | -ms-transform: perspective( $transform-3d-perspective ) translate3d( unquote($transform-3d-3dtransformation) );
69 | transform: perspective( $transform-3d-perspective ) translate3d( unquote($transform-3d-3dtransformation) );
70 | }
71 |
72 | @mixin linear-gradient($fromColor, $toColor) {
73 | background-color: $toColor;
74 | background-image: -webkit-gradient(linear, left top, left bottom, from($fromColor), to($toColor));
75 | background-image: -webkit-linear-gradient(top, $fromColor, $toColor);
76 | background-image: -moz-linear-gradient(top, $fromColor, $toColor);
77 | background-image: -ms-linear-gradient(top, $fromColor, $toColor);
78 | background-image: -o-linear-gradient(top, $fromColor, $toColor);
79 | background-image: linear-gradient(top, $fromColor, $toColor);
80 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#{$fromColor}', EndColorStr='#{$toColor}');
81 | }
82 |
83 | @mixin gradient-background($fromColor, $toColor) {
84 | background: $fromColor;
85 | background: -webkit-linear-gradient( $fromColor, $toColor );
86 | background: linear-gradient( $fromColor, $toColor );
87 | }
88 |
89 | @mixin simple-gradient($deg, $fromColor, $toColor) {
90 | background: $fromColor;
91 | background: -webkit-linear-gradient( $deg, $fromColor, $toColor );
92 | background: linear-gradient( $deg, $fromColor, $toColor );
93 | }
94 |
95 | @mixin rotate-keyframes-animation( $name, $degStart, $degEnd ) {
96 | @-webkit-keyframes unquote('#{$name}') {
97 | 0% {
98 | -webkit-transform: rotate( $degStart );
99 | }
100 | 50% {
101 | -webkit-transform: rotate( $degEnd );
102 | }
103 | 100% {
104 | -webkit-transform: rotate( $degStart );
105 | }
106 | }
107 |
108 | @keyframes unquote('#{$name}') {
109 | 0% {
110 | transform: rotate( $degStart );
111 | }
112 | 50% {
113 | transform: rotate( $degEnd );
114 | }
115 | 100% {
116 | transform: rotate( $degStart );
117 | }
118 | }
119 | }
--------------------------------------------------------------------------------
/sass/_reset.scss:
--------------------------------------------------------------------------------
1 | /* 1.0 Reset */
2 | /* Modified from Normalize.css to provide cross-browser consistency and a smart default styling of HTML elements. */
3 | /* http://git.io/normalize */
4 |
5 | * {
6 | -webkit-box-sizing: border-box;
7 | -moz-box-sizing: border-box;
8 | box-sizing: border-box;
9 | }
10 |
11 | article,
12 | aside,
13 | details,
14 | figcaption,
15 | figure,
16 | footer,
17 | header,
18 | nav,
19 | section,
20 | summary {
21 | display: block;
22 | }
23 |
24 | audio,
25 | canvas,
26 | video {
27 | display: inline-block;
28 | }
29 |
30 | audio:not([controls]) {
31 | display: none;
32 | height: 0;
33 | }
34 |
35 | [hidden] {
36 | display: none;
37 | }
38 |
39 | html {
40 | font-size: 100%;
41 | overflow-y: scroll;
42 | -webkit-text-size-adjust: 100%;
43 | -ms-text-size-adjust: 100%;
44 | }
45 |
46 | html,
47 | button,
48 | input,
49 | select,
50 | textarea {
51 | font-family: "Source Sans Pro", Helvetica, sans-serif;
52 | }
53 |
54 | body {
55 | color: #141412;
56 | line-height: 1.5;
57 | margin: 0;
58 | }
59 |
60 | a {
61 | color: #ca3c08;
62 | text-decoration: none;
63 | }
64 |
65 | a:visited {
66 | color: #ac0404;
67 | }
68 |
69 | a:focus {
70 | outline: none;
71 | -webkit-appearance: none;
72 | -webkit-border: none;
73 | }
74 |
75 | a:active,
76 | a:hover {
77 | color: #ea9629;
78 | outline: 0;
79 | }
80 |
81 | a:hover {
82 | text-decoration: underline;
83 | }
84 |
85 | h1,
86 | h2,
87 | h3,
88 | h4,
89 | h5,
90 | h6 {
91 | clear: both;
92 | font-family: Bitter, Georgia, serif;
93 | line-height: 1.3;
94 | }
95 |
96 | h1 {
97 | font-size: 48px;
98 | margin: 33px 0;
99 | }
100 |
101 | h2 {
102 | font-size: 30px;
103 | margin: 25px 0;
104 | }
105 |
106 | h3 {
107 | font-size: 22px;
108 | margin: 22px 0;
109 | }
110 |
111 | h4 {
112 | font-size: 20px;
113 | margin: 25px 0;
114 | }
115 |
116 | h5 {
117 | font-size: 18px;
118 | margin: 30px 0;
119 | }
120 |
121 | h6 {
122 | font-size: 16px;
123 | margin: 36px 0;
124 | }
125 |
126 | address {
127 | font-style: italic;
128 | margin: 0 0 24px;
129 | }
130 |
131 | abbr[title] {
132 | border-bottom: 1px dotted;
133 | }
134 |
135 | b,
136 | strong {
137 | font-weight: bold;
138 | }
139 |
140 | dfn {
141 | font-style: italic;
142 | }
143 |
144 | mark {
145 | background: #ff0;
146 | color: #000;
147 | }
148 |
149 | p {
150 | margin: 0 0 24px;
151 | }
152 |
153 | code,
154 | kbd,
155 | pre,
156 | samp {
157 | font-family: monospace, serif;
158 | font-size: 14px;
159 | -webkit-hyphens: none;
160 | -moz-hyphens: none;
161 | -ms-hyphens: none;
162 | hyphens: none;
163 | }
164 |
165 | pre {
166 | background: #f5f5f5;
167 | color: #666;
168 | font-family: monospace;
169 | font-size: 14px;
170 | margin: 20px 0;
171 | overflow: auto;
172 | padding: 20px;
173 | white-space: pre;
174 | white-space: pre-wrap;
175 | word-wrap: break-word;
176 | }
177 |
178 | blockquote,
179 | q {
180 | -webkit-hyphens: none;
181 | -moz-hyphens: none;
182 | -ms-hyphens: none;
183 | hyphens: none;
184 | quotes: none;
185 | }
186 |
187 | blockquote:before,
188 | blockquote:after,
189 | q:before,
190 | q:after {
191 | content: "";
192 | content: none;
193 | }
194 |
195 | blockquote {
196 | font-size: 18px;
197 | font-style: italic;
198 | font-weight: 300;
199 | margin: 24px 40px;
200 | }
201 |
202 | blockquote blockquote {
203 | margin-right: 0;
204 | }
205 |
206 | blockquote cite,
207 | blockquote small {
208 | font-size: 14px;
209 | font-weight: normal;
210 | text-transform: uppercase;
211 | }
212 |
213 | blockquote em,
214 | blockquote i {
215 | font-style: normal;
216 | font-weight: 300;
217 | }
218 |
219 | blockquote strong,
220 | blockquote b {
221 | font-weight: 400;
222 | }
223 |
224 | small {
225 | font-size: smaller;
226 | }
227 |
228 | sub,
229 | sup {
230 | font-size: 75%;
231 | line-height: 0;
232 | position: relative;
233 | vertical-align: baseline;
234 | }
235 |
236 | sup {
237 | top: -0.5em;
238 | }
239 |
240 | sub {
241 | bottom: -0.25em;
242 | }
243 |
244 | dl {
245 | margin: 0 20px;
246 | }
247 |
248 | dt {
249 | font-weight: bold;
250 | }
251 |
252 | dd {
253 | margin: 0 0 20px;
254 | }
255 |
256 | menu,
257 | ol,
258 | ul {
259 | margin: 16px 0;
260 | padding: 0 0 0 40px;
261 | }
262 |
263 | ul {
264 | list-style-type: square;
265 | }
266 |
267 | nav ul,
268 | nav ol {
269 | list-style: none;
270 | list-style-image: none;
271 | }
272 |
273 | li > ul,
274 | li > ol {
275 | margin: 0;
276 | }
277 |
278 | img {
279 | -ms-interpolation-mode: bicubic;
280 | border: 0;
281 | vertical-align: middle;
282 | }
283 |
284 | svg:not(:root) {
285 | overflow: hidden;
286 | }
287 |
288 | figure {
289 | margin: 0;
290 | }
291 |
292 | form {
293 | margin: 0;
294 | }
295 |
296 | fieldset {
297 | border: 1px solid #c0c0c0;
298 | margin: 0 2px;
299 | padding: 0.35em 0.625em 0.75em;
300 | }
301 |
302 | legend {
303 | border: 0;
304 | padding: 0;
305 | white-space: normal;
306 | }
307 |
308 | button,
309 | input,
310 | select,
311 | textarea {
312 | font-size: 100%;
313 | margin: 0;
314 | max-width: 100%;
315 | vertical-align: baseline;
316 | }
317 |
318 | button,
319 | input {
320 | line-height: normal;
321 | }
322 |
323 | button,
324 | html input[type="button"],
325 | input[type="reset"],
326 | input[type="submit"] {
327 | -webkit-appearance: button;
328 | cursor: pointer;
329 | }
330 |
331 | button[disabled],
332 | input[disabled] {
333 | cursor: default;
334 | }
335 |
336 | input[type="checkbox"],
337 | input[type="radio"] {
338 | padding: 0;
339 | }
340 |
341 | input[type="search"] {
342 | -webkit-appearance: textfield;
343 | padding-right: 2px; /* Don't cut off the webkit search cancel button */
344 | width: 270px;
345 | }
346 |
347 | input[type="search"]::-webkit-search-decoration {
348 | -webkit-appearance: none;
349 | }
350 |
351 | button::-moz-focus-inner,
352 | input::-moz-focus-inner {
353 | border: 0;
354 | padding: 0;
355 | }
356 |
357 | textarea {
358 | overflow: auto;
359 | vertical-align: top;
360 | }
361 |
362 | table {
363 | border-bottom: 1px solid #ededed;
364 | border-collapse: collapse;
365 | border-spacing: 0;
366 | font-size: 14px;
367 | line-height: 2;
368 | margin: 0 0 20px;
369 | width: 100%;
370 | }
371 |
372 | caption,
373 | th,
374 | td {
375 | font-weight: normal;
376 | text-align: left;
377 | }
378 |
379 | caption {
380 | font-size: 16px;
381 | margin: 20px 0;
382 | }
383 |
384 | th {
385 | font-weight: bold;
386 | text-transform: uppercase;
387 | }
388 |
389 | td {
390 | border-top: 1px solid #ededed;
391 | padding: 6px 10px 6px 0;
392 | }
393 |
394 | del {
395 | color: #333;
396 | }
397 |
398 | ins {
399 | background: #fff9c0;
400 | text-decoration: none;
401 | }
402 |
403 | hr {
404 | /*background: url(images/dotted-line.png) repeat center top;*/
405 | background-size: 4px 4px;
406 | border: 0;
407 | height: 1px;
408 | margin: 0 0 24px;
409 | }
--------------------------------------------------------------------------------
/sass/style.scss:
--------------------------------------------------------------------------------
1 | /*
2 | Youtube Playlist Player Styles
3 | Repo: https://github.com/hAPPckathon/youtube-playlist-player
4 | Version: 1.0
5 | */
6 |
7 |
8 | // FONTS
9 | @import url('https://fonts.googleapis.com/css?family=Passion+One:400,700,900');
10 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:400,300,300italic,400italic,600,600italic,700,700italic,800,800italic');
11 |
12 |
13 | // FONTS
14 | $font-passion: 'Passion One';
15 | $font-open: 'Open Sans';
16 |
17 |
18 | // COLOURS
19 | $color-one: #3460F2;
20 | $color-two: #1BD27F;
21 |
22 |
23 | // IMPORTS
24 | @import 'reset';
25 | @import 'helpers';
26 | @import '../src/components/Footer/index';
27 | @import '../src/components/Header/index';
28 | @import '../src/components/PlayerManager/index';
29 | @import '../src/components/Search/index';
30 | @import '../src/components/Share/index';
31 |
32 |
33 | // GENERAL
34 | * {
35 | outline: none;
36 | -webkit-appearance: none;
37 | }
38 |
39 | html {
40 | overflow: hidden;
41 | @include simple-gradient( -45deg, $color-one, $color-two );
42 | }
43 |
44 | body {
45 | overflow: hidden;
46 | }
47 |
48 | html,
49 | body {
50 | text-rendering: optimizeLegibility;
51 | -webkit-text-size-adjust: 100%;
52 | -webkit-font-smoothing: antialiased;
53 | font-smoothing: antialiased;
54 | font-smooth: always;
55 | width: 100%;
56 | height: 100%;
57 | font-family: $font-open;
58 | font-weight: 600;
59 | font-size: 12px;
60 | line-height: 13px;
61 | letter-spacing: -0.04em;
62 | color: #FFFFFF;
63 |
64 | @media (max-width: 740px) and (orientation: landscape){
65 | overflow: visible;
66 | height: auto;
67 | min-height: 100%;
68 | }
69 | }
70 |
71 | #appContainer {
72 | height: 100%;
73 |
74 | .app-inner-wrapper {
75 | height: 100%;
76 | }
77 | }
78 |
79 |
80 | // HOME
81 | .home-wrapper {
82 | height: 100%;
83 | width: 100%;
84 | display: table;
85 | position: relative;
86 |
87 | .home-body {
88 | display: table-cell;
89 | vertical-align: middle;
90 | text-align: center;
91 | vertical-align: top;
92 | padding-top: 100px;
93 |
94 | .intro-logo {
95 | margin-bottom: 30px;
96 | width: 170px;
97 | height: 70px;
98 |
99 | @media (max-width: 600px){
100 | width: 150px;
101 | height: (150*70px)/170;
102 | }
103 |
104 | @media (max-width: 740px) and (orientation: landscape){
105 | width: 100px;
106 | height: (100*70px)/170;
107 | }
108 |
109 | @media (max-width: 400px){
110 | width: 110px;
111 | height: (110*70px)/170;
112 | }
113 | }
114 |
115 | .intro-text {
116 | font-size: 30px;
117 | font-weight: 300;
118 | letter-spacing: -0.02em;
119 | line-height: 35px;
120 | width: 500px;
121 | margin: 0 auto;
122 |
123 | @media (max-width: 600px){
124 | width: 100%;
125 | padding: 0 50px;
126 | font-size: 26px;
127 | line-height: 25px;
128 | }
129 |
130 | @media (max-width: 400px){
131 | width: 100%;
132 | padding: 0 30px;
133 | font-size: 22px;
134 | line-height: 24px;
135 | }
136 | }
137 |
138 | @media (max-width: 740px) and (orientation: landscape){
139 | padding-top: 40px;
140 | }
141 |
142 | @media (max-width: 400px){
143 | padding-top: 40px;
144 | }
145 | }
146 | }
147 |
148 |
149 | //TRANSITIONS
150 | .screen-fade-enter {
151 | opacity: 0.01;
152 | }
153 |
154 | .screen-fade-enter.screen-fade-enter-active {
155 | opacity: 1;
156 | transition: opacity 500ms ease-in;
157 | }
158 |
159 | .screen-fade-leave {
160 | opacity: 1;
161 | }
162 |
163 | .screen-fade-leave.screen-fade-leave-active {
164 | opacity: 0.01;
165 | transition: opacity 300ms ease-in;
166 | }
167 |
168 | .screen-fade-appear {
169 | opacity: 0.01;
170 | }
171 |
172 | .screen-fade-appear.screen-fade-appear-active {
173 | opacity: 1;
174 | transition: opacity .5s ease-in;
175 | }
176 |
177 |
178 | // LOADING MESSAGE
179 | .AppLoading {
180 | @include border-radius();
181 | width: 300px;
182 | height: 300px;
183 | background-color: $color-two;
184 | background-image: url('/../images/20v_loading.gif');
185 | background-position: center center;
186 | background-repeat: no-repeat;
187 | background-size: 200px 200px;
188 | display: block;
189 | position: absolute;
190 | top: 50%;
191 | left: 50%;
192 | margin-top: -150px;
193 | margin-left: -150px;
194 | z-index: 4;
195 |
196 | @media (max-width: 400px){
197 | width: 200px;
198 | height: 200px;
199 | background-size: 160px 160px;
200 | margin-top: -100px;
201 | margin-left: -100px;
202 | }
203 | }
204 |
205 | // ERROR MESSAGE
206 | a.AppFail:hover,
207 | a.AppFail:visited,
208 | a.AppFail:link {
209 | color: #000;
210 | }
211 |
212 | .AppFail {
213 | @include border-radius();
214 | width: 300px;
215 | height: 300px;
216 | background-color: $color-two;
217 | background-image: url('/../images/20v_error.gif');
218 | background-position: center center;
219 | background-repeat: no-repeat;
220 | background-size: 200px 200px;
221 | display: block;
222 | position: absolute;
223 | top: 50%;
224 | left: 50%;
225 | margin-top: -150px;
226 | margin-left: -150px;
227 | z-index: 4;
228 | text-decoration: none;
229 |
230 | .copy {
231 | width: 100%;
232 | position: absolute;
233 | top: 50%;
234 | left: 50%;
235 | margin-top: 180px;
236 | margin-left: -50%;
237 | height: 60px;
238 | text-align: center;
239 | color: #414141;
240 |
241 | .line1 {
242 | font-size: 20px;
243 | display: block;
244 | line-height: 20px;
245 | }
246 |
247 | .line2 {
248 | font-size: 15px;
249 | line-height: 20px;
250 | }
251 | }
252 |
253 | @media (max-width: 400px){
254 | width: 200px;
255 | height: 200px;
256 | background-size: 160px 160px;
257 | margin-top: -100px;
258 | margin-left: -100px;
259 |
260 | .copy {
261 | margin-top: 130px;
262 | }
263 | }
264 | }
265 |
266 |
267 | // VIDEO PLAYER
268 | .playerContainer {
269 | position: absolute;
270 | width: 100%;
271 | height: 100%;
272 | z-index: 1;
273 | }
274 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var, strict */
2 | var webpack = require('webpack');
3 | var WebpackDevServer = require('webpack-dev-server');
4 | var config = require('./webpack.config');
5 |
6 | new WebpackDevServer(webpack(config), {
7 | contentBase: './public',
8 | publicPath: config.output.publicPath,
9 | hot: true,
10 | historyApiFallback: true
11 | }).listen(3000, 'localhost', function (err) {
12 | if (err) {
13 | console.log(err);
14 | }
15 | console.log('Listening at localhost:3000');
16 | });
17 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';// eslint-disable-line no-unused-vars
2 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group';// eslint-disable-line no-unused-vars
3 | import PlayerManager from './components/PlayerManager';// eslint-disable-line no-unused-vars
4 | import Search from './components/Search';// eslint-disable-line no-unused-vars
5 | import Footer from './components/Footer';// eslint-disable-line no-unused-vars
6 | import Header from './components/Header';// eslint-disable-line no-unused-vars
7 | import Share from './components/Share';// eslint-disable-line no-unused-vars
8 | import { connect } from 'react-redux';
9 | import * as playerActions from './actions/player';
10 | import * as appActions from './actions/app';
11 | import { VIEWS, IS_IPHONE } from './constants/app';
12 |
13 | class App extends Component {
14 | constructor(props) {
15 | super(props);
16 | this._handleStopPlaying = this._handleStopPlaying.bind(this);
17 | this._getFormattedList = this._getFormattedList.bind(this);
18 | this._isIPhone = IS_IPHONE;
19 | }
20 |
21 | render() {
22 | let iPhoneClass = this._isIPhone ? 'iphone' : '';
23 | return (
24 |
25 |
32 | { this.props.view === VIEWS.HOME ? this._getHomeLayout() : null }
33 | { this.props.view === VIEWS.RESULTS ? this._getResultsLayout() : null }
34 | { this.props.view === VIEWS.PLAYER ? this._getPlayerLayout() : null }
35 |
36 |
37 | );
38 | }
39 |
40 | _getHomeLayout() {
41 | return (
42 |
43 |
44 |
45 |
46 |

47 |
48 | Create and enjoy a custom music channel based on a single song
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
56 | _getResultsLayout() {
57 | return (
58 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 | }
66 |
67 | _getPlayerLayout() {
68 | return (
69 |
75 | );
76 | }
77 |
78 | _handleStopPlaying() {
79 | this.props.goToHome();
80 | }
81 |
82 | _getFormattedList(spotifyList) {
83 | return spotifyList.map(track => {
84 | let song = track.name;
85 | let artist = track.artists[0].name.substring(0, 40);
86 | return `${song} - ${artist} official vevo`;
87 | });
88 | }
89 |
90 | }
91 |
92 | function mapStateToProps(state) {
93 | const { app, search, player } = state;
94 | return {
95 | searchText: search.searchText,
96 | resultList: search.resultList,
97 | showPlayer: player.isOpen,
98 | view: app.view
99 | };
100 | }
101 |
102 | export default connect(
103 | mapStateToProps,
104 | {
105 | openPlayer: playerActions.openPlayer,
106 | closePlayer: playerActions.closePlayer,
107 | goToHome: appActions.navigateToHome
108 | }
109 | )(App);
110 |
--------------------------------------------------------------------------------
/src/actions/app.js:
--------------------------------------------------------------------------------
1 | import { CHANGE_APP_VIEW, VIEWS } from '../constants/app';
2 |
3 | export function changeAppView(view = '') {
4 | return {
5 | type: CHANGE_APP_VIEW,
6 | view: view
7 | };
8 | }
9 | export function navigateToHome() {
10 | return changeAppView(VIEWS.HOME);
11 | }
12 |
13 | export function navigateToResults() {
14 | return changeAppView(VIEWS.RESULTS);
15 | }
16 |
17 | export function navigateToPlayer() {
18 | return changeAppView(VIEWS.PLAYER);
19 | }
20 |
--------------------------------------------------------------------------------
/src/actions/player.js:
--------------------------------------------------------------------------------
1 | import { CHANGE_PLAYER_STATUS } from '../constants/player';
2 |
3 | export function changePlayerStatus(isOpen = false) {
4 | return {
5 | type: CHANGE_PLAYER_STATUS,
6 | isOpen: isOpen
7 | };
8 | }
9 |
10 | export function openPlayer() {
11 | return changePlayerStatus(true);
12 | }
13 |
14 | export function closePlayer() {
15 | return changePlayerStatus(false);
16 | }
17 |
--------------------------------------------------------------------------------
/src/actions/search.js:
--------------------------------------------------------------------------------
1 | import { ADD_SEARCH, RETURN_SEARCH, SEARCHING } from '../constants/search';
2 | import Spotify from '../core/Spotify';
3 | import * as appActions from './app';
4 |
5 | export function setSearch(searchText) {
6 | return {
7 | type: ADD_SEARCH,
8 | searchText
9 | };
10 | }
11 |
12 | export function returnSearch(tracks) {
13 | return {
14 | type: RETURN_SEARCH,
15 | tracks
16 | };
17 | }
18 |
19 | export function startSearching() {
20 | return {
21 | type: SEARCHING,
22 | isSearching: true
23 | };
24 | }
25 |
26 | export function stopSearching() {
27 | return {
28 | type: SEARCHING,
29 | isSearching: false
30 | };
31 | }
32 |
33 | export function fetchSearch(text) {
34 | return (dispatch) => {
35 | dispatch(startSearching());
36 | Spotify.search(text, 'US', (tracks) => {
37 | {
38 | // delayed state change prevent UI changes before user navigates to player
39 | const delayedDispatch = ()=> {
40 | dispatch(stopSearching());
41 | };
42 | setTimeout(delayedDispatch, 3000);
43 | }
44 |
45 | dispatch(returnSearch(tracks));
46 | dispatch(appActions.navigateToPlayer());
47 | });
48 | };
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/Footer/_index.scss:
--------------------------------------------------------------------------------
1 | /* 20V - FOOTER */
2 |
3 | .footer {
4 | position: absolute;
5 | bottom: 0;
6 | left: 0;
7 | width: 100%;
8 | font-size: 0;
9 | letter-spacing: 0;
10 | line-height: 0;
11 | padding-left: 15px;
12 | padding-right: 15px;
13 |
14 | p {
15 | width: 100%;
16 | display: inline-block;
17 | vertical-align: top;
18 | text-align: center;
19 | margin: 0;
20 | padding-bottom: 30px;
21 | font-family: $font-open;
22 | font-weight: 600;
23 | font-size: 12px;
24 | line-height: 16px;
25 | letter-spacing: -0.04em;
26 | color: #FFFFFF;
27 |
28 | a {
29 | color: #FFFFFF;
30 | text-decoration: underline;
31 | }
32 | }
33 |
34 | @media (max-width: 740px) and (orientation: landscape){
35 | position: fixed;
36 | }
37 | }
--------------------------------------------------------------------------------
/src/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';// eslint-disable-line no-unused-vars
2 |
3 | let Footer = () => (
4 |
20 | );
21 |
22 | export default Footer;
23 |
--------------------------------------------------------------------------------
/src/components/Header/_index.scss:
--------------------------------------------------------------------------------
1 | /* 20V - HEADER */
--------------------------------------------------------------------------------
/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';// eslint-disable-line no-unused-vars
2 |
3 | let Header = ({ children }) => (
4 |
5 | {children}
6 |
7 | );
8 |
9 | export default Header;
10 |
--------------------------------------------------------------------------------
/src/components/PlayerManager/CGBand.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React, { Component } from 'react';// eslint-disable-line no-unused-vars
3 |
4 | let DEFAULT_ARTIST_NAME = 'Now Playing';
5 |
6 | class CGBand extends Component {
7 | constructor(...props) {
8 | super(...props);
9 | this._timeoutId = null;
10 | this.state = {
11 | show: false,
12 | videoTitle: null
13 | };
14 | }
15 |
16 | componentWillUnmount() {
17 | clearTimeout(this._timeoutId);
18 | }
19 |
20 | render() {
21 | let CGClass = 'CGBand skew' + (this.state.show ? ' in' : '');
22 | let videoTitle = this.state.videoTitle || this.props.videoTitle || '';
23 | let BGRandomGradient = 'background gradient-' + (Math.round(Math.random() * 4) + 1);
24 | let videoDescription = videoTitle.split(' - ');
25 | let artistName = videoDescription[0] ? videoDescription[0] : DEFAULT_ARTIST_NAME;
26 | let songTitle = videoDescription[1] ? videoDescription[1] : videoTitle;
27 | return (
28 |
29 |
30 |
31 |
32 |
{artistName}
33 |
34 |
35 |
{songTitle}
36 |
37 |
38 |
39 | );
40 | }
41 |
42 | show(title, hideTimeout = 999999999) {
43 | this.setState({ show: true, videoTitle: title || this.state.videoTitle });
44 | this._timeoutId = setTimeout(() => {
45 | this.setState({ show: false });
46 | }, hideTimeout);
47 | }
48 |
49 | hide() {
50 | this.setState({ show: false });
51 | clearTimeout(this._timeoutId);
52 | }
53 | }
54 |
55 | export default CGBand;
56 |
--------------------------------------------------------------------------------
/src/components/PlayerManager/Player.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React, { Component } from 'react';// eslint-disable-line no-unused-vars
3 | import Youtube from 'react-youtube';// eslint-disable-line no-unused-vars
4 | import { IS_IPHONE } from './../../constants/app';
5 |
6 | class Player extends Component {
7 |
8 | render() {
9 | let { list, ...other } = this.props;
10 | let firstVideoId = list.shift();
11 | let playerVars = {
12 | controls: 0, // show/hide controls
13 | autohide: 2, // controls autohide
14 | disablekb: 0, // allow keyboard control
15 | fs: 0, // hide fullscreen button
16 | iv_load_policy: 3, // disable anotations
17 | loop: 1,
18 | rel: 0,
19 | showinfo: 0,
20 | modestbranding: 1 // remove watermark/logo
21 | };
22 |
23 | if (!IS_IPHONE) {
24 | playerVars.autoplay = 1;
25 | playerVars.playlist = list.join(',');
26 | }
27 |
28 | let opts = {
29 | height: '100%',
30 | width: '100%',
31 | playerVars
32 | };
33 | return (
34 |
39 | );
40 | }
41 | }
42 |
43 | export default Player;
44 |
--------------------------------------------------------------------------------
/src/components/PlayerManager/PlayerControls.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';// eslint-disable-line no-unused-vars
2 |
3 | class PlayerControls extends Component {
4 | constructor(props) {
5 | super(props);
6 | this._handleMouseMove = this._handleMouseMove.bind(this);
7 | this._handleClose = this._handleClose.bind(this);
8 | this._handleSkip = this._handleSkip.bind(this);
9 | this._handlePlayPause = this._handlePlayPause.bind(this);
10 | this._handleRewind10 = this._handleRewind10.bind(this);
11 | this._handleForward10 = this._handleForward10.bind(this);
12 | this.state = { show: false, status: 1 };
13 | this.timeoutId = null;
14 | }
15 |
16 | componentDidMount() {
17 | this.props.onReady(this);
18 | }
19 |
20 | componentWillUnmount() {
21 | clearTimeout(this.timeoutId);
22 | }
23 |
24 | render() {
25 | let showClass = this.state.show ? 'in' : '';
26 | return (
27 |
30 |
31 |
36 |
37 |
42 |
43 |
44 |
52 |
53 |
58 |
59 |
64 |
65 |
66 |
67 |
{this.props.videoTitle}
68 |
69 |
70 |
71 | );
72 | }
73 |
74 | _handleRewind10(event) {
75 | event.preventDefault();
76 | this.props.onRewind(this);
77 | ga('send', 'event', 'click', 'player', 'rewind10');// eslint-disable-line no-undef
78 | }
79 |
80 | _handlePlayPause(event) {
81 | event.preventDefault();
82 | this.props.onPlayPause(this);
83 | ga('send', 'event', 'click', 'player', 'play/pause');// eslint-disable-line no-undef
84 | }
85 |
86 | _handleForward10(event) {
87 | event.preventDefault();
88 | this.props.onForward(this);
89 | ga('send', 'event', 'click', 'player', 'forward10');// eslint-disable-line no-undef
90 | }
91 |
92 | _handleSkip(event) {
93 | event.preventDefault();
94 | this.props.onSkip(this);
95 | ga('send', 'event', 'click', 'player', 'skip');// eslint-disable-line no-undef
96 | }
97 |
98 | _handleClose(event) {
99 | event.preventDefault();
100 | this.props.onClose(this);
101 | ga('send', 'event', 'click', 'player', 'close');// eslint-disable-line no-undef
102 | }
103 |
104 | _handleMouseMove(event) {
105 | event.preventDefault();
106 | if (!this.state.show) {
107 | this.setState(Object.assign(this.state, { show: true }));
108 | } else {
109 | clearTimeout(this.timeoutId);
110 | }
111 | this.timeoutId = setTimeout(()=> {
112 | this.setState(Object.assign(this.state, { show: false }));
113 | }, 2500);
114 | }
115 |
116 | setStatus(statusCode) {
117 | this.setState(Object.assign(this.state, { status: statusCode }));
118 | }
119 | }
120 |
121 | PlayerControls.defaultProps = {
122 | videoTitle: '',
123 | onReady: ()=> {},
124 | onPlayPause: ()=> {},
125 | onRewind: ()=> {},
126 | onForward: ()=> {},
127 | onSkip: ()=> {},
128 | onClose: ()=> {}
129 | };
130 |
131 | class IphoneControls extends Component {
132 | constructor(props) {
133 | super(props);
134 | this._handleClose = this._handleClose.bind(this);
135 | this._handleSkip = this._handleSkip.bind(this);
136 | }
137 |
138 | render() {
139 | return (
140 |
141 |
146 |
147 |
152 |
153 |
154 | For a better experience, use 20v.co in a wide-screen device
155 |
156 |
157 | );
158 | }
159 |
160 | _handleSkip(event) {
161 | event.preventDefault();
162 | this.props.onSkip(this);
163 | ga('send', 'event', 'click', 'player', 'skip');// eslint-disable-line no-undef
164 | }
165 |
166 | _handleClose(event) {
167 | event.preventDefault();
168 | this.props.onClose(this);
169 | ga('send', 'event', 'click', 'player', 'close');// eslint-disable-line no-undef
170 | }
171 | }
172 |
173 | IphoneControls.defaultProps = {
174 | onSkip: ()=> {},
175 | onClose: ()=> {}
176 | };
177 |
178 | export { PlayerControls as default, IphoneControls };
179 |
--------------------------------------------------------------------------------
/src/components/PlayerManager/_index.scss:
--------------------------------------------------------------------------------
1 | /* 20V - PLAYER */
2 |
3 | iframe {
4 | }
5 |
6 | .iphone {
7 | .custom-player-controls-wrapper {
8 | display: none
9 | }
10 | }
11 |
12 | .PlayerControls-iphone {
13 | position: absolute;
14 | width: 100%;
15 | z-index: 10;
16 | left: 0;
17 | top: 0;
18 | height: 35px;
19 | padding: 10px;
20 | @include simple-gradient( -45deg, $color-one, $color-two );
21 |
22 | .video-close {
23 | float: left;
24 | font-size: 30px;
25 |
26 | a {
27 | color: #FFFFFF;
28 | }
29 | }
30 |
31 | .video-next {
32 | float: right;
33 |
34 | a {
35 | color: #FFFFFF;
36 | text-decoration: none;
37 |
38 | i {
39 | padding-left: 5px;
40 | }
41 | }
42 | }
43 |
44 | .iphone-disclaimer {
45 | width: 100%;
46 | position: absolute;
47 | top: 35px;
48 | left: 0;
49 | text-align: center;
50 | background-color: transparentize( #000000, 0.4 );
51 | color: #FFFFFF;
52 | padding: 10px 50px;
53 | font-size: 13px;
54 | }
55 | }
56 |
57 | .PlayerControls {
58 | position: absolute;
59 | width: 100%;
60 | height: 100%;
61 | top: 0;
62 | right: 0;
63 | bottom: 0;
64 | left: 0;
65 | z-index: 10;
66 |
67 | &.custom-player-controls-wrapper {
68 | @include transition();
69 | opacity: 0;
70 |
71 | &.in {
72 | opacity: 1;
73 | }
74 | }
75 |
76 | .video-close {
77 | width: 50px;
78 | height: 50px;
79 | position: absolute;
80 | top: 10px;
81 | right: 10px;
82 | text-align: center;
83 | line-height: 50px;
84 |
85 | a {
86 | font-size: 70px;
87 | color: #FFFFFF;
88 | text-decoration: none;
89 | }
90 | }
91 |
92 | .video-next {
93 | @include border-radius(5px 0 0 5px);
94 | @include simple-gradient( -45deg, $color-one, $color-two );
95 | box-shadow: 0 0 20px transparentize( #000000, 0.6 );
96 | height: 60px;
97 | position: absolute;
98 | top: 50%;
99 | right: 0;
100 | margin-top: -30px;
101 | text-align: right;
102 | line-height: 60px;
103 | padding: 0 20px;
104 |
105 | a {
106 | font-size: 24px;
107 | color: #FFFFFF;
108 | text-decoration: none;
109 |
110 | .fa {
111 | font-size: 24px;
112 | line-height: 60px;
113 | margin-left: -5px;
114 | padding-left: 10px;
115 | }
116 | }
117 |
118 | @media (max-width: 800px){
119 | span {
120 | display: none;
121 | }
122 | }
123 | }
124 |
125 | .video-playback-controls {
126 | .video-backward {
127 | @include border-radius();
128 | box-sizing: border-box;
129 | width: 60px;
130 | height: 60px;
131 | position: absolute;
132 | left: 50%;
133 | top: 50%;
134 | margin-left: -110px;
135 | margin-top: -30px;
136 | text-align: center;
137 | box-shadow: 0 0 20px transparentize( #000000, 0.6 );
138 | @include simple-gradient( -45deg, $color-one, $color-two );
139 |
140 | .fa {
141 | color: #FFFFFF;
142 | font-size: 24px;
143 | line-height: 60px;
144 | margin-left: -5px;
145 | }
146 | }
147 |
148 | .video-forward {
149 | @include border-radius();
150 | box-sizing: border-box;
151 | width: 60px;
152 | height: 60px;
153 | position: absolute;
154 | left: 50%;
155 | top: 50%;
156 | margin-left: 50px;
157 | margin-top: -30px;
158 | text-align: center;
159 | box-shadow: 0 0 20px transparentize( #000000, 0.6 );
160 | @include simple-gradient( -45deg, $color-one, $color-two );
161 |
162 | .fa {
163 | color: #FFFFFF;
164 | font-size: 24px;
165 | line-height: 60px;
166 | margin-left: 5px;
167 | }
168 | }
169 |
170 | .video-play-pause {
171 | @include border-radius();
172 | box-sizing: border-box;
173 | width: 80px;
174 | height: 80px;
175 | position: absolute;
176 | left: 50%;
177 | top: 50%;
178 | margin-left: -40px;
179 | margin-top: -40px;
180 | text-align: center;
181 | box-shadow: 0 0 20px transparentize( #000000, 0.6 );
182 | @include simple-gradient( -45deg, $color-one, $color-two );
183 |
184 | .fa {
185 | color: #FFFFFF;
186 | font-size: 34px;
187 | line-height: 80px;
188 |
189 | &.fa-play {
190 | padding-left: 4px;
191 | }
192 | }
193 | }
194 | }
195 |
196 | .upper-options-panel {
197 | display: none;
198 | }
199 | }
200 |
201 |
202 | /* VIDEO INFORMATION */
203 | .CGBand {
204 |
205 | /* SKEW */
206 | &.skew {
207 | width: 33%;
208 | width: 600px;
209 | height: 100%;
210 | position: absolute;
211 | z-index: 10;
212 |
213 | @media (max-width: 800px){
214 | width: 100%;
215 | height: 30%;
216 | top: auto;
217 | bottom: 0;
218 | }
219 |
220 | @media (max-width: 740px) and (orientation: landscape){
221 | height: 80px;
222 | }
223 |
224 | @media (max-width: 400px){
225 | height: 150px;
226 | }
227 |
228 | .background {
229 | @include transition( 0.8s, all, ease-in-out );
230 | @include backface-visibility-fix();
231 | @include simple-gradient( -45deg, $color-one, $color-two );
232 | display: block;
233 | position: absolute;
234 | left: 0;
235 | top: 0;
236 | opacity: 0.9;
237 | margin-top: -110px;
238 | margin-left: -800px;
239 | height: 120%;
240 | width: 120%;
241 | box-shadow: 0 0 100px 40px rgba(0,0,0,0.3);
242 |
243 | -webkit-animation: background-rotate 12s infinite linear;
244 | animation: background-rotate 12s infinite linear;
245 |
246 | @media (max-width: 800px){
247 | -webkit-animation: background-rotate 20s infinite linear;
248 | animation: background-rotate 20s infinite linear;
249 | margin-top: 0;
250 | margin-left: -1000px;
251 | }
252 |
253 | @media (max-width: 740px) and (orientation: landscape){
254 | -webkit-animation: background-rotate-wide 10s infinite linear;
255 | animation: background-rotate-wide 10s infinite linear;
256 | margin-left: -900px;
257 | height: 150%;
258 | }
259 |
260 | @media (max-width: 400px){
261 | margin-left: -500px;
262 | }
263 | }
264 |
265 | .content-wrapper {
266 | width: 100%;
267 | height: 100%;
268 | display: block;
269 | position: absolute;
270 | left: 0;
271 | top: 0;
272 |
273 | .upper-row {
274 | display: table;
275 | height: 50%;
276 | width: 100%;
277 | padding: 0 150px 0 50px;
278 |
279 | @media (max-width: 800px){
280 | padding: 20px;
281 | }
282 |
283 | @media (max-width: 740px) and (orientation: landscape){
284 | padding: 5px 20px;
285 | }
286 | }
287 |
288 | .bottom-row {
289 | display: table;
290 | height: 50%;
291 | width: 75%;
292 | padding: 0 150px 0 50px;
293 |
294 | @media (max-width: 800px){
295 | width: 100%;
296 | padding: 20px;
297 | }
298 |
299 | @media (max-width: 740px) and (orientation: landscape){
300 | padding: 0 20px;
301 | }
302 |
303 | @media (max-width: 400px){
304 | padding: 0 20px;
305 | }
306 | }
307 |
308 | h1 {
309 | /* OUT ANIMATION */
310 | @include transition( 0.2s, all, ease-in-out );
311 | @include transition-delay( 0 );
312 | @include backface-visibility-fix();
313 | @include transform-3d( 1000px, '-100%, -100px, 300px' );
314 | font-family: $font-passion;
315 | font-weight: 700;
316 | font-size: 90px;
317 | line-height: 70px;
318 | text-transform: uppercase;
319 | letter-spacing: -0.02em;
320 | color: #FFFFFF;
321 | margin: 0;
322 | padding-bottom: 45px;
323 | display: table-cell;
324 | vertical-align: bottom;
325 | position: relative;
326 | text-shadow: 1px 1px 2px rgba(0,0,0,0.2);
327 |
328 | @media (max-width: 800px){
329 | font-size: 60px;
330 | line-height: 45px;
331 | padding-bottom: 40px;
332 | }
333 |
334 | @media (max-width: 740px) and (orientation: landscape){
335 | font-size: 30px;
336 | line-height: 30px;
337 | padding-bottom: 0;
338 | }
339 |
340 | @media (max-width: 400px){
341 | font-size: 30px;
342 | line-height: 35px;
343 | padding-bottom: 20px;
344 | }
345 |
346 | &:after {
347 | @include transition( 0.5s, all, ease-in-out );
348 | @include transition-delay( 1.5s );
349 | content: '';
350 | position: absolute;
351 | bottom: 0;
352 | left: 0;
353 | width: 0;
354 | height: 4px;
355 | background-color: #FFFFFF;
356 | opacity: 0.5;
357 |
358 | @media (max-width: 800px){
359 | height: 2px;
360 | }
361 |
362 | @media (max-width: 740px) and (orientation: landscape){
363 | display: none;
364 | }
365 | }
366 | }
367 |
368 | h2 {
369 | /* OUT ANIMATION */
370 | @include transition( 0.2s, all, ease-in-out );
371 | @include transition-delay( 0 );
372 | @include backface-visibility-fix();
373 | @include transform-3d( 1000px, '-100%, 0, 500px' );
374 | font-family: $font-open;
375 | font-weight: 300;
376 | font-size: 44px;
377 | line-height: 40px;
378 | text-transform: uppercase;
379 | letter-spacing: -0.02em;
380 | color: #FFFFFF;
381 | margin: 0;
382 | display: table-cell;
383 | vertical-align: middle;
384 | text-shadow: 1px 1px 2px rgba(0,0,0,0.2);
385 | -webkit-transform-origin: center center;
386 | transform-origin: center center;
387 |
388 | @media (max-width: 800px){
389 | font-size: 30px;
390 | line-height: 30px;
391 | vertical-align: top;
392 | }
393 |
394 | @media (max-width: 740px) and (orientation: landscape){
395 | font-size: 20px;
396 | line-height: 22px;
397 | vertical-align: top;
398 | }
399 |
400 | @media (max-width: 400px){
401 | font-size: 20px;
402 | line-height: 22px;
403 | vertical-align: top;
404 | }
405 | }
406 | }
407 |
408 | &.in {
409 | .background {
410 | margin-left: -200px;
411 |
412 | @media (max-width: 800px){
413 | margin-left: -100px;
414 | top: -20px;
415 | }
416 |
417 | @media (max-width: 740px) and (orientation: landscape){
418 | margin-left: -10px;
419 | }
420 |
421 | @media (max-width: 400px){
422 | margin-left: -50px;
423 | }
424 | }
425 |
426 | .content-wrapper {
427 |
428 | .upper-row {
429 | h1 {
430 | /* IN ANIMATION */
431 | @include transition( 1s, all, ease-in-out );
432 | @include transform-3d( 1000px, '0, 0, 0' );
433 | @include transition-delay( 0.5s );
434 |
435 | &:after {
436 | width: 100px;
437 | }
438 | }
439 | }
440 |
441 | .bottom-row {
442 | h2 {
443 | /* IN ANIMATION */
444 | @include transition( 1s, all, ease-in-out );
445 | @include transform-3d( 1000px, '0, 0, 0' );
446 | @include transition-delay( 1s );
447 | }
448 | }
449 | }
450 | }
451 | }
452 | }
453 |
454 | @include rotate-keyframes-animation( 'background-rotate', -5deg, 5deg );
455 | @include rotate-keyframes-animation( 'background-rotate-wide', -2deg, 2deg );
456 |
--------------------------------------------------------------------------------
/src/components/PlayerManager/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React, { Component } from 'react';// eslint-disable-line no-unused-vars
3 | import Player, { IphonePlayer } from './Player';// eslint-disable-line no-unused-vars
4 | import CGBand from './CGBand';// eslint-disable-line no-unused-vars
5 | import PlayerControls, { IphoneControls } from './PlayerControls';// eslint-disable-line no-unused-vars
6 | import { Config, Video } from 'youtube-client-wrapper';// eslint-disable-line no-unused-vars
7 | import { YT_API_CLIENT } from 'appConfig';
8 | import { IS_IPHONE } from './../../constants/app';
9 |
10 | let bootYoutubeClient = () => {
11 | return Config.set({
12 | apiKey: YT_API_CLIENT
13 | })
14 | .boot();
15 | };
16 |
17 | let videos = new Map();
18 | let externalList = [];
19 | class PlayerManager extends Component {
20 | constructor(...props) {
21 | super(...props);
22 | this._onReadyHandler = this._onReadyHandler.bind(this);
23 | this._onEndHandler = this._onEndHandler.bind(this);
24 | this._onPauseHandler = this._onPauseHandler.bind(this);
25 | this._onPlayHandler = this._onPlayHandler.bind(this);
26 | this._onStateChangeHandler = this._onStateChangeHandler.bind(this);
27 | this._onCPReady = this._onCPReady.bind(this);
28 | this._onCPPlayPauseHandler = this._onCPPlayPauseHandler.bind(this);
29 | this._onCPForwardHandler = this._onCPForwardHandler.bind(this);
30 | this._onCPRewindHandler = this._onCPRewindHandler.bind(this);
31 | this._onCPSkipHandler = this._onCPSkipHandler.bind(this);
32 | this._onCPCloseHandler = this._onCPCloseHandler.bind(this);
33 | this._onIphoneCPSkipHandler = this._onIphoneCPSkipHandler.bind(this);
34 | this.close = this.close.bind(this);
35 | /* boot first, then start to process*/
36 | bootYoutubeClient()
37 | .then(() => {
38 | this._collectVideosId();
39 | });
40 |
41 | /* initialization*/
42 | this._player = null;
43 | this._lazyList = [];
44 | this._isLazy = false;
45 | this._currentVideoIndex = null;
46 | this._onScreenTimeoutIds = [];
47 | /* custom youtube player controls*/
48 | this._playerControls = null;
49 |
50 | /* note: when we implement resuming playing, we must
51 | * preserve videos map.
52 | * by now clear it to renew the videos map with externalPlaylist
53 | */
54 | videos = new Map();
55 | externalList = this.props.playlist;
56 |
57 | /* on-screen info configuration*/
58 | this._visualsOnScreen = [{
59 | showAt: 5, // percent
60 | hideIn: 8000, // milliseconds
61 | type: CGBand,
62 | getData: (player) => {
63 | return player.getVideoData().title;
64 | }
65 | }, {
66 | showAt: 50, // percent
67 | hideIn: 8000, // milliseconds
68 | type: CGBand,
69 | getData: (player) => {
70 | return player.getVideoData().title;
71 | }
72 | }, {
73 | showAt: 95, // percent
74 | hideIn: 8000, // milliseconds
75 | type: CGBand,
76 | getData: (player) => {
77 | return player.getVideoData().title;
78 | }
79 | }];
80 |
81 | this.state = {
82 | loading: true,
83 | error: false,
84 | playlist: videos.keys(),
85 | showCGBand: false,
86 | currentVideoTitle: ''
87 | };
88 | }
89 |
90 | componentDidMount() {
91 | this._attachCloseWarning();
92 | }
93 |
94 | componentWillUnmount() {
95 | for (let TId of this._onScreenTimeoutIds) {
96 | clearTimeout(TId);
97 | }
98 | }
99 |
100 | render() {
101 | let content = null;
102 | content = this.state.loading ? this._getLoadingRender() : content;
103 | content = this.state.error ? this._getErrorRender() : content;
104 | content = !this.state.loading && !this.state.error ? this._getYoutubeVideoRender() : content;
105 | return (content);
106 | }
107 |
108 | _collectVideosId() {
109 | let hasTitlesToSearch = !!externalList.length;
110 | let nextAction = hasTitlesToSearch ? this._searchMoreTitles.bind(this) : this._allTitlesSearched.bind(this);
111 | nextAction();
112 | }
113 |
114 | _allTitlesSearched() {
115 | let hasFoundVideos = videos.size > 0;
116 | if (hasFoundVideos) {
117 | // inject playlist into player for lazy loading
118 | this._lazyLoad(Array.from(videos.keys()));
119 | } else {
120 | this._failedFindingVideos();
121 | }
122 | }
123 |
124 | _searchMoreTitles() {
125 | let videoTitle = externalList.shift();
126 | let config = { order: 'viewCount' };
127 | Video.where(videoTitle, config)
128 | .then(page=> {
129 |
130 | let hasFoundVideos = page.elements.length;
131 | if (hasFoundVideos) {
132 | let element = page.firstElement();
133 | // improve selection intelligence here iterating page.elements
134 | videos.set(element.id, element);
135 | }
136 |
137 | let isFirstVideo = videos.size === 1;
138 | if (isFirstVideo) {
139 | // start with first collected video. Others are lazy loaded.
140 | let firstId = Array.from(videos.keys())[0];
141 | this.setState({
142 | loading: false,
143 | error: false,
144 | playlist: [firstId],
145 | showCGBand: false,
146 | currentVideoTitle: videos.get(firstId).title
147 | });
148 | // needed to handle NEXT event on iPhone
149 | this._currentVideoIndex = 0;
150 | }
151 | // updates every loop for UI actions before full list loaded
152 | // in case user click NEXT before all titles are searched.
153 | this._lazyLoad(Array.from(videos.keys()));
154 | this._collectVideosId();
155 | });
156 | }
157 |
158 | _getLoadingRender() {
159 | return (
160 |
161 | );
162 | }
163 |
164 | _getErrorRender() {
165 | return (
166 |
167 |
168 | Sorry, we've run out of videos for your search.
169 | Try again with more devotion
170 |
171 |
172 | );
173 | }
174 |
175 | _getYoutubeVideoRender() {
176 | let onScreenComp = IS_IPHONE
177 | ? this._getIphonePlayerConponents()
178 | : this._getPlayerComponents();
179 | return onScreenComp;
180 | }
181 |
182 | _getIphonePlayerConponents() {
183 | return (
184 |
195 | );
196 | }
197 |
198 | _getPlayerComponents() {
199 | return (
200 |
219 | );
220 | }
221 |
222 | _onCloseHandler(evt) {
223 | evt.preventDefault();
224 | this.close();
225 | }
226 |
227 | _onReadyHandler(evt) {
228 | this._player = evt.target;
229 | if (!IS_IPHONE) {
230 | // onStateChange prop on Player doesn't work.
231 | this._player.addEventListener('onStateChange', this._onStateChangeHandler.bind(this));
232 | }
233 | }
234 |
235 | _onEndHandler() {
236 | if (IS_IPHONE) {
237 | this._iPhoneOnEndHandler();
238 | } else {
239 | this._regularOnEndHandler();
240 | }
241 | }
242 |
243 | _onIphoneCPSkipHandler() {
244 | this._iPhoneNextVideo();
245 | }
246 |
247 | _iPhoneOnEndHandler() {
248 | this._iPhoneNextVideo();
249 | }
250 |
251 | _iPhoneNextVideo() {
252 | // has next?
253 | if (this._currentVideoIndex + 1 === this._lazyList.length) {
254 | // first
255 | this._currentVideoIndex = 0;
256 | } else {
257 | // next
258 | this._currentVideoIndex++;
259 | }
260 |
261 | let videoId = this._lazyList[this._currentVideoIndex];
262 | // update state
263 | this.setState({
264 | loading: false,
265 | error: false,
266 | playlist: [videoId],
267 | showCGBand: false,
268 | currentVideoTitle: videos.get(videoId).title
269 | });
270 | }
271 |
272 | _regularOnEndHandler() {
273 | if (this._isLazy) {
274 | this._isLazy = false;
275 | this._player.loadPlaylist(this._lazyList, 1);
276 | this._player.setLoop(true);
277 | }
278 | this._resetOnScreenTimers();
279 | }
280 |
281 | _onPauseHandler() {
282 | this.refs.CGBand.show(this._player.getVideoData().title);
283 | this._resetOnScreenTimers();
284 | }
285 |
286 | _onPlayHandler() {
287 | const theCGBand = this.refs.CGBand;
288 | const hasTimeouts = !!this._onScreenTimeoutIds.length;
289 | const duration = this._player.getDuration();
290 | const startedVideo = !!duration;
291 |
292 | if (!hasTimeouts && startedVideo) {
293 | // starting video
294 | const videoData = this._player.getVideoData();
295 | const currentTime = this._player.getCurrentTime();
296 | /* eslint-disable no-undef*/
297 | ga('send', 'event', 'video', 'play', `${videoData.video_id}-${videoData.title}`);
298 | /* eslint-enable no-undef*/
299 |
300 | let visualsToDisplay = this._visualsOnScreen.filter((config) => {
301 | let timeout = config.showAt * duration / 100;
302 | return timeout >= currentTime;
303 | });
304 |
305 | let setTimer = (playerData, msToShow, msToHide)=> {
306 | this._onScreenTimeoutIds.push(
307 | setTimeout(() => {
308 | theCGBand.show(playerData, msToHide);
309 | }, msToShow * 1000)
310 | );
311 | };
312 | setTimer = setTimer.bind(this);
313 |
314 | visualsToDisplay.forEach((config)=> {
315 | let msToShow = (config.showAt * duration / 100) - currentTime;
316 | setTimer(config.getData(this._player), msToShow, config.hideIn);
317 | });
318 | }
319 | theCGBand.hide();
320 | }
321 |
322 | _onStateChangeHandler() {
323 | let status = this._player.getPlayerState();
324 | this._playerControls.setStatus(status);
325 | }
326 |
327 | _onCPReady(panel) {
328 | this._playerControls = panel;
329 | }
330 |
331 | _onCPPlayPauseHandler() {
332 | let status = this._player.getPlayerState();
333 | if (status === 1) {
334 | this._player.pauseVideo();
335 | } else {
336 | this._player.playVideo();
337 | }
338 | }
339 |
340 | _onCPCloseHandler() {
341 | this.close();
342 | }
343 |
344 | _onCPForwardHandler() {
345 | let curTime = this._player.getCurrentTime();
346 | let newTime = curTime + 10;
347 | this._player.seekTo(newTime, true);
348 | }
349 |
350 | _onCPRewindHandler() {
351 | let curTime = this._player.getCurrentTime();
352 | let newTime = curTime > 10 ? curTime - 10 : 0;
353 | this._player.seekTo(newTime);
354 | }
355 |
356 | _onCPSkipHandler() {
357 | let videoData = this._player.getVideoData();
358 | /* eslint-disable no-undef*/
359 | ga('send', 'event', 'video', 'skip', `${videoData.video_id}-${videoData.title}`);
360 | /* eslint-enable no-undef*/
361 | if (this._isLazy) {
362 | this._onEndHandler();
363 | } else {
364 | this._player.nextVideo();
365 | }
366 | this._resetOnScreenTimers();
367 | }
368 |
369 | _resetOnScreenTimers() {
370 | this._onScreenTimeoutIds.forEach(Tid=> clearTimeout(Tid));
371 | this._onScreenTimeoutIds = [];
372 | }
373 |
374 | _lazyLoad(list) {
375 | this._lazyList = list;
376 | this._isLazy = true;
377 | }
378 |
379 | _failedFindingVideos() {
380 | this.setState(Object.assign(
381 | this.state,
382 | { error: true, loading: false }
383 | ));
384 | }
385 |
386 | _attachCloseWarning() {
387 | window.onbeforeunload = (event)=> {
388 | const message = 'Sure you want to close?';
389 | if (typeof event === 'undefined') {
390 | event = window.event;
391 | }
392 |
393 | if (event) {
394 | event.returnValue = message;
395 | }
396 | return message;
397 | };
398 | }
399 |
400 | _removeCloseWarning() {
401 | window.onbeforeunload = ()=> null;
402 | }
403 |
404 | close() {
405 | this._removeCloseWarning();
406 | this.props.onClose();
407 | }
408 |
409 |
410 | }
411 |
412 | PlayerManager.defaultProps = {
413 | onClose: ()=> {}
414 | };
415 |
416 | export default PlayerManager;
417 |
--------------------------------------------------------------------------------
/src/components/Search/_index.scss:
--------------------------------------------------------------------------------
1 | /* 20V - SEARCH */
2 |
3 | .search-wrapper {
4 | width: 500px;
5 | position: relative;
6 | margin: 0 auto;
7 | margin-top: 30px;
8 |
9 | &.search-mask {
10 | opacity: 0.5;
11 | }
12 |
13 | input {
14 | @include border-radius( 50px );
15 | width: 100%;
16 | height: 50px;
17 | background-color: #FFFFFF;
18 | border: none;
19 | box-shadow: none;
20 | text-align: center;
21 | padding: 0 50px;
22 | top: 0;
23 | left: 0;
24 | font-size: 17px;
25 | font-weight: 400;
26 | line-height: 50px;
27 | color: $color-one;
28 |
29 | &::-webkit-input-placeholder {
30 | color: $color-one;
31 | }
32 |
33 | &:-moz-placeholder {
34 | color: $color-one;
35 | }
36 |
37 | &::-moz-placeholder {
38 | color: $color-one;
39 | }
40 |
41 | &:-ms-input-placeholder {
42 | color: $color-one;
43 | }
44 |
45 | @media (max-width: 600px){
46 | padding: 0 10px;
47 | }
48 |
49 | @media (max-width: 400px){
50 | height: 40px;
51 | line-height: 40px;
52 | font-size: 13px;
53 | }
54 | }
55 |
56 | .search-icon {
57 | position: absolute;
58 | right: 18px;
59 | top: 13px;
60 |
61 | * {
62 | fill: $color-one;
63 | }
64 |
65 | @media (max-width: 600px){
66 | right: 68px;
67 | }
68 |
69 | @media (max-width: 500px){
70 | display: none;
71 | }
72 | }
73 |
74 | .hide {
75 | display: none;
76 | }
77 |
78 | .react-autosuggest__suggestions {
79 | @include border-radius(15px);
80 | margin: 10px 0 70px 0;
81 | padding: 0;
82 | list-style: none;
83 | z-index: 2;
84 | background-color: #FFFFFF;
85 | overflow: hidden;
86 |
87 | @media (max-width: 600px){
88 | width: 100%;
89 | padding: 0;
90 | }
91 | }
92 |
93 | .react-autosuggest__suggestion {
94 | color: $color-one;
95 | border-bottom: 1px solid transparentize( $color-one, 0.8 );
96 | padding: 15px 0;
97 | cursor: pointer;
98 | text-transform: uppercase;
99 | }
100 |
101 | .react-autosuggest__suggestion--focused {
102 | background-color: $color-one;
103 | color: #FFFFFF;
104 | }
105 |
106 | @media (max-width: 600px){
107 | width: 100%;
108 | padding: 0 20px;
109 | }
110 |
111 | @media (max-width: 400px){
112 | padding: 0 10px;
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/components/Search/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React, { Component } from 'react';// eslint-disable-line no-unused-vars
3 | import { connect } from 'react-redux';
4 | import Autosuggest from 'react-autosuggest';// eslint-disable-line no-unused-vars
5 | import * as searchActions from './../../actions/search';
6 | import Spotify from './../../core/Spotify';
7 |
8 | class Search extends Component {
9 | componentDidMount() {
10 | this._setInitialUIState();
11 | }
12 |
13 | render() {
14 | let time;
15 | let getSuggestions = (input, callback) => {
16 | if (time) {
17 | clearTimeout(time);
18 | }
19 | time = setTimeout(() => {
20 | Spotify.autocomplete(input, 'AR').then((tracks) => {
21 | callback(null, tracks);
22 | });
23 | }, 500);
24 | };
25 |
26 | let suggestionRenderer = (track) => {
27 | return {track.name}, {track.artists.first().name};
28 | };
29 |
30 | let getSuggestionValue = (track) => {
31 | return `${track.name}, ${track.artists.first().name}`;
32 | };
33 |
34 | let showWhen = (input) => {
35 | return input.trim().length > 3;
36 | };
37 |
38 | let onSuggestionSelected = (suggestion) => {
39 | this.props.fetchSearch(suggestion);
40 | ga('send', 'event', 'event', 'new-search', suggestion);
41 | };
42 | onSuggestionSelected = onSuggestionSelected.bind(this);
43 |
44 | let handleKeyPress = (event) => {
45 | if (event.key === 'Enter') {
46 | const text = event.target.value;
47 | if (text.length > 3) {
48 | this.props.fetchSearch(text);
49 | ga('send', 'event', 'key', 'press', 'search-box-enter');
50 | }
51 | }
52 | };
53 |
54 | const disabled = this.props.isSearching ? { disabled: 'disabled' } : {};
55 | const inputAttributes = Object.assign({
56 | id: 'search-input',
57 | type: 'text',
58 | ref: 'searchInput',
59 | className: 'input-search',
60 | placeholder: 'Type a song name and select an option',
61 | onKeyPress: handleKeyPress.bind(this)
62 | }, disabled);
63 | return (
64 |
65 |
76 |

77 |
{this.props.searchText}
78 |
79 | );
80 | }
81 |
82 | _setInitialUIState() {
83 | this.refs.searchbox.refs.input.focus();
84 | }
85 | }
86 |
87 | function mapStateToProps(state) {
88 | const { search } = state;
89 | return {
90 | searchText: search.searchText,
91 | isSearching: search.isSearching
92 | };
93 | }
94 |
95 | export default connect(
96 | mapStateToProps,
97 | {
98 | fetchSearch: searchActions.fetchSearch
99 | }
100 | )(Search);
101 |
--------------------------------------------------------------------------------
/src/components/Share/_index.scss:
--------------------------------------------------------------------------------
1 | /* 20V - Share */
2 |
3 | .share-wrapper {
4 | width: 100%;
5 | margin: 100px auto 0;
6 | position: absolute;
7 | bottom: 100px;
8 | text-align: center;
9 |
10 | span {
11 | clear: both;
12 | margin: 0 auto;
13 | display: block;
14 | text-transform: uppercase;
15 | }
16 |
17 | a {
18 | @include border-radius();
19 | @include transition();
20 | width: 40px;
21 | height: 40px;
22 | display: inline-block;
23 | background-color: transparentize( #FFFFFF, 0.3 );
24 | text-align: center;
25 | padding-top: 10px;
26 | margin: 5px 3px 0;
27 |
28 | img {
29 | display: inline-block;
30 | width: 20px;
31 | height: 20px;
32 | }
33 |
34 | &:hover {
35 | background-color: #FFFFFF;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/Share/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React, { Component } from 'react';// eslint-disable-line no-unused-vars
3 |
4 | class Share extends Component {
5 | constructor() {
6 | super();
7 | this._shareFB = this._shareFB.bind(this);
8 | this._shareTW = this._shareTW.bind(this);
9 | }
10 |
11 | render() {
12 | return (
13 | Share
14 | {this._tw()}
15 | {this._fb()}
16 |
17 | );
18 | }
19 |
20 | _fb() {
21 | return (
22 |
23 | );
24 | }
25 |
26 | _tw() {
27 | return (
28 |
29 | );
30 | }
31 |
32 | _shareFB() {
33 | let url = `http://facebook.com/sharer.php?s=100&p[url]=http://www.20v.co`;
34 | this._share(url);
35 | }
36 |
37 | _shareTW() {
38 | let url = `https://twitter.com/intent/tweet?text=Check out this new app to create video playlists with just one click. Discover 20v, music for your eyes here!&url=http://www.20v.co`;
39 | this._share(url);
40 | }
41 |
42 | _share(url) {
43 | open(
44 | url,
45 | 'Share',
46 | 'height=380,width=660,resizable=0,toolbar=0,menubar=0,status=0,location=0,scrollbars=0'
47 | );
48 | }
49 | }
50 |
51 | export default Share;
52 |
--------------------------------------------------------------------------------
/src/components/player/Player.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | import React, {Component} from 'react';
3 | import Youtube from 'react-youtube';
4 |
5 | class Player extends Component {
6 |
7 | render() {
8 | let {list, ...other} = this.props;
9 | let firstVideoId = list.shift();
10 |
11 | let opts = {
12 | height: '100%',
13 | width: '100%',
14 | playerVars: {
15 | autoplay: 1,
16 | controls: 0,//show/hide controls
17 | autohide: 2,//controls autohide
18 | disablekb: 0,//allow keyboard control
19 | fs: 0,//hide fullscreen button
20 | iv_load_policy: 3,//disable anotations
21 | loop: 1,
22 | rel: 0,
23 | class: 'youtube-player',
24 | showinfo: 0,
25 | modestbranding: 1,//remove watermark/logo
26 | playlist: list.join(',')
27 | }
28 | };
29 | return (
30 |
34 | );
35 | }
36 | }
37 |
38 | export default Player;
39 |
--------------------------------------------------------------------------------
/src/constants/app.js:
--------------------------------------------------------------------------------
1 | export const CHANGE_APP_VIEW = 'change_app_view';
2 | export const VIEWS = {
3 | HOME: 'home',
4 | RESULTS: 'results',
5 | PLAYER: 'player'
6 | };
7 | export const IS_IPHONE = navigator.userAgent.match(/iPhone|iPad|iPod/i);
8 |
--------------------------------------------------------------------------------
/src/constants/player.js:
--------------------------------------------------------------------------------
1 | export const CHANGE_PLAYER_STATUS = 'change_player_status';
2 |
--------------------------------------------------------------------------------
/src/constants/search.js:
--------------------------------------------------------------------------------
1 | export const ADD_SEARCH = 'add_search';
2 | export const RETURN_SEARCH = 'return_search';
3 | export const SEARCHING = 'searching';
4 |
--------------------------------------------------------------------------------
/src/core/Magic.js:
--------------------------------------------------------------------------------
1 | // Magic algorithm
2 | let closest = function(list, x, cant) {
3 | let final_list = [];
4 | let final_cant = list.length > cant ? cant : list.length;
5 |
6 | let search_closest = function(x) {
7 | return list.sort(function(prev, next) {
8 | return Math.abs(x - prev.popularity) - Math.abs(x - next.popularity);
9 | }).splice(0, 1)[0];
10 | };
11 |
12 | let get = function() {
13 | if (final_list.length !== final_cant) {
14 | final_list.push(search_closest(x));
15 | return get();
16 | } else {
17 | return final_list;
18 | }
19 | };
20 | return get(x);
21 | };
22 |
23 | let alternate = function(list) {
24 | let index = 0;
25 | let list_size = list.length;
26 | let process = function(list_process) {
27 | // Search the next item different, remove and return this.
28 | let serchNextDifferent = function(number) {
29 | for (let i = index + 1; i <= list_size; i++) {
30 | if (list_process[i] && list_process[i].artists.first().id !== number) {
31 | return list_process.splice(i, 1)[0];
32 | }
33 | };
34 | };
35 | // Search the next item different, remove and return this.
36 | let serchPrevDifferent = function(number, index) {
37 | for (let i = index - 1; i >= 0; i--) {
38 | if (list_process[i] &&
39 | list_process[i].artists.first().id !== number &&
40 | list_process[i].artists.first().id !== list_process[index].artists.first().id &&
41 | number !== list_process[i - 1].artists.first().id &&
42 | i) {
43 | return list_process.splice(i, 1)[0];
44 | }
45 | };
46 | };
47 | // Check if the current item and the prev are equals
48 | if (list_process[index - 1] &&
49 | list_process[index - 1].artists.first().id === list_process[index].artists.first().id) {
50 | let next = serchNextDifferent(list_process[index].artists.first().id);
51 | if (next) {
52 | list_process.splice(index, 0, next);
53 | } else {
54 | let prev = serchPrevDifferent(list_process[index].artists.first().id, index);
55 | if (prev) {
56 | list_process.splice(index - 1, 0, prev);
57 | } else {
58 | list_process.push(list_process.splice(index, 1)[0]);
59 | }
60 | }
61 | }
62 | // next
63 | if (list_size - 1 !== index) {
64 | index++;
65 | return process(list_process);
66 | } else {
67 | return list_process;
68 | }
69 | };
70 | return process(list);
71 | };
72 |
73 | let orderByPopularity = (list) => {
74 | return list.sort((a, b) => {
75 | return a.popularity - b.popularity;
76 | }).reverse();
77 | };
78 |
79 | let magic = (list, points) => {
80 | return alternate(orderByPopularity(closest(alternate(orderByPopularity(list)), points, 20)));
81 | };
82 |
83 | export default {
84 | closest,
85 | alternate,
86 | magic
87 | };
88 |
--------------------------------------------------------------------------------
/src/core/Spotify.js:
--------------------------------------------------------------------------------
1 | import {Client, TrackHandler, ArtistHandler} from 'spotify-sdk';
2 | import {magic} from './Magic';
3 |
4 | let client = Client.instance;
5 | client.settings = {
6 | clientId: '',
7 | secretId: '',
8 | scopes: 'playlist-modify-public playlist-modify-private',
9 | redirect_uri: 'http://localhost:3000/app/login/index.html'
10 | };
11 | let settings = {
12 | tracks: 20,
13 | artists: 20
14 | };
15 | let track = new TrackHandler();
16 | let total = 0;
17 |
18 | let Spotify = {
19 | trackList: [],
20 | autocomplete: (text, country) => {
21 | return track.search(text, {
22 | limit: 5,
23 | market: country
24 | });
25 | },
26 | search: (text, country, callback, fail) => {
27 | if (text.id) {
28 | return Spotify.getTracks(text, country, callback, fail);
29 | } else {
30 | track.search(text, {
31 | limit: 1,
32 | market: country
33 | }).then((trackCollection) => {
34 | if (trackCollection.length) {
35 | Spotify.getTracks(trackCollection.first(), country, callback, fail);
36 | } else {
37 | callback([]);
38 | }
39 | }).catch(fail);
40 | }
41 | },
42 | getTracks: (track, country, callback, fail) => {
43 | Spotify.trackList = [];
44 | track.artists.first().relatedArtists().then((relatedArtists) => {
45 | relatedArtists = relatedArtists.slice(0, settings.artists - 1);
46 | if (relatedArtists.length) {
47 | relatedArtists.push(track.artists.first());
48 | for (var i = relatedArtists.length - 1; i >= 0; i--) {
49 | total = relatedArtists.length - 1;
50 | relatedArtists[i].topTracks({
51 | country: country
52 | }).then((tracks) => {
53 | if (tracks.length) {
54 | for (var e = tracks.length - 1; e >= 0; e--) {
55 | Spotify.trackList.push(tracks[e]);
56 | if (e === 0) {
57 | total -= 1;
58 | if (total === 0) {
59 | callback(
60 | magic(
61 | Spotify.trackList,
62 | track.popularity
63 | ), track
64 | );
65 | }
66 | }
67 | };
68 | } else {
69 | total -= 1;
70 | }
71 | }).catch(fail);
72 | };
73 | } else {
74 | callback([]);
75 | }
76 | }).catch(fail);
77 | }
78 | };
79 |
80 | export default Spotify;
81 |
--------------------------------------------------------------------------------
/src/devel-config.js:
--------------------------------------------------------------------------------
1 | const KEYS = {
2 | YT_API_CLIENT: 'AIzaSyB8_0tIV6QuSA5Qb1zx3kXW8UAB-cATQXU'
3 | };
4 |
5 | export default KEYS;
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';// eslint-disable-line no-unused-vars
2 | import ReactDom from 'react-dom';
3 | import { Provider } from 'react-redux';// eslint-disable-line no-unused-vars
4 | import App from './App';// eslint-disable-line no-unused-vars
5 | import configureStore from './store/StoreManager';
6 |
7 | require('./../sass/style.scss');
8 |
9 | let store = configureStore();
10 |
11 | ReactDom.render(
12 |
13 |
14 | ,
15 | document.getElementById('appContainer')
16 | );
17 |
--------------------------------------------------------------------------------
/src/reducers/app.js:
--------------------------------------------------------------------------------
1 | import { CHANGE_APP_VIEW, VIEWS } from '../constants/app';
2 |
3 | const initialState = {
4 | view: VIEWS.HOME
5 | };
6 |
7 | export default function (state = initialState, action) {
8 | switch (action.type) {
9 | case CHANGE_APP_VIEW:
10 | return Object.assign({}, state, action);
11 | default:
12 | return state;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import search from './search';
3 | import player from './player';
4 | import app from './app';
5 |
6 | const rootReducer = combineReducers({
7 | search,
8 | player,
9 | app
10 | });
11 |
12 | export default rootReducer;
13 |
--------------------------------------------------------------------------------
/src/reducers/player.js:
--------------------------------------------------------------------------------
1 | import { CHANGE_PLAYER_STATUS } from '../constants/player';
2 |
3 | const initialState = {
4 | isOpen: false
5 | };
6 |
7 | export default function (state = initialState, action) {
8 | switch (action.type) {
9 | case CHANGE_PLAYER_STATUS:
10 | return Object.assign({}, state, action);
11 | default:
12 | return state;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/reducers/search.js:
--------------------------------------------------------------------------------
1 | import { ADD_SEARCH, RETURN_SEARCH, SEARCHING } from '../constants/search';
2 |
3 | const initialState = {
4 | searchText: 'demo',
5 | isSearching: false,
6 | resultList: []
7 | };
8 |
9 | export default function search(state = initialState, action) {
10 | switch (action.type) {
11 | case ADD_SEARCH:
12 | case SEARCHING:
13 | return Object.assign({}, state, action);
14 | case RETURN_SEARCH:
15 | return Object.assign({}, state, { resultList: action.tracks });
16 | default:
17 | return state;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/store/StoreManager.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import thunkMiddleWare from 'redux-thunk';
3 | import rootReducer from './../reducers';
4 |
5 | const createStoreWithMiddleware = compose(
6 | applyMiddleware(thunkMiddleWare),
7 | window.devToolsExtension ? window.devToolsExtension() : f => f
8 | )(createStore);
9 |
10 | export default function configureStore() {
11 | return createStoreWithMiddleware(rootReducer);
12 | }
13 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | var webpack = require('webpack');
3 | var path = require('path');
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
5 |
6 | module.exports = {
7 | entry: [
8 | 'webpack-dev-server/client?http://localhost:3000',
9 | 'webpack/hot/dev-server',
10 | './src/index'
11 | ],
12 | output: {
13 | path: __dirname,
14 | filename: 'bundle.js',
15 | publicPath: '/static/'
16 | },
17 | resolve: {
18 | root: path.resolve(__dirname),
19 | alias: {
20 | 'appConfig': 'src/devel-config.js'
21 | },
22 | extensions: ['', '.js']
23 | },
24 | devtool: 'eval-source-map',
25 | plugins: [
26 | new webpack.HotModuleReplacementPlugin(),
27 | new webpack.NoErrorsPlugin(),
28 | new ExtractTextPlugin('style.css', {
29 | allChunks: true
30 | })
31 | ],
32 | module: {
33 | loaders: [
34 | {
35 | test: /\.jsx?$/,
36 | loaders: ['react-hot', 'babel'],
37 | include: path.join(__dirname, 'src')
38 | },
39 | // the loaders will be applied from right to left
40 | //@see http://stackoverflow.com/questions/26851120/how-can-i-setup-webpack-to-minify-and-combine-scss-and-javascript-like-codekit
41 | {
42 | test: /\.scss$/,
43 | loader: ExtractTextPlugin.extract(
44 | "style",
45 | "css?minimize!sass"
46 | )
47 | },
48 | {
49 | test: /\.jpe?g$|\.gif$|\.png$|\.svg$|\.woff$|\.ttf$/,
50 | loader: 'file'
51 | }
52 | ]
53 | }
54 | };
55 |
--------------------------------------------------------------------------------
/webpack.config.production.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-var */
2 | var webpack = require('webpack');
3 | var path = require('path');
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
5 |
6 | module.exports = {
7 | entry: ['./src/index'],
8 | output: {
9 | path: path.join(__dirname, 'dist'),
10 | filename: 'bundle.js',
11 | publicPath: '/static/'
12 | },
13 | resolve: {
14 | root: path.resolve(__dirname),
15 | alias: {
16 | 'appConfig': 'src/prod-config.js'
17 | },
18 | extensions: ['', '.js']
19 | },
20 | devtool: 'source-map',
21 | plugins: [
22 | new webpack.optimize.OccurenceOrderPlugin(),
23 | new webpack.DefinePlugin({
24 | 'process.env': {
25 | 'NODE_ENV': JSON.stringify('production')
26 | }
27 | }),
28 | new webpack.optimize.UglifyJsPlugin({
29 | compress: {
30 | warnings: false
31 | }
32 | }),
33 | new ExtractTextPlugin('style.css', {
34 | allChunks: true
35 | }),
36 | new webpack.ProvidePlugin({
37 | 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch',
38 | 'Map': 'core-js/fn/map',
39 | 'Symbol': 'core-js/fn/symbol',
40 | 'Promise': 'core-js/fn/promise',
41 | 'Object.assign': 'core-js/fn/object/assign'
42 | })
43 | ],
44 | module: {
45 | loaders: [
46 | {
47 | test: /\.jsx?$/,
48 | loaders: ['babel'],
49 | include: path.join(__dirname, 'src')
50 | },
51 | {
52 | test: /\.scss$/,
53 | loader: ExtractTextPlugin.extract(
54 | "style",
55 | "css?minimize!sass"
56 | )
57 | },
58 | {
59 | test: /\.jpe?g$|\.gif$|\.png$|\.svg$|\.woff$|\.ttf$/,
60 | loader: 'file'
61 | }
62 | ]
63 | }
64 | };
65 |
--------------------------------------------------------------------------------