Skip to content

Commit dc95ea8

Browse files
authored
Merge pull request #4 from richardr1126/single-page-viewer
Single page viewer
2 parents 86c263c + 21e1b8f commit dc95ea8

File tree

12 files changed

+768
-744
lines changed

12 files changed

+768
-744
lines changed

src/app/pdf/[id]/page.tsx

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import dynamic from 'next/dynamic';
44
import { usePDF } from '@/contexts/PDFContext';
55
import { useParams } from 'next/navigation';
66
import Link from 'next/link';
7-
import { useEffect, useState } from 'react';
7+
import { useCallback, useEffect, useState } from 'react';
88
import { PDFSkeleton } from '@/components/PDFSkeleton';
9-
import TTSPlayer from '@/components/TTSPlayer';
109
import { useTTS } from '@/contexts/TTSContext';
1110

1211
// Dynamic import for client-side rendering only
@@ -20,32 +19,32 @@ const PDFViewer = dynamic(
2019

2120
export default function PDFViewerPage() {
2221
const { id } = useParams();
23-
const { getDocument } = usePDF();
24-
const { setText, stop } = useTTS();
25-
const [document, setDocument] = useState<{ name: string; data: Blob } | null>(null);
22+
const { setCurrentDocument, currDocName, clearCurrDoc } = usePDF();
23+
const { stop } = useTTS();
2624
const [error, setError] = useState<string | null>(null);
2725
const [isLoading, setIsLoading] = useState(true);
2826
const [zoomLevel, setZoomLevel] = useState<number>(100);
2927

30-
useEffect(() => {
31-
async function loadDocument() {
32-
try {
33-
const doc = await getDocument(id as string);
34-
if (!doc) {
35-
setError('Document not found');
36-
return;
37-
}
38-
setDocument(doc);
39-
} catch (err) {
40-
console.error('Error loading document:', err);
41-
setError('Failed to load document');
42-
} finally {
43-
setIsLoading(false);
28+
const loadDocument = useCallback(async () => {
29+
if (!isLoading) return; // Prevent calls when not loading new doc
30+
console.log('Loading new document (from page.tsx)');
31+
try {
32+
if (!id) {
33+
setError('Document not found');
34+
return;
4435
}
36+
setCurrentDocument(id as string);
37+
} catch (err) {
38+
console.error('Error loading document:', err);
39+
setError('Failed to load document');
40+
} finally {
41+
setIsLoading(false);
4542
}
43+
}, [isLoading, id, setCurrentDocument]);
4644

45+
useEffect(() => {
4746
loadDocument();
48-
}, [id, getDocument]);
47+
}, [loadDocument]);
4948

5049
const handleZoomIn = () => setZoomLevel(prev => Math.min(prev + 10, 200));
5150
const handleZoomOut = () => setZoomLevel(prev => Math.max(prev - 10, 50));
@@ -56,10 +55,7 @@ export default function PDFViewerPage() {
5655
<p className="text-red-500 mb-4">{error}</p>
5756
<Link
5857
href="/"
59-
onClick={() => {
60-
setText('');
61-
stop();
62-
}}
58+
onClick={() => {clearCurrDoc(); stop();}}
6359
className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition-colors"
6460
>
6561
<svg className="w-4 h-4 mr-2 text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -73,16 +69,12 @@ export default function PDFViewerPage() {
7369

7470
return (
7571
<>
76-
<TTSPlayer />
7772
<div className="p-2 pb-2 border-b border-offbase">
7873
<div className="flex flex-wrap items-center justify-between">
7974
<div className="flex items-center gap-4">
8075
<Link
8176
href="/"
82-
onClick={() => {
83-
setText('');
84-
stop();
85-
}}
77+
onClick={() => {clearCurrDoc(); stop();}}
8678
className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition-colors"
8779
>
8880
<svg className="w-4 h-4 mr-2 text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -109,7 +101,7 @@ export default function PDFViewerPage() {
109101
</div>
110102
</div>
111103
<h1 className="mr-2 text-md font-semibold text-foreground">
112-
{isLoading ? 'Loading...' : document?.name}
104+
{isLoading ? 'Loading...' : currDocName}
113105
</h1>
114106
</div>
115107
</div>
@@ -118,7 +110,7 @@ export default function PDFViewerPage() {
118110
<PDFSkeleton />
119111
</div>
120112
) : (
121-
<PDFViewer pdfData={document?.data} zoomLevel={zoomLevel} />
113+
<PDFViewer zoomLevel={zoomLevel} />
122114
)}
123115
</>
124116
);

src/components/PDFViewer.tsx

Lines changed: 53 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,41 @@
11
'use client';
22

3-
import { RefObject } from 'react';
3+
import { RefObject, useCallback } from 'react';
44
import { Document, Page } from 'react-pdf';
55
import 'react-pdf/dist/Page/AnnotationLayer.css';
66
import 'react-pdf/dist/Page/TextLayer.css';
77
import { useState, useEffect, useRef } from 'react';
88
import { PDFSkeleton } from './PDFSkeleton';
99
import { useTTS } from '@/contexts/TTSContext';
1010
import { usePDF } from '@/contexts/PDFContext';
11+
import TTSPlayer from '@/components/player/TTSPlayer';
1112

1213
interface PDFViewerProps {
13-
pdfData: Blob | undefined;
1414
zoomLevel: number;
1515
}
1616

17-
export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) {
18-
const [numPages, setNumPages] = useState<number>();
17+
export function PDFViewer({ zoomLevel }: PDFViewerProps) {
1918
const [containerWidth, setContainerWidth] = useState<number>(0);
20-
const { setText, currentSentence, stopAndPlayFromIndex, isProcessing } = useTTS();
21-
const [pdfText, setPdfText] = useState('');
22-
const [pdfDataUrl, setPdfDataUrl] = useState<string>();
23-
const [loadingError, setLoadingError] = useState<string>();
2419
const containerRef = useRef<HTMLDivElement>(null);
25-
const { extractTextFromPDF, highlightPattern, clearHighlights, handleTextClick } = usePDF();
20+
21+
// TTS context
22+
const {
23+
currentSentence,
24+
stopAndPlayFromIndex,
25+
isProcessing
26+
} = useTTS();
27+
28+
// PDF context
29+
const {
30+
highlightPattern,
31+
clearHighlights,
32+
handleTextClick,
33+
onDocumentLoadSuccess,
34+
currDocURL,
35+
currDocPages,
36+
currDocText,
37+
currDocPage,
38+
} = usePDF();
2639

2740
// Add static styles once during component initialization
2841
const styleElement = document.createElement('style');
@@ -44,58 +57,6 @@ export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) {
4457
};
4558
}, [styleElement]);
4659

47-
useEffect(() => {
48-
/*
49-
* Converts PDF blob to a data URL for display.
50-
* Cleans up by clearing the data URL when component unmounts.
51-
*
52-
* Dependencies:
53-
* - pdfData: Re-run when the PDF blob changes to convert it to a new data URL
54-
*/
55-
if (!pdfData) return;
56-
57-
const reader = new FileReader();
58-
reader.onload = () => {
59-
setPdfDataUrl(reader.result as string);
60-
};
61-
reader.onerror = () => {
62-
console.error('Error reading file:', reader.error);
63-
setLoadingError('Failed to load PDF');
64-
};
65-
reader.readAsDataURL(pdfData);
66-
67-
return () => {
68-
setPdfDataUrl(undefined);
69-
};
70-
}, [pdfData]);
71-
72-
useEffect(() => {
73-
/*
74-
* Extracts text content from the PDF once it's loaded.
75-
* Sets the extracted text for both display and text-to-speech.
76-
*
77-
* Dependencies:
78-
* - pdfDataUrl: Re-run when the data URL is ready
79-
* - extractTextFromPDF: Function from context that could change
80-
* - setText: Function from context that could change
81-
* - pdfData: Source PDF blob that's being processed
82-
*/
83-
if (!pdfDataUrl || !pdfData) return;
84-
85-
const loadPdfText = async () => {
86-
try {
87-
const text = await extractTextFromPDF(pdfData);
88-
setPdfText(text);
89-
setText(text);
90-
} catch (error) {
91-
console.error('Error loading PDF text:', error);
92-
setLoadingError('Failed to extract PDF text');
93-
}
94-
};
95-
96-
loadPdfText();
97-
}, [pdfDataUrl, extractTextFromPDF, setText, pdfData]);
98-
9960
useEffect(() => {
10061
/*
10162
* Sets up click event listeners for text selection in the PDF.
@@ -108,10 +69,11 @@ export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) {
10869
*/
10970
const container = containerRef.current;
11071
if (!container) return;
72+
if (!currDocText) return;
11173

11274
const handleClick = (event: MouseEvent) => handleTextClick(
11375
event,
114-
pdfText,
76+
currDocText,
11577
containerRef as RefObject<HTMLDivElement>,
11678
stopAndPlayFromIndex,
11779
isProcessing
@@ -120,7 +82,7 @@ export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) {
12082
return () => {
12183
container.removeEventListener('click', handleClick);
12284
};
123-
}, [pdfText, handleTextClick, stopAndPlayFromIndex, isProcessing]);
85+
}, [currDocText, handleTextClick, stopAndPlayFromIndex, isProcessing]);
12486

12587
useEffect(() => {
12688
/*
@@ -133,25 +95,27 @@ export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) {
13395
* - highlightPattern: Function from context that could change
13496
* - clearHighlights: Function from context that could change
13597
*/
98+
if (!currDocText) return;
99+
136100
const highlightTimeout = setTimeout(() => {
137101
if (containerRef.current) {
138-
highlightPattern(pdfText, currentSentence || '', containerRef as RefObject<HTMLDivElement>);
102+
highlightPattern(currDocText, currentSentence || '', containerRef as RefObject<HTMLDivElement>);
139103
}
140-
}, 100);
104+
}, 200);
141105

142106
return () => {
143107
clearTimeout(highlightTimeout);
144108
clearHighlights();
145109
};
146-
}, [pdfText, currentSentence, highlightPattern, clearHighlights]);
110+
}, [currDocText, currentSentence, highlightPattern, clearHighlights]);
147111

148112
// Add scale calculation function
149-
const calculateScale = (pageWidth: number = 595) => { // 595 is default PDF width in points
113+
const calculateScale = useCallback((pageWidth = 595): number => {
150114
const margin = 24; // 24px padding on each side
151115
const targetWidth = containerWidth - margin;
152116
const baseScale = targetWidth / pageWidth;
153117
return baseScale * (zoomLevel / 100);
154-
};
118+
}, [containerWidth, zoomLevel]);
155119

156120
// Add resize observer effect
157121
useEffect(() => {
@@ -168,48 +132,34 @@ export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) {
168132
return () => observer.disconnect();
169133
}, []);
170134

171-
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
172-
setNumPages(numPages);
173-
}
174-
175135
return (
176-
<div
177-
ref={containerRef}
178-
className="flex flex-col items-center overflow-auto max-h-[calc(100vh-100px)] w-full px-6"
179-
style={{ WebkitTapHighlightColor: 'transparent' }}
180-
>
181-
{loadingError ? (
182-
<div className="text-red-500 mb-4">{loadingError}</div>
183-
) : null}
136+
<div ref={containerRef} className="flex flex-col items-center overflow-auto max-h-[calc(100vh-100px)] w-full px-6">
184137
<Document
185138
loading={<PDFSkeleton />}
186139
noData={<PDFSkeleton />}
187-
file={pdfDataUrl}
188-
onLoadSuccess={onDocumentLoadSuccess}
140+
file={currDocURL}
141+
onLoadSuccess={(pdf) => {
142+
onDocumentLoadSuccess(pdf);
143+
//handlePageChange(1); // Load first page text
144+
}}
189145
className="flex flex-col items-center m-0"
190146
>
191-
{Array.from(
192-
new Array(numPages),
193-
(el, index) => (
194-
<div key={`page_${index + 1}`}>
195-
<div className="bg-offbase my-4 px-2 py-0.5 rounded-full w-fit">
196-
<p className="text-xs">
197-
{index + 1} / {numPages}
198-
</p>
199-
</div>
200-
<div className="flex justify-center">
201-
<Page
202-
pageNumber={index + 1}
203-
renderAnnotationLayer={true}
204-
renderTextLayer={true}
205-
className="shadow-lg"
206-
scale={calculateScale()}
207-
/>
208-
</div>
209-
</div>
210-
),
211-
)}
147+
<div>
148+
<div className="flex justify-center">
149+
<Page
150+
pageNumber={currDocPage}
151+
renderAnnotationLayer={true}
152+
renderTextLayer={true}
153+
className="shadow-lg"
154+
scale={calculateScale()}
155+
/>
156+
</div>
157+
</div>
212158
</Document>
159+
<TTSPlayer
160+
currentPage={currDocPage}
161+
numPages={currDocPages}
162+
/>
213163
</div>
214164
);
215165
}

src/components/Spinner.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Loading spinner component
2+
export function LoadingSpinner() {
3+
return (
4+
<div className="absolute inset-0 flex items-center justify-center">
5+
<div className="animate-spin h-4 w-4 border-2 border-foreground border-t-transparent rounded-full" />
6+
</div>
7+
);
8+
}

0 commit comments

Comments
 (0)