Skip to content

Commit 21e3cfb

Browse files
authored
Merge pull request #268 from makeopensource/112-pdf-comment-frontend
112 pdf comment frontend
2 parents 37dfdfe + ed01180 commit 21e3cfb

File tree

3 files changed

+259
-33
lines changed

3 files changed

+259
-33
lines changed
Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import React, { useEffect, useState } from 'react'
2-
import { useParams } from 'react-router-dom'
2+
import { useParams, useHistory } from 'react-router-dom'
33
import RequestService from 'services/request.service'
44
import { Submission } from 'devu-shared-modules'
5-
import {Document, Page} from 'react-pdf'
5+
// import { Document, Page } from 'react-pdf'
66
import { pdfjs } from 'react-pdf'
7+
import 'react-pdf/dist/Page/TextLayer.css';
78
import PageWrapper from 'components/shared/layouts/pageWrapper'
89
import { getToken } from 'utils/authentication.utils'
10+
import PDFViewer from 'components/utils/pdfViewer'
911

1012
import config from '../../../config'
1113
const proxiedUrls = {
@@ -14,14 +16,14 @@ const proxiedUrls = {
1416

1517
function _replaceUrl(userUrl: string) {
1618
const proxy: string | undefined = Object.keys(proxiedUrls).find((key) => {
17-
if (userUrl.startsWith(key)) return true
18-
return false
19+
if (userUrl.startsWith(key)) return true
20+
return false
1921
})
20-
22+
2123
if (!proxy) return userUrl
22-
24+
2325
return userUrl.replace(proxy, proxiedUrls[proxy as keyof typeof proxiedUrls])
24-
}
26+
}
2527

2628

2729
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
@@ -30,11 +32,11 @@ const SubmissionFileView = () => {
3032
const { courseId, assignmentId, submissionId } = useParams<{ courseId: string, assignmentId: string, submissionId: string }>()
3133
const [bucket, setBucket] = useState('')
3234
const [filename, setFilename] = useState('')
35+
const history = useHistory()
3336
const authToken = getToken()
3437

3538
const [file, setFile] = useState<File | Blob | null>(null)
36-
const [numPages, setNumPages] = useState(0)
37-
39+
3840
useEffect(() => {
3941
fetchData()
4042
}, [])
@@ -48,14 +50,14 @@ const SubmissionFileView = () => {
4850
const fetchData = async () => {
4951
try {
5052
await RequestService.get<Submission>(`/api/course/${courseId}/assignment/${assignmentId}/submissions/${submissionId}`)
51-
.then((data) => {
52-
const submissionFiles = JSON.parse(data.content)
53-
const [tempbucket,tempfilename] = submissionFiles.filepaths[0].split('/')
54-
setBucket(tempbucket)
55-
setFilename(tempfilename)
53+
.then((data) => {
54+
const submissionFiles = JSON.parse(data.content)
55+
const [tempbucket, tempfilename] = submissionFiles.filepaths[0].split('/')
56+
setBucket(tempbucket)
57+
setFilename(tempfilename)
5658

5759

58-
})
60+
})
5961
} catch (e) {
6062
console.error(e)
6163
}
@@ -74,39 +76,36 @@ const SubmissionFileView = () => {
7476
if (!response.ok) {
7577
throw new Error(`HTTP error! status: ${response.status}`);
7678
}
77-
79+
7880
const arrayBuffer = await response.arrayBuffer();
7981
const blob = new Blob([arrayBuffer], { type: 'application/pdf' });
8082
const file = new File([blob], filename, { type: 'application/pdf' });
81-
8283
setFile(file);
8384
} catch (e) {
8485
console.error(e)
8586
}
8687
}
8788

88-
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
89-
setNumPages(numPages)
90-
}
91-
9289
return (
9390
<PageWrapper>
94-
<div style={{maxWidth:"900px", margin:"auto"}}>
95-
{file &&
96-
<Document file={file} onLoadSuccess={onDocumentLoadSuccess}>
97-
{[...Array(numPages)].map((_, index) => (
98-
<div style={{marginBottom:"20px"}}>
99-
<Page key={index} pageNumber={index + 1} renderTextLayer={false} renderAnnotationLayer={false} scale={1.5}/>
100-
</div>
101-
))}
102-
</Document>
103-
}
91+
<div className="pageHeader">
92+
<h1>View Files</h1>
93+
<button className="pageHeaderBtn" onClick={() => {
94+
history.push(`/course/${courseId}/assignment/${assignmentId}`)
95+
}}>Back to Assignment</button>
96+
</div>
97+
<div style={{ maxWidth: "min-content", margin: "auto" }}>
98+
{file ? (
99+
<PDFViewer file={file}/>
100+
) : (
101+
<p>No files found for this submission</p>
102+
)}
104103
</div>
105-
<br/>
104+
<br />
106105

107106
</PageWrapper>
108107
)
109108

110109
}
111110

112-
export default SubmissionFileView
111+
export default SubmissionFileView;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
@import 'variables';
2+
3+
.viewPdfWrapper {
4+
display: flex;
5+
gap: 20px;
6+
}
7+
8+
.annotationOptions {
9+
position: sticky;
10+
height: fit-content;
11+
top: 40px;
12+
}
13+
14+
.highlightColors {
15+
min-width: 200px;
16+
display: flex;
17+
justify-content: flex-start;
18+
gap: 10px;
19+
margin-top: 20px;
20+
// background-color: #555;
21+
}
22+
23+
.colorBtn {
24+
width: min-content;
25+
padding: 5px 10px;
26+
font-weight: 700;
27+
color: $text-color;
28+
border-radius: 5px;
29+
}
30+
31+
.blue {
32+
border: 2px solid rgba(0, 225, 255, 0.75);
33+
background-color:rgba(0, 225, 255, 0.25);
34+
}
35+
36+
.red {
37+
border: 2px solid rgba(255, 60, 0, 0.75);
38+
background-color: rgba(255, 60, 0, 0.25);
39+
}
40+
41+
.green {
42+
border: 2px solid rgba(80, 255, 0, 0.75);
43+
background-color:rgba(80, 255, 0, 0.25);
44+
}
45+
46+
.yellow {
47+
border: 2px solid rgba(255, 247, 0, 0.75);
48+
background-color:rgba(255, 247, 0, 0.25);
49+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import React, { useState, useRef } from 'react';
2+
import { Document, Page } from 'react-pdf';
3+
import "react-pdf/dist/esm/Page/TextLayer.css";
4+
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
5+
import { useAppSelector } from 'redux/hooks'
6+
import styles from "./pdfViewer.scss";
7+
8+
interface PDFWithHighlightProps {
9+
file: Blob | File;
10+
}
11+
12+
interface Highlight {
13+
color: string,
14+
text: string,
15+
rect: {
16+
top: number;
17+
left: number;
18+
width: number;
19+
height: number;
20+
}
21+
}
22+
23+
const PDFViewer: React.FC<PDFWithHighlightProps> = ({ file }) => {
24+
const [highlights, setHighlights] = useState<Highlight[]>([]);
25+
const [numPages, setNumPages] = useState(0);
26+
const [annotate, setAnnotate] = useState(false);
27+
const [annotationColor, setAnnotationColor] = useState('rgba(0, 225, 255, 0.25)');
28+
const [annotationBorderColor, setAnnotationBorderColor] = useState('rgba(0, 225, 255, 0.75)');
29+
const containerRef = useRef<HTMLDivElement | null>(null);
30+
const role = useAppSelector((store) => store.roleMode)
31+
32+
const [startPoint, setStartPoint] = useState<{ x: number; y: number } | null>(null);
33+
const [currentRect, setCurrentRect] = useState<Highlight['rect'] | null>(null);
34+
35+
const handleMouseDown = (e: React.MouseEvent) => {
36+
if (!containerRef.current) return;
37+
const containerRect = containerRef.current.getBoundingClientRect();
38+
setStartPoint({
39+
x: e.clientX - containerRect.left,
40+
y: e.clientY - containerRect.top,
41+
});
42+
};
43+
44+
const handleMouseMove = (e: React.MouseEvent) => {
45+
if (!startPoint || !containerRef.current) return;
46+
const containerRect = containerRef.current.getBoundingClientRect();
47+
const x = e.clientX - containerRect.left;
48+
const y = e.clientY - containerRect.top;
49+
50+
const width = Math.abs(x - startPoint.x);
51+
const height = Math.abs(y - startPoint.y);
52+
53+
setCurrentRect({
54+
left: Math.min(x, startPoint.x),
55+
top: Math.min(y, startPoint.y),
56+
width,
57+
height,
58+
});
59+
};
60+
61+
const handleMouseUp = () => {
62+
if (currentRect) {
63+
const text = prompt("Enter annotation text:");
64+
setHighlights((prev) => [
65+
...prev,
66+
{
67+
color: annotationColor,
68+
rect: currentRect,
69+
text: text || "", // Add text to the highlight object
70+
},
71+
]);
72+
}
73+
setStartPoint(null);
74+
setCurrentRect(null);
75+
};
76+
77+
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
78+
setNumPages(numPages)
79+
}
80+
81+
const toggleAnnotate = () => {
82+
setAnnotate(!annotate);
83+
}
84+
85+
const setColors = (e: React.MouseEvent<HTMLButtonElement>) => {
86+
const computedStyle = window.getComputedStyle(e.currentTarget);
87+
const color = computedStyle.backgroundColor;
88+
const borderColor = color.replace("0.25", "0.75");
89+
console.log(borderColor);
90+
91+
setAnnotationColor(color);
92+
93+
// color is in rgba. change the opacity to 0.75
94+
setAnnotationBorderColor(borderColor);
95+
}
96+
97+
return (
98+
<div className={styles.viewPdfWrapper}>
99+
<div
100+
ref={containerRef}
101+
style={{ position: 'relative' }}
102+
onMouseDown={annotate ? handleMouseDown : undefined}
103+
onMouseMove={annotate ? handleMouseMove : undefined}
104+
onMouseUp={annotate ? handleMouseUp : undefined}
105+
>
106+
{/* react-pdf document */}
107+
<Document
108+
file={file}
109+
onLoadSuccess={onDocumentLoadSuccess}
110+
>
111+
{[...Array(numPages)].map((_, index) => {
112+
const pageNumber = index + 1;
113+
return (
114+
<Page
115+
key={pageNumber}
116+
pageNumber={pageNumber}
117+
renderTextLayer={false}
118+
renderAnnotationLayer={false}
119+
scale={1.0}
120+
/>
121+
);
122+
})}
123+
</Document>
124+
{/* Render current rectangle while dragging */}
125+
{annotate && currentRect && (
126+
<div
127+
style={{
128+
position: 'absolute',
129+
left: `${currentRect.left}px`,
130+
top: `${currentRect.top}px`,
131+
width: `${currentRect.width}px`,
132+
height: `${currentRect.height}px`,
133+
backgroundColor: annotationColor,
134+
border: `2px dashed ${annotationBorderColor}`,
135+
pointerEvents: 'none',
136+
}}
137+
/>
138+
)}
139+
{/* Show all highlights */}
140+
{highlights.map((highlight, index) => (
141+
<div
142+
key={index}
143+
style={{
144+
position: 'absolute',
145+
left: `${highlight.rect.left}px`,
146+
top: `${highlight.rect.top}px`,
147+
width: `${highlight.rect.width}px`,
148+
height: `${highlight.rect.height}px`,
149+
backgroundColor: highlight.color,
150+
pointerEvents: 'none',
151+
textAlign: 'center',
152+
padding: '2px',
153+
}}
154+
><span style={{ color: '#000', margin: 'auto', fontWeight: '700' }}>{highlight.text}</span></div>
155+
))}
156+
</div>
157+
158+
{/* annotation options */}
159+
{role.isInstructor() && (<div className={styles.annotationOptions}>
160+
<button
161+
className="btnSecondary"
162+
onClick={toggleAnnotate}
163+
style={{ whiteSpace: 'nowrap' }}
164+
>
165+
{annotate ? 'Set View Mode' : 'Set Annotate Mode'}
166+
</button>
167+
{annotate && (<div className={styles.highlightColors}>
168+
<button className={`${styles.colorBtn} ${styles.blue}`} onClick={setColors}>Blue</button>
169+
<button className={`${styles.colorBtn} ${styles.green}`} onClick={setColors}>Green</button>
170+
<button className={`${styles.colorBtn} ${styles.red}`} onClick={setColors}>Red</button>
171+
<button className={`${styles.colorBtn} ${styles.yellow}`} onClick={setColors}>Yellow</button>
172+
</div>)}
173+
</div>)}
174+
</div>
175+
);
176+
};
177+
178+
export default PDFViewer;

0 commit comments

Comments
 (0)