diff --git a/apps/app/components/ui/index.ts b/apps/app/components/ui/index.ts index 855655062..3f50d1a21 100644 --- a/apps/app/components/ui/index.ts +++ b/apps/app/components/ui/index.ts @@ -20,3 +20,5 @@ export * from "./progress-bar"; export * from "./spinner"; export * from "./tooltip"; export * from "./toggle-switch"; +export * from "./markdown-to-component"; +export * from "./product-updates-modal"; diff --git a/apps/app/components/ui/markdown-to-component.tsx b/apps/app/components/ui/markdown-to-component.tsx new file mode 100644 index 000000000..cb716bc22 --- /dev/null +++ b/apps/app/components/ui/markdown-to-component.tsx @@ -0,0 +1,77 @@ +import React from "react"; +import ReactMarkdown from "react-markdown"; + +interface CustomComponentProps { + href: string; + children: React.ReactNode; +} + +type CustomComponent = React.ComponentType; + +interface Props { + markdown: string; + components?: { + a?: CustomComponent; + blockquote?: CustomComponent; + code?: CustomComponent; + del?: CustomComponent; + em?: CustomComponent; + heading?: CustomComponent; + hr?: CustomComponent; + image?: CustomComponent; + inlineCode?: CustomComponent; + link?: CustomComponent; + list?: CustomComponent; + listItem?: CustomComponent; + paragraph?: CustomComponent; + strong?: CustomComponent; + table?: CustomComponent; + tableCell?: CustomComponent; + tableHead?: CustomComponent; + tableRow?: CustomComponent; + }; + options?: any; +} + +const HeadingPrimary: CustomComponent = ({ children }) => ( +

{children}

+); + +const HeadingSecondary: CustomComponent = ({ children }) => ( +

{children}

+); + +const Paragraph: CustomComponent = ({ children }) => ( +

{children}

+); + +const OrderedList: CustomComponent = ({ children }) => ( +
    {children}
+); + +const UnorderedList: CustomComponent = ({ children }) => ( + +); + +const Link: CustomComponent = ({ href, children }) => ( + + {children} + +); + +export const MarkdownRenderer: React.FC = ({ markdown, options = {} }) => { + const customComponents = { + h1: HeadingPrimary, + h3: HeadingSecondary, + p: Paragraph, + ol: OrderedList, + ul: UnorderedList, + a: Link, + }; + + return ( + + {markdown} + + ); +}; diff --git a/apps/app/components/ui/product-updates-modal.tsx b/apps/app/components/ui/product-updates-modal.tsx new file mode 100644 index 000000000..24214b488 --- /dev/null +++ b/apps/app/components/ui/product-updates-modal.tsx @@ -0,0 +1,99 @@ +import React from "react"; +import useSWR from "swr"; + +// headless ui +import { Dialog, Transition } from "@headlessui/react"; +// component +import { MarkdownRenderer, Spinner } from "components/ui"; +// icons +import { XMarkIcon } from "@heroicons/react/20/solid"; +// services +import workspaceService from "services/workspace.service"; +// helper +import { renderLongDateFormat } from "helpers/date-time.helper"; + +type Props = { + isOpen: boolean; + setIsOpen: React.Dispatch>; +}; + +export const ProductUpdatesModal: React.FC = ({ isOpen, setIsOpen }) => { + const { data: updates } = useSWR("PRODUCT_UPDATES", () => workspaceService.getProductUpdates()); + return ( + + + +
+ + +
+
+ + +
+
+
+ + Product Updates + + + + + {updates && updates.length > 0 ? ( + updates.map((item, index: number) => ( + <> +
+ + {item.tag_name} + + {renderLongDateFormat(item.published_at)} + {index === 0 && ( + + New + + )} +
+ + + )) + ) : ( +
+ + Loading... +
+ )} +
+
+
+
+
+
+
+
+
+ ); +}; diff --git a/apps/app/package.json b/apps/app/package.json index 6911625b2..d3c414210 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -36,6 +36,7 @@ "react-dom": "18.2.0", "react-dropzone": "^14.2.3", "react-hook-form": "^7.38.0", + "react-markdown": "^8.0.7", "recharts": "^2.3.2", "remirror": "^2.0.23", "swr": "^1.3.0", diff --git a/apps/app/pages/[workspaceSlug]/index.tsx b/apps/app/pages/[workspaceSlug]/index.tsx index adf893f88..5edef60eb 100644 --- a/apps/app/pages/[workspaceSlug]/index.tsx +++ b/apps/app/pages/[workspaceSlug]/index.tsx @@ -15,6 +15,7 @@ import { IssuesPieChart, IssuesStats, } from "components/workspace"; +import { ProductUpdatesModal } from "components/ui"; // types import type { NextPage } from "next"; // fetch-keys @@ -22,6 +23,7 @@ import { USER_WORKSPACE_DASHBOARD } from "constants/fetch-keys"; const WorkspacePage: NextPage = () => { const [month, setMonth] = useState(new Date().getMonth() + 1); + const [isProductUpdatesModalOpen, setIsProductUpdatesModalOpen] = useState(false); const router = useRouter(); const { workspaceSlug } = router.query; @@ -39,6 +41,10 @@ const WorkspacePage: NextPage = () => { return ( +
{ Plane is open source, support us by starring us on GitHub.

- {/* - View roadmap - */} + { + return this.get("/api/release-notes/") + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } export default new WorkspaceService(); diff --git a/apps/app/types/workspace.d.ts b/apps/app/types/workspace.d.ts index e31d8f481..91948af46 100644 --- a/apps/app/types/workspace.d.ts +++ b/apps/app/types/workspace.d.ts @@ -82,3 +82,55 @@ export interface IWorkspaceSearchResults { page: IWorkspaceDefaultSearchResult[]; }; } + +export interface IProductUpdateResponse { + url: string; + assets_url: string; + upload_url: string; + html_url: string; + id: number; + author: { + login: string; + id: string; + node_id: string; + avatar_url: string; + gravatar_id: ""; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: false; + }; + node_id: string; + tag_name: string; + target_commitish: string; + name: string; + draft: boolean; + prerelease: true; + created_at: string; + published_at: string; + assets: []; + tarball_url: string; + zipball_url: string; + body: string; + reactions: { + url: string; + total_count: number; + "+1": number; + "-1": number; + laugh: number; + hooray: number; + confused: number; + heart: number; + rocket: number; + eyes: number; + }; +}