mirror of
https://github.com/usatiuk/ustk-todolist.git
synced 2025-10-28 15:47:48 +01:00
Add 'react/' from commit 'e508ac031e5d832aa95ad8ef040e82277db804d2'
git-subtree-dir: react git-subtree-mainline:53976e7b43git-subtree-split:e508ac031e
This commit is contained in:
30
react/.eslintrc.json
Normal file
30
react/.eslintrc.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": [
|
||||
"airbnb",
|
||||
"plugin:jest/recommended",
|
||||
"prettier",
|
||||
"prettier/react"
|
||||
],
|
||||
|
||||
"plugins": ["jest", "prettier"],
|
||||
"rules": {
|
||||
"react/jsx-filename-extension": [
|
||||
1,
|
||||
{
|
||||
"extensions": [".js", ".jsx"]
|
||||
}
|
||||
],
|
||||
"linebreak-style": "off",
|
||||
"no-unused-expressions": [
|
||||
"error",
|
||||
{
|
||||
"allowTernary": true
|
||||
}
|
||||
],
|
||||
"react/forbid-prop-types": "off",
|
||||
"prettier/prettier": "error"
|
||||
},
|
||||
"env": {
|
||||
"browser": true
|
||||
}
|
||||
}
|
||||
21
react/.gitignore
vendored
Normal file
21
react/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
4
react/.prettierrc
Normal file
4
react/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
5
react/.vscode/settings.json
vendored
Normal file
5
react/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true,
|
||||
"prettier.eslintIntegration": true
|
||||
}
|
||||
2444
react/README.md
Normal file
2444
react/README.md
Normal file
File diff suppressed because it is too large
Load Diff
11783
react/package-lock.json
generated
Normal file
11783
react/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
react/package.json
Normal file
38
react/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "todolist-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.0-14",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.1.0-11",
|
||||
"@fortawesome/react-fontawesome": "0.1.0-10",
|
||||
"normalize.css": "^8.0.0",
|
||||
"prop-types": "^15.6.1",
|
||||
"react": "^16.4.0",
|
||||
"react-dom": "^16.4.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-router-redux": "^4.0.8",
|
||||
"react-scripts": "1.1.4",
|
||||
"react-spring": "^5.3.7",
|
||||
"redux": "^4.0.0",
|
||||
"redux-form": "^7.3.0",
|
||||
"redux-thunk": "^2.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"proxy": "http://localhost:4000",
|
||||
"devDependencies": {
|
||||
"eslint-config-airbnb": "^16.1.0",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
"eslint-plugin-import": "^2.12.0",
|
||||
"eslint-plugin-jest": "^21.17.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
||||
"eslint-plugin-react": "^7.8.2",
|
||||
"prettier-eslint": "^8.8.1"
|
||||
}
|
||||
}
|
||||
BIN
react/public/favicon.ico
Normal file
BIN
react/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
40
react/public/index.html
Normal file
40
react/public/index.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
15
react/public/manifest.json
Normal file
15
react/public/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
159
react/src/actions/lists.js
Normal file
159
react/src/actions/lists.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import { API_ROOT, getToken } from './util';
|
||||
|
||||
export const ADD_LIST = 'ADD_LIST';
|
||||
export const REMOVE_LIST = 'REMOVE_LIST';
|
||||
export const EDIT_LIST_NAME = 'EDIT_LIST_NAME';
|
||||
export const RECIEVE_LISTS = 'RECIEVE_LISTS';
|
||||
export const REQUEST_LISTS = 'REQUEST_LISTS';
|
||||
export const INVALIDATE_LISTS = 'INVALIDATE_LISTS';
|
||||
export const VALIDATE_LISTS = 'VALIDATE_LISTS';
|
||||
export const CHANGE_LIST = 'CHANGE_LIST';
|
||||
export const START_CREATE_LIST = 'START_CREATE_LIST';
|
||||
export const START_EDIT_LIST = 'START_EDIT_LIST';
|
||||
|
||||
function requestLists() {
|
||||
return { type: REQUEST_LISTS };
|
||||
}
|
||||
function recieveLists(lists) {
|
||||
return { type: RECIEVE_LISTS, lists };
|
||||
}
|
||||
|
||||
function invalidateLists() {
|
||||
return { type: INVALIDATE_LISTS };
|
||||
}
|
||||
function validateLists() {
|
||||
return { type: VALIDATE_LISTS };
|
||||
}
|
||||
export function changeList(list) {
|
||||
return { type: CHANGE_LIST, list };
|
||||
}
|
||||
|
||||
export function startCreateList() {
|
||||
return { type: START_CREATE_LIST };
|
||||
}
|
||||
export function startEditList() {
|
||||
return { type: START_EDIT_LIST };
|
||||
}
|
||||
function addListToState(list) {
|
||||
return { type: ADD_LIST, list };
|
||||
}
|
||||
|
||||
export function addList(name) {
|
||||
return async dispatch => {
|
||||
dispatch(invalidateLists());
|
||||
const response = await fetch(`${API_ROOT}/lists`, {
|
||||
body: JSON.stringify({ name }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
const json = await response.json();
|
||||
const list = json.data;
|
||||
dispatch(addListToState(list));
|
||||
dispatch(changeList(list.id));
|
||||
dispatch(validateLists());
|
||||
};
|
||||
}
|
||||
|
||||
function removeListFromState(id) {
|
||||
return { type: REMOVE_LIST, id };
|
||||
}
|
||||
|
||||
export function removeList() {
|
||||
return async (dispatch, getState) => {
|
||||
let state = getState();
|
||||
const { list } = state.lists;
|
||||
dispatch(invalidateLists());
|
||||
const response = await fetch(`${API_ROOT}/lists/${list}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'DELETE',
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
dispatch(removeListFromState(list));
|
||||
state = getState();
|
||||
const newList = state.lists.lists[Object.keys(state.lists.lists)[0]]
|
||||
? state.lists.lists[Object.keys(state.lists.lists)[0]].id
|
||||
: '';
|
||||
dispatch(changeList(newList));
|
||||
}
|
||||
dispatch(validateLists());
|
||||
};
|
||||
}
|
||||
|
||||
function editListNameInState(id, name) {
|
||||
return { type: EDIT_LIST_NAME, id, name };
|
||||
}
|
||||
|
||||
export function editList(name) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { list } = state.lists;
|
||||
dispatch(invalidateLists());
|
||||
const response = await fetch(`${API_ROOT}/lists/${list}`, {
|
||||
body: JSON.stringify({ name }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'PATCH',
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
dispatch(editListNameInState(list, name));
|
||||
}
|
||||
dispatch(validateLists());
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchLists() {
|
||||
return async dispatch => {
|
||||
dispatch(requestLists());
|
||||
const response = await fetch(`${API_ROOT}/lists`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
},
|
||||
});
|
||||
const json = await response.json();
|
||||
const lists = json.data;
|
||||
const listsObj = lists.reduce((obj, list) => {
|
||||
const newObj = { ...obj };
|
||||
newObj[list.id] = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
todos: null,
|
||||
editing: false,
|
||||
...list,
|
||||
};
|
||||
return newObj;
|
||||
}, {});
|
||||
|
||||
dispatch(recieveLists(listsObj));
|
||||
if (lists.length !== 0) {
|
||||
dispatch(changeList(listsObj[Object.keys(listsObj)[0]].id));
|
||||
}
|
||||
localStorage.setItem('lists', JSON.stringify(listsObj));
|
||||
};
|
||||
}
|
||||
|
||||
export function loadLists() {
|
||||
return async dispatch => {
|
||||
dispatch(requestLists());
|
||||
|
||||
try {
|
||||
const listsJson = localStorage.getTodo('lists');
|
||||
const listsObj = JSON.parse(listsJson);
|
||||
dispatch(recieveLists(listsObj));
|
||||
dispatch(changeList(listsObj[Object.keys(listsObj)[0]].id));
|
||||
} catch (e) {
|
||||
localStorage.removeItem('lists');
|
||||
}
|
||||
|
||||
dispatch(fetchLists());
|
||||
};
|
||||
}
|
||||
147
react/src/actions/todos.js
Normal file
147
react/src/actions/todos.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import { API_ROOT, getToken } from './util';
|
||||
|
||||
export const ADD_TODO = 'ADD_TODO';
|
||||
export const REMOVE_TODO = 'REMOVE_TODO';
|
||||
export const TOGGLE_TODO = 'TOGGLE_TODO';
|
||||
export const EDIT_TODO = 'EDIT_TODO';
|
||||
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
|
||||
export const RECIEVE_TODOS = 'RECIEVE_TODOS';
|
||||
export const REQUEST_TODOS = 'REQUEST_TODOS';
|
||||
export const INVALIDATE_TODOS = 'INVALIDATE_TODOS';
|
||||
export const VALIDATE_TODOS = 'VALIDATE_TODOS';
|
||||
|
||||
export const VisibilityFilters = {
|
||||
SHOW_ALL: 'SHOW_ALL',
|
||||
SHOW_COMPLETED: 'SHOW_COMPLETED',
|
||||
SHOW_ACTIVE: 'SHOW_ACTIVE',
|
||||
};
|
||||
|
||||
function toggleTodoInList(id) {
|
||||
return { type: TOGGLE_TODO, id };
|
||||
}
|
||||
|
||||
export function setVisibilityFilter(filter) {
|
||||
return { type: SET_VISIBILITY_FILTER, filter };
|
||||
}
|
||||
|
||||
function requestTodos(list) {
|
||||
return { type: REQUEST_TODOS, list };
|
||||
}
|
||||
function recieveTodos(list, todos) {
|
||||
return { type: RECIEVE_TODOS, list, todos };
|
||||
}
|
||||
function invalidateTodos() {
|
||||
return { type: INVALIDATE_TODOS };
|
||||
}
|
||||
function validateTodos() {
|
||||
return { type: VALIDATE_TODOS };
|
||||
}
|
||||
|
||||
function addTodoToList(todo) {
|
||||
return { type: ADD_TODO, todo };
|
||||
}
|
||||
|
||||
export function addTodo(text) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { list } = state.lists;
|
||||
if (list) {
|
||||
dispatch(invalidateTodos());
|
||||
const response = await fetch(`${API_ROOT}/lists/${list}/todos`, {
|
||||
body: JSON.stringify({ text }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
const json = await response.json();
|
||||
const todo = json.data;
|
||||
dispatch(addTodoToList(todo));
|
||||
dispatch(validateTodos());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function removeTodoFromList(id) {
|
||||
return { type: REMOVE_TODO, id };
|
||||
}
|
||||
|
||||
export function removeTodo(id) {
|
||||
return async dispatch => {
|
||||
dispatch(invalidateTodos());
|
||||
const response = await fetch(`${API_ROOT}/todos/${id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'DELETE',
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
dispatch(removeTodoFromList(id));
|
||||
}
|
||||
dispatch(validateTodos());
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleTodo(id) {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch(invalidateTodos());
|
||||
const state = getState();
|
||||
const listObj = state.lists.lists[state.lists.list];
|
||||
const todoObj = listObj.todos.find(todo => todo.id === id);
|
||||
const completed = !todoObj.completed;
|
||||
const response = await fetch(`${API_ROOT}/todos/${id}`, {
|
||||
body: JSON.stringify({ completed }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'PATCH',
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
dispatch(toggleTodoInList(id));
|
||||
}
|
||||
dispatch(validateTodos());
|
||||
};
|
||||
}
|
||||
|
||||
function editTodoInList(id, todo) {
|
||||
return { type: EDIT_TODO, id, todo };
|
||||
}
|
||||
|
||||
export function editTodo(id, text) {
|
||||
return async dispatch => {
|
||||
dispatch(invalidateTodos());
|
||||
const response = await fetch(`${API_ROOT}/todos/${id}`, {
|
||||
body: JSON.stringify({ text }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'PATCH',
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
const todo = json.data;
|
||||
dispatch(editTodoInList(id, todo));
|
||||
}
|
||||
dispatch(validateTodos());
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchTodos(list) {
|
||||
return async dispatch => {
|
||||
dispatch(requestTodos(list));
|
||||
const response = await fetch(`${API_ROOT}/lists/${list.id}/todos`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
},
|
||||
});
|
||||
const json = await response.json();
|
||||
const todos = json.data;
|
||||
dispatch(recieveTodos(list, todos));
|
||||
};
|
||||
}
|
||||
107
react/src/actions/user.js
Normal file
107
react/src/actions/user.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import { API_ROOT, getToken } from './util';
|
||||
|
||||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
|
||||
export const LOGIN_FAIL = 'LOGIN_FAIL';
|
||||
export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS';
|
||||
export const SIGNUP_FAIL = 'SIGNUP_FAIL';
|
||||
export const LOGOUT = 'LOGOUT';
|
||||
export const START_LOGIN = 'INVALIDATE_USER';
|
||||
export const REQUEST_USER = 'REQUEST_USER';
|
||||
export const VALIDATE_USER = 'VALIDATE_USER';
|
||||
export const RESET_USER = 'RESET_USER';
|
||||
|
||||
function startLogin() {
|
||||
return { type: START_LOGIN };
|
||||
}
|
||||
|
||||
function loginSuccess(user) {
|
||||
return { type: LOGIN_SUCCESS, user };
|
||||
}
|
||||
|
||||
function loginFail(error) {
|
||||
return { type: LOGIN_FAIL, error };
|
||||
}
|
||||
|
||||
function validateUser() {
|
||||
return { type: VALIDATE_USER };
|
||||
}
|
||||
|
||||
export function loadUser() {
|
||||
return async dispatch => {
|
||||
if (getToken()) {
|
||||
const response = await fetch(`${API_ROOT}/users/user`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
localStorage.setItem('jwt', json.data.jwt);
|
||||
dispatch(loginSuccess(json.data));
|
||||
} else {
|
||||
dispatch(loginFail(json.error));
|
||||
}
|
||||
} else {
|
||||
dispatch(validateUser());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function login(user) {
|
||||
return async dispatch => {
|
||||
dispatch(startLogin());
|
||||
const response = await fetch(`${API_ROOT}/users/login`, {
|
||||
body: JSON.stringify(user),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
localStorage.setItem('jwt', json.data.jwt);
|
||||
dispatch(loginSuccess(json.data));
|
||||
} else {
|
||||
dispatch(loginFail(json.error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function signupSuccess(user) {
|
||||
return { type: SIGNUP_SUCCESS, user };
|
||||
}
|
||||
|
||||
function signupFail(error) {
|
||||
return { type: SIGNUP_FAIL, error };
|
||||
}
|
||||
|
||||
export function signup(user) {
|
||||
return async dispatch => {
|
||||
dispatch(startLogin());
|
||||
const response = await fetch(`${API_ROOT}/users`, {
|
||||
body: JSON.stringify(user),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
localStorage.setItem('jwt', json.data.jwt);
|
||||
dispatch(signupSuccess(json.data));
|
||||
} else {
|
||||
dispatch(signupFail(json.error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
return { type: RESET_USER };
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
localStorage.removeItem('jwt');
|
||||
return { type: LOGOUT };
|
||||
}
|
||||
5
react/src/actions/util.js
Normal file
5
react/src/actions/util.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const API_ROOT = '/api';
|
||||
|
||||
export function getToken() {
|
||||
return localStorage.getItem('jwt');
|
||||
}
|
||||
183
react/src/components/App.css
Normal file
183
react/src/components/App.css
Normal file
@@ -0,0 +1,183 @@
|
||||
#lists-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
|
||||
#lists {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 0;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
#listactions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0.2rem 0.1rem;
|
||||
}
|
||||
|
||||
#listactions button {
|
||||
font-size: 0.9rem;
|
||||
color: #555555;
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0.1rem 0.3rem;
|
||||
padding: 0.3rem 0.7em;
|
||||
}
|
||||
|
||||
#listactions button:hover {
|
||||
transition: 0.1s ease-in-out;
|
||||
color: #222222;
|
||||
}
|
||||
|
||||
#filters {
|
||||
margin-right: 0.75rem;
|
||||
align-self: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#inputs {
|
||||
transition: 0.4s ease-in-out;
|
||||
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
#input {
|
||||
color: black;
|
||||
background: white;
|
||||
font-family: "Roboto";
|
||||
box-sizing: border-box;
|
||||
font-size: 1rem;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-left: 1rem;
|
||||
height: 100%;
|
||||
line-height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#add {
|
||||
color: black;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
max-width: 5rem;
|
||||
background-color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
li:first-child .delete,
|
||||
li:first-child .edit,
|
||||
li:first-child .save,
|
||||
li:first-child .todo {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.done {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
li button {
|
||||
color: black;
|
||||
outline: none;
|
||||
text-align: left;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
border: none;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: none;
|
||||
width: 2rem;
|
||||
font-size: 1rem;
|
||||
transition: 0.4s ease-in-out;
|
||||
overflow: hidden;
|
||||
box-shadow: inset -3px 0 6px -3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
li button.todo {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.delete {
|
||||
background-color: pink;
|
||||
}
|
||||
|
||||
.edit {
|
||||
background-color: lightcyan;
|
||||
}
|
||||
|
||||
.save {
|
||||
background-color: lightgreen;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.todo {
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
padding-left: 1rem;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
font-weight: 400;
|
||||
flex-grow: 2;
|
||||
flex-shrink: 1;
|
||||
transition: 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.todo--input {
|
||||
background: white;
|
||||
color: black;
|
||||
resize: none;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
transition: 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
width: 0;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.filter {
|
||||
margin: 0.1rem;
|
||||
padding: 0.5rem;
|
||||
color: #555555;
|
||||
border: none;
|
||||
background: none;
|
||||
transition: 0.1s ease-in-out;
|
||||
font-size: 1rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.filter--active {
|
||||
font-weight: 400;
|
||||
color: black;
|
||||
}
|
||||
33
react/src/components/App.js
Normal file
33
react/src/components/App.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { BrowserRouter as Router, Route } from 'react-router-dom';
|
||||
|
||||
import 'normalize.css';
|
||||
import './Container.css';
|
||||
import './App.css';
|
||||
|
||||
import TodosContainer from '../containers/TodosContainer';
|
||||
import LoginForm from '../components/user/LoginForm';
|
||||
import SignupForm from '../components/user/SignupForm';
|
||||
|
||||
export default class App extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.loadUser();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Router>
|
||||
<div id="container">
|
||||
<Route exact path="/" component={TodosContainer} />
|
||||
<Route path="/login" component={LoginForm} />
|
||||
<Route path="/signup" component={SignupForm} />
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
loadUser: PropTypes.func.isRequired,
|
||||
};
|
||||
50
react/src/components/Container.css
Normal file
50
react/src/components/Container.css
Normal file
@@ -0,0 +1,50 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500');
|
||||
|
||||
body {
|
||||
background: white;
|
||||
color: black;
|
||||
font-family: 'Roboto';
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#root {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
|
||||
#container {
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
max-width: 25rem;
|
||||
border: 1px solid #dddddd;
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
#user-header {
|
||||
display: flex;
|
||||
height: 2rem;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
|
||||
justify-content: flex-end;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
#user-header button {
|
||||
box-sizing: border-box;
|
||||
margin-right: 0.2rem;
|
||||
height: 100%;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
color: #888888;
|
||||
border: none;
|
||||
background: none;
|
||||
transition: 0.1s ease-in-out;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
#user-header button:hover {
|
||||
color: #555555;
|
||||
}
|
||||
22
react/src/components/Form.css
Normal file
22
react/src/components/Form.css
Normal file
@@ -0,0 +1,22 @@
|
||||
#form {
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
input {
|
||||
border: none;
|
||||
border-bottom: 1px solid #999999;
|
||||
margin-left: 1rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 1rem;
|
||||
color: red;
|
||||
}
|
||||
|
||||
form button {
|
||||
padding: 0.25rem 1rem;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
25
react/src/components/Header.js
Normal file
25
react/src/components/Header.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import FilterLink from '../containers/FilterLink';
|
||||
import LogoutLink from '../containers/LogoutLink';
|
||||
import { VisibilityFilters } from '../actions/todos';
|
||||
import Lists from '../components/Lists';
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<div id="header">
|
||||
<div id="user-header">
|
||||
<LogoutLink>logout</LogoutLink>
|
||||
</div>
|
||||
<div id="lists-header">
|
||||
<Lists />
|
||||
<div id="filters">
|
||||
<FilterLink filter={VisibilityFilters.SHOW_ALL}>all</FilterLink>
|
||||
<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>active</FilterLink>
|
||||
<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>
|
||||
completed
|
||||
</FilterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
react/src/components/Input.js
Normal file
34
react/src/components/Input.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function Input(props) {
|
||||
let input;
|
||||
|
||||
function submit() {
|
||||
if (input.value.trim() !== '') {
|
||||
props.onClick(input.value);
|
||||
}
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="inputs">
|
||||
<input
|
||||
ref={node => {
|
||||
input = node;
|
||||
}}
|
||||
id="input"
|
||||
type="text"
|
||||
/>
|
||||
<button id="add" onClick={() => submit()}>
|
||||
add
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Input.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Input;
|
||||
44
react/src/components/ListActions.js
Normal file
44
react/src/components/ListActions.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { faTrash, faEdit, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function ListActions({
|
||||
startCreateList,
|
||||
removeList,
|
||||
startEditList,
|
||||
creating,
|
||||
list,
|
||||
}) {
|
||||
const editRemoveButtons = list
|
||||
? [
|
||||
<button onClick={() => removeList()}>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</button>,
|
||||
<button onClick={() => startEditList()}>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</button>,
|
||||
]
|
||||
: null;
|
||||
return (
|
||||
<div id="listactions">
|
||||
<button onClick={() => startCreateList()}>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</button>
|
||||
{!list && !creating ? 'add list' : null}
|
||||
{editRemoveButtons}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ListActions.defaultProps = {
|
||||
list: '',
|
||||
};
|
||||
|
||||
ListActions.propTypes = {
|
||||
startCreateList: PropTypes.func.isRequired,
|
||||
removeList: PropTypes.func.isRequired,
|
||||
startEditList: PropTypes.func.isRequired,
|
||||
creating: PropTypes.bool.isRequired,
|
||||
list: PropTypes.string,
|
||||
};
|
||||
12
react/src/components/Lists.js
Normal file
12
react/src/components/Lists.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import ListActionsContainer from '../containers/ListActionsContainer';
|
||||
import SelectorContainer from '../containers/SelectorContainer';
|
||||
|
||||
export default function Lists() {
|
||||
return (
|
||||
<div id="lists">
|
||||
<ListActionsContainer />
|
||||
<SelectorContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
react/src/components/Selector.css
Normal file
50
react/src/components/Selector.css
Normal file
@@ -0,0 +1,50 @@
|
||||
#listselector {
|
||||
display: flex;
|
||||
margin-left: 0.2rem;
|
||||
overflow: hidden;
|
||||
align-self: center;
|
||||
background-color: #fbfbfb;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#listselector input {
|
||||
padding: 0;
|
||||
color: black;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: #fbfbfb;
|
||||
border-bottom: 1px solid #888888;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
#listselector button {
|
||||
width: 20%;
|
||||
font-size: 0.9rem;
|
||||
color: #1b881b;
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0.1rem 0.3rem;
|
||||
padding: 0.3rem 0.7em;
|
||||
}
|
||||
|
||||
#listselector select {
|
||||
max-width: 100%;
|
||||
color: black;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
|
||||
#listselector select option {
|
||||
max-width: 100%;
|
||||
color: black;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
86
react/src/components/Selector.js
Normal file
86
react/src/components/Selector.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPlus, faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import './Selector.css';
|
||||
|
||||
export default function Selector({
|
||||
lists,
|
||||
list,
|
||||
onChange,
|
||||
editing,
|
||||
creating,
|
||||
addList,
|
||||
editList,
|
||||
dirty,
|
||||
}) {
|
||||
if (creating) {
|
||||
let input = null;
|
||||
return (
|
||||
<div id="listselector">
|
||||
<input
|
||||
ref={node => {
|
||||
input = node;
|
||||
}}
|
||||
id="input"
|
||||
type="text"
|
||||
/>
|
||||
<button onClick={() => addList(input.value)}>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (editing) {
|
||||
let input = null;
|
||||
return (
|
||||
<div id="listselector">
|
||||
<input
|
||||
ref={node => {
|
||||
input = node;
|
||||
}}
|
||||
defaultValue={lists.lists[list].name}
|
||||
id="input"
|
||||
type="text"
|
||||
/>
|
||||
<button onClick={() => editList(input.value)}>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (list === null && dirty) {
|
||||
return <div id="listselector">loading</div>;
|
||||
}
|
||||
const listElements = Object.values(lists.lists).map(elem => (
|
||||
<option key={elem.id} value={elem.id}>
|
||||
{elem.name}
|
||||
</option>
|
||||
));
|
||||
if (list) {
|
||||
return (
|
||||
<div id="listselector">
|
||||
<select value={list} onChange={e => onChange(e.target.value)}>
|
||||
{listElements}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Selector.defaultProps = {
|
||||
list: '',
|
||||
};
|
||||
|
||||
Selector.propTypes = {
|
||||
list: PropTypes.string,
|
||||
editing: PropTypes.bool.isRequired,
|
||||
creating: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
editList: PropTypes.func.isRequired,
|
||||
addList: PropTypes.func.isRequired,
|
||||
lists: PropTypes.object.isRequired,
|
||||
dirty: PropTypes.bool.isRequired,
|
||||
};
|
||||
129
react/src/components/Todo.js
Normal file
129
react/src/components/Todo.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import { faTrash, faEdit, faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { animated } from 'react-spring';
|
||||
|
||||
class Todo extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hover: false,
|
||||
};
|
||||
this.onMouseOver = this.onMouseOver.bind(this);
|
||||
this.onMouseOut = this.onMouseOut.bind(this);
|
||||
this.startEdit = this.startEdit.bind(this);
|
||||
this.stopEdit = this.stopEdit.bind(this);
|
||||
}
|
||||
|
||||
onMouseOver() {
|
||||
this.setState({
|
||||
...this.state,
|
||||
hover: true,
|
||||
});
|
||||
}
|
||||
onMouseOut() {
|
||||
this.setState({
|
||||
...this.state,
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
|
||||
startEdit() {
|
||||
this.setState({
|
||||
...this.state,
|
||||
editing: true,
|
||||
});
|
||||
}
|
||||
stopEdit(value) {
|
||||
this.props.editTodo(value);
|
||||
this.setState({
|
||||
...this.state,
|
||||
editing: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const deleteClasses = ['delete'];
|
||||
const editClasses = ['edit'];
|
||||
if (!this.state.hover) {
|
||||
deleteClasses.push('disabled');
|
||||
editClasses.push('disabled');
|
||||
}
|
||||
|
||||
const todoClasses = ['todo'];
|
||||
if (this.props.todo.completed) {
|
||||
todoClasses.push('done');
|
||||
}
|
||||
|
||||
let input;
|
||||
|
||||
const text = this.state.editing ? (
|
||||
<div className={todoClasses.join(' ')}>
|
||||
<textarea
|
||||
className="todo--input"
|
||||
defaultValue={this.props.todo.text}
|
||||
ref={node => {
|
||||
input = node;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button className={todoClasses.join(' ')} onClick={this.props.toggleTodo}>
|
||||
{this.props.todo.text}
|
||||
</button>
|
||||
);
|
||||
const buttons = this.state.editing
|
||||
? [
|
||||
<animated.button
|
||||
key="save"
|
||||
className="save"
|
||||
onClick={() => this.stopEdit(input.value)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCheck} />
|
||||
</animated.button>,
|
||||
]
|
||||
: [
|
||||
<animated.button
|
||||
key="remove"
|
||||
className={deleteClasses.join(' ')}
|
||||
onClick={this.props.removeTodo}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</animated.button>,
|
||||
<animated.button
|
||||
key="edit"
|
||||
className={editClasses.join(' ')}
|
||||
onClick={this.startEdit}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</animated.button>,
|
||||
];
|
||||
return (
|
||||
<animated.li
|
||||
style={this.props.style}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onFocus={this.onMouseOver}
|
||||
onMouseOut={this.onMouseOut}
|
||||
onBlur={this.onMouseOut}
|
||||
>
|
||||
{buttons}
|
||||
{text}
|
||||
</animated.li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Todo.propTypes = {
|
||||
todo: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
completed: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
removeTodo: PropTypes.func.isRequired,
|
||||
toggleTodo: PropTypes.func.isRequired,
|
||||
editTodo: PropTypes.func.isRequired,
|
||||
style: PropTypes.shape({ height: PropTypes.object.isRequired }).isRequired,
|
||||
};
|
||||
|
||||
export default Todo;
|
||||
49
react/src/components/TodoList.js
Normal file
49
react/src/components/TodoList.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Transition } from 'react-spring';
|
||||
|
||||
import Todo from './Todo';
|
||||
|
||||
export default function TodosContainer({
|
||||
todos,
|
||||
toggleTodo,
|
||||
removeTodo,
|
||||
editTodo,
|
||||
}) {
|
||||
return (
|
||||
<ul id="list">
|
||||
<Transition
|
||||
native
|
||||
items={todos}
|
||||
keys={todo => todo.id}
|
||||
from={{ height: 0, borderColor: '#f0f0f0', opacity: 0.9 }}
|
||||
enter={{ height: 60, borderColor: '#f0f0f0', opacity: 1 }}
|
||||
leave={{ height: 0, borderColor: '#ffffff', opacity: 0.5 }}
|
||||
>
|
||||
{todos.map(todo => styles => (
|
||||
<Todo
|
||||
key={todo.id}
|
||||
todo={todo}
|
||||
style={styles}
|
||||
toggleTodo={() => toggleTodo(todo.id)}
|
||||
removeTodo={() => removeTodo(todo.id)}
|
||||
editTodo={text => editTodo(todo.id, text)}
|
||||
/>
|
||||
))}
|
||||
</Transition>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
TodosContainer.propTypes = {
|
||||
todos: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
text: PropTypes.string.isRequired,
|
||||
completed: PropTypes.bool.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
removeTodo: PropTypes.func.isRequired,
|
||||
toggleTodo: PropTypes.func.isRequired,
|
||||
editTodo: PropTypes.func.isRequired,
|
||||
};
|
||||
27
react/src/components/Todos.js
Normal file
27
react/src/components/Todos.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import InputContainer from '../containers/InputContainer';
|
||||
import TodoListContainer from '../containers/TodoListContainer';
|
||||
import Header from './Header';
|
||||
|
||||
export default function Todos({ user, loadLists, history }) {
|
||||
if (user.user) {
|
||||
loadLists();
|
||||
} else if (!user.dirty) {
|
||||
history.push('/login');
|
||||
}
|
||||
return (
|
||||
<div id="todos">
|
||||
<Header />
|
||||
<InputContainer />
|
||||
<TodoListContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Todos.propTypes = {
|
||||
loadLists: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
};
|
||||
30
react/src/components/user/InputField.js
Normal file
30
react/src/components/user/InputField.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function InputField({
|
||||
required,
|
||||
input,
|
||||
label,
|
||||
meta: { touched, error },
|
||||
type,
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor={input.name}>
|
||||
{label} <input required={required} {...input} type={type} />
|
||||
</label>
|
||||
{touched && error && <span className="error">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
InputField.propTypes = {
|
||||
required: PropTypes.bool.isRequired,
|
||||
input: PropTypes.any.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
meta: PropTypes.shape({
|
||||
touched: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
}).isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
};
|
||||
81
react/src/components/user/LoginForm.js
Normal file
81
react/src/components/user/LoginForm.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import InputField from './InputField';
|
||||
|
||||
import '../Form.css';
|
||||
import UserErrors from './UserErrors';
|
||||
|
||||
import { login, reset } from '../../actions/user';
|
||||
|
||||
function LoginForm({ handleSubmit, onLogin, user, history, resetUser }) {
|
||||
if (user.user) {
|
||||
history.push('/');
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div id="user-header">
|
||||
<button
|
||||
onClick={() => {
|
||||
resetUser();
|
||||
history.push('/signup');
|
||||
}}
|
||||
>
|
||||
signup
|
||||
</button>
|
||||
</div>
|
||||
<div id="form">
|
||||
<UserErrors user={user} />
|
||||
<form onSubmit={handleSubmit(onLogin)}>
|
||||
<Field
|
||||
label="username"
|
||||
name="username"
|
||||
required
|
||||
component={InputField}
|
||||
type="text"
|
||||
/>
|
||||
<Field
|
||||
label="password"
|
||||
name="password"
|
||||
required
|
||||
component={InputField}
|
||||
type="password"
|
||||
/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LoginForm.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
onLogin: PropTypes.func.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
history: PropTypes.any.isRequired,
|
||||
resetUser: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
user: state.user,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
resetUser: () => dispatch(reset()),
|
||||
onLogin: ({ username, password }) =>
|
||||
dispatch(login({ username, password })),
|
||||
};
|
||||
}
|
||||
|
||||
export default reduxForm({
|
||||
form: 'loginForm',
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
})(withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginForm)));
|
||||
99
react/src/components/user/SignupForm.js
Normal file
99
react/src/components/user/SignupForm.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import { Field, reduxForm } from 'redux-form';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import InputField from './InputField';
|
||||
import UserErrors from './UserErrors';
|
||||
|
||||
import '../Form.css';
|
||||
|
||||
import { signup, reset } from '../../actions/user';
|
||||
|
||||
function validate(values) {
|
||||
const errors = {};
|
||||
if (values.password !== values.passwordRepeat) {
|
||||
errors.passwordRepeat = 'Invalid';
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function SignupForm({ handleSubmit, onSignup, user, history, resetUser }) {
|
||||
if (user.user) {
|
||||
history.push('/');
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div id="user-header">
|
||||
<button
|
||||
onClick={() => {
|
||||
resetUser();
|
||||
history.push('/login');
|
||||
}}
|
||||
>
|
||||
login
|
||||
</button>
|
||||
</div>
|
||||
<div id="form">
|
||||
<UserErrors user={user} />
|
||||
<form onSubmit={handleSubmit(onSignup)}>
|
||||
<Field
|
||||
label="username"
|
||||
name="username"
|
||||
required
|
||||
component={InputField}
|
||||
type="text"
|
||||
/>
|
||||
<Field
|
||||
label="password"
|
||||
name="password"
|
||||
required
|
||||
component={InputField}
|
||||
type="password"
|
||||
/>
|
||||
<Field
|
||||
label="repeat pasword"
|
||||
name="passwordRepeat"
|
||||
required
|
||||
component={InputField}
|
||||
type="password"
|
||||
/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SignupForm.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
onSignup: PropTypes.func.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
history: PropTypes.any.isRequired,
|
||||
resetUser: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
user: state.user,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
resetUser: () => dispatch(reset()),
|
||||
onSignup: ({ username, password }) =>
|
||||
dispatch(signup({ username, password })),
|
||||
};
|
||||
}
|
||||
|
||||
export default reduxForm({
|
||||
form: 'signupForm',
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
passwordRepeat: '',
|
||||
},
|
||||
validate,
|
||||
})(withRouter(connect(mapStateToProps, mapDispatchToProps)(SignupForm)));
|
||||
32
react/src/components/user/UserErrors.js
Normal file
32
react/src/components/user/UserErrors.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
function UserErrors({ user }) {
|
||||
let errors = [];
|
||||
if (user.errors) {
|
||||
if (user.errors.name === 'AuthenticationError') {
|
||||
errors.push(
|
||||
<div key="wrongauth" className="error">
|
||||
Wrong username or password
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
if (user.errors.name === 'ValidationError') {
|
||||
if (user.errors.message.split(' ').includes('unique.')) {
|
||||
errors.push(
|
||||
<div key="exists" className="error">
|
||||
User already exists
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
errors.push(
|
||||
<div key="invalid" className="error">
|
||||
Validation error
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors || null;
|
||||
}
|
||||
|
||||
export default UserErrors;
|
||||
19
react/src/containers/AppContainer.js
Normal file
19
react/src/containers/AppContainer.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import App from '../components/App';
|
||||
|
||||
import { loadUser } from '../actions/user';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
user: state.user,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
loadUser: () => dispatch(loadUser()),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||
42
react/src/containers/FilterLink.js
Normal file
42
react/src/containers/FilterLink.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { connect } from 'react-redux';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { setVisibilityFilter } from '../actions/todos';
|
||||
|
||||
function Link({ active, onClick, children }) {
|
||||
const classes = ['filter'];
|
||||
if (active) {
|
||||
classes.push('filter--active');
|
||||
}
|
||||
return (
|
||||
<button
|
||||
className={classes.join(' ')}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Link.propTypes = {
|
||||
active: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
active: ownProps.filter === state.visibilityFilter,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch, ownProps) {
|
||||
return {
|
||||
onClick: () => dispatch(setVisibilityFilter(ownProps.filter)),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Link);
|
||||
12
react/src/containers/InputContainer.js
Normal file
12
react/src/containers/InputContainer.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Input from '../components/Input';
|
||||
import { addTodo } from '../actions/todos';
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onClick: text => dispatch(addTodo(text)),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Input);
|
||||
19
react/src/containers/ListActionsContainer.js
Normal file
19
react/src/containers/ListActionsContainer.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ListActions from '../components/ListActions';
|
||||
import { startCreateList, startEditList, removeList } from '../actions/lists';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
list: state.lists.list,
|
||||
creating: state.lists.creating,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
startCreateList: () => dispatch(startCreateList()),
|
||||
startEditList: () => dispatch(startEditList()),
|
||||
removeList: () => dispatch(removeList()),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ListActions);
|
||||
33
react/src/containers/LogoutLink.js
Normal file
33
react/src/containers/LogoutLink.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { logout } from '../actions/user';
|
||||
|
||||
function Link({ onClick, children }) {
|
||||
const classes = ['logout'];
|
||||
return (
|
||||
<button
|
||||
className={classes.join(' ')}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
Link.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onClick: () => dispatch(logout()),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Link);
|
||||
27
react/src/containers/SelectorContainer.js
Normal file
27
react/src/containers/SelectorContainer.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { connect } from 'react-redux';
|
||||
import Selector from '../components/Selector';
|
||||
import { changeList, addList, editList } from '../actions/lists';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const editing =
|
||||
state.lists.list && !state.lists.dirty
|
||||
? state.lists.lists[state.lists.list].editing
|
||||
: false;
|
||||
return {
|
||||
lists: state.lists,
|
||||
list: state.lists.list,
|
||||
editing,
|
||||
creating: state.lists.creating,
|
||||
dirty: state.lists.dirty,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onChange: list => dispatch(changeList(list)),
|
||||
addList: name => dispatch(addList(name)),
|
||||
editList: (id, name) => dispatch(editList(id, name)),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Selector);
|
||||
37
react/src/containers/TodoListContainer.js
Normal file
37
react/src/containers/TodoListContainer.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { connect } from 'react-redux';
|
||||
import TodoList from '../components/TodoList';
|
||||
import { toggleTodo, removeTodo, editTodo } from '../actions/todos';
|
||||
|
||||
import getVisibleTodos from './getVisibleTodos';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const { list } = state.lists;
|
||||
try {
|
||||
const listObj = state.lists.lists[list];
|
||||
const listTodos = state.lists.lists[list].todos;
|
||||
|
||||
return {
|
||||
list,
|
||||
todos: getVisibleTodos(listTodos, state.visibilityFilter),
|
||||
dirty: listObj.dirty,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
list: '',
|
||||
todos: [],
|
||||
dirty: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
toggleTodo: id => dispatch(toggleTodo(id)),
|
||||
removeTodo: id => dispatch(removeTodo(id)),
|
||||
editTodo: (id, text) => dispatch(editTodo(id, text)),
|
||||
};
|
||||
}
|
||||
|
||||
const TodosContainer = connect(mapStateToProps, mapDispatchToProps)(TodoList);
|
||||
|
||||
export default TodosContainer;
|
||||
20
react/src/containers/TodosContainer.js
Normal file
20
react/src/containers/TodosContainer.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import Todos from '../components/Todos';
|
||||
|
||||
import { loadLists } from '../actions/lists';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
user: state.user,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
loadLists: () => dispatch(loadLists()),
|
||||
};
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Todos));
|
||||
14
react/src/containers/getVisibleTodos.js
Normal file
14
react/src/containers/getVisibleTodos.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { VisibilityFilters } from '../actions/todos';
|
||||
|
||||
export default function getVisibleTodos(todos, filter) {
|
||||
switch (filter) {
|
||||
case VisibilityFilters.SHOW_ALL:
|
||||
return todos;
|
||||
case VisibilityFilters.SHOW_ACTIVE:
|
||||
return todos.filter(todo => !todo.completed);
|
||||
case VisibilityFilters.SHOW_COMPLETED:
|
||||
return todos.filter(todo => todo.completed);
|
||||
default:
|
||||
return todos;
|
||||
}
|
||||
}
|
||||
19
react/src/index.js
Normal file
19
react/src/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import thunk from 'redux-thunk';
|
||||
import { Provider } from 'react-redux';
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
import AppContainer from './containers/AppContainer';
|
||||
import registerServiceWorker from './registerServiceWorker';
|
||||
import todoApp from './reducers';
|
||||
|
||||
const store = createStore(todoApp, applyMiddleware(thunk));
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<AppContainer />
|
||||
</Provider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
||||
registerServiceWorker();
|
||||
15
react/src/reducers/index.js
Normal file
15
react/src/reducers/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
|
||||
import lists from './lists';
|
||||
import visibilityFilter from './visibilityFilter';
|
||||
import user from './user';
|
||||
|
||||
const todoApp = combineReducers({
|
||||
lists,
|
||||
visibilityFilter,
|
||||
form: formReducer,
|
||||
user,
|
||||
});
|
||||
|
||||
export default todoApp;
|
||||
75
react/src/reducers/list.js
Normal file
75
react/src/reducers/list.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
ADD_TODO,
|
||||
REMOVE_TODO,
|
||||
TOGGLE_TODO,
|
||||
RECIEVE_TODOS,
|
||||
REQUEST_TODOS,
|
||||
INVALIDATE_TODOS,
|
||||
VALIDATE_TODOS,
|
||||
EDIT_TODO,
|
||||
} from '../actions/todos';
|
||||
|
||||
export default function todos(
|
||||
state = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
todos: null,
|
||||
editing: false,
|
||||
},
|
||||
action,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case RECIEVE_TODOS:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
fetching: false,
|
||||
todos: action.todos,
|
||||
};
|
||||
case ADD_TODO:
|
||||
return {
|
||||
...state,
|
||||
todos: [action.todo, ...state.todos],
|
||||
};
|
||||
case INVALIDATE_TODOS:
|
||||
return {
|
||||
...state,
|
||||
dirty: true,
|
||||
};
|
||||
case VALIDATE_TODOS:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
};
|
||||
case EDIT_TODO:
|
||||
return {
|
||||
...state,
|
||||
todos: state.todos.map(
|
||||
todo => (todo.id === action.id ? action.todo : todo),
|
||||
),
|
||||
};
|
||||
case REQUEST_TODOS:
|
||||
return {
|
||||
...state,
|
||||
fetching: true,
|
||||
};
|
||||
case REMOVE_TODO:
|
||||
return {
|
||||
...state,
|
||||
todos: state.todos.filter(todo => todo.id !== action.id),
|
||||
};
|
||||
case TOGGLE_TODO: {
|
||||
return {
|
||||
...state,
|
||||
todos: state.todos.map(
|
||||
todo =>
|
||||
todo.id === action.id
|
||||
? { ...todo, completed: !todo.completed }
|
||||
: todo,
|
||||
),
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
122
react/src/reducers/lists.js
Normal file
122
react/src/reducers/lists.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
CHANGE_LIST,
|
||||
INVALIDATE_LISTS,
|
||||
VALIDATE_LISTS,
|
||||
REQUEST_LISTS,
|
||||
RECIEVE_LISTS,
|
||||
ADD_LIST,
|
||||
REMOVE_LIST,
|
||||
EDIT_LIST_NAME,
|
||||
START_CREATE_LIST,
|
||||
START_EDIT_LIST,
|
||||
} from '../actions/lists';
|
||||
import {
|
||||
ADD_TODO,
|
||||
INVALIDATE_TODOS,
|
||||
VALIDATE_TODOS,
|
||||
REQUEST_TODOS,
|
||||
RECIEVE_TODOS,
|
||||
REMOVE_TODO,
|
||||
TOGGLE_TODO,
|
||||
EDIT_TODO,
|
||||
} from '../actions/todos';
|
||||
import list from './list';
|
||||
|
||||
export default function lists(
|
||||
state = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
lists: null,
|
||||
creating: false,
|
||||
list: null,
|
||||
},
|
||||
action,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case CHANGE_LIST:
|
||||
return { ...state, list: action.list };
|
||||
case RECIEVE_LISTS:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
fetching: false,
|
||||
lists: action.lists,
|
||||
};
|
||||
case START_CREATE_LIST:
|
||||
return {
|
||||
...state,
|
||||
creating: true,
|
||||
};
|
||||
case ADD_LIST:
|
||||
return {
|
||||
...state,
|
||||
creating: false,
|
||||
lists: { ...state.lists, [action.list.id]: action.list },
|
||||
};
|
||||
case REMOVE_LIST: {
|
||||
const newLists = { ...state.lists };
|
||||
delete newLists[action.id];
|
||||
return {
|
||||
...state,
|
||||
lists: newLists,
|
||||
};
|
||||
}
|
||||
case START_EDIT_LIST: {
|
||||
return {
|
||||
...state,
|
||||
lists: {
|
||||
...state.lists,
|
||||
[state.list]: {
|
||||
...state.lists[state.list],
|
||||
editing: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case EDIT_LIST_NAME: {
|
||||
return {
|
||||
...state,
|
||||
lists: {
|
||||
...state.lists,
|
||||
[action.id]: {
|
||||
...state.lists[action.id],
|
||||
name: action.name,
|
||||
editing: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case INVALIDATE_LISTS:
|
||||
return {
|
||||
...state,
|
||||
dirty: true,
|
||||
};
|
||||
case VALIDATE_LISTS:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
};
|
||||
case REQUEST_LISTS:
|
||||
return {
|
||||
...state,
|
||||
fetching: true,
|
||||
};
|
||||
case RECIEVE_TODOS:
|
||||
case ADD_TODO:
|
||||
case EDIT_TODO:
|
||||
case INVALIDATE_TODOS:
|
||||
case VALIDATE_TODOS:
|
||||
case REQUEST_TODOS:
|
||||
case REMOVE_TODO:
|
||||
case TOGGLE_TODO:
|
||||
return {
|
||||
...state,
|
||||
lists: {
|
||||
...state.lists,
|
||||
[state.list]: list(state.lists[state.list], action),
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
65
react/src/reducers/user.js
Normal file
65
react/src/reducers/user.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
LOGIN_SUCCESS,
|
||||
LOGIN_FAIL,
|
||||
START_LOGIN,
|
||||
LOGOUT,
|
||||
SIGNUP_FAIL,
|
||||
SIGNUP_SUCCESS,
|
||||
VALIDATE_USER,
|
||||
RESET_USER,
|
||||
} from '../actions/user';
|
||||
|
||||
export default function user(
|
||||
state = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
user: null,
|
||||
errors: null,
|
||||
},
|
||||
action,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case VALIDATE_USER:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
};
|
||||
case START_LOGIN:
|
||||
return {
|
||||
...state,
|
||||
fetching: true,
|
||||
};
|
||||
case SIGNUP_SUCCESS:
|
||||
case LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
user: action.user,
|
||||
errors: null,
|
||||
dirty: false,
|
||||
fetching: false,
|
||||
};
|
||||
case SIGNUP_FAIL:
|
||||
case LOGIN_FAIL:
|
||||
return {
|
||||
...state,
|
||||
user: null,
|
||||
errors: action.error,
|
||||
dirty: false,
|
||||
fetching: false,
|
||||
};
|
||||
case RESET_USER:
|
||||
return {
|
||||
...state,
|
||||
fetching: false,
|
||||
user: null,
|
||||
errors: null,
|
||||
};
|
||||
case LOGOUT:
|
||||
return {
|
||||
...state,
|
||||
user: null,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
12
react/src/reducers/visibilityFilter.js
Normal file
12
react/src/reducers/visibilityFilter.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { VisibilityFilters, SET_VISIBILITY_FILTER } from '../actions/todos';
|
||||
|
||||
const { SHOW_ALL } = VisibilityFilters;
|
||||
|
||||
export default function visibilityFilter(state = SHOW_ALL, action) {
|
||||
switch (action.type) {
|
||||
case SET_VISIBILITY_FILTER:
|
||||
return action.filter;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
109
react/src/registerServiceWorker.js
Normal file
109
react/src/registerServiceWorker.js
Normal file
@@ -0,0 +1,109 @@
|
||||
// In production, we register a service worker to serve assets from local cache.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on the "N+1" visit to a page, since previously
|
||||
// cached resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
|
||||
// This link also includes instructions on opting out of this behavior.
|
||||
|
||||
const isLocalhost = Boolean(window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/));
|
||||
|
||||
export default function register() {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Lets check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log('This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://goo.gl/SC7cgQ');
|
||||
});
|
||||
} else {
|
||||
// Is not local host. Just register service worker
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the old content will have been purged and
|
||||
// the fresh content will have been added to the cache.
|
||||
// It's the perfect time to display a "New content is
|
||||
// available; please refresh." message in your web app.
|
||||
console.log('New content is available; please refresh.');
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl)
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
if (
|
||||
response.status === 404 ||
|
||||
response.headers.get('content-type').indexOf('javascript') === -1
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('No internet connection found. App is running in offline mode.');
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user