Autocomplete search now in sidebar
This commit is contained in:
@ -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 : {})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 : []),
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
@import 'global/forms';
|
||||
|
||||
@import 'components/context-menu';
|
||||
@import 'components/loader';
|
||||
@import 'components/modal';
|
||||
@import 'components/dragger';
|
||||
@import 'components/images';
|
||||
|
||||
@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 96;
|
||||
padding: 10px;
|
||||
background: rgba(0,0,0,0.5);
|
||||
color: #FFFFFF;
|
||||
|
||||
22
src/scss/components/_loader.scss
Executable file
22
src/scss/components/_loader.scss
Executable 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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
.intro {
|
||||
position: relative;
|
||||
padding: 60px 40px;
|
||||
z-index: 1;
|
||||
|
||||
.parallax {
|
||||
position: absolute;
|
||||
|
||||
Reference in New Issue
Block a user