Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3bfc90009 |
@@ -0,0 +1,256 @@
|
||||
"use client";
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { SerializedInlineBlockNode } from "@payloadcms/richtext-lexical";
|
||||
import { RichText, JSXConvertersFunction, JSXConverters } from "@payloadcms/richtext-lexical/react";
|
||||
import useSWR from "swr";
|
||||
import { Info, Lightbulb, Search, Zap, Megaphone } from "lucide-react";
|
||||
|
||||
type ChangelogCtaFields = {
|
||||
title: string;
|
||||
icon: "Zap" | "Lens" | "Info" | "Idea" | "Announce" | null;
|
||||
color: "Red" | "Yellow" | "Blue" | "Green";
|
||||
description: any;
|
||||
};
|
||||
type ColoredTextFields = {
|
||||
text?: string;
|
||||
tag?: string;
|
||||
id?: string;
|
||||
blockType?: "colored-text";
|
||||
};
|
||||
|
||||
type InlineBlockRendererProps = {
|
||||
node: SerializedInlineBlockNode<ColoredTextFields>;
|
||||
};
|
||||
|
||||
type InlineBlockRendererFn = (props: InlineBlockRendererProps) => React.ReactElement | null;
|
||||
|
||||
type InlineBlockRendererMap = {
|
||||
[key: string]: InlineBlockRendererFn;
|
||||
};
|
||||
|
||||
type InLineBlockConverterType = {
|
||||
inlineBlocks: InlineBlockRendererMap;
|
||||
};
|
||||
|
||||
const inLineBlockConverter: InLineBlockConverterType = {
|
||||
inlineBlocks: {
|
||||
["colored-text"]: ({ node }) => {
|
||||
const text = node.fields.text;
|
||||
|
||||
if (!text) {
|
||||
console.warn("Node for 'colored-text' inlineBlock is missing 'text' field:", node);
|
||||
return null;
|
||||
}
|
||||
|
||||
return <span className="text-blue-500 font-bold font-mono break-all">{text}</span>;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const UploadJSXConverter: JSXConverters<any> = {
|
||||
upload: ({ node }) => {
|
||||
if (node.value && node.value.url) {
|
||||
return (
|
||||
<img src={node.value.url} alt={node.value.alt || "Uploaded image"} className="w-[100%] h-auto object-cover" />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const CalloutJSXConverter: any = {
|
||||
blocks: {
|
||||
Callout: ({ node }: { node: SerializedInlineBlockNode<ChangelogCtaFields> }) => {
|
||||
const { fields } = node;
|
||||
|
||||
if (!fields) return null;
|
||||
|
||||
const iconMap = {
|
||||
Zap: <Zap />,
|
||||
Lens: <Search />,
|
||||
Info: <Info />,
|
||||
Idea: <Lightbulb />,
|
||||
Announce: <Megaphone />,
|
||||
};
|
||||
|
||||
const colorClasses = {
|
||||
Red: "border border-[#DD4167] dark:border-[#4C182C] dark:bg-[#4C182C]/40 bg-[#DD4167]/40",
|
||||
Yellow: "border border-[#D4A72C66] dark:border-[#BF8700] bg-[#FFF8C5] dark:bg-[#332E1B]",
|
||||
Blue: "border border-[#3f76ff] dark:border-[#224f6a] bg-[#d9efff] dark:bg-[#1e2934]",
|
||||
Green: "border border-[#5CD3B5] dark:border-[#235645] bg-[#D3F9E7] dark:bg-[#1E2B2A]",
|
||||
};
|
||||
const iconColorClasses = {
|
||||
Red: "text-[#DD4167]",
|
||||
Yellow: "text-[#9A6700]",
|
||||
Blue: "text-[#3f76ff] dark:text-[#4d9ed0]",
|
||||
Green: "text-[#208779] dark:text-[#A8F3D0]",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-4 pb-2 h-full w-full">
|
||||
<div className={`p-4 rounded-lg flex flex-row gap-3 ${colorClasses[fields.color] ?? colorClasses.Yellow}`}>
|
||||
<div className={`${iconColorClasses[fields.color] ?? iconColorClasses.Yellow}`}>
|
||||
{fields.icon && iconMap[fields.icon]}
|
||||
</div>
|
||||
<div>
|
||||
{fields.title && <h4 className="font-semibold mb-1 -mt-0">{fields.title}</h4>}
|
||||
{fields.description && (
|
||||
<div className="text-sm">
|
||||
<RichText
|
||||
converters={jsxConverters}
|
||||
data={fields.description}
|
||||
className="[&>ul]:list-disc [&>ol]:list-decimal [&_a]:underline"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
video: ({ node }: { node: any }) => {
|
||||
const { fields } = node;
|
||||
const { video } = fields;
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
<video controls={false} autoPlay loop muted playsInline>
|
||||
<source src={video.url} type={video.mimeType || "video/mp4"} />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({
|
||||
...defaultConverters,
|
||||
...UploadJSXConverter,
|
||||
...CalloutJSXConverter,
|
||||
...inLineBlockConverter,
|
||||
});
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
|
||||
export const ProductUpdatesBody = () => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data, isLoading, error } = useSWR("https://content.plane.so/api/community-releases?limit=3", fetcher);
|
||||
|
||||
console.log("data", data);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!containerRef.current) return;
|
||||
|
||||
// console.log("data containerRef.current", containerRef.current);
|
||||
// const container = containerRef.current;
|
||||
|
||||
// const walker = document.createTreeWalker(
|
||||
// container,
|
||||
// NodeFilter.SHOW_TEXT, // Only process text nodes
|
||||
// null // No custom filter function needed
|
||||
// // false // deprecated argument
|
||||
// );
|
||||
|
||||
// let node;
|
||||
// while ((node = walker.nextNode())) {
|
||||
// if (node.nodeValue && node.nodeValue.includes("\u00A0")) {
|
||||
// // \u00A0 is the Unicode char for
|
||||
// node.nodeValue = node.nodeValue.replace(/\u00A0/g, " "); // Replace all occurrences with a regular space
|
||||
// }
|
||||
// }
|
||||
|
||||
// const createId = (text: string): string =>
|
||||
// text
|
||||
// .toLowerCase()
|
||||
// .replace(/[^a-z0-9-\s]/g, "")
|
||||
// .replace(/\s+/g, "-");
|
||||
|
||||
// const headings = container.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
||||
// headings.forEach((heading) => {
|
||||
// const text = heading.textContent?.trim() || "";
|
||||
// const id = createId(text);
|
||||
// heading.classList.add("text-neutral-text-primary");
|
||||
// if (!heading.id) {
|
||||
// heading.id = id;
|
||||
// }
|
||||
// const htmlHeading = heading as HTMLElement;
|
||||
// switch (htmlHeading.tagName) {
|
||||
// case "H1":
|
||||
// htmlHeading.style.marginTop = "35px";
|
||||
// htmlHeading.style.marginBottom = "15px";
|
||||
// break;
|
||||
// case "H2":
|
||||
// htmlHeading.style.marginTop = "35px";
|
||||
// htmlHeading.style.marginBottom = "20px";
|
||||
// break;
|
||||
// case "H3":
|
||||
// htmlHeading.style.marginTop = "20px";
|
||||
// htmlHeading.style.marginBottom = "18px";
|
||||
// break;
|
||||
// }
|
||||
// });
|
||||
|
||||
// const listItems = container.querySelectorAll("li");
|
||||
// listItems.forEach((listItem) => {
|
||||
// listItem.classList.add("text-[#3e76fe]");
|
||||
// listItem.style.marginTop = "10px";
|
||||
// listItem.style.lineHeight = "1.5";
|
||||
// // listItem.style.overflowWrap = "break-word";
|
||||
// });
|
||||
|
||||
// const paragraphs = container.querySelectorAll("p");
|
||||
// paragraphs.forEach((paragraph) => {
|
||||
// paragraph.classList.add("text-neutral-text-primary");
|
||||
// paragraph.style.fontSize = "16px";
|
||||
// paragraph.style.marginBottom = "1px";
|
||||
// paragraph.style.marginTop = "10px";
|
||||
// paragraph.style.lineHeight = "24px";
|
||||
// // paragraph.style.overflowWrap = "break-word";
|
||||
// // paragraph.style.whiteSpace = "break-spaces";
|
||||
// });
|
||||
|
||||
// const links = container.querySelectorAll("a");
|
||||
// links.forEach((link) => {
|
||||
// const htmlLink = link as HTMLAnchorElement;
|
||||
// htmlLink.style.color = "#3191ff";
|
||||
// htmlLink.style.textDecoration = "underline";
|
||||
// htmlLink.target = "_blank";
|
||||
// });
|
||||
|
||||
// const strongElements = container.querySelectorAll("strong");
|
||||
// strongElements.forEach((strongElement) => {
|
||||
// // strongElement.classList.add("font-mono", "font-bold", "text-blue-500");
|
||||
// strongElement.classList.add("font-bold", "text-neutral-text-primary");
|
||||
// });
|
||||
|
||||
// const codeElements = container.querySelectorAll("code");
|
||||
// codeElements.forEach((codeElement) => {
|
||||
// const htmlCode = codeElement as HTMLElement;
|
||||
// htmlCode.classList.add("font-mono", "break-all", "font-bold", "text-blue-500");
|
||||
// htmlCode.style.wordBreak = "break-all";
|
||||
// });
|
||||
|
||||
// const blockquotes = container.querySelectorAll("blockquote");
|
||||
// blockquotes.forEach((blockquote) => {
|
||||
// const htmlBlockquote = blockquote as HTMLElement;
|
||||
// htmlBlockquote.style.padding = "0 16px";
|
||||
// });
|
||||
// }, []);
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-[60vh] vertical-scrollbar scrollbar-xs overflow-hidden overflow-y-scroll px-6 mx-0.5">
|
||||
{data?.docs?.map((doc: any) => (
|
||||
<div key={doc.id} className="flex flex-col gap-4 mb-6">
|
||||
<h1 className="text-2xl font-bold">{doc.title}</h1>
|
||||
<div ref={containerRef}>
|
||||
<RichText data={doc.description} converters={jsxConverters} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +1,12 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
// ui
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||
// components
|
||||
import { ProductUpdatesFooter } from "@/components/global";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { ProductUpdatesHeader } from "@/plane-web/components/global";
|
||||
import { ProductUpdatesBody } from "@/plane-web/components/global/product-updates-body";
|
||||
|
||||
export type ProductUpdatesModalProps = {
|
||||
isOpen: boolean;
|
||||
@@ -17,32 +15,11 @@ export type ProductUpdatesModalProps = {
|
||||
|
||||
export const ProductUpdatesModal: FC<ProductUpdatesModalProps> = observer((props) => {
|
||||
const { isOpen, handleClose } = props;
|
||||
const { t } = useTranslation();
|
||||
const { config } = useInstance();
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXXXL}>
|
||||
<ProductUpdatesHeader />
|
||||
<div className="flex flex-col h-[60vh] vertical-scrollbar scrollbar-xs overflow-hidden overflow-y-scroll px-6 mx-0.5">
|
||||
{config?.instance_changelog_url && config?.instance_changelog_url !== "" ? (
|
||||
<iframe src={config?.instance_changelog_url} className="w-full h-full" />
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full mb-8">
|
||||
<div className="text-lg font-medium">{t("we_are_having_trouble_fetching_the_updates")}</div>
|
||||
<div className="text-sm text-custom-text-200">
|
||||
{t("please_visit")}
|
||||
<a
|
||||
href="https://go.plane.so/p-changelog"
|
||||
target="_blank"
|
||||
className="text-sm text-custom-primary-100 font-medium hover:text-custom-primary-200 underline underline-offset-1 outline-none"
|
||||
>
|
||||
{t("our_changelogs")}
|
||||
</a>{" "}
|
||||
{t("for_the_latest_updates")}.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ProductUpdatesBody />
|
||||
<ProductUpdatesFooter />
|
||||
</ModalCore>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||
"@headlessui/react": "^1.7.3",
|
||||
"@intercom/messenger-js-sdk": "^0.0.12",
|
||||
"@payloadcms/richtext-lexical": "^3.42.0",
|
||||
"@plane/constants": "*",
|
||||
"@plane/editor": "*",
|
||||
"@plane/hooks": "*",
|
||||
|
||||
Reference in New Issue
Block a user