chore: add prettier and run it on all files

This commit is contained in:
2024-08-17 17:30:56 +02:00
parent 143b48c7a9
commit 2f8ff0d004
28 changed files with 1609 additions and 1117 deletions

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"semi": false,
"singleQuote": true,
"jsxSingleQuote": true,
"trailingComma": "es5",
"singleAttributePerLine": true
}

View File

@@ -54,5 +54,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"prettier": "3.3.3"
}
}

View File

@@ -1,20 +1,35 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link
rel="icon"
href="%PUBLIC_URL%/favicon.ico"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<meta
name="theme-color"
content="#000000"
/>
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link
rel="apple-touch-icon"
href="%PUBLIC_URL%/logo192.png"
/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link
rel="manifest"
href="%PUBLIC_URL%/manifest.json"
/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.

View File

@@ -3,47 +3,59 @@ import axios from 'axios'
const settings = {
withCredentials: true,
headers: {
'API-KEY': '1cdd9f77-c60e-4af5-b194-659e4ebd5d41'
}
'API-KEY': '1cdd9f77-c60e-4af5-b194-659e4ebd5d41',
},
}
const instance = axios.create({
baseURL: 'https://social-network.samuraijs.com/api/1.1/',
...settings
...settings,
})
// api
export const todolistsAPI = {
getTodolists() {
const promise = instance.get<TodolistType[]>('todo-lists');
return promise;
const promise = instance.get<TodolistType[]>('todo-lists')
return promise
},
createTodolist(title: string) {
const promise = instance.post<ResponseType<{ item: TodolistType }>>('todo-lists', {title: title});
return promise;
const promise = instance.post<ResponseType<{ item: TodolistType }>>(
'todo-lists',
{ title: title }
)
return promise
},
deleteTodolist(id: string) {
const promise = instance.delete<ResponseType>(`todo-lists/${id}`);
return promise;
const promise = instance.delete<ResponseType>(`todo-lists/${id}`)
return promise
},
updateTodolist(id: string, title: string) {
const promise = instance.put<ResponseType>(`todo-lists/${id}`, {title: title});
return promise;
const promise = instance.put<ResponseType>(`todo-lists/${id}`, {
title: title,
})
return promise
},
getTasks(todolistId: string) {
return instance.get<GetTasksResponse>(`todo-lists/${todolistId}/tasks`);
return instance.get<GetTasksResponse>(`todo-lists/${todolistId}/tasks`)
},
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) {
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) {
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 = {
email: string
password: string
@@ -53,17 +65,24 @@ export type LoginParamsType = {
export const authAPI = {
login(data: LoginParamsType) {
const promise = instance.post<ResponseType<{userId?: number}>>('auth/login', data);
return promise;
const promise = instance.post<ResponseType<{ userId?: number }>>(
'auth/login',
data
)
return promise
},
logout() {
const promise = instance.delete<ResponseType<{userId?: number}>>('auth/login');
return promise;
const promise =
instance.delete<ResponseType<{ userId?: number }>>('auth/login')
return promise
},
me() {
const promise = instance.get<ResponseType<{id: number; email: string; login: string}>>('auth/me');
const promise =
instance.get<ResponseType<{ id: number; email: string; login: string }>>(
'auth/me'
)
return promise
}
},
}
// types
@@ -78,19 +97,22 @@ export type ResponseType<D = {}> = {
messages: Array<string>
data: D
}
export enum TaskStatuses {
New = 0,
InProgress = 1,
Completed = 2,
Draft = 3
Draft = 3,
}
export enum TaskPriorities {
Low = 0,
Middle = 1,
Hi = 2,
Urgently = 3,
Later = 4
Later = 4,
}
export type TaskType = {
description: string
title: string

View File

@@ -16,18 +16,24 @@ import {
IconButton,
LinearProgress,
Toolbar,
Typography
} from '@mui/material';
Typography,
} from '@mui/material'
import { Menu } from '@mui/icons-material'
type PropsType = {
demo?: boolean
}
function App({demo = false}: PropsType) {
const status = useSelector<AppRootStateType, RequestStatusType>((state) => state.app.status)
const isInitialized = useSelector<AppRootStateType, boolean>((state) => state.app.isInitialized)
const isLoggedIn = useSelector<AppRootStateType, boolean>(state => state.auth.isLoggedIn)
function App({ demo = false }: PropsType) {
const status = useSelector<AppRootStateType, RequestStatusType>(
(state) => state.app.status
)
const isInitialized = useSelector<AppRootStateType, boolean>(
(state) => state.app.isInitialized
)
const isLoggedIn = useSelector<AppRootStateType, boolean>(
(state) => state.auth.isLoggedIn
)
const dispatch = useDispatch<any>()
useEffect(() => {
@@ -39,32 +45,55 @@ function App({demo = false}: PropsType) {
}, [])
if (!isInitialized) {
return <div
style={{position: 'fixed', top: '30%', textAlign: 'center', width: '100%'}}>
<CircularProgress/>
return (
<div
style={{
position: 'fixed',
top: '30%',
textAlign: 'center',
width: '100%',
}}
>
<CircularProgress />
</div>
)
}
return (
<BrowserRouter>
<div className="App">
<ErrorSnackbar/>
<AppBar position="static">
<div className='App'>
<ErrorSnackbar />
<AppBar position='static'>
<Toolbar>
<IconButton edge="start" color="inherit" aria-label="menu">
<Menu/>
<IconButton
edge='start'
color='inherit'
aria-label='menu'
>
<Menu />
</IconButton>
<Typography variant="h6">
News
</Typography>
{isLoggedIn && <Button color="inherit" onClick={logoutHandler}>Log out</Button>}
<Typography variant='h6'>News</Typography>
{isLoggedIn && (
<Button
color='inherit'
onClick={logoutHandler}
>
Log out
</Button>
)}
</Toolbar>
{status === 'loading' && <LinearProgress/>}
{status === 'loading' && <LinearProgress />}
</AppBar>
<Container fixed>
<Routes>
<Route path={'/'} element={<TodolistsList demo={demo}/>}/>
<Route path={'/login'} element={<Login/>}/>
<Route
path={'/'}
element={<TodolistsList demo={demo} />}
/>
<Route
path={'/login'}
element={<Login />}
/>
</Routes>
</Container>
</div>

View File

@@ -1,22 +1,26 @@
import { appReducer, InitialStateType, setAppErrorAC, setAppStatusAC } from './app-reducer'
import {
appReducer,
InitialStateType,
setAppErrorAC,
setAppStatusAC,
} from './app-reducer'
let startState: InitialStateType;
let startState: InitialStateType
beforeEach(() => {
startState = {
error: null,
status: 'idle',
isInitialized: false
isInitialized: false,
}
})
test('correct error message should be set', () => {
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', () => {
const endState = appReducer(startState, setAppStatusAC('loading'))
expect(endState.status).toBe('loading');
expect(endState.status).toBe('loading')
})

View File

@@ -1,23 +1,26 @@
import {Dispatch} from 'redux'
import {authAPI} from '../api/todolists-api'
import {setIsLoggedInAC} from '../features/Login/auth-reducer'
import { Dispatch } from 'redux'
import { authAPI } from '../api/todolists-api'
import { setIsLoggedInAC } from '../features/Login/auth-reducer'
const initialState: InitialStateType = {
status: 'idle',
error: null,
isInitialized: false
isInitialized: false,
}
export const appReducer = (state: InitialStateType = initialState, action: ActionsType): InitialStateType => {
export const appReducer = (
state: InitialStateType = initialState,
action: ActionsType
): InitialStateType => {
switch (action.type) {
case 'APP/SET-STATUS':
return {...state, status: action.status}
return { ...state, status: action.status }
case 'APP/SET-ERROR':
return {...state, error: action.error}
return { ...state, error: action.error }
case 'APP/SET-IS-INITIALIED':
return {...state, isInitialized: action.value}
return { ...state, isInitialized: action.value }
default:
return {...state}
return { ...state }
}
}
@@ -31,26 +34,27 @@ export type InitialStateType = {
isInitialized: boolean
}
export const setAppErrorAC = (error: string | null) => ({type: 'APP/SET-ERROR', error} as const)
export const setAppStatusAC = (status: RequestStatusType) => ({type: 'APP/SET-STATUS', status} as const)
export const setAppInitializedAC = (value: boolean) => ({type: 'APP/SET-IS-INITIALIED', value} as const)
export const setAppErrorAC = (error: string | null) =>
({ type: 'APP/SET-ERROR', error }) as const
export const setAppStatusAC = (status: RequestStatusType) =>
({ type: 'APP/SET-STATUS', status }) as const
export const setAppInitializedAC = (value: boolean) =>
({ type: 'APP/SET-IS-INITIALIED', value }) as const
export const initializeAppTC = () => (dispatch: Dispatch) => {
authAPI.me().then(res => {
authAPI.me().then((res) => {
if (res.data.resultCode === 0) {
dispatch(setIsLoggedInAC(true));
dispatch(setIsLoggedInAC(true))
} else {
}
dispatch(setAppInitializedAC(true));
dispatch(setAppInitializedAC(true))
})
}
export type SetAppErrorActionType = ReturnType<typeof setAppErrorAC>
export type SetAppStatusActionType = ReturnType<typeof setAppStatusAC>
type ActionsType =
| SetAppErrorActionType
| SetAppStatusActionType

View File

@@ -1,27 +1,36 @@
import {tasksReducer} from '../features/TodolistsList/tasks-reducer';
import {todolistsReducer} from '../features/TodolistsList/todolists-reducer';
import {applyMiddleware, combineReducers, createStore} from 'redux'
import thunkMiddleware, {ThunkAction, ThunkDispatch} from 'redux-thunk'
import {appReducer} from './app-reducer'
import {authReducer} from '../features/Login/auth-reducer'
import {configureStore, UnknownAction} from "@reduxjs/toolkit";
import { tasksReducer } from '../features/TodolistsList/tasks-reducer'
import { todolistsReducer } from '../features/TodolistsList/todolists-reducer'
import { applyMiddleware, combineReducers, createStore } from 'redux'
import thunkMiddleware, { ThunkAction, ThunkDispatch } from 'redux-thunk'
import { appReducer } from './app-reducer'
import { authReducer } from '../features/Login/auth-reducer'
import { configureStore, UnknownAction } from '@reduxjs/toolkit'
const rootReducer = combineReducers({
tasks: tasksReducer,
todolists: todolistsReducer,
app: appReducer,
auth: authReducer
auth: authReducer,
})
// ❗старая запись, с новыми версиями не работает
// const store = createStore(rootReducer, applyMiddleware(thunkMiddleware));
export const store = configureStore({reducer: rootReducer},)
export const store = configureStore({ reducer: rootReducer })
export type AppRootStateType = ReturnType<typeof rootReducer>
// ❗ UnknownAction вместо AnyAction
export type AppThunk<ReturnType = void> = ThunkAction<ReturnType, AppRootStateType, unknown, UnknownAction>
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
AppRootStateType,
unknown,
UnknownAction
>
// export type AppDispatch = typeof store.dispatch
// ❗ UnknownAction вместо AnyAction
export type AppDispatch = ThunkDispatch<AppRootStateType, unknown, UnknownAction>
export type AppDispatch = ThunkDispatch<
AppRootStateType,
unknown,
UnknownAction
>

View File

@@ -1,23 +1,25 @@
import React, { ChangeEvent, KeyboardEvent, useState } from 'react';
import { IconButton, TextField } from '@mui/material';
import { AddBox } from '@mui/icons-material';
import React, { ChangeEvent, KeyboardEvent, useState } from 'react'
import { IconButton, TextField } from '@mui/material'
import { AddBox } from '@mui/icons-material'
type AddItemFormPropsType = {
addItem: (title: string) => void
disabled?: boolean
}
export const AddItemForm = React.memo(function ({addItem, disabled = false}: AddItemFormPropsType) {
export const AddItemForm = React.memo(function ({
addItem,
disabled = false,
}: AddItemFormPropsType) {
let [title, setTitle] = useState('')
let [error, setError] = useState<string | null>(null)
const addItemHandler = () => {
if (title.trim() !== '') {
addItem(title);
setTitle('');
addItem(title)
setTitle('')
} 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>) => {
if (error !== null) {
setError(null);
setError(null)
}
if (e.charCode === 13) {
addItemHandler();
addItemHandler()
}
}
return <div>
<TextField variant="outlined"
return (
<div>
<TextField
variant='outlined'
disabled={disabled}
error={!!error}
value={title}
onChange={onChangeHandler}
onKeyPress={onKeyPressHandler}
label="Title"
label='Title'
helperText={error}
/>
<IconButton color="primary" onClick={addItemHandler} disabled={disabled}>
<AddBox/>
<IconButton
color='primary'
onClick={addItemHandler}
disabled={disabled}
>
<AddBox />
</IconButton>
</div>
)
})

View File

@@ -1,5 +1,5 @@
import React, {ChangeEvent, useState} from 'react';
import { TextField } from '@mui/material';
import React, { ChangeEvent, useState } from 'react'
import { TextField } from '@mui/material'
type EditableSpanPropsType = {
value: string
@@ -7,22 +7,29 @@ type EditableSpanPropsType = {
}
export const EditableSpan = React.memo(function (props: EditableSpanPropsType) {
let [editMode, setEditMode] = useState(false);
let [title, setTitle] = useState(props.value);
let [editMode, setEditMode] = useState(false)
let [title, setTitle] = useState(props.value)
const activateEditMode = () => {
setEditMode(true);
setTitle(props.value);
setEditMode(true)
setTitle(props.value)
}
const activateViewMode = () => {
setEditMode(false);
props.onChange(title);
setEditMode(false)
props.onChange(title)
}
const changeTitle = (e: ChangeEvent<HTMLInputElement>) => {
setTitle(e.currentTarget.value)
}
return editMode
? <TextField value={title} onChange={changeTitle} autoFocus onBlur={activateViewMode} />
: <span onDoubleClick={activateEditMode}>{props.value}</span>
});
return editMode ? (
<TextField
value={title}
onChange={changeTitle}
autoFocus
onBlur={activateViewMode}
/>
) : (
<span onDoubleClick={activateEditMode}>{props.value}</span>
)
})

View File

@@ -1,35 +1,51 @@
import React from 'react'
import {useDispatch, useSelector} from 'react-redux'
import {AppRootStateType} from '../../app/store'
import {setAppErrorAC} from '../../app/app-reducer'
import { useDispatch, useSelector } from 'react-redux'
import { AppRootStateType } from '../../app/store'
import { setAppErrorAC } from '../../app/app-reducer'
import { AlertProps, Snackbar } from '@mui/material'
import MuiAlert from '@mui/material/Alert';
import MuiAlert from '@mui/material/Alert'
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert(
props,
ref,
) {
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
});
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
function Alert(props, ref) {
return (
<MuiAlert
elevation={6}
ref={ref}
variant='filled'
{...props}
/>
)
}
)
export function ErrorSnackbar() {
const error = useSelector<AppRootStateType, string | null>(state => state.app.error);
const error = useSelector<AppRootStateType, string | null>(
(state) => state.app.error
)
const dispatch = useDispatch()
const handleClose = (event?: React.SyntheticEvent | Event, reason?: string) => {
const handleClose = (
event?: React.SyntheticEvent | Event,
reason?: string
) => {
if (reason === 'clickaway') {
return
}
dispatch(setAppErrorAC(null));
dispatch(setAppErrorAC(null))
}
const isOpen = error !== null;
const isOpen = error !== null
return (
<Snackbar open={isOpen} autoHideDuration={6000} onClose={handleClose}>
<Alert onClose={handleClose} severity="error">
<Snackbar
open={isOpen}
autoHideDuration={6000}
onClose={handleClose}
>
<Alert
onClose={handleClose}
severity='error'
>
{error}
</Alert>
</Snackbar>

View File

@@ -4,86 +4,113 @@ import { useSelector } from 'react-redux'
import { loginTC } from './auth-reducer'
import { AppRootStateType } from '../../app/store'
import { Navigate } from 'react-router-dom'
import { useAppDispatch } from '../../hooks/useAppDispatch';
import { Button, Checkbox, FormControl, FormControlLabel, FormGroup, FormLabel, Grid, TextField } from '@mui/material'
import { useAppDispatch } from '../../hooks/useAppDispatch'
import {
Button,
Checkbox,
FormControl,
FormControlLabel,
FormGroup,
FormLabel,
Grid,
TextField,
} from '@mui/material'
export const Login = () => {
const dispatch = useAppDispatch()
const isLoggedIn = useSelector<AppRootStateType, boolean>(state => state.auth.isLoggedIn);
const isLoggedIn = useSelector<AppRootStateType, boolean>(
(state) => state.auth.isLoggedIn
)
const formik = useFormik({
validate: (values) => {
if (!values.email) {
return {
email: 'Email is required'
email: 'Email is required',
}
}
if (!values.password) {
return {
password: 'Password is required'
password: 'Password is required',
}
}
},
initialValues: {
email: '',
password: '',
rememberMe: false
rememberMe: false,
},
onSubmit: values => {
dispatch(loginTC(values));
onSubmit: (values) => {
dispatch(loginTC(values))
},
})
if (isLoggedIn) {
return <Navigate to={"/"} />
return <Navigate to={'/'} />
}
return <Grid container justifyContent="center">
<Grid item xs={4}>
return (
<Grid
container
justifyContent='center'
>
<Grid
item
xs={4}
>
<form onSubmit={formik.handleSubmit}>
<FormControl>
<FormLabel>
<p>
To log in get registered <a href={'https://social-network.samuraijs.com/'}
target={'_blank'}>here</a>
</p>
<p>
or use common test account credentials:
</p>
<p> Email: free@samuraijs.com
</p>
<p>
Password: free
To log in get registered{' '}
<a
href={'https://social-network.samuraijs.com/'}
target={'_blank'}
>
here
</a>
</p>
<p>or use common test account credentials:</p>
<p> Email: free@samuraijs.com</p>
<p>Password: free</p>
</FormLabel>
<FormGroup>
<TextField
label="Email"
margin="normal"
{...formik.getFieldProps("email")}
label='Email'
margin='normal'
{...formik.getFieldProps('email')}
/>
{formik.errors.email ? <div>{formik.errors.email}</div> : null}
<TextField
type="password"
label="Password"
margin="normal"
{...formik.getFieldProps("password")}
type='password'
label='Password'
margin='normal'
{...formik.getFieldProps('password')}
/>
{formik.errors.password ? <div>{formik.errors.password}</div> : null}
{formik.errors.password ? (
<div>{formik.errors.password}</div>
) : null}
<FormControlLabel
label={'Remember me'}
control={<Checkbox
{...formik.getFieldProps("rememberMe")}
control={
<Checkbox
{...formik.getFieldProps('rememberMe')}
checked={formik.values.rememberMe}
/>}
/>
<Button type={'submit'} variant={'contained'} color={'primary'}>Login</Button>
}
/>
<Button
type={'submit'}
variant={'contained'}
color={'primary'}
>
Login
</Button>
</FormGroup>
</FormControl>
</form>
</Grid>
</Grid>
)
}

View File

@@ -1,16 +1,26 @@
import {Dispatch} from 'redux'
import {SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../../app/app-reducer'
import {authAPI, LoginParamsType} from '../../api/todolists-api'
import {handleServerAppError, handleServerNetworkError} from '../../utils/error-utils'
import { Dispatch } from 'redux'
import {
SetAppErrorActionType,
setAppStatusAC,
SetAppStatusActionType,
} from '../../app/app-reducer'
import { authAPI, LoginParamsType } from '../../api/todolists-api'
import {
handleServerAppError,
handleServerNetworkError,
} from '../../utils/error-utils'
const initialState: InitialStateType = {
isLoggedIn: false
isLoggedIn: false,
}
export const authReducer = (state: InitialStateType = initialState, action: ActionsType): InitialStateType => {
export const authReducer = (
state: InitialStateType = initialState,
action: ActionsType
): InitialStateType => {
switch (action.type) {
case 'login/SET-IS-LOGGED-IN':
return {...state, isLoggedIn: action.value}
return { ...state, isLoggedIn: action.value }
default:
return state
}
@@ -19,14 +29,20 @@ export const authReducer = (state: InitialStateType = initialState, action: Acti
// actions
export const setIsLoggedInAC = (value: boolean) =>
({type: 'login/SET-IS-LOGGED-IN', value} as const)
({ type: 'login/SET-IS-LOGGED-IN', value }) as const
// thunks
export const loginTC = (data: LoginParamsType) => (dispatch: Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>) => {
export const loginTC =
(data: LoginParamsType) =>
(
dispatch: Dispatch<
ActionsType | SetAppStatusActionType | SetAppErrorActionType
>
) => {
dispatch(setAppStatusAC('loading'))
authAPI.login(data)
.then(res => {
authAPI
.login(data)
.then((res) => {
if (res.data.resultCode === 0) {
dispatch(setIsLoggedInAC(true))
dispatch(setAppStatusAC('succeeded'))
@@ -37,11 +53,18 @@ export const loginTC = (data: LoginParamsType) => (dispatch: Dispatch<ActionsTyp
.catch((error) => {
handleServerNetworkError(error, dispatch)
})
}
export const logoutTC = () => (dispatch: Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>) => {
}
export const logoutTC =
() =>
(
dispatch: Dispatch<
ActionsType | SetAppStatusActionType | SetAppErrorActionType
>
) => {
dispatch(setAppStatusAC('loading'))
authAPI.logout()
.then(res => {
authAPI
.logout()
.then((res) => {
if (res.data.resultCode === 0) {
dispatch(setIsLoggedInAC(false))
dispatch(setAppStatusAC('succeeded'))
@@ -52,7 +75,7 @@ export const logoutTC = () => (dispatch: Dispatch<ActionsType | SetAppStatusActi
.catch((error) => {
handleServerNetworkError(error, dispatch)
})
}
}
// types
@@ -61,4 +84,6 @@ type InitialStateType = {
isLoggedIn: boolean
}
type ThunkDispatch = Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>
type ThunkDispatch = Dispatch<
ActionsType | SetAppStatusActionType | SetAppErrorActionType
>

View File

@@ -7,32 +7,61 @@ import { TaskStatuses, TaskType } from '../../../../api/todolists-api'
type TaskPropsType = {
task: TaskType
todolistId: string
changeTaskStatus: (id: string, status: TaskStatuses, todolistId: string) => void
changeTaskTitle: (taskId: string, newTitle: string, todolistId: string) => void
changeTaskStatus: (
id: string,
status: TaskStatuses,
todolistId: string
) => void
changeTaskTitle: (
taskId: string,
newTitle: string,
todolistId: string
) => void
removeTask: (taskId: string, todolistId: string) => void
}
export const Task = React.memo((props: TaskPropsType) => {
const onClickHandler = useCallback(() => props.removeTask(props.task.id, props.todolistId), [props.task.id, props.todolistId]);
const onClickHandler = useCallback(
() => props.removeTask(props.task.id, props.todolistId),
[props.task.id, props.todolistId]
)
const onChangeHandler = useCallback((e: ChangeEvent<HTMLInputElement>) => {
const onChangeHandler = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
let newIsDoneValue = e.currentTarget.checked
props.changeTaskStatus(props.task.id, newIsDoneValue ? TaskStatuses.Completed : TaskStatuses.New, props.todolistId)
}, [props.task.id, props.todolistId]);
props.changeTaskStatus(
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.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
checked={props.task.status === TaskStatuses.Completed}
color="primary"
color='primary'
onChange={onChangeHandler}
/>
<EditableSpan value={props.task.title} onChange={onTitleChangeHandler}/>
<EditableSpan
value={props.task.title}
onChange={onTitleChangeHandler}
/>
<IconButton onClick={onClickHandler}>
<Delete/>
<Delete />
</IconButton>
</div>
)
})

View File

@@ -5,7 +5,7 @@ import { Task } from './Task/Task'
import { TaskStatuses, TaskType } from '../../../api/todolists-api'
import { FilterValuesType, TodolistDomainType } from '../todolists-reducer'
import { fetchTasksTC } from '../tasks-reducer'
import { useAppDispatch } from '../../../hooks/useAppDispatch';
import { useAppDispatch } from '../../../hooks/useAppDispatch'
import { Button, IconButton } from '@mui/material'
import { Delete } from '@mui/icons-material'
@@ -14,16 +14,26 @@ type PropsType = {
tasks: Array<TaskType>
changeFilter: (value: FilterValuesType, todolistId: string) => void
addTask: (title: string, todolistId: string) => void
changeTaskStatus: (id: string, status: TaskStatuses, todolistId: string) => void
changeTaskTitle: (taskId: string, newTitle: string, todolistId: string) => void
changeTaskStatus: (
id: string,
status: TaskStatuses,
todolistId: string
) => void
changeTaskTitle: (
taskId: string,
newTitle: string,
todolistId: string
) => void
removeTask: (taskId: string, todolistId: string) => void
removeTodolist: (id: string) => void
changeTodolistTitle: (id: string, newTitle: string) => void
demo?: boolean
}
export const Todolist = React.memo(function ({demo = false, ...props}: PropsType) {
export const Todolist = React.memo(function ({
demo = false,
...props
}: PropsType) {
const dispatch = useAppDispatch()
useEffect(() => {
@@ -34,63 +44,100 @@ export const Todolist = React.memo(function ({demo = false, ...props}: PropsType
dispatch(thunk)
}, [])
const addTask = useCallback((title: string) => {
const addTask = useCallback(
(title: string) => {
props.addTask(title, props.todolist.id)
}, [props.addTask, props.todolist.id])
},
[props.addTask, props.todolist.id]
)
const removeTodolist = () => {
props.removeTodolist(props.todolist.id)
}
const changeTodolistTitle = useCallback((title: string) => {
const changeTodolistTitle = useCallback(
(title: string) => {
props.changeTodolistTitle(props.todolist.id, title)
}, [props.todolist.id, props.changeTodolistTitle])
const onAllClickHandler = useCallback(() => props.changeFilter('all', props.todolist.id), [props.todolist.id, props.changeFilter])
const onActiveClickHandler = useCallback(() => props.changeFilter('active', props.todolist.id), [props.todolist.id, props.changeFilter])
const onCompletedClickHandler = useCallback(() => props.changeFilter('completed', props.todolist.id), [props.todolist.id, props.changeFilter])
},
[props.todolist.id, props.changeTodolistTitle]
)
const onAllClickHandler = useCallback(
() => props.changeFilter('all', props.todolist.id),
[props.todolist.id, props.changeFilter]
)
const onActiveClickHandler = useCallback(
() => props.changeFilter('active', props.todolist.id),
[props.todolist.id, props.changeFilter]
)
const onCompletedClickHandler = useCallback(
() => props.changeFilter('completed', props.todolist.id),
[props.todolist.id, props.changeFilter]
)
let tasksForTodolist = props.tasks
if (props.todolist.filter === 'active') {
tasksForTodolist = props.tasks.filter(t => t.status === TaskStatuses.New)
tasksForTodolist = props.tasks.filter((t) => t.status === TaskStatuses.New)
}
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>
<h3><EditableSpan value={props.todolist.title} onChange={changeTodolistTitle}/>
<IconButton onClick={removeTodolist} disabled={props.todolist.entityStatus === 'loading'}>
<Delete/>
return (
<div>
<h3>
<EditableSpan
value={props.todolist.title}
onChange={changeTodolistTitle}
/>
<IconButton
onClick={removeTodolist}
disabled={props.todolist.entityStatus === 'loading'}
>
<Delete />
</IconButton>
</h3>
<AddItemForm addItem={addTask} disabled={props.todolist.entityStatus === 'loading'}/>
<AddItemForm
addItem={addTask}
disabled={props.todolist.entityStatus === 'loading'}
/>
<div>
{
tasksForTodolist.map(t => <Task key={t.id} task={t} todolistId={props.todolist.id}
{tasksForTodolist.map((t) => (
<Task
key={t.id}
task={t}
todolistId={props.todolist.id}
removeTask={props.removeTask}
changeTaskTitle={props.changeTaskTitle}
changeTaskStatus={props.changeTaskStatus}
/>)
}
/>
))}
</div>
<div style={{paddingTop: '10px'}}>
<Button variant={props.todolist.filter === 'all' ? 'outlined' : 'text'}
<div style={{ paddingTop: '10px' }}>
<Button
variant={props.todolist.filter === 'all' ? 'outlined' : 'text'}
onClick={onAllClickHandler}
color={'inherit'}
>All
>
All
</Button>
<Button variant={props.todolist.filter === 'active' ? 'outlined' : 'text'}
<Button
variant={props.todolist.filter === 'active' ? 'outlined' : 'text'}
onClick={onActiveClickHandler}
color={'primary'}>Active
color={'primary'}
>
Active
</Button>
<Button variant={props.todolist.filter === 'completed' ? 'outlined' : 'text'}
<Button
variant={props.todolist.filter === 'completed' ? 'outlined' : 'text'}
onClick={onCompletedClickHandler}
color={'secondary'}>Completed
color={'secondary'}
>
Completed
</Button>
</div>
</div>
)
})

View File

@@ -8,30 +8,41 @@ import {
fetchTodolistsTC,
FilterValuesType,
removeTodolistTC,
TodolistDomainType
TodolistDomainType,
} from './todolists-reducer'
import { addTaskTC, removeTaskTC, TasksStateType, updateTaskTC } from './tasks-reducer'
import {
addTaskTC,
removeTaskTC,
TasksStateType,
updateTaskTC,
} from './tasks-reducer'
import { TaskStatuses } from '../../api/todolists-api'
import { Grid, Paper } from '@mui/material'
import { AddItemForm } from '../../components/AddItemForm/AddItemForm'
import { Todolist } from './Todolist/Todolist'
import { Navigate } from 'react-router-dom'
import { useAppDispatch } from '../../hooks/useAppDispatch';
import { useAppDispatch } from '../../hooks/useAppDispatch'
type PropsType = {
demo?: boolean
}
export const TodolistsList: React.FC<PropsType> = ({demo = false}) => {
const todolists = useSelector<AppRootStateType, Array<TodolistDomainType>>(state => state.todolists)
const tasks = useSelector<AppRootStateType, TasksStateType>(state => state.tasks)
const isLoggedIn = useSelector<AppRootStateType, boolean>(state => state.auth.isLoggedIn)
export const TodolistsList: React.FC<PropsType> = ({ demo = false }) => {
const todolists = useSelector<AppRootStateType, Array<TodolistDomainType>>(
(state) => state.todolists
)
const tasks = useSelector<AppRootStateType, TasksStateType>(
(state) => state.tasks
)
const isLoggedIn = useSelector<AppRootStateType, boolean>(
(state) => state.auth.isLoggedIn
)
const dispatch = useAppDispatch()
useEffect(() => {
if (demo || !isLoggedIn) {
return;
return
}
const thunk = fetchTodolistsTC()
dispatch(thunk)
@@ -47,17 +58,28 @@ export const TodolistsList: React.FC<PropsType> = ({demo = false}) => {
dispatch(thunk)
}, [])
const changeStatus = useCallback(function (id: string, status: TaskStatuses, todolistId: string) {
const thunk = updateTaskTC(id, {status}, todolistId)
const changeStatus = useCallback(function (
id: string,
status: TaskStatuses,
todolistId: string
) {
const thunk = updateTaskTC(id, { status }, todolistId)
dispatch(thunk)
}, [])
const changeTaskTitle = useCallback(function (id: string, newTitle: string, todolistId: string) {
const thunk = updateTaskTC(id, {title: newTitle}, todolistId)
const changeTaskTitle = useCallback(function (
id: string,
newTitle: string,
todolistId: string
) {
const thunk = updateTaskTC(id, { title: newTitle }, todolistId)
dispatch(thunk)
}, [])
const changeFilter = useCallback(function (value: FilterValuesType, todolistId: string) {
const changeFilter = useCallback(function (
value: FilterValuesType,
todolistId: string
) {
const action = changeTodolistFilterAC(todolistId, value)
dispatch(action)
}, [])
@@ -72,26 +94,39 @@ export const TodolistsList: React.FC<PropsType> = ({demo = false}) => {
dispatch(thunk)
}, [])
const addTodolist = useCallback((title: string) => {
const addTodolist = useCallback(
(title: string) => {
const thunk = addTodolistTC(title)
dispatch(thunk)
}, [dispatch])
},
[dispatch]
)
if (!isLoggedIn) {
return <Navigate to={"/login"} />
return <Navigate to={'/login'} />
}
return <>
<Grid container style={{padding: '20px'}}>
<AddItemForm addItem={addTodolist}/>
return (
<>
<Grid
container
style={{ padding: '20px' }}
>
<AddItemForm addItem={addTodolist} />
</Grid>
<Grid container spacing={3}>
{
todolists.map(tl => {
<Grid
container
spacing={3}
>
{todolists.map((tl) => {
let allTodolistTasks = tasks[tl.id]
return <Grid item key={tl.id}>
<Paper style={{padding: '10px'}}>
return (
<Grid
item
key={tl.id}
>
<Paper style={{ padding: '10px' }}>
<Todolist
todolist={tl}
tasks={allTodolistTasks}
@@ -106,8 +141,9 @@ export const TodolistsList: React.FC<PropsType> = ({demo = false}) => {
/>
</Paper>
</Grid>
})
}
)
})}
</Grid>
</>
)
}

View File

@@ -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 {TaskPriorities, TaskStatuses} from '../../api/todolists-api'
import {
addTodolistAC,
removeTodolistAC,
setTodolistsAC,
} from './todolists-reducer'
import { TaskPriorities, TaskStatuses } from '../../api/todolists-api'
let startState: TasksStateType = {};
let startState: TasksStateType = {}
beforeEach(() => {
startState = {
"todolistId1": [
{ id: "1", title: "CSS", status: TaskStatuses.New, todoListId: "todolistId1", description: '',
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
{ id: "2", title: "JS", status: TaskStatuses.Completed, todoListId: "todolistId1", description: '',
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
{ id: "3", title: "React", status: TaskStatuses.New, todoListId: "todolistId1", description: '',
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low }
todolistId1: [
{
id: '1',
title: 'CSS',
status: TaskStatuses.New,
todoListId: 'todolistId1',
description: '',
startDate: '',
deadline: '',
addedDate: '',
order: 0,
priority: TaskPriorities.Low,
},
{
id: '2',
title: 'JS',
status: TaskStatuses.Completed,
todoListId: 'todolistId1',
description: '',
startDate: '',
deadline: '',
addedDate: '',
order: 0,
priority: TaskPriorities.Low,
},
{
id: '3',
title: 'React',
status: TaskStatuses.New,
todoListId: 'todolistId1',
description: '',
startDate: '',
deadline: '',
addedDate: '',
order: 0,
priority: TaskPriorities.Low,
},
],
"todolistId2": [
{ id: "1", title: "bread", status: TaskStatuses.New, todoListId: "todolistId2", description: '',
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
{ id: "2", title: "milk", status: TaskStatuses.Completed, todoListId: "todolistId2", description: '',
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
{ id: "3", title: "tea", status: TaskStatuses.New, todoListId: "todolistId2", description: '',
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low }
]
};
});
todolistId2: [
{
id: '1',
title: 'bread',
status: TaskStatuses.New,
todoListId: 'todolistId2',
description: '',
startDate: '',
deadline: '',
addedDate: '',
order: 0,
priority: TaskPriorities.Low,
},
{
id: '2',
title: 'milk',
status: TaskStatuses.Completed,
todoListId: 'todolistId2',
description: '',
startDate: '',
deadline: '',
addedDate: '',
order: 0,
priority: TaskPriorities.Low,
},
{
id: '3',
title: 'tea',
status: TaskStatuses.New,
todoListId: 'todolistId2',
description: '',
startDate: '',
deadline: '',
addedDate: '',
order: 0,
priority: TaskPriorities.Low,
},
],
}
})
test('correct task should be deleted from correct array', () => {
const action = removeTaskAC("2", "todolistId2");
const action = removeTaskAC('2', 'todolistId2')
const endState = tasksReducer(startState, action)
expect(endState["todolistId1"].length).toBe(3);
expect(endState["todolistId2"].length).toBe(2);
expect(endState["todolistId2"].every(t => t.id != "2")).toBeTruthy();
});
expect(endState['todolistId1'].length).toBe(3)
expect(endState['todolistId2'].length).toBe(2)
expect(endState['todolistId2'].every((t) => t.id != '2')).toBeTruthy()
})
test('correct task should be added to correct array', () => {
//const action = addTaskAC("juce", "todolistId2");
const action = addTaskAC({
todoListId: "todolistId2",
title: "juce",
todoListId: 'todolistId2',
title: 'juce',
status: TaskStatuses.New,
addedDate: "",
deadline: "",
description: "",
addedDate: '',
deadline: '',
description: '',
order: 0,
priority: 0,
startDate: "",
id: "id exists"
});
startDate: '',
id: 'id exists',
})
const endState = tasksReducer(startState, action)
expect(endState["todolistId1"].length).toBe(3);
expect(endState["todolistId2"].length).toBe(4);
expect(endState["todolistId2"][0].id).toBeDefined();
expect(endState["todolistId2"][0].title).toBe("juce");
expect(endState["todolistId2"][0].status).toBe(TaskStatuses.New);
});
expect(endState['todolistId1'].length).toBe(3)
expect(endState['todolistId2'].length).toBe(4)
expect(endState['todolistId2'][0].id).toBeDefined()
expect(endState['todolistId2'][0].title).toBe('juce')
expect(endState['todolistId2'][0].status).toBe(TaskStatuses.New)
})
test('status of specified task should be changed', () => {
const action = updateTaskAC("2", {status: TaskStatuses.New}, "todolistId2");
const action = updateTaskAC('2', { status: TaskStatuses.New }, 'todolistId2')
const endState = tasksReducer(startState, action)
expect(endState["todolistId1"][1].status).toBe(TaskStatuses.Completed);
expect(endState["todolistId2"][1].status).toBe(TaskStatuses.New);
});
expect(endState['todolistId1'][1].status).toBe(TaskStatuses.Completed)
expect(endState['todolistId2'][1].status).toBe(TaskStatuses.New)
})
test('title of specified task should be changed', () => {
const action = updateTaskAC("2", {title: "yogurt"}, "todolistId2");
const action = updateTaskAC('2', { title: 'yogurt' }, 'todolistId2')
const endState = tasksReducer(startState, action)
expect(endState["todolistId1"][1].title).toBe("JS");
expect(endState["todolistId2"][1].title).toBe("yogurt");
expect(endState["todolistId2"][0].title).toBe("bread");
});
expect(endState['todolistId1'][1].title).toBe('JS')
expect(endState['todolistId2'][1].title).toBe('yogurt')
expect(endState['todolistId2'][0].title).toBe('bread')
})
test('new array should be added when new todolist is added', () => {
const action = addTodolistAC({
id: "blabla",
title: "new todolist",
id: 'blabla',
title: 'new todolist',
order: 0,
addedDate: ''
});
addedDate: '',
})
const endState = tasksReducer(startState, action)
const keys = Object.keys(endState);
const newKey = keys.find(k => k != "todolistId1" && k != "todolistId2");
const keys = Object.keys(endState)
const newKey = keys.find((k) => k != 'todolistId1' && k != 'todolistId2')
if (!newKey) {
throw Error("new key should be added")
throw Error('new key should be added')
}
expect(keys.length).toBe(3);
expect(endState[newKey]).toEqual([]);
});
expect(keys.length).toBe(3)
expect(endState[newKey]).toEqual([])
})
test('propertry with todolistId should be deleted', () => {
const action = removeTodolistAC("todolistId2");
const action = removeTodolistAC('todolistId2')
const endState = tasksReducer(startState, action)
const keys = Object.keys(endState);
const keys = Object.keys(endState)
expect(keys.length).toBe(1);
expect(endState["todolistId2"]).not.toBeDefined();
});
expect(keys.length).toBe(1)
expect(endState['todolistId2']).not.toBeDefined()
})
test('empty arrays should be added when we set todolists', () => {
const action = setTodolistsAC([
{id: "1", title: "title 1", order: 0, addedDate: ""},
{id: "2", title: "title 2", order: 0, addedDate: ""}
{ id: '1', title: 'title 1', order: 0, addedDate: '' },
{ id: '2', title: 'title 2', order: 0, addedDate: '' },
])
const endState = tasksReducer({}, action)
@@ -120,14 +190,16 @@ test('empty arrays should be added when we set todolists', () => {
expect(endState['2']).toBeDefined()
})
test('tasks should be added for todolist', () => {
const action = setTasksAC(startState["todolistId1"], "todolistId1");
const action = setTasksAC(startState['todolistId1'], 'todolistId1')
const endState = tasksReducer({
"todolistId2": [],
"todolistId1": []
}, action)
const endState = tasksReducer(
{
todolistId2: [],
todolistId1: [],
},
action
)
expect(endState["todolistId1"].length).toBe(3)
expect(endState["todolistId2"].length).toBe(0)
expect(endState['todolistId1'].length).toBe(3)
expect(endState['todolistId2'].length).toBe(0)
})

View File

@@ -1,39 +1,72 @@
import {AddTodolistActionType, RemoveTodolistActionType, SetTodolistsActionType} from './todolists-reducer'
import {TaskPriorities, TaskStatuses, TaskType, todolistsAPI, UpdateTaskModelType} from '../../api/todolists-api'
import {Dispatch} from 'redux'
import {AppRootStateType} from '../../app/store'
import {setAppErrorAC, SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../../app/app-reducer'
import {handleServerAppError, handleServerNetworkError} from '../../utils/error-utils'
import {
AddTodolistActionType,
RemoveTodolistActionType,
SetTodolistsActionType,
} from './todolists-reducer'
import {
TaskPriorities,
TaskStatuses,
TaskType,
todolistsAPI,
UpdateTaskModelType,
} from '../../api/todolists-api'
import { Dispatch } from 'redux'
import { AppRootStateType } from '../../app/store'
import {
setAppErrorAC,
SetAppErrorActionType,
setAppStatusAC,
SetAppStatusActionType,
} from '../../app/app-reducer'
import {
handleServerAppError,
handleServerNetworkError,
} from '../../utils/error-utils'
const initialState: TasksStateType = {}
export const tasksReducer = (state: TasksStateType = initialState, action: ActionsType): TasksStateType => {
export const tasksReducer = (
state: TasksStateType = initialState,
action: ActionsType
): TasksStateType => {
switch (action.type) {
case 'REMOVE-TASK':
return {...state, [action.todolistId]: state[action.todolistId].filter(t => t.id != action.taskId)}
return {
...state,
[action.todolistId]: state[action.todolistId].filter(
(t) => t.id != action.taskId
),
}
case 'ADD-TASK':
return {...state, [action.task.todoListId]: [action.task, ...state[action.task.todoListId]]}
return {
...state,
[action.task.todoListId]: [
action.task,
...state[action.task.todoListId],
],
}
case 'UPDATE-TASK':
return {
...state,
[action.todolistId]: state[action.todolistId]
.map(t => t.id === action.taskId ? {...t, ...action.model} : t)
[action.todolistId]: state[action.todolistId].map((t) =>
t.id === action.taskId ? { ...t, ...action.model } : t
),
}
case 'ADD-TODOLIST':
return {...state, [action.todolist.id]: []}
return { ...state, [action.todolist.id]: [] }
case 'REMOVE-TODOLIST':
const copyState = {...state}
const copyState = { ...state }
delete copyState[action.id]
return copyState
case 'SET-TODOLISTS': {
const copyState = {...state}
action.todolists.forEach(tl => {
const copyState = { ...state }
action.todolists.forEach((tl) => {
copyState[tl.id] = []
})
return copyState
}
case 'SET-TASKS':
return {...state, [action.todolistId]: action.tasks}
return { ...state, [action.todolistId]: action.tasks }
default:
return state
}
@@ -41,52 +74,68 @@ export const tasksReducer = (state: TasksStateType = initialState, action: Actio
// actions
export const removeTaskAC = (taskId: string, todolistId: string) =>
({type: 'REMOVE-TASK', taskId, todolistId} as const)
({ type: 'REMOVE-TASK', taskId, todolistId }) as const
export const addTaskAC = (task: TaskType) =>
({type: 'ADD-TASK', task} as const)
export const updateTaskAC = (taskId: string, model: UpdateDomainTaskModelType, todolistId: string) =>
({type: 'UPDATE-TASK', model, todolistId, taskId} as const)
({ type: 'ADD-TASK', task }) as const
export const updateTaskAC = (
taskId: string,
model: UpdateDomainTaskModelType,
todolistId: string
) => ({ type: 'UPDATE-TASK', model, todolistId, taskId }) as const
export const setTasksAC = (tasks: Array<TaskType>, todolistId: string) =>
({type: 'SET-TASKS', tasks, todolistId} as const)
({ type: 'SET-TASKS', tasks, todolistId }) as const
// thunks
export const fetchTasksTC = (todolistId: string) => (dispatch: Dispatch<ActionsType | SetAppStatusActionType>) => {
export const fetchTasksTC =
(todolistId: string) =>
(dispatch: Dispatch<ActionsType | SetAppStatusActionType>) => {
dispatch(setAppStatusAC('loading'))
todolistsAPI.getTasks(todolistId)
.then((res) => {
todolistsAPI.getTasks(todolistId).then((res) => {
const tasks = res.data.items
dispatch(setTasksAC(tasks, todolistId))
dispatch(setAppStatusAC('succeeded'))
})
}
export const removeTaskTC = (taskId: string, todolistId: string) => (dispatch: Dispatch<ActionsType>) => {
todolistsAPI.deleteTask(todolistId, taskId)
.then(res => {
}
export const removeTaskTC =
(taskId: string, todolistId: string) => (dispatch: Dispatch<ActionsType>) => {
todolistsAPI.deleteTask(todolistId, taskId).then((res) => {
const action = removeTaskAC(taskId, todolistId)
dispatch(action)
})
}
export const addTaskTC = (title: string, todolistId: string) => (dispatch: Dispatch<ActionsType | SetAppErrorActionType | SetAppStatusActionType>) => {
}
export const addTaskTC =
(title: string, todolistId: string) =>
(
dispatch: Dispatch<
ActionsType | SetAppErrorActionType | SetAppStatusActionType
>
) => {
dispatch(setAppStatusAC('loading'))
todolistsAPI.createTask(todolistId, title)
.then(res => {
todolistsAPI
.createTask(todolistId, title)
.then((res) => {
if (res.data.resultCode === 0) {
const task = res.data.data.item
const action = addTaskAC(task)
dispatch(action)
dispatch(setAppStatusAC('succeeded'))
} else {
handleServerAppError(res.data, dispatch);
handleServerAppError(res.data, dispatch)
}
})
.catch((error) => {
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) => {
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) {
//throw new Error("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,
title: task.title,
status: task.status,
...domainModel
...domainModel,
}
todolistsAPI.updateTask(todolistId, taskId, apiModel)
.then(res => {
todolistsAPI
.updateTask(todolistId, taskId, apiModel)
.then((res) => {
if (res.data.resultCode === 0) {
const action = updateTaskAC(taskId, domainModel, todolistId)
dispatch(action)
} else {
handleServerAppError(res.data, dispatch);
handleServerAppError(res.data, dispatch)
}
})
.catch((error) => {
handleServerNetworkError(error, dispatch);
handleServerNetworkError(error, dispatch)
})
}
@@ -137,4 +187,6 @@ type ActionsType =
| RemoveTodolistActionType
| SetTodolistsActionType
| ReturnType<typeof setTasksAC>
type ThunkDispatch = Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>
type ThunkDispatch = Dispatch<
ActionsType | SetAppStatusActionType | SetAppErrorActionType
>

View File

@@ -1,13 +1,17 @@
import {
addTodolistAC, changeTodolistEntityStatusAC,
addTodolistAC,
changeTodolistEntityStatusAC,
changeTodolistFilterAC,
changeTodolistTitleAC, FilterValuesType,
removeTodolistAC, setTodolistsAC, TodolistDomainType,
todolistsReducer
changeTodolistTitleAC,
FilterValuesType,
removeTodolistAC,
setTodolistsAC,
TodolistDomainType,
todolistsReducer,
} from './todolists-reducer'
import {v1} from 'uuid'
import {TodolistType} from '../../api/todolists-api'
import {RequestStatusType} from '../../app/app-reducer'
import { v1 } from 'uuid'
import { TodolistType } from '../../api/todolists-api'
import { RequestStatusType } from '../../app/app-reducer'
let todolistId1: string
let todolistId2: string
@@ -17,8 +21,22 @@ beforeEach(() => {
todolistId1 = v1()
todolistId2 = v1()
startState = [
{id: todolistId1, title: 'What to learn', filter: 'all', entityStatus: 'idle', addedDate: '', order: 0},
{id: todolistId2, title: 'What to buy', filter: 'all', entityStatus: 'idle', addedDate: '', order: 0}
{
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',
id: 'any id',
addedDate: '',
order: 0
order: 0,
}
const endState = todolistsReducer(startState, addTodolistAC(todolist))
expect(endState.length).toBe(3)
@@ -67,7 +84,6 @@ test('correct filter of todolist should be changed', () => {
expect(endState[1].filter).toBe(newFilter)
})
test('todolists should be added', () => {
const action = setTodolistsAC(startState)
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[1].entityStatus).toBe(newStatus)
})

View File

@@ -1,59 +1,93 @@
import {todolistsAPI, TodolistType} from '../../api/todolists-api'
import {Dispatch} from 'redux'
import {RequestStatusType, SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../../app/app-reducer'
import {handleServerNetworkError} from '../../utils/error-utils'
import { AppThunk } from '../../app/store';
import { todolistsAPI, TodolistType } from '../../api/todolists-api'
import { Dispatch } from 'redux'
import {
RequestStatusType,
SetAppErrorActionType,
setAppStatusAC,
SetAppStatusActionType,
} from '../../app/app-reducer'
import { handleServerNetworkError } from '../../utils/error-utils'
import { AppThunk } from '../../app/store'
const initialState: Array<TodolistDomainType> = []
export const todolistsReducer = (state: Array<TodolistDomainType> = initialState, action: ActionsType): Array<TodolistDomainType> => {
export const todolistsReducer = (
state: Array<TodolistDomainType> = initialState,
action: ActionsType
): Array<TodolistDomainType> => {
switch (action.type) {
case 'REMOVE-TODOLIST':
return state.filter(tl => tl.id != action.id)
return state.filter((tl) => tl.id != action.id)
case 'ADD-TODOLIST':
return [{...action.todolist, filter: 'all', entityStatus: 'idle'}, ...state]
return [
{ ...action.todolist, filter: 'all', entityStatus: 'idle' },
...state,
]
case 'CHANGE-TODOLIST-TITLE':
return state.map(tl => tl.id === action.id ? {...tl, title: action.title} : tl)
return state.map((tl) =>
tl.id === action.id ? { ...tl, title: action.title } : tl
)
case 'CHANGE-TODOLIST-FILTER':
return state.map(tl => tl.id === action.id ? {...tl, filter: action.filter} : tl)
return state.map((tl) =>
tl.id === action.id ? { ...tl, filter: action.filter } : tl
)
case 'CHANGE-TODOLIST-ENTITY-STATUS':
return state.map(tl => tl.id === action.id ? {...tl, entityStatus: action.status} : tl)
return state.map((tl) =>
tl.id === action.id ? { ...tl, entityStatus: action.status } : tl
)
case 'SET-TODOLISTS':
return action.todolists.map(tl => ({...tl, filter: 'all', entityStatus: 'idle'}))
return action.todolists.map((tl) => ({
...tl,
filter: 'all',
entityStatus: 'idle',
}))
default:
return state
}
}
// actions
export const removeTodolistAC = (id: string) => ({type: 'REMOVE-TODOLIST', id} as const)
export const addTodolistAC = (todolist: TodolistType) => ({type: 'ADD-TODOLIST', todolist} as const)
export const changeTodolistTitleAC = (id: string, title: string) => ({
export const removeTodolistAC = (id: string) =>
({ type: 'REMOVE-TODOLIST', id }) as const
export const addTodolistAC = (todolist: TodolistType) =>
({ type: 'ADD-TODOLIST', todolist }) as const
export const changeTodolistTitleAC = (id: string, title: string) =>
({
type: 'CHANGE-TODOLIST-TITLE',
id,
title
} as const)
export const changeTodolistFilterAC = (id: string, filter: FilterValuesType) => ({
title,
}) as const
export const changeTodolistFilterAC = (id: string, filter: FilterValuesType) =>
({
type: 'CHANGE-TODOLIST-FILTER',
id,
filter
} as const)
export const changeTodolistEntityStatusAC = (id: string, status: RequestStatusType) => ({
type: 'CHANGE-TODOLIST-ENTITY-STATUS', id, status } as const)
export const setTodolistsAC = (todolists: Array<TodolistType>) => ({type: 'SET-TODOLISTS', todolists} as const)
filter,
}) as const
export const changeTodolistEntityStatusAC = (
id: string,
status: RequestStatusType
) =>
({
type: 'CHANGE-TODOLIST-ENTITY-STATUS',
id,
status,
}) as const
export const setTodolistsAC = (todolists: Array<TodolistType>) =>
({ type: 'SET-TODOLISTS', todolists }) as const
// thunks
export const fetchTodolistsTC = (): AppThunk => {
return (dispatch) => {
dispatch(setAppStatusAC('loading'))
todolistsAPI.getTodolists()
todolistsAPI
.getTodolists()
.then((res) => {
dispatch(setTodolistsAC(res.data))
dispatch(setAppStatusAC('succeeded'))
})
.catch(error => {
handleServerNetworkError(error, dispatch);
.catch((error) => {
handleServerNetworkError(error, dispatch)
})
}
}
@@ -63,8 +97,7 @@ export const removeTodolistTC = (todolistId: string) => {
dispatch(setAppStatusAC('loading'))
//изменим статус конкретного тудулиста, чтобы он мог задизеблить что надо
dispatch(changeTodolistEntityStatusAC(todolistId, 'loading'))
todolistsAPI.deleteTodolist(todolistId)
.then((res) => {
todolistsAPI.deleteTodolist(todolistId).then((res) => {
dispatch(removeTodolistAC(todolistId))
//скажем глобально приложению, что асинхронная операция завершена
dispatch(setAppStatusAC('succeeded'))
@@ -74,8 +107,7 @@ export const removeTodolistTC = (todolistId: string) => {
export const addTodolistTC = (title: string) => {
return (dispatch: ThunkDispatch) => {
dispatch(setAppStatusAC('loading'))
todolistsAPI.createTodolist(title)
.then((res) => {
todolistsAPI.createTodolist(title).then((res) => {
dispatch(addTodolistAC(res.data.data.item))
dispatch(setAppStatusAC('succeeded'))
})
@@ -83,17 +115,16 @@ export const addTodolistTC = (title: string) => {
}
export const changeTodolistTitleTC = (id: string, title: string) => {
return (dispatch: Dispatch<ActionsType>) => {
todolistsAPI.updateTodolist(id, title)
.then((res) => {
todolistsAPI.updateTodolist(id, title).then((res) => {
dispatch(changeTodolistTitleAC(id, title))
})
}
}
// types
export type AddTodolistActionType = ReturnType<typeof addTodolistAC>;
export type RemoveTodolistActionType = ReturnType<typeof removeTodolistAC>;
export type SetTodolistsActionType = ReturnType<typeof setTodolistsAC>;
export type AddTodolistActionType = ReturnType<typeof addTodolistAC>
export type RemoveTodolistActionType = ReturnType<typeof removeTodolistAC>
export type SetTodolistsActionType = ReturnType<typeof setTodolistsAC>
type ActionsType =
| RemoveTodolistActionType
| AddTodolistActionType
@@ -101,9 +132,11 @@ type ActionsType =
| ReturnType<typeof changeTodolistFilterAC>
| SetTodolistsActionType
| ReturnType<typeof changeTodolistEntityStatusAC>
export type FilterValuesType = 'all' | 'active' | 'completed';
export type FilterValuesType = 'all' | 'active' | 'completed'
export type TodolistDomainType = TodolistType & {
filter: FilterValuesType
entityStatus: RequestStatusType
}
type ThunkDispatch = Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>
type ThunkDispatch = Dispatch<
ActionsType | SetAppStatusActionType | SetAppErrorActionType
>

View File

@@ -1,27 +1,31 @@
import {addTodolistAC, TodolistDomainType, todolistsReducer} from './todolists-reducer'
import {tasksReducer, TasksStateType} from './tasks-reducer'
import {TodolistType} from '../../api/todolists-api'
import {
addTodolistAC,
TodolistDomainType,
todolistsReducer,
} from './todolists-reducer'
import { tasksReducer, TasksStateType } from './tasks-reducer'
import { TodolistType } from '../../api/todolists-api'
test('ids should be equals', () => {
const startTasksState: TasksStateType = {};
const startTodolistsState: Array<TodolistDomainType> = [];
const startTasksState: TasksStateType = {}
const startTodolistsState: Array<TodolistDomainType> = []
let todolist: TodolistType = {
title: 'new todolist',
id: 'any id',
addedDate: '',
order: 0
order: 0,
}
const action = addTodolistAC(todolist);
const action = addTodolistAC(todolist)
const endTasksState = tasksReducer(startTasksState, action)
const endTodolistsState = todolistsReducer(startTodolistsState, action)
const keys = Object.keys(endTasksState);
const idFromTasks = keys[0];
const idFromTodolists = endTodolistsState[0].id;
const keys = Object.keys(endTasksState)
const idFromTasks = keys[0]
const idFromTodolists = endTodolistsState[0].id
expect(idFromTasks).toBe(action.todolist.id);
expect(idFromTodolists).toBe(action.todolist.id);
});
expect(idFromTasks).toBe(action.todolist.id)
expect(idFromTodolists).toBe(action.todolist.id)
})

View File

@@ -1,6 +1,5 @@
import { useDispatch } from 'react-redux';
import { AppDispatch } from '../app/store';
import { useDispatch } from 'react-redux'
import { AppDispatch } from '../app/store'
// export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppDispatch = () => useDispatch<AppDispatch>()

View File

@@ -1,15 +1,13 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './app/App';
import { store } from './app/store';
import { Provider } from 'react-redux';
import React from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './app/App'
import { store } from './app/store'
import { Provider } from 'react-redux'
const root = createRoot(document.getElementById('root') as HTMLElement);
const root = createRoot(document.getElementById('root') as HTMLElement)
root.render(
<Provider store={store}>
<App/>
<App />
</Provider>
);
)

View File

@@ -18,33 +18,30 @@ const isLocalhost = Boolean(
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
)
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
onSuccess?: (registration: ServiceWorkerRegistration) => void
onUpdate?: (registration: ServiceWorkerRegistration) => void
}
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL,
window.location.href
);
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
return
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
checkValidServiceWorker(swUrl, config)
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
@@ -52,24 +49,24 @@ export function register(config?: Config) {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
)
})
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
registerValidSW(swUrl, config)
}
});
})
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
const installingWorker = registration.installing
if (installingWorker == null) {
return;
return
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
@@ -80,70 +77,70 @@ function registerValidSW(swUrl: string, config?: Config) {
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
)
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
config.onUpdate(registration)
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
console.log('Content is cached for offline use.')
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
config.onSuccess(registration)
}
}
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
.catch((error) => {
console.error('Error during service worker registration:', error)
})
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }
headers: { 'Service-Worker': 'script' },
})
.then(response => {
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
const contentType = response.headers.get('content-type')
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
window.location.reload()
})
})
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
registerValidSW(swUrl, config)
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
)
})
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
.then((registration) => {
registration.unregister()
})
.catch((error) => {
console.error(error.message)
})
.catch(error => {
console.error(error.message);
});
}
}

View File

@@ -2,4 +2,4 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import '@testing-library/jest-dom'

View File

@@ -1,8 +1,16 @@
import {setAppErrorAC, SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../app/app-reducer'
import {ResponseType} from '../api/todolists-api'
import {Dispatch} from 'redux'
import {
setAppErrorAC,
SetAppErrorActionType,
setAppStatusAC,
SetAppStatusActionType,
} from '../app/app-reducer'
import { ResponseType } from '../api/todolists-api'
import { Dispatch } from 'redux'
export const handleServerAppError = <D>(data: ResponseType<D>, dispatch: Dispatch<SetAppErrorActionType | SetAppStatusActionType>) => {
export const handleServerAppError = <D>(
data: ResponseType<D>,
dispatch: Dispatch<SetAppErrorActionType | SetAppStatusActionType>
) => {
if (data.messages.length) {
dispatch(setAppErrorAC(data.messages[0]))
} else {
@@ -11,7 +19,10 @@ export const handleServerAppError = <D>(data: ResponseType<D>, dispatch: Dispatc
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(setAppStatusAC('failed'))
}

View File

@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@@ -19,7 +15,5 @@
"noEmit": true,
"jsx": "react"
},
"include": [
"src"
]
"include": ["src"]
}

View File

@@ -7973,6 +7973,11 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
prettier@3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105"
integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==
pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
@@ -8972,7 +8977,16 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -9055,7 +9069,14 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -10105,7 +10126,16 @@ workbox-window@6.6.1:
"@types/trusted-types" "^2.0.2"
workbox-core "6.6.1"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==