commit a414888b2a9b6abaac4c7df6392f82170dda0a77 Author: James Barnsley Date: Mon Oct 3 14:44:27 2016 +1300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6747ffe2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/node_modules/ +/production/ +*.orig +Thumbs.db diff --git a/package.json b/package.json new file mode 100755 index 00000000..925ccddf --- /dev/null +++ b/package.json @@ -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" +} \ No newline at end of file diff --git a/src/App.js b/src/App.js new file mode 100755 index 00000000..b7eb9178 --- /dev/null +++ b/src/App.js @@ -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 ( +
+ + {this.props.children} +
+ ); + } +} + +/** + * 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) \ No newline at end of file diff --git a/src/about/index.js b/src/about/index.js new file mode 100755 index 00000000..cc272b63 --- /dev/null +++ b/src/about/index.js @@ -0,0 +1,8 @@ + +import React from 'react' + +export default React.createClass({ + render() { + return
About
+ } +}) \ No newline at end of file diff --git a/src/actions.js b/src/actions.js new file mode 100755 index 00000000..8d539dee --- /dev/null +++ b/src/actions.js @@ -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 + } +} + + diff --git a/src/common/Album.js b/src/common/Album.js new file mode 100755 index 00000000..562fdcb0 --- /dev/null +++ b/src/common/Album.js @@ -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 ( +
+

{ this.state.album.name }

+

{ this.state.album.label }

+ +
+ ); + } + return null; + } + + render(){ + return ( +
+

Single album

+ { this.renderAlbum() } +
+ ); + } +} + + +/** + * 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) \ No newline at end of file diff --git a/src/common/Tracklist.js b/src/common/Tracklist.js new file mode 100755 index 00000000..8546e0d2 --- /dev/null +++ b/src/common/Tracklist.js @@ -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 ( + + ); + } + return null; + } +} \ No newline at end of file diff --git a/src/common/actions.js b/src/common/actions.js new file mode 100755 index 00000000..d7864d46 --- /dev/null +++ b/src/common/actions.js @@ -0,0 +1,12 @@ + +/** + * Actions and Action Creators + **/ + +export const LOAD_ALBUMS = 'LOAD_ALBUMS' + +export function loadAlbums(){ + return { + type: LOAD_ALBUMS + } +} \ No newline at end of file diff --git a/src/common/reducer.js b/src/common/reducer.js new file mode 100755 index 00000000..b1ce8565 --- /dev/null +++ b/src/common/reducer.js @@ -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 + } +} + + + diff --git a/src/config/Mopidy.js b/src/config/Mopidy.js new file mode 100755 index 00000000..9101bdd2 --- /dev/null +++ b/src/config/Mopidy.js @@ -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) \ No newline at end of file diff --git a/src/config/Spotify.js b/src/config/Spotify.js new file mode 100755 index 00000000..5b4f5dac --- /dev/null +++ b/src/config/Spotify.js @@ -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 ( +
+ Authorizing... + +
+ ); + } + + 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) \ No newline at end of file diff --git a/src/config/actions.js b/src/config/actions.js new file mode 100755 index 00000000..8c52a76b --- /dev/null +++ b/src/config/actions.js @@ -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 + } +} \ No newline at end of file diff --git a/src/config/reducer.js b/src/config/reducer.js new file mode 100755 index 00000000..706b8409 --- /dev/null +++ b/src/config/reducer.js @@ -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 + } +} + + + diff --git a/src/index.html b/src/index.html new file mode 100755 index 00000000..656e218f --- /dev/null +++ b/src/index.html @@ -0,0 +1,20 @@ + + + + Testing React and Redux + + + + + + + +
+ +
+ + + + + + \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100755 index 00000000..bf3257e0 --- /dev/null +++ b/src/index.js @@ -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() + + + + + + **/ + +ReactDOM.render( + + + + + + + + + + + + , + document.getElementById('app') +); \ No newline at end of file diff --git a/src/library/LibraryAlbums.js b/src/library/LibraryAlbums.js new file mode 100755 index 00000000..24260f42 --- /dev/null +++ b/src/library/LibraryAlbums.js @@ -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 ( +
+

Library album

+ { + this.state.albums.map(album => +

{ album.webformatURL }

+ ) + } +
+ ); + } +} + + +/** + * 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) \ No newline at end of file diff --git a/src/library/actions.js b/src/library/actions.js new file mode 100755 index 00000000..d7864d46 --- /dev/null +++ b/src/library/actions.js @@ -0,0 +1,12 @@ + +/** + * Actions and Action Creators + **/ + +export const LOAD_ALBUMS = 'LOAD_ALBUMS' + +export function loadAlbums(){ + return { + type: LOAD_ALBUMS + } +} \ No newline at end of file diff --git a/src/library/reducer.js b/src/library/reducer.js new file mode 100755 index 00000000..4697fd92 --- /dev/null +++ b/src/library/reducer.js @@ -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 + } +} + + + diff --git a/src/reducer.js b/src/reducer.js new file mode 100755 index 00000000..8d2dc149 --- /dev/null +++ b/src/reducer.js @@ -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 +}) + + + diff --git a/src/routes/NowPlaying.js b/src/routes/NowPlaying.js new file mode 100755 index 00000000..323efb71 --- /dev/null +++ b/src/routes/NowPlaying.js @@ -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 ( + + ); + } + return null; + } + + render(){ + return ( +
+

Now playing

+ { this.renderTracks() } +
+ ); + } +} + + +/** + * 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) \ No newline at end of file diff --git a/src/services/MopidyService.js b/src/services/MopidyService.js new file mode 100755 index 00000000..fd947a43 --- /dev/null +++ b/src/services/MopidyService.js @@ -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; \ No newline at end of file diff --git a/src/services/Services.js b/src/services/Services.js new file mode 100755 index 00000000..5136678b --- /dev/null +++ b/src/services/Services.js @@ -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; \ No newline at end of file diff --git a/src/services/SpotifyService.js b/src/services/SpotifyService.js new file mode 100755 index 00000000..30c65172 --- /dev/null +++ b/src/services/SpotifyService.js @@ -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 ( +
+ Authorizing... + +
+ ); + } + + 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) \ No newline at end of file diff --git a/src/services/actions.js b/src/services/actions.js new file mode 100755 index 00000000..041e620e --- /dev/null +++ b/src/services/actions.js @@ -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 + } +} \ No newline at end of file diff --git a/src/services/reducer.js b/src/services/reducer.js new file mode 100755 index 00000000..89ebda8c --- /dev/null +++ b/src/services/reducer.js @@ -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 + } +} + + + diff --git a/src/todos/index.js b/src/todos/index.js new file mode 100755 index 00000000..3dbaa37b --- /dev/null +++ b/src/todos/index.js @@ -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 ( + + ); + } +} + + + +/** + * 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 ( +
+
    + { + this.props.todos.map(todo => + + ) + } +
+ +
+ ); + } +} + + +/** + * 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
  • { this.props.todo.title }
  • ; + } +} + + +/** + * 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 + } +} + +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 ( +
    + + +
    + ); + } +} + +/* +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) \ No newline at end of file diff --git a/src/todos/reducer.js b/src/todos/reducer.js new file mode 100755 index 00000000..d9d3330f --- /dev/null +++ b/src/todos/reducer.js @@ -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 + } +} + + + diff --git a/webpack.config.js b/webpack.config.js new file mode 100755 index 00000000..54f04fcb --- /dev/null +++ b/webpack.config.js @@ -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;