├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── index.css ├── app │ ├── content │ │ ├── alert-icons │ │ │ ├── check.png │ │ │ └── exclamation.png │ │ ├── playlists │ │ │ └── playlists.js │ │ └── playlist-details │ │ │ ├── hybrid-rap-details.json │ │ │ └── experimental-details.json │ ├── components │ │ ├── Header │ │ │ ├── synthetic-logo@2x.png │ │ │ └── Header.js │ │ ├── Player │ │ │ ├── Scrubber.js │ │ │ ├── TrackInformation.js │ │ │ ├── Timestamps.js │ │ │ ├── Controls.js │ │ │ └── Player.js │ │ ├── Button.js │ │ ├── Slider │ │ │ ├── utils.js │ │ │ ├── SliderSelector.js │ │ │ └── Slider.js │ │ ├── SongStats │ │ │ ├── StatGraphRow.js │ │ │ ├── StatElements.js │ │ │ └── SongStatistics.js │ │ ├── PlaylistDetails │ │ │ ├── PlaylistDetails.js │ │ │ └── PlaylistDetailRow.js │ │ ├── Avatar │ │ │ └── avatar.js │ │ ├── PlaylistSelector │ │ │ ├── PlaylistSelector.js │ │ │ └── playlistsList.js │ │ ├── Radar │ │ │ └── RadarSection.js │ │ ├── Instructions │ │ │ ├── Promotion.js │ │ │ └── HowItWorks.js │ │ └── GenreSelector │ │ │ └── GenreSelector.js │ ├── styles │ │ ├── details.css │ │ ├── playlist-details.css │ │ ├── select.css │ │ ├── how-it-works.css │ │ ├── slider.css │ │ ├── buttons.css │ │ ├── compiled-player.css │ │ └── main.css │ ├── genres.json │ ├── javascripts │ │ └── helpers.js │ └── App.js ├── App.test.js ├── index.js └── registerServiceWorker.js ├── .gitignore ├── README.md ├── package.json └── LICENSE /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gillkyle/synthetic/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/content/alert-icons/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gillkyle/synthetic/HEAD/src/app/content/alert-icons/check.png -------------------------------------------------------------------------------- /src/app/content/alert-icons/exclamation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gillkyle/synthetic/HEAD/src/app/content/alert-icons/exclamation.png -------------------------------------------------------------------------------- /src/app/components/Header/synthetic-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gillkyle/synthetic/HEAD/src/app/components/Header/synthetic-logo@2x.png -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './app/App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /src/app/components/Player/Scrubber.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Scrubber extends Component{ 4 | render() { 5 | return ( 6 |
7 | {/*
*/} 8 |
9 | ) 10 | } 11 | }; 12 | 13 | export default Scrubber; -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Synthetic", 3 | "name": "Synthetic App - Spotify Add-on", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#191414", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # vscode 24 | .vscode 25 | 26 | # eslint 27 | .eslintcache -------------------------------------------------------------------------------- /src/app/components/Player/TrackInformation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class TrackInformation extends Component{ 4 | render() { 5 | return ( 6 |
7 |
{this.props.track.name}
8 |
{this.props.track.artist}
9 |
{this.props.track.album}
10 |
11 | ) 12 | } 13 | }; 14 | 15 | export default TrackInformation; -------------------------------------------------------------------------------- /src/app/styles/details.css: -------------------------------------------------------------------------------- 1 | .song-info { 2 | font-size: 18px; 3 | min-height: 75px; 4 | transition: 0.5s; 5 | } 6 | 7 | .example-enter { 8 | opacity: 0.01; 9 | } 10 | 11 | .example-enter.example-enter-active { 12 | opacity: 1; 13 | transition: opacity 500ms ease-in; 14 | } 15 | 16 | .example-leave { 17 | opacity: 1; 18 | } 19 | 20 | .example-leave.example-leave-active { 21 | opacity: 0.01; 22 | transition: opacity 300ms ease-in; 23 | } -------------------------------------------------------------------------------- /src/app/components/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const buttonStyles = { 4 | borderColor: '#eee', 5 | borderWidth: 0, 6 | outline: 'none' 7 | }; 8 | 9 | const Button = ({ className, disabled, id, onClick, value, style = {} }) => ( 10 | 19 | ); 20 | 21 | export default Button; -------------------------------------------------------------------------------- /src/app/genres.json: -------------------------------------------------------------------------------- 1 | { 2 | "genres" : [ "acoustic", "alt-rock", "alternative", "ambient", "bluegrass", "blues", "chill", "classical", "country", "dance", "deep-house", "disco", "drum-and-bass", "dubstep", "edm", "electro", "electronic", "folk", "happy", "heavy-metal", "hip-hop", "indie", "indie-pop", "jazz", "latin", "metal", "minimal-techno", "piano", "pop", "progressive-house", "punk", "r-n-b", "reggae", "reggaeton", "road-trip", "rock", "rock-n-roll", "romance", "sad", "salsa", "show-tunes", "singer-songwriter", "ska", "sleep", "soul", "soundtracks", "spanish", "study", "summer", "synth-pop", "techno", "trance", "work-out" ] 3 | } -------------------------------------------------------------------------------- /src/app/components/Slider/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Capitalize first letter of string 3 | * @private 4 | * @param {string} - String 5 | * @return {string} - String with first letter capitalized 6 | */ 7 | export function capitalize (str) { 8 | return str.charAt(0).toUpperCase() + str.substr(1) 9 | } 10 | 11 | /** 12 | * Clamp position between a range 13 | * @param {number} - Value to be clamped 14 | * @param {number} - Minimum value in range 15 | * @param {number} - Maximum value in range 16 | * @return {number} - Clamped value 17 | */ 18 | export function clamp (value, min, max) { 19 | return Math.min(Math.max(value, min), max) 20 | } -------------------------------------------------------------------------------- /src/app/components/SongStats/StatGraphRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { StatRow, StatTitle, StatValue, StatGraphHolder, StatGraph } from './StatElements'; 3 | 4 | class StatGraphRow extends Component{ 5 | render() { 6 | const { trackDetails, label, detailName } = this.props; 7 | return ( 8 | 9 | {label} 10 | {(trackDetails[detailName] * 100).toFixed(0)} 11 | 12 | 13 | 14 | 15 | ) 16 | } 17 | }; 18 | 19 | export default StatGraphRow; -------------------------------------------------------------------------------- /src/app/components/Player/Timestamps.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Timestamps extends Component{ 4 | convertTime = (timestamp) => { 5 | let minutes = Math.floor(timestamp / 60); 6 | let seconds = timestamp - (minutes * 60); 7 | if(seconds < 10) { 8 | seconds = '0' + seconds; 9 | } 10 | timestamp = minutes + ':' + seconds; 11 | return timestamp; 12 | }; 13 | 14 | render() { 15 | return ( 16 |
17 |
{this.convertTime(this.props.currentTime)}
18 |
{this.convertTime(this.props.duration)}
19 |
20 | ) 21 | } 22 | }; 23 | 24 | export default Timestamps; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Synthetic

2 | 3 |
4 | 5 |
6 |
7 | A client-side React app for discovering music 8 |
9 | 10 |
11 | 12 | ### Setup 13 | 14 | Clone this repo to your desktop and run `npm install` or `yarn install` to install all the dependencies. 15 | 16 | ## Deployment 17 | 18 | Run `npm run build` or `yarn build` to create the build folder for deploys 19 | 20 | You can run the build files on a static server by first installing serve 21 | 22 | ``` 23 | npm install -g serve 24 | ``` 25 | 26 | and then running: 27 | 28 | ``` 29 | npm install -g serve 30 | serve -s build 31 | ``` 32 | 33 | ## Built With 34 | 35 | * [React](https://reactjs.org/) - UI and state management 36 | * [Spotify Web API](https://developer.spotify.com/web-api/) - API data 37 | * [Yarn](https://yarnpkg.com/en/) - dependency management 38 | 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "musicvault", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.16.2", 7 | "chart.js": "^2.7.0", 8 | "glamor": "^2.20.40", 9 | "glamorous": "^4.9.7", 10 | "jquery": "^3.2.1", 11 | "prop-types": "^15.6.0", 12 | "react": "^15.6.1", 13 | "react-alert": "^2.4.0", 14 | "react-chartjs-2": "^2.6.4", 15 | "react-dom": "^15.6.1", 16 | "react-ga": "^2.3.5", 17 | "react-icons": "^2.2.7", 18 | "react-rangeslider": "^2.2.0", 19 | "react-scripts": "1.0.13", 20 | "react-select": "^1.0.0-rc.10", 21 | "react-slick": "^0.15.4", 22 | "react-spinkit": "^3.0.0", 23 | "slick-carousel": "^1.8.1", 24 | "spotify-web-api-js": "^0.22.1" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test --env=jsdom", 30 | "eject": "react-scripts eject" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/components/PlaylistDetails/PlaylistDetails.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PlaylistDetailRow from "./PlaylistDetailRow"; 3 | import playlistsList from "../PlaylistSelector/playlistsList"; 4 | 5 | class PlaylistDetails extends Component { 6 | render() { 7 | let playlistRows = playlistsList.map(playlist => { 8 | return ( 9 | 18 | ); 19 | }); 20 | return ( 21 |
22 |
{playlistRows}
23 |
24 | ); 25 | } 26 | } 27 | 28 | export default PlaylistDetails; 29 | -------------------------------------------------------------------------------- /src/app/components/Player/Controls.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Controls extends Component{ 4 | render() { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | ) 24 | } 25 | }; 26 | 27 | export default Controls; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kyle Gill 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/app/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Avatar from "../Avatar/avatar"; 3 | import BigButton from "../Button"; 4 | import { spotifyImplicitAuth } from "../../javascripts/helpers.js"; 5 | import logo from "./synthetic-logo@2x.png"; 6 | 7 | class Header extends Component { 8 | render() { 9 | return ( 10 |
11 |
12 |
13 | synthetic logo 18 |
19 |
20 |
21 | {this.props.params.access_token ? ( 22 | 23 | ) : ( 24 | spotifyImplicitAuth(this.props.params)} 30 | /> 31 | )} 32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | export default Header; 39 | -------------------------------------------------------------------------------- /src/app/styles/playlist-details.css: -------------------------------------------------------------------------------- 1 | .playlist-detail-row { 2 | display: grid; 3 | grid-template-areas: "playlist-art playlist-description"; 4 | grid-template-columns: 205px 2fr; 5 | margin: 10px 0px; 6 | } 7 | .playlist-art { 8 | height: 175px; 9 | width: 175px; 10 | } 11 | #playlist-details-section-title { 12 | grid-area: playlist-header; 13 | font-size: 36px; 14 | color: #eee; 15 | text-align: center; 16 | align-self: end; 17 | } 18 | #playlist-subheader { 19 | align-self: center; 20 | } 21 | .playlist-info { 22 | grid-area: playlist-description; 23 | text-align: left; 24 | } 25 | .playlist-name { 26 | color: #ccc; 27 | font-size: 28px; 28 | line-height: 1; 29 | margin-bottom: 10px; 30 | vertical-align: bottom; 31 | } 32 | .playlist-description { 33 | font-size: 18px; 34 | color: #aaa; 35 | margin-bottom: 10px; 36 | } 37 | .playlist-follow { 38 | padding: 10px; 39 | border: 1px solid #666; 40 | height: 20px; 41 | background-color: transparent !important; 42 | } 43 | 44 | @media only screen and (max-width: 768px) { 45 | .playlist-name { 46 | color: #ccc; 47 | font-size: 20px; 48 | line-height: 1; 49 | margin-bottom: 10px; 50 | vertical-align: bottom; 51 | } 52 | .playlist-description { 53 | font-size: 12px; 54 | color: #aaa; 55 | margin-bottom: 10px; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/components/Slider/SliderSelector.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import glamorous from 'glamorous'; 3 | import Slider from './Slider'; 4 | 5 | 6 | const SliderRow = glamorous.div({ 7 | maxWidth: 820, 8 | margin: '0 auto', 9 | paddingBottom: 20, 10 | lineHeight: 1.25, 11 | '@media only screen and (max-width: 768px)': { 12 | paddingBottom: 5, 13 | minWidth: 300 14 | } 15 | }); 16 | 17 | 18 | class SliderSelector extends Component{ 19 | 20 | render() { 21 | const { label, value, filterOn } = this.props; 22 | const RadioSelect = glamorous.span({ 23 | color: filterOn ? "#27b7ff" : "#5e5a5a" 24 | }); 25 | return ( 26 | 27 |
28 |
{label}
29 | 37 |
38 | {value} 39 | 40 | 41 | 42 |
43 |
44 |
45 | ) 46 | } 47 | }; 48 | 49 | export default SliderSelector; -------------------------------------------------------------------------------- /src/app/styles/select.css: -------------------------------------------------------------------------------- 1 | /* .Select { 2 | } 3 | .Select-control { 4 | background-color: #191414 !important; 5 | border: 1px solid #e6e6e6; 6 | color: #eee; 7 | } 8 | .Select-placeholder { 9 | color: #eee; 10 | } 11 | .Select-input > input { 12 | color: #eee; 13 | } 14 | .Select-control>.Select-value { 15 | background-color: rgba(153, 153, 153, .0) !important; 16 | border: 1px solid rgba(153, 153, 153, 0.24); 17 | color: #999; 18 | } 19 | .Select-control>.Select-value>.Select-value-icon { 20 | border-right: 1px solid rgba(153, 153, 153, 0.24); 21 | } 22 | .Select-control>.Select-value>.Select-value-label, .Select-value>a { 23 | color: #999; 24 | } 25 | .Select-menu-outer { 26 | background-color: #191414 !important; 27 | } 28 | .Select-option { 29 | background-color: #191414 !important; 30 | color: #eee; 31 | } 32 | .Select-option:hover { 33 | background-color: #1E2028 !important; 34 | color: #ccc; 35 | } 36 | .Select-option:focus { 37 | background-color: #1E2028 !important; 38 | color: #ccc; 39 | } 40 | .Select-option:active { 41 | background-color: #1E2028 !important; 42 | color: #ccc; 43 | } 44 | */ 45 | 46 | .Select-control { 47 | background-color: rgb(230, 230, 230) !important; 48 | border-radius: 18px; 49 | color: #eee; 50 | height: 25px !important; 51 | } 52 | .Select-value { 53 | border-radius: 11px !important; 54 | background-color: #eee !important; 55 | } 56 | .Select-value-label { 57 | color: #999; 58 | background-color: none; 59 | } 60 | .Select-value-icon { 61 | color: #999 !important; 62 | border-top-left-radius: 11px !important; 63 | border-bottom-left-radius: 11px !important; 64 | } 65 | 66 | .Select-menu-outer { 67 | border-bottom-left-radius: 18px; 68 | border-bottom-right-radius: 18px; 69 | } 70 | 71 | .Select-menu { 72 | border-bottom-left-radius: 18px; 73 | border-bottom-right-radius: 18px; 74 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 29 | Synthetic - Find Music and Create Playlists 30 | 31 | 32 | 35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /src/app/components/Avatar/avatar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import glamorous from 'glamorous'; 3 | import { signOut } from '../../javascripts/helpers'; 4 | 5 | 6 | class Avatar extends Component{ 7 | render() { 8 | const AvatarContainer = glamorous.div({ 9 | display: 'grid', 10 | gridTemplateAreas: ` 11 | "text image" 12 | `, 13 | gridTemplateColumns: '1fr 40px' 14 | }) 15 | let bgImage = this.props.me.images[0] ? this.props.me.images[0].url : "https://cdn.vox-cdn.com/images/verge/default-avatar.v989902574302a6378709709f7baab789b242ebbb.gif"; 16 | const AvatarImg = glamorous.div({ 17 | gridArea: 'image', 18 | width: 40, 19 | height: 40, 20 | borderRadius: 20, 21 | background: `url( ${bgImage} )`, 22 | backgroundSize: 'contain', 23 | marginRight: 10 24 | }) 25 | const AvatarText = glamorous.div({ 26 | textAlign: 'right', 27 | gridArea: 'text', 28 | padding: 5, 29 | lineHeight: 1 30 | }) 31 | const AvatarSubText = glamorous.span({ 32 | color: '#aaa', 33 | fontSize: 10, 34 | letterSpacing: 2, 35 | cursor: 'pointer' 36 | }) 37 | return ( 38 | 39 | 40 | 41 | {this.props.me.display_name ? this.props.me.display_name : this.props.me.id} 42 |
43 | signOut()}> 44 | Sign out 45 | 46 |
47 |
48 |
49 | ) 50 | } 51 | } 52 | 53 | Avatar.defaultProps = { 54 | me: { 55 | display_name: "User", 56 | images: [ 57 | { 58 | url: "https://cdn.vox-cdn.com/images/verge/default-avatar.v989902574302a6378709709f7baab789b242ebbb.gif" 59 | } 60 | ] 61 | } 62 | } 63 | 64 | export default Avatar; -------------------------------------------------------------------------------- /src/app/components/PlaylistSelector/PlaylistSelector.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Carousel from "react-slick"; 3 | import "slick-carousel/slick/slick.css"; 4 | import "slick-carousel/slick/slick-theme.css"; 5 | 6 | class PlaylistSelector extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | activeSlide: 1 11 | }; 12 | } 13 | 14 | render() { 15 | const settings = { 16 | infinite: true, 17 | speed: 200, 18 | slidesToShow: 1, 19 | slidesToScroll: 1, 20 | centerMode: true, 21 | swipeToSlide: true, 22 | afterChange: this.props.onChange, 23 | variableWidth: false 24 | }; 25 | return ( 26 |
27 | 28 |
29 |

{this.props.playlists[0].name}

30 |
31 |
32 |

{this.props.playlists[1].name}

33 |
34 |
35 |

{this.props.playlists[2].name}

36 |
37 |
38 |

{this.props.playlists[3].name}

39 |
40 |
41 |

{this.props.playlists[4].name}

42 |
43 |
44 |

{this.props.playlists[5].name}

45 |
46 |
47 |

{this.props.playlists[6].name}

48 |
49 |
50 |

{this.props.playlists[7].name}

51 |
52 |
53 |

{this.props.playlists[8].name}

54 |
55 |
56 |

{this.props.playlists[9].name}

57 |
58 |
59 |

Spotify Library

60 |
61 |
62 |
63 | ); 64 | } 65 | } 66 | 67 | export default PlaylistSelector; 68 | -------------------------------------------------------------------------------- /src/app/styles/how-it-works.css: -------------------------------------------------------------------------------- 1 | .hiw-text { 2 | max-width: 1200px; 3 | } 4 | .blue-gradient { 5 | color: #70d5ff; 6 | } 7 | 8 | .title { 9 | color: #eee; 10 | font-size: 36px; 11 | text-align: center; 12 | line-height: 1; 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | } 17 | .title-subtitle { 18 | font-size: 18px; 19 | color: #aaa; 20 | margin-bottom: 15px; 21 | line-height: 1.5; 22 | } 23 | .title-subtext { 24 | font-size: 16px; 25 | text-align: center; 26 | padding: 15px; 27 | } 28 | .subtitle { 29 | color: #aaa; 30 | font-size: 28px; 31 | line-height: 28px; 32 | margin: 0px 20px 0px 20px; 33 | grid-area: subtitle; 34 | } 35 | .subtitle-icon { 36 | grid-area: icon; 37 | font-size: 54px; 38 | color: #70d5ff; 39 | text-align: center; 40 | padding: 15px; 41 | } 42 | 43 | .section-3-col { 44 | display: grid; 45 | grid-template-areas: "section1 section2 section3"; 46 | grid-template-columns: 1fr 1fr 1fr; 47 | } 48 | .section { 49 | display: grid; 50 | margin: 20px 0px 20px 0px; 51 | grid-template-areas: 52 | "... icon ..." "... subtitle ..." 53 | "content content content"; 54 | grid-template-columns: 1fr 1fr 1fr; 55 | grid-gap: 10px; 56 | align-content: center; 57 | text-align: center; 58 | } 59 | .content { 60 | grid-area: content; 61 | font-size: 20px; 62 | line-height: 24px; 63 | } 64 | .content-step { 65 | margin: 15px; 66 | } 67 | @media only screen and (max-width: 768px) { 68 | .section-3-col { 69 | display: grid; 70 | grid-template-areas: "section1" "section2" "section3"; 71 | grid-template-columns: 1fr; 72 | } 73 | } 74 | 75 | .hiw-image { 76 | grid-area: image; 77 | display: flex; 78 | align-items: center; 79 | justify-content: center; 80 | } 81 | .hiw-image-icon { 82 | font-size: 160px; 83 | color: #fff; 84 | background: linear-gradient(to top left, #27b7ff, #70d5ff); 85 | border-radius: 125px; 86 | height: 250px; 87 | width: 250px; 88 | justify-content: center; 89 | align-items: center; 90 | display: flex; 91 | } 92 | .hiw-image-icon > svg { 93 | margin: 0px 0px 8px 8px; 94 | } 95 | -------------------------------------------------------------------------------- /src/app/components/SongStats/StatElements.js: -------------------------------------------------------------------------------- 1 | import glamorous from 'glamorous'; 2 | 3 | const KeySignatures = { 4 | 0: 'C', 5 | 1: 'C♯, D♭', 6 | 2: 'D', 7 | 3: 'D♯, E♭', 8 | 4: 'E', 9 | 5: 'F', 10 | 6: 'F♯, G♭', 11 | 7: 'G', 12 | 8: 'G♯, A♭', 13 | 9: 'A', 14 | 10: 'A♯, B♭', 15 | 11: 'B' 16 | } 17 | 18 | const StatRow = glamorous.div({ 19 | '@supports (display: grid)': { 20 | display: 'grid', 21 | gridTemplateColumns: '1.5fr 0.5fr 3fr', 22 | gridTemplateAreas: ` 23 | "title value graph" 24 | `, 25 | gridGap: '10px', 26 | }, 27 | fontSize: 17, 28 | marginBottom: 15 29 | }) 30 | const TitleGraph = glamorous.div({ 31 | '@supports (display: grid)': { 32 | display: 'grid', 33 | gridTemplateColumns: '2fr 3fr', 34 | gridTemplateAreas: ` 35 | "title graph" 36 | `, 37 | gridGap: '20px', 38 | }, 39 | fontSize: 17, 40 | marginBottom: 15 41 | }) 42 | const StatText = glamorous.div({ 43 | '@supports (display: grid)': { 44 | display: 'grid', 45 | gridTemplateColumns: '1.5fr 3.5fr', 46 | gridTemplateAreas: ` 47 | "title label" 48 | `, 49 | gridGap: '10px', 50 | }, 51 | fontSize: 17, 52 | marginBottom: 15 53 | }) 54 | const StatTitle = glamorous.div({ 55 | color: '#bbb', 56 | gridArea: 'title', 57 | textAlign: 'left' 58 | }) 59 | const StatTag = glamorous.div({ 60 | color: '#bbb', 61 | gridArea: 'title', 62 | textAlign: 'center', 63 | border: 'solid 1px #ccc', 64 | borderRadius: 2, 65 | padding: 5 66 | }) 67 | const StatValue = glamorous.div({ 68 | color: '#777', 69 | gridArea: 'value', 70 | textAlign: 'right' 71 | }) 72 | const StatLabel = glamorous.div({ 73 | color: '#777', 74 | gridArea: 'label', 75 | textAlign: 'left' 76 | }) 77 | const StatGraphHolder = glamorous.div({ 78 | gridArea: 'graph', 79 | display: 'flex' 80 | }) 81 | const StatGraph = glamorous.div({ 82 | color: '#777', 83 | background: 'linear-gradient(to left, #27b7ff, #70D5FF)', 84 | transition: 'width 0.5s ease', 85 | height: 8, 86 | alignSelf: 'center', 87 | borderRadius: '3px' 88 | }) 89 | 90 | export { KeySignatures, StatRow, StatTitle, StatTag, StatLabel, StatValue, StatGraphHolder, StatGraph, StatText, TitleGraph } -------------------------------------------------------------------------------- /src/app/styles/slider.css: -------------------------------------------------------------------------------- 1 | .slider-grid { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | } 6 | .slider-label { 7 | width: 30% !important; 8 | font-size: 20px; 9 | vertical-align: center; 10 | color: #eee; 11 | text-align: right; 12 | } 13 | .slider-value { 14 | display: grid; 15 | grid-template-areas: "number button"; 16 | grid-template-columns: 50px 1fr; 17 | width: 30% !important; 18 | font-size: 20px; 19 | color: #eee; 20 | text-align: left; 21 | } 22 | 23 | .rangeslider-horizontal { 24 | width: 300px; 25 | margin: 0 auto; 26 | height: 20px; 27 | border-radius: 20px; 28 | } 29 | @media only screen and (max-width: 768px) { 30 | .slider-grid { 31 | display: grid; 32 | grid-template-areas: 33 | "... slider slider slider ..." 34 | "... label label value ..."; 35 | grid-template-columns: 1fr 2fr 2fr 2fr 1fr; 36 | margin: 3px 0; 37 | } 38 | .slider-label { 39 | width: 50% !important; 40 | font-size: 20px; 41 | color: #eee; 42 | text-align: left; 43 | grid-area: label; 44 | } 45 | .slider-value { 46 | width: 50% !important; 47 | font-size: 20px; 48 | color: #eee; 49 | text-align: center; 50 | grid-area: value; 51 | align-self: right; 52 | } 53 | .rangeslider-horizontal { 54 | width: 100%; 55 | margin: 0 auto; 56 | height: 20px; 57 | border-radius: 20px; 58 | grid-area: slider; 59 | } 60 | } 61 | 62 | .slider { 63 | padding: 40px; 64 | } 65 | .rangeslider__handle { 66 | width: 60px !important; 67 | height: 25px !important; 68 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05), 0 1.5px 1.5px rgba(0, 0, 0, 0.12) !important; 69 | } 70 | .rangeslider__handle:focus { 71 | outline: none; 72 | } 73 | .rangeslider__handle::after { 74 | display: none; 75 | } 76 | .rangeslider__fill { 77 | border-radius: 20px !important; 78 | min-width: 50px; 79 | } 80 | .scale-emphasis { 81 | transform: scale(1); 82 | transition: transform 0.2s ease; 83 | cursor: pointer; 84 | } 85 | .scale-emphasis:hover { 86 | transform: scale(1.25); 87 | transition: transform 0.2s ease; 88 | } 89 | 90 | .slick-slide { 91 | color: transparent; 92 | transition: color 0.3s linear; 93 | } 94 | .slick-active { 95 | color: #ccc; 96 | } 97 | -------------------------------------------------------------------------------- /src/app/styles/buttons.css: -------------------------------------------------------------------------------- 1 | .calculateButton-section { 2 | margin-bottom: 30px; 3 | } 4 | .calculateButton { 5 | border-radius: 38px !important; 6 | height: 60px; 7 | width: 225px; 8 | font-size: 20px !important; 9 | color: #eee !important; 10 | background: linear-gradient(to left, #27b7ff, #70d5ff) !important; 11 | transform: scale(1) !important; 12 | transition: transform 0.15s ease !important; 13 | cursor: pointer; 14 | } 15 | .calculateButton:hover { 16 | background: linear-gradient(to left, #39bdff, #80d9ff) !important; 17 | transform: scale(1.035) !important; 18 | transition: transform 0.2s ease !important; 19 | } 20 | .calculateButton:active { 21 | color: #ccc !important; 22 | transform: scale(0.95) !important; 23 | transition: transform 0.1s ease !important; 24 | } 25 | 26 | .loginButton { 27 | border-radius: 38px !important; 28 | height: 40px; 29 | width: 125px; 30 | font-size: 14px !important; 31 | color: #eee !important; 32 | border: 1.5px solid #aaa !important; 33 | background-color: rgba(0, 0, 0, 0) !important; 34 | transform: scale(1) !important; 35 | transition: transform 0.15s ease !important; 36 | cursor: pointer; 37 | } 38 | .loginButton:hover { 39 | transform: scale(1.035) !important; 40 | transition: transform 0.2s ease !important; 41 | border: 2px solid #aaa !important; 42 | } 43 | .loginButton:active { 44 | color: #ccc !important; 45 | transform: scale(0.95) !important; 46 | transition: transform 0.1s ease !important; 47 | } 48 | @media only screen and (max-width: 768px) { 49 | .loginButton { 50 | border-radius: 38px !important; 51 | height: 35px; 52 | width: 100px; 53 | } 54 | } 55 | 56 | .followButton { 57 | border-radius: 38px !important; 58 | height: 40px; 59 | width: 125px; 60 | font-size: 14px !important; 61 | color: #eee !important; 62 | border: 1.5px solid #aaa !important; 63 | background-color: rgba(0, 0, 0, 0) !important; 64 | transform: scale(1) !important; 65 | transition: transform 0.15s ease !important; 66 | cursor: pointer; 67 | } 68 | .followButton:hover { 69 | transform: scale(1.035) !important; 70 | transition: transform 0.2s ease !important; 71 | border: 2px solid #aaa !important; 72 | } 73 | .followButton:active { 74 | color: #ccc !important; 75 | transform: scale(0.95) !important; 76 | transition: transform 0.1s ease !important; 77 | } 78 | @media only screen and (max-width: 768px) { 79 | .followButton { 80 | border-radius: 38px !important; 81 | height: 35px; 82 | width: 100px; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/content/playlists/playlists.js: -------------------------------------------------------------------------------- 1 | // playlist imports 2 | import sampler from "./sampler.json"; 3 | import syntheticLibrary from "./synthetic-library.json"; 4 | import indietronic from "./indietronic.json"; 5 | import hipsterSynthpop from "./hipster-synthpop.json"; 6 | import absoluteFocus from "./absolute-focus.json"; 7 | import tropicalVibes from "./tropical-vibes.json"; 8 | import chillFolk from "./chill-folk.json"; 9 | import epicCinematic from "./epic-cinematic.json"; 10 | import experimental from "./experimental.json"; 11 | import hybridRap from "./hybrid-rap.json"; 12 | import subwoofer from "./subwoofer-sounds.json"; 13 | // playlist detail imports 14 | import samplerDetails from "../playlist-details/sampler-details.json"; 15 | import syntheticLibraryDetails from "../playlist-details/synthetic-library-details.json"; 16 | import indietronicDetails from "../playlist-details/indietronic-details.json"; 17 | import hipsterSynthpopDetails from "../playlist-details/hipster-synthpop-details.json"; 18 | import absoluteFocusDetails from "../playlist-details/absolute-focus-details.json"; 19 | import tropicalVibesDetails from "../playlist-details/tropical-vibes-details.json"; 20 | import chillFolkDetails from "../playlist-details/chill-folk-details.json"; 21 | import epicCinematicDetails from "../playlist-details/epic-cinematic-details.json"; 22 | import experimentalDetails from "../playlist-details/experimental-details.json"; 23 | import hybridRapDetails from "../playlist-details/hybrid-rap-details.json"; 24 | import subwooferDetails from "../playlist-details/subwoofer-details.json"; 25 | 26 | const playlists = [ 27 | { 28 | name: "Synthetic Library", 29 | data: syntheticLibrary, 30 | details: syntheticLibraryDetails 31 | }, 32 | { 33 | name: "Indietronic", 34 | data: indietronic, 35 | details: indietronicDetails 36 | }, 37 | { 38 | name: "Hipster Synthpop", 39 | data: hipsterSynthpop, 40 | details: hipsterSynthpopDetails 41 | }, 42 | { 43 | name: "Absolute Focus", 44 | data: absoluteFocus, 45 | details: absoluteFocusDetails 46 | }, 47 | { 48 | name: "Tropical Vibes", 49 | data: tropicalVibes, 50 | details: tropicalVibesDetails 51 | }, 52 | { 53 | name: "Chill Folk", 54 | data: chillFolk, 55 | details: chillFolkDetails 56 | }, 57 | { 58 | name: "Experimental Sounds", 59 | data: experimental, 60 | details: experimentalDetails 61 | }, 62 | { 63 | name: "Epic Cinematic", 64 | data: epicCinematic, 65 | details: epicCinematicDetails 66 | }, 67 | { 68 | name: "Subwoofer Bass", 69 | data: subwoofer, 70 | details: subwooferDetails 71 | }, 72 | { 73 | name: "Hybrid Rap", 74 | data: hybridRap, 75 | details: hybridRapDetails 76 | }, 77 | { 78 | name: "Spotify Library", 79 | data: sampler, 80 | details: samplerDetails 81 | } 82 | ]; 83 | 84 | export default playlists; 85 | -------------------------------------------------------------------------------- /src/app/components/Radar/RadarSection.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Radar } from "react-chartjs-2"; 3 | import glamorous from "glamorous"; 4 | 5 | const Title = glamorous.div({ 6 | color: "#eee", 7 | fontSize: 26, 8 | marginBottom: 25, 9 | textAlign: "left" 10 | }); 11 | 12 | class RadarSection extends Component { 13 | render() { 14 | const { 15 | track, 16 | trackDetails, 17 | energyValue, 18 | valenceValue, 19 | acousticValue, 20 | danceValue, 21 | popularityValue, 22 | vocalnessValue 23 | } = this.props; 24 | let data = { 25 | labels: [ 26 | "Energy", 27 | "Valence", 28 | "Acoustic", 29 | "Dance", 30 | "Popularity", 31 | "Vocalness" 32 | ], 33 | datasets: [ 34 | { 35 | label: "Input", 36 | lineTension: 0.075, 37 | backgroundColor: "rgba(39, 183, 255, 0.25)", 38 | borderColor: "rgba(39, 183, 255, 1)", 39 | borderWidth: 2, 40 | pointRadius: 0, 41 | pointHitRadius: 5, 42 | pointBackgroundColor: "rgba(39, 183, 255, 1)", 43 | pointBorderColor: "#191414", 44 | pointHoverBorderColor: "rrgba(255,255,255, 0.5)", 45 | data: [ 46 | energyValue, 47 | valenceValue, 48 | acousticValue, 49 | danceValue, 50 | popularityValue, 51 | vocalnessValue 52 | ] 53 | }, 54 | { 55 | label: "Actual", 56 | lineTension: 0.075, 57 | backgroundColor: "rgba(112,213,255, 0.25)", 58 | borderColor: "rgba(255,255,255, 1)", 59 | borderWidth: 2, 60 | pointRadius: 0, 61 | pointHitRadius: 5, 62 | pointBackgroundColor: "rgba(255,255,255, 1)", 63 | pointBorderColor: "#191414", 64 | pointHoverBorderColor: "rgba(255,255,255, 0.5)", 65 | data: [ 66 | trackDetails.energy * 100, 67 | trackDetails.valence * 100, 68 | trackDetails.acousticness * 100, 69 | trackDetails.danceability * 100, 70 | track.popularity, 71 | Math.abs(trackDetails.instrumentalness * 100 - 100) 72 | ] 73 | } 74 | ] 75 | }; 76 | return ( 77 |
78 | Comparison 79 | 111 |
112 | ); 113 | } 114 | } 115 | 116 | export default RadarSection; 117 | -------------------------------------------------------------------------------- /src/app/components/SongStats/SongStatistics.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import glamorous from "glamorous"; 3 | import StatGraphRow from "./StatGraphRow"; 4 | import { 5 | KeySignatures, 6 | StatRow, 7 | StatTitle, 8 | StatTag, 9 | StatLabel, 10 | StatValue, 11 | StatGraphHolder, 12 | StatGraph, 13 | StatText 14 | } from "./StatElements"; 15 | 16 | const Title = glamorous.div({ 17 | color: "#eee", 18 | fontSize: 26, 19 | marginBottom: 25, 20 | textAlign: "left" 21 | }); 22 | const Subtitle = glamorous.div({ 23 | color: "#eee", 24 | fontSize: 18, 25 | marginTop: 10, 26 | marginBottom: 20, 27 | textAlign: "left" 28 | }); 29 | 30 | class SongStatistics extends Component { 31 | render() { 32 | const { track, trackDetails } = this.props; 33 | return ( 34 |
35 | Audio Analysis 36 | 42 | 48 | 54 | 60 | 61 | Popularity 62 | {track.popularity.toFixed(0)} 63 | 64 | 65 | 66 | 67 | 68 | Vocalness 69 | 70 | {Math.abs((trackDetails.instrumentalness * 100).toFixed(0) - 100)} 71 | 72 | 73 | 80 | {" "} 81 | 82 | 83 | 84 | 85 | Composite Score{" "} 86 | 87 | {600 - track.ResultDifference || 0} 88 | 89 | 90 | 91 | {track.explicit ? "EXPLICIT" : "CLEAN"} 92 | 93 | 94 | BPM 95 | {trackDetails.tempo.toFixed(0)} 96 | {/* BPM 97 | {(trackDetails.tempo).toFixed(0)} */} 98 | 99 | 100 | Key 101 | {KeySignatures[trackDetails.key]} 102 | 103 |
104 | * all analyses and metrics are approximations and may not accurately 105 | reflect the true nature of track 106 |
107 |
108 | ); 109 | } 110 | } 111 | 112 | export default SongStatistics; 113 | -------------------------------------------------------------------------------- /src/app/styles/compiled-player.css: -------------------------------------------------------------------------------- 1 | .Player { 2 | grid-area: player; 3 | background: #383838; 4 | overflow: hidden; 5 | box-shadow: 0 5px 10px -5px rgba(18, 18, 18, 1); 6 | height: 475px; 7 | position: relative; 8 | width: 300px; 9 | } 10 | .Player .Background { 11 | width: 150%; 12 | height: 150%; 13 | position: absolute; 14 | top: -25%; 15 | left: -25%; 16 | background-size: cover; 17 | background-position: center center; 18 | opacity: 0.4; 19 | filter: blur(8px); 20 | } 21 | .Player .Artwork { 22 | width: 300px; 23 | height: 300px; 24 | background-size: cover; 25 | background-position: center center; 26 | margin: auto; 27 | box-shadow: 0 5px 10px -5px rgba(18, 18, 18, .25); 28 | position: relative; 29 | } 30 | .Player .TrackInformation { 31 | width: 300px; 32 | margin: 20px auto; 33 | text-align: center; 34 | position: relative; 35 | padding: 0px 20px 0px 20px; 36 | } 37 | .Player .TrackInformation .Name { 38 | font-size: 20px; 39 | margin-bottom: 10px; 40 | font-weight: 300; 41 | } 42 | .Player .TrackInformation .Artist { 43 | font-size: 16px; 44 | font-weight: 500; 45 | } 46 | .Player .TrackInformation .Album { 47 | font-size: 12px; 48 | opacity: 0.5; 49 | } 50 | .Player .Scrubber { 51 | position: absolute; 52 | bottom: 0; 53 | left: 0; 54 | width: 100%; 55 | height: 20%; 56 | opacity: 0.2; 57 | transition: opacity 0.25s ease; 58 | } 59 | .Player .Scrubber .Scrubber-Progress { 60 | background: -moz-linear-gradient(top, rgba(255, 71, 0, 0) 0%, #34BAFD 100%); 61 | background: -webkit-gradient(left top, left bottom, color-stop(0%, rgba(255, 71, 0, 0)), color-stop(100%, #34BAFD)); 62 | background: -webkit-linear-gradient(top, rgba(255, 71, 0, 0) 0%, #34BAFD 100%); 63 | background: -o-linear-gradient(top, rgba(255, 71, 0, 0) 0%, #34BAFD 100%); 64 | background: -ms-linear-gradient(top, rgba(255, 71, 0, 0) 0%, #34BAFD 100%); 65 | background: linear-gradient(to bottom, rgba(255, 71, 0, 0) 0%, #34BAFD 100%); 66 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#34BAFD', endColorstr='#34BAFD', GradientType=0); 67 | height: 100%; 68 | width: 0%; 69 | transition: width 0.25s ease; 70 | pointer-events: none; 71 | } 72 | .Player .Timestamps { 73 | display: flex; 74 | justify-content: space-between; 75 | box-sizing: border-box; 76 | padding: 20px; 77 | position: absolute; 78 | top: 0; 79 | left: 0; 80 | pointer-events: none; 81 | width: 100%; 82 | } 83 | .Player .Timestamps .Time { 84 | font-size: 12px; 85 | } 86 | .Player .Controls { 87 | position: absolute; 88 | top: 10px; 89 | pointer-events: none; 90 | margin: auto; 91 | left: 0; 92 | right: 0; 93 | z-index: 5; 94 | display: flex; 95 | width: 200px; 96 | } 97 | .Player .Controls .Button { 98 | height: 40px; 99 | width: 40px; 100 | box-sizing: border-box; 101 | display: flex; 102 | align-items: center; 103 | justify-content: center; 104 | margin: auto; 105 | pointer-events: all; 106 | cursor: pointer; 107 | } 108 | .Player .Controls .Button:hover { 109 | transform: scale(1.09); 110 | } 111 | .Player .Controls .Button:active { 112 | transform: scale(0.98); 113 | } 114 | .Player .Controls .Button:active .fa { 115 | color: #34BAFD; 116 | opacity: 1; 117 | } 118 | .Player .Controls .Button .fa { 119 | color: #fff; 120 | opacity: 0.5; 121 | font-size: 16px; 122 | } 123 | .Player .Controls .Button .fa.fa-play { 124 | margin-left: 5px; 125 | } 126 | .PlayButton { 127 | border: 2px solid rgba(255, 255, 255, .5); 128 | border-radius: 75px; 129 | height: 40px; 130 | width: 40px; 131 | } 132 | .PlayButton:active { 133 | border: 2px solid #34BAFD; 134 | } 135 | 136 | .EmptyHeader { 137 | height: 60px; 138 | } 139 | #button-outer-left { 140 | text-align: right !important; 141 | margin-left: 5px; 142 | } 143 | #button-outer-right { 144 | text-align: left !important; 145 | margin-right: 5px; 146 | } -------------------------------------------------------------------------------- /src/app/styles/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | /* background: linear-gradient(to left, #27b7ff, #70D5FF); */ 3 | background: #191414; 4 | font-family: "Montserrat", sans-serif; 5 | color: #5e5a5a; 6 | } 7 | h4 { 8 | letter-spacing: 4px; 9 | color: #5e5a5a; 10 | } 11 | 12 | .App { 13 | text-align: center; 14 | } 15 | 16 | .app-header { 17 | background-color: #1e1b1b; 18 | height: 60px; 19 | color: white; 20 | display: grid; 21 | grid-template-areas: "... title title title title login"; 22 | grid-template-columns: 1.5fr repeat(4, 2fr) 1.5fr; 23 | text-align: center; 24 | } 25 | .app-header-title { 26 | letter-spacing: 12px; 27 | font-weight: 900; 28 | font-size: 42px; 29 | grid-area: title; 30 | align-self: center; 31 | margin: 0 auto; 32 | } 33 | .playlist-selector { 34 | text-align: center; 35 | margin: 10px; 36 | display: grid; 37 | grid-template-areas: "... carousel ..."; 38 | grid-template-columns: 1fr 320px 1fr; 39 | } 40 | .app-footer { 41 | margin-top: 30px; 42 | margin-bottom: 30px; 43 | } 44 | 45 | .login-section { 46 | grid-area: login; 47 | align-self: right; 48 | padding: 10px; 49 | } 50 | 51 | .player-section { 52 | display: grid; 53 | grid-template-areas: "... radar player stats ..."; 54 | grid-template-columns: 0.75fr 1fr 300px 1fr 0.75fr; 55 | grid-gap: 20px; 56 | align-items: flex-start; 57 | font-family: "Montserrat", sans-serif; 58 | color: white; 59 | } 60 | .radar-section { 61 | grid-area: radar; 62 | margin-top: 20px; 63 | max-width: 300px; 64 | max-height: 300px; 65 | justify-self: end; 66 | } 67 | .stats-section { 68 | grid-area: stats; 69 | width: 300px; 70 | margin-top: 20px; 71 | } 72 | .promotion-section { 73 | background: #1e1b1b; 74 | margin: 60px 0px 0px 0px; 75 | padding: 60px; 76 | } 77 | .instructions-section { 78 | display: grid; 79 | grid-template-areas: "section1" "section2" "section3"; 80 | grid-auto-rows: auto auto 1fr; 81 | grid-gap: 60px; 82 | padding: 60px; 83 | justify-content: center; 84 | } 85 | .genre-section { 86 | display: grid; 87 | grid-template-areas: "... label selector details ..."; 88 | grid-template-columns: 2fr 1fr 300px 1fr 2fr; 89 | grid-gap: 15px; 90 | margin-bottom: 20px; 91 | text-align: left; 92 | color: #eee; 93 | } 94 | .selector-label { 95 | grid-area: label; 96 | text-align: right; 97 | font-size: 20px; 98 | align-self: center; 99 | } 100 | .selector-details-label { 101 | grid-area: details; 102 | font-size: 20px; 103 | align-self: center; 104 | } 105 | .playlist-details-section { 106 | display: grid; 107 | grid-template-areas: "playlists"; 108 | grid-auto-rows: 1fr; 109 | } 110 | 111 | @media only screen and (max-width: 768px) { 112 | .app-header { 113 | height: inherit; 114 | grid-template-areas: "title login"; 115 | grid-template-columns: 1fr 1fr; 116 | grid-gap: 10px; 117 | line-height: 1; 118 | padding: 10px 10px 10px 10px; 119 | } 120 | .app-header-title { 121 | letter-spacing: 4px; 122 | font-weight: 900; 123 | font-size: 36px; 124 | } 125 | .promotion-section { 126 | padding: 20px; 127 | } 128 | .instructions-section { 129 | padding: 20px; 130 | } 131 | .genre-section { 132 | grid-template-areas: 133 | "... selector selector selector ..." 134 | "... label label details ..."; 135 | grid-template-columns: 1fr 1fr 2fr 1fr 1fr; 136 | grid-gap: 5px; 137 | } 138 | .selector-label { 139 | text-align: left; 140 | } 141 | .player-section { 142 | grid-template-areas: "... player ..." "... stats ..." "... radar ..."; 143 | grid-template-columns: 1fr 300px 1fr; 144 | grid-gap: 10px; 145 | align-items: center; 146 | } 147 | .radar-section { 148 | margin-bottom: 30px; 149 | justify-self: start; 150 | } 151 | .playlist-selector { 152 | grid-template-columns: 1fr 200px 1fr; 153 | margin: 30px; 154 | } 155 | } 156 | 157 | .loading-spinner { 158 | text-align: center; 159 | margin-top: 6px; 160 | } 161 | -------------------------------------------------------------------------------- /src/app/components/Instructions/Promotion.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "../../styles/how-it-works.css"; 3 | import * as Fa from "react-icons/lib/fa"; 4 | import logo from "../Header/synthetic-logo@2x.png"; 5 | 6 | class Promotion extends Component { 7 | render() { 8 | return ( 9 |
10 |
11 | synthetic logo 16 |
17 | Spotify powered, data-driven music discovery 18 |
19 |
20 |
21 |
22 |
23 | 24 |
25 |
Customize
26 |
27 |
28 | 29 | Select a playlist 30 |
31 |
32 | Adjust filter levels 33 |
34 |
35 | Turn filters on/off 36 |
37 |
38 | 50 | 51 | 52 | 53 | 54 | Calculate 55 |
56 |
57 |
58 |
59 |
60 | 61 |
62 |
Discover
63 |
64 |
65 | 66 | Play/Pause a song 67 |
68 |
69 | 70 | Cycle through songs in result set 71 |
72 |
73 | Add song to your library on Spotify 74 |
75 |
76 | Add top results into a playlist 77 |
78 |
79 |
80 |
81 |
82 | 83 |
84 |
Visualize
85 |
86 |
Easily digest song stats
87 |
88 | Compare your search to actual stats 89 |
90 |
91 | Avoid songs with explicit lyrics 92 |
93 |
94 | Sort instrumental or vocal tracks 95 |
96 |
97 |
98 |
99 |
100 | ); 101 | } 102 | } 103 | 104 | export default Promotion; 105 | -------------------------------------------------------------------------------- /src/app/components/Player/Player.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import glamorous from 'glamorous'; 3 | import TrackInformation from './TrackInformation'; 4 | import Scrubber from './Scrubber'; 5 | import Controls from './Controls'; 6 | import Timestamps from './Timestamps'; 7 | 8 | 9 | const GreenPlayerDivider = glamorous.div({ 10 | borderBottom: '3px solid #34BAFD' 11 | }); 12 | 13 | class Player extends Component{ 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | playStatus: 'play', 18 | currentTime: 0 19 | } 20 | }; 21 | 22 | componentDiDMount() { 23 | this.props.ref(this); 24 | this.setState({ 25 | currentTime: 0, 26 | playStatus: 'play' 27 | }); 28 | } 29 | componentWillUnmount() { 30 | this.loadInterval && clearInterval(this.loadInterval); 31 | this.loadInterval = false; 32 | } 33 | shouldComponentUpdate(nextProps, nextState) { 34 | // set of conditions that check to make sure the Player should actually update UI 35 | // rather than get stuck infintie looping 36 | return (nextProps.track.name !== this.props.track.name) || 37 | (nextProps.track.source !== this.props.track.source) || 38 | (this.props.songInLibrary !== nextProps.songInLibrary) || 39 | (this.props.createdPlaylist !== nextProps.createdPlaylist) || 40 | (this.state.playStatus !== nextState.playStatus) || 41 | (this.props.playStatus !== nextProps.playStatus) || 42 | (this.state.currentTime !== nextState.currentTime); 43 | } 44 | 45 | stopPlayback = () => { 46 | // activated from App.js by higher level functions to stop the player playback 47 | let audio = document.getElementById('audio'); 48 | audio.pause(); 49 | this.setState({ playStatus: 'play' }); 50 | audio.load(); 51 | } 52 | 53 | updateTime = (timestamp) => { 54 | timestamp = Math.floor(timestamp); 55 | this.setState({ currentTime: timestamp }); 56 | } 57 | 58 | updateScrubber = (percent) => { 59 | // Set scrubber width 60 | let innerScrubber = document.querySelector('.Scrubber-Progress'); 61 | if (innerScrubber){innerScrubber.style['width'] = percent;} 62 | } 63 | 64 | togglePlay = () => { 65 | let status = this.state.playStatus; 66 | let audio = document.getElementById('audio'); 67 | if(status === 'play') { 68 | status = 'pause'; 69 | audio.play(); 70 | let that = this; 71 | this.loadInterval = setInterval(function() { 72 | let currentTime = audio.currentTime; 73 | let duration = that.props.track.duration; 74 | 75 | // Calculate percent of song 76 | let percent = (currentTime / duration) * 100 + '%'; 77 | that.updateScrubber(percent); 78 | that.updateTime(currentTime); 79 | }, 100); 80 | } else { 81 | status = 'play'; 82 | audio.pause(); 83 | } 84 | this.setState({ playStatus: status }); 85 | }; 86 | 87 | render() { 88 | return ( 89 |
90 |
91 | 92 | 102 | 103 |
104 |
105 | 106 | 107 | 108 | 111 |
112 | ) 113 | } 114 | }; 115 | 116 | Player.defaultProps = { 117 | track: { 118 | name: 'We Were Young', 119 | artist: 'Odesza', 120 | album: 'Summers Gone', 121 | artwork: 'https://funkadelphia.files.wordpress.com/2012/09/odesza-summers-gone-lp.jpg', 122 | duration: 192, 123 | source: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/557257/wwy.mp3' 124 | } 125 | }; 126 | 127 | export default Player; -------------------------------------------------------------------------------- /src/app/components/PlaylistDetails/PlaylistDetailRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "../../styles/playlist-details.css"; 3 | import Button from "../Button"; 4 | import Spotify from "spotify-web-api-js"; 5 | 6 | class PlaylistDetailRow extends Component { 7 | constructor(props, context) { 8 | super(props, context); 9 | this.state = { 10 | following: false, 11 | beenChanged: false 12 | }; 13 | } 14 | 15 | shouldComponentUpdate(nextProps, nextState) { 16 | // easier to read conditions for whether the row should update to show a change in the following button 17 | let shouldUpdate = true; 18 | if (nextProps.userId === null) { 19 | shouldUpdate = false; 20 | } 21 | if (nextProps.loggedIn === false) { 22 | shouldUpdate = false; 23 | } 24 | if (nextProps.initialLoad === false) { 25 | shouldUpdate = false; 26 | } 27 | if (nextState.following === true && this.state.following === true) { 28 | shouldUpdate = false; 29 | } 30 | return shouldUpdate; 31 | } 32 | 33 | onClickFollow() { 34 | // follow playlist 35 | if (!this.props.loggedIn) { 36 | this.props.showFollowAlert(); 37 | } else if (!this.state.following) { 38 | const spotifyApi = new Spotify(); 39 | spotifyApi.setAccessToken(this.props.accessToken); 40 | spotifyApi 41 | .followPlaylist(this.props.playlist.owner.id, this.props.playlist.id) 42 | .then(response => { 43 | if (response[0] == true) { 44 | this.setState({ 45 | following: true 46 | }); 47 | } 48 | }) 49 | .catch(function(error) { 50 | console.error(error); 51 | }); 52 | } else if (this.state.following) { 53 | // unfollow playlist 54 | const spotifyApi = new Spotify(); 55 | spotifyApi.setAccessToken(this.props.accessToken); 56 | spotifyApi 57 | .unfollowPlaylist(this.props.playlist.owner.id, this.props.playlist.id) 58 | .then(response => { 59 | if (response[0] == true) { 60 | this.setState({ 61 | following: false 62 | }); 63 | } 64 | }) 65 | .catch(function(error) { 66 | console.error(error); 67 | }); 68 | } 69 | this.setState({ 70 | following: !this.state.following, 71 | beenChanged: true 72 | }); 73 | } 74 | 75 | render() { 76 | //console.log(this.props); 77 | if (this.props.loggedIn && !this.state.beenChanged) { 78 | const spotifyApi = new Spotify(); 79 | spotifyApi.setAccessToken(this.props.accessToken); 80 | spotifyApi 81 | .areFollowingPlaylist( 82 | this.props.playlist.owner.id, 83 | this.props.playlist.id, 84 | [this.props.userId] 85 | ) 86 | .then(response => { 87 | if (response[0] == true) { 88 | this.setState({ 89 | following: true 90 | }); 91 | } 92 | }) 93 | .catch(function(error) { 94 | console.error(error); 95 | }); 96 | } 97 | 98 | return ( 99 |
100 |
101 | playlist art 106 |
107 |
108 |
{this.props.playlist.name}
109 |
110 | {this.props.playlist.description || "description"} 111 |
112 |
113 | 127 |
128 |
129 |
130 | ); 131 | } 132 | } 133 | 134 | export default PlaylistDetailRow; 135 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (!isLocalhost) { 36 | // Is not local host. Just register service worker 37 | registerValidSW(swUrl); 38 | } else { 39 | // This is running on localhost. Lets check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/app/components/GenreSelector/GenreSelector.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import glamorous from 'glamorous'; 3 | import Select from 'react-select'; 4 | import 'react-select/dist/react-select.css'; 5 | import '../../styles/select.css'; 6 | 7 | const options = [ 8 | { value: 'acoustic', label: 'acoustic'}, 9 | { value: 'alt-rock', label: 'alt-rock'}, 10 | { value: 'alternative', label: 'alternative'}, 11 | { value: 'ambient', label: 'ambient'}, 12 | { value: 'bluegrass', label: 'bluegrass'}, 13 | { value: 'blues', label: 'blues'}, 14 | { value: 'chill', label: 'chill'}, 15 | { value: 'classical', label: 'classical'}, 16 | { value: 'country', label: 'country'}, 17 | { value: 'dance', label: 'dance'}, 18 | { value: 'deep-house', label: 'deep-house'}, 19 | { value: 'disco', label: 'disco'}, 20 | { value: 'drum-and-bass', label: 'drum-and-bass'}, 21 | { value: 'dubstep', label: 'dubstep'}, 22 | { value: 'edm', label: 'edm'}, 23 | { value: 'electro', label: 'electro'}, 24 | { value: 'electronic', label: 'electronic'}, 25 | { value: 'folk', label: 'folk'}, 26 | { value: 'happy', label: 'happy'}, 27 | { value: 'heavy-metal', label: 'heavy-metal'}, 28 | { value: 'hip-hop', label: 'hip-hop'}, 29 | { value: 'indie', label: 'indie'}, 30 | { value: 'indie-pop', label: 'indie-pop'}, 31 | { value: 'jazz', label: 'jazz'}, 32 | { value: 'latin', label: 'latin'}, 33 | { value: 'metal', label: 'metal'}, 34 | { value: 'minimal-techno', label: 'minimal-techno'}, 35 | { value: 'piano', label: 'piano'}, 36 | { value: 'pop', label: 'pop'}, 37 | { value: 'progressive-house', label: 'progressive-house'}, 38 | { value: 'punk', label: 'punk'}, 39 | { value: 'r-n-b', label: 'r-n-b'}, 40 | { value: 'reggae', label: 'reggae'}, 41 | { value: 'reggaeton', label: 'reggaeton'}, 42 | { value: 'road-trip', label: 'road-trip'}, 43 | { value: 'rock', label: 'rock'}, 44 | { value: 'rock-n-roll', label: 'rock-n-roll'}, 45 | { value: 'romance', label: 'romance'}, 46 | { value: 'sad', label: 'sad'}, 47 | { value: 'salsa', label: 'salsa'}, 48 | { value: 'show-tunes', label: 'show-tunes'}, 49 | { value: 'singer-songwriter', label: 'singer-songwriter'}, 50 | { value: 'ska', label: 'ska'}, 51 | { value: 'sleep', label: 'sleep'}, 52 | { value: 'soul', label: 'soul'}, 53 | { value: 'soundtracks', label: 'soundtracks'}, 54 | { value: 'spanish', label: 'spanish'}, 55 | { value: 'study', label: 'study'}, 56 | { value: 'summer', label: 'summer'}, 57 | { value: 'synth-pop', label: 'synth-pop'}, 58 | { value: 'techno', label: 'techno'}, 59 | { value: 'trance', label: 'trance'}, 60 | { value: 'work-out', label: 'work-out'} 61 | ]; 62 | 63 | class GenreSelector extends Component{ 64 | constructor(props) { 65 | super(props); 66 | this.state = { 67 | stayOpen: false 68 | } 69 | } 70 | 71 | render() { 72 | const { stayOpen, disabled } = this.state; 73 | const { filterOn, seed_genres, onChange } = this.props; 74 | const RadioSelect = glamorous.span({ 75 | color: filterOn ? "#27b7ff" : "#5e5a5a" 76 | }); 77 | // if (disabled) { 78 | // let genreInput = document.querySelector('.Select-control'); 79 | // genreInput.setAttribute('style', 'background-color: #191414 !important;'); 80 | // } else { 81 | // let genreInput = document.querySelector('.Select-control'); 82 | // genreInput.setAttribute('style', 'background-color: rgb(230,230,230) !important;'); 83 | // }; 84 | 85 | return ( 86 |
87 |
88 | GENRES 89 |
90 |
91 |