Skip to content

Commit 5c65bcb

Browse files
committed
feat: add PDF export with annotations and clean up unused props
1 parent 973032c commit 5c65bcb

10 files changed

Lines changed: 593 additions & 47 deletions

package-lock.json

Lines changed: 235 additions & 33 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"dependencies": {
1919
"@supabase/supabase-js": "^2.74.0",
2020
"@tailwindcss/postcss": "^4.1.16",
21+
"jspdf": "^3.0.4",
2122
"lucide-react": "^0.552.0",
2223
"next": "15.5.3",
2324
"pdfjs-dist": "^5.4.296",

src/components/documents/AnnotationSidebar.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ describe('AnnotationSidebar', () => {
9090
];
9191

9292
const defaultProps = {
93-
documentId: 'doc-1',
9493
annotations: [],
9594
selectedAnnotationId: null,
9695
canCreate: true,

src/components/documents/AnnotationSidebar.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { AnnotationWithUser, AnnotationThread } from '@/lib/types/annotation';
55
import CommentThread from './CommentThread';
66

77
interface AnnotationSidebarProps {
8-
documentId: string;
98
annotations: AnnotationWithUser[];
109
selectedAnnotationId: string | null;
1110
canCreate: boolean;
@@ -16,7 +15,6 @@ interface AnnotationSidebarProps {
1615
}
1716

1817
export default function AnnotationSidebar({
19-
documentId,
2018
annotations,
2119
selectedAnnotationId,
2220
canCreate,
@@ -85,7 +83,6 @@ export default function AnnotationSidebar({
8583
key={thread.highlight.id}
8684
highlight={thread.highlight}
8785
comments={thread.comments}
88-
documentId={documentId}
8986
isSelected={selectedAnnotationId === thread.highlight.id}
9087
canCreate={canCreate}
9188
canDelete={canDelete}

src/components/documents/CommentThread.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ describe('CommentThread', () => {
4646
const defaultProps = {
4747
highlight: mockHighlight,
4848
comments: [],
49-
documentId: 'doc-1',
5049
isSelected: false,
5150
canCreate: true,
5251
canDelete: jest.fn(() => true),

src/components/documents/CommentThread.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { AnnotationWithUser } from '@/lib/types/annotation';
66
interface CommentThreadProps {
77
highlight: AnnotationWithUser;
88
comments: AnnotationWithUser[];
9-
documentId: string;
109
isSelected: boolean;
1110
canCreate: boolean;
1211
canDelete: (annotation: AnnotationWithUser) => boolean;
@@ -18,7 +17,6 @@ interface CommentThreadProps {
1817
export default function CommentThread({
1918
highlight,
2019
comments,
21-
documentId,
2220
isSelected,
2321
canCreate,
2422
canDelete,

src/components/documents/DocumentViewer.test.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@ import { supabase } from '@/lib/supabaseClient';
88
import type { Document } from '@/lib/types/document';
99
import type { AnnotationWithUser } from '@/lib/types/annotation';
1010

11-
type Selection = {
12-
removeAllRanges: () => void;
13-
rangeCount: number;
14-
toString: () => string;
15-
getRangeAt: (index: number) => Range;
16-
};
1711

1812
jest.mock('@/services/documentService');
1913
jest.mock('@/services/annotationService');

src/components/documents/DocumentViewer.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { supabase } from '@/lib/supabaseClient';
1212
import HighlightedText from './HighlightedText';
1313
import AnnotationSidebar from './AnnotationSidebar';
1414
import AnnotationToolbar from './AnnotationToolbar';
15+
import { generateAnnotatedPDF } from '@/services/pdfExportService';
1516
interface DocumentViewerProps {
1617
documentId: string;
1718
}
@@ -229,6 +230,17 @@ export default function DocumentViewer({ documentId }: DocumentViewerProps) {
229230
}
230231
};
231232

233+
const handleExportPDF = async () => {
234+
if (!document) {
235+
return;
236+
}
237+
238+
try {
239+
await generateAnnotatedPDF(document, annotations);
240+
} catch (err) {
241+
console.error('Failed to export PDF:', err);
242+
}
243+
};
232244

233245
if (loading) {
234246
return (
@@ -303,6 +315,16 @@ export default function DocumentViewer({ documentId }: DocumentViewerProps) {
303315
<span>{formatDate(document.created_at)}</span>
304316
</div>
305317
</div>
318+
<button
319+
onClick={handleExportPDF}
320+
disabled={!document.raw_text}
321+
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center space-x-2 transition-colors"
322+
>
323+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
324+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
325+
</svg>
326+
<span>Export PDF</span>
327+
</button>
306328
</div>
307329
</div>
308330
</div>
@@ -382,7 +404,6 @@ export default function DocumentViewer({ documentId }: DocumentViewerProps) {
382404

383405
{permissions.canView && (
384406
<AnnotationSidebar
385-
documentId={documentId}
386407
annotations={annotations}
387408
selectedAnnotationId={selectedAnnotationId}
388409
canCreate={permissions.canCreate}

src/lib/utils/pdfHelpers.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import jsPDF from 'jspdf';
2+
3+
export function wrapText(pdf: jsPDF, text: string, maxWidth: number): string[] {
4+
const lines: string[] = [];
5+
const words = text.split(' ');
6+
let selectedLine = '';
7+
8+
for (const word of words) {
9+
// try adding this word to the current line
10+
let proposedLine;
11+
if (selectedLine) {
12+
proposedLine = `${selectedLine} ${word}`;
13+
} else {
14+
proposedLine = word;
15+
}
16+
17+
const textWidth = pdf.getTextWidth(proposedLine);
18+
19+
// if it doesn't fit start new line
20+
if (textWidth > maxWidth && selectedLine) {
21+
lines.push(selectedLine);
22+
selectedLine = word;
23+
} else {
24+
selectedLine = proposedLine;
25+
}
26+
}
27+
28+
if (selectedLine) {
29+
lines.push(selectedLine);
30+
}
31+
32+
return lines;
33+
}
34+
35+
export function addPageIfNeeded(
36+
pdf: jsPDF,
37+
currentY: number,
38+
requiredSpace: number,
39+
margin: number,
40+
pageHeight: number
41+
): number {
42+
if (currentY + requiredSpace > pageHeight - margin) {
43+
pdf.addPage();
44+
return margin;
45+
}
46+
return currentY;
47+
}
48+
49+
export function cleanFilename(title: string | null): string {
50+
if (!title) {
51+
return 'Document_annotated.pdf';
52+
}
53+
54+
const cleaned = title
55+
.replace(/[^a-zA-Z0-9_\-\s]/g, '')
56+
.replace(/\s+/g, '_')
57+
.substring(0, 100);
58+
59+
if (cleaned) {
60+
return `${cleaned}_annotated.pdf`;
61+
} else {
62+
return 'Document_annotated.pdf';
63+
}
64+
}

0 commit comments

Comments
 (0)