This commit is contained in:
2024-08-17 17:13:28 +02:00
commit 143b48c7a9
38 changed files with 11982 additions and 0 deletions

118
src/api/todolists-api.ts Normal file
View File

@@ -0,0 +1,118 @@
import axios from 'axios'
const settings = {
withCredentials: true,
headers: {
'API-KEY': '1cdd9f77-c60e-4af5-b194-659e4ebd5d41'
}
}
const instance = axios.create({
baseURL: 'https://social-network.samuraijs.com/api/1.1/',
...settings
})
// api
export const todolistsAPI = {
getTodolists() {
const promise = instance.get<TodolistType[]>('todo-lists');
return promise;
},
createTodolist(title: string) {
const promise = instance.post<ResponseType<{ item: TodolistType }>>('todo-lists', {title: title});
return promise;
},
deleteTodolist(id: string) {
const promise = instance.delete<ResponseType>(`todo-lists/${id}`);
return promise;
},
updateTodolist(id: string, title: string) {
const promise = instance.put<ResponseType>(`todo-lists/${id}`, {title: title});
return promise;
},
getTasks(todolistId: string) {
return instance.get<GetTasksResponse>(`todo-lists/${todolistId}/tasks`);
},
deleteTask(todolistId: string, taskId: string) {
return instance.delete<ResponseType>(`todo-lists/${todolistId}/tasks/${taskId}`);
},
createTask(todolistId: string, taskTitile: string) {
return instance.post<ResponseType<{ item: TaskType}>>(`todo-lists/${todolistId}/tasks`, {title: taskTitile});
},
updateTask(todolistId: string, taskId: string, model: UpdateTaskModelType) {
return instance.put<ResponseType<TaskType>>(`todo-lists/${todolistId}/tasks/${taskId}`, model);
}
}
export type LoginParamsType = {
email: string
password: string
rememberMe: boolean
captcha?: string
}
export const authAPI = {
login(data: LoginParamsType) {
const promise = instance.post<ResponseType<{userId?: number}>>('auth/login', data);
return promise;
},
logout() {
const promise = instance.delete<ResponseType<{userId?: number}>>('auth/login');
return promise;
},
me() {
const promise = instance.get<ResponseType<{id: number; email: string; login: string}>>('auth/me');
return promise
}
}
// types
export type TodolistType = {
id: string
title: string
addedDate: string
order: number
}
export type ResponseType<D = {}> = {
resultCode: number
messages: Array<string>
data: D
}
export enum TaskStatuses {
New = 0,
InProgress = 1,
Completed = 2,
Draft = 3
}
export enum TaskPriorities {
Low = 0,
Middle = 1,
Hi = 2,
Urgently = 3,
Later = 4
}
export type TaskType = {
description: string
title: string
status: TaskStatuses
priority: TaskPriorities
startDate: string
deadline: string
id: string
todoListId: string
order: number
addedDate: string
}
export type UpdateTaskModelType = {
title: string
description: string
status: TaskStatuses
priority: TaskPriorities
startDate: string
deadline: string
}
type GetTasksResponse = {
error: string | null
totalCount: number
items: TaskType[]
}

26
src/app/App.css Normal file
View File

@@ -0,0 +1,26 @@
/*
.App {
padding: 30px;
display: flex;
}
.App > div {
margin-right: 50px;
}
.error {
border: red 1px solid;
}
.error-message {
color: red;
}
.active-filter {
background-color: aquamarine;
}
.is-done {
opacity: 0.5;
}
*/

75
src/app/App.tsx Normal file
View File

@@ -0,0 +1,75 @@
import React, { useCallback, useEffect } from 'react'
import './App.css'
import { TodolistsList } from '../features/TodolistsList/TodolistsList'
import { ErrorSnackbar } from '../components/ErrorSnackbar/ErrorSnackbar'
import { useDispatch, useSelector } from 'react-redux'
import { AppRootStateType } from './store'
import { initializeAppTC, RequestStatusType } from './app-reducer'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { Login } from '../features/Login/Login'
import { logoutTC } from '../features/Login/auth-reducer'
import {
AppBar,
Button,
CircularProgress,
Container,
IconButton,
LinearProgress,
Toolbar,
Typography
} from '@mui/material';
import { Menu } from '@mui/icons-material'
type PropsType = {
demo?: boolean
}
function App({demo = false}: PropsType) {
const status = useSelector<AppRootStateType, RequestStatusType>((state) => state.app.status)
const isInitialized = useSelector<AppRootStateType, boolean>((state) => state.app.isInitialized)
const isLoggedIn = useSelector<AppRootStateType, boolean>(state => state.auth.isLoggedIn)
const dispatch = useDispatch<any>()
useEffect(() => {
dispatch(initializeAppTC())
}, [])
const logoutHandler = useCallback(() => {
dispatch(logoutTC())
}, [])
if (!isInitialized) {
return <div
style={{position: 'fixed', top: '30%', textAlign: 'center', width: '100%'}}>
<CircularProgress/>
</div>
}
return (
<BrowserRouter>
<div className="App">
<ErrorSnackbar/>
<AppBar position="static">
<Toolbar>
<IconButton edge="start" color="inherit" aria-label="menu">
<Menu/>
</IconButton>
<Typography variant="h6">
News
</Typography>
{isLoggedIn && <Button color="inherit" onClick={logoutHandler}>Log out</Button>}
</Toolbar>
{status === 'loading' && <LinearProgress/>}
</AppBar>
<Container fixed>
<Routes>
<Route path={'/'} element={<TodolistsList demo={demo}/>}/>
<Route path={'/login'} element={<Login/>}/>
</Routes>
</Container>
</div>
</BrowserRouter>
)
}
export default App

View File

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

57
src/app/app-reducer.ts Normal file
View File

@@ -0,0 +1,57 @@
import {Dispatch} from 'redux'
import {authAPI} from '../api/todolists-api'
import {setIsLoggedInAC} from '../features/Login/auth-reducer'
const initialState: InitialStateType = {
status: 'idle',
error: null,
isInitialized: false
}
export const appReducer = (state: InitialStateType = initialState, action: ActionsType): InitialStateType => {
switch (action.type) {
case 'APP/SET-STATUS':
return {...state, status: action.status}
case 'APP/SET-ERROR':
return {...state, error: action.error}
case 'APP/SET-IS-INITIALIED':
return {...state, isInitialized: action.value}
default:
return {...state}
}
}
export type RequestStatusType = 'idle' | 'loading' | 'succeeded' | 'failed'
export type InitialStateType = {
// происходит ли сейчас взаимодействие с сервером
status: RequestStatusType
// если ошибка какая-то глобальная произойдёт - мы запишем текст ошибки сюда
error: string | null
// true когда приложение проинициализировалось (проверили юзера, настройки получили и т.д.)
isInitialized: boolean
}
export const setAppErrorAC = (error: string | null) => ({type: 'APP/SET-ERROR', error} as const)
export const setAppStatusAC = (status: RequestStatusType) => ({type: 'APP/SET-STATUS', status} as const)
export const setAppInitializedAC = (value: boolean) => ({type: 'APP/SET-IS-INITIALIED', value} as const)
export const initializeAppTC = () => (dispatch: Dispatch) => {
authAPI.me().then(res => {
if (res.data.resultCode === 0) {
dispatch(setIsLoggedInAC(true));
} else {
}
dispatch(setAppInitializedAC(true));
})
}
export type SetAppErrorActionType = ReturnType<typeof setAppErrorAC>
export type SetAppStatusActionType = ReturnType<typeof setAppStatusAC>
type ActionsType =
| SetAppErrorActionType
| SetAppStatusActionType
| ReturnType<typeof setAppInitializedAC>

27
src/app/store.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,64 @@
import {Dispatch} from 'redux'
import {SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../../app/app-reducer'
import {authAPI, LoginParamsType} from '../../api/todolists-api'
import {handleServerAppError, handleServerNetworkError} from '../../utils/error-utils'
const initialState: InitialStateType = {
isLoggedIn: false
}
export const authReducer = (state: InitialStateType = initialState, action: ActionsType): InitialStateType => {
switch (action.type) {
case 'login/SET-IS-LOGGED-IN':
return {...state, isLoggedIn: action.value}
default:
return state
}
}
// actions
export const setIsLoggedInAC = (value: boolean) =>
({type: 'login/SET-IS-LOGGED-IN', value} as const)
// thunks
export const loginTC = (data: LoginParamsType) => (dispatch: Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>) => {
dispatch(setAppStatusAC('loading'))
authAPI.login(data)
.then(res => {
if (res.data.resultCode === 0) {
dispatch(setIsLoggedInAC(true))
dispatch(setAppStatusAC('succeeded'))
} else {
handleServerAppError(res.data, dispatch)
}
})
.catch((error) => {
handleServerNetworkError(error, dispatch)
})
}
export const logoutTC = () => (dispatch: Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>) => {
dispatch(setAppStatusAC('loading'))
authAPI.logout()
.then(res => {
if (res.data.resultCode === 0) {
dispatch(setIsLoggedInAC(false))
dispatch(setAppStatusAC('succeeded'))
} else {
handleServerAppError(res.data, dispatch)
}
})
.catch((error) => {
handleServerNetworkError(error, dispatch)
})
}
// types
type ActionsType = ReturnType<typeof setIsLoggedInAC>
type InitialStateType = {
isLoggedIn: boolean
}
type ThunkDispatch = Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>

View File

@@ -0,0 +1,38 @@
import React, { ChangeEvent, useCallback } from 'react'
import { Checkbox, IconButton } from '@mui/material'
import { EditableSpan } from '../../../../components/EditableSpan/EditableSpan'
import { Delete } from '@mui/icons-material'
import { TaskStatuses, TaskType } from '../../../../api/todolists-api'
type TaskPropsType = {
task: TaskType
todolistId: string
changeTaskStatus: (id: string, status: TaskStatuses, todolistId: string) => void
changeTaskTitle: (taskId: string, newTitle: string, todolistId: string) => void
removeTask: (taskId: string, todolistId: string) => void
}
export const Task = React.memo((props: TaskPropsType) => {
const onClickHandler = useCallback(() => props.removeTask(props.task.id, props.todolistId), [props.task.id, props.todolistId]);
const onChangeHandler = useCallback((e: ChangeEvent<HTMLInputElement>) => {
let newIsDoneValue = e.currentTarget.checked
props.changeTaskStatus(props.task.id, newIsDoneValue ? TaskStatuses.Completed : TaskStatuses.New, props.todolistId)
}, [props.task.id, props.todolistId]);
const onTitleChangeHandler = useCallback((newValue: string) => {
props.changeTaskTitle(props.task.id, newValue, props.todolistId)
}, [props.task.id, props.todolistId]);
return <div key={props.task.id} className={props.task.status === TaskStatuses.Completed ? 'is-done' : ''}>
<Checkbox
checked={props.task.status === TaskStatuses.Completed}
color="primary"
onChange={onChangeHandler}
/>
<EditableSpan value={props.task.title} onChange={onTitleChangeHandler}/>
<IconButton onClick={onClickHandler}>
<Delete/>
</IconButton>
</div>
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,89 @@
import {
addTodolistAC, changeTodolistEntityStatusAC,
changeTodolistFilterAC,
changeTodolistTitleAC, FilterValuesType,
removeTodolistAC, setTodolistsAC, TodolistDomainType,
todolistsReducer
} from './todolists-reducer'
import {v1} from 'uuid'
import {TodolistType} from '../../api/todolists-api'
import {RequestStatusType} from '../../app/app-reducer'
let todolistId1: string
let todolistId2: string
let startState: Array<TodolistDomainType> = []
beforeEach(() => {
todolistId1 = v1()
todolistId2 = v1()
startState = [
{id: todolistId1, title: 'What to learn', filter: 'all', entityStatus: 'idle', addedDate: '', order: 0},
{id: todolistId2, title: 'What to buy', filter: 'all', entityStatus: 'idle', addedDate: '', order: 0}
]
})
test('correct todolist should be removed', () => {
const endState = todolistsReducer(startState, removeTodolistAC(todolistId1))
expect(endState.length).toBe(1)
expect(endState[0].id).toBe(todolistId2)
})
test('correct todolist should be added', () => {
let todolist: TodolistType = {
title: 'New Todolist',
id: 'any id',
addedDate: '',
order: 0
}
const endState = todolistsReducer(startState, addTodolistAC(todolist))
expect(endState.length).toBe(3)
expect(endState[0].title).toBe(todolist.title)
expect(endState[0].filter).toBe('all')
})
test('correct todolist should change its name', () => {
let newTodolistTitle = 'New Todolist'
const action = changeTodolistTitleAC(todolistId2, newTodolistTitle)
const endState = todolistsReducer(startState, action)
expect(endState[0].title).toBe('What to learn')
expect(endState[1].title).toBe(newTodolistTitle)
})
test('correct filter of todolist should be changed', () => {
let newFilter: FilterValuesType = 'completed'
const action = changeTodolistFilterAC(todolistId2, newFilter)
const endState = todolistsReducer(startState, action)
expect(endState[0].filter).toBe('all')
expect(endState[1].filter).toBe(newFilter)
})
test('todolists should be added', () => {
const action = setTodolistsAC(startState)
const endState = todolistsReducer([], action)
expect(endState.length).toBe(2)
})
test('correct entity status of todolist should be changed', () => {
let newStatus: RequestStatusType = 'loading'
const action = changeTodolistEntityStatusAC(todolistId2, newStatus)
const endState = todolistsReducer(startState, action)
expect(endState[0].entityStatus).toBe('idle')
expect(endState[1].entityStatus).toBe(newStatus)
})

View File

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

View File

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

View File

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

13
src/index.css Normal file
View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

15
src/index.tsx Normal file
View File

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

7
src/logo.svg Normal file
View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

1
src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

149
src/serviceWorker.ts Normal file
View File

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

5
src/setupTests.ts Normal file
View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

17
src/utils/error-utils.ts Normal file
View File

@@ -0,0 +1,17 @@
import {setAppErrorAC, SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../app/app-reducer'
import {ResponseType} from '../api/todolists-api'
import {Dispatch} from 'redux'
export const handleServerAppError = <D>(data: ResponseType<D>, dispatch: Dispatch<SetAppErrorActionType | SetAppStatusActionType>) => {
if (data.messages.length) {
dispatch(setAppErrorAC(data.messages[0]))
} else {
dispatch(setAppErrorAC('Some error occurred'))
}
dispatch(setAppStatusAC('failed'))
}
export const handleServerNetworkError = (error: { message: string }, dispatch: Dispatch<SetAppErrorActionType | SetAppStatusActionType>) => {
dispatch(setAppErrorAC(error.message ? error.message : 'Some error occurred'))
dispatch(setAppStatusAC('failed'))
}