mirror of
https://github.com/usatiuk/ustk-todolist.git
synced 2025-10-28 23:57:49 +01:00
rename 'react' to 'client'
This commit is contained in:
30
client/.eslintrc.json
Normal file
30
client/.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
client/.gitignore
vendored
Normal file
21
client/.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
client/.prettierrc
Normal file
4
client/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
2444
client/README.md
Normal file
2444
client/README.md
Normal file
File diff suppressed because it is too large
Load Diff
12630
client/package-lock.json
generated
Normal file
12630
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
client/package.json
Normal file
44
client/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "todolist-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^3.0.3",
|
||||
"@material-ui/icons": "^3.0.1",
|
||||
"@redux-offline/redux-offline": "^2.4.0",
|
||||
"localforage": "^1.7.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.5.0",
|
||||
"react-dom": "^16.5.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-router-redux": "^4.0.8",
|
||||
"react-scripts": "1.1.5",
|
||||
"react-spring": "^5.7.2",
|
||||
"redux": "^4.0.0",
|
||||
"redux-form": "^7.4.2",
|
||||
"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": {
|
||||
"/__": {
|
||||
"target": "http://localhost:4000",
|
||||
"ws": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^5.5.0",
|
||||
"eslint-config-airbnb": "^17.1.0",
|
||||
"eslint-config-prettier": "^3.0.1",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-jest": "^21.22.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.1.1",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"prettier-eslint": "^8.8.2"
|
||||
}
|
||||
}
|
||||
BIN
client/public/favicon.ico
Normal file
BIN
client/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
44
client/public/index.html
Normal file
44
client/public/index.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<!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="#4ba6fa">
|
||||
<!--
|
||||
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">
|
||||
<link rel="preconnect" href="http://fonts.googleapis.com">
|
||||
<!--
|
||||
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>Todolist</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
client/public/manifest.json
Normal file
15
client/public/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "Todolist",
|
||||
"name": "Simple todo list",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": "./",
|
||||
"display": "standalone",
|
||||
"theme_color": "#4ba6fa",
|
||||
"background_color": "#fbfbfb"
|
||||
}
|
||||
38
client/src/actions/defs.js
Normal file
38
client/src/actions/defs.js
Normal file
@@ -0,0 +1,38 @@
|
||||
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';
|
||||
export const STOP_CREATE_LIST = 'STOP_CREATE_LIST';
|
||||
export const STOP_EDIT_LIST = 'STOP_EDIT_LIST';
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
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';
|
||||
162
client/src/actions/lists.js
Normal file
162
client/src/actions/lists.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import {
|
||||
REQUEST_LISTS,
|
||||
RECIEVE_LISTS,
|
||||
CHANGE_LIST,
|
||||
START_CREATE_LIST,
|
||||
START_EDIT_LIST,
|
||||
STOP_CREATE_LIST,
|
||||
STOP_EDIT_LIST,
|
||||
ADD_LIST,
|
||||
INVALIDATE_LISTS,
|
||||
REMOVE_LIST,
|
||||
EDIT_LIST_NAME,
|
||||
RECIEVE_TODOS,
|
||||
} from './defs';
|
||||
|
||||
import { API_ROOT, getToken, mongoObjectId } from './util';
|
||||
|
||||
function requestLists() {
|
||||
return { type: REQUEST_LISTS };
|
||||
}
|
||||
function recieveLists(lists) {
|
||||
return { type: RECIEVE_LISTS, 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 };
|
||||
}
|
||||
export function stopCreateList() {
|
||||
return { type: STOP_CREATE_LIST };
|
||||
}
|
||||
export function stopEditList() {
|
||||
return { type: STOP_EDIT_LIST };
|
||||
}
|
||||
|
||||
export function addList(name) {
|
||||
return async dispatch => {
|
||||
const id = mongoObjectId();
|
||||
dispatch({
|
||||
type: ADD_LIST,
|
||||
list: {
|
||||
name,
|
||||
id,
|
||||
todos: [],
|
||||
},
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/lists`,
|
||||
body: JSON.stringify({ name, id }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function removeList() {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { list } = state.lists;
|
||||
dispatch({
|
||||
type: REMOVE_LIST,
|
||||
list,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/lists/${list}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'DELETE',
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function editList(name) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { list } = state.lists;
|
||||
dispatch({
|
||||
type: EDIT_LIST_NAME,
|
||||
list,
|
||||
name,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/lists/${list}`,
|
||||
body: JSON.stringify({ name }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'PATCH',
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTodos(lists) {
|
||||
return lists.reduce((todos, list) => {
|
||||
const listTodosObj = list.todos.reduce(
|
||||
(listTodos, todo) => ({
|
||||
...listTodos,
|
||||
[todo.id]: { ...todo },
|
||||
}),
|
||||
{},
|
||||
);
|
||||
return { ...todos, ...listTodosObj };
|
||||
}, {});
|
||||
}
|
||||
|
||||
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, curList) => {
|
||||
const newObj = { ...obj };
|
||||
newObj[curList.id] = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
editing: false,
|
||||
...curList,
|
||||
todos: curList.todos.map(todo => todo.id),
|
||||
};
|
||||
return newObj;
|
||||
}, {});
|
||||
|
||||
dispatch({ type: RECIEVE_TODOS, todos: normalizeTodos(lists) });
|
||||
dispatch(recieveLists(listsObj));
|
||||
};
|
||||
}
|
||||
137
client/src/actions/todos.js
Normal file
137
client/src/actions/todos.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import {
|
||||
REQUEST_TODOS,
|
||||
RECIEVE_TODOS,
|
||||
ADD_TODO,
|
||||
REMOVE_TODO,
|
||||
TOGGLE_TODO,
|
||||
EDIT_TODO,
|
||||
INVALIDATE_LISTS,
|
||||
} from './defs';
|
||||
|
||||
import { API_ROOT, getToken, mongoObjectId } from './util';
|
||||
|
||||
export function fetchTodos() {
|
||||
return async dispatch => {
|
||||
dispatch({ type: REQUEST_TODOS });
|
||||
const response = await fetch(`${API_ROOT}/todos`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
},
|
||||
});
|
||||
const json = await response.json();
|
||||
const todos = json.data;
|
||||
dispatch({ type: RECIEVE_TODOS, todos });
|
||||
};
|
||||
}
|
||||
|
||||
export function addTodo(text) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { list } = state.lists;
|
||||
const id = mongoObjectId();
|
||||
if (list) {
|
||||
dispatch({
|
||||
type: ADD_TODO,
|
||||
todo: {
|
||||
text,
|
||||
id,
|
||||
completed: false,
|
||||
},
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/lists/${list}/todos`,
|
||||
body: JSON.stringify({ text, id }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function removeTodo(id) {
|
||||
return async dispatch => {
|
||||
dispatch({
|
||||
type: REMOVE_TODO,
|
||||
id,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/todos/${id}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'DELETE',
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleTodo(id) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const todoObj = state.todos.todos[id];
|
||||
const completed = !todoObj.completed;
|
||||
dispatch({
|
||||
type: TOGGLE_TODO,
|
||||
id,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/todos/${id}`,
|
||||
body: JSON.stringify({ completed }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'PATCH',
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function editTodo(id, text) {
|
||||
return async dispatch => {
|
||||
dispatch({
|
||||
type: EDIT_TODO,
|
||||
id,
|
||||
text,
|
||||
meta: {
|
||||
offline: {
|
||||
effect: {
|
||||
url: `${API_ROOT}/todos/${id}`,
|
||||
body: JSON.stringify({ text }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'PATCH',
|
||||
},
|
||||
rollback: {
|
||||
type: INVALIDATE_LISTS,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
133
client/src/actions/user.js
Normal file
133
client/src/actions/user.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import {
|
||||
START_LOGIN,
|
||||
LOGIN_SUCCESS,
|
||||
LOGIN_FAIL,
|
||||
VALIDATE_USER,
|
||||
SIGNUP_SUCCESS,
|
||||
SIGNUP_FAIL,
|
||||
RESET_USER,
|
||||
LOGOUT,
|
||||
} from './defs';
|
||||
|
||||
import { API_ROOT, getToken, setToken } from './util';
|
||||
import { fetchLists } from './lists';
|
||||
|
||||
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) {
|
||||
dispatch(loginSuccess(json.data));
|
||||
dispatch(fetchLists());
|
||||
} 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) {
|
||||
setToken(json.data.jwt);
|
||||
dispatch(loginSuccess(json.data));
|
||||
dispatch(fetchLists());
|
||||
} else {
|
||||
dispatch(loginFail(json.error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loginJWT(jwt) {
|
||||
return async dispatch => {
|
||||
dispatch(startLogin());
|
||||
const response = await fetch(`${API_ROOT}/users/user`, {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
method: 'GET',
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.success) {
|
||||
setToken(jwt);
|
||||
dispatch(loginSuccess(json.data));
|
||||
dispatch(fetchLists());
|
||||
} 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) {
|
||||
setToken(json.data.jwt);
|
||||
dispatch(signupSuccess(json.data));
|
||||
dispatch(fetchLists());
|
||||
} else {
|
||||
dispatch(signupFail(json.error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
return { type: RESET_USER };
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return async dispatch => {
|
||||
dispatch({ type: LOGOUT });
|
||||
};
|
||||
}
|
||||
23
client/src/actions/util.js
Normal file
23
client/src/actions/util.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export const API_ROOT = '/__';
|
||||
|
||||
let token = null;
|
||||
|
||||
export function setToken(_token) {
|
||||
token = _token;
|
||||
}
|
||||
|
||||
export function getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
export function mongoObjectId() {
|
||||
// eslint-disable-next-line
|
||||
const timestamp = ((new Date().getTime() / 1000) | 0).toString(16);
|
||||
return (
|
||||
timestamp +
|
||||
'xxxxxxxxxxxxxxxx'
|
||||
// eslint-disable-next-line
|
||||
.replace(/[x]/g, () => ((Math.random() * 16) | 0).toString(16))
|
||||
.toLowerCase()
|
||||
);
|
||||
}
|
||||
5
client/src/actions/visibilityFilter.js
Normal file
5
client/src/actions/visibilityFilter.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SET_VISIBILITY_FILTER } from './defs';
|
||||
|
||||
export default function setVisibilityFilter(filter) {
|
||||
return { type: SET_VISIBILITY_FILTER, filter };
|
||||
}
|
||||
208
client/src/components/App.css
Normal file
208
client/src/components/App.css
Normal file
@@ -0,0 +1,208 @@
|
||||
#lists-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
|
||||
#lists {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
width: 80%;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.loading {
|
||||
align-self: center;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
#listactions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: 0.2rem 0.1rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
#listactions button {
|
||||
color: #555555;
|
||||
background: none;
|
||||
border: none;
|
||||
margin: 0.1rem 0.3rem;
|
||||
padding: 0.3rem 0.7em;
|
||||
}
|
||||
|
||||
#listactions .backbutton {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
#filters {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
align-self: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
position: sticky;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
#filters button {
|
||||
min-width: 3rem;
|
||||
}
|
||||
|
||||
#inputs {
|
||||
box-shadow: 0 2px 7px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
height: 2.5rem;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
#input::placeholder {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
#input:focus::placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#add {
|
||||
color: black;
|
||||
flex-grow: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
max-width: 2rem;
|
||||
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;
|
||||
text-align: center;
|
||||
border: none;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: none;
|
||||
width: 2rem;
|
||||
font-size: 1rem;
|
||||
transition: 0.17s ease-in-out;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 3px 0 6px -3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* make buttons bigger on smartphones */
|
||||
@media only screen and (max-width: 600px) {
|
||||
li button {
|
||||
padding: 0 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
li button.todo {
|
||||
padding: 0.5rem;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
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.1s ease-in-out;
|
||||
font-family: Roboto;
|
||||
}
|
||||
|
||||
textarea.todo--input {
|
||||
background: #fafafa;
|
||||
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%;
|
||||
font-family: 'Roboto';
|
||||
font-size: 1rem;
|
||||
transition: 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
width: 2rem;
|
||||
background-color: #fafafa;
|
||||
color: #555555;
|
||||
box-shadow: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.filter {
|
||||
margin: 0.1rem;
|
||||
color: #555555;
|
||||
border: none;
|
||||
background: none;
|
||||
transition: 0.1s ease-in-out;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
font-family: Roboto;
|
||||
}
|
||||
|
||||
.filter--active {
|
||||
font-weight: 500;
|
||||
color: black;
|
||||
}
|
||||
37
client/src/components/App.js
Normal file
37
client/src/components/App.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { BrowserRouter as Router, Route } from 'react-router-dom';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
|
||||
import './Container.css';
|
||||
import './App.css';
|
||||
|
||||
import TodosView from './todolist/TodosView';
|
||||
import LoginForm from './user/LoginForm';
|
||||
import SignupForm from './user/SignupForm';
|
||||
|
||||
export default class App extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
const { loadUser } = this.props;
|
||||
loadUser();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<CssBaseline />
|
||||
<Router>
|
||||
<div id="container">
|
||||
<Route exact path="/" component={TodosView} />
|
||||
<Route path="/login" component={LoginForm} />
|
||||
<Route path="/signup" component={SignupForm} />
|
||||
</div>
|
||||
</Router>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
loadUser: PropTypes.func.isRequired,
|
||||
};
|
||||
22
client/src/components/AppContainer.js
Normal file
22
client/src/components/AppContainer.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import App from './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);
|
||||
57
client/src/components/Container.css
Normal file
57
client/src/components/Container.css
Normal file
@@ -0,0 +1,57 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500|Material+Icons');
|
||||
|
||||
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.1);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
#container {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#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;
|
||||
font-family: Roboto;
|
||||
}
|
||||
|
||||
#user-header button:hover {
|
||||
color: #555555;
|
||||
}
|
||||
12
client/src/components/Header.js
Normal file
12
client/src/components/Header.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import UserHeader from './user/UserHeader';
|
||||
import Lists from './lists/Lists';
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<div id="header">
|
||||
<UserHeader />
|
||||
<Lists />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
client/src/components/filters/FilterLink.js
Normal file
51
client/src/components/filters/FilterLink.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { connect } from 'react-redux';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ButtonBase } from '@material-ui/core';
|
||||
import setVisibilityFilter from '../../actions/visibilityFilter';
|
||||
|
||||
function Link({ active, onClick, children }) {
|
||||
const classes = ['filter'];
|
||||
if (active) {
|
||||
classes.push('filter--active');
|
||||
}
|
||||
return (
|
||||
<ButtonBase
|
||||
style={{
|
||||
padding: '0 1rem',
|
||||
color: active ? 'black' : '#444444',
|
||||
height: '2rem',
|
||||
}}
|
||||
className={classes.join(' ')}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
17
client/src/components/filters/Filters.js
Normal file
17
client/src/components/filters/Filters.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import FilterLink from './FilterLink';
|
||||
import { VisibilityFilters } from '../../actions/defs';
|
||||
|
||||
function Filters(styles) {
|
||||
return (
|
||||
<div style={styles} id="filters">
|
||||
<FilterLink filter={VisibilityFilters.SHOW_ALL}>all</FilterLink>
|
||||
<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>active</FilterLink>
|
||||
<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>
|
||||
completed
|
||||
</FilterLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Filters;
|
||||
112
client/src/components/lists/ListActions.js
Normal file
112
client/src/components/lists/ListActions.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import BackButton from '@material-ui/icons/ArrowBack';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Transition, config, animated } from 'react-spring';
|
||||
|
||||
const button = {
|
||||
width: 30,
|
||||
height: 30,
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
const icon = {
|
||||
fontSize: 24,
|
||||
};
|
||||
|
||||
export default function ListActions({
|
||||
startCreateList,
|
||||
removeList,
|
||||
startEditList,
|
||||
stopCreateList,
|
||||
stopEditList,
|
||||
creating,
|
||||
editing,
|
||||
list,
|
||||
}) {
|
||||
function back() {
|
||||
if (editing) {
|
||||
stopEditList();
|
||||
}
|
||||
if (creating) {
|
||||
stopCreateList();
|
||||
}
|
||||
}
|
||||
const actions = [];
|
||||
if (!creating && !editing) {
|
||||
actions.push(styles => (
|
||||
<animated.button
|
||||
key="create"
|
||||
style={{ ...button, ...styles }}
|
||||
onClick={() => startCreateList()}
|
||||
>
|
||||
<AddIcon style={icon} />
|
||||
</animated.button>
|
||||
));
|
||||
}
|
||||
if (list && !creating && !editing) {
|
||||
actions.push(styles => (
|
||||
<animated.button
|
||||
key="remove"
|
||||
style={{ ...button, ...styles }}
|
||||
onClick={() => removeList()}
|
||||
>
|
||||
<DeleteIcon style={icon} />
|
||||
</animated.button>
|
||||
));
|
||||
}
|
||||
if (list && !creating && !editing) {
|
||||
actions.push(styles => (
|
||||
<animated.button
|
||||
key="edit"
|
||||
style={{ ...button, ...styles }}
|
||||
onClick={() => startEditList()}
|
||||
>
|
||||
<EditIcon style={icon} />
|
||||
</animated.button>
|
||||
));
|
||||
}
|
||||
if (creating || editing) {
|
||||
actions.push(styles => (
|
||||
<animated.button
|
||||
key="back"
|
||||
style={{ ...button, ...styles }}
|
||||
className="backbutton"
|
||||
onClick={() => back()}
|
||||
>
|
||||
<BackButton style={icon} />
|
||||
</animated.button>
|
||||
));
|
||||
}
|
||||
return (
|
||||
<div id="listactions">
|
||||
<Transition
|
||||
native
|
||||
config={config.stiff}
|
||||
keys={actions.map(action => action({}).key)}
|
||||
from={{ opacity: 0, height: 0, margin: 0, padding: 0 }}
|
||||
enter={{ opacity: 1, height: 30, margin: 0, padding: 0 }}
|
||||
leave={{ opacity: 0, height: 0, margin: 0, padding: 0 }}
|
||||
>
|
||||
{actions}
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ListActions.defaultProps = {
|
||||
list: '',
|
||||
};
|
||||
|
||||
ListActions.propTypes = {
|
||||
startCreateList: PropTypes.func.isRequired,
|
||||
removeList: PropTypes.func.isRequired,
|
||||
startEditList: PropTypes.func.isRequired,
|
||||
creating: PropTypes.bool.isRequired,
|
||||
editing: PropTypes.bool.isRequired,
|
||||
list: PropTypes.string,
|
||||
stopCreateList: PropTypes.func.isRequired,
|
||||
stopEditList: PropTypes.func.isRequired,
|
||||
};
|
||||
31
client/src/components/lists/ListActionsContainer.js
Normal file
31
client/src/components/lists/ListActionsContainer.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ListActions from './ListActions';
|
||||
import {
|
||||
startCreateList,
|
||||
startEditList,
|
||||
removeList,
|
||||
stopCreateList,
|
||||
stopEditList,
|
||||
} from '../../actions/lists';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
list: state.lists.list,
|
||||
creating: state.lists.creating,
|
||||
editing: state.lists.editing,
|
||||
};
|
||||
}
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
startCreateList: () => dispatch(startCreateList()),
|
||||
startEditList: () => dispatch(startEditList()),
|
||||
stopCreateList: () => dispatch(stopCreateList()),
|
||||
stopEditList: () => dispatch(stopEditList()),
|
||||
removeList: () => dispatch(removeList()),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(ListActions);
|
||||
36
client/src/components/lists/Lists.js
Normal file
36
client/src/components/lists/Lists.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { connect } from 'react-redux';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ListActionsContainer from './ListActionsContainer';
|
||||
import SelectorContainer from './SelectorContainer';
|
||||
|
||||
function Lists({ userLoaded, listsLoaded }) {
|
||||
return (
|
||||
<div id="lists-header">
|
||||
{userLoaded &&
|
||||
listsLoaded && (
|
||||
<div id="lists">
|
||||
<ListActionsContainer />
|
||||
<SelectorContainer />
|
||||
</div>
|
||||
)}
|
||||
{!userLoaded && <span className="loading">loading.</span>}
|
||||
{userLoaded && !listsLoaded && <span className="loading">loading..</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Lists.propTypes = {
|
||||
userLoaded: PropTypes.bool.isRequired,
|
||||
listsLoaded: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
userLoaded: state.user.loaded,
|
||||
listsLoaded: state.lists.loaded,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Lists);
|
||||
53
client/src/components/lists/Selector.css
Normal file
53
client/src/components/lists/Selector.css
Normal file
@@ -0,0 +1,53 @@
|
||||
#listselector {
|
||||
display: flex;
|
||||
margin-left: 1rem;
|
||||
overflow: hidden;
|
||||
align-self: center;
|
||||
background-color: #fbfbfb;
|
||||
flex-grow: 1;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#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 {
|
||||
align-self: center;
|
||||
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;
|
||||
}
|
||||
134
client/src/components/lists/Selector.js
Normal file
134
client/src/components/lists/Selector.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Select, MenuItem } from '@material-ui/core';
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
import CheckIcon from '@material-ui/icons/Check';
|
||||
import { Transition, config } from 'react-spring';
|
||||
|
||||
import './Selector.css';
|
||||
|
||||
const button = {
|
||||
width: 36,
|
||||
height: 36,
|
||||
};
|
||||
|
||||
const icon = {
|
||||
fontSize: 24,
|
||||
};
|
||||
|
||||
export default function Selector({
|
||||
lists,
|
||||
list,
|
||||
onChange,
|
||||
editing,
|
||||
creating,
|
||||
addList,
|
||||
editList,
|
||||
}) {
|
||||
if (creating) {
|
||||
let input = null;
|
||||
return (
|
||||
<div id="listselector" className="list--input">
|
||||
<Transition
|
||||
native
|
||||
config={config.stiff}
|
||||
from={{ paddingBottom: 18 }}
|
||||
enter={{ paddingBottom: 0 }}
|
||||
leave={{ paddingBottom: 18 }}
|
||||
>
|
||||
{styles => (
|
||||
<input
|
||||
ref={node => {
|
||||
input = node;
|
||||
}}
|
||||
style={styles}
|
||||
id="input"
|
||||
type="text"
|
||||
onKeyPress={e => {
|
||||
if (e.key === 'Enter') {
|
||||
addList(input.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Transition>
|
||||
<button
|
||||
style={button}
|
||||
type="submit"
|
||||
onClick={() => input.value.trim() && addList(input.value)}
|
||||
>
|
||||
<AddIcon style={icon} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (editing) {
|
||||
let input = null;
|
||||
return (
|
||||
<div id="listselector" className="list--input">
|
||||
<Transition
|
||||
config={config.stiff}
|
||||
from={{ paddingBottom: 18 }}
|
||||
enter={{ paddingBottom: 0 }}
|
||||
leave={{ paddingBottom: 18 }}
|
||||
>
|
||||
{styles => (
|
||||
<input
|
||||
ref={node => {
|
||||
input = node;
|
||||
}}
|
||||
style={styles}
|
||||
defaultValue={lists.lists[list].name}
|
||||
id="input"
|
||||
type="text"
|
||||
onKeyPress={e => {
|
||||
if (e.key === 'Enter') {
|
||||
editList(input.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Transition>
|
||||
<button
|
||||
style={button}
|
||||
type="submit"
|
||||
onClick={() => input.value.trim() && editList(input.value)}
|
||||
>
|
||||
<CheckIcon style={icon} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (list) {
|
||||
return (
|
||||
<div id="listselector">
|
||||
<Select
|
||||
style={{ fontSize: '1.5rem', width: '100%' }}
|
||||
value={list}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
>
|
||||
{Object.values(lists.lists).map(elem => (
|
||||
<MenuItem key={elem.id} value={elem.id}>
|
||||
{elem.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</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,
|
||||
};
|
||||
25
client/src/components/lists/SelectorContainer.js
Normal file
25
client/src/components/lists/SelectorContainer.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { connect } from 'react-redux';
|
||||
import Selector from './Selector';
|
||||
import { changeList, addList, editList } from '../../actions/lists';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
lists: state.lists,
|
||||
list: state.lists.list,
|
||||
editing: state.lists.editing,
|
||||
creating: state.lists.creating,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onChange: list => dispatch(changeList(list)),
|
||||
addList: name => dispatch(addList(name)),
|
||||
editList: name => dispatch(editList(name)),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Selector);
|
||||
162
client/src/components/todolist/Todo.js
Normal file
162
client/src/components/todolist/Todo.js
Normal file
@@ -0,0 +1,162 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { animated } from 'react-spring';
|
||||
import { ButtonBase } from '@material-ui/core';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import CheckIcon from '@material-ui/icons/Check';
|
||||
|
||||
const icon = {
|
||||
fontSize: 24,
|
||||
padding: 0,
|
||||
};
|
||||
const disabledAction = {
|
||||
backgroundColor: '#fafafa',
|
||||
color: '#dddddd',
|
||||
};
|
||||
|
||||
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() {
|
||||
const { state } = this;
|
||||
this.setState({
|
||||
...state,
|
||||
hover: true,
|
||||
});
|
||||
}
|
||||
|
||||
onMouseOut() {
|
||||
const { state } = this;
|
||||
this.setState({
|
||||
...state,
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
|
||||
startEdit() {
|
||||
const { state } = this;
|
||||
this.setState({
|
||||
...state,
|
||||
editing: true,
|
||||
});
|
||||
}
|
||||
|
||||
stopEdit(value) {
|
||||
const { editTodo } = this.props;
|
||||
editTodo(value);
|
||||
const { state } = this;
|
||||
this.setState({
|
||||
...state,
|
||||
editing: false,
|
||||
hover: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const deleteClasses = ['delete'];
|
||||
const editClasses = ['edit'];
|
||||
const { hover, editing } = this.state;
|
||||
const { todo, removeTodo, toggleTodo, style } = this.props;
|
||||
if (!hover) {
|
||||
deleteClasses.push('disabled');
|
||||
editClasses.push('disabled');
|
||||
}
|
||||
|
||||
let input;
|
||||
|
||||
const text = editing ? (
|
||||
<div className="todo">
|
||||
<textarea
|
||||
className="todo--input"
|
||||
defaultValue={todo.text}
|
||||
ref={node => {
|
||||
input = node;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ButtonBase
|
||||
style={{
|
||||
justifyContent: 'left',
|
||||
paddingLeft: '1rem',
|
||||
textDecoration: todo.completed ? 'line-through' : 'none',
|
||||
color: todo.completed ? '#888888' : 'black',
|
||||
}}
|
||||
className="todo"
|
||||
onClick={() => {
|
||||
toggleTodo();
|
||||
}}
|
||||
>
|
||||
{todo.text}
|
||||
</ButtonBase>
|
||||
);
|
||||
const ButtonBases = editing
|
||||
? [
|
||||
<ButtonBase
|
||||
key="save"
|
||||
style={{ backgroundColor: 'lightgreen' }}
|
||||
className="save"
|
||||
onClick={() => this.stopEdit(input.value)}
|
||||
>
|
||||
<CheckIcon style={icon} />
|
||||
</ButtonBase>,
|
||||
]
|
||||
: [
|
||||
<ButtonBase
|
||||
key="remove"
|
||||
style={hover ? { backgroundColor: 'pink' } : disabledAction}
|
||||
className={deleteClasses.join(' ')}
|
||||
onClick={removeTodo}
|
||||
>
|
||||
<DeleteIcon style={icon} />
|
||||
</ButtonBase>,
|
||||
<ButtonBase
|
||||
key="edit"
|
||||
style={hover ? { backgroundColor: 'lightcyan' } : disabledAction}
|
||||
className={editClasses.join(' ')}
|
||||
onClick={this.startEdit}
|
||||
>
|
||||
<EditIcon style={icon} />
|
||||
</ButtonBase>,
|
||||
];
|
||||
return (
|
||||
<animated.li
|
||||
style={{
|
||||
...style,
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
}}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onFocus={this.onMouseOver}
|
||||
onMouseOut={this.onMouseOut}
|
||||
onBlur={this.onMouseOut}
|
||||
>
|
||||
{text}
|
||||
{ButtonBases}
|
||||
</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;
|
||||
71
client/src/components/todolist/TodoList.js
Normal file
71
client/src/components/todolist/TodoList.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Transition, config } from 'react-spring';
|
||||
|
||||
import Todo from './Todo';
|
||||
|
||||
export default function TodosContainer({
|
||||
todos,
|
||||
toggleTodo,
|
||||
removeTodo,
|
||||
editTodo,
|
||||
}) {
|
||||
return (
|
||||
<ul id="list">
|
||||
<Transition
|
||||
native
|
||||
config={{
|
||||
...config.default,
|
||||
overshootClamping: true,
|
||||
restSpeedThreshold: 1,
|
||||
restDisplacementThreshold: 1,
|
||||
}}
|
||||
items={todos}
|
||||
keys={todo => todo.id}
|
||||
from={{
|
||||
height: 0,
|
||||
borderColor: '#f0f0f0',
|
||||
opacity: 0.7,
|
||||
}}
|
||||
enter={{
|
||||
height: 60,
|
||||
borderColor: '#f0f0f0',
|
||||
opacity: 1,
|
||||
}}
|
||||
leave={{
|
||||
height: 0,
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 0,
|
||||
opacity: 0.3,
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{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,
|
||||
};
|
||||
34
client/src/components/todolist/TodoListContainer.js
Normal file
34
client/src/components/todolist/TodoListContainer.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { connect } from 'react-redux';
|
||||
import TodoList from './TodoList';
|
||||
import { toggleTodo, removeTodo, editTodo } from '../../actions/todos';
|
||||
|
||||
import getVisibleTodos from './getVisibleTodos';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
todos: state.lists.list
|
||||
? getVisibleTodos(
|
||||
state.lists.lists[state.lists.list].todos.map(
|
||||
id => state.todos.todos[id],
|
||||
),
|
||||
state.visibilityFilter,
|
||||
)
|
||||
: [],
|
||||
dirty: state.todos.dirty,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
59
client/src/components/todolist/TodosView.js
Normal file
59
client/src/components/todolist/TodosView.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Transition } from 'react-spring';
|
||||
|
||||
import withRouter from 'react-router-dom/withRouter';
|
||||
import Input from '../todos/Input';
|
||||
import TodoListContainer from './TodoListContainer';
|
||||
import Header from '../Header';
|
||||
import Filters from '../filters/Filters';
|
||||
|
||||
class Todos extends React.PureComponent {
|
||||
componentDidUpdate() {
|
||||
const { user, history } = this.props;
|
||||
if (!user.user && !user.dirty) {
|
||||
history.replace('/login');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { list } = this.props;
|
||||
return (
|
||||
<div id="todos">
|
||||
<Header />
|
||||
<Transition
|
||||
from={{ opacity: 0, maxHeight: 0 }}
|
||||
enter={{ opacity: 1, maxHeight: 38 }}
|
||||
leave={{ opacity: 0, maxHeight: 0 }}
|
||||
>
|
||||
{list && (styles => <Input styles={styles} />)}
|
||||
</Transition>
|
||||
<TodoListContainer />
|
||||
<Transition
|
||||
from={{ opacity: 0, maxHeight: 0 }}
|
||||
enter={{ opacity: 1, maxHeight: 32 }}
|
||||
leave={{ opacity: 0, maxHeight: 0 }}
|
||||
>
|
||||
{list && Filters}
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Todos.propTypes = {
|
||||
list: PropTypes.bool.isRequired,
|
||||
user: PropTypes.any.isRequired,
|
||||
history: PropTypes.any.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
list: Boolean(state.lists.list),
|
||||
user: state.user,
|
||||
};
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(Todos));
|
||||
14
client/src/components/todolist/getVisibleTodos.js
Normal file
14
client/src/components/todolist/getVisibleTodos.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { VisibilityFilters } from '../../actions/defs';
|
||||
|
||||
export default function getVisibleTodos(todos, filter) {
|
||||
switch (filter) {
|
||||
case VisibilityFilters.SHOW_ALL:
|
||||
return todos.filter(todo => todo);
|
||||
case VisibilityFilters.SHOW_ACTIVE:
|
||||
return todos.filter(todo => todo).filter(todo => !todo.completed);
|
||||
case VisibilityFilters.SHOW_COMPLETED:
|
||||
return todos.filter(todo => todo).filter(todo => todo.completed);
|
||||
default:
|
||||
return todos.filter(todo => todo);
|
||||
}
|
||||
}
|
||||
59
client/src/components/todos/Input.js
Normal file
59
client/src/components/todos/Input.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import * as React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@material-ui/core';
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
import { addTodo } from '../../actions/todos';
|
||||
|
||||
function Input({ onClick, styles }) {
|
||||
let input;
|
||||
|
||||
function submit() {
|
||||
if (input.value.trim() !== '') {
|
||||
onClick(input.value);
|
||||
}
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles} id="inputs">
|
||||
<input
|
||||
ref={node => {
|
||||
input = node;
|
||||
}}
|
||||
id="input"
|
||||
type="text"
|
||||
placeholder="Add something!"
|
||||
onKeyPress={e => {
|
||||
if (e.key === 'Enter') {
|
||||
submit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button style={{ borderRadius: 0 }} id="add" onClick={() => submit()}>
|
||||
<AddIcon />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Input.propTypes = {
|
||||
styles: PropTypes.any.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return { ...ownProps };
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onClick: text => dispatch(addTodo(text)),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(Input);
|
||||
40
client/src/components/user/FetchButton.js
Normal file
40
client/src/components/user/FetchButton.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ButtonBase } from '@material-ui/core';
|
||||
|
||||
import { fetchLists } from '../../actions/lists';
|
||||
|
||||
function FetchButton({ onClick, children }) {
|
||||
return (
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginLeft: 0,
|
||||
marginRight: 'auto',
|
||||
padding: '0 1rem',
|
||||
}}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
|
||||
FetchButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onClick: () => dispatch(fetchLists()),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps,
|
||||
)(FetchButton);
|
||||
33
client/src/components/user/Form.css
Normal file
33
client/src/components/user/Form.css
Normal file
@@ -0,0 +1,33 @@
|
||||
#form {
|
||||
margin: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
form {
|
||||
max-width: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 1rem;
|
||||
color: red;
|
||||
}
|
||||
|
||||
#googlebutton {
|
||||
margin: auto;
|
||||
margin-left: 0rem;
|
||||
}
|
||||
|
||||
#submitbutton {
|
||||
margin: auto;
|
||||
margin-right: 0rem;
|
||||
}
|
||||
|
||||
#buttons {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
35
client/src/components/user/InputField.js
Normal file
35
client/src/components/user/InputField.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TextField } from '@material-ui/core';
|
||||
|
||||
export default function InputField({
|
||||
required,
|
||||
input,
|
||||
label,
|
||||
meta: { touched, error },
|
||||
type,
|
||||
}) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
label={label}
|
||||
required={required}
|
||||
{...input}
|
||||
type={type}
|
||||
style={{ marginBottom: '1rem' }}
|
||||
/>
|
||||
{touched && error && <span className="error">{error}</span>}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
132
client/src/components/user/LoginForm.js
Normal file
132
client/src/components/user/LoginForm.js
Normal file
@@ -0,0 +1,132 @@
|
||||
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 { ButtonBase, Button } from '@material-ui/core';
|
||||
|
||||
import InputField from './InputField';
|
||||
import UserErrors from './UserErrors';
|
||||
|
||||
import './Form.css';
|
||||
|
||||
import { login, reset, loginJWT } from '../../actions/user';
|
||||
|
||||
class LoginForm extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
const { setJWT } = this.props;
|
||||
const params = new URLSearchParams(new URL(window.location).search);
|
||||
if (params.has('jwt')) {
|
||||
const jwt = params.get('jwt');
|
||||
setJWT(jwt);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { user, history } = this.props;
|
||||
if (user.user) {
|
||||
history.push('/');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { resetUser, history, handleSubmit, user, onLogin } = this.props;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div id="user-header">
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginRight: '1rem',
|
||||
padding: '0 0.5rem',
|
||||
borderRadius: '7px',
|
||||
}}
|
||||
onClick={() => {
|
||||
resetUser();
|
||||
history.push('/signup');
|
||||
}}
|
||||
>
|
||||
signup
|
||||
</ButtonBase>
|
||||
</div>
|
||||
<div id="form">
|
||||
<form onSubmit={handleSubmit(onLogin)}>
|
||||
<UserErrors user={user} />
|
||||
<Field
|
||||
label="username"
|
||||
name="username"
|
||||
required
|
||||
component={InputField}
|
||||
type="text"
|
||||
/>
|
||||
<Field
|
||||
label="password"
|
||||
name="password"
|
||||
required
|
||||
component={InputField}
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<div id="buttons">
|
||||
<Button
|
||||
id="googlebutton"
|
||||
variant="raised"
|
||||
onClick={() => {
|
||||
window.location = '/__/users/login/google/';
|
||||
}}
|
||||
>
|
||||
Google
|
||||
</Button>
|
||||
<Button
|
||||
id="submitbutton"
|
||||
variant="raised"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoginForm.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
onLogin: PropTypes.func.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
history: PropTypes.any.isRequired,
|
||||
resetUser: PropTypes.func.isRequired,
|
||||
setJWT: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
user: state.user,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
resetUser: () => dispatch(reset()),
|
||||
onLogin: ({ username, password }) =>
|
||||
dispatch(login({ username, password })),
|
||||
setJWT: jwt => dispatch(loginJWT(jwt)),
|
||||
};
|
||||
}
|
||||
|
||||
export default reduxForm({
|
||||
form: 'loginForm',
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
})(
|
||||
withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(LoginForm),
|
||||
),
|
||||
);
|
||||
40
client/src/components/user/LogoutLink.js
Normal file
40
client/src/components/user/LogoutLink.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ButtonBase } from '@material-ui/core';
|
||||
|
||||
import { logout } from '../../actions/user';
|
||||
|
||||
function Link({ onClick, children }) {
|
||||
return (
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 0,
|
||||
padding: '0 1rem',
|
||||
}}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
|
||||
Link.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onClick: () => dispatch(logout()),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps,
|
||||
)(Link);
|
||||
121
client/src/components/user/SignupForm.js
Normal file
121
client/src/components/user/SignupForm.js
Normal file
@@ -0,0 +1,121 @@
|
||||
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 { ButtonBase, Button } from '@material-ui/core';
|
||||
|
||||
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 = 'Passwords should match';
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function SignupForm({ handleSubmit, onSignup, user, history, resetUser }) {
|
||||
if (user.user) {
|
||||
history.push('/');
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div id="user-header">
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginRight: '1rem',
|
||||
padding: '0 0.5rem',
|
||||
borderRadius: '7px',
|
||||
}}
|
||||
onClick={() => {
|
||||
resetUser();
|
||||
history.push('/login');
|
||||
}}
|
||||
>
|
||||
login
|
||||
</ButtonBase>
|
||||
</div>
|
||||
<div id="form">
|
||||
<form onSubmit={handleSubmit(onSignup)}>
|
||||
<UserErrors user={user} />
|
||||
<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"
|
||||
/>
|
||||
<div id="buttons">
|
||||
<Button
|
||||
id="submitbutton"
|
||||
variant="raised"
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Signup
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
);
|
||||
34
client/src/components/user/Status.js
Normal file
34
client/src/components/user/Status.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ButtonBase } from '@material-ui/core';
|
||||
|
||||
function Status({ userFetching, listsFetching }) {
|
||||
return (
|
||||
<ButtonBase
|
||||
style={{
|
||||
marginRight: 'auto',
|
||||
padding: '0 0.5rem',
|
||||
borderRadius: '7px',
|
||||
marginLeft: '1rem',
|
||||
}}
|
||||
>
|
||||
{userFetching ? 'loading user' : null}
|
||||
{listsFetching ? 'loading lists' : null}
|
||||
</ButtonBase>
|
||||
);
|
||||
}
|
||||
|
||||
Status.propTypes = {
|
||||
userFetching: PropTypes.bool.isRequired,
|
||||
listsFetching: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
userFetching: state.user.fetching,
|
||||
listsFetching: state.lists.fetching,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Status);
|
||||
32
client/src/components/user/UserErrors.js
Normal file
32
client/src/components/user/UserErrors.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
function UserErrors({ user }) {
|
||||
const 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;
|
||||
15
client/src/components/user/UserHeader.js
Normal file
15
client/src/components/user/UserHeader.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
import LogoutLink from './LogoutLink';
|
||||
import FetchButton from './FetchButton';
|
||||
import Status from './Status';
|
||||
|
||||
export default function UserHeader() {
|
||||
return (
|
||||
<div id="user-header">
|
||||
<FetchButton>sync</FetchButton>
|
||||
<Status />
|
||||
<LogoutLink>logout</LogoutLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
client/src/index.js
Normal file
38
client/src/index.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import thunk from 'redux-thunk';
|
||||
import { Provider } from 'react-redux';
|
||||
import { applyMiddleware, createStore, compose } from 'redux';
|
||||
import { offline } from '@redux-offline/redux-offline';
|
||||
import offlineConfig from '@redux-offline/redux-offline/lib/defaults';
|
||||
|
||||
import AppContainer from './components/AppContainer';
|
||||
import registerServiceWorker from './registerServiceWorker';
|
||||
import todoApp from './reducers';
|
||||
import { setToken } from './actions/util';
|
||||
import keepSynced from './middleware/keepSynced';
|
||||
|
||||
let store;
|
||||
|
||||
const persistCallback = () => {
|
||||
const state = store.getState();
|
||||
if (state.user.user) {
|
||||
setToken(state.user.user.jwt);
|
||||
}
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<AppContainer />
|
||||
</Provider>,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
};
|
||||
|
||||
store = createStore(
|
||||
todoApp,
|
||||
compose(
|
||||
offline({ ...offlineConfig, persistCallback }),
|
||||
applyMiddleware(thunk, keepSynced),
|
||||
),
|
||||
);
|
||||
|
||||
registerServiceWorker();
|
||||
20
client/src/middleware/keepSynced.js
Normal file
20
client/src/middleware/keepSynced.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { REQUEST_LISTS, INVALIDATE_LISTS } from '../actions/defs';
|
||||
import { fetchLists } from '../actions/lists';
|
||||
|
||||
export default store => next => action => {
|
||||
next(action);
|
||||
if (action.type !== REQUEST_LISTS && typeof action !== 'function') {
|
||||
const state = store.getState();
|
||||
if (state.user.user) {
|
||||
const dirtyLists = state.lists.dirty || false;
|
||||
const dirtyTodos = state.todos.dirty || false;
|
||||
const fetchingLists = state.lists.fetching || false;
|
||||
if (
|
||||
((dirtyLists || dirtyTodos) && !fetchingLists) ||
|
||||
action.type === INVALIDATE_LISTS
|
||||
) {
|
||||
store.dispatch(fetchLists());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
17
client/src/reducers/index.js
Normal file
17
client/src/reducers/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { reducer as formReducer } from 'redux-form';
|
||||
|
||||
import lists from './lists';
|
||||
import visibilityFilter from './visibilityFilter';
|
||||
import user from './user';
|
||||
import todos from './todos';
|
||||
|
||||
const todoApp = combineReducers({
|
||||
lists,
|
||||
todos,
|
||||
visibilityFilter,
|
||||
form: formReducer,
|
||||
user,
|
||||
});
|
||||
|
||||
export default todoApp;
|
||||
160
client/src/reducers/lists.js
Normal file
160
client/src/reducers/lists.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import {
|
||||
CHANGE_LIST,
|
||||
INVALIDATE_LISTS,
|
||||
VALIDATE_LISTS,
|
||||
REQUEST_LISTS,
|
||||
RECIEVE_LISTS,
|
||||
ADD_LIST,
|
||||
REMOVE_LIST,
|
||||
EDIT_LIST_NAME,
|
||||
START_CREATE_LIST,
|
||||
START_EDIT_LIST,
|
||||
STOP_CREATE_LIST,
|
||||
STOP_EDIT_LIST,
|
||||
REMOVE_TODO,
|
||||
ADD_TODO,
|
||||
LOGOUT,
|
||||
} from '../actions/defs';
|
||||
|
||||
export default function lists(
|
||||
state = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
lists: null,
|
||||
loaded: false,
|
||||
creating: false,
|
||||
list: null,
|
||||
editing: false,
|
||||
},
|
||||
action,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case LOGOUT:
|
||||
return {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
lists: null,
|
||||
loaded: false,
|
||||
creating: false,
|
||||
list: null,
|
||||
editing: false,
|
||||
};
|
||||
case CHANGE_LIST:
|
||||
return { ...state, list: action.list };
|
||||
case RECIEVE_LISTS: {
|
||||
const newLists = Object.values(action.lists);
|
||||
let { list } = state;
|
||||
if (newLists.length !== 0) {
|
||||
if (!newLists.some(curList => curList.id === list)) {
|
||||
list = newLists[0].id || null;
|
||||
}
|
||||
} else {
|
||||
list = null;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
loaded: true,
|
||||
fetching: false,
|
||||
lists: action.lists,
|
||||
list,
|
||||
};
|
||||
}
|
||||
case START_CREATE_LIST:
|
||||
return {
|
||||
...state,
|
||||
creating: true,
|
||||
};
|
||||
case STOP_CREATE_LIST:
|
||||
return {
|
||||
...state,
|
||||
creating: false,
|
||||
};
|
||||
case ADD_LIST:
|
||||
return {
|
||||
...state,
|
||||
creating: false,
|
||||
lists: { ...state.lists, [action.list.id]: action.list },
|
||||
list: action.list.id,
|
||||
};
|
||||
case REMOVE_LIST: {
|
||||
const newLists = { ...state.lists };
|
||||
delete newLists[action.list];
|
||||
const listsObjs = Object.values(newLists);
|
||||
const list = listsObjs.length ? listsObjs[listsObjs.length - 1].id : null;
|
||||
return {
|
||||
...state,
|
||||
list,
|
||||
lists: newLists,
|
||||
};
|
||||
}
|
||||
case START_EDIT_LIST: {
|
||||
return {
|
||||
...state,
|
||||
editing: true,
|
||||
};
|
||||
}
|
||||
case STOP_EDIT_LIST: {
|
||||
return {
|
||||
...state,
|
||||
editing: false,
|
||||
};
|
||||
}
|
||||
case EDIT_LIST_NAME: {
|
||||
return {
|
||||
...state,
|
||||
editing: false,
|
||||
lists: {
|
||||
...state.lists,
|
||||
[action.list]: {
|
||||
...state.lists[action.list],
|
||||
name: action.name,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case REMOVE_TODO: {
|
||||
return {
|
||||
...state,
|
||||
lists: {
|
||||
...state.lists,
|
||||
[state.list]: {
|
||||
...state.lists[state.list],
|
||||
todos: state.lists[state.list].todos.filter(
|
||||
todo => todo !== action.id,
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case ADD_TODO: {
|
||||
return {
|
||||
...state,
|
||||
lists: {
|
||||
...state.lists,
|
||||
[state.list]: {
|
||||
...state.lists[state.list],
|
||||
todos: [action.todo.id, ...state.lists[state.list].todos],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case INVALIDATE_LISTS:
|
||||
return {
|
||||
...state,
|
||||
dirty: true,
|
||||
};
|
||||
case VALIDATE_LISTS:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
};
|
||||
case REQUEST_LISTS:
|
||||
return {
|
||||
...state,
|
||||
fetching: true,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
99
client/src/reducers/todos.js
Normal file
99
client/src/reducers/todos.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
ADD_TODO,
|
||||
REMOVE_TODO,
|
||||
TOGGLE_TODO,
|
||||
RECIEVE_TODOS,
|
||||
REQUEST_TODOS,
|
||||
INVALIDATE_TODOS,
|
||||
VALIDATE_TODOS,
|
||||
EDIT_TODO,
|
||||
REMOVE_LIST,
|
||||
LOGOUT,
|
||||
} from '../actions/defs';
|
||||
|
||||
export default function todos(
|
||||
state = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
todos: null,
|
||||
},
|
||||
action,
|
||||
) {
|
||||
switch (action.type) {
|
||||
case LOGOUT:
|
||||
return {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
todos: null,
|
||||
};
|
||||
case RECIEVE_TODOS:
|
||||
return {
|
||||
...state,
|
||||
dirty: false,
|
||||
fetching: false,
|
||||
todos: action.todos,
|
||||
};
|
||||
case ADD_TODO:
|
||||
return {
|
||||
...state,
|
||||
todos: { [action.todo.id]: 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,
|
||||
[action.id]: { ...state.todos[action.id], text: action.text },
|
||||
},
|
||||
};
|
||||
case REQUEST_TODOS:
|
||||
return {
|
||||
...state,
|
||||
fetching: true,
|
||||
};
|
||||
case REMOVE_TODO: {
|
||||
const newTodos = { ...state.todos };
|
||||
delete newTodos[action.id];
|
||||
return {
|
||||
...state,
|
||||
todos: newTodos,
|
||||
};
|
||||
}
|
||||
case REMOVE_LIST: {
|
||||
const newTodos = { ...state.todos };
|
||||
Object.keys(newTodos).forEach(todoId => {
|
||||
if (newTodos[todoId].list === action.list) {
|
||||
delete newTodos[todoId];
|
||||
}
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
todos: newTodos,
|
||||
};
|
||||
}
|
||||
case TOGGLE_TODO: {
|
||||
return {
|
||||
...state,
|
||||
todos: {
|
||||
...state.todos,
|
||||
[action.id]: {
|
||||
...state.todos[action.id],
|
||||
completed: !state.todos[action.id].completed,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
70
client/src/reducers/user.js
Normal file
70
client/src/reducers/user.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
LOGIN_SUCCESS,
|
||||
LOGIN_FAIL,
|
||||
START_LOGIN,
|
||||
LOGOUT,
|
||||
SIGNUP_FAIL,
|
||||
SIGNUP_SUCCESS,
|
||||
VALIDATE_USER,
|
||||
RESET_USER,
|
||||
} from '../actions/defs';
|
||||
|
||||
export default function user(
|
||||
state = {
|
||||
dirty: true,
|
||||
fetching: false,
|
||||
user: null,
|
||||
loaded: false,
|
||||
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,
|
||||
loaded: true,
|
||||
fetching: false,
|
||||
};
|
||||
case SIGNUP_FAIL:
|
||||
case LOGIN_FAIL:
|
||||
return {
|
||||
...state,
|
||||
user: null,
|
||||
errors: action.error,
|
||||
dirty: false,
|
||||
fetching: false,
|
||||
loaded: false,
|
||||
};
|
||||
case RESET_USER:
|
||||
return {
|
||||
...state,
|
||||
fetching: false,
|
||||
loaded: false,
|
||||
user: null,
|
||||
errors: null,
|
||||
};
|
||||
case LOGOUT:
|
||||
return {
|
||||
...state,
|
||||
loaded: false,
|
||||
user: null,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
12
client/src/reducers/visibilityFilter.js
Normal file
12
client/src/reducers/visibilityFilter.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { VisibilityFilters, SET_VISIBILITY_FILTER } from '../actions/defs';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
118
client/src/registerServiceWorker.js
Normal file
118
client/src/registerServiceWorker.js
Normal file
@@ -0,0 +1,118 @@
|
||||
// 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}$/,
|
||||
),
|
||||
);
|
||||
|
||||
function registerValidSW(swUrl) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
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 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister();
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user