Initial commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/node_modules/
|
||||
/production/
|
||||
*.orig
|
||||
Thumbs.db
|
||||
35
package.json
Executable file
35
package.json
Executable file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "test-reactor",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"dependencies": {
|
||||
"babel": "^5.8.29",
|
||||
"babel-core": "6.14.0",
|
||||
"babel-loader": "6.2.5",
|
||||
"babel-preset-es2015": "*",
|
||||
"babel-preset-react": "*",
|
||||
"flux": "^2.1.1",
|
||||
"redux": "*",
|
||||
"react": "*",
|
||||
"react-dom": "*",
|
||||
"react-router": "*",
|
||||
"redux": "*",
|
||||
"react-redux": "*",
|
||||
"redux-devtools": "*",
|
||||
"webpack": "^1.13.2",
|
||||
"webpack-strip": "*",
|
||||
"node-sass": "*",
|
||||
"extract-text-webpack-plugin": "*",
|
||||
"jquery": "3.1.1",
|
||||
"mopidy": "^0.5.0",
|
||||
"expose-loader": "*",
|
||||
"kioc": "1.0.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cp src/index.html production/index.html & NODE_ENV=development webpack",
|
||||
"prod": "NODE_ENV=production webpack"
|
||||
},
|
||||
"author": "James Barnsley",
|
||||
"license": "ISC"
|
||||
}
|
||||
81
src/App.js
Executable file
81
src/App.js
Executable file
@ -0,0 +1,81 @@
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { createStore, bindActionCreators } from 'redux'
|
||||
import { Link } from 'react-router'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import * as actions from './actions'
|
||||
import Services from './services/Services'
|
||||
|
||||
|
||||
/**
|
||||
* The application 'brain'
|
||||
*
|
||||
* All data handling and fetching is done through this handler
|
||||
**/
|
||||
class App extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick(){
|
||||
console.log('click');
|
||||
//this.props.providerActions.authorizeSpotify();
|
||||
Services.get('services.mopidy')
|
||||
.then( function(MopidyService){
|
||||
MopidyService.connection.playback.getState()
|
||||
.then( function(state){
|
||||
console.log('App.js > playback state', state);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
Services.get('services.mopidy')
|
||||
.then( function(MopidyService){
|
||||
MopidyService.connection.playback.getState()
|
||||
.then( function(state){
|
||||
console.log('App.js > playback state', state);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
render(){
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ul role="nav">
|
||||
<li><Link to="/now-playing">Now playing</Link></li>
|
||||
<li><Link to="/album/6N51k5TP5pSZYPf7bLffLe">808's and Heartbreak</Link></li>
|
||||
<li><Link to="/album/1PgfRdl3lPyACfUGH4pquG">A million</Link></li>
|
||||
<li><Link to="/library/albums">Library: Albums</Link></li>
|
||||
<li><Link to="/album">Album</Link></li>
|
||||
<li><a onClick={this.handleClick}>Authorize</a></li>
|
||||
</ul>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
**/
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App)
|
||||
8
src/about/index.js
Executable file
8
src/about/index.js
Executable file
@ -0,0 +1,8 @@
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export default React.createClass({
|
||||
render() {
|
||||
return <div>About</div>
|
||||
}
|
||||
})
|
||||
32
src/actions.js
Executable file
32
src/actions.js
Executable file
@ -0,0 +1,32 @@
|
||||
|
||||
/**
|
||||
* Actions and Action Creators
|
||||
**/
|
||||
|
||||
export const TOGGLE_DONE = 'TOGGLE_DONE'
|
||||
export const ADD_TODO = 'ADD_TODO'
|
||||
export const SET_ALBUM = 'SET_ALBUM'
|
||||
|
||||
export function toggleDone( id ){
|
||||
return {
|
||||
type: TOGGLE_DONE,
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
export function addTodo( title ){
|
||||
return {
|
||||
type: ADD_TODO,
|
||||
id: title,
|
||||
title: title
|
||||
}
|
||||
}
|
||||
|
||||
export function setAlbum( album ){
|
||||
return {
|
||||
type: SET_ALBUM,
|
||||
album: album
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
84
src/common/Album.js
Executable file
84
src/common/Album.js
Executable file
@ -0,0 +1,84 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import Tracklist from '../common/Tracklist'
|
||||
import * as actions from './actions'
|
||||
|
||||
class Album extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
album: false
|
||||
}
|
||||
}
|
||||
|
||||
// on render
|
||||
componentDidMount(){
|
||||
this.loadAlbum( this.props.params.id );
|
||||
}
|
||||
|
||||
// when props changed
|
||||
componentWillReceiveProps( nextProps ){
|
||||
if( nextProps.params.id != this.props.params.id ){
|
||||
this.loadAlbum( nextProps.params.id );
|
||||
}
|
||||
}
|
||||
|
||||
loadAlbum( id ){
|
||||
let self = this;
|
||||
|
||||
$.ajax({
|
||||
method: 'GET',
|
||||
cache: true,
|
||||
url: 'https://api.spotify.com/v1/albums/'+id,
|
||||
success: function(album){
|
||||
self.setState({ album: album });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderAlbum(){
|
||||
if( this.state.album ){
|
||||
return (
|
||||
<div>
|
||||
<h3>{ this.state.album.name }</h3>
|
||||
<h3>{ this.state.album.label }</h3>
|
||||
<Tracklist tracks={this.state.album.tracks.items} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div>
|
||||
<h3>Single album</h3>
|
||||
{ this.renderAlbum() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
**/
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Album)
|
||||
42
src/common/Tracklist.js
Executable file
42
src/common/Tracklist.js
Executable file
@ -0,0 +1,42 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
|
||||
export default class Tracklist extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
flattenTracks(){
|
||||
var tracks = [];
|
||||
if( this.props.tracks ){
|
||||
var originalTracks = this.props.tracks;
|
||||
|
||||
for( var i = 0; i < originalTracks.length; i++ ){
|
||||
var track = originalTracks[i];
|
||||
if( typeof(track.track) !== 'undefined' ){
|
||||
track.track.tlid = track.tlid;
|
||||
track = track.track;
|
||||
}
|
||||
tracks.push( track );
|
||||
}
|
||||
}
|
||||
return tracks;
|
||||
}
|
||||
|
||||
render(){
|
||||
|
||||
if( this.flattenTracks ){
|
||||
return (
|
||||
<ul>
|
||||
{
|
||||
this.flattenTracks().map( track =>
|
||||
<li key={track.uri}>{ track.name }</li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
12
src/common/actions.js
Executable file
12
src/common/actions.js
Executable file
@ -0,0 +1,12 @@
|
||||
|
||||
/**
|
||||
* Actions and Action Creators
|
||||
**/
|
||||
|
||||
export const LOAD_ALBUMS = 'LOAD_ALBUMS'
|
||||
|
||||
export function loadAlbums(){
|
||||
return {
|
||||
type: LOAD_ALBUMS
|
||||
}
|
||||
}
|
||||
16
src/common/reducer.js
Executable file
16
src/common/reducer.js
Executable file
@ -0,0 +1,16 @@
|
||||
|
||||
import * as actions from './actions'
|
||||
|
||||
export default function album(album = {}, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case actions.LOAD_ALBUM:
|
||||
return album//Object.assign({}, album, action.album);
|
||||
|
||||
default:
|
||||
return album
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
37
src/config/Mopidy.js
Executable file
37
src/config/Mopidy.js
Executable file
@ -0,0 +1,37 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import * as actions from './actions'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
class Mopidy extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
let self = this;
|
||||
console.log('Mopidy > constructor')
|
||||
}
|
||||
|
||||
render(){
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
**/
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Mopidy)
|
||||
79
src/config/Spotify.js
Executable file
79
src/config/Spotify.js
Executable file
@ -0,0 +1,79 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import * as actions from './actions'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
class Spotify extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
let self = this;
|
||||
this.urlBase = 'https://api.spotify.com/v1/';
|
||||
console.log('SpotifyProvider > constructor')
|
||||
|
||||
// listen for incoming messages from the authorization iframe
|
||||
// this is triggered when authentication is granted from the popup
|
||||
window.addEventListener('message', function(event){
|
||||
if( !/^https?:\/\/jamesbarnsley\.co\.nz/.test(event.origin) ) return false;
|
||||
self.handleMessage( event );
|
||||
}, false);
|
||||
}
|
||||
|
||||
handleMessage( event ){
|
||||
|
||||
// convert to json
|
||||
var data = JSON.parse(event.data);
|
||||
|
||||
// fire event
|
||||
this.props.actions.authorizeSpotifySuccess( data );
|
||||
}
|
||||
|
||||
test(){
|
||||
console.log('test stuff');
|
||||
}
|
||||
|
||||
getAlbum( albumid ){
|
||||
return $.when(
|
||||
$.ajax({
|
||||
method: 'GET',
|
||||
cache: true,
|
||||
url: this.urlBase+'albums/'+albumid
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
render(){
|
||||
|
||||
if( this.props.config.authorizingSpotify ){
|
||||
var src = '//jamesbarnsley.co.nz/spotmop.php?action=authorize&app='+location.protocol+'//'+window.location.host;
|
||||
return (
|
||||
<div>
|
||||
Authorizing...
|
||||
<iframe id="authorization-frame" src={src}></iframe>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
**/
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Spotify)
|
||||
21
src/config/actions.js
Executable file
21
src/config/actions.js
Executable file
@ -0,0 +1,21 @@
|
||||
|
||||
/**
|
||||
* Actions and Action Creators
|
||||
**/
|
||||
|
||||
export const AUTHORIZE_SPOTIFY = 'AUTHORIZE_SPOTIFY'
|
||||
export const AUTHORIZE_SPOTIFY_SUCCESS = 'AUTHORIZE_SPOTIFY_SUCCESS'
|
||||
|
||||
export function authorizeSpotify( authorize = true ){
|
||||
return {
|
||||
type: AUTHORIZE_SPOTIFY,
|
||||
authorize: authorize
|
||||
}
|
||||
}
|
||||
|
||||
export function authorizeSpotifySuccess( authorization ){
|
||||
return {
|
||||
type: AUTHORIZE_SPOTIFY_SUCCESS,
|
||||
authorization: authorization
|
||||
}
|
||||
}
|
||||
24
src/config/reducer.js
Executable file
24
src/config/reducer.js
Executable file
@ -0,0 +1,24 @@
|
||||
|
||||
import * as actions from './actions'
|
||||
|
||||
export default function config(config = {}, action){
|
||||
switch (action.type) {
|
||||
|
||||
case actions.AUTHORIZE_SPOTIFY:
|
||||
return Object.assign({}, config, {
|
||||
authorizingSpotify: action.authorize
|
||||
});
|
||||
|
||||
case actions.AUTHORIZE_SPOTIFY_SUCCESS:
|
||||
console.info('Spotify authorization successful');
|
||||
var config = Object.assign({}, config, action.authorization)
|
||||
config = Object.assign({}, config, { authorizingSpotify: false })
|
||||
return config
|
||||
|
||||
default:
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
20
src/index.html
Executable file
20
src/index.html
Executable file
@ -0,0 +1,20 @@
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<title>Testing React and Redux</title>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
|
||||
<meta base-href="/alto/production" />
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app">
|
||||
<!-- ReactJS application here -->
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
<script type="text/javascript" src="app.js"></script>
|
||||
|
||||
</html>
|
||||
54
src/index.js
Executable file
54
src/index.js
Executable file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Base-level application wrapper
|
||||
**/
|
||||
|
||||
console.info('Application initiated');
|
||||
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux'
|
||||
import { createStore } from 'redux'
|
||||
import { Router, Route, Link, hashHistory } from 'react-router'
|
||||
import reducer from './reducer'
|
||||
|
||||
import App from './App'
|
||||
import LibraryAlbums from './library/LibraryAlbums'
|
||||
import Album from './common/Album'
|
||||
import NowPlaying from './routes/NowPlaying'
|
||||
|
||||
import Services from './services/Services'
|
||||
|
||||
Promise.all(Services.setup())
|
||||
.then(() => {
|
||||
console.info('Services started...');
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Render our application into the real DOM
|
||||
*
|
||||
* Sets up all of our routes, and the relevant components for each.
|
||||
* Provider facilitates global access to the store by using connect()
|
||||
|
||||
<Route path="playlists" component={LibraryPlaylists} />
|
||||
<Route path="albums" component={LibraryAlbums} />
|
||||
<Route path="artists" component={LibraryArtists} />
|
||||
<Route path="tracks" component={LibraryTracks} />
|
||||
**/
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={ createStore( reducer, { todos: [] } ) }>
|
||||
<Router history={hashHistory}>
|
||||
<Route path="/" component={App}>
|
||||
|
||||
<Route path="album/:id" component={Album} />
|
||||
<Route path="library/albums" component={LibraryAlbums} />
|
||||
|
||||
<Route path="now-playing" component={NowPlaying} />
|
||||
|
||||
</Route>
|
||||
</Router>
|
||||
</Provider>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
68
src/library/LibraryAlbums.js
Executable file
68
src/library/LibraryAlbums.js
Executable file
@ -0,0 +1,68 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import * as actions from './actions'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
class LibraryAlbums extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
albums: []
|
||||
};
|
||||
}
|
||||
|
||||
// on load of this component
|
||||
componentDidMount(){
|
||||
|
||||
var url = 'http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=4320a3ef51c9b3d69de552ac083c55e3&artist=Cher&album=Believe&format=json';
|
||||
//var url = 'https://pixabay.com/api/?key=3190651-5f431f6c829ace6d75ff07701&q=yellow+flowers&image_type=photo&pretty=true';
|
||||
|
||||
var self = this;
|
||||
|
||||
// url (required), options (optional)
|
||||
fetch(url, {
|
||||
method: 'get'
|
||||
}).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function( json ) {
|
||||
self.setState({ albums: json.hits });
|
||||
}).catch(function(err) {
|
||||
console.error( err );
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h3>Library album</h3>
|
||||
{
|
||||
this.state.albums.map(album =>
|
||||
<h4 key={album.webformatURL}>{ album.webformatURL }</h4>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
**/
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LibraryAlbums)
|
||||
12
src/library/actions.js
Executable file
12
src/library/actions.js
Executable file
@ -0,0 +1,12 @@
|
||||
|
||||
/**
|
||||
* Actions and Action Creators
|
||||
**/
|
||||
|
||||
export const LOAD_ALBUMS = 'LOAD_ALBUMS'
|
||||
|
||||
export function loadAlbums(){
|
||||
return {
|
||||
type: LOAD_ALBUMS
|
||||
}
|
||||
}
|
||||
16
src/library/reducer.js
Executable file
16
src/library/reducer.js
Executable file
@ -0,0 +1,16 @@
|
||||
|
||||
import * as actions from './actions'
|
||||
|
||||
export default function album(album = {}, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case actions.SET_ALBUM:
|
||||
return Object.assign({}, album, action.album);
|
||||
|
||||
default:
|
||||
return album
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
21
src/reducer.js
Executable file
21
src/reducer.js
Executable file
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Root reducer
|
||||
*
|
||||
* This combines all our application reducers into a single, top-level reducer.
|
||||
* We need to combine reducers to create the unified application-level store.
|
||||
**/
|
||||
|
||||
// import all of our application reducers
|
||||
import todos from './todos/reducer'
|
||||
import config from './config/reducer'
|
||||
|
||||
import { combineReducers } from 'redux'
|
||||
|
||||
// combine them into one root reducer
|
||||
export default combineReducers({
|
||||
todos,
|
||||
config
|
||||
})
|
||||
|
||||
|
||||
|
||||
67
src/routes/NowPlaying.js
Executable file
67
src/routes/NowPlaying.js
Executable file
@ -0,0 +1,67 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import Services from '../services/Services'
|
||||
import Tracklist from '../common/Tracklist'
|
||||
import * as actions from '../actions'
|
||||
|
||||
class NowPlaying extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tracks: []
|
||||
}
|
||||
}
|
||||
|
||||
// on render
|
||||
componentDidMount(){
|
||||
var self = this;
|
||||
Services.get('services.mopidy')
|
||||
.then( function(MopidyService){
|
||||
MopidyService.connection.tracklist.getTlTracks()
|
||||
.then( function(tracks){
|
||||
self.setState({ tracks : tracks });
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
renderTracks(){
|
||||
if( this.state.tracks ){
|
||||
return (
|
||||
<Tracklist tracks={this.state.tracks} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div>
|
||||
<h3>Now playing</h3>
|
||||
{ this.renderTracks() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
**/
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(NowPlaying)
|
||||
35
src/services/MopidyService.js
Executable file
35
src/services/MopidyService.js
Executable file
@ -0,0 +1,35 @@
|
||||
|
||||
import Mopidy from 'mopidy'
|
||||
|
||||
function MopidyService( connection ){
|
||||
this.connection = connection;
|
||||
};
|
||||
|
||||
MopidyService.attachKey = 'services.mopidy';
|
||||
|
||||
MopidyService.attach = function( app ){
|
||||
console.info('MopidyService: Attaching...');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
var mopidyhost = 'music.plasticstudio.co';//window.location.hostname;
|
||||
var mopidyport = "6680";
|
||||
var protocol = 'ws';
|
||||
var connection = new Mopidy({
|
||||
webSocketUrl: protocol+"://" + mopidyhost + ":" + mopidyport + "/mopidy/ws",
|
||||
callingConvention: 'by-position-or-by-name'
|
||||
});
|
||||
connection.on((a, msg) => {
|
||||
//console.log(msg);
|
||||
// msg && msg.method ?
|
||||
// console.log('<-', msg.method) :
|
||||
// (msg ? console.log('->', JSON.parse(msg.data)) : console.log('->', msg));
|
||||
});
|
||||
connection.on('state:online', () => {
|
||||
console.info('MopidyService: Attached');
|
||||
var service = new MopidyService( connection );
|
||||
resolve( service );
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default MopidyService;
|
||||
11
src/services/Services.js
Executable file
11
src/services/Services.js
Executable file
@ -0,0 +1,11 @@
|
||||
|
||||
import ServicesContainer from 'kioc';
|
||||
|
||||
import MopidyService from './MopidyService';
|
||||
import SpotifyService from './SpotifyService';
|
||||
|
||||
var Services = new ServicesContainer();
|
||||
Services.use(MopidyService, true);
|
||||
//Services.use(SpotifyService, true);
|
||||
|
||||
export default Services;
|
||||
79
src/services/SpotifyService.js
Executable file
79
src/services/SpotifyService.js
Executable file
@ -0,0 +1,79 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import * as actions from './actions'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
class SpotifyService extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
let self = this;
|
||||
this.urlBase = 'https://api.spotify.com/v1/';
|
||||
console.log('SpotifyProvider > constructor')
|
||||
|
||||
// listen for incoming messages from the authorization iframe
|
||||
// this is triggered when authentication is granted from the popup
|
||||
window.addEventListener('message', function(event){
|
||||
if( !/^https?:\/\/jamesbarnsley\.co\.nz/.test(event.origin) ) return false;
|
||||
self.handleMessage( event );
|
||||
}, false);
|
||||
}
|
||||
|
||||
handleMessage( event ){
|
||||
|
||||
// convert to json
|
||||
var data = JSON.parse(event.data);
|
||||
|
||||
// fire event
|
||||
this.props.actions.authorizeSpotifySuccess( data );
|
||||
}
|
||||
|
||||
test(){
|
||||
console.log('test stuff');
|
||||
}
|
||||
|
||||
getAlbum( albumid ){
|
||||
return $.when(
|
||||
$.ajax({
|
||||
method: 'GET',
|
||||
cache: true,
|
||||
url: this.urlBase+'albums/'+albumid
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
render(){
|
||||
|
||||
if( this.props.config.authorizingSpotify ){
|
||||
var src = '//jamesbarnsley.co.nz/spotmop.php?action=authorize&app='+location.protocol+'//'+window.location.host;
|
||||
return (
|
||||
<div>
|
||||
Authorizing...
|
||||
<iframe id="authorization-frame" src={src}></iframe>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
**/
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SpotifyService)
|
||||
29
src/services/actions.js
Executable file
29
src/services/actions.js
Executable file
@ -0,0 +1,29 @@
|
||||
|
||||
/**
|
||||
* Actions and Action Creators
|
||||
**/
|
||||
|
||||
export const AUTHORIZE_SPOTIFY = 'AUTHORIZE_SPOTIFY'
|
||||
export const AUTHORIZE_SPOTIFY_SUCCESS = 'AUTHORIZE_SPOTIFY_SUCCESS'
|
||||
export const MOPIDY_ONLINE = 'MOPIDY_ONLINE'
|
||||
|
||||
export function authorizeSpotify( authorize = true ){
|
||||
return {
|
||||
type: AUTHORIZE_SPOTIFY,
|
||||
authorize: authorize
|
||||
}
|
||||
}
|
||||
|
||||
export function authorizeSpotifySuccess( authorization ){
|
||||
return {
|
||||
type: AUTHORIZE_SPOTIFY_SUCCESS,
|
||||
authorization: authorization
|
||||
}
|
||||
}
|
||||
|
||||
export function mopidyOnline( online ){
|
||||
return {
|
||||
type: MOPIDY_ONLINE,
|
||||
online: online
|
||||
}
|
||||
}
|
||||
29
src/services/reducer.js
Executable file
29
src/services/reducer.js
Executable file
@ -0,0 +1,29 @@
|
||||
|
||||
import * as actions from './actions'
|
||||
|
||||
export default function reducer(state = {}, action){
|
||||
switch (action.type) {
|
||||
|
||||
case actions.AUTHORIZE_SPOTIFY:
|
||||
return Object.assign({}, state.spotify, {
|
||||
authorizing: action.authorize
|
||||
});
|
||||
|
||||
case actions.AUTHORIZE_SPOTIFY_SUCCESS:
|
||||
console.info('Spotify authorization successful');
|
||||
var state = Object.assign({}, state.spotify, action.authorization)
|
||||
state = Object.assign({}, state.spotify, { online: true, authorizingSpotify: false })
|
||||
return state
|
||||
|
||||
case actions.MOPIDY_ONLINE:
|
||||
return Object.assign({}, state.mopidy, {
|
||||
online: action.online
|
||||
});
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
158
src/todos/index.js
Executable file
158
src/todos/index.js
Executable file
@ -0,0 +1,158 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import * as actions from '../actions'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
class Todos extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
// on load of this component
|
||||
componentDidMount(){
|
||||
this.props.actions.load();
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<Todos_View todos={this.props.todos} actions={this.props.actions} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The Todo's presenter, or view
|
||||
*
|
||||
* Any UI and UX display occurs here, and is passed back to it's brain through props
|
||||
* This allows for reuse of the brain for different contexts without duplicating functionality
|
||||
**/
|
||||
class Todos_View extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div>
|
||||
<ul className="todos">
|
||||
{
|
||||
this.props.todos.map(todo =>
|
||||
<TodoItem todo={todo} key={todo.id} actions={this.props.actions} />
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
<AddTodoForm actions={this.props.actions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The Todo item
|
||||
**/
|
||||
class TodoItem extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick(){
|
||||
this.props.actions.toggleDone(this.props.todo.id);
|
||||
this.props.actions.save();
|
||||
}
|
||||
|
||||
render(){
|
||||
var style = {};
|
||||
if( this.props.todo.completed ) style.textDecoration = 'line-through';
|
||||
return <li onClick={this.handleClick} style={style}>{ this.props.todo.title }</li>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The Todo item
|
||||
**/
|
||||
class AddTodoForm extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
title: ''
|
||||
}
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
handleSubmit(){
|
||||
this.props.actions.addTodo( this.state.title );
|
||||
this.props.actions.save();
|
||||
this.setState({ title: '' });
|
||||
}
|
||||
|
||||
handleChange( newTitle ){
|
||||
this.setState({ title: newTitle });
|
||||
}
|
||||
|
||||
render(){
|
||||
return <AddTodoForm_View title={this.state.title} handleChange={this.handleChange} handleSubmit={this.handleSubmit} />
|
||||
}
|
||||
}
|
||||
|
||||
class AddTodoForm_View extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
handleChange( event ){
|
||||
this.props.handleChange( event.target.value );
|
||||
}
|
||||
|
||||
handleSubmit( event ){
|
||||
event.preventDefault();
|
||||
this.props.handleSubmit();
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<form onSubmit={ this.handleSubmit }>
|
||||
<input type="text" value={ this.props.title } onChange={this.handleChange} />
|
||||
<input type="submit" value="Add" />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Todos.propTypes = {
|
||||
todos: React.PropTypes.array.isRequired
|
||||
};
|
||||
*/
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
**/
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Todos)
|
||||
38
src/todos/reducer.js
Executable file
38
src/todos/reducer.js
Executable file
@ -0,0 +1,38 @@
|
||||
|
||||
import * as actions from '../actions'
|
||||
|
||||
export default function todos(todos = [], action){
|
||||
switch( action.type ){
|
||||
|
||||
case actions.LOAD:
|
||||
if( localStorage.getItem( 'todos' ) ){
|
||||
return JSON.parse( localStorage.getItem( 'todos' ) );
|
||||
}
|
||||
return [];
|
||||
|
||||
case actions.SAVE:
|
||||
localStorage.setItem( 'todos', JSON.stringify(todos) );
|
||||
return todos;
|
||||
|
||||
case actions.ADD_TODO:
|
||||
return Object.assign([], todos, [...todos, { id: action.id, title: action.title, completed: false }] )
|
||||
|
||||
case actions.TOGGLE_DONE:
|
||||
return Object.assign([], todos,
|
||||
todos.map(todo => {
|
||||
if( todo.id === action.id ){
|
||||
return Object.assign({}, todo, {
|
||||
completed: !todo.completed
|
||||
})
|
||||
}
|
||||
return todo
|
||||
})
|
||||
);
|
||||
|
||||
default:
|
||||
return todos
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
107
webpack.config.js
Executable file
107
webpack.config.js
Executable file
@ -0,0 +1,107 @@
|
||||
var dev = process.env.NODE_ENV !== "production";
|
||||
var webpack = require('webpack');
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
|
||||
var node_dir = __dirname + '/node_modules';
|
||||
var output_dir = __dirname +"/production"
|
||||
|
||||
var config = {
|
||||
|
||||
context: __dirname,
|
||||
entry: "./src/index.js",
|
||||
|
||||
output: {
|
||||
path: output_dir,
|
||||
filename: 'app.js'
|
||||
},
|
||||
|
||||
module: {
|
||||
loaders: [
|
||||
{ test: require.resolve('jquery'), loader: 'expose?jQuery!expose?$' },
|
||||
{
|
||||
// loading JSX (aka Babel) into browser-friendly ES6
|
||||
test: /\.js$/,
|
||||
exclude: [
|
||||
/(node_modules|bower_components)/,
|
||||
'index.js',
|
||||
],
|
||||
loader: 'babel-loader',
|
||||
query: {
|
||||
presets: ['es2015', 'react']
|
||||
}
|
||||
},
|
||||
{
|
||||
// loading sass asset files
|
||||
test: /\.scss$/,
|
||||
loader: ExtractTextPlugin.extract([
|
||||
'css'+(dev? '?sourceMap=true': ''),
|
||||
'sass'+(dev? '?outputStyle=expanded&sourceMap=true&sourceMapContents=true': '')
|
||||
])
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.optimize.DedupePlugin(),
|
||||
new webpack.optimize.OccurenceOrderPlugin(),
|
||||
new webpack.ProvidePlugin({
|
||||
$: "jquery",
|
||||
jQuery: "jquery",
|
||||
"window.jQuery": "jquery"
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Development-only configuration values
|
||||
**/
|
||||
if( dev ){
|
||||
|
||||
// set compiled css location
|
||||
config.plugins.push( new ExtractTextPlugin("app.css") );
|
||||
|
||||
// we want source maps
|
||||
config.devtool = 'source-map';
|
||||
|
||||
|
||||
/**
|
||||
* Production-only configuration values
|
||||
**/
|
||||
}else{
|
||||
|
||||
// set our final output filename
|
||||
config.output.filename = 'app.min.js';
|
||||
|
||||
// re-iterate our production value as a string (for ReactJS building)
|
||||
config.plugins.push(
|
||||
new webpack.DefinePlugin({
|
||||
'process.env':{
|
||||
'NODE_ENV': JSON.stringify('production')
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// remove all debug and console code
|
||||
config.module.loaders.push(
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
loader: "webpack-strip?strip[]=console.log,strip[]=console.info,strip[]=debug"
|
||||
}
|
||||
);
|
||||
|
||||
// set compiled css location
|
||||
config.plugins.push( new ExtractTextPlugin("app.min.css") );
|
||||
|
||||
// uglify our js, with no sourcemaps
|
||||
config.plugins.push(
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: true,
|
||||
mangle: false,
|
||||
sourceMap: false,
|
||||
comments: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// now export our collated config object
|
||||
module.exports = config;
|
||||
Reference in New Issue
Block a user