diff --git a/client/package.json b/client/package.json
index d71cd2e..1f7e8d9 100644
--- a/client/package.json
+++ b/client/package.json
@@ -21,6 +21,7 @@
"react-dom": "^18.2.0",
"react-hook-form": "^7.33.1",
"react-scripts": "5.0.1",
+ "swr": "^1.3.0",
"typescript": "^4.4.2",
"web-vitals": "^2.1.0"
},
diff --git a/client/src/App.tsx b/client/src/App.tsx
index c4a3e12..3bcd647 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -2,7 +2,7 @@ import "@fontsource/roboto/300.css";
import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css";
-import { Container } from "@mui/material";
+import { Box, Container } from "@mui/material";
import { CookiesProvider } from "react-cookie";
import { LoginDialog } from "./components/LoginDialog";
import { Topbar } from "./components/Topbar";
@@ -18,10 +18,12 @@ function App() {
- }
- unauthenticatedContent={}
- />
+
+ }
+ unauthenticatedContent={}
+ />
+
diff --git a/client/src/pages/Projects/Projects.tsx b/client/src/pages/Projects/Projects.tsx
index 5cf6d2b..7d3abad 100644
--- a/client/src/pages/Projects/Projects.tsx
+++ b/client/src/pages/Projects/Projects.tsx
@@ -1,5 +1,26 @@
+import { Box } from "@mui/material";
+import useSWR from "swr";
+import { useAuth } from "../../hooks/useAuth";
+import { createSWRFetcher } from "../../utils/swrFetcher";
+import { Project } from "./components/Project";
+
+type APIProjectList = {
+ data: {
+ id: number;
+ name: string;
+ }[];
+};
+
export const Projects = () => {
+ const { token } = useAuth();
+ const fetcher = createSWRFetcher(token);
+ const { data } = useSWR("projects", fetcher);
+
return (
- <>>
- )
-}
\ No newline at end of file
+
+ {data?.data.map((project) => (
+
+ ))}
+
+ );
+};
diff --git a/client/src/pages/Projects/components/Project.tsx b/client/src/pages/Projects/components/Project.tsx
new file mode 100644
index 0000000..84598da
--- /dev/null
+++ b/client/src/pages/Projects/components/Project.tsx
@@ -0,0 +1,67 @@
+import { Add } from "@mui/icons-material";
+import MoreVertIcon from "@mui/icons-material/MoreVert";
+import {
+ Card,
+ CardActions,
+ CardContent,
+ CardHeader,
+ IconButton,
+} from "@mui/material";
+import useSWR from "swr";
+import { useAuth } from "../../../hooks/useAuth";
+import { createSWRFetcher } from "../../../utils/swrFetcher";
+import { TaskListProps, TasksList } from "./TasksList";
+
+export type APIProjectTasksList = {
+ data: TaskListProps["tasks"];
+};
+
+export type ProjectProps = {
+ id: number;
+ name: string;
+};
+
+export const Project = (props: ProjectProps) => {
+ const { token } = useAuth();
+ const fetcher = createSWRFetcher(token);
+
+ const { data, mutate } = useSWR(
+ `projects/${props.id}/tasks`,
+ fetcher
+ );
+
+ const uncompletedTasks = data?.data.filter((task) => !task.finishedAt) ?? [];
+ const completedTasks = data?.data.filter((task) => task.finishedAt) ?? [];
+
+ return (
+
+
+
+
+ }
+ title={props.name}
+ />
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/pages/Projects/components/TasksList.tsx b/client/src/pages/Projects/components/TasksList.tsx
new file mode 100644
index 0000000..3002711
--- /dev/null
+++ b/client/src/pages/Projects/components/TasksList.tsx
@@ -0,0 +1,54 @@
+import { List, ListSubheader } from "@mui/material";
+import { KeyedMutator } from "swr";
+import { useAuth } from "../../../hooks/useAuth";
+import { APIProjectTasksList } from "./Project";
+import { Task, TasksListItem } from "./TasksListItem";
+
+export type TaskListProps = {
+ projectId: number;
+ title: string;
+ tasks: Task[];
+ mutate: KeyedMutator;
+};
+
+export const TasksList = ({
+ projectId,
+ title,
+ tasks,
+ mutate,
+}: TaskListProps) => {
+ const { apiClient } = useAuth();
+ const handleCheck = (taskId: number) => {
+ apiClient(`projects/${projectId}/tasks/${taskId}/finish`).then((res) => {
+ mutate();
+ });
+ };
+
+ const handleDelete = (taskId: number) => {
+ apiClient(`projects/${projectId}/tasks/${taskId}`, {
+ method: "DELETE",
+ }).then((res) => {
+ mutate();
+ });
+ };
+
+ return (
+
+ );
+};
diff --git a/client/src/pages/Projects/components/TasksListItem.tsx b/client/src/pages/Projects/components/TasksListItem.tsx
new file mode 100644
index 0000000..917ac71
--- /dev/null
+++ b/client/src/pages/Projects/components/TasksListItem.tsx
@@ -0,0 +1,60 @@
+import DeleteIcon from "@mui/icons-material/Delete";
+import {
+ Checkbox,
+ IconButton,
+ ListItem,
+ ListItemAvatar,
+ ListItemText
+} from "@mui/material";
+import { useState } from "react";
+
+export type Task = {
+ id: number;
+ description: string;
+ createdAt: Date;
+ finishedAt?: Date;
+};
+
+export type TasksListItemProps = {
+ task: Task;
+ onCheck: (taskId: number) => void;
+ onDelete: (taskId: number) => void;
+};
+
+export const TasksListItem = ({
+ task,
+ onCheck,
+ onDelete,
+}: TasksListItemProps) => {
+ const finished = !!task.finishedAt;
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleCheck = () => {
+ setIsLoading(true);
+ onCheck(task.id);
+ };
+
+ const handleDelete = () => {
+ setIsLoading(true);
+ onDelete(task.id);
+ };
+
+ const blockInteration = finished || isLoading;
+
+ return (
+
+
+
+ )
+ }
+ >
+
+
+
+
+
+ );
+};
diff --git a/client/src/providers/AuthProvider.tsx b/client/src/providers/AuthProvider.tsx
index d69a6b9..ebad2e8 100644
--- a/client/src/providers/AuthProvider.tsx
+++ b/client/src/providers/AuthProvider.tsx
@@ -46,7 +46,7 @@ export const AuthProvider = ({ children, ...rest }: AuthProviderProps) => {
password,
}),
}).then(async (res) => {
- setToken(await res.json());
+ setToken((await res.json()).token);
setIsLoginDialogOpen(false);
});
},
diff --git a/client/src/utils/swrFetcher.ts b/client/src/utils/swrFetcher.ts
new file mode 100644
index 0000000..5601f2c
--- /dev/null
+++ b/client/src/utils/swrFetcher.ts
@@ -0,0 +1,18 @@
+const host = "http://localhost:5000/";
+
+export const createSWRFetcher =
+ (token: string | null) => async (input: RequestInfo | URL, init?: RequestInit) => {
+ const { headers, ...rest } = init ?? {};
+
+ const customInt: RequestInit = {
+ headers: {
+ "Content-Type": "application/json",
+ "X-Access-Token": token ?? "",
+ ...headers,
+ },
+ ...rest,
+ };
+
+ const res = await fetch(host + input, customInt);
+ return await res.json();
+ };
diff --git a/client/yarn.lock b/client/yarn.lock
index 4cfc0ca..fc0a5f8 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -8484,6 +8484,11 @@ svgo@^2.7.0:
picocolors "^1.0.0"
stable "^0.1.8"
+swr@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8"
+ integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==
+
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"