Skip to content

Commit de4c5d6

Browse files
authored
Merge pull request #1613 from kleros/feat/evidence-attachment-display
Feat/evidence attachment display
2 parents 3da67bd + 0df6943 commit de4c5d6

File tree

15 files changed

+717
-76
lines changed

15 files changed

+717
-76
lines changed

web/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"vite-tsconfig-paths": "^4.3.2"
7575
},
7676
"dependencies": {
77+
"@cyntler/react-doc-viewer": "^1.16.3",
7778
"@filebase/client": "^0.0.5",
7879
"@kleros/kleros-sdk": "workspace:^",
7980
"@kleros/ui-components-library": "^2.12.0",
+10
Loading

web/src/assets/svgs/icons/new-tab.svg

+3
Loading

web/src/components/EvidenceCard.tsx

+27-10
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@ import styled, { css } from "styled-components";
33

44
import Identicon from "react-identicons";
55
import ReactMarkdown from "react-markdown";
6+
import { Link } from "react-router-dom";
67

78
import { Card } from "@kleros/ui-components-library";
89

910
import AttachmentIcon from "svgs/icons/attachment.svg";
1011

1112
import { useIPFSQuery } from "hooks/useIPFSQuery";
13+
import { formatDate } from "utils/date";
1214
import { getIpfsUrl } from "utils/getIpfsUrl";
1315
import { shortenAddress } from "utils/shortenAddress";
14-
import { formatDate } from "utils/date";
1516

1617
import { landscapeStyle } from "styles/landscapeStyle";
1718
import { responsiveSize } from "styles/responsiveSize";
@@ -65,13 +66,13 @@ const StyledA = styled.a`
6566
margin-left: auto;
6667
gap: ${responsiveSize(5, 6)};
6768
${landscapeStyle(
68-
() => css`
69+
() => css`
6970
> svg {
7071
width: 16px;
7172
fill: ${({ theme }) => theme.primaryBlue};
7273
}
7374
`
74-
)}
75+
)}
7576
`;
7677

7778
const AccountContainer = styled.div`
@@ -95,22 +96,37 @@ const AccountContainer = styled.div`
9596
const DesktopText = styled.span`
9697
display: none;
9798
${landscapeStyle(
98-
() => css`
99+
() => css`
99100
display: inline;
100101
`
101-
)}
102+
)}
102103
`;
103104

104105
const Timestamp = styled.p`
105-
color: ${({ theme }) => theme.secondaryText};
106+
color: ${({ theme }) => theme.secondaryText};
106107
`;
107108

108109
const MobileText = styled.span`
109110
${landscapeStyle(
110-
() => css`
111+
() => css`
111112
display: none;
112113
`
113-
)}
114+
)}
115+
`;
116+
117+
const StyledLink = styled(Link)`
118+
height: fit-content;
119+
display: flex;
120+
margin-left: auto;
121+
gap: ${responsiveSize(5, 6)};
122+
${landscapeStyle(
123+
() => css`
124+
> svg {
125+
width: 16px;
126+
fill: ${({ theme }) => theme.primaryBlue};
127+
}
128+
`
129+
)}
114130
`;
115131

116132
const AttachedFileText: React.FC = () => (
@@ -129,6 +145,7 @@ interface IEvidenceCard {
129145

130146
const EvidenceCard: React.FC<IEvidenceCard> = ({ evidence, sender, index, timestamp }) => {
131147
const { data } = useIPFSQuery(evidence);
148+
132149
return (
133150
<StyledCard>
134151
<TextContainer>
@@ -149,10 +166,10 @@ const EvidenceCard: React.FC<IEvidenceCard> = ({ evidence, sender, index, timest
149166
</AccountContainer>
150167
<Timestamp>{formatDate(Number(timestamp))}</Timestamp>
151168
{data && typeof data.fileURI !== "undefined" && (
152-
<StyledA href={getIpfsUrl(data.fileURI)} target="_blank" rel="noreferrer">
169+
<StyledLink to={`attachment/?url=${getIpfsUrl(data.fileURI)}`}>
153170
<AttachmentIcon />
154171
<AttachedFileText />
155-
</StyledA>
172+
</StyledLink>
156173
)}
157174
</BottomShade>
158175
</StyledCard>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
4+
import { type DocRenderer } from "@cyntler/react-doc-viewer";
5+
import ReactMarkdown from "react-markdown";
6+
7+
const Container = styled.div`
8+
padding: 16px;
9+
`;
10+
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 } }) => {
22+
if (!currentDocument) return null;
23+
const base64String = (currentDocument.fileData as string).split(",")[1];
24+
25+
// Decode the base64 string
26+
const decodedData = atob(base64String);
27+
28+
return (
29+
<Container id="md-renderer">
30+
<StyledMarkdown>{decodedData}</StyledMarkdown>
31+
</Container>
32+
);
33+
};
34+
35+
MarkdownRenderer.fileTypes = ["md", "text/plain"];
36+
MarkdownRenderer.weight = 1;
37+
38+
export default MarkdownRenderer;
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
4+
import DocViewer, { DocViewerRenderers } from "@cyntler/react-doc-viewer";
5+
6+
import "@cyntler/react-doc-viewer/dist/index.css";
7+
import { customScrollbar } from "styles/customScrollbar";
8+
9+
import MarkdownRenderer from "./Viewers/MarkdownViewer";
10+
11+
const Wrapper = styled.div`
12+
background-color: ${({ theme }) => theme.whiteBackground};
13+
border-radius: 3px;
14+
box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.06);
15+
max-height: 750px;
16+
overflow: scroll;
17+
18+
${customScrollbar}
19+
`;
20+
21+
const StyledDocViewer = styled(DocViewer)`
22+
background-color: ${({ theme }) => theme.whiteBackground} !important;
23+
`;
24+
25+
/**
26+
* @description this viewer supports loading multiple files, it can load urls, local files, etc
27+
* @param url The url of the file to be displayed
28+
* @returns renders the file
29+
*/
30+
const FileViewer: React.FC<{ url: string }> = ({ url }) => {
31+
const docs = [{ uri: url }];
32+
return (
33+
<Wrapper className="file-viewer-wrapper">
34+
<StyledDocViewer
35+
documents={docs}
36+
pluginRenderers={[...DocViewerRenderers, MarkdownRenderer]}
37+
config={{
38+
header: {
39+
disableHeader: true,
40+
disableFileName: true,
41+
},
42+
pdfZoom: {
43+
defaultZoom: 0.8,
44+
zoomJump: 0.1,
45+
},
46+
pdfVerticalScrollByDefault: true, // false as default
47+
}}
48+
/>
49+
</Wrapper>
50+
);
51+
};
52+
53+
export default FileViewer;

web/src/components/Loader.tsx

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from "react";
2+
import styled, { type CSSProperties, keyframes } from "styled-components";
3+
4+
import KlerosIcon from "svgs/icons/kleros.svg";
5+
6+
type Width = CSSProperties["width"];
7+
type Height = CSSProperties["height"];
8+
9+
const breathing = keyframes`
10+
0% {
11+
transform: scale(1);
12+
}
13+
14+
50% {
15+
transform: scale(1.3);
16+
}
17+
18+
100% {
19+
transform: scale(1);
20+
}
21+
`;
22+
23+
const StyledKlerosIcon = styled(KlerosIcon)`
24+
path {
25+
fill: ${({ theme }) => theme.klerosUIComponentsStroke};
26+
}
27+
animation: ${breathing} 2s ease-out infinite normal;
28+
`;
29+
30+
const Container = styled.div<{ width?: Width; height?: Height }>`
31+
width: ${({ width }) => width ?? "100%"};
32+
height: ${({ height }) => height ?? "100%"};
33+
`;
34+
35+
interface ILoader {
36+
width?: Width;
37+
height?: Height;
38+
className?: string;
39+
}
40+
41+
const Loader: React.FC<ILoader> = ({ width, height, className }) => {
42+
return (
43+
<Container {...{ width, height, className }}>
44+
<StyledKlerosIcon />
45+
</Container>
46+
);
47+
};
48+
49+
export default Loader;

web/src/hooks/queries/useEvidences.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export type { EvidencesQuery };
88

99
const evidencesQuery = graphql(`
1010
query Evidences($evidenceGroupID: String) {
11-
evidences(where: { evidenceGroup: $evidenceGroupID }, orderBy: id, orderDirection: asc) {
11+
evidences(where: { evidenceGroup: $evidenceGroupID }, orderBy: timestamp, orderDirection: desc) {
1212
id
1313
evidence
1414
sender {

web/src/layout/Header/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { TestnetBanner } from "./TestnetBanner";
99

1010
const Container = styled.div`
1111
position: sticky;
12-
z-index: 1;
12+
z-index: 10;
1313
top: 0;
1414
width: 100%;
1515
background-color: ${({ theme }) => theme.primaryPurple};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
4+
import { useNavigate, useParams } from "react-router-dom";
5+
6+
import { Button } from "@kleros/ui-components-library";
7+
8+
import Arrow from "svgs/icons/arrow-left.svg";
9+
import PaperClip from "svgs/icons/paperclip.svg";
10+
11+
import { responsiveSize } from "styles/responsiveSize";
12+
13+
const Container = styled.div`
14+
width: 100%;
15+
display: flex;
16+
justify-content: space-between;
17+
align-items: center;
18+
margin-bottom: 38px;
19+
`;
20+
21+
const TitleContainer = styled.div`
22+
display: flex;
23+
flex-direction: row;
24+
align-items: center;
25+
gap: 8px;
26+
`;
27+
28+
const Title = styled.h1`
29+
margin: 0px;
30+
font-size: ${responsiveSize(16, 24)};
31+
`;
32+
33+
const StyledPaperClip = styled(PaperClip)`
34+
width: ${responsiveSize(16, 24)};
35+
height: ${responsiveSize(16, 24)};
36+
path {
37+
fill: ${({ theme }) => theme.primaryPurple};
38+
}
39+
`;
40+
41+
const StyledButton = styled(Button)`
42+
background-color: transparent;
43+
padding: 0;
44+
.button-text {
45+
color: ${({ theme }) => theme.primaryBlue};
46+
font-weight: 400;
47+
}
48+
.button-svg {
49+
path {
50+
fill: ${({ theme }) => theme.primaryBlue};
51+
}
52+
}
53+
:focus,
54+
:hover {
55+
background-color: transparent;
56+
}
57+
`;
58+
59+
const Header: React.FC = () => {
60+
const { id } = useParams();
61+
const navigate = useNavigate();
62+
63+
return (
64+
<Container>
65+
<TitleContainer>
66+
<StyledPaperClip />
67+
<Title>Attachment File</Title>{" "}
68+
</TitleContainer>
69+
<StyledButton text="Return" Icon={Arrow} onClick={() => navigate(`/cases/${id}/evidence`)} />
70+
</Container>
71+
);
72+
};
73+
74+
export default Header;

0 commit comments

Comments
 (0)