diff --git a/apps/space/.env.example b/apps/space/.env.example index 7cecf3739..4fb0e4df6 100644 --- a/apps/space/.env.example +++ b/apps/space/.env.example @@ -1 +1 @@ -NEXT_PUBLIC_VERCEL_ENV=local \ No newline at end of file +NEXT_PUBLIC_API_BASE_URL='' \ No newline at end of file diff --git a/apps/space/app/[workspace_slug]/[project_slug]/layout.tsx b/apps/space/app/[workspace_slug]/[project_slug]/layout.tsx new file mode 100644 index 000000000..8cc3ee8c8 --- /dev/null +++ b/apps/space/app/[workspace_slug]/[project_slug]/layout.tsx @@ -0,0 +1,33 @@ +"use client"; + +// next imports +import Link from "next/link"; +import Image from "next/image"; +// components +import IssueNavbar from "components/issues/navbar"; +import IssueFilter from "components/issues/filters-render"; + +const RootLayout = ({ children }: { children: React.ReactNode }) => ( +
+
+ +
+ {/*
+ +
*/} +
{children}
+ +
+ +
+ plane logo +
+
+ Powered by Plane Deploy +
+ +
+
+); + +export default RootLayout; diff --git a/apps/space/app/[workspace_slug]/[project_slug]/page.tsx b/apps/space/app/[workspace_slug]/[project_slug]/page.tsx new file mode 100644 index 000000000..0aa9b164d --- /dev/null +++ b/apps/space/app/[workspace_slug]/[project_slug]/page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useEffect } from "react"; +// next imports +import { useRouter, useParams, useSearchParams } from "next/navigation"; +// mobx +import { observer } from "mobx-react-lite"; +// components +import { IssueListView } from "components/issues/board-views/list"; +import { IssueKanbanView } from "components/issues/board-views/kanban"; +import { IssueCalendarView } from "components/issues/board-views/calendar"; +import { IssueSpreadsheetView } from "components/issues/board-views/spreadsheet"; +import { IssueGanttView } from "components/issues/board-views/gantt"; +// mobx store +import { RootStore } from "store/root"; +import { useMobxStore } from "lib/mobx/store-provider"; +// types +import { TIssueBoardKeys } from "store/types"; + +const WorkspaceProjectPage = observer(() => { + const store: RootStore = useMobxStore(); + + const router = useRouter(); + const routerParams = useParams(); + const routerSearchparams = useSearchParams(); + + const { workspace_slug, project_slug } = routerParams as { workspace_slug: string; project_slug: string }; + const board = routerSearchparams.get("board") as TIssueBoardKeys | ""; + + // updating default board view when we are in the issues page + useEffect(() => { + if (workspace_slug && project_slug) { + if (!board) { + store.issue.setCurrentIssueBoardView("list"); + router.replace(`/${workspace_slug}/${project_slug}?board=${store?.issue?.currentIssueBoardView}`); + } else { + if (board != store?.issue?.currentIssueBoardView) store.issue.setCurrentIssueBoardView(board); + } + } + }, [workspace_slug, project_slug, board, router, store?.issue]); + + useEffect(() => { + if (workspace_slug && project_slug) { + store?.project?.getProjectSettingsAsync(workspace_slug, project_slug); + store?.issue?.getIssuesAsync(workspace_slug, project_slug); + } + }, [workspace_slug, project_slug, store?.project, store?.issue]); + + return ( +
+ {store?.issue?.loader && !store.issue.issues ? ( +
Loading...
+ ) : ( + <> + {store?.issue?.error ? ( +
Something went wrong.
+ ) : ( + store?.issue?.currentIssueBoardView && ( + <> + {store?.issue?.currentIssueBoardView === "list" && ( +
+
+ +
+
+ )} + {store?.issue?.currentIssueBoardView === "kanban" && ( +
+ +
+ )} + {store?.issue?.currentIssueBoardView === "calendar" && } + {store?.issue?.currentIssueBoardView === "spreadsheet" && } + {store?.issue?.currentIssueBoardView === "gantt" && } + + ) + )} + + )} +
+ ); +}); + +export default WorkspaceProjectPage; diff --git a/apps/space/app/[workspace_project_slug]/page.tsx b/apps/space/app/[workspace_slug]/page.tsx similarity index 60% rename from apps/space/app/[workspace_project_slug]/page.tsx rename to apps/space/app/[workspace_slug]/page.tsx index 638d36e77..c35662f5a 100644 --- a/apps/space/app/[workspace_project_slug]/page.tsx +++ b/apps/space/app/[workspace_slug]/page.tsx @@ -1,9 +1,7 @@ -import React from "react"; +"use client"; const WorkspaceProjectPage = () => ( -
- Plane Workspace project Space -
+
Plane Workspace Space
); export default WorkspaceProjectPage; diff --git a/apps/space/app/layout.tsx b/apps/space/app/layout.tsx index 5c7de32ff..b63f748e8 100644 --- a/apps/space/app/layout.tsx +++ b/apps/space/app/layout.tsx @@ -1,10 +1,18 @@ +"use client"; + // root styles import "styles/globals.css"; +// mobx store provider +import { MobxStoreProvider } from "lib/mobx/store-provider"; +import MobxStoreInit from "lib/mobx/store-init"; const RootLayout = ({ children }: { children: React.ReactNode }) => ( -
{children}
+ + +
{children}
+
); diff --git a/apps/space/app/page.tsx b/apps/space/app/page.tsx index bbfe9c3ea..c1b2926b3 100644 --- a/apps/space/app/page.tsx +++ b/apps/space/app/page.tsx @@ -1,3 +1,5 @@ +"use client"; + import React from "react"; const HomePage = () => ( diff --git a/apps/space/components/icons/index.ts b/apps/space/components/icons/index.ts new file mode 100644 index 000000000..5f23e0f3a --- /dev/null +++ b/apps/space/components/icons/index.ts @@ -0,0 +1,5 @@ +export * from "./issue-group/backlog-state-icon"; +export * from "./issue-group/unstarted-state-icon"; +export * from "./issue-group/started-state-icon"; +export * from "./issue-group/completed-state-icon"; +export * from "./issue-group/cancelled-state-icon"; diff --git a/apps/space/components/icons/issue-group/backlog-state-icon.tsx b/apps/space/components/icons/issue-group/backlog-state-icon.tsx new file mode 100644 index 000000000..f2f62d24a --- /dev/null +++ b/apps/space/components/icons/issue-group/backlog-state-icon.tsx @@ -0,0 +1,23 @@ +import React from "react"; +// types +import type { Props } from "../types"; +// constants +import { issueGroupColors } from "constants/data"; + +export const BacklogStateIcon: React.FC = ({ + width = "14", + height = "14", + className, + color = issueGroupColors["backlog"], +}) => ( + + + +); diff --git a/apps/space/components/icons/issue-group/cancelled-state-icon.tsx b/apps/space/components/icons/issue-group/cancelled-state-icon.tsx new file mode 100644 index 000000000..e244c191a --- /dev/null +++ b/apps/space/components/icons/issue-group/cancelled-state-icon.tsx @@ -0,0 +1,74 @@ +import React from "react"; +// types +import type { Props } from "../types"; +// constants +import { issueGroupColors } from "constants/data"; + +export const CancelledStateIcon: React.FC = ({ + width = "14", + height = "14", + className, + color = issueGroupColors["cancelled"], +}) => ( + + + + + + + + + + + + + +); diff --git a/apps/space/components/icons/issue-group/completed-state-icon.tsx b/apps/space/components/icons/issue-group/completed-state-icon.tsx new file mode 100644 index 000000000..417ebbf3f --- /dev/null +++ b/apps/space/components/icons/issue-group/completed-state-icon.tsx @@ -0,0 +1,65 @@ +import React from "react"; +// types +import type { Props } from "../types"; +// constants +import { issueGroupColors } from "constants/data"; + +export const CompletedStateIcon: React.FC = ({ + width = "14", + height = "14", + className, + color = issueGroupColors["completed"], +}) => ( + + + + + + + + + + + + +); diff --git a/apps/space/components/icons/issue-group/started-state-icon.tsx b/apps/space/components/icons/issue-group/started-state-icon.tsx new file mode 100644 index 000000000..4ebd1771f --- /dev/null +++ b/apps/space/components/icons/issue-group/started-state-icon.tsx @@ -0,0 +1,73 @@ +import React from "react"; +// types +import type { Props } from "../types"; +// constants +import { issueGroupColors } from "constants/data"; + +export const StartedStateIcon: React.FC = ({ + width = "14", + height = "14", + className, + color = issueGroupColors["started"], +}) => ( + + + + + + + + + + + + +); diff --git a/apps/space/components/icons/issue-group/unstarted-state-icon.tsx b/apps/space/components/icons/issue-group/unstarted-state-icon.tsx new file mode 100644 index 000000000..f79bc00fc --- /dev/null +++ b/apps/space/components/icons/issue-group/unstarted-state-icon.tsx @@ -0,0 +1,55 @@ +import React from "react"; +// types +import type { Props } from "../types"; +// constants +import { issueGroupColors } from "constants/data"; + +export const UnstartedStateIcon: React.FC = ({ + width = "14", + height = "14", + className, + color = issueGroupColors["unstarted"], +}) => ( + + + + + + + + + + +); diff --git a/apps/space/components/icons/types.d.ts b/apps/space/components/icons/types.d.ts new file mode 100644 index 000000000..f82a18147 --- /dev/null +++ b/apps/space/components/icons/types.d.ts @@ -0,0 +1,6 @@ +export type Props = { + width?: string | number; + height?: string | number; + color?: string; + className?: string; +}; diff --git a/apps/space/components/issues/board-views/block-due-date.tsx b/apps/space/components/issues/board-views/block-due-date.tsx new file mode 100644 index 000000000..6d3cc3cc0 --- /dev/null +++ b/apps/space/components/issues/board-views/block-due-date.tsx @@ -0,0 +1,32 @@ +"use client"; + +// helpers +import { renderDateFormat } from "constants/helpers"; + +export const findHowManyDaysLeft = (date: string | Date) => { + const today = new Date(); + const eventDate = new Date(date); + const timeDiff = Math.abs(eventDate.getTime() - today.getTime()); + return Math.ceil(timeDiff / (1000 * 3600 * 24)); +}; + +const validDate = (date: any, state: string): string => { + if (date === null || ["backlog", "unstarted", "cancelled"].includes(state)) + return `bg-gray-500/10 text-gray-500 border-gray-500/50`; + else { + const today = new Date(); + const dueDate = new Date(date); + + if (dueDate < today) return `bg-red-500/10 text-red-500 border-red-500/50`; + else return `bg-green-500/10 text-green-500 border-green-500/50`; + } +}; + +export const IssueBlockDueDate = ({ due_date, state }: any) => ( +
+ {renderDateFormat(due_date)} +
+); diff --git a/apps/space/components/issues/board-views/block-labels.tsx b/apps/space/components/issues/board-views/block-labels.tsx new file mode 100644 index 000000000..90cc1629c --- /dev/null +++ b/apps/space/components/issues/board-views/block-labels.tsx @@ -0,0 +1,17 @@ +"use client"; + +export const IssueBlockLabels = ({ labels }: any) => ( +
+ {labels && + labels.length > 0 && + labels.map((_label: any) => ( +
+
+
{_label?.name}
+
+ ))} +
+); diff --git a/apps/space/components/issues/board-views/block-priority.tsx b/apps/space/components/issues/board-views/block-priority.tsx new file mode 100644 index 000000000..61ca50765 --- /dev/null +++ b/apps/space/components/issues/board-views/block-priority.tsx @@ -0,0 +1,17 @@ +"use client"; + +// types +import { TIssuePriorityKey } from "store/types/issue"; +// constants +import { issuePriorityFilter } from "constants/data"; + +export const IssueBlockPriority = ({ priority }: { priority: TIssuePriorityKey | null }) => { + const priority_detail = priority != null ? issuePriorityFilter(priority) : null; + + if (priority_detail === null) return <>; + return ( +
+ {priority_detail?.icon} +
+ ); +}; diff --git a/apps/space/components/issues/board-views/block-state.tsx b/apps/space/components/issues/board-views/block-state.tsx new file mode 100644 index 000000000..87cd65938 --- /dev/null +++ b/apps/space/components/issues/board-views/block-state.tsx @@ -0,0 +1,18 @@ +"use client"; + +// constants +import { issueGroupFilter } from "constants/data"; + +export const IssueBlockState = ({ state }: any) => { + const stateGroup = issueGroupFilter(state.group); + + if (stateGroup === null) return <>; + return ( +
+ +
{state?.name}
+
+ ); +}; diff --git a/apps/space/components/issues/board-views/calendar/index.tsx b/apps/space/components/issues/board-views/calendar/index.tsx new file mode 100644 index 000000000..0edeca96c --- /dev/null +++ b/apps/space/components/issues/board-views/calendar/index.tsx @@ -0,0 +1 @@ +export const IssueCalendarView = () =>
; diff --git a/apps/space/components/issues/board-views/gantt/index.tsx b/apps/space/components/issues/board-views/gantt/index.tsx new file mode 100644 index 000000000..5da924b2c --- /dev/null +++ b/apps/space/components/issues/board-views/gantt/index.tsx @@ -0,0 +1 @@ +export const IssueGanttView = () =>
; diff --git a/apps/space/components/issues/board-views/kanban/block.tsx b/apps/space/components/issues/board-views/kanban/block.tsx new file mode 100644 index 000000000..304e05612 --- /dev/null +++ b/apps/space/components/issues/board-views/kanban/block.tsx @@ -0,0 +1,57 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// components +import { IssueBlockPriority } from "components/issues/board-views/block-priority"; +import { IssueBlockState } from "components/issues/board-views/block-state"; +import { IssueBlockLabels } from "components/issues/board-views/block-labels"; +import { IssueBlockDueDate } from "components/issues/board-views/block-due-date"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +// interfaces +import { IIssue } from "store/types/issue"; +import { RootStore } from "store/root"; + +export const IssueListBlock = ({ issue }: { issue: IIssue }) => { + const store: RootStore = useMobxStore(); + + return ( +
+ {/* id */} +
+ {store?.project?.project?.identifier}-{issue?.sequence_id} +
+ + {/* name */} +
{issue.name}
+ + {/* priority */} +
+ {issue?.priority && ( +
+ +
+ )} + {/* state */} + {issue?.state_detail && ( +
+ +
+ )} + {/* labels */} + {issue?.label_details && issue?.label_details.length > 0 && ( +
+ +
+ )} + {/* due date */} + {issue?.target_date && ( +
+ +
+ )} +
+
+ ); +}; diff --git a/apps/space/components/issues/board-views/kanban/header.tsx b/apps/space/components/issues/board-views/kanban/header.tsx new file mode 100644 index 000000000..43c19f5f5 --- /dev/null +++ b/apps/space/components/issues/board-views/kanban/header.tsx @@ -0,0 +1,31 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// interfaces +import { IIssueState } from "store/types/issue"; +// constants +import { issueGroupFilter } from "constants/data"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const IssueListHeader = observer(({ state }: { state: IIssueState }) => { + const store: RootStore = useMobxStore(); + + const stateGroup = issueGroupFilter(state.group); + + if (stateGroup === null) return <>; + + return ( +
+
+ +
+
{state?.name}
+
+ {store.issue.getCountOfIssuesByState(state.id)} +
+
+ ); +}); diff --git a/apps/space/components/issues/board-views/kanban/index.tsx b/apps/space/components/issues/board-views/kanban/index.tsx new file mode 100644 index 000000000..d716356ff --- /dev/null +++ b/apps/space/components/issues/board-views/kanban/index.tsx @@ -0,0 +1,44 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// components +import { IssueListHeader } from "components/issues/board-views/kanban/header"; +import { IssueListBlock } from "components/issues/board-views/kanban/block"; +// interfaces +import { IIssueState, IIssue } from "store/types/issue"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const IssueKanbanView = observer(() => { + const store: RootStore = useMobxStore(); + + return ( +
+ {store?.issue?.states && + store?.issue?.states.length > 0 && + store?.issue?.states.map((_state: IIssueState) => ( +
+
+ +
+
+ {store.issue.getFilteredIssuesByState(_state.id) && + store.issue.getFilteredIssuesByState(_state.id).length > 0 ? ( +
+ {store.issue.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => ( + + ))} +
+ ) : ( +
+ No Issues are available. +
+ )} +
+
+ ))} +
+ ); +}); diff --git a/apps/space/components/issues/board-views/list/block.tsx b/apps/space/components/issues/board-views/list/block.tsx new file mode 100644 index 000000000..b9dfcc6ab --- /dev/null +++ b/apps/space/components/issues/board-views/list/block.tsx @@ -0,0 +1,59 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// components +import { IssueBlockPriority } from "components/issues/board-views/block-priority"; +import { IssueBlockState } from "components/issues/board-views/block-state"; +import { IssueBlockLabels } from "components/issues/board-views/block-labels"; +import { IssueBlockDueDate } from "components/issues/board-views/block-due-date"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +// interfaces +import { IIssue } from "store/types/issue"; +import { RootStore } from "store/root"; + +export const IssueListBlock = ({ issue }: { issue: IIssue }) => { + const store: RootStore = useMobxStore(); + + return ( +
+
+ {/* id */} +
+ {store?.project?.project?.identifier}-{issue?.sequence_id} +
+ {/* name */} +
{issue.name}
+
+ + {/* priority */} + {issue?.priority && ( +
+ +
+ )} + + {/* state */} + {issue?.state_detail && ( +
+ +
+ )} + + {/* labels */} + {issue?.label_details && issue?.label_details.length > 0 && ( +
+ +
+ )} + + {/* due date */} + {issue?.target_date && ( +
+ +
+ )} +
+ ); +}; diff --git a/apps/space/components/issues/board-views/list/header.tsx b/apps/space/components/issues/board-views/list/header.tsx new file mode 100644 index 000000000..e87cac6f7 --- /dev/null +++ b/apps/space/components/issues/board-views/list/header.tsx @@ -0,0 +1,31 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// interfaces +import { IIssueState } from "store/types/issue"; +// constants +import { issueGroupFilter } from "constants/data"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const IssueListHeader = observer(({ state }: { state: IIssueState }) => { + const store: RootStore = useMobxStore(); + + const stateGroup = issueGroupFilter(state.group); + + if (stateGroup === null) return <>; + + return ( +
+
+ +
+
{state?.name}
+
+ {store.issue.getCountOfIssuesByState(state.id)} +
+
+ ); +}); diff --git a/apps/space/components/issues/board-views/list/index.tsx b/apps/space/components/issues/board-views/list/index.tsx new file mode 100644 index 000000000..7a7ec0de1 --- /dev/null +++ b/apps/space/components/issues/board-views/list/index.tsx @@ -0,0 +1,38 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// components +import { IssueListHeader } from "components/issues/board-views/list/header"; +import { IssueListBlock } from "components/issues/board-views/list/block"; +// interfaces +import { IIssueState, IIssue } from "store/types/issue"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const IssueListView = observer(() => { + const store: RootStore = useMobxStore(); + + return ( + <> + {store?.issue?.states && + store?.issue?.states.length > 0 && + store?.issue?.states.map((_state: IIssueState) => ( +
+ + {store.issue.getFilteredIssuesByState(_state.id) && + store.issue.getFilteredIssuesByState(_state.id).length > 0 ? ( +
+ {store.issue.getFilteredIssuesByState(_state.id).map((_issue: IIssue) => ( + + ))} +
+ ) : ( +
No Issues are available.
+ )} +
+ ))} + + ); +}); diff --git a/apps/space/components/issues/board-views/spreadsheet/index.tsx b/apps/space/components/issues/board-views/spreadsheet/index.tsx new file mode 100644 index 000000000..45ebf2792 --- /dev/null +++ b/apps/space/components/issues/board-views/spreadsheet/index.tsx @@ -0,0 +1 @@ +export const IssueSpreadsheetView = () =>
; diff --git a/apps/space/components/issues/filters-render/date.tsx b/apps/space/components/issues/filters-render/date.tsx new file mode 100644 index 000000000..e01d0ae58 --- /dev/null +++ b/apps/space/components/issues/filters-render/date.tsx @@ -0,0 +1,38 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; + +const IssueDateFilter = observer(() => { + const store = useMobxStore(); + + return ( + <> +
+
Due Date
+
+ {/*
+
+ close +
+
Backlog
+
+ close +
+
*/} +
+
+ close +
+
+ + ); +}); + +export default IssueDateFilter; diff --git a/apps/space/components/issues/filters-render/index.tsx b/apps/space/components/issues/filters-render/index.tsx new file mode 100644 index 000000000..366ae1030 --- /dev/null +++ b/apps/space/components/issues/filters-render/index.tsx @@ -0,0 +1,40 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// components +import IssueStateFilter from "./state"; +import IssueLabelFilter from "./label"; +import IssuePriorityFilter from "./priority"; +import IssueDateFilter from "./date"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +const IssueFilter = observer(() => { + const store: RootStore = useMobxStore(); + + const clearAllFilters = () => {}; + + return ( +
+ {/* state */} + {store?.issue?.states && } + {/* labels */} + {store?.issue?.labels && } + {/* priority */} + + {/* due date */} + + {/* clear all filters */} +
+
Clear all filters
+
+
+ ); +}); + +export default IssueFilter; diff --git a/apps/space/components/issues/filters-render/label/filter-label-block.tsx b/apps/space/components/issues/filters-render/label/filter-label-block.tsx new file mode 100644 index 000000000..0606bfc95 --- /dev/null +++ b/apps/space/components/issues/filters-render/label/filter-label-block.tsx @@ -0,0 +1,34 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +// interfaces +import { IIssueLabel } from "store/types/issue"; +// constants +import { issueGroupFilter } from "constants/data"; + +export const RenderIssueLabel = observer(({ label }: { label: IIssueLabel }) => { + const store = useMobxStore(); + + const removeLabelFromFilter = () => {}; + + return ( +
+
+
+
+
{label?.name}
+
+ close +
+
+ ); +}); diff --git a/apps/space/components/issues/filters-render/label/index.tsx b/apps/space/components/issues/filters-render/label/index.tsx new file mode 100644 index 000000000..7d313153a --- /dev/null +++ b/apps/space/components/issues/filters-render/label/index.tsx @@ -0,0 +1,37 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// components +import { RenderIssueLabel } from "./filter-label-block"; +// interfaces +import { IIssueLabel } from "store/types/issue"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +const IssueLabelFilter = observer(() => { + const store: RootStore = useMobxStore(); + + const clearLabelFilters = () => {}; + + return ( + <> +
+
Labels
+
+ {store?.issue?.labels && + store?.issue?.labels.map((_label: IIssueLabel, _index: number) => )} +
+
+ close +
+
+ + ); +}); + +export default IssueLabelFilter; diff --git a/apps/space/components/issues/filters-render/priority/filter-priority-block.tsx b/apps/space/components/issues/filters-render/priority/filter-priority-block.tsx new file mode 100644 index 000000000..98173fd66 --- /dev/null +++ b/apps/space/components/issues/filters-render/priority/filter-priority-block.tsx @@ -0,0 +1,33 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +// interfaces +import { IIssuePriorityFilters } from "store/types/issue"; + +export const RenderIssuePriority = observer(({ priority }: { priority: IIssuePriorityFilters }) => { + const store = useMobxStore(); + + const removePriorityFromFilter = () => {}; + + return ( +
+
+ {priority?.icon} +
+
{priority?.title}
+
+ close +
+
+ ); +}); diff --git a/apps/space/components/issues/filters-render/priority/index.tsx b/apps/space/components/issues/filters-render/priority/index.tsx new file mode 100644 index 000000000..2253a0be2 --- /dev/null +++ b/apps/space/components/issues/filters-render/priority/index.tsx @@ -0,0 +1,36 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +// components +import { RenderIssuePriority } from "./filter-priority-block"; +// interfaces +import { IIssuePriorityFilters } from "store/types/issue"; +// constants +import { issuePriorityFilters } from "constants/data"; + +const IssuePriorityFilter = observer(() => { + const store = useMobxStore(); + + return ( + <> +
+
Priority
+
+ {issuePriorityFilters.map((_priority: IIssuePriorityFilters, _index: number) => ( + + ))} +
+
+ close +
+
{" "} + + ); +}); + +export default IssuePriorityFilter; diff --git a/apps/space/components/issues/filters-render/state/filter-state-block.tsx b/apps/space/components/issues/filters-render/state/filter-state-block.tsx new file mode 100644 index 000000000..95a4f4c70 --- /dev/null +++ b/apps/space/components/issues/filters-render/state/filter-state-block.tsx @@ -0,0 +1,38 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +// interfaces +import { IIssueState } from "store/types/issue"; +// constants +import { issueGroupFilter } from "constants/data"; + +export const RenderIssueState = observer(({ state }: { state: IIssueState }) => { + const store = useMobxStore(); + + const stateGroup = issueGroupFilter(state.group); + + const removeStateFromFilter = () => {}; + + if (stateGroup === null) return <>; + return ( +
+
+ +
+
{state?.name}
+
+ close +
+
+ ); +}); diff --git a/apps/space/components/issues/filters-render/state/index.tsx b/apps/space/components/issues/filters-render/state/index.tsx new file mode 100644 index 000000000..fc73af381 --- /dev/null +++ b/apps/space/components/issues/filters-render/state/index.tsx @@ -0,0 +1,37 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// components +import { RenderIssueState } from "./filter-state-block"; +// interfaces +import { IIssueState } from "store/types/issue"; +// mobx hook +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +const IssueStateFilter = observer(() => { + const store: RootStore = useMobxStore(); + + const clearStateFilters = () => {}; + + return ( + <> +
+
State
+
+ {store?.issue?.states && + store?.issue?.states.map((_state: IIssueState, _index: number) => )} +
+
+ close +
+
+ + ); +}); + +export default IssueStateFilter; diff --git a/apps/space/components/issues/navbar/index.tsx b/apps/space/components/issues/navbar/index.tsx new file mode 100644 index 000000000..0207aaee2 --- /dev/null +++ b/apps/space/components/issues/navbar/index.tsx @@ -0,0 +1,54 @@ +"use client"; + +// components +import { NavbarSearch } from "./search"; +import { NavbarIssueBoardView } from "./issue-board-view"; +import { NavbarIssueFilter } from "./issue-filter"; +import { NavbarIssueView } from "./issue-view"; +import { NavbarTheme } from "./theme"; +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +const IssueNavbar = observer(() => { + const store: RootStore = useMobxStore(); + + return ( +
+ {/* project detail */} +
+
+ {store?.project?.project && store?.project?.project?.icon ? store?.project?.project?.icon : "😊"} +
+
+ {store?.project?.project?.name || `...`} +
+
+ + {/* issue search bar */} +
+ +
+ + {/* issue views */} +
+ +
+ + {/* issue filters */} + {/*
+ + +
*/} + + {/* theming */} + {/*
+ +
*/} +
+ ); +}); + +export default IssueNavbar; diff --git a/apps/space/components/issues/navbar/issue-board-view.tsx b/apps/space/components/issues/navbar/issue-board-view.tsx new file mode 100644 index 000000000..57c8b27c1 --- /dev/null +++ b/apps/space/components/issues/navbar/issue-board-view.tsx @@ -0,0 +1,54 @@ +"use client"; + +// next imports +import { useRouter, useParams } from "next/navigation"; +// mobx react lite +import { observer } from "mobx-react-lite"; +// constants +import { issueViews } from "constants/data"; +// interfaces +import { TIssueBoardKeys } from "store/types"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const NavbarIssueBoardView = observer(() => { + const store: RootStore = useMobxStore(); + + const router = useRouter(); + const routerParams = useParams(); + + const { workspace_slug, project_slug } = routerParams as { workspace_slug: string; project_slug: string }; + + const handleCurrentBoardView = (boardView: TIssueBoardKeys) => { + store?.issue?.setCurrentIssueBoardView(boardView); + router.replace(`/${workspace_slug}/${project_slug}?board=${boardView}`); + }; + + return ( + <> + {store?.project?.workspaceProjectSettings && + issueViews && + issueViews.length > 0 && + issueViews.map( + (_view) => + store?.project?.workspaceProjectSettings?.views[_view?.key] && ( +
handleCurrentBoardView(_view?.key)} + title={_view?.title} + > + + {_view?.icon} + +
+ ) + )} + + ); +}); diff --git a/apps/space/components/issues/navbar/issue-filter.tsx b/apps/space/components/issues/navbar/issue-filter.tsx new file mode 100644 index 000000000..10255882d --- /dev/null +++ b/apps/space/components/issues/navbar/issue-filter.tsx @@ -0,0 +1,13 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const NavbarIssueFilter = observer(() => { + const store: RootStore = useMobxStore(); + + return
Filter
; +}); diff --git a/apps/space/components/issues/navbar/issue-view.tsx b/apps/space/components/issues/navbar/issue-view.tsx new file mode 100644 index 000000000..0a8f5c860 --- /dev/null +++ b/apps/space/components/issues/navbar/issue-view.tsx @@ -0,0 +1,13 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const NavbarIssueView = observer(() => { + const store: RootStore = useMobxStore(); + + return
View
; +}); diff --git a/apps/space/components/issues/navbar/search.tsx b/apps/space/components/issues/navbar/search.tsx new file mode 100644 index 000000000..d1cafea6a --- /dev/null +++ b/apps/space/components/issues/navbar/search.tsx @@ -0,0 +1,13 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const NavbarSearch = observer(() => { + const store: RootStore = useMobxStore(); + + return
; +}); diff --git a/apps/space/components/issues/navbar/theme.tsx b/apps/space/components/issues/navbar/theme.tsx new file mode 100644 index 000000000..c122f8478 --- /dev/null +++ b/apps/space/components/issues/navbar/theme.tsx @@ -0,0 +1,28 @@ +"use client"; + +// mobx react lite +import { observer } from "mobx-react-lite"; +// mobx +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +export const NavbarTheme = observer(() => { + const store: RootStore = useMobxStore(); + + const handleTheme = () => { + store?.theme?.setTheme(store?.theme?.theme === "light" ? "dark" : "light"); + }; + + return ( +
+ {store?.theme?.theme === "light" ? ( + dark_mode + ) : ( + light_mode + )} +
+ ); +}); diff --git a/apps/space/constants/data.ts b/apps/space/constants/data.ts new file mode 100644 index 000000000..81ccae116 --- /dev/null +++ b/apps/space/constants/data.ts @@ -0,0 +1,153 @@ +// interfaces +import { + IIssueBoardViews, + // priority + TIssuePriorityKey, + // state groups + TIssueGroupKey, + IIssuePriorityFilters, + IIssueGroup, +} from "store/types/issue"; +// icons +import { + BacklogStateIcon, + UnstartedStateIcon, + StartedStateIcon, + CompletedStateIcon, + CancelledStateIcon, +} from "components/icons"; + +// all issue views +export const issueViews: IIssueBoardViews[] = [ + { + key: "list", + title: "List View", + icon: "format_list_bulleted", + className: "", + }, + { + key: "kanban", + title: "Board View", + icon: "grid_view", + className: "", + }, + // { + // key: "calendar", + // title: "Calendar View", + // icon: "calendar_month", + // className: "", + // }, + // { + // key: "spreadsheet", + // title: "Spreadsheet View", + // icon: "table_chart", + // className: "", + // }, + // { + // key: "gantt", + // title: "Gantt Chart View", + // icon: "waterfall_chart", + // className: "rotate-90", + // }, +]; + +// issue priority filters +export const issuePriorityFilters: IIssuePriorityFilters[] = [ + { + key: "urgent", + title: "Urgent", + className: "border border-red-500/50 bg-red-500/20 text-red-500", + icon: "error", + }, + { + key: "high", + title: "High", + className: "border border-orange-500/50 bg-orange-500/20 text-orange-500", + icon: "signal_cellular_alt", + }, + { + key: "medium", + title: "Medium", + className: "border border-yellow-500/50 bg-yellow-500/20 text-yellow-500", + icon: "signal_cellular_alt_2_bar", + }, + { + key: "low", + title: "Low", + className: "border border-green-500/50 bg-green-500/20 text-green-500", + icon: "signal_cellular_alt_1_bar", + }, + { + key: "none", + title: "None", + className: "border border-gray-500/50 bg-gray-500/20 text-gray-500", + icon: "block", + }, +]; + +export const issuePriorityFilter = (priorityKey: TIssuePriorityKey): IIssuePriorityFilters | null => { + const currentIssuePriority: IIssuePriorityFilters | undefined | null = + issuePriorityFilters && issuePriorityFilters.length > 0 + ? issuePriorityFilters.find((_priority) => _priority.key === priorityKey) + : null; + + if (currentIssuePriority === undefined || currentIssuePriority === null) return null; + return { ...currentIssuePriority }; +}; + +// issue group filters +export const issueGroupColors: { + [key: string]: string; +} = { + backlog: "#d9d9d9", + unstarted: "#3f76ff", + started: "#f59e0b", + completed: "#16a34a", + cancelled: "#dc2626", +}; + +export const issueGroups: IIssueGroup[] = [ + { + key: "backlog", + title: "Backlog", + color: "#d9d9d9", + className: `border-[#d9d9d9]/50 text-[#d9d9d9] bg-[#d9d9d9]/10`, + icon: BacklogStateIcon, + }, + { + key: "unstarted", + title: "Unstarted", + color: "#3f76ff", + className: `border-[#3f76ff]/50 text-[#3f76ff] bg-[#3f76ff]/10`, + icon: UnstartedStateIcon, + }, + { + key: "started", + title: "Started", + color: "#f59e0b", + className: `border-[#f59e0b]/50 text-[#f59e0b] bg-[#f59e0b]/10`, + icon: StartedStateIcon, + }, + { + key: "completed", + title: "Completed", + color: "#16a34a", + className: `border-[#16a34a]/50 text-[#16a34a] bg-[#16a34a]/10`, + icon: CompletedStateIcon, + }, + { + key: "cancelled", + title: "Cancelled", + color: "#dc2626", + className: `border-[#dc2626]/50 text-[#dc2626] bg-[#dc2626]/10`, + icon: CancelledStateIcon, + }, +]; + +export const issueGroupFilter = (issueKey: TIssueGroupKey): IIssueGroup | null => { + const currentIssueStateGroup: IIssueGroup | undefined | null = + issueGroups && issueGroups.length > 0 ? issueGroups.find((group) => group.key === issueKey) : null; + + if (currentIssueStateGroup === undefined || currentIssueStateGroup === null) return null; + return { ...currentIssueStateGroup }; +}; diff --git a/apps/space/constants/helpers.ts b/apps/space/constants/helpers.ts new file mode 100644 index 000000000..fd4dba217 --- /dev/null +++ b/apps/space/constants/helpers.ts @@ -0,0 +1,13 @@ +export const renderDateFormat = (date: string | Date | null) => { + if (!date) return "N/A"; + + var d = new Date(date), + month = "" + (d.getMonth() + 1), + day = "" + d.getDate(), + year = d.getFullYear(); + + if (month.length < 2) month = "0" + month; + if (day.length < 2) day = "0" + day; + + return [year, month, day].join("-"); +}; diff --git a/apps/space/lib/mobx-store/root.ts b/apps/space/lib/index.ts similarity index 100% rename from apps/space/lib/mobx-store/root.ts rename to apps/space/lib/index.ts diff --git a/apps/space/lib/mobx/store-init.tsx b/apps/space/lib/mobx/store-init.tsx new file mode 100644 index 000000000..2ba2f9024 --- /dev/null +++ b/apps/space/lib/mobx/store-init.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useEffect } from "react"; +// next imports +import { useSearchParams } from "next/navigation"; +// interface +import { TIssueBoardKeys } from "store/types"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +import { RootStore } from "store/root"; + +const MobxStoreInit = () => { + const store: RootStore = useMobxStore(); + + // search params + const routerSearchparams = useSearchParams(); + + const board = routerSearchparams.get("board") as TIssueBoardKeys; + + useEffect(() => { + // theme + const _theme = localStorage && localStorage.getItem("app_theme") ? localStorage.getItem("app_theme") : "light"; + if (_theme && store?.theme?.theme != _theme) store.theme.setTheme(_theme); + else localStorage.setItem("app_theme", _theme && _theme != "light" ? "dark" : "light"); + }, [store?.theme]); + + // updating default board view when we are in the issues page + useEffect(() => { + if (board && board != store?.issue?.currentIssueBoardView) store.issue.setCurrentIssueBoardView(board); + }, [board, store?.issue]); + + return <>; +}; + +export default MobxStoreInit; diff --git a/apps/space/lib/mobx/store-provider.tsx b/apps/space/lib/mobx/store-provider.tsx new file mode 100644 index 000000000..c6fde14ae --- /dev/null +++ b/apps/space/lib/mobx/store-provider.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { createContext, useContext } from "react"; +// mobx store +import { RootStore } from "store/root"; + +let rootStore: RootStore = new RootStore(); + +export const MobxStoreContext = createContext(rootStore); + +const initializeStore = () => { + const _rootStore: RootStore = rootStore ?? new RootStore(); + if (typeof window === "undefined") return _rootStore; + if (!rootStore) rootStore = _rootStore; + return _rootStore; +}; + +export const MobxStoreProvider = ({ children }: any) => { + const store: RootStore = initializeStore(); + return {children}; +}; + +// hook +export const useMobxStore = () => { + const context = useContext(MobxStoreContext); + if (context === undefined) throw new Error("useMobxStore must be used within MobxStoreProvider"); + return context; +}; diff --git a/apps/space/package.json b/apps/space/package.json index 7ace168aa..4af31d312 100644 --- a/apps/space/package.json +++ b/apps/space/package.json @@ -18,6 +18,8 @@ "eslint": "8.34.0", "eslint-config-next": "13.2.1", "js-cookie": "^3.0.1", + "mobx": "^6.10.0", + "mobx-react-lite": "^4.0.3", "next": "^13.4.13", "nprogress": "^0.2.0", "react": "^18.2.0", diff --git a/apps/space/public/plane-logo.webp b/apps/space/public/plane-logo.webp new file mode 100644 index 000000000..52e7c98da Binary files /dev/null and b/apps/space/public/plane-logo.webp differ diff --git a/apps/space/services/api.service.ts b/apps/space/services/api.service.ts new file mode 100644 index 000000000..900d5d15f --- /dev/null +++ b/apps/space/services/api.service.ts @@ -0,0 +1,100 @@ +// axios +import axios from "axios"; +// js cookie +import Cookies from "js-cookie"; + +const base_url: string | null = "https://boarding.plane.so"; + +abstract class APIService { + protected baseURL: string; + protected headers: any = {}; + + constructor(baseURL: string) { + this.baseURL = base_url ? base_url : baseURL; + } + + setRefreshToken(token: string) { + Cookies.set("refreshToken", token); + } + + getRefreshToken() { + return Cookies.get("refreshToken"); + } + + purgeRefreshToken() { + Cookies.remove("refreshToken", { path: "/" }); + } + + setAccessToken(token: string) { + Cookies.set("accessToken", token); + } + + getAccessToken() { + return Cookies.get("accessToken"); + } + + purgeAccessToken() { + Cookies.remove("accessToken", { path: "/" }); + } + + getHeaders() { + return { + Authorization: `Bearer ${this.getAccessToken()}`, + }; + } + + get(url: string, config = {}): Promise { + return axios({ + method: "get", + url: this.baseURL + url, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + post(url: string, data = {}, config = {}): Promise { + return axios({ + method: "post", + url: this.baseURL + url, + data, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + put(url: string, data = {}, config = {}): Promise { + return axios({ + method: "put", + url: this.baseURL + url, + data, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + patch(url: string, data = {}, config = {}): Promise { + return axios({ + method: "patch", + url: this.baseURL + url, + data, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + delete(url: string, data?: any, config = {}): Promise { + return axios({ + method: "delete", + url: this.baseURL + url, + data: data, + headers: this.getAccessToken() ? this.getHeaders() : {}, + ...config, + }); + } + + request(config = {}) { + return axios(config); + } +} + +export default APIService; diff --git a/apps/space/services/issue.service.ts b/apps/space/services/issue.service.ts new file mode 100644 index 000000000..4b40bdf5c --- /dev/null +++ b/apps/space/services/issue.service.ts @@ -0,0 +1,20 @@ +// services +import APIService from "services/api.service"; + +const { NEXT_PUBLIC_API_BASE_URL } = process.env; + +class IssueService extends APIService { + constructor() { + super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + } + + async getPublicIssues(workspace_slug: string, project_slug: string): Promise { + return this.get(`/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/issues/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } +} + +export default IssueService; diff --git a/apps/space/services/project.service.ts b/apps/space/services/project.service.ts new file mode 100644 index 000000000..4d973051f --- /dev/null +++ b/apps/space/services/project.service.ts @@ -0,0 +1,20 @@ +// services +import APIService from "services/api.service"; + +const { NEXT_PUBLIC_API_BASE_URL } = process.env; + +class ProjectService extends APIService { + constructor() { + super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + } + + async getProjectSettingsAsync(workspace_slug: string, project_slug: string): Promise { + return this.get(`/api/public/workspaces/${workspace_slug}/project-boards/${project_slug}/settings/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } +} + +export default ProjectService; diff --git a/apps/space/services/user.service.ts b/apps/space/services/user.service.ts new file mode 100644 index 000000000..d724374b6 --- /dev/null +++ b/apps/space/services/user.service.ts @@ -0,0 +1,20 @@ +// services +import APIService from "services/api.service"; + +const { NEXT_PUBLIC_API_BASE_URL } = process.env; + +class UserService extends APIService { + constructor() { + super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); + } + + async currentUser(): Promise { + return this.get("/api/users/me/") + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } +} + +export default UserService; diff --git a/apps/space/store/issue.ts b/apps/space/store/issue.ts new file mode 100644 index 000000000..79ad4b910 --- /dev/null +++ b/apps/space/store/issue.ts @@ -0,0 +1,91 @@ +// mobx +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// service +import IssueService from "services/issue.service"; +// types +import { TIssueBoardKeys } from "store/types/issue"; +import { IIssueStore, IIssue, IIssueState, IIssueLabel } from "./types"; + +class IssueStore implements IIssueStore { + currentIssueBoardView: TIssueBoardKeys | null = null; + + loader: boolean = false; + error: any | null = null; + + states: IIssueState[] | null = null; + labels: IIssueLabel[] | null = null; + issues: IIssue[] | null = null; + + userSelectedStates: string[] = []; + userSelectedLabels: string[] = []; + // root store + rootStore; + // service + issueService; + + constructor(_rootStore: any) { + makeObservable(this, { + // observable + currentIssueBoardView: observable, + + loader: observable, + error: observable, + + states: observable.ref, + labels: observable.ref, + issues: observable.ref, + + userSelectedStates: observable, + userSelectedLabels: observable, + // action + setCurrentIssueBoardView: action, + getIssuesAsync: action, + // computed + }); + + this.rootStore = _rootStore; + this.issueService = new IssueService(); + } + + // computed + getCountOfIssuesByState(state_id: string): number { + return this.issues?.filter((issue) => issue.state == state_id).length || 0; + } + + getFilteredIssuesByState(state_id: string): IIssue[] | [] { + return this.issues?.filter((issue) => issue.state == state_id) || []; + } + + // action + setCurrentIssueBoardView = async (view: TIssueBoardKeys) => { + this.currentIssueBoardView = view; + }; + + getIssuesAsync = async (workspace_slug: string, project_slug: string) => { + try { + this.loader = true; + this.error = null; + + const response = await this.issueService.getPublicIssues(workspace_slug, project_slug); + + if (response) { + const _states: IIssueState[] = [...response?.states]; + const _labels: IIssueLabel[] = [...response?.labels]; + const _issues: IIssue[] = [...response?.issues]; + runInAction(() => { + this.states = _states; + this.labels = _labels; + this.issues = _issues; + this.loader = false; + }); + return response; + } + } catch (error) { + this.loader = false; + this.error = error; + return error; + } + }; +} + +export default IssueStore; diff --git a/apps/space/store/project.ts b/apps/space/store/project.ts new file mode 100644 index 000000000..e5ac58261 --- /dev/null +++ b/apps/space/store/project.ts @@ -0,0 +1,69 @@ +// mobx +import { observable, action, makeObservable, runInAction } from "mobx"; +// service +import ProjectService from "services/project.service"; +// types +import { IProjectStore, IWorkspace, IProject, IProjectSettings } from "./types"; + +class ProjectStore implements IProjectStore { + loader: boolean = false; + error: any | null = null; + + workspace: IWorkspace | null = null; + project: IProject | null = null; + workspaceProjectSettings: IProjectSettings | null = null; + // root store + rootStore; + // service + projectService; + + constructor(_rootStore: any | null = null) { + makeObservable(this, { + // observable + workspace: observable.ref, + project: observable.ref, + workspaceProjectSettings: observable.ref, + loader: observable, + error: observable.ref, + // action + getProjectSettingsAsync: action, + // computed + }); + + this.rootStore = _rootStore; + this.projectService = new ProjectService(); + } + + getProjectSettingsAsync = async (workspace_slug: string, project_slug: string) => { + try { + this.loader = true; + this.error = null; + + const response = await this.projectService.getProjectSettingsAsync(workspace_slug, project_slug); + + if (response) { + const _project: IProject = { ...response?.project_details }; + const _workspace: IWorkspace = { ...response?.workspace_detail }; + const _workspaceProjectSettings: IProjectSettings = { + comments: response?.comments, + reactions: response?.reactions, + votes: response?.votes, + views: { ...response?.views }, + }; + runInAction(() => { + this.project = _project; + this.workspace = _workspace; + this.workspaceProjectSettings = _workspaceProjectSettings; + this.loader = false; + }); + } + return response; + } catch (error) { + this.loader = false; + this.error = error; + return error; + } + }; +} + +export default ProjectStore; diff --git a/apps/space/store/root.ts b/apps/space/store/root.ts index a10356821..dd6d620c0 100644 --- a/apps/space/store/root.ts +++ b/apps/space/store/root.ts @@ -1 +1,25 @@ -export const init = {}; +// mobx lite +import { enableStaticRendering } from "mobx-react-lite"; +// store imports +import UserStore from "./user"; +import ThemeStore from "./theme"; +import IssueStore from "./issue"; +import ProjectStore from "./project"; +// types +import { IIssueStore, IProjectStore, IThemeStore, IUserStore } from "./types"; + +enableStaticRendering(typeof window === "undefined"); + +export class RootStore { + user: IUserStore; + theme: IThemeStore; + issue: IIssueStore; + project: IProjectStore; + + constructor() { + this.user = new UserStore(this); + this.theme = new ThemeStore(this); + this.issue = new IssueStore(this); + this.project = new ProjectStore(this); + } +} diff --git a/apps/space/store/theme.ts b/apps/space/store/theme.ts new file mode 100644 index 000000000..809d56b97 --- /dev/null +++ b/apps/space/store/theme.ts @@ -0,0 +1,33 @@ +// mobx +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// types +import { IThemeStore } from "./types"; + +class ThemeStore implements IThemeStore { + theme: "light" | "dark" = "light"; + // root store + rootStore; + + constructor(_rootStore: any | null = null) { + makeObservable(this, { + // observable + theme: observable, + // action + setTheme: action, + // computed + }); + + this.rootStore = _rootStore; + } + + setTheme = async (_theme: "light" | "dark" | string) => { + try { + localStorage.setItem("app_theme", _theme); + this.theme = _theme === "light" ? "light" : "dark"; + } catch (error) { + console.error("setting user theme error", error); + } + }; +} + +export default ThemeStore; diff --git a/apps/space/store/types/index.ts b/apps/space/store/types/index.ts new file mode 100644 index 000000000..5a0a51eda --- /dev/null +++ b/apps/space/store/types/index.ts @@ -0,0 +1,4 @@ +export * from "./user"; +export * from "./theme"; +export * from "./project"; +export * from "./issue"; diff --git a/apps/space/store/types/issue.ts b/apps/space/store/types/issue.ts new file mode 100644 index 000000000..5feeba7bd --- /dev/null +++ b/apps/space/store/types/issue.ts @@ -0,0 +1,72 @@ +export type TIssueBoardKeys = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt"; + +export interface IIssueBoardViews { + key: TIssueBoardKeys; + title: string; + icon: string; + className: string; +} + +export type TIssuePriorityKey = "urgent" | "high" | "medium" | "low" | "none"; +export type TIssuePriorityTitle = "Urgent" | "High" | "Medium" | "Low" | "None"; +export interface IIssuePriorityFilters { + key: TIssuePriorityKey; + title: TIssuePriorityTitle; + className: string; + icon: string; +} + +export type TIssueGroupKey = "backlog" | "unstarted" | "started" | "completed" | "cancelled"; +export type TIssueGroupTitle = "Backlog" | "Unstarted" | "Started" | "Completed" | "Cancelled"; + +export interface IIssueGroup { + key: TIssueGroupKey; + title: TIssueGroupTitle; + color: string; + className: string; + icon: React.FC; +} + +export interface IIssue { + id: string; + sequence_id: number; + name: string; + description_html: string; + priority: TIssuePriorityKey | null; + state: string; + state_detail: any; + label_details: any; + target_date: any; +} + +export interface IIssueState { + id: string; + name: string; + group: TIssueGroupKey; + color: string; +} + +export interface IIssueLabel { + id: string; + name: string; + color: string; +} + +export interface IIssueStore { + currentIssueBoardView: TIssueBoardKeys | null; + loader: boolean; + error: any | null; + + states: IIssueState[] | null; + labels: IIssueLabel[] | null; + issues: IIssue[] | null; + + userSelectedStates: string[]; + userSelectedLabels: string[]; + + getCountOfIssuesByState: (state: string) => number; + getFilteredIssuesByState: (state: string) => IIssue[]; + + setCurrentIssueBoardView: (view: TIssueBoardKeys) => void; + getIssuesAsync: (workspace_slug: string, project_slug: string) => Promise; +} diff --git a/apps/space/store/types/project.ts b/apps/space/store/types/project.ts new file mode 100644 index 000000000..a55f30be0 --- /dev/null +++ b/apps/space/store/types/project.ts @@ -0,0 +1,39 @@ +export interface IWorkspace { + id: string; + name: string; + slug: string; +} + +export interface IProject { + id: string; + identifier: string; + name: string; + icon: string; + cover_image: string | null; + icon_prop: string | null; + emoji: string | null; +} + +export interface IProjectSettings { + comments: boolean; + reactions: boolean; + votes: boolean; + views: { + list: boolean; + gantt: boolean; + kanban: boolean; + calendar: boolean; + spreadsheet: boolean; + }; +} + +export interface IProjectStore { + loader: boolean; + error: any | null; + + workspace: IWorkspace | null; + project: IProject | null; + workspaceProjectSettings: IProjectSettings | null; + + getProjectSettingsAsync: (workspace_slug: string, project_slug: string) => Promise; +} diff --git a/apps/space/store/types/theme.ts b/apps/space/store/types/theme.ts new file mode 100644 index 000000000..ca306be51 --- /dev/null +++ b/apps/space/store/types/theme.ts @@ -0,0 +1,4 @@ +export interface IThemeStore { + theme: string; + setTheme: (theme: "light" | "dark" | string) => void; +} diff --git a/apps/space/store/types/user.ts b/apps/space/store/types/user.ts new file mode 100644 index 000000000..0293c5381 --- /dev/null +++ b/apps/space/store/types/user.ts @@ -0,0 +1,4 @@ +export interface IUserStore { + currentUser: any | null; + getUserAsync: () => void; +} diff --git a/apps/space/store/user.ts b/apps/space/store/user.ts new file mode 100644 index 000000000..2f4782236 --- /dev/null +++ b/apps/space/store/user.ts @@ -0,0 +1,43 @@ +// mobx +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// service +import UserService from "services/user.service"; +// types +import { IUserStore } from "./types"; + +class UserStore implements IUserStore { + currentUser: any | null = null; + // root store + rootStore; + // service + userService; + + constructor(_rootStore: any) { + makeObservable(this, { + // observable + currentUser: observable, + // actions + // computed + }); + this.rootStore = _rootStore; + this.userService = new UserService(); + } + + getUserAsync = async () => { + try { + const response = this.userService.currentUser(); + if (response) { + runInAction(() => { + this.currentUser = response; + }); + } + } catch (error) { + console.error("error", error); + runInAction(() => { + // render error actions + }); + } + }; +} + +export default UserStore; diff --git a/apps/space/tailwind.config.js b/apps/space/tailwind.config.js index 145c65b5a..55aaa9a31 100644 --- a/apps/space/tailwind.config.js +++ b/apps/space/tailwind.config.js @@ -6,6 +6,7 @@ module.exports = { "./pages/**/*.{js,ts,jsx,tsx}", "./layouts/**/*.tsx", "./components/**/*.{js,ts,jsx,tsx}", + "./constants/**/*.{js,ts,jsx,tsx}", ], theme: { extend: { diff --git a/yarn.lock b/yarn.lock index 01e9d2a84..578f10026 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3617,6 +3617,14 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== + dependencies: + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" + a11y-status@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/a11y-status/-/a11y-status-2.0.1.tgz#a7883105910b9e3cd09ea90e5acf8404dc01b47e" @@ -3641,6 +3649,11 @@ acorn@^8.8.2, acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== +acorn@^8.9.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -5180,6 +5193,56 @@ eslint@8.34.0: strip-json-comments "^3.1.0" text-table "^0.2.0" +eslint-visitor-keys@^3.4.1: + version "3.4.2" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz#8c2095440eca8c933bedcadf16fefa44dbe9ba5f" + integrity sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw== + +eslint@8.34.0: + version "8.34.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.34.0.tgz#fe0ab0ef478104c1f9ebc5537e303d25a8fb22d6" + integrity sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg== + dependencies: + "@eslint/eslintrc" "^1.4.1" + "@humanwhocodes/config-array" "^0.11.8" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.4.0" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + eslint@^7.23.0, eslint@^7.32.0: version "7.32.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" @@ -5397,6 +5460,17 @@ fast-glob@^3.2.12, fast-glob@^3.2.9, fast-glob@^3.3.0: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -6247,6 +6321,13 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" @@ -7569,6 +7650,15 @@ postcss@^8.4.14, postcss@^8.4.21, postcss@^8.4.23: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.21: + version "8.4.27" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" + integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prebuild-install@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" @@ -8619,6 +8709,11 @@ streamx@^2.15.0: fast-fifo "^1.1.0" queue-tick "^1.0.1" +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -9049,6 +9144,11 @@ tslib@~2.5.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== +tslib@^2.5.0, tslib@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" + integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"