├── .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 | [![title](https://cloud.githubusercontent.com/assets/1700100/13030218/440b0534-d281-11e5-8f4b-6d5b7b1bbd05.png)](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 | 4 | 7 | -------------------------------------------------------------------------------- /public/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | -------------------------------------------------------------------------------- /public/images/meta-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fusenlabs/20v/6d33fbeb0219392799b5a2ece87a464768972997/public/images/meta-logo.jpg -------------------------------------------------------------------------------- /public/images/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | -------------------------------------------------------------------------------- /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 |
53 | ); 54 | } 55 | 56 | _getResultsLayout() { 57 | return ( 58 |
59 |
60 | 61 | 62 |
64 | ); 65 | } 66 | 67 | _getPlayerLayout() { 68 | return ( 69 |
70 | 74 |
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 |
5 |

6 | Created by   7 | 8 | Fūsenlabs 9 | 10 |   /   11 | 12 | @twitter 13 | 14 |   /   15 | ♥ Open Source 18 |

19 |
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 |
32 | 33 | × 34 | 35 |
36 | 37 |
38 | 39 | NEXT VIDEO 40 | 41 |
42 | 43 |
44 |
45 | 46 | { this.state.status === 1 ? 47 | : 48 | 49 | } 50 | 51 |
52 | 53 |
54 | 55 | 56 | 57 |
58 | 59 |
60 | 61 | 62 | 63 |
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 |
142 | 143 | × 144 | 145 |
146 | 147 |
148 | 149 | NEXT VIDEO 150 | 151 |
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 |
185 | 190 | 194 |
195 | ); 196 | } 197 | 198 | _getPlayerComponents() { 199 | return ( 200 |
201 | 209 | 210 | 218 |
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 | --------------------------------------------------------------------------------