add project options
This commit is contained in:
12
client/src/hooks/useProject.ts
Normal file
12
client/src/hooks/useProject.ts
Normal 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;
|
||||
};
|
||||
@@ -14,12 +14,17 @@ type APIProjectList = {
|
||||
export const Projects = () => {
|
||||
const { token } = useAuth();
|
||||
const fetcher = createSWRFetcher(token);
|
||||
const { data } = useSWR<APIProjectList>("projects", fetcher);
|
||||
const { data, mutate } = useSWR<APIProjectList>("projects", fetcher);
|
||||
|
||||
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) => (
|
||||
<Project key={project.id} {...project} />
|
||||
<Project projectMutate={mutate} key={project.id} {...project} />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -2,31 +2,26 @@ import AddTaskIcon from "@mui/icons-material/AddTask";
|
||||
import { Box, Button, TextField, Toolbar } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import { KeyedMutator } from "swr";
|
||||
import { useAuth } from "../../../hooks/useAuth";
|
||||
import { APIProjectTasksList } from "./Project";
|
||||
import { useProject } from "../../../hooks/useProject";
|
||||
|
||||
type NewTaskForm = {
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type AddTaskProps = {
|
||||
projectId: number;
|
||||
mutate: KeyedMutator<APIProjectTasksList>;
|
||||
};
|
||||
|
||||
export const AddTask = ({ projectId, mutate }: AddTaskProps) => {
|
||||
export const AddTask = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { apiClient } = useAuth();
|
||||
const { register, handleSubmit, reset } = useForm<NewTaskForm>();
|
||||
const { tasksMutate, project } = useProject();
|
||||
|
||||
const onSubmit: SubmitHandler<NewTaskForm> = (data) => {
|
||||
setIsLoading(true);
|
||||
apiClient(`projects/${projectId}/tasks`, {
|
||||
apiClient(`projects/${project.id}/tasks`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
}).then(() => {
|
||||
mutate();
|
||||
tasksMutate();
|
||||
reset();
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
58
client/src/pages/Projects/components/DeleteProjectDialog.tsx
Normal file
58
client/src/pages/Projects/components/DeleteProjectDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Add } from "@mui/icons-material";
|
||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||
import {
|
||||
Card,
|
||||
CardActions,
|
||||
@@ -9,9 +8,12 @@ import {
|
||||
} from "@mui/material";
|
||||
import useSWR from "swr";
|
||||
import { useAuth } from "../../../hooks/useAuth";
|
||||
import { ProjectProvider } from "../../../providers/ProjectProvider";
|
||||
import { createSWRFetcher } from "../../../utils/swrFetcher";
|
||||
import { TaskListProps, TasksList } from "./TasksList";
|
||||
import { AddTask } from "./AddTask";
|
||||
import { ProjectOptions } from "./ProjectOptions";
|
||||
import { TaskListProps, TasksList } from "./TasksList";
|
||||
|
||||
export type APIProjectTasksList = {
|
||||
data: TaskListProps["tasks"];
|
||||
};
|
||||
@@ -19,6 +21,7 @@ export type APIProjectTasksList = {
|
||||
export type ProjectProps = {
|
||||
id: number;
|
||||
name: string;
|
||||
projectMutate: Function;
|
||||
};
|
||||
|
||||
export const Project = (props: ProjectProps) => {
|
||||
@@ -34,29 +37,17 @@ export const Project = (props: ProjectProps) => {
|
||||
const completedTasks = data?.data.filter((task) => task.finishedAt) ?? [];
|
||||
|
||||
return (
|
||||
<ProjectProvider
|
||||
project={props}
|
||||
tasksMutate={mutate}
|
||||
projectMutate={props.projectMutate}
|
||||
>
|
||||
<Card sx={{ margin: 4 }}>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton aria-label="settings">
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
}
|
||||
title={props.name}
|
||||
/>
|
||||
<CardHeader action={<ProjectOptions />} title={props.name} />
|
||||
<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}
|
||||
/>
|
||||
<AddTask />
|
||||
<TasksList title="To Do" tasks={uncompletedTasks} />
|
||||
<TasksList title="Done" tasks={completedTasks} />
|
||||
</CardContent>
|
||||
<CardActions disableSpacing>
|
||||
<IconButton aria-label="add task">
|
||||
@@ -64,5 +55,6 @@ export const Project = (props: ProjectProps) => {
|
||||
</IconButton>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</ProjectProvider>
|
||||
);
|
||||
};
|
||||
|
||||
56
client/src/pages/Projects/components/ProjectOptions.tsx
Normal file
56
client/src/pages/Projects/components/ProjectOptions.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
81
client/src/pages/Projects/components/RenameProjectDialog.tsx
Normal file
81
client/src/pages/Projects/components/RenameProjectDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -1,34 +1,28 @@
|
||||
import { List, ListSubheader } from "@mui/material";
|
||||
import { KeyedMutator } from "swr";
|
||||
import { useAuth } from "../../../hooks/useAuth";
|
||||
import { APIProjectTasksList } from "./Project";
|
||||
import { useProject } from "../../../hooks/useProject";
|
||||
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) => {
|
||||
export const TasksList = ({ title, tasks }: TaskListProps) => {
|
||||
const { apiClient } = useAuth();
|
||||
const { tasksMutate, project } = useProject();
|
||||
|
||||
const handleCheck = (taskId: number) => {
|
||||
apiClient(`projects/${projectId}/tasks/${taskId}/finish`).then((res) => {
|
||||
mutate();
|
||||
apiClient(`projects/${project.id}/tasks/${taskId}/finish`).then((res) => {
|
||||
tasksMutate();
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (taskId: number) => {
|
||||
apiClient(`projects/${projectId}/tasks/${taskId}`, {
|
||||
apiClient(`projects/${project.id}/tasks/${taskId}`, {
|
||||
method: "DELETE",
|
||||
}).then((res) => {
|
||||
mutate();
|
||||
tasksMutate();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
25
client/src/providers/ProjectProvider.tsx
Normal file
25
client/src/providers/ProjectProvider.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user