diff --git a/src/js/components/ArtistGrid.js b/src/js/components/ArtistGrid.js
index e9f3aaed..a5fdc8fb 100755
--- a/src/js/components/ArtistGrid.js
+++ b/src/js/components/ArtistGrid.js
@@ -17,8 +17,7 @@ export default class ArtistGrid extends React.Component{
{
this.props.artists.map(
(artist, index) => {
- var link = artist.uri;
- return
+ return
}
)
}
diff --git a/src/js/components/CategoryGrid.js b/src/js/components/CategoryGrid.js
new file mode 100755
index 00000000..38ef492b
--- /dev/null
+++ b/src/js/components/CategoryGrid.js
@@ -0,0 +1,30 @@
+
+import React, { PropTypes } from 'react'
+import GridItem from './GridItem'
+
+export default class CategoryGrid extends React.Component{
+
+ constructor(props) {
+ super(props);
+ }
+
+ render(){
+ if( this.props.categories ){
+ var className = "grid category-grid"
+ if( this.props.className ) className += ' '+this.props.className
+ return (
+
+ {
+ this.props.categories.map(
+ (category, index) => {
+ return
+ }
+ )
+ }
+
+ );
+ }
+ return null;
+ }
+}
+
diff --git a/src/js/components/GridItem.js b/src/js/components/GridItem.js
index 973070f9..7a958c15 100755
--- a/src/js/components/GridItem.js
+++ b/src/js/components/GridItem.js
@@ -25,10 +25,12 @@ export default class GridItem extends React.Component{
}
return (
this.handleClick(e) }>
- { item.images ?
: item.name }
+ { item.images ?
: null }
+ { item.icons ?
: null }
{ item.name }
{ item.artists ?
: null }
+ { item.type == 'playlist' ? item.tracks.total+' tracks' : null }
);
diff --git a/src/js/components/Parallax.js b/src/js/components/Parallax.js
index b9330889..1b42ecf2 100755
--- a/src/js/components/Parallax.js
+++ b/src/js/components/Parallax.js
@@ -15,10 +15,12 @@ export default class Parallax extends React.Component{
width: 0,
height: 0
},
- original: {},
image: {},
- imageObject: {}
+ loaded: false
}
+
+ // we need to manually bind this as eventListener doesn't work with anonymous functions
+ this.handleResize = this.handleResize.bind(this);
}
componentDidMount(){
@@ -26,14 +28,22 @@ export default class Parallax extends React.Component{
this.loadImage( url )
.then(
response => {
- this.setState({ image: response })
+ this.setState({ image: response, loaded: true })
this.updateCanvas( response )
}
)
- $(window).resize(() => this.updateCanvas( this.state.image ));
+ window.addEventListener("resize", this.handleResize);
}
+ componentWillUnmount() {
+ window.removeEventListener("resize", this.handleResize);
+ }
+
+ handleResize(){
+ this.updateCanvas( this.state.image );
+ }
+
loadImage( url ){
return new Promise( (resolve, reject) => {
@@ -137,7 +147,12 @@ export default class Parallax extends React.Component{
render(){
return (
-
+
);
}
diff --git a/src/js/components/PlaylistGrid.js b/src/js/components/PlaylistGrid.js
new file mode 100755
index 00000000..59794170
--- /dev/null
+++ b/src/js/components/PlaylistGrid.js
@@ -0,0 +1,30 @@
+
+import React, { PropTypes } from 'react'
+import GridItem from './GridItem'
+
+export default class PlaylistGrid extends React.Component{
+
+ constructor(props) {
+ super(props);
+ }
+
+ render(){
+ if( this.props.playlists ){
+ var className = "grid playlist-grid"
+ if( this.props.className ) className += ' '+this.props.className
+ return (
+
+ {
+ this.props.playlists.map(
+ (playlist, index) => {
+ return
+ }
+ )
+ }
+
+ );
+ }
+ return null;
+ }
+}
+
diff --git a/src/js/components/Sidebar.js b/src/js/components/Sidebar.js
index 32f73022..45f3038c 100755
--- a/src/js/components/Sidebar.js
+++ b/src/js/components/Sidebar.js
@@ -38,6 +38,18 @@ class Sidebar extends React.Component{
Discover
+
+
+ Discover
+
+
+
+ Genre / Mood
+
+
+
+ Featured playlists
+
diff --git a/src/js/index.js b/src/js/index.js
index 39bb8ef9..1dd82549 100755
--- a/src/js/index.js
+++ b/src/js/index.js
@@ -19,6 +19,11 @@ import Playlist from './views/Playlist'
import Queue from './views/Queue'
import Settings from './views/Settings'
+import Discover from './views/discover/Discover'
+import DiscoverFeatured from './views/discover/DiscoverFeatured'
+import DiscoverCategories from './views/discover/DiscoverCategories'
+import DiscoverCategory from './views/discover/DiscoverCategory'
+
import LibraryArtists from './views/library/LibraryArtists'
import LibraryAlbums from './views/library/LibraryAlbums'
import LibraryTracks from './views/library/LibraryTracks'
@@ -32,15 +37,20 @@ ReactDOM.render(
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js
index 194eb416..1ccfba67 100755
--- a/src/js/services/spotify/actions.js
+++ b/src/js/services/spotify/actions.js
@@ -330,7 +330,7 @@ export function getLibraryAlbums(){
}
export function getLibraryTracks(){
- return (dispatch, getState) => {
+ return (dispatch, getState) => {
dispatch({ type: 'SPOTIFY_LIBRARY_TRACKS_LOADED', data: false });
@@ -341,6 +341,81 @@ export function getLibraryTracks(){
data: response
});
});
+ }
+}
+
+export function getFeaturedPlaylists(){
+ return (dispatch, getState) => {
+
+ dispatch({ type: 'SPOTIFY_FEATURED_PLAYLISTS_LOADED', data: false });
+
+ var date = new Date();
+ var year = date.getFullYear();
+ var month = date.getMonth();
+ if( month < 10 ) month = '0'+month;
+ var day = date.getDay();
+ if( day < 10 ) day = '0'+day;
+ var hour = date.getHours();
+ if( hour < 10 ) hour = '0'+hour;
+ var min = date.getMinutes();
+ if( min < 10 ) min = '0'+min;
+ var sec = date.getSeconds();
+ if( sec < 10 ) sec = '0'+sec;
+
+ var timestamp = year+'-'+month+'-'+day+'T'+hour+':'+min+':'+sec;
+
+ sendRequest( dispatch, getState, 'browse/featured-playlists?timestamp='+timestamp+'&country='+getState().spotify.country+'&limit=50' )
+ .then( response => {
+ dispatch({
+ type: 'SPOTIFY_FEATURED_PLAYLISTS_LOADED',
+ data: response
+ });
+ });
+ }
+}
+
+export function getCategories(){
+ return (dispatch, getState) => {
+
+ dispatch({ type: 'SPOTIFY_CATEGORIES_LOADED', data: false });
+
+ sendRequest( dispatch, getState, 'browse/categories?limit=50' )
+ .then( response => {
+ dispatch({
+ type: 'SPOTIFY_CATEGORIES_LOADED',
+ data: response.categories
+ });
+ });
+ }
+}
+
+export function getCategory( id ){
+ return (dispatch, getState) => {
+
+ dispatch({ type: 'SPOTIFY_CATEGORY_LOADED', data: false });
+
+ sendRequest( dispatch, getState, 'browse/categories/'+id )
+ .then( response => {
+ dispatch({
+ type: 'SPOTIFY_CATEGORY_LOADED',
+ data: response
+ });
+ });
+ }
+}
+
+export function getCategoryPlaylists( id ){
+ return (dispatch, getState) => {
+
+ dispatch({ type: 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED', data: false });
+
+ sendRequest( dispatch, getState, 'browse/categories/'+id+'/playlists?limit=50' )
+ .then( response => {
+ dispatch({
+ type: 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED',
+ data: response.playlists
+ });
+ });
}
}
diff --git a/src/js/services/spotify/reducer.js b/src/js/services/spotify/reducer.js
index 972667f5..34736502 100755
--- a/src/js/services/spotify/reducer.js
+++ b/src/js/services/spotify/reducer.js
@@ -59,16 +59,28 @@ export default function reducer(spotify = {}, action){
return Object.assign({}, spotify, { artist_albums: action.data });
case 'SPOTIFY_LIBRARY_PLAYLISTS_LOADED':
- return Object.assign({}, spotify, { libraryPlaylists: action.data });
+ return Object.assign({}, spotify, { library_playlists: action.data });
case 'SPOTIFY_LIBRARY_ARTISTS_LOADED':
- return Object.assign({}, spotify, { libraryArtists: action.data });
+ return Object.assign({}, spotify, { library_artists: action.data });
case 'SPOTIFY_LIBRARY_ALBUMS_LOADED':
- return Object.assign({}, spotify, { libraryAlbums: action.data });
+ return Object.assign({}, spotify, { library_albums: action.data });
case 'SPOTIFY_LIBRARY_TRACKS_LOADED':
- return Object.assign({}, spotify, { libraryTracks: action.data });
+ return Object.assign({}, spotify, { library_tracks: action.data });
+
+ case 'SPOTIFY_FEATURED_PLAYLISTS_LOADED':
+ return Object.assign({}, spotify, { featured_playlists: action.data });
+
+ case 'SPOTIFY_CATEGORIES_LOADED':
+ return Object.assign({}, spotify, { categories: action.data });
+
+ case 'SPOTIFY_CATEGORY_LOADED':
+ return Object.assign({}, spotify, { category: action.data });
+
+ case 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED':
+ return Object.assign({}, spotify, { category_playlists: action.data });
default:
return spotify
diff --git a/src/js/views/Album.js b/src/js/views/Album.js
index 199d81fc..8a42599d 100755
--- a/src/js/views/Album.js
+++ b/src/js/views/Album.js
@@ -30,7 +30,7 @@ class Album extends React.Component{
render(){
if( this.props.spotify.album ){
return (
-
+
+
@@ -49,11 +49,13 @@ class Artist extends React.Component{
-
+
Related artists
{ this.props.spotify.artist.related_artists ?
: null }
+
+
Albums
{ this.props.spotify.artist_albums ?
: null }
diff --git a/src/js/views/Playlist.js b/src/js/views/Playlist.js
index 5dcecc92..f9b22349 100755
--- a/src/js/views/Playlist.js
+++ b/src/js/views/Playlist.js
@@ -30,7 +30,7 @@ class Playlist extends React.Component{
render(){
if( this.props.spotify.playlist ){
return (
-
+
diff --git a/src/js/views/Queue.js b/src/js/views/Queue.js
index 91359b5b..3040cfaf 100755
--- a/src/js/views/Queue.js
+++ b/src/js/views/Queue.js
@@ -61,7 +61,7 @@ class Queue extends React.Component{
render(){
return (
-
+
+
+ );
+ }
+ return null;
+ }
+}
+
+
+/**
+ * Export our component
+ *
+ * We also integrate our global store, using connect()
+ **/
+
+const mapStateToProps = (state, ownProps) => {
+ return state;
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ spotifyActions: bindActionCreators(spotifyActions, dispatch)
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Discover)
\ No newline at end of file
diff --git a/src/js/views/discover/DiscoverCategories.js b/src/js/views/discover/DiscoverCategories.js
new file mode 100755
index 00000000..f3d05e1e
--- /dev/null
+++ b/src/js/views/discover/DiscoverCategories.js
@@ -0,0 +1,49 @@
+
+import React, { PropTypes } from 'react'
+import { connect } from 'react-redux'
+import { bindActionCreators } from 'redux'
+
+import Header from '../../components/Header'
+import CategoryGrid from '../../components/CategoryGrid'
+
+import * as spotifyActions from '../../services/spotify/actions'
+
+class DiscoverCategories extends React.Component{
+
+ constructor(props) {
+ super(props);
+ }
+
+ // on render
+ componentDidMount(){
+ this.props.spotifyActions.getCategories();
+ }
+
+ render(){
+ return (
+
+
+ { this.props.spotify.categories ? : null }
+
+ );
+ }
+}
+
+
+/**
+ * Export our component
+ *
+ * We also integrate our global store, using connect()
+ **/
+
+const mapStateToProps = (state, ownProps) => {
+ return state;
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ spotifyActions: bindActionCreators(spotifyActions, dispatch)
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(DiscoverCategories)
\ No newline at end of file
diff --git a/src/js/views/discover/DiscoverCategory.js b/src/js/views/discover/DiscoverCategory.js
new file mode 100755
index 00000000..d12724bd
--- /dev/null
+++ b/src/js/views/discover/DiscoverCategory.js
@@ -0,0 +1,58 @@
+
+import React, { PropTypes } from 'react'
+import { connect } from 'react-redux'
+import { bindActionCreators } from 'redux'
+
+import Header from '../../components/Header'
+import PlaylistGrid from '../../components/PlaylistGrid'
+
+import * as spotifyActions from '../../services/spotify/actions'
+
+class DiscoverCategory extends React.Component{
+
+ constructor(props) {
+ super(props);
+ }
+
+ componentDidMount(){
+ this.props.spotifyActions.getCategory( this.props.params.id );
+ this.props.spotifyActions.getCategoryPlaylists( this.props.params.id );
+ }
+
+ componentWillReceiveProps( nextProps ){
+ if( nextProps.params.id != this.props.params.id ){
+ this.props.spotifyActions.getCategory( nextProps.params.id );
+ this.props.spotifyActions.getCategoryPlaylists( nextProps.params.id );
+ }
+ }
+
+ render(){
+ if( !this.props.spotify.category ) return null;
+
+ return (
+
+
+ { this.props.spotify.category_playlists ?
: null }
+
+ );
+ }
+}
+
+
+/**
+ * Export our component
+ *
+ * We also integrate our global store, using connect()
+ **/
+
+const mapStateToProps = (state, ownProps) => {
+ return state;
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ spotifyActions: bindActionCreators(spotifyActions, dispatch)
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(DiscoverCategory)
\ No newline at end of file
diff --git a/src/js/views/discover/DiscoverFeatured.js b/src/js/views/discover/DiscoverFeatured.js
new file mode 100755
index 00000000..3969e2e1
--- /dev/null
+++ b/src/js/views/discover/DiscoverFeatured.js
@@ -0,0 +1,49 @@
+
+import React, { PropTypes } from 'react'
+import { connect } from 'react-redux'
+import { bindActionCreators } from 'redux'
+
+import Header from '../../components/Header'
+import PlaylistGrid from '../../components/PlaylistGrid'
+
+import * as spotifyActions from '../../services/spotify/actions'
+
+class DiscoverFeatured extends React.Component{
+
+ constructor(props) {
+ super(props);
+ }
+
+ // on render
+ componentDidMount(){
+ this.props.spotifyActions.getFeaturedPlaylists();
+ }
+
+ render(){
+ return (
+
+
+ { this.props.spotify.featuredPlaylists ?
: null }
+
+ );
+ }
+}
+
+
+/**
+ * Export our component
+ *
+ * We also integrate our global store, using connect()
+ **/
+
+const mapStateToProps = (state, ownProps) => {
+ return state;
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ spotifyActions: bindActionCreators(spotifyActions, dispatch)
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(DiscoverFeatured)
\ No newline at end of file
diff --git a/src/js/views/library/LibraryAlbums.js b/src/js/views/library/LibraryAlbums.js
index fe0f9603..176b6fd2 100755
--- a/src/js/views/library/LibraryAlbums.js
+++ b/src/js/views/library/LibraryAlbums.js
@@ -22,18 +22,15 @@ class LibraryAlbums extends React.Component{
}
render(){
- if( this.props.spotify.libraryAlbums ){
- return (
-
- );
- }
- return null;
+ return (
+
+
+ { this.props.spotify.library_albums ?
: null }
+
+ );
}
}
diff --git a/src/js/views/library/LibraryArtists.js b/src/js/views/library/LibraryArtists.js
index f323843d..84b0682f 100755
--- a/src/js/views/library/LibraryArtists.js
+++ b/src/js/views/library/LibraryArtists.js
@@ -22,18 +22,15 @@ class LibraryArtists extends React.Component{
}
render(){
- if( this.props.spotify.libraryArtists ){
- return (
-
- );
- }
- return null;
+ return (
+
+
+ { this.props.spotify.library_artists ?
: null }
+
+ );
}
}
diff --git a/src/js/views/library/LibraryPlaylists.js b/src/js/views/library/LibraryPlaylists.js
index 744486cd..9e661aa2 100755
--- a/src/js/views/library/LibraryPlaylists.js
+++ b/src/js/views/library/LibraryPlaylists.js
@@ -37,8 +37,8 @@ class LibraryPlaylists extends React.Component{
Object.assign(playlists, this.props.mopidy.playlists)
}
- if( this.props.spotify.libraryPlaylists ){
- Object.assign(playlists, this.props.spotify.libraryPlaylists.items)
+ if( this.props.spotify.library_playlists ){
+ Object.assign(playlists, this.props.spotify.library_playlists.items)
}
return playlists;
@@ -67,7 +67,7 @@ class LibraryPlaylists extends React.Component{
render(){
return (
-
+
- );
- }
- return null;
+ return (
+
+
+ { this.props.spotify.library_tracks ? : null }
+
+ );
}
}
diff --git a/src/scss/components/_grid.scss b/src/scss/components/_grid.scss
index 54bcac5c..100cc054 100755
--- a/src/scss/components/_grid.scss
+++ b/src/scss/components/_grid.scss
@@ -34,4 +34,21 @@
opacity: 0.75;
}
}
+
+ &.category-grid {
+ .grid-item {
+ position: relative;
+
+ .name {
+ position: absolute;
+ bottom: 15%;
+ left: 0;
+ right: 0;
+ text-align: center;
+ font-size: 14px;
+ font-weight: 600;
+ color: #FFFFFF;
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/src/scss/components/_images.scss b/src/scss/components/_images.scss
index 268ca1bb..0f04bd21 100755
--- a/src/scss/components/_images.scss
+++ b/src/scss/components/_images.scss
@@ -28,10 +28,16 @@
height: 50vh;
width: 100%;
overflow: hidden;
- background: $light_grey;
+ background: $dark_grey;
canvas {
width: 100%;
height: 100%;
+ opacity: 0;
+ @include animate( 0.4s );
+
+ &.loaded {
+ opacity: 0.5;
+ }
}
}
\ No newline at end of file
diff --git a/src/scss/global/_variables.scss b/src/scss/global/_variables.scss
index 14404b1c..d020e7ac 100755
--- a/src/scss/global/_variables.scss
+++ b/src/scss/global/_variables.scss
@@ -1,6 +1,7 @@
$turquoise: #08d58f;
$light_grey: #DDDDDD;
+$dark_grey: #333333;
$secondary_grey: #888888;
@mixin one_line_text {
@@ -15,4 +16,11 @@ $secondary_grey: #888888;
clear: both;
display: block;
}
+}
+
+@mixin animate( $speed: 0.2s ){
+ -webkit-transition: all $speed ease-in-out;
+ -moz-transition: all $speed ease-in-out;
+ -o-transition: all $speed ease-in-out;
+ transition: all $speed ease-in-out;
}
\ No newline at end of file
diff --git a/src/scss/views/_artist.scss b/src/scss/views/_artist.scss
index 8b137891..25d2152b 100755
--- a/src/scss/views/_artist.scss
+++ b/src/scss/views/_artist.scss
@@ -1 +1,14 @@
+.artist-view {
+ .intro {
+ position: relative;
+ margin-bottom: 20px;
+ @include clearfix;
+
+ .thumbnail {
+ max-width: 200px;
+ margin: -100px 20px 0 40px;
+ float: left;
+ }
+ }
+}