mirror of
https://github.com/ershisan99/it-incubator-todolist-ts-17-live-2024-08-17.git
synced 2026-02-04 21:02:13 +00:00
init
This commit is contained in:
118
src/api/todolists-api.ts
Normal file
118
src/api/todolists-api.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const settings = {
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'API-KEY': '1cdd9f77-c60e-4af5-b194-659e4ebd5d41'
|
||||
}
|
||||
}
|
||||
const instance = axios.create({
|
||||
baseURL: 'https://social-network.samuraijs.com/api/1.1/',
|
||||
...settings
|
||||
})
|
||||
|
||||
// api
|
||||
export const todolistsAPI = {
|
||||
getTodolists() {
|
||||
const promise = instance.get<TodolistType[]>('todo-lists');
|
||||
return promise;
|
||||
},
|
||||
createTodolist(title: string) {
|
||||
const promise = instance.post<ResponseType<{ item: TodolistType }>>('todo-lists', {title: title});
|
||||
return promise;
|
||||
},
|
||||
deleteTodolist(id: string) {
|
||||
const promise = instance.delete<ResponseType>(`todo-lists/${id}`);
|
||||
return promise;
|
||||
},
|
||||
updateTodolist(id: string, title: string) {
|
||||
const promise = instance.put<ResponseType>(`todo-lists/${id}`, {title: title});
|
||||
return promise;
|
||||
},
|
||||
getTasks(todolistId: string) {
|
||||
return instance.get<GetTasksResponse>(`todo-lists/${todolistId}/tasks`);
|
||||
},
|
||||
deleteTask(todolistId: string, taskId: string) {
|
||||
return instance.delete<ResponseType>(`todo-lists/${todolistId}/tasks/${taskId}`);
|
||||
},
|
||||
createTask(todolistId: string, taskTitile: string) {
|
||||
return instance.post<ResponseType<{ item: TaskType}>>(`todo-lists/${todolistId}/tasks`, {title: taskTitile});
|
||||
},
|
||||
updateTask(todolistId: string, taskId: string, model: UpdateTaskModelType) {
|
||||
return instance.put<ResponseType<TaskType>>(`todo-lists/${todolistId}/tasks/${taskId}`, model);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type LoginParamsType = {
|
||||
email: string
|
||||
password: string
|
||||
rememberMe: boolean
|
||||
captcha?: string
|
||||
}
|
||||
|
||||
export const authAPI = {
|
||||
login(data: LoginParamsType) {
|
||||
const promise = instance.post<ResponseType<{userId?: number}>>('auth/login', data);
|
||||
return promise;
|
||||
},
|
||||
logout() {
|
||||
const promise = instance.delete<ResponseType<{userId?: number}>>('auth/login');
|
||||
return promise;
|
||||
},
|
||||
me() {
|
||||
const promise = instance.get<ResponseType<{id: number; email: string; login: string}>>('auth/me');
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
// types
|
||||
export type TodolistType = {
|
||||
id: string
|
||||
title: string
|
||||
addedDate: string
|
||||
order: number
|
||||
}
|
||||
export type ResponseType<D = {}> = {
|
||||
resultCode: number
|
||||
messages: Array<string>
|
||||
data: D
|
||||
}
|
||||
export enum TaskStatuses {
|
||||
New = 0,
|
||||
InProgress = 1,
|
||||
Completed = 2,
|
||||
Draft = 3
|
||||
}
|
||||
export enum TaskPriorities {
|
||||
Low = 0,
|
||||
Middle = 1,
|
||||
Hi = 2,
|
||||
Urgently = 3,
|
||||
Later = 4
|
||||
}
|
||||
export type TaskType = {
|
||||
description: string
|
||||
title: string
|
||||
status: TaskStatuses
|
||||
priority: TaskPriorities
|
||||
startDate: string
|
||||
deadline: string
|
||||
id: string
|
||||
todoListId: string
|
||||
order: number
|
||||
addedDate: string
|
||||
}
|
||||
export type UpdateTaskModelType = {
|
||||
title: string
|
||||
description: string
|
||||
status: TaskStatuses
|
||||
priority: TaskPriorities
|
||||
startDate: string
|
||||
deadline: string
|
||||
}
|
||||
type GetTasksResponse = {
|
||||
error: string | null
|
||||
totalCount: number
|
||||
items: TaskType[]
|
||||
}
|
||||
26
src/app/App.css
Normal file
26
src/app/App.css
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
.App {
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.App > div {
|
||||
margin-right: 50px;
|
||||
}
|
||||
|
||||
.error {
|
||||
border: red 1px solid;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.active-filter {
|
||||
background-color: aquamarine;
|
||||
}
|
||||
|
||||
.is-done {
|
||||
opacity: 0.5;
|
||||
}
|
||||
*/
|
||||
75
src/app/App.tsx
Normal file
75
src/app/App.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import './App.css'
|
||||
import { TodolistsList } from '../features/TodolistsList/TodolistsList'
|
||||
import { ErrorSnackbar } from '../components/ErrorSnackbar/ErrorSnackbar'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { AppRootStateType } from './store'
|
||||
import { initializeAppTC, RequestStatusType } from './app-reducer'
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
||||
import { Login } from '../features/Login/Login'
|
||||
import { logoutTC } from '../features/Login/auth-reducer'
|
||||
import {
|
||||
AppBar,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Container,
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
Toolbar,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { Menu } from '@mui/icons-material'
|
||||
|
||||
type PropsType = {
|
||||
demo?: boolean
|
||||
}
|
||||
|
||||
function App({demo = false}: PropsType) {
|
||||
const status = useSelector<AppRootStateType, RequestStatusType>((state) => state.app.status)
|
||||
const isInitialized = useSelector<AppRootStateType, boolean>((state) => state.app.isInitialized)
|
||||
const isLoggedIn = useSelector<AppRootStateType, boolean>(state => state.auth.isLoggedIn)
|
||||
const dispatch = useDispatch<any>()
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(initializeAppTC())
|
||||
}, [])
|
||||
|
||||
const logoutHandler = useCallback(() => {
|
||||
dispatch(logoutTC())
|
||||
}, [])
|
||||
|
||||
if (!isInitialized) {
|
||||
return <div
|
||||
style={{position: 'fixed', top: '30%', textAlign: 'center', width: '100%'}}>
|
||||
<CircularProgress/>
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div className="App">
|
||||
<ErrorSnackbar/>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<IconButton edge="start" color="inherit" aria-label="menu">
|
||||
<Menu/>
|
||||
</IconButton>
|
||||
<Typography variant="h6">
|
||||
News
|
||||
</Typography>
|
||||
{isLoggedIn && <Button color="inherit" onClick={logoutHandler}>Log out</Button>}
|
||||
</Toolbar>
|
||||
{status === 'loading' && <LinearProgress/>}
|
||||
</AppBar>
|
||||
<Container fixed>
|
||||
<Routes>
|
||||
<Route path={'/'} element={<TodolistsList demo={demo}/>}/>
|
||||
<Route path={'/login'} element={<Login/>}/>
|
||||
</Routes>
|
||||
</Container>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
22
src/app/app-reducer.test.ts
Normal file
22
src/app/app-reducer.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { appReducer, InitialStateType, setAppErrorAC, setAppStatusAC } from './app-reducer'
|
||||
|
||||
let startState: InitialStateType;
|
||||
|
||||
beforeEach(() => {
|
||||
startState = {
|
||||
error: null,
|
||||
status: 'idle',
|
||||
isInitialized: false
|
||||
}
|
||||
})
|
||||
|
||||
test('correct error message should be set', () => {
|
||||
const endState = appReducer(startState, setAppErrorAC('some error'))
|
||||
expect(endState.error).toBe('some error');
|
||||
})
|
||||
|
||||
test('correct status should be set', () => {
|
||||
const endState = appReducer(startState, setAppStatusAC('loading'))
|
||||
expect(endState.status).toBe('loading');
|
||||
})
|
||||
|
||||
57
src/app/app-reducer.ts
Normal file
57
src/app/app-reducer.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {Dispatch} from 'redux'
|
||||
import {authAPI} from '../api/todolists-api'
|
||||
import {setIsLoggedInAC} from '../features/Login/auth-reducer'
|
||||
|
||||
const initialState: InitialStateType = {
|
||||
status: 'idle',
|
||||
error: null,
|
||||
isInitialized: false
|
||||
}
|
||||
|
||||
export const appReducer = (state: InitialStateType = initialState, action: ActionsType): InitialStateType => {
|
||||
switch (action.type) {
|
||||
case 'APP/SET-STATUS':
|
||||
return {...state, status: action.status}
|
||||
case 'APP/SET-ERROR':
|
||||
return {...state, error: action.error}
|
||||
case 'APP/SET-IS-INITIALIED':
|
||||
return {...state, isInitialized: action.value}
|
||||
default:
|
||||
return {...state}
|
||||
}
|
||||
}
|
||||
|
||||
export type RequestStatusType = 'idle' | 'loading' | 'succeeded' | 'failed'
|
||||
export type InitialStateType = {
|
||||
// происходит ли сейчас взаимодействие с сервером
|
||||
status: RequestStatusType
|
||||
// если ошибка какая-то глобальная произойдёт - мы запишем текст ошибки сюда
|
||||
error: string | null
|
||||
// true когда приложение проинициализировалось (проверили юзера, настройки получили и т.д.)
|
||||
isInitialized: boolean
|
||||
}
|
||||
|
||||
export const setAppErrorAC = (error: string | null) => ({type: 'APP/SET-ERROR', error} as const)
|
||||
export const setAppStatusAC = (status: RequestStatusType) => ({type: 'APP/SET-STATUS', status} as const)
|
||||
export const setAppInitializedAC = (value: boolean) => ({type: 'APP/SET-IS-INITIALIED', value} as const)
|
||||
|
||||
export const initializeAppTC = () => (dispatch: Dispatch) => {
|
||||
authAPI.me().then(res => {
|
||||
if (res.data.resultCode === 0) {
|
||||
dispatch(setIsLoggedInAC(true));
|
||||
} else {
|
||||
|
||||
}
|
||||
|
||||
dispatch(setAppInitializedAC(true));
|
||||
})
|
||||
}
|
||||
|
||||
export type SetAppErrorActionType = ReturnType<typeof setAppErrorAC>
|
||||
export type SetAppStatusActionType = ReturnType<typeof setAppStatusAC>
|
||||
|
||||
|
||||
type ActionsType =
|
||||
| SetAppErrorActionType
|
||||
| SetAppStatusActionType
|
||||
| ReturnType<typeof setAppInitializedAC>
|
||||
27
src/app/store.ts
Normal file
27
src/app/store.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {tasksReducer} from '../features/TodolistsList/tasks-reducer';
|
||||
import {todolistsReducer} from '../features/TodolistsList/todolists-reducer';
|
||||
import {applyMiddleware, combineReducers, createStore} from 'redux'
|
||||
import thunkMiddleware, {ThunkAction, ThunkDispatch} from 'redux-thunk'
|
||||
import {appReducer} from './app-reducer'
|
||||
import {authReducer} from '../features/Login/auth-reducer'
|
||||
import {configureStore, UnknownAction} from "@reduxjs/toolkit";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
tasks: tasksReducer,
|
||||
todolists: todolistsReducer,
|
||||
app: appReducer,
|
||||
auth: authReducer
|
||||
})
|
||||
|
||||
// ❗старая запись, с новыми версиями не работает
|
||||
// const store = createStore(rootReducer, applyMiddleware(thunkMiddleware));
|
||||
export const store = configureStore({reducer: rootReducer},)
|
||||
|
||||
export type AppRootStateType = ReturnType<typeof rootReducer>
|
||||
|
||||
// ❗ UnknownAction вместо AnyAction
|
||||
export type AppThunk<ReturnType = void> = ThunkAction<ReturnType, AppRootStateType, unknown, UnknownAction>
|
||||
|
||||
// export type AppDispatch = typeof store.dispatch
|
||||
// ❗ UnknownAction вместо AnyAction
|
||||
export type AppDispatch = ThunkDispatch<AppRootStateType, unknown, UnknownAction>
|
||||
51
src/components/AddItemForm/AddItemForm.tsx
Normal file
51
src/components/AddItemForm/AddItemForm.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { ChangeEvent, KeyboardEvent, useState } from 'react';
|
||||
import { IconButton, TextField } from '@mui/material';
|
||||
import { AddBox } from '@mui/icons-material';
|
||||
|
||||
type AddItemFormPropsType = {
|
||||
addItem: (title: string) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const AddItemForm = React.memo(function ({addItem, disabled = false}: AddItemFormPropsType) {
|
||||
|
||||
let [title, setTitle] = useState('')
|
||||
let [error, setError] = useState<string | null>(null)
|
||||
|
||||
const addItemHandler = () => {
|
||||
if (title.trim() !== '') {
|
||||
addItem(title);
|
||||
setTitle('');
|
||||
} else {
|
||||
setError('Title is required');
|
||||
}
|
||||
}
|
||||
|
||||
const onChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(e.currentTarget.value)
|
||||
}
|
||||
|
||||
const onKeyPressHandler = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (error !== null) {
|
||||
setError(null);
|
||||
}
|
||||
if (e.charCode === 13) {
|
||||
addItemHandler();
|
||||
}
|
||||
}
|
||||
|
||||
return <div>
|
||||
<TextField variant="outlined"
|
||||
disabled={disabled}
|
||||
error={!!error}
|
||||
value={title}
|
||||
onChange={onChangeHandler}
|
||||
onKeyPress={onKeyPressHandler}
|
||||
label="Title"
|
||||
helperText={error}
|
||||
/>
|
||||
<IconButton color="primary" onClick={addItemHandler} disabled={disabled}>
|
||||
<AddBox/>
|
||||
</IconButton>
|
||||
</div>
|
||||
})
|
||||
28
src/components/EditableSpan/EditableSpan.tsx
Normal file
28
src/components/EditableSpan/EditableSpan.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, {ChangeEvent, useState} from 'react';
|
||||
import { TextField } from '@mui/material';
|
||||
|
||||
type EditableSpanPropsType = {
|
||||
value: string
|
||||
onChange: (newValue: string) => void
|
||||
}
|
||||
|
||||
export const EditableSpan = React.memo(function (props: EditableSpanPropsType) {
|
||||
let [editMode, setEditMode] = useState(false);
|
||||
let [title, setTitle] = useState(props.value);
|
||||
|
||||
const activateEditMode = () => {
|
||||
setEditMode(true);
|
||||
setTitle(props.value);
|
||||
}
|
||||
const activateViewMode = () => {
|
||||
setEditMode(false);
|
||||
props.onChange(title);
|
||||
}
|
||||
const changeTitle = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(e.currentTarget.value)
|
||||
}
|
||||
|
||||
return editMode
|
||||
? <TextField value={title} onChange={changeTitle} autoFocus onBlur={activateViewMode} />
|
||||
: <span onDoubleClick={activateEditMode}>{props.value}</span>
|
||||
});
|
||||
37
src/components/ErrorSnackbar/ErrorSnackbar.tsx
Normal file
37
src/components/ErrorSnackbar/ErrorSnackbar.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import {useDispatch, useSelector} from 'react-redux'
|
||||
import {AppRootStateType} from '../../app/store'
|
||||
import {setAppErrorAC} from '../../app/app-reducer'
|
||||
import { AlertProps, Snackbar } from '@mui/material'
|
||||
import MuiAlert from '@mui/material/Alert';
|
||||
|
||||
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert(
|
||||
props,
|
||||
ref,
|
||||
) {
|
||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
|
||||
});
|
||||
|
||||
export function ErrorSnackbar() {
|
||||
|
||||
const error = useSelector<AppRootStateType, string | null>(state => state.app.error);
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const handleClose = (event?: React.SyntheticEvent | Event, reason?: string) => {
|
||||
if (reason === 'clickaway') {
|
||||
return
|
||||
}
|
||||
dispatch(setAppErrorAC(null));
|
||||
}
|
||||
|
||||
|
||||
const isOpen = error !== null;
|
||||
|
||||
return (
|
||||
<Snackbar open={isOpen} autoHideDuration={6000} onClose={handleClose}>
|
||||
<Alert onClose={handleClose} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
)
|
||||
}
|
||||
89
src/features/Login/Login.tsx
Normal file
89
src/features/Login/Login.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react'
|
||||
import { useFormik } from 'formik'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { loginTC } from './auth-reducer'
|
||||
import { AppRootStateType } from '../../app/store'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { useAppDispatch } from '../../hooks/useAppDispatch';
|
||||
import { Button, Checkbox, FormControl, FormControlLabel, FormGroup, FormLabel, Grid, TextField } from '@mui/material'
|
||||
|
||||
export const Login = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const isLoggedIn = useSelector<AppRootStateType, boolean>(state => state.auth.isLoggedIn);
|
||||
|
||||
const formik = useFormik({
|
||||
validate: (values) => {
|
||||
if (!values.email) {
|
||||
return {
|
||||
email: 'Email is required'
|
||||
}
|
||||
}
|
||||
if (!values.password) {
|
||||
return {
|
||||
password: 'Password is required'
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
rememberMe: false
|
||||
},
|
||||
onSubmit: values => {
|
||||
dispatch(loginTC(values));
|
||||
},
|
||||
})
|
||||
|
||||
if (isLoggedIn) {
|
||||
return <Navigate to={"/"} />
|
||||
}
|
||||
|
||||
|
||||
return <Grid container justifyContent="center">
|
||||
<Grid item xs={4}>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<FormControl>
|
||||
<FormLabel>
|
||||
<p>
|
||||
To log in get registered <a href={'https://social-network.samuraijs.com/'}
|
||||
target={'_blank'}>here</a>
|
||||
</p>
|
||||
<p>
|
||||
or use common test account credentials:
|
||||
</p>
|
||||
<p> Email: free@samuraijs.com
|
||||
</p>
|
||||
<p>
|
||||
Password: free
|
||||
</p>
|
||||
</FormLabel>
|
||||
<FormGroup>
|
||||
<TextField
|
||||
label="Email"
|
||||
margin="normal"
|
||||
{...formik.getFieldProps("email")}
|
||||
/>
|
||||
{formik.errors.email ? <div>{formik.errors.email}</div> : null}
|
||||
<TextField
|
||||
type="password"
|
||||
label="Password"
|
||||
margin="normal"
|
||||
{...formik.getFieldProps("password")}
|
||||
/>
|
||||
{formik.errors.password ? <div>{formik.errors.password}</div> : null}
|
||||
<FormControlLabel
|
||||
label={'Remember me'}
|
||||
control={<Checkbox
|
||||
{...formik.getFieldProps("rememberMe")}
|
||||
checked={formik.values.rememberMe}
|
||||
/>}
|
||||
/>
|
||||
<Button type={'submit'} variant={'contained'} color={'primary'}>Login</Button>
|
||||
</FormGroup>
|
||||
</FormControl>
|
||||
</form>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
64
src/features/Login/auth-reducer.ts
Normal file
64
src/features/Login/auth-reducer.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {Dispatch} from 'redux'
|
||||
import {SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../../app/app-reducer'
|
||||
import {authAPI, LoginParamsType} from '../../api/todolists-api'
|
||||
import {handleServerAppError, handleServerNetworkError} from '../../utils/error-utils'
|
||||
|
||||
const initialState: InitialStateType = {
|
||||
isLoggedIn: false
|
||||
}
|
||||
|
||||
export const authReducer = (state: InitialStateType = initialState, action: ActionsType): InitialStateType => {
|
||||
switch (action.type) {
|
||||
case 'login/SET-IS-LOGGED-IN':
|
||||
return {...state, isLoggedIn: action.value}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// actions
|
||||
|
||||
export const setIsLoggedInAC = (value: boolean) =>
|
||||
({type: 'login/SET-IS-LOGGED-IN', value} as const)
|
||||
|
||||
|
||||
// thunks
|
||||
export const loginTC = (data: LoginParamsType) => (dispatch: Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>) => {
|
||||
dispatch(setAppStatusAC('loading'))
|
||||
authAPI.login(data)
|
||||
.then(res => {
|
||||
if (res.data.resultCode === 0) {
|
||||
dispatch(setIsLoggedInAC(true))
|
||||
dispatch(setAppStatusAC('succeeded'))
|
||||
} else {
|
||||
handleServerAppError(res.data, dispatch)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
handleServerNetworkError(error, dispatch)
|
||||
})
|
||||
}
|
||||
export const logoutTC = () => (dispatch: Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>) => {
|
||||
dispatch(setAppStatusAC('loading'))
|
||||
authAPI.logout()
|
||||
.then(res => {
|
||||
if (res.data.resultCode === 0) {
|
||||
dispatch(setIsLoggedInAC(false))
|
||||
dispatch(setAppStatusAC('succeeded'))
|
||||
} else {
|
||||
handleServerAppError(res.data, dispatch)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
handleServerNetworkError(error, dispatch)
|
||||
})
|
||||
}
|
||||
|
||||
// types
|
||||
|
||||
type ActionsType = ReturnType<typeof setIsLoggedInAC>
|
||||
type InitialStateType = {
|
||||
isLoggedIn: boolean
|
||||
}
|
||||
|
||||
type ThunkDispatch = Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>
|
||||
38
src/features/TodolistsList/Todolist/Task/Task.tsx
Normal file
38
src/features/TodolistsList/Todolist/Task/Task.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { ChangeEvent, useCallback } from 'react'
|
||||
import { Checkbox, IconButton } from '@mui/material'
|
||||
import { EditableSpan } from '../../../../components/EditableSpan/EditableSpan'
|
||||
import { Delete } from '@mui/icons-material'
|
||||
import { TaskStatuses, TaskType } from '../../../../api/todolists-api'
|
||||
|
||||
type TaskPropsType = {
|
||||
task: TaskType
|
||||
todolistId: string
|
||||
changeTaskStatus: (id: string, status: TaskStatuses, todolistId: string) => void
|
||||
changeTaskTitle: (taskId: string, newTitle: string, todolistId: string) => void
|
||||
removeTask: (taskId: string, todolistId: string) => void
|
||||
}
|
||||
export const Task = React.memo((props: TaskPropsType) => {
|
||||
const onClickHandler = useCallback(() => props.removeTask(props.task.id, props.todolistId), [props.task.id, props.todolistId]);
|
||||
|
||||
const onChangeHandler = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
let newIsDoneValue = e.currentTarget.checked
|
||||
props.changeTaskStatus(props.task.id, newIsDoneValue ? TaskStatuses.Completed : TaskStatuses.New, props.todolistId)
|
||||
}, [props.task.id, props.todolistId]);
|
||||
|
||||
const onTitleChangeHandler = useCallback((newValue: string) => {
|
||||
props.changeTaskTitle(props.task.id, newValue, props.todolistId)
|
||||
}, [props.task.id, props.todolistId]);
|
||||
|
||||
return <div key={props.task.id} className={props.task.status === TaskStatuses.Completed ? 'is-done' : ''}>
|
||||
<Checkbox
|
||||
checked={props.task.status === TaskStatuses.Completed}
|
||||
color="primary"
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
|
||||
<EditableSpan value={props.task.title} onChange={onTitleChangeHandler}/>
|
||||
<IconButton onClick={onClickHandler}>
|
||||
<Delete/>
|
||||
</IconButton>
|
||||
</div>
|
||||
})
|
||||
96
src/features/TodolistsList/Todolist/Todolist.tsx
Normal file
96
src/features/TodolistsList/Todolist/Todolist.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { AddItemForm } from '../../../components/AddItemForm/AddItemForm'
|
||||
import { EditableSpan } from '../../../components/EditableSpan/EditableSpan'
|
||||
import { Task } from './Task/Task'
|
||||
import { TaskStatuses, TaskType } from '../../../api/todolists-api'
|
||||
import { FilterValuesType, TodolistDomainType } from '../todolists-reducer'
|
||||
import { fetchTasksTC } from '../tasks-reducer'
|
||||
import { useAppDispatch } from '../../../hooks/useAppDispatch';
|
||||
import { Button, IconButton } from '@mui/material'
|
||||
import { Delete } from '@mui/icons-material'
|
||||
|
||||
type PropsType = {
|
||||
todolist: TodolistDomainType
|
||||
tasks: Array<TaskType>
|
||||
changeFilter: (value: FilterValuesType, todolistId: string) => void
|
||||
addTask: (title: string, todolistId: string) => void
|
||||
changeTaskStatus: (id: string, status: TaskStatuses, todolistId: string) => void
|
||||
changeTaskTitle: (taskId: string, newTitle: string, todolistId: string) => void
|
||||
removeTask: (taskId: string, todolistId: string) => void
|
||||
removeTodolist: (id: string) => void
|
||||
changeTodolistTitle: (id: string, newTitle: string) => void
|
||||
demo?: boolean
|
||||
}
|
||||
|
||||
export const Todolist = React.memo(function ({demo = false, ...props}: PropsType) {
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
if (demo) {
|
||||
return
|
||||
}
|
||||
const thunk = fetchTasksTC(props.todolist.id)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const addTask = useCallback((title: string) => {
|
||||
props.addTask(title, props.todolist.id)
|
||||
}, [props.addTask, props.todolist.id])
|
||||
|
||||
const removeTodolist = () => {
|
||||
props.removeTodolist(props.todolist.id)
|
||||
}
|
||||
const changeTodolistTitle = useCallback((title: string) => {
|
||||
props.changeTodolistTitle(props.todolist.id, title)
|
||||
}, [props.todolist.id, props.changeTodolistTitle])
|
||||
|
||||
const onAllClickHandler = useCallback(() => props.changeFilter('all', props.todolist.id), [props.todolist.id, props.changeFilter])
|
||||
const onActiveClickHandler = useCallback(() => props.changeFilter('active', props.todolist.id), [props.todolist.id, props.changeFilter])
|
||||
const onCompletedClickHandler = useCallback(() => props.changeFilter('completed', props.todolist.id), [props.todolist.id, props.changeFilter])
|
||||
|
||||
|
||||
let tasksForTodolist = props.tasks
|
||||
|
||||
if (props.todolist.filter === 'active') {
|
||||
tasksForTodolist = props.tasks.filter(t => t.status === TaskStatuses.New)
|
||||
}
|
||||
if (props.todolist.filter === 'completed') {
|
||||
tasksForTodolist = props.tasks.filter(t => t.status === TaskStatuses.Completed)
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h3><EditableSpan value={props.todolist.title} onChange={changeTodolistTitle}/>
|
||||
<IconButton onClick={removeTodolist} disabled={props.todolist.entityStatus === 'loading'}>
|
||||
<Delete/>
|
||||
</IconButton>
|
||||
</h3>
|
||||
<AddItemForm addItem={addTask} disabled={props.todolist.entityStatus === 'loading'}/>
|
||||
<div>
|
||||
{
|
||||
tasksForTodolist.map(t => <Task key={t.id} task={t} todolistId={props.todolist.id}
|
||||
removeTask={props.removeTask}
|
||||
changeTaskTitle={props.changeTaskTitle}
|
||||
changeTaskStatus={props.changeTaskStatus}
|
||||
/>)
|
||||
}
|
||||
</div>
|
||||
<div style={{paddingTop: '10px'}}>
|
||||
<Button variant={props.todolist.filter === 'all' ? 'outlined' : 'text'}
|
||||
onClick={onAllClickHandler}
|
||||
color={'inherit'}
|
||||
>All
|
||||
</Button>
|
||||
<Button variant={props.todolist.filter === 'active' ? 'outlined' : 'text'}
|
||||
onClick={onActiveClickHandler}
|
||||
color={'primary'}>Active
|
||||
</Button>
|
||||
<Button variant={props.todolist.filter === 'completed' ? 'outlined' : 'text'}
|
||||
onClick={onCompletedClickHandler}
|
||||
color={'secondary'}>Completed
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
|
||||
|
||||
113
src/features/TodolistsList/TodolistsList.tsx
Normal file
113
src/features/TodolistsList/TodolistsList.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { AppRootStateType } from '../../app/store'
|
||||
import {
|
||||
addTodolistTC,
|
||||
changeTodolistFilterAC,
|
||||
changeTodolistTitleTC,
|
||||
fetchTodolistsTC,
|
||||
FilterValuesType,
|
||||
removeTodolistTC,
|
||||
TodolistDomainType
|
||||
} from './todolists-reducer'
|
||||
import { addTaskTC, removeTaskTC, TasksStateType, updateTaskTC } from './tasks-reducer'
|
||||
import { TaskStatuses } from '../../api/todolists-api'
|
||||
import { Grid, Paper } from '@mui/material'
|
||||
import { AddItemForm } from '../../components/AddItemForm/AddItemForm'
|
||||
import { Todolist } from './Todolist/Todolist'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { useAppDispatch } from '../../hooks/useAppDispatch';
|
||||
|
||||
type PropsType = {
|
||||
demo?: boolean
|
||||
}
|
||||
|
||||
export const TodolistsList: React.FC<PropsType> = ({demo = false}) => {
|
||||
const todolists = useSelector<AppRootStateType, Array<TodolistDomainType>>(state => state.todolists)
|
||||
const tasks = useSelector<AppRootStateType, TasksStateType>(state => state.tasks)
|
||||
const isLoggedIn = useSelector<AppRootStateType, boolean>(state => state.auth.isLoggedIn)
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
if (demo || !isLoggedIn) {
|
||||
return;
|
||||
}
|
||||
const thunk = fetchTodolistsTC()
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const removeTask = useCallback(function (id: string, todolistId: string) {
|
||||
const thunk = removeTaskTC(id, todolistId)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const addTask = useCallback(function (title: string, todolistId: string) {
|
||||
const thunk = addTaskTC(title, todolistId)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const changeStatus = useCallback(function (id: string, status: TaskStatuses, todolistId: string) {
|
||||
const thunk = updateTaskTC(id, {status}, todolistId)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const changeTaskTitle = useCallback(function (id: string, newTitle: string, todolistId: string) {
|
||||
const thunk = updateTaskTC(id, {title: newTitle}, todolistId)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const changeFilter = useCallback(function (value: FilterValuesType, todolistId: string) {
|
||||
const action = changeTodolistFilterAC(todolistId, value)
|
||||
dispatch(action)
|
||||
}, [])
|
||||
|
||||
const removeTodolist = useCallback(function (id: string) {
|
||||
const thunk = removeTodolistTC(id)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const changeTodolistTitle = useCallback(function (id: string, title: string) {
|
||||
const thunk = changeTodolistTitleTC(id, title)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const addTodolist = useCallback((title: string) => {
|
||||
const thunk = addTodolistTC(title)
|
||||
dispatch(thunk)
|
||||
}, [dispatch])
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <Navigate to={"/login"} />
|
||||
}
|
||||
|
||||
return <>
|
||||
<Grid container style={{padding: '20px'}}>
|
||||
<AddItemForm addItem={addTodolist}/>
|
||||
</Grid>
|
||||
<Grid container spacing={3}>
|
||||
{
|
||||
todolists.map(tl => {
|
||||
let allTodolistTasks = tasks[tl.id]
|
||||
|
||||
return <Grid item key={tl.id}>
|
||||
<Paper style={{padding: '10px'}}>
|
||||
<Todolist
|
||||
todolist={tl}
|
||||
tasks={allTodolistTasks}
|
||||
removeTask={removeTask}
|
||||
changeFilter={changeFilter}
|
||||
addTask={addTask}
|
||||
changeTaskStatus={changeStatus}
|
||||
removeTodolist={removeTodolist}
|
||||
changeTaskTitle={changeTaskTitle}
|
||||
changeTodolistTitle={changeTodolistTitle}
|
||||
demo={demo}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
})
|
||||
}
|
||||
</Grid>
|
||||
</>
|
||||
}
|
||||
133
src/features/TodolistsList/tasks-reducer.test.ts
Normal file
133
src/features/TodolistsList/tasks-reducer.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { addTaskAC, removeTaskAC, setTasksAC, tasksReducer, TasksStateType, updateTaskAC } from './tasks-reducer'
|
||||
|
||||
import {addTodolistAC, removeTodolistAC, setTodolistsAC} from './todolists-reducer'
|
||||
import {TaskPriorities, TaskStatuses} from '../../api/todolists-api'
|
||||
|
||||
let startState: TasksStateType = {};
|
||||
beforeEach(() => {
|
||||
startState = {
|
||||
"todolistId1": [
|
||||
{ id: "1", title: "CSS", status: TaskStatuses.New, todoListId: "todolistId1", description: '',
|
||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
|
||||
{ id: "2", title: "JS", status: TaskStatuses.Completed, todoListId: "todolistId1", description: '',
|
||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
|
||||
{ id: "3", title: "React", status: TaskStatuses.New, todoListId: "todolistId1", description: '',
|
||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low }
|
||||
],
|
||||
"todolistId2": [
|
||||
{ id: "1", title: "bread", status: TaskStatuses.New, todoListId: "todolistId2", description: '',
|
||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
|
||||
{ id: "2", title: "milk", status: TaskStatuses.Completed, todoListId: "todolistId2", description: '',
|
||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
|
||||
{ id: "3", title: "tea", status: TaskStatuses.New, todoListId: "todolistId2", description: '',
|
||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low }
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
test('correct task should be deleted from correct array', () => {
|
||||
const action = removeTaskAC("2", "todolistId2");
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
|
||||
expect(endState["todolistId1"].length).toBe(3);
|
||||
expect(endState["todolistId2"].length).toBe(2);
|
||||
expect(endState["todolistId2"].every(t => t.id != "2")).toBeTruthy();
|
||||
});
|
||||
test('correct task should be added to correct array', () => {
|
||||
//const action = addTaskAC("juce", "todolistId2");
|
||||
const action = addTaskAC({
|
||||
todoListId: "todolistId2",
|
||||
title: "juce",
|
||||
status: TaskStatuses.New,
|
||||
addedDate: "",
|
||||
deadline: "",
|
||||
description: "",
|
||||
order: 0,
|
||||
priority: 0,
|
||||
startDate: "",
|
||||
id: "id exists"
|
||||
});
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
|
||||
expect(endState["todolistId1"].length).toBe(3);
|
||||
expect(endState["todolistId2"].length).toBe(4);
|
||||
expect(endState["todolistId2"][0].id).toBeDefined();
|
||||
expect(endState["todolistId2"][0].title).toBe("juce");
|
||||
expect(endState["todolistId2"][0].status).toBe(TaskStatuses.New);
|
||||
});
|
||||
test('status of specified task should be changed', () => {
|
||||
const action = updateTaskAC("2", {status: TaskStatuses.New}, "todolistId2");
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
|
||||
expect(endState["todolistId1"][1].status).toBe(TaskStatuses.Completed);
|
||||
expect(endState["todolistId2"][1].status).toBe(TaskStatuses.New);
|
||||
});
|
||||
test('title of specified task should be changed', () => {
|
||||
const action = updateTaskAC("2", {title: "yogurt"}, "todolistId2");
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
|
||||
expect(endState["todolistId1"][1].title).toBe("JS");
|
||||
expect(endState["todolistId2"][1].title).toBe("yogurt");
|
||||
expect(endState["todolistId2"][0].title).toBe("bread");
|
||||
});
|
||||
test('new array should be added when new todolist is added', () => {
|
||||
const action = addTodolistAC({
|
||||
id: "blabla",
|
||||
title: "new todolist",
|
||||
order: 0,
|
||||
addedDate: ''
|
||||
});
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
|
||||
|
||||
const keys = Object.keys(endState);
|
||||
const newKey = keys.find(k => k != "todolistId1" && k != "todolistId2");
|
||||
if (!newKey) {
|
||||
throw Error("new key should be added")
|
||||
}
|
||||
|
||||
expect(keys.length).toBe(3);
|
||||
expect(endState[newKey]).toEqual([]);
|
||||
});
|
||||
test('propertry with todolistId should be deleted', () => {
|
||||
const action = removeTodolistAC("todolistId2");
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
|
||||
const keys = Object.keys(endState);
|
||||
|
||||
expect(keys.length).toBe(1);
|
||||
expect(endState["todolistId2"]).not.toBeDefined();
|
||||
});
|
||||
|
||||
test('empty arrays should be added when we set todolists', () => {
|
||||
const action = setTodolistsAC([
|
||||
{id: "1", title: "title 1", order: 0, addedDate: ""},
|
||||
{id: "2", title: "title 2", order: 0, addedDate: ""}
|
||||
])
|
||||
|
||||
const endState = tasksReducer({}, action)
|
||||
|
||||
const keys = Object.keys(endState)
|
||||
|
||||
expect(keys.length).toBe(2)
|
||||
expect(endState['1']).toBeDefined()
|
||||
expect(endState['2']).toBeDefined()
|
||||
})
|
||||
test('tasks should be added for todolist', () => {
|
||||
const action = setTasksAC(startState["todolistId1"], "todolistId1");
|
||||
|
||||
const endState = tasksReducer({
|
||||
"todolistId2": [],
|
||||
"todolistId1": []
|
||||
}, action)
|
||||
|
||||
expect(endState["todolistId1"].length).toBe(3)
|
||||
expect(endState["todolistId2"].length).toBe(0)
|
||||
})
|
||||
|
||||
140
src/features/TodolistsList/tasks-reducer.ts
Normal file
140
src/features/TodolistsList/tasks-reducer.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import {AddTodolistActionType, RemoveTodolistActionType, SetTodolistsActionType} from './todolists-reducer'
|
||||
import {TaskPriorities, TaskStatuses, TaskType, todolistsAPI, UpdateTaskModelType} from '../../api/todolists-api'
|
||||
import {Dispatch} from 'redux'
|
||||
import {AppRootStateType} from '../../app/store'
|
||||
import {setAppErrorAC, SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../../app/app-reducer'
|
||||
import {handleServerAppError, handleServerNetworkError} from '../../utils/error-utils'
|
||||
|
||||
const initialState: TasksStateType = {}
|
||||
|
||||
export const tasksReducer = (state: TasksStateType = initialState, action: ActionsType): TasksStateType => {
|
||||
switch (action.type) {
|
||||
case 'REMOVE-TASK':
|
||||
return {...state, [action.todolistId]: state[action.todolistId].filter(t => t.id != action.taskId)}
|
||||
case 'ADD-TASK':
|
||||
return {...state, [action.task.todoListId]: [action.task, ...state[action.task.todoListId]]}
|
||||
case 'UPDATE-TASK':
|
||||
return {
|
||||
...state,
|
||||
[action.todolistId]: state[action.todolistId]
|
||||
.map(t => t.id === action.taskId ? {...t, ...action.model} : t)
|
||||
}
|
||||
case 'ADD-TODOLIST':
|
||||
return {...state, [action.todolist.id]: []}
|
||||
case 'REMOVE-TODOLIST':
|
||||
const copyState = {...state}
|
||||
delete copyState[action.id]
|
||||
return copyState
|
||||
case 'SET-TODOLISTS': {
|
||||
const copyState = {...state}
|
||||
action.todolists.forEach(tl => {
|
||||
copyState[tl.id] = []
|
||||
})
|
||||
return copyState
|
||||
}
|
||||
case 'SET-TASKS':
|
||||
return {...state, [action.todolistId]: action.tasks}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// actions
|
||||
export const removeTaskAC = (taskId: string, todolistId: string) =>
|
||||
({type: 'REMOVE-TASK', taskId, todolistId} as const)
|
||||
export const addTaskAC = (task: TaskType) =>
|
||||
({type: 'ADD-TASK', task} as const)
|
||||
export const updateTaskAC = (taskId: string, model: UpdateDomainTaskModelType, todolistId: string) =>
|
||||
({type: 'UPDATE-TASK', model, todolistId, taskId} as const)
|
||||
export const setTasksAC = (tasks: Array<TaskType>, todolistId: string) =>
|
||||
({type: 'SET-TASKS', tasks, todolistId} as const)
|
||||
|
||||
// thunks
|
||||
export const fetchTasksTC = (todolistId: string) => (dispatch: Dispatch<ActionsType | SetAppStatusActionType>) => {
|
||||
dispatch(setAppStatusAC('loading'))
|
||||
todolistsAPI.getTasks(todolistId)
|
||||
.then((res) => {
|
||||
const tasks = res.data.items
|
||||
dispatch(setTasksAC(tasks, todolistId))
|
||||
dispatch(setAppStatusAC('succeeded'))
|
||||
})
|
||||
}
|
||||
export const removeTaskTC = (taskId: string, todolistId: string) => (dispatch: Dispatch<ActionsType>) => {
|
||||
todolistsAPI.deleteTask(todolistId, taskId)
|
||||
.then(res => {
|
||||
const action = removeTaskAC(taskId, todolistId)
|
||||
dispatch(action)
|
||||
})
|
||||
}
|
||||
export const addTaskTC = (title: string, todolistId: string) => (dispatch: Dispatch<ActionsType | SetAppErrorActionType | SetAppStatusActionType>) => {
|
||||
dispatch(setAppStatusAC('loading'))
|
||||
todolistsAPI.createTask(todolistId, title)
|
||||
.then(res => {
|
||||
if (res.data.resultCode === 0) {
|
||||
const task = res.data.data.item
|
||||
const action = addTaskAC(task)
|
||||
dispatch(action)
|
||||
dispatch(setAppStatusAC('succeeded'))
|
||||
} else {
|
||||
handleServerAppError(res.data, dispatch);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
handleServerNetworkError(error, dispatch)
|
||||
})
|
||||
}
|
||||
export const updateTaskTC = (taskId: string, domainModel: UpdateDomainTaskModelType, todolistId: string) =>
|
||||
(dispatch: ThunkDispatch, getState: () => AppRootStateType) => {
|
||||
const state = getState()
|
||||
const task = state.tasks[todolistId].find(t => t.id === taskId)
|
||||
if (!task) {
|
||||
//throw new Error("task not found in the state");
|
||||
console.warn('task not found in the state')
|
||||
return
|
||||
}
|
||||
|
||||
const apiModel: UpdateTaskModelType = {
|
||||
deadline: task.deadline,
|
||||
description: task.description,
|
||||
priority: task.priority,
|
||||
startDate: task.startDate,
|
||||
title: task.title,
|
||||
status: task.status,
|
||||
...domainModel
|
||||
}
|
||||
|
||||
todolistsAPI.updateTask(todolistId, taskId, apiModel)
|
||||
.then(res => {
|
||||
if (res.data.resultCode === 0) {
|
||||
const action = updateTaskAC(taskId, domainModel, todolistId)
|
||||
dispatch(action)
|
||||
} else {
|
||||
handleServerAppError(res.data, dispatch);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
handleServerNetworkError(error, dispatch);
|
||||
})
|
||||
}
|
||||
|
||||
// types
|
||||
export type UpdateDomainTaskModelType = {
|
||||
title?: string
|
||||
description?: string
|
||||
status?: TaskStatuses
|
||||
priority?: TaskPriorities
|
||||
startDate?: string
|
||||
deadline?: string
|
||||
}
|
||||
export type TasksStateType = {
|
||||
[key: string]: Array<TaskType>
|
||||
}
|
||||
type ActionsType =
|
||||
| ReturnType<typeof removeTaskAC>
|
||||
| ReturnType<typeof addTaskAC>
|
||||
| ReturnType<typeof updateTaskAC>
|
||||
| AddTodolistActionType
|
||||
| RemoveTodolistActionType
|
||||
| SetTodolistsActionType
|
||||
| ReturnType<typeof setTasksAC>
|
||||
type ThunkDispatch = Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>
|
||||
89
src/features/TodolistsList/todolists-reducer.test.ts
Normal file
89
src/features/TodolistsList/todolists-reducer.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
addTodolistAC, changeTodolistEntityStatusAC,
|
||||
changeTodolistFilterAC,
|
||||
changeTodolistTitleAC, FilterValuesType,
|
||||
removeTodolistAC, setTodolistsAC, TodolistDomainType,
|
||||
todolistsReducer
|
||||
} from './todolists-reducer'
|
||||
import {v1} from 'uuid'
|
||||
import {TodolistType} from '../../api/todolists-api'
|
||||
import {RequestStatusType} from '../../app/app-reducer'
|
||||
|
||||
let todolistId1: string
|
||||
let todolistId2: string
|
||||
let startState: Array<TodolistDomainType> = []
|
||||
|
||||
beforeEach(() => {
|
||||
todolistId1 = v1()
|
||||
todolistId2 = v1()
|
||||
startState = [
|
||||
{id: todolistId1, title: 'What to learn', filter: 'all', entityStatus: 'idle', addedDate: '', order: 0},
|
||||
{id: todolistId2, title: 'What to buy', filter: 'all', entityStatus: 'idle', addedDate: '', order: 0}
|
||||
]
|
||||
})
|
||||
|
||||
test('correct todolist should be removed', () => {
|
||||
const endState = todolistsReducer(startState, removeTodolistAC(todolistId1))
|
||||
|
||||
expect(endState.length).toBe(1)
|
||||
expect(endState[0].id).toBe(todolistId2)
|
||||
})
|
||||
|
||||
test('correct todolist should be added', () => {
|
||||
let todolist: TodolistType = {
|
||||
title: 'New Todolist',
|
||||
id: 'any id',
|
||||
addedDate: '',
|
||||
order: 0
|
||||
}
|
||||
|
||||
|
||||
const endState = todolistsReducer(startState, addTodolistAC(todolist))
|
||||
|
||||
expect(endState.length).toBe(3)
|
||||
expect(endState[0].title).toBe(todolist.title)
|
||||
expect(endState[0].filter).toBe('all')
|
||||
})
|
||||
|
||||
test('correct todolist should change its name', () => {
|
||||
let newTodolistTitle = 'New Todolist'
|
||||
|
||||
const action = changeTodolistTitleAC(todolistId2, newTodolistTitle)
|
||||
|
||||
const endState = todolistsReducer(startState, action)
|
||||
|
||||
expect(endState[0].title).toBe('What to learn')
|
||||
expect(endState[1].title).toBe(newTodolistTitle)
|
||||
})
|
||||
|
||||
test('correct filter of todolist should be changed', () => {
|
||||
let newFilter: FilterValuesType = 'completed'
|
||||
|
||||
const action = changeTodolistFilterAC(todolistId2, newFilter)
|
||||
|
||||
const endState = todolistsReducer(startState, action)
|
||||
|
||||
expect(endState[0].filter).toBe('all')
|
||||
expect(endState[1].filter).toBe(newFilter)
|
||||
})
|
||||
test('todolists should be added', () => {
|
||||
|
||||
const action = setTodolistsAC(startState)
|
||||
|
||||
const endState = todolistsReducer([], action)
|
||||
|
||||
expect(endState.length).toBe(2)
|
||||
})
|
||||
test('correct entity status of todolist should be changed', () => {
|
||||
let newStatus: RequestStatusType = 'loading'
|
||||
|
||||
const action = changeTodolistEntityStatusAC(todolistId2, newStatus)
|
||||
|
||||
const endState = todolistsReducer(startState, action)
|
||||
|
||||
expect(endState[0].entityStatus).toBe('idle')
|
||||
expect(endState[1].entityStatus).toBe(newStatus)
|
||||
})
|
||||
|
||||
|
||||
|
||||
109
src/features/TodolistsList/todolists-reducer.ts
Normal file
109
src/features/TodolistsList/todolists-reducer.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import {todolistsAPI, TodolistType} from '../../api/todolists-api'
|
||||
import {Dispatch} from 'redux'
|
||||
import {RequestStatusType, SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../../app/app-reducer'
|
||||
import {handleServerNetworkError} from '../../utils/error-utils'
|
||||
import { AppThunk } from '../../app/store';
|
||||
|
||||
const initialState: Array<TodolistDomainType> = []
|
||||
|
||||
export const todolistsReducer = (state: Array<TodolistDomainType> = initialState, action: ActionsType): Array<TodolistDomainType> => {
|
||||
switch (action.type) {
|
||||
case 'REMOVE-TODOLIST':
|
||||
return state.filter(tl => tl.id != action.id)
|
||||
case 'ADD-TODOLIST':
|
||||
return [{...action.todolist, filter: 'all', entityStatus: 'idle'}, ...state]
|
||||
|
||||
case 'CHANGE-TODOLIST-TITLE':
|
||||
return state.map(tl => tl.id === action.id ? {...tl, title: action.title} : tl)
|
||||
case 'CHANGE-TODOLIST-FILTER':
|
||||
return state.map(tl => tl.id === action.id ? {...tl, filter: action.filter} : tl)
|
||||
case 'CHANGE-TODOLIST-ENTITY-STATUS':
|
||||
return state.map(tl => tl.id === action.id ? {...tl, entityStatus: action.status} : tl)
|
||||
case 'SET-TODOLISTS':
|
||||
return action.todolists.map(tl => ({...tl, filter: 'all', entityStatus: 'idle'}))
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// actions
|
||||
export const removeTodolistAC = (id: string) => ({type: 'REMOVE-TODOLIST', id} as const)
|
||||
export const addTodolistAC = (todolist: TodolistType) => ({type: 'ADD-TODOLIST', todolist} as const)
|
||||
export const changeTodolistTitleAC = (id: string, title: string) => ({
|
||||
type: 'CHANGE-TODOLIST-TITLE',
|
||||
id,
|
||||
title
|
||||
} as const)
|
||||
export const changeTodolistFilterAC = (id: string, filter: FilterValuesType) => ({
|
||||
type: 'CHANGE-TODOLIST-FILTER',
|
||||
id,
|
||||
filter
|
||||
} as const)
|
||||
export const changeTodolistEntityStatusAC = (id: string, status: RequestStatusType) => ({
|
||||
type: 'CHANGE-TODOLIST-ENTITY-STATUS', id, status } as const)
|
||||
export const setTodolistsAC = (todolists: Array<TodolistType>) => ({type: 'SET-TODOLISTS', todolists} as const)
|
||||
|
||||
// thunks
|
||||
export const fetchTodolistsTC = (): AppThunk => {
|
||||
return (dispatch) => {
|
||||
dispatch(setAppStatusAC('loading'))
|
||||
todolistsAPI.getTodolists()
|
||||
.then((res) => {
|
||||
dispatch(setTodolistsAC(res.data))
|
||||
dispatch(setAppStatusAC('succeeded'))
|
||||
})
|
||||
.catch(error => {
|
||||
handleServerNetworkError(error, dispatch);
|
||||
})
|
||||
}
|
||||
}
|
||||
export const removeTodolistTC = (todolistId: string) => {
|
||||
return (dispatch: ThunkDispatch) => {
|
||||
//изменим глобальный статус приложения, чтобы вверху полоса побежала
|
||||
dispatch(setAppStatusAC('loading'))
|
||||
//изменим статус конкретного тудулиста, чтобы он мог задизеблить что надо
|
||||
dispatch(changeTodolistEntityStatusAC(todolistId, 'loading'))
|
||||
todolistsAPI.deleteTodolist(todolistId)
|
||||
.then((res) => {
|
||||
dispatch(removeTodolistAC(todolistId))
|
||||
//скажем глобально приложению, что асинхронная операция завершена
|
||||
dispatch(setAppStatusAC('succeeded'))
|
||||
})
|
||||
}
|
||||
}
|
||||
export const addTodolistTC = (title: string) => {
|
||||
return (dispatch: ThunkDispatch) => {
|
||||
dispatch(setAppStatusAC('loading'))
|
||||
todolistsAPI.createTodolist(title)
|
||||
.then((res) => {
|
||||
dispatch(addTodolistAC(res.data.data.item))
|
||||
dispatch(setAppStatusAC('succeeded'))
|
||||
})
|
||||
}
|
||||
}
|
||||
export const changeTodolistTitleTC = (id: string, title: string) => {
|
||||
return (dispatch: Dispatch<ActionsType>) => {
|
||||
todolistsAPI.updateTodolist(id, title)
|
||||
.then((res) => {
|
||||
dispatch(changeTodolistTitleAC(id, title))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// types
|
||||
export type AddTodolistActionType = ReturnType<typeof addTodolistAC>;
|
||||
export type RemoveTodolistActionType = ReturnType<typeof removeTodolistAC>;
|
||||
export type SetTodolistsActionType = ReturnType<typeof setTodolistsAC>;
|
||||
type ActionsType =
|
||||
| RemoveTodolistActionType
|
||||
| AddTodolistActionType
|
||||
| ReturnType<typeof changeTodolistTitleAC>
|
||||
| ReturnType<typeof changeTodolistFilterAC>
|
||||
| SetTodolistsActionType
|
||||
| ReturnType<typeof changeTodolistEntityStatusAC>
|
||||
export type FilterValuesType = 'all' | 'active' | 'completed';
|
||||
export type TodolistDomainType = TodolistType & {
|
||||
filter: FilterValuesType
|
||||
entityStatus: RequestStatusType
|
||||
}
|
||||
type ThunkDispatch = Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>
|
||||
27
src/features/TodolistsList/todolists-tasks-reducer.test.ts
Normal file
27
src/features/TodolistsList/todolists-tasks-reducer.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {addTodolistAC, TodolistDomainType, todolistsReducer} from './todolists-reducer'
|
||||
import {tasksReducer, TasksStateType} from './tasks-reducer'
|
||||
import {TodolistType} from '../../api/todolists-api'
|
||||
|
||||
test('ids should be equals', () => {
|
||||
const startTasksState: TasksStateType = {};
|
||||
const startTodolistsState: Array<TodolistDomainType> = [];
|
||||
|
||||
let todolist: TodolistType = {
|
||||
title: 'new todolist',
|
||||
id: 'any id',
|
||||
addedDate: '',
|
||||
order: 0
|
||||
}
|
||||
|
||||
const action = addTodolistAC(todolist);
|
||||
|
||||
const endTasksState = tasksReducer(startTasksState, action)
|
||||
const endTodolistsState = todolistsReducer(startTodolistsState, action)
|
||||
|
||||
const keys = Object.keys(endTasksState);
|
||||
const idFromTasks = keys[0];
|
||||
const idFromTodolists = endTodolistsState[0].id;
|
||||
|
||||
expect(idFromTasks).toBe(action.todolist.id);
|
||||
expect(idFromTodolists).toBe(action.todolist.id);
|
||||
});
|
||||
6
src/hooks/useAppDispatch.ts
Normal file
6
src/hooks/useAppDispatch.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { AppDispatch } from '../app/store';
|
||||
|
||||
|
||||
// export const useAppDispatch: () => AppDispatch = useDispatch
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>()
|
||||
13
src/index.css
Normal file
13
src/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
15
src/index.tsx
Normal file
15
src/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './app/App';
|
||||
import { store } from './app/store';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
|
||||
const root = createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<App/>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
7
src/logo.svg
Normal file
7
src/logo.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
||||
<g fill="#61DAFB">
|
||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
||||
<path d="M520.5 78.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
1
src/react-app-env.d.ts
vendored
Normal file
1
src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
149
src/serviceWorker.ts
Normal file
149
src/serviceWorker.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// 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 subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
type Config = {
|
||||
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
||||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||
};
|
||||
|
||||
export function register(config?: Config) {
|
||||
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.href
|
||||
);
|
||||
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/facebook/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. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// 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://bit.ly/CRA-PWA'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl: string, config?: Config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} 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.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' }
|
||||
})
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.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, config);
|
||||
}
|
||||
})
|
||||
.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();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
5
src/setupTests.ts
Normal file
5
src/setupTests.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
17
src/utils/error-utils.ts
Normal file
17
src/utils/error-utils.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {setAppErrorAC, SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../app/app-reducer'
|
||||
import {ResponseType} from '../api/todolists-api'
|
||||
import {Dispatch} from 'redux'
|
||||
|
||||
export const handleServerAppError = <D>(data: ResponseType<D>, dispatch: Dispatch<SetAppErrorActionType | SetAppStatusActionType>) => {
|
||||
if (data.messages.length) {
|
||||
dispatch(setAppErrorAC(data.messages[0]))
|
||||
} else {
|
||||
dispatch(setAppErrorAC('Some error occurred'))
|
||||
}
|
||||
dispatch(setAppStatusAC('failed'))
|
||||
}
|
||||
|
||||
export const handleServerNetworkError = (error: { message: string }, dispatch: Dispatch<SetAppErrorActionType | SetAppStatusActionType>) => {
|
||||
dispatch(setAppErrorAC(error.message ? error.message : 'Some error occurred'))
|
||||
dispatch(setAppStatusAC('failed'))
|
||||
}
|
||||
Reference in New Issue
Block a user