Skip to content

Commit 0e96a87

Browse files
authored
Merge pull request #2140 from kleros/feat/markdown-editor-and-markdown-renderer
feat: markdown editor and markdown renderer
2 parents b9a7a97 + a8d32b8 commit 0e96a87

File tree

20 files changed

+3966
-163
lines changed

20 files changed

+3966
-163
lines changed

web/package.json

Lines changed: 4 additions & 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.45.0",
9495
"@reown/appkit": "^1.7.1",
9596
"@reown/appkit-adapter-wagmi": "^1.7.1",
9697
"@sentry/react": "^7.120.0",
@@ -127,6 +128,9 @@
127128
"react-scripts": "^5.0.1",
128129
"react-toastify": "^9.1.3",
129130
"react-use": "^17.5.1",
131+
"rehype-raw": "^7.0.0",
132+
"rehype-sanitize": "^6.0.0",
133+
"remark-gfm": "^3.0.1",
130134
"styled-components": "^5.3.3",
131135
"subgraph-status": "^1.2.4",
132136
"viem": "^2.24.1",

web/src/components/DisputePreview/DisputeContext.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
import React, { useMemo } from "react";
22
import styled from "styled-components";
33

4-
import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes";
54
import { useAccount } from "wagmi";
65

6+
import { DisputeDetails } from "@kleros/kleros-sdk/src/dataMappings/utils/disputeDetailsTypes";
7+
78
import { INVALID_DISPUTE_DATA_ERROR, RPC_ERROR } from "consts/index";
89
import { Answer as IAnswer } from "context/NewDisputeContext";
910
import { isUndefined } from "utils/index";
1011

11-
import { responsiveSize } from "styles/responsiveSize";
12-
1312
import { DisputeDetailsQuery, VotingHistoryQuery } from "src/graphql/graphql";
1413

15-
import ReactMarkdown from "components/ReactMarkdown";
14+
import { responsiveSize } from "styles/responsiveSize";
15+
16+
import MarkdownRenderer from "components/MarkdownRenderer";
1617
import { StyledSkeleton } from "components/StyledSkeleton";
1718

19+
import CardLabel from "../DisputeView/CardLabels";
1820
import { Divider } from "../Divider";
1921
import { ExternalLink } from "../ExternalLink";
22+
import RulingAndRewardsIndicators from "../Verdict/RulingAndRewardsIndicators";
2023

2124
import AliasDisplay from "./Alias";
22-
import RulingAndRewardsIndicators from "../Verdict/RulingAndRewardsIndicators";
23-
import CardLabel from "../DisputeView/CardLabels";
2425

2526
const StyledH1 = styled.h1`
2627
margin: 0;
@@ -134,12 +135,12 @@ export const DisputeContext: React.FC<IDisputeContext> = ({
134135
<div>
135136
{disputeDetails?.question?.trim() ? (
136137
<ReactMarkdownWrapper dir="auto">
137-
<ReactMarkdown>{disputeDetails.question}</ReactMarkdown>
138+
<MarkdownRenderer content={disputeDetails.question} />
138139
</ReactMarkdownWrapper>
139140
) : null}
140141
{disputeDetails?.description?.trim() ? (
141142
<ReactMarkdownWrapper dir="auto">
142-
<ReactMarkdown>{disputeDetails.description}</ReactMarkdown>
143+
<MarkdownRenderer content={disputeDetails.description} />
143144
</ReactMarkdownWrapper>
144145
) : null}
145146
</div>

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: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import React from "react";
2+
import styled, { css } from "styled-components";
3+
4+
import Modal from "react-modal";
5+
6+
import { Button } from "@kleros/ui-components-library";
7+
8+
import WarningIcon from "svgs/icons/warning-outline.svg";
9+
10+
import { landscapeStyle } from "styles/landscapeStyle";
11+
12+
const StyledModal = styled(Modal)`
13+
position: absolute;
14+
top: 50%;
15+
left: 50%;
16+
right: auto;
17+
bottom: auto;
18+
margin-right: -50%;
19+
transform: translate(-50%, -50%);
20+
height: auto;
21+
max-height: 90vh;
22+
width: min(90%, 480px);
23+
border: 1px solid ${({ theme }) => theme.stroke};
24+
border-radius: 8px;
25+
background-color: ${({ theme }) => theme.whiteBackground};
26+
padding: 32px;
27+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
28+
z-index: 10002;
29+
overflow-y: auto;
30+
`;
31+
32+
const Overlay = styled.div`
33+
position: fixed;
34+
top: 0;
35+
left: 0;
36+
right: 0;
37+
bottom: 0;
38+
background-color: rgba(0, 0, 0, 0.5);
39+
z-index: 10001;
40+
`;
41+
42+
const Header = styled.div`
43+
display: flex;
44+
align-items: center;
45+
gap: 12px;
46+
margin-bottom: 16px;
47+
`;
48+
49+
const StyledWarningIcon = styled(WarningIcon)`
50+
width: 24px;
51+
height: 24px;
52+
fill: ${({ theme }) => theme.warning};
53+
`;
54+
55+
const Title = styled.h3`
56+
color: ${({ theme }) => theme.primaryText};
57+
font-size: 18px;
58+
font-weight: 600;
59+
margin: 0;
60+
`;
61+
62+
const Message = styled.p`
63+
color: ${({ theme }) => theme.primaryText};
64+
font-size: 14px;
65+
line-height: 1.5;
66+
margin: 0 0 16px 0;
67+
`;
68+
69+
const UrlContainer = styled.div`
70+
background-color: ${({ theme }) => theme.lightGrey};
71+
border: 1px solid ${({ theme }) => theme.stroke};
72+
border-radius: 4px;
73+
padding: 12px;
74+
margin: 16px 0;
75+
word-break: break-all;
76+
`;
77+
78+
const Url = styled.code`
79+
color: ${({ theme }) => theme.secondaryText};
80+
font-size: 13px;
81+
font-family: monospace;
82+
`;
83+
84+
const ButtonContainer = styled.div`
85+
display: flex;
86+
gap: 12px;
87+
justify-content: center;
88+
flex-wrap: wrap;
89+
margin-top: 24px;
90+
91+
${landscapeStyle(
92+
() => css`
93+
justify-content: flex-end;
94+
`
95+
)}
96+
`;
97+
98+
const CancelButton = styled(Button)`
99+
background-color: ${({ theme }) => theme.whiteBackground};
100+
border: 1px solid ${({ theme }) => theme.stroke};
101+
102+
p {
103+
color: ${({ theme }) => theme.primaryText} !important;
104+
}
105+
106+
&:hover {
107+
background-color: ${({ theme }) => theme.mediumBlue};
108+
}
109+
`;
110+
111+
const ConfirmButton = styled(Button)`
112+
background-color: ${({ theme }) => theme.warning};
113+
color: ${({ theme }) => theme.whiteBackground};
114+
border: 1px solid ${({ theme }) => theme.warning};
115+
116+
&:hover {
117+
background-color: ${({ theme }) => theme.warning}BB;
118+
}
119+
`;
120+
121+
interface IExternalLinkWarning {
122+
isOpen: boolean;
123+
url: string;
124+
onConfirm: () => void;
125+
onCancel: () => void;
126+
}
127+
128+
const ExternalLinkWarning: React.FC<IExternalLinkWarning> = ({ isOpen, url, onConfirm, onCancel }) => {
129+
return (
130+
<StyledModal
131+
isOpen={isOpen}
132+
onRequestClose={onCancel}
133+
overlayElement={(props, contentElement) => <Overlay {...props}>{contentElement}</Overlay>}
134+
ariaHideApp={false}
135+
role="dialog"
136+
aria-labelledby="external-link-title"
137+
aria-describedby="external-link-description"
138+
>
139+
<Header>
140+
<StyledWarningIcon />
141+
<Title id="external-link-title">External Link Warning</Title>
142+
</Header>
143+
144+
<Message id="external-link-description">
145+
You are about to navigate to an external website. Please verify the URL before proceeding to ensure it&apos;s
146+
safe and legitimate.
147+
</Message>
148+
149+
<UrlContainer>
150+
<Url>{url}</Url>
151+
</UrlContainer>
152+
153+
<Message>
154+
<strong>Safety Tips:</strong>
155+
<br />
156+
• Verify the domain name is correct
157+
<br />
158+
• Check for suspicious characters or typos
159+
<br />• Only proceed if you trust this destination
160+
</Message>
161+
162+
<ButtonContainer>
163+
<CancelButton text="Cancel" onClick={onCancel} />
164+
<ConfirmButton text="Continue to External Site" onClick={onConfirm} />
165+
</ButtonContainer>
166+
</StyledModal>
167+
);
168+
};
169+
170+
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;

0 commit comments

Comments
 (0)