Skip to content

Commit 1e7c186

Browse files
authored
Merge pull request #126 from kleros/feat/markdown-everywhere
feat: add markdown features
2 parents c382dc8 + ef7fed6 commit 1e7c186

File tree

11 files changed

+4403
-182
lines changed

11 files changed

+4403
-182
lines changed

web/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"@cyntler/react-doc-viewer": "^1.16.3",
7272
"@kleros/kleros-app": "^2.1.0",
7373
"@kleros/ui-components-library": "^2.19.0",
74+
"@mdxeditor/editor": "^3.45.0",
7475
"@reown/appkit": "^1.6.6",
7576
"@reown/appkit-adapter-wagmi": "^1.6.6",
7677
"@sentry/react": "^7.93.0",
@@ -99,6 +100,9 @@
99100
"react-scripts": "^5.0.1",
100101
"react-toastify": "^9.1.3",
101102
"react-use": "^17.4.3",
103+
"rehype-raw": "^6.1.1",
104+
"rehype-sanitize": "^5.0.1",
105+
"remark-gfm": "^3.0.1",
102106
"styled-components": "^5.3.11",
103107
"subgraph-status": "^1.2.4",
104108
"viem": "^2.27.2",
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React, { useRef } from "react";
2+
import styled from "styled-components";
3+
import { useClickAway } from "react-use";
4+
import { Button } from "@kleros/ui-components-library";
5+
import { Overlay } from "components/Overlay";
6+
import { StyledModal } from "pages/MyTransactions/Modal/StyledModal";
7+
8+
const WarningModal = styled(StyledModal)`
9+
width: 400px;
10+
max-width: 90vw;
11+
`;
12+
13+
const Title = styled.h3`
14+
margin: 0 0 16px 0;
15+
color: ${({ theme }) => theme.primaryText};
16+
font-size: 20px;
17+
font-weight: 600;
18+
`;
19+
20+
const Message = styled.p`
21+
margin: 0 0 24px 0;
22+
color: ${({ theme }) => theme.secondaryText};
23+
font-size: 16px;
24+
line-height: 1.5;
25+
`;
26+
27+
const UrlContainer = styled.div`
28+
background-color: ${({ theme }) => theme.lightBackground};
29+
border: 1px solid ${({ theme }) => theme.stroke};
30+
border-radius: 4px;
31+
padding: 8px 12px;
32+
margin: 16px 0 24px 0;
33+
word-break: break-all;
34+
font-family: monospace;
35+
font-size: 14px;
36+
color: ${({ theme }) => theme.primaryText};
37+
`;
38+
39+
const ButtonContainer = styled.div`
40+
display: flex;
41+
gap: 12px;
42+
justify-content: flex-end;
43+
width: 100%;
44+
`;
45+
46+
const CancelButton = styled(Button)`
47+
background-color: transparent;
48+
color: ${({ theme }) => theme.primaryText};
49+
border: 1px solid ${({ theme }) => theme.stroke};
50+
51+
&:hover {
52+
background-color: ${({ theme }) => theme.lightGrey};
53+
}
54+
`;
55+
56+
const ConfirmButton = styled(Button)`
57+
background-color: ${({ theme }) => theme.primaryBlue};
58+
color: ${({ theme }) => theme.whiteBackground};
59+
60+
&:hover {
61+
background-color: ${({ theme }) => theme.secondaryBlue};
62+
}
63+
`;
64+
65+
interface IExternalLinkWarning {
66+
isOpen: boolean;
67+
url: string;
68+
onConfirm: () => void;
69+
onCancel: () => void;
70+
}
71+
72+
const ExternalLinkWarning: React.FC<IExternalLinkWarning> = ({ isOpen, url, onConfirm, onCancel }) => {
73+
const modalRef = useRef<HTMLDivElement>(null);
74+
75+
useClickAway(modalRef, onCancel);
76+
77+
if (!isOpen) return null;
78+
79+
return (
80+
<>
81+
<Overlay />
82+
<WarningModal ref={modalRef}>
83+
<Title>External Link Warning</Title>
84+
<Message>
85+
You are about to navigate to an external website. Please verify the URL before proceeding to ensure your safety.
86+
</Message>
87+
<UrlContainer>{url}</UrlContainer>
88+
<ButtonContainer>
89+
<CancelButton text="Cancel" onClick={onCancel} />
90+
<ConfirmButton text="Continue" onClick={onConfirm} />
91+
</ButtonContainer>
92+
</WarningModal>
93+
</>
94+
);
95+
};
96+
97+
export default ExternalLinkWarning;

web/src/components/FileViewer/Viewers/MarkdownViewer.tsx

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,14 @@ import React from "react";
22
import styled from "styled-components";
33

44
import { type DocRenderer } from "@cyntler/react-doc-viewer";
5-
import ReactMarkdown from "react-markdown";
5+
6+
import MarkdownRenderer from "../../MarkdownRenderer";
67

78
const Container = styled.div`
89
padding: 16px;
910
`;
1011

11-
const StyledMarkdown = styled(ReactMarkdown)`
12-
background-color: ${({ theme }) => theme.whiteBackground};
13-
a {
14-
font-size: 16px;
15-
}
16-
code {
17-
color: ${({ theme }) => theme.secondaryText};
18-
}
19-
`;
20-
21-
const MarkdownRenderer: DocRenderer = ({ mainState: { currentDocument } }) => {
12+
const MarkdownDocRenderer: DocRenderer = ({ mainState: { currentDocument } }) => {
2213
if (!currentDocument) return null;
2314
const base64String = (currentDocument.fileData as string).split(",")[1];
2415

@@ -27,12 +18,12 @@ const MarkdownRenderer: DocRenderer = ({ mainState: { currentDocument } }) => {
2718

2819
return (
2920
<Container id="md-renderer">
30-
<StyledMarkdown>{decodedData}</StyledMarkdown>
21+
<MarkdownRenderer content={decodedData} />
3122
</Container>
3223
);
3324
};
3425

35-
MarkdownRenderer.fileTypes = ["md", "text/plain"];
36-
MarkdownRenderer.weight = 1;
26+
MarkdownDocRenderer.fileTypes = ["md", "text/plain"];
27+
MarkdownDocRenderer.weight = 1;
3728

38-
export default MarkdownRenderer;
29+
export default MarkdownDocRenderer;
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import React, { useRef } from "react";
2+
import styled from "styled-components";
3+
4+
import {
5+
MDXEditor,
6+
type MDXEditorMethods,
7+
type MDXEditorProps,
8+
headingsPlugin,
9+
listsPlugin,
10+
quotePlugin,
11+
thematicBreakPlugin,
12+
markdownShortcutPlugin,
13+
linkPlugin,
14+
linkDialogPlugin,
15+
tablePlugin,
16+
codeBlockPlugin,
17+
codeMirrorPlugin,
18+
toolbarPlugin,
19+
UndoRedo,
20+
BoldItalicUnderlineToggles,
21+
ListsToggle,
22+
CreateLink,
23+
InsertTable,
24+
InsertCodeBlock,
25+
BlockTypeSelect,
26+
Separator,
27+
} from "@mdxeditor/editor";
28+
29+
import { isValidUrl } from "utils/urlValidation";
30+
31+
import { MDXEditorContainer, MDXEditorGlobalStyles } from "styles/mdxEditorTheme";
32+
33+
import "@mdxeditor/editor/style.css";
34+
35+
const Container = styled(MDXEditorContainer)<{ isEmpty: boolean }>``;
36+
37+
interface IMarkdownEditor {
38+
value: string;
39+
onChange: (value: string) => void;
40+
placeholder?: string;
41+
}
42+
43+
const MarkdownEditor: React.FC<IMarkdownEditor> = ({
44+
value,
45+
onChange,
46+
placeholder,
47+
}) => {
48+
const editorRef = useRef<MDXEditorMethods>(null);
49+
50+
const handleChange = (markdown: string) => {
51+
let cleanedMarkdown = markdown === "\u200B" ? "" : markdown.replace(/^\u200B/, "");
52+
// Remove ALL escape characters - no exceptions
53+
cleanedMarkdown = cleanedMarkdown.replace(/\\([`[]*_#|>-+=~^{}()!&<$%\\])/g, "$1");
54+
// Also handle multiple consecutive backslashes that might accumulate
55+
cleanedMarkdown = cleanedMarkdown.replace(/\\+/g, "");
56+
57+
onChange(cleanedMarkdown);
58+
};
59+
60+
const handleContainerClick = () => {
61+
if (isEmpty && editorRef.current) {
62+
editorRef.current.setMarkdown("\u200B");
63+
setTimeout(() => {
64+
if (editorRef.current) {
65+
editorRef.current.focus();
66+
}
67+
}, 0);
68+
}
69+
};
70+
71+
const isEmpty = !value || value.trim() === "";
72+
73+
const editorProps: MDXEditorProps = {
74+
markdown: value,
75+
onChange: handleChange,
76+
placeholder,
77+
suppressHtmlProcessing: true,
78+
plugins: [
79+
headingsPlugin(),
80+
listsPlugin(),
81+
quotePlugin(),
82+
thematicBreakPlugin(),
83+
markdownShortcutPlugin(),
84+
linkPlugin({
85+
validateUrl: (url) => isValidUrl(url),
86+
}),
87+
linkDialogPlugin(),
88+
tablePlugin(),
89+
codeBlockPlugin({ defaultCodeBlockLanguage: "text" }),
90+
codeMirrorPlugin({
91+
codeBlockLanguages: {
92+
text: "Code",
93+
},
94+
}),
95+
toolbarPlugin({
96+
toolbarContents: () => (
97+
<>
98+
<UndoRedo />
99+
<Separator />
100+
<BoldItalicUnderlineToggles />
101+
<InsertCodeBlock />
102+
<Separator />
103+
<BlockTypeSelect />
104+
<Separator />
105+
<ListsToggle />
106+
<Separator />
107+
<CreateLink />
108+
<InsertTable />
109+
</>
110+
),
111+
}),
112+
],
113+
};
114+
115+
return (
116+
<>
117+
<MDXEditorGlobalStyles />
118+
<Container isEmpty={isEmpty} onClick={handleContainerClick} role="region" aria-label="Markdown editor">
119+
<MDXEditor ref={editorRef} {...editorProps} aria-label="Rich text editor for markdown content" />
120+
</Container>
121+
</>
122+
);
123+
};
124+
125+
export default MarkdownEditor;

0 commit comments

Comments
 (0)