Discovery engine prototype
This commit is contained in:
BIN
src/assets/backgrounds/discover.jpg
Executable file
BIN
src/assets/backgrounds/discover.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
3
src/js/bootstrap.js
vendored
3
src/js/bootstrap.js
vendored
@ -49,7 +49,8 @@ var initialState = {
|
||||
},
|
||||
spotify: {
|
||||
connected: false,
|
||||
me: false
|
||||
me: false,
|
||||
autocomplete_results: {}
|
||||
},
|
||||
ui: {
|
||||
current_tracklist: [],
|
||||
|
||||
@ -25,10 +25,10 @@ export default class ArtistSentence extends React.Component{
|
||||
}
|
||||
if (!artist.name){
|
||||
var content = <span>-</span>
|
||||
} else if (artist.uri){
|
||||
var content = <Link className="artist" to={global.baseURL+'artist/'+artist.uri}>{ artist.name }</Link>
|
||||
}else{
|
||||
} else if (!artist.uri || this.props.nolinks){
|
||||
var content = <span>{ artist.name }</span>
|
||||
}else{
|
||||
var content = <Link className="artist" to={global.baseURL+'artist/'+artist.uri}>{ artist.name }</Link>
|
||||
}
|
||||
return (
|
||||
<span key={'index_'+artist.uri}>
|
||||
|
||||
136
src/js/components/AutocompleteField.js
Executable file
136
src/js/components/AutocompleteField.js
Executable file
@ -0,0 +1,136 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import ArtistSentence from './ArtistSentence'
|
||||
import * as helpers from '../helpers'
|
||||
import * as spotifyActions from '../services/spotify/actions'
|
||||
|
||||
class AutocompleteField 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)
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
window.removeEventListener("click", this.handleClick, false)
|
||||
}
|
||||
|
||||
handleClick(e){
|
||||
if ($(e.target).closest('.autocomplete-field').length <= 0){
|
||||
this.props.spotifyActions.clearAutocompleteResults()
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(value){
|
||||
var self = this
|
||||
clearTimeout(this.timer)
|
||||
this.setState({value: value})
|
||||
this.timer = setTimeout(
|
||||
function(){
|
||||
self.setState({searching: true})
|
||||
self.props.spotifyActions.getAutocompleteResults(self.id,value,self.props.types)
|
||||
},
|
||||
500
|
||||
)
|
||||
}
|
||||
|
||||
handleSelect(item){
|
||||
if (this.props.clearOnSelect){
|
||||
this.setState({value: ''})
|
||||
} else {
|
||||
this.setState({value: item.name})
|
||||
}
|
||||
|
||||
this.props.handleSelect(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 (
|
||||
<div className="type">
|
||||
<h4 className="grey-text">{type}</h4>
|
||||
{
|
||||
items.map(item => {
|
||||
return (
|
||||
<div className="result" key={item.uri} onClick={() => this.handleSelect(item)}>
|
||||
{item.name}
|
||||
{type == 'tracks' ? <span className="grey-text"> <ArtistSentence artists={item.artists} nolinks /></span> : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render(){
|
||||
var className = "field autocomplete-field"
|
||||
if (this.results() && this.results().loading){
|
||||
className += " loading"
|
||||
}
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="input">
|
||||
<input
|
||||
type="text"
|
||||
value={this.state.value}
|
||||
onChange={e => this.handleChange(e.target.value)}
|
||||
placeholder={this.props.placeholder ? this.props.placeholder : "Start typing..."} />
|
||||
</div>
|
||||
<div className="results">
|
||||
{
|
||||
this.props.types.map(type => {
|
||||
return (
|
||||
<div key={type}>
|
||||
{this.renderResults(type+'s')}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
results: (state.spotify.autocomplete_results ? state.spotify.autocomplete_results : {})
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AutocompleteField)
|
||||
@ -39,7 +39,7 @@ class Sidebar extends React.Component{
|
||||
|
||||
<section>
|
||||
<title>Discover</title>
|
||||
<Link activeClassName="active" disabled={!this.props.spotify_authorized} to={this.props.spotify_authorized ? global.baseURL+"discover" : null}>
|
||||
<Link activeClassName="active" to={global.baseURL+"discover"}>
|
||||
<Icon name="compass" className="white" />
|
||||
Discover
|
||||
</Link>
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -406,8 +406,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 +456,62 @@ 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) => {
|
||||
console.log(types)
|
||||
|
||||
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
|
||||
|
||||
$.when(
|
||||
sendRequest(dispatch, getState, endpoint),
|
||||
sendRequest(dispatch, getState, 'recommendations/available-genre-seeds')
|
||||
|
||||
).then((search_response, genres_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]
|
||||
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: (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 : []),
|
||||
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 +623,99 @@ 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 = []){
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch({ type: 'SPOTIFY_RECOMMENDATIONS_LOADED', data: false })
|
||||
|
||||
// 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 endpoint = 'recommendations'
|
||||
endpoint += '?seed_artists='+artists_ids.join(',')
|
||||
endpoint += '&seed_tracks='+tracks_ids.join(',')
|
||||
endpoint += '&seed_genres='+genres.join(',')
|
||||
|
||||
sendRequest(dispatch, getState, endpoint)
|
||||
.then( response => {
|
||||
dispatch({
|
||||
type: 'SPOTIFY_RECOMMENDATIONS_LOADED',
|
||||
tracks: response.tracks
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,6 +179,59 @@ 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: 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
|
||||
}
|
||||
|
||||
@ -2,10 +2,14 @@
|
||||
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 SidebarToggleButton from '../../components/SidebarToggleButton'
|
||||
import GridSlider from '../../components/GridSlider'
|
||||
import ArtistSentence from '../../components/ArtistSentence'
|
||||
import TrackList from '../../components/TrackList'
|
||||
import Parallax from '../../components/Parallax'
|
||||
import AutocompleteField from '../../components/AutocompleteField'
|
||||
|
||||
import * as helpers from '../../helpers'
|
||||
import * as spotifyActions from '../../services/spotify/actions'
|
||||
@ -13,35 +17,88 @@ 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: [],
|
||||
adding_seed: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
this.props.spotifyActions.getDiscover()
|
||||
this.props.spotifyActions.getGenres()
|
||||
if (this.props.authorized){
|
||||
this.props.spotifyActions.getFavorites()
|
||||
}
|
||||
}
|
||||
|
||||
renderRecommendations(){
|
||||
if (!this.props.discover || this.props.discover.length <= 0) return null
|
||||
componentWillReceiveProps(newProps, newState){
|
||||
if (this.props.favorite_artists.length <= 0 && newProps.favorite_artists.length > 0 && this.state.seeds.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){
|
||||
var uris = helpers.asURIs(seeds)
|
||||
this.props.spotifyActions.getRecommendations(uris)
|
||||
}
|
||||
|
||||
removeSeed(index){
|
||||
var seeds = this.state.seeds
|
||||
seeds.splice(index,1)
|
||||
this.setState({seeds: seeds})
|
||||
this.getRecommendations(seeds)
|
||||
}
|
||||
|
||||
handleSelect(item){
|
||||
var seeds = this.state.seeds
|
||||
seeds.push(item)
|
||||
this.setState({seeds: seeds})
|
||||
this.getRecommendations(seeds)
|
||||
}
|
||||
|
||||
renderSeeds(){
|
||||
return (
|
||||
<section className="recommendations">
|
||||
<div className="seeds">
|
||||
{
|
||||
this.props.discover.map(
|
||||
(discover, index) => {
|
||||
var title = <h4>Because you listened to <ArtistSentence artists={discover.seed.artists} /></h4>
|
||||
return <GridSlider title={title} tracks={discover.tracks} key={index} />
|
||||
this.state.seeds.map((seed,index) => {
|
||||
var type = helpers.uriType(seed.uri)
|
||||
if (!type){
|
||||
type = 'genre'
|
||||
}
|
||||
)
|
||||
return (
|
||||
<span className="seed" key={seed.uri}>
|
||||
{seed.name}
|
||||
<span className="type">({type})</span>
|
||||
<FontAwesome name="close" className="remove" onClick={() => this.removeSeed(index)} />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</section>
|
||||
<AutocompleteField types={['artist','track','genre']} placeholder="Add seed" handleSelect={item => this.handleSelect(item)} clearOnSelect />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div className="view discover-view">
|
||||
<Header icon="compass" title="Discover" />
|
||||
{ this.renderRecommendations() }
|
||||
<SidebarToggleButton />
|
||||
<div className="intro">
|
||||
<Parallax image="/iris/assets/backgrounds/discover.jpg" />
|
||||
<div className="liner">
|
||||
<h1>Discover new music</h1>
|
||||
<h3>Add seeds below to build your sound</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}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -57,7 +114,10 @@ class Discover extends React.Component{
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
authorized: state.spotify.authorized,
|
||||
discover: state.spotify.discover
|
||||
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 : [])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
@import 'components/header';
|
||||
@import 'components/notifications';
|
||||
@import 'components/dropdown-field';
|
||||
@import 'components/autocomplete-field';
|
||||
@import 'components/debug';
|
||||
|
||||
@import 'views/artist';
|
||||
|
||||
62
src/scss/components/_autocomplete-field.scss
Executable file
62
src/scss/components/_autocomplete-field.scss
Executable file
@ -0,0 +1,62 @@
|
||||
|
||||
.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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,92 @@
|
||||
|
||||
.discover-view {
|
||||
section {
|
||||
padding: 40px;
|
||||
|
||||
.intro {
|
||||
position: relative;
|
||||
padding: 60px 40px;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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 ){
|
||||
|
||||
Reference in New Issue
Block a user