From 5d67029b5a51d2f42fd421dc95da8172050f1872 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Sat, 25 Mar 2023 23:39:46 +0530 Subject: [PATCH] feat: pages (#533) * style: page details * style: page blocks design * chore: pages list end points * feat: add blocks, push blocks to issues * feat: page labels, color options * feat: added labels to pages * fix: update page mutation --- .../components/icons/color-pallette-icon.tsx | 24 + apps/app/components/icons/index.ts | 3 + .../components/icons/pencil-scribble-icon.tsx | 23 + apps/app/components/icons/water-drop-icon.tsx | 24 + apps/app/components/issues/select/label.tsx | 312 ++++++----- apps/app/components/pages/all-pages-list.tsx | 34 ++ .../pages/create-update-page-modal.tsx | 27 +- .../components/pages/delete-page-modal.tsx | 24 +- .../components/pages/favorite-pages-list.tsx | 34 ++ apps/app/components/pages/index.ts | 9 +- apps/app/components/pages/my-pages-list.tsx | 34 ++ .../app/components/pages/other-pages-list.tsx | 34 ++ apps/app/components/pages/page-form.tsx | 12 +- apps/app/components/pages/page-grid-item.tsx | 172 ------ apps/app/components/pages/page-label.tsx | 23 - apps/app/components/pages/pages-grid.tsx | 58 --- apps/app/components/pages/pages-list.tsx | 65 --- apps/app/components/pages/pages-masonry.tsx | 82 --- apps/app/components/pages/pages-view.tsx | 172 ++++++ .../components/pages/recent-pages-list.tsx | 46 ++ .../components/pages/single-page-block.tsx | 230 +++++++++ .../pages/single-page-detailed-item.tsx | 126 +++++ .../pages/single-page-list-item.tsx | 197 +++---- .../project/single-sidebar-project.tsx | 4 +- .../app/components/rich-text-editor/index.tsx | 6 +- apps/app/components/ui/buttons/index.ts | 1 + .../ui/buttons/no-border-button.tsx | 36 ++ apps/app/components/ui/empty-state.tsx | 2 +- apps/app/constants/fetch-keys.ts | 8 +- apps/app/helpers/date-time.helper.ts | 28 +- .../projects/[projectId]/pages/[pageId].tsx | 488 ++++++++++++++---- .../projects/[projectId]/pages/index.tsx | 357 +++++++------ apps/app/public/empty-state/empty-page.svg | 40 ++ apps/app/services/pages.service.ts | 74 ++- apps/app/styles/editor.css | 3 +- apps/app/types/pages.d.ts | 88 ++-- 36 files changed, 1842 insertions(+), 1058 deletions(-) create mode 100644 apps/app/components/icons/color-pallette-icon.tsx create mode 100644 apps/app/components/icons/pencil-scribble-icon.tsx create mode 100644 apps/app/components/icons/water-drop-icon.tsx create mode 100644 apps/app/components/pages/all-pages-list.tsx create mode 100644 apps/app/components/pages/favorite-pages-list.tsx create mode 100644 apps/app/components/pages/my-pages-list.tsx create mode 100644 apps/app/components/pages/other-pages-list.tsx delete mode 100644 apps/app/components/pages/page-grid-item.tsx delete mode 100644 apps/app/components/pages/page-label.tsx delete mode 100644 apps/app/components/pages/pages-grid.tsx delete mode 100644 apps/app/components/pages/pages-list.tsx delete mode 100644 apps/app/components/pages/pages-masonry.tsx create mode 100644 apps/app/components/pages/pages-view.tsx create mode 100644 apps/app/components/pages/recent-pages-list.tsx create mode 100644 apps/app/components/pages/single-page-block.tsx create mode 100644 apps/app/components/pages/single-page-detailed-item.tsx create mode 100644 apps/app/components/ui/buttons/no-border-button.tsx create mode 100644 apps/app/public/empty-state/empty-page.svg diff --git a/apps/app/components/icons/color-pallette-icon.tsx b/apps/app/components/icons/color-pallette-icon.tsx new file mode 100644 index 000000000..23f0e41d3 --- /dev/null +++ b/apps/app/components/icons/color-pallette-icon.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const ColorPalletteIcon: React.FC = ({ + width = "20", + height = "20", + className, + color = "#858e96", +}) => ( + + + +); diff --git a/apps/app/components/icons/index.ts b/apps/app/components/icons/index.ts index 0da820635..9478f5fca 100644 --- a/apps/app/components/icons/index.ts +++ b/apps/app/components/icons/index.ts @@ -7,6 +7,7 @@ export * from "./calendar-month-icon"; export * from "./cancel-icon"; export * from "./cancelled-state-icon"; export * from "./clipboard-icon"; +export * from "./color-pallette-icon"; export * from "./comment-icon"; export * from "./completed-cycle-icon"; export * from "./completed-state-icon"; @@ -23,6 +24,7 @@ export * from "./started-state-icon"; export * from "./layer-diagonal-icon"; export * from "./lock-icon"; export * from "./menu-icon"; +export * from "./pencil-scribble-icon"; export * from "./plus-icon"; export * from "./priority-icon"; export * from "./question-mark-circle-icon"; @@ -52,3 +54,4 @@ export * from "./cloud-upload"; export * from "./users"; export * from "./import-layers"; export * from "./check"; +export * from "./water-drop-icon"; diff --git a/apps/app/components/icons/pencil-scribble-icon.tsx b/apps/app/components/icons/pencil-scribble-icon.tsx new file mode 100644 index 000000000..561a5bcc3 --- /dev/null +++ b/apps/app/components/icons/pencil-scribble-icon.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const PencilScribbleIcon: React.FC = ({ + width = "20", + height = "20", + className, + color = "#000000", +}) => ( + + + +); diff --git a/apps/app/components/icons/water-drop-icon.tsx b/apps/app/components/icons/water-drop-icon.tsx new file mode 100644 index 000000000..8123db09b --- /dev/null +++ b/apps/app/components/icons/water-drop-icon.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import type { Props } from "./types"; + +export const WaterDropIcon: React.FC = ({ + width = "24", + height = "24", + className, + color = "#858e96", +}) => ( + + + +); diff --git a/apps/app/components/issues/select/label.tsx b/apps/app/components/issues/select/label.tsx index b09513415..c05c8a8c8 100644 --- a/apps/app/components/issues/select/label.tsx +++ b/apps/app/components/issues/select/label.tsx @@ -49,174 +49,170 @@ export const IssueLabelSelect: React.FC = ({ setIsOpen, value, onChange, : issueLabels?.filter((l) => l.name.toLowerCase().includes(query.toLowerCase())); return ( - <> - onChange(val)} - className="relative flex-shrink-0" - multiple - > - {({ open }: any) => ( - <> - - `flex cursor-pointer items-center rounded-md border text-xs shadow-sm duration-200 + onChange(val)} + className="relative flex-shrink-0" + multiple + > + {({ open }: any) => ( + <> + + `flex cursor-pointer items-center rounded-md border text-xs shadow-sm duration-200 ${ open ? "border-theme bg-theme/5 outline-none ring-1 ring-theme " : "hover:bg-theme/5 " }` - } - > - {value && value.length > 0 ? ( - - issueLabels?.find((l) => l.id === v)?.color) ?? []} - length={3} - showLength - /> - {value.length} Labels - - ) : ( - - - Label - - )} - + } + > + {value && value.length > 0 ? ( + + issueLabels?.find((l) => l.id === v)?.color) ?? []} + length={3} + showLength + /> + {value.length} Labels + + ) : ( + + + Label + + )} + - - + -
- - setQuery(event.target.value)} - placeholder="Search for label..." - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {issueLabels && filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((label) => { - const children = issueLabels?.filter((l) => l.parent === label.id); + > +
+ + setQuery(event.target.value)} + placeholder="Search for label..." + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {issueLabels && filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((label) => { + const children = issueLabels?.filter((l) => l.parent === label.id); - if (children.length === 0) { - if (!label.parent) - return ( - - `${ - active ? "bg-gray-200" : "" - } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600` - } - value={label.id} - > - {({ selected }) => ( -
-
- - {label.name} -
-
- -
-
- )} -
- ); - } else + if (children.length === 0) { + if (!label.parent) return ( -
-
- {label.name} -
-
- {children.map((child) => ( - - `${ - active ? "bg-gray-200" : "" - } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600` - } - value={child.id} - > - {({ selected }) => ( -
-
- - {child.name} -
-
- -
-
- )} -
- ))} -
-
+ + `${ + active ? "bg-gray-200" : "" + } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600` + } + value={label.id} + > + {({ selected }) => ( +
+
+ + {label.name} +
+
+ +
+
+ )} +
); - }) - ) : ( -

No labels found

- ) + } else + return ( +
+
+ {label.name} +
+
+ {children.map((child) => ( + + `${ + active ? "bg-gray-200" : "" + } group flex min-w-[14rem] cursor-pointer select-none items-center gap-2 truncate rounded px-1 py-1.5 text-gray-600` + } + value={child.id} + > + {({ selected }) => ( +
+
+ + {child.name} +
+
+ +
+
+ )} +
+ ))} +
+
+ ); + }) ) : ( -

Loading...

- )} - -
- - - - )} - - +

No labels found

+ ) + ) : ( +

Loading...

+ )} + +
+
+
+ + )} +
); }; diff --git a/apps/app/components/pages/all-pages-list.tsx b/apps/app/components/pages/all-pages-list.tsx new file mode 100644 index 000000000..19a8c8cd7 --- /dev/null +++ b/apps/app/components/pages/all-pages-list.tsx @@ -0,0 +1,34 @@ +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import pagesService from "services/pages.service"; +// components +import { PagesView } from "components/pages"; +// types +import { TPageViewProps } from "types"; +// fetch-keys +import { ALL_PAGES_LIST } from "constants/fetch-keys"; + +type Props = { + viewType: TPageViewProps; +}; + +export const AllPagesList: React.FC = ({ viewType }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: pages } = useSWR( + workspaceSlug && projectId ? ALL_PAGES_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => pagesService.getAllPages(workspaceSlug as string, projectId as string) + : null + ); + + return ( +
+ +
+ ); +}; diff --git a/apps/app/components/pages/create-update-page-modal.tsx b/apps/app/components/pages/create-update-page-modal.tsx index e96c41e36..092e5430c 100644 --- a/apps/app/components/pages/create-update-page-modal.tsx +++ b/apps/app/components/pages/create-update-page-modal.tsx @@ -13,14 +13,14 @@ import useToast from "hooks/use-toast"; // components import { PageForm } from "./page-form"; // types -import { IPage, IPageForm } from "types"; +import { IPage } from "types"; // fetch-keys -import { PAGE_LIST } from "constants/fetch-keys"; +import { RECENT_PAGES_LIST } from "constants/fetch-keys"; type Props = { isOpen: boolean; handleClose: () => void; - data?: IPage; + data?: IPage | null; }; export const CreateUpdatePageModal: React.FC = ({ isOpen, handleClose, data }) => { @@ -33,11 +33,11 @@ export const CreateUpdatePageModal: React.FC = ({ isOpen, handleClose, da handleClose(); }; - const createPage = async (payload: IPageForm) => { + const createPage = async (payload: IPage) => { await pagesService .createPage(workspaceSlug as string, projectId as string, payload) .then(() => { - mutate(PAGE_LIST(projectId as string)); + mutate(RECENT_PAGES_LIST(projectId as string)); onClose(); setToastAlert({ @@ -55,20 +55,11 @@ export const CreateUpdatePageModal: React.FC = ({ isOpen, handleClose, da }); }; - const updatePage = async (payload: IPageForm) => { + const updatePage = async (payload: IPage) => { await pagesService .patchPage(workspaceSlug as string, projectId as string, data?.id ?? "", payload) - .then((res) => { - mutate( - PAGE_LIST(projectId as string), - (prevData) => - prevData?.map((p) => { - if (p.id === res.id) return { ...p, ...payload }; - - return p; - }), - false - ); + .then(() => { + mutate(RECENT_PAGES_LIST(projectId as string)); onClose(); setToastAlert({ @@ -86,7 +77,7 @@ export const CreateUpdatePageModal: React.FC = ({ isOpen, handleClose, da }); }; - const handleFormSubmit = async (formData: IPageForm) => { + const handleFormSubmit = async (formData: IPage) => { if (!workspaceSlug || !projectId) return; if (!data) await createPage(formData); diff --git a/apps/app/components/pages/delete-page-modal.tsx b/apps/app/components/pages/delete-page-modal.tsx index f78b5f36e..38dff3264 100644 --- a/apps/app/components/pages/delete-page-modal.tsx +++ b/apps/app/components/pages/delete-page-modal.tsx @@ -1,8 +1,7 @@ import React, { useState } from "react"; -// next + import { useRouter } from "next/router"; -// swr -import { mutate } from "swr"; + // headless ui import { Dialog, Transition } from "@headlessui/react"; // services @@ -15,13 +14,12 @@ import { DangerButton, SecondaryButton } from "components/ui"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types import type { IPage } from "types"; + type TConfirmPageDeletionProps = { isOpen: boolean; setIsOpen: React.Dispatch>; - data?: IPage; + data?: IPage | null; }; -// fetch-keys -import { PAGE_LIST } from "constants/fetch-keys"; export const DeletePageModal: React.FC = ({ isOpen, @@ -31,7 +29,7 @@ export const DeletePageModal: React.FC = ({ const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId } = router.query; const { setToastAlert } = useToast(); @@ -42,22 +40,16 @@ export const DeletePageModal: React.FC = ({ const handleDeletion = async () => { setIsDeleteLoading(true); - if (!data || !workspaceSlug) return; + if (!data || !workspaceSlug || !projectId) return; await pagesService .deletePage(workspaceSlug as string, data.project, data.id) .then(() => { - mutate( - PAGE_LIST(data.project), - (prevData) => prevData?.filter((page) => page.id !== data?.id), - false - ); handleClose(); - setToastAlert({ - title: "Success", type: "success", - message: "Page deleted successfully", + title: "Success!", + message: "Page deleted successfully.", }); }) .catch(() => { diff --git a/apps/app/components/pages/favorite-pages-list.tsx b/apps/app/components/pages/favorite-pages-list.tsx new file mode 100644 index 000000000..505195e21 --- /dev/null +++ b/apps/app/components/pages/favorite-pages-list.tsx @@ -0,0 +1,34 @@ +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import pagesService from "services/pages.service"; +// components +import { PagesView } from "components/pages"; +// types +import { TPageViewProps } from "types"; +// fetch-keys +import { FAVORITE_PAGES_LIST } from "constants/fetch-keys"; + +type Props = { + viewType: TPageViewProps; +}; + +export const FavoritePagesList: React.FC = ({ viewType }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: pages } = useSWR( + workspaceSlug && projectId ? FAVORITE_PAGES_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => pagesService.getFavoritePages(workspaceSlug as string, projectId as string) + : null + ); + + return ( +
+ +
+ ); +}; diff --git a/apps/app/components/pages/index.ts b/apps/app/components/pages/index.ts index d9700fc66..99c3700e0 100644 --- a/apps/app/components/pages/index.ts +++ b/apps/app/components/pages/index.ts @@ -1,5 +1,12 @@ +export * from "./all-pages-list"; export * from "./create-update-page-modal"; export * from "./delete-page-modal"; +export * from "./favorite-pages-list"; +export * from "./my-pages-list"; +export * from "./other-pages-list"; export * from "./page-form"; -export * from "./pages-list"; +export * from "./pages-view"; +export * from "./recent-pages-list"; +export * from "./single-page-block"; +export * from "./single-page-detailed-item"; export * from "./single-page-list-item"; diff --git a/apps/app/components/pages/my-pages-list.tsx b/apps/app/components/pages/my-pages-list.tsx new file mode 100644 index 000000000..0bf5d56ea --- /dev/null +++ b/apps/app/components/pages/my-pages-list.tsx @@ -0,0 +1,34 @@ +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import pagesService from "services/pages.service"; +// components +import { PagesView } from "components/pages"; +// types +import { TPageViewProps } from "types"; +// fetch-keys +import { MY_PAGES_LIST } from "constants/fetch-keys"; + +type Props = { + viewType: TPageViewProps; +}; + +export const MyPagesList: React.FC = ({ viewType }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: pages } = useSWR( + workspaceSlug && projectId ? MY_PAGES_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => pagesService.getMyPages(workspaceSlug as string, projectId as string) + : null + ); + + return ( +
+ +
+ ); +}; diff --git a/apps/app/components/pages/other-pages-list.tsx b/apps/app/components/pages/other-pages-list.tsx new file mode 100644 index 000000000..f0d5e8afb --- /dev/null +++ b/apps/app/components/pages/other-pages-list.tsx @@ -0,0 +1,34 @@ +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import pagesService from "services/pages.service"; +// components +import { PagesView } from "components/pages"; +// types +import { TPageViewProps } from "types"; +// fetch-keys +import { OTHER_PAGES_LIST } from "constants/fetch-keys"; + +type Props = { + viewType: TPageViewProps; +}; + +export const OtherPagesList: React.FC = ({ viewType }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: pages } = useSWR( + workspaceSlug && projectId ? OTHER_PAGES_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => pagesService.getOtherPages(workspaceSlug as string, projectId as string) + : null + ); + + return ( +
+ +
+ ); +}; diff --git a/apps/app/components/pages/page-form.tsx b/apps/app/components/pages/page-form.tsx index 452fa35db..377d35e8c 100644 --- a/apps/app/components/pages/page-form.tsx +++ b/apps/app/components/pages/page-form.tsx @@ -3,16 +3,16 @@ import { useForm } from "react-hook-form"; // ui import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; // types -import { IPageForm } from "types"; +import { IPage } from "types"; type Props = { - handleFormSubmit: (values: IPageForm) => Promise; + handleFormSubmit: (values: IPage) => Promise; handleClose: () => void; status: boolean; - data?: IPageForm; + data?: IPage | null; }; -const defaultValues: IPageForm = { +const defaultValues = { name: "", description: "", }; @@ -23,11 +23,11 @@ export const PageForm: React.FC = ({ handleFormSubmit, handleClose, statu formState: { errors, isSubmitting }, handleSubmit, reset, - } = useForm({ + } = useForm({ defaultValues, }); - const handleCreateUpdatePage = async (formData: IPageForm) => { + const handleCreateUpdatePage = async (formData: IPage) => { await handleFormSubmit(formData); reset({ diff --git a/apps/app/components/pages/page-grid-item.tsx b/apps/app/components/pages/page-grid-item.tsx deleted file mode 100644 index 5f8b7b90d..000000000 --- a/apps/app/components/pages/page-grid-item.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import React from "react"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { mutate } from "swr"; - -// services -import pagesService from "services/pages.service"; -// ui -import { CustomMenu } from "components/ui"; -// icons -import { PencilIcon, StarIcon, TrashIcon } from "@heroicons/react/24/outline"; -// helpers -import { truncateText } from "helpers/string.helper"; -// hooks -import useToast from "hooks/use-toast"; -// types -import { IPage } from "types"; -// fetch keys -import { PAGE_LIST } from "constants/fetch-keys"; -import Label from "./page-label"; - -type TSingleStatProps = { - page: IPage; - handleEditPage: () => void; - handleDeletePage: () => void; -}; - -export const SinglePageGridItem: React.FC = (props) => { - const { page, handleEditPage, handleDeletePage } = props; - - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { setToastAlert } = useToast(); - - const handleAddToFavorites = () => { - if (!workspaceSlug && !projectId && !page) return; - - pagesService - .addPageToFavorites(workspaceSlug as string, projectId as string, { - page: page.id, - }) - .then(() => { - mutate( - PAGE_LIST(projectId as string), - (prevData) => - (prevData ?? []).map((m) => ({ - ...m, - is_favorite: m.id === page.id ? true : m.is_favorite, - })), - false - ); - setToastAlert({ - type: "success", - title: "Success!", - message: "Successfully added the page to favorites.", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't add the page to favorites. Please try again.", - }); - }); - }; - - const handleRemoveFromFavorites = () => { - if (!workspaceSlug || !page) return; - - pagesService - .removePageFromFavorites(workspaceSlug as string, projectId as string, page.id) - .then(() => { - mutate( - PAGE_LIST(projectId as string), - (prevData) => - (prevData ?? []).map((m) => ({ - ...m, - is_favorite: m.id === page.id ? false : m.is_favorite, - })), - false - ); - setToastAlert({ - type: "success", - title: "Success!", - message: "Successfully removed the page from favorites.", - }); - }) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Couldn't remove the page from favorites. Please try again.", - }); - }); - }; - - return ( -
-
  • -
    -
    -
    - - -

    {truncateText(page.name, 75)}

    -
    - - - - -
    - -
    -
    -

    - {new Date(page.updated_at).toLocaleTimeString()} -

    - {page.is_favorite ? ( - - ) : ( - - )} - - - - - Edit Page - - - - - - Delete Page - - - -
    -
    -
    -
    -

    - Lorem ipsum dolor sit amet consectetur adipisicing elit. Reiciendis atque aliquam - saepe sapiente illo ratione delectus dolorem repellat, id autem, molestiae neque - quaerat ipsum perspiciatis pariatur? Unde consectetur quibusdam ut. -

    -

    - Quisquam quas expedita cupiditate ipsum cumque fugit at, optio quia ea? Id doloribus - assumenda ad magni laborum aut, aspernatur nemo similique, suscipit dolores porro - necessitatibus, inventore ab aliquid molestias. Aspernatur. -

    -

    - Beatae obcaecati minus temporibus sunt, quo nulla, tenetur nisi sit maiores aspernatur - numquam facilis asperiores eos rerum, ad dolorem quos laboriosam dicta eaque! Pariatur - magni eos, architecto itaque esse minus. -

    -

    - Dolorum saepe impedit officiis odit! Porro aliquid dolorum corporis impedit eaque - iusto, illo hic neque quia vero aperiam? Nemo aliquam, hic incidunt mollitia totam - asperiores sunt nam inventore voluptatibus eum? -

    -
    -
    -
    -
  • -
    - ); -}; diff --git a/apps/app/components/pages/page-label.tsx b/apps/app/components/pages/page-label.tsx deleted file mode 100644 index 38ffd0496..000000000 --- a/apps/app/components/pages/page-label.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; - -type TLabelProps = { - variant: "red" | "blue" | string; - children?: React.ReactNode; -}; - -const Label: React.FC = (props) => { - let color = "bg-green-100 text-green-800"; - - if (props.variant === "red") { - color = "bg-red-100 text-red-800"; - } else if (props.variant === "blue") { - color = "bg-blue-100 text-blue-800"; - } - return ( -

    - {props.children} -

    - ); -}; - -export default Label; diff --git a/apps/app/components/pages/pages-grid.tsx b/apps/app/components/pages/pages-grid.tsx deleted file mode 100644 index 0895331b1..000000000 --- a/apps/app/components/pages/pages-grid.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useState } from "react"; -// components -import { DeletePageModal } from "components/pages"; -import { Loader } from "components/ui"; -import { SinglePageGridItem } from "components/pages/page-grid-item"; -// types -import { IPage } from "types"; - -export const PagesGrid: React.FC = ({ pages, setCreateUpdatePageModal, setSelectedPage }) => { - const [pageDeleteModal, setPageDeleteModal] = useState(false); - const [selectedPageForDelete, setSelectedPageForDelete] = useState(); - - const handleDeletePage = (page: IPage) => { - setSelectedPageForDelete({ ...page, actionType: "delete" }); - setPageDeleteModal(true); - }; - - const handleEditPage = (page: IPage) => { - setSelectedPage({ ...page, actionType: "edit" }); - setCreateUpdatePageModal(true); - }; - - return ( - <> - - {pages ? ( - pages.length > 0 ? ( -
    -
      - {pages.map((page: any) => ( - handleDeletePage(page)} - handleEditPage={() => handleEditPage(page)} - /> - ))} -
    -
    - ) : ( - "No Pages found" - ) - ) : ( - - - - )} - - ); -}; diff --git a/apps/app/components/pages/pages-list.tsx b/apps/app/components/pages/pages-list.tsx deleted file mode 100644 index d595b4b4c..000000000 --- a/apps/app/components/pages/pages-list.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useState } from "react"; -// components -import { DeletePageModal } from "components/pages"; -import { Loader } from "components/ui"; -// types -import { IPage } from "types"; -import { SinglePageListItem } from "./single-page-list-item"; -type TPagesListProps = { - pages: IPage[] | undefined; - setCreateUpdatePageModal: React.Dispatch>; - setSelectedPage: React.Dispatch>; -}; - -export const PagesList: React.FC = ({ - pages, - setCreateUpdatePageModal, - setSelectedPage, -}) => { - const [pageDeleteModal, setPageDeleteModal] = useState(false); - const [selectedPageForDelete, setSelectedPageForDelete] = useState(); - - const handleDeletePage = (page: IPage) => { - setSelectedPageForDelete({ ...page, actionType: "delete" }); - setPageDeleteModal(true); - }; - - const handleEditPage = (page: IPage) => { - setSelectedPage({ ...page, actionType: "edit" }); - setCreateUpdatePageModal(true); - }; - - return ( - <> - - {pages ? ( - pages.length > 0 ? ( -
      - {pages.map((page) => ( - handleDeletePage(page)} - handleEditPage={() => handleEditPage(page)} - /> - ))} -
    - ) : ( - "No Pages found" - ) - ) : ( - - - - )} - - ); -}; diff --git a/apps/app/components/pages/pages-masonry.tsx b/apps/app/components/pages/pages-masonry.tsx deleted file mode 100644 index 10a0a726f..000000000 --- a/apps/app/components/pages/pages-masonry.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react"; - -// ui -import { CustomMenu } from "components/ui"; -// icons -import { PencilIcon, StarIcon, SwatchIcon, TrashIcon } from "@heroicons/react/24/outline"; - -const MasonryItem: React.FC = (props) => ( -
    -

    Personal Diary

    -

    {props.children}

    -
    -
    -
    - - {false ? ( - - ) : ( - - )} - - {}}> - - - Edit Page - - - {}}> - - - Delete Page - - - -
    -

    9:41 PM

    -
    -
    -
    -); - -export default function PagesMasonry() { - return ( -
    - - Lorem, ipsum dolor sit amet consectetur adipisicing elit. Impedit porro mollitia iure, - reiciendis quo tempora rem debitis velit quas doloremque. Dicta velit voluptas, blanditiis - excepturi vitae eum corporis totam eius? - - - Lorem, ipsum dolor sit amet consectetur adipisicing elit. Impedit porro mollitia iure, - reiciendis quo tempora rem debitis velit quas doloremque. Dicta velit voluptas, blanditiis - excepturi vitae eum corporis totam eius? Lorem, ipsum dolor sit amet consectetur adipisicing - elit. Impedit porro mollitia iure, reiciendis quo tempora rem debitis velit quas doloremque. - Dicta velit voluptas, blanditiis excepturi vitae eum corporis totam eius? - - - Lorem, ipsum dolor sit amet consectetur adipisicing elit. Impedit porro mollitia iure, - reiciendis quo tempora rem debitis velit quas doloremque. Dicta velit voluptas, blanditiis - excepturi vitae eum corporis totam eius? - - - Lorem, ipsum dolor sit amet consectetur adipisicing elit. Impedit porro mollitia iure, - reiciendis quo tempora rem debitis velit quas doloremque. Dicta velit voluptas, blanditiis - excepturi vitae eum corporis totam eius? Lorem, ipsum dolor sit amet consectetur adipisicing - elit. Impedit porro mollitia iure, reiciendis quo tempora rem debitis velit quas doloremque. - Dicta velit voluptas, blanditiis excepturi vitae eum corporis totam eius? - -
    - ); -} diff --git a/apps/app/components/pages/pages-view.tsx b/apps/app/components/pages/pages-view.tsx new file mode 100644 index 000000000..f4bb59741 --- /dev/null +++ b/apps/app/components/pages/pages-view.tsx @@ -0,0 +1,172 @@ +import { useState } from "react"; + +import { useRouter } from "next/router"; + +// services +import pagesService from "services/pages.service"; +// hooks +import useToast from "hooks/use-toast"; +// components +import { + CreateUpdatePageModal, + DeletePageModal, + SinglePageDetailedItem, + SinglePageListItem, +} from "components/pages"; +// ui +import { EmptyState, Loader } from "components/ui"; +// images +import emptyPage from "public/empty-state/empty-page.svg"; +// types +import { IPage, TPageViewProps } from "types"; + +type Props = { + pages: IPage[] | undefined; + viewType: TPageViewProps; +}; + +export const PagesView: React.FC = ({ pages, viewType }) => { + const [createUpdatePageModal, setCreateUpdatePageModal] = useState(false); + const [selectedPageToUpdate, setSelectedPageToUpdate] = useState(null); + + const [deletePageModal, setDeletePageModal] = useState(false); + const [selectedPageToDelete, setSelectedPageToDelete] = useState(null); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { setToastAlert } = useToast(); + + const handleEditPage = (page: IPage) => { + setSelectedPageToUpdate(page); + setCreateUpdatePageModal(true); + }; + + const handleDeletePage = (page: IPage) => { + setSelectedPageToDelete(page); + setDeletePageModal(true); + }; + + const handleAddToFavorites = (page: IPage) => { + if (!workspaceSlug || !projectId) return; + + pagesService + .addPageToFavorites(workspaceSlug as string, projectId as string, { + page: page.id, + }) + .then(() => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Successfully added the page to favorites.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't add the page to favorites. Please try again.", + }); + }); + }; + + const handleRemoveFromFavorites = (page: IPage) => { + if (!workspaceSlug || !projectId) return; + + pagesService + .removePageFromFavorites(workspaceSlug as string, projectId as string, page.id) + .then(() => { + setToastAlert({ + type: "success", + title: "Success!", + message: "Successfully removed the page from favorites.", + }); + }) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Couldn't remove the page from favorites. Please try again.", + }); + }); + }; + + return ( + <> + setCreateUpdatePageModal(false)} + data={selectedPageToUpdate} + /> + + {pages ? ( + pages.length > 0 ? ( + viewType === "list" ? ( +
      + {pages.map((page) => ( + handleEditPage(page)} + handleDeletePage={() => handleDeletePage(page)} + handleAddToFavorites={() => handleAddToFavorites(page)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} + /> + ))} +
    + ) : viewType === "detailed" ? ( +
    + {pages.map((page) => ( + handleEditPage(page)} + handleDeletePage={() => handleDeletePage(page)} + handleAddToFavorites={() => handleAddToFavorites(page)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} + /> + ))} +
    + ) : ( +
    + {pages.map((page) => ( + handleEditPage(page)} + handleDeletePage={() => handleDeletePage(page)} + handleAddToFavorites={() => handleAddToFavorites(page)} + handleRemoveFromFavorites={() => handleRemoveFromFavorites(page)} + /> + ))} +
    + ) + ) : ( + + ) + ) : viewType === "list" ? ( + + + + + + ) : ( + + + + + + )} + + ); +}; diff --git a/apps/app/components/pages/recent-pages-list.tsx b/apps/app/components/pages/recent-pages-list.tsx new file mode 100644 index 000000000..e45d23a55 --- /dev/null +++ b/apps/app/components/pages/recent-pages-list.tsx @@ -0,0 +1,46 @@ +import React from "react"; + +// components +import { PagesView } from "components/pages"; +// ui +import { Loader } from "components/ui"; +// helpers +import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; +// types +import { RecentPagesResponse, TPageViewProps } from "types"; + +type Props = { + pages: RecentPagesResponse | undefined; + viewType: TPageViewProps; +}; + +export const RecentPagesList: React.FC = ({ pages, viewType }) => ( + <> + {pages ? ( + Object.keys(pages).length > 0 ? ( +
    + {Object.keys(pages).map((key) => { + if (pages[key].length === 0) return null; + + return ( + +

    + {replaceUnderscoreIfSnakeCase(key)} +

    + +
    + ); + })} +
    + ) : ( +

    No issues found

    + ) + ) : ( + + + + + + )} + +); diff --git a/apps/app/components/pages/single-page-block.tsx b/apps/app/components/pages/single-page-block.tsx new file mode 100644 index 000000000..873d88014 --- /dev/null +++ b/apps/app/components/pages/single-page-block.tsx @@ -0,0 +1,230 @@ +import { useEffect, useState } from "react"; + +import { useRouter } from "next/router"; +import dynamic from "next/dynamic"; + +import { mutate } from "swr"; + +// react-hook-form +import { Controller, useForm } from "react-hook-form"; +// services +import pagesService from "services/pages.service"; +// hooks +import useToast from "hooks/use-toast"; +// ui +import { CustomMenu, Loader, TextArea } from "components/ui"; +// icons +import { WaterDropIcon } from "components/icons"; +// helpers +import { copyTextToClipboard } from "helpers/string.helper"; +// types +import { IPageBlock } from "types"; +// fetch-keys +import { PAGE_BLOCKS_LIST } from "constants/fetch-keys"; +import { CreateUpdateIssueModal } from "components/issues"; + +type Props = { + block: IPageBlock; +}; + +const RemirrorRichTextEditor = dynamic(() => import("components/rich-text-editor"), { + ssr: false, + loading: () => ( + + + + ), +}); + +export const SinglePageBlock: React.FC = ({ block }) => { + const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); + + const router = useRouter(); + const { workspaceSlug, projectId, pageId } = router.query; + + const { setToastAlert } = useToast(); + + const { handleSubmit, watch, reset, setValue, control } = useForm({ + defaultValues: { + name: "", + description: {}, + description_html: "

    ", + }, + }); + + const updatePageBlock = async (formData: Partial) => { + if (!workspaceSlug || !projectId || !pageId) return; + + if (!formData.name || formData.name.length === 0 || formData.name === "") return; + + mutate( + PAGE_BLOCKS_LIST(pageId as string), + (prevData) => + prevData?.map((p) => { + if (p.id === block.id) return { ...p, ...formData }; + + return p; + }), + false + ); + + await pagesService.patchPageBlock( + workspaceSlug as string, + projectId as string, + pageId as string, + block.id, + { + name: formData.name, + description: formData.description, + description_html: formData.description_html, + } + ); + }; + + const pushBlockIntoIssues = async () => { + if (!workspaceSlug || !projectId || !pageId) return; + + await pagesService + .convertPageBlockToIssue( + workspaceSlug as string, + projectId as string, + pageId as string, + block.id + ) + .then((res) => { + mutate( + PAGE_BLOCKS_LIST(pageId as string), + (prevData) => + (prevData ?? []).map((p) => { + if (p.id === block.id) return { ...p, issue: res.id }; + + return p; + }), + false + ); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Page block converted to issue successfully.", + }); + }) + .catch((res) => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Page block could not be converted to issue. Please try again.", + }); + }); + }; + + const editAndPushBlockIntoIssues = async () => { + setCreateUpdateIssueModal(true); + }; + + const deletePageBlock = async () => { + if (!workspaceSlug || !projectId || !pageId) return; + + mutate( + PAGE_BLOCKS_LIST(pageId as string), + (prevData) => (prevData ?? []).filter((p) => p.id !== block.id), + false + ); + + await pagesService + .deletePageBlock(workspaceSlug as string, projectId as string, pageId as string, block.id) + .catch(() => { + setToastAlert({ + type: "error", + title: "Error!", + message: "Page could not be deleted. Please try again.", + }); + }); + }; + + const handleCopyText = () => { + const originURL = + typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; + + copyTextToClipboard( + `${originURL}/${workspaceSlug}/projects/${projectId}/issues/${block.issue}` + ).then(() => { + setToastAlert({ + type: "success", + title: "Link Copied!", + message: "Issue link copied to clipboard.", + }); + }); + }; + + useEffect(() => { + if (!block) return; + + reset({ ...block }); + }, [reset, block]); + + return ( +
    + setCreateUpdateIssueModal(false)} + prePopulateData={{ + name: watch("name"), + description: watch("description"), + description_html: watch("description_html"), + }} + /> +
    +