Add 'react/' from commit 'e508ac031e5d832aa95ad8ef040e82277db804d2'

git-subtree-dir: react
git-subtree-mainline: 53976e7b43
git-subtree-split: e508ac031e
This commit is contained in:
2018-06-03 13:39:31 +03:00
47 changed files with 16424 additions and 0 deletions

30
react/.eslintrc.json Normal file
View 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
View 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
View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

5
react/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"editor.tabSize": 2,
"editor.insertSpaces": true,
"prettier.eslintIntegration": true
}

2444
react/README.md Normal file

File diff suppressed because it is too large Load Diff

11783
react/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
react/package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

40
react/public/index.html Normal file
View 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>

View 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
View 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
View 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
View 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 };
}

View File

@@ -0,0 +1,5 @@
export const API_ROOT = '/api';
export function getToken() {
return localStorage.getItem('jwt');
}

View 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;
}

View 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,
};

View 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;
}

View 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);
}

View 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>
);
}

View 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;

View 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,
};

View 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>
);
}

View 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;
}

View 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,
};

View 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;

View 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,
};

View 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,
};

View 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,
};

View 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)));

View 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)));

View 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;

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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;

View 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));

View 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
View 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();

View 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;

View 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
View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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();
});
}
}