hery
This commit is contained in:
parent
2130d23ff4
commit
a533e1ac32
@ -3,6 +3,10 @@ import {
|
||||
mdiCheckCircleOutline,
|
||||
mdiCommentTextOutline,
|
||||
mdiFilePdfBox,
|
||||
mdiFileWordOutline,
|
||||
mdiMagnifyMinusOutline,
|
||||
mdiMagnifyPlusOutline,
|
||||
mdiRefresh,
|
||||
mdiShieldLockOutline,
|
||||
} from '@mdi/js';
|
||||
import axios from 'axios';
|
||||
@ -22,6 +26,9 @@ import { useRouter } from 'next/router';
|
||||
const VIEWER_SESSION_STORAGE_KEY = 'viewerAccessToken';
|
||||
const PDFJS_SCRIPT_URL = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js';
|
||||
const PDFJS_WORKER_URL = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';
|
||||
const MIN_ZOOM_LEVEL = 0.5;
|
||||
const MAX_ZOOM_LEVEL = 2;
|
||||
const ZOOM_STEP = 0.25;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@ -93,6 +100,178 @@ function getStatusStyles(status: string) {
|
||||
return 'bg-amber-100 text-amber-700 border-amber-200';
|
||||
}
|
||||
}
|
||||
function clampZoomLevel(value: number) {
|
||||
return Math.min(MAX_ZOOM_LEVEL, Math.max(MIN_ZOOM_LEVEL, value));
|
||||
}
|
||||
|
||||
function sanitizeFileName(value: string) {
|
||||
const normalizedValue = value
|
||||
.replace(/\.pdf$/i, '')
|
||||
.replace(/[^a-zA-Z0-9-_]+/g, '-')
|
||||
.replace(/-{2,}/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
return normalizedValue || 'halaman-pdf';
|
||||
}
|
||||
|
||||
function escapeHtml(value: string) {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function extractPdfTextLines(items: Array<{ str?: string; transform?: number[] }>) {
|
||||
const lines: Array<{ y: number; text: string }> = [];
|
||||
|
||||
items.forEach((item) => {
|
||||
const textValue = String(item?.str || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
if (!textValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yPosition = Array.isArray(item?.transform) ? Number(item.transform[5]) : Number.NaN;
|
||||
const previousLine = lines[lines.length - 1];
|
||||
|
||||
if (
|
||||
!previousLine ||
|
||||
Number.isNaN(yPosition) ||
|
||||
Number.isNaN(previousLine.y) ||
|
||||
Math.abs(previousLine.y - yPosition) > 4
|
||||
) {
|
||||
lines.push({ y: yPosition, text: textValue });
|
||||
return;
|
||||
}
|
||||
|
||||
previousLine.text = `${previousLine.text} ${textValue}`.replace(/\s+/g, ' ').trim();
|
||||
});
|
||||
|
||||
return lines.map((line) => line.text).filter(Boolean);
|
||||
}
|
||||
|
||||
function downloadBlob(blob: Blob, fileName: string) {
|
||||
const downloadUrl = window.URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
|
||||
anchor.href = downloadUrl;
|
||||
anchor.download = fileName;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
|
||||
window.setTimeout(() => {
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function buildWordDocumentHtml({
|
||||
title,
|
||||
pageNumber,
|
||||
viewerName,
|
||||
ruleName,
|
||||
exportedAt,
|
||||
previewImageDataUrl,
|
||||
extractedLines,
|
||||
}: {
|
||||
title: string;
|
||||
pageNumber: number;
|
||||
viewerName: string;
|
||||
ruleName: string;
|
||||
exportedAt: string;
|
||||
previewImageDataUrl: string;
|
||||
extractedLines: string[];
|
||||
}) {
|
||||
const extractedTextHtml = extractedLines.length
|
||||
? extractedLines
|
||||
.map((line) => `<p>${escapeHtml(line)}</p>`)
|
||||
.join('')
|
||||
: '<p><em>PDF ini kemungkinan berupa scan atau gambar, sehingga file Word berisi snapshot halaman untuk ditinjau.</em></p>';
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${escapeHtml(title)}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: #0f172a;
|
||||
line-height: 1.6;
|
||||
margin: 32px;
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
margin: 24px 0;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.preview img {
|
||||
width: 100%;
|
||||
max-width: 760px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-top: 28px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${escapeHtml(title)}</h1>
|
||||
<p class="meta"><strong>Viewer:</strong> ${escapeHtml(viewerName)}</p>
|
||||
<p class="meta"><strong>Halaman yang diizinkan:</strong> ${pageNumber}</p>
|
||||
<p class="meta"><strong>Aturan akses:</strong> ${escapeHtml(ruleName)}</p>
|
||||
<p class="meta"><strong>Diekspor:</strong> ${escapeHtml(exportedAt)}</p>
|
||||
|
||||
<div class="preview">
|
||||
<h2 class="section-title">Snapshot halaman</h2>
|
||||
<img src="${previewImageDataUrl}" alt="Preview halaman PDF" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="section-title">Isi teks halaman</h2>
|
||||
${extractedTextHtml}
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
async function renderPdfPageToImageDataUrl(pdfPage: any) {
|
||||
if (typeof document === 'undefined') {
|
||||
throw new Error('Export Word hanya tersedia di browser.');
|
||||
}
|
||||
|
||||
const exportViewport = pdfPage.getViewport({ scale: 1.4 });
|
||||
const exportCanvas = document.createElement('canvas');
|
||||
const exportContext = exportCanvas.getContext('2d');
|
||||
|
||||
if (!exportContext) {
|
||||
throw new Error('Canvas export Word tidak tersedia.');
|
||||
}
|
||||
|
||||
exportCanvas.width = exportViewport.width;
|
||||
exportCanvas.height = exportViewport.height;
|
||||
exportContext.fillStyle = '#ffffff';
|
||||
exportContext.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
|
||||
|
||||
await pdfPage.render({
|
||||
canvasContext: exportContext,
|
||||
viewport: exportViewport,
|
||||
}).promise;
|
||||
|
||||
return exportCanvas.toDataURL('image/png');
|
||||
}
|
||||
|
||||
function loadPdfJsLibrary() {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
@ -127,6 +306,11 @@ function loadPdfJsLibrary() {
|
||||
export default function PdfViewerPage() {
|
||||
const router = useRouter();
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const pdfContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const pdfDocumentRef = useRef<any | null>(null);
|
||||
const pdfRenderTaskRef = useRef<any | null>(null);
|
||||
const pdfRenderNonceRef = useRef(0);
|
||||
const cachedDocumentKeyRef = useRef('');
|
||||
const [viewerToken, setViewerToken] = useState('');
|
||||
const [session, setSession] = useState<ViewerSession | null>(null);
|
||||
const [feedbackItems, setFeedbackItems] = useState<ViewerFeedbackItem[]>([]);
|
||||
@ -134,8 +318,11 @@ export default function PdfViewerPage() {
|
||||
const [isPageLoading, setIsPageLoading] = useState(true);
|
||||
const [isPdfLoading, setIsPdfLoading] = useState(false);
|
||||
const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false);
|
||||
const [isExportingWord, setIsExportingWord] = useState(false);
|
||||
const [pdfZoom, setPdfZoom] = useState(1);
|
||||
const [pageError, setPageError] = useState('');
|
||||
const [pdfError, setPdfError] = useState('');
|
||||
const [wordError, setWordError] = useState('');
|
||||
const [feedbackError, setFeedbackError] = useState('');
|
||||
const [feedbackSuccess, setFeedbackSuccess] = useState('');
|
||||
|
||||
@ -157,22 +344,53 @@ export default function PdfViewerPage() {
|
||||
setFeedbackItems(response.data || []);
|
||||
}, []);
|
||||
|
||||
const renderAssignedPage = useCallback(async (token: string, pageNumber: number) => {
|
||||
if (!pageNumber) {
|
||||
return;
|
||||
const resetPdfCache = useCallback(() => {
|
||||
if (pdfRenderTaskRef.current?.cancel) {
|
||||
try {
|
||||
pdfRenderTaskRef.current.cancel();
|
||||
} catch (error) {
|
||||
console.error('Failed to cancel PDF render task:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setPdfError('');
|
||||
setIsPdfLoading(true);
|
||||
pdfRenderTaskRef.current = null;
|
||||
pdfRenderNonceRef.current += 1;
|
||||
setIsPdfLoading(false);
|
||||
|
||||
try {
|
||||
const pdfjsLib = await loadPdfJsLibrary();
|
||||
if (pdfDocumentRef.current?.destroy) {
|
||||
pdfDocumentRef.current.destroy().catch((error: unknown) => {
|
||||
console.error('Failed to release cached PDF document:', error);
|
||||
});
|
||||
}
|
||||
|
||||
if (!pdfjsLib) {
|
||||
throw new Error('Viewer PDF tidak tersedia di browser ini.');
|
||||
pdfDocumentRef.current = null;
|
||||
cachedDocumentKeyRef.current = '';
|
||||
|
||||
if (canvasRef.current) {
|
||||
const context = canvasRef.current.getContext('2d');
|
||||
|
||||
if (context) {
|
||||
context.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
||||
}
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_URL;
|
||||
canvasRef.current.width = 0;
|
||||
canvasRef.current.height = 0;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getPdfDocument = useCallback(async (token: string, assignmentId: string) => {
|
||||
const pdfjsLib = await loadPdfJsLibrary();
|
||||
|
||||
if (!pdfjsLib) {
|
||||
throw new Error('Viewer PDF tidak tersedia di browser ini.');
|
||||
}
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_URL;
|
||||
|
||||
const cacheKey = `${assignmentId}:${token}`;
|
||||
|
||||
if (!pdfDocumentRef.current || cachedDocumentKeyRef.current !== cacheKey) {
|
||||
resetPdfCache();
|
||||
|
||||
const response = await axios.get('/portal-access/document', {
|
||||
headers: {
|
||||
@ -181,35 +399,86 @@ export default function PdfViewerPage() {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
|
||||
const pdfDocument = await pdfjsLib.getDocument({ data: response.data }).promise;
|
||||
cachedDocumentKeyRef.current = cacheKey;
|
||||
pdfDocumentRef.current = await pdfjsLib.getDocument({
|
||||
data: new Uint8Array(response.data),
|
||||
}).promise;
|
||||
}
|
||||
|
||||
return pdfDocumentRef.current;
|
||||
}, [resetPdfCache]);
|
||||
|
||||
const renderAssignedPage = useCallback(async (
|
||||
token: string,
|
||||
assignmentId: string,
|
||||
pageNumber: number,
|
||||
zoomLevel: number,
|
||||
) => {
|
||||
if (!assignmentId || !pageNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
let renderNonce = 0;
|
||||
setPdfError('');
|
||||
setIsPdfLoading(true);
|
||||
|
||||
try {
|
||||
const pdfDocument = await getPdfDocument(token, assignmentId);
|
||||
const pdfPage = await pdfDocument.getPage(pageNumber);
|
||||
const canvas = canvasRef.current;
|
||||
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Canvas viewer tidak tersedia.');
|
||||
}
|
||||
|
||||
const parentWidth = canvas.parentElement?.clientWidth || 900;
|
||||
renderNonce = pdfRenderNonceRef.current + 1;
|
||||
pdfRenderNonceRef.current = renderNonce;
|
||||
|
||||
if (pdfRenderTaskRef.current?.cancel) {
|
||||
try {
|
||||
pdfRenderTaskRef.current.cancel();
|
||||
} catch (error) {
|
||||
console.error('Failed to cancel previous PDF render:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const containerWidth =
|
||||
pdfContainerRef.current?.clientWidth || canvas.parentElement?.clientWidth || 900;
|
||||
const initialViewport = pdfPage.getViewport({ scale: 1 });
|
||||
const scale = Math.min(Math.max(parentWidth / initialViewport.width, 1), 2.2);
|
||||
const viewport = pdfPage.getViewport({ scale });
|
||||
const fitScale = Math.max(containerWidth / initialViewport.width, 0.35);
|
||||
const viewport = pdfPage.getViewport({
|
||||
scale: Math.max(fitScale * clampZoomLevel(zoomLevel), 0.35),
|
||||
});
|
||||
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
canvas.style.width = '100%';
|
||||
canvas.style.height = 'auto';
|
||||
canvas.style.width = `${viewport.width}px`;
|
||||
canvas.style.height = `${viewport.height}px`;
|
||||
|
||||
await pdfPage.render({
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const renderTask = pdfPage.render({
|
||||
canvasContext: context,
|
||||
viewport,
|
||||
}).promise;
|
||||
} catch (error) {
|
||||
});
|
||||
|
||||
pdfRenderTaskRef.current = renderTask;
|
||||
await renderTask.promise;
|
||||
|
||||
if (pdfRenderTaskRef.current === renderTask) {
|
||||
pdfRenderTaskRef.current = null;
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error?.name === 'RenderingCancelledException') {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('PDF render failed:', error);
|
||||
setPdfError(
|
||||
getErrorMessage(
|
||||
@ -218,9 +487,11 @@ export default function PdfViewerPage() {
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setIsPdfLoading(false);
|
||||
if (renderNonce === pdfRenderNonceRef.current) {
|
||||
setIsPdfLoading(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}, [getPdfDocument]);
|
||||
|
||||
const loadViewerSession = useCallback(async (token: string) => {
|
||||
setIsPageLoading(true);
|
||||
@ -235,7 +506,6 @@ export default function PdfViewerPage() {
|
||||
|
||||
setSession(response.data);
|
||||
await loadFeedbackHistory(token);
|
||||
await renderAssignedPage(token, response.data?.assignment?.pageNumber);
|
||||
} catch (error) {
|
||||
const message = getErrorMessage(
|
||||
error,
|
||||
@ -246,7 +516,7 @@ export default function PdfViewerPage() {
|
||||
} finally {
|
||||
setIsPageLoading(false);
|
||||
}
|
||||
}, [loadFeedbackHistory, renderAssignedPage]);
|
||||
}, [loadFeedbackHistory]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) {
|
||||
@ -264,6 +534,108 @@ export default function PdfViewerPage() {
|
||||
loadViewerSession(savedViewerToken);
|
||||
}, [loadViewerSession, router]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
resetPdfCache();
|
||||
};
|
||||
}, [resetPdfCache]);
|
||||
|
||||
useEffect(() => {
|
||||
setPdfZoom(1);
|
||||
setPdfError('');
|
||||
setWordError('');
|
||||
resetPdfCache();
|
||||
}, [resetPdfCache, session?.assignment?.id, viewerToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isPageLoading ||
|
||||
!viewerToken ||
|
||||
!session?.assignment?.id ||
|
||||
!session.assignment.pageNumber
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderAssignedPage(
|
||||
viewerToken,
|
||||
session.assignment.id,
|
||||
session.assignment.pageNumber,
|
||||
pdfZoom,
|
||||
);
|
||||
}, [
|
||||
isPageLoading,
|
||||
pdfZoom,
|
||||
renderAssignedPage,
|
||||
session?.assignment?.id,
|
||||
session?.assignment?.pageNumber,
|
||||
viewerToken,
|
||||
]);
|
||||
|
||||
const handleZoomOut = () => {
|
||||
setPdfZoom((currentZoom) => clampZoomLevel(currentZoom - ZOOM_STEP));
|
||||
setWordError('');
|
||||
};
|
||||
|
||||
const handleZoomIn = () => {
|
||||
setPdfZoom((currentZoom) => clampZoomLevel(currentZoom + ZOOM_STEP));
|
||||
setWordError('');
|
||||
};
|
||||
|
||||
const handleResetZoom = () => {
|
||||
setPdfZoom(1);
|
||||
setWordError('');
|
||||
};
|
||||
|
||||
const handleExportWord = async () => {
|
||||
if (!viewerToken || !session?.assignment?.id || !session.assignment.pageNumber) {
|
||||
setWordError('Halaman PDF belum siap untuk diekspor ke Word.');
|
||||
return;
|
||||
}
|
||||
|
||||
setWordError('');
|
||||
|
||||
try {
|
||||
setIsExportingWord(true);
|
||||
const pdfDocument = await getPdfDocument(viewerToken, session.assignment.id);
|
||||
const pdfPage = await pdfDocument.getPage(session.assignment.pageNumber);
|
||||
const textContent = await pdfPage.getTextContent({ normalizeWhitespace: true });
|
||||
const previewImageDataUrl = await renderPdfPageToImageDataUrl(pdfPage);
|
||||
const extractedLines = extractPdfTextLines(textContent.items || []);
|
||||
const exportedAt = new Intl.DateTimeFormat('id-ID', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
}).format(new Date());
|
||||
const wordHtml = buildWordDocumentHtml({
|
||||
title: assignmentTitle,
|
||||
pageNumber: session.assignment.pageNumber,
|
||||
viewerName: session.viewer.name,
|
||||
ruleName: session.assignment.ruleName || 'Assignment aktif',
|
||||
exportedAt,
|
||||
previewImageDataUrl,
|
||||
extractedLines,
|
||||
});
|
||||
const fileName = `${sanitizeFileName(assignmentTitle)}-halaman-${session.assignment.pageNumber}.doc`;
|
||||
|
||||
downloadBlob(
|
||||
new Blob(['\ufeff', wordHtml], {
|
||||
type: 'application/msword;charset=utf-8',
|
||||
}),
|
||||
fileName,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Word export failed:', error);
|
||||
setWordError(
|
||||
getErrorMessage(
|
||||
error,
|
||||
'File Word belum dapat dibuat. Coba lagi beberapa saat lagi.',
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setIsExportingWord(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExitViewer = async () => {
|
||||
sessionStorage.removeItem(VIEWER_SESSION_STORAGE_KEY);
|
||||
await router.push('/');
|
||||
@ -307,6 +679,16 @@ export default function PdfViewerPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const zoomPercentage = Math.round(pdfZoom * 100);
|
||||
const isZoomOutDisabled = isPdfLoading || pdfZoom <= MIN_ZOOM_LEVEL;
|
||||
const isZoomInDisabled = isPdfLoading || pdfZoom >= MAX_ZOOM_LEVEL;
|
||||
const isWordExportDisabled =
|
||||
isPdfLoading ||
|
||||
isExportingWord ||
|
||||
!viewerToken ||
|
||||
!session?.assignment?.id ||
|
||||
!session.assignment.pageNumber;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@ -438,16 +820,66 @@ export default function PdfViewerPage() {
|
||||
<div className='grid gap-6 xl:grid-cols-[1.15fr,0.85fr]'>
|
||||
<CardBox>
|
||||
<div className='space-y-5'>
|
||||
<div className='flex flex-wrap items-center justify-between gap-3'>
|
||||
<div className='flex flex-wrap items-start justify-between gap-3'>
|
||||
<div>
|
||||
<p className='text-xs font-semibold uppercase tracking-[0.24em] text-sky-700'>Halaman PDF</p>
|
||||
<h3 className='mt-1 text-2xl font-bold text-slate-950'>Preview halaman yang diizinkan</h3>
|
||||
<p className='mt-2 text-sm leading-6 text-slate-500'>
|
||||
Toolbar ini hanya berlaku untuk halaman {session.assignment.pageNumber}{' '}
|
||||
yang diizinkan admin untuk akun Anda.
|
||||
</p>
|
||||
</div>
|
||||
<div className='rounded-full bg-slate-100 px-4 py-2 text-sm font-semibold text-slate-700'>
|
||||
Halaman {session.assignment.pageNumber}
|
||||
<div className='flex flex-wrap items-center justify-end gap-2'>
|
||||
<div className='rounded-full bg-slate-100 px-4 py-2 text-sm font-semibold text-slate-700'>
|
||||
Halaman {session.assignment.pageNumber}
|
||||
</div>
|
||||
<div className='rounded-full border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700'>
|
||||
Zoom {zoomPercentage}%
|
||||
</div>
|
||||
<BaseButton
|
||||
small
|
||||
color='light'
|
||||
outline
|
||||
icon={mdiMagnifyMinusOutline}
|
||||
label='Zoom Out'
|
||||
onClick={handleZoomOut}
|
||||
disabled={isZoomOutDisabled}
|
||||
/>
|
||||
<BaseButton
|
||||
small
|
||||
color='light'
|
||||
outline
|
||||
icon={mdiRefresh}
|
||||
label='Reset 100%'
|
||||
onClick={handleResetZoom}
|
||||
disabled={isPdfLoading}
|
||||
/>
|
||||
<BaseButton
|
||||
small
|
||||
color='light'
|
||||
outline
|
||||
icon={mdiMagnifyPlusOutline}
|
||||
label='Zoom In'
|
||||
onClick={handleZoomIn}
|
||||
disabled={isZoomInDisabled}
|
||||
/>
|
||||
<BaseButton
|
||||
small
|
||||
color='info'
|
||||
icon={mdiFileWordOutline}
|
||||
label={isExportingWord ? 'Menyiapkan Word...' : 'Convert ke Word'}
|
||||
onClick={handleExportWord}
|
||||
disabled={isWordExportDisabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='rounded-3xl border border-sky-100 bg-sky-50 px-4 py-3 text-sm leading-6 text-slate-600'>
|
||||
Export Word membuat file <strong>.doc</strong> dari halaman yang diizinkan.
|
||||
Jika PDF berisi teks, isi Word akan lebih mudah diedit; jika PDF berupa scan,
|
||||
file Word tetap menyertakan snapshot halaman.
|
||||
</div>
|
||||
|
||||
{isPdfLoading ? <LoadingSpinner /> : null}
|
||||
|
||||
{pdfError ? (
|
||||
@ -456,8 +888,14 @@ export default function PdfViewerPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{wordError ? (
|
||||
<div className='rounded-3xl border border-red-200 bg-red-50 p-4 text-sm text-red-700'>
|
||||
{wordError}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className='overflow-hidden rounded-[28px] border border-slate-200 bg-slate-50 p-3 shadow-inner'>
|
||||
<div className='overflow-auto rounded-[22px] bg-white p-3'>
|
||||
<div ref={pdfContainerRef} className='overflow-auto rounded-[22px] bg-white p-3'>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user