diff --git a/client/src/hooks/useProject.ts b/client/src/hooks/useProject.ts new file mode 100644 index 0000000..4e9a9ad --- /dev/null +++ b/client/src/hooks/useProject.ts @@ -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 ."); + } + + return context; +}; diff --git a/client/src/pages/Projects/Projects.tsx b/client/src/pages/Projects/Projects.tsx index 7d3abad..3457d2f 100644 --- a/client/src/pages/Projects/Projects.tsx +++ b/client/src/pages/Projects/Projects.tsx @@ -14,12 +14,17 @@ type APIProjectList = { export const Projects = () => { const { token } = useAuth(); const fetcher = createSWRFetcher(token); - const { data } = useSWR("projects", fetcher); + const { data, mutate } = useSWR("projects", fetcher); return ( - + {data?.data.map((project) => ( - + ))} ); diff --git a/client/src/pages/Projects/components/AddTask.tsx b/client/src/pages/Projects/components/AddTask.tsx index a097d16..a4434e5 100644 --- a/client/src/pages/Projects/components/AddTask.tsx +++ b/client/src/pages/Projects/components/AddTask.tsx @@ -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; -}; - -export const AddTask = ({ projectId, mutate }: AddTaskProps) => { +export const AddTask = () => { const [isLoading, setIsLoading] = useState(false); const { apiClient } = useAuth(); const { register, handleSubmit, reset } = useForm(); + const { tasksMutate, project } = useProject(); const onSubmit: SubmitHandler = (data) => { setIsLoading(true); - apiClient(`projects/${projectId}/tasks`, { + apiClient(`projects/${project.id}/tasks`, { method: "POST", body: JSON.stringify(data), }).then(() => { - mutate(); + tasksMutate(); reset(); setIsLoading(false); }); diff --git a/client/src/pages/Projects/components/DeleteProjectDialog.tsx b/client/src/pages/Projects/components/DeleteProjectDialog.tsx new file mode 100644 index 0000000..449d290 --- /dev/null +++ b/client/src/pages/Projects/components/DeleteProjectDialog.tsx @@ -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) => 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 ( + + Delete project + + + Attention! This is an irreversible action. + + + + + + + + ); +}; diff --git a/client/src/pages/Projects/components/Project.tsx b/client/src/pages/Projects/components/Project.tsx index 0b00481..1804269 100644 --- a/client/src/pages/Projects/components/Project.tsx +++ b/client/src/pages/Projects/components/Project.tsx @@ -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,35 +37,24 @@ export const Project = (props: ProjectProps) => { const completedTasks = data?.data.filter((task) => task.finishedAt) ?? []; return ( - - - + + + } title={props.name} /> + + + + + + + + - } - title={props.name} - /> - - - - - - - - - - - + + + ); }; diff --git a/client/src/pages/Projects/components/ProjectOptions.tsx b/client/src/pages/Projects/components/ProjectOptions.tsx new file mode 100644 index 0000000..1e9b94e --- /dev/null +++ b/client/src/pages/Projects/components/ProjectOptions.tsx @@ -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); + const open = Boolean(anchorEl); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleRenameDialogOpen = () => { + handleClose(); + setRenameDialogOpen(true); + }; + + const handleDeleteDialogOpen = () => { + handleClose(); + setDeleteDialogOption(true); + }; + + return ( + <> + + + + + Rename + + Delete + + + + + ); +}; diff --git a/client/src/pages/Projects/components/RenameProjectDialog.tsx b/client/src/pages/Projects/components/RenameProjectDialog.tsx new file mode 100644 index 0000000..ae07dcc --- /dev/null +++ b/client/src/pages/Projects/components/RenameProjectDialog.tsx @@ -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) => void; +}; + +export const RenameProjectDialog = ({ + open, + setOpen, +}: RenameProjectDialogProps) => { + const [isLoading, setIsLoading] = useState(false); + const { apiClient } = useAuth(); + const { projectMutate, project } = useProject(); + const { register, handleSubmit, reset } = useForm({ + defaultValues: { + name: project.name, + }, + }); + + const handleClose = () => { + reset(); + setOpen(false); + }; + + const onSubmit: SubmitHandler = (data) => { + setIsLoading(true); + + apiClient(`projects/${project.id}`, { + method: "PUT", + body: JSON.stringify({ + name: data.name, + }), + }).then(() => { + projectMutate(); + setIsLoading(false); + handleClose(); + }); + }; + + return ( + +
+ Rename project + + + + + + + +
+
+ ); +}; diff --git a/client/src/pages/Projects/components/TasksList.tsx b/client/src/pages/Projects/components/TasksList.tsx index 3002711..c7abe82 100644 --- a/client/src/pages/Projects/components/TasksList.tsx +++ b/client/src/pages/Projects/components/TasksList.tsx @@ -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; }; -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(); }); }; diff --git a/client/src/providers/ProjectProvider.tsx b/client/src/providers/ProjectProvider.tsx new file mode 100644 index 0000000..16c6916 --- /dev/null +++ b/client/src/providers/ProjectProvider.tsx @@ -0,0 +1,25 @@ +import { createContext } from "react"; + +type ProjectProviderValue = { + project: { + id: number; + name: string; + }; + projectMutate: Function; + tasksMutate: Function; +}; + +export const ProjectContext = createContext(null); + +type ProjectProviderProps = ProjectProviderValue & { + children: React.ReactNode; +}; + +export const ProjectProvider = ({ + children, + ...rest +}: ProjectProviderProps) => { + return ( + {children} + ); +};