diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..2a4a32d
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,6 @@
+{
+ "presets": [
+ "es2015",
+ "react"
+ ]
+}
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..ba8d9cb
--- /dev/null
+++ b/index.js
@@ -0,0 +1,16 @@
+/**
+ * Created by tema on 27.11.16.
+ */
+
+const express = require('express');
+
+const app = express();
+
+app.use('/', express.static(`${__dirname}/prod`));
+app.use('/css', express.static(`${__dirname}/prod/css`));
+app.use('/js', express.static(`${__dirname}/prod/js`));
+
+const port = process.env.PORT || 5000;
+app.listen(port, () => {
+ console.log(`Listening on ${port}`);
+});
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..21777ad
--- /dev/null
+++ b/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "react-homework",
+ "version": "1.0.0",
+ "description": "Реализовать todomvc из прошлого домашнего задания на чистом React",
+ "main": "index.js",
+ "dependencies": {
+ "express": "^4.14.0",
+ "react": "^15.4.1",
+ "react-dom": "^15.4.1",
+ "todomvc-app-css": "^2.0.6",
+ "todomvc-common": "^1.0.3",
+ "webpack": "^1.14.0",
+ "babel-core": "^6.20.0",
+ "babel-loader": "^6.2.9",
+ "babel-preset-es2015": "^6.18.0",
+ "babel-preset-react": "^6.16.0",
+ "css-loader": "^0.26.1",
+ "extract-text-webpack-plugin": "^1.0.1",
+ "file-loader": "^0.9.0",
+ "jsx-loader": "^0.13.2",
+ "react-hot-loader": "^1.3.1",
+ "style-loader": "^0.13.1"
+ },
+ "devDependencies": {
+ "webpack-dev-server": "^1.16.2"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "dev": "node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --hot --inline --history-api-fallback",
+ "postinstall": "webpack --config ./webpack-prod.config.js --progress --colors",
+ "start": "node index.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/artmaks/react-homework.git"
+ },
+ "author": "artem",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/artmaks/react-homework/issues"
+ },
+ "homepage": "https://github.com/artmaks/react-homework#readme"
+}
diff --git a/prod/index.html b/prod/index.html
new file mode 100644
index 0000000..6c43c0a
--- /dev/null
+++ b/prod/index.html
@@ -0,0 +1,13 @@
+
+
+
+ Your app name
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/Footer/Footer.css b/src/components/Footer/Footer.css
new file mode 100644
index 0000000..d73b8dc
--- /dev/null
+++ b/src/components/Footer/Footer.css
@@ -0,0 +1,74 @@
+.footer {
+ color: #777;
+ padding: 10px 15px;
+ height: 20px;
+ text-align: center;
+ border-top: 1px solid #e6e6e6;
+}
+
+.footer:before {
+ content: '';
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ height: 50px;
+ overflow: hidden;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
+ 0 8px 0 -3px #f6f6f6,
+ 0 9px 1px -3px rgba(0, 0, 0, 0.2),
+ 0 16px 0 -6px #f6f6f6,
+ 0 17px 2px -6px rgba(0, 0, 0, 0.2);
+}
+
+.todo-count {
+ float: left;
+ text-align: left;
+}
+
+.todo-count strong {
+ font-weight: 300;
+}
+
+.filters {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ position: absolute;
+ right: 0;
+ left: 0;
+}
+
+.filters li {
+ display: inline;
+}
+
+.filters li a {
+ color: inherit;
+ margin: 3px;
+ padding: 3px 7px;
+ text-decoration: none;
+ border: 1px solid transparent;
+ border-radius: 3px;
+}
+
+.filters li a:hover {
+ border-color: rgba(175, 47, 47, 0.1);
+}
+
+.filters li a.selected {
+ border-color: rgba(175, 47, 47, 0.2);
+}
+
+.clear-completed,
+html .clear-completed:active {
+ float: right;
+ position: relative;
+ line-height: 20px;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.clear-completed:hover {
+ text-decoration: underline;
+}
diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js
new file mode 100644
index 0000000..d4ddbe6
--- /dev/null
+++ b/src/components/Footer/Footer.js
@@ -0,0 +1,39 @@
+import React, {Component} from 'react';
+require("./Footer.css");
+
+
+export default class Footer extends Component {
+
+ showAll() {
+ this.props.setFilter('all');
+ }
+
+ showActive() {
+ this.props.setFilter('active');
+ }
+
+ showCompleted() {
+ this.props.setFilter('completed');
+ }
+
+ render() {
+ return (
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/components/Header/Header.css b/src/components/Header/Header.css
new file mode 100644
index 0000000..2ae1ec2
--- /dev/null
+++ b/src/components/Header/Header.css
@@ -0,0 +1,12 @@
+.header h1 {
+ position: absolute;
+ top: -155px;
+ width: 100%;
+ font-size: 100px;
+ font-weight: 100;
+ text-align: center;
+ color: rgba(175, 47, 47, 0.15);
+ -webkit-text-rendering: optimizeLegibility;
+ -moz-text-rendering: optimizeLegibility;
+ text-rendering: optimizeLegibility;
+}
\ No newline at end of file
diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js
new file mode 100644
index 0000000..2c16657
--- /dev/null
+++ b/src/components/Header/Header.js
@@ -0,0 +1,15 @@
+import React, { Component } from 'react';
+import LargeInput from './../LargeInput/LargeInput'
+require("./Header.css");
+
+
+export default class Header extends Component {
+ render() {
+ return (
+
+
todos
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/components/LargeInput/LargeInput.css b/src/components/LargeInput/LargeInput.css
new file mode 100644
index 0000000..8d47e8a
--- /dev/null
+++ b/src/components/LargeInput/LargeInput.css
@@ -0,0 +1,25 @@
+.new-todo,
+.edit {
+ position: relative;
+ margin: 0;
+ width: 100%;
+ font-size: 24px;
+ font-family: inherit;
+ font-weight: inherit;
+ line-height: 1.4em;
+ border: 0;
+ color: inherit;
+ padding: 6px;
+ border: 1px solid #999;
+ box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
+ box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.new-todo {
+ padding: 16px 16px 16px 60px;
+ border: none;
+ background: rgba(0, 0, 0, 0.003);
+ box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
+}
\ No newline at end of file
diff --git a/src/components/LargeInput/LargeInput.js b/src/components/LargeInput/LargeInput.js
new file mode 100644
index 0000000..379bf88
--- /dev/null
+++ b/src/components/LargeInput/LargeInput.js
@@ -0,0 +1,17 @@
+import React, { Component } from 'react';
+require("./LargeInput.css");
+
+export default class LargeInput extends Component {
+
+ AddTodoKeyPress(e) {
+ if (e.keyCode == 13) {
+ this.props.onAddTodo(e);
+ }
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/components/Todo/Todo.css b/src/components/Todo/Todo.css
new file mode 100644
index 0000000..81ef9a7
--- /dev/null
+++ b/src/components/Todo/Todo.css
@@ -0,0 +1,97 @@
+.todo-list li {
+ position: relative;
+ font-size: 24px;
+ border-bottom: 1px solid #ededed;
+}
+
+.todo-list li:last-child {
+ border-bottom: none;
+}
+
+.todo-list li.editing {
+ border-bottom: none;
+ padding: 0;
+}
+
+.todo-list li.editing .edit {
+ display: block;
+ width: 506px;
+ padding: 12px 16px;
+ margin: 0 0 0 43px;
+}
+
+.todo-list li.editing .view {
+ display: none;
+}
+
+.todo-list li .toggle {
+ text-align: center;
+ width: 40px;
+ /* auto, since non-WebKit browsers doesn't support input styling */
+ height: auto;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ margin: auto 0;
+ border: none; /* Mobile Safari */
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+.todo-list li .toggle:after {
+ content: url('data:image/svg+xml;utf8,');
+}
+
+.todo-list li .toggle:checked:after {
+ content: url('data:image/svg+xml;utf8,');
+}
+
+.todo-list li label {
+ word-break: break-all;
+ padding: 15px 60px 15px 15px;
+ margin-left: 45px;
+ display: block;
+ line-height: 1.2;
+ transition: color 0.4s;
+}
+
+.todo-list li.completed label {
+ color: #d9d9d9;
+ text-decoration: line-through;
+}
+
+.todo-list li .destroy {
+ display: none;
+ position: absolute;
+ top: 0;
+ right: 10px;
+ bottom: 0;
+ width: 40px;
+ height: 40px;
+ margin: auto 0;
+ font-size: 30px;
+ color: #cc9a9a;
+ margin-bottom: 11px;
+ transition: color 0.2s ease-out;
+}
+
+.todo-list li .destroy:hover {
+ color: #af5b5e;
+ cursor: pointer;
+}
+
+.todo-list li .destroy:after {
+ content: '×';
+}
+
+.todo-list li:hover .destroy {
+ display: block;
+}
+
+.todo-list li .edit {
+ display: none;
+}
+
+.todo-list li.editing:last-child {
+ margin-bottom: -1px;
+}
\ No newline at end of file
diff --git a/src/components/Todo/Todo.js b/src/components/Todo/Todo.js
new file mode 100644
index 0000000..9d13fa6
--- /dev/null
+++ b/src/components/Todo/Todo.js
@@ -0,0 +1,43 @@
+import React, {Component} from 'react';
+require("./Todo.css");
+
+export default class Todo extends Component {
+
+ toggleTodo(e) {
+ this.props.onToggleTodo(e, this.props.id);
+ }
+
+ deleteTodo(e) {
+ this.props.onDeleteItem(e, this.props.id);
+ }
+
+ editTodo(e) {
+ this.props.editTodo(e, this.props.id);
+ }
+
+ saveTodo(e) {
+ if (e.keyCode == 13) {
+ this.props.saveTodo(e, this.props.id, e.target.value);
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ var node = this.refs.editField;
+ node.focus();
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/components/TodoApp/TodoApp.css b/src/components/TodoApp/TodoApp.css
new file mode 100644
index 0000000..6d62595
--- /dev/null
+++ b/src/components/TodoApp/TodoApp.css
@@ -0,0 +1,102 @@
+html,
+body {
+ margin: 0;
+ padding: 0;
+ background-color: #f5f5f5;
+}
+
+body {
+ font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ line-height: 1.4em;
+ background: #f5f5f5;
+ color: #4d4d4d;
+ min-width: 230px;
+ max-width: 550px;
+ margin: 0 auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-weight: 300;
+}
+
+button {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: none;
+ font-size: 100%;
+ vertical-align: baseline;
+ font-family: inherit;
+ font-weight: inherit;
+ color: inherit;
+ -webkit-appearance: none;
+ appearance: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+
+.todoapp {
+ background: #fff;
+ margin: 130px 0 40px 0;
+ position: relative;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
+ 0 25px 50px 0 rgba(0, 0, 0, 0.1);
+}
+
+:focus {
+ outline: 0;
+}
+
+.hidden {
+ display: none;
+}
+
+.todoapp input::-webkit-input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp input::-moz-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp input::input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+/*
+ Hack to remove background from Mobile Safari.
+ Can't use it globally since it destroys checkboxes in Firefox
+*/
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ .toggle-all,
+ .todo-list li .toggle {
+ background: none;
+ }
+
+ .todo-list li .toggle {
+ height: 40px;
+ }
+
+ .toggle-all {
+ -webkit-transform: rotate(90deg);
+ transform: rotate(90deg);
+ -webkit-appearance: none;
+ appearance: none;
+ }
+}
+
+@media (max-width: 430px) {
+ .footer {
+ height: 50px;
+ }
+
+ .filters {
+ bottom: 10px;
+ }
+}
\ No newline at end of file
diff --git a/src/components/TodoApp/TodoApp.js b/src/components/TodoApp/TodoApp.js
new file mode 100644
index 0000000..8a97466
--- /dev/null
+++ b/src/components/TodoApp/TodoApp.js
@@ -0,0 +1,176 @@
+import React, {Component} from 'react';
+import Header from './../Header/Header'
+import TodoList from './../TodoList/TodoList'
+import Footer from './../Footer/Footer'
+import guid from './../../utils/GUID';
+require("./TodoApp.css");
+
+export default class Todo extends Component {
+
+ constructor(props) {
+ super(props);
+
+ const loadState = JSON.parse(localStorage.getItem('todos'));
+ this.state = loadState || {
+ todos: [],
+ inputText: "",
+ toggledAll: false,
+ itemsLeft: 0,
+ filter: 'all'
+ };
+
+ this.onTextChange = this.onTextChange.bind(this);
+ this.onAddTodo = this.onAddTodo.bind(this);
+ this.onToggleTodo = this.onToggleTodo.bind(this);
+ this.onDeleteItem = this.onDeleteItem.bind(this);
+ this.onToggleAll = this.onToggleAll.bind(this);
+ this.setFilter = this.setFilter.bind(this);
+ this.clearCompleted = this.clearCompleted.bind(this);
+ this.editTodo = this.editTodo.bind(this);
+ this.saveTodo = this.saveTodo.bind(this);
+ }
+
+ onTextChange(event) {
+ this.setState({
+ inputText: event.target.value
+ }, this.saveState);
+ }
+
+ onAddTodo(event) {
+ const newTodo = {
+ title: event.target.value,
+ completed: false,
+ id: guid(),
+ editable: false
+ };
+ this.setState((prevState) => ({
+ todos: prevState.todos.concat(newTodo),
+ inputText: "",
+ toggledAll: false
+ }), this.checkToggles);
+ }
+
+ onToggleTodo(event, id) {
+ const updatedItems = this.state.todos.map(todo => {
+ if (id === todo.id)
+ todo.completed = event.target.checked;
+ return todo;
+ });
+ this.setState({
+ todos: [].concat(updatedItems),
+ }, this.checkToggles);
+ }
+
+ onDeleteItem(event, id) {
+ var updatedItems = this.state.todos.filter(todo => {
+ return todo.id !== id;
+ });
+ this.setState({
+ todos: [].concat(updatedItems),
+ }, this.checkToggles);
+ }
+
+ onToggleAll(event) {
+ const updatedItems = this.state.todos.map(todo => {
+ todo.completed = !this.state.toggledAll;
+ return todo;
+ });
+ this.setState({
+ todos: [].concat(updatedItems),
+ }, this.checkToggles);
+ }
+
+ checkToggles() {
+ const itemsLeft = this.state.todos.filter(todo => {
+ return todo.completed !== true;
+ }).length;
+ this.setState({
+ toggledAll: this.state.todos.length !== 0 && itemsLeft === 0,
+ itemsLeft: itemsLeft
+ }, this.saveState);
+ }
+
+ setFilter(filter) {
+ this.setState({
+ filter: filter
+ }, this.saveState);
+ }
+
+ filterTodos() {
+ switch (this.state.filter) {
+ case 'all':
+ return this.state.todos;
+ case 'active':
+ return this.state.todos.filter(todo => {
+ return todo.completed !== true
+ });
+ case 'completed':
+ return this.state.todos.filter(todo => {
+ return todo.completed === true
+ });
+ }
+ }
+
+ clearCompleted() {
+ var updatedItems = this.state.todos.filter(todo => {
+ return todo.completed !== true;
+ });
+ this.setState({
+ todos: [].concat(updatedItems),
+ }, this.checkToggles);
+ }
+
+ saveState() {
+ localStorage.setItem('todos', JSON.stringify(this.state));
+ }
+
+ editTodo(event, id) {
+ const updatedItems = this.state.todos.map(todo => {
+ if (id === todo.id)
+ todo.editable = true;
+ else
+ todo.editable = false;
+ return todo;
+ });
+ this.setState({
+ todos: [].concat(updatedItems),
+ }, this.checkToggles);
+ }
+
+ saveTodo(event, id, text) {
+ const updatedItems = this.state.todos.map(todo => {
+ if (id === todo.id) {
+ todo.title = text;
+ todo.editable = false;
+ }
+ return todo;
+ });
+ this.setState({
+ todos: [].concat(updatedItems),
+ }, this.checkToggles);
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/components/TodoList/TodoList.css b/src/components/TodoList/TodoList.css
new file mode 100644
index 0000000..75298e0
--- /dev/null
+++ b/src/components/TodoList/TodoList.css
@@ -0,0 +1,40 @@
+.main {
+ position: relative;
+ z-index: 2;
+ border-top: 1px solid #e6e6e6;
+}
+
+/*Toggle all checkbox*/
+
+label[for='toggle-all'] {
+ display: none;
+}
+
+.toggle-all {
+ position: absolute;
+ top: -55px;
+ left: -12px;
+ width: 60px;
+ height: 34px;
+ text-align: center;
+ border: none; /* Mobile Safari */
+}
+
+.toggle-all:before {
+ content: '❯';
+ font-size: 22px;
+ color: #e6e6e6;
+ padding: 10px 27px 10px 27px;
+}
+
+.toggle-all:checked:before {
+ color: #737373;
+}
+
+/*Todo list*/
+
+.todo-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
\ No newline at end of file
diff --git a/src/components/TodoList/TodoList.js b/src/components/TodoList/TodoList.js
new file mode 100644
index 0000000..52a69d3
--- /dev/null
+++ b/src/components/TodoList/TodoList.js
@@ -0,0 +1,29 @@
+import React, {Component} from 'react';
+import Todo from './../Todo/Todo'
+require("./TodoList.css");
+
+export default class TodoList extends Component {
+ render() {
+ return (
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/js/app.js b/src/js/app.js
new file mode 100644
index 0000000..50704f9
--- /dev/null
+++ b/src/js/app.js
@@ -0,0 +1,8 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import TodoApp from '../components/TodoApp/TodoApp';
+
+ReactDOM.render(
+ ,
+ document.getElementById('app')
+);
\ No newline at end of file
diff --git a/src/utils/GUID.js b/src/utils/GUID.js
new file mode 100644
index 0000000..36f8378
--- /dev/null
+++ b/src/utils/GUID.js
@@ -0,0 +1,16 @@
+/**
+ * Created by tema on 05.12.16.
+ */
+
+function guid() {
+ function s4() {
+ return Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
+ }
+
+ return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
+ s4() + '-' + s4() + s4() + s4();
+}
+
+module.exports = guid;
diff --git a/webpack-prod.config.js b/webpack-prod.config.js
new file mode 100644
index 0000000..38952a1
--- /dev/null
+++ b/webpack-prod.config.js
@@ -0,0 +1,39 @@
+var ExtractTextPlugin = require("extract-text-webpack-plugin");
+
+module.exports = {
+ entry: {
+ javascript: "./src/js/app.js",
+ html: "./prod/index.html",
+ },
+
+ output: {
+ filename: "/js/app.js",
+ path: "./prod",
+ },
+
+ resolve: {
+ extensions: ['', '.js', '.jsx', '.json']
+ },
+
+ module: {
+ loaders: [
+ {
+ test: /\.jsx?$/,
+ exclude: /node_modules/,
+ loaders: ["react-hot", "babel-loader"],
+ },
+ {
+ test: /\.html$/,
+ loader: "file?name=[name].[ext]",
+ },
+ {
+ test: /\.css$/,
+ loader: ExtractTextPlugin.extract("style-loader", "css-loader")
+ }
+ ]
+ },
+ // Use the plugin to specify the resulting filename (and add needed behavior to the compiler)
+ plugins: [
+ new ExtractTextPlugin("./css/styles.css")
+ ]
+};
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..ed0c574
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,39 @@
+var ExtractTextPlugin = require("extract-text-webpack-plugin");
+
+module.exports = {
+ entry: {
+ javascript: "./src/js/app.js",
+ html: "./prod/index.html",
+ },
+
+ output: {
+ filename: "app.js",
+ path: "./prod",
+ },
+
+ resolve: {
+ extensions: ['', '.js', '.jsx', '.json']
+ },
+
+ module: {
+ loaders: [
+ {
+ test: /\.jsx?$/,
+ exclude: /node_modules/,
+ loaders: ["react-hot", "babel-loader"],
+ },
+ {
+ test: /\.html$/,
+ loader: "file?name=[name].[ext]",
+ },
+ {
+ test: /\.css$/,
+ loader: ExtractTextPlugin.extract("style-loader", "css-loader")
+ }
+ ]
+ },
+ // Use the plugin to specify the resulting filename (and add needed behavior to the compiler)
+ plugins: [
+ new ExtractTextPlugin("./styles.css")
+ ]
+};
\ No newline at end of file