From 252f04e0ae8b26536840f2227df9671c8829c4d4 Mon Sep 17 00:00:00 2001 From: Bohdan Ivankov Date: Mon, 23 Sep 2024 18:48:48 +0100 Subject: [PATCH] add task solution --- src/App.tsx | 82 ++++-- src/app/store.ts | 5 +- src/components/TodoFilter/TodoFilter.tsx | 42 ++- src/components/TodoList/TodoList.tsx | 334 ++++++++--------------- src/components/TodoModal/TodoModal.tsx | 83 ++++-- src/features/currentTodo.ts | 13 +- src/features/filter.ts | 16 +- src/features/todos.ts | 10 +- src/tools/constants.ts | 5 + src/tools/prepareTodos.tsx | 32 +++ 10 files changed, 346 insertions(+), 276 deletions(-) create mode 100644 src/tools/constants.ts create mode 100644 src/tools/prepareTodos.tsx diff --git a/src/App.tsx b/src/App.tsx index e10753461..783c50545 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,76 @@ import 'bulma/css/bulma.css'; import '@fortawesome/fontawesome-free/css/all.css'; import { Loader, TodoFilter, TodoList, TodoModal } from './components'; +import { useEffect, useState } from 'react'; +import { getTodos } from './api'; +import { prepareTodos } from './tools/prepareTodos'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from './app/store'; +import { todosActions } from './features/todos'; +import { filterActions } from './features/filter'; +import { currentTodoActions } from './features/currentTodo'; -export const App = () => ( - <> -
-
-
-

Todos:

+export const App = () => { + const dispatch = useDispatch(); + const todos = useSelector((state: RootState) => state.todos); + const { query, status } = useSelector((state: RootState) => state.filter); + const selectedTodo = useSelector((state: RootState) => state.currentTodo); -
- -
+ const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + setIsLoading(true); + getTodos() + .then(readyTodos => dispatch(todosActions.setTodos(readyTodos))) + .finally(() => setIsLoading(false)); + }, [dispatch]); + + const readyTodos = prepareTodos(todos, query, status); + + return ( + <> +
+
+
+

Todos:

+ +
+ + dispatch(filterActions.setQuery(newQuery)) + } + setStatus={newStatus => + dispatch(filterActions.setStatus(newStatus)) + } + /> +
+ +
+ {isLoading && } -
- - + {!isLoading && todos.length > 0 && ( + + dispatch(currentTodoActions.setCurrentTodo(newSelectedTodo)) + } + /> + )} +
-
- - -); + {selectedTodo && ( + + dispatch(currentTodoActions.setCurrentTodo(newSelectedTodo)) + } + /> + )} + + ); +}; diff --git a/src/app/store.ts b/src/app/store.ts index 3a9b384be..d390bac6e 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -1,6 +1,9 @@ import { combineSlices, configureStore } from '@reduxjs/toolkit'; +import { currentTodoSlice } from '../features/currentTodo'; +import { filterSlice } from '../features/filter'; +import { todosSlice } from '../features/todos'; -const rootReducer = combineSlices(); +const rootReducer = combineSlices(todosSlice, filterSlice, currentTodoSlice); export const store = configureStore({ reducer: rootReducer, diff --git a/src/components/TodoFilter/TodoFilter.tsx b/src/components/TodoFilter/TodoFilter.tsx index c1b574ee7..b147857a6 100644 --- a/src/components/TodoFilter/TodoFilter.tsx +++ b/src/components/TodoFilter/TodoFilter.tsx @@ -1,6 +1,17 @@ import React from 'react'; +import { Status } from '../../tools/constants'; -export const TodoFilter: React.FC = () => { +type Props = { + query: string; + setStatus: (filterField: string) => void; + setTitle: (title: string) => void; +}; + +export const TodoFilter: React.FC = ({ + query, + setStatus: setFilterField, + setTitle, +}) => { return (
{ >

- setFilterField(event.target.value)} + > + {Object.values(Status).map((field: Status) => ( + + ))}

@@ -22,18 +38,22 @@ export const TodoFilter: React.FC = () => { type="text" className="input" placeholder="Search..." + value={query} + onChange={event => setTitle(event.target.value)} /> - {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} - + + + + +3 + + + +

fugiat veniam minus

+ + + + + + */ +} diff --git a/src/components/TodoModal/TodoModal.tsx b/src/components/TodoModal/TodoModal.tsx index e16a079d5..a4f4094a3 100644 --- a/src/components/TodoModal/TodoModal.tsx +++ b/src/components/TodoModal/TodoModal.tsx @@ -1,42 +1,71 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Loader } from '../Loader'; +import { Todo } from '../../types/Todo'; +import { getUser } from '../../api'; +import { User } from '../../types/User'; + +type Props = { + selectedTodo: Todo; + setSelectedTodo: (selectedTodo: Todo | null) => void; +}; + +export const TodoModal: React.FC = ({ + selectedTodo, + setSelectedTodo, +}) => { + const [user, setUser] = useState(); + const [userLoading, setUserLoading] = useState(false); + + useEffect(() => { + setUserLoading(true); + getUser(selectedTodo.userId) + .then(setUser) + .finally(() => setUserLoading(false)); + }, [selectedTodo]); -export const TodoModal: React.FC = () => { return (
- + {userLoading ? ( + + ) : ( +
+
+
+ {`Todo #${selectedTodo.id}`} +
-
-
-
- Todo #3 -
+
- {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} -
+
+

+ {selectedTodo.title} +

-
-

- fugiat veniam minus -

+

+ {selectedTodo.completed ? ( + Done + ) : ( + Planned + )} -

- {/* For not completed */} - Planned + {' by '} - {/* For completed */} - Done - {' by '} - Leanne Graham -

+ {user && {user.name}} +

+
-
+ )}
); }; diff --git a/src/features/currentTodo.ts b/src/features/currentTodo.ts index 9e69e2240..94fe8cdad 100644 --- a/src/features/currentTodo.ts +++ b/src/features/currentTodo.ts @@ -1,4 +1,4 @@ -import { createSlice } from '@reduxjs/toolkit'; +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { Todo } from '../types/Todo'; const initialState = null as Todo | null; @@ -6,5 +6,14 @@ const initialState = null as Todo | null; export const currentTodoSlice = createSlice({ name: 'currentTodo', initialState, - reducers: {}, + reducers: { + setCurrentTodo: (_, action: PayloadAction) => { + return action.payload; + }, + clearCurrentTodo: () => { + return null; + }, + }, }); + +export const currentTodoActions = currentTodoSlice.actions; diff --git a/src/features/filter.ts b/src/features/filter.ts index d0eaf4640..f2bf5d164 100644 --- a/src/features/filter.ts +++ b/src/features/filter.ts @@ -1,4 +1,4 @@ -import { createSlice } from '@reduxjs/toolkit'; +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; const initialState = { query: '', @@ -8,5 +8,17 @@ const initialState = { export const filterSlice = createSlice({ name: 'filter', initialState, - reducers: {}, + reducers: { + setQuery: (state, action: PayloadAction) => { + return { ...state, query: action.payload }; + }, + setStatus: (state, action: PayloadAction) => { + return { ...state, status: action.payload }; + }, + clearQuery: state => { + return { ...state, query: '', status: 'all' }; + }, + }, }); + +export const filterActions = filterSlice.actions; diff --git a/src/features/todos.ts b/src/features/todos.ts index 4555dea8d..7c66cc993 100644 --- a/src/features/todos.ts +++ b/src/features/todos.ts @@ -1,8 +1,14 @@ -import { createSlice } from '@reduxjs/toolkit'; +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { Todo } from '../types/Todo'; export const todosSlice = createSlice({ name: 'todos', initialState: [] as Todo[], - reducers: {}, + reducers: { + setTodos(todos, action: PayloadAction) { + todos.push(...action.payload); + }, + }, }); + +export const todosActions = todosSlice.actions; diff --git a/src/tools/constants.ts b/src/tools/constants.ts new file mode 100644 index 000000000..82183d905 --- /dev/null +++ b/src/tools/constants.ts @@ -0,0 +1,5 @@ +export enum Status { + ALL = 'all', + ACTIVE = 'active', + COMPLETED = 'completed', +} diff --git a/src/tools/prepareTodos.tsx b/src/tools/prepareTodos.tsx new file mode 100644 index 000000000..7e11a4109 --- /dev/null +++ b/src/tools/prepareTodos.tsx @@ -0,0 +1,32 @@ +import { Todo } from '../types/Todo'; +import { Status } from './constants'; + +export const prepareTodos = ( + readyTodos: Todo[], + query: string, + status: string, +) => { + return readyTodos.filter((todo: Todo) => { + const titleCondition = todo.title + .toLowerCase() + .includes(query.toLowerCase()); + + if (status) { + switch (status) { + case Status.ALL: + return titleCondition; + + case Status.ACTIVE: + return titleCondition && !todo.completed; + + case Status.COMPLETED: + return titleCondition && todo.completed; + + default: + return titleCondition; + } + } + + return titleCondition; + }); +};