add task check and delete
This commit is contained in:
@@ -21,6 +21,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.33.1",
|
"react-hook-form": "^7.33.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"swr": "^1.3.0",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^4.4.2",
|
||||||
"web-vitals": "^2.1.0"
|
"web-vitals": "^2.1.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import "@fontsource/roboto/300.css";
|
|||||||
import "@fontsource/roboto/400.css";
|
import "@fontsource/roboto/400.css";
|
||||||
import "@fontsource/roboto/500.css";
|
import "@fontsource/roboto/500.css";
|
||||||
import "@fontsource/roboto/700.css";
|
import "@fontsource/roboto/700.css";
|
||||||
import { Container } from "@mui/material";
|
import { Box, Container } from "@mui/material";
|
||||||
import { CookiesProvider } from "react-cookie";
|
import { CookiesProvider } from "react-cookie";
|
||||||
import { LoginDialog } from "./components/LoginDialog";
|
import { LoginDialog } from "./components/LoginDialog";
|
||||||
import { Topbar } from "./components/Topbar";
|
import { Topbar } from "./components/Topbar";
|
||||||
@@ -18,10 +18,12 @@ function App() {
|
|||||||
<Topbar />
|
<Topbar />
|
||||||
<LoginDialog />
|
<LoginDialog />
|
||||||
<Container>
|
<Container>
|
||||||
|
<Box marginTop={3}>
|
||||||
<UnautorizedBlock
|
<UnautorizedBlock
|
||||||
protectedContent={<Projects />}
|
protectedContent={<Projects />}
|
||||||
unauthenticatedContent={<NewAccount />}
|
unauthenticatedContent={<NewAccount />}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</CookiesProvider>
|
</CookiesProvider>
|
||||||
|
|||||||
@@ -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 = () => {
|
export const Projects = () => {
|
||||||
|
const { token } = useAuth();
|
||||||
|
const fetcher = createSWRFetcher(token);
|
||||||
|
const { data } = useSWR<APIProjectList>("projects", fetcher);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<></>
|
<Box sx={{ display: "grid", gridAutoColumns: 'repeat(auto-fit, minmax(250px, 1fr))' }}>
|
||||||
)
|
{data?.data.map((project) => (
|
||||||
}
|
<Project key={project.id} {...project} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
67
client/src/pages/Projects/components/Project.tsx
Normal file
67
client/src/pages/Projects/components/Project.tsx
Normal file
@@ -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<APIProjectTasksList>(
|
||||||
|
`projects/${props.id}/tasks`,
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
const uncompletedTasks = data?.data.filter((task) => !task.finishedAt) ?? [];
|
||||||
|
const completedTasks = data?.data.filter((task) => task.finishedAt) ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card sx={{ margin: 4 }}>
|
||||||
|
<CardHeader
|
||||||
|
action={
|
||||||
|
<IconButton aria-label="settings">
|
||||||
|
<MoreVertIcon />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
title={props.name}
|
||||||
|
/>
|
||||||
|
<CardContent>
|
||||||
|
<TasksList
|
||||||
|
projectId={props.id}
|
||||||
|
mutate={mutate}
|
||||||
|
title="To Do"
|
||||||
|
tasks={uncompletedTasks}
|
||||||
|
/>
|
||||||
|
<TasksList
|
||||||
|
projectId={props.id}
|
||||||
|
mutate={mutate}
|
||||||
|
title="Done"
|
||||||
|
tasks={completedTasks}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
<CardActions disableSpacing>
|
||||||
|
<IconButton aria-label="add task">
|
||||||
|
<Add />
|
||||||
|
</IconButton>
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
54
client/src/pages/Projects/components/TasksList.tsx
Normal file
54
client/src/pages/Projects/components/TasksList.tsx
Normal file
@@ -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<APIProjectTasksList>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<List
|
||||||
|
dense
|
||||||
|
subheader={
|
||||||
|
<ListSubheader component="div" id="nested-list-subheader">
|
||||||
|
{title}
|
||||||
|
</ListSubheader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tasks.map((task) => (
|
||||||
|
<TasksListItem
|
||||||
|
key={task.id}
|
||||||
|
task={task}
|
||||||
|
onCheck={handleCheck}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
60
client/src/pages/Projects/components/TasksListItem.tsx
Normal file
60
client/src/pages/Projects/components/TasksListItem.tsx
Normal file
@@ -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 (
|
||||||
|
<ListItem
|
||||||
|
secondaryAction={
|
||||||
|
finished ? null : (
|
||||||
|
<IconButton onClick={handleDelete} disabled={blockInteration} edge="end" aria-label="delete">
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Checkbox onChange={handleCheck} disabled={blockInteration} checked={blockInteration} />
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary="Single-line item" />
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -46,7 +46,7 @@ export const AuthProvider = ({ children, ...rest }: AuthProviderProps) => {
|
|||||||
password,
|
password,
|
||||||
}),
|
}),
|
||||||
}).then(async (res) => {
|
}).then(async (res) => {
|
||||||
setToken(await res.json());
|
setToken((await res.json()).token);
|
||||||
setIsLoginDialogOpen(false);
|
setIsLoginDialogOpen(false);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
18
client/src/utils/swrFetcher.ts
Normal file
18
client/src/utils/swrFetcher.ts
Normal file
@@ -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();
|
||||||
|
};
|
||||||
@@ -8484,6 +8484,11 @@ svgo@^2.7.0:
|
|||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
stable "^0.1.8"
|
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:
|
symbol-tree@^3.2.4:
|
||||||
version "3.2.4"
|
version "3.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||||
|
|||||||
Reference in New Issue
Block a user