Skip to content

Commit e59e79b

Browse files
committed
feat: markdown editor and markdown renderer
1 parent cbc111a commit e59e79b

File tree

10 files changed

+2905
-122
lines changed

10 files changed

+2905
-122
lines changed

web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"@kleros/ui-components-library": "^2.20.0",
9292
"@lifi/wallet-management": "^3.7.1",
9393
"@lifi/widget": "^3.18.1",
94+
"@mdxeditor/editor": "^3.44.2",
9495
"@reown/appkit": "^1.7.1",
9596
"@reown/appkit-adapter-wagmi": "^1.7.1",
9697
"@sentry/react": "^7.120.0",

web/src/components/EvidenceCard.tsx

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, { useMemo } from "react";
22
import styled, { css } from "styled-components";
33

4-
import ReactMarkdown from "react-markdown";
54
import { useParams } from "react-router-dom";
65

76
import { Card } from "@kleros/ui-components-library";
@@ -18,9 +17,11 @@ import { hoverShortTransitionTiming } from "styles/commonStyles";
1817
import { landscapeStyle } from "styles/landscapeStyle";
1918
import { responsiveSize } from "styles/responsiveSize";
2019

20+
import JurorTitle from "pages/Home/TopJurors/JurorCard/JurorTitle";
21+
2122
import { ExternalLink } from "./ExternalLink";
2223
import { InternalLink } from "./InternalLink";
23-
import JurorTitle from "pages/Home/TopJurors/JurorCard/JurorTitle";
24+
import MarkdownRenderer from "./MarkdownRenderer";
2425

2526
const StyledCard = styled(Card)`
2627
width: 100%;
@@ -66,17 +67,6 @@ const Index = styled.p`
6667
`;
6768

6869
const ReactMarkdownWrapper = styled.div``;
69-
const StyledReactMarkdown = styled(ReactMarkdown)`
70-
a {
71-
font-size: 16px;
72-
}
73-
code {
74-
color: ${({ theme }) => theme.secondaryText};
75-
}
76-
p {
77-
margin: 0;
78-
}
79-
`;
8070

8171
const BottomShade = styled.div`
8272
background-color: ${({ theme }) => theme.lightBlue};
@@ -227,10 +217,12 @@ const EvidenceCard: React.FC<IEvidenceCard> = ({
227217
</IndexAndName>
228218
{name && description ? (
229219
<ReactMarkdownWrapper dir="auto">
230-
<StyledReactMarkdown>{description}</StyledReactMarkdown>
220+
<MarkdownRenderer content={description} />
231221
</ReactMarkdownWrapper>
232222
) : (
233-
<p>{evidence}</p>
223+
<ReactMarkdownWrapper dir="auto">
224+
<MarkdownRenderer content={evidence} />
225+
</ReactMarkdownWrapper>
234226
)}
235227
</TopContent>
236228
<BottomShade>
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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+
toolbarPlugin,
17+
UndoRedo,
18+
BoldItalicUnderlineToggles,
19+
CodeToggle,
20+
ListsToggle,
21+
CreateLink,
22+
InsertTable,
23+
BlockTypeSelect,
24+
Separator,
25+
} from "@mdxeditor/editor";
26+
27+
import "@mdxeditor/editor/style.css";
28+
29+
import InfoIcon from "svgs/icons/info-circle.svg";
30+
31+
const Container = styled.div<{ isEmpty: boolean }>`
32+
width: 100%;
33+
34+
[class*="mdxeditor"] {
35+
background-color: ${({ theme }) => theme.whiteBackground} !important;
36+
border: 1px solid ${({ theme }) => theme.stroke} !important;
37+
border-radius: 3px;
38+
font-family: "Open Sans", sans-serif;
39+
}
40+
41+
[class*="toolbar"] {
42+
background-color: ${({ theme }) => theme.lightGrey} !important;
43+
border-bottom: none !important;
44+
45+
button {
46+
color: ${({ theme }) => theme.whiteBackground} !important;
47+
}
48+
49+
svg {
50+
fill: ${({ theme }) => theme.whiteBackground} !important;
51+
}
52+
53+
select {
54+
background-color: ${({ theme }) => theme.whiteBackground} !important;
55+
color: ${({ theme }) => theme.primaryText} !important;
56+
}
57+
}
58+
59+
[class*="contentEditable"] {
60+
background-color: ${({ theme }) => theme.whiteBackground} !important;
61+
color: ${({ theme }) => theme.primaryText} !important;
62+
min-height: 200px;
63+
padding: 16px;
64+
font-size: 16px;
65+
line-height: 1.5;
66+
67+
p {
68+
color: ${({ theme, isEmpty }) => (isEmpty ? theme.secondaryText : theme.primaryText)} !important;
69+
margin: 0 0 12px 0;
70+
}
71+
72+
p:empty::before {
73+
color: ${({ theme }) => theme.secondaryText} !important;
74+
opacity: 0.6 !important;
75+
}
76+
77+
h1,
78+
h2,
79+
h3,
80+
h4,
81+
h5,
82+
h6 {
83+
color: ${({ theme }) => theme.primaryText} !important;
84+
font-weight: 600;
85+
margin: 16px 0 8px 0;
86+
}
87+
88+
blockquote {
89+
color: ${({ theme }) => theme.secondaryText} !important;
90+
border-left: 3px solid ${({ theme }) => theme.mediumBlue} !important;
91+
font-style: italic;
92+
margin: 16px 0;
93+
padding-left: 12px;
94+
}
95+
96+
code {
97+
background-color: ${({ theme }) => theme.lightGrey} !important;
98+
color: ${({ theme }) => theme.primaryText} !important;
99+
}
100+
101+
pre {
102+
background-color: ${({ theme }) => theme.lightGrey} !important;
103+
color: ${({ theme }) => theme.primaryText} !important;
104+
}
105+
106+
a {
107+
color: ${({ theme }) => theme.primaryBlue} !important;
108+
}
109+
110+
th {
111+
background-color: ${({ theme }) => theme.lightGrey} !important;
112+
color: ${({ theme }) => theme.primaryText} !important;
113+
}
114+
115+
td {
116+
color: ${({ theme }) => theme.primaryText} !important;
117+
}
118+
}
119+
`;
120+
121+
const MessageContainer = styled.div`
122+
display: flex;
123+
align-items: flex-start;
124+
gap: 8px;
125+
margin-top: 8px;
126+
`;
127+
128+
const MessageText = styled.small`
129+
font-size: 14px;
130+
font-weight: 400;
131+
color: ${({ theme }) => theme.secondaryText};
132+
hyphens: auto;
133+
line-height: 1.4;
134+
`;
135+
136+
const StyledInfoIcon = styled(InfoIcon)`
137+
width: 16px;
138+
height: 16px;
139+
fill: ${({ theme }) => theme.secondaryText} !important;
140+
flex-shrink: 0;
141+
margin-top: 2px;
142+
143+
path {
144+
fill: ${({ theme }) => theme.secondaryText} !important;
145+
}
146+
147+
* {
148+
fill: ${({ theme }) => theme.secondaryText} !important;
149+
}
150+
`;
151+
152+
interface IMarkdownEditor {
153+
value: string;
154+
onChange: (value: string) => void;
155+
placeholder?: string;
156+
showMessage?: boolean;
157+
}
158+
159+
const MarkdownEditor: React.FC<IMarkdownEditor> = ({
160+
value,
161+
onChange,
162+
placeholder = "Justify your vote...",
163+
showMessage = true,
164+
}) => {
165+
const editorRef = useRef<MDXEditorMethods>(null);
166+
167+
const handleChange = (markdown: string) => {
168+
onChange(markdown);
169+
};
170+
171+
const isEmpty = !value || value.trim() === "";
172+
173+
const editorProps: MDXEditorProps = {
174+
markdown: value,
175+
onChange: handleChange,
176+
placeholder,
177+
plugins: [
178+
headingsPlugin(),
179+
listsPlugin(),
180+
quotePlugin(),
181+
thematicBreakPlugin(),
182+
markdownShortcutPlugin(),
183+
linkPlugin(),
184+
linkDialogPlugin(),
185+
tablePlugin(),
186+
toolbarPlugin({
187+
toolbarContents: () => (
188+
<>
189+
<UndoRedo />
190+
<Separator />
191+
<BoldItalicUnderlineToggles />
192+
<CodeToggle />
193+
<Separator />
194+
<BlockTypeSelect />
195+
<Separator />
196+
<ListsToggle />
197+
<Separator />
198+
<CreateLink />
199+
<InsertTable />
200+
</>
201+
),
202+
}),
203+
],
204+
};
205+
206+
return (
207+
<Container isEmpty={isEmpty}>
208+
<MDXEditor ref={editorRef} {...editorProps} />
209+
{showMessage && (
210+
<MessageContainer>
211+
<StyledInfoIcon />
212+
<MessageText>
213+
Please provide a comprehensive justification for your decision. Quality explanations are essential for the
214+
parties involved and may be eligible for additional compensation in accordance with our justification
215+
policy.
216+
</MessageText>
217+
</MessageContainer>
218+
)}
219+
</Container>
220+
);
221+
};
222+
223+
export default MarkdownEditor;

0 commit comments

Comments
 (0)