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/TodoItem/style.css:
--------------------------------------------------------------------------------
1 |
2 | .normal .toggle {
3 | text-align: center;
4 | width: 40px;
5 | /* auto, since non-WebKit browsers doesn't support input styling */
6 | height: auto;
7 | position: absolute;
8 | top: 0;
9 | bottom: 0;
10 | margin: auto 0;
11 | border: none; /* Mobile Safari */
12 | appearance: none;
13 | }
14 |
15 | .normal .toggle:after {
16 | content: url('data:image/svg+xml;utf8,');
17 | }
18 |
19 | .normal .toggle:checked:after {
20 | content: url('data:image/svg+xml;utf8,');
21 | }
22 |
23 | .normal label {
24 | white-space: pre-line;
25 | word-break: break-all;
26 | padding: 15px 60px 15px 15px;
27 | margin-left: 45px;
28 | display: block;
29 | line-height: 1.2;
30 | transition: color 0.4s;
31 | }
32 |
33 | .normal .destroy {
34 | display: none;
35 | position: absolute;
36 | top: 0;
37 | right: 10px;
38 | bottom: 0;
39 | width: 40px;
40 | height: 40px;
41 | margin: auto 0;
42 | font-size: 30px;
43 | color: #cc9a9a;
44 | margin-bottom: 11px;
45 | transition: color 0.2s ease-out;
46 | }
47 |
48 | .normal .destroy:hover {
49 | color: #af5b5e;
50 | }
51 |
52 | .normal .destroy:after {
53 | content: '×';
54 | }
55 |
56 | .normal:hover .destroy {
57 | display: block;
58 | }
59 |
60 | .normal .edit {
61 | display: none;
62 | }
63 |
64 | .editing {
65 | border-bottom: none;
66 | padding: 0;
67 | composes: normal;
68 | }
69 |
70 | .editing:last-child {
71 | margin-bottom: -1px;
72 | }
73 |
74 | .editing .edit {
75 | display: block;
76 | width: 506px;
77 | padding: 13px 17px 12px 17px;
78 | margin: 0 0 0 43px;
79 | }
80 |
81 | .editing .view {
82 | display: none;
83 | }
84 |
85 | .completed label {
86 | color: #d9d9d9;
87 | text-decoration: line-through;
88 | }
89 |
90 | /*
91 | Hack to remove background from Mobile Safari.
92 | Can't use it globally since it destroys checkboxes in Firefox
93 | */
94 | @media screen and (-webkit-min-device-pixel-ratio:0) {
95 | .normal .toggle {
96 | background: none;
97 | }
98 |
99 | .normal .toggle {
100 | height: 40px;
101 | }
102 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Boilerplate
6 |
7 |
8 |
9 |
10 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 |
2 | import { Router, Route, browserHistory } from 'react-router'
3 | import { syncHistoryWithStore } from 'react-router-redux'
4 | import { Provider } from 'react-redux'
5 | import ReactDOM from 'react-dom'
6 | import React from 'react'
7 |
8 | import App from './containers/App'
9 | import configure from './store'
10 |
11 | const store = configure()
12 | const history = syncHistoryWithStore(browserHistory, store)
13 |
14 | ReactDOM.render(
15 |
16 |
17 |
18 |
19 |
20 | ,
21 | document.getElementById('root')
22 | )
23 |
--------------------------------------------------------------------------------
/client/middleware/index.js:
--------------------------------------------------------------------------------
1 |
2 | import logger from './logger'
3 |
4 | export {
5 | logger
6 | }
--------------------------------------------------------------------------------
/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 |
6 | export default combineReducers({
7 | routing,
8 | todos
9 | })
10 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/client/store/index.js:
--------------------------------------------------------------------------------
1 |
2 | import { createStore, applyMiddleware } from 'redux'
3 |
4 | import { logger } 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 | )(create)
15 |
16 | const store = createStoreWithMiddleware(rootReducer, initialState)
17 |
18 | if (module.hot) {
19 | module.hot.accept('../reducers', () => {
20 | const nextReducer = require('../reducers')
21 | store.replaceReducer(nextReducer)
22 | })
23 | }
24 |
25 | return store
26 | }
27 |
--------------------------------------------------------------------------------
/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-runtime": "^6.5.2",
17 | "babel-preset-es2015": "^6.5.0",
18 | "babel-preset-react": "^6.5.0",
19 | "babel-preset-stage-0": "^6.5.0",
20 | "css-loader": "^0.23.1",
21 | "file-loader": "^0.8.5",
22 | "postcss-loader": "^0.8.1",
23 | "rucksack-css": "^0.8.5",
24 | "style-loader": "^0.13.0",
25 | "webpack": "^1.12.14",
26 | "webpack-dev-server": "^1.14.1",
27 | "webpack-hot-middleware": "^2.7.1",
28 | "babel-runtime": "^6.5.0",
29 | "classnames": "^2.2.3",
30 | "react": "^15.0.0",
31 | "react-dom": "^15.0.0",
32 | "react-hot-loader": "^1.3.0",
33 | "react-redux": "^4.4.0",
34 | "react-router": "^2.0.0",
35 | "react-router-redux": "^4.0.0",
36 | "redux": "^3.3.1",
37 | "redux-actions": "^0.9.1"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/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=[local]___[hash:base64:5]',
35 | 'postcss-loader'
36 | ]
37 | },
38 | {
39 | test: /\.css$/,
40 | exclude: /client/,
41 | loader: 'style!css'
42 | },
43 | {
44 | test: /\.(js|jsx)$/,
45 | exclude: /node_modules/,
46 | loaders: [
47 | 'react-hot',
48 | 'babel-loader'
49 | ]
50 | },
51 | ],
52 | },
53 | resolve: {
54 | extensions: ['', '.js', '.jsx']
55 | },
56 | postcss: [
57 | rucksack({
58 | autoprefixer: true
59 | })
60 | ],
61 | plugins: [
62 | new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js'),
63 | new webpack.DefinePlugin({
64 | 'process.env': { NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development') }
65 | })
66 | ],
67 | devServer: {
68 | contentBase: './client',
69 | hot: true
70 | }
71 | }
72 |
--------------------------------------------------------------------------------