├── .babelrc ├── .eslintrc ├── .gitignore ├── Readme.md ├── client ├── actions │ └── index.js ├── components │ ├── IonItemLink │ │ ├── index.js │ │ └── style.css │ ├── ModalPlaylistCreate │ │ ├── index.js │ │ └── style.css │ ├── _BASE │ │ ├── index.js │ │ └── style.css │ └── _JSON │ │ └── index.js ├── containers │ ├── About │ │ ├── index.js │ │ └── style.css │ ├── App │ │ ├── index.js │ │ └── style.css │ ├── NoMatch │ │ ├── index.js │ │ └── style.css │ ├── PlaylistInfo │ │ ├── index.js │ │ └── style.css │ ├── Playlists │ │ ├── index.js │ │ └── style.css │ └── _BASE │ │ ├── index.js │ │ └── style.css ├── decorators │ └── index.js ├── fonts │ ├── ionicons.eot │ ├── ionicons.svg │ ├── ionicons.ttf │ └── ionicons.woff ├── index.html ├── index.js ├── middleware │ ├── index.js │ ├── layout.js │ └── logger.js ├── reducers │ ├── index.js │ ├── layout.js │ ├── navbar.js │ └── playlists.js ├── selectors │ └── playlists.js └── store │ └── index.js ├── package.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": [ 4 | ["transform-decorators-legacy"], 5 | ["transform-runtime"], 6 | ["transform-object-rest-spread"] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "jsx": true, 4 | "modules": true 5 | }, 6 | "env": { 7 | "browser": true, 8 | "es6": true, 9 | "node": true 10 | }, 11 | "parser": "babel-eslint", 12 | "rules": { 13 | "quotes": [2, "single"], 14 | "strict": 0, 15 | "no-unused-vars": ["error", { "vars": "all", "args": "after-used" }], 16 | "react/jsx-uses-react": 2, 17 | "react/jsx-uses-vars": 2, 18 | "react/react-in-jsx-scope": 2 19 | }, 20 | "plugins": [ 21 | "react" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | static 4 | .module-cache 5 | *.log* 6 | 7 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # React-Ionic-Redux Demo 3 | 4 | A demo/example/boilerplate/seed/{buzzWord} using React + Ionic + Redux 5 | 6 | ## Contains 7 | 8 | - [x] [React](https://facebook.github.io/react/) 9 | - [x] [Reactionic](https://github.com/reactionic/reactionic) 10 | - [x] [Redux](https://github.com/reactjs/redux) 11 | - [x] [Autoprefixer](https://github.com/postcss/autoprefixer) 12 | - [x] [PostCSS](https://github.com/postcss/postcss) 13 | - [x] [CSS modules](https://github.com/outpunk/postcss-modules) 14 | - [x] [Rucksack](http://simplaio.github.io/rucksack/docs) 15 | - [x] [React Router Redux](https://github.com/reactjs/react-router-redux) 16 | - [x] [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension) 17 | - [x] CRUD example 18 | 19 | ## Setup 20 | 21 | ``` 22 | $ npm install 23 | ``` 24 | 25 | ## Running 26 | 27 | ``` 28 | $ npm start 29 | ``` 30 | 31 | ## Build 32 | 33 | ``` 34 | $ npm run build 35 | ``` 36 | 37 | ## Todo 38 | 39 | - [ ] Add [redux-sagas](https://github.com/yelouafi/redux-saga) 40 | - [ ] Be responsible (add testing with [tape](https://github.com/substack/tape)) 41 | - [ ] replace base folders with [plop](https://github.com/amwmedia/plop) 42 | - [ ] add scaffold for actions/reducers/sagas/etc... 43 | - [ ] do more stuff with example 😁 44 | 45 | 46 | ## Note 47 | 48 | Inspired by [TJ's](https://github.com/tj) [boilerplate](https://github.com/tj/frontend-boilerplate) 49 | 50 | # License 51 | 52 | MIT 53 | -------------------------------------------------------------------------------- /client/actions/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { createAction } from 'redux-actions' 3 | 4 | export const addPlaylist = createAction('add playlist') 5 | export const deletePlaylist = createAction('delete playlist') 6 | export const editPlaylist = createAction('edit playlist') 7 | 8 | export const setNavbarInfo = createAction('set navbar info') 9 | 10 | export const layoutReset = createAction('layout reset') 11 | export const layoutSideMenu = createAction('layout side menu') 12 | export const layoutModal = createAction('layout modal') 13 | -------------------------------------------------------------------------------- /client/components/IonItemLink/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { browserHistory } from 'react-router'; 4 | import { IonIcon, IonItem } from 'reactionic'; 5 | 6 | import style from './style.css' 7 | 8 | const IonItemLink = ({label, link}) => { 9 | 10 | return ( 11 | browserHistory.push( link ) }> 12 | {label} 13 | 14 | ) 15 | } 16 | 17 | export default IonItemLink 18 | -------------------------------------------------------------------------------- /client/components/IonItemLink/style.css: -------------------------------------------------------------------------------- 1 | 2 | .base { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /client/components/ModalPlaylistCreate/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import style from './style.css' 4 | 5 | import { bindActionCreators } from 'redux' 6 | import { connect } from 'react-redux' 7 | import * as MainActions from 'actions' 8 | 9 | import { IonModal } from 'reactionic'; 10 | 11 | @connect( 12 | state => ({ //mapStateToProps 13 | playlists: state.playlists, 14 | }), 15 | dispatch => ({ //mapDispatchToProps 16 | actions: bindActionCreators(MainActions, dispatch) 17 | }) 18 | ) 19 | class ModalPlaylistCreate extends Component { 20 | constructor(props) { 21 | super(props); 22 | 23 | this.state = { 24 | text: this.props.text || '' 25 | } 26 | } 27 | 28 | static contextTypes = { 29 | ionShowModal: React.PropTypes.func 30 | } 31 | 32 | handleChange(e) { 33 | this.setState({ text: e.target.value }) 34 | } 35 | 36 | handleCloseModal(){ 37 | this.context.ionShowModal( false ) 38 | } 39 | 40 | handleSave(e){ 41 | 42 | const text = e.target.value.trim() 43 | 44 | // disreguard if we have no text 45 | if (!text.length) { 46 | return; 47 | } 48 | 49 | // handle if we have an enter key 50 | if (e.which === 13) { 51 | 52 | this.props.actions.addPlaylist(text) 53 | 54 | this.setState({ text: '' }) 55 | 56 | this.handleCloseModal() 57 | } 58 | 59 | } 60 | 61 | renderModalBody(){ 62 | return ( 63 |
64 | 65 |
66 | 74 |
75 | 76 |
77 |

Tip: press enter to save...

78 |
79 | 80 |
81 | ) 82 | } 83 | 84 | render() { 85 | 86 | return ( 87 | 93 | 94 | {this.renderModalBody()} 95 | 96 | 97 | ); 98 | } 99 | } 100 | 101 | export default ModalPlaylistCreate 102 | -------------------------------------------------------------------------------- /client/components/ModalPlaylistCreate/style.css: -------------------------------------------------------------------------------- 1 | .base { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /client/components/_BASE/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import style from './style.css' 4 | import Json from 'components/_JSON'; // just for dev purposes 5 | 6 | const BASE = (props) => { 7 | 8 | return ( 9 |
10 | BASE 11 | 12 |
13 | ) 14 | } 15 | 16 | export default BASE 17 | -------------------------------------------------------------------------------- /client/components/_BASE/style.css: -------------------------------------------------------------------------------- 1 | 2 | .base { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /client/components/_JSON/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const Json = ({data}) => { 5 | return ( 6 |
{JSON.stringify(data, null, 2) }
7 | ) 8 | } 9 | 10 | export default Json 11 | -------------------------------------------------------------------------------- /client/containers/About/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { IonContent } from 'reactionic'; 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | import * as MainActions from 'actions' 6 | 7 | import {layoutHelper} from 'decorators'; 8 | 9 | import style from './style.css' 10 | 11 | @connect( 12 | state => ({ //mapStateToProps 13 | layout: state.layout, 14 | navbar: state.navbar, 15 | }), 16 | dispatch => ({ //mapDispatchToProps 17 | actions: bindActionCreators(MainActions, dispatch) 18 | }) 19 | ) 20 | @layoutHelper 21 | class About extends Component { 22 | constructor(props) { 23 | super(props); 24 | } 25 | 26 | componentDidMount() { 27 | 28 | const { actions } = this.props 29 | 30 | actions.setNavbarInfo({ 31 | title: 'About this project', 32 | }); 33 | 34 | } 35 | 36 | render() { 37 | return ( 38 | 39 |

Ionic + React + Redux = 🔥

40 |

This project is being used as an example of how you can structure your reactonic application.

41 |
42 | ) 43 | } 44 | } 45 | 46 | export default About 47 | -------------------------------------------------------------------------------- /client/containers/About/style.css: -------------------------------------------------------------------------------- 1 | 2 | .base { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /client/containers/App/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | 6 | import * as MainActions from 'actions' 7 | 8 | import IonItemLink from 'components/IonItemLink'; 9 | 10 | 11 | import style from './style.css' 12 | 13 | import { 14 | IonNavView, 15 | IonView, 16 | IonNavBar, 17 | IonNavBackButton, 18 | IonButton, 19 | IonSideMenuContainer, 20 | IonSideMenus, 21 | IonSideMenu, 22 | IonSideMenuContent, 23 | IonBody, 24 | } from 'reactionic'; 25 | 26 | 27 | @connect( 28 | state => { 29 | return { //mapStateToProps 30 | navbar: state.navbar 31 | } 32 | }, 33 | dispatch => ({ //mapDispatchToProps 34 | actions: bindActionCreators(MainActions, dispatch) 35 | }) 36 | ) 37 | class App extends Component { 38 | constructor(props, context) { 39 | super(props, context); 40 | 41 | } 42 | 43 | renderSideMenu(){ 44 | const menuItems = [ 45 | { 46 | label: 'Playlists', 47 | route: '/' 48 | }, 49 | { 50 | label: 'About', 51 | route: '/about' 52 | } 53 | ] 54 | 55 | return ( 56 | 57 | 58 |
59 |

Main Menu

60 |
61 | 62 |
63 |
64 | 65 | {menuItems.map(item => 66 | 67 | )} 68 | 69 |
70 |
71 |
72 |
73 | ) 74 | } 75 | 76 | renderNavbar(){ 77 | const { location:{ pathname }, navbar, actions } = this.props 78 | 79 | // if the patch id dafault we need no back buton 80 | const backButton = pathname !== '/' 81 | ? 82 | : null 83 | 84 | // our menu button to launch side nav 85 | const menuButton = { actions.layoutSideMenu(true)}} /> 86 | 87 | return 92 | } 93 | render() { 94 | const { children, location } = this.props 95 | 96 | const sidemenuSettings = { disable: 'left', hyperextensible: false } 97 | 98 | return ( 99 | 100 | 101 | 102 | {::this.renderSideMenu()} 103 | 104 | 105 | {::this.renderNavbar()} 106 | 107 | 108 | 109 | 110 | {/*our child routes*/} 111 | {children} 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | ); 122 | } 123 | } 124 | 125 | export default App 126 | -------------------------------------------------------------------------------- /client/containers/App/style.css: -------------------------------------------------------------------------------- 1 | 2 | html, 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | -------------------------------------------------------------------------------- /client/containers/NoMatch/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { IonContent } from 'reactionic'; 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | import * as MainActions from 'actions' 6 | 7 | import {layoutHelper} from 'decorators'; 8 | 9 | import style from './style.css' 10 | 11 | @connect( 12 | state => ({ //mapStateToProps 13 | layout: state.layout, 14 | navbar: state.navbar, 15 | }), 16 | dispatch => ({ //mapDispatchToProps 17 | actions: bindActionCreators(MainActions, dispatch) 18 | }) 19 | ) 20 | @layoutHelper 21 | class NoMatch extends Component { 22 | constructor(props) { 23 | super(props); 24 | } 25 | 26 | componentDidMount() { 27 | 28 | const { actions } = this.props 29 | 30 | actions.setNavbarInfo({ 31 | title: 'Welp...', 32 | }); 33 | 34 | } 35 | 36 | render() { 37 | return ( 38 | 39 |

Nothing here...

40 |
41 | ) 42 | } 43 | } 44 | 45 | export default NoMatch 46 | -------------------------------------------------------------------------------- /client/containers/NoMatch/style.css: -------------------------------------------------------------------------------- 1 | 2 | .base { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /client/containers/PlaylistInfo/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { browserHistory } from 'react-router'; 5 | import { connect } from 'react-redux' 6 | import * as MainActions from 'actions' 7 | 8 | import { IonContent, IonButton } from 'reactionic'; 9 | 10 | import {getPlaylistInfo} from 'selectors/playlists'; 11 | import {layoutHelper} from 'decorators'; 12 | 13 | import JSON from 'components/_JSON'; 14 | 15 | import style from './style.css' 16 | 17 | @connect( 18 | (state, props) => ({ //mapStateToProps 19 | layout: state.layout, 20 | navbar: state.navbar, 21 | playlistInfo: getPlaylistInfo(state, props) 22 | }), 23 | dispatch => ({ //mapDispatchToProps 24 | actions: bindActionCreators(MainActions, dispatch) 25 | }) 26 | ) 27 | @layoutHelper 28 | class PlaylistInfo extends Component { 29 | constructor(props) { 30 | super(props); 31 | 32 | this.state = { 33 | editing: false, 34 | text: this.props.playlistInfo.label || '', 35 | } 36 | } 37 | 38 | componentDidMount() { 39 | 40 | const { actions, playlistInfo } = this.props 41 | 42 | actions.setNavbarInfo({ 43 | title: `Playlist: ${playlistInfo.label}`, 44 | }); 45 | 46 | } 47 | 48 | handleDelete(){ 49 | const { playlistInfo:{ id }, actions } = this.props 50 | 51 | actions.deletePlaylist(id) 52 | 53 | browserHistory.push( '/' ) 54 | } 55 | 56 | handleEdit(){ 57 | this.setState({ editing: true }) 58 | } 59 | 60 | handleChange(e) { 61 | this.setState({ text: e.target.value }) 62 | } 63 | 64 | handleSave(){ 65 | const payload = { 66 | id: this.props.playlistInfo.id, 67 | label: this.state.text 68 | } 69 | 70 | this.props.actions.editPlaylist( payload ) 71 | this.setState({ editing: false }) 72 | } 73 | 74 | render() { 75 | const { playlistInfo } = this.props 76 | 77 | // we may need this since the delete happens before the redirect 78 | if (!playlistInfo) { 79 | return null 80 | } 81 | 82 | const title = this.state.editing 83 | ? 84 | :

{playlistInfo.label}

85 | 86 | const editSaveButton = this.state.editing 87 | ? Save 88 | : Edit 89 | 90 | return ( 91 | 92 | 93 | {title} 94 | 95 | 96 | 97 | {editSaveButton} 98 | 99 | Delete 100 | 101 | 102 | ) 103 | } 104 | } 105 | 106 | export default PlaylistInfo 107 | -------------------------------------------------------------------------------- /client/containers/PlaylistInfo/style.css: -------------------------------------------------------------------------------- 1 | 2 | .base { 3 | 4 | } 5 | 6 | .edit{ 7 | font-size: 25px !important; 8 | border-bottom: 2px solid #ccc !important; 9 | width: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /client/containers/Playlists/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | import * as MainActions from 'actions' 6 | 7 | import { IonContent, IonIcon, IonItem } from 'reactionic'; 8 | 9 | import IonItemLink from 'components/IonItemLink'; 10 | 11 | import style from './style.css' 12 | 13 | import {layoutHelper} from 'decorators'; 14 | 15 | import ModalPlaylistCreate from 'components/ModalPlaylistCreate'; 16 | 17 | 18 | @connect( 19 | state => ({ //mapStateToProps 20 | layout: state.layout, 21 | playlists: state.playlists, 22 | }), 23 | dispatch => ({ //mapDispatchToProps 24 | actions: bindActionCreators(MainActions, dispatch) 25 | }) 26 | ) 27 | @layoutHelper 28 | class Playlists extends Component { 29 | constructor(props) { 30 | super(props); 31 | } 32 | 33 | static contextTypes = { 34 | ionShowModal: React.PropTypes.func 35 | } 36 | 37 | componentDidMount() { 38 | this.props.actions.setNavbarInfo({ 39 | title: 'Playlists', 40 | }) 41 | } 42 | 43 | handleNewPlaylist(){ 44 | this.context.ionShowModal( ) 45 | } 46 | 47 | render() { 48 | const { playlists } = this.props 49 | 50 | return ( 51 | 52 | 53 | 54 | These are your playlists 55 | 56 | 57 | {playlists.map( entry => 58 | 59 | )} 60 | 61 | 62 | ) 63 | } 64 | } 65 | 66 | export default Playlists 67 | -------------------------------------------------------------------------------- /client/containers/Playlists/style.css: -------------------------------------------------------------------------------- 1 | 2 | .base { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /client/containers/_BASE/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { IonContent } from 'reactionic'; 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | import * as MainActions from 'actions' 6 | 7 | import {layoutHelper} from 'decorators'; 8 | 9 | import style from './style.css' 10 | 11 | @connect( 12 | state => ({ //mapStateToProps 13 | layout: state.layout, 14 | navbar: state.navbar, 15 | }), 16 | dispatch => ({ //mapDispatchToProps 17 | actions: bindActionCreators(MainActions, dispatch) 18 | }) 19 | ) 20 | @layoutHelper 21 | class BASE extends Component { 22 | constructor(props) { 23 | super(props); 24 | } 25 | 26 | componentDidMount() { 27 | 28 | const { actions } = this.props 29 | 30 | actions.setNavbarInfo({ 31 | title: 'Base page', 32 | }); 33 | 34 | } 35 | 36 | render() { 37 | return ( 38 | 39 | BASE 40 | 41 | ) 42 | } 43 | } 44 | 45 | export default BASE 46 | -------------------------------------------------------------------------------- /client/containers/_BASE/style.css: -------------------------------------------------------------------------------- 1 | 2 | .base { 3 | 4 | } 5 | -------------------------------------------------------------------------------- /client/decorators/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | // using this to mitigate context crap 4 | // need layout state loaded 5 | // TODO find a better solution -__- 6 | export const layoutHelper = function(Component) { 7 | 8 | const DecoratedComponent = React.createClass({ 9 | getInitialState() { 10 | return this.props; 11 | }, 12 | render() { 13 | const { layout } = this.props 14 | 15 | if (layout) { 16 | 17 | //if we have a reset, need to start to remove stuff after a check 18 | if(layout.reset) { 19 | const getSnapper = this.context.ionGetSnapper(); 20 | 21 | if (getSnapper && getSnapper.state().state === 'right') { 22 | this.context.ionSnapper.toggle('right') 23 | } 24 | } 25 | 26 | if (layout.sidemenu) { 27 | this.context.ionSnapper.toggle('right') 28 | } 29 | 30 | } 31 | 32 | return 33 | } 34 | }); 35 | 36 | DecoratedComponent.contextTypes = { 37 | ionSnapper: React.PropTypes.object, 38 | ionGetSnapper: React.PropTypes.func, 39 | ionShowPopover: React.PropTypes.func, 40 | ionShowModal: React.PropTypes.func, 41 | ionModal: React.PropTypes.bool, 42 | ionPlatform: React.PropTypes.object, 43 | router: React.PropTypes.object.isRequired, 44 | location: React.PropTypes.object 45 | } 46 | 47 | return DecoratedComponent; 48 | }; 49 | -------------------------------------------------------------------------------- /client/fonts/ionicons.eot: -------------------------------------------------------------------------------- 1 | ../../node_modules/reactionic/dist/scss/fonts/ionicons.eot -------------------------------------------------------------------------------- /client/fonts/ionicons.svg: -------------------------------------------------------------------------------- 1 | ../../node_modules/reactionic/dist/scss/fonts/ionicons.svg -------------------------------------------------------------------------------- /client/fonts/ionicons.ttf: -------------------------------------------------------------------------------- 1 | ../../node_modules/reactionic/dist/scss/fonts/ionicons.ttf -------------------------------------------------------------------------------- /client/fonts/ionicons.woff: -------------------------------------------------------------------------------- 1 | ../../node_modules/reactionic/dist/scss/fonts/ionicons.woff -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Boilerplate 6 | 7 | 8 | 9 | 10 |
11 | 12 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import { syncHistoryWithStore } from 'react-router-redux' 2 | import { Provider } from 'react-redux' 3 | import ReactDOM from 'react-dom' 4 | import React from 'react' 5 | 6 | import App from './containers/App' 7 | import configure from './store' 8 | 9 | const store = configure() 10 | const history = syncHistoryWithStore(browserHistory, store) 11 | 12 | import { Router, Route, IndexRoute, browserHistory } from 'react-router'; 13 | 14 | import About from 'containers/About'; 15 | import NoMatch from 'containers/NoMatch'; 16 | import Playlists from 'containers/Playlists'; 17 | import PlaylistInfo from 'containers/PlaylistInfo'; 18 | 19 | require('../node_modules/reactionic/dist/scss/styles/_reactionic.scss'); 20 | 21 | const routes = ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {/*error catch all*/} 33 | 34 | 35 | ); 36 | 37 | ReactDOM.render( 38 | 39 | 40 | , 41 | document.getElementById('root') 42 | ); 43 | -------------------------------------------------------------------------------- /client/middleware/index.js: -------------------------------------------------------------------------------- 1 | 2 | import logger from './logger' 3 | import layout from './layout' 4 | 5 | export { 6 | logger, 7 | layout 8 | } 9 | -------------------------------------------------------------------------------- /client/middleware/layout.js: -------------------------------------------------------------------------------- 1 | 2 | import * as actions from 'actions' 3 | 4 | export default store => next => action => { 5 | // Experimental - need to kill all layout actions once location changes 6 | if (action.type === '@@router/LOCATION_CHANGE') { 7 | store.dispatch( actions.layoutReset() ) 8 | } 9 | 10 | return next(action) 11 | } 12 | -------------------------------------------------------------------------------- /client/middleware/logger.js: -------------------------------------------------------------------------------- 1 | 2 | export default store => next => action => { 3 | console.log(action) 4 | return next(action) 5 | } -------------------------------------------------------------------------------- /client/reducers/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { routerReducer as routing } from 'react-router-redux' 3 | import { combineReducers } from 'redux' 4 | // import todos from './todos' 5 | import navbar from './navbar' 6 | import layout from './layout' 7 | import playlists from './playlists' 8 | 9 | export default combineReducers({ 10 | routing, 11 | // todos, 12 | navbar, 13 | layout, 14 | playlists 15 | }) 16 | -------------------------------------------------------------------------------- /client/reducers/layout.js: -------------------------------------------------------------------------------- 1 | 2 | import { handleActions } from 'redux-actions' 3 | 4 | const initialState = { 5 | sidemenu: false, 6 | } 7 | 8 | export default handleActions({ 9 | 'layout side menu' (state, action) { 10 | return { 11 | sidemenu: action.payload 12 | } 13 | }, 14 | 'layout modal' (state, action) { // XXX is this needed? 15 | return { 16 | modal: action.payload 17 | } 18 | }, 19 | 'layout reset' (state, action) { 20 | return { 21 | reset: true 22 | } 23 | } 24 | }, initialState) 25 | -------------------------------------------------------------------------------- /client/reducers/navbar.js: -------------------------------------------------------------------------------- 1 | 2 | import { handleActions } from 'redux-actions' 3 | 4 | const initialState = { 5 | title: 'Ionic React/Redux', 6 | } 7 | 8 | export default handleActions({ 9 | 'set navbar info' (state, action) { 10 | return action.payload 11 | } 12 | }, initialState) 13 | -------------------------------------------------------------------------------- /client/reducers/playlists.js: -------------------------------------------------------------------------------- 1 | 2 | import { handleActions } from 'redux-actions' 3 | 4 | // gonna load up some initial entries 5 | const initialState = [ 6 | { 7 | label: 'Todays Biggest Hits', 8 | id: 0 9 | }, 10 | { 11 | label: 'Summer Break', 12 | id: 1 13 | }, 14 | { 15 | label: 'New Music', 16 | id: 2 17 | }, 18 | { 19 | label: 'Handing out', 20 | id: 3 21 | }, 22 | { 23 | label: 'Shindig Tunes', 24 | id: 4 25 | }, 26 | { 27 | label: 'Working out', 28 | id: 5 29 | }, 30 | { 31 | label: '90\'s Hip Hop', 32 | id: 6 33 | }, 34 | 35 | ] 36 | 37 | export default handleActions({ 38 | 'add playlist' (state, action) { 39 | return [{ 40 | id: state.reduce((maxId, playlist) => Math.max(playlist.id, maxId), -1) + 1, 41 | label: action.payload 42 | }, ...state] 43 | }, 44 | 45 | 'delete playlist' (state, action) { 46 | return state.filter(playlist => playlist.id !== action.payload ) 47 | }, 48 | 49 | 'edit playlist' (state, action) { 50 | return state.map(playlist => { 51 | return playlist.id === action.payload.id 52 | ? { ...playlist, label: action.payload.label } 53 | : playlist 54 | }) 55 | }, 56 | }, initialState) 57 | -------------------------------------------------------------------------------- /client/selectors/playlists.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | 3 | const playListSelector = (state) => state.playlists 4 | 5 | const playlistIdSelector = (state, props) => parseInt( props.routeParams.id ) 6 | 7 | export const getPlaylistInfo = createSelector( 8 | playListSelector, 9 | playlistIdSelector, 10 | (playlists, playlistId) => playlists.find( entry => entry.id === playlistId ) 11 | ) 12 | -------------------------------------------------------------------------------- /client/store/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { createStore, applyMiddleware } from 'redux' 3 | 4 | import { logger, layout } from '../middleware' 5 | import rootReducer from '../reducers' 6 | 7 | export default function configure(initialState) { 8 | const create = window.devToolsExtension 9 | ? window.devToolsExtension()(createStore) 10 | : createStore 11 | 12 | const createStoreWithMiddleware = applyMiddleware( 13 | logger, 14 | layout 15 | )(create) 16 | 17 | const store = createStoreWithMiddleware(rootReducer, initialState) 18 | 19 | if (module.hot) { 20 | module.hot.accept('../reducers', () => { 21 | const nextReducer = require('../reducers') 22 | store.replaceReducer(nextReducer) 23 | }) 24 | } 25 | 26 | return store 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend-boilerplate", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "A boilerplate of things that shouldn't exist", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "webpack-dev-server -d --history-api-fallback --hot --inline --progress --colors --port 3000", 10 | "build": "NODE_ENV=production webpack --progress --colors" 11 | }, 12 | "license": "MIT", 13 | "devDependencies": { 14 | "babel-core": "^6.5.2", 15 | "babel-loader": "^6.2.3", 16 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 17 | "babel-plugin-transform-runtime": "^6.5.2", 18 | "babel-preset-es2015": "^6.5.0", 19 | "babel-preset-react": "^6.5.0", 20 | "babel-preset-stage-0": "^6.5.0", 21 | "babel-runtime": "^6.5.0", 22 | "classnames": "^2.2.3", 23 | "css-loader": "^0.23.1", 24 | "file-loader": "^0.8.5", 25 | "node-sass": "^3.7.0", 26 | "postcss-loader": "^0.8.1", 27 | "react": "^15.0.0", 28 | "react-dom": "^15.0.0", 29 | "react-hot-loader": "^1.3.0", 30 | "react-redux": "^4.4.0", 31 | "react-router": "^2.0.0", 32 | "react-router-redux": "^4.0.0", 33 | "redux": "^3.3.1", 34 | "redux-actions": "^0.9.1", 35 | "rucksack-css": "^0.8.5", 36 | "sass-loader": "^3.2.0", 37 | "style-loader": "^0.13.0", 38 | "webpack": "^1.13.1", 39 | "webpack-dev-server": "^1.14.1", 40 | "webpack-hot-middleware": "^2.7.1" 41 | }, 42 | "dependencies": { 43 | "babel-eslint": "^6.0.4", 44 | "babel-plugin-transform-decorators": "^6.8.0", 45 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 46 | "babel-plugin-transform-runtime": "^6.9.0", 47 | "eslint": "^2.11.1", 48 | "eslint-plugin-react": "^5.1.1", 49 | "reactionic": "^1.1.2", 50 | "reselect": "^2.5.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var rucksack = require('rucksack-css') 2 | var webpack = require('webpack') 3 | var path = require('path') 4 | 5 | module.exports = { 6 | context: path.join(__dirname, './client'), 7 | entry: { 8 | jsx: './index.js', 9 | html: './index.html', 10 | vendor: [ 11 | 'react', 12 | 'react-dom', 13 | 'react-redux', 14 | 'react-router', 15 | 'react-router-redux', 16 | 'redux' 17 | ] 18 | }, 19 | output: { 20 | path: path.join(__dirname, './static'), 21 | filename: 'bundle.js', 22 | }, 23 | module: { 24 | loaders: [ 25 | { 26 | test: /\.html$/, 27 | loader: 'file?name=[name].[ext]' 28 | }, 29 | { 30 | test: /\.css$/, 31 | include: /client/, 32 | loaders: [ 33 | 'style-loader', 34 | 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 35 | 'postcss-loader' 36 | ] 37 | }, 38 | { 39 | test: /\.css$/, 40 | exclude: /client/, 41 | loader: 'style!css' 42 | }, 43 | { 44 | test: /\.scss$/, 45 | loaders: ["style", "css", "sass"] 46 | }, 47 | { 48 | test: /\.(js|jsx)$/, 49 | exclude: /node_modules/, 50 | loaders: [ 51 | 'react-hot', 52 | 'babel-loader' 53 | ] 54 | }, 55 | ], 56 | }, 57 | resolve: { 58 | extensions: ['', '.js', '.jsx'], 59 | modulesDirectories: ['client', 'node_modules'], 60 | }, 61 | postcss: [ 62 | rucksack({ 63 | autoprefixer: true 64 | }) 65 | ], 66 | plugins: [ 67 | new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js'), 68 | new webpack.DefinePlugin({ 69 | 'process.env': { NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development') } 70 | }) 71 | ], 72 | devServer: { 73 | contentBase: './client', 74 | hot: true 75 | } 76 | } 77 | --------------------------------------------------------------------------------