diff --git a/.babelrc b/.babelrc
new file mode 100755
index 0000000..d0962f5
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,8 @@
+{
+ "presets": ["es2015", "react"],
+ "env": {
+ "development": {
+ "presets": ["react-hmre"]
+ }
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..816863f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.idea
+/*.env
+node_modules
+public/
+dist/
+static/
+npm-debug.log
+libs/
\ No newline at end of file
diff --git a/README.md b/README.md
deleted file mode 100644
index 4b53e96..0000000
--- a/README.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# Задача к лекциям «REST» и «Touch» – «TODOхи»
-
-В отзывах вы писали нам, что задачи стали простые и однобокие,
-и мы решили подготовить сложную и многобокую задачу, а именно – реализовать
-целый сервис «TODOхи» для ведения списка задач.
-
-Сервис должен быть ориентирован на touch и выглядеть следующим образом:
-
-
-
-Задачи прелагаем хранить в памяти на сервере. Клиент общается с ним
-асинхронными запросами (включая получение списка задач), соблюдая REST.
-
-**Внимание!** Мы знаем о существовании клёвых библиотек для работы с xmlhttprequest,
-свайпами, тач-событиями и прочим, но настаиваем на выполнении задания без них (даже без «джиквери»).
-
-**Внимание!** Мы будем счастливы, если вы положите решение в Heroku, чтобы нам было удобнее проверять.
-
-При сдвиге (swipe) задачи влево появляется иконка удаления,
-по нажатию на которую на сервер отправляется запрос на удаление,
-и по факту удаления, задача исчезает из списка.
-
-
-
-При коротком нажатии (tap) на задачу вместо неё появляется поле для редактирования
-с кнопкой «Сохранить». По нажатию на кнопку, на сервер отправляется запрос
-с отредактированной задачей.
-
-
-
-При сдвиге списка задач вниз, появляется иконка загрузки и на сервер отправляется
-запрос за новыми задачами (pull-and-refresh). По факту выполнения запроса,
-новые задачи добавляются в начало списка.
-
-То есть, вы можете открыть две вкладки с вашим приложением в браузере. В одной
-добавить задачу, а в другой выполнить pull-and-refresh и увидеть только что добавленную.
-
-
-
-# Дополнительное задание
-
-В рамках дополнительного задания предлагаем реализовать сортировку задач.
-При длительном нажатии (long tap) на задачу она всплывает над остальными
-и появляется возможность перемещать (drag-n-drop) её вверх и вниз.
-
-
-
-После того как пользователь отпустил задачу, приложение должно сохранить новый
-порядок заметок.
-
-
diff --git a/actions/index.js b/actions/index.js
new file mode 100755
index 0000000..4882a3b
--- /dev/null
+++ b/actions/index.js
@@ -0,0 +1,226 @@
+import * as types from '../constants/ActionTypes'
+import fetch from 'isomorphic-fetch'
+// require('es6-promise').polyfill();
+
+// export function addTodo(text) {
+// return {type: types.ADD_TODO, text}
+// }
+
+export function verticalStart(target) {
+ return {
+ type: types.VERTICAL_STARTED,
+ verticalSwipe: {
+ state: true,
+ target
+ }
+ }
+}
+
+export function verticalMove(target, offset) {
+ return {
+ type: types.VERTICAL_MOVED,
+ verticalSwipe: {
+ state: true,
+ target,
+ offset: offset[0]
+ }
+ }
+}
+
+export function verticalStop(target, offset) {
+ return {
+ type: types.VERTICAL_STOPPED,
+ verticalSwipe: {
+ state: false,
+ target,
+ offset: offset[0]
+ }
+ }
+}
+
+export function horizontalStart(target) {
+ return {
+ type: types.HORIZONTAL_STARTED,
+ horizontalSwipe: {
+ state: true,
+ target
+ }
+ }
+}
+
+export function horizontalMove(target, offset) {
+ return {
+ type: types.HORIZONTAL_MOVED,
+ horizontalSwipe: {
+ state: true,
+ target,
+ offset: offset[0]
+ }
+ }
+}
+
+export function horizontalStop(target, offset) {
+ return {
+ type: types.HORIZONTAL_STOPPED,
+ horizontalSwipe: {
+ state: false,
+ target,
+ offset: offset[0]
+ }
+ }
+}
+
+export function tap(target) {
+ return {
+ type: types.TAPPED,
+ tap: {
+ target
+ }
+ }
+}
+
+export function todoAdded(json) {
+ return {
+ type: types.TODO_ADDED,
+ status: json.status,
+ todo: json.userTodo
+ }
+}
+
+export function addTodo(text) {
+ return dispatch => {
+ return fetch('/api/todos', {
+ credentials: 'same-origin',
+ method: 'post',
+ headers: {
+ "Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
+ },
+ body: `text=${text}`
+ })
+ .then(function (response) {
+ return response.json();
+ })
+ .then(json => {
+ dispatch(todoAdded(json));
+ })
+ }
+}
+
+// export function deleteTodo(id) {
+// return {type: types.DELETE_TODO, id}
+// }
+
+export function todoDeleted(json) {
+ return {
+ type: types.TODO_DELETED,
+ status: json.status,
+ todo: json.userTodo
+ }
+}
+
+export function deleteTodo(id) {
+ return dispatch => {
+ return fetch(`/api/todos/${id}`, {
+ credentials: 'same-origin',
+ method: 'delete'
+ })
+ .then(function (response) {
+ return response.json();
+ })
+ .then(json => {
+ dispatch(todoDeleted(json));
+ })
+ }
+}
+
+// export function editTodo(id, text) {
+// return {type: types.EDIT_TODO, id, text}
+// }
+
+export function todoEdited(json) {
+ return {
+ type: types.TODO_EDITED,
+ status: json.status,
+ todo: json.userTodo
+ }
+}
+
+export function editTodo(id, text) {
+ return dispatch => {
+ return fetch(`/api/todos/${id}`, {
+ credentials: 'same-origin',
+ method: 'put',
+ headers: {
+ "Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
+ },
+ mode: 'cors',
+ body: `text=${text}`
+ })
+ .then(function (response) {
+ return response.json();
+ })
+ .then(json => {
+ dispatch(todoEdited(json));
+ })
+ }
+}
+
+// export function invalidateReddit(reddit) {
+// return {
+// type: INVALIDATE_REDDIT,
+// reddit
+// }
+// }
+
+function requestTodos() {
+ return {
+ type: types.REQUEST_TODOS
+ }
+}
+
+function receiveTodos(json) {
+ return {
+ type: types.RECEIVE_TODOS,
+ status: json.status, // ok | failed
+ todos: json.userTodo,
+ receivedAt: Date.now()
+ }
+}
+
+export function fetchTodos() {
+ return dispatch => {
+ dispatch(requestTodos());
+ return fetch(`/api/todos/`, {
+ credentials: 'same-origin'
+ })
+ .then(response => {
+ return response.json();
+ })
+ .then(json => {
+ console.log(json);
+ return json;
+ })
+ .then(json => {
+ dispatch(receiveTodos(json));
+ })
+ }
+}
+
+// function shouldFetchTodos(state) {
+// const posts = state.postsByReddit[reddit];
+// if (!posts) {
+// return true
+// }
+// if (posts.isFetching) {
+// return false
+// }
+// return posts.didInvalidate
+// }
+//
+// export function fetchTodosIfNeeded() {
+// return (dispatch, getState) => {
+// if (shouldFetchTodos(getState())) {
+// return dispatch(fetchTodos())
+// }
+// }
+// }
diff --git a/components/Footer.js b/components/Footer.js
new file mode 100755
index 0000000..8603c09
--- /dev/null
+++ b/components/Footer.js
@@ -0,0 +1,28 @@
+import React, {PropTypes, Component} from 'react'
+import TodoTextInput from './TodoTextInput'
+import classnames from 'classnames'
+
+class Footer extends Component {
+ handleSave(text) {
+ if (text.length !== 0) {
+ this.props.addTodo(text);
+ }
+ }
+ render() {
+ return (
+
+ )
+ }
+}
+
+Footer.propTypes = {
+ addTodo: PropTypes.func.isRequired
+};
+
+export default Footer
diff --git a/components/Header.js b/components/Header.js
new file mode 100755
index 0000000..6bc0530
--- /dev/null
+++ b/components/Header.js
@@ -0,0 +1,12 @@
+import React, {PropTypes, Component} from 'react'
+
+class Header extends Component {
+ render() {
+ return (
+
+ )
+ }
+}
+export default Header
diff --git a/components/MainSection.js b/components/MainSection.js
new file mode 100755
index 0000000..0d03b8e
--- /dev/null
+++ b/components/MainSection.js
@@ -0,0 +1,28 @@
+import React, {Component, PropTypes} from 'react'
+import TodoItem from './TodoItem'
+
+class MainSection extends Component {
+ constructor(props, context) {
+ super(props, context);
+ }
+
+ render() {
+ const {todos, swipe, actions} = this.props;
+
+ return (
+
+ {todos.map(todo =>
+
+ )}
+
+ )
+ }
+}
+
+MainSection.propTypes = {
+ todos: PropTypes.array.isRequired,
+ swipe: PropTypes.object.isRequired,
+ actions: PropTypes.object.isRequired
+};
+
+export default MainSection
diff --git a/components/TodoItem.js b/components/TodoItem.js
new file mode 100755
index 0000000..c29eb19
--- /dev/null
+++ b/components/TodoItem.js
@@ -0,0 +1,172 @@
+import React, {Component, PropTypes} from 'react'
+import classnames from 'classnames'
+import TodoTextInput from './TodoTextInput'
+
+class TodoItem extends Component {
+ constructor(props, context) {
+ super(props, context);
+ this.state = {
+ editing: false,
+
+ swipedLeft: false,
+ startMovingLeft: false,
+ startMovingRight: false,
+ moveOffset: 0,
+ startOffset: 0
+ };
+ this.startPoint = {};
+ this.toDelete;
+ }
+
+ componentWillMount() {
+ //React.initializeTouchEvents(true);
+ }
+
+ handleClick() {
+ const {todo, swipe} = this.props;
+
+ if (this.state.moveOffset === 0) {
+ this.setState({editing: true})
+ }
+ }
+
+ handleSave(id, text) {
+ if (text.length === 0) {
+ this.props.deleteTodo(id)
+ } else {
+ this.props.editTodo(id, text)
+ }
+ this.setState({editing: false})
+ }
+
+ handleTrashBox() {
+ const {todo, swipe} = this.props;
+ if (this.state.moveOffset === -100) {
+ if (swipe.tap.target.className === 'todo') {
+ this.props.deleteTodo(this.props.todo.id);
+ }
+ }
+ }
+
+ handleTouchStart(event) {
+ }
+
+ handleTouchMove(event) {
+ const {todo, swipe} = this.props;
+ if (swipe.horizontalSwipe.state) {
+ let newSwipePos = parseInt(this.state.startOffset, 10) + swipe.horizontalSwipe.offset;
+ console.log(this.state.startOffset, this.state.moveOffset, swipe.horizontalSwipe.offset, newSwipePos);
+ if (newSwipePos <= -100) {
+ this.setState({
+ moveOffset: -100,
+ startOffset: -100
+ });
+ } else if (newSwipePos > 0) {
+ this.setState({
+ moveOffset: 0,
+ startOffset: 0
+ });
+ } else {
+ this.setState({
+ moveOffset: newSwipePos
+ });
+ }
+ }
+ }
+
+ componentDidUpdate(nextProps, nextState) {
+ const {todo, swipe} = this.props;
+
+ // Проверим, тапал ли кто-нибудь глобально
+ if (swipe.tap.target) {
+ this.handleTrashBox.bind(this)();
+ }
+
+ // Если начали скролить - закрываем корзину
+ if (swipe.verticalSwipe.state) {
+ if (this.state.moveOffset === -100) {
+ this.setState({
+ moveOffset: 0,
+ startOffset: 0
+ });
+ }
+ }
+ }
+
+ handleTouchEnd(event) {
+ const {todo, swipe} = this.props;
+ //this.handleTrashBox.bind(this)(event);
+ if (this.state.moveOffset != -100 && this.state.moveOffset != 0) {
+ let newSwipePos = parseInt(this.state.startOffset, 10) + swipe.horizontalSwipe.offset;
+ if (Math.abs(Math.abs(this.state.moveOffset) - 100) > Math.abs(this.state.moveOffset)) {
+ this.setState({
+ moveOffset: 0,
+ startOffset: 0
+ });
+ } else {
+ this.setState({
+ moveOffset: -100,
+ startOffset: -100
+ });
+ }
+ }
+ }
+
+ render() {
+ const {todo, deleteTodo} = this.props;
+
+ let element;
+ let todoTextId = `todo__text-${todo.id}`;
+ let imgId = `todo__trashbox-${todo.id}`;
+ let itemId = `todo__item-${todo.id}`;
+ let todoId = `todo-${todo.id}`;
+
+ var todoItemClass = classnames({
+ 'todo__item': true
+ });
+ var todoTextClass = classnames({
+ 'todo__text': true
+ });
+ if (this.state.editing) {
+ element = (
+
+ this.handleSave(todo.id, text)}
+ todoId={todoId}
+ />
+
+ )
+ } else {
+ element = (
+
+ {todo.text}
+
+ )
+ }
+ return (
+
+
+
+ {element}
+
+
+ )
+ }
+}
+
+TodoItem.propTypes = {
+ todo: PropTypes.object.isRequired,
+ swipe: PropTypes.object.isRequired,
+ editTodo: PropTypes.func.isRequired,
+ deleteTodo: PropTypes.func.isRequired
+};
+
+export default TodoItem
diff --git a/components/TodoTextInput.js b/components/TodoTextInput.js
new file mode 100755
index 0000000..7e29d5b
--- /dev/null
+++ b/components/TodoTextInput.js
@@ -0,0 +1,65 @@
+import React, {Component, PropTypes} from 'react'
+import classnames from 'classnames'
+
+class TodoTextInput extends Component {
+ constructor(props, context) {
+ super(props, context);
+ this.state = {
+ text: this.props.text || ''
+ }
+ }
+
+ handleSubmit(e) {
+ const text = this.state.text.trim();
+ this.props.onSave(text);
+ if (this.props.newTodo) {
+ this.setState({text: ''})
+ }
+ // const text = e.target.value.trim();
+ // if (e.which === 13) {
+ // this.props.onSave(text);
+ // if (this.props.newTodo) {
+ // this.setState({text: ''})
+ // }
+ // }
+ }
+
+ handleChange(e) {
+ this.setState({text: e.target.value})
+ }
+
+ render() {
+ let buttonText = this.props.newTodo ? 'Добавить' : 'Изменить';
+ let editTextId = `edit-form-text-${this.props.todoId}`;
+ let editButId = `edit-form-but-${this.props.todoId}`;
+ return (
+
+ )
+ }
+}
+
+TodoTextInput.propTypes = {
+ onSave: PropTypes.func.isRequired,
+ text: PropTypes.string,
+ placeholder: PropTypes.string,
+ editing: PropTypes.bool,
+ newTodo: PropTypes.bool,
+ todoId: PropTypes.string
+};
+
+export default TodoTextInput
diff --git a/constants/ActionTypes.js b/constants/ActionTypes.js
new file mode 100755
index 0000000..e7bc52d
--- /dev/null
+++ b/constants/ActionTypes.js
@@ -0,0 +1,18 @@
+export const ADD_TODO = 'ADD_TODO';
+export const DELETE_TODO = 'DELETE_TODO';
+export const EDIT_TODO = 'EDIT_TODO';
+
+export const REQUEST_TODOS = 'REQUEST_TODOS';
+export const RECEIVE_TODOS = 'RECEIVE_TODOS';
+
+export const TODO_ADDED = 'TODO_ADDED';
+export const TODO_DELETED = 'TODO_DELETED';
+export const TODO_EDITED = 'TODO_EDITED';
+
+export const VERTICAL_STARTED = 'VERTICAL_STARTED';
+export const VERTICAL_MOVED = 'VERTICAL_MOVED';
+export const VERTICAL_STOPPED = 'VERTICAL_STOPPED';
+export const HORIZONTAL_STARTED = 'HORIZONTAL_STARTED';
+export const HORIZONTAL_MOVED = 'HORIZONTAL_MOVED';
+export const HORIZONTAL_STOPPED = 'HORIZONTAL_STOPPED';
+export const TAPPED = 'TAPPED';
\ No newline at end of file
diff --git a/containers/App.js b/containers/App.js
new file mode 100755
index 0000000..09af85d
--- /dev/null
+++ b/containers/App.js
@@ -0,0 +1,193 @@
+import React, {Component, PropTypes} from 'react'
+import {bindActionCreators} from 'redux'
+import {connect} from 'react-redux'
+import Header from '../components/Header'
+import Footer from '../components/Footer'
+import MainSection from '../components/MainSection'
+import * as TodoActions from '../actions'
+import {fetchTodos} from '../actions'
+import classnames from 'classnames'
+
+class App extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ pulling: false,
+ pullPixels: 0,
+ spinning: false
+ };
+ this.startPoint = {};
+ }
+
+ componentDidMount() {
+ const {todos, actions, dispatch} = this.props;
+ dispatch(fetchTodos());
+ }
+
+ handleTouchStart(event) {
+ // console.log(event.currentTarget);
+ // console.log(event.target);
+
+ this.startPoint.x = event.changedTouches[0].pageX;
+ this.startPoint.y = event.changedTouches[0].pageY;
+ this.ldelay = new Date();
+ this.startTarget = event.target;
+ console.log('handleTouchStart');
+ }
+
+ handleTouchMove(event) {
+ //event.preventDefault(); // Можно конечно, но тогда писать свой скролл
+ //event.stopPropagation();
+ const {todos, swipe, actions, dispatch} = this.props;
+ let nowPoint = event.changedTouches[0];
+ //console.log(nowPoint.pageX, nowPoint.pageY);
+ var offset = {
+ x: [nowPoint.pageX - this.startPoint.x],
+ y: [nowPoint.pageY - this.startPoint.y]
+ };
+ if (swipe.verticalSwipe.state) {
+ actions.verticalMove(event.target, offset.y);
+
+ // Дальше код, который относится к локальной обработке pull-to-refresh
+ if (swipe.verticalSwipe.offset > 0) {
+ this.setState({
+ pullPixels: swipe.verticalSwipe.offset
+ });
+ if (swipe.verticalSwipe.offset >= 100) {
+ this.setState({
+ spinning: true,
+ pullPixels: 100
+ })
+ }
+ }
+ }
+ if (swipe.horizontalSwipe.state) {
+ actions.horizontalMove(event.target, offset.x);
+ }
+ if (Math.abs(offset.y) > 20 || Math.abs(offset.x) > 20) {
+ if (Math.abs(offset.y) > Math.abs(offset.x)) {
+ if (!swipe.verticalSwipe.state && !swipe.horizontalSwipe.state) {
+ actions.verticalStart(event.target);
+ }
+ } else {
+ if (!swipe.verticalSwipe.state && !swipe.horizontalSwipe.state) {
+ actions.horizontalStart(event.target);
+ }
+ }
+ }
+ }
+
+
+ handleTouchEnd(event) {
+ const {todos, swipe, actions} = this.props;
+ console.log('handleTouchEnd');
+ let nowPoint = event.changedTouches[0];
+ let delay = new Date();
+ var offset = {
+ x: [nowPoint.pageX - this.startPoint.x],
+ y: [nowPoint.pageY - this.startPoint.y]
+ };
+
+ if (swipe.verticalSwipe.state) {
+ actions.verticalStop(event.target, offset.y);
+ }
+ if (swipe.horizontalSwipe.state) {
+ actions.horizontalStop(event.target, offset.x);
+ }
+ // Уже точно не свайп - проверим на tap
+ if (delay - this.ldelay < 300) {
+ if (this.startTarget == event.target) {
+ //console.log('Eq target');
+ actions.tap(event.target);
+ }
+ }
+ // Дальше код, который относится к локальной обработке pull-to-refresh
+ if (this.state.pullPixels > 0) {
+ this.setState({
+ pulling: false,
+ pullPixels: 100,
+ spinning: true
+ });
+ this.props.dispatch(fetchTodos())
+ .then(
+ this.setState({
+ pulling: false,
+ pullPixels: 0,
+ spinning: false
+ })
+ );
+ }
+ }
+
+ render() {
+ const {todos, swipe, actions} = this.props;
+ let spinner;
+ var spinnerClass = classnames({
+ 'todo__refresh': true,
+ 'animate': this.state.spinning
+ });
+ if (this.state.pullPixels > 15) {
+ if (this.state.spinning) {
+ spinner = (
+
+
+ )
+ } else {
+ spinner = (
+
+
+ )
+ }
+ }
+ let space = (
+
+
+ );
+ return (
+
+ {spinner}
+ {space}
+
+
+
+ )
+ }
+}
+
+App.propTypes = {
+ todos: PropTypes.array.isRequired,
+ swipe: PropTypes.object.isRequired,
+ actions: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired
+};
+
+function mapStateToProps(state) {
+ return {
+ todos: state.todos,
+ swipe: state.swipe
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators(TodoActions, dispatch),
+ dispatch: dispatch
+ }
+}
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(App)
diff --git a/index.js b/index.js
new file mode 100755
index 0000000..bd0ea95
--- /dev/null
+++ b/index.js
@@ -0,0 +1,15 @@
+import 'babel-polyfill'
+import React from 'react'
+import {render} from 'react-dom'
+import {Provider} from 'react-redux'
+import App from './containers/App'
+import configureStore from './store/configureStore'
+
+const store = configureStore();
+
+render(
+
+
+ ,
+ document.getElementById('root')
+);
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..0eb4fbe
--- /dev/null
+++ b/package.json
@@ -0,0 +1,62 @@
+{
+ "name": "webdev-tasks-5",
+ "version": "2.0.0",
+ "description": "Todo",
+ "scripts": {
+ "start": "node server.js",
+ "test": "cross-env NODE_ENV=test mocha --recursive --compilers js:babel-register --require ./test/setup.js",
+ "test:watch": "npm test -- --watch"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/danmir/webdev-tasks-5.git"
+ },
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/danmir/webdev-tasks-5/issues"
+ },
+ "homepage": "https://github.com/danmir/webdev-tasks-5#readme",
+ "dependencies": {
+ "babel-polyfill": "^6.3.14",
+ "body-parser": "^1.15.0",
+ "classnames": "^2.1.2",
+ "cookie-parser": "^1.4.1",
+ "css-loader": "^0.23.1",
+ "es6-promise": "^3.1.2",
+ "extract-text-webpack-plugin": "^1.0.1",
+ "file-loader": "^0.8.5",
+ "isomorphic-fetch": "^2.2.1",
+ "morgan": "^1.7.0",
+ "react": "^0.14.7",
+ "react-dom": "^0.14.7",
+ "react-redux": "^4.2.1",
+ "redux": "^3.2.1",
+ "redux-logger": "^2.6.1",
+ "redux-thunk": "^2.0.1",
+ "serve-static": "^1.10.2",
+ "style-loader": "^0.12.4",
+ "stylus": "^0.54.2",
+ "stylus-loader": "^2.0.0"
+ },
+ "devDependencies": {
+ "babel-core": "^6.3.15",
+ "babel-loader": "^6.2.0",
+ "babel-preset-es2015": "^6.3.13",
+ "babel-preset-react": "^6.3.13",
+ "babel-preset-react-hmre": "^1.1.1",
+ "babel-register": "^6.3.13",
+ "cross-env": "^1.0.7",
+ "expect": "^1.8.0",
+ "express": "^4.13.3",
+ "jsdom": "^5.6.1",
+ "mocha": "^2.2.5",
+ "node-libs-browser": "^0.5.2",
+ "raw-loader": "^0.5.1",
+ "react-addons-test-utils": "^0.14.7",
+ "style-loader": "^0.12.3",
+ "todomvc-app-css": "^2.0.1",
+ "webpack": "^1.9.11",
+ "webpack-dev-middleware": "^1.2.0",
+ "webpack-hot-middleware": "^2.9.1"
+ }
+}
diff --git a/reducers/index.js b/reducers/index.js
new file mode 100755
index 0000000..b7bf060
--- /dev/null
+++ b/reducers/index.js
@@ -0,0 +1,10 @@
+import {combineReducers} from 'redux'
+import todos from './todos'
+import swipe from './swipe'
+
+const rootReducer = combineReducers({
+ todos,
+ swipe
+});
+
+export default rootReducer
diff --git a/reducers/swipe.js b/reducers/swipe.js
new file mode 100644
index 0000000..0d67b66
--- /dev/null
+++ b/reducers/swipe.js
@@ -0,0 +1,145 @@
+import {VERTICAL_STARTED, VERTICAL_MOVED, VERTICAL_STOPPED, HORIZONTAL_STARTED, HORIZONTAL_MOVED, HORIZONTAL_STOPPED, TAPPED} from '../constants/ActionTypes'
+
+const initialState = {
+ verticalSwipe: {
+ state: false,
+ target: null,
+ offset: 0
+ },
+ horizontalSwipe: {
+ state: false,
+ target: null,
+ offset: 0
+ },
+ tap: {
+ target: null
+ }
+};
+
+export default function swipe(state = initialState, action) {
+ switch (action.type) {
+ case VERTICAL_STARTED:
+ return {
+ verticalSwipe: {
+ state: true,
+ target: action.verticalSwipe.target,
+ offset: 0
+ },
+ horizontalSwipe: {
+ state: false,
+ target: null,
+ offset: 0
+ },
+ tap: {
+ target: null
+ }
+ };
+
+ case VERTICAL_MOVED:
+ return {
+ verticalSwipe: {
+ state: true,
+ target: action.verticalSwipe.target,
+ offset: action.verticalSwipe.offset
+ },
+ horizontalSwipe: {
+ state: false,
+ target: null,
+ offset: 0
+ },
+ tap: {
+ target: null
+ }
+ };
+
+ case VERTICAL_STOPPED:
+ return {
+ verticalSwipe: {
+ state: false,
+ target: action.verticalSwipe.target,
+ offset: action.verticalSwipe.offset
+ },
+ horizontalSwipe: {
+ state: false,
+ target: null,
+ offset: 0
+ },
+ tap: {
+ target: null
+ }
+ };
+
+ case HORIZONTAL_STARTED:
+ return {
+ verticalSwipe: {
+ state: false,
+ target: null,
+ offset: 0
+ },
+ horizontalSwipe: {
+ state: true,
+ target: action.horizontalSwipe.target,
+ offset: 0
+ },
+ tap: {
+ target: null
+ }
+ };
+
+ case HORIZONTAL_MOVED:
+ return {
+ verticalSwipe: {
+ state: false,
+ target: null,
+ offset: 0
+ },
+ horizontalSwipe: {
+ state: true,
+ target: action.horizontalSwipe.target,
+ offset: action.horizontalSwipe.offset
+ },
+ tap: {
+ target: null
+ }
+ };
+
+ case HORIZONTAL_STOPPED:
+ return {
+ verticalSwipe: {
+ state: false,
+ target: null,
+ offset: 0
+ },
+ horizontalSwipe: {
+ state: false,
+ target: action.horizontalSwipe.target,
+ offset: action.horizontalSwipe.offset
+ },
+ tap: {
+ target: null
+ }
+ };
+
+ case TAPPED:
+ return {
+ verticalSwipe: {
+ state: false,
+ target: null,
+ offset: 0
+ },
+ horizontalSwipe: {
+ state: false,
+ target: null,
+ offset: 0
+ },
+ tap: {
+ target: action.tap.target
+ }
+ };
+
+ default:
+ return state
+ }
+}
+
+
diff --git a/reducers/todos.js b/reducers/todos.js
new file mode 100755
index 0000000..f812f2a
--- /dev/null
+++ b/reducers/todos.js
@@ -0,0 +1,74 @@
+import {TODO_ADDED, TODO_DELETED, TODO_EDITED, RECEIVE_TODOS} from '../constants/ActionTypes'
+
+const initialState = [
+ // {
+ // id: 0,
+ // text: 'Use Redux',
+ // createdAt: 1459448312865
+ // }
+];
+
+export default function todos(state = initialState, action) {
+ switch (action.type) {
+ case TODO_ADDED:
+ return [
+ ...state,
+ {
+ id: action.todo.id,
+ text: action.todo.text,
+ createdAt: action.todo.createdAt
+ }
+ ];
+
+ case TODO_DELETED:
+ if (action.status === 'ok') {
+ console.log(action);
+ return state.filter(todo =>
+ todo.id !== action.todo.id
+ );
+ } else {
+ return state
+ }
+
+ case TODO_EDITED:
+ if (action.status === 'ok') {
+ return state.map(todo =>
+ todo.id === action.todo.id ?
+ Object.assign({}, todo, {text: action.todo.text}) :
+ todo
+ );
+ } else {
+ return state
+ }
+
+ case RECEIVE_TODOS:
+ if (action.status === 'ok') {
+ var arr = [];
+ for (var key in action.todos) {
+ arr.push(Object.assign({}, action.todos[key], {id: key}));
+ }
+ return arr;
+ } else {
+ return state
+ }
+
+ // case COMPLETE_TODO:
+ // return state.map(todo =>
+ // todo.id === action.id ?
+ // Object.assign({}, todo, {completed: !todo.completed}) :
+ // todo
+ // );
+ //
+ // case COMPLETE_ALL:
+ // const areAllMarked = state.every(todo => todo.completed)
+ // return state.map(todo => Object.assign({}, todo, {
+ // completed: !areAllMarked
+ // }));
+ //
+ // case CLEAR_COMPLETED:
+ // return state.filter(todo => todo.completed === false)
+
+ default:
+ return state
+ }
+}
diff --git a/server.js b/server.js
new file mode 100755
index 0000000..65f0e95
--- /dev/null
+++ b/server.js
@@ -0,0 +1,62 @@
+var webpack = require('webpack');
+var webpackDevMiddleware = require('webpack-dev-middleware');
+var webpackHotMiddleware = require('webpack-hot-middleware');
+var config = require('./webpack.config');
+
+var serveStatic = require('serve-static');
+
+const morgan = require('morgan');
+const bodyParser = require('body-parser');
+var cookieParser = require('cookie-parser');
+
+var app = new (require('express'))();
+var port = 3000;
+
+var compiler = webpack(config);
+app.use(webpackDevMiddleware(compiler, {noInfo: true, publicPath: config.output.publicPath}));
+app.use(webpackHotMiddleware(compiler));
+
+app.use(morgan('dev'));
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({
+ extended: false
+}));
+
+app.use(cookieParser());
+app.use(require('./server/userToken'));
+
+app.use(function(req, res, next) {
+ // res.header("Access-Control-Allow-Origin", "*");
+ res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
+ res.header('Access-Control-Allow-Methods', "GET,PUT,POST,DELETE");
+ next();
+});
+
+app.use((req, res, next) => {
+ req.commonData = {
+ meta: {
+ description: 'TODO',
+ charset: 'utf-8'
+ },
+ page: {
+ title: 'TODO'
+ },
+ isDev: process.env.NODE_ENV === 'development'
+ };
+
+ next();
+});
+require('./server/routes/routes')(app);
+
+// app.get("/", function (req, res) {
+// res.sendFile(__dirname + '/index.html')
+// });
+
+app.listen(port, function (error) {
+ if (error) {
+ console.error(error)
+ } else {
+ console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
+ }
+});
+module.exports = app;
diff --git a/server/blocks/todo-app/__header/todo-app__header.styl b/server/blocks/todo-app/__header/todo-app__header.styl
new file mode 100644
index 0000000..9b3af05
--- /dev/null
+++ b/server/blocks/todo-app/__header/todo-app__header.styl
@@ -0,0 +1,3 @@
+.todo-app__header
+ display: flex
+ justify-content: center
\ No newline at end of file
diff --git a/server/blocks/todo-app/todo-app.styl b/server/blocks/todo-app/todo-app.styl
new file mode 100644
index 0000000..4cf480f
--- /dev/null
+++ b/server/blocks/todo-app/todo-app.styl
@@ -0,0 +1,21 @@
+@import url('https://fonts.googleapis.com/css?family=Open+Sans:300&subset=latin,cyrillic');
+
+html
+ height: 100%
+
+body
+ height: 100%
+ font-family: 'Open Sans', sans-serif;
+
+#root
+ height: 100%
+ padding-top: 10px
+
+.app-container
+ height: 100%
+
+.todo-app
+ display: flex
+ flex-direction: column
+ align-items: center
+ justify-content: center
diff --git a/server/blocks/todo/__add-form/todo__add-form.styl b/server/blocks/todo/__add-form/todo__add-form.styl
new file mode 100644
index 0000000..527821f
--- /dev/null
+++ b/server/blocks/todo/__add-form/todo__add-form.styl
@@ -0,0 +1,7 @@
+.todo__add-form
+ display: flex
+ flex-flow: row wrap
+ align-items: center
+ justify-content: center
+ width: 100%
+ margin: 10px 30px 10px 30px
diff --git a/server/blocks/todo/__edit-form/todo__edit-form.styl b/server/blocks/todo/__edit-form/todo__edit-form.styl
new file mode 100644
index 0000000..07dcad4
--- /dev/null
+++ b/server/blocks/todo/__edit-form/todo__edit-form.styl
@@ -0,0 +1,22 @@
+.todo__edit-form
+ //z-index: 1
+ //height: 100%
+ //width: 100%
+ //display: block
+ //margin: 0 auto
+ //min-height: 100px
+ //background-color: #eaeaea
+
+ //display: flex
+ //flex-flow: row wrap
+ //align-items: center
+ //justify-content: center
+ //width: 100%
+ //margin: 10px 30px 10px 30px
+
+ display: flex
+ margin: 0 auto
+ align-items: center
+ justify-content: center
+ height: 100px
+ background-color: #eaeaea
diff --git a/server/blocks/todo/__item/todo__item.styl b/server/blocks/todo/__item/todo__item.styl
new file mode 100644
index 0000000..29ae161
--- /dev/null
+++ b/server/blocks/todo/__item/todo__item.styl
@@ -0,0 +1,20 @@
+.todo__item
+ z-index: 1
+ height: 100%
+ width: 100%
+ display: block
+ margin: 0 auto
+ min-height: 100px
+ background-color: #eaeaea
+
+.todo__item.animate-left
+ transform: translate(-100px);
+ transition-property: transform;
+ transition-duration: 0.4s;
+ transition-timing-function: linear;
+
+.todo__item.animate-right
+ transform: translate(0);
+ transition-property: transform ;
+ transition-duration: 0.4s;
+ transition-timing-function: linear;
diff --git a/server/blocks/todo/__refresh/todo__refresh.styl b/server/blocks/todo/__refresh/todo__refresh.styl
new file mode 100644
index 0000000..d66a996
--- /dev/null
+++ b/server/blocks/todo/__refresh/todo__refresh.styl
@@ -0,0 +1,12 @@
+.todo__refresh
+ display: flex;
+ margin: 0 auto;
+ align-items: center;
+ justify-content: center;
+ height: 30px;
+ width: 30px;
+
+.todo__refresh.animate
+ animation: spin 1s linear infinite;
+
+@keyframes spin {100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
\ No newline at end of file
diff --git a/server/blocks/todo/__text/todo__text.styl b/server/blocks/todo/__text/todo__text.styl
new file mode 100644
index 0000000..4c7e6d9
--- /dev/null
+++ b/server/blocks/todo/__text/todo__text.styl
@@ -0,0 +1,7 @@
+.todo__text
+ display: flex;
+ margin: 0 auto;
+ align-items: center;
+ justify-content: center;
+ height: 100px;
+ font-size: 20px
\ No newline at end of file
diff --git a/server/blocks/todo/__trashbox/todo__trashbox.styl b/server/blocks/todo/__trashbox/todo__trashbox.styl
new file mode 100644
index 0000000..a4278f1
--- /dev/null
+++ b/server/blocks/todo/__trashbox/todo__trashbox.styl
@@ -0,0 +1,7 @@
+.todo__trashbox
+ float: right
+ z-index: -1
+ position: relative
+ width: 100px
+ height: 100px
+ margin-left: -100px
diff --git a/server/blocks/todo/todo.styl b/server/blocks/todo/todo.styl
new file mode 100644
index 0000000..ab0006b
--- /dev/null
+++ b/server/blocks/todo/todo.styl
@@ -0,0 +1,9 @@
+.todo
+ //display: flex
+ max-width: 500px
+ width: 100%
+ box-sizing: border-box
+ min-height: 100px
+ min-width: 150px
+ max-height: 100px
+ margin: 10px 0 10px 0
diff --git a/server/bundles/page/es6-promise.min.js b/server/bundles/page/es6-promise.min.js
new file mode 100644
index 0000000..f26f3c8
--- /dev/null
+++ b/server/bundles/page/es6-promise.min.js
@@ -0,0 +1,9 @@
+/*!
+ * @overview es6-promise - a tiny implementation of Promises/A+.
+ * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
+ * @license Licensed under MIT license
+ * See https://raw.githubusercontent.com/jakearchibald/es6-promise/master/LICENSE
+ * @version 3.2.1
+ */
+
+(function(){"use strict";function t(t){return"function"==typeof t||"object"==typeof t&&null!==t}function e(t){return"function"==typeof t}function n(t){G=t}function r(t){Q=t}function o(){return function(){process.nextTick(a)}}function i(){return function(){B(a)}}function s(){var t=0,e=new X(a),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2}}function u(){var t=new MessageChannel;return t.port1.onmessage=a,function(){t.port2.postMessage(0)}}function c(){return function(){setTimeout(a,1)}}function a(){for(var t=0;J>t;t+=2){var e=tt[t],n=tt[t+1];e(n),tt[t]=void 0,tt[t+1]=void 0}J=0}function f(){try{var t=require,e=t("vertx");return B=e.runOnLoop||e.runOnContext,i()}catch(n){return c()}}function l(t,e){var n=this,r=new this.constructor(p);void 0===r[rt]&&k(r);var o=n._state;if(o){var i=arguments[o-1];Q(function(){x(o,r,i,n._result)})}else E(n,r,t,e);return r}function h(t){var e=this;if(t&&"object"==typeof t&&t.constructor===e)return t;var n=new e(p);return g(n,t),n}function p(){}function _(){return new TypeError("You cannot resolve a promise with itself")}function d(){return new TypeError("A promises callback cannot return that same promise.")}function v(t){try{return t.then}catch(e){return ut.error=e,ut}}function y(t,e,n,r){try{t.call(e,n,r)}catch(o){return o}}function m(t,e,n){Q(function(t){var r=!1,o=y(n,e,function(n){r||(r=!0,e!==n?g(t,n):S(t,n))},function(e){r||(r=!0,j(t,e))},"Settle: "+(t._label||" unknown promise"));!r&&o&&(r=!0,j(t,o))},t)}function b(t,e){e._state===it?S(t,e._result):e._state===st?j(t,e._result):E(e,void 0,function(e){g(t,e)},function(e){j(t,e)})}function w(t,n,r){n.constructor===t.constructor&&r===et&&constructor.resolve===nt?b(t,n):r===ut?j(t,ut.error):void 0===r?S(t,n):e(r)?m(t,n,r):S(t,n)}function g(e,n){e===n?j(e,_()):t(n)?w(e,n,v(n)):S(e,n)}function A(t){t._onerror&&t._onerror(t._result),T(t)}function S(t,e){t._state===ot&&(t._result=e,t._state=it,0!==t._subscribers.length&&Q(T,t))}function j(t,e){t._state===ot&&(t._state=st,t._result=e,Q(A,t))}function E(t,e,n,r){var o=t._subscribers,i=o.length;t._onerror=null,o[i]=e,o[i+it]=n,o[i+st]=r,0===i&&t._state&&Q(T,t)}function T(t){var e=t._subscribers,n=t._state;if(0!==e.length){for(var r,o,i=t._result,s=0;si;i++)e.resolve(t[i]).then(n,r)}:function(t,e){e(new TypeError("You must pass an array to race."))})}function F(t){var e=this,n=new e(p);return j(n,t),n}function D(){throw new TypeError("You must pass a resolver function as the first argument to the promise constructor")}function K(){throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.")}function L(t){this[rt]=O(),this._result=this._state=void 0,this._subscribers=[],p!==t&&("function"!=typeof t&&D(),this instanceof L?C(this,t):K())}function N(t,e){this._instanceConstructor=t,this.promise=new t(p),this.promise[rt]||k(this.promise),Array.isArray(e)?(this._input=e,this.length=e.length,this._remaining=e.length,this._result=new Array(this.length),0===this.length?S(this.promise,this._result):(this.length=this.length||0,this._enumerate(),0===this._remaining&&S(this.promise,this._result))):j(this.promise,U())}function U(){return new Error("Array Methods must be provided an Array")}function W(){var t;if("undefined"!=typeof global)t=global;else if("undefined"!=typeof self)t=self;else try{t=Function("return this")()}catch(e){throw new Error("polyfill failed because global object is unavailable in this environment")}var n=t.Promise;(!n||"[object Promise]"!==Object.prototype.toString.call(n.resolve())||n.cast)&&(t.Promise=pt)}var z;z=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)};var B,G,H,I=z,J=0,Q=function(t,e){tt[J]=t,tt[J+1]=e,J+=2,2===J&&(G?G(a):H())},R="undefined"!=typeof window?window:void 0,V=R||{},X=V.MutationObserver||V.WebKitMutationObserver,Z="undefined"==typeof self&&"undefined"!=typeof process&&"[object process]"==={}.toString.call(process),$="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel,tt=new Array(1e3);H=Z?o():X?s():$?u():void 0===R&&"function"==typeof require?f():c();var et=l,nt=h,rt=Math.random().toString(36).substring(16),ot=void 0,it=1,st=2,ut=new M,ct=new M,at=0,ft=Y,lt=q,ht=F,pt=L;L.all=ft,L.race=lt,L.resolve=nt,L.reject=ht,L._setScheduler=n,L._setAsap=r,L._asap=Q,L.prototype={constructor:L,then:et,"catch":function(t){return this.then(null,t)}};var _t=N;N.prototype._enumerate=function(){for(var t=this.length,e=this._input,n=0;this._state===ot&&t>n;n++)this._eachEntry(e[n],n)},N.prototype._eachEntry=function(t,e){var n=this._instanceConstructor,r=n.resolve;if(r===nt){var o=v(t);if(o===et&&t._state!==ot)this._settledAt(t._state,e,t._result);else if("function"!=typeof o)this._remaining--,this._result[e]=t;else if(n===pt){var i=new n(p);w(i,t,o),this._willSettleAt(i,e)}else this._willSettleAt(new n(function(e){e(t)}),e)}else this._willSettleAt(r(t),e)},N.prototype._settledAt=function(t,e,n){var r=this.promise;r._state===ot&&(this._remaining--,t===st?j(r,n):this._result[e]=n),0===this._remaining&&S(r,this._result)},N.prototype._willSettleAt=function(t,e){var n=this;E(t,void 0,function(t){n._settledAt(it,e,t)},function(t){n._settledAt(st,e,t)})};var dt=W,vt={Promise:pt,polyfill:dt};"function"==typeof define&&define.amd?define(function(){return vt}):"undefined"!=typeof module&&module.exports?module.exports=vt:"undefined"!=typeof this&&(this.ES6Promise=vt),dt()}).call(this);
\ No newline at end of file
diff --git a/server/bundles/page/fetch.js b/server/bundles/page/fetch.js
new file mode 100644
index 0000000..fac11e4
--- /dev/null
+++ b/server/bundles/page/fetch.js
@@ -0,0 +1,389 @@
+(function(self) {
+ 'use strict';
+
+ if (self.fetch) {
+ return
+ }
+
+ function normalizeName(name) {
+ if (typeof name !== 'string') {
+ name = String(name)
+ }
+ if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) {
+ throw new TypeError('Invalid character in header field name')
+ }
+ return name.toLowerCase()
+ }
+
+ function normalizeValue(value) {
+ if (typeof value !== 'string') {
+ value = String(value)
+ }
+ return value
+ }
+
+ function Headers(headers) {
+ this.map = {}
+
+ if (headers instanceof Headers) {
+ headers.forEach(function(value, name) {
+ this.append(name, value)
+ }, this)
+
+ } else if (headers) {
+ Object.getOwnPropertyNames(headers).forEach(function(name) {
+ this.append(name, headers[name])
+ }, this)
+ }
+ }
+
+ Headers.prototype.append = function(name, value) {
+ name = normalizeName(name)
+ value = normalizeValue(value)
+ var list = this.map[name]
+ if (!list) {
+ list = []
+ this.map[name] = list
+ }
+ list.push(value)
+ }
+
+ Headers.prototype['delete'] = function(name) {
+ delete this.map[normalizeName(name)]
+ }
+
+ Headers.prototype.get = function(name) {
+ var values = this.map[normalizeName(name)]
+ return values ? values[0] : null
+ }
+
+ Headers.prototype.getAll = function(name) {
+ return this.map[normalizeName(name)] || []
+ }
+
+ Headers.prototype.has = function(name) {
+ return this.map.hasOwnProperty(normalizeName(name))
+ }
+
+ Headers.prototype.set = function(name, value) {
+ this.map[normalizeName(name)] = [normalizeValue(value)]
+ }
+
+ Headers.prototype.forEach = function(callback, thisArg) {
+ Object.getOwnPropertyNames(this.map).forEach(function(name) {
+ this.map[name].forEach(function(value) {
+ callback.call(thisArg, value, name, this)
+ }, this)
+ }, this)
+ }
+
+ function consumed(body) {
+ if (body.bodyUsed) {
+ return Promise.reject(new TypeError('Already read'))
+ }
+ body.bodyUsed = true
+ }
+
+ function fileReaderReady(reader) {
+ return new Promise(function(resolve, reject) {
+ reader.onload = function() {
+ resolve(reader.result)
+ }
+ reader.onerror = function() {
+ reject(reader.error)
+ }
+ })
+ }
+
+ function readBlobAsArrayBuffer(blob) {
+ var reader = new FileReader()
+ reader.readAsArrayBuffer(blob)
+ return fileReaderReady(reader)
+ }
+
+ function readBlobAsText(blob) {
+ var reader = new FileReader()
+ reader.readAsText(blob)
+ return fileReaderReady(reader)
+ }
+
+ var support = {
+ blob: 'FileReader' in self && 'Blob' in self && (function() {
+ try {
+ new Blob();
+ return true
+ } catch(e) {
+ return false
+ }
+ })(),
+ formData: 'FormData' in self,
+ arrayBuffer: 'ArrayBuffer' in self
+ }
+
+ function Body() {
+ this.bodyUsed = false
+
+
+ this._initBody = function(body) {
+ this._bodyInit = body
+ if (typeof body === 'string') {
+ this._bodyText = body
+ } else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
+ this._bodyBlob = body
+ } else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
+ this._bodyFormData = body
+ } else if (!body) {
+ this._bodyText = ''
+ } else if (support.arrayBuffer && ArrayBuffer.prototype.isPrototypeOf(body)) {
+ // Only support ArrayBuffers for POST method.
+ // Receiving ArrayBuffers happens via Blobs, instead.
+ } else {
+ throw new Error('unsupported BodyInit type')
+ }
+
+ if (!this.headers.get('content-type')) {
+ if (typeof body === 'string') {
+ this.headers.set('content-type', 'text/plain;charset=UTF-8')
+ } else if (this._bodyBlob && this._bodyBlob.type) {
+ this.headers.set('content-type', this._bodyBlob.type)
+ }
+ }
+ }
+
+ if (support.blob) {
+ this.blob = function() {
+ var rejected = consumed(this)
+ if (rejected) {
+ return rejected
+ }
+
+ if (this._bodyBlob) {
+ return Promise.resolve(this._bodyBlob)
+ } else if (this._bodyFormData) {
+ throw new Error('could not read FormData body as blob')
+ } else {
+ return Promise.resolve(new Blob([this._bodyText]))
+ }
+ }
+
+ this.arrayBuffer = function() {
+ return this.blob().then(readBlobAsArrayBuffer)
+ }
+
+ this.text = function() {
+ var rejected = consumed(this)
+ if (rejected) {
+ return rejected
+ }
+
+ if (this._bodyBlob) {
+ return readBlobAsText(this._bodyBlob)
+ } else if (this._bodyFormData) {
+ throw new Error('could not read FormData body as text')
+ } else {
+ return Promise.resolve(this._bodyText)
+ }
+ }
+ } else {
+ this.text = function() {
+ var rejected = consumed(this)
+ return rejected ? rejected : Promise.resolve(this._bodyText)
+ }
+ }
+
+ if (support.formData) {
+ this.formData = function() {
+ return this.text().then(decode)
+ }
+ }
+
+ this.json = function() {
+ return this.text().then(JSON.parse)
+ }
+
+ return this
+ }
+
+ // HTTP methods whose capitalization should be normalized
+ var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']
+
+ function normalizeMethod(method) {
+ var upcased = method.toUpperCase()
+ return (methods.indexOf(upcased) > -1) ? upcased : method
+ }
+
+ function Request(input, options) {
+ options = options || {}
+ var body = options.body
+ if (Request.prototype.isPrototypeOf(input)) {
+ if (input.bodyUsed) {
+ throw new TypeError('Already read')
+ }
+ this.url = input.url
+ this.credentials = input.credentials
+ if (!options.headers) {
+ this.headers = new Headers(input.headers)
+ }
+ this.method = input.method
+ this.mode = input.mode
+ if (!body) {
+ body = input._bodyInit
+ input.bodyUsed = true
+ }
+ } else {
+ this.url = input
+ }
+
+ this.credentials = options.credentials || this.credentials || 'omit'
+ if (options.headers || !this.headers) {
+ this.headers = new Headers(options.headers)
+ }
+ this.method = normalizeMethod(options.method || this.method || 'GET')
+ this.mode = options.mode || this.mode || null
+ this.referrer = null
+
+ if ((this.method === 'GET' || this.method === 'HEAD') && body) {
+ throw new TypeError('Body not allowed for GET or HEAD requests')
+ }
+ this._initBody(body)
+ }
+
+ Request.prototype.clone = function() {
+ return new Request(this)
+ }
+
+ function decode(body) {
+ var form = new FormData()
+ body.trim().split('&').forEach(function(bytes) {
+ if (bytes) {
+ var split = bytes.split('=')
+ var name = split.shift().replace(/\+/g, ' ')
+ var value = split.join('=').replace(/\+/g, ' ')
+ form.append(decodeURIComponent(name), decodeURIComponent(value))
+ }
+ })
+ return form
+ }
+
+ function headers(xhr) {
+ var head = new Headers()
+ var pairs = xhr.getAllResponseHeaders().trim().split('\n')
+ pairs.forEach(function(header) {
+ var split = header.trim().split(':')
+ var key = split.shift().trim()
+ var value = split.join(':').trim()
+ head.append(key, value)
+ })
+ return head
+ }
+
+ Body.call(Request.prototype)
+
+ function Response(bodyInit, options) {
+ if (!options) {
+ options = {}
+ }
+
+ this.type = 'default'
+ this.status = options.status
+ this.ok = this.status >= 200 && this.status < 300
+ this.statusText = options.statusText
+ this.headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers)
+ this.url = options.url || ''
+ this._initBody(bodyInit)
+ }
+
+ Body.call(Response.prototype)
+
+ Response.prototype.clone = function() {
+ return new Response(this._bodyInit, {
+ status: this.status,
+ statusText: this.statusText,
+ headers: new Headers(this.headers),
+ url: this.url
+ })
+ }
+
+ Response.error = function() {
+ var response = new Response(null, {status: 0, statusText: ''})
+ response.type = 'error'
+ return response
+ }
+
+ var redirectStatuses = [301, 302, 303, 307, 308]
+
+ Response.redirect = function(url, status) {
+ if (redirectStatuses.indexOf(status) === -1) {
+ throw new RangeError('Invalid status code')
+ }
+
+ return new Response(null, {status: status, headers: {location: url}})
+ }
+
+ self.Headers = Headers;
+ self.Request = Request;
+ self.Response = Response;
+
+ self.fetch = function(input, init) {
+ return new Promise(function(resolve, reject) {
+ var request
+ if (Request.prototype.isPrototypeOf(input) && !init) {
+ request = input
+ } else {
+ request = new Request(input, init)
+ }
+
+ var xhr = new XMLHttpRequest()
+
+ function responseURL() {
+ if ('responseURL' in xhr) {
+ return xhr.responseURL
+ }
+
+ // Avoid security warnings on getResponseHeader when not allowed by CORS
+ if (/^X-Request-URL:/m.test(xhr.getAllResponseHeaders())) {
+ return xhr.getResponseHeader('X-Request-URL')
+ }
+
+ return;
+ }
+
+ xhr.onload = function() {
+ var status = (xhr.status === 1223) ? 204 : xhr.status
+ if (status < 100 || status > 599) {
+ reject(new TypeError('Network request failed'))
+ return
+ }
+ var options = {
+ status: status,
+ statusText: xhr.statusText,
+ headers: headers(xhr),
+ url: responseURL()
+ }
+ var body = 'response' in xhr ? xhr.response : xhr.responseText;
+ resolve(new Response(body, options))
+ }
+
+ xhr.onerror = function() {
+ reject(new TypeError('Network request failed'))
+ }
+
+ xhr.open(request.method, request.url, true)
+
+ if (request.credentials === 'include') {
+ xhr.withCredentials = true
+ }
+
+ if ('responseType' in xhr && support.blob) {
+ xhr.responseType = 'blob'
+ }
+
+ request.headers.forEach(function(value, name) {
+ xhr.setRequestHeader(name, value)
+ })
+
+ xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
+ })
+ }
+ self.fetch.polyfill = true
+})(typeof self !== 'undefined' ? self : this);
diff --git a/server/bundles/page/page.js b/server/bundles/page/page.js
new file mode 100644
index 0000000..a8fab37
--- /dev/null
+++ b/server/bundles/page/page.js
@@ -0,0 +1,13 @@
+require('../../blocks/todo/todo.styl');
+require('../../blocks/todo/__add-form/todo__add-form.styl');
+require('../../blocks/todo/__edit-form/todo__edit-form.styl');
+require('../../blocks/todo/__refresh/todo__refresh.styl');
+require('../../blocks/todo/__item/todo__item.styl');
+require('../../blocks/todo/__text/todo__text.styl');
+require('../../blocks/todo/__trashbox/todo__trashbox.styl');
+
+require('../../blocks/todo-app/todo-app.styl');
+require('../../blocks/todo-app/__header/todo-app__header.styl');
+
+require('file?name=refresh.png!./refresh.png');
+require('file?name=trashbox.png!./trashbox.png');
\ No newline at end of file
diff --git a/server/bundles/page/promise.min.js b/server/bundles/page/promise.min.js
new file mode 100644
index 0000000..2d48351
--- /dev/null
+++ b/server/bundles/page/promise.min.js
@@ -0,0 +1,9 @@
+/*!
+ * @overview es6-promise - a tiny implementation of Promises/A+.
+ * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
+ * @license Licensed under MIT license
+ * See https://raw.githubusercontent.com/jakearchibald/es6-promise/master/LICENSE
+ * @version 3.0.2
+ */
+
+(function(){"use strict";function lib$es6$promise$utils$$objectOrFunction(x){return typeof x==="function"||typeof x==="object"&&x!==null}function lib$es6$promise$utils$$isFunction(x){return typeof x==="function"}function lib$es6$promise$utils$$isMaybeThenable(x){return typeof x==="object"&&x!==null}var lib$es6$promise$utils$$_isArray;if(!Array.isArray){lib$es6$promise$utils$$_isArray=function(x){return Object.prototype.toString.call(x)==="[object Array]"}}else{lib$es6$promise$utils$$_isArray=Array.isArray}var lib$es6$promise$utils$$isArray=lib$es6$promise$utils$$_isArray;var lib$es6$promise$asap$$len=0;var lib$es6$promise$asap$$toString={}.toString;var lib$es6$promise$asap$$vertxNext;var lib$es6$promise$asap$$customSchedulerFn;var lib$es6$promise$asap$$asap=function asap(callback,arg){lib$es6$promise$asap$$queue[lib$es6$promise$asap$$len]=callback;lib$es6$promise$asap$$queue[lib$es6$promise$asap$$len+1]=arg;lib$es6$promise$asap$$len+=2;if(lib$es6$promise$asap$$len===2){if(lib$es6$promise$asap$$customSchedulerFn){lib$es6$promise$asap$$customSchedulerFn(lib$es6$promise$asap$$flush)}else{lib$es6$promise$asap$$scheduleFlush()}}};function lib$es6$promise$asap$$setScheduler(scheduleFn){lib$es6$promise$asap$$customSchedulerFn=scheduleFn}function lib$es6$promise$asap$$setAsap(asapFn){lib$es6$promise$asap$$asap=asapFn}var lib$es6$promise$asap$$browserWindow=typeof window!=="undefined"?window:undefined;var lib$es6$promise$asap$$browserGlobal=lib$es6$promise$asap$$browserWindow||{};var lib$es6$promise$asap$$BrowserMutationObserver=lib$es6$promise$asap$$browserGlobal.MutationObserver||lib$es6$promise$asap$$browserGlobal.WebKitMutationObserver;var lib$es6$promise$asap$$isNode=typeof process!=="undefined"&&{}.toString.call(process)==="[object process]";var lib$es6$promise$asap$$isWorker=typeof Uint8ClampedArray!=="undefined"&&typeof importScripts!=="undefined"&&typeof MessageChannel!=="undefined";function lib$es6$promise$asap$$useNextTick(){return function(){process.nextTick(lib$es6$promise$asap$$flush)}}function lib$es6$promise$asap$$useVertxTimer(){return function(){lib$es6$promise$asap$$vertxNext(lib$es6$promise$asap$$flush)}}function lib$es6$promise$asap$$useMutationObserver(){var iterations=0;var observer=new lib$es6$promise$asap$$BrowserMutationObserver(lib$es6$promise$asap$$flush);var node=document.createTextNode("");observer.observe(node,{characterData:true});return function(){node.data=iterations=++iterations%2}}function lib$es6$promise$asap$$useMessageChannel(){var channel=new MessageChannel;channel.port1.onmessage=lib$es6$promise$asap$$flush;return function(){channel.port2.postMessage(0)}}function lib$es6$promise$asap$$useSetTimeout(){return function(){setTimeout(lib$es6$promise$asap$$flush,1)}}var lib$es6$promise$asap$$queue=new Array(1e3);function lib$es6$promise$asap$$flush(){for(var i=0;i {
+ const todos = db[req.cookies['userToken']];
+ if (todos) {
+ res.json({status: 'ok', userTodo: todos});
+ } else {
+ res.json({status: 'failed', comment: 'No todo for user'});
+ }
+};
+
+exports.add = (req, res) => {
+ var userTodo = db[req.cookies['userToken']];
+ var id = 0;
+ if (userTodo) {
+ id = Math.max(Math.max.apply(null, Object.keys(userTodo)) + 1, 0);
+ userTodo[id] = {
+ text: req.body.text,
+ createdAt: Date.now()
+ };
+ } else {
+ userTodo = {};
+ userTodo[0] = {
+ text: req.body.text,
+ createdAt: Date.now()
+ };
+ db[req.cookies['userToken']] = userTodo;
+ }
+ res.json({
+ status: 'ok', userTodo: {
+ id: `${id}`,
+ text: req.body.text,
+ createdAt: Date.now()
+ }
+ });
+};
+
+exports.getById = (req, res) => {
+ var userTodo = db[req.cookies['userToken']];
+ if (userTodo) {
+ if (userTodo[req.params.id]) {
+ return res.json({status: 'ok', todo: userTodo[req.params.id]});
+ }
+ }
+ res.json({status: 'failed', comment: 'No todo by given id'});
+};
+
+exports.putById = (req, res) => {
+ var userTodo = db[req.cookies['userToken']];
+ if (userTodo) {
+ if (userTodo[req.params.id]) {
+ userTodo[req.params.id].text = req.body.text;
+ return res.json({
+ status: 'ok',
+ userTodo: Object.assign({}, userTodo[req.params.id], {id: req.params.id})
+ });
+ }
+ }
+ res.json({status: 'failed', comment: 'No todo by given id'});
+};
+
+exports.deleteById = (req, res) => {
+ var userTodo = db[req.cookies['userToken']];
+ if (userTodo) {
+ if (userTodo[req.params.id]) {
+ const todo = userTodo[req.params.id];
+ delete userTodo[req.params.id];
+ return res.json({status: 'ok', userTodo: Object.assign({}, todo, {id: req.params.id})});
+ }
+ }
+ res.json({status: 'failed', comment: 'No todo by given id'});
+};
\ No newline at end of file
diff --git a/server/db.js b/server/db.js
new file mode 100644
index 0000000..34bcf84
--- /dev/null
+++ b/server/db.js
@@ -0,0 +1,7 @@
+// База данных в формате
+// { id_1: {
+// 0: "Купить котика"
+// 1: "Поесть"
+// }
+// }
+module.exports = {};
\ No newline at end of file
diff --git a/server/index.html b/server/index.html
new file mode 100755
index 0000000..d956cdb
--- /dev/null
+++ b/server/index.html
@@ -0,0 +1,11 @@
+
+
+
+ Redux TodoMVC example
+
+
+
+
+
+
+
diff --git a/server/routes/apiRoutes.js b/server/routes/apiRoutes.js
new file mode 100644
index 0000000..469e361
--- /dev/null
+++ b/server/routes/apiRoutes.js
@@ -0,0 +1,23 @@
+'use strict';
+
+var express = require('express');
+var router = express.Router();
+var db = require('../db.js');
+
+const todos = require('../controllers/todos');
+
+router.use(function timeLog(req, res, next) {
+ //console.log('Time: ', Date.now());
+ console.log(db);
+ next();
+});
+
+router.get('/', todos.all);
+router.post('/', todos.add);
+
+router.get('/:id', todos.getById);
+router.put('/:id', todos.putById);
+router.delete('/:id', todos.deleteById);
+
+
+module.exports = router;
\ No newline at end of file
diff --git a/server/routes/routes.js b/server/routes/routes.js
new file mode 100644
index 0000000..7060d43
--- /dev/null
+++ b/server/routes/routes.js
@@ -0,0 +1,36 @@
+'use strict';
+
+var api = require('./apiRoutes');
+
+module.exports = function (app) {
+ app.get('/', (req, res) => {
+ res.send(
+ `
+
+
+ Todo
+
+
+
+
+
+
+
+
+
+ `
+ )
+ });
+
+ app.use('/api/todos', api);
+
+ // app.all('*', pages.error404);
+
+ /* eslint no-unused-vars: 0 */
+ /* eslint max-params: [2, 4] */
+ app.use((err, req, res, next) => {
+ console.error(err);
+
+ res.sendStatus(500);
+ });
+};
\ No newline at end of file
diff --git a/server/userToken.js b/server/userToken.js
new file mode 100644
index 0000000..94acfd2
--- /dev/null
+++ b/server/userToken.js
@@ -0,0 +1,14 @@
+module.exports = (req, res, next) => {
+ if (req.cookies['userToken']) {
+ console.log('Returning user');
+ next();
+ } else {
+ console.log('New user');
+ require('crypto').randomBytes(40, function(err, buffer) {
+ var token = buffer.toString('hex');
+ // console.log(token);
+ res.cookie('userToken' , token);
+ next()
+ });
+ }
+};
\ No newline at end of file
diff --git a/store/configureStore.js b/store/configureStore.js
new file mode 100755
index 0000000..8801e6f
--- /dev/null
+++ b/store/configureStore.js
@@ -0,0 +1,26 @@
+import {createStore, applyMiddleware, compose} from 'redux'
+import thunkMiddleware from 'redux-thunk'
+import createLogger from 'redux-logger'
+import rootReducer from '../reducers'
+
+export default function configureStore(initialState) {
+ const store = createStore(
+ rootReducer,
+ initialState,
+ compose(
+ applyMiddleware(thunkMiddleware, createLogger()),
+ window.devToolsExtension ? window.devToolsExtension(): f => f
+ )
+ );
+
+
+ if (module.hot) {
+ // Enable Webpack hot module replacement for reducers
+ module.hot.accept('../reducers', () => {
+ const nextReducer = require('../reducers').default;
+ store.replaceReducer(nextReducer)
+ })
+ }
+
+ return store
+}
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100755
index 0000000..8e12194
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,46 @@
+var path = require('path');
+var webpack = require('webpack');
+var ExtractTextPlugin = require('extract-text-webpack-plugin');
+
+module.exports = {
+ devtool: 'cheap-module-eval-source-map',
+ entry: [
+ 'webpack-hot-middleware/client',
+ './index',
+ './server/bundles/page/page'
+ ],
+ output: {
+ path: path.join(__dirname, 'dist'),
+ filename: '[name].js',
+ publicPath: '/static/'
+ },
+ plugins: [
+ new webpack.optimize.OccurenceOrderPlugin(),
+ new webpack.HotModuleReplacementPlugin(),
+ new ExtractTextPlugin('[name].css')
+ ],
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ loaders: ['babel'],
+ exclude: /node_modules/,
+ include: __dirname
+ },
+ {
+ test: /\.css?$/,
+ loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
+ // include: __dirname
+ },
+ {
+ test: /\.styl$/,
+ loader: ExtractTextPlugin.extract('css-loader!stylus-loader'),
+ exclude: /node_modules/
+ }
+ // {
+ // test: /\.png$/,
+ // loader: 'file-loader'
+ // }
+ ]
+ }
+};