diff --git a/frontend/package.json b/frontend/package.json
index 4c674c1..8970741 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -79,5 +79,5 @@
"prettier": {
"trailingComma": "all"
},
- "proxy": "http://localhost:5050"
+ "proxy": "http://127.0.0.1:5050"
}
diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx
index 9d01e4b..5222208 100644
--- a/frontend/src/Router.tsx
+++ b/frontend/src/Router.tsx
@@ -2,6 +2,7 @@ import { BrowserRouter, Route, Routes } from "react-router-dom";
import ScrollToTop from "./components/ScrollToTop";
import TopBar from "./components/TopBar";
+
import Register from "./pages/Register";
import ConfirmEmail from "./pages/ConfirmEmail";
import Login from "./pages/Login";
@@ -10,6 +11,10 @@ import ChangePassword from "./pages/ChangePassword";
import ForgottenPassword from "./pages/ForgottenPassword";
import ResetPassword from "./pages/ResetPassword";
+import CreateTodo from "./pages/CreateTodo";
+import EditTodo from "./pages/EditTodo";
+import Todos from "./pages/Todos";
+
const Router = () => (
@@ -28,6 +33,30 @@ const Router = () => (
/>
} />
} />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
);
diff --git a/frontend/src/components/Todo.tsx b/frontend/src/components/Todo.tsx
new file mode 100644
index 0000000..6ccf550
--- /dev/null
+++ b/frontend/src/components/Todo.tsx
@@ -0,0 +1,61 @@
+import Checkbox from "@mui/material/Checkbox";
+import IconButton from "@mui/material/IconButton";
+import ListItem from "@mui/material/ListItem";
+import ListItemButton from "@mui/material/ListItemButton";
+import ListItemIcon from "@mui/material/ListItemIcon";
+import ListItemText from "@mui/material/ListItemText";
+import Skeleton from "@mui/material/Skeleton";
+import DeleteIcon from "@mui/icons-material/Delete";
+import { format } from "date-fns";
+import { Link } from "react-router-dom";
+
+import { Todo as TodoModel } from "../models";
+import { useDeleteTodoMutation } from "../queries";
+
+interface IProps {
+ todo?: TodoModel;
+}
+
+const Todo = ({ todo }: IProps) => {
+ const { mutateAsync: deleteTodo } = useDeleteTodoMutation();
+ let secondary;
+ if (todo === undefined) {
+ secondary = ;
+ } else if (todo.due !== null) {
+ secondary = format(todo.due, "P");
+ }
+ return (
+ deleteTodo(todo?.id!)}
+ >
+
+
+ }
+ >
+
+
+
+
+ }
+ secondary={secondary}
+ />
+
+
+ );
+};
+export default Todo;
diff --git a/frontend/src/components/TodoForm.tsx b/frontend/src/components/TodoForm.tsx
new file mode 100644
index 0000000..e3fb837
--- /dev/null
+++ b/frontend/src/components/TodoForm.tsx
@@ -0,0 +1,44 @@
+import { Form, Formik } from "formik";
+import * as yup from "yup";
+
+import CheckboxField from "../components/CheckboxField";
+import DateField from "../components/DateField";
+import FormActions from "../components/FormActions";
+import TextField from "../components/TextField";
+import type { ITodoData } from "../queries";
+
+interface IProps {
+ initialValues: ITodoData;
+ label: string;
+ onSubmit: (data: ITodoData) => Promise;
+}
+
+const validationSchema = yup.object({
+ complete: yup.boolean(),
+ due: yup.date().nullable(),
+ task: yup.string().required("Required"),
+});
+
+const TodoForm = ({ initialValues, label, onSubmit }: IProps) => (
+
+ initialValues={initialValues}
+ onSubmit={onSubmit}
+ validationSchema={validationSchema}
+ >
+ {({ dirty, isSubmitting }) => (
+
+ )}
+
+);
+
+export default TodoForm;
diff --git a/frontend/src/pages/CreateTodo.tsx b/frontend/src/pages/CreateTodo.tsx
new file mode 100644
index 0000000..acfb4ad
--- /dev/null
+++ b/frontend/src/pages/CreateTodo.tsx
@@ -0,0 +1,36 @@
+import { useContext } from "react";
+import { useNavigate } from "react-router-dom";
+
+import TodoForm from "../components/TodoForm";
+import Title from "../components/Title";
+import type { ITodoData } from "../queries";
+import { useCreateTodoMutation } from "../queries";
+import { ToastContext } from "../ToastContext";
+
+const CreateTodo = () => {
+ const navigate = useNavigate();
+ const { addToast } = useContext(ToastContext);
+ const { mutateAsync: createTodo } = useCreateTodoMutation();
+
+ const onSubmit = async (data: ITodoData) => {
+ try {
+ await createTodo(data);
+ navigate("/");
+ } catch {
+ addToast("Try Again", "error");
+ }
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default CreateTodo;
diff --git a/frontend/src/pages/EditTodo.tsx b/frontend/src/pages/EditTodo.tsx
new file mode 100644
index 0000000..3369d11
--- /dev/null
+++ b/frontend/src/pages/EditTodo.tsx
@@ -0,0 +1,52 @@
+import Skeleton from "@mui/material/Skeleton";
+import { useContext } from "react";
+import { useNavigate, useParams } from "react-router";
+
+import TodoForm from "../components/TodoForm";
+import Title from "../components/Title";
+import type { ITodoData } from "../queries";
+import { useEditTodoMutation, useTodoQuery } from "../queries";
+import { ToastContext } from "../ToastContext";
+
+interface Iparams {
+ id: string;
+}
+
+const EditTodo = () => {
+ const navigate = useNavigate();
+ const params = useParams() as Iparams;
+ const todoId = parseInt(params.id, 10);
+ const { addToast } = useContext(ToastContext);
+ const { data: todo } = useTodoQuery(todoId);
+ const { mutateAsync: editTodo } = useEditTodoMutation(todoId);
+
+ const onSubmit = async (data: ITodoData) => {
+ try {
+ await editTodo(data);
+ navigate("/");
+ } catch {
+ addToast("Try again", "error");
+ }
+ };
+
+ return (
+ <>
+
+ {todo === undefined ? (
+
+ ) : (
+
+ )}
+ >
+ );
+};
+
+export default EditTodo;
diff --git a/frontend/src/pages/Todos.tsx b/frontend/src/pages/Todos.tsx
new file mode 100644
index 0000000..55b4935
--- /dev/null
+++ b/frontend/src/pages/Todos.tsx
@@ -0,0 +1,38 @@
+import Fab from "@mui/material/Fab";
+import List from "@mui/material/List";
+import AddIcon from "@mui/icons-material/Add";
+import { Link, Navigate } from "react-router-dom";
+
+import Todo from "../components/Todo";
+import { useTodosQuery } from "../queries";
+
+const Todos = () => {
+ const { data: todos } = useTodosQuery();
+
+ if (todos?.length === 0) {
+ return ;
+ } else {
+ return (
+ <>
+
+ {todos !== undefined
+ ? todos.map((todo) => )
+ : [1, 2, 3].map((id) => )}
+
+ theme.spacing(2),
+ position: "fixed",
+ right: (theme) => theme.spacing(2),
+ }}
+ to="/todos/new/"
+ >
+
+
+ >
+ );
+ }
+};
+
+export default Todos;
diff --git a/frontend/src/queries.ts b/frontend/src/queries.ts
new file mode 100644
index 0000000..c7f451a
--- /dev/null
+++ b/frontend/src/queries.ts
@@ -0,0 +1,72 @@
+import axios from "axios";
+import { useQueryClient } from "@tanstack/react-query";
+
+import { Todo } from "./models";
+import { useMutation, useQuery } from "./query";
+
+export const STALE_TIME = 1000 * 60 * 5; // 5 mins
+
+export const useTodosQuery = () =>
+ useQuery(
+ ["todos"],
+ async () => {
+ const response = await axios.get("/todos/");
+ return response.data.todos.map((json: any) => new Todo(json));
+ },
+ { staleTime: STALE_TIME },
+ );
+
+export const useTodoQuery = (id: number) => {
+ const queryClient = useQueryClient();
+ return useQuery(
+ ["todos", id.toString()],
+ async () => {
+ const response = await axios.get(`/todos/${id}/`);
+ return new Todo(response.data);
+ },
+ {
+ initialData: () => {
+ return queryClient
+ .getQueryData(["todos"])
+ ?.filter((todo: Todo) => todo.id === id)[0];
+ },
+ staleTime: STALE_TIME,
+ },
+ );
+};
+
+export interface ITodoData {
+ complete: boolean;
+ due: Date | null;
+ task: string;
+}
+
+export const useCreateTodoMutation = () => {
+ const queryClient = useQueryClient();
+ return useMutation(
+ async (data: ITodoData) => await axios.post("/todos/", data),
+ {
+ onSuccess: () => queryClient.invalidateQueries(["todos"]),
+ },
+ );
+};
+
+export const useEditTodoMutation = (id: number) => {
+ const queryClient = useQueryClient();
+ return useMutation(
+ async (data: ITodoData) => await axios.put(`/todos/${id}/`, data),
+ {
+ onSuccess: () => queryClient.invalidateQueries(["todos"]),
+ },
+ );
+};
+
+export const useDeleteTodoMutation = () => {
+ const queryClient = useQueryClient();
+ return useMutation(
+ async (id: number) => await axios.delete(`/todos/${id}/`),
+ {
+ onSuccess: () => queryClient.invalidateQueries(["todos"]),
+ },
+ );
+};