MDX-Editor

This commit is contained in:
cdricms
2025-02-20 17:34:13 +01:00
parent 5c86b67870
commit 07c632dafe
6 changed files with 240 additions and 101 deletions

View File

@@ -1,101 +1,36 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import {
MDXEditor,
toolbarPlugin,
headingsPlugin,
listsPlugin,
quotePlugin,
thematicBreakPlugin,
markdownShortcutPlugin,
BoldItalicUnderlineToggles,
ListsToggle,
BoldItalicUnderlineTogglesProps,
MDXEditorProps,
UndoRedo,
InsertImage,
CreateLink
} from "@mdxeditor/editor";
import DOMPurify from "isomorphic-dompurify";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import useApiMutation from "@/hooks/use-api"; import useApiMutation from "@/hooks/use-api";
import dynamic from "next/dynamic";
export default function Editor() { const Editor = dynamic(
const [blogContent, setBlogContent] = useState(""); () => import("@/components/editor").then((mod) => mod.Editor),
const [cursor, setCursor] = useState<{ line: number; column: number }>({ { ssr: false },
line: 0, );
column: 0,
});
const getCursorPosition = (newText: string) => {
const cursorPos = newText.length;
const lines = newText.substring(0, cursorPos).split("\n");
const line = lines.length; // Current line number
const column = lines[lines.length - 1].length + 1; // Current column number
setCursor({ line, column });
};
const sanitized = DOMPurify.sanitize(blogContent);
const propsa: BoldItalicUnderlineTogglesProps = { options: ["Bold", "Italic", "Underline"] }
const mdxEditorProps: MDXEditorProps = {}
export default function BlogEditor() {
const { const {
trigger, trigger,
isMutating: loading, isMutating: loading,
isSuccess, isSuccess,
} = useApiMutation( } = useApiMutation("/blog/new", undefined, "POST", false, true);
"/blog/new",
undefined,
"POST",
false,
true,
);
return ( return (
<section className="m-10"> <section className="m-10">
<div className="flex"> <div className="flex">
<div className="flex-1"> <div className="flex-1">
<MDXEditor <Editor
plugins={[ markdown={localStorage.getItem("blog_draft") ?? ""}
headingsPlugin(),
listsPlugin(),
quotePlugin(),
thematicBreakPlugin(),
markdownShortcutPlugin(),
toolbarPlugin({
toolbarContents: () => (
<div className="flex flex-row">
<UndoRedo />
<BoldItalicUnderlineToggles {...propsa} />
<InsertImage />
<CreateLink />
<ListsToggle />
</div>
),
}),
]}
onChange={setBlogContent}
onBlur={() => getCursorPosition(blogContent)}
markdown="ok"
/>
<div>
Line: {cursor.line}; Column: {cursor.column}
</div>
</div>
<div
className="mt-4 w-1/2 p-2 bg-gray-100 border rounded-md text-sm text-black"
dangerouslySetInnerHTML={{
__html: sanitized,
}}
/> />
</div> </div>
</div>
<Button className="text-black bg-white" onClick={async () => { <Button
className="text-black bg-white"
onClick={async () => {
try { try {
const blogContent = localStorage.getItem("blog_draft");
const res = await trigger({ const res = await trigger({
label: "This is my label", label: "This is my label",
summary: "A summary", summary: "A summary",
@@ -106,15 +41,19 @@ export default function Editor() {
content: blogContent, content: blogContent,
published: "", published: "",
}); });
if (!res) throw new Error("The server hasn't responded."); if (!res)
if (res.status === "Error") throw new Error(res.message); throw new Error("The server hasn't responded.");
if (res.status === "Error")
throw new Error(res.message);
if (res.data) console.log(res.data); if (res.data) console.log(res.data);
return res; return res;
} catch (error: any) { } catch (error: any) {
throw new Error(error.message); throw new Error(error.message);
} }
}}>Sauvegarder</Button> }}
>
Sauvegarder
</Button>
</section> </section>
); );
} }

View File

@@ -1,3 +1,4 @@
@import "../node_modules/@mdxeditor/editor/dist/style.css";
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View File

@@ -0,0 +1,73 @@
"use client";
import * as React from "react";
import {
MDXEditor,
headingsPlugin,
listsPlugin,
quotePlugin,
thematicBreakPlugin,
markdownShortcutPlugin,
toolbarPlugin,
linkPlugin,
linkDialogPlugin,
type MDXEditorMethods,
KitchenSinkToolbar,
tablePlugin,
imagePlugin,
frontmatterPlugin,
codeBlockPlugin,
directivesPlugin,
AdmonitionDirectiveDescriptor,
} from "@mdxeditor/editor";
import { cn } from "@/lib/utils";
import { Card } from "@/components/ui/card";
interface EditorProps {
markdown: string;
onChange?: (markdown: string) => void;
className?: string;
}
export function Editor({ markdown, onChange, className }: EditorProps) {
const ref = React.useRef<MDXEditorMethods>(null);
const onChangeDebounce = (content: string) => {
localStorage.setItem("blog_draft", content);
};
return (
<Card className={cn("border rounded-lg overflow-hidden", className)}>
<MDXEditor
ref={ref}
className="mdx-editor dark-theme dark-editor prose dark:prose-invert p-4"
markdown={markdown}
onChange={onChangeDebounce}
spellCheck
plugins={[
toolbarPlugin({
toolbarContents: () => <KitchenSinkToolbar />,
}),
listsPlugin(),
quotePlugin(),
headingsPlugin(),
linkPlugin(),
linkDialogPlugin(),
// eslint-disable-next-line @typescript-eslint/require-await
imagePlugin({
imageUploadHandler: async () => "/sample-image.png",
}),
tablePlugin(),
thematicBreakPlugin(),
frontmatterPlugin(),
codeBlockPlugin({ defaultCodeBlockLanguage: "txt" }),
directivesPlugin({
directiveDescriptors: [AdmonitionDirectiveDescriptor],
}),
markdownShortcutPlugin(),
]}
/>
</Card>
);
}

View File

@@ -35,6 +35,7 @@
"@schedule-x/resize": "^2.15.1", "@schedule-x/resize": "^2.15.1",
"@schedule-x/theme-default": "^2.14.3", "@schedule-x/theme-default": "^2.14.3",
"@schedule-x/theme-shadcn": "^2.14.3", "@schedule-x/theme-shadcn": "^2.14.3",
"@tailwindcss/typography": "^0.5.16",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cookies-next": "^5.1.0", "cookies-next": "^5.1.0",
@@ -3551,6 +3552,34 @@
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
}, },
"node_modules/@tailwindcss/typography": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
"integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==",
"license": "MIT",
"dependencies": {
"lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.merge": "^4.6.2",
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@types/acorn": { "node_modules/@types/acorn": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz",
@@ -7314,11 +7343,22 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/longest-streak": { "node_modules/longest-streak": {

View File

@@ -36,6 +36,7 @@
"@schedule-x/resize": "^2.15.1", "@schedule-x/resize": "^2.15.1",
"@schedule-x/theme-default": "^2.14.3", "@schedule-x/theme-default": "^2.14.3",
"@schedule-x/theme-shadcn": "^2.14.3", "@schedule-x/theme-shadcn": "^2.14.3",
"@tailwindcss/typography": "^0.5.16",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cookies-next": "^5.1.0", "cookies-next": "^5.1.0",

View File

@@ -1,5 +1,81 @@
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss";
import { slate, blue, blueDark, slateDark, pink } from "@radix-ui/colors";
const accent = {
base: blue.blue1,
bgSubtle: blue.blue2,
bg: blue.blue3,
bgHover: blue.blue4,
bgActive: blue.blue5,
line: blue.blue6,
border: blue.blue7,
borderHover: blue.blue8,
solid: blue.blue9,
solidHover: blue.blue10,
text: blue.blue11,
textContrast: blue.blue12,
};
const secondary = {
base: pink.pink1,
bgSubtle: pink.pink2,
bg: pink.pink3,
bgHover: pink.pink4,
bgActive: pink.pink5,
line: pink.pink6,
border: pink.pink7,
borderHover: pink.pink8,
solid: pink.pink9,
solidHover: pink.pink10,
text: pink.pink11,
textContrast: pink.pink12,
};
const neutral = {
base: slate.slate1,
bgSubtle: slate.slate2,
bg: slate.slate3,
bgHover: slate.slate4,
bgActive: slate.slate5,
line: slate.slate6,
border: slate.slate7,
borderHover: slate.slate8,
solid: slate.slate9,
solidHover: slate.slate10,
text: slate.slate11,
textContrast: slate.slate12,
};
const darkAccent = {
base: blueDark.blue1,
bgSubtle: blueDark.blue2,
bg: blueDark.blue3,
bgHover: blueDark.blue4,
bgActive: blueDark.blue5,
line: blueDark.blue6,
border: blueDark.blue7,
borderHover: blueDark.blue8,
solid: blueDark.blue9,
solidHover: blueDark.blue10,
text: blueDark.blue11,
textContrast: blueDark.blue12,
};
const darkNeutral = {
base: slateDark.slate1,
bgSubtle: slateDark.slate2,
bg: slateDark.slate3,
bgHover: slateDark.slate4,
bgActive: slateDark.slate5,
line: slateDark.slate6,
border: slateDark.slate7,
borderHover: slateDark.slate8,
solid: slateDark.slate9,
solidHover: slateDark.slate10,
text: slateDark.slate11,
textContrast: slateDark.slate12,
};
export default { export default {
darkMode: ["class"], darkMode: ["class"],
content: [ content: [
@@ -13,6 +89,9 @@ export default {
times: ["Times New Roman", "Times", "serif"], times: ["Times New Roman", "Times", "serif"],
}, },
colors: { colors: {
neutral,
darkAccent,
darkNeutral,
background: "hsl(var(--background))", background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))", foreground: "hsl(var(--foreground))",
card: { card: {
@@ -28,6 +107,7 @@ export default {
foreground: "hsl(var(--primary-foreground))", foreground: "hsl(var(--primary-foreground))",
}, },
secondary: { secondary: {
...secondary,
DEFAULT: "hsl(var(--secondary))", DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))", foreground: "hsl(var(--secondary-foreground))",
}, },
@@ -36,6 +116,7 @@ export default {
foreground: "hsl(var(--muted-foreground))", foreground: "hsl(var(--muted-foreground))",
}, },
accent: { accent: {
...accent,
DEFAULT: "hsl(var(--accent))", DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))", foreground: "hsl(var(--accent-foreground))",
}, },
@@ -95,5 +176,9 @@ export default {
}, },
}, },
}, },
plugins: [require("tailwindcss-animate")], plugins: [
require("tailwindcss-animate"),
require("@tailwindcss/typography"),
],
} satisfies Config; } satisfies Config;