diff --git a/frontend/app/(auth)/dashboard/blogs/new/editor.tsx b/frontend/app/(auth)/dashboard/blogs/new/editor.tsx index 3f0552a..2e06fa8 100644 --- a/frontend/app/(auth)/dashboard/blogs/new/editor.tsx +++ b/frontend/app/(auth)/dashboard/blogs/new/editor.tsx @@ -1,120 +1,59 @@ "use client"; - 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 useApiMutation from "@/hooks/use-api"; +import dynamic from "next/dynamic"; -export default function Editor() { - const [blogContent, setBlogContent] = useState(""); - const [cursor, setCursor] = useState<{ line: number; column: number }>({ - 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 = {} +const Editor = dynamic( + () => import("@/components/editor").then((mod) => mod.Editor), + { ssr: false }, +); +export default function BlogEditor() { const { trigger, isMutating: loading, isSuccess, - } = useApiMutation( - "/blog/new", - undefined, - "POST", - false, - true, - ); + } = useApiMutation("/blog/new", undefined, "POST", false, true); return (
- ( -
- - - - - -
- ), - }), - ]} - onChange={setBlogContent} - onBlur={() => getCursorPosition(blogContent)} - markdown="ok" + - -
- Line: {cursor.line}; Column: {cursor.column} -
- -
- -
+ + ); } - diff --git a/frontend/app/globals.css b/frontend/app/globals.css index f4f4112..51e99d9 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1,3 +1,4 @@ +@import "../node_modules/@mdxeditor/editor/dist/style.css"; @tailwind base; @tailwind components; @tailwind utilities; diff --git a/frontend/components/editor.tsx b/frontend/components/editor.tsx new file mode 100644 index 0000000..cd766ca --- /dev/null +++ b/frontend/components/editor.tsx @@ -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(null); + + const onChangeDebounce = (content: string) => { + localStorage.setItem("blog_draft", content); + }; + + return ( + + , + }), + 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(), + ]} + /> + + ); +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b6ba7cf..e99fadb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -35,6 +35,7 @@ "@schedule-x/resize": "^2.15.1", "@schedule-x/theme-default": "^2.14.3", "@schedule-x/theme-shadcn": "^2.14.3", + "@tailwindcss/typography": "^0.5.16", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cookies-next": "^5.1.0", @@ -3551,6 +3552,34 @@ "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": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", @@ -7314,11 +7343,22 @@ "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": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, "license": "MIT" }, "node_modules/longest-streak": { diff --git a/frontend/package.json b/frontend/package.json index 965eec8..f616096 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "@schedule-x/resize": "^2.15.1", "@schedule-x/theme-default": "^2.14.3", "@schedule-x/theme-shadcn": "^2.14.3", + "@tailwindcss/typography": "^0.5.16", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cookies-next": "^5.1.0", diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index f7890c6..e23e534 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -1,5 +1,81 @@ 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 { darkMode: ["class"], content: [ @@ -13,6 +89,9 @@ export default { times: ["Times New Roman", "Times", "serif"], }, colors: { + neutral, + darkAccent, + darkNeutral, background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", card: { @@ -28,6 +107,7 @@ export default { foreground: "hsl(var(--primary-foreground))", }, secondary: { + ...secondary, DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, @@ -36,6 +116,7 @@ export default { foreground: "hsl(var(--muted-foreground))", }, accent: { + ...accent, DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))", }, @@ -95,5 +176,9 @@ export default { }, }, }, - plugins: [require("tailwindcss-animate")], + plugins: [ + require("tailwindcss-animate"), + + require("@tailwindcss/typography"), + ], } satisfies Config;