add project options

This commit is contained in:
João Geonizeli
2022-07-10 11:34:40 -03:00
parent 10e448b025
commit 53a31f645c
9 changed files with 276 additions and 58 deletions

View File

@@ -0,0 +1,12 @@
import { useContext } from "react";
import { ProjectContext } from "../providers/ProjectProvider";
export const useProject = () => {
const context = useContext(ProjectContext);
if (context === null) {
throw new Error("You probably forgot to put <ProjectProvider>.");
}
return context;
};

View File

@@ -14,12 +14,17 @@ type APIProjectList = {
export const Projects = () => { export const Projects = () => {
const { token } = useAuth(); const { token } = useAuth();
const fetcher = createSWRFetcher(token); const fetcher = createSWRFetcher(token);
const { data } = useSWR<APIProjectList>("projects", fetcher); const { data, mutate } = useSWR<APIProjectList>("projects", fetcher);
return ( return (
<Box sx={{ display: "grid", gridAutoColumns: 'repeat(auto-fit, minmax(250px, 1fr))' }}> <Box
sx={{
display: "grid",
gridAutoColumns: "repeat(auto-fit, minmax(250px, 1fr))",
}}
>
{data?.data.map((project) => ( {data?.data.map((project) => (
<Project key={project.id} {...project} /> <Project projectMutate={mutate} key={project.id} {...project} />
))} ))}
</Box> </Box>
); );

View File

@@ -2,31 +2,26 @@ import AddTaskIcon from "@mui/icons-material/AddTask";
import { Box, Button, TextField, Toolbar } from "@mui/material"; import { Box, Button, TextField, Toolbar } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form"; import { SubmitHandler, useForm } from "react-hook-form";
import { KeyedMutator } from "swr";
import { useAuth } from "../../../hooks/useAuth"; import { useAuth } from "../../../hooks/useAuth";
import { APIProjectTasksList } from "./Project"; import { useProject } from "../../../hooks/useProject";
type NewTaskForm = { type NewTaskForm = {
description: string; description: string;
}; };
export type AddTaskProps = { export const AddTask = () => {
projectId: number;
mutate: KeyedMutator<APIProjectTasksList>;
};
export const AddTask = ({ projectId, mutate }: AddTaskProps) => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const { register, handleSubmit, reset } = useForm<NewTaskForm>(); const { register, handleSubmit, reset } = useForm<NewTaskForm>();
const { tasksMutate, project } = useProject();
const onSubmit: SubmitHandler<NewTaskForm> = (data) => { const onSubmit: SubmitHandler<NewTaskForm> = (data) => {
setIsLoading(true); setIsLoading(true);
apiClient(`projects/${projectId}/tasks`, { apiClient(`projects/${project.id}/tasks`, {
method: "POST", method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
}).then(() => { }).then(() => {
mutate(); tasksMutate();
reset(); reset();
setIsLoading(false); setIsLoading(false);
}); });

View File

@@ -0,0 +1,58 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle
} from "@mui/material";
import { SetStateAction, useState } from "react";
import { useAuth } from "../../../hooks/useAuth";
import { useProject } from "../../../hooks/useProject";
export type DeleteProjectDialogProps = {
open: boolean;
setOpen: (value: SetStateAction<boolean>) => void;
};
export const DeleteProjectDialog = ({
open,
setOpen,
}: DeleteProjectDialogProps) => {
const [isLoading, setIsLoading] = useState(false);
const { project, projectMutate } = useProject();
const { apiClient } = useAuth();
const handleClose = () => {
setOpen(false);
};
const handleDelete = () => {
setIsLoading(true);
apiClient(`projects/${project.id}`, {
method: "DELETE",
}).then(() => {
projectMutate();
});
};
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Delete project</DialogTitle>
<DialogContent>
<DialogContentText>
Attention! This is an irreversible action.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button disabled={isLoading} onClick={handleClose}>
Cancel
</Button>
<Button disabled={isLoading} onClick={handleDelete} color="error">
Delete
</Button>
</DialogActions>
</Dialog>
);
};

View File

@@ -1,5 +1,4 @@
import { Add } from "@mui/icons-material"; import { Add } from "@mui/icons-material";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import { import {
Card, Card,
CardActions, CardActions,
@@ -9,9 +8,12 @@ import {
} from "@mui/material"; } from "@mui/material";
import useSWR from "swr"; import useSWR from "swr";
import { useAuth } from "../../../hooks/useAuth"; import { useAuth } from "../../../hooks/useAuth";
import { ProjectProvider } from "../../../providers/ProjectProvider";
import { createSWRFetcher } from "../../../utils/swrFetcher"; import { createSWRFetcher } from "../../../utils/swrFetcher";
import { TaskListProps, TasksList } from "./TasksList";
import { AddTask } from "./AddTask"; import { AddTask } from "./AddTask";
import { ProjectOptions } from "./ProjectOptions";
import { TaskListProps, TasksList } from "./TasksList";
export type APIProjectTasksList = { export type APIProjectTasksList = {
data: TaskListProps["tasks"]; data: TaskListProps["tasks"];
}; };
@@ -19,6 +21,7 @@ export type APIProjectTasksList = {
export type ProjectProps = { export type ProjectProps = {
id: number; id: number;
name: string; name: string;
projectMutate: Function;
}; };
export const Project = (props: ProjectProps) => { export const Project = (props: ProjectProps) => {
@@ -34,35 +37,24 @@ export const Project = (props: ProjectProps) => {
const completedTasks = data?.data.filter((task) => task.finishedAt) ?? []; const completedTasks = data?.data.filter((task) => task.finishedAt) ?? [];
return ( return (
<Card sx={{ margin: 4 }}> <ProjectProvider
<CardHeader project={props}
action={ tasksMutate={mutate}
<IconButton aria-label="settings"> projectMutate={props.projectMutate}
<MoreVertIcon /> >
<Card sx={{ margin: 4 }}>
<CardHeader action={<ProjectOptions />} title={props.name} />
<CardContent>
<AddTask />
<TasksList title="To Do" tasks={uncompletedTasks} />
<TasksList title="Done" tasks={completedTasks} />
</CardContent>
<CardActions disableSpacing>
<IconButton aria-label="add task">
<Add />
</IconButton> </IconButton>
} </CardActions>
title={props.name} </Card>
/> </ProjectProvider>
<CardContent>
<AddTask projectId={props.id} mutate={mutate} />
<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>
); );
}; };

View File

@@ -0,0 +1,56 @@
import MoreVertIcon from "@mui/icons-material/MoreVert";
import { Divider, IconButton, Menu, MenuItem } from "@mui/material";
import { useState } from "react";
import { DeleteProjectDialog } from "./DeleteProjectDialog";
import { RenameProjectDialog } from "./RenameProjectDialog";
export const ProjectOptions = () => {
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
const [deleteDialogOption, setDeleteDialogOption] = useState(false);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleRenameDialogOpen = () => {
handleClose();
setRenameDialogOpen(true);
};
const handleDeleteDialogOpen = () => {
handleClose();
setDeleteDialogOption(true);
};
return (
<>
<IconButton onClick={handleClick} aria-label="settings">
<MoreVertIcon />
</IconButton>
<Menu
open={open}
id="basic-menu"
anchorEl={anchorEl}
onClose={handleClose}
>
<MenuItem onClick={handleRenameDialogOpen}>Rename</MenuItem>
<Divider />
<MenuItem onClick={handleDeleteDialogOpen}>Delete</MenuItem>
</Menu>
<RenameProjectDialog
open={renameDialogOpen}
setOpen={setRenameDialogOpen}
/>
<DeleteProjectDialog
open={deleteDialogOption}
setOpen={setDeleteDialogOption}
/>
</>
);
};

View File

@@ -0,0 +1,81 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
} from "@mui/material";
import { SetStateAction, useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import { useAuth } from "../../../hooks/useAuth";
import { useProject } from "../../../hooks/useProject";
type RenameProjectForm = {
name: string;
};
export type RenameProjectDialogProps = {
open: boolean;
setOpen: (value: SetStateAction<boolean>) => void;
};
export const RenameProjectDialog = ({
open,
setOpen,
}: RenameProjectDialogProps) => {
const [isLoading, setIsLoading] = useState(false);
const { apiClient } = useAuth();
const { projectMutate, project } = useProject();
const { register, handleSubmit, reset } = useForm<RenameProjectForm>({
defaultValues: {
name: project.name,
},
});
const handleClose = () => {
reset();
setOpen(false);
};
const onSubmit: SubmitHandler<RenameProjectForm> = (data) => {
setIsLoading(true);
apiClient(`projects/${project.id}`, {
method: "PUT",
body: JSON.stringify({
name: data.name,
}),
}).then(() => {
projectMutate();
setIsLoading(false);
handleClose();
});
};
return (
<Dialog open={open} onClose={handleClose}>
<form onSubmit={handleSubmit(onSubmit)}>
<DialogTitle>Rename project</DialogTitle>
<DialogContent>
<TextField
{...register("name")}
disabled={isLoading}
autoFocus
fullWidth
margin="dense"
variant="standard"
/>
</DialogContent>
<DialogActions>
<Button disabled={isLoading} onClick={handleClose}>
Cancel
</Button>
<Button type="submit" disabled={isLoading}>
Rename
</Button>
</DialogActions>
</form>
</Dialog>
);
};

View File

@@ -1,34 +1,28 @@
import { List, ListSubheader } from "@mui/material"; import { List, ListSubheader } from "@mui/material";
import { KeyedMutator } from "swr";
import { useAuth } from "../../../hooks/useAuth"; import { useAuth } from "../../../hooks/useAuth";
import { APIProjectTasksList } from "./Project"; import { useProject } from "../../../hooks/useProject";
import { Task, TasksListItem } from "./TasksListItem"; import { Task, TasksListItem } from "./TasksListItem";
export type TaskListProps = { export type TaskListProps = {
projectId: number;
title: string; title: string;
tasks: Task[]; tasks: Task[];
mutate: KeyedMutator<APIProjectTasksList>;
}; };
export const TasksList = ({ export const TasksList = ({ title, tasks }: TaskListProps) => {
projectId,
title,
tasks,
mutate,
}: TaskListProps) => {
const { apiClient } = useAuth(); const { apiClient } = useAuth();
const { tasksMutate, project } = useProject();
const handleCheck = (taskId: number) => { const handleCheck = (taskId: number) => {
apiClient(`projects/${projectId}/tasks/${taskId}/finish`).then((res) => { apiClient(`projects/${project.id}/tasks/${taskId}/finish`).then((res) => {
mutate(); tasksMutate();
}); });
}; };
const handleDelete = (taskId: number) => { const handleDelete = (taskId: number) => {
apiClient(`projects/${projectId}/tasks/${taskId}`, { apiClient(`projects/${project.id}/tasks/${taskId}`, {
method: "DELETE", method: "DELETE",
}).then((res) => { }).then((res) => {
mutate(); tasksMutate();
}); });
}; };

View File

@@ -0,0 +1,25 @@
import { createContext } from "react";
type ProjectProviderValue = {
project: {
id: number;
name: string;
};
projectMutate: Function;
tasksMutate: Function;
};
export const ProjectContext = createContext<ProjectProviderValue | null>(null);
type ProjectProviderProps = ProjectProviderValue & {
children: React.ReactNode;
};
export const ProjectProvider = ({
children,
...rest
}: ProjectProviderProps) => {
return (
<ProjectContext.Provider value={rest}>{children}</ProjectContext.Provider>
);
};