├── .gitignore ├── README.md ├── actions └── todos.js ├── components ├── Footer.js ├── Header.js ├── MainSection.js ├── SyncStatus.js ├── TodoItem.js └── TodoTextInput.js ├── constants ├── ActionTypes.js └── TodoFilters.js ├── containers └── App.js ├── index.html ├── index.js ├── package.json ├── reducers ├── index.js ├── syncState.js └── todos.js ├── server.js ├── store └── configureStore.js ├── web └── .gitignore ├── webpack.config.js └── websocket-server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | coverage 4 | todos-server 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pouch-websocket-sync-example 2 | 3 | Example "Todo-MVC" application of using [`pouch-websocket-sync`](https://github.com/pgte/pouch-websocket-sync#readme) together with React and Redux, keeping local database in sync with remote. 4 | 5 | [demo video](http://www.youtube.com/watch?v=8jOF23dfvl4) 6 | 7 | ## Pre-requisites 8 | 9 | You must have [Node.js](https://nodejs.org/en/) installed. 10 | 11 | ## Download 12 | 13 | Clone this repo: 14 | 15 | ``` 16 | $ git clone git@github.com:pgte/pouch-websocket-sync-example.git 17 | $ cd pouch-websocket-sync-example 18 | ``` 19 | 20 | ## Install dependencies: 21 | 22 | ``` 23 | $ npm install 24 | ``` 25 | 26 | ## Start 27 | 28 | Start web server: 29 | 30 | ``` 31 | $ npm start 32 | ``` 33 | 34 | Start websocket server: 35 | 36 | ``` 37 | $ node websocket-server 38 | ``` 39 | 40 | Open [http://localhost:3000](http://localhost:3000) in your browsers. 41 | 42 | ## License 43 | 44 | ISC -------------------------------------------------------------------------------- /actions/todos.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes' 2 | 3 | export function addTodo(text) { 4 | return { type: types.ADD_TODO, text } 5 | } 6 | 7 | export function deleteTodo(id) { 8 | return { type: types.DELETE_TODO, id } 9 | } 10 | 11 | export function editTodo(id, text) { 12 | return { type: types.EDIT_TODO, id, text } 13 | } 14 | 15 | export function completeTodo(id) { 16 | return { type: types.COMPLETE_TODO, id } 17 | } 18 | 19 | export function completeAll() { 20 | return { type: types.COMPLETE_ALL } 21 | } 22 | 23 | export function clearCompleted() { 24 | return { type: types.CLEAR_COMPLETED } 25 | } 26 | -------------------------------------------------------------------------------- /components/Footer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import classnames from 'classnames' 3 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 4 | 5 | const FILTER_TITLES = { 6 | [SHOW_ALL]: 'All', 7 | [SHOW_ACTIVE]: 'Active', 8 | [SHOW_COMPLETED]: 'Completed' 9 | } 10 | 11 | class Footer extends Component { 12 | renderTodoCount() { 13 | const { activeCount } = this.props 14 | const itemWord = activeCount === 1 ? 'item' : 'items' 15 | 16 | return ( 17 | 18 | {activeCount || 'No'} {itemWord} left 19 | 20 | ) 21 | } 22 | 23 | renderFilterLink(filter) { 24 | const title = FILTER_TITLES[filter] 25 | const { filter: selectedFilter, onShow } = this.props 26 | 27 | return ( 28 | onShow(filter)}> 31 | {title} 32 | 33 | ) 34 | } 35 | 36 | renderClearButton() { 37 | const { completedCount, onClearCompleted } = this.props 38 | if (completedCount > 0) { 39 | return ( 40 | 44 | ) 45 | } 46 | } 47 | 48 | render() { 49 | return ( 50 | 61 | ) 62 | } 63 | } 64 | 65 | Footer.propTypes = { 66 | completedCount: PropTypes.number.isRequired, 67 | activeCount: PropTypes.number.isRequired, 68 | filter: PropTypes.string.isRequired, 69 | onClearCompleted: PropTypes.func.isRequired, 70 | onShow: PropTypes.func.isRequired 71 | } 72 | 73 | export default Footer 74 | -------------------------------------------------------------------------------- /components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react' 2 | import TodoTextInput from './TodoTextInput' 3 | 4 | class Header extends Component { 5 | handleSave(text) { 6 | if (text.length !== 0) { 7 | this.props.addTodo(text) 8 | } 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 |

todos

15 | 18 |
19 | ) 20 | } 21 | } 22 | 23 | Header.propTypes = { 24 | addTodo: PropTypes.func.isRequired 25 | } 26 | 27 | export default Header 28 | -------------------------------------------------------------------------------- /components/MainSection.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import TodoItem from './TodoItem' 3 | import Footer from './Footer' 4 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters' 5 | 6 | const TODO_FILTERS = { 7 | [SHOW_ALL]: () => true, 8 | [SHOW_ACTIVE]: todo => !todo.completed, 9 | [SHOW_COMPLETED]: todo => todo.completed 10 | } 11 | 12 | class MainSection extends Component { 13 | constructor(props, context) { 14 | super(props, context) 15 | this.state = { filter: SHOW_ALL } 16 | } 17 | 18 | handleClearCompleted() { 19 | const atLeastOneCompleted = this.props.todos.some(todo => todo.completed) 20 | if (atLeastOneCompleted) { 21 | this.props.actions.clearCompleted() 22 | } 23 | } 24 | 25 | handleShow(filter) { 26 | this.setState({ filter }) 27 | } 28 | 29 | renderToggleAll(completedCount) { 30 | const { todos, actions } = this.props 31 | if (todos.length > 0) { 32 | return ( 33 | 37 | ) 38 | } 39 | } 40 | 41 | renderFooter(completedCount) { 42 | const { todos } = this.props 43 | const { filter } = this.state 44 | const activeCount = todos.length - completedCount 45 | 46 | if (todos.length) { 47 | return ( 48 |