Autocomplete search now in sidebar

This commit is contained in:
James Barnsley
2017-05-14 07:16:11 +12:00
parent f6369ee895
commit a09348fb8c
12 changed files with 193 additions and 43 deletions

View File

@ -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 (
<div className="all-results" onClick={e => this.handleSelect(e,{uri:'iris:search:all:'+this.state.value, name: ''})}>
All results
</div>
)
}
}
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 (
<div className="result" key={item.uri} onClick={() => this.handleSelect(item)}>
<div className="result" key={item.uri} onClick={e => this.handleSelect(e,item)}>
{item.name}
{type == 'tracks' ? <span className="grey-text"> <ArtistSentence artists={item.artists} nolinks /></span> : null}
</div>
@ -102,7 +159,7 @@ class AutocompleteField extends React.Component{
<input
type="text"
value={this.state.value}
onChange={e => this.handleChange(e.target.value)}
onChange={e => this.handleChange(e,e.target.value)}
placeholder={this.props.placeholder ? this.props.placeholder : "Start typing..."} />
</div>
<div className="results">
@ -115,6 +172,7 @@ class AutocompleteField extends React.Component{
)
})
}
{this.renderAllResultsButton()}
</div>
</div>
);
@ -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 : {})
}
}

View File

@ -31,8 +31,8 @@ export default class LazyLoadListener extends React.Component{
render(){
return (
<div className={this.state.loading ? "lazy-loader loading" : "lazy-loader"}>
<div className="loader"></div>
<div className="lazy-loader body-loader">
{this.state.loading ? <div className="loader"></div> : null}
</div>
)
}

View File

@ -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 (
<form className={this.props.context+' search-form'} onSubmit={ e => this.handleSubmit(e) }>
<input
type="text"
<form className={this.props.context+' search-form'} onSubmit={e => this.handleSubmit(e, this.state.query)}>
<AutocompleteField
value={this.state.query}
types={['artist','album','playlist']}
placeholder="Search"
onChange={ e => this.setState({ query: e.target.value }) }
value={ this.state.query } />
onChange={(e,value) => this.setState({query: value})} />
</form>
)
}

View File

@ -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
}

View File

@ -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
});
})
}

View File

@ -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{
)
})
}
<AutocompleteField types={['artist','track','genre']} placeholder="Add seed" handleSelect={item => this.handleSelect(item)} clearOnSelect />
<AutocompleteField
value={this.state.add_seed}
types={['artist','track','genre']}
placeholder="Add seed"
onSelect={(e,item) => this.handleSelect(e,item)}
clearOnSelect
hideAllResultsButton />
</div>
)
}
@ -94,11 +107,14 @@ class Discover extends React.Component{
<Parallax image="/iris/assets/backgrounds/discover.jpg" />
<div className="liner">
<h1>Discover new music</h1>
<h3>Add seeds below to build your sound</h3>
<h3>
Add seeds below to build your sound. Let's start with
{this.props.authorized ? " two of your favorite artists" : " the chill genre"}.
</h3>
{this.renderSeeds()}
</div>
</div>
{this.props.recommendations ? <section className="list-wrapper"><TrackList className="discover-track-list" uri="iris:discover" tracks={this.props.recommendations} /></section> : null}
{helpers.isLoading(this.props.load_queue, 'spotify_recommendations') ? <div className="body-loader"><div className="loader"></div></div> : <section className="list-wrapper"><TrackList className="discover-track-list" uri="iris:discover" tracks={this.props.recommendations} /></section>}
</div>
)
}
@ -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 : []),

View File

@ -7,6 +7,7 @@
@import 'global/forms';
@import 'components/context-menu';
@import 'components/loader';
@import 'components/modal';
@import 'components/dragger';
@import 'components/images';

View File

@ -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%);
}
}
}
}

View File

@ -4,6 +4,7 @@
position: fixed;
top: 0;
right: 0;
z-index: 96;
padding: 10px;
background: rgba(0,0,0,0.5);
color: #FFFFFF;

View File

@ -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;
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -4,6 +4,7 @@
.intro {
position: relative;
padding: 60px 40px;
z-index: 1;
.parallax {
position: absolute;