diff --git a/src/js/components/AutocompleteField.js b/src/js/components/AutocompleteField.js index bd58da3e..729b313b 100755 --- a/src/js/components/AutocompleteField.js +++ b/src/js/components/AutocompleteField.js @@ -2,6 +2,7 @@ import React, { PropTypes } from 'react' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' +import { Link, hashHistory } from 'react-router' import ArtistSentence from './ArtistSentence' import * as helpers from '../helpers' @@ -11,6 +12,7 @@ class AutocompleteField extends React.Component{ constructor(props) { super(props) + this.state = { value: '' } @@ -21,6 +23,10 @@ class AutocompleteField extends React.Component{ componentDidMount(){ window.addEventListener("click", this.handleClick, false) + + if (this.props.types.includes('genre') && !this.props.genres){ + this.props.spotifyActions.getGenres() + } } componentWillUnmount(){ @@ -33,10 +39,20 @@ class AutocompleteField extends React.Component{ } } - handleChange(value){ + handleChange(e,value){ var self = this - clearTimeout(this.timer) + + // update our local state this.setState({value: value}) + + // pass the change up the line + if (this.props.onChange){ + this.props.onChange(e,value) + } + + // start a timer to perform the actual search + // this provides a wee delay between key presses to avoid request spamming + clearTimeout(this.timer) this.timer = setTimeout( function(){ self.setState({searching: true}) @@ -46,14 +62,43 @@ class AutocompleteField extends React.Component{ ) } - handleSelect(item){ + handleSelect(e,item){ if (this.props.clearOnSelect){ this.setState({value: ''}) } else { this.setState({value: item.name}) } - this.props.handleSelect(item) + // if we have a handler to pass down to + if (this.props.onSelect){ + this.props.onSelect(e,item) + + // no handler, so let's just go to this asset + } else { + switch (helpers.uriType(item.uri)){ + + case 'album': + hashHistory.push(global.baseURL+'album/'+item.uri) + break + + case 'artist': + hashHistory.push(global.baseURL+'artist/'+item.uri) + break + + case 'playlist': + hashHistory.push(global.baseURL+'playlist/'+item.uri) + break + + case 'track': + hashHistory.push(global.baseURL+'album/'+item.album.uri) + break + + case 'search': + hashHistory.push(global.baseURL+'search/'+item.uri) + break + } + } + this.props.spotifyActions.clearAutocompleteResults(this.id) } @@ -67,6 +112,18 @@ class AutocompleteField extends React.Component{ } } + renderAllResultsButton(){ + if (this.props.hideAllResultsButton || !this.results()){ + return null + } else { + return ( +
this.handleSelect(e,{uri:'iris:search:all:'+this.state.value, name: ''})}> + All results +
+ ) + } + } + renderResults(type){ var results = this.results() if (!results || typeof(results[type]) === 'undefined' || results[type].length <= 0) return null @@ -80,7 +137,7 @@ class AutocompleteField extends React.Component{ { items.map(item => { return ( -
this.handleSelect(item)}> +
this.handleSelect(e,item)}> {item.name} {type == 'tracks' ? : null}
@@ -102,7 +159,7 @@ class AutocompleteField extends React.Component{ this.handleChange(e.target.value)} + onChange={e => this.handleChange(e,e.target.value)} placeholder={this.props.placeholder ? this.props.placeholder : "Start typing..."} />
@@ -115,6 +172,7 @@ class AutocompleteField extends React.Component{ ) }) } + {this.renderAllResultsButton()}
); @@ -123,6 +181,7 @@ class AutocompleteField extends React.Component{ const mapStateToProps = (state, ownProps) => { return { + genres: (state.spotify.genres ? state.spotify.genres : null), results: (state.spotify.autocomplete_results ? state.spotify.autocomplete_results : {}) } } diff --git a/src/js/components/LazyLoadListener.js b/src/js/components/LazyLoadListener.js index a9d29778..92020ec2 100755 --- a/src/js/components/LazyLoadListener.js +++ b/src/js/components/LazyLoadListener.js @@ -31,8 +31,8 @@ export default class LazyLoadListener extends React.Component{ render(){ return ( -
-
+
+ {this.state.loading ?
: null}
) } diff --git a/src/js/components/SearchForm.js b/src/js/components/SearchForm.js index ffc367f1..facfd73f 100755 --- a/src/js/components/SearchForm.js +++ b/src/js/components/SearchForm.js @@ -5,6 +5,7 @@ import { Link, hashHistory } from 'react-router' import { createStore, bindActionCreators } from 'redux' import FontAwesome from 'react-fontawesome' +import AutocompleteField from './AutocompleteField' import * as helpers from '../helpers' import * as uiActions from '../services/ui/actions' @@ -18,7 +19,7 @@ class SearchForm extends React.Component{ } } - handleSubmit(e){ + handleSubmit(e,value){ e.preventDefault() // check for uri type matching @@ -46,12 +47,12 @@ class SearchForm extends React.Component{ render(){ return ( -
this.handleSubmit(e) }> - this.handleSubmit(e, this.state.query)}> + this.setState({ query: e.target.value }) } - value={ this.state.query } /> + onChange={(e,value) => this.setState({query: value})} /> ) } diff --git a/src/js/helpers.js b/src/js/helpers.js index fa771163..8ba2b1d0 100755 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -442,4 +442,26 @@ export let setWindowTitle = function (track = false, play_state = false){ } document.title = title +} + + +/** + * Detect if an item is in the loading queue. We simply loop all load items to + * see if any items contain our searched key. + * + * TODO: Explore performance of this + * + * @param load_queue = obj (passed from store) + * @param key = string (the string to lookup) + * @return boolean + **/ +export let isLoading = function(load_queue = [], key = ''){ + for (var load_queue_key in load_queue) { + if (load_queue.hasOwnProperty(load_queue_key)){ + if (load_queue[load_queue_key] == key){ + return true + } + } + } + return false } \ No newline at end of file diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js index c095ff65..6c1685b0 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -34,7 +34,13 @@ const sendRequest = ( dispatch, getState, endpoint, method = 'GET', data = false } // only if we've got data do we add it to the request (this prevents appending of "&false" to the URL) - if (data) config.data = JSON.stringify(data) + if (data){ + if (typeof(data) === 'string'){ + config.data = data + } else { + config.data = JSON.stringify(data) + } + } // add reference to loader queue var loader_key = helpers.generateGuid() @@ -461,7 +467,6 @@ export function getSearchResults(query, type = 'album,artist,playlist,track', li export function getAutocompleteResults(field_id, query, types = ['album','artist','playlist','track']){ return (dispatch, getState) => { - console.log(types) dispatch({type: 'SPOTIFY_AUTOCOMPLETE_LOADING', field_id: field_id}) @@ -475,16 +480,14 @@ export function getAutocompleteResults(field_id, query, types = ['album','artist endpoint += '&type='+types.join(',') endpoint += '&country='+getState().spotify.country - $.when( - sendRequest(dispatch, getState, endpoint), - sendRequest(dispatch, getState, 'recommendations/available-genre-seeds') - - ).then((search_response, genres_response) => { + sendRequest(dispatch, getState, endpoint) + .then(response => { var genres = [] if (genre_included){ - for (var i = 0; i < genres_response.genres.length; i++){ - if (genres_response.genres[i].includes(query)){ - var genre = genres_response.genres[i] + var available_genres = getState().spotify.genres + for (var i = 0; i < available_genres.length; i++){ + if (available_genres[i].includes(query)){ + var genre = available_genres[i] genres.push({ name: (genre.charAt(0).toUpperCase()+genre.slice(1)).replace('-',' '), uri: 'spotify:genre:'+genre @@ -496,10 +499,10 @@ export function getAutocompleteResults(field_id, query, types = ['album','artist type: 'SPOTIFY_AUTOCOMPLETE_LOADED', field_id: field_id, results: { - artists: (search_response.artists ? search_response.artists.items : []), - albums: (search_response.albums ? search_response.albums.items : []), - playlists: (search_response.playlists ? search_response.playlists.items : []), - tracks: (search_response.tracks ? search_response.tracks.items : []), + artists: (response.artists ? response.artists.items : []), + albums: (response.albums ? response.albums.items : []), + playlists: (response.playlists ? response.playlists.items : []), + tracks: (response.tracks ? response.tracks.items : []), genres: genres } }); @@ -656,7 +659,7 @@ export function getFavorites(limit = 50, term = 'long_term'){ export function getRecommendations(uris = []){ return (dispatch, getState) => { - dispatch({ type: 'SPOTIFY_RECOMMENDATIONS_LOADED', data: false }) + dispatch({type: 'SPOTIFY_RECOMMENDATIONS_LOADED', tracks: []}) // build our starting point var artists_ids = [] @@ -687,12 +690,11 @@ export function getRecommendations(uris = []){ } // construct our endpoint URL with all the appropriate arguments - var endpoint = 'recommendations' - endpoint += '?seed_artists='+artists_ids.join(',') - endpoint += '&seed_tracks='+tracks_ids.join(',') - endpoint += '&seed_genres='+genres.join(',') + var data = 'seed_artists='+artists_ids.join(',') + data += '&seed_tracks='+tracks_ids.join(',') + data += '&seed_genres='+genres.join(',') - sendRequest(dispatch, getState, endpoint) + sendRequest(dispatch, getState, 'recommendations', 'GET', data) .then( response => { dispatch({ type: 'SPOTIFY_RECOMMENDATIONS_LOADED', @@ -714,7 +716,7 @@ export function getGenres(){ .then( response => { dispatch({ type: 'SPOTIFY_GENRES_LOADED', - genres: response + genres: response.genres }); }) } diff --git a/src/js/views/discover/Discover.js b/src/js/views/discover/Discover.js index c1cca3c1..c4d71870 100755 --- a/src/js/views/discover/Discover.js +++ b/src/js/views/discover/Discover.js @@ -22,20 +22,27 @@ class Discover extends React.Component{ this._autocomplete_timer = false this.state = { - seeds: [], + seeds: [ + { + uri: 'spotify:genre:chill', + name: 'Chill' + } + ], + add_seed: '', adding_seed: false } } componentDidMount(){ - this.props.spotifyActions.getGenres() if (this.props.authorized){ this.props.spotifyActions.getFavorites() + } else { + this.getRecommendations() } } componentWillReceiveProps(newProps, newState){ - if (this.props.favorite_artists.length <= 0 && newProps.favorite_artists.length > 0 && this.state.seeds.length <= 0){ + if (this.props.favorite_artists.length <= 0 && newProps.favorite_artists.length){ var initial_seeds = newProps.favorite_artists.sort(() => .5 - Math.random()) initial_seeds = initial_seeds.slice(0,2) @@ -56,7 +63,7 @@ class Discover extends React.Component{ this.getRecommendations(seeds) } - handleSelect(item){ + handleSelect(e,item){ var seeds = this.state.seeds seeds.push(item) this.setState({seeds: seeds}) @@ -81,7 +88,13 @@ class Discover extends React.Component{ ) }) } - this.handleSelect(item)} clearOnSelect /> + this.handleSelect(e,item)} + clearOnSelect + hideAllResultsButton />
) } @@ -94,11 +107,14 @@ class Discover extends React.Component{

Discover new music

-

Add seeds below to build your sound

+

+ Add seeds below to build your sound. Let's start with + {this.props.authorized ? " two of your favorite artists" : " the chill genre"}. +

{this.renderSeeds()}
- {this.props.recommendations ?
: null} + {helpers.isLoading(this.props.load_queue, 'spotify_recommendations') ?
:
} ) } @@ -114,6 +130,7 @@ class Discover extends React.Component{ const mapStateToProps = (state, ownProps) => { return { authorized: state.spotify.authorized, + load_queue: state.ui.load_queue, quick_search_results: (state.spotify.quick_search_results ? state.spotify.quick_search_results : {artists: [], tracks: []}), recommendations: (state.spotify.recommendations ? state.spotify.recommendations : []), favorite_artists: (state.spotify.favorite_artists ? state.spotify.favorite_artists : []), diff --git a/src/scss/app.scss b/src/scss/app.scss index e8757d28..319ea1cd 100755 --- a/src/scss/app.scss +++ b/src/scss/app.scss @@ -7,6 +7,7 @@ @import 'global/forms'; @import 'components/context-menu'; +@import 'components/loader'; @import 'components/modal'; @import 'components/dragger'; @import 'components/images'; diff --git a/src/scss/components/_autocomplete-field.scss b/src/scss/components/_autocomplete-field.scss index 7fdfd324..b78a7bf7 100755 --- a/src/scss/components/_autocomplete-field.scss +++ b/src/scss/components/_autocomplete-field.scss @@ -57,6 +57,18 @@ } } } + + .all-results { + display: block; + padding: 10px 14px; + border-top: 1px solid lighten($dark_grey, 10%); + cursor: pointer; + font-weight: bold; + + &:hover { + background: lighten($dark_grey, 10%); + } + } } } diff --git a/src/scss/components/_debug.scss b/src/scss/components/_debug.scss index 4b11cc90..0e7a92e4 100755 --- a/src/scss/components/_debug.scss +++ b/src/scss/components/_debug.scss @@ -4,6 +4,7 @@ position: fixed; top: 0; right: 0; + z-index: 96; padding: 10px; background: rgba(0,0,0,0.5); color: #FFFFFF; diff --git a/src/scss/components/_loader.scss b/src/scss/components/_loader.scss new file mode 100755 index 00000000..97af7870 --- /dev/null +++ b/src/scss/components/_loader.scss @@ -0,0 +1,22 @@ + +.body-loader { + padding: 50px 0; + + .loader { + @include spin(); + margin: 0 auto; + width: 30px; + height: 30px; + border: { + radius: 100%; + style: solid; + color: $mid_grey; + width: 2px; + } + border-top-color: transparent; + } + + &.lazy-loader { + padding: 20px 0 40px; + } +} \ No newline at end of file diff --git a/src/scss/components/_search-form.scss b/src/scss/components/_search-form.scss index f80e7847..00be09d5 100755 --- a/src/scss/components/_search-form.scss +++ b/src/scss/components/_search-form.scss @@ -1,6 +1,6 @@ .search-form { - z-index: 2; + z-index: 3; position: relative; input { @@ -18,6 +18,18 @@ &.sidebar { padding: 10px 15px 0 15px; + + .autocomplete-field { + .results { + top: 42px; + } + + &.loading { + .input:after { + background: $turquoise; + } + } + } } } diff --git a/src/scss/views/_discover.scss b/src/scss/views/_discover.scss index 7338c326..622aec53 100755 --- a/src/scss/views/_discover.scss +++ b/src/scss/views/_discover.scss @@ -4,6 +4,7 @@ .intro { position: relative; padding: 60px 40px; + z-index: 1; .parallax { position: absolute;