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 ( +
+
+

Artists

+ +
+
+

Albums

+ +
+
+

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 ){