From 74c7e4f0fe1db9d69611f86045752ae6a47acdeb Mon Sep 17 00:00:00 2001 From: Oleg Polyakov Date: Thu, 9 Feb 2017 15:06:53 +0300 Subject: [PATCH 1/9] Start --- .gitignore | 41 ++++- README.md | 11 -- package.json | 22 +++ public/app.css | 293 +++++++++++++++++++++++++++++++++++ public/favicon.ico | Bin 0 -> 4286 bytes public/index.html | 17 ++ src/App.jsx | 97 ++++++++++++ src/components/Button.jsx | 23 +++ src/components/Checkbox.jsx | 18 +++ src/components/Form.jsx | 53 +++++++ src/components/Header.jsx | 20 +++ src/components/List.jsx | 34 ++++ src/components/Stats.jsx | 36 +++++ src/components/Stopwatch.jsx | 89 +++++++++++ src/components/Todo.jsx | 85 ++++++++++ src/index.jsx | 7 + src/todos.js | 24 +++ webpack.config.js | 34 ++++ 18 files changed, 892 insertions(+), 12 deletions(-) delete mode 100644 README.md create mode 100644 package.json create mode 100644 public/app.css create mode 100644 public/favicon.ico create mode 100644 public/index.html create mode 100644 src/App.jsx create mode 100644 src/components/Button.jsx create mode 100644 src/components/Checkbox.jsx create mode 100644 src/components/Form.jsx create mode 100644 src/components/Header.jsx create mode 100644 src/components/List.jsx create mode 100644 src/components/Stats.jsx create mode 100644 src/components/Stopwatch.jsx create mode 100644 src/components/Todo.jsx create mode 100644 src/index.jsx create mode 100644 src/todos.js create mode 100644 webpack.config.js diff --git a/.gitignore b/.gitignore index b512c09..2053430 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,40 @@ -node_modules \ No newline at end of file +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# VS Code +.vscode \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 9b93df6..0000000 --- a/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Основы Redux - -Исходный код для курса "Основы Redux" - ---- - -## Использование Redux - -```bash -git checkout using-redux -``` \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..4071d73 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "react-redux", + "version": "1.0.0", + "scripts": { + "start": "webpack-dev-server --inline --hot --content-base ./public", + "build": "webpack -p" + }, + "dependencies": { + "react": "15.4.2", + "react-dom": "15.4.2", + "redux": "3.6.0" + }, + "devDependencies": { + "babel-core": "6.21.0", + "babel-loader": "6.2.10", + "babel-preset-es2015": "6.22.0", + "babel-preset-react": "6.22.0", + "react-hot-loader": "1.3.1", + "webpack": "2.2.1", + "webpack-dev-server": "2.3.0" + } +} \ No newline at end of file diff --git a/public/app.css b/public/app.css new file mode 100644 index 0000000..4efdd4b --- /dev/null +++ b/public/app.css @@ -0,0 +1,293 @@ +body { + background-color: #fafafa; + color: #757575; + font-family: "Roboto", Arial, Helvetica, sans-serif; + margin: 0; +} + +button { + background: 0 0; + border: none; + border-radius: 2px; + color: #757575; + position: relative; + height: 36px; + margin: 0; + min-width: 64px; + padding: 0 16px; + display: inline-block; + font-family: "Roboto","Helvetica","Arial",sans-serif; + font-size: 14px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0; + overflow: hidden; + will-change: box-shadow; + transition: box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1); + outline: none; + cursor: pointer; + text-decoration: none; + text-align: center; + line-height: 36px; + vertical-align: middle; +} + +button:hover { + background-color: rgba(158,158,158,.2); +} + +button:active { + background-color: rgba(158,158,158,.4); +} + +button:disabled { + color: rgba(0, 0, 0, 0.26); + pointer-events: none; +} + +button.icon { + border-radius: 50%; + font-size: 24px; + height: 32px; + margin-left: 0; + margin-right: 0; + min-width: 32px; + width: 32px; + padding: 0; + overflow: hidden; + line-height: normal; +} + +button .material-icons { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-12px,-12px); + line-height: 24px; + width: 24px; + vertical-align: middle; +} + +input { + font-family: "Roboto", Arial, Helvetica, sans-serif; + font-size: 1rem; + color: #757575; + padding: .5em; + border-radius: 2px; + border: 1px solid lightgray; + outline: none; +} + +main { + background: #fff; + width: 700px; + margin: 50px auto; + border-radius: 2px; + overflow: hidden; + box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); +} + + + +/* Header */ +header { + display: flex; + align-items: center; + padding: 0 1rem; + color: white; + background-color: #222; + text-align: center; + text-transform: uppercase; +} + +header h1 { + display: inline-block; + color: #764abc; + margin: 1rem auto; +} + + + +/* Stats */ +.stats { + margin: 10px; +} + +.stats table { + margin: auto; +} + +.stats th { + text-align: right; + font-weight: normal; + letter-spacing: 2px; + font-size: .7em; +} + +.stats td { + text-align: left; + font-size: 1.5rem; + font-family: monospace; +} + + + +/* Stopwatch */ +.stopwatch { + padding: 15px 10px 5px 10px; +} + +.stopwatch-time { + font-family: monospace; + font-size: 2em; +} + +.stopwatch-controls button { + margin: 5px; + color: white; +} + + + +/* Filter */ +.todo-filter { + display: flex; +} + +.todo-filter a { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + height: 40px; + position: relative; + cursor: pointer; + transition: all .2s; +} + +.todo-filter a:hover { + background-color: rgba(158,158,158,.1); +} + +.todo-filter a.is-active i { + color: #764abc; +} + +.todo-filter a.is-active::after { + height: 2px; + width: 100%; + display: block; + content: " "; + bottom: -1px; + left: 0; + position: absolute; + background: #764abc; +} + + +/* Todo */ +.todo { + display: flex; + font-size: 1rem; + border-top: 1px solid rgba(0,0,0,.1); + background-color: #fff; + -webkit-user-select: none; + user-select: none; + padding: 1em; + align-items: center; +} + +.todo.completed * { + color: lightgray; + transition: color .2s; +} + +.todo.completed .todo-title { + text-decoration: line-through; +} + +.todo .todo-title { + margin-right: auto; +} + +.todo .checkbox { + margin-right: .5rem; +} + +.todo button:not(.checkbox) { + opacity: 0; + transition: all .2s; +} + +.todo:hover button:not(.checkbox) { + opacity: 1; +} + +.todo-edit-form { + display: flex; + font-size: 1rem; + padding: .85em; + border-top: 1px solid rgba(0,0,0,.1); +} + +.todo-edit-form input { + flex: 1; +} + +.todo-edit-form .save { + margin-left: .5em; +} + + + +/* Todo form */ +.todo-add-form { + display: flex; + background-color: #FAFAFA; + border-top: 1px solid rgba(0,0,0,.1); + padding: 10px; +} + +.todo-add-form input { + flex: 1; + outline: none; + transition: all .2s; +} + +.todo-add-form input:focus { + border: 1px solid #764abc; +} + +.todo-add-form button { + margin-left: 10px; +} + + + +/* Animations */ +.slide-enter { + transform: translateX(-100%); +} + +.slide-enter.slide-enter-active { + transform: translateX(0); + transition: all .5s; +} + +.slide-leave { + transform: translateX(0); +} + +.slide-leave.slide-leave-active { + transform: translateX(100%); + transition: all .5s; +} + +.slide-appear { + opacity: 0; +} + +.slide-appear.slide-appear-active { + opacity: 1; + transition: all .5s; +} \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f10af8fac99379dfe86989fa675af590e85eceab GIT binary patch literal 4286 zcmd7Udx%t39Ki9rO|_ZkxTKPqjZdstWzks^UvtQkf2P}7L)!H7{{q0__Mbluzcdp&2Ead%iPgEl^%bIaWj=<5VL$*BAAW!N!y07118e*M$ z=Qv-ZDVE3msZ3)X={P)uU$6&Da4fPhEBlxa<(|KD=*4o3fOQMuTIPr6eY^L24(;6c z3laC?b@HRE8|cq+xDTG~4$Of2{0_FD5mjA%Mm;>oM{z}Xj%zZVf;DgrkHd4S8pj9A zf>u)7dTt}J9v{N@r`*qc;JfFYw2$+ii&z)a(<%EV)H}zlygXRQgmUktXZ8%dFKh7; zYQ=w!I%7v&3DpLIM?mhOMGX5nPFm^?J`z=F^|0?xT@F~2f`!2iuY+c|u*W+3o6UwYR8}7UJ^Cpzn67!_)m9f5r@n*rbbfVn89CVOO z=;QsKjql+;`p#Qc)!XObWK!qvnRLMTa}n!JhroL|;O}W@>)E=;UGSUn8}5O9t769o z%LDI;@n3`Yz6G)FO!CXH27b%4wUFA!u?v`q=dl&e{|k7Jv-=j~)keuj*bVQ|Cftt#>|ZNnRe5OZTHZqmQ$pED()Eb(V`?nVb{jUp zcW55Q!F{P3f3OGJ<1j46pYXl5%=2!8XJTyc>Q$J57Mud-=-C-}6x_$bLRQt=K6r}M zIG$%L8%F+8ct$e-;4-&5l+h-HI~6WUEA-2%_T{TQrUzk2)N z40!(C7><7JDhsR++^;=&1)fD^v;E532j2G;*a^=#8^6{7q27D*BHXv@LRqaYHOpf^ z(mC*bT8Wx_NnKp~Ipnv)Z|Hc-|LLE7;cxmIa6ON~`&YFO@tM^3tsCzB&8TX3p!10R zNl(Hja3B2t-0AR+XXBH4#(tyXw`no?LGR1{%!hGe41b$E1HU!yulYr2gYkYuFJ8tN z{HwKB&Ml5bIvTfQ9sC{jOyu`yE#@PSgBE*$dDJn!-?e63jk)m7S@xgAt(?Qb&ZqqK zrxe6h2yzLalo@5#5X#%2_II&;O=h56#V2MWb&x(lV%#pX0u vn$}z2)6|mhoscwRf%=+y7W6fvpi-LVQNKA&+uG8!JC~-#`ZO&S44wWBOWFJ< literal 0 HcmV?d00001 diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..af63d6b --- /dev/null +++ b/public/index.html @@ -0,0 +1,17 @@ + + + + + Redux Todo + + + + + + + +
Загрузка...
+ + + + \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..19bd0e3 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,97 @@ +import React from 'react'; + +import Header from './components/Header'; +import List from './components/List'; +import Form from './components/Form'; + +class App extends React.Component { + constructor(props) { + super(props); + + this.state = { + todos: this.props.initialData + }; + + this._nextId = this.state.todos.length; + + this.handleAdd = this.handleAdd.bind(this); + this.handleDelete = this.handleDelete.bind(this); + this.handleToggle = this.handleToggle.bind(this); + this.handleEdit = this.handleEdit.bind(this); + } + + nextId() { + return this._nextId += 1; + } + + handleAdd(title) { + const todo = { + id: this.nextId(), + title, + completed: false + }; + + let todos = [...this.state.todos, todo]; + + this.setState({ todos }); + } + + handleDelete(id) { + const todos = this.state.todos.filter(todo => todo.id !== id); + + this.setState({ todos }); + } + + handleToggle(id) { + const todos = this.state.todos.map(todo => { + if (todo.id === id) { + todo.completed = !todo.completed; + } + + return todo; + }); + + this.setState({ todos }); + } + + handleEdit(id, title) { + const todos = this.state.todos.map(todo => { + if (todo.id === id) { + todo.title = title; + } + + return todo; + }); + + this.setState({ todos }); + } + + render() { + const todos = this.state.todos; + + return ( +
+
+ + + +
+
+ ); + } +} + +App.propTypes = { + initialData: React.PropTypes.arrayOf(React.PropTypes.shape({ + id: React.PropTypes.number.isRequired, + title: React.PropTypes.string.isRequired, + completed: React.PropTypes.bool.isRequired + })).isRequired +}; + +export default App; \ No newline at end of file diff --git a/src/components/Button.jsx b/src/components/Button.jsx new file mode 100644 index 0000000..a285b97 --- /dev/null +++ b/src/components/Button.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +function Button(props) { + return ( + + ); +} + +Button.propTypes = { + children: React.PropTypes.node, + className: React.PropTypes.string, + icon: React.PropTypes.string, + disabled: React.PropTypes.bool, + onClick: React.PropTypes.func +}; + +export default Button; \ No newline at end of file diff --git a/src/components/Checkbox.jsx b/src/components/Checkbox.jsx new file mode 100644 index 0000000..eff7c65 --- /dev/null +++ b/src/components/Checkbox.jsx @@ -0,0 +1,18 @@ +import React from 'react'; + +function Checkbox(props) { + return ( + + ); +} + +Checkbox.propTypes = { + checked: React.PropTypes.bool.isRequired, + onChange: React.PropTypes.func.isRequired +}; + +export default Checkbox; \ No newline at end of file diff --git a/src/components/Form.jsx b/src/components/Form.jsx new file mode 100644 index 0000000..5e9b7ae --- /dev/null +++ b/src/components/Form.jsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import Button from './Button'; + +class Form extends React.Component { + constructor(props) { + super(props); + + this.state = { + title: '' + }; + + this.handleSubmit = this.handleSubmit.bind(this); + this.handleChange = this.handleChange.bind(this); + } + + handleSubmit(event) { + event.preventDefault(); + + const title = this.state.title; + + if (title) { + this.props.onAdd(title); + this.setState({ title: '' }); + } + } + + handleChange(event) { + const title = event.target.value; + + this.setState({ title }); + } + + render() { + return ( + + + + + + ); + } +} + +Form.propTypes = { + onAdd: React.PropTypes.func.isRequired +}; + +export default Form; \ No newline at end of file diff --git a/src/components/Header.jsx b/src/components/Header.jsx new file mode 100644 index 0000000..ed30454 --- /dev/null +++ b/src/components/Header.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import Stats from './stats'; +import Stopwatch from './stopwatch'; + +function Header(props) { + return ( +
+ +

Redux Todo

+ +
+ ); +} + +Header.propTypes = { + todos: React.PropTypes.array.isRequired +}; + +export default Header; \ No newline at end of file diff --git a/src/components/List.jsx b/src/components/List.jsx new file mode 100644 index 0000000..e3ddcd2 --- /dev/null +++ b/src/components/List.jsx @@ -0,0 +1,34 @@ +import React from 'react'; + +import Todo from './Todo'; + +function List(props) { + return ( +
+ {props.todos.map(todo => + ) + } +
+ ); +} + +List.propTypes = { + todos: React.PropTypes.arrayOf(React.PropTypes.shape({ + id: React.PropTypes.number.isRequired, + title: React.PropTypes.string.isRequired, + completed: React.PropTypes.bool.isRequired + })).isRequired, + onDelete: React.PropTypes.func.isRequired, + onToggle: React.PropTypes.func.isRequired, + onEdit: React.PropTypes.func.isRequired +}; + +export default List; \ No newline at end of file diff --git a/src/components/Stats.jsx b/src/components/Stats.jsx new file mode 100644 index 0000000..22a8957 --- /dev/null +++ b/src/components/Stats.jsx @@ -0,0 +1,36 @@ +import React from 'react'; + +function Stats(props) { + let total = props.todos.length; + let completed = props.todos.filter(todo => todo.completed).length; + let uncompleted = total - completed; + + return ( + + + + + + + + + + + + + + + +
Всего задач:{total}
Выполнено:{completed}
Осталось:{uncompleted}
+ ); +} + +Stats.propTypes = { + todos: React.PropTypes.arrayOf(React.PropTypes.shape({ + id: React.PropTypes.number.isRequired, + title: React.PropTypes.string.isRequired, + completed: React.PropTypes.bool.isRequired + })).isRequired +}; + +export default Stats; \ No newline at end of file diff --git a/src/components/Stopwatch.jsx b/src/components/Stopwatch.jsx new file mode 100644 index 0000000..3636916 --- /dev/null +++ b/src/components/Stopwatch.jsx @@ -0,0 +1,89 @@ +import React from 'react'; + +import Button from './Button'; + +class Stopwatch extends React.Component { + constructor(props) { + super(props); + + this.state = { + running: false, + elapsed: 0, + lastTick: 0 + }; + + this.handleStart = this.handleStart.bind(this); + this.handlePause = this.handlePause.bind(this); + this.handleStop = this.handleStop.bind(this); + this.tick = this.tick.bind(this); + } + + componentDidMount() { + this.interval = setInterval(this.tick, 1000); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + tick() { + if (this.state.running) { + let now = Date.now(); + let diff = now - this.state.lastTick; + + this.setState({ + elapsed: this.state.elapsed + diff, + lastTick: now + }); + } + } + + handleStart() { + this.setState({ + running: true, + lastTick: Date.now() + }); + } + + handlePause() { + this.setState({ running: false }); + } + + handleStop() { + this.setState({ + running: false, + elapsed: 0, + lastTick: 0 + }); + } + + format(milliseconds) { + let totalSeconds = Math.floor(milliseconds / 1000); + let minutes = Math.floor(totalSeconds / 60); + let seconds = totalSeconds % 60; + + return `${minutes > 9 ? minutes : '0' + minutes}:${seconds > 9 ? seconds : '0' + seconds}`; + } + + render() { + let time = this.format(this.state.elapsed); + + return ( +
+
{time}
+ +
+ {this.state.running ? +
+
+ ); + } +} + +export default Stopwatch; \ No newline at end of file diff --git a/src/components/Todo.jsx b/src/components/Todo.jsx new file mode 100644 index 0000000..d96b317 --- /dev/null +++ b/src/components/Todo.jsx @@ -0,0 +1,85 @@ +import React from 'react'; + +import Checkbox from './Checkbox'; +import Button from './Button'; + +class Todo extends React.Component { + constructor(props) { + super(props); + + this.state = { + editing: false + }; + + this.handleSubmit = this.handleSubmit.bind(this); + this.handleDelete = this.handleDelete.bind(this); + this.handleToggle = this.handleToggle.bind(this); + this.handleEdit = this.handleEdit.bind(this); + } + + componentDidUpdate(prevProps, prevState) { + if (this.state.editing) { + this.refs.title.focus(); + } + } + + handleSubmit(event) { + event.preventDefault(); + + const title = this.refs.title.value; + + this.props.onEdit(this.props.id, title); + this.setState({ editing: false }); + } + + handleDelete() { + this.props.onDelete(this.props.id); + } + + handleToggle() { + this.props.onToggle(this.props.id); + } + + handleEdit() { + this.setState({ editing: true }); + } + + renderDisplay() { + const className = `todo${this.props.completed ? ' completed' : ''}`; + + return ( +
+ + + {this.props.title} + +
+ ); + } + + renderForm() { + return ( +
+ +