Skip to content

Commit

Permalink
TodoList sample with server side persistence. (#107)
Browse files Browse the repository at this point in the history
* auto disable ssr based on load

* if autossr is needed

* making auto ssr an option

* remove unused variable

* move auto ssr to a module

* removing comments

* package electrode-auto-ssr

* adding hapi as dev dependency

* add local package to gulpfile

* consistent variable name

* missing variable

* basic todo app

* correctly initialize redux Store

* modify default reducer

* read file

* read todo list from file

* add middleware

* component changes

* update reducers and view

* add plugin for updating file

* undo debugging changes

* fix esLint

* error handling

* using isomorphic fetch

* lint

* response status is a number

* enable csrf
  • Loading branch information
animesh10 authored and jchip committed Jan 17, 2017
1 parent 6685f90 commit 4229a77
Show file tree
Hide file tree
Showing 22 changed files with 401 additions and 12 deletions.
22 changes: 22 additions & 0 deletions samples/universal-react-node/client/actions/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
let nextTodoId = 0;
export const addTodo = (text) => {
return {
type: "ADD_TODO",
id: nextTodoId++,
text
};
};

export const setVisibilityFilter = (filter) => {
return {
type: "SET_VISIBILITY_FILTER",
filter
};
};

export const toggleTodo = (id) => {
return {
type: "TOGGLE_TODO",
id
};
};
4 changes: 3 additions & 1 deletion samples/universal-react-node/client/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import React from "react";
import {render} from "react-dom";
import {routes} from "./routes";
import {Router, browserHistory} from "react-router";
import {createStore, compose} from "redux";
import {createStore, compose, applyMiddleware} from "redux";
import {Provider} from "react-redux";
import {notify} from "react-notify-toast";
import "styles/base.css";
import rootReducer from "./reducers";
import DevTools from "../client/devtools";
import updateStorage from "./middleware";

require.ensure(["./sw-registration"], (require) => {
require("./sw-registration")(notify);
Expand All @@ -16,6 +17,7 @@ require.ensure(["./sw-registration"], (require) => {
const enhancer = compose(
// Add middlewares you want to use in development:
// applyMiddleware(d1, d2, d3),
applyMiddleware(updateStorage),
DevTools.instrument()
);

Expand Down
6 changes: 3 additions & 3 deletions samples/universal-react-node/client/components/csrf.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";

const HTTP_OK = 200;
export class CSRF extends React.Component {

constructor() {
Expand All @@ -23,7 +23,7 @@ export class CSRF extends React.Component {
body: JSON.stringify({message: "hello"})
})
.then((resp) => {
if (resp.status === "200") {
if (resp.status === HTTP_OK) {
this.setState({testResult: `POST SUCCEEDED with status ${resp.status}` });
} else {
this.setState({testResult: `POST FAILED with status ${resp.status}` });
Expand All @@ -39,7 +39,7 @@ export class CSRF extends React.Component {
this.setState({testResult: "valid"});
fetch("/1", {credentials: "same-origin"}) // eslint-disable-line
.then((resp) => {
if (resp.status === "200") {
if (resp.status === HTTP_OK) {
const token = resp.headers.get("x-csrf-jwt");
if (token !== "") {
console.log("Got CSRF token OK"); // eslint-disable-line
Expand Down
27 changes: 27 additions & 0 deletions samples/universal-react-node/client/components/footer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";
import FilterLink from "../containers/filter-link";

const Footer = () => (
<div>
<p>
Show:
{" "}
<FilterLink filter="SHOW_ALL">
All
</FilterLink>
{", "}
<FilterLink filter="SHOW_ACTIVE">
Active
</FilterLink>
{", "}
<FilterLink filter="SHOW_COMPLETED">
Completed
</FilterLink>
</p>
<a href="/">
Home
</a>
</div>
);

export default Footer;
1 change: 1 addition & 0 deletions samples/universal-react-node/client/components/home.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class Home extends React.Component {
<li><a href="/ssrcachingsimpletype">SSR Caching Simple Type Example</a></li>
<li><a href="/ssrcachingtemplatetype">SSR Caching Template Type Example</a></li>
<li><a href="/push-notifications">Push Notifications Example</a></li>
<li><a href="/todo-app">Todo List Example</a></li>
</ul>
<p>{this.props.data}</p>
</div>
Expand Down
26 changes: 26 additions & 0 deletions samples/universal-react-node/client/components/link.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { PropTypes } from "react";

const Link = ({ active, children, onClick }) => {
if (active) {
return <span>{children}</span>;
}

return (
<a href="#"
onClick={(e) => {
e.preventDefault();
onClick();
}}
>
{children}
</a>
);
};

Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
};

export default Link;
14 changes: 14 additions & 0 deletions samples/universal-react-node/client/components/todo-app.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react";
import Footer from "./footer";
import AddTodo from "../containers/add-todo";
import VisibleTodoList from "../containers/visible-todo-list";

const TodoApp = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
);

export default TodoApp;
25 changes: 25 additions & 0 deletions samples/universal-react-node/client/components/todo-list.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { PropTypes } from "react";
import Todo from "./todo";

const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map((todo) =>
<Todo
key={todo.id}
{...todo}
onClick={() => onTodoClick(todo.id)}
/>
)}
</ul>
);

TodoList.propTypes = {
todos: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired).isRequired,
onTodoClick: PropTypes.func.isRequired
};

export default TodoList;
20 changes: 20 additions & 0 deletions samples/universal-react-node/client/components/todo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React, { PropTypes } from "react";

const Todo = ({ onClick, completed, text }) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? "line-through" : "none"
}}
>
{text}
</li>
);

Todo.propTypes = {
onClick: PropTypes.func.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
};

export default Todo;
34 changes: 34 additions & 0 deletions samples/universal-react-node/client/containers/add-todo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, {PropTypes} from "react";
import { connect } from "react-redux";
import { addTodo } from "../actions";

let AddTodo = ({ dispatch }) => {
let input;

return (
<div>
<form onSubmit={(e) => {
e.preventDefault();
if (!input.value.trim()) {
return;
}
dispatch(addTodo(input.value));
input.value = "";
}}>
<input ref={(node) => {
input = node;
}} />
<button type="submit">
Add Todo
</button>
</form>
</div>
);
};

AddTodo.propTypes = {
dispatch: PropTypes.func
};
AddTodo = connect()(AddTodo);

export default AddTodo;
24 changes: 24 additions & 0 deletions samples/universal-react-node/client/containers/filter-link.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { connect } from "react-redux";
import { setVisibilityFilter } from "../actions";
import Link from "../components/link";

const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
};
};

const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter));
}
};
};

const FilterLink = connect(
mapStateToProps,
mapDispatchToProps
)(Link);

export default FilterLink;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { connect } from "react-redux";
import { toggleTodo } from "../actions";
import TodoList from "../components/todo-list";

const getVisibleTodos = (todos, filter) => {
switch (filter) {
case "SHOW_ALL":
return todos;
case "SHOW_COMPLETED":
return todos.filter((t) => t.completed);
case "SHOW_ACTIVE":
return todos.filter((t) => !t.completed);
default:
return todos;
}
};

const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
};
};

const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id));
}
};
};

const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList);

export default VisibleTodoList;
42 changes: 42 additions & 0 deletions samples/universal-react-node/client/middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import "es6-promise";
import "isomorphic-fetch";
const HTTP_BAD_REQUEST = 400;
const HTTP_OK = 200;
let token;

const updateStorage = (store) => (next) => (action) => {
const result = next(action);
const completeState = store.getState();
fetch("/1", {credentials: "same-origin"})
.then((resp) => {
if (resp.status === HTTP_OK) {
token = resp.headers.get("x-csrf-jwt");
} else {
throw new Error("token generation failed");
}
fetch("/updateStorage", {
credentials: "same-origin",
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"x-csrf-jwt": token
},
body: JSON.stringify(completeState)
})
.then((response) => {
if (response.status >= HTTP_BAD_REQUEST) {
throw new Error("Bad response from server");
}
return result;
})
.catch((err) => {
throw new Error("Error Updating Storage", err);
});
})
.catch((err) => {
throw new Error("Error Fetching Csrf Token", err);
});
};

export default updateStorage;
12 changes: 8 additions & 4 deletions samples/universal-react-node/client/reducers/index.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
const rootReducer = (state) => {
return state || {};
};
import todos from "./todos";
import visibilityFilter from "./visibilityFilter";

export default rootReducer;
export default function rootReducer(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
};
}
41 changes: 41 additions & 0 deletions samples/universal-react-node/client/reducers/todos.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const todo = (state = {}, action) => {
let currentId;
if (state.length > 0) {
currentId = state[state.length - 1].id;
}
switch (action.type) {
case "ADD_TODO":
return {
id: ++currentId || action.id,
text: action.text,
completed: false
};
case "TOGGLE_TODO":
if (state.id !== action.id) {
return state;
}
return Object.assign({}, state, {
completed: !state.completed
});
default:
return state;
}
};

const todos = (state = [], action) => {
switch (action.type) {
case "ADD_TODO":
return [
...state,
todo(state, action)
];
case "TOGGLE_TODO":
return state.map((t) =>
todo(t, action)
);
default:
return state;
}
};

export default todos;
Loading

0 comments on commit 4229a77

Please sign in to comment.