├── .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 ├── .babelrc ├── scripts ├── index.jade ├── devServer.js ├── webpack.config.dev.js ├── build.js ├── webpack.config.prod.js └── webpack.config.base.js ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | static 4 | .module-cache 5 | *.log* 6 | 7 | -------------------------------------------------------------------------------- /client/middleware/index.js: -------------------------------------------------------------------------------- 1 | import logger from './logger' 2 | 3 | export { 4 | logger 5 | } 6 | -------------------------------------------------------------------------------- /client/middleware/logger.js: -------------------------------------------------------------------------------- 1 | export default store => next => action => { 2 | console.log(action) 3 | return next(action) 4 | } 5 | -------------------------------------------------------------------------------- /client/constants/filters.js: -------------------------------------------------------------------------------- 1 | export const SHOW_ALL = 'show_all' 2 | export const SHOW_COMPLETED = 'show_completed' 3 | export const SHOW_ACTIVE = 'show_active' 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["transform-runtime"], 4 | "env": { 5 | "development": { 6 | "presets": ["react-hmre"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { routeReducer as routing } from 'react-router-redux' 2 | import { combineReducers } from 'redux' 3 | import todos from './todos' 4 | 5 | export default combineReducers({ 6 | routing, 7 | todos 8 | }) 9 | -------------------------------------------------------------------------------- /scripts/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset="utf-8") 5 | title= htmlWebpackPlugin.options.title 6 | each css in htmlWebpackPlugin.files.css 7 | link(rel="stylesheet" href=css) 8 | body 9 | #root 10 | each js in htmlWebpackPlugin.files.js 11 | script(src=js) 12 | -------------------------------------------------------------------------------- /client/actions/todos.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions' 2 | 3 | export const addTodo = createAction('add todo') 4 | export const deleteTodo = createAction('delete todo') 5 | export const editTodo = createAction('edit todo') 6 | export const completeTodo = createAction('complete todo') 7 | export const completeAll = createAction('complete all') 8 | export const clearCompleted = createAction('clear complete') 9 | -------------------------------------------------------------------------------- /scripts/devServer.js: -------------------------------------------------------------------------------- 1 | const server = require('webpack-hot-server') 2 | const opn = require('opn') 3 | const config = require('./webpack.config.dev') 4 | 5 | const port = process.env.PORT || 7888 6 | const noBrowser = process.env.BROWSER 7 | 8 | server({ 9 | config, 10 | port, 11 | customIndex: true 12 | }).then(() => { 13 | console.log(`running at http://localhost:${port}, building...`) 14 | if (!noBrowser) { 15 | opn(`http://localhost:${port}`) 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import { Router, Route, browserHistory } from 'react-router' 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 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById('root') 19 | ) 20 | -------------------------------------------------------------------------------- /client/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import TodoTextInput from '../TodoTextInput' 3 | 4 | class Header extends Component { 5 | handleSave(text) { 6 | if (text.length) { 7 | this.props.addTodo(text) 8 | } 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 |

Todos

15 | 19 |
20 | ) 21 | } 22 | } 23 | 24 | export default Header 25 | -------------------------------------------------------------------------------- /scripts/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const config = require('./webpack.config.base') 3 | 4 | config.devtool = 'cheap-module-eval-source-map' 5 | config.entry.push('webpack-hot-middleware/client') 6 | config.plugins.push( 7 | new webpack.HotModuleReplacementPlugin(), 8 | new webpack.NoErrorsPlugin() 9 | ) 10 | config.module.loaders.push( 11 | { 12 | test: /\.css$/, 13 | loaders: [ 14 | 'style', 15 | 'css?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 16 | 'postcss' 17 | ] 18 | } 19 | ) 20 | 21 | module.exports = config 22 | -------------------------------------------------------------------------------- /client/components/TodoTextInput/style.css: -------------------------------------------------------------------------------- 1 | .new, 2 | .edit { 3 | position: relative; 4 | margin: 0; 5 | width: 100%; 6 | font-size: 24px; 7 | font-family: inherit; 8 | font-weight: inherit; 9 | line-height: 1.4em; 10 | border: 0; 11 | outline: none; 12 | color: inherit; 13 | padding: 6px; 14 | border: 1px solid #999; 15 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 16 | box-sizing: border-box; 17 | font-smoothing: antialiased; 18 | } 19 | 20 | .new { 21 | padding: 16px 16px 16px 60px; 22 | border: none; 23 | background: rgba(0, 0, 0, 0.003); 24 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 25 | } 26 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const config = require('./webpack.config.prod') 3 | const ProgressPlugin = require('webpack/lib/ProgressPlugin') 4 | const ProgressBar = require('progress') 5 | 6 | const bar = new ProgressBar('[:bar] :message', { 7 | complete: '=', 8 | incomplete: ' ', 9 | width: 20, 10 | total: 100, 11 | clear: true 12 | }) 13 | const compiler = webpack(config) 14 | compiler.apply(new ProgressPlugin(function(percentage, msg) { 15 | bar.update(percentage, { 16 | message: msg 17 | }) 18 | })) 19 | 20 | compiler.run((err, stats) => { 21 | console.log(stats.toString({ 22 | colors: true, 23 | children: false, 24 | chunks: false 25 | })) 26 | }) 27 | -------------------------------------------------------------------------------- /scripts/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const config = require('./webpack.config.base') 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | const extractCSS = new ExtractTextPlugin('style.[contenthash].css') 6 | 7 | config.devtool = 'source-map' 8 | config.output.filename = 'bundle.[chunkhash].js' 9 | config.plugins.push( 10 | new webpack.optimize.OccurenceOrderPlugin(), 11 | new webpack.optimize.UglifyJsPlugin({ 12 | compressor: { 13 | warnings: false 14 | }, 15 | comments: false 16 | }), 17 | extractCSS 18 | ) 19 | config.module.loaders.push( 20 | { 21 | test: /\.css$/, 22 | loader: extractCSS.extract([ 23 | 'css?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 24 | 'postcss' 25 | ]) 26 | } 27 | ) 28 | 29 | module.exports = config 30 | -------------------------------------------------------------------------------- /client/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import { syncHistory } from 'react-router-redux' 3 | import { browserHistory } from 'react-router' 4 | 5 | import { logger } from '../middleware' 6 | import rootReducer from '../reducers' 7 | 8 | export default function configure(initialState) { 9 | const create = window.devToolsExtension 10 | ? window.devToolsExtension()(createStore) 11 | : createStore 12 | 13 | const createStoreWithMiddleware = applyMiddleware( 14 | logger, 15 | syncHistory(browserHistory) 16 | )(create) 17 | 18 | const store = createStoreWithMiddleware(rootReducer, initialState) 19 | 20 | if (module.hot) { 21 | module.hot.accept('../reducers', () => { 22 | const nextReducer = require('../reducers') 23 | store.replaceReducer(nextReducer) 24 | }) 25 | } 26 | 27 | return store 28 | } 29 | -------------------------------------------------------------------------------- /client/containers/App/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux' 4 | import Header from '../../components/Header' 5 | import MainSection from '../../components/MainSection' 6 | import * as TodoActions from '../../actions/todos' 7 | import style from './style.css' 8 | 9 | class App extends Component { 10 | render() { 11 | const { todos, actions, children } = this.props 12 | return ( 13 |
14 |
15 | 16 | {children} 17 |
18 | ) 19 | } 20 | } 21 | 22 | function mapStateToProps(state) { 23 | return { 24 | todos: state.todos 25 | } 26 | } 27 | 28 | function mapDispatchToProps(dispatch) { 29 | return { 30 | actions: bindActionCreators(TodoActions, dispatch) 31 | } 32 | } 33 | 34 | export default connect( 35 | mapStateToProps, 36 | mapDispatchToProps 37 | )(App) 38 | -------------------------------------------------------------------------------- /scripts/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | 5 | module.exports = { 6 | context: path.join(__dirname, '../'), 7 | entry: ['./client/index'], 8 | output: { 9 | path: path.join(__dirname, '../static'), 10 | filename: 'bundle.js', 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.(js|jsx)$/, 16 | exclude: /node_modules/, 17 | loaders: ['babel'] 18 | }, 19 | { 20 | test: /\.jade$/, 21 | loaders: ['jade'] 22 | } 23 | ] 24 | }, 25 | resolve: { 26 | extensions: ['', '.js', '.jsx'] 27 | }, 28 | postcss: [ 29 | require('rucksack-css')({ 30 | autoprefixer: true 31 | }) 32 | ], 33 | plugins: [ 34 | new webpack.DefinePlugin({ 35 | 'process.env': { 36 | NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development') 37 | }, 38 | '__DEV__': !!process.env.NODE_ENV 39 | }), 40 | new HtmlWebpackPlugin({ 41 | title: 'React boilerplate', 42 | template: './scripts/index.jade', 43 | inject: false 44 | }) 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /client/reducers/todos.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | const initialState = [{ 4 | text: 'Use Redux', 5 | completed: false, 6 | id: 0 7 | }] 8 | 9 | export default handleActions({ 10 | 'add todo' (state, action) { 11 | return [{ 12 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, 13 | completed: false, 14 | text: action.payload 15 | }, ...state] 16 | }, 17 | 18 | 'delete todo' (state, action) { 19 | return state.filter(todo => todo.id !== action.payload ) 20 | }, 21 | 22 | 'edit todo' (state, action) { 23 | return state.map(todo => { 24 | return todo.id === action.payload.id 25 | ? { ...todo, text: action.payload.text } 26 | : todo 27 | }) 28 | }, 29 | 30 | 'complete todo' (state, action) { 31 | return state.map(todo => { 32 | return todo.id === action.payload 33 | ? { ...todo, completed: !todo.completed } 34 | : todo 35 | }) 36 | }, 37 | 38 | 'complete all' (state, action) { 39 | const areAllMarked = state.every(todo => todo.completed) 40 | return state.map(todo => { 41 | return { 42 | ...todo, 43 | completed: !areAllMarked 44 | } 45 | }) 46 | }, 47 | 48 | 'clear complete' (state, action) { 49 | return state.filter(todo => todo.completed === false) 50 | } 51 | }, initialState) 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Boilerplate 2 | 3 | A fork of [tj/frontend-boilerplate](https://github.com/tj/frontend-boilerplate), but more complete and advanced. 4 | 5 | ## Contains 6 | 7 | - [x] [Webpack](https://webpack.github.io) 8 | - [x] [React](https://facebook.github.io/react/) 9 | - [x] [Redux](https://github.com/rackt/redux) 10 | - [x] [Babel](https://babeljs.io/) 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](https://github.com/rackt/react-router) 16 | - [ ] Redux effects 17 | - [x] TodoMVC example 18 | - [x] Errors on the screen 19 | - [x] Custom index template 20 | - [x] Automatically open browser for you 21 | - [ ] BrowserSync 22 | - [ ] ESlint 23 | 24 | ## Setup 25 | 26 | ```bash 27 | $ npm install 28 | ``` 29 | 30 | ## Coding 31 | 32 | ```bash 33 | # hot reloading 34 | # specific PORT env to use custom port, default 7888 35 | # eg: PORT=3000 npm run dev 36 | $ npm run dev 37 | ``` 38 | 39 | ## Build 40 | 41 | ```bash 42 | # minify and optimize 43 | $ npm run build 44 | ``` 45 | 46 | ## Note 47 | 48 | This is just my personal boilerplate, it may or may not be a good fit for your project(s). 49 | 50 | # License 51 | 52 | MIT © [EGOIST](https://github.com/egoist) 53 | -------------------------------------------------------------------------------- /client/components/TodoTextInput/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import classnames from 'classnames' 3 | import style from './style.css' 4 | 5 | class TodoTextInput extends Component { 6 | constructor(props, context) { 7 | super(props, context) 8 | this.state = { 9 | text: this.props.text || '' 10 | } 11 | } 12 | 13 | handleSubmit(e) { 14 | const text = e.target.value.trim() 15 | if (e.which === 13) { 16 | this.props.onSave(text) 17 | if (this.props.newTodo) { 18 | this.setState({ text: '' }) 19 | } 20 | } 21 | } 22 | 23 | handleChange(e) { 24 | this.setState({ text: e.target.value }) 25 | } 26 | 27 | handleBlur(e) { 28 | const text = e.target.value.trim() 29 | if (!this.props.newTodo) { 30 | this.props.onSave(text) 31 | } 32 | } 33 | 34 | render() { 35 | const classes = classnames({ 36 | [style.edit]: this.props.editing, 37 | [style.new]: this.props.newTodo 38 | }, style.normal) 39 | 40 | return ( 41 | 49 | ) 50 | } 51 | } 52 | 53 | export default TodoTextInput 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-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 | "build": "rimraf ./static && cross-env NODE_ENV=production node scripts/build", 10 | "dev": "node scripts/devServer", 11 | "deploy": "npm run build && surge -p ./static -d rb.surge.sh" 12 | }, 13 | "license": "MIT", 14 | "devDependencies": { 15 | "babel-core": "^6.3.26", 16 | "babel-loader": "^6.2.0", 17 | "babel-plugin-transform-runtime": "^6.3.13", 18 | "babel-preset-es2015": "^6.3.13", 19 | "babel-preset-react": "^6.3.13", 20 | "babel-preset-react-hmre": "^1.0.1", 21 | "babel-preset-stage-0": "^6.3.13", 22 | "cross-env": "^1.0.7", 23 | "css-loader": "^0.23.1", 24 | "extract-text-webpack-plugin": "^1.0.1", 25 | "file-loader": "^0.8.4", 26 | "html-webpack-plugin": "^2.7.2", 27 | "jade": "^1.11.0", 28 | "jade-loader": "^0.8.0", 29 | "opn": "^4.0.0", 30 | "postcss-loader": "^0.8.0", 31 | "postcss-modules": "^0.1.3", 32 | "progress": "^1.1.8", 33 | "rimraf": "^2.5.1", 34 | "rucksack-css": "^0.8.5", 35 | "style-loader": "^0.12.4", 36 | "webpack": "^1.12.2", 37 | "webpack-dev-server": "^1.12.0", 38 | "webpack-hot-middleware": "^2.2.0", 39 | "webpack-hot-server": "^0.2.2" 40 | }, 41 | "dependencies": { 42 | "babel-runtime": "^6.3.19", 43 | "classnames": "^2.1.2", 44 | "react": "^0.14.0", 45 | "react-dom": "^0.14.0", 46 | "react-redux": "^4.0.6", 47 | "react-router": "^2.0.0-rc5", 48 | "react-router-redux": "^2.1.0", 49 | "redux": "^3.0.2", 50 | "redux-actions": "^0.9.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client/containers/App/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | appearance: none; 18 | font-smoothing: antialiased; 19 | } 20 | 21 | body { 22 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 23 | line-height: 1.4em; 24 | background: #f5f5f5; 25 | color: #4d4d4d; 26 | min-width: 230px; 27 | max-width: 550px; 28 | margin: 0 auto; 29 | -webkit-font-smoothing: antialiased; 30 | -moz-font-smoothing: antialiased; 31 | -ms-font-smoothing: antialiased; 32 | font-smoothing: antialiased; 33 | font-weight: 300; 34 | } 35 | 36 | button, 37 | input[type="checkbox"] { 38 | outline: none; 39 | } 40 | 41 | .normal { 42 | background: #fff; 43 | margin: 200px 0 40px 0; 44 | position: relative; 45 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 46 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 47 | } 48 | 49 | .normal input::-webkit-input-placeholder { 50 | font-style: italic; 51 | font-weight: 300; 52 | color: #e6e6e6; 53 | } 54 | 55 | .normal input::-moz-placeholder { 56 | font-style: italic; 57 | font-weight: 300; 58 | color: #e6e6e6; 59 | } 60 | 61 | .normal input::input-placeholder { 62 | font-style: italic; 63 | font-weight: 300; 64 | color: #e6e6e6; 65 | } 66 | 67 | .normal h1 { 68 | position: absolute; 69 | top: -155px; 70 | width: 100%; 71 | font-size: 100px; 72 | font-weight: 100; 73 | text-align: center; 74 | color: rgba(175, 47, 47, 0.15); 75 | -webkit-text-rendering: optimizeLegibility; 76 | -moz-text-rendering: optimizeLegibility; 77 | -ms-text-rendering: optimizeLegibility; 78 | text-rendering: optimizeLegibility; 79 | } 80 | -------------------------------------------------------------------------------- /client/components/Footer/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../../constants/filters' 3 | import classnames from 'classnames' 4 | import style from './style.css' 5 | 6 | const FILTER_TITLES = { 7 | [SHOW_ALL]: 'All', 8 | [SHOW_ACTIVE]: 'Active', 9 | [SHOW_COMPLETED]: 'Completed' 10 | } 11 | 12 | class Footer extends Component { 13 | renderTodoCount() { 14 | const { activeCount } = this.props 15 | const itemWord = activeCount === 1 ? 'item' : 'items' 16 | 17 | return ( 18 | 19 | {activeCount || 'No'} {itemWord} left 20 | 21 | ) 22 | } 23 | 24 | renderFilterLink(filter) { 25 | const title = FILTER_TITLES[filter] 26 | const { filter: selectedFilter, onShow } = this.props 27 | 28 | return ( 29 | onShow(filter)}> 32 | {title} 33 | 34 | ) 35 | } 36 | 37 | renderClearButton() { 38 | const { completedCount, onClearCompleted } = this.props 39 | if (completedCount > 0) { 40 | return ( 41 | 44 | ) 45 | } 46 | } 47 | 48 | render() { 49 | return ( 50 | 61 | ) 62 | } 63 | } 64 | 65 | export default Footer 66 | -------------------------------------------------------------------------------- /client/components/TodoItem/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import TodoTextInput from '../TodoTextInput' 3 | import classnames from 'classnames' 4 | import style from './style.css' 5 | 6 | class TodoItem extends Component { 7 | constructor(props, context) { 8 | super(props, context) 9 | this.state = { 10 | editing: false 11 | } 12 | } 13 | 14 | handleDoubleClick() { 15 | this.setState({ editing: true }) 16 | } 17 | 18 | handleSave(id, text) { 19 | if (text.length === 0) { 20 | this.props.deleteTodo(id) 21 | } else { 22 | this.props.editTodo({ id, text }) 23 | } 24 | this.setState({ editing: false }) 25 | } 26 | 27 | render() { 28 | const {todo, completeTodo, deleteTodo} = this.props 29 | 30 | let element 31 | if (this.state.editing) { 32 | element = ( 33 | this.handleSave(todo.id, text)} /> 36 | ) 37 | } else { 38 | element = ( 39 |
40 | completeTodo(todo.id)} /> 44 | 45 | 48 | 49 |
51 | ) 52 | } 53 | 54 | // TODO: compose 55 | const classes = classnames({ 56 | [style.completed]: todo.completed, 57 | [style.editing]: this.state.editing, 58 | [style.normal]: !this.state.editing 59 | }) 60 | 61 | return ( 62 |
  • 63 | {element} 64 |
  • 65 | ) 66 | } 67 | } 68 | 69 | export default TodoItem 70 | -------------------------------------------------------------------------------- /client/components/Footer/style.css: -------------------------------------------------------------------------------- 1 | .normal { 2 | color: #777; 3 | padding: 10px 15px; 4 | height: 20px; 5 | text-align: center; 6 | border-top: 1px solid #e6e6e6; 7 | } 8 | 9 | .normal:before { 10 | content: ''; 11 | position: absolute; 12 | right: 0; 13 | bottom: 0; 14 | left: 0; 15 | height: 50px; 16 | overflow: hidden; 17 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 18 | 0 8px 0 -3px #f6f6f6, 19 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 20 | 0 16px 0 -6px #f6f6f6, 21 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 22 | } 23 | 24 | .filters { 25 | margin: 0; 26 | padding: 0; 27 | list-style: none; 28 | position: absolute; 29 | right: 0; 30 | left: 0; 31 | } 32 | 33 | .filters li { 34 | display: inline; 35 | } 36 | 37 | .filters li a { 38 | color: inherit; 39 | margin: 3px; 40 | padding: 3px 7px; 41 | text-decoration: none; 42 | border: 1px solid transparent; 43 | border-radius: 3px; 44 | } 45 | 46 | .filters li a.selected, 47 | .filters li a:hover { 48 | border-color: rgba(175, 47, 47, 0.1); 49 | } 50 | 51 | .filters li a.selected { 52 | border-color: rgba(175, 47, 47, 0.2); 53 | } 54 | 55 | .count { 56 | float: left; 57 | text-align: left; 58 | } 59 | 60 | .count strong { 61 | font-weight: 300; 62 | } 63 | 64 | .clearCompleted, 65 | html .clearCompleted:active { 66 | float: right; 67 | position: relative; 68 | line-height: 20px; 69 | text-decoration: none; 70 | cursor: pointer; 71 | visibility: hidden; 72 | position: relative; 73 | } 74 | 75 | .clearCompleted::after { 76 | visibility: visible; 77 | content: 'Clear completed'; 78 | position: absolute; 79 | right: 0; 80 | white-space: nowrap; 81 | } 82 | 83 | .clearCompleted:hover::after { 84 | text-decoration: underline; 85 | } 86 | 87 | @media (max-width: 430px) { 88 | .normal { 89 | height: 50px; 90 | } 91 | 92 | .filters { 93 | bottom: 10px; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /client/components/MainSection/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import TodoItem from '../TodoItem' 3 | import Footer from '../Footer' 4 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../../constants/filters' 5 | import style from './style.css' 6 | 7 | const TODO_FILTERS = { 8 | [SHOW_ALL]: () => true, 9 | [SHOW_ACTIVE]: todo => !todo.completed, 10 | [SHOW_COMPLETED]: todo => todo.completed 11 | } 12 | 13 | class MainSection extends Component { 14 | constructor(props, context) { 15 | super(props, context) 16 | this.state = { filter: SHOW_ALL } 17 | } 18 | 19 | handleClearCompleted() { 20 | const atLeastOneCompleted = this.props.todos.some(todo => todo.completed) 21 | if (atLeastOneCompleted) { 22 | this.props.actions.clearCompleted() 23 | } 24 | } 25 | 26 | handleShow(filter) { 27 | this.setState({ filter }) 28 | } 29 | 30 | renderToggleAll(completedCount) { 31 | const { todos, actions } = this.props 32 | if (todos.length > 0) { 33 | return 38 | } 39 | } 40 | 41 | renderFooter(completedCount) { 42 | const { todos } = this.props 43 | const { filter } = this.state 44 | const activeCount = todos.length - completedCount 45 | 46 | if (todos.length) { 47 | return ( 48 |