This commit is contained in:
Benjamin Toby
2025-07-20 10:35:54 +01:00
parent dd1d05251d
commit a0a0ab8ee4
99 changed files with 5678 additions and 909 deletions
@@ -0,0 +1,88 @@
import React, { ComponentProps } 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";
type Props = {
value?: string;
setValue?: React.Dispatch<React.SetStateAction<string>>;
defaultSideBySide?: boolean;
changeHandler?: (content: string) => void;
editorProps?: ComponentProps<typeof AceEditor>;
maxHeight?: string;
};
export default function MarkdownEditor({
value: existingValue,
setValue: setExistingValue,
defaultSideBySide,
changeHandler,
editorProps,
maxHeight: existingMaxHeight,
}: Props) {
const [value, setValue] = React.useState<any>(existingValue || ``);
const [sideBySide, setSideBySide] = React.useState(
defaultSideBySide || false
);
const [preview, setPreview] = React.useState(false);
const maxHeight = existingMaxHeight || "600px";
React.useEffect(() => {
setExistingValue?.(value);
changeHandler?.(value);
}, [value]);
return (
<Stack className="w-full items-stretch">
<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}
{...editorProps}
/>
<MarkdownEditorPreviewComponent
setValue={setValue}
value={value}
maxHeight={maxHeight}
/>
</Row>
) : (
<Stack
className={twMerge(`w-full max-h-[${maxHeight}] h-full`)}
>
{preview ? (
<MarkdownEditorPreviewComponent
setValue={setValue}
value={value}
maxHeight={maxHeight}
/>
) : (
<MarkdownEditorComponent
setValue={setValue}
value={value}
maxHeight={maxHeight}
{...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,28 @@
import MDEditor from "@uiw/react-md-editor";
import rehypeSanitize from "rehype-sanitize";
import React from "react";
import remarkGfm from "remark-gfm";
import rehypePrismPlus from "rehype-prism-plus";
import ReactDOM from "react-dom";
export default function MarkdownEditor() {
const [value, setValue] = React.useState<any>(
`**Hello world!!!** <IFRAME SRC=\"javascript:javascript:alert(window.origin);\"></IFRAME>`
);
React.useEffect(() => {
console.log("value", value);
}, [value]);
return ReactDOM.createPortal(
<MDEditor
value={value}
onChange={setValue}
previewOptions={{
rehypePlugins: [rehypeSanitize, rehypePrismPlus],
remarkPlugins: [remarkGfm],
}}
/>,
document.getElementById("markdown-modal-root") as HTMLElement
);
}
@@ -0,0 +1,77 @@
import React 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;
setValue: React.Dispatch<any>;
maxHeight: string;
};
export default function MarkdownEditorPreviewComponent({
value,
setValue,
maxHeight,
}: 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) => {
console.log(`Markdown Parsing Error => ${err.message}`);
});
} catch (error) {}
}, [value]);
return (
<Border
className={twMerge(
`w-full max-h-[${maxHeight}] h-[${maxHeight}] block px-6 pb-10`,
"overflow-auto"
)}
>
{mdxSource ? (
<MDXRemote
{...mdxSource}
components={{
...components,
}}
/>
) : null}
</Border>
);
} catch (error) {
return <EmptyContent title={`Markdown Syntax Error.`} />;
}
}
@@ -0,0 +1,111 @@
import React from "react";
import Button from "../../layout/Button";
import Border from "../../elements/Border";
import { LocalStorageDict } from "@/package-shared/dict/local-storage-dict";
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);
React.useEffect(() => {
if (!ready) return;
if (sideBySide) {
localStorage.setItem(
LocalStorageDict["MarkdownEditorDefaultSideBySide"],
"true"
);
} else {
localStorage.removeItem(
LocalStorageDict["MarkdownEditorDefaultSideBySide"]
);
}
if (preview) {
localStorage.setItem(
LocalStorageDict["MarkdownEditorDefaultPreview"],
"true"
);
} else {
localStorage.removeItem(
LocalStorageDict["MarkdownEditorDefaultPreview"]
);
}
}, [sideBySide, preview, ready]);
React.useEffect(() => {
if (
localStorage.getItem(
LocalStorageDict["MarkdownEditorDefaultPreview"]
)
) {
setPreview(true);
}
if (
!localStorage.getItem(
LocalStorageDict["MarkdownEditorDefaultSideBySide"]
)
) {
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>
);
}