Merge backend + frontend #1

Merged
minhtrannhat merged 17 commits from frontend into master 2023-11-02 01:59:49 +00:00
8 changed files with 333 additions and 1 deletions
Showing only changes of commit 141eac7d17 - Show all commits

View File

@ -79,5 +79,5 @@
"prettier": {
"trailingComma": "all"
},
"proxy": "http://localhost:5050"
"proxy": "http://127.0.0.1:5050"
}

View File

@ -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 = () => (
<BrowserRouter>
<ScrollToTop />
@ -28,6 +33,30 @@ const Router = () => (
/>
<Route path="/forgotten-password/" element={<ForgottenPassword />} />
<Route path="/reset-password/:token/" element={<ResetPassword />} />
<Route
path="/"
element={
<RequireAuth>
<Todos />
</RequireAuth>
}
/>
<Route
path="/todos/new/"
element={
<RequireAuth>
<CreateTodo />
</RequireAuth>
}
/>
<Route
path="/todos/:id/"
element={
<RequireAuth>
<EditTodo />
</RequireAuth>
}
/>
</Routes>
</BrowserRouter>
);

View File

@ -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 = <Skeleton width="200px" />;
} else if (todo.due !== null) {
secondary = format(todo.due, "P");
}
return (
<ListItem
secondaryAction={
<IconButton
disabled={todo === undefined}
edge="end"
onClick={() => deleteTodo(todo?.id!)}
>
<DeleteIcon />
</IconButton>
}
>
<ListItemButton
component={Link}
disabled={todo === undefined}
to={`/todos/${todo?.id}/`}
>
<ListItemIcon>
<Checkbox
checked={todo?.complete ?? false}
disabled
disableRipple
edge="start"
tabIndex={-1}
/>
</ListItemIcon>
<ListItemText
primary={todo?.task ?? <Skeleton />}
secondary={secondary}
/>
</ListItemButton>
</ListItem>
);
};
export default Todo;

View File

@ -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<any>;
}
const validationSchema = yup.object({
complete: yup.boolean(),
due: yup.date().nullable(),
task: yup.string().required("Required"),
});
const TodoForm = ({ initialValues, label, onSubmit }: IProps) => (
<Formik<ITodoData>
initialValues={initialValues}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
{({ dirty, isSubmitting }) => (
<Form>
<TextField fullWidth label="Task" name="task" required />
<DateField fullWidth label="Due" name="due" />
<CheckboxField fullWidth label="Complete" name="complete" />
<FormActions
disabled={!dirty}
isSubmitting={isSubmitting}
label={label}
links={[{ label: "Back", to: "/" }]}
/>
</Form>
)}
</Formik>
);
export default TodoForm;

View File

@ -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 (
<>
<Title title="Create a Todo" />
<TodoForm
initialValues={{ complete: false, due: null, task: "" }}
label="Create"
onSubmit={onSubmit}
/>
</>
);
};
export default CreateTodo;

View File

@ -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<keyof Iparams>() 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 (
<>
<Title title="Edit todo" />
{todo === undefined ? (
<Skeleton height="80px" />
) : (
<TodoForm
initialValues={{
complete: todo.complete,
due: todo.due,
task: todo.task,
}}
label="Edit"
onSubmit={onSubmit}
/>
)}
</>
);
};
export default EditTodo;

View File

@ -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 <Navigate to="/todos/new/" />;
} else {
return (
<>
<List>
{todos !== undefined
? todos.map((todo) => <Todo key={todo.id} todo={todo} />)
: [1, 2, 3].map((id) => <Todo key={-id} />)}
</List>
<Fab
component={Link}
sx={{
bottom: (theme) => theme.spacing(2),
position: "fixed",
right: (theme) => theme.spacing(2),
}}
to="/todos/new/"
>
<AddIcon />
</Fab>
</>
);
}
};
export default Todos;

72
frontend/src/queries.ts Normal file
View File

@ -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<Todo[]>(
["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<Todo>(
["todos", id.toString()],
async () => {
const response = await axios.get(`/todos/${id}/`);
return new Todo(response.data);
},
{
initialData: () => {
return queryClient
.getQueryData<Todo[]>(["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"]),
},
);
};