mirror of
https://github.com/ershisan99/it-incubator-todolist-ts-17-live-2024-08-17.git
synced 2025-12-16 05:09:26 +00:00
chore: add prettier and run it on all files
This commit is contained in:
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"singleAttributePerLine": true
|
||||
}
|
||||
115
package.json
115
package.json
@@ -1,58 +1,61 @@
|
||||
{
|
||||
"name": "it-incubator-todolist-ts",
|
||||
"jest": {
|
||||
"moduleNameMapper": {
|
||||
"axios": "axios/dist/node/axios.cjs"
|
||||
}
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.7",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.2",
|
||||
"@mui/material": "^5.15.2",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@testing-library/jest-dom": "^6.1.6",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"axios": "^1.6.3",
|
||||
"formik": "^2.4.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^9.0.4",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"typescript": "^5.3.3",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
"name": "it-incubator-todolist-ts",
|
||||
"jest": {
|
||||
"moduleNameMapper": {
|
||||
"axios": "axios/dist/node/axios.cjs"
|
||||
}
|
||||
},
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.7",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.2",
|
||||
"@mui/material": "^5.15.2",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@testing-library/jest-dom": "^6.1.6",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"axios": "^1.6.3",
|
||||
"formik": "^2.4.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^9.0.4",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"typescript": "^5.3.3",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "3.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link
|
||||
rel="icon"
|
||||
href="%PUBLIC_URL%/favicon.ico"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#000000"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
href="%PUBLIC_URL%/logo192.png"
|
||||
/>
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link
|
||||
rel="manifest"
|
||||
href="%PUBLIC_URL%/manifest.json"
|
||||
/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
|
||||
@@ -1,118 +1,140 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const settings = {
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'API-KEY': '1cdd9f77-c60e-4af5-b194-659e4ebd5d41'
|
||||
}
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'API-KEY': '1cdd9f77-c60e-4af5-b194-659e4ebd5d41',
|
||||
},
|
||||
}
|
||||
const instance = axios.create({
|
||||
baseURL: 'https://social-network.samuraijs.com/api/1.1/',
|
||||
...settings
|
||||
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);
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
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
|
||||
id: string
|
||||
title: string
|
||||
addedDate: string
|
||||
order: number
|
||||
}
|
||||
export type ResponseType<D = {}> = {
|
||||
resultCode: number
|
||||
messages: Array<string>
|
||||
data: D
|
||||
resultCode: number
|
||||
messages: Array<string>
|
||||
data: D
|
||||
}
|
||||
|
||||
export enum TaskStatuses {
|
||||
New = 0,
|
||||
InProgress = 1,
|
||||
Completed = 2,
|
||||
Draft = 3
|
||||
New = 0,
|
||||
InProgress = 1,
|
||||
Completed = 2,
|
||||
Draft = 3,
|
||||
}
|
||||
|
||||
export enum TaskPriorities {
|
||||
Low = 0,
|
||||
Middle = 1,
|
||||
Hi = 2,
|
||||
Urgently = 3,
|
||||
Later = 4
|
||||
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
|
||||
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
|
||||
title: string
|
||||
description: string
|
||||
status: TaskStatuses
|
||||
priority: TaskPriorities
|
||||
startDate: string
|
||||
deadline: string
|
||||
}
|
||||
type GetTasksResponse = {
|
||||
error: string | null
|
||||
totalCount: number
|
||||
items: TaskType[]
|
||||
error: string | null
|
||||
totalCount: number
|
||||
items: TaskType[]
|
||||
}
|
||||
|
||||
133
src/app/App.tsx
133
src/app/App.tsx
@@ -9,67 +9,96 @@ 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';
|
||||
AppBar,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Container,
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
Toolbar,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { Menu } from '@mui/icons-material'
|
||||
|
||||
type PropsType = {
|
||||
demo?: boolean
|
||||
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>()
|
||||
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())
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
dispatch(initializeAppTC())
|
||||
}, [])
|
||||
|
||||
const logoutHandler = useCallback(() => {
|
||||
dispatch(logoutTC())
|
||||
}, [])
|
||||
const logoutHandler = useCallback(() => {
|
||||
dispatch(logoutTC())
|
||||
}, [])
|
||||
|
||||
if (!isInitialized) {
|
||||
return <div
|
||||
style={{position: 'fixed', top: '30%', textAlign: 'center', width: '100%'}}>
|
||||
<CircularProgress/>
|
||||
</div>
|
||||
}
|
||||
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>
|
||||
)
|
||||
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
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import { appReducer, InitialStateType, setAppErrorAC, setAppStatusAC } from './app-reducer'
|
||||
import {
|
||||
appReducer,
|
||||
InitialStateType,
|
||||
setAppErrorAC,
|
||||
setAppStatusAC,
|
||||
} from './app-reducer'
|
||||
|
||||
let startState: InitialStateType;
|
||||
let startState: InitialStateType
|
||||
|
||||
beforeEach(() => {
|
||||
startState = {
|
||||
error: null,
|
||||
status: 'idle',
|
||||
isInitialized: false
|
||||
}
|
||||
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');
|
||||
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');
|
||||
const endState = appReducer(startState, setAppStatusAC('loading'))
|
||||
expect(endState.status).toBe('loading')
|
||||
})
|
||||
|
||||
|
||||
@@ -1,57 +1,61 @@
|
||||
import {Dispatch} from 'redux'
|
||||
import {authAPI} from '../api/todolists-api'
|
||||
import {setIsLoggedInAC} from '../features/Login/auth-reducer'
|
||||
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
|
||||
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 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
|
||||
// происходит ли сейчас взаимодействие с сервером
|
||||
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 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 {
|
||||
authAPI.me().then((res) => {
|
||||
if (res.data.resultCode === 0) {
|
||||
dispatch(setIsLoggedInAC(true))
|
||||
} else {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dispatch(setAppInitializedAC(true));
|
||||
})
|
||||
dispatch(setAppInitializedAC(true))
|
||||
})
|
||||
}
|
||||
|
||||
export type SetAppErrorActionType = ReturnType<typeof setAppErrorAC>
|
||||
export type SetAppStatusActionType = ReturnType<typeof setAppStatusAC>
|
||||
|
||||
|
||||
type ActionsType =
|
||||
| SetAppErrorActionType
|
||||
| SetAppStatusActionType
|
||||
| ReturnType<typeof setAppInitializedAC>
|
||||
| SetAppErrorActionType
|
||||
| SetAppStatusActionType
|
||||
| ReturnType<typeof setAppInitializedAC>
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
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";
|
||||
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
|
||||
tasks: tasksReducer,
|
||||
todolists: todolistsReducer,
|
||||
app: appReducer,
|
||||
auth: authReducer,
|
||||
})
|
||||
|
||||
// ❗старая запись, с новыми версиями не работает
|
||||
// const store = createStore(rootReducer, applyMiddleware(thunkMiddleware));
|
||||
export const store = configureStore({reducer: rootReducer},)
|
||||
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 AppThunk<ReturnType = void> = ThunkAction<
|
||||
ReturnType,
|
||||
AppRootStateType,
|
||||
unknown,
|
||||
UnknownAction
|
||||
>
|
||||
|
||||
// export type AppDispatch = typeof store.dispatch
|
||||
// ❗ UnknownAction вместо AnyAction
|
||||
export type AppDispatch = ThunkDispatch<AppRootStateType, unknown, UnknownAction>
|
||||
export type AppDispatch = ThunkDispatch<
|
||||
AppRootStateType,
|
||||
unknown,
|
||||
UnknownAction
|
||||
>
|
||||
|
||||
@@ -1,51 +1,60 @@
|
||||
import React, { ChangeEvent, KeyboardEvent, useState } from 'react';
|
||||
import { IconButton, TextField } from '@mui/material';
|
||||
import { AddBox } from '@mui/icons-material';
|
||||
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
|
||||
addItem: (title: string) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const AddItemForm = React.memo(function ({addItem, disabled = false}: AddItemFormPropsType) {
|
||||
export const AddItemForm = React.memo(function ({
|
||||
addItem,
|
||||
disabled = false,
|
||||
}: AddItemFormPropsType) {
|
||||
let [title, setTitle] = useState('')
|
||||
let [error, setError] = useState<string | null>(null)
|
||||
|
||||
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 addItemHandler = () => {
|
||||
if (title.trim() !== '') {
|
||||
addItem(title);
|
||||
setTitle('');
|
||||
} else {
|
||||
setError('Title is required');
|
||||
}
|
||||
}
|
||||
const onChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(e.currentTarget.value)
|
||||
}
|
||||
|
||||
const onChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(e.currentTarget.value)
|
||||
}
|
||||
const onKeyPressHandler = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (error !== null) {
|
||||
setError(null)
|
||||
}
|
||||
if (e.charCode === 13) {
|
||||
addItemHandler()
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
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>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
import React, {ChangeEvent, useState} from 'react';
|
||||
import { TextField } from '@mui/material';
|
||||
import React, { ChangeEvent, useState } from 'react'
|
||||
import { TextField } from '@mui/material'
|
||||
|
||||
type EditableSpanPropsType = {
|
||||
value: string
|
||||
onChange: (newValue: string) => void
|
||||
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);
|
||||
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)
|
||||
}
|
||||
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>
|
||||
});
|
||||
return editMode ? (
|
||||
<TextField
|
||||
value={title}
|
||||
onChange={changeTitle}
|
||||
autoFocus
|
||||
onBlur={activateViewMode}
|
||||
/>
|
||||
) : (
|
||||
<span onDoubleClick={activateEditMode}>{props.value}</span>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,37 +1,53 @@
|
||||
import React from 'react'
|
||||
import {useDispatch, useSelector} from 'react-redux'
|
||||
import {AppRootStateType} from '../../app/store'
|
||||
import {setAppErrorAC} from '../../app/app-reducer'
|
||||
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';
|
||||
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} />;
|
||||
});
|
||||
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 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 handleClose = (
|
||||
event?: React.SyntheticEvent | Event,
|
||||
reason?: string
|
||||
) => {
|
||||
if (reason === 'clickaway') {
|
||||
return
|
||||
}
|
||||
dispatch(setAppErrorAC(null))
|
||||
}
|
||||
|
||||
const isOpen = error !== null
|
||||
|
||||
const isOpen = error !== null;
|
||||
|
||||
return (
|
||||
<Snackbar open={isOpen} autoHideDuration={6000} onClose={handleClose}>
|
||||
<Alert onClose={handleClose} severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
)
|
||||
return (
|
||||
<Snackbar
|
||||
open={isOpen}
|
||||
autoHideDuration={6000}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<Alert
|
||||
onClose={handleClose}
|
||||
severity='error'
|
||||
>
|
||||
{error}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,86 +4,113 @@ 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'
|
||||
import { useAppDispatch } from '../../hooks/useAppDispatch'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
FormLabel,
|
||||
Grid,
|
||||
TextField,
|
||||
} from '@mui/material'
|
||||
|
||||
export const Login = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const isLoggedIn = useSelector<AppRootStateType, boolean>(state => state.auth.isLoggedIn);
|
||||
const isLoggedIn = useSelector<AppRootStateType, boolean>(
|
||||
(state) => state.auth.isLoggedIn
|
||||
)
|
||||
|
||||
const formik = useFormik({
|
||||
validate: (values) => {
|
||||
if (!values.email) {
|
||||
return {
|
||||
email: 'Email is required'
|
||||
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}
|
||||
/>
|
||||
}
|
||||
}
|
||||
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>
|
||||
/>
|
||||
<Button
|
||||
type={'submit'}
|
||||
variant={'contained'}
|
||||
color={'primary'}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</FormControl>
|
||||
</form>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,64 +1,89 @@
|
||||
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'
|
||||
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
|
||||
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
|
||||
}
|
||||
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)
|
||||
|
||||
({ type: 'login/SET-IS-LOGGED-IN', value }) as const
|
||||
|
||||
// thunks
|
||||
export const loginTC = (data: LoginParamsType) => (dispatch: Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>) => {
|
||||
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>) => {
|
||||
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)
|
||||
})
|
||||
}
|
||||
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
|
||||
isLoggedIn: boolean
|
||||
}
|
||||
|
||||
type ThunkDispatch = Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>
|
||||
type ThunkDispatch = Dispatch<
|
||||
ActionsType | SetAppStatusActionType | SetAppErrorActionType
|
||||
>
|
||||
|
||||
@@ -5,34 +5,63 @@ 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
|
||||
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 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 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]);
|
||||
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}
|
||||
/>
|
||||
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>
|
||||
<EditableSpan
|
||||
value={props.task.title}
|
||||
onChange={onTitleChangeHandler}
|
||||
/>
|
||||
<IconButton onClick={onClickHandler}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -5,92 +5,139 @@ 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 { 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
|
||||
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) {
|
||||
export const Todolist = React.memo(function ({
|
||||
demo = false,
|
||||
...props
|
||||
}: PropsType) {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
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)
|
||||
useEffect(() => {
|
||||
if (demo) {
|
||||
return
|
||||
}
|
||||
const changeTodolistTitle = useCallback((title: string) => {
|
||||
props.changeTodolistTitle(props.todolist.id, title)
|
||||
}, [props.todolist.id, props.changeTodolistTitle])
|
||||
const thunk = fetchTasksTC(props.todolist.id)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
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])
|
||||
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]
|
||||
)
|
||||
|
||||
let tasksForTodolist = props.tasks
|
||||
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]
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
let tasksForTodolist = props.tasks
|
||||
|
||||
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>
|
||||
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>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -2,112 +2,148 @@ import React, { useCallback, useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { AppRootStateType } from '../../app/store'
|
||||
import {
|
||||
addTodolistTC,
|
||||
changeTodolistFilterAC,
|
||||
changeTodolistTitleTC,
|
||||
fetchTodolistsTC,
|
||||
FilterValuesType,
|
||||
removeTodolistTC,
|
||||
TodolistDomainType
|
||||
addTodolistTC,
|
||||
changeTodolistFilterAC,
|
||||
changeTodolistTitleTC,
|
||||
fetchTodolistsTC,
|
||||
FilterValuesType,
|
||||
removeTodolistTC,
|
||||
TodolistDomainType,
|
||||
} from './todolists-reducer'
|
||||
import { addTaskTC, removeTaskTC, TasksStateType, updateTaskTC } from './tasks-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';
|
||||
import { useAppDispatch } from '../../hooks/useAppDispatch'
|
||||
|
||||
type PropsType = {
|
||||
demo?: boolean
|
||||
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)
|
||||
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()
|
||||
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"} />
|
||||
useEffect(() => {
|
||||
if (demo || !isLoggedIn) {
|
||||
return
|
||||
}
|
||||
const thunk = fetchTodolistsTC()
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
return <>
|
||||
<Grid container style={{padding: '20px'}}>
|
||||
<AddItemForm addItem={addTodolist}/>
|
||||
</Grid>
|
||||
<Grid container spacing={3}>
|
||||
{
|
||||
todolists.map(tl => {
|
||||
let allTodolistTasks = tasks[tl.id]
|
||||
const removeTask = useCallback(function (id: string, todolistId: string) {
|
||||
const thunk = removeTaskTC(id, todolistId)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
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>
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,133 +1,205 @@
|
||||
import { addTaskAC, removeTaskAC, setTasksAC, tasksReducer, TasksStateType, updateTaskAC } from './tasks-reducer'
|
||||
import {
|
||||
addTaskAC,
|
||||
removeTaskAC,
|
||||
setTasksAC,
|
||||
tasksReducer,
|
||||
TasksStateType,
|
||||
updateTaskAC,
|
||||
} from './tasks-reducer'
|
||||
|
||||
import {addTodolistAC, removeTodolistAC, setTodolistsAC} from './todolists-reducer'
|
||||
import {TaskPriorities, TaskStatuses} from '../../api/todolists-api'
|
||||
import {
|
||||
addTodolistAC,
|
||||
removeTodolistAC,
|
||||
setTodolistsAC,
|
||||
} from './todolists-reducer'
|
||||
import { TaskPriorities, TaskStatuses } from '../../api/todolists-api'
|
||||
|
||||
let startState: TasksStateType = {};
|
||||
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 }
|
||||
]
|
||||
};
|
||||
});
|
||||
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 action = removeTaskAC('2', 'todolistId2')
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
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();
|
||||
});
|
||||
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 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)
|
||||
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);
|
||||
});
|
||||
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 action = updateTaskAC('2', { status: TaskStatuses.New }, 'todolistId2')
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
const endState = tasksReducer(startState, action)
|
||||
|
||||
expect(endState["todolistId1"][1].status).toBe(TaskStatuses.Completed);
|
||||
expect(endState["todolistId2"][1].status).toBe(TaskStatuses.New);
|
||||
});
|
||||
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 action = updateTaskAC('2', { title: 'yogurt' }, 'todolistId2')
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
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");
|
||||
});
|
||||
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 action = addTodolistAC({
|
||||
id: 'blabla',
|
||||
title: 'new todolist',
|
||||
order: 0,
|
||||
addedDate: '',
|
||||
})
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
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')
|
||||
}
|
||||
|
||||
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([]);
|
||||
});
|
||||
expect(keys.length).toBe(3)
|
||||
expect(endState[newKey]).toEqual([])
|
||||
})
|
||||
test('propertry with todolistId should be deleted', () => {
|
||||
const action = removeTodolistAC("todolistId2");
|
||||
const action = removeTodolistAC('todolistId2')
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
const endState = tasksReducer(startState, action)
|
||||
|
||||
const keys = Object.keys(endState);
|
||||
const keys = Object.keys(endState)
|
||||
|
||||
expect(keys.length).toBe(1);
|
||||
expect(endState["todolistId2"]).not.toBeDefined();
|
||||
});
|
||||
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 action = setTodolistsAC([
|
||||
{ id: '1', title: 'title 1', order: 0, addedDate: '' },
|
||||
{ id: '2', title: 'title 2', order: 0, addedDate: '' },
|
||||
])
|
||||
|
||||
const endState = tasksReducer({}, action)
|
||||
const endState = tasksReducer({}, action)
|
||||
|
||||
const keys = Object.keys(endState)
|
||||
const keys = Object.keys(endState)
|
||||
|
||||
expect(keys.length).toBe(2)
|
||||
expect(endState['1']).toBeDefined()
|
||||
expect(endState['2']).toBeDefined()
|
||||
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 action = setTasksAC(startState['todolistId1'], 'todolistId1')
|
||||
|
||||
const endState = tasksReducer({
|
||||
"todolistId2": [],
|
||||
"todolistId1": []
|
||||
}, action)
|
||||
const endState = tasksReducer(
|
||||
{
|
||||
todolistId2: [],
|
||||
todolistId1: [],
|
||||
},
|
||||
action
|
||||
)
|
||||
|
||||
expect(endState["todolistId1"].length).toBe(3)
|
||||
expect(endState["todolistId2"].length).toBe(0)
|
||||
expect(endState['todolistId1'].length).toBe(3)
|
||||
expect(endState['todolistId2'].length).toBe(0)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,140 +1,192 @@
|
||||
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'
|
||||
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
|
||||
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)
|
||||
({ 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)
|
||||
({ 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)
|
||||
({ type: 'SET-TASKS', tasks, todolistId }) as const
|
||||
|
||||
// thunks
|
||||
export const fetchTasksTC = (todolistId: string) => (dispatch: Dispatch<ActionsType | SetAppStatusActionType>) => {
|
||||
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>) => {
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
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);
|
||||
})
|
||||
})
|
||||
.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
|
||||
title?: string
|
||||
description?: string
|
||||
status?: TaskStatuses
|
||||
priority?: TaskPriorities
|
||||
startDate?: string
|
||||
deadline?: string
|
||||
}
|
||||
export type TasksStateType = {
|
||||
[key: string]: Array<TaskType>
|
||||
[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>
|
||||
| ReturnType<typeof removeTaskAC>
|
||||
| ReturnType<typeof addTaskAC>
|
||||
| ReturnType<typeof updateTaskAC>
|
||||
| AddTodolistActionType
|
||||
| RemoveTodolistActionType
|
||||
| SetTodolistsActionType
|
||||
| ReturnType<typeof setTasksAC>
|
||||
type ThunkDispatch = Dispatch<
|
||||
ActionsType | SetAppStatusActionType | SetAppErrorActionType
|
||||
>
|
||||
|
||||
@@ -1,89 +1,102 @@
|
||||
import {
|
||||
addTodolistAC, changeTodolistEntityStatusAC,
|
||||
changeTodolistFilterAC,
|
||||
changeTodolistTitleAC, FilterValuesType,
|
||||
removeTodolistAC, setTodolistsAC, TodolistDomainType,
|
||||
todolistsReducer
|
||||
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'
|
||||
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}
|
||||
]
|
||||
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))
|
||||
const endState = todolistsReducer(startState, removeTodolistAC(todolistId1))
|
||||
|
||||
expect(endState.length).toBe(1)
|
||||
expect(endState[0].id).toBe(todolistId2)
|
||||
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
|
||||
}
|
||||
let todolist: TodolistType = {
|
||||
title: 'New Todolist',
|
||||
id: 'any id',
|
||||
addedDate: '',
|
||||
order: 0,
|
||||
}
|
||||
|
||||
const endState = todolistsReducer(startState, addTodolistAC(todolist))
|
||||
|
||||
const endState = todolistsReducer(startState, addTodolistAC(todolist))
|
||||
|
||||
expect(endState.length).toBe(3)
|
||||
expect(endState[0].title).toBe(todolist.title)
|
||||
expect(endState[0].filter).toBe('all')
|
||||
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'
|
||||
let newTodolistTitle = 'New Todolist'
|
||||
|
||||
const action = changeTodolistTitleAC(todolistId2, newTodolistTitle)
|
||||
const action = changeTodolistTitleAC(todolistId2, newTodolistTitle)
|
||||
|
||||
const endState = todolistsReducer(startState, action)
|
||||
const endState = todolistsReducer(startState, action)
|
||||
|
||||
expect(endState[0].title).toBe('What to learn')
|
||||
expect(endState[1].title).toBe(newTodolistTitle)
|
||||
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'
|
||||
let newFilter: FilterValuesType = 'completed'
|
||||
|
||||
const action = changeTodolistFilterAC(todolistId2, newFilter)
|
||||
const action = changeTodolistFilterAC(todolistId2, newFilter)
|
||||
|
||||
const endState = todolistsReducer(startState, action)
|
||||
const endState = todolistsReducer(startState, action)
|
||||
|
||||
expect(endState[0].filter).toBe('all')
|
||||
expect(endState[1].filter).toBe(newFilter)
|
||||
expect(endState[0].filter).toBe('all')
|
||||
expect(endState[1].filter).toBe(newFilter)
|
||||
})
|
||||
test('todolists should be added', () => {
|
||||
const action = setTodolistsAC(startState)
|
||||
|
||||
const action = setTodolistsAC(startState)
|
||||
const endState = todolistsReducer([], action)
|
||||
|
||||
const endState = todolistsReducer([], action)
|
||||
|
||||
expect(endState.length).toBe(2)
|
||||
expect(endState.length).toBe(2)
|
||||
})
|
||||
test('correct entity status of todolist should be changed', () => {
|
||||
let newStatus: RequestStatusType = 'loading'
|
||||
let newStatus: RequestStatusType = 'loading'
|
||||
|
||||
const action = changeTodolistEntityStatusAC(todolistId2, newStatus)
|
||||
const action = changeTodolistEntityStatusAC(todolistId2, newStatus)
|
||||
|
||||
const endState = todolistsReducer(startState, action)
|
||||
const endState = todolistsReducer(startState, action)
|
||||
|
||||
expect(endState[0].entityStatus).toBe('idle')
|
||||
expect(endState[1].entityStatus).toBe(newStatus)
|
||||
expect(endState[0].entityStatus).toBe('idle')
|
||||
expect(endState[1].entityStatus).toBe(newStatus)
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,109 +1,142 @@
|
||||
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';
|
||||
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]
|
||||
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
|
||||
}
|
||||
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) => ({
|
||||
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) => ({
|
||||
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)
|
||||
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);
|
||||
})
|
||||
}
|
||||
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'))
|
||||
})
|
||||
}
|
||||
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'))
|
||||
})
|
||||
}
|
||||
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))
|
||||
})
|
||||
}
|
||||
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>;
|
||||
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';
|
||||
| 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
|
||||
filter: FilterValuesType
|
||||
entityStatus: RequestStatusType
|
||||
}
|
||||
type ThunkDispatch = Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>
|
||||
type ThunkDispatch = Dispatch<
|
||||
ActionsType | SetAppStatusActionType | SetAppErrorActionType
|
||||
>
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
import {addTodolistAC, TodolistDomainType, todolistsReducer} from './todolists-reducer'
|
||||
import {tasksReducer, TasksStateType} from './tasks-reducer'
|
||||
import {TodolistType} from '../../api/todolists-api'
|
||||
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> = [];
|
||||
const startTasksState: TasksStateType = {}
|
||||
const startTodolistsState: Array<TodolistDomainType> = []
|
||||
|
||||
let todolist: TodolistType = {
|
||||
title: 'new todolist',
|
||||
id: 'any id',
|
||||
addedDate: '',
|
||||
order: 0
|
||||
}
|
||||
let todolist: TodolistType = {
|
||||
title: 'new todolist',
|
||||
id: 'any id',
|
||||
addedDate: '',
|
||||
order: 0,
|
||||
}
|
||||
|
||||
const action = addTodolistAC(todolist);
|
||||
const action = addTodolistAC(todolist)
|
||||
|
||||
const endTasksState = tasksReducer(startTasksState, action)
|
||||
const endTodolistsState = todolistsReducer(startTodolistsState, action)
|
||||
const endTasksState = tasksReducer(startTasksState, action)
|
||||
const endTodolistsState = todolistsReducer(startTodolistsState, action)
|
||||
|
||||
const keys = Object.keys(endTasksState);
|
||||
const idFromTasks = keys[0];
|
||||
const idFromTodolists = endTodolistsState[0].id;
|
||||
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);
|
||||
});
|
||||
expect(idFromTasks).toBe(action.todolist.id)
|
||||
expect(idFromTodolists).toBe(action.todolist.id)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { AppDispatch } from '../app/store';
|
||||
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { AppDispatch } from '../app/store'
|
||||
|
||||
// export const useAppDispatch: () => AppDispatch = useDispatch
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>()
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>()
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
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';
|
||||
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);
|
||||
const root = createRoot(document.getElementById('root') as HTMLElement)
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<App/>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
)
|
||||
|
||||
@@ -18,33 +18,30 @@ const isLocalhost = Boolean(
|
||||
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;
|
||||
};
|
||||
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
|
||||
);
|
||||
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;
|
||||
return
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
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);
|
||||
checkValidServiceWorker(swUrl, config)
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
@@ -52,24 +49,24 @@ export function register(config?: Config) {
|
||||
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);
|
||||
registerValidSW(swUrl, config)
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl: string, config?: Config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
const installingWorker = registration.installing
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
@@ -80,70 +77,70 @@ function registerValidSW(swUrl: string, config?: Config) {
|
||||
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);
|
||||
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.');
|
||||
console.log('Content is cached for offline use.')
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
config.onSuccess(registration)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error)
|
||||
})
|
||||
.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' }
|
||||
headers: { 'Service-Worker': 'script' },
|
||||
})
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
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 => {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
window.location.reload()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
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();
|
||||
.then((registration) => {
|
||||
registration.unregister()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error.message)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
// 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';
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import {setAppErrorAC, SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../app/app-reducer'
|
||||
import {ResponseType} from '../api/todolists-api'
|
||||
import {Dispatch} from 'redux'
|
||||
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 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'))
|
||||
export const handleServerNetworkError = (
|
||||
error: { message: string },
|
||||
dispatch: Dispatch<SetAppErrorActionType | SetAppStatusActionType>
|
||||
) => {
|
||||
dispatch(setAppErrorAC(error.message ? error.message : 'Some error occurred'))
|
||||
dispatch(setAppStatusAC('failed'))
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
@@ -19,7 +15,5 @@
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
36
yarn.lock
36
yarn.lock
@@ -7973,6 +7973,11 @@ prelude-ls@~1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
|
||||
|
||||
prettier@3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"
|
||||
integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==
|
||||
|
||||
pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
||||
@@ -8972,7 +8977,16 @@ string-natural-compare@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0:
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -9055,7 +9069,14 @@ stringify-object@^3.3.0:
|
||||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -10105,7 +10126,16 @@ workbox-window@6.6.1:
|
||||
"@types/trusted-types" "^2.0.2"
|
||||
workbox-core "6.6.1"
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
||||
Reference in New Issue
Block a user