diff --git a/client/src/App.tsx b/client/src/App.tsx
index ed36c91..79a006b 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -2,6 +2,8 @@ import { CssBaseline, Typography } from "@material-ui/core";
import { makeStyles, ThemeProvider } from "@material-ui/core/styles";
import { Assignment } from "@material-ui/icons";
import React from "react";
+import { DndProvider } from "react-dnd";
+import { HTML5Backend } from "react-dnd-html5-backend";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import "./App.css";
import Layout from "./Layout";
@@ -34,43 +36,45 @@ function App() {
);
return (
-
-
-
-
-
- ,
- text: "Tasks",
- url: "/tasks",
- },
- ]}
- >
-
-
-
- Test.
-
-
-
-
- Error!
-
-
-
- 404: Not Found
-
-
-
-
+
+
+
+
+
+
+ ,
+ text: "Tasks",
+ url: "/tasks",
+ },
+ ]}
+ >
+
+
+
+ Test.
+
+
+
+
+ Error!
+
+
+
+ 404: Not Found
+
+
+
+
+
-
-
-
+
+
+
);
}
diff --git a/client/src/components/Kanban.tsx b/client/src/components/Kanban.tsx
deleted file mode 100644
index 2fb8de8..0000000
--- a/client/src/components/Kanban.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-import { Avatar, Grid, makeStyles, Paper, Typography } from "@material-ui/core";
-import clsx from "clsx";
-import React from "react";
-
-const useStyles = makeStyles((theme) => ({
- header: {
- padding: theme.spacing(2, 1),
- background:
- theme.palette.type === "light"
- ? theme.palette.background.default
- : theme.palette.background.paper,
- },
- title: {
- ...theme.typography.subtitle1,
- padding: theme.spacing(1),
- textAlign: "center",
- borderRadius: theme.palette.type === "light" ? "0px" : theme.shape.borderRadius,
- },
- title_1: {
- backgroundColor: theme.palette.type === "light" ? "#a8ff78" : "#134e5e",
- background:
- theme.palette.type === "light"
- ? "linear-gradient(to right, #a8ff78, #78ffd6)"
- : "linear-gradient(to right, #134e5e, #71b280)",
- color:
- theme.palette.type === "light"
- ? theme.palette.getContrastText("#a8ff78")
- : theme.palette.getContrastText("#134e5e"),
- },
- title_2: {
- backgroundColor: theme.palette.type === "light" ? "#f85032" : "#333333",
- background:
- theme.palette.type === "light"
- ? "linear-gradient(to right, #f85032, #e73827)"
- : "linear-gradient(to right, #333333, #dd1818)",
- color:
- theme.palette.type === "light"
- ? theme.palette.getContrastText("#f85032")
- : theme.palette.getContrastText("#333333"),
- },
- title_3: {
- backgroundColor: theme.palette.type === "light" ? "#2193b0" : "#373b44",
- background:
- theme.palette.type === "light"
- ? "linear-gradient(to right, #2193b0, #6dd5ed)"
- : "linear-gradient(to right, #373b44, #4286f4)",
- color:
- theme.palette.type === "light"
- ? theme.palette.getContrastText("#2193b0")
- : theme.palette.getContrastText("#373b44"),
- },
- kanban: {
- display: "flex",
- background:
- theme.palette.type === "light"
- ? theme.palette.background.default
- : theme.palette.background.paper,
- flexDirection: "column",
- padding: theme.spacing(0, 2),
- boxShadow: theme.palette.type === "dark" ? theme.shadows[2] : "none",
- },
- sections: {
- flexBasis: 0,
- flexGrow: 1,
- },
- sectionContainer: {
- flexBasis: 0,
- flexGrow: 1,
- display: "flex",
- flexDirection: "column",
- },
- section: {
- flexBasis: 0,
- flexGrow: 1,
- display: "flex",
- flexDirection: "column",
- boxShadow: theme.palette.type === "light" ? theme.shadows[2] : "none",
- borderRadius: theme.palette.type === "light" ? theme.shape.borderRadius : "0px",
- paddingBottom: theme.spacing(2),
- overflow: "hidden",
- },
- member: {
- flexBasis: 0,
- flexGrow: 1,
- overflowX: "auto",
- background: theme.palette.background.paper,
- },
- task: {
- background: theme.palette.background.default,
- padding: theme.spacing(2),
- margin: theme.spacing(1),
- borderRadius:
- theme.palette.type === "light" ? theme.shape.borderRadius : "0px",
- },
- purple: {
- background: "#915AFE",
- },
- blue: {
- background: "#35CDFB",
- },
- taskFooter: {
- display: "flex",
- alignItems: "center",
- justifyContent: "space-between",
- },
-}));
-
-interface KanbanProps {
- className?: string;
-}
-
-function Kanban({ className }: KanbanProps) {
- const classes = useStyles();
-
- const DummyTask = () => {
- const rand = React.useMemo(() => Math.random(), []);
-
- return (
-
- Title
-
- Description.
-
-
-
-
0.5 ? classes.purple : classes.blue}>
- {rand > 0.5 ? "DB" : "RM"}
-
-
- 01.01.2021
-
-
-
- );
- };
-
- return (
-
-
- Kanban
-
-
-
-
-
- To Do
-
-
-
-
-
-
-
-
-
- In Progress
-
-
-
-
-
-
-
-
-
- Done
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default Kanban;
diff --git a/client/src/components/kanban/AddTaskDialog.tsx b/client/src/components/kanban/AddTaskDialog.tsx
new file mode 100644
index 0000000..65e6ebf
--- /dev/null
+++ b/client/src/components/kanban/AddTaskDialog.tsx
@@ -0,0 +1,68 @@
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogProps,
+ DialogTitle,
+ TextField,
+} from "@material-ui/core";
+import React from "react";
+
+interface AddTaskDialogProps extends DialogProps {
+ handleClose: () => void;
+ handleAdd: (title: string, description: string) => void;
+ status: string;
+}
+
+function AddTaskDialog({
+ status,
+ handleClose,
+ handleAdd,
+ ...props
+}: AddTaskDialogProps) {
+ const [title, setTitle] = React.useState("");
+ const [description, setDescription] = React.useState("");
+
+ return (
+
+ );
+}
+
+export default AddTaskDialog;
diff --git a/client/src/components/kanban/Column.tsx b/client/src/components/kanban/Column.tsx
new file mode 100644
index 0000000..f71dd64
--- /dev/null
+++ b/client/src/components/kanban/Column.tsx
@@ -0,0 +1,112 @@
+import {
+ Grid,
+ IconButton,
+ makeStyles,
+ Paper,
+ useMediaQuery,
+ useTheme,
+} from "@material-ui/core";
+import { PlaylistAdd } from "@material-ui/icons";
+import clsx from "clsx";
+import React from "react";
+import AddTaskDialog from "./AddTaskDialog";
+
+const useStyles = makeStyles((theme) => ({
+ title: {
+ ...theme.typography.subtitle1,
+ padding: theme.spacing(1, 1, 1, 3),
+ textAlign: "center",
+ borderRadius:
+ theme.palette.type === "light" ? "0px" : theme.shape.borderRadius,
+ margin: theme.palette.type === "light" ? "0" : theme.spacing(0, 1),
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+ },
+ sectionContainer: {
+ flexBasis: 0,
+ flexGrow: 1,
+ display: "flex",
+ flexDirection: "column",
+ },
+ section: {
+ flexBasis: 0,
+ flexGrow: 1,
+ display: "flex",
+ flexDirection: "column",
+ boxShadow: theme.palette.type === "light" ? theme.shadows[2] : "none",
+ borderRadius:
+ theme.palette.type === "light" ? theme.shape.borderRadius : "0px",
+ paddingBottom: theme.spacing(2),
+ overflow: "hidden",
+ },
+ member: {
+ flexBasis: 0,
+ flexGrow: 1,
+ overflowX: "auto",
+ background: theme.palette.background.paper,
+ display: "flex",
+ flexDirection: "column",
+ padding: theme.spacing(0, 1),
+ marginTop: theme.spacing(1),
+ },
+}));
+
+interface ColumnProps {
+ className?: string;
+ status: string;
+ children: React.ReactNode;
+ addTask: (status: string, title: string, description: string) => void;
+}
+
+function Column({ className, status, children, addTask }: ColumnProps) {
+ const classes = useStyles();
+ const [open, setOpen] = React.useState(false);
+ const theme = useTheme();
+ const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
+
+ const handleClickOpen = () => {
+ setOpen(true);
+ };
+
+ const handleClose = () => {
+ setOpen(false);
+ };
+
+ const handleAdd = (title: string, description: string) => {
+ setOpen(false);
+ addTask(status, title, description);
+ };
+
+ return (
+ <>
+
+
+
+ {status}
+
+
+
+
+
+ {children}
+
+
+
+
+ >
+ );
+}
+
+export default Column;
diff --git a/client/src/components/kanban/DropZone.tsx b/client/src/components/kanban/DropZone.tsx
new file mode 100644
index 0000000..85eb625
--- /dev/null
+++ b/client/src/components/kanban/DropZone.tsx
@@ -0,0 +1,32 @@
+import { makeStyles } from "@material-ui/core";
+import React from "react";
+import { useDrop } from "react-dnd";
+import { TaskItem } from "./Task";
+
+const useStyles = makeStyles((theme) => ({
+ dropZone: {
+ flexGrow: 1,
+ },
+}));
+
+interface DropZoneProps {
+ status: string;
+ setTaskStatus: (id: number, status: string) => void;
+}
+
+function DropZone({ status, setTaskStatus }: DropZoneProps) {
+ const classes = useStyles();
+
+ const ref = React.useRef(null);
+ const [, drop] = useDrop({
+ accept: "card",
+ drop(item) {
+ setTaskStatus(item.id, status);
+ },
+ });
+ drop(ref);
+
+ return ;
+}
+
+export default DropZone;
diff --git a/client/src/components/kanban/Kanban.tsx b/client/src/components/kanban/Kanban.tsx
new file mode 100644
index 0000000..34bb02e
--- /dev/null
+++ b/client/src/components/kanban/Kanban.tsx
@@ -0,0 +1,220 @@
+import { Grid, makeStyles, Paper, Typography } from "@material-ui/core";
+import clsx from "clsx";
+import React from "react";
+import Column from "./Column";
+import DropZone from "./DropZone";
+import Task from "./Task";
+
+const statuses = ["To Do", "In Progress", "Done"];
+
+const defaultTasks = Array.from({ length: 9 }).map((_, index) => {
+ const rand = Math.random();
+ const status = statuses[Math.floor(Math.random() * statuses.length)];
+
+ return {
+ status,
+ index,
+ id: index,
+ title: "Title",
+ description: "Description",
+ date: "01.01.2020",
+ author: rand > 0.5 ? "DM" : "RM",
+ authorColor: rand > 0.5 ? "#915AFE" : "#35CDFB",
+ };
+});
+
+const useStyles = makeStyles((theme) => ({
+ header: {
+ padding: theme.spacing(2, 1),
+ background:
+ theme.palette.type === "light"
+ ? theme.palette.background.default
+ : theme.palette.background.paper,
+ },
+ title_1: {
+ backgroundColor: theme.palette.type === "light" ? "#a8ff78" : "#134e5e",
+ background:
+ theme.palette.type === "light"
+ ? "linear-gradient(to right, #a8ff78, #78ffd6)"
+ : "linear-gradient(to right, #134e5e, #71b280)",
+ color:
+ theme.palette.type === "light"
+ ? theme.palette.getContrastText("#a8ff78")
+ : theme.palette.getContrastText("#134e5e"),
+ },
+ title_2: {
+ backgroundColor: theme.palette.type === "light" ? "#f85032" : "#333333",
+ background:
+ theme.palette.type === "light"
+ ? "linear-gradient(to right, #f85032, #e73827)"
+ : "linear-gradient(to right, #333333, #dd1818)",
+ color:
+ theme.palette.type === "light"
+ ? theme.palette.getContrastText("#f85032")
+ : theme.palette.getContrastText("#333333"),
+ },
+ title_3: {
+ backgroundColor: theme.palette.type === "light" ? "#2193b0" : "#373b44",
+ background:
+ theme.palette.type === "light"
+ ? "linear-gradient(to right, #2193b0, #6dd5ed)"
+ : "linear-gradient(to right, #373b44, #4286f4)",
+ color:
+ theme.palette.type === "light"
+ ? theme.palette.getContrastText("#2193b0")
+ : theme.palette.getContrastText("#373b44"),
+ },
+ kanban: {
+ display: "flex",
+ background:
+ theme.palette.type === "light"
+ ? theme.palette.background.default
+ : theme.palette.background.paper,
+ flexDirection: "column",
+ padding: theme.spacing(0, 2),
+ boxShadow: theme.palette.type === "dark" ? theme.shadows[2] : "none",
+ },
+ sections: {
+ flexBasis: 0,
+ flexGrow: 1,
+ },
+ section: {
+ flexBasis: 0,
+ flexGrow: 1,
+ display: "flex",
+ flexDirection: "column",
+ boxShadow: theme.palette.type === "light" ? theme.shadows[2] : "none",
+ borderRadius:
+ theme.palette.type === "light" ? theme.shape.borderRadius : "0px",
+ paddingBottom: theme.spacing(2),
+ overflow: "hidden",
+ },
+ member: {
+ flexBasis: 0,
+ flexGrow: 1,
+ overflowX: "auto",
+ background: theme.palette.background.paper,
+ },
+}));
+
+interface KanbanProps {
+ className?: string;
+}
+
+function Kanban({ className }: KanbanProps) {
+ const classes = useStyles();
+
+ const [tasks, setTasks] = React.useState(defaultTasks);
+
+ const setTaskStatus = React.useCallback(
+ (id: number, status: string) => {
+ setTasks((tasks) =>
+ tasks.map((task) => {
+ if (task.id === id) {
+ const index =
+ Math.max(
+ -1,
+ ...tasks
+ .filter((task) => task.status === status)
+ .map((t) => t.index)
+ ) + 1;
+ return { ...task, status, index };
+ }
+ return task;
+ })
+ );
+ },
+ [setTasks]
+ );
+
+ const moveTask = React.useCallback(
+ (id: number, status: string, index: number) => {
+ setTasks((tasks) => {
+ const task = tasks.find((t) => t.id === id);
+ const oldIndex = task!.index;
+
+ return tasks.map((task) => {
+ if (task.id === id) {
+ return { ...task, status, index };
+ } else if (task.status === status && oldIndex > index) {
+ if (task.index >= index) {
+ return { ...task, index: task.index + 1 };
+ }
+ } else {
+ if (task.index === index) {
+ return { ...task, index: oldIndex };
+ }
+ }
+ return task;
+ });
+ });
+ },
+ [setTasks]
+ );
+
+ const statusClasses = React.useMemo(
+ () => [classes.title_1, classes.title_2, classes.title_3],
+ [classes]
+ );
+
+ const addTask = React.useCallback(
+ (status: string, title: string, description: string) => {
+ setTasks((tasks) => {
+ const rand = Math.random();
+ const id = Math.max(-1, ...tasks.map((task) => task.id)) + 1;
+ const index =
+ Math.max(
+ -1,
+ ...tasks
+ .filter((task) => task.status === status)
+ .map((task) => task.index)
+ ) + 1;
+
+ return [
+ ...tasks,
+ {
+ id,
+ index,
+ status,
+ title,
+ description,
+ date: new Date().toLocaleDateString(),
+ author: rand > 0.5 ? "DM" : "RM",
+ authorColor: rand > 0.5 ? "#915AFE" : "#35CDFB",
+ },
+ ];
+ });
+ },
+ [setTasks]
+ );
+
+ return (
+
+
+ Kanban
+
+
+ {statuses.map((status, index) => (
+
+ <>
+ {tasks
+ .filter((task) => task.status === status)
+ .sort((task1, task2) => (task1.index < task2.index ? -1 : 1))
+ .map((task) => (
+
+ ))}
+
+ >
+
+ ))}
+
+
+ );
+}
+
+export default Kanban;
diff --git a/client/src/components/kanban/Task.tsx b/client/src/components/kanban/Task.tsx
new file mode 100644
index 0000000..e9dcd24
--- /dev/null
+++ b/client/src/components/kanban/Task.tsx
@@ -0,0 +1,98 @@
+import {
+ Avatar,
+ makeStyles,
+ Paper,
+ Theme,
+ Typography,
+} from "@material-ui/core";
+import React from "react";
+import { useDrag, useDrop } from "react-dnd";
+
+interface StyleProps {
+ authorColor: string;
+ isDragging: boolean;
+}
+
+const useStyles = makeStyles((theme) => ({
+ task: {
+ background: theme.palette.background.default,
+ padding: theme.spacing(2),
+ marginBottom: theme.spacing(1),
+ borderRadius:
+ theme.palette.type === "light" ? theme.shape.borderRadius : "0px",
+ opacity: (props) => (props.isDragging ? 0 : 1),
+ cursor: "move",
+ },
+ avatar: {
+ background: (props) => props.authorColor,
+ },
+ taskFooter: {
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+ },
+}));
+
+export interface ITask {
+ id: number;
+ title: string;
+ description: string;
+ date: string;
+ author: string;
+ authorColor: string;
+ status: string;
+ index: number;
+}
+
+export interface TaskItem extends ITask {
+ type: "card";
+}
+
+interface TaskProps {
+ moveTask: (id: number, status: string, index: number) => void;
+}
+
+function Task({ moveTask, ...task }: TaskProps & ITask) {
+ const ref = React.useRef(null);
+ const [, drop] = useDrop({
+ accept: "card",
+ hover(item: TaskItem) {
+ if (!ref.current) {
+ return;
+ }
+
+ if (item.id !== task.id) {
+ moveTask(item.id, task.status, task.index);
+ }
+ },
+ });
+
+ const [{ isDragging }, drag] = useDrag({
+ item: { ...task, type: "card" },
+ collect: (monitor) => ({
+ isDragging: monitor.isDragging(),
+ }),
+ });
+
+ drag(drop(ref));
+
+ const classes = useStyles({ authorColor: task.authorColor, isDragging });
+
+ return (
+
+ {task.title}
+
+ {task.description}
+
+
+
+
{task.author}
+
+ {task.date}
+
+
+
+ );
+}
+
+export default Task;
diff --git a/client/src/pages/Project.tsx b/client/src/pages/Project.tsx
index 4bfb3bd..4ff5149 100644
--- a/client/src/pages/Project.tsx
+++ b/client/src/pages/Project.tsx
@@ -7,7 +7,7 @@ import {
} from "@material-ui/core";
import clsx from "clsx";
import React from "react";
-import Kanban from "../components/Kanban";
+import Kanban from "../components/kanban/Kanban";
const useStyles = makeStyles((theme) => ({
sidebar: {