diff --git a/src/app/App.test.tsx b/src/app/App.test.tsx index 4dedfe5..4180cf9 100644 --- a/src/app/App.test.tsx +++ b/src/app/App.test.tsx @@ -3,7 +3,7 @@ import { createStore } from "redux"; import { Provider } from "react-redux"; import App from './App'; -import {todoReducer} from "../state/todo/reducers"; +import todoReducer from "../features/todo/todosSlice"; test('renders learn react link', () => { const store = createStore(todoReducer); diff --git a/src/app/App.tsx b/src/app/App.tsx index f8ccbe6..97efa11 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -2,23 +2,17 @@ import React, {useState} from 'react'; import { useSelector, useDispatch } from "react-redux"; import Todo from "../components/todo/Todo"; -import { TodoItem } from "../state/todo/types"; -import { ADD_TODO } from "../state/todo/reducers"; +import { addTodo } from "../features/todo/todosSlice"; +import { RootState } from "./store"; function App() { const [text, setText] = useState(""); - const todos = useSelector((state: { todos: TodoItem[] }) => state.todos); + const todos = useSelector((state: RootState) => state.todos.todos); const dispatch = useDispatch(); const handleAddTodo = () => { if (text.trim() === "") return; - const newTodo: TodoItem = { - id: todos.length + 1, - text: text, - completed: false, - }; - - dispatch({ type: ADD_TODO, payload: newTodo}); + dispatch(addTodo(text)); setText(""); } @@ -33,12 +27,7 @@ function App() { diff --git a/src/app/store.ts b/src/app/store.ts new file mode 100644 index 0000000..baea1e6 --- /dev/null +++ b/src/app/store.ts @@ -0,0 +1,11 @@ +import { configureStore } from "@reduxjs/toolkit"; + +import todosReducer from '../features/todo/todosSlice'; + +export const store = configureStore({ + reducer: { + todos: todosReducer, + }, +}); + +export type RootState = ReturnType; diff --git a/src/components/todo/Todo.test.tsx b/src/components/todo/Todo.test.tsx index 11d5064..3a7fa8a 100644 --- a/src/components/todo/Todo.test.tsx +++ b/src/components/todo/Todo.test.tsx @@ -1,53 +1,56 @@ import React from "react"; -import {screen, render, fireEvent, getByRole} from "@testing-library/react" -import { createStore } from "redux"; +import {render, fireEvent} from "@testing-library/react" import { Provider } from "react-redux"; import Todo from "./Todo"; -import {todoReducer} from "../../state/todo/reducers"; -import {store} from "../../state/store"; +import todoReducer from '../../features/todo/todosSlice' +import {configureStore} from "@reduxjs/toolkit"; -describe("Todo", () => { - - const props = { - id: 1, - text: "Buy milk", - completed: false, +const renderWithRedux = ( + ui: React.ReactElement, + { store = configureStore({ reducer: { todos: todoReducer } }) } = {} +) => { + return { + ...render({ui}), + store, }; - - it("should render todo text", () => { - render( - - - - ) - expect(screen.getByText(props.text)).toBeInTheDocument(); +}; +describe("Todo", () => { + it("renders todo text and toggle button", () => { + const todo = { + id: 1, + text: "Test todo", + completed: false, + }; + const { getByText, getByRole } = renderWithRedux(); + const todoText = getByText("Test todo"); + expect(todoText).toBeInTheDocument(); + const toggleButton = getByRole("checkbox"); + expect(toggleButton).toBeInTheDocument(); }); - it("should call onToggle when checkbox is clicked", () => { - render( - - - - ) - fireEvent.click(screen.getByRole("checkbox")); + it("toggles todo when toggle button is clicked", () => { + const todo = { + id: 1, + text: "Test todo", + completed: false, + }; + const { getByRole, store } = renderWithRedux(); + const toggleButton = getByRole("checkbox"); + fireEvent.click(toggleButton); + expect(store.getState().todos.todos[0].completed).toBe(true); }); - it("should call onDelete when delete button is clicked", () => { - render( - - - - ) - fireEvent.click(screen.getByRole("button")); - }) + it("deletes todo when delete button is clicked", () => { + const todo = { + id: 1, + text: "Test todo", + completed: false, + }; + const { getByRole, store } = renderWithRedux(); + const deleteButton = getByRole("button"); + fireEvent.click(deleteButton); + expect(store.getState().todos.todos).toHaveLength(0); + }); - it("should have line-through style when completed is true", () => { - render( - - - - ) - expect(screen.getByText(props.text)).toHaveStyle("text-decoration: line-through"); - }) }) diff --git a/src/components/todo/Todo.tsx b/src/components/todo/Todo.tsx index 7bbc99c..cf91134 100644 --- a/src/components/todo/Todo.tsx +++ b/src/components/todo/Todo.tsx @@ -1,28 +1,26 @@ import React from "react"; import {useDispatch} from "react-redux"; -import { DELETE_TODO, TOGGLE_TODO } from "../../state/todo/reducers"; +import { toggleTodo, deleteTodo, TodoItem } from "../../features/todo/todosSlice"; interface TodoProps { - id: number; - text: string; - completed: boolean; + todo: TodoItem; } -const Todo = ( { id, text, completed }: TodoProps) => { +const Todo = ( { todo }: TodoProps) => { const dispatch = useDispatch(); const handleToggle = () => { - dispatch({ type: TOGGLE_TODO, payload: id }); + dispatch(toggleTodo(todo.id)); }; const handleDelete = () => { - dispatch({ type: DELETE_TODO, payload: id }); + dispatch(deleteTodo(todo.id)); }; return (
  • - - - {text} + + + {todo.text}
  • diff --git a/src/features/todo/todosSlice.ts b/src/features/todo/todosSlice.ts new file mode 100644 index 0000000..2d8e05b --- /dev/null +++ b/src/features/todo/todosSlice.ts @@ -0,0 +1,46 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +export interface TodoItem { + id: number; + text: string; + completed: boolean; +} + +interface TodoState { + todos: TodoItem[]; +} + +const initialState: TodoState = { + todos: [] +} + +export const todosSlice = createSlice({ + name: 'todo', + initialState, + reducers: { + addTodo: { + reducer: (state, action: PayloadAction) => { + state.todos.push(action.payload); + }, + prepare: (text: string) => ({ + payload: { + id: new Date().getTime(), + text, + completed: false, + }, + }), + }, + toggleTodo: (state, action: PayloadAction) => { + const todo = state.todos.find((todo) => todo.id === action.payload); + if (todo) { + todo.completed = !todo.completed; + } + }, + deleteTodo: (state, action: PayloadAction) => { + state.todos = state.todos.filter((todo) => todo.id !== action.payload); + }, + }, +}) + +export const { addTodo, toggleTodo, deleteTodo } = todosSlice.actions; +export default todosSlice.reducer; diff --git a/src/index.tsx b/src/index.tsx index 93b5708..2a1c996 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,8 @@ import React from 'react'; import ReactDOM from "react-dom"; import { Provider } from "react-redux"; -import { store } from "./state/store"; + +import { store } from "./app/store"; import App from './app/App'; ReactDOM.render( diff --git a/src/state/store.ts b/src/state/store.ts deleted file mode 100644 index 348730a..0000000 --- a/src/state/store.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createStore } from "redux"; - -import {todoReducer} from "./todo/reducers"; - -export const store = createStore(todoReducer); diff --git a/src/state/todo/reducers.ts b/src/state/todo/reducers.ts deleted file mode 100644 index 187de7a..0000000 --- a/src/state/todo/reducers.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { TodoItem } from "./types"; - -interface TodoState { - todos: TodoItem[]; -} - -const initialState: TodoState = { - todos: [], -} - -export const ADD_TODO = "ADD_TODO"; -export const TOGGLE_TODO = "TOGGLE_TODO"; -export const DELETE_TODO = "DELETE_TODO"; - -interface AddTodoAction { - type: typeof ADD_TODO; - payload: TodoItem; -} - -interface ToggleTodoAction { - type: typeof TOGGLE_TODO; - payload: number; -} - -interface DeleteTodoAction { - type: typeof DELETE_TODO; - payload: number; -} - -type TodoActionTypes = AddTodoAction | ToggleTodoAction | DeleteTodoAction; - -export function todoReducer( - state = initialState, - action: TodoActionTypes -): TodoState { - switch (action.type) { - case ADD_TODO: - return { - ...state, - todos: [...state.todos, action.payload], - }; - case TOGGLE_TODO: - return { - ...state, - todos: state.todos.map((todo) => { - if (todo.id === action.payload) { - return { ...todo, completed: !todo.completed }; - } - return todo; - }), - }; - case DELETE_TODO: - return { - ...state, - todos: state.todos.filter((todo) => todo.id !== action.payload), - }; - default: - return state; - } -} diff --git a/src/state/todo/types.ts b/src/state/todo/types.ts deleted file mode 100644 index 1161454..0000000 --- a/src/state/todo/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface TodoItem { - id: number; - text: string; - completed: boolean; -}