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 ( +
    + + + +
    + {this.props.todos.map(item => ( + + ))} +
    + +
    + ); + } +} \ 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