Improve PDF export with multi-page support and better UX

- Fixed multi-page PDF generation - now properly splits long documents across pages
- Added isExportingPDF state for loading feedback
- Replaced generic download icon with document/PDF icon
- Button shows spinning loader during PDF generation
- Button changes to blue background while exporting
- Added success alert with filename and download location
- Button disabled during export to prevent multiple clicks
- Improved visual feedback throughout export process
This commit is contained in:
drelich
2026-03-17 09:42:27 +01:00
parent d53a454d7b
commit 9d3c8b5e3c

View File

@@ -27,6 +27,7 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges }: N
const [isSaving, setIsSaving] = useState(false);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [titleManuallyEdited, setTitleManuallyEdited] = useState(false);
const [isExportingPDF, setIsExportingPDF] = useState(false);
const previousNoteIdRef = useRef<number | null>(null);
// Notify parent component when unsaved changes state changes
@@ -141,10 +142,15 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges }: N
const handleExportPDF = async () => {
if (!note || !editor) return;
setIsExportingPDF(true);
try {
// Get the editor content element
const editorElement = document.querySelector('.ProseMirror');
if (!editorElement) return;
if (!editorElement) {
setIsExportingPDF(false);
return;
}
// Create a temporary container with better styling for PDF
const container = document.createElement('div');
@@ -167,6 +173,7 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges }: N
const contentClone = editorElement.cloneNode(true) as HTMLElement;
contentClone.style.fontSize = '12px';
contentClone.style.lineHeight = '1.6';
contentClone.style.color = '#000000';
container.appendChild(contentClone);
document.body.appendChild(container);
@@ -181,25 +188,46 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges }: N
// Remove temporary container
document.body.removeChild(container);
// Create PDF
const imgData = canvas.toDataURL('image/png');
// Create PDF with multi-page support
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4',
});
const imgWidth = 210; // A4 width in mm
const pageWidth = 210; // A4 width in mm
const pageHeight = 297; // A4 height in mm
const imgWidth = pageWidth;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
let heightLeft = imgHeight;
let position = 0;
// Add first page
pdf.addImage(canvas.toDataURL('image/png'), 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
// Add additional pages if needed
while (heightLeft > 0) {
position = heightLeft - imgHeight;
pdf.addPage();
pdf.addImage(canvas.toDataURL('image/png'), 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
// Save the PDF
const fileName = `${localTitle || 'note'}.pdf`;
pdf.save(fileName);
// Show success message
setTimeout(() => {
alert(`PDF exported successfully!\n\nFile: ${fileName}\nLocation: Downloads folder`);
setIsExportingPDF(false);
}, 500);
} catch (error) {
console.error('PDF export failed:', error);
alert('Failed to export PDF. Please try again.');
setIsExportingPDF(false);
}
};
@@ -285,12 +313,26 @@ export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges }: N
<button
onClick={handleExportPDF}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors text-gray-700 dark:text-gray-300"
title="Export as PDF"
disabled={isExportingPDF}
className={`p-2 rounded-lg transition-colors ${
isExportingPDF
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 cursor-wait'
: 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'
}`}
title={isExportingPDF ? "Generating PDF..." : "Export as PDF"}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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" />
{isExportingPDF ? (
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
) : (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"/>
<path d="M14 2v6h6"/>
<path fill="white" d="M8 13h8v1H8v-1zm0 2h8v1H8v-1zm0 2h5v1H8v-1z"/>
</svg>
)}
</button>
<button