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 ( +
+

Todo App

+
+ ) + } +} +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' + // } + ] + } +};