MDX-Editor
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@import "../node_modules/@mdxeditor/editor/dist/style.css";
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|||||||
73
frontend/components/editor.tsx
Normal file
73
frontend/components/editor.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
frontend/package-lock.json
generated
42
frontend/package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user