Compare commits

...
Author SHA1 Message Date
sriram veeraghanta d3bfc90009 chore: add changelog info from cms endpoint 2025-06-13 19:14:24 +05:30
4 changed files with 1491 additions and 56 deletions
@@ -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 &nbsp;
// 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>
);
+1
View File
@@ -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": "*",
+1232 -31
View File
File diff suppressed because it is too large Load Diff