├── .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 |
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 |
53 | )
54 | }
55 | }
56 |
57 | render() {
58 | const { todos, actions } = this.props
59 | const { filter } = this.state
60 |
61 | const filteredTodos = todos.filter(TODO_FILTERS[filter])
62 | const completedCount = todos.reduce((count, todo) => {
63 | return todo.completed ? count + 1 : count
64 | }, 0)
65 |
66 | return (
67 |
68 | {this.renderToggleAll(completedCount)}
69 |
70 | {filteredTodos.map(todo =>
71 |
72 | )}
73 |
74 | {this.renderFooter(completedCount)}
75 |
76 | )
77 | }
78 | }
79 |
80 | export default MainSection
81 |
--------------------------------------------------------------------------------
/client/components/TodoItem/style.css:
--------------------------------------------------------------------------------
1 | .normal .toggle {
2 | text-align: center;
3 | width: 40px;
4 | /* auto, since non-WebKit browsers doesn't support input styling */
5 | height: auto;
6 | position: absolute;
7 | top: 0;
8 | bottom: 0;
9 | margin: auto 0;
10 | border: none; /* Mobile Safari */
11 | appearance: none;
12 | }
13 |
14 | .normal .toggle:after {
15 | content: url('data:image/svg+xml;utf8,');
16 | }
17 |
18 | .normal .toggle:checked:after {
19 | content: url('data:image/svg+xml;utf8,');
20 | }
21 |
22 | .normal label {
23 | white-space: pre-line;
24 | word-break: break-all;
25 | padding: 15px 60px 15px 15px;
26 | margin-left: 45px;
27 | display: block;
28 | line-height: 1.2;
29 | transition: color 0.4s;
30 | }
31 |
32 | .normal .destroy {
33 | display: none;
34 | position: absolute;
35 | top: 0;
36 | right: 10px;
37 | bottom: 0;
38 | width: 40px;
39 | height: 40px;
40 | margin: auto 0;
41 | font-size: 30px;
42 | color: #cc9a9a;
43 | margin-bottom: 11px;
44 | transition: color 0.2s ease-out;
45 | }
46 |
47 | .normal .destroy:hover {
48 | color: #af5b5e;
49 | }
50 |
51 | .normal .destroy:after {
52 | content: '×';
53 | }
54 |
55 | .normal:hover .destroy {
56 | display: block;
57 | }
58 |
59 | .normal .edit {
60 | display: none;
61 | }
62 |
63 | .editing {
64 | border-bottom: none;
65 | padding: 0;
66 | composes: normal;
67 | }
68 |
69 | .editing:last-child {
70 | margin-bottom: -1px;
71 | }
72 |
73 | .editing .edit {
74 | display: block;
75 | width: 506px;
76 | padding: 13px 17px 12px 17px;
77 | margin: 0 0 0 43px;
78 | }
79 |
80 | .editing .view {
81 | display: none;
82 | }
83 |
84 | .completed label {
85 | color: #d9d9d9;
86 | text-decoration: line-through;
87 | }
88 |
89 | /*
90 | Hack to remove background from Mobile Safari.
91 | Can't use it globally since it destroys checkboxes in Firefox
92 | */
93 | @media screen and (-webkit-min-device-pixel-ratio:0) {
94 | .normal .toggle {
95 | background: none;
96 | }
97 |
98 | .normal .toggle {
99 | height: 40px;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/client/components/MainSection/style.css:
--------------------------------------------------------------------------------
1 | .main {
2 | position: relative;
3 | z-index: 2;
4 | border-top: 1px solid #e6e6e6;
5 | }
6 |
7 | .normal {
8 | margin: 0;
9 | padding: 0;
10 | list-style: none;
11 | }
12 |
13 | .normal li {
14 | position: relative;
15 | font-size: 24px;
16 | border-bottom: 1px solid #ededed;
17 | }
18 |
19 | .normal li:last-child {
20 | border-bottom: none;
21 | }
22 |
23 | .normal li.editing {
24 | border-bottom: none;
25 | padding: 0;
26 | }
27 |
28 | .normal li.editing .edit {
29 | display: block;
30 | width: 506px;
31 | padding: 13px 17px 12px 17px;
32 | margin: 0 0 0 43px;
33 | }
34 |
35 | .normal li.editing .view {
36 | display: none;
37 | }
38 |
39 | .normal li .toggle {
40 | text-align: center;
41 | width: 40px;
42 | /* auto, since non-WebKit browsers doesn't support input styling */
43 | height: auto;
44 | position: absolute;
45 | top: 0;
46 | bottom: 0;
47 | margin: auto 0;
48 | border: none; /* Mobile Safari */
49 | appearance: none;
50 | }
51 |
52 | .normal li .toggle:after {
53 | content: url('data:image/svg+xml;utf8,');
54 | }
55 |
56 | .normal li .toggle:checked:after {
57 | content: url('data:image/svg+xml;utf8,');
58 | }
59 |
60 | .normal li label {
61 | white-space: pre-line;
62 | word-break: break-all;
63 | padding: 15px 60px 15px 15px;
64 | margin-left: 45px;
65 | display: block;
66 | line-height: 1.2;
67 | transition: color 0.4s;
68 | }
69 |
70 | .normal li.completed label {
71 | color: #d9d9d9;
72 | text-decoration: line-through;
73 | }
74 |
75 | .normal li .destroy {
76 | display: none;
77 | position: absolute;
78 | top: 0;
79 | right: 10px;
80 | bottom: 0;
81 | width: 40px;
82 | height: 40px;
83 | margin: auto 0;
84 | font-size: 30px;
85 | color: #cc9a9a;
86 | margin-bottom: 11px;
87 | transition: color 0.2s ease-out;
88 | }
89 |
90 | .normal li .destroy:hover {
91 | color: #af5b5e;
92 | }
93 |
94 | .normal li .destroy:after {
95 | content: '×';
96 | }
97 |
98 | .normal li:hover .destroy {
99 | display: block;
100 | }
101 |
102 | .normal li .edit {
103 | display: none;
104 | }
105 |
106 | .normal li.editing:last-child {
107 | margin-bottom: -1px;
108 | }
109 |
110 | .toggleAll {
111 | position: absolute;
112 | top: -55px;
113 | left: -12px;
114 | width: 60px;
115 | height: 34px;
116 | text-align: center;
117 | border: none; /* Mobile Safari */
118 | }
119 |
120 | .toggleAll:before {
121 | content: '❯';
122 | font-size: 22px;
123 | color: #e6e6e6;
124 | padding: 10px 27px 10px 27px;
125 | }
126 |
127 | .toggleAll:checked:before {
128 | color: #737373;
129 | }
130 |
131 | /*
132 | Hack to remove background from Mobile Safari.
133 | Can't use it globally since it destroys checkboxes in Firefox
134 | */
135 | @media screen and (-webkit-min-device-pixel-ratio:0) {
136 | .toggleAll,
137 | .normal li .toggle {
138 | background: none;
139 | }
140 |
141 | .normal li .toggle {
142 | height: 40px;
143 | }
144 |
145 | .toggleAll {
146 | transform: rotate(90deg);
147 | appearance: none;
148 | }
149 | }
150 |
--------------------------------------------------------------------------------