diff --git a/Screenshots/discover.jpg b/Screenshots/discover.jpg
new file mode 100755
index 00000000..ce24d300
Binary files /dev/null and b/Screenshots/discover.jpg differ
diff --git a/src/assets/backgrounds/discover.jpg b/src/assets/backgrounds/discover.jpg
new file mode 100755
index 00000000..ffaa3120
Binary files /dev/null and b/src/assets/backgrounds/discover.jpg differ
diff --git a/src/js/bootstrap.js b/src/js/bootstrap.js
index 365064c6..2bee3123 100755
--- a/src/js/bootstrap.js
+++ b/src/js/bootstrap.js
@@ -49,7 +49,8 @@ var initialState = {
},
spotify: {
connected: false,
- me: false
+ me: false,
+ autocomplete_results: {}
},
ui: {
current_tracklist: [],
diff --git a/src/js/components/AddSeedField.js b/src/js/components/AddSeedField.js
new file mode 100755
index 00000000..e36cfb0a
--- /dev/null
+++ b/src/js/components/AddSeedField.js
@@ -0,0 +1,137 @@
+
+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'
+import * as spotifyActions from '../services/spotify/actions'
+
+class AddSeedField extends React.Component{
+
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ value: ''
+ }
+ this.id = helpers.generateGuid()
+ this.timer = null
+ this.handleClick = this.handleClick.bind(this)
+ }
+
+ componentDidMount(){
+ window.addEventListener("click", this.handleClick, false)
+
+ if (!this.props.genres){
+ this.props.spotifyActions.getGenres()
+ }
+ }
+
+ componentWillUnmount(){
+ window.removeEventListener("click", this.handleClick, false)
+ }
+
+ handleClick(e){
+ if ($(e.target).closest('.add-seed-field').length <= 0){
+ this.props.spotifyActions.clearAutocompleteResults(this.id)
+ }
+ }
+
+ handleChange(e,value){
+ var self = this
+
+ // update our local state
+ this.setState({value: 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})
+ self.props.spotifyActions.getAutocompleteResults(self.id,value,['artist','track','genre'])
+ },
+ 500
+ )
+ }
+
+ handleSelect(e,item){
+ this.setState({value: ''})
+ this.props.onSelect(e,item)
+ this.props.spotifyActions.clearAutocompleteResults(this.id)
+ }
+
+ results(){
+ if (typeof(this.props.results) === 'undefined'){
+ return null
+ } else if (typeof(this.props.results[this.id]) === 'undefined'){
+ return null
+ } else {
+ return this.props.results[this.id]
+ }
+ }
+
+ renderResults(type){
+ var results = this.results()
+ if (!results || typeof(results[type]) === 'undefined' || results[type].length <= 0) return null
+
+ // only show the first 3
+ var items = results[type].slice(0,3)
+
+ return (
+
+
{type}
+ {
+ items.map(item => {
+ return (
+
this.handleSelect(e,item)}>
+ {item.name}
+ {type == 'tracks' ?
: null}
+
+ )
+ })
+ }
+
+ )
+ }
+
+ render(){
+ var className = "field autocomplete-field add-seed-field"
+ if (this.results() && this.results().loading){
+ className += " loading"
+ }
+ return (
+
+
+ this.handleChange(e,e.target.value)}
+ placeholder={this.props.placeholder ? this.props.placeholder : "Start typing..."} />
+
+
+ {this.renderResults('artists')}
+ {this.renderResults('tracks')}
+ {this.renderResults('genres')}
+
+
+ );
+ }
+}
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ genres: (state.spotify.genres ? state.spotify.genres : null),
+ results: (state.spotify.autocomplete_results ? state.spotify.autocomplete_results : {})
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ spotifyActions: bindActionCreators(spotifyActions, dispatch)
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(AddSeedField)
\ No newline at end of file
diff --git a/src/js/components/ArtistSentence.js b/src/js/components/ArtistSentence.js
index 74677d94..5f342b14 100755
--- a/src/js/components/ArtistSentence.js
+++ b/src/js/components/ArtistSentence.js
@@ -25,10 +25,10 @@ export default class ArtistSentence extends React.Component{
}
if (!artist.name){
var content = -
- } else if (artist.uri){
- var content = { artist.name }
- }else{
+ } else if (!artist.uri || this.props.nolinks){
var content = { artist.name }
+ }else{
+ var content = { artist.name }
}
return (
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/Sidebar.js b/src/js/components/Sidebar.js
index 2115136f..0f8af507 100755
--- a/src/js/components/Sidebar.js
+++ b/src/js/components/Sidebar.js
@@ -39,7 +39,7 @@ class Sidebar extends React.Component{
Discover
-
+
Discover
diff --git a/src/js/helpers.js b/src/js/helpers.js
index 1c3ed18a..8ba2b1d0 100755
--- a/src/js/helpers.js
+++ b/src/js/helpers.js
@@ -203,6 +203,12 @@ export let getFromUri = function(element,uri){
return exploded[2]
}
break
+
+ case 'genreid':
+ if (exploded[1] == 'genre'){
+ return exploded[2]
+ }
+ break
}
return null
}
@@ -225,6 +231,7 @@ export let uriType = function( uri ){
case 'album':
case 'playlist':
case 'search':
+ case 'genre':
return exploded[1]
break
@@ -435,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 32447445..f21f6fd7 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()
@@ -406,8 +412,8 @@ export function getURL( url, action_name, key = false ){
}
}
-export function getSearchResults( query, type = 'album,artist,playlist,track', limit = 50, offset = 0 ){
- return (dispatch, getState) => {
+export function getSearchResults(query, type = 'album,artist,playlist,track', limit = 50, offset = 0){
+ return (dispatch, getState) => {
var url = 'search?q='+query
url += '&type='+type
@@ -456,7 +462,59 @@ export function getSearchResults( query, type = 'album,artist,playlist,track', l
tracks_more: response.tracks.next
});
});
- }
+ }
+}
+
+export function getAutocompleteResults(field_id, query, types = ['album','artist','playlist','track']){
+ return (dispatch, getState) => {
+
+ dispatch({type: 'SPOTIFY_AUTOCOMPLETE_LOADING', field_id: field_id})
+
+ var genre_included = types.includes('genre')
+ if (genre_included){
+ var index = types.indexOf('genre')
+ types.splice(index,1)
+ }
+
+ var endpoint = 'search?q='+query
+ endpoint += '&type='+types.join(',')
+ endpoint += '&country='+getState().spotify.country
+
+ sendRequest(dispatch, getState, endpoint)
+ .then(response => {
+ var genres = []
+ if (genre_included){
+ 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
+ })
+ }
+ }
+ }
+ dispatch({
+ type: 'SPOTIFY_AUTOCOMPLETE_LOADED',
+ field_id: field_id,
+ results: {
+ 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
+ }
+ });
+ })
+ }
+}
+
+export function clearAutocompleteResults(field_id = null){
+ return {
+ type: 'SPOTIFY_AUTOCOMPLETE_CLEAR',
+ field_id: field_id
+ }
}
export function following(uri, method = 'GET'){
@@ -568,43 +626,142 @@ export function resolveRadioSeeds( radio ){
/**
- * Get our recommendations
- * This is based off our 'favorites' and then we use those as seeds
+ * Get my favorites
*
* @param uri string
**/
-export function getDiscover(){
+export function getFavorites(limit = 50, term = 'long_term'){
return (dispatch, getState) => {
- dispatch({ type: 'SPOTIFY_DISCOVER_LOADED', data: false })
+ dispatch({type: 'SPOTIFY_FAVORITES_LOADED', artists: [], tracks: []})
- // get favorite tracks
- sendRequest( dispatch, getState, 'me/top/tracks?limit=50&time_range=long_term' )
+ $.when(
+ sendRequest(dispatch, getState, 'me/top/artists?limit='+limit+'&time_range='+term),
+ sendRequest(dispatch, getState, 'me/top/tracks?limit='+limit+'&time_range='+term)
+
+ ).then((artists_response, tracks_response) => {
+ dispatch({
+ type: 'SPOTIFY_FAVORITES_LOADED',
+ artists: artists_response.items,
+ tracks: tracks_response.items
+ });
+ })
+ }
+}
+
+
+/**
+ * Get our recommendations
+ * This is based off our 'favorites' and then we use those as seeds
+ *
+ * @param uris = array of artist or track URIs or a genre string
+ **/
+export function getRecommendations(uris = [], limit = 20){
+ return (dispatch, getState) => {
+
+ dispatch({type: 'SPOTIFY_RECOMMENDATIONS_LOADED', tracks: []})
+
+ // build our starting point
+ var artists_ids = []
+ var tracks_ids = []
+ var genres = []
+
+ for (var i = 0; i < uris.length; i++){
+ var uri = uris[i]
+
+ switch (helpers.uriType(uri)){
+
+ case 'artist':
+ artists_ids.push(helpers.getFromUri('artistid',uri))
+ break
+
+ case 'track':
+ tracks_ids.push(helpers.getFromUri('trackid',uri))
+ break
+
+ case 'genre':
+ genres.push(helpers.getFromUri('genreid',uri))
+ break
+
+ case 'default':
+ genres.push(uri)
+ break
+ }
+ }
+
+ // construct our endpoint URL with all the appropriate arguments
+ var data = 'seed_artists='+artists_ids.join(',')
+ data += '&seed_tracks='+tracks_ids.join(',')
+ data += '&seed_genres='+genres.join(',')
+ data += '&limit='+limit
+
+ sendRequest(dispatch, getState, 'recommendations', 'GET', data)
.then( response => {
- var favorite_tracks_uris = helpers.asURIs(response.items)
- favorite_tracks_uris = favorite_tracks_uris.sort(() => .5 - Math.random())
- favorite_tracks_uris = favorite_tracks_uris.slice(0,10)
+ // We only get simple artist objects, so we need to
+ // get the full object. We'll add URIs to our recommendations
+ // anyway so we can proceed in the meantime
+ var artists_uris = []
+ if (response.tracks.length > artists_ids.length && response.tracks.length > 10){
+ while (artists_uris.length < 5){
+ var random_index = Math.round(Math.random() * (response.tracks.length - 1))
+ var artist = response.tracks[random_index].artists[0]
- for (var i = 0; i < favorite_tracks_uris.length; i++){
-
- var seed_id = helpers.getFromUri('trackid',favorite_tracks_uris[i] )
-
- $.when(
-
- sendRequest( dispatch, getState, 'recommendations?seed_tracks='+seed_id ),
- sendRequest( dispatch, getState, 'tracks/'+seed_id )
-
- ).then( ( tracks_response, seed_response ) => {
- dispatch({
- type: 'SPOTIFY_DISCOVER_LOADED',
- data: {
- tracks: tracks_response.tracks,
- seed: seed_response
- }
- });
- });
+ // Make sure this artist is not already in our sample, and
+ // is not one of the seeds
+ if (!artists_uris.includes(artist.uri) && !artists_ids.includes(artist.id)){
+ artists_uris.push(artist.uri)
+ dispatch(getArtist(artist.uri))
+ }
+ }
}
+
+ // Copy already loaded albums into array
+ var albums = []
+ var albums_uris = []
+ if (response.tracks.length > 10){
+ while (albums.length < 5){
+ var random_index = Math.round(Math.random() * (response.tracks.length - 1))
+ var album = response.tracks[random_index].album
+
+ // Make sure this album is not already in our sample
+ if (!albums_uris.includes(album.uri)){
+ albums_uris.push(album.uri)
+ albums.push(album)
+ }
+ }
+ }
+
+ // Officially add albums to index
+ dispatch({
+ type: 'ALBUMS_LOADED',
+ albums: albums
+ })
+
+ dispatch({
+ type: 'SPOTIFY_RECOMMENDATIONS_LOADED',
+ tracks: response.tracks,
+ artists_uris: artists_uris,
+ albums_uris: helpers.asURIs(albums)
+ })
+ })
+ }
+}
+
+
+/**
+ * Get all the available genres
+ *
+ * @param uri string
+ **/
+export function getGenres(){
+ return (dispatch, getState) => {
+ sendRequest(dispatch, getState, 'recommendations/available-genre-seeds')
+ .then( response => {
+ dispatch({
+ type: 'SPOTIFY_GENRES_LOADED',
+ genres: response.genres
+ });
})
}
}
diff --git a/src/js/services/spotify/reducer.js b/src/js/services/spotify/reducer.js
index b5f36d21..0ad9dfb9 100755
--- a/src/js/services/spotify/reducer.js
+++ b/src/js/services/spotify/reducer.js
@@ -179,6 +179,63 @@ export default function reducer(spotify = {}, action){
if( !action.data ) return Object.assign({}, spotify, { discover: [] })
return Object.assign({}, spotify, { discover: [...spotify.discover, ...[action.data]] })
+ case 'SPOTIFY_GENRES_LOADED':
+ return Object.assign({}, spotify, {genres: action.genres})
+
+ case 'SPOTIFY_RECOMMENDATIONS_LOADED':
+ return Object.assign(
+ {},
+ spotify,
+ {
+ recommendations: {
+ artists_uris: action.artists_uris,
+ albums_uris: action.albums_uris,
+ tracks: action.tracks
+ }
+ })
+
+ case 'SPOTIFY_FAVORITES_LOADED':
+ return Object.assign(
+ {},
+ spotify,
+ {
+ favorite_artists: action.artists,
+ favorite_tracks: action.tracks
+ })
+
+ case 'SPOTIFY_AUTOCOMPLETE_LOADING':
+ var autocomplete_results = spotify.autocomplete_results
+ autocomplete_results[action.field_id] = {loading: true}
+ return Object.assign(
+ {},
+ spotify,
+ {
+ autocomplete_results: autocomplete_results
+ })
+
+ case 'SPOTIFY_AUTOCOMPLETE_LOADED':
+ var autocomplete_results = spotify.autocomplete_results
+ autocomplete_results[action.field_id] = action.results
+ autocomplete_results[action.field_id].loading = false
+ return Object.assign(
+ {},
+ spotify,
+ {
+ autocomplete_results: autocomplete_results
+ })
+
+ case 'SPOTIFY_AUTOCOMPLETE_CLEAR':
+ var autocomplete_results = spotify.autocomplete_results
+ if (typeof(autocomplete_results[action.field_id]) !== 'undefined'){
+ delete autocomplete_results[action.field_id]
+ }
+ return Object.assign(
+ {},
+ spotify,
+ {
+ autocomplete_results: autocomplete_results
+ })
+
default:
return spotify
}
diff --git a/src/js/views/discover/Discover.js b/src/js/views/discover/Discover.js
index ae2023aa..81051a93 100755
--- a/src/js/views/discover/Discover.js
+++ b/src/js/views/discover/Discover.js
@@ -2,10 +2,15 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
+import FontAwesome from 'react-fontawesome'
-import Header from '../../components/Header'
-import GridSlider from '../../components/GridSlider'
+import SidebarToggleButton from '../../components/SidebarToggleButton'
import ArtistSentence from '../../components/ArtistSentence'
+import ArtistGrid from '../../components/ArtistGrid'
+import AlbumGrid from '../../components/AlbumGrid'
+import TrackList from '../../components/TrackList'
+import Parallax from '../../components/Parallax'
+import AddSeedField from '../../components/AddSeedField'
import * as helpers from '../../helpers'
import * as spotifyActions from '../../services/spotify/actions'
@@ -13,35 +18,155 @@ import * as spotifyActions from '../../services/spotify/actions'
class Discover extends React.Component{
constructor(props) {
- super(props);
+ super(props)
+
+ this._autocomplete_timer = false
+
+ this.state = {
+ seeds: [
+ {
+ uri: 'spotify:genre:chill',
+ name: 'Chill'
+ }
+ ],
+ add_seed: '',
+ adding_seed: false
+ }
}
componentDidMount(){
- this.props.spotifyActions.getDiscover()
+ if (this.props.authorized){
+ this.props.spotifyActions.getFavorites()
+ } else {
+ this.getRecommendations()
+ }
}
- renderRecommendations(){
- if (!this.props.discover || this.props.discover.length <= 0) return null
+ componentWillReceiveProps(newProps, newState){
+ // When we've loaded favorite_artists.
+ // This indirectly listens for when the action has
+ // loaded new data.
+ if (this.props.favorite_artists.length <= 0 && newProps.favorite_artists.length > 0){
+ var initial_seeds = newProps.favorite_artists.sort(() => .5 - Math.random())
+ initial_seeds = initial_seeds.slice(0,2)
+
+ this.setState({seeds: initial_seeds})
+ this.getRecommendations(initial_seeds)
+ }
+ }
+
+ getRecommendations(seeds = this.state.seeds){
+ if (seeds.length > 0){
+ var uris = helpers.asURIs(seeds)
+ this.props.spotifyActions.getRecommendations(uris, 50)
+ }
+ }
+
+ removeSeed(index){
+ var seeds = this.state.seeds
+ seeds.splice(index,1)
+ this.setState({seeds: seeds})
+ this.getRecommendations(seeds)
+ }
+
+ handleSelect(e,item){
+ var seeds = this.state.seeds
+ seeds.push(item)
+ this.setState({seeds: seeds})
+ this.getRecommendations(seeds)
+ }
+
+ renderSeeds(){
return (
-
+
{
- this.props.discover.map(
- (discover, index) => {
- var title =
Because you listened to
- return
+ this.state.seeds.map((seed,index) => {
+ var type = helpers.uriType(seed.uri)
+ if (!type){
+ type = 'genre'
}
- )
+ return (
+
+ {seed.name}
+ ({type})
+ this.removeSeed(index)} />
+
+ )
+ })
}
-
+
this.handleSelect(e,item)} />
+
+ )
+ }
+
+ renderResults(){
+ if (helpers.isLoading(this.props.load_queue, 'spotify_recommendations')){
+ return (
+
+ )
+ }
+
+ if (!this.props.recommendations || typeof(this.props.recommendations.albums_uris) === 'undefined' || typeof(this.props.recommendations.artists_uris) === 'undefined'){
+ return null
+ }
+
+ var albums = []
+ if (this.props.recommendations.albums_uris && this.props.albums){
+ for (var i = 0; i < this.props.recommendations.albums_uris.length; i++){
+ var uri = this.props.recommendations.albums_uris[i]
+ if (this.props.albums.hasOwnProperty(uri)){
+ albums.push(this.props.albums[uri])
+ }
+ }
+ }
+
+ var artists = []
+ if (this.props.recommendations.artists_uris && this.props.artists){
+ for (var i = 0; i < this.props.recommendations.artists_uris.length; i++){
+ var uri = this.props.recommendations.artists_uris[i]
+ if (this.props.artists.hasOwnProperty(uri)){
+ artists.push(this.props.artists[uri])
+ }
+ }
+ }
+
+ return (
+
+
+
+
+ Tracks
+ {this.props.recommendations.tracks ? : null}
+
+
)
}
render(){
return (
-
- { this.renderRecommendations() }
+
+
+
+
+
Discover new music
+
+ 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.renderResults()}
)
}
@@ -56,8 +181,14 @@ class Discover extends React.Component{
const mapStateToProps = (state, ownProps) => {
return {
+ albums: state.ui.albums,
+ artists: state.ui.artists,
authorized: state.spotify.authorized,
- discover: state.spotify.discover
+ 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 : []),
+ favorite_tracks: (state.spotify.favorite_tracks ? state.spotify.favorite_tracks : [])
}
}
diff --git a/src/scss/app.scss b/src/scss/app.scss
index 9f34beac..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';
@@ -22,6 +23,7 @@
@import 'components/header';
@import 'components/notifications';
@import 'components/dropdown-field';
+@import 'components/autocomplete-field';
@import 'components/debug';
@import 'views/artist';
diff --git a/src/scss/components/_autocomplete-field.scss b/src/scss/components/_autocomplete-field.scss
new file mode 100755
index 00000000..b78a7bf7
--- /dev/null
+++ b/src/scss/components/_autocomplete-field.scss
@@ -0,0 +1,74 @@
+
+.autocomplete-field {
+ position: relative;
+
+ .input {
+ width: 100%;
+ }
+
+ &.loading {
+ .input {
+ position: relative;
+
+ &:after {
+ position: absolute;
+ display: block;
+ content: '';
+ background: rgba(255,255,255,0.2);
+ opacity: 1;
+ animation: slideloader 1s infinite;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 4px;
+ z-index: 1;
+ }
+ }
+ }
+
+ .results {
+ position: absolute;
+ top: 37px;
+ left: 0;
+ right: 0;
+ z-index: 97;
+ background: $dark_grey;
+ color: $white;
+
+ .type {
+ &:not(:first-child){
+ padding-top: 5px;
+ }
+
+ h4 {
+ padding: 10px 14px 4px;
+ margin: 0;
+ font-size: 12px;
+ }
+
+ .result {
+ display: block;
+ padding: 10px 14px;
+ border-top: 1px solid lighten($dark_grey, 10%);
+ cursor: pointer;
+
+ &:hover {
+ background: lighten($dark_grey, 10%);
+ }
+ }
+ }
+
+ .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/_grid.scss b/src/scss/components/_grid.scss
index bc5aa3de..6d4185e5 100755
--- a/src/scss/components/_grid.scss
+++ b/src/scss/components/_grid.scss
@@ -39,6 +39,17 @@
opacity: 0.75;
}
}
+
+ &:active,
+ &:focus {
+ .thumbnail {
+ .image {
+ margin: 2%;
+ padding-bottom: 96%;
+ width: 96%;
+ }
+ }
+ }
}
&.category-grid {
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/global/_forms.scss b/src/scss/global/_forms.scss
index aec813f4..4010cfdf 100755
--- a/src/scss/global/_forms.scss
+++ b/src/scss/global/_forms.scss
@@ -80,16 +80,30 @@ input[type="submit"] {
}
&.secondary {
- color: #000000;
- border-color: #000000;
+ color: $black;
+ border-color: $black;
&:hover {
- color: lighten( #000000, 40% );
- border-color: lighten( #000000, 40% );
+ color: lighten( $black, 40% );
+ border-color: lighten( $black, 40% );
}
&:active {
- color: #FFFFFF;
- background: #000000;
- border-color: #000000;
+ color: $white;
+ background: $black;
+ border-color: $black;
+ }
+ }
+
+ &.alternative {
+ color: $blue;
+ border-color: $blue;
+ &:hover {
+ color: darken($blue, 10%);
+ border-color: darken($blue, 10%);
+ }
+ &:active {
+ color: $white;
+ background: $blue;
+ border-color: $blue;
}
}
diff --git a/src/scss/global/_variables.scss b/src/scss/global/_variables.scss
index 1b34689b..21922ebc 100755
--- a/src/scss/global/_variables.scss
+++ b/src/scss/global/_variables.scss
@@ -159,5 +159,9 @@ $bp_shallow: 650px;
@media( min-width: $min_width ){
@content;
}
+ }@else if $min_height {
+ @media( min-height: $min_height ){
+ @content;
+ }
}
}
diff --git a/src/scss/views/_discover.scss b/src/scss/views/_discover.scss
index 004561b7..9dd5f06b 100755
--- a/src/scss/views/_discover.scss
+++ b/src/scss/views/_discover.scss
@@ -1,7 +1,101 @@
.discover-view {
- section {
- padding: 40px;
+
+ .intro {
+ position: relative;
+ padding: 60px 40px;
+ z-index: 1;
+
+ .parallax {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ }
+
+ .liner {
+ z-index: 2;
+ position: relative;
+ color: $white;
+
+ h1 {
+ padding-bottom: 8px;
+ }
+
+ h3 {
+ padding-bottom: 40px;
+ opacity: 0.5;
+ }
+ }
+ }
+
+ section.grid-wrapper {
+ padding-bottom: 10px;
+
+ &:not(:first-child){
+ padding-top: 0;
+ }
+ }
+
+ .seeds {
+
+ .seed {
+ display: inline-block;
+ background: $blue;
+ color: $white;
+ padding: 8px 26px 8px 14px;
+ font-size: 16px;
+ position: relative;
+ vertical-align: top;
+ margin: 0 10px 10px 0;
+
+ .type {
+ display: inline-block;
+ font-size: 12px;
+ opacity: 0.5;
+ padding: 0 10px 0 5px;
+ }
+
+ .remove {
+ font-size: 10px;
+ padding: 12px;
+ position: absolute;
+ top: 0;
+ right: 0;
+ cursor: pointer;
+ }
+ }
+ }
+
+ .autocomplete-field {
+ width: 250px;
+ display: inline-block;
+ vertical-align: top;
+ padding: 0;
+
+ .input {
+ float: none;
+ margin: 0 10px 10px 0;
+
+ input {
+ background: rgba(100,100,100,0.5);
+ color: $white;
+ padding: 7px 13px;
+ font-size: 16px;
+ border: 1px solid transparent;
+
+ &:focus,
+ &:active {
+ border-color: $blue;
+ }
+ }
+
+ &:after {
+ background: $blue;
+ }
+ }
}
@include responsive( $bp_medium ){