diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..b4c13cc --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..797acea --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/swpp-redux-final.iml b/.idea/swpp-redux-final.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/swpp-redux-final.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..35d6016 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + 1664439658287 + + + + \ No newline at end of file diff --git a/package.json b/package.json index c583f14..e51db5b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,9 @@ "name": "swpp-p3-react-tutorial", "version": "0.1.0", "private": true, + "proxy": "http://127.0.0.1:8000", "dependencies": { + "@reduxjs/toolkit": "1.8.5", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "13.3.0", "@testing-library/user-event": "13.5.0", @@ -10,11 +12,14 @@ "@types/node": "16.11.56", "@types/react": "18.0.17", "@types/react-dom": "18.0.6", + "axios": "0.27.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-redux": "8.0.2", "react-router": "6.3.0", "react-router-dom": "6.3.0", "react-scripts": "5.0.1", + "redux": "4.2.0", "typescript": "4.7.4", "web-vitals": "2.1.4" }, @@ -42,4 +47,4 @@ "last 1 safari version" ] } -} +} \ No newline at end of file diff --git a/src/components/Todo/Todo.tsx b/src/components/Todo/Todo.tsx index c207bad..0544e55 100644 --- a/src/components/Todo/Todo.tsx +++ b/src/components/Todo/Todo.tsx @@ -2,18 +2,22 @@ import "./Todo.css"; interface IProps { title: string; - clicked?: React.MouseEventHandler; // Defined by React + clickDetail?: React.MouseEventHandler; // Defined by React + clickDone?: () => void; + clickDelete?: () => void; done: boolean; } const Todo = (props: IProps) => { return (
-
+
{props.title}
{props.done &&
} + +
); }; -export default Todo; +export default Todo; \ No newline at end of file diff --git a/src/components/TodoDetail/TodoDetail.tsx b/src/components/TodoDetail/TodoDetail.tsx index dd6fc30..483b7ae 100644 --- a/src/components/TodoDetail/TodoDetail.tsx +++ b/src/components/TodoDetail/TodoDetail.tsx @@ -1,22 +1,31 @@ +import { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useParams } from "react-router"; +import { AppDispatch } from "../../store"; +import { selectTodo, fetchTodo } from "../../store/slices/todo"; import "./TodoDetail.css"; -type Props = { - title: string; - content: string; -}; +const TodoDetail = () => { + const { id } = useParams(); + const dispatch = useDispatch(); + const todoState = useSelector(selectTodo); + + useEffect(() => { + dispatch(fetchTodo(Number(id))); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id]); -const TodoDetail = (props: Props) => { return (
Name:
-
{props.title}
+
{todoState.selectedTodo?.title}
Content:
-
{props.content}
+
{todoState.selectedTodo?.content}
); }; -export default TodoDetail; +export default TodoDetail; \ No newline at end of file diff --git a/src/containers/TodoList/NewTodo/NewTodo.tsx b/src/containers/TodoList/NewTodo/NewTodo.tsx index c23a5fb..b26873d 100644 --- a/src/containers/TodoList/NewTodo/NewTodo.tsx +++ b/src/containers/TodoList/NewTodo/NewTodo.tsx @@ -1,12 +1,16 @@ import { useState } from "react"; +import { useDispatch } from "react-redux"; import { Navigate } from "react-router-dom"; // import { useNavigate } from "react-router-dom"; +import { AppDispatch } from "../../../store"; +import { postTodo } from "../../../store/slices/todo"; import "./NewTodo.css"; export default function NewTodo() { const [title, setTitle] = useState(""); const [content, setContent] = useState(""); const [submitted, setSubmitted] = useState(false); + const dispatch = useDispatch(); // const navigate = useNavigate() // const postTodoHandler = () => { @@ -16,10 +20,14 @@ export default function NewTodo() { // navigate('/todos') // }; - const postTodoHandler = () => { + const postTodoHandler = async () => { const data = { title: title, content: content }; - alert("Submitted\n" + data.title + "\n" + data.content); - setSubmitted(true); + const result = await dispatch(postTodo(data)); + if (result.type === `${postTodo.typePrefix}/fulfilled`) { + setSubmitted(true); + } else { + alert("Error on post Todo"); + } }; if (submitted) { @@ -44,4 +52,4 @@ export default function NewTodo() {
); } -} +} \ No newline at end of file diff --git a/src/containers/TodoList/TodoList.tsx b/src/containers/TodoList/TodoList.tsx index fa0fe61..bd5f084 100644 --- a/src/containers/TodoList/TodoList.tsx +++ b/src/containers/TodoList/TodoList.tsx @@ -1,8 +1,15 @@ -import { useMemo, useState } from "react"; -import { NavLink } from "react-router-dom"; +import { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { NavLink, useNavigate } from "react-router-dom"; import Todo from "../../components/Todo/Todo"; -import TodoDetail from "../../components/TodoDetail/TodoDetail"; +import { + fetchTodos, + selectTodo, + toggleDone, + deleteTodo, +} from "../../store/slices/todo"; import "./TodoList.css"; +import { AppDispatch } from "../../store"; interface IProps { title: string; @@ -11,46 +18,39 @@ interface IProps { type TodoType = { id: number; title: string; content: string; done: boolean }; export default function TodoList(props: IProps) { + const navigate = useNavigate(); const { title } = props; - const [selectedTodo, setSelectedTodo] = useState(null); - const [todos, setTodos] = useState([ - { id: 1, title: "SWPP", content: "take swpp class", done: true }, - { id: 2, title: "Movie", content: "watch movie", done: false }, - { id: 3, title: "Dinner", content: "eat dinner", done: false }, - ]); + const todoState = useSelector(selectTodo); + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchTodos()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const clickTodoHandler = (td: TodoType) => { - if (selectedTodo === td) { - setSelectedTodo(null); - } else { - setSelectedTodo(td); - } + navigate("/todos/" + td.id); }; - const todoDetail = useMemo(() => { - return selectedTodo ? ( - - ) : null; - }, [selectedTodo]); - return (
{title}
- {todos.map((td) => { + {todoState.todos.map((td) => { return ( clickTodoHandler(td)} + clickDetail={() => clickTodoHandler(td)} + clickDone={() => dispatch(toggleDone(td.id))} + clickDelete={() => dispatch(deleteTodo(td.id))} /> ); })} - {todoDetail} - New Todo + New Todo
); -} +} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 1fd12b7..1de1f2a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,13 +1,18 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import './index.css'; -import App from './App'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import { Provider } from "react-redux"; + +import "./index.css"; +import App from "./App"; +import { store } from "./store"; const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement + document.getElementById("root") as HTMLElement ); root.render( - + + + -); +); \ No newline at end of file diff --git a/src/store/slices/index.ts b/src/store/slices/index.ts new file mode 100644 index 0000000..0f9a109 --- /dev/null +++ b/src/store/slices/index.ts @@ -0,0 +1,11 @@ +import { configureStore } from "@reduxjs/toolkit"; +import todoReducer from "./slices/todo"; + +export const store = configureStore({ + reducer: { + todo: todoReducer, + }, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; \ No newline at end of file diff --git a/src/store/slices/todo.ts b/src/store/slices/todo.ts new file mode 100644 index 0000000..7a605e5 --- /dev/null +++ b/src/store/slices/todo.ts @@ -0,0 +1,115 @@ +import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; +import axios from "axios"; +import { RootState } from ".."; + +export interface TodoType { + id: number; + title: string; + content: string; + done: boolean; +} + +export interface TodoState { + todos: TodoType[]; + selectedTodo: TodoType | null; +} + +const initialState: TodoState = { + todos: [], + selectedTodo: null, +}; + +export const fetchTodos = createAsyncThunk("todo/fetchTodos", async () => { + const response = await axios.get("/api/todo/"); + return response.data; +}); + +export const fetchTodo = createAsyncThunk( + "todo/fetchTodo", + async (id: TodoType["id"], { dispatch }) => { + const response = await axios.get(`/api/todo/${id}/`); + return response.data ?? null; + } +); + +export const postTodo = createAsyncThunk( + "todo/postTodo", + async (td: Pick, { dispatch }) => { + const response = await axios.post("/api/todo/", td); + dispatch(todoActions.addTodo(response.data)); + } +); + +export const deleteTodo = createAsyncThunk( + "todo/deleteTodo", + async (id: TodoType["id"], { dispatch }) => { + await axios.delete(`/api/todo/${id}/`); + dispatch(todoActions.deleteTodo({ targetId: id })); + } +); + +export const toggleDone = createAsyncThunk( + "todo/toggleDone", + async (id: TodoType["id"], { dispatch }) => { + await axios.put(`/api/todo/${id}/`); + dispatch(todoActions.toggleDone({ targetId: id })); + } +); + +export const todoSlice = createSlice({ + name: "todo", + initialState, + reducers: { + getAll: (state, action: PayloadAction<{ todos: TodoType[] }>) => {}, + getTodo: (state, action: PayloadAction<{ targetId: number }>) => { + const target = state.todos.find( + (td) => td.id === action.payload.targetId + ); + state.selectedTodo = target ?? null; + }, + toggleDone: (state, action: PayloadAction<{ targetId: number }>) => { + const todo = state.todos.find( + (value) => value.id === action.payload.targetId + ); + if (todo) { + todo.done = !todo.done; + } + }, + deleteTodo: (state, action: PayloadAction<{ targetId: number }>) => { + const deleted = state.todos.filter((todo) => { + return todo.id !== action.payload.targetId; + }); + state.todos = deleted; + }, + addTodo: ( + state, + action: PayloadAction<{ title: string; content: string }> + ) => { + const newTodo = { + id: state.todos[state.todos.length - 1].id + 1, // temporary + title: action.payload.title, + content: action.payload.content, + done: false, + }; + state.todos.push(newTodo); + }, + }, + extraReducers: (builder) => { + // Add reducers for additional action types here, and handle loading state as needed + builder.addCase(fetchTodos.fulfilled, (state, action) => { + // Add user to the state array + state.todos = action.payload; + }); + builder.addCase(fetchTodo.fulfilled, (state, action) => { + state.selectedTodo = action.payload; + }); + builder.addCase(postTodo.rejected, (_state, action) => { + console.error(action.error); + }); + }, +}); + +export const todoActions = todoSlice.actions; +export const selectTodo = (state: RootState) => state.todo; + +export default todoSlice.reducer; \ No newline at end of file