This commit is contained in:
2026-03-04 17:35:14 +01:00
parent f73b56cdc4
commit db26e26495
113 changed files with 12433 additions and 169 deletions
+106
View File
@@ -0,0 +1,106 @@
import React, { ComponentProps, useRef } from "react";
import { twMerge } from "tailwind-merge";
import MarkdownEditorPreviewComponent from "./MarkdownEditorPreviewComponent";
import MarkdownEditorComponent from "./MarkdownEditorComponent";
import MarkdownEditorSelectorButtons from "./MarkdownEditorSelectorButtons";
import Row from "../../layout/Row";
import Stack from "../../layout/Stack";
import AceEditor from "../../editors/AceEditor";
import useStatus from "../../hooks/useStatus";
type Props = {
value?: string;
setValue?: React.Dispatch<React.SetStateAction<string>>;
defaultSideBySide?: boolean;
changeHandler?: (content: string) => void;
editorProps?: ComponentProps<typeof AceEditor>;
maxHeight?: string;
noToggleButtons?: boolean;
};
export default function MarkdownEditor({
value: existingValue,
setValue: setExistingValue,
defaultSideBySide,
changeHandler,
editorProps,
maxHeight: existingMaxHeight,
noToggleButtons,
}: Props) {
const [value, setValue] = React.useState<any>(existingValue || ``);
const [sideBySide, setSideBySide] = React.useState(
defaultSideBySide || false,
);
const [preview, setPreview] = React.useState(false);
const { refresh, setRefresh } = useStatus();
const updatingFromExtValueRef = useRef(false);
const maxHeight = existingMaxHeight || "600px";
React.useEffect(() => {
if (updatingFromExtValueRef.current) return;
setExistingValue?.(value);
changeHandler?.(value);
}, [value]);
React.useEffect(() => {
if (!existingValue) return;
updatingFromExtValueRef.current = true;
setValue(existingValue);
setTimeout(() => {
updatingFromExtValueRef.current = false;
setRefresh((prev) => prev + 1);
}, 500);
}, [existingValue]);
return (
<Stack className="w-full items-stretch">
{!noToggleButtons && (
<MarkdownEditorSelectorButtons
{...{ preview, setPreview, setSideBySide, sideBySide }}
/>
)}
{sideBySide ? (
<Row
className={twMerge(
`w-full grid xl:grid-cols-2 gap-6 max-h-[${maxHeight}]`,
"overflow-auto",
)}
>
<MarkdownEditorComponent
setValue={setValue}
value={value}
maxHeight={maxHeight}
refreshDepArr={[refresh]}
{...editorProps}
/>
<MarkdownEditorPreviewComponent
value={value}
maxHeight={maxHeight}
/>
</Row>
) : (
<Stack
className={twMerge(`w-full max-h-[${maxHeight}] h-full`)}
>
{preview ? (
<MarkdownEditorPreviewComponent
value={value}
maxHeight={maxHeight}
/>
) : (
<MarkdownEditorComponent
setValue={setValue}
value={value}
maxHeight={maxHeight}
refreshDepArr={[refresh]}
{...editorProps}
/>
)}
</Stack>
)}
</Stack>
);
}
@@ -0,0 +1,32 @@
import React, { ComponentProps } from "react";
import AceEditor from "../../editors/AceEditor";
type Props = ComponentProps<typeof AceEditor> & {
value: string;
setValue: React.Dispatch<any>;
maxHeight: string;
};
export default function MarkdownEditorComponent({
value,
setValue,
maxHeight,
...props
}: Props) {
return (
<AceEditor
mode="markdown"
content={value}
onChange={(newValue) => {
setValue(newValue);
}}
wrapperProps={{
style: { height: maxHeight },
className: `max-h-[${maxHeight}]`,
}}
placeholder="## Write Some markdown ..."
fontSize="14px"
{...props}
/>
);
}
@@ -0,0 +1,77 @@
import React, { ComponentProps, RefObject } from "react";
import { twMerge } from "tailwind-merge";
import { serialize } from "next-mdx-remote/serialize";
import remarkGfm from "remark-gfm";
import rehypePrismPlus from "rehype-prism-plus";
import { MDXRemote, MDXRemoteSerializeResult } from "next-mdx-remote";
import Border from "../../elements/Border";
import { useMDXComponents } from "../mdx-components";
import EmptyContent from "../../elements/EmptyContent";
type Props = {
value: string;
maxHeight: string;
wrapperProps?: ComponentProps<typeof Border>;
};
export default function MarkdownEditorPreviewComponent({
value,
maxHeight,
wrapperProps,
}: Props) {
try {
const [mdxSource, setMdxSource] =
React.useState<
MDXRemoteSerializeResult<
Record<string, unknown>,
Record<string, unknown>
>
>();
const { components } = useMDXComponents();
React.useEffect(() => {
try {
// const { data, content } = matter(value);
const parsedValue = value
.replace(/---([^(---)]+)---/, "")
.trim();
serialize(parsedValue, {
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypePrismPlus],
},
// scope: data,
})
.then((mdxSrc) => {
setMdxSource(mdxSrc);
})
.catch((err) => {});
} catch (error) {}
}, [value]);
return (
<Border
{...wrapperProps}
className={twMerge(
`w-full max-h-[${maxHeight}] h-[${maxHeight}] block px-6 pb-10`,
"overflow-auto",
wrapperProps?.className
)}
>
{mdxSource ? (
<MDXRemote
{...mdxSource}
components={{
...components,
}}
/>
) : null}
</Border>
);
} catch (error) {
return <EmptyContent title={`Markdown Syntax Error.`} />;
}
}
@@ -0,0 +1,95 @@
import React from "react";
import Button from "../../layout/Button";
import Border from "../../elements/Border";
type Props = {
preview: boolean;
setPreview: React.Dispatch<React.SetStateAction<boolean>>;
sideBySide: boolean;
setSideBySide: React.Dispatch<React.SetStateAction<boolean>>;
};
export default function MarkdownEditorSelectorButtons({
preview,
setPreview,
setSideBySide,
sideBySide,
}: Props) {
const [ready, setReady] = React.useState(false);
const defSbsName = "TWUIMarkdownEditorDefaultSideBySide";
const defPrevName = "TWUIMarkdownEditorDefaultPreview";
React.useEffect(() => {
if (!ready) return;
if (sideBySide) {
localStorage.setItem(defSbsName, "true");
} else {
localStorage.removeItem(defSbsName);
}
if (preview) {
localStorage.setItem(defPrevName, "true");
} else {
localStorage.removeItem(defPrevName);
}
}, [sideBySide, preview, ready]);
React.useEffect(() => {
if (localStorage.getItem(defPrevName)) {
setPreview(true);
}
if (!localStorage.getItem(defSbsName)) {
setSideBySide(false);
}
setTimeout(() => {
setReady(true);
}, 200);
}, []);
return (
<Border className="p-1.5">
<Button
title="Code"
size="smaller"
color={sideBySide ? "gray" : preview ? "gray" : "primary"}
variant={
sideBySide ? "outlined" : preview ? "outlined" : "normal"
}
onClick={() => {
setSideBySide(false);
setPreview(false);
}}
>
Code
</Button>
<Button
title="Preview"
size="smaller"
color={sideBySide ? "gray" : preview ? "primary" : "gray"}
variant={
sideBySide ? "outlined" : preview ? "normal" : "outlined"
}
onClick={() => {
setSideBySide(false);
setPreview(true);
}}
>
Preview
</Button>
<Button
title="Side By Side"
size="smaller"
color={sideBySide ? "primary" : "gray"}
variant={sideBySide ? "normal" : "outlined"}
onClick={() => {
setSideBySide(true);
}}
>
Side By Side
</Button>
</Border>
);
}