From 3e4cbf1ab9b10710bc2bab1f2c4ad323069f0027 Mon Sep 17 00:00:00 2001 From: caverav Date: Thu, 30 Jan 2025 21:58:27 -0300 Subject: [PATCH 01/19] =?UTF-8?q?=F0=9F=94=A7=20chore:=20update=20frontend?= =?UTF-8?q?=20dependencies=20including=20tiptap=20extensions=20and=20react?= =?UTF-8?q?=20package=20to=20version=202.11.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 10568834..865e5f9a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,14 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.3", + "@tiptap/extension-bubble-menu": "^2.11.4", + "@tiptap/extension-floating-menu": "^2.11.4", + "@tiptap/extension-image": "^2.11.4", + "@tiptap/extension-link": "^2.11.4", + "@tiptap/extension-placeholder": "^2.11.4", + "@tiptap/extension-underline": "^2.11.4", + "@tiptap/react": "^2.11.4", + "@tiptap/starter-kit": "^2.11.4", "@types/react-router-dom": "^5.3.3", "@visx/text": "^3.3.0", "@visx/wordcloud": "^3.3.0", @@ -68,7 +76,7 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.9.0", "eslint-plugin-no-secrets": "^1.0.2", - "eslint-plugin-prefer-arrow-functions": "^3.4.1", + "eslint-plugin-prefer-arrow-functions": "3.4.1", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.35.0", "eslint-plugin-react-hooks": "^4.6.2", From 5adcaf6b73e9ccaac595977d945ddf956c199819 Mon Sep 17 00:00:00 2001 From: caverav Date: Thu, 30 Jan 2025 21:59:08 -0300 Subject: [PATCH 02/19] =?UTF-8?q?=E2=9C=A8=20feat:=20Update=20RichText=20c?= =?UTF-8?q?omponent=20with=20Tiptap=20editor=20and=20custom=20styling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/text/RichText.tsx | 212 ++++++++++++++++------ 1 file changed, 157 insertions(+), 55 deletions(-) diff --git a/frontend/src/components/text/RichText.tsx b/frontend/src/components/text/RichText.tsx index 497c6fd8..b2457a98 100644 --- a/frontend/src/components/text/RichText.tsx +++ b/frontend/src/components/text/RichText.tsx @@ -1,72 +1,174 @@ -import './css/quill.snow.css'; -import './css/quill-styles.css'; +import './css/RichText.css'; -import { Field, Label } from '@headlessui/react'; -import ReactQuill from 'react-quill-new'; +import Image from '@tiptap/extension-image'; +import Link from '@tiptap/extension-link'; +import Placeholder from '@tiptap/extension-placeholder'; +import Underline from '@tiptap/extension-underline'; +import { Editor, EditorContent, useEditor } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import type { FC } from 'react'; +import React, { useRef } from 'react'; -type RichTextEditorProps = { - label: string; +export type RichTextProps = { + label?: string; + placeholder?: string; value: string; - placeholder: string; - onChange: (content: string) => void; + onChange: (value: string) => void; }; -const RichText: React.FC = ({ - label, +const RichText: FC = ({ + label = 'Rich Text', + placeholder = 'Escribe aquí...', value, - placeholder, onChange, }) => { - const modules = { - toolbar: [ - [{ header: [1, 2, 3, false] }, { font: [] }], - [{ size: [] }], - ['bold', 'italic', 'underline', 'strike', 'blockquote'], - [ - { list: 'ordered' }, - { list: 'bullet' }, - { indent: '-1' }, - { indent: '+1' }, - ], - ['link', 'image', 'code-block'], - ['clean'], + const fileInputRef = useRef(null); + + const editor = useEditor({ + extensions: [ + StarterKit, + Underline, + Link.configure({ openOnClick: false }), + Image, + Placeholder.configure({ placeholder }), ], - clipboard: { - matchVisual: false, + content: value, + onUpdate: ({ editor }) => { + onChange(editor.getHTML()); }, + }); + + const handleUploadClick = () => { + fileInputRef.current?.click(); }; + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file || !editor) { + return; + } + + const reader = new FileReader(); + reader.onload = e => { + if (e.target?.result) { + editor + .chain() + .focus() + .setImage({ src: e.target.result as string }) + .run(); + } + }; + reader.readAsDataURL(file); + + event.target.value = ''; + }; + + if (!editor) { + return null; + } + return ( -
- - - onChange(value)} - placeholder={placeholder} - theme="snow" - value={value} - /> - +
+ {label ? : null} + + + + + + {/* Área editable */} +
+ +
); }; + +const Toolbar: FC<{ editor: Editor; onUploadClick: () => void }> = ({ + editor, + onUploadClick, +}) => { + const setLink = () => { + const previousUrl = editor.getAttributes('link').href; + const url = window.prompt('URL', previousUrl || 'https://'); + if (url === null) { + return; + } + if (url === '') { + editor.chain().focus().extendMarkRange('link').unsetLink().run(); + return; + } + editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run(); + }; + + return ( +
+ + + + + + + {/* Encabezados */} + + + + + {/* Listas */} + + + + {/* Link */} + + + + +
+ ); +}; + export default RichText; From 3955940db588aa3942b0b16cd28faae671bf7337 Mon Sep 17 00:00:00 2001 From: caverav Date: Thu, 30 Jan 2025 22:01:11 -0300 Subject: [PATCH 03/19] =?UTF-8?q?=F0=9F=92=84=20style:=20Add=20CSS=20style?= =?UTF-8?q?s=20for=20a=20rich=20text=20editor=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/text/css/RichText.css | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 frontend/src/components/text/css/RichText.css diff --git a/frontend/src/components/text/css/RichText.css b/frontend/src/components/text/css/RichText.css new file mode 100644 index 00000000..99bc24ca --- /dev/null +++ b/frontend/src/components/text/css/RichText.css @@ -0,0 +1,76 @@ +.rich-text-container { + width: 100%; + margin-bottom: 1rem; +} + +.dark-mode { + background-color: #2d2d2d; + color: #dddddd; + border: 1px solid #444; + padding: 16px; + border-radius: 6px; +} + +.rich-text-label { + display: block; + margin-bottom: 8px; + font-weight: bold; +} + +.toolbar { + display: flex; + flex-wrap: wrap; + gap: 6px; + background-color: #3c3c3c; + padding: 6px; + border-radius: 4px; +} + +.toolbar button { + background-color: #555; + color: #fff; + border: none; + outline: none; + padding: 6px 10px; + border-radius: 4px; + cursor: pointer; + transition: all 0.15s ease-in-out; +} + +.toolbar button:hover { + background-color: #666; +} + +.toolbar button.is-active { + background-color: #888; + font-weight: bold; +} + +.rich-text-editor { + background-color: #1e1e1e; + border: 1px solid #444; + margin-top: 8px; + min-height: 200px; + border-radius: 4px; + padding: 8px; +} + +.ProseMirror { + color: #eee; + min-height: 180px; + outline: none; + font-family: 'Inter', sans-serif; + line-height: 1.6; + background: transparent; +} + +.ProseMirror:focus { + outline: none; +} + +.ProseMirror h2, +.ProseMirror h3 { + color: #fff; + margin-top: 1rem; +} + From 4d65e63fdd6c9e04b18acd6838f52db9cca9cb46 Mon Sep 17 00:00:00 2001 From: caverav Date: Fri, 31 Jan 2025 13:35:59 -0300 Subject: [PATCH 04/19] =?UTF-8?q?=E2=9C=A8=20feat:=20update=20frontend=20d?= =?UTF-8?q?ependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated frontend package.json with new dependencies: - added "@ant-design/icons": "^5.6.0" - added "@tiptap/extension-heading": "^2.11.4" - added "@tiptap/extension-highlight": "^2.11.4" - added "diff": "^7.0.0" - added "katex": "^0.16.21" - added "reactjs-tiptap-editor": "^0.1.14" --- frontend/package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index 865e5f9a..55078da6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "preview": "vite preview" }, "dependencies": { + "@ant-design/icons": "^5.6.0", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@headlessui/react": "^2.1.2", @@ -28,6 +29,8 @@ "@radix-ui/react-tooltip": "^1.1.3", "@tiptap/extension-bubble-menu": "^2.11.4", "@tiptap/extension-floating-menu": "^2.11.4", + "@tiptap/extension-heading": "^2.11.4", + "@tiptap/extension-highlight": "^2.11.4", "@tiptap/extension-image": "^2.11.4", "@tiptap/extension-link": "^2.11.4", "@tiptap/extension-placeholder": "^2.11.4", @@ -45,10 +48,12 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "dayjs": "^1.11.7", + "diff": "^7.0.0", "html2canvas": "^1.4.1", "i18next": "^23.12.2", "i18next-browser-languagedetector": "^8.0.0", "jspdf": "^2.5.2", + "katex": "^0.16.21", "lucide-react": "^0.435.0", "react": "^18.3.1", "react-chartjs-2": "^5.2.0", @@ -57,6 +62,7 @@ "react-icons": "^5.3.0", "react-quill-new": "^3.3.0", "react-router-dom": "^6.25.1", + "reactjs-tiptap-editor": "^0.1.14", "sonner": "^1.5.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7" From 18f7f01321866fb2bd13024ad909ef00904b37ba Mon Sep 17 00:00:00 2001 From: caverav Date: Fri, 31 Jan 2025 14:29:24 -0300 Subject: [PATCH 05/19] =?UTF-8?q?=E2=9C=A8=20feat:=20Update=20frontend=20d?= =?UTF-8?q?ependencies,=20adding=20"@tiptap/core"=20version=20"^2.11.5"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/package.json b/frontend/package.json index 55078da6..5ce19271 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.3", + "@tiptap/core": "^2.11.5", "@tiptap/extension-bubble-menu": "^2.11.4", "@tiptap/extension-floating-menu": "^2.11.4", "@tiptap/extension-heading": "^2.11.4", From 76c608e323648907d35f4f6d13baf1ec9df29b65 Mon Sep 17 00:00:00 2001 From: caverav Date: Fri, 31 Jan 2025 14:29:47 -0300 Subject: [PATCH 06/19] =?UTF-8?q?=E2=9C=A8=20feat:=20Updated=20RichText=20?= =?UTF-8?q?component=20with=20additional=20features=20like=20Katex,=20Colo?= =?UTF-8?q?r,=20and=20Font=20options,=20and=20improved=20image=20handling?= =?UTF-8?q?=20through=20drag-and-drop=20functionality.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/text/RichText.tsx | 335 +++++++++++----------- 1 file changed, 175 insertions(+), 160 deletions(-) diff --git a/frontend/src/components/text/RichText.tsx b/frontend/src/components/text/RichText.tsx index b2457a98..8e712b84 100644 --- a/frontend/src/components/text/RichText.tsx +++ b/frontend/src/components/text/RichText.tsx @@ -1,173 +1,188 @@ -import './css/RichText.css'; - -import Image from '@tiptap/extension-image'; -import Link from '@tiptap/extension-link'; -import Placeholder from '@tiptap/extension-placeholder'; -import Underline from '@tiptap/extension-underline'; -import { Editor, EditorContent, useEditor } from '@tiptap/react'; -import StarterKit from '@tiptap/starter-kit'; -import type { FC } from 'react'; -import React, { useRef } from 'react'; - -export type RichTextProps = { - label?: string; - placeholder?: string; - value: string; - onChange: (value: string) => void; -}; - -const RichText: FC = ({ - label = 'Rich Text', - placeholder = 'Escribe aquí...', - value, - onChange, -}) => { - const fileInputRef = useRef(null); - - const editor = useEditor({ - extensions: [ - StarterKit, - Underline, - Link.configure({ openOnClick: false }), - Image, - Placeholder.configure({ placeholder }), - ], - content: value, - onUpdate: ({ editor }) => { - onChange(editor.getHTML()); +import 'katex/dist/katex.min.css'; +import 'reactjs-tiptap-editor/style.css'; + +// import { Label } from '@headlessui/react'; +// import Placeholder from '@tiptap/extension-placeholder'; +import { useRef, useState } from 'react'; +import RcTiptapEditor, { + Attachment, + BaseKit, + Blockquote, + Bold, + BulletList, + Clear, + Code, + CodeBlock, + Color, + ColumnActionButton, + Emoji, + Excalidraw, + FontFamily, + FontSize, + FormatPainter, + Heading, + Highlight, + History, + Iframe, + Image, + Indent, + Italic, + Katex, + Link, + Mention, + MoreMark, + OrderedList, + SearchAndReplace, + SlashCommand, + Strike, + Table, + TableOfContents, + TaskList, + Underline, +} from 'reactjs-tiptap-editor'; + +const imagesUrl = '/api/images/'; + +const extensions = [ + BaseKit.configure({ + multiColumn: true, + placeholder: { + showOnlyCurrent: true, }, - }); - - const handleUploadClick = () => { - fileInputRef.current?.click(); - }; - - const handleFileChange = (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (!file || !editor) { - return; - } + characterCount: { + limit: 50_000, + }, + }), + History, + SearchAndReplace, + TableOfContents, + FormatPainter.configure({ spacer: true }), + Clear, + FontFamily, + Heading.configure({ spacer: true }), + FontSize, + Bold, + Italic, + Underline, + Strike, + MoreMark, + Katex, + Emoji, + Color.configure({ spacer: true }), + Highlight, + BulletList, + OrderedList, + Indent, + TaskList.configure({ + spacer: true, + taskItem: { + nested: true, + }, + }), + Link, + Image.configure({ + upload: async (file: File) => { + const base64Value = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + if (reader.result) { + resolve(reader.result.toString()); + } else { + reject(new Error('File reading failed')); + } + }; + reader.onerror = error => reject(error); + }); + + const audit = { + name: file.name, + value: base64Value, + }; + + const response = await fetch(imagesUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(audit), + }); + + const data = await response.json(); + return data.datas._id; + }, + }), + Blockquote.configure({ spacer: true }), + SlashCommand, + Code.configure({ + toolbar: false, + }), + CodeBlock.configure({ defaultTheme: 'dracula' }), + ColumnActionButton, + Table, + Iframe, + Excalidraw, + Mention, + Attachment.configure({ + upload: async (file: File) => { + const formData = new FormData(); + formData.append('value', await file.text()); + const response = await fetch('/api/images/', { + method: 'POST', + body: formData, + }); + const data = await response.json(); + return data.value; + }, + }), +]; - const reader = new FileReader(); - reader.onload = e => { - if (e.target?.result) { - editor - .chain() - .focus() - .setImage({ src: e.target.result as string }) - .run(); - } - }; - reader.readAsDataURL(file); +function debounce(func: (value: string) => void, wait: number) { + let timeout: NodeJS.Timeout; + return function (this: unknown, ...args: [string]) { + clearTimeout(timeout); - event.target.value = ''; + timeout = setTimeout(() => func.apply(this, args), wait); }; +} - if (!editor) { - return null; - } - - return ( -
- {label ? : null} - - - - - - {/* Área editable */} -
- -
-
- ); +type RichTextProps = { + label: string; + onChange: (value: string) => void; + placeholder: string; + value: string; }; -const Toolbar: FC<{ editor: Editor; onUploadClick: () => void }> = ({ - editor, - onUploadClick, -}) => { - const setLink = () => { - const previousUrl = editor.getAttributes('link').href; - const url = window.prompt('URL', previousUrl || 'https://'); - if (url === null) { - return; - } - if (url === '') { - editor.chain().focus().extendMarkRange('link').unsetLink().run(); - return; - } - editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run(); - }; +const RichText = ({ label, onChange, placeholder, value }: RichTextProps) => { + const [content, setContent] = useState(value || placeholder); + const refEditor = useRef(null); - return ( -
- - - - - - - {/* Encabezados */} - - - + const disable = false; - {/* Listas */} - - + const onValueChange = debounce((value: string) => { + onChange(value); + setContent(value); + }, 300); - {/* Link */} - - - - -
+ return ( +
+

{label}

+
+ +
+
); }; From a9b1426c53781ab294df4575de7b1600b67fbda4 Mon Sep 17 00:00:00 2001 From: caverav Date: Fri, 31 Jan 2025 14:30:03 -0300 Subject: [PATCH 07/19] =?UTF-8?q?=F0=9F=92=84=20style:=20Improve=20editor?= =?UTF-8?q?=20styling=20and=20add=20new=20features=20including=20table=20c?= =?UTF-8?q?ustomization,=20code=20block=20formatting,=20and=20text=20selec?= =?UTF-8?q?tion=20enhancements.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/text/css/RichText.css | 178 ++++++++++++------ 1 file changed, 124 insertions(+), 54 deletions(-) diff --git a/frontend/src/components/text/css/RichText.css b/frontend/src/components/text/css/RichText.css index 99bc24ca..3cb16e31 100644 --- a/frontend/src/components/text/css/RichText.css +++ b/frontend/src/components/text/css/RichText.css @@ -1,76 +1,146 @@ -.rich-text-container { - width: 100%; - margin-bottom: 1rem; +.editor { + outline: none; } -.dark-mode { - background-color: #2d2d2d; - color: #dddddd; - border: 1px solid #444; - padding: 16px; - border-radius: 6px; +.editor-toolbar { + display: flex; + gap: 8px; + margin-bottom: 8px; } -.rich-text-label { - display: block; - margin-bottom: 8px; - font-weight: bold; +.editor__content { + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; } -.toolbar { - display: flex; - flex-wrap: wrap; - gap: 6px; - background-color: #3c3c3c; - padding: 6px; - border-radius: 4px; +.ProseMirror { + min-height: 200px; + cursor: auto; } -.toolbar button { - background-color: #555; - color: #fff; - border: none; - outline: none; - padding: 6px 10px; - border-radius: 4px; - cursor: pointer; - transition: all 0.15s ease-in-out; +h1 { + font-size: 4.25rem; } -.toolbar button:hover { - background-color: #666; +pre { + padding: 0.7rem 1rem; + border-radius: 5px; + background: black; + color: white; + font-size: 0.8rem; + overflow-x: auto; + white-space: pre-wrap; } -.toolbar button.is-active { - background-color: #888; - font-weight: bold; +p code { + padding: 0.2rem 0.4rem; + border-radius: 5px; + font-size: 0.8rem; + font-weight: bold; + background: rgba(black, 0.1); + color: rgba(black, 0.8); } -.rich-text-editor { - background-color: #1e1e1e; - border: 1px solid #444; - margin-top: 8px; - min-height: 200px; - border-radius: 4px; - padding: 8px; +ul, +ol { + padding-left: 1rem; } -.ProseMirror { - color: #eee; - min-height: 180px; - outline: none; - font-family: 'Inter', sans-serif; - line-height: 1.6; - background: transparent; +li > p, +li > ol, +li > ul { + margin: 0; +} + +a { + color: inherit; +} + +blockquote { + border-left: 3px solid rgba(black, 0.1); + color: rgba(black, 0.8); + padding-left: 0.8rem; + font-style: italic; +} + +img { + max-width: 100%; + border-radius: 3px; +} + +.selected { + outline-style: solid; + outline-color: blue; +} + +table { + border-collapse: collapse; + table-layout: fixed; + width: 100%; + margin: 0; + overflow: hidden; +} + +td, th { + min-width: 1em; + border: 2px solid grey; + padding: 3px 5px; + vertical-align: top; + box-sizing: border-box; + position: relative; +} + +th { + font-weight: bold; + text-align: left; +} + +.selectedCell:after { + z-index: 2; + position: absolute; + content: ""; + left: 0; right: 0; top: 0; bottom: 0; + background: rgba(200, 200, 255, 0.4); + pointer-events: none; +} + +.column-resize-handle { + position: absolute; + right: -2px; top: 0; bottom: 0; + width: 4px; + z-index: 20; + background-color: #adf; + pointer-events: none; +} + +.tableWrapper { + margin: 1em 0; + overflow-x: auto; +} + +.resize-cursor { + cursor: ew-resize; + cursor: col-resize; +} + +.is-active { + color: green; +} + +.is-active-highlight { + background-color: grey; +} + +.diffrem { + background-color: #fdb8c0; } -.ProseMirror:focus { - outline: none; +.diffadd { + background-color: #acf2bd; } -.ProseMirror h2, -.ProseMirror h3 { - color: #fff; - margin-top: 1rem; +.text-negative .editor:not(.q-dark) { + color: var(--q-color-primary) !important; } From 11611a6bd93d6db51a73331694fcd24136844bbc Mon Sep 17 00:00:00 2001 From: caverav Date: Fri, 31 Jan 2025 16:40:29 -0300 Subject: [PATCH 08/19] =?UTF-8?q?=E2=9C=A8=20feat:=20update=20packages=20i?= =?UTF-8?q?n=20frontend/package.json=20by=20adding=20"@tiptap/pm"=20and=20?= =?UTF-8?q?"lodash"=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/package.json b/frontend/package.json index 5ce19271..c094cec7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "@tiptap/extension-link": "^2.11.4", "@tiptap/extension-placeholder": "^2.11.4", "@tiptap/extension-underline": "^2.11.4", + "@tiptap/pm": "^2.11.5", "@tiptap/react": "^2.11.4", "@tiptap/starter-kit": "^2.11.4", "@types/react-router-dom": "^5.3.3", @@ -55,6 +56,7 @@ "i18next-browser-languagedetector": "^8.0.0", "jspdf": "^2.5.2", "katex": "^0.16.21", + "lodash": "^4.17.21", "lucide-react": "^0.435.0", "react": "^18.3.1", "react-chartjs-2": "^5.2.0", From 2c743c1b683c9b5b7a4e5c95f07ae71fc80592ac Mon Sep 17 00:00:00 2001 From: caverav Date: Fri, 31 Jan 2025 16:40:55 -0300 Subject: [PATCH 09/19] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20CustomImage=20c?= =?UTF-8?q?omponent=20and=20update=20images=20URL=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduced CustomImage component for handling images. - Updated imagesUrl to use import.meta.env.VITE_API_URL dynamically instead of hardcoding it. --- frontend/src/components/text/RichText.tsx | 40 ++++------------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/frontend/src/components/text/RichText.tsx b/frontend/src/components/text/RichText.tsx index 8e712b84..cb0b260c 100644 --- a/frontend/src/components/text/RichText.tsx +++ b/frontend/src/components/text/RichText.tsx @@ -41,7 +41,9 @@ import RcTiptapEditor, { Underline, } from 'reactjs-tiptap-editor'; -const imagesUrl = '/api/images/'; +import CustomImage from './ImageHandler'; + +const imagesUrl = import.meta.env.VITE_API_URL + '/api/images'; const extensions = [ BaseKit.configure({ @@ -80,38 +82,8 @@ const extensions = [ }, }), Link, - Image.configure({ - upload: async (file: File) => { - const base64Value = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - if (reader.result) { - resolve(reader.result.toString()); - } else { - reject(new Error('File reading failed')); - } - }; - reader.onerror = error => reject(error); - }); - - const audit = { - name: file.name, - value: base64Value, - }; - - const response = await fetch(imagesUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(audit), - }); - - const data = await response.json(); - return data.datas._id; - }, - }), + Image, + CustomImage, Blockquote.configure({ spacer: true }), SlashCommand, Code.configure({ @@ -127,7 +99,7 @@ const extensions = [ upload: async (file: File) => { const formData = new FormData(); formData.append('value', await file.text()); - const response = await fetch('/api/images/', { + const response = await fetch(imagesUrl, { method: 'POST', body: formData, }); From d399932c29b76c1b45a10fdaeb773b6e14048c08 Mon Sep 17 00:00:00 2001 From: caverav Date: Fri, 31 Jan 2025 16:41:17 -0300 Subject: [PATCH 10/19] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20image=20service?= =?UTF-8?q?=20with=20functions=20for=20fetching,=20creating,=20and=20delet?= =?UTF-8?q?ing=20images.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/services/image.ts | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 frontend/src/services/image.ts diff --git a/frontend/src/services/image.ts b/frontend/src/services/image.ts new file mode 100644 index 00000000..04190c32 --- /dev/null +++ b/frontend/src/services/image.ts @@ -0,0 +1,43 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL + '/api/'; + +const getImage = async (imageId: string) => { + const response = await fetch(`${API_BASE_URL}images/${imageId}`); + if (!response.ok) { + throw new Error('Network response was not ok (getImage)'); + } + return response.json(); +}; + +const createImage = async (image: { + value: string; + name: string; + auditId: string | null; +}) => { + const response = await fetch(`${API_BASE_URL}images`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(image), + }); + if (!response.ok) { + throw new Error('Network response was not ok (createImage)'); + } + return response.json(); +}; + +const deleteImage = async (imageId: string) => { + const response = await fetch(`${API_BASE_URL}images/${imageId}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error('Network response was not ok (deleteImage)'); + } + return response.json(); +}; + +export default { + getImage, + createImage, + deleteImage, +}; From e4cb4d5b9b566bcee69b173952efcb7e710550e4 Mon Sep 17 00:00:00 2001 From: caverav Date: Fri, 31 Jan 2025 16:42:33 -0300 Subject: [PATCH 11/19] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20util=20function?= =?UTF-8?q?s.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/services/utils.ts | 236 +++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 frontend/src/services/utils.ts diff --git a/frontend/src/services/utils.ts b/frontend/src/services/utils.ts new file mode 100644 index 00000000..eae1baf8 --- /dev/null +++ b/frontend/src/services/utils.ts @@ -0,0 +1,236 @@ +import { t } from 'i18next'; +import _ from 'lodash'; + +export default { + htmlEncode: (html: string) => { + if (typeof html !== 'string') { + return ''; + } + + return html + .replace(/[^\x20-\x7E]/g, '') // Non printable characters except NewLine + .replace(//g, 'ΏΠг') + .replace( + /ΩΠгimg.+?src="(.*?)".+?alt="(.*?)".*?ΏΠг/g, + '$2', + ) + .replace( + /ΩΠгlegend.+?label="(.*?)".+?alt="(.*?)".*?ΏΠг/g, + '', + ) + .replace(/ΩΠг\/legendΏΠг/g, '') + .replace( + /ΩΠгmark.+?data-color="(.*?)".+?style="(.*?)".*?ΏΠг/g, + '', + ) + .replace(/ΩΠг\/markΏΠг/g, '') + .replace(/ΩΠгpΏΠг/g, '

') + .replace(/ΩΠг\/pΏΠг/g, '

') + .replace(/ΩΠгpreΏΠг/g, '
')
+      .replace(/ΩΠг\/preΏΠг/g, '
') + .replace(/ΩΠгbΏΠг/g, '') + .replace(/ΩΠг\/bΏΠг/g, '') + .replace(/ΩΠгstrongΏΠг/g, '') + .replace(/ΩΠг\/strongΏΠг/g, '') + .replace(/ΩΠгiΏΠг/g, '') + .replace(/ΩΠг\/iΏΠг/g, '') + .replace(/ΩΠгemΏΠг/g, '') + .replace(/ΩΠг\/emΏΠг/g, '') + .replace(/ΩΠгuΏΠг/g, '') + .replace(/ΩΠг\/uΏΠг/g, '') + .replace(/ΩΠгsΏΠг/g, '') + .replace(/ΩΠг\/sΏΠг/g, '') + .replace(/ΩΠгstrikeΏΠг/g, '') + .replace(/ΩΠг\/strikeΏΠг/g, '') + .replace(/ΩΠгbrΏΠг/g, '
') + .replace(/ΩΠгcodeΏΠг/g, '') + .replace(/ΩΠг\/codeΏΠг/g, '') + .replace(/ΩΠгulΏΠг/g, '
    ') + .replace(/ΩΠг\/ulΏΠг/g, '
') + .replace(/ΩΠгolΏΠг/g, '
    ') + .replace(/ΩΠг\/olΏΠг/g, '
') + .replace(/ΩΠгliΏΠг/g, '
  • ') + .replace(/ΩΠг\/liΏΠг/g, '
  • ') + .replace(/ΩΠгh1ΏΠг/g, '

    ') + .replace(/ΩΠг\/h1ΏΠг/g, '

    ') + .replace(/ΩΠгh2ΏΠг/g, '

    ') + .replace(/ΩΠг\/h2ΏΠг/g, '

    ') + .replace(/ΩΠгh3ΏΠг/g, '

    ') + .replace(/ΩΠг\/h3ΏΠг/g, '

    ') + .replace(/ΩΠгh4ΏΠг/g, '

    ') + .replace(/ΩΠг\/h4ΏΠг/g, '

    ') + .replace(/ΩΠгh5ΏΠг/g, '
    ') + .replace(/ΩΠг\/h5ΏΠг/g, '
    ') + .replace(/ΩΠгh6ΏΠг/g, '
    ') + .replace(/ΩΠг\/h6ΏΠг/g, '
    ') + .replace(/ΩΠг/g, '<') + .replace(/ΏΠг/g, '>'); + }, + + // Compress images to allow more storage in database since limit in a mongo document is 16MB + resizeImg: (imageB64: string): Promise => + new Promise((resolve, _) => { + const oldSize = JSON.stringify(imageB64).length; + const maxWidth = 1920; + + const img = new Image(); + img.src = imageB64; + img.onload = () => { + //scale the image and keep aspect ratio + const resizeWidth = img.width > maxWidth ? maxWidth : img.width; + const scaleFactor = resizeWidth / img.width; + const resizeHeight = img.height * scaleFactor; + + // Create a temporary canvas to draw the downscaled image on. + const canvas = document.createElement('canvas'); + canvas.width = resizeWidth; + canvas.height = resizeHeight; + + //draw in canvas + const ctx = canvas.getContext('2d'); + if (!ctx) { + resolve(imageB64); + return; + } + ctx.drawImage(img, 0, 0, resizeWidth, resizeHeight); + + const result = canvas.toDataURL('image/jpeg'); + const newSize = JSON.stringify(result).length; + if (newSize >= oldSize) { + resolve(imageB64); + } else { + resolve(result); + } + }; + }), + + customFilter: ( + rows: Record[], + terms: Record, + ) => { + return ( + rows && + rows.filter(row => { + for (const [key, value] of Object.entries(terms)) { + // for each search term + let searchString = _.get(row, key) || ''; + if (typeof searchString === 'string') { + searchString = searchString + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''); + } + let termString = value || ''; + if (typeof termString === 'string') { + termString = termString + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''); + } + if ( + typeof searchString !== 'string' || + typeof termString !== 'string' + ) { + return searchString === termString; + } + if ( + typeof searchString === 'string' && + searchString.indexOf(termString) < 0 + ) { + return false; + } + } + return true; + }) + ); + }, + + normalizeString: (value: string): string => + value + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''), + + AUDIT_VIEW_STATE: { + EDIT: 0, + EDIT_READONLY: 1, + REVIEW: 2, + REVIEW_EDITOR: 3, + REVIEW_APPROVED: 4, + REVIEW_ADMIN: 5, + REVIEW_ADMIN_APPROVED: 6, + REVIEW_READONLY: 7, + APPROVED: 8, + APPROVED_APPROVED: 9, + APPROVED_READONLY: 10, + }, + + strongPassword: (value: string): boolean | string => { + const regExp = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}/; + if (regExp.test(value)) { + return true; + } + return t('msg.passwordComplexity'); + }, + + // Return black or white color depending on background color + getTextColor: (bgColor: string): string => { + const regex = /^#[0-9a-fA-F]{6}$/; + if (!regex.test(bgColor)) { + return '#000000'; + } //black + + const color = bgColor.substring(1, 7); + const red = parseInt(color.substring(0, 2), 16); // hexToR + const green = parseInt(color.substring(2, 4), 16); // hexToG + const blue = parseInt(color.substring(4, 6), 16); // hexToB + + return red * 0.299 + green * 0.587 + blue * 0.114 > 186 + ? '#000000' + : '#ffffff'; + }, + + getRelativeDate: (date: string | Date): string => { + const now = new Date(); + const diff = now.getTime() - new Date(date).getTime(); + + const seconds = Math.floor(diff / 1000); + if (seconds < 60) { + return `${seconds} seconds ago`; + } + + const minutes = Math.floor(diff / 60000); + if (minutes < 60) { + return `${minutes} minutes ago`; + } + + const hours = Math.floor(diff / 3600000); + if (hours < 24) { + return `${hours} hours ago`; + } + + const days = Math.floor(diff / 86400000); + if (days < 30) { + return `${days} days ago`; + } + + const months = Math.floor(diff / 2592000000); + if (months < 12) { + return `${months} months ago`; + } + + const years = Math.floor(diff / 31536000000); + return `${years} years ago`; + }, + + bytesToHumanReadable: (bytes: number): string => { + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + if (bytes === 0) { + return '0 B'; + } + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + const size = bytes / Math.pow(1024, i); + return `${size.toFixed(2)} ${sizes[i]}`; + }, +}; From e727293f16922d9116f321bbc4ff66c0e5dc5e12 Mon Sep 17 00:00:00 2001 From: caverav Date: Fri, 31 Jan 2025 16:42:54 -0300 Subject: [PATCH 12/19] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20ImageHandler=20?= =?UTF-8?q?component=20with=20image=20handling=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/text/ImageHandler.tsx | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 frontend/src/components/text/ImageHandler.tsx diff --git a/frontend/src/components/text/ImageHandler.tsx b/frontend/src/components/text/ImageHandler.tsx new file mode 100644 index 00000000..dabaa04d --- /dev/null +++ b/frontend/src/components/text/ImageHandler.tsx @@ -0,0 +1,124 @@ +import { Image as TipTapImage } from '@tiptap/extension-image'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; +import { EditorContent, useEditor } from '@tiptap/react'; + +import ImageService from '@/services/image'; +import Utils from '@/services/utils'; + +const ImageHandler: React.FC = () => { + const editor = useEditor({ + extensions: [ + TipTapImage.extend({ + addProseMirrorPlugins: () => [ + new Plugin({ + key: new PluginKey('eventHandler'), + props: { + handleDrop: (view, event) => { + let isImage = false; + const file = event.dataTransfer?.files[0]; + + let auditId: string | null = null; + const path = window.location.pathname.split('/'); + if (path && path.length > 3 && path[1] === 'audits') { + auditId = path[2]; + } + + if (file && file.type.startsWith('image')) { + isImage = true; + const fileReader = new FileReader(); + + fileReader.onloadend = () => { + if ( + fileReader.result instanceof ArrayBuffer || + !fileReader.result + ) { + return; + } + Utils.resizeImg(fileReader.result) + .then(data => { + return ImageService.createImage({ + value: data, + name: file.name, + auditId, + }); + }) + .then(data => { + const node = view.state.schema.nodes.image.create({ + src: data.data.datas._id, + alt: file.name, + }); + const transaction = + view.state.tr.replaceSelectionWith(node); + view.dispatch(transaction); + }) + .catch(err => console.error(err)); + }; + + fileReader.readAsDataURL(file); + } + + if (isImage) { + event.preventDefault(); + return true; + } + }, + handlePaste: (view, event) => { + let isImage = false; + const file = event.clipboardData?.files[0]; + + let auditId: string | null = null; + const path = window.location.pathname.split('/'); + if (path && path.length > 3 && path[1] === 'audits') { + auditId = path[2]; + } + + if (file && file.type.startsWith('image')) { + isImage = true; + const fileReader = new FileReader(); + + fileReader.onloadend = () => { + if ( + fileReader.result instanceof ArrayBuffer || + !fileReader.result + ) { + return; + } + Utils.resizeImg(fileReader.result) + .then(data => { + return ImageService.createImage({ + value: data, + name: file.name, + auditId, + }); + }) + .then(data => { + const node = view.state.schema.nodes.image.create({ + src: data.data.datas._id, + alt: file.name, + }); + const transaction = + view.state.tr.replaceSelectionWith(node); + view.dispatch(transaction); + }) + .catch(err => console.error(err)); + }; + + fileReader.readAsDataURL(file); + } + + if (isImage) { + event.preventDefault(); + return true; + } + }, + }, + }), + ], + }), + ], + }); + + return ; +}; + +export default ImageHandler; From 0a463a966602c535a8254aa5f2f54486d9c1e5cc Mon Sep 17 00:00:00 2001 From: caverav Date: Fri, 31 Jan 2025 21:14:36 -0300 Subject: [PATCH 13/19] =?UTF-8?q?=E2=9C=A8=20feat:=20Configure=20Image=20e?= =?UTF-8?q?xtension=20in=20RichText=20editor=20to=20handle=20image=20uploa?= =?UTF-8?q?ds=20including=20base64=20conversion=20and=20API=20posting.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/text/RichText.tsx | 39 ++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/text/RichText.tsx b/frontend/src/components/text/RichText.tsx index cb0b260c..83ef1e27 100644 --- a/frontend/src/components/text/RichText.tsx +++ b/frontend/src/components/text/RichText.tsx @@ -41,8 +41,6 @@ import RcTiptapEditor, { Underline, } from 'reactjs-tiptap-editor'; -import CustomImage from './ImageHandler'; - const imagesUrl = import.meta.env.VITE_API_URL + '/api/images'; const extensions = [ @@ -82,8 +80,41 @@ const extensions = [ }, }), Link, - Image, - CustomImage, + Image.configure({ + upload: async (file: File) => { + const base64Value = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + if (reader.result) { + resolve(reader.result.toString()); + } else { + reject(new Error('File reading failed')); + } + }; + reader.onerror = error => reject(error); + }); + + return base64Value + ''; + + // const audit = { + // name: file.name, + // value: base64Value, + // alt: '', + // }; + // + // const response = await fetch(imagesUrl, { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify(audit), + // }); + // + // const data = await response.json(); + // return data.datas._id; + }, + }), Blockquote.configure({ spacer: true }), SlashCommand, Code.configure({ From 01d14d40035f8a6cd87a8742882e732ab4ba5798 Mon Sep 17 00:00:00 2001 From: caverav Date: Tue, 4 Feb 2025 17:44:39 -0300 Subject: [PATCH 14/19] refactor(useAuth.tsx): extract API endpoint URL to a constant for consistency and maintainability --- frontend/src/hooks/useAuth.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/hooks/useAuth.tsx b/frontend/src/hooks/useAuth.tsx index 9f4319d2..1e675e07 100644 --- a/frontend/src/hooks/useAuth.tsx +++ b/frontend/src/hooks/useAuth.tsx @@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom'; const tokenUrl = `${import.meta.env.VITE_API_URL}/api/users/token`; const checktokenUrl = `${import.meta.env.VITE_API_URL}/api/users/checktoken`; const refreshTokenUrl = `${import.meta.env.VITE_API_URL}/api/users/refreshtoken`; +const initUrl = `${import.meta.env.VITE_API_URL}/api/users/init`; export const checktoken = async (): Promise => { try { @@ -105,7 +106,7 @@ const useAuth = () => { }); try { - const response = await fetch('/api/users/init', { + const response = await fetch(initUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', From 35851aec58820d1bbb49994c4a47903ccd345791 Mon Sep 17 00:00:00 2001 From: caverav Date: Tue, 4 Feb 2025 17:48:44 -0300 Subject: [PATCH 15/19] =?UTF-8?q?=F0=9F=90=9B=20fix(report-generator.js):?= =?UTF-8?q?=20fix=20regex=20to=20handle=20optional=20alt=20attribute=20in?= =?UTF-8?q?=20img=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/lib/report-generator.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/lib/report-generator.js b/backend/src/lib/report-generator.js index 2cc08133..0fa8786e 100644 --- a/backend/src/lib/report-generator.js +++ b/backend/src/lib/report-generator.js @@ -664,9 +664,10 @@ async function splitHTMLParagraphs(data) { var result = []; if (!data) return result; - var splitted = data.split(/()/); + var splitted = data.split(/()/); for (var value of splitted) { + if (!value) continue; if (value.startsWith(' Date: Tue, 4 Feb 2025 17:49:40 -0300 Subject: [PATCH 16/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(RichText.ts?= =?UTF-8?q?x):=20remove=20commented-out=20code=20and=20simplify=20return?= =?UTF-8?q?=20statement=20in=20image=20upload=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/text/RichText.tsx | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/text/RichText.tsx b/frontend/src/components/text/RichText.tsx index 83ef1e27..9885ff4c 100644 --- a/frontend/src/components/text/RichText.tsx +++ b/frontend/src/components/text/RichText.tsx @@ -82,6 +82,7 @@ const extensions = [ Link, Image.configure({ upload: async (file: File) => { + // eslint-disable-next-line sonarjs/prefer-immediate-return -- we need to wait for the file to be read const base64Value = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); @@ -94,25 +95,7 @@ const extensions = [ }; reader.onerror = error => reject(error); }); - - return base64Value + ''; - - // const audit = { - // name: file.name, - // value: base64Value, - // alt: '', - // }; - // - // const response = await fetch(imagesUrl, { - // method: 'POST', - // headers: { - // 'Content-Type': 'application/json', - // }, - // body: JSON.stringify(audit), - // }); - // - // const data = await response.json(); - // return data.datas._id; + return base64Value; }, }), Blockquote.configure({ spacer: true }), From e39f7e91ee6aec33ef251ff51e6b2070a29ba4a2 Mon Sep 17 00:00:00 2001 From: caverav Date: Tue, 4 Feb 2025 21:19:13 -0300 Subject: [PATCH 17/19] =?UTF-8?q?=F0=9F=94=A5=20remove(frontend):=20remove?= =?UTF-8?q?=20unused=20ImageHandler=20component=20and=20related=20services?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/text/ImageHandler.tsx | 124 --- frontend/src/components/text/RichText.tsx | 2 - .../src/components/text/css/quill-styles.css | 4 - .../src/components/text/css/quill.snow.css | 969 ------------------ frontend/src/services/image.ts | 43 - frontend/src/services/utils.ts | 236 ----- 6 files changed, 1378 deletions(-) delete mode 100644 frontend/src/components/text/ImageHandler.tsx delete mode 100644 frontend/src/components/text/css/quill-styles.css delete mode 100644 frontend/src/components/text/css/quill.snow.css delete mode 100644 frontend/src/services/image.ts delete mode 100644 frontend/src/services/utils.ts diff --git a/frontend/src/components/text/ImageHandler.tsx b/frontend/src/components/text/ImageHandler.tsx deleted file mode 100644 index dabaa04d..00000000 --- a/frontend/src/components/text/ImageHandler.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { Image as TipTapImage } from '@tiptap/extension-image'; -import { Plugin, PluginKey } from '@tiptap/pm/state'; -import { EditorContent, useEditor } from '@tiptap/react'; - -import ImageService from '@/services/image'; -import Utils from '@/services/utils'; - -const ImageHandler: React.FC = () => { - const editor = useEditor({ - extensions: [ - TipTapImage.extend({ - addProseMirrorPlugins: () => [ - new Plugin({ - key: new PluginKey('eventHandler'), - props: { - handleDrop: (view, event) => { - let isImage = false; - const file = event.dataTransfer?.files[0]; - - let auditId: string | null = null; - const path = window.location.pathname.split('/'); - if (path && path.length > 3 && path[1] === 'audits') { - auditId = path[2]; - } - - if (file && file.type.startsWith('image')) { - isImage = true; - const fileReader = new FileReader(); - - fileReader.onloadend = () => { - if ( - fileReader.result instanceof ArrayBuffer || - !fileReader.result - ) { - return; - } - Utils.resizeImg(fileReader.result) - .then(data => { - return ImageService.createImage({ - value: data, - name: file.name, - auditId, - }); - }) - .then(data => { - const node = view.state.schema.nodes.image.create({ - src: data.data.datas._id, - alt: file.name, - }); - const transaction = - view.state.tr.replaceSelectionWith(node); - view.dispatch(transaction); - }) - .catch(err => console.error(err)); - }; - - fileReader.readAsDataURL(file); - } - - if (isImage) { - event.preventDefault(); - return true; - } - }, - handlePaste: (view, event) => { - let isImage = false; - const file = event.clipboardData?.files[0]; - - let auditId: string | null = null; - const path = window.location.pathname.split('/'); - if (path && path.length > 3 && path[1] === 'audits') { - auditId = path[2]; - } - - if (file && file.type.startsWith('image')) { - isImage = true; - const fileReader = new FileReader(); - - fileReader.onloadend = () => { - if ( - fileReader.result instanceof ArrayBuffer || - !fileReader.result - ) { - return; - } - Utils.resizeImg(fileReader.result) - .then(data => { - return ImageService.createImage({ - value: data, - name: file.name, - auditId, - }); - }) - .then(data => { - const node = view.state.schema.nodes.image.create({ - src: data.data.datas._id, - alt: file.name, - }); - const transaction = - view.state.tr.replaceSelectionWith(node); - view.dispatch(transaction); - }) - .catch(err => console.error(err)); - }; - - fileReader.readAsDataURL(file); - } - - if (isImage) { - event.preventDefault(); - return true; - } - }, - }, - }), - ], - }), - ], - }); - - return ; -}; - -export default ImageHandler; diff --git a/frontend/src/components/text/RichText.tsx b/frontend/src/components/text/RichText.tsx index 9885ff4c..d5802eda 100644 --- a/frontend/src/components/text/RichText.tsx +++ b/frontend/src/components/text/RichText.tsx @@ -1,8 +1,6 @@ import 'katex/dist/katex.min.css'; import 'reactjs-tiptap-editor/style.css'; -// import { Label } from '@headlessui/react'; -// import Placeholder from '@tiptap/extension-placeholder'; import { useRef, useState } from 'react'; import RcTiptapEditor, { Attachment, diff --git a/frontend/src/components/text/css/quill-styles.css b/frontend/src/components/text/css/quill-styles.css deleted file mode 100644 index 32211722..00000000 --- a/frontend/src/components/text/css/quill-styles.css +++ /dev/null @@ -1,4 +0,0 @@ -.ql-editor { - min-height: 10em; - max-height: 15em; -} diff --git a/frontend/src/components/text/css/quill.snow.css b/frontend/src/components/text/css/quill.snow.css deleted file mode 100644 index ee39fe27..00000000 --- a/frontend/src/components/text/css/quill.snow.css +++ /dev/null @@ -1,969 +0,0 @@ -/*! - * Quill Editor v1.3.7 - * https://quilljs.com/ - * Copyright (c) 2014, Jason Chen - * Copyright (c) 2013, salesforce.com - */ -.ql-container { - box-sizing: border-box; - font-family: Helvetica, Arial, sans-serif; - font-size: 13px; - height: 100%; - margin: 0px; - position: relative; -} -.ql-container.ql-disabled .ql-tooltip { - visibility: hidden; -} -.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before { - pointer-events: none; -} -.ql-clipboard { - left: -100000px; - height: 1px; - overflow-y: hidden; - position: absolute; - top: 50%; -} -.ql-clipboard p { - margin: 0; - padding: 0; -} -.ql-editor { - box-sizing: border-box; - line-height: 1.42; - height: 100%; - outline: none; - overflow-y: auto; - padding: 12px 15px; - tab-size: 4; - -moz-tab-size: 4; - text-align: left; - white-space: pre-wrap; - word-wrap: break-word; -} -.ql-editor > * { - cursor: text; -} -.ql-editor p, -.ql-editor ol, -.ql-editor ul, -.ql-editor pre, -.ql-editor blockquote, -.ql-editor h1, -.ql-editor h2, -.ql-editor h3, -.ql-editor h4, -.ql-editor h5, -.ql-editor h6 { - margin: 0; - padding: 0; - counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; -} -.ql-editor ol, -.ql-editor ul { - padding-left: 1.5em; -} -.ql-editor ol > li, -.ql-editor ul > li { - list-style-type: none; -} -.ql-editor ul > li::before { - content: '\2022'; -} -.ql-editor ul[data-checked='true'], -.ql-editor ul[data-checked='false'] { - pointer-events: none; -} -.ql-editor ul[data-checked='true'] > li *, -.ql-editor ul[data-checked='false'] > li * { - pointer-events: all; -} -.ql-editor ul[data-checked='true'] > li::before, -.ql-editor ul[data-checked='false'] > li::before { - color: #777; - cursor: pointer; - pointer-events: all; -} -.ql-editor ul[data-checked='true'] > li::before { - content: '\2611'; -} -.ql-editor ul[data-checked='false'] > li::before { - content: '\2610'; -} -.ql-editor li::before { - display: inline-block; - white-space: nowrap; - width: 1.2em; -} -.ql-editor li:not(.ql-direction-rtl)::before { - margin-left: -1.5em; - margin-right: 0.3em; - text-align: right; -} -.ql-editor li.ql-direction-rtl::before { - margin-left: 0.3em; - margin-right: -1.5em; -} -.ql-editor ol li:not(.ql-direction-rtl), -.ql-editor ul li:not(.ql-direction-rtl) { - padding-left: 1.5em; -} -.ql-editor ol li.ql-direction-rtl, -.ql-editor ul li.ql-direction-rtl { - padding-right: 1.5em; -} -.ql-editor ol li { - counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; - counter-increment: list-0; -} -.ql-editor ol li:before { - content: counter(list-0, decimal) '. '; -} -.ql-editor ol li.ql-indent-1 { - counter-increment: list-1; -} -.ql-editor ol li.ql-indent-1:before { - content: counter(list-1, lower-alpha) '. '; -} -.ql-editor ol li.ql-indent-1 { - counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; -} -.ql-editor ol li.ql-indent-2 { - counter-increment: list-2; -} -.ql-editor ol li.ql-indent-2:before { - content: counter(list-2, lower-roman) '. '; -} -.ql-editor ol li.ql-indent-2 { - counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9; -} -.ql-editor ol li.ql-indent-3 { - counter-increment: list-3; -} -.ql-editor ol li.ql-indent-3:before { - content: counter(list-3, decimal) '. '; -} -.ql-editor ol li.ql-indent-3 { - counter-reset: list-4 list-5 list-6 list-7 list-8 list-9; -} -.ql-editor ol li.ql-indent-4 { - counter-increment: list-4; -} -.ql-editor ol li.ql-indent-4:before { - content: counter(list-4, lower-alpha) '. '; -} -.ql-editor ol li.ql-indent-4 { - counter-reset: list-5 list-6 list-7 list-8 list-9; -} -.ql-editor ol li.ql-indent-5 { - counter-increment: list-5; -} -.ql-editor ol li.ql-indent-5:before { - content: counter(list-5, lower-roman) '. '; -} -.ql-editor ol li.ql-indent-5 { - counter-reset: list-6 list-7 list-8 list-9; -} -.ql-editor ol li.ql-indent-6 { - counter-increment: list-6; -} -.ql-editor ol li.ql-indent-6:before { - content: counter(list-6, decimal) '. '; -} -.ql-editor ol li.ql-indent-6 { - counter-reset: list-7 list-8 list-9; -} -.ql-editor ol li.ql-indent-7 { - counter-increment: list-7; -} -.ql-editor ol li.ql-indent-7:before { - content: counter(list-7, lower-alpha) '. '; -} -.ql-editor ol li.ql-indent-7 { - counter-reset: list-8 list-9; -} -.ql-editor ol li.ql-indent-8 { - counter-increment: list-8; -} -.ql-editor ol li.ql-indent-8:before { - content: counter(list-8, lower-roman) '. '; -} -.ql-editor ol li.ql-indent-8 { - counter-reset: list-9; -} -.ql-editor ol li.ql-indent-9 { - counter-increment: list-9; -} -.ql-editor ol li.ql-indent-9:before { - content: counter(list-9, decimal) '. '; -} -.ql-editor .ql-indent-1:not(.ql-direction-rtl) { - padding-left: 3em; -} -.ql-editor li.ql-indent-1:not(.ql-direction-rtl) { - padding-left: 4.5em; -} -.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right { - padding-right: 3em; -} -.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right { - padding-right: 4.5em; -} -.ql-editor .ql-indent-2:not(.ql-direction-rtl) { - padding-left: 6em; -} -.ql-editor li.ql-indent-2:not(.ql-direction-rtl) { - padding-left: 7.5em; -} -.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right { - padding-right: 6em; -} -.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right { - padding-right: 7.5em; -} -.ql-editor .ql-indent-3:not(.ql-direction-rtl) { - padding-left: 9em; -} -.ql-editor li.ql-indent-3:not(.ql-direction-rtl) { - padding-left: 10.5em; -} -.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right { - padding-right: 9em; -} -.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right { - padding-right: 10.5em; -} -.ql-editor .ql-indent-4:not(.ql-direction-rtl) { - padding-left: 12em; -} -.ql-editor li.ql-indent-4:not(.ql-direction-rtl) { - padding-left: 13.5em; -} -.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right { - padding-right: 12em; -} -.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right { - padding-right: 13.5em; -} -.ql-editor .ql-indent-5:not(.ql-direction-rtl) { - padding-left: 15em; -} -.ql-editor li.ql-indent-5:not(.ql-direction-rtl) { - padding-left: 16.5em; -} -.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right { - padding-right: 15em; -} -.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right { - padding-right: 16.5em; -} -.ql-editor .ql-indent-6:not(.ql-direction-rtl) { - padding-left: 18em; -} -.ql-editor li.ql-indent-6:not(.ql-direction-rtl) { - padding-left: 19.5em; -} -.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right { - padding-right: 18em; -} -.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right { - padding-right: 19.5em; -} -.ql-editor .ql-indent-7:not(.ql-direction-rtl) { - padding-left: 21em; -} -.ql-editor li.ql-indent-7:not(.ql-direction-rtl) { - padding-left: 22.5em; -} -.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right { - padding-right: 21em; -} -.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right { - padding-right: 22.5em; -} -.ql-editor .ql-indent-8:not(.ql-direction-rtl) { - padding-left: 24em; -} -.ql-editor li.ql-indent-8:not(.ql-direction-rtl) { - padding-left: 25.5em; -} -.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right { - padding-right: 24em; -} -.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right { - padding-right: 25.5em; -} -.ql-editor .ql-indent-9:not(.ql-direction-rtl) { - padding-left: 27em; -} -.ql-editor li.ql-indent-9:not(.ql-direction-rtl) { - padding-left: 28.5em; -} -.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right { - padding-right: 27em; -} -.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right { - padding-right: 28.5em; -} -.ql-editor .ql-video { - display: block; - max-width: 100%; -} -.ql-editor .ql-video.ql-align-center { - margin: 0 auto; -} -.ql-editor .ql-video.ql-align-right { - margin: 0 0 0 auto; -} -.ql-editor .ql-bg-black { - background-color: #000; -} -.ql-editor .ql-bg-red { - background-color: #e60000; -} -.ql-editor .ql-bg-orange { - background-color: #f90; -} -.ql-editor .ql-bg-yellow { - background-color: #ff0; -} -.ql-editor .ql-bg-green { - background-color: #008a00; -} -.ql-editor .ql-bg-blue { - background-color: #06c; -} -.ql-editor .ql-bg-purple { - background-color: #93f; -} -.ql-editor .ql-color-white { - color: #fff; -} -.ql-editor .ql-color-red { - color: #e60000; -} -.ql-editor .ql-color-orange { - color: #f90; -} -.ql-editor .ql-color-yellow { - color: #ff0; -} -.ql-editor .ql-color-green { - color: #008a00; -} -.ql-editor .ql-color-blue { - color: #06c; -} -.ql-editor .ql-color-purple { - color: #93f; -} -.ql-editor .ql-font-serif { - font-family: - Georgia, - Times New Roman, - serif; -} -.ql-editor .ql-font-monospace { - font-family: - Monaco, - Courier New, - monospace; -} -.ql-editor .ql-size-small { - font-size: 0.75em; -} -.ql-editor .ql-size-large { - font-size: 1.5em; -} -.ql-editor .ql-size-huge { - font-size: 2.5em; -} -.ql-editor .ql-direction-rtl { - direction: rtl; - text-align: inherit; -} -.ql-editor .ql-align-center { - text-align: center; -} -.ql-editor .ql-align-justify { - text-align: justify; -} -.ql-editor .ql-align-right { - text-align: right; -} -.ql-editor.ql-blank::before { - color: rgba(0, 0, 0, 0.6); - content: attr(data-placeholder); - font-style: italic; - left: 15px; - pointer-events: none; - position: absolute; - right: 15px; -} -.ql-snow.ql-toolbar:after, -.ql-snow .ql-toolbar:after { - clear: both; - content: ''; - display: table; -} -.ql-snow.ql-toolbar button, -.ql-snow .ql-toolbar button { - background: none; - border: none; - cursor: pointer; - display: inline-block; - float: left; - height: 24px; - padding: 3px 5px; - width: 28px; -} -.ql-snow.ql-toolbar button svg, -.ql-snow .ql-toolbar button svg { - float: left; - height: 100%; -} -.ql-snow.ql-toolbar button:active:hover, -.ql-snow .ql-toolbar button:active:hover { - outline: none; -} -.ql-snow.ql-toolbar input.ql-image[type='file'], -.ql-snow .ql-toolbar input.ql-image[type='file'] { - display: none; -} -.ql-snow.ql-toolbar button:hover, -.ql-snow .ql-toolbar button:hover, -.ql-snow.ql-toolbar button:focus, -.ql-snow .ql-toolbar button:focus, -.ql-snow.ql-toolbar button.ql-active, -.ql-snow .ql-toolbar button.ql-active, -.ql-snow.ql-toolbar .ql-picker-label:hover, -.ql-snow .ql-toolbar .ql-picker-label:hover, -.ql-snow.ql-toolbar .ql-picker-label.ql-active, -.ql-snow .ql-toolbar .ql-picker-label.ql-active, -.ql-snow.ql-toolbar .ql-picker-item:hover, -.ql-snow .ql-toolbar .ql-picker-item:hover, -.ql-snow.ql-toolbar .ql-picker-item.ql-selected, -.ql-snow .ql-toolbar .ql-picker-item.ql-selected { - color: #06c; -} -.ql-snow.ql-toolbar button:hover .ql-fill, -.ql-snow .ql-toolbar button:hover .ql-fill, -.ql-snow.ql-toolbar button:focus .ql-fill, -.ql-snow .ql-toolbar button:focus .ql-fill, -.ql-snow.ql-toolbar button.ql-active .ql-fill, -.ql-snow .ql-toolbar button.ql-active .ql-fill, -.ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill, -.ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill, -.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill, -.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill, -.ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill, -.ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill, -.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill, -.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill, -.ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill, -.ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill, -.ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill, -.ql-snow .ql-toolbar button:focus .ql-stroke.ql-fill, -.ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill, -.ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill, -.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, -.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, -.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, -.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, -.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, -.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, -.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill, -.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill { - fill: #06c; -} -.ql-snow.ql-toolbar button:hover .ql-stroke, -.ql-snow .ql-toolbar button:hover .ql-stroke, -.ql-snow.ql-toolbar button:focus .ql-stroke, -.ql-snow .ql-toolbar button:focus .ql-stroke, -.ql-snow.ql-toolbar button.ql-active .ql-stroke, -.ql-snow .ql-toolbar button.ql-active .ql-stroke, -.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke, -.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke, -.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke, -.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke, -.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke, -.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke, -.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke, -.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke, -.ql-snow.ql-toolbar button:hover .ql-stroke-miter, -.ql-snow .ql-toolbar button:hover .ql-stroke-miter, -.ql-snow.ql-toolbar button:focus .ql-stroke-miter, -.ql-snow .ql-toolbar button:focus .ql-stroke-miter, -.ql-snow.ql-toolbar button.ql-active .ql-stroke-miter, -.ql-snow .ql-toolbar button.ql-active .ql-stroke-miter, -.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter, -.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter, -.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, -.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, -.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter, -.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter, -.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter, -.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter { - stroke: #06c; -} -@media (pointer: coarse) { - .ql-snow.ql-toolbar button:hover:not(.ql-active), - .ql-snow .ql-toolbar button:hover:not(.ql-active) { - color: #444; - } - .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-fill, - .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-fill, - .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill, - .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill { - fill: #444; - } - .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke, - .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke, - .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter, - .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter { - stroke: #444; - } -} -.ql-snow { - box-sizing: border-box; -} -.ql-snow * { - box-sizing: border-box; -} -.ql-snow .ql-hidden { - display: none; -} -.ql-snow .ql-out-bottom, -.ql-snow .ql-out-top { - visibility: hidden; -} -.ql-snow .ql-tooltip { - position: absolute; - transform: translateY(10px); -} -.ql-snow .ql-tooltip a { - cursor: pointer; - text-decoration: none; -} -.ql-snow .ql-tooltip.ql-flip { - transform: translateY(-10px); -} -.ql-snow .ql-formats { - display: inline-block; - vertical-align: middle; -} -.ql-snow .ql-formats:after { - clear: both; - content: ''; - display: table; -} -.ql-snow .ql-stroke { - fill: none; - stroke: #444; - stroke-linecap: round; - stroke-linejoin: round; - stroke-width: 2; -} -.ql-snow .ql-stroke-miter { - fill: none; - stroke: #444; - stroke-miterlimit: 10; - stroke-width: 2; -} -.ql-snow .ql-fill, -.ql-snow .ql-stroke.ql-fill { - fill: #444; -} -.ql-snow .ql-empty { - fill: none; -} -.ql-snow .ql-even { - fill-rule: evenodd; -} -.ql-snow .ql-thin, -.ql-snow .ql-stroke.ql-thin { - stroke-width: 1; -} -.ql-snow .ql-transparent { - opacity: 0.4; -} -.ql-snow .ql-direction svg:last-child { - display: none; -} -.ql-snow .ql-direction.ql-active svg:last-child { - display: inline; -} -.ql-snow .ql-direction.ql-active svg:first-child { - display: none; -} -.ql-snow .ql-editor h1 { - font-size: 2em; -} -.ql-snow .ql-editor h2 { - font-size: 1.5em; -} -.ql-snow .ql-editor h3 { - font-size: 1.17em; -} -.ql-snow .ql-editor h4 { - font-size: 1em; -} -.ql-snow .ql-editor h5 { - font-size: 0.83em; -} -.ql-snow .ql-editor h6 { - font-size: 0.67em; -} -.ql-snow .ql-editor a { - text-decoration: underline; -} -.ql-snow .ql-editor blockquote { - border-left: 4px solid #ccc; - margin-bottom: 5px; - margin-top: 5px; - padding-left: 16px; -} -.ql-snow .ql-editor code, -.ql-snow .ql-editor pre { - background-color: #f0f0f0; - border-radius: 3px; -} -.ql-snow .ql-editor pre { - white-space: pre-wrap; - margin-bottom: 5px; - margin-top: 5px; - padding: 5px 10px; -} -.ql-snow .ql-editor code { - font-size: 85%; - padding: 2px 4px; -} -.ql-snow .ql-editor pre.ql-syntax { - background-color: #23241f; - color: #f8f8f2; - overflow: visible; -} -.ql-snow .ql-editor img { - max-width: 100%; -} -.ql-snow .ql-picker { - color: #444; - display: inline-block; - float: left; - font-size: 14px; - font-weight: 500; - height: 24px; - position: relative; - vertical-align: middle; -} -.ql-snow .ql-picker-label { - cursor: pointer; - display: inline-block; - height: 100%; - padding-left: 8px; - padding-right: 2px; - position: relative; - width: 100%; -} -.ql-snow .ql-picker-label::before { - display: inline-block; - line-height: 22px; -} -.ql-snow .ql-picker-options { - background-color: #fff; - display: none; - min-width: 100%; - padding: 4px 8px; - position: absolute; - white-space: nowrap; -} -.ql-snow .ql-picker-options .ql-picker-item { - cursor: pointer; - display: block; - padding-bottom: 5px; - padding-top: 5px; -} -.ql-snow .ql-picker.ql-expanded .ql-picker-label { - color: #ccc; - z-index: 2; -} -.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill { - fill: #ccc; -} -.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke { - stroke: #ccc; -} -.ql-snow .ql-picker.ql-expanded .ql-picker-options { - display: block; - margin-top: -1px; - top: 100%; - z-index: 1; -} -.ql-snow .ql-color-picker, -.ql-snow .ql-icon-picker { - width: 28px; -} -.ql-snow .ql-color-picker .ql-picker-label, -.ql-snow .ql-icon-picker .ql-picker-label { - padding: 2px 4px; -} -.ql-snow .ql-color-picker .ql-picker-label svg, -.ql-snow .ql-icon-picker .ql-picker-label svg { - right: 4px; -} -.ql-snow .ql-icon-picker .ql-picker-options { - padding: 4px 0px; -} -.ql-snow .ql-icon-picker .ql-picker-item { - height: 24px; - width: 24px; - padding: 2px 4px; -} -.ql-snow .ql-color-picker .ql-picker-options { - padding: 3px 5px; - width: 152px; -} -.ql-snow .ql-color-picker .ql-picker-item { - border: 1px solid transparent; - float: left; - height: 16px; - margin: 2px; - padding: 0px; - width: 16px; -} -.ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg { - position: absolute; - margin-top: -9px; - right: 0; - top: 50%; - width: 18px; -} -.ql-snow - .ql-picker.ql-header - .ql-picker-label[data-label]:not([data-label=''])::before, -.ql-snow - .ql-picker.ql-font - .ql-picker-label[data-label]:not([data-label=''])::before, -.ql-snow - .ql-picker.ql-size - .ql-picker-label[data-label]:not([data-label=''])::before, -.ql-snow - .ql-picker.ql-header - .ql-picker-item[data-label]:not([data-label=''])::before, -.ql-snow - .ql-picker.ql-font - .ql-picker-item[data-label]:not([data-label=''])::before, -.ql-snow - .ql-picker.ql-size - .ql-picker-item[data-label]:not([data-label=''])::before { - content: attr(data-label); -} -.ql-snow .ql-picker.ql-header { - width: 98px; -} -.ql-snow .ql-picker.ql-header .ql-picker-label::before, -.ql-snow .ql-picker.ql-header .ql-picker-item::before { - content: 'Normal'; -} -.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before, -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before { - content: 'Heading 1'; -} -.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before, -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before { - content: 'Heading 2'; -} -.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before, -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before { - content: 'Heading 3'; -} -.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before, -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before { - content: 'Heading 4'; -} -.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before, -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before { - content: 'Heading 5'; -} -.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before, -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before { - content: 'Heading 6'; -} -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before { - font-size: 2em; -} -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before { - font-size: 1.5em; -} -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before { - font-size: 1.17em; -} -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before { - font-size: 1em; -} -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before { - font-size: 0.83em; -} -.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before { - font-size: 0.67em; -} -.ql-snow .ql-picker.ql-font { - width: 108px; -} -.ql-snow .ql-picker.ql-font .ql-picker-label::before, -.ql-snow .ql-picker.ql-font .ql-picker-item::before { - content: 'Sans Serif'; -} -.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before { - content: 'Serif'; -} -.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before { - content: 'Monospace'; -} -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before { - font-family: - Georgia, - Times New Roman, - serif; -} -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before { - font-family: - Monaco, - Courier New, - monospace; -} -.ql-snow .ql-picker.ql-size { - width: 98px; -} -.ql-snow .ql-picker.ql-size .ql-picker-label::before, -.ql-snow .ql-picker.ql-size .ql-picker-item::before { - content: 'Normal'; -} -.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before { - content: 'Small'; -} -.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before { - content: 'Large'; -} -.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before, -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before { - content: 'Huge'; -} -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before { - font-size: 10px; -} -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before { - font-size: 18px; -} -.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before { - font-size: 32px; -} -.ql-snow .ql-color-picker.ql-background .ql-picker-item { - background-color: #fff; -} -.ql-snow .ql-color-picker.ql-color .ql-picker-item { - background-color: #000; -} -.ql-toolbar.ql-snow { - border: 1px solid #ccc; - box-sizing: border-box; - font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; - padding: 8px; -} -.ql-toolbar.ql-snow .ql-formats { - margin-right: 15px; -} -.ql-toolbar.ql-snow .ql-picker-label { - border: 1px solid transparent; -} -.ql-toolbar.ql-snow .ql-picker-options { - border: 1px solid transparent; - box-shadow: rgba(0, 0, 0, 0.2) 0 2px 8px; -} -.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label { - border-color: #ccc; -} -.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options { - border-color: #ccc; -} -.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item.ql-selected, -.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item:hover { - border-color: #000; -} -.ql-toolbar.ql-snow + .ql-container.ql-snow { - border-top: 0px; -} -.ql-snow .ql-tooltip { - background-color: #fff; - border: 1px solid #ccc; - box-shadow: 0px 0px 5px #ddd; - color: #444; - padding: 5px 12px; - white-space: nowrap; -} -.ql-snow .ql-tooltip::before { - content: 'Visit URL:'; - line-height: 26px; - margin-right: 8px; -} -.ql-snow .ql-tooltip input[type='text'] { - display: none; - border: 1px solid #ccc; - font-size: 13px; - height: 26px; - margin: 0px; - padding: 3px 5px; - width: 170px; -} -.ql-snow .ql-tooltip a.ql-preview { - display: inline-block; - max-width: 200px; - overflow-x: hidden; - text-overflow: ellipsis; - vertical-align: top; -} -.ql-snow .ql-tooltip a.ql-action::after { - border-right: 1px solid #ccc; - content: 'Edit'; - margin-left: 16px; - padding-right: 8px; -} -.ql-snow .ql-tooltip a.ql-remove::before { - content: 'Remove'; - margin-left: 8px; -} -.ql-snow .ql-tooltip a { - line-height: 26px; -} -.ql-snow .ql-tooltip.ql-editing a.ql-preview, -.ql-snow .ql-tooltip.ql-editing a.ql-remove { - display: none; -} -.ql-snow .ql-tooltip.ql-editing input[type='text'] { - display: inline-block; -} -.ql-snow .ql-tooltip.ql-editing a.ql-action::after { - border-right: 0px; - content: 'Save'; - padding-right: 0px; -} -.ql-snow .ql-tooltip[data-mode='link']::before { - content: 'Enter link:'; -} -.ql-snow .ql-tooltip[data-mode='formula']::before { - content: 'Enter formula:'; -} -.ql-snow .ql-tooltip[data-mode='video']::before { - content: 'Enter video:'; -} -.ql-snow a { - color: #06c; -} -.ql-container.ql-snow { - border: 1px solid #ccc; -} diff --git a/frontend/src/services/image.ts b/frontend/src/services/image.ts deleted file mode 100644 index 04190c32..00000000 --- a/frontend/src/services/image.ts +++ /dev/null @@ -1,43 +0,0 @@ -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL + '/api/'; - -const getImage = async (imageId: string) => { - const response = await fetch(`${API_BASE_URL}images/${imageId}`); - if (!response.ok) { - throw new Error('Network response was not ok (getImage)'); - } - return response.json(); -}; - -const createImage = async (image: { - value: string; - name: string; - auditId: string | null; -}) => { - const response = await fetch(`${API_BASE_URL}images`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(image), - }); - if (!response.ok) { - throw new Error('Network response was not ok (createImage)'); - } - return response.json(); -}; - -const deleteImage = async (imageId: string) => { - const response = await fetch(`${API_BASE_URL}images/${imageId}`, { - method: 'DELETE', - }); - if (!response.ok) { - throw new Error('Network response was not ok (deleteImage)'); - } - return response.json(); -}; - -export default { - getImage, - createImage, - deleteImage, -}; diff --git a/frontend/src/services/utils.ts b/frontend/src/services/utils.ts deleted file mode 100644 index eae1baf8..00000000 --- a/frontend/src/services/utils.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { t } from 'i18next'; -import _ from 'lodash'; - -export default { - htmlEncode: (html: string) => { - if (typeof html !== 'string') { - return ''; - } - - return html - .replace(/[^\x20-\x7E]/g, '') // Non printable characters except NewLine - .replace(//g, 'ΏΠг') - .replace( - /ΩΠгimg.+?src="(.*?)".+?alt="(.*?)".*?ΏΠг/g, - '$2', - ) - .replace( - /ΩΠгlegend.+?label="(.*?)".+?alt="(.*?)".*?ΏΠг/g, - '', - ) - .replace(/ΩΠг\/legendΏΠг/g, '') - .replace( - /ΩΠгmark.+?data-color="(.*?)".+?style="(.*?)".*?ΏΠг/g, - '', - ) - .replace(/ΩΠг\/markΏΠг/g, '') - .replace(/ΩΠгpΏΠг/g, '

    ') - .replace(/ΩΠг\/pΏΠг/g, '

    ') - .replace(/ΩΠгpreΏΠг/g, '
    ')
    -      .replace(/ΩΠг\/preΏΠг/g, '
    ') - .replace(/ΩΠгbΏΠг/g, '') - .replace(/ΩΠг\/bΏΠг/g, '') - .replace(/ΩΠгstrongΏΠг/g, '') - .replace(/ΩΠг\/strongΏΠг/g, '') - .replace(/ΩΠгiΏΠг/g, '') - .replace(/ΩΠг\/iΏΠг/g, '') - .replace(/ΩΠгemΏΠг/g, '') - .replace(/ΩΠг\/emΏΠг/g, '') - .replace(/ΩΠгuΏΠг/g, '') - .replace(/ΩΠг\/uΏΠг/g, '') - .replace(/ΩΠгsΏΠг/g, '') - .replace(/ΩΠг\/sΏΠг/g, '') - .replace(/ΩΠгstrikeΏΠг/g, '') - .replace(/ΩΠг\/strikeΏΠг/g, '') - .replace(/ΩΠгbrΏΠг/g, '
    ') - .replace(/ΩΠгcodeΏΠг/g, '') - .replace(/ΩΠг\/codeΏΠг/g, '') - .replace(/ΩΠгulΏΠг/g, '
      ') - .replace(/ΩΠг\/ulΏΠг/g, '
    ') - .replace(/ΩΠгolΏΠг/g, '
      ') - .replace(/ΩΠг\/olΏΠг/g, '
    ') - .replace(/ΩΠгliΏΠг/g, '
  • ') - .replace(/ΩΠг\/liΏΠг/g, '
  • ') - .replace(/ΩΠгh1ΏΠг/g, '

    ') - .replace(/ΩΠг\/h1ΏΠг/g, '

    ') - .replace(/ΩΠгh2ΏΠг/g, '

    ') - .replace(/ΩΠг\/h2ΏΠг/g, '

    ') - .replace(/ΩΠгh3ΏΠг/g, '

    ') - .replace(/ΩΠг\/h3ΏΠг/g, '

    ') - .replace(/ΩΠгh4ΏΠг/g, '

    ') - .replace(/ΩΠг\/h4ΏΠг/g, '

    ') - .replace(/ΩΠгh5ΏΠг/g, '
    ') - .replace(/ΩΠг\/h5ΏΠг/g, '
    ') - .replace(/ΩΠгh6ΏΠг/g, '
    ') - .replace(/ΩΠг\/h6ΏΠг/g, '
    ') - .replace(/ΩΠг/g, '<') - .replace(/ΏΠг/g, '>'); - }, - - // Compress images to allow more storage in database since limit in a mongo document is 16MB - resizeImg: (imageB64: string): Promise => - new Promise((resolve, _) => { - const oldSize = JSON.stringify(imageB64).length; - const maxWidth = 1920; - - const img = new Image(); - img.src = imageB64; - img.onload = () => { - //scale the image and keep aspect ratio - const resizeWidth = img.width > maxWidth ? maxWidth : img.width; - const scaleFactor = resizeWidth / img.width; - const resizeHeight = img.height * scaleFactor; - - // Create a temporary canvas to draw the downscaled image on. - const canvas = document.createElement('canvas'); - canvas.width = resizeWidth; - canvas.height = resizeHeight; - - //draw in canvas - const ctx = canvas.getContext('2d'); - if (!ctx) { - resolve(imageB64); - return; - } - ctx.drawImage(img, 0, 0, resizeWidth, resizeHeight); - - const result = canvas.toDataURL('image/jpeg'); - const newSize = JSON.stringify(result).length; - if (newSize >= oldSize) { - resolve(imageB64); - } else { - resolve(result); - } - }; - }), - - customFilter: ( - rows: Record[], - terms: Record, - ) => { - return ( - rows && - rows.filter(row => { - for (const [key, value] of Object.entries(terms)) { - // for each search term - let searchString = _.get(row, key) || ''; - if (typeof searchString === 'string') { - searchString = searchString - .toLowerCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, ''); - } - let termString = value || ''; - if (typeof termString === 'string') { - termString = termString - .toLowerCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, ''); - } - if ( - typeof searchString !== 'string' || - typeof termString !== 'string' - ) { - return searchString === termString; - } - if ( - typeof searchString === 'string' && - searchString.indexOf(termString) < 0 - ) { - return false; - } - } - return true; - }) - ); - }, - - normalizeString: (value: string): string => - value - .toLowerCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, ''), - - AUDIT_VIEW_STATE: { - EDIT: 0, - EDIT_READONLY: 1, - REVIEW: 2, - REVIEW_EDITOR: 3, - REVIEW_APPROVED: 4, - REVIEW_ADMIN: 5, - REVIEW_ADMIN_APPROVED: 6, - REVIEW_READONLY: 7, - APPROVED: 8, - APPROVED_APPROVED: 9, - APPROVED_READONLY: 10, - }, - - strongPassword: (value: string): boolean | string => { - const regExp = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}/; - if (regExp.test(value)) { - return true; - } - return t('msg.passwordComplexity'); - }, - - // Return black or white color depending on background color - getTextColor: (bgColor: string): string => { - const regex = /^#[0-9a-fA-F]{6}$/; - if (!regex.test(bgColor)) { - return '#000000'; - } //black - - const color = bgColor.substring(1, 7); - const red = parseInt(color.substring(0, 2), 16); // hexToR - const green = parseInt(color.substring(2, 4), 16); // hexToG - const blue = parseInt(color.substring(4, 6), 16); // hexToB - - return red * 0.299 + green * 0.587 + blue * 0.114 > 186 - ? '#000000' - : '#ffffff'; - }, - - getRelativeDate: (date: string | Date): string => { - const now = new Date(); - const diff = now.getTime() - new Date(date).getTime(); - - const seconds = Math.floor(diff / 1000); - if (seconds < 60) { - return `${seconds} seconds ago`; - } - - const minutes = Math.floor(diff / 60000); - if (minutes < 60) { - return `${minutes} minutes ago`; - } - - const hours = Math.floor(diff / 3600000); - if (hours < 24) { - return `${hours} hours ago`; - } - - const days = Math.floor(diff / 86400000); - if (days < 30) { - return `${days} days ago`; - } - - const months = Math.floor(diff / 2592000000); - if (months < 12) { - return `${months} months ago`; - } - - const years = Math.floor(diff / 31536000000); - return `${years} years ago`; - }, - - bytesToHumanReadable: (bytes: number): string => { - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - if (bytes === 0) { - return '0 B'; - } - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - const size = bytes / Math.pow(1024, i); - return `${size.toFixed(2)} ${sizes[i]}`; - }, -}; From ea43193dc35291f8e9fec3d7eeaabe2ae1669810 Mon Sep 17 00:00:00 2001 From: caverav Date: Tue, 4 Feb 2025 21:27:01 -0300 Subject: [PATCH 18/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(report-gene?= =?UTF-8?q?rator.js):=20refactor=20splitHTMLParagraphs=20for=20clarity=20a?= =?UTF-8?q?nd=20maintainability=20by=20extracting=20image=20attribute=20pr?= =?UTF-8?q?ocessing=20into=20separate=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/lib/report-generator.js | 48 +++++++++++++++++------------ 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/backend/src/lib/report-generator.js b/backend/src/lib/report-generator.js index 0fa8786e..f42cd609 100644 --- a/backend/src/lib/report-generator.js +++ b/backend/src/lib/report-generator.js @@ -661,37 +661,47 @@ async function prepAuditData(data, settings) { } async function splitHTMLParagraphs(data) { - var result = []; - if (!data) return result; + if (!data) return []; - var splitted = data.split(/()/); + const result = []; + const splitted = data.split(/()/); - for (var value of splitted) { + for (const value of splitted) { if (!value) continue; + if (value.startsWith(' 1) src = src[1]; - if (alt && alt.length > 1) alt = _.unescape(alt[1]); - - if (!src.startsWith('data')) { - try { - src = (await Image.getOne(src)).value; - } catch (error) { - src = 'data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA='; - } - } + const { src, alt } = extractImageAttributes(value); + const processedSrc = await processImageSrc(src); + if (result.length === 0) result.push({ text: '', images: [] }); - result[result.length - 1].images.push({ image: src, caption: alt ?? '' }); - } else if (value === '') { - continue; + result[result.length - 1].images.push({ image: processedSrc, caption: alt }); } else { result.push({ text: value, images: [] }); } } + return result; } +function extractImageAttributes(value) { + const srcMatch = value.match(/ 1 ? srcMatch[1] : ''; + const alt = altMatch.length > 1 ? _.unescape(altMatch[1]) : ''; + return { src, alt }; +} + +async function processImageSrc(src) { + if (!src.startsWith('data')) { + try { + src = (await Image.getOne(src)).value; + } catch (error) { + src = 'data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA='; + } + } + return src; +} + function replaceSubTemplating(o, originalData = o) { var regexp = /\{_\{([a-zA-Z0-9\[\]\_\.]{1,})\}_\}/gm; if (Array.isArray(o)) From e4a329294ab78ec4be910e1b98fb3f0bb0bd0c52 Mon Sep 17 00:00:00 2001 From: caverav Date: Tue, 4 Feb 2025 21:38:30 -0300 Subject: [PATCH 19/19] =?UTF-8?q?=F0=9F=94=A7=20chore:=20Remove=20unused?= =?UTF-8?q?=20tiptap=20packages=20and=20react-quill-new=20from=20frontend?= =?UTF-8?q?=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index c094cec7..ca664245 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,18 +27,6 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.3", - "@tiptap/core": "^2.11.5", - "@tiptap/extension-bubble-menu": "^2.11.4", - "@tiptap/extension-floating-menu": "^2.11.4", - "@tiptap/extension-heading": "^2.11.4", - "@tiptap/extension-highlight": "^2.11.4", - "@tiptap/extension-image": "^2.11.4", - "@tiptap/extension-link": "^2.11.4", - "@tiptap/extension-placeholder": "^2.11.4", - "@tiptap/extension-underline": "^2.11.4", - "@tiptap/pm": "^2.11.5", - "@tiptap/react": "^2.11.4", - "@tiptap/starter-kit": "^2.11.4", "@types/react-router-dom": "^5.3.3", "@visx/text": "^3.3.0", "@visx/wordcloud": "^3.3.0", @@ -63,7 +51,6 @@ "react-dom": "^18.3.1", "react-i18next": "^15.0.0", "react-icons": "^5.3.0", - "react-quill-new": "^3.3.0", "react-router-dom": "^6.25.1", "reactjs-tiptap-editor": "^0.1.14", "sonner": "^1.5.0",