diff --git a/package-lock.json b/package-lock.json
index 2be6d1de6..34e81e351 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,7 +22,7 @@
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
- "@mate-academy/scripts": "^1.8.5",
+ "@mate-academy/scripts": "^1.9.12",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
@@ -1187,10 +1187,11 @@
}
},
"node_modules/@mate-academy/scripts": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.5.tgz",
- "integrity": "sha512-mHRY2FkuoYCf5U0ahIukkaRo5LSZsxrTSgMJheFoyf3VXsTvfM9OfWcZIDIDB521kdPrScHHnRp+JRNjCfUO5A==",
+ "version": "1.9.12",
+ "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.9.12.tgz",
+ "integrity": "sha512-/OcmxMa34lYLFlGx7Ig926W1U1qjrnXbjFJ2TzUcDaLmED+A5se652NcWwGOidXRuMAOYLPU2jNYBEkKyXrFJA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@octokit/rest": "^17.11.2",
"@types/get-port": "^4.2.0",
diff --git a/package.json b/package.json
index 4f3d32089..7ad4f3696 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
},
"devDependencies": {
"@cypress/react18": "^2.0.1",
- "@mate-academy/scripts": "^1.8.5",
+ "@mate-academy/scripts": "^1.9.12",
"@mate-academy/students-ts-config": "*",
"@mate-academy/stylelint-config": "*",
"@types/node": "^20.14.10",
diff --git a/src/App.tsx b/src/App.tsx
index e10753461..fb4ae2d30 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,26 +1,52 @@
+import React, { useEffect, useState } from 'react';
import 'bulma/css/bulma.css';
import '@fortawesome/fontawesome-free/css/all.css';
-import { Loader, TodoFilter, TodoList, TodoModal } from './components';
-export const App = () => (
- <>
-
-
-
-
Todos:
+import { TodoList } from './components/TodoList';
+import { TodoFilter } from './components/TodoFilter';
+import { TodoModal } from './components/TodoModal';
+import { Loader } from './components/Loader';
+import { getTodos } from './api';
+import { useAppSelector } from './app/hooks';
+import { useDispatch } from 'react-redux';
+import { setTodos } from './features/todos';
+import { selectTodos, selectCurrentTodo } from './features/selectors';
-
-
-
+export const App: React.FC = () => {
+ const dispatch = useDispatch();
+ const todos = useAppSelector(selectTodos);
+ const currentTodo = useAppSelector(selectCurrentTodo);
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ setIsLoading(true);
+ getTodos()
+ .then(getTodo => {
+ dispatch(setTodos(getTodo));
+ })
+ .finally(() => setIsLoading(false));
+ }, [dispatch]);
+
+ return (
+ <>
+
+
+
+
Todos:
+
+
+
+
-
-
-
+
+ {isLoading && }
+ {!isLoading && !!todos.length && }
+
-
-
- >
-);
+ {currentTodo &&
}
+ >
+ );
+};
diff --git a/src/app/hooks.ts b/src/app/hooks.ts
new file mode 100644
index 000000000..99763399a
--- /dev/null
+++ b/src/app/hooks.ts
@@ -0,0 +1,4 @@
+import { TypedUseSelectorHook, useSelector } from 'react-redux';
+import { RootState } from './store';
+
+export const useAppSelector: TypedUseSelectorHook
= useSelector;
diff --git a/src/app/store.ts b/src/app/store.ts
index 3a9b384be..93d7dfffd 100644
--- a/src/app/store.ts
+++ b/src/app/store.ts
@@ -1,6 +1,9 @@
import { combineSlices, configureStore } from '@reduxjs/toolkit';
+import { todosSlice } from '../features/todos';
+import { filterSlice } from '../features/filter';
+import { currentTodoSlice } from '../features/currentTodo';
-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..5b90ae283 100644
--- a/src/components/TodoFilter/TodoFilter.tsx
+++ b/src/components/TodoFilter/TodoFilter.tsx
@@ -1,17 +1,29 @@
import React from 'react';
+import { Status } from '../../types/Status';
+import { useAppSelector } from '../../app/hooks';
+import { useDispatch } from 'react-redux';
+import { setQuery, setStatus } from '../../features/filter';
export const TodoFilter: React.FC = () => {
+ const dispatch = useDispatch();
+ const { query, status } = useAppSelector(state => state.filter);
+
+ const handleQuery = (event: React.ChangeEvent) => {
+ dispatch(setQuery(event.target.value));
+ };
+
+ const handleStatus = (event: React.ChangeEvent) => {
+ dispatch(setStatus(event.target.value));
+ };
+
return (
-
diff --git a/src/components/TodoItem/TodoItem.tsx b/src/components/TodoItem/TodoItem.tsx
new file mode 100644
index 000000000..89502e945
--- /dev/null
+++ b/src/components/TodoItem/TodoItem.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import cn from 'classnames';
+import { Todo } from '../../types/Todo';
+
+interface TodoItemProps {
+ todo: Todo;
+ currentTodoId: number | null;
+ onSelect: (todo: Todo) => void;
+}
+
+export const TodoItem: React.FC = ({
+ todo,
+ currentTodoId,
+ onSelect,
+}) => {
+ const { id, completed, title } = todo;
+
+ return (
+
+ {id} |
+
+ {completed && (
+
+
+
+ )}
+ |
+
+
+ {title}
+
+ |
+
+
+ |
+
+ );
+};
diff --git a/src/components/TodoItem/index.ts b/src/components/TodoItem/index.ts
new file mode 100644
index 000000000..21f4abac3
--- /dev/null
+++ b/src/components/TodoItem/index.ts
@@ -0,0 +1 @@
+export * from './TodoItem';
diff --git a/src/components/TodoList/TodoList.tsx b/src/components/TodoList/TodoList.tsx
index 5089eb030..033a21eed 100644
--- a/src/components/TodoList/TodoList.tsx
+++ b/src/components/TodoList/TodoList.tsx
@@ -1,224 +1,49 @@
/* eslint-disable */
import React from 'react';
+import { Todo } from '../../types/Todo';
+import { useAppSelector } from '../../app/hooks';
+import { useDispatch } from 'react-redux';
+import { setCurrentTodo } from '../../features/currentTodo';
+import { TodoItem } from '../TodoItem/TodoItem';
+import { filterTodos } from '../../features/filterTodos';
export const TodoList: React.FC = () => {
- return (
- <>
-
- There are no todos matching current filter criteria
-
-
-
-
-
- # |
-
-
-
-
-
- |
-
- Title |
- |
-
-
-
-
-
- 1 |
- |
-
-
- delectus aut autem
- |
-
-
-
- |
-
-
-
- 2 |
- |
-
-
-
- quis ut nam facilis et officia qui
-
- |
-
-
-
- |
-
-
-
- 3 |
- |
-
-
- fugiat veniam minus
- |
-
-
-
- |
-
-
-
- 4 |
-
-
-
-
- |
-
- et porro tempora
- |
-
-
-
- |
-
-
-
- 5 |
- |
-
-
-
- laboriosam mollitia et enim quasi adipisci quia provident illum
-
- |
+ const dispatch = useDispatch();
+ const todos = useAppSelector(state => state.todos);
+ const currentTodo = useAppSelector(state => state.currentTodo);
+ const { query, status } = useAppSelector(state => state.filter);
-
-
- |
-
+ const handleSelectTodo = (todo: Todo) => {
+ dispatch(setCurrentTodo(todo));
+ };
-
- 6 |
- |
+ const filteredTodos = filterTodos(todos, status, query);
-
-
- qui ullam ratione quibusdam voluptatem quia omnis
-
- |
-
-
-
- |
-
-
-
- 7 |
- |
-
-
-
- illo expedita consequatur quia in
-
- |
-
-
-
- |
-
-
-
- 8 |
-
-
-
-
-
- |
-
-
- quo adipisci enim quam ut ab
- |
-
-
-
- |
-
-
-
- 9 |
- |
-
-
- molestiae perspiciatis ipsa
- |
-
-
-
- |
-
-
-
- 10 |
-
-
-
-
-
- |
-
-
-
- illo est ratione doloremque quia maiores aut
-
- |
-
-
-
- |
-
-
-
- >
+ return (
+
+
+
+ # |
+
+
+
+
+ |
+ Title |
+ |
+
+
+
+
+ {filteredTodos.map(todo => (
+
+ ))}
+
+
);
};
diff --git a/src/components/TodoModal/TodoModal.tsx b/src/components/TodoModal/TodoModal.tsx
index e16a079d5..54e016f9e 100644
--- a/src/components/TodoModal/TodoModal.tsx
+++ b/src/components/TodoModal/TodoModal.tsx
@@ -1,42 +1,74 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import { Loader } from '../Loader';
+import { getUser } from '../../api';
+import { User } from '../../types/User';
+import { useAppSelector } from '../../app/hooks';
+import { useDispatch } from 'react-redux';
+import { setCurrentTodo } from '../../features/currentTodo';
export const TodoModal: React.FC = () => {
+ const dispatch = useDispatch();
+ const currentTodo = useAppSelector(state => state.currentTodo);
+ const [user, setUser] = useState();
+ const [isLoading, setIsLoading] = useState(true);
+
+ const handleClose = () => {
+ dispatch(setCurrentTodo(null));
+ };
+
+ useEffect(() => {
+ if (currentTodo) {
+ setIsLoading(true);
+ getUser(currentTodo.userId)
+ .then(setUser)
+ .finally(() => setIsLoading(false));
+ } else {
+ setIsLoading(false);
+ }
+ }, [currentTodo]);
+
return (
-
+ {isLoading ? (
+
+ ) : (
+
);
};
diff --git a/src/components/index.ts b/src/components/index.ts
index bce637539..b66ec7293 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -2,3 +2,4 @@ export * from './Loader';
export * from './TodoList';
export * from './TodoFilter';
export * from './TodoModal';
+export * from './TodoItem';
diff --git a/src/features/currentTodo.ts b/src/features/currentTodo.ts
index 9e69e2240..c75d49608 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/react';
import { Todo } from '../types/Todo';
const initialState = null as Todo | null;
@@ -6,5 +6,9 @@ const initialState = null as Todo | null;
export const currentTodoSlice = createSlice({
name: 'currentTodo',
initialState,
- reducers: {},
+ reducers: {
+ setCurrentTodo: (_, { payload }: PayloadAction
) => payload,
+ },
});
+
+export const { setCurrentTodo } = currentTodoSlice.actions;
diff --git a/src/features/filter.ts b/src/features/filter.ts
index d0eaf4640..aa7efc311 100644
--- a/src/features/filter.ts
+++ b/src/features/filter.ts
@@ -1,12 +1,29 @@
-import { createSlice } from '@reduxjs/toolkit';
+import { createSlice, PayloadAction, Slice } from '@reduxjs/toolkit';
+import { Status } from '../types/Status';
-const initialState = {
+type FilterState = {
+ query: string;
+ status: Status;
+};
+
+const initialState: FilterState = {
query: '',
- status: 'all',
+ status: Status.All,
};
-export const filterSlice = createSlice({
+export const filterSlice: Slice = createSlice({
name: 'filter',
initialState,
- reducers: {},
+ reducers: {
+ setStatus: (state, { payload }: PayloadAction) => ({
+ ...state,
+ status: payload,
+ }),
+ setQuery: (state, { payload }: PayloadAction) => ({
+ ...state,
+ query: payload,
+ }),
+ },
});
+
+export const { setStatus, setQuery } = filterSlice.actions;
diff --git a/src/features/filterTodos.ts b/src/features/filterTodos.ts
new file mode 100644
index 000000000..5df6e3989
--- /dev/null
+++ b/src/features/filterTodos.ts
@@ -0,0 +1,23 @@
+import { Todo } from '../types/Todo';
+import { Status } from '../types/Status';
+
+export const filterTodos = (
+ todos: Todo[],
+ status: Status,
+ query: string,
+): Todo[] => {
+ if (status === Status.All && query === '') {
+ return todos;
+ }
+
+ return todos.filter(todo => {
+ const matchesStatus =
+ status === Status.All ||
+ (status === Status.Active && !todo.completed) ||
+ (status === Status.Completed && todo.completed);
+
+ const matchesQuery = todo.title.toLowerCase().includes(query.toLowerCase());
+
+ return matchesStatus && matchesQuery;
+ });
+};
diff --git a/src/features/selectors.ts b/src/features/selectors.ts
new file mode 100644
index 000000000..d9deb6308
--- /dev/null
+++ b/src/features/selectors.ts
@@ -0,0 +1,4 @@
+import { RootState } from '../app/store';
+
+export const selectTodos = (state: RootState) => state.todos;
+export const selectCurrentTodo = (state: RootState) => state.currentTodo;
diff --git a/src/features/todos.ts b/src/features/todos.ts
index 4555dea8d..af8decc04 100644
--- a/src/features/todos.ts
+++ b/src/features/todos.ts
@@ -1,8 +1,14 @@
-import { createSlice } from '@reduxjs/toolkit';
+import { createSlice, PayloadAction, Slice } from '@reduxjs/toolkit';
import { Todo } from '../types/Todo';
-export const todosSlice = createSlice({
+export const todosSlice: Slice = createSlice({
name: 'todos',
initialState: [] as Todo[],
- reducers: {},
+ reducers: {
+ setTodos(todos, action: PayloadAction) {
+ todos.push(...action.payload);
+ },
+ },
});
+
+export const { setTodos } = todosSlice.actions;
diff --git a/src/types/Status.ts b/src/types/Status.ts
index 6eb05b9b2..2ec4b8714 100644
--- a/src/types/Status.ts
+++ b/src/types/Status.ts
@@ -1 +1,5 @@
-export type Status = 'all' | 'active' | 'completed';
+export enum Status {
+ All = 'all',
+ Active = 'active',
+ Completed = 'completed',
+}