├── .gitignore ├── client ├── middleware │ ├── index.js │ └── logger.js ├── constants │ └── filters.js ├── reducers │ ├── index.js │ └── todos.js ├── actions │ └── todos.js ├── index.js ├── components │ ├── Header │ │ └── index.js │ ├── TodoTextInput │ │ ├── style.css │ │ └── index.js │ ├── Footer │ │ ├── index.js │ │ └── style.css │ ├── TodoItem │ │ ├── index.js │ │ └── style.css │ └── MainSection │ │ ├── index.js │ │ └── style.css ├── store │ └── index.js ├── containers │ └── App │ │ ├── index.js │ │ └── style.css └── index.html ├── .babelrc ├── Readme.md ├── package.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | static 4 | .module-cache 5 | *.log* 6 | 7 | -------------------------------------------------------------------------------- /client/middleware/index.js: -------------------------------------------------------------------------------- 1 | 2 | import logger from './logger' 3 | 4 | export { 5 | logger 6 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /client/middleware/logger.js: -------------------------------------------------------------------------------- 1 | 2 | export default store => next => action => { 3 | console.log(action) 4 | return next(action) 5 | } -------------------------------------------------------------------------------- /client/constants/filters.js: -------------------------------------------------------------------------------- 1 | 2 | export const SHOW_ALL = 'show_all' 3 | export const SHOW_COMPLETED = 'show_completed' 4 | export const SHOW_ACTIVE = 'show_active' 5 | -------------------------------------------------------------------------------- /client/reducers/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { routeReducer as routing } from 'react-router-redux' 3 | import { combineReducers } from 'redux' 4 | import todos from './todos' 5 | 6 | export default combineReducers({ 7 | routing, 8 | todos 9 | }) 10 | -------------------------------------------------------------------------------- /client/actions/todos.js: -------------------------------------------------------------------------------- 1 | 2 | import { createAction } from 'redux-actions' 3 | 4 | export const addTodo = createAction('add todo') 5 | export const deleteTodo = createAction('delete todo') 6 | export const editTodo = createAction('edit todo') 7 | export const completeTodo = createAction('complete todo') 8 | export const completeAll = createAction('complete all') 9 | export const clearCompleted = createAction('clear complete') 10 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { Router, Route, browserHistory } from 'react-router' 3 | import { Provider } from 'react-redux' 4 | import ReactDOM from 'react-dom' 5 | import React from 'react' 6 | 7 | import App from './containers/App' 8 | import configure from './store' 9 | 10 | const store = configure() 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 17 | 18 | , 19 | document.getElementById('root') 20 | ) 21 | -------------------------------------------------------------------------------- /client/components/Header/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import TodoTextInput from '../TodoTextInput' 4 | 5 | class Header extends Component { 6 | handleSave(text) { 7 | if (text.length) { 8 | this.props.addTodo(text) 9 | } 10 | } 11 | 12 | render() { 13 | return ( 14 |
15 |

Todos

16 | 20 |
21 | ) 22 | } 23 | } 24 | 25 | export default Header 26 | -------------------------------------------------------------------------------- /client/components/TodoTextInput/style.css: -------------------------------------------------------------------------------- 1 | 2 | .new, 3 | .edit { 4 | position: relative; 5 | margin: 0; 6 | width: 100%; 7 | font-size: 24px; 8 | font-family: inherit; 9 | font-weight: inherit; 10 | line-height: 1.4em; 11 | border: 0; 12 | outline: none; 13 | color: inherit; 14 | padding: 6px; 15 | border: 1px solid #999; 16 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 17 | box-sizing: border-box; 18 | font-smoothing: antialiased; 19 | } 20 | 21 | .new { 22 | padding: 16px 16px 16px 60px; 23 | border: none; 24 | background: rgba(0, 0, 0, 0.003); 25 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 26 | } -------------------------------------------------------------------------------- /client/store/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { createStore, applyMiddleware } from 'redux' 3 | import { syncHistory } from 'react-router-redux' 4 | import { browserHistory } from 'react-router' 5 | 6 | import { logger } from '../middleware' 7 | import rootReducer from '../reducers' 8 | 9 | export default function configure(initialState) { 10 | const create = window.devToolsExtension 11 | ? window.devToolsExtension()(createStore) 12 | : createStore 13 | 14 | const createStoreWithMiddleware = applyMiddleware( 15 | logger, 16 | syncHistory(browserHistory) 17 | )(create) 18 | 19 | const store = createStoreWithMiddleware(rootReducer, initialState) 20 | 21 | if (module.hot) { 22 | module.hot.accept('../reducers', () => { 23 | const nextReducer = require('../reducers') 24 | store.replaceReducer(nextReducer) 25 | }) 26 | } 27 | 28 | return store 29 | } 30 | -------------------------------------------------------------------------------- /client/containers/App/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | import Header from '../../components/Header' 6 | import MainSection from '../../components/MainSection' 7 | import * as TodoActions from '../../actions/todos' 8 | import style from './style.css' 9 | 10 | class App extends Component { 11 | render() { 12 | const { todos, actions, children } = this.props 13 | return ( 14 |
15 |
16 | 17 | {children} 18 |
19 | ) 20 | } 21 | } 22 | 23 | function mapStateToProps(state) { 24 | return { 25 | todos: state.todos 26 | } 27 | } 28 | 29 | function mapDispatchToProps(dispatch) { 30 | return { 31 | actions: bindActionCreators(TodoActions, dispatch) 32 | } 33 | } 34 | 35 | export default connect( 36 | mapStateToProps, 37 | mapDispatchToProps 38 | )(App) 39 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Boilerplate 6 | 7 | 8 |
9 | 10 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Frontend Boilerplate 3 | 4 | A boilerplate of things that mostly shouldn't exist. 5 | 6 | ## Contains 7 | 8 | - [x] [Webpack](https://webpack.github.io) 9 | - [x] [React](https://facebook.github.io/react/) 10 | - [x] [Redux](https://github.com/rackt/redux) 11 | - [x] [Babel](https://babeljs.io/) 12 | - [x] [Autoprefixer](https://github.com/postcss/autoprefixer) 13 | - [x] [PostCSS](https://github.com/postcss/postcss) 14 | - [x] [CSS modules](https://github.com/outpunk/postcss-modules) 15 | - [x] [Rucksack](http://simplaio.github.io/rucksack/docs) 16 | - [x] [React Router Redux](https://github.com/rackt/react-router-redux) 17 | - [x] [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension) 18 | - [ ] Redux effects 19 | - [x] TodoMVC example 20 | 21 | ## Setup 22 | 23 | ``` 24 | $ npm install 25 | ``` 26 | 27 | ## Running 28 | 29 | ``` 30 | $ npm start 31 | ``` 32 | 33 | ## Note 34 | 35 | This is just my personal boilerplate, it may or may not be a good fit for your project(s). 36 | 37 | # License 38 | 39 | MIT 40 | -------------------------------------------------------------------------------- /client/reducers/todos.js: -------------------------------------------------------------------------------- 1 | 2 | import { handleActions } from 'redux-actions' 3 | 4 | const initialState = [{ 5 | text: 'Use Redux', 6 | completed: false, 7 | id: 0 8 | }] 9 | 10 | export default handleActions({ 11 | 'add todo' (state, action) { 12 | return [{ 13 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, 14 | completed: false, 15 | text: action.payload 16 | }, ...state] 17 | }, 18 | 19 | 'delete todo' (state, action) { 20 | return state.filter(todo => todo.id !== action.payload ) 21 | }, 22 | 23 | 'edit todo' (state, action) { 24 | return state.map(todo => { 25 | return todo.id === action.payload.id 26 | ? { ...todo, text: action.payload.text } 27 | : todo 28 | }) 29 | }, 30 | 31 | 'complete todo' (state, action) { 32 | return state.map(todo => { 33 | return todo.id === action.payload 34 | ? { ...todo, completed: !todo.completed } 35 | : todo 36 | }) 37 | }, 38 | 39 | 'complete all' (state, action) { 40 | const areAllMarked = state.every(todo => todo.completed) 41 | return state.map(todo => { 42 | return { 43 | ...todo, 44 | completed: !areAllMarked 45 | } 46 | }) 47 | }, 48 | 49 | 'clear complete' (state, action) { 50 | return state.filter(todo => todo.completed === false) 51 | } 52 | }, initialState) 53 | -------------------------------------------------------------------------------- /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 --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.3.26", 15 | "babel-loader": "^6.2.0", 16 | "babel-plugin-transform-runtime": "^6.3.13", 17 | "babel-preset-es2015": "^6.3.13", 18 | "babel-preset-react": "^6.3.13", 19 | "babel-preset-stage-0": "^6.3.13", 20 | "css-loader": "^0.23.1", 21 | "file-loader": "^0.8.4", 22 | "postcss-loader": "^0.8.0", 23 | "rucksack-css": "^0.8.5", 24 | "style-loader": "^0.12.4", 25 | "webpack": "^1.12.2", 26 | "webpack-dev-server": "^1.12.0", 27 | "webpack-hot-middleware": "^2.2.0", 28 | "babel-runtime": "^6.3.19", 29 | "classnames": "^2.1.2", 30 | "react": "^0.14.0", 31 | "react-dom": "^0.14.0", 32 | "react-hot-loader": "^1.3.0", 33 | "react-redux": "^4.0.6", 34 | "react-router": "^2.0.0-rc5", 35 | "react-router-redux": "^2.1.0", 36 | "redux": "^3.0.2", 37 | "redux-actions": "^0.9.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/components/TodoTextInput/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import classnames from 'classnames' 4 | import style from './style.css' 5 | 6 | class TodoTextInput extends Component { 7 | constructor(props, context) { 8 | super(props, context) 9 | this.state = { 10 | text: this.props.text || '' 11 | } 12 | } 13 | 14 | handleSubmit(e) { 15 | const text = e.target.value.trim() 16 | if (e.which === 13) { 17 | this.props.onSave(text) 18 | if (this.props.newTodo) { 19 | this.setState({ text: '' }) 20 | } 21 | } 22 | } 23 | 24 | handleChange(e) { 25 | this.setState({ text: e.target.value }) 26 | } 27 | 28 | handleBlur(e) { 29 | const text = e.target.value.trim() 30 | if (!this.props.newTodo) { 31 | this.props.onSave(text) 32 | } 33 | } 34 | 35 | render() { 36 | const classes = classnames({ 37 | [style.edit]: this.props.editing, 38 | [style.new]: this.props.newTodo 39 | }, style.normal) 40 | 41 | return ( 42 | 50 | ) 51 | } 52 | } 53 | 54 | export default TodoTextInput 55 | -------------------------------------------------------------------------------- /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: ['react'] 11 | }, 12 | output: { 13 | path: path.join(__dirname, './static'), 14 | filename: 'bundle.js', 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.html$/, 20 | loader: 'file?name=[name].[ext]' 21 | }, 22 | { 23 | test: /\.css$/, 24 | loaders: [ 25 | 'style-loader', 26 | 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 27 | 'postcss-loader' 28 | ] 29 | }, 30 | { 31 | test: /\.(js|jsx)$/, 32 | exclude: /node_modules/, 33 | loaders: [ 34 | 'react-hot', 35 | 'babel-loader' 36 | ] 37 | }, 38 | ], 39 | }, 40 | resolve: { 41 | extensions: ['', '.js', '.jsx'] 42 | }, 43 | postcss: [ 44 | rucksack({ 45 | autoprefixer: true 46 | }) 47 | ], 48 | plugins: [ 49 | new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js'), 50 | new webpack.DefinePlugin({ 51 | 'process.env': { NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development') } 52 | }) 53 | ], 54 | devServer: { 55 | contentBase: './client', 56 | hot: true 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /client/containers/App/style.css: -------------------------------------------------------------------------------- 1 | 2 | html, 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | button { 9 | margin: 0; 10 | padding: 0; 11 | border: 0; 12 | background: none; 13 | font-size: 100%; 14 | vertical-align: baseline; 15 | font-family: inherit; 16 | font-weight: inherit; 17 | color: inherit; 18 | appearance: none; 19 | font-smoothing: antialiased; 20 | } 21 | 22 | body { 23 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 24 | line-height: 1.4em; 25 | background: #f5f5f5; 26 | color: #4d4d4d; 27 | min-width: 230px; 28 | max-width: 550px; 29 | margin: 0 auto; 30 | -webkit-font-smoothing: antialiased; 31 | -moz-font-smoothing: antialiased; 32 | -ms-font-smoothing: antialiased; 33 | font-smoothing: antialiased; 34 | font-weight: 300; 35 | } 36 | 37 | button, 38 | input[type="checkbox"] { 39 | outline: none; 40 | } 41 | 42 | .normal { 43 | background: #fff; 44 | margin: 200px 0 40px 0; 45 | position: relative; 46 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 47 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 48 | } 49 | 50 | .normal input::-webkit-input-placeholder { 51 | font-style: italic; 52 | font-weight: 300; 53 | color: #e6e6e6; 54 | } 55 | 56 | .normal input::-moz-placeholder { 57 | font-style: italic; 58 | font-weight: 300; 59 | color: #e6e6e6; 60 | } 61 | 62 | .normal input::input-placeholder { 63 | font-style: italic; 64 | font-weight: 300; 65 | color: #e6e6e6; 66 | } 67 | 68 | .normal h1 { 69 | position: absolute; 70 | top: -155px; 71 | width: 100%; 72 | font-size: 100px; 73 | font-weight: 100; 74 | text-align: center; 75 | color: rgba(175, 47, 47, 0.15); 76 | -webkit-text-rendering: optimizeLegibility; 77 | -moz-text-rendering: optimizeLegibility; 78 | -ms-text-rendering: optimizeLegibility; 79 | text-rendering: optimizeLegibility; 80 | } 81 | -------------------------------------------------------------------------------- /client/components/Footer/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../../constants/filters' 4 | import classnames from 'classnames' 5 | import style from './style.css' 6 | 7 | const FILTER_TITLES = { 8 | [SHOW_ALL]: 'All', 9 | [SHOW_ACTIVE]: 'Active', 10 | [SHOW_COMPLETED]: 'Completed' 11 | } 12 | 13 | class Footer extends Component { 14 | renderTodoCount() { 15 | const { activeCount } = this.props 16 | const itemWord = activeCount === 1 ? 'item' : 'items' 17 | 18 | return ( 19 | 20 | {activeCount || 'No'} {itemWord} left 21 | 22 | ) 23 | } 24 | 25 | renderFilterLink(filter) { 26 | const title = FILTER_TITLES[filter] 27 | const { filter: selectedFilter, onShow } = this.props 28 | 29 | return ( 30 | onShow(filter)}> 33 | {title} 34 | 35 | ) 36 | } 37 | 38 | renderClearButton() { 39 | const { completedCount, onClearCompleted } = this.props 40 | if (completedCount > 0) { 41 | return ( 42 | 45 | ) 46 | } 47 | } 48 | 49 | render() { 50 | return ( 51 | 62 | ) 63 | } 64 | } 65 | 66 | export default Footer 67 | -------------------------------------------------------------------------------- /client/components/Footer/style.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | color: #777; 4 | padding: 10px 15px; 5 | height: 20px; 6 | text-align: center; 7 | border-top: 1px solid #e6e6e6; 8 | } 9 | 10 | .normal:before { 11 | content: ''; 12 | position: absolute; 13 | right: 0; 14 | bottom: 0; 15 | left: 0; 16 | height: 50px; 17 | overflow: hidden; 18 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 19 | 0 8px 0 -3px #f6f6f6, 20 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 21 | 0 16px 0 -6px #f6f6f6, 22 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 23 | } 24 | 25 | .filters { 26 | margin: 0; 27 | padding: 0; 28 | list-style: none; 29 | position: absolute; 30 | right: 0; 31 | left: 0; 32 | } 33 | 34 | .filters li { 35 | display: inline; 36 | } 37 | 38 | .filters li a { 39 | color: inherit; 40 | margin: 3px; 41 | padding: 3px 7px; 42 | text-decoration: none; 43 | border: 1px solid transparent; 44 | border-radius: 3px; 45 | } 46 | 47 | .filters li a.selected, 48 | .filters li a:hover { 49 | border-color: rgba(175, 47, 47, 0.1); 50 | } 51 | 52 | .filters li a.selected { 53 | border-color: rgba(175, 47, 47, 0.2); 54 | } 55 | 56 | .count { 57 | float: left; 58 | text-align: left; 59 | } 60 | 61 | .count strong { 62 | font-weight: 300; 63 | } 64 | 65 | .clearCompleted, 66 | html .clearCompleted:active { 67 | float: right; 68 | position: relative; 69 | line-height: 20px; 70 | text-decoration: none; 71 | cursor: pointer; 72 | visibility: hidden; 73 | position: relative; 74 | } 75 | 76 | .clearCompleted::after { 77 | visibility: visible; 78 | content: 'Clear completed'; 79 | position: absolute; 80 | right: 0; 81 | white-space: nowrap; 82 | } 83 | 84 | .clearCompleted:hover::after { 85 | text-decoration: underline; 86 | } 87 | 88 | @media (max-width: 430px) { 89 | .normal { 90 | height: 50px; 91 | } 92 | 93 | .filters { 94 | bottom: 10px; 95 | } 96 | } -------------------------------------------------------------------------------- /client/components/TodoItem/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import TodoTextInput from '../TodoTextInput' 4 | import classnames from 'classnames' 5 | import style from './style.css' 6 | 7 | class TodoItem extends Component { 8 | constructor(props, context) { 9 | super(props, context) 10 | this.state = { 11 | editing: false 12 | } 13 | } 14 | 15 | handleDoubleClick() { 16 | this.setState({ editing: true }) 17 | } 18 | 19 | handleSave(id, text) { 20 | if (text.length === 0) { 21 | this.props.deleteTodo(id) 22 | } else { 23 | this.props.editTodo({ id, text }) 24 | } 25 | this.setState({ editing: false }) 26 | } 27 | 28 | render() { 29 | const {todo, completeTodo, deleteTodo} = this.props 30 | 31 | let element 32 | if (this.state.editing) { 33 | element = ( 34 | this.handleSave(todo.id, text)} /> 37 | ) 38 | } else { 39 | element = ( 40 |
41 | completeTodo(todo.id)} /> 45 | 46 | 49 | 50 |
52 | ) 53 | } 54 | 55 | // TODO: compose 56 | const classes = classnames({ 57 | [style.completed]: todo.completed, 58 | [style.editing]: this.state.editing, 59 | [style.normal]: !this.state.editing 60 | }) 61 | 62 | return ( 63 |
  • 64 | {element} 65 |
  • 66 | ) 67 | } 68 | } 69 | 70 | export default TodoItem 71 | -------------------------------------------------------------------------------- /client/components/MainSection/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import TodoItem from '../TodoItem' 4 | import Footer from '../Footer' 5 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../../constants/filters' 6 | import style from './style.css' 7 | 8 | const TODO_FILTERS = { 9 | [SHOW_ALL]: () => true, 10 | [SHOW_ACTIVE]: todo => !todo.completed, 11 | [SHOW_COMPLETED]: todo => todo.completed 12 | } 13 | 14 | class MainSection extends Component { 15 | constructor(props, context) { 16 | super(props, context) 17 | this.state = { filter: SHOW_ALL } 18 | } 19 | 20 | handleClearCompleted() { 21 | const atLeastOneCompleted = this.props.todos.some(todo => todo.completed) 22 | if (atLeastOneCompleted) { 23 | this.props.actions.clearCompleted() 24 | } 25 | } 26 | 27 | handleShow(filter) { 28 | this.setState({ filter }) 29 | } 30 | 31 | renderToggleAll(completedCount) { 32 | const { todos, actions } = this.props 33 | if (todos.length > 0) { 34 | return 39 | } 40 | } 41 | 42 | renderFooter(completedCount) { 43 | const { todos } = this.props 44 | const { filter } = this.state 45 | const activeCount = todos.length - completedCount 46 | 47 | if (todos.length) { 48 | return ( 49 |