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 = () => {
|
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
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 { 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,29 +37,17 @@ export const Project = (props: ProjectProps) => {
|
|||||||
const completedTasks = data?.data.filter((task) => task.finishedAt) ?? [];
|
const completedTasks = data?.data.filter((task) => task.finishedAt) ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ProjectProvider
|
||||||
|
project={props}
|
||||||
|
tasksMutate={mutate}
|
||||||
|
projectMutate={props.projectMutate}
|
||||||
|
>
|
||||||
<Card sx={{ margin: 4 }}>
|
<Card sx={{ margin: 4 }}>
|
||||||
<CardHeader
|
<CardHeader action={<ProjectOptions />} title={props.name} />
|
||||||
action={
|
|
||||||
<IconButton aria-label="settings">
|
|
||||||
<MoreVertIcon />
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
title={props.name}
|
|
||||||
/>
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<AddTask projectId={props.id} mutate={mutate} />
|
<AddTask />
|
||||||
<TasksList
|
<TasksList title="To Do" tasks={uncompletedTasks} />
|
||||||
projectId={props.id}
|
<TasksList title="Done" tasks={completedTasks} />
|
||||||
mutate={mutate}
|
|
||||||
title="To Do"
|
|
||||||
tasks={uncompletedTasks}
|
|
||||||
/>
|
|
||||||
<TasksList
|
|
||||||
projectId={props.id}
|
|
||||||
mutate={mutate}
|
|
||||||
title="Done"
|
|
||||||
tasks={completedTasks}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardActions disableSpacing>
|
<CardActions disableSpacing>
|
||||||
<IconButton aria-label="add task">
|
<IconButton aria-label="add task">
|
||||||
@@ -64,5 +55,6 @@ export const Project = (props: ProjectProps) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
</Card>
|
</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 { 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();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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