mirror of
https://github.com/ershisan99/it-incubator-todolist-ts-17-live-2024-08-17.git
synced 2025-12-16 12:33:29 +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
|
||||||
|
}
|
||||||
@@ -54,5 +54,8 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "3.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,35 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
rel="icon"
|
||||||
<meta name="theme-color" content="#000000" />
|
href="%PUBLIC_URL%/favicon.ico"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="theme-color"
|
||||||
|
content="#000000"
|
||||||
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Web site created using create-react-app"
|
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
|
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/
|
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.
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
|||||||
@@ -3,46 +3,58 @@ import axios from 'axios'
|
|||||||
const settings = {
|
const settings = {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
headers: {
|
headers: {
|
||||||
'API-KEY': '1cdd9f77-c60e-4af5-b194-659e4ebd5d41'
|
'API-KEY': '1cdd9f77-c60e-4af5-b194-659e4ebd5d41',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
baseURL: 'https://social-network.samuraijs.com/api/1.1/',
|
baseURL: 'https://social-network.samuraijs.com/api/1.1/',
|
||||||
...settings
|
...settings,
|
||||||
})
|
})
|
||||||
|
|
||||||
// api
|
// api
|
||||||
export const todolistsAPI = {
|
export const todolistsAPI = {
|
||||||
getTodolists() {
|
getTodolists() {
|
||||||
const promise = instance.get<TodolistType[]>('todo-lists');
|
const promise = instance.get<TodolistType[]>('todo-lists')
|
||||||
return promise;
|
return promise
|
||||||
},
|
},
|
||||||
createTodolist(title: string) {
|
createTodolist(title: string) {
|
||||||
const promise = instance.post<ResponseType<{ item: TodolistType }>>('todo-lists', {title: title});
|
const promise = instance.post<ResponseType<{ item: TodolistType }>>(
|
||||||
return promise;
|
'todo-lists',
|
||||||
|
{ title: title }
|
||||||
|
)
|
||||||
|
return promise
|
||||||
},
|
},
|
||||||
deleteTodolist(id: string) {
|
deleteTodolist(id: string) {
|
||||||
const promise = instance.delete<ResponseType>(`todo-lists/${id}`);
|
const promise = instance.delete<ResponseType>(`todo-lists/${id}`)
|
||||||
return promise;
|
return promise
|
||||||
},
|
},
|
||||||
updateTodolist(id: string, title: string) {
|
updateTodolist(id: string, title: string) {
|
||||||
const promise = instance.put<ResponseType>(`todo-lists/${id}`, {title: title});
|
const promise = instance.put<ResponseType>(`todo-lists/${id}`, {
|
||||||
return promise;
|
title: title,
|
||||||
|
})
|
||||||
|
return promise
|
||||||
},
|
},
|
||||||
getTasks(todolistId: string) {
|
getTasks(todolistId: string) {
|
||||||
return instance.get<GetTasksResponse>(`todo-lists/${todolistId}/tasks`);
|
return instance.get<GetTasksResponse>(`todo-lists/${todolistId}/tasks`)
|
||||||
},
|
},
|
||||||
deleteTask(todolistId: string, taskId: string) {
|
deleteTask(todolistId: string, taskId: string) {
|
||||||
return instance.delete<ResponseType>(`todo-lists/${todolistId}/tasks/${taskId}`);
|
return instance.delete<ResponseType>(
|
||||||
|
`todo-lists/${todolistId}/tasks/${taskId}`
|
||||||
|
)
|
||||||
},
|
},
|
||||||
createTask(todolistId: string, taskTitile: string) {
|
createTask(todolistId: string, taskTitile: string) {
|
||||||
return instance.post<ResponseType<{ item: TaskType}>>(`todo-lists/${todolistId}/tasks`, {title: taskTitile});
|
return instance.post<ResponseType<{ item: TaskType }>>(
|
||||||
|
`todo-lists/${todolistId}/tasks`,
|
||||||
|
{ title: taskTitile }
|
||||||
|
)
|
||||||
},
|
},
|
||||||
updateTask(todolistId: string, taskId: string, model: UpdateTaskModelType) {
|
updateTask(todolistId: string, taskId: string, model: UpdateTaskModelType) {
|
||||||
return instance.put<ResponseType<TaskType>>(`todo-lists/${todolistId}/tasks/${taskId}`, model);
|
return instance.put<ResponseType<TaskType>>(
|
||||||
|
`todo-lists/${todolistId}/tasks/${taskId}`,
|
||||||
|
model
|
||||||
|
)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export type LoginParamsType = {
|
export type LoginParamsType = {
|
||||||
email: string
|
email: string
|
||||||
@@ -53,17 +65,24 @@ export type LoginParamsType = {
|
|||||||
|
|
||||||
export const authAPI = {
|
export const authAPI = {
|
||||||
login(data: LoginParamsType) {
|
login(data: LoginParamsType) {
|
||||||
const promise = instance.post<ResponseType<{userId?: number}>>('auth/login', data);
|
const promise = instance.post<ResponseType<{ userId?: number }>>(
|
||||||
return promise;
|
'auth/login',
|
||||||
|
data
|
||||||
|
)
|
||||||
|
return promise
|
||||||
},
|
},
|
||||||
logout() {
|
logout() {
|
||||||
const promise = instance.delete<ResponseType<{userId?: number}>>('auth/login');
|
const promise =
|
||||||
return promise;
|
instance.delete<ResponseType<{ userId?: number }>>('auth/login')
|
||||||
|
return promise
|
||||||
},
|
},
|
||||||
me() {
|
me() {
|
||||||
const promise = instance.get<ResponseType<{id: number; email: string; login: string}>>('auth/me');
|
const promise =
|
||||||
|
instance.get<ResponseType<{ id: number; email: string; login: string }>>(
|
||||||
|
'auth/me'
|
||||||
|
)
|
||||||
return promise
|
return promise
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// types
|
// types
|
||||||
@@ -78,19 +97,22 @@ export type ResponseType<D = {}> = {
|
|||||||
messages: Array<string>
|
messages: Array<string>
|
||||||
data: D
|
data: D
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TaskStatuses {
|
export enum TaskStatuses {
|
||||||
New = 0,
|
New = 0,
|
||||||
InProgress = 1,
|
InProgress = 1,
|
||||||
Completed = 2,
|
Completed = 2,
|
||||||
Draft = 3
|
Draft = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TaskPriorities {
|
export enum TaskPriorities {
|
||||||
Low = 0,
|
Low = 0,
|
||||||
Middle = 1,
|
Middle = 1,
|
||||||
Hi = 2,
|
Hi = 2,
|
||||||
Urgently = 3,
|
Urgently = 3,
|
||||||
Later = 4
|
Later = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TaskType = {
|
export type TaskType = {
|
||||||
description: string
|
description: string
|
||||||
title: string
|
title: string
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
LinearProgress,
|
LinearProgress,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
Typography
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material'
|
||||||
import { Menu } from '@mui/icons-material'
|
import { Menu } from '@mui/icons-material'
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
@@ -25,9 +25,15 @@ type PropsType = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function App({ demo = false }: PropsType) {
|
function App({ demo = false }: PropsType) {
|
||||||
const status = useSelector<AppRootStateType, RequestStatusType>((state) => state.app.status)
|
const status = useSelector<AppRootStateType, RequestStatusType>(
|
||||||
const isInitialized = useSelector<AppRootStateType, boolean>((state) => state.app.isInitialized)
|
(state) => state.app.status
|
||||||
const isLoggedIn = useSelector<AppRootStateType, boolean>(state => state.auth.isLoggedIn)
|
)
|
||||||
|
const isInitialized = useSelector<AppRootStateType, boolean>(
|
||||||
|
(state) => state.app.isInitialized
|
||||||
|
)
|
||||||
|
const isLoggedIn = useSelector<AppRootStateType, boolean>(
|
||||||
|
(state) => state.auth.isLoggedIn
|
||||||
|
)
|
||||||
const dispatch = useDispatch<any>()
|
const dispatch = useDispatch<any>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -39,32 +45,55 @@ function App({demo = false}: PropsType) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
return <div
|
return (
|
||||||
style={{position: 'fixed', top: '30%', textAlign: 'center', width: '100%'}}>
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: '30%',
|
||||||
|
textAlign: 'center',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<div className="App">
|
<div className='App'>
|
||||||
<ErrorSnackbar />
|
<ErrorSnackbar />
|
||||||
<AppBar position="static">
|
<AppBar position='static'>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton edge="start" color="inherit" aria-label="menu">
|
<IconButton
|
||||||
|
edge='start'
|
||||||
|
color='inherit'
|
||||||
|
aria-label='menu'
|
||||||
|
>
|
||||||
<Menu />
|
<Menu />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant="h6">
|
<Typography variant='h6'>News</Typography>
|
||||||
News
|
{isLoggedIn && (
|
||||||
</Typography>
|
<Button
|
||||||
{isLoggedIn && <Button color="inherit" onClick={logoutHandler}>Log out</Button>}
|
color='inherit'
|
||||||
|
onClick={logoutHandler}
|
||||||
|
>
|
||||||
|
Log out
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
{status === 'loading' && <LinearProgress />}
|
{status === 'loading' && <LinearProgress />}
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<Container fixed>
|
<Container fixed>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={'/'} element={<TodolistsList demo={demo}/>}/>
|
<Route
|
||||||
<Route path={'/login'} element={<Login/>}/>
|
path={'/'}
|
||||||
|
element={<TodolistsList demo={demo} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={'/login'}
|
||||||
|
element={<Login />}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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(() => {
|
beforeEach(() => {
|
||||||
startState = {
|
startState = {
|
||||||
error: null,
|
error: null,
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
isInitialized: false
|
isInitialized: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test('correct error message should be set', () => {
|
test('correct error message should be set', () => {
|
||||||
const endState = appReducer(startState, setAppErrorAC('some error'))
|
const endState = appReducer(startState, setAppErrorAC('some error'))
|
||||||
expect(endState.error).toBe('some error');
|
expect(endState.error).toBe('some error')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('correct status should be set', () => {
|
test('correct status should be set', () => {
|
||||||
const endState = appReducer(startState, setAppStatusAC('loading'))
|
const endState = appReducer(startState, setAppStatusAC('loading'))
|
||||||
expect(endState.status).toBe('loading');
|
expect(endState.status).toBe('loading')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import {setIsLoggedInAC} from '../features/Login/auth-reducer'
|
|||||||
const initialState: InitialStateType = {
|
const initialState: InitialStateType = {
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
error: null,
|
error: null,
|
||||||
isInitialized: false
|
isInitialized: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appReducer = (state: InitialStateType = initialState, action: ActionsType): InitialStateType => {
|
export const appReducer = (
|
||||||
|
state: InitialStateType = initialState,
|
||||||
|
action: ActionsType
|
||||||
|
): InitialStateType => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'APP/SET-STATUS':
|
case 'APP/SET-STATUS':
|
||||||
return { ...state, status: action.status }
|
return { ...state, status: action.status }
|
||||||
@@ -31,26 +34,27 @@ export type InitialStateType = {
|
|||||||
isInitialized: boolean
|
isInitialized: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setAppErrorAC = (error: string | null) => ({type: 'APP/SET-ERROR', error} as const)
|
export const setAppErrorAC = (error: string | null) =>
|
||||||
export const setAppStatusAC = (status: RequestStatusType) => ({type: 'APP/SET-STATUS', status} as const)
|
({ type: 'APP/SET-ERROR', error }) as const
|
||||||
export const setAppInitializedAC = (value: boolean) => ({type: 'APP/SET-IS-INITIALIED', value} 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) => {
|
export const initializeAppTC = () => (dispatch: Dispatch) => {
|
||||||
authAPI.me().then(res => {
|
authAPI.me().then((res) => {
|
||||||
if (res.data.resultCode === 0) {
|
if (res.data.resultCode === 0) {
|
||||||
dispatch(setIsLoggedInAC(true));
|
dispatch(setIsLoggedInAC(true))
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(setAppInitializedAC(true));
|
dispatch(setAppInitializedAC(true))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SetAppErrorActionType = ReturnType<typeof setAppErrorAC>
|
export type SetAppErrorActionType = ReturnType<typeof setAppErrorAC>
|
||||||
export type SetAppStatusActionType = ReturnType<typeof setAppStatusAC>
|
export type SetAppStatusActionType = ReturnType<typeof setAppStatusAC>
|
||||||
|
|
||||||
|
|
||||||
type ActionsType =
|
type ActionsType =
|
||||||
| SetAppErrorActionType
|
| SetAppErrorActionType
|
||||||
| SetAppStatusActionType
|
| SetAppStatusActionType
|
||||||
|
|||||||
@@ -1,27 +1,36 @@
|
|||||||
import {tasksReducer} from '../features/TodolistsList/tasks-reducer';
|
import { tasksReducer } from '../features/TodolistsList/tasks-reducer'
|
||||||
import {todolistsReducer} from '../features/TodolistsList/todolists-reducer';
|
import { todolistsReducer } from '../features/TodolistsList/todolists-reducer'
|
||||||
import { applyMiddleware, combineReducers, createStore } from 'redux'
|
import { applyMiddleware, combineReducers, createStore } from 'redux'
|
||||||
import thunkMiddleware, { ThunkAction, ThunkDispatch } from 'redux-thunk'
|
import thunkMiddleware, { ThunkAction, ThunkDispatch } from 'redux-thunk'
|
||||||
import { appReducer } from './app-reducer'
|
import { appReducer } from './app-reducer'
|
||||||
import { authReducer } from '../features/Login/auth-reducer'
|
import { authReducer } from '../features/Login/auth-reducer'
|
||||||
import {configureStore, UnknownAction} from "@reduxjs/toolkit";
|
import { configureStore, UnknownAction } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
tasks: tasksReducer,
|
tasks: tasksReducer,
|
||||||
todolists: todolistsReducer,
|
todolists: todolistsReducer,
|
||||||
app: appReducer,
|
app: appReducer,
|
||||||
auth: authReducer
|
auth: authReducer,
|
||||||
})
|
})
|
||||||
|
|
||||||
// ❗старая запись, с новыми версиями не работает
|
// ❗старая запись, с новыми версиями не работает
|
||||||
// const store = createStore(rootReducer, applyMiddleware(thunkMiddleware));
|
// const store = createStore(rootReducer, applyMiddleware(thunkMiddleware));
|
||||||
export const store = configureStore({reducer: rootReducer},)
|
export const store = configureStore({ reducer: rootReducer })
|
||||||
|
|
||||||
export type AppRootStateType = ReturnType<typeof rootReducer>
|
export type AppRootStateType = ReturnType<typeof rootReducer>
|
||||||
|
|
||||||
// ❗ UnknownAction вместо AnyAction
|
// ❗ 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
|
// export type AppDispatch = typeof store.dispatch
|
||||||
// ❗ UnknownAction вместо AnyAction
|
// ❗ UnknownAction вместо AnyAction
|
||||||
export type AppDispatch = ThunkDispatch<AppRootStateType, unknown, UnknownAction>
|
export type AppDispatch = ThunkDispatch<
|
||||||
|
AppRootStateType,
|
||||||
|
unknown,
|
||||||
|
UnknownAction
|
||||||
|
>
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import React, { ChangeEvent, KeyboardEvent, useState } from 'react';
|
import React, { ChangeEvent, KeyboardEvent, useState } from 'react'
|
||||||
import { IconButton, TextField } from '@mui/material';
|
import { IconButton, TextField } from '@mui/material'
|
||||||
import { AddBox } from '@mui/icons-material';
|
import { AddBox } from '@mui/icons-material'
|
||||||
|
|
||||||
type AddItemFormPropsType = {
|
type AddItemFormPropsType = {
|
||||||
addItem: (title: string) => void
|
addItem: (title: string) => void
|
||||||
disabled?: boolean
|
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 [title, setTitle] = useState('')
|
||||||
let [error, setError] = useState<string | null>(null)
|
let [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
const addItemHandler = () => {
|
const addItemHandler = () => {
|
||||||
if (title.trim() !== '') {
|
if (title.trim() !== '') {
|
||||||
addItem(title);
|
addItem(title)
|
||||||
setTitle('');
|
setTitle('')
|
||||||
} else {
|
} else {
|
||||||
setError('Title is required');
|
setError('Title is required')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,25 +29,32 @@ export const AddItemForm = React.memo(function ({addItem, disabled = false}: Add
|
|||||||
|
|
||||||
const onKeyPressHandler = (e: KeyboardEvent<HTMLInputElement>) => {
|
const onKeyPressHandler = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (error !== null) {
|
if (error !== null) {
|
||||||
setError(null);
|
setError(null)
|
||||||
}
|
}
|
||||||
if (e.charCode === 13) {
|
if (e.charCode === 13) {
|
||||||
addItemHandler();
|
addItemHandler()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return (
|
||||||
<TextField variant="outlined"
|
<div>
|
||||||
|
<TextField
|
||||||
|
variant='outlined'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
error={!!error}
|
error={!!error}
|
||||||
value={title}
|
value={title}
|
||||||
onChange={onChangeHandler}
|
onChange={onChangeHandler}
|
||||||
onKeyPress={onKeyPressHandler}
|
onKeyPress={onKeyPressHandler}
|
||||||
label="Title"
|
label='Title'
|
||||||
helperText={error}
|
helperText={error}
|
||||||
/>
|
/>
|
||||||
<IconButton color="primary" onClick={addItemHandler} disabled={disabled}>
|
<IconButton
|
||||||
|
color='primary'
|
||||||
|
onClick={addItemHandler}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
<AddBox />
|
<AddBox />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, {ChangeEvent, useState} from 'react';
|
import React, { ChangeEvent, useState } from 'react'
|
||||||
import { TextField } from '@mui/material';
|
import { TextField } from '@mui/material'
|
||||||
|
|
||||||
type EditableSpanPropsType = {
|
type EditableSpanPropsType = {
|
||||||
value: string
|
value: string
|
||||||
@@ -7,22 +7,29 @@ type EditableSpanPropsType = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const EditableSpan = React.memo(function (props: EditableSpanPropsType) {
|
export const EditableSpan = React.memo(function (props: EditableSpanPropsType) {
|
||||||
let [editMode, setEditMode] = useState(false);
|
let [editMode, setEditMode] = useState(false)
|
||||||
let [title, setTitle] = useState(props.value);
|
let [title, setTitle] = useState(props.value)
|
||||||
|
|
||||||
const activateEditMode = () => {
|
const activateEditMode = () => {
|
||||||
setEditMode(true);
|
setEditMode(true)
|
||||||
setTitle(props.value);
|
setTitle(props.value)
|
||||||
}
|
}
|
||||||
const activateViewMode = () => {
|
const activateViewMode = () => {
|
||||||
setEditMode(false);
|
setEditMode(false)
|
||||||
props.onChange(title);
|
props.onChange(title)
|
||||||
}
|
}
|
||||||
const changeTitle = (e: ChangeEvent<HTMLInputElement>) => {
|
const changeTitle = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setTitle(e.currentTarget.value)
|
setTitle(e.currentTarget.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return editMode
|
return editMode ? (
|
||||||
? <TextField value={title} onChange={changeTitle} autoFocus onBlur={activateViewMode} />
|
<TextField
|
||||||
: <span onDoubleClick={activateEditMode}>{props.value}</span>
|
value={title}
|
||||||
});
|
onChange={changeTitle}
|
||||||
|
autoFocus
|
||||||
|
onBlur={activateViewMode}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span onDoubleClick={activateEditMode}>{props.value}</span>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
@@ -3,33 +3,49 @@ import {useDispatch, useSelector} from 'react-redux'
|
|||||||
import { AppRootStateType } from '../../app/store'
|
import { AppRootStateType } from '../../app/store'
|
||||||
import { setAppErrorAC } from '../../app/app-reducer'
|
import { setAppErrorAC } from '../../app/app-reducer'
|
||||||
import { AlertProps, Snackbar } from '@mui/material'
|
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(
|
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
||||||
props,
|
function Alert(props, ref) {
|
||||||
ref,
|
return (
|
||||||
) {
|
<MuiAlert
|
||||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
|
elevation={6}
|
||||||
});
|
ref={ref}
|
||||||
|
variant='filled'
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export function ErrorSnackbar() {
|
export function ErrorSnackbar() {
|
||||||
|
const error = useSelector<AppRootStateType, string | null>(
|
||||||
const error = useSelector<AppRootStateType, string | null>(state => state.app.error);
|
(state) => state.app.error
|
||||||
|
)
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
const handleClose = (event?: React.SyntheticEvent | Event, reason?: string) => {
|
const handleClose = (
|
||||||
|
event?: React.SyntheticEvent | Event,
|
||||||
|
reason?: string
|
||||||
|
) => {
|
||||||
if (reason === 'clickaway') {
|
if (reason === 'clickaway') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dispatch(setAppErrorAC(null));
|
dispatch(setAppErrorAC(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isOpen = error !== null
|
||||||
const isOpen = error !== null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Snackbar open={isOpen} autoHideDuration={6000} onClose={handleClose}>
|
<Snackbar
|
||||||
<Alert onClose={handleClose} severity="error">
|
open={isOpen}
|
||||||
|
autoHideDuration={6000}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
onClose={handleClose}
|
||||||
|
severity='error'
|
||||||
|
>
|
||||||
{error}
|
{error}
|
||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
|
|||||||
@@ -4,86 +4,113 @@ import { useSelector } from 'react-redux'
|
|||||||
import { loginTC } from './auth-reducer'
|
import { loginTC } from './auth-reducer'
|
||||||
import { AppRootStateType } from '../../app/store'
|
import { AppRootStateType } from '../../app/store'
|
||||||
import { Navigate } from 'react-router-dom'
|
import { Navigate } from 'react-router-dom'
|
||||||
import { useAppDispatch } from '../../hooks/useAppDispatch';
|
import { useAppDispatch } from '../../hooks/useAppDispatch'
|
||||||
import { Button, Checkbox, FormControl, FormControlLabel, FormGroup, FormLabel, Grid, TextField } from '@mui/material'
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
FormGroup,
|
||||||
|
FormLabel,
|
||||||
|
Grid,
|
||||||
|
TextField,
|
||||||
|
} from '@mui/material'
|
||||||
|
|
||||||
export const Login = () => {
|
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({
|
const formik = useFormik({
|
||||||
validate: (values) => {
|
validate: (values) => {
|
||||||
if (!values.email) {
|
if (!values.email) {
|
||||||
return {
|
return {
|
||||||
email: 'Email is required'
|
email: 'Email is required',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!values.password) {
|
if (!values.password) {
|
||||||
return {
|
return {
|
||||||
password: 'Password is required'
|
password: 'Password is required',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
initialValues: {
|
initialValues: {
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
rememberMe: false
|
rememberMe: false,
|
||||||
},
|
},
|
||||||
onSubmit: values => {
|
onSubmit: (values) => {
|
||||||
dispatch(loginTC(values));
|
dispatch(loginTC(values))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
return <Navigate to={"/"} />
|
return <Navigate to={'/'} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
return <Grid container justifyContent="center">
|
<Grid
|
||||||
<Grid item xs={4}>
|
container
|
||||||
|
justifyContent='center'
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={4}
|
||||||
|
>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<p>
|
<p>
|
||||||
To log in get registered <a href={'https://social-network.samuraijs.com/'}
|
To log in get registered{' '}
|
||||||
target={'_blank'}>here</a>
|
<a
|
||||||
</p>
|
href={'https://social-network.samuraijs.com/'}
|
||||||
<p>
|
target={'_blank'}
|
||||||
or use common test account credentials:
|
>
|
||||||
</p>
|
here
|
||||||
<p> Email: free@samuraijs.com
|
</a>
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Password: free
|
|
||||||
</p>
|
</p>
|
||||||
|
<p>or use common test account credentials:</p>
|
||||||
|
<p> Email: free@samuraijs.com</p>
|
||||||
|
<p>Password: free</p>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<TextField
|
<TextField
|
||||||
label="Email"
|
label='Email'
|
||||||
margin="normal"
|
margin='normal'
|
||||||
{...formik.getFieldProps("email")}
|
{...formik.getFieldProps('email')}
|
||||||
/>
|
/>
|
||||||
{formik.errors.email ? <div>{formik.errors.email}</div> : null}
|
{formik.errors.email ? <div>{formik.errors.email}</div> : null}
|
||||||
<TextField
|
<TextField
|
||||||
type="password"
|
type='password'
|
||||||
label="Password"
|
label='Password'
|
||||||
margin="normal"
|
margin='normal'
|
||||||
{...formik.getFieldProps("password")}
|
{...formik.getFieldProps('password')}
|
||||||
/>
|
/>
|
||||||
{formik.errors.password ? <div>{formik.errors.password}</div> : null}
|
{formik.errors.password ? (
|
||||||
|
<div>{formik.errors.password}</div>
|
||||||
|
) : null}
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
label={'Remember me'}
|
label={'Remember me'}
|
||||||
control={<Checkbox
|
control={
|
||||||
{...formik.getFieldProps("rememberMe")}
|
<Checkbox
|
||||||
|
{...formik.getFieldProps('rememberMe')}
|
||||||
checked={formik.values.rememberMe}
|
checked={formik.values.rememberMe}
|
||||||
/>}
|
|
||||||
/>
|
/>
|
||||||
<Button type={'submit'} variant={'contained'} color={'primary'}>Login</Button>
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type={'submit'}
|
||||||
|
variant={'contained'}
|
||||||
|
color={'primary'}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</form>
|
</form>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import { Dispatch } from 'redux'
|
import { Dispatch } from 'redux'
|
||||||
import {SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../../app/app-reducer'
|
import {
|
||||||
|
SetAppErrorActionType,
|
||||||
|
setAppStatusAC,
|
||||||
|
SetAppStatusActionType,
|
||||||
|
} from '../../app/app-reducer'
|
||||||
import { authAPI, LoginParamsType } from '../../api/todolists-api'
|
import { authAPI, LoginParamsType } from '../../api/todolists-api'
|
||||||
import {handleServerAppError, handleServerNetworkError} from '../../utils/error-utils'
|
import {
|
||||||
|
handleServerAppError,
|
||||||
|
handleServerNetworkError,
|
||||||
|
} from '../../utils/error-utils'
|
||||||
|
|
||||||
const initialState: InitialStateType = {
|
const initialState: InitialStateType = {
|
||||||
isLoggedIn: false
|
isLoggedIn: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authReducer = (state: InitialStateType = initialState, action: ActionsType): InitialStateType => {
|
export const authReducer = (
|
||||||
|
state: InitialStateType = initialState,
|
||||||
|
action: ActionsType
|
||||||
|
): InitialStateType => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'login/SET-IS-LOGGED-IN':
|
case 'login/SET-IS-LOGGED-IN':
|
||||||
return { ...state, isLoggedIn: action.value }
|
return { ...state, isLoggedIn: action.value }
|
||||||
@@ -19,14 +29,20 @@ export const authReducer = (state: InitialStateType = initialState, action: Acti
|
|||||||
// actions
|
// actions
|
||||||
|
|
||||||
export const setIsLoggedInAC = (value: boolean) =>
|
export const setIsLoggedInAC = (value: boolean) =>
|
||||||
({type: 'login/SET-IS-LOGGED-IN', value} as const)
|
({ type: 'login/SET-IS-LOGGED-IN', value }) as const
|
||||||
|
|
||||||
|
|
||||||
// thunks
|
// thunks
|
||||||
export const loginTC = (data: LoginParamsType) => (dispatch: Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>) => {
|
export const loginTC =
|
||||||
|
(data: LoginParamsType) =>
|
||||||
|
(
|
||||||
|
dispatch: Dispatch<
|
||||||
|
ActionsType | SetAppStatusActionType | SetAppErrorActionType
|
||||||
|
>
|
||||||
|
) => {
|
||||||
dispatch(setAppStatusAC('loading'))
|
dispatch(setAppStatusAC('loading'))
|
||||||
authAPI.login(data)
|
authAPI
|
||||||
.then(res => {
|
.login(data)
|
||||||
|
.then((res) => {
|
||||||
if (res.data.resultCode === 0) {
|
if (res.data.resultCode === 0) {
|
||||||
dispatch(setIsLoggedInAC(true))
|
dispatch(setIsLoggedInAC(true))
|
||||||
dispatch(setAppStatusAC('succeeded'))
|
dispatch(setAppStatusAC('succeeded'))
|
||||||
@@ -38,10 +54,17 @@ export const loginTC = (data: LoginParamsType) => (dispatch: Dispatch<ActionsTyp
|
|||||||
handleServerNetworkError(error, dispatch)
|
handleServerNetworkError(error, dispatch)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
export const logoutTC = () => (dispatch: Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>) => {
|
export const logoutTC =
|
||||||
|
() =>
|
||||||
|
(
|
||||||
|
dispatch: Dispatch<
|
||||||
|
ActionsType | SetAppStatusActionType | SetAppErrorActionType
|
||||||
|
>
|
||||||
|
) => {
|
||||||
dispatch(setAppStatusAC('loading'))
|
dispatch(setAppStatusAC('loading'))
|
||||||
authAPI.logout()
|
authAPI
|
||||||
.then(res => {
|
.logout()
|
||||||
|
.then((res) => {
|
||||||
if (res.data.resultCode === 0) {
|
if (res.data.resultCode === 0) {
|
||||||
dispatch(setIsLoggedInAC(false))
|
dispatch(setIsLoggedInAC(false))
|
||||||
dispatch(setAppStatusAC('succeeded'))
|
dispatch(setAppStatusAC('succeeded'))
|
||||||
@@ -61,4 +84,6 @@ type InitialStateType = {
|
|||||||
isLoggedIn: boolean
|
isLoggedIn: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThunkDispatch = Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>
|
type ThunkDispatch = Dispatch<
|
||||||
|
ActionsType | SetAppStatusActionType | SetAppErrorActionType
|
||||||
|
>
|
||||||
|
|||||||
@@ -7,32 +7,61 @@ import { TaskStatuses, TaskType } from '../../../../api/todolists-api'
|
|||||||
type TaskPropsType = {
|
type TaskPropsType = {
|
||||||
task: TaskType
|
task: TaskType
|
||||||
todolistId: string
|
todolistId: string
|
||||||
changeTaskStatus: (id: string, status: TaskStatuses, todolistId: string) => void
|
changeTaskStatus: (
|
||||||
changeTaskTitle: (taskId: string, newTitle: string, todolistId: string) => void
|
id: string,
|
||||||
|
status: TaskStatuses,
|
||||||
|
todolistId: string
|
||||||
|
) => void
|
||||||
|
changeTaskTitle: (
|
||||||
|
taskId: string,
|
||||||
|
newTitle: string,
|
||||||
|
todolistId: string
|
||||||
|
) => void
|
||||||
removeTask: (taskId: string, todolistId: string) => void
|
removeTask: (taskId: string, todolistId: string) => void
|
||||||
}
|
}
|
||||||
export const Task = React.memo((props: TaskPropsType) => {
|
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>) => {
|
const onChangeHandler = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
let newIsDoneValue = e.currentTarget.checked
|
let newIsDoneValue = e.currentTarget.checked
|
||||||
props.changeTaskStatus(props.task.id, newIsDoneValue ? TaskStatuses.Completed : TaskStatuses.New, props.todolistId)
|
props.changeTaskStatus(
|
||||||
}, [props.task.id, props.todolistId]);
|
props.task.id,
|
||||||
|
newIsDoneValue ? TaskStatuses.Completed : TaskStatuses.New,
|
||||||
|
props.todolistId
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[props.task.id, props.todolistId]
|
||||||
|
)
|
||||||
|
|
||||||
const onTitleChangeHandler = useCallback((newValue: string) => {
|
const onTitleChangeHandler = useCallback(
|
||||||
|
(newValue: string) => {
|
||||||
props.changeTaskTitle(props.task.id, newValue, props.todolistId)
|
props.changeTaskTitle(props.task.id, newValue, props.todolistId)
|
||||||
}, [props.task.id, props.todolistId]);
|
},
|
||||||
|
[props.task.id, props.todolistId]
|
||||||
|
)
|
||||||
|
|
||||||
return <div key={props.task.id} className={props.task.status === TaskStatuses.Completed ? 'is-done' : ''}>
|
return (
|
||||||
|
<div
|
||||||
|
key={props.task.id}
|
||||||
|
className={props.task.status === TaskStatuses.Completed ? 'is-done' : ''}
|
||||||
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={props.task.status === TaskStatuses.Completed}
|
checked={props.task.status === TaskStatuses.Completed}
|
||||||
color="primary"
|
color='primary'
|
||||||
onChange={onChangeHandler}
|
onChange={onChangeHandler}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EditableSpan value={props.task.title} onChange={onTitleChangeHandler}/>
|
<EditableSpan
|
||||||
|
value={props.task.title}
|
||||||
|
onChange={onTitleChangeHandler}
|
||||||
|
/>
|
||||||
<IconButton onClick={onClickHandler}>
|
<IconButton onClick={onClickHandler}>
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Task } from './Task/Task'
|
|||||||
import { TaskStatuses, TaskType } from '../../../api/todolists-api'
|
import { TaskStatuses, TaskType } from '../../../api/todolists-api'
|
||||||
import { FilterValuesType, TodolistDomainType } from '../todolists-reducer'
|
import { FilterValuesType, TodolistDomainType } from '../todolists-reducer'
|
||||||
import { fetchTasksTC } from '../tasks-reducer'
|
import { fetchTasksTC } from '../tasks-reducer'
|
||||||
import { useAppDispatch } from '../../../hooks/useAppDispatch';
|
import { useAppDispatch } from '../../../hooks/useAppDispatch'
|
||||||
import { Button, IconButton } from '@mui/material'
|
import { Button, IconButton } from '@mui/material'
|
||||||
import { Delete } from '@mui/icons-material'
|
import { Delete } from '@mui/icons-material'
|
||||||
|
|
||||||
@@ -14,16 +14,26 @@ type PropsType = {
|
|||||||
tasks: Array<TaskType>
|
tasks: Array<TaskType>
|
||||||
changeFilter: (value: FilterValuesType, todolistId: string) => void
|
changeFilter: (value: FilterValuesType, todolistId: string) => void
|
||||||
addTask: (title: string, todolistId: string) => void
|
addTask: (title: string, todolistId: string) => void
|
||||||
changeTaskStatus: (id: string, status: TaskStatuses, todolistId: string) => void
|
changeTaskStatus: (
|
||||||
changeTaskTitle: (taskId: string, newTitle: string, todolistId: string) => void
|
id: string,
|
||||||
|
status: TaskStatuses,
|
||||||
|
todolistId: string
|
||||||
|
) => void
|
||||||
|
changeTaskTitle: (
|
||||||
|
taskId: string,
|
||||||
|
newTitle: string,
|
||||||
|
todolistId: string
|
||||||
|
) => void
|
||||||
removeTask: (taskId: string, todolistId: string) => void
|
removeTask: (taskId: string, todolistId: string) => void
|
||||||
removeTodolist: (id: string) => void
|
removeTodolist: (id: string) => void
|
||||||
changeTodolistTitle: (id: string, newTitle: string) => void
|
changeTodolistTitle: (id: string, newTitle: string) => void
|
||||||
demo?: boolean
|
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(() => {
|
useEffect(() => {
|
||||||
@@ -34,63 +44,100 @@ export const Todolist = React.memo(function ({demo = false, ...props}: PropsType
|
|||||||
dispatch(thunk)
|
dispatch(thunk)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const addTask = useCallback((title: string) => {
|
const addTask = useCallback(
|
||||||
|
(title: string) => {
|
||||||
props.addTask(title, props.todolist.id)
|
props.addTask(title, props.todolist.id)
|
||||||
}, [props.addTask, props.todolist.id])
|
},
|
||||||
|
[props.addTask, props.todolist.id]
|
||||||
|
)
|
||||||
|
|
||||||
const removeTodolist = () => {
|
const removeTodolist = () => {
|
||||||
props.removeTodolist(props.todolist.id)
|
props.removeTodolist(props.todolist.id)
|
||||||
}
|
}
|
||||||
const changeTodolistTitle = useCallback((title: string) => {
|
const changeTodolistTitle = useCallback(
|
||||||
|
(title: string) => {
|
||||||
props.changeTodolistTitle(props.todolist.id, title)
|
props.changeTodolistTitle(props.todolist.id, title)
|
||||||
}, [props.todolist.id, props.changeTodolistTitle])
|
},
|
||||||
|
[props.todolist.id, props.changeTodolistTitle]
|
||||||
const onAllClickHandler = useCallback(() => props.changeFilter('all', props.todolist.id), [props.todolist.id, props.changeFilter])
|
)
|
||||||
const onActiveClickHandler = useCallback(() => props.changeFilter('active', props.todolist.id), [props.todolist.id, props.changeFilter])
|
|
||||||
const onCompletedClickHandler = useCallback(() => props.changeFilter('completed', props.todolist.id), [props.todolist.id, props.changeFilter])
|
|
||||||
|
|
||||||
|
const onAllClickHandler = useCallback(
|
||||||
|
() => props.changeFilter('all', props.todolist.id),
|
||||||
|
[props.todolist.id, props.changeFilter]
|
||||||
|
)
|
||||||
|
const onActiveClickHandler = useCallback(
|
||||||
|
() => props.changeFilter('active', props.todolist.id),
|
||||||
|
[props.todolist.id, props.changeFilter]
|
||||||
|
)
|
||||||
|
const onCompletedClickHandler = useCallback(
|
||||||
|
() => props.changeFilter('completed', props.todolist.id),
|
||||||
|
[props.todolist.id, props.changeFilter]
|
||||||
|
)
|
||||||
|
|
||||||
let tasksForTodolist = props.tasks
|
let tasksForTodolist = props.tasks
|
||||||
|
|
||||||
if (props.todolist.filter === 'active') {
|
if (props.todolist.filter === 'active') {
|
||||||
tasksForTodolist = props.tasks.filter(t => t.status === TaskStatuses.New)
|
tasksForTodolist = props.tasks.filter((t) => t.status === TaskStatuses.New)
|
||||||
}
|
}
|
||||||
if (props.todolist.filter === 'completed') {
|
if (props.todolist.filter === 'completed') {
|
||||||
tasksForTodolist = props.tasks.filter(t => t.status === TaskStatuses.Completed)
|
tasksForTodolist = props.tasks.filter(
|
||||||
|
(t) => t.status === TaskStatuses.Completed
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return (
|
||||||
<h3><EditableSpan value={props.todolist.title} onChange={changeTodolistTitle}/>
|
<div>
|
||||||
<IconButton onClick={removeTodolist} disabled={props.todolist.entityStatus === 'loading'}>
|
<h3>
|
||||||
|
<EditableSpan
|
||||||
|
value={props.todolist.title}
|
||||||
|
onChange={changeTodolistTitle}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={removeTodolist}
|
||||||
|
disabled={props.todolist.entityStatus === 'loading'}
|
||||||
|
>
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</h3>
|
</h3>
|
||||||
<AddItemForm addItem={addTask} disabled={props.todolist.entityStatus === 'loading'}/>
|
<AddItemForm
|
||||||
|
addItem={addTask}
|
||||||
|
disabled={props.todolist.entityStatus === 'loading'}
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
{
|
{tasksForTodolist.map((t) => (
|
||||||
tasksForTodolist.map(t => <Task key={t.id} task={t} todolistId={props.todolist.id}
|
<Task
|
||||||
|
key={t.id}
|
||||||
|
task={t}
|
||||||
|
todolistId={props.todolist.id}
|
||||||
removeTask={props.removeTask}
|
removeTask={props.removeTask}
|
||||||
changeTaskTitle={props.changeTaskTitle}
|
changeTaskTitle={props.changeTaskTitle}
|
||||||
changeTaskStatus={props.changeTaskStatus}
|
changeTaskStatus={props.changeTaskStatus}
|
||||||
/>)
|
/>
|
||||||
}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ paddingTop: '10px' }}>
|
<div style={{ paddingTop: '10px' }}>
|
||||||
<Button variant={props.todolist.filter === 'all' ? 'outlined' : 'text'}
|
<Button
|
||||||
|
variant={props.todolist.filter === 'all' ? 'outlined' : 'text'}
|
||||||
onClick={onAllClickHandler}
|
onClick={onAllClickHandler}
|
||||||
color={'inherit'}
|
color={'inherit'}
|
||||||
>All
|
>
|
||||||
|
All
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant={props.todolist.filter === 'active' ? 'outlined' : 'text'}
|
<Button
|
||||||
|
variant={props.todolist.filter === 'active' ? 'outlined' : 'text'}
|
||||||
onClick={onActiveClickHandler}
|
onClick={onActiveClickHandler}
|
||||||
color={'primary'}>Active
|
color={'primary'}
|
||||||
|
>
|
||||||
|
Active
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant={props.todolist.filter === 'completed' ? 'outlined' : 'text'}
|
<Button
|
||||||
|
variant={props.todolist.filter === 'completed' ? 'outlined' : 'text'}
|
||||||
onClick={onCompletedClickHandler}
|
onClick={onCompletedClickHandler}
|
||||||
color={'secondary'}>Completed
|
color={'secondary'}
|
||||||
|
>
|
||||||
|
Completed
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,30 +8,41 @@ import {
|
|||||||
fetchTodolistsTC,
|
fetchTodolistsTC,
|
||||||
FilterValuesType,
|
FilterValuesType,
|
||||||
removeTodolistTC,
|
removeTodolistTC,
|
||||||
TodolistDomainType
|
TodolistDomainType,
|
||||||
} from './todolists-reducer'
|
} 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 { TaskStatuses } from '../../api/todolists-api'
|
||||||
import { Grid, Paper } from '@mui/material'
|
import { Grid, Paper } from '@mui/material'
|
||||||
import { AddItemForm } from '../../components/AddItemForm/AddItemForm'
|
import { AddItemForm } from '../../components/AddItemForm/AddItemForm'
|
||||||
import { Todolist } from './Todolist/Todolist'
|
import { Todolist } from './Todolist/Todolist'
|
||||||
import { Navigate } from 'react-router-dom'
|
import { Navigate } from 'react-router-dom'
|
||||||
import { useAppDispatch } from '../../hooks/useAppDispatch';
|
import { useAppDispatch } from '../../hooks/useAppDispatch'
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
demo?: boolean
|
demo?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TodolistsList: React.FC<PropsType> = ({ demo = false }) => {
|
export const TodolistsList: React.FC<PropsType> = ({ demo = false }) => {
|
||||||
const todolists = useSelector<AppRootStateType, Array<TodolistDomainType>>(state => state.todolists)
|
const todolists = useSelector<AppRootStateType, Array<TodolistDomainType>>(
|
||||||
const tasks = useSelector<AppRootStateType, TasksStateType>(state => state.tasks)
|
(state) => state.todolists
|
||||||
const isLoggedIn = useSelector<AppRootStateType, boolean>(state => state.auth.isLoggedIn)
|
)
|
||||||
|
const tasks = useSelector<AppRootStateType, TasksStateType>(
|
||||||
|
(state) => state.tasks
|
||||||
|
)
|
||||||
|
const isLoggedIn = useSelector<AppRootStateType, boolean>(
|
||||||
|
(state) => state.auth.isLoggedIn
|
||||||
|
)
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (demo || !isLoggedIn) {
|
if (demo || !isLoggedIn) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const thunk = fetchTodolistsTC()
|
const thunk = fetchTodolistsTC()
|
||||||
dispatch(thunk)
|
dispatch(thunk)
|
||||||
@@ -47,17 +58,28 @@ export const TodolistsList: React.FC<PropsType> = ({demo = false}) => {
|
|||||||
dispatch(thunk)
|
dispatch(thunk)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const changeStatus = useCallback(function (id: string, status: TaskStatuses, todolistId: string) {
|
const changeStatus = useCallback(function (
|
||||||
|
id: string,
|
||||||
|
status: TaskStatuses,
|
||||||
|
todolistId: string
|
||||||
|
) {
|
||||||
const thunk = updateTaskTC(id, { status }, todolistId)
|
const thunk = updateTaskTC(id, { status }, todolistId)
|
||||||
dispatch(thunk)
|
dispatch(thunk)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const changeTaskTitle = useCallback(function (id: string, newTitle: string, todolistId: string) {
|
const changeTaskTitle = useCallback(function (
|
||||||
|
id: string,
|
||||||
|
newTitle: string,
|
||||||
|
todolistId: string
|
||||||
|
) {
|
||||||
const thunk = updateTaskTC(id, { title: newTitle }, todolistId)
|
const thunk = updateTaskTC(id, { title: newTitle }, todolistId)
|
||||||
dispatch(thunk)
|
dispatch(thunk)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const changeFilter = useCallback(function (value: FilterValuesType, todolistId: string) {
|
const changeFilter = useCallback(function (
|
||||||
|
value: FilterValuesType,
|
||||||
|
todolistId: string
|
||||||
|
) {
|
||||||
const action = changeTodolistFilterAC(todolistId, value)
|
const action = changeTodolistFilterAC(todolistId, value)
|
||||||
dispatch(action)
|
dispatch(action)
|
||||||
}, [])
|
}, [])
|
||||||
@@ -72,25 +94,38 @@ export const TodolistsList: React.FC<PropsType> = ({demo = false}) => {
|
|||||||
dispatch(thunk)
|
dispatch(thunk)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const addTodolist = useCallback((title: string) => {
|
const addTodolist = useCallback(
|
||||||
|
(title: string) => {
|
||||||
const thunk = addTodolistTC(title)
|
const thunk = addTodolistTC(title)
|
||||||
dispatch(thunk)
|
dispatch(thunk)
|
||||||
}, [dispatch])
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
return <Navigate to={"/login"} />
|
return <Navigate to={'/login'} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return (
|
||||||
<Grid container style={{padding: '20px'}}>
|
<>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
style={{ padding: '20px' }}
|
||||||
|
>
|
||||||
<AddItemForm addItem={addTodolist} />
|
<AddItemForm addItem={addTodolist} />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid container spacing={3}>
|
<Grid
|
||||||
{
|
container
|
||||||
todolists.map(tl => {
|
spacing={3}
|
||||||
|
>
|
||||||
|
{todolists.map((tl) => {
|
||||||
let allTodolistTasks = tasks[tl.id]
|
let allTodolistTasks = tasks[tl.id]
|
||||||
|
|
||||||
return <Grid item key={tl.id}>
|
return (
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
key={tl.id}
|
||||||
|
>
|
||||||
<Paper style={{ padding: '10px' }}>
|
<Paper style={{ padding: '10px' }}>
|
||||||
<Todolist
|
<Todolist
|
||||||
todolist={tl}
|
todolist={tl}
|
||||||
@@ -106,8 +141,9 @@ export const TodolistsList: React.FC<PropsType> = ({demo = false}) => {
|
|||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
})
|
)
|
||||||
}
|
})}
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,114 +1,184 @@
|
|||||||
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 {
|
||||||
|
addTodolistAC,
|
||||||
|
removeTodolistAC,
|
||||||
|
setTodolistsAC,
|
||||||
|
} from './todolists-reducer'
|
||||||
import { TaskPriorities, TaskStatuses } from '../../api/todolists-api'
|
import { TaskPriorities, TaskStatuses } from '../../api/todolists-api'
|
||||||
|
|
||||||
let startState: TasksStateType = {};
|
let startState: TasksStateType = {}
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
startState = {
|
startState = {
|
||||||
"todolistId1": [
|
todolistId1: [
|
||||||
{ id: "1", title: "CSS", status: TaskStatuses.New, todoListId: "todolistId1", description: '',
|
{
|
||||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
|
id: '1',
|
||||||
{ id: "2", title: "JS", status: TaskStatuses.Completed, todoListId: "todolistId1", description: '',
|
title: 'CSS',
|
||||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
|
status: TaskStatuses.New,
|
||||||
{ id: "3", title: "React", status: TaskStatuses.New, todoListId: "todolistId1", description: '',
|
todoListId: 'todolistId1',
|
||||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low }
|
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": [
|
todolistId2: [
|
||||||
{ id: "1", title: "bread", status: TaskStatuses.New, todoListId: "todolistId2", description: '',
|
{
|
||||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
|
id: '1',
|
||||||
{ id: "2", title: "milk", status: TaskStatuses.Completed, todoListId: "todolistId2", description: '',
|
title: 'bread',
|
||||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
|
status: TaskStatuses.New,
|
||||||
{ id: "3", title: "tea", status: TaskStatuses.New, todoListId: "todolistId2", description: '',
|
todoListId: 'todolistId2',
|
||||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low }
|
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', () => {
|
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['todolistId1'].length).toBe(3)
|
||||||
expect(endState["todolistId2"].length).toBe(2);
|
expect(endState['todolistId2'].length).toBe(2)
|
||||||
expect(endState["todolistId2"].every(t => t.id != "2")).toBeTruthy();
|
expect(endState['todolistId2'].every((t) => t.id != '2')).toBeTruthy()
|
||||||
});
|
})
|
||||||
test('correct task should be added to correct array', () => {
|
test('correct task should be added to correct array', () => {
|
||||||
//const action = addTaskAC("juce", "todolistId2");
|
//const action = addTaskAC("juce", "todolistId2");
|
||||||
const action = addTaskAC({
|
const action = addTaskAC({
|
||||||
todoListId: "todolistId2",
|
todoListId: 'todolistId2',
|
||||||
title: "juce",
|
title: 'juce',
|
||||||
status: TaskStatuses.New,
|
status: TaskStatuses.New,
|
||||||
addedDate: "",
|
addedDate: '',
|
||||||
deadline: "",
|
deadline: '',
|
||||||
description: "",
|
description: '',
|
||||||
order: 0,
|
order: 0,
|
||||||
priority: 0,
|
priority: 0,
|
||||||
startDate: "",
|
startDate: '',
|
||||||
id: "id exists"
|
id: 'id exists',
|
||||||
});
|
})
|
||||||
|
|
||||||
const endState = tasksReducer(startState, action)
|
const endState = tasksReducer(startState, action)
|
||||||
|
|
||||||
expect(endState["todolistId1"].length).toBe(3);
|
expect(endState['todolistId1'].length).toBe(3)
|
||||||
expect(endState["todolistId2"].length).toBe(4);
|
expect(endState['todolistId2'].length).toBe(4)
|
||||||
expect(endState["todolistId2"][0].id).toBeDefined();
|
expect(endState['todolistId2'][0].id).toBeDefined()
|
||||||
expect(endState["todolistId2"][0].title).toBe("juce");
|
expect(endState['todolistId2'][0].title).toBe('juce')
|
||||||
expect(endState["todolistId2"][0].status).toBe(TaskStatuses.New);
|
expect(endState['todolistId2'][0].status).toBe(TaskStatuses.New)
|
||||||
});
|
})
|
||||||
test('status of specified task should be changed', () => {
|
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['todolistId1'][1].status).toBe(TaskStatuses.Completed)
|
||||||
expect(endState["todolistId2"][1].status).toBe(TaskStatuses.New);
|
expect(endState['todolistId2'][1].status).toBe(TaskStatuses.New)
|
||||||
});
|
})
|
||||||
test('title of specified task should be changed', () => {
|
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['todolistId1'][1].title).toBe('JS')
|
||||||
expect(endState["todolistId2"][1].title).toBe("yogurt");
|
expect(endState['todolistId2'][1].title).toBe('yogurt')
|
||||||
expect(endState["todolistId2"][0].title).toBe("bread");
|
expect(endState['todolistId2'][0].title).toBe('bread')
|
||||||
});
|
})
|
||||||
test('new array should be added when new todolist is added', () => {
|
test('new array should be added when new todolist is added', () => {
|
||||||
const action = addTodolistAC({
|
const action = addTodolistAC({
|
||||||
id: "blabla",
|
id: 'blabla',
|
||||||
title: "new todolist",
|
title: 'new todolist',
|
||||||
order: 0,
|
order: 0,
|
||||||
addedDate: ''
|
addedDate: '',
|
||||||
});
|
})
|
||||||
|
|
||||||
const endState = tasksReducer(startState, action)
|
const endState = tasksReducer(startState, action)
|
||||||
|
|
||||||
|
const keys = Object.keys(endState)
|
||||||
const keys = Object.keys(endState);
|
const newKey = keys.find((k) => k != 'todolistId1' && k != 'todolistId2')
|
||||||
const newKey = keys.find(k => k != "todolistId1" && k != "todolistId2");
|
|
||||||
if (!newKey) {
|
if (!newKey) {
|
||||||
throw Error("new key should be added")
|
throw Error('new key should be added')
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(keys.length).toBe(3);
|
expect(keys.length).toBe(3)
|
||||||
expect(endState[newKey]).toEqual([]);
|
expect(endState[newKey]).toEqual([])
|
||||||
});
|
})
|
||||||
test('propertry with todolistId should be deleted', () => {
|
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(keys.length).toBe(1)
|
||||||
expect(endState["todolistId2"]).not.toBeDefined();
|
expect(endState['todolistId2']).not.toBeDefined()
|
||||||
});
|
})
|
||||||
|
|
||||||
test('empty arrays should be added when we set todolists', () => {
|
test('empty arrays should be added when we set todolists', () => {
|
||||||
const action = setTodolistsAC([
|
const action = setTodolistsAC([
|
||||||
{id: "1", title: "title 1", order: 0, addedDate: ""},
|
{ id: '1', title: 'title 1', order: 0, addedDate: '' },
|
||||||
{id: "2", title: "title 2", order: 0, addedDate: ""}
|
{ id: '2', title: 'title 2', order: 0, addedDate: '' },
|
||||||
])
|
])
|
||||||
|
|
||||||
const endState = tasksReducer({}, action)
|
const endState = tasksReducer({}, action)
|
||||||
@@ -120,14 +190,16 @@ test('empty arrays should be added when we set todolists', () => {
|
|||||||
expect(endState['2']).toBeDefined()
|
expect(endState['2']).toBeDefined()
|
||||||
})
|
})
|
||||||
test('tasks should be added for todolist', () => {
|
test('tasks should be added for todolist', () => {
|
||||||
const action = setTasksAC(startState["todolistId1"], "todolistId1");
|
const action = setTasksAC(startState['todolistId1'], 'todolistId1')
|
||||||
|
|
||||||
const endState = tasksReducer({
|
const endState = tasksReducer(
|
||||||
"todolistId2": [],
|
{
|
||||||
"todolistId1": []
|
todolistId2: [],
|
||||||
}, action)
|
todolistId1: [],
|
||||||
|
},
|
||||||
|
action
|
||||||
|
)
|
||||||
|
|
||||||
expect(endState["todolistId1"].length).toBe(3)
|
expect(endState['todolistId1'].length).toBe(3)
|
||||||
expect(endState["todolistId2"].length).toBe(0)
|
expect(endState['todolistId2'].length).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,56 @@
|
|||||||
import {AddTodolistActionType, RemoveTodolistActionType, SetTodolistsActionType} from './todolists-reducer'
|
import {
|
||||||
import {TaskPriorities, TaskStatuses, TaskType, todolistsAPI, UpdateTaskModelType} from '../../api/todolists-api'
|
AddTodolistActionType,
|
||||||
|
RemoveTodolistActionType,
|
||||||
|
SetTodolistsActionType,
|
||||||
|
} from './todolists-reducer'
|
||||||
|
import {
|
||||||
|
TaskPriorities,
|
||||||
|
TaskStatuses,
|
||||||
|
TaskType,
|
||||||
|
todolistsAPI,
|
||||||
|
UpdateTaskModelType,
|
||||||
|
} from '../../api/todolists-api'
|
||||||
import { Dispatch } from 'redux'
|
import { Dispatch } from 'redux'
|
||||||
import { AppRootStateType } from '../../app/store'
|
import { AppRootStateType } from '../../app/store'
|
||||||
import {setAppErrorAC, SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../../app/app-reducer'
|
import {
|
||||||
import {handleServerAppError, handleServerNetworkError} from '../../utils/error-utils'
|
setAppErrorAC,
|
||||||
|
SetAppErrorActionType,
|
||||||
|
setAppStatusAC,
|
||||||
|
SetAppStatusActionType,
|
||||||
|
} from '../../app/app-reducer'
|
||||||
|
import {
|
||||||
|
handleServerAppError,
|
||||||
|
handleServerNetworkError,
|
||||||
|
} from '../../utils/error-utils'
|
||||||
|
|
||||||
const initialState: TasksStateType = {}
|
const initialState: TasksStateType = {}
|
||||||
|
|
||||||
export const tasksReducer = (state: TasksStateType = initialState, action: ActionsType): TasksStateType => {
|
export const tasksReducer = (
|
||||||
|
state: TasksStateType = initialState,
|
||||||
|
action: ActionsType
|
||||||
|
): TasksStateType => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'REMOVE-TASK':
|
case 'REMOVE-TASK':
|
||||||
return {...state, [action.todolistId]: state[action.todolistId].filter(t => t.id != action.taskId)}
|
return {
|
||||||
|
...state,
|
||||||
|
[action.todolistId]: state[action.todolistId].filter(
|
||||||
|
(t) => t.id != action.taskId
|
||||||
|
),
|
||||||
|
}
|
||||||
case 'ADD-TASK':
|
case 'ADD-TASK':
|
||||||
return {...state, [action.task.todoListId]: [action.task, ...state[action.task.todoListId]]}
|
return {
|
||||||
|
...state,
|
||||||
|
[action.task.todoListId]: [
|
||||||
|
action.task,
|
||||||
|
...state[action.task.todoListId],
|
||||||
|
],
|
||||||
|
}
|
||||||
case 'UPDATE-TASK':
|
case 'UPDATE-TASK':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[action.todolistId]: state[action.todolistId]
|
[action.todolistId]: state[action.todolistId].map((t) =>
|
||||||
.map(t => t.id === action.taskId ? {...t, ...action.model} : t)
|
t.id === action.taskId ? { ...t, ...action.model } : t
|
||||||
|
),
|
||||||
}
|
}
|
||||||
case 'ADD-TODOLIST':
|
case 'ADD-TODOLIST':
|
||||||
return { ...state, [action.todolist.id]: [] }
|
return { ...state, [action.todolist.id]: [] }
|
||||||
@@ -27,7 +60,7 @@ export const tasksReducer = (state: TasksStateType = initialState, action: Actio
|
|||||||
return copyState
|
return copyState
|
||||||
case 'SET-TODOLISTS': {
|
case 'SET-TODOLISTS': {
|
||||||
const copyState = { ...state }
|
const copyState = { ...state }
|
||||||
action.todolists.forEach(tl => {
|
action.todolists.forEach((tl) => {
|
||||||
copyState[tl.id] = []
|
copyState[tl.id] = []
|
||||||
})
|
})
|
||||||
return copyState
|
return copyState
|
||||||
@@ -41,52 +74,68 @@ export const tasksReducer = (state: TasksStateType = initialState, action: Actio
|
|||||||
|
|
||||||
// actions
|
// actions
|
||||||
export const removeTaskAC = (taskId: string, todolistId: string) =>
|
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) =>
|
export const addTaskAC = (task: TaskType) =>
|
||||||
({type: 'ADD-TASK', task} as const)
|
({ type: 'ADD-TASK', task }) as const
|
||||||
export const updateTaskAC = (taskId: string, model: UpdateDomainTaskModelType, todolistId: string) =>
|
export const updateTaskAC = (
|
||||||
({type: 'UPDATE-TASK', model, todolistId, taskId} as const)
|
taskId: string,
|
||||||
|
model: UpdateDomainTaskModelType,
|
||||||
|
todolistId: string
|
||||||
|
) => ({ type: 'UPDATE-TASK', model, todolistId, taskId }) as const
|
||||||
export const setTasksAC = (tasks: Array<TaskType>, todolistId: string) =>
|
export const setTasksAC = (tasks: Array<TaskType>, todolistId: string) =>
|
||||||
({type: 'SET-TASKS', tasks, todolistId} as const)
|
({ type: 'SET-TASKS', tasks, todolistId }) as const
|
||||||
|
|
||||||
// thunks
|
// thunks
|
||||||
export const fetchTasksTC = (todolistId: string) => (dispatch: Dispatch<ActionsType | SetAppStatusActionType>) => {
|
export const fetchTasksTC =
|
||||||
|
(todolistId: string) =>
|
||||||
|
(dispatch: Dispatch<ActionsType | SetAppStatusActionType>) => {
|
||||||
dispatch(setAppStatusAC('loading'))
|
dispatch(setAppStatusAC('loading'))
|
||||||
todolistsAPI.getTasks(todolistId)
|
todolistsAPI.getTasks(todolistId).then((res) => {
|
||||||
.then((res) => {
|
|
||||||
const tasks = res.data.items
|
const tasks = res.data.items
|
||||||
dispatch(setTasksAC(tasks, todolistId))
|
dispatch(setTasksAC(tasks, todolistId))
|
||||||
dispatch(setAppStatusAC('succeeded'))
|
dispatch(setAppStatusAC('succeeded'))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
export const removeTaskTC = (taskId: string, todolistId: string) => (dispatch: Dispatch<ActionsType>) => {
|
export const removeTaskTC =
|
||||||
todolistsAPI.deleteTask(todolistId, taskId)
|
(taskId: string, todolistId: string) => (dispatch: Dispatch<ActionsType>) => {
|
||||||
.then(res => {
|
todolistsAPI.deleteTask(todolistId, taskId).then((res) => {
|
||||||
const action = removeTaskAC(taskId, todolistId)
|
const action = removeTaskAC(taskId, todolistId)
|
||||||
dispatch(action)
|
dispatch(action)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
export const addTaskTC = (title: string, todolistId: string) => (dispatch: Dispatch<ActionsType | SetAppErrorActionType | SetAppStatusActionType>) => {
|
export const addTaskTC =
|
||||||
|
(title: string, todolistId: string) =>
|
||||||
|
(
|
||||||
|
dispatch: Dispatch<
|
||||||
|
ActionsType | SetAppErrorActionType | SetAppStatusActionType
|
||||||
|
>
|
||||||
|
) => {
|
||||||
dispatch(setAppStatusAC('loading'))
|
dispatch(setAppStatusAC('loading'))
|
||||||
todolistsAPI.createTask(todolistId, title)
|
todolistsAPI
|
||||||
.then(res => {
|
.createTask(todolistId, title)
|
||||||
|
.then((res) => {
|
||||||
if (res.data.resultCode === 0) {
|
if (res.data.resultCode === 0) {
|
||||||
const task = res.data.data.item
|
const task = res.data.data.item
|
||||||
const action = addTaskAC(task)
|
const action = addTaskAC(task)
|
||||||
dispatch(action)
|
dispatch(action)
|
||||||
dispatch(setAppStatusAC('succeeded'))
|
dispatch(setAppStatusAC('succeeded'))
|
||||||
} else {
|
} else {
|
||||||
handleServerAppError(res.data, dispatch);
|
handleServerAppError(res.data, dispatch)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
handleServerNetworkError(error, dispatch)
|
handleServerNetworkError(error, dispatch)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
export const updateTaskTC = (taskId: string, domainModel: UpdateDomainTaskModelType, todolistId: string) =>
|
export const updateTaskTC =
|
||||||
|
(
|
||||||
|
taskId: string,
|
||||||
|
domainModel: UpdateDomainTaskModelType,
|
||||||
|
todolistId: string
|
||||||
|
) =>
|
||||||
(dispatch: ThunkDispatch, getState: () => AppRootStateType) => {
|
(dispatch: ThunkDispatch, getState: () => AppRootStateType) => {
|
||||||
const state = getState()
|
const state = getState()
|
||||||
const task = state.tasks[todolistId].find(t => t.id === taskId)
|
const task = state.tasks[todolistId].find((t) => t.id === taskId)
|
||||||
if (!task) {
|
if (!task) {
|
||||||
//throw new Error("task not found in the state");
|
//throw new Error("task not found in the state");
|
||||||
console.warn('task not found in the state')
|
console.warn('task not found in the state')
|
||||||
@@ -100,20 +149,21 @@ export const updateTaskTC = (taskId: string, domainModel: UpdateDomainTaskModelT
|
|||||||
startDate: task.startDate,
|
startDate: task.startDate,
|
||||||
title: task.title,
|
title: task.title,
|
||||||
status: task.status,
|
status: task.status,
|
||||||
...domainModel
|
...domainModel,
|
||||||
}
|
}
|
||||||
|
|
||||||
todolistsAPI.updateTask(todolistId, taskId, apiModel)
|
todolistsAPI
|
||||||
.then(res => {
|
.updateTask(todolistId, taskId, apiModel)
|
||||||
|
.then((res) => {
|
||||||
if (res.data.resultCode === 0) {
|
if (res.data.resultCode === 0) {
|
||||||
const action = updateTaskAC(taskId, domainModel, todolistId)
|
const action = updateTaskAC(taskId, domainModel, todolistId)
|
||||||
dispatch(action)
|
dispatch(action)
|
||||||
} else {
|
} else {
|
||||||
handleServerAppError(res.data, dispatch);
|
handleServerAppError(res.data, dispatch)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
handleServerNetworkError(error, dispatch);
|
handleServerNetworkError(error, dispatch)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,4 +187,6 @@ type ActionsType =
|
|||||||
| RemoveTodolistActionType
|
| RemoveTodolistActionType
|
||||||
| SetTodolistsActionType
|
| SetTodolistsActionType
|
||||||
| ReturnType<typeof setTasksAC>
|
| ReturnType<typeof setTasksAC>
|
||||||
type ThunkDispatch = Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>
|
type ThunkDispatch = Dispatch<
|
||||||
|
ActionsType | SetAppStatusActionType | SetAppErrorActionType
|
||||||
|
>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
addTodolistAC, changeTodolistEntityStatusAC,
|
addTodolistAC,
|
||||||
|
changeTodolistEntityStatusAC,
|
||||||
changeTodolistFilterAC,
|
changeTodolistFilterAC,
|
||||||
changeTodolistTitleAC, FilterValuesType,
|
changeTodolistTitleAC,
|
||||||
removeTodolistAC, setTodolistsAC, TodolistDomainType,
|
FilterValuesType,
|
||||||
todolistsReducer
|
removeTodolistAC,
|
||||||
|
setTodolistsAC,
|
||||||
|
TodolistDomainType,
|
||||||
|
todolistsReducer,
|
||||||
} from './todolists-reducer'
|
} from './todolists-reducer'
|
||||||
import { v1 } from 'uuid'
|
import { v1 } from 'uuid'
|
||||||
import { TodolistType } from '../../api/todolists-api'
|
import { TodolistType } from '../../api/todolists-api'
|
||||||
@@ -17,8 +21,22 @@ beforeEach(() => {
|
|||||||
todolistId1 = v1()
|
todolistId1 = v1()
|
||||||
todolistId2 = v1()
|
todolistId2 = v1()
|
||||||
startState = [
|
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}
|
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,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -34,10 +52,9 @@ test('correct todolist should be added', () => {
|
|||||||
title: 'New Todolist',
|
title: 'New Todolist',
|
||||||
id: 'any id',
|
id: 'any id',
|
||||||
addedDate: '',
|
addedDate: '',
|
||||||
order: 0
|
order: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const endState = todolistsReducer(startState, addTodolistAC(todolist))
|
const endState = todolistsReducer(startState, addTodolistAC(todolist))
|
||||||
|
|
||||||
expect(endState.length).toBe(3)
|
expect(endState.length).toBe(3)
|
||||||
@@ -67,7 +84,6 @@ test('correct filter of todolist should be changed', () => {
|
|||||||
expect(endState[1].filter).toBe(newFilter)
|
expect(endState[1].filter).toBe(newFilter)
|
||||||
})
|
})
|
||||||
test('todolists should be added', () => {
|
test('todolists should be added', () => {
|
||||||
|
|
||||||
const action = setTodolistsAC(startState)
|
const action = setTodolistsAC(startState)
|
||||||
|
|
||||||
const endState = todolistsReducer([], action)
|
const endState = todolistsReducer([], action)
|
||||||
@@ -84,6 +100,3 @@ test('correct entity status of todolist should be changed', () => {
|
|||||||
expect(endState[0].entityStatus).toBe('idle')
|
expect(endState[0].entityStatus).toBe('idle')
|
||||||
expect(endState[1].entityStatus).toBe(newStatus)
|
expect(endState[1].entityStatus).toBe(newStatus)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +1,93 @@
|
|||||||
import { todolistsAPI, TodolistType } from '../../api/todolists-api'
|
import { todolistsAPI, TodolistType } from '../../api/todolists-api'
|
||||||
import { Dispatch } from 'redux'
|
import { Dispatch } from 'redux'
|
||||||
import {RequestStatusType, SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../../app/app-reducer'
|
import {
|
||||||
|
RequestStatusType,
|
||||||
|
SetAppErrorActionType,
|
||||||
|
setAppStatusAC,
|
||||||
|
SetAppStatusActionType,
|
||||||
|
} from '../../app/app-reducer'
|
||||||
import { handleServerNetworkError } from '../../utils/error-utils'
|
import { handleServerNetworkError } from '../../utils/error-utils'
|
||||||
import { AppThunk } from '../../app/store';
|
import { AppThunk } from '../../app/store'
|
||||||
|
|
||||||
const initialState: Array<TodolistDomainType> = []
|
const initialState: Array<TodolistDomainType> = []
|
||||||
|
|
||||||
export const todolistsReducer = (state: Array<TodolistDomainType> = initialState, action: ActionsType): Array<TodolistDomainType> => {
|
export const todolistsReducer = (
|
||||||
|
state: Array<TodolistDomainType> = initialState,
|
||||||
|
action: ActionsType
|
||||||
|
): Array<TodolistDomainType> => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'REMOVE-TODOLIST':
|
case 'REMOVE-TODOLIST':
|
||||||
return state.filter(tl => tl.id != action.id)
|
return state.filter((tl) => tl.id != action.id)
|
||||||
case 'ADD-TODOLIST':
|
case 'ADD-TODOLIST':
|
||||||
return [{...action.todolist, filter: 'all', entityStatus: 'idle'}, ...state]
|
return [
|
||||||
|
{ ...action.todolist, filter: 'all', entityStatus: 'idle' },
|
||||||
|
...state,
|
||||||
|
]
|
||||||
|
|
||||||
case 'CHANGE-TODOLIST-TITLE':
|
case 'CHANGE-TODOLIST-TITLE':
|
||||||
return state.map(tl => tl.id === action.id ? {...tl, title: action.title} : tl)
|
return state.map((tl) =>
|
||||||
|
tl.id === action.id ? { ...tl, title: action.title } : tl
|
||||||
|
)
|
||||||
case 'CHANGE-TODOLIST-FILTER':
|
case 'CHANGE-TODOLIST-FILTER':
|
||||||
return state.map(tl => tl.id === action.id ? {...tl, filter: action.filter} : tl)
|
return state.map((tl) =>
|
||||||
|
tl.id === action.id ? { ...tl, filter: action.filter } : tl
|
||||||
|
)
|
||||||
case 'CHANGE-TODOLIST-ENTITY-STATUS':
|
case 'CHANGE-TODOLIST-ENTITY-STATUS':
|
||||||
return state.map(tl => tl.id === action.id ? {...tl, entityStatus: action.status} : tl)
|
return state.map((tl) =>
|
||||||
|
tl.id === action.id ? { ...tl, entityStatus: action.status } : tl
|
||||||
|
)
|
||||||
case 'SET-TODOLISTS':
|
case 'SET-TODOLISTS':
|
||||||
return action.todolists.map(tl => ({...tl, filter: 'all', entityStatus: 'idle'}))
|
return action.todolists.map((tl) => ({
|
||||||
|
...tl,
|
||||||
|
filter: 'all',
|
||||||
|
entityStatus: 'idle',
|
||||||
|
}))
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
export const removeTodolistAC = (id: string) => ({type: 'REMOVE-TODOLIST', id} as const)
|
export const removeTodolistAC = (id: string) =>
|
||||||
export const addTodolistAC = (todolist: TodolistType) => ({type: 'ADD-TODOLIST', todolist} as const)
|
({ type: 'REMOVE-TODOLIST', id }) as const
|
||||||
export const changeTodolistTitleAC = (id: string, title: string) => ({
|
export const addTodolistAC = (todolist: TodolistType) =>
|
||||||
|
({ type: 'ADD-TODOLIST', todolist }) as const
|
||||||
|
export const changeTodolistTitleAC = (id: string, title: string) =>
|
||||||
|
({
|
||||||
type: 'CHANGE-TODOLIST-TITLE',
|
type: 'CHANGE-TODOLIST-TITLE',
|
||||||
id,
|
id,
|
||||||
title
|
title,
|
||||||
} as const)
|
}) as const
|
||||||
export const changeTodolistFilterAC = (id: string, filter: FilterValuesType) => ({
|
export const changeTodolistFilterAC = (id: string, filter: FilterValuesType) =>
|
||||||
|
({
|
||||||
type: 'CHANGE-TODOLIST-FILTER',
|
type: 'CHANGE-TODOLIST-FILTER',
|
||||||
id,
|
id,
|
||||||
filter
|
filter,
|
||||||
} as const)
|
}) as const
|
||||||
export const changeTodolistEntityStatusAC = (id: string, status: RequestStatusType) => ({
|
export const changeTodolistEntityStatusAC = (
|
||||||
type: 'CHANGE-TODOLIST-ENTITY-STATUS', id, status } as const)
|
id: string,
|
||||||
export const setTodolistsAC = (todolists: Array<TodolistType>) => ({type: 'SET-TODOLISTS', todolists} as const)
|
status: RequestStatusType
|
||||||
|
) =>
|
||||||
|
({
|
||||||
|
type: 'CHANGE-TODOLIST-ENTITY-STATUS',
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
}) as const
|
||||||
|
export const setTodolistsAC = (todolists: Array<TodolistType>) =>
|
||||||
|
({ type: 'SET-TODOLISTS', todolists }) as const
|
||||||
|
|
||||||
// thunks
|
// thunks
|
||||||
export const fetchTodolistsTC = (): AppThunk => {
|
export const fetchTodolistsTC = (): AppThunk => {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(setAppStatusAC('loading'))
|
dispatch(setAppStatusAC('loading'))
|
||||||
todolistsAPI.getTodolists()
|
todolistsAPI
|
||||||
|
.getTodolists()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
dispatch(setTodolistsAC(res.data))
|
dispatch(setTodolistsAC(res.data))
|
||||||
dispatch(setAppStatusAC('succeeded'))
|
dispatch(setAppStatusAC('succeeded'))
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
handleServerNetworkError(error, dispatch);
|
handleServerNetworkError(error, dispatch)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,8 +97,7 @@ export const removeTodolistTC = (todolistId: string) => {
|
|||||||
dispatch(setAppStatusAC('loading'))
|
dispatch(setAppStatusAC('loading'))
|
||||||
//изменим статус конкретного тудулиста, чтобы он мог задизеблить что надо
|
//изменим статус конкретного тудулиста, чтобы он мог задизеблить что надо
|
||||||
dispatch(changeTodolistEntityStatusAC(todolistId, 'loading'))
|
dispatch(changeTodolistEntityStatusAC(todolistId, 'loading'))
|
||||||
todolistsAPI.deleteTodolist(todolistId)
|
todolistsAPI.deleteTodolist(todolistId).then((res) => {
|
||||||
.then((res) => {
|
|
||||||
dispatch(removeTodolistAC(todolistId))
|
dispatch(removeTodolistAC(todolistId))
|
||||||
//скажем глобально приложению, что асинхронная операция завершена
|
//скажем глобально приложению, что асинхронная операция завершена
|
||||||
dispatch(setAppStatusAC('succeeded'))
|
dispatch(setAppStatusAC('succeeded'))
|
||||||
@@ -74,8 +107,7 @@ export const removeTodolistTC = (todolistId: string) => {
|
|||||||
export const addTodolistTC = (title: string) => {
|
export const addTodolistTC = (title: string) => {
|
||||||
return (dispatch: ThunkDispatch) => {
|
return (dispatch: ThunkDispatch) => {
|
||||||
dispatch(setAppStatusAC('loading'))
|
dispatch(setAppStatusAC('loading'))
|
||||||
todolistsAPI.createTodolist(title)
|
todolistsAPI.createTodolist(title).then((res) => {
|
||||||
.then((res) => {
|
|
||||||
dispatch(addTodolistAC(res.data.data.item))
|
dispatch(addTodolistAC(res.data.data.item))
|
||||||
dispatch(setAppStatusAC('succeeded'))
|
dispatch(setAppStatusAC('succeeded'))
|
||||||
})
|
})
|
||||||
@@ -83,17 +115,16 @@ export const addTodolistTC = (title: string) => {
|
|||||||
}
|
}
|
||||||
export const changeTodolistTitleTC = (id: string, title: string) => {
|
export const changeTodolistTitleTC = (id: string, title: string) => {
|
||||||
return (dispatch: Dispatch<ActionsType>) => {
|
return (dispatch: Dispatch<ActionsType>) => {
|
||||||
todolistsAPI.updateTodolist(id, title)
|
todolistsAPI.updateTodolist(id, title).then((res) => {
|
||||||
.then((res) => {
|
|
||||||
dispatch(changeTodolistTitleAC(id, title))
|
dispatch(changeTodolistTitleAC(id, title))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// types
|
// types
|
||||||
export type AddTodolistActionType = ReturnType<typeof addTodolistAC>;
|
export type AddTodolistActionType = ReturnType<typeof addTodolistAC>
|
||||||
export type RemoveTodolistActionType = ReturnType<typeof removeTodolistAC>;
|
export type RemoveTodolistActionType = ReturnType<typeof removeTodolistAC>
|
||||||
export type SetTodolistsActionType = ReturnType<typeof setTodolistsAC>;
|
export type SetTodolistsActionType = ReturnType<typeof setTodolistsAC>
|
||||||
type ActionsType =
|
type ActionsType =
|
||||||
| RemoveTodolistActionType
|
| RemoveTodolistActionType
|
||||||
| AddTodolistActionType
|
| AddTodolistActionType
|
||||||
@@ -101,9 +132,11 @@ type ActionsType =
|
|||||||
| ReturnType<typeof changeTodolistFilterAC>
|
| ReturnType<typeof changeTodolistFilterAC>
|
||||||
| SetTodolistsActionType
|
| SetTodolistsActionType
|
||||||
| ReturnType<typeof changeTodolistEntityStatusAC>
|
| ReturnType<typeof changeTodolistEntityStatusAC>
|
||||||
export type FilterValuesType = 'all' | 'active' | 'completed';
|
export type FilterValuesType = 'all' | 'active' | 'completed'
|
||||||
export type TodolistDomainType = TodolistType & {
|
export type TodolistDomainType = TodolistType & {
|
||||||
filter: FilterValuesType
|
filter: FilterValuesType
|
||||||
entityStatus: RequestStatusType
|
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 {
|
||||||
|
addTodolistAC,
|
||||||
|
TodolistDomainType,
|
||||||
|
todolistsReducer,
|
||||||
|
} from './todolists-reducer'
|
||||||
import { tasksReducer, TasksStateType } from './tasks-reducer'
|
import { tasksReducer, TasksStateType } from './tasks-reducer'
|
||||||
import { TodolistType } from '../../api/todolists-api'
|
import { TodolistType } from '../../api/todolists-api'
|
||||||
|
|
||||||
test('ids should be equals', () => {
|
test('ids should be equals', () => {
|
||||||
const startTasksState: TasksStateType = {};
|
const startTasksState: TasksStateType = {}
|
||||||
const startTodolistsState: Array<TodolistDomainType> = [];
|
const startTodolistsState: Array<TodolistDomainType> = []
|
||||||
|
|
||||||
let todolist: TodolistType = {
|
let todolist: TodolistType = {
|
||||||
title: 'new todolist',
|
title: 'new todolist',
|
||||||
id: 'any id',
|
id: 'any id',
|
||||||
addedDate: '',
|
addedDate: '',
|
||||||
order: 0
|
order: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = addTodolistAC(todolist);
|
const action = addTodolistAC(todolist)
|
||||||
|
|
||||||
const endTasksState = tasksReducer(startTasksState, action)
|
const endTasksState = tasksReducer(startTasksState, action)
|
||||||
const endTodolistsState = todolistsReducer(startTodolistsState, action)
|
const endTodolistsState = todolistsReducer(startTodolistsState, action)
|
||||||
|
|
||||||
const keys = Object.keys(endTasksState);
|
const keys = Object.keys(endTasksState)
|
||||||
const idFromTasks = keys[0];
|
const idFromTasks = keys[0]
|
||||||
const idFromTodolists = endTodolistsState[0].id;
|
const idFromTodolists = endTodolistsState[0].id
|
||||||
|
|
||||||
expect(idFromTasks).toBe(action.todolist.id);
|
expect(idFromTasks).toBe(action.todolist.id)
|
||||||
expect(idFromTodolists).toBe(action.todolist.id);
|
expect(idFromTodolists).toBe(action.todolist.id)
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux'
|
||||||
import { AppDispatch } from '../app/store';
|
import { AppDispatch } from '../app/store'
|
||||||
|
|
||||||
|
|
||||||
// export const useAppDispatch: () => AppDispatch = useDispatch
|
// export const useAppDispatch: () => AppDispatch = useDispatch
|
||||||
export const useAppDispatch = () => useDispatch<AppDispatch>()
|
export const useAppDispatch = () => useDispatch<AppDispatch>()
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css';
|
import './index.css'
|
||||||
import App from './app/App';
|
import App from './app/App'
|
||||||
import { store } from './app/store';
|
import { store } from './app/store'
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux'
|
||||||
|
|
||||||
|
const root = createRoot(document.getElementById('root') as HTMLElement)
|
||||||
const root = createRoot(document.getElementById('root') as HTMLElement);
|
|
||||||
root.render(
|
root.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<App />
|
<App />
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -18,33 +18,30 @@ const isLocalhost = Boolean(
|
|||||||
window.location.hostname.match(
|
window.location.hostname.match(
|
||||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||||
)
|
)
|
||||||
);
|
)
|
||||||
|
|
||||||
type Config = {
|
type Config = {
|
||||||
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
onSuccess?: (registration: ServiceWorkerRegistration) => void
|
||||||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
onUpdate?: (registration: ServiceWorkerRegistration) => void
|
||||||
};
|
}
|
||||||
|
|
||||||
export function register(config?: Config) {
|
export function register(config?: Config) {
|
||||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
// The URL constructor is available in all browsers that support SW.
|
// The URL constructor is available in all browsers that support SW.
|
||||||
const publicUrl = new URL(
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
|
||||||
process.env.PUBLIC_URL,
|
|
||||||
window.location.href
|
|
||||||
);
|
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different 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
|
// 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
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
|
||||||
|
|
||||||
if (isLocalhost) {
|
if (isLocalhost) {
|
||||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
// 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
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
// service worker/PWA documentation.
|
// service worker/PWA documentation.
|
||||||
@@ -52,24 +49,24 @@ export function register(config?: Config) {
|
|||||||
console.log(
|
console.log(
|
||||||
'This web app is being served cache-first by a service ' +
|
'This web app is being served cache-first by a service ' +
|
||||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
// Is not localhost. Just register service worker
|
// Is not localhost. Just register service worker
|
||||||
registerValidSW(swUrl, config);
|
registerValidSW(swUrl, config)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerValidSW(swUrl: string, config?: Config) {
|
function registerValidSW(swUrl: string, config?: Config) {
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
.register(swUrl)
|
.register(swUrl)
|
||||||
.then(registration => {
|
.then((registration) => {
|
||||||
registration.onupdatefound = () => {
|
registration.onupdatefound = () => {
|
||||||
const installingWorker = registration.installing;
|
const installingWorker = registration.installing
|
||||||
if (installingWorker == null) {
|
if (installingWorker == null) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
installingWorker.onstatechange = () => {
|
installingWorker.onstatechange = () => {
|
||||||
if (installingWorker.state === 'installed') {
|
if (installingWorker.state === 'installed') {
|
||||||
@@ -80,70 +77,70 @@ function registerValidSW(swUrl: string, config?: Config) {
|
|||||||
console.log(
|
console.log(
|
||||||
'New content is available and will be used when all ' +
|
'New content is available and will be used when all ' +
|
||||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||||
);
|
)
|
||||||
|
|
||||||
// Execute callback
|
// Execute callback
|
||||||
if (config && config.onUpdate) {
|
if (config && config.onUpdate) {
|
||||||
config.onUpdate(registration);
|
config.onUpdate(registration)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// At this point, everything has been precached.
|
// At this point, everything has been precached.
|
||||||
// It's the perfect time to display a
|
// It's the perfect time to display a
|
||||||
// "Content is cached for offline use." message.
|
// "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
|
// Execute callback
|
||||||
if (config && config.onSuccess) {
|
if (config && config.onSuccess) {
|
||||||
config.onSuccess(registration);
|
config.onSuccess(registration)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error('Error during service worker registration:', error);
|
console.error('Error during service worker registration:', error)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||||
// Check if the service worker can be found. If it can't reload the page.
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
fetch(swUrl, {
|
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.
|
// 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 (
|
if (
|
||||||
response.status === 404 ||
|
response.status === 404 ||
|
||||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||||
) {
|
) {
|
||||||
// No service worker found. Probably a different app. Reload the page.
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
registration.unregister().then(() => {
|
registration.unregister().then(() => {
|
||||||
window.location.reload();
|
window.location.reload()
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
// Service worker found. Proceed as normal.
|
// Service worker found. Proceed as normal.
|
||||||
registerValidSW(swUrl, config);
|
registerValidSW(swUrl, config)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.log(
|
console.log(
|
||||||
'No internet connection found. App is running in offline mode.'
|
'No internet connection found. App is running in offline mode.'
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unregister() {
|
export function unregister() {
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.ready
|
navigator.serviceWorker.ready
|
||||||
.then(registration => {
|
.then((registration) => {
|
||||||
registration.unregister();
|
registration.unregister()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error.message)
|
||||||
})
|
})
|
||||||
.catch(error => {
|
|
||||||
console.error(error.message);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
// allows you to do things like:
|
// allows you to do things like:
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom'
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import {setAppErrorAC, SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../app/app-reducer'
|
import {
|
||||||
|
setAppErrorAC,
|
||||||
|
SetAppErrorActionType,
|
||||||
|
setAppStatusAC,
|
||||||
|
SetAppStatusActionType,
|
||||||
|
} from '../app/app-reducer'
|
||||||
import { ResponseType } from '../api/todolists-api'
|
import { ResponseType } from '../api/todolists-api'
|
||||||
import { Dispatch } from 'redux'
|
import { Dispatch } from 'redux'
|
||||||
|
|
||||||
export const handleServerAppError = <D>(data: ResponseType<D>, dispatch: Dispatch<SetAppErrorActionType | SetAppStatusActionType>) => {
|
export const handleServerAppError = <D>(
|
||||||
|
data: ResponseType<D>,
|
||||||
|
dispatch: Dispatch<SetAppErrorActionType | SetAppStatusActionType>
|
||||||
|
) => {
|
||||||
if (data.messages.length) {
|
if (data.messages.length) {
|
||||||
dispatch(setAppErrorAC(data.messages[0]))
|
dispatch(setAppErrorAC(data.messages[0]))
|
||||||
} else {
|
} else {
|
||||||
@@ -11,7 +19,10 @@ export const handleServerAppError = <D>(data: ResponseType<D>, dispatch: Dispatc
|
|||||||
dispatch(setAppStatusAC('failed'))
|
dispatch(setAppStatusAC('failed'))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handleServerNetworkError = (error: { message: string }, dispatch: Dispatch<SetAppErrorActionType | SetAppStatusActionType>) => {
|
export const handleServerNetworkError = (
|
||||||
|
error: { message: string },
|
||||||
|
dispatch: Dispatch<SetAppErrorActionType | SetAppStatusActionType>
|
||||||
|
) => {
|
||||||
dispatch(setAppErrorAC(error.message ? error.message : 'Some error occurred'))
|
dispatch(setAppErrorAC(error.message ? error.message : 'Some error occurred'))
|
||||||
dispatch(setAppStatusAC('failed'))
|
dispatch(setAppStatusAC('failed'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
@@ -19,7 +15,5 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react"
|
"jsx": "react"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src"]
|
||||||
"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"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||||
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
|
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:
|
pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
|
||||||
version "5.6.0"
|
version "5.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
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"
|
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
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"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@@ -9055,7 +9069,14 @@ stringify-object@^3.3.0:
|
|||||||
is-obj "^1.0.1"
|
is-obj "^1.0.1"
|
||||||
is-regexp "^1.0.0"
|
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"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
@@ -10105,7 +10126,16 @@ workbox-window@6.6.1:
|
|||||||
"@types/trusted-types" "^2.0.2"
|
"@types/trusted-types" "^2.0.2"
|
||||||
workbox-core "6.6.1"
|
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"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
|
|||||||
Reference in New Issue
Block a user