diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..f5bd20c
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,26 @@
+{
+ "name": "tasker",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "lodash": "^4.17.21"
+ },
+ "devDependencies": {
+ "@types/lodash": "^4.17.13"
+ }
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.17.13",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz",
+ "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==",
+ "dev": true
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..5647d48
--- /dev/null
+++ b/package.json
@@ -0,0 +1,8 @@
+{
+ "dependencies": {
+ "lodash": "^4.17.21"
+ },
+ "devDependencies": {
+ "@types/lodash": "^4.17.13"
+ }
+}
diff --git a/tasker-client/src/app/(components)/Header/index.tsx b/tasker-client/src/app/components/Header/index.tsx
similarity index 100%
rename from tasker-client/src/app/(components)/Header/index.tsx
rename to tasker-client/src/app/components/Header/index.tsx
diff --git a/tasker-client/src/app/components/Modal/index.tsx b/tasker-client/src/app/components/Modal/index.tsx
new file mode 100644
index 0000000..969a929
--- /dev/null
+++ b/tasker-client/src/app/components/Modal/index.tsx
@@ -0,0 +1,38 @@
+import React from "react";
+import ReactDOM from "react-dom";
+import Header from "../Header";
+import { X } from "lucide-react";
+
+type Props = {
+ children: React.ReactNode;
+ isOpen: boolean;
+ onClose: () => void;
+ name: string;
+};
+
+const Modal = ({ children, isOpen, onClose, name }: Props) => {
+ if (!isOpen) return null;
+
+ return ReactDOM.createPortal(
+
+
+
+
+
+ }
+ isSmallText
+ />
+ {children}
+
+
,
+ document.body,
+ );
+};
+
+export default Modal;
diff --git a/tasker-client/src/app/components/ModalNewTask/index.tsx b/tasker-client/src/app/components/ModalNewTask/index.tsx
new file mode 100644
index 0000000..507f7f6
--- /dev/null
+++ b/tasker-client/src/app/components/ModalNewTask/index.tsx
@@ -0,0 +1,169 @@
+import Modal from "@/app/components/Modal";
+import { Priority, Status, useCreateTaskMutation } from "@/state/api";
+import React, { useState } from "react";
+import { formatISO } from "date-fns";
+
+type Props = {
+ isOpen: boolean;
+ onClose: () => void;
+ id?: string | null;
+};
+
+const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => {
+ const [createTask, { isLoading }] = useCreateTaskMutation();
+ const [title, setTitle] = useState("");
+ const [description, setDescription] = useState("");
+ const [status, setStatus] = useState(Status.Backlog);
+ const [priority, setPriority] = useState(Priority.Backlog);
+ const [tags, setTags] = useState("");
+ const [startDate, setStartDate] = useState("");
+ const [dueDate, setDueDate] = useState("");
+ const [authorUserId, setAuthorUserId] = useState("");
+ const [assignedUserId, setAssignedUserId] = useState("");
+ const [projectId, setProjectId] = useState("");
+
+ const handleSubmit = async () => {
+ if (!title || !authorUserId || !(id !== null || projectId)) return;
+
+ const formattedStartDate = formatISO(new Date(startDate), {
+ representation: "complete",
+ });
+ const formattedDueDate = formatISO(new Date(dueDate), {
+ representation: "complete",
+ });
+
+ await createTask({
+ title,
+ description,
+ status,
+ priority,
+ tags,
+ startDate: formattedStartDate,
+ dueDate: formattedDueDate,
+ authorUserId: parseInt(authorUserId),
+ assignedUserId: parseInt(assignedUserId),
+ projectId: id !== null ? Number(id) : Number(projectId),
+ });
+ };
+
+ const isFormValid = () => {
+ return title && authorUserId && !(id !== null || projectId);
+ };
+
+ const selectStyles =
+ "mb-4 block w-full rounded border border-gray-300 px-3 py-2 dark:border-dark-tertiary dark:bg-dark-tertiary dark:text-white dark:focus:outline-none";
+
+ const inputStyles =
+ "w-full rounded border border-gray-300 p-2 shadow-sm dark:border-dark-tertiary dark:bg-dark-tertiary dark:text-white dark:focus:outline-none";
+
+ return (
+
+
+
+ );
+};
+
+export default ModalNewTask;
diff --git a/tasker-client/src/app/(components)/Navbar/index.tsx b/tasker-client/src/app/components/Navbar/index.tsx
similarity index 100%
rename from tasker-client/src/app/(components)/Navbar/index.tsx
rename to tasker-client/src/app/components/Navbar/index.tsx
diff --git a/tasker-client/src/app/components/ProjectCard/index.tsx b/tasker-client/src/app/components/ProjectCard/index.tsx
new file mode 100644
index 0000000..9ebc72b
--- /dev/null
+++ b/tasker-client/src/app/components/ProjectCard/index.tsx
@@ -0,0 +1,19 @@
+import { Project } from "@/state/api";
+import React from "react";
+
+type Props = {
+ project: Project;
+};
+
+const ProjectCard = ({ project }: Props) => {
+ return (
+
+
{project.name}
+
{project.description}
+
Start Date: {project.startDate}
+
End Date: {project.endDate}
+
+ );
+};
+
+export default ProjectCard;
diff --git a/tasker-client/src/app/(components)/Sidebar/index.tsx b/tasker-client/src/app/components/Sidebar/index.tsx
similarity index 100%
rename from tasker-client/src/app/(components)/Sidebar/index.tsx
rename to tasker-client/src/app/components/Sidebar/index.tsx
diff --git a/tasker-client/src/app/components/TaskCard/index.tsx b/tasker-client/src/app/components/TaskCard/index.tsx
new file mode 100644
index 0000000..7b1700f
--- /dev/null
+++ b/tasker-client/src/app/components/TaskCard/index.tsx
@@ -0,0 +1,68 @@
+import { Task } from "@/state/api";
+import { format } from "date-fns";
+import Image from "next/image";
+import React from "react";
+
+type Props = {
+ task: Task;
+};
+
+const TaskCard = ({ task }: Props) => {
+ return (
+
+ {task.attachments && task.attachments.length > 0 && (
+
+
Attachments:
+
+ {task.attachments && task.attachments.length > 0 && (
+
+ )}
+
+
+ )}
+
+ ID: {task.id}
+
+
+ Title: {task.title}
+
+
+ Description:{" "}
+ {task.description || "No description provided"}
+
+
+ Status: {task.status}
+
+
+ Priority: {task.priority}
+
+
+ Tags: {task.tags || "No tags"}
+
+
+ Start Date:{" "}
+ {task.startDate ? format(new Date(task.startDate), "P") : "Not set"}
+
+
+ Due Date:{" "}
+ {task.dueDate ? format(new Date(task.dueDate), "P") : "Not set"}
+
+
+ Author:{" "}
+ {task.author ? task.author.username : "Unknown"}
+
+
+ Assignee:{" "}
+ {task.assignee ? task.assignee.username : "Unassigned"}
+
+
+ );
+};
+
+export default TaskCard;
diff --git a/tasker-client/src/app/components/UserCard/index.tsx b/tasker-client/src/app/components/UserCard/index.tsx
new file mode 100644
index 0000000..f003d0d
--- /dev/null
+++ b/tasker-client/src/app/components/UserCard/index.tsx
@@ -0,0 +1,29 @@
+import { User } from "@/state/api";
+import Image from "next/image";
+import React from "react";
+
+type Props = {
+ user: User;
+};
+
+const UserCard = ({ user }: Props) => {
+ return (
+
+ {user.profilePictureUrl && (
+
+ )}
+
+
{user.username}
+
{user.email}
+
+
+ );
+};
+
+export default UserCard;
diff --git a/tasker-client/src/app/dashboardWrapper.tsx b/tasker-client/src/app/dashboardWrapper.tsx
index 6062e0f..04fda7d 100644
--- a/tasker-client/src/app/dashboardWrapper.tsx
+++ b/tasker-client/src/app/dashboardWrapper.tsx
@@ -1,8 +1,8 @@
"use client";
import React, { useEffect } from "react";
-import Navbar from "@/app/(components)/Navbar";
-import Sidebar from "@/app/(components)/Sidebar";
+import Navbar from "@/app/components/Navbar";
+import Sidebar from "@/app/components/Sidebar";
import StoreProvider, { useAppSelector } from "./redux";
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
diff --git a/tasker-client/src/app/home/page.tsx b/tasker-client/src/app/home/page.tsx
new file mode 100644
index 0000000..6488aca
--- /dev/null
+++ b/tasker-client/src/app/home/page.tsx
@@ -0,0 +1,162 @@
+"use client";
+
+import {
+ Priority,
+ Project,
+ Task,
+ useGetProjectsQuery,
+ useGetTasksQuery,
+} from "@/state/api";
+import React from "react";
+import { useAppSelector } from "../redux";
+import { DataGrid, GridColDef } from "@mui/x-data-grid";
+import Header from "@/app/components/Header";
+import {
+ Bar,
+ BarChart,
+ CartesianGrid,
+ Cell,
+ Legend,
+ Pie,
+ PieChart,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+import { dataGridSxStyles } from "@/lib/utils";
+
+const taskColumns: GridColDef[] = [
+ { field: "title", headerName: "Title", width: 200 },
+ { field: "status", headerName: "Status", width: 150 },
+ { field: "priority", headerName: "Priority", width: 150 },
+ { field: "dueDate", headerName: "Due Date", width: 150 },
+];
+
+const statusColors = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042"];
+
+const HomePage = () => {
+ const {
+ data: tasks,
+ isLoading: tasksLoading,
+ isError: tasksError,
+ } = useGetTasksQuery({ projectId: parseInt("1") });
+ const { data: projects, isLoading: isProjectsLoading } =
+ useGetProjectsQuery();
+
+ const isDarkMode = useAppSelector((state) => state.global.isDarkMode);
+
+ if (tasksLoading || isProjectsLoading) return Loading..
;
+ if (tasksError || !tasks || !projects) return Error fetching data
;
+
+ const priorityCount = tasks.reduce(
+ (acc: Record, task: Task) => {
+ const { priority } = task;
+ acc[priority as Priority] = (acc[priority as Priority] || 0) + 1;
+ return acc;
+ },
+ {},
+ );
+
+ const taskDistribution = Object.keys(priorityCount).map((key) => ({
+ name: key,
+ count: priorityCount[key],
+ }));
+
+ const statusCount = projects.reduce(
+ (acc: Record, project: Project) => {
+ const status = project.endDate ? "Completed" : "Active";
+ acc[status] = (acc[status] || 0) + 1;
+ return acc;
+ },
+ {},
+ );
+
+ const projectStatus = Object.keys(statusCount).map((key) => ({
+ name: key,
+ count: statusCount[key],
+ }));
+
+ const chartColors = isDarkMode
+ ? {
+ bar: "#8884d8",
+ barGrid: "#303030",
+ pieFill: "#4A90E2",
+ text: "#FFFFFF",
+ }
+ : {
+ bar: "#8884d8",
+ barGrid: "#E0E0E0",
+ pieFill: "#82ca9d",
+ text: "#000000",
+ };
+
+ return (
+
+
+
+
+
+ Task Priority Distribution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Project Status
+
+
+
+
+ {projectStatus.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+
+
+
+
+ Your Tasks
+
+
+ "data-grid-row"}
+ getCellClassName={() => "data-grid-cell"}
+ className="border border-gray-200 bg-white shadow dark:border-stroke-dark dark:bg-dark-secondary dark:text-gray-200"
+ sx={dataGridSxStyles(isDarkMode)}
+ />
+
+
+
+
+ );
+};
+
+export default HomePage;
diff --git a/tasker-client/src/app/page.tsx b/tasker-client/src/app/page.tsx
index bf8a85c..ef098f6 100644
--- a/tasker-client/src/app/page.tsx
+++ b/tasker-client/src/app/page.tsx
@@ -1,7 +1,5 @@
+import HomePage from "./home/page";
+
export default function Home() {
- return (
-
- New
-
- );
+ return ;
}
diff --git a/tasker-client/src/app/projects/ListView/index.tsx b/tasker-client/src/app/projects/ListView/index.tsx
new file mode 100644
index 0000000..10bd442
--- /dev/null
+++ b/tasker-client/src/app/projects/ListView/index.tsx
@@ -0,0 +1,44 @@
+import Header from "@/app/components/Header";
+import TaskCard from "@/app/components/TaskCard";
+import { Task, useGetTasksQuery } from "@/state/api";
+import React from "react";
+
+type Props = {
+ id: string;
+ setIsModalNewTaskOpen: (isOpen: boolean) => void;
+};
+
+const ListView = ({ id, setIsModalNewTaskOpen }: Props) => {
+ const {
+ data: tasks,
+ error,
+ isLoading,
+ } = useGetTasksQuery({ projectId: Number(id) });
+
+ if (isLoading) return Loading...
;
+ if (error) return An error occurred while fetching tasks
;
+
+ return (
+
+
+ setIsModalNewTaskOpen(true)}
+ >
+ Add Task
+
+ }
+ isSmallText
+ />
+
+
+ {tasks?.map((task: Task) => )}
+
+
+ );
+};
+
+export default ListView;
diff --git a/tasker-client/src/app/projects/ModalNewProject/index.tsx b/tasker-client/src/app/projects/ModalNewProject/index.tsx
new file mode 100644
index 0000000..834edfd
--- /dev/null
+++ b/tasker-client/src/app/projects/ModalNewProject/index.tsx
@@ -0,0 +1,93 @@
+import Modal from "@/app/components/Modal";
+import { useCreateProjectMutation } from "@/state/api";
+import React, { useState } from "react";
+import { formatISO } from "date-fns";
+
+type Props = {
+ isOpen: boolean;
+ onClose: () => void;
+};
+
+const ModalNewProject = ({ isOpen, onClose }: Props) => {
+ const [createProject, { isLoading }] = useCreateProjectMutation();
+ const [projectName, setProjectName] = useState("");
+ const [description, setDescription] = useState("");
+ const [startDate, setStartDate] = useState("");
+ const [endDate, setEndDate] = useState("");
+
+ const handleSubmit = async () => {
+ if (!projectName || !startDate || !endDate) return;
+
+ const formattedStartDate = formatISO(new Date(startDate), {
+ representation: "complete",
+ });
+ const formattedEndDate = formatISO(new Date(endDate), {
+ representation: "complete",
+ });
+
+ await createProject({
+ name: projectName,
+ description,
+ startDate: formattedStartDate,
+ endDate: formattedEndDate,
+ });
+ };
+
+ const isFormValid = () => {
+ return projectName && description && startDate && endDate;
+ };
+
+ const inputStyles =
+ "w-full rounded border border-gray-300 p-2 shadow-sm dark:border-dark-tertiary dark:bg-dark-tertiary dark:text-white dark:focus:outline-none";
+
+ return (
+
+
+
+ );
+};
+
+export default ModalNewProject;
diff --git a/tasker-client/src/app/projects/ProjectHeader.tsx b/tasker-client/src/app/projects/ProjectHeader.tsx
index d555f59..3709d8f 100644
--- a/tasker-client/src/app/projects/ProjectHeader.tsx
+++ b/tasker-client/src/app/projects/ProjectHeader.tsx
@@ -8,7 +8,8 @@ import {
Table,
} from "lucide-react";
import React, { useState } from "react";
-import Header from "../(components)/Header";
+import Header from "@/app/components/Header";
+import ModalNewProject from "./ModalNewProject";
type Props = {
activeTab: string;
@@ -20,6 +21,10 @@ const ProjectHeader = ({ activeTab, setActiveTab }: Props) => {
return (
+
setIsModalNewProjectOpen(false)}
+ />