diff --git a/apps/app/components/ui/custom-menu.tsx b/apps/app/components/ui/custom-menu.tsx
index 1eacd5ba9..f37d2202f 100644
--- a/apps/app/components/ui/custom-menu.tsx
+++ b/apps/app/components/ui/custom-menu.tsx
@@ -12,9 +12,11 @@ type Props = {
className?: string;
ellipsis?: boolean;
verticalEllipsis?: boolean;
+ height?: "sm" | "md" | "rg" | "lg";
width?: "sm" | "md" | "lg" | "xl" | "auto";
textAlignment?: "left" | "center" | "right";
noBorder?: boolean;
+ noChevron?: boolean;
optionsPosition?: "left" | "right";
customButton?: JSX.Element;
};
@@ -33,9 +35,11 @@ const CustomMenu = ({
className = "",
ellipsis = false,
verticalEllipsis = false,
+ height = "md",
width = "auto",
textAlignment,
noBorder = false,
+ noChevron = false,
optionsPosition = "right",
customButton,
}: Props) => (
@@ -77,7 +81,7 @@ const CustomMenu = ({
}`}
>
{label}
- {!noBorder && }
+ {!noChevron && }
)}
@@ -93,8 +97,18 @@ const CustomMenu = ({
leaveTo="transform opacity-0 scale-95"
>
{
+ const ref = useRef(null);
+
+ const [width, setWidth] = useState(0);
+
+ const router = useRouter();
+ const { workspaceSlug } = router.query;
+
+ const { data: userActivity } = useSWR(
+ workspaceSlug ? USER_ACTIVITY(workspaceSlug as string) : null,
+ workspaceSlug ? () => userService.userActivity(workspaceSlug as string) : null
+ );
+
+ const today = new Date();
+ const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1);
+ const twoMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 2, 1);
+ const threeMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 3, 1);
+ const fourMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 4, 1);
+ const fiveMonthsAgo = new Date(today.getFullYear(), today.getMonth() - 5, 1);
+
+ const recentMonths = [
+ fiveMonthsAgo,
+ fourMonthsAgo,
+ threeMonthsAgo,
+ twoMonthsAgo,
+ lastMonth,
+ today,
+ ];
+
+ const getDatesOfMonth = (dateOfMonth: Date) => {
+ const month = dateOfMonth.getMonth();
+ const year = dateOfMonth.getFullYear();
+
+ const dates = [];
+ const date = new Date(year, month, 1);
+
+ while (date.getMonth() === month && date < new Date()) {
+ dates.push(new Date(date));
+ date.setDate(date.getDate() + 1);
+ }
+
+ return dates;
+ };
+
+ const recentDates = [
+ ...getDatesOfMonth(recentMonths[0]),
+ ...getDatesOfMonth(recentMonths[1]),
+ ...getDatesOfMonth(recentMonths[2]),
+ ...getDatesOfMonth(recentMonths[3]),
+ ...getDatesOfMonth(recentMonths[4]),
+ ...getDatesOfMonth(recentMonths[5]),
+ ];
+
+ const getDatesOnDay = (dates: Date[], day: number) => {
+ const datesOnDay = [];
+
+ for (let i = 0; i < dates.length; i++)
+ if (dates[i].getDay() === day) datesOnDay.push(renderDateFormat(new Date(dates[i])));
+
+ return datesOnDay;
+ };
+
+ const activitiesIntensity = (activityCount: number) => {
+ if (activityCount <= 3) return "opacity-50";
+ else if (activityCount > 3 && activityCount <= 6) return "opacity-70";
+ else if (activityCount > 6 && activityCount <= 9) return "opacity-90";
+ else return "";
+ };
+
+ useEffect(() => {
+ if (!ref.current) return;
+
+ setWidth(ref.current.offsetWidth);
+ }, [ref]);
+
+ return (
+
+
+
+ {DAYS.map((day, index) => (
+
+ {index % 2 === 0 && day.substring(0, 3)}
+
+ ))}
+
+
+
+ {recentMonths.map((month) => (
+
+ {MONTHS[month.getMonth()].substring(0, 3)}
+
+ ))}
+
+
+ {DAYS.map((day, index) => (
+
+ {getDatesOnDay(recentDates, index).map((date) => {
+ const isActive = userActivity?.find((a) => a.created_date === date);
+
+ return (
+
+
+
+ );
+ })}
+
+ ))}
+
+ {/*
+ {recentDates.map((date) => (
+
+ ))}
+
*/}
+
+ Less
+
+
+
+
+
+ More
+
+
+
+
+ );
+};
diff --git a/apps/app/components/workspace/completed-issues-graph.tsx b/apps/app/components/workspace/completed-issues-graph.tsx
new file mode 100644
index 000000000..2a2b132cc
--- /dev/null
+++ b/apps/app/components/workspace/completed-issues-graph.tsx
@@ -0,0 +1,79 @@
+import { useState } from "react";
+
+// recharts
+import {
+ CartesianGrid,
+ Legend,
+ Line,
+ LineChart,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+// ui
+import { CustomMenu } from "components/ui";
+// types
+import { IIssue } from "types";
+// constants
+import { MONTHS } from "constants/project";
+
+type Props = {
+ issues: IIssue[] | undefined;
+};
+
+export const CompletedIssuesGraph: React.FC = ({ issues }) => {
+ const [month, setMonth] = useState(new Date().getMonth());
+
+ const weeks = month === 1 ? 4 : 5;
+
+ const monthIssues =
+ issues?.filter(
+ (i) =>
+ new Date(i.created_at).getMonth() === month &&
+ new Date(i.created_at).getFullYear() === new Date().getFullYear()
+ ) ?? [];
+
+ const data: any[] = [];
+
+ for (let j = 1; j <= weeks; j++) {
+ const weekIssues = monthIssues.filter(
+ (i) => i.completed_at && Math.ceil(new Date(i.completed_at).getDate() / 7) === j
+ );
+
+ data.push({ name: `Week ${j}`, completedIssues: weekIssues.length });
+ }
+
+ return (
+
+
+
Issues closed by you
+ {MONTHS[month]}} noBorder>
+ {MONTHS.map((month, index) => (
+ setMonth(index)}>
+ {month}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/app/components/workspace/home-cards-list.tsx b/apps/app/components/workspace/home-cards-list.tsx
deleted file mode 100644
index cca5989e7..000000000
--- a/apps/app/components/workspace/home-cards-list.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { FC } from "react";
-import { IIssue, IProject } from "types";
-
-export interface WorkspaceHomeCardsListProps {
- groupedIssues: any;
- myIssues: IIssue[];
- projects: IProject[];
-}
-
-export const WorkspaceHomeCardsList: FC = (props) => {
- const { groupedIssues, myIssues, projects } = props;
- const cards = [
- {
- title: "Issues completed",
- number: groupedIssues.completed.length,
- },
- {
- title: "Issues pending",
- number: myIssues.length - groupedIssues.completed.length,
- },
- {
- title: "Projects",
- number: projects?.length ?? 0,
- },
- ];
-
- return (
-
- {cards.map((card, index) => (
-
-
{card.title}
-
{card.number}
-
- ))}
-
- );
-};
diff --git a/apps/app/components/workspace/home-greetings.tsx b/apps/app/components/workspace/home-greetings.tsx
deleted file mode 100644
index f4942eadb..000000000
--- a/apps/app/components/workspace/home-greetings.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-// hooks
-import useUser from "hooks/use-user";
-// ui
-import { Loader } from "components/ui";
-
-export const WorkspaceHomeGreetings = () => {
- // user information
- const { user } = useUser();
-
- const hours = new Date().getHours();
-
- return (
- <>
- {user ? (
-
- Good{" "}
- {hours >= 4 && hours < 12
- ? "Morning"
- : hours >= 12 && hours < 17
- ? "Afternoon"
- : "Evening"}
- , {user.first_name}!
-
- ) : (
-
-
-
- )}
- >
- );
-};
diff --git a/apps/app/components/workspace/index.ts b/apps/app/components/workspace/index.ts
index 645aaed82..db509a70d 100644
--- a/apps/app/components/workspace/index.ts
+++ b/apps/app/components/workspace/index.ts
@@ -1,7 +1,10 @@
+export * from "./activity-graph";
+export * from "./completed-issues-graph";
export * from "./create-workspace-form";
export * from "./delete-workspace-modal";
+export * from "./help-section";
+export * from "./issues-list";
+export * from "./issues-pie-chart";
+export * from "./issues-stats";
export * from "./sidebar-dropdown";
export * from "./sidebar-menu";
-export * from "./help-section";
-export * from "./home-greetings";
-export * from "./home-cards-list";
diff --git a/apps/app/components/workspace/issues-list.tsx b/apps/app/components/workspace/issues-list.tsx
new file mode 100644
index 000000000..e8c7dd0f5
--- /dev/null
+++ b/apps/app/components/workspace/issues-list.tsx
@@ -0,0 +1,105 @@
+import { useRouter } from "next/router";
+import Link from "next/link";
+
+// icons
+import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
+// helpers
+import { renderShortDateWithYearFormat } from "helpers/date-time.helper";
+import { truncateText } from "helpers/string.helper";
+// types
+import { IIssue } from "types";
+import { Loader } from "components/ui";
+import { LayerDiagonalIcon } from "components/icons";
+
+type Props = {
+ issues: IIssue[] | undefined;
+ type: "overdue" | "upcoming";
+};
+
+export const IssuesList: React.FC = ({ issues, type }) => {
+ const router = useRouter();
+ const { workspaceSlug } = router.query;
+
+ const getDateDifference = (date: Date) => {
+ const today = new Date();
+
+ let diffDays = 0;
+ if (type === "overdue") {
+ const diffTime = Math.abs(today.valueOf() - date.valueOf());
+ diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+ } else return date.getDate() - today.getDate();
+
+ return diffDays;
+ };
+
+ return (
+
+
{type} Issues
+ {issues ? (
+
+
+
{type}
+ Issue
+ Due Date
+
+
+
+ ) : (
+
+
+
+ )}
+
+ );
+};
diff --git a/apps/app/components/workspace/issues-pie-chart.tsx b/apps/app/components/workspace/issues-pie-chart.tsx
new file mode 100644
index 000000000..ef0eef461
--- /dev/null
+++ b/apps/app/components/workspace/issues-pie-chart.tsx
@@ -0,0 +1,142 @@
+import { useCallback, useState } from "react";
+
+// recharts
+import { Cell, Legend, Pie, PieChart, ResponsiveContainer, Sector } from "recharts";
+// helpers
+import { groupBy } from "helpers/array.helper";
+// types
+import { IIssue } from "types";
+// constants
+import { STATE_GROUP_COLORS } from "constants/state";
+
+type Props = {
+ issues: IIssue[] | undefined;
+};
+
+export const IssuesPieChart: React.FC = ({ issues }) => {
+ const [activeIndex, setActiveIndex] = useState(0);
+
+ const groupedIssues = {
+ backlog: [],
+ unstarted: [],
+ started: [],
+ cancelled: [],
+ completed: [],
+ ...groupBy(issues ?? [], "state_detail.group"),
+ };
+
+ const data = [
+ {
+ name: "Backlog",
+ value: groupedIssues.backlog.length,
+ },
+ {
+ name: "Unstarted",
+ value: groupedIssues.unstarted.length,
+ },
+ {
+ name: "Started",
+ value: groupedIssues.started.length,
+ },
+ {
+ name: "Cancelled",
+ value: groupedIssues.cancelled.length,
+ },
+ {
+ name: "Completed",
+ value: groupedIssues.completed.length,
+ },
+ ];
+
+ const onPieEnter = useCallback(
+ (_: any, index: number) => {
+ setActiveIndex(index);
+ },
+ [setActiveIndex]
+ );
+
+ const renderActiveShape = ({
+ cx,
+ cy,
+ midAngle,
+ innerRadius,
+ outerRadius,
+ startAngle,
+ endAngle,
+ fill,
+ payload,
+ value,
+ }: any) => {
+ const RADIAN = Math.PI / 180;
+ const sin = Math.sin(-RADIAN * midAngle);
+ const cos = Math.cos(-RADIAN * midAngle);
+ const sx = cx + (outerRadius + 10) * cos;
+ const sy = cy + (outerRadius + 10) * sin;
+ const mx = cx + (outerRadius + 30) * cos;
+ const my = cy + (outerRadius + 30) * sin;
+ const ex = mx + (cos >= 0 ? 1 : -1) * 22;
+ const ey = my;
+ const textAnchor = cos >= 0 ? "start" : "end";
+
+ return (
+
+
+ {payload.name}
+
+
+
+
+
+ = 0 ? 1 : -1) * 12} y={ey} textAnchor={textAnchor} fill="#333">
+ {value} issues
+
+
+ );
+ };
+
+ return (
+
+
Issues by States
+
+
+
+
+ {data.map((cell: any) => (
+ |
+ ))}
+
+
+
+
+
+
+ );
+};
diff --git a/apps/app/components/workspace/issues-stats.tsx b/apps/app/components/workspace/issues-stats.tsx
new file mode 100644
index 000000000..773bc0b5c
--- /dev/null
+++ b/apps/app/components/workspace/issues-stats.tsx
@@ -0,0 +1,54 @@
+// components
+import { ActivityGraph } from "components/workspace";
+// helpers
+import { groupBy } from "helpers/array.helper";
+// types
+import { IIssue } from "types";
+
+type Props = {
+ issues: IIssue[] | undefined;
+};
+
+export const IssuesStats: React.FC = ({ issues }) => {
+ const groupedIssues = {
+ backlog: [],
+ unstarted: [],
+ started: [],
+ cancelled: [],
+ completed: [],
+ ...groupBy(issues ?? [], "state_detail.group"),
+ };
+
+ return (
+
+ {issues ? (
+ <>
+
+
+
Issues assigned to you
+ {issues.length}
+
+
+
Pending issues
+
+ {issues.length - groupedIssues.completed.length}
+
+
+
+
Completed issues
+ {groupedIssues.completed.length}
+
+
+
Issues due by this week
+ 24
+
+
+
+ >
+ ) : null}
+
+ );
+};
diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts
index 4f66ce10e..d7ce1a5be 100644
--- a/apps/app/constants/fetch-keys.ts
+++ b/apps/app/constants/fetch-keys.ts
@@ -51,6 +51,7 @@ export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId}`;
export const STATE_DETAIL = "STATE_DETAILS";
export const USER_ISSUE = (workspaceSlug: string) => `USER_ISSUE_${workspaceSlug}`;
+export const USER_ACTIVITY = (workspaceSlug: string) => `USER_ACTIVITY_${workspaceSlug}`;
export const USER_PROJECT_VIEW = (projectId: string) => `USER_PROJECT_VIEW_${projectId}`;
export const MODULE_LIST = (projectId: string) => `MODULE_LIST_${projectId}`;
diff --git a/apps/app/constants/project.ts b/apps/app/constants/project.ts
index fee47e28d..9ddae96c8 100644
--- a/apps/app/constants/project.ts
+++ b/apps/app/constants/project.ts
@@ -9,3 +9,20 @@ export const GROUP_CHOICES = {
};
export const PRIORITIES = ["urgent", "high", "medium", "low", null];
+
+export const MONTHS = [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+];
+
+export const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
diff --git a/apps/app/constants/state.ts b/apps/app/constants/state.ts
new file mode 100644
index 000000000..565a2c517
--- /dev/null
+++ b/apps/app/constants/state.ts
@@ -0,0 +1,9 @@
+export const STATE_GROUP_COLORS: {
+ [key: string]: string;
+} = {
+ backlog: "#ced4da",
+ unstarted: "#26b5ce",
+ started: "#f7ae59",
+ cancelled: "#d687ff",
+ completed: "#09a953",
+};
diff --git a/apps/app/contexts/user.context.tsx b/apps/app/contexts/user.context.tsx
index 5a5efe0e4..be606bf52 100644
--- a/apps/app/contexts/user.context.tsx
+++ b/apps/app/contexts/user.context.tsx
@@ -1,13 +1,12 @@
import React, { createContext, ReactElement } from "react";
// swr
-import useSWR from "swr";
+import useSWR, { KeyedMutator } from "swr";
// services
import userService from "services/user.service";
// constants
import { CURRENT_USER } from "constants/fetch-keys";
// types
-import type { KeyedMutator } from "swr";
import type { IUser } from "types";
interface IUserContextProps {
diff --git a/apps/app/helpers/date-time.helper.ts b/apps/app/helpers/date-time.helper.ts
index d0596727c..ccced81e3 100644
--- a/apps/app/helpers/date-time.helper.ts
+++ b/apps/app/helpers/date-time.helper.ts
@@ -144,4 +144,4 @@ export const renderShortDate = (date: Date) => {
const day = date.getDate();
const month = months[date.getMonth()];
return isNaN(date.getTime()) ? "N/A" : `${day} ${month}`;
-};
\ No newline at end of file
+};
diff --git a/apps/app/hooks/use-issues.tsx b/apps/app/hooks/use-issues.tsx
index e30a3fea8..33e099676 100644
--- a/apps/app/hooks/use-issues.tsx
+++ b/apps/app/hooks/use-issues.tsx
@@ -14,7 +14,7 @@ const useIssues = (workspaceSlug: string | undefined) => {
);
return {
- myIssues: myIssues || [],
+ myIssues: myIssues,
mutateMyIssues,
};
};
diff --git a/apps/app/pages/[workspaceSlug]/index.tsx b/apps/app/pages/[workspaceSlug]/index.tsx
index 22b6d5596..5990e2292 100644
--- a/apps/app/pages/[workspaceSlug]/index.tsx
+++ b/apps/app/pages/[workspaceSlug]/index.tsx
@@ -1,6 +1,5 @@
import React from "react";
-import Link from "next/link";
import { useRouter } from "next/router";
// lib
@@ -8,227 +7,74 @@ import { requiredAuth } from "lib/auth";
// layouts
import AppLayout from "layouts/app-layout";
// hooks
-import useProjects from "hooks/use-projects";
-import useWorkspaceDetails from "hooks/use-workspace-details";
import useIssues from "hooks/use-issues";
// components
-import { WorkspaceHomeCardsList, WorkspaceHomeGreetings } from "components/workspace";
-// ui
-import { Spinner, Tooltip } from "components/ui";
-// icons
import {
- ArrowRightIcon,
- CalendarDaysIcon,
- ClipboardDocumentListIcon,
-} from "@heroicons/react/24/outline";
-import { LayerDiagonalIcon } from "components/icons";
-import { getPriorityIcon } from "components/icons/priority-icon";
+ CompletedIssuesGraph,
+ IssuesList,
+ IssuesPieChart,
+ IssuesStats,
+} from "components/workspace";
// helpers
-import { renderShortNumericDateFormat, findHowManyDaysLeft } from "helpers/date-time.helper";
-import { addSpaceIfCamelCase } from "helpers/string.helper";
-import { groupBy } from "helpers/array.helper";
+import { orderArrayBy } from "helpers/array.helper";
// types
import type { NextPage, GetServerSidePropsContext } from "next";
const WorkspacePage: NextPage = () => {
- // router
const router = useRouter();
const { workspaceSlug } = router.query;
- // API Fetching
- const { myIssues } = useIssues(workspaceSlug?.toString());
- const { projects, recentProjects } = useProjects();
- const {} = useWorkspaceDetails();
- const groupedIssues = {
- backlog: [],
- unstarted: [],
- started: [],
- cancelled: [],
- completed: [],
- ...groupBy(myIssues ?? [], "state_detail.group"),
- };
+ const { myIssues } = useIssues(workspaceSlug as string);
+
+ const overdueIssues = orderArrayBy(
+ myIssues?.filter(
+ (i) =>
+ i.target_date &&
+ i.target_date !== "" &&
+ !i.completed_at &&
+ new Date(i.target_date) < new Date()
+ ) ?? [],
+ "target_date",
+ "descending"
+ );
+
+ const incomingIssues = orderArrayBy(
+ myIssues?.filter(
+ (i) => i.target_date && i.target_date !== "" && new Date(i.target_date) > new Date()
+ ) ?? [],
+ "target_date"
+ );
return (
-
-
-
-
- {/** TODO: Convert the below mentioned HTML Content to separate component */}
-
-
-
- {myIssues ? (
- myIssues.length > 0 ? (
-
-
-
-
-
-
My Issues
-
{myIssues.length}
-
-
-
- {myIssues.map((issue) => (
-
-
-
-
-
- {getPriorityIcon(issue.priority)}
-
-
-
-
-
-
- {addSpaceIfCamelCase(issue.state_detail.name)}
-
-
-
-
-
- {issue.target_date
- ? renderShortNumericDateFormat(issue.target_date)
- : "N/A"}
-
-
-
-
- ))}
-
-
-
- ) : (
-
-
-
- No issues found. Create a new issue with{" "}
- C
.
-
-
- )
- ) : (
-
-
-
- )}
-
-
-
-
-
-
Recent Projects
-
-
+
+
+
+
Plane is a open source application, to support us you can star us on GitHub!
+
+
+
+
+
+
+
+
diff --git a/apps/app/services/user.service.ts b/apps/app/services/user.service.ts
index 8e4a9f22c..0f0f85e9a 100644
--- a/apps/app/services/user.service.ts
+++ b/apps/app/services/user.service.ts
@@ -1,6 +1,6 @@
// services
import APIService from "services/api.service";
-import type { IUser } from "types";
+import type { IUser, IUserActivity } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
@@ -49,6 +49,14 @@ class UserService extends APIService {
throw error?.response?.data;
});
}
+
+ async userActivity(workspaceSlug: string): Promise
{
+ return this.get(`/api/users/me/workspaces/${workspaceSlug}/activity-graph/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
}
export default new UserService();
diff --git a/apps/app/types/users.d.ts b/apps/app/types/users.d.ts
index 0d4a9ead4..19dac7ed6 100644
--- a/apps/app/types/users.d.ts
+++ b/apps/app/types/users.d.ts
@@ -35,6 +35,11 @@ export interface IUserLite {
created_at: Date;
}
+export interface IUserActivity {
+ created_date: string;
+ activity_count: number;
+}
+
export type UserAuth = {
isMember: boolean;
isOwner: boolean;