├── .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 |
--------------------------------------------------------------------------------