import { useState, useEffect, useRef } from 'react'; import { useEditor, EditorContent } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import Underline from '@tiptap/extension-underline'; import Strike from '@tiptap/extension-strike'; import TurndownService from 'turndown'; import { marked } from 'marked'; import jsPDF from 'jspdf'; import html2canvas from 'html2canvas'; import { Note } from '../types'; interface NoteEditorProps { note: Note | null; onUpdateNote: (note: Note) => void; fontSize: number; onUnsavedChanges?: (hasChanges: boolean) => void; } const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', }); export function NoteEditor({ note, onUpdateNote, fontSize, onUnsavedChanges }: NoteEditorProps) { const [localTitle, setLocalTitle] = useState(''); const [localFavorite, setLocalFavorite] = useState(false); const [isSaving, setIsSaving] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [titleManuallyEdited, setTitleManuallyEdited] = useState(false); const previousNoteIdRef = useRef(null); // Notify parent component when unsaved changes state changes useEffect(() => { onUnsavedChanges?.(hasUnsavedChanges); }, [hasUnsavedChanges, onUnsavedChanges]); const editor = useEditor({ extensions: [ StarterKit, Underline, Strike, ], content: '', editorProps: { attributes: { class: 'prose prose-slate max-w-none focus:outline-none p-6', style: `font-size: ${fontSize}px`, }, }, onUpdate: ({ editor }) => { setHasUnsavedChanges(true); if (!titleManuallyEdited) { const text = editor.getText(); const firstLine = text.split('\n')[0].trim(); if (firstLine) { setLocalTitle(firstLine.substring(0, 50)); } } }, }); useEffect(() => { const loadNewNote = () => { if (note && editor) { setLocalTitle(note.title); setLocalFavorite(note.favorite); setHasUnsavedChanges(false); // Only reset titleManuallyEdited when switching to a different note // Check if the current title matches the first line of content const firstLine = note.content.split('\n')[0].replace(/^#+\s*/, '').trim(); const titleMatchesFirstLine = note.title === firstLine || note.title === firstLine.substring(0, 50); setTitleManuallyEdited(!titleMatchesFirstLine); previousNoteIdRef.current = note.id; // Convert markdown to HTML using marked library const html = marked.parse(note.content || '', { async: false }) as string; editor.commands.setContent(html); } }; // If switching notes, save the previous note first if (previousNoteIdRef.current !== null && previousNoteIdRef.current !== note?.id) { // Save if there are unsaved changes if (hasUnsavedChanges && editor) { handleSave(); } // Load new note after a brief delay to ensure save completes setTimeout(loadNewNote, 100); } else { // First load or same note, load immediately loadNewNote(); } }, [note?.id, editor]); const handleSave = () => { if (!note || !hasUnsavedChanges || !editor) return; // Convert HTML to markdown const html = editor.getHTML(); const markdown = turndownService.turndown(html); console.log('Saving note content length:', markdown.length); console.log('Last 50 chars:', markdown.slice(-50)); setIsSaving(true); setHasUnsavedChanges(false); onUpdateNote({ ...note, title: localTitle, content: markdown, category: '', favorite: localFavorite, }); setTimeout(() => setIsSaving(false), 500); }; const handleTitleChange = (value: string) => { setLocalTitle(value); setTitleManuallyEdited(true); setHasUnsavedChanges(true); }; const handleDiscard = () => { if (!note || !editor) return; // Reload original note content setLocalTitle(note.title); setLocalFavorite(note.favorite); setHasUnsavedChanges(false); const firstLine = note.content.split('\n')[0].replace(/^#+\s*/, '').trim(); const titleMatchesFirstLine = note.title === firstLine || note.title === firstLine.substring(0, 50); setTitleManuallyEdited(!titleMatchesFirstLine); const html = marked.parse(note.content || '', { async: false }) as string; editor.commands.setContent(html); }; const handleExportPDF = async () => { if (!note || !editor) return; try { // Get the editor content element const editorElement = document.querySelector('.ProseMirror'); if (!editorElement) return; // Create a temporary container with better styling for PDF const container = document.createElement('div'); container.style.width = '210mm'; // A4 width container.style.padding = '20mm'; container.style.backgroundColor = 'white'; container.style.position = 'absolute'; container.style.left = '-9999px'; container.style.fontFamily = 'Arial, sans-serif'; // Add title const titleElement = document.createElement('h1'); titleElement.textContent = localTitle || 'Untitled'; titleElement.style.marginBottom = '20px'; titleElement.style.fontSize = '24px'; titleElement.style.fontWeight = 'bold'; container.appendChild(titleElement); // Clone and add content const contentClone = editorElement.cloneNode(true) as HTMLElement; contentClone.style.fontSize = '12px'; contentClone.style.lineHeight = '1.6'; container.appendChild(contentClone); document.body.appendChild(container); // Convert to canvas const canvas = await html2canvas(container, { scale: 2, useCORS: true, backgroundColor: '#ffffff', }); // Remove temporary container document.body.removeChild(container); // Create PDF const imgData = canvas.toDataURL('image/png'); const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4', }); const imgWidth = 210; // A4 width in mm const imgHeight = (canvas.height * imgWidth) / canvas.width; pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight); // Save the PDF const fileName = `${localTitle || 'note'}.pdf`; pdf.save(fileName); } catch (error) { console.error('PDF export failed:', error); alert('Failed to export PDF. Please try again.'); } }; const handleFavoriteToggle = () => { setLocalFavorite(!localFavorite); if (note) { onUpdateNote({ ...note, title: localTitle, content: editor ? turndownService.turndown(editor.getHTML()) : note.content, category: '', favorite: !localFavorite, }); } }; if (!note) { return (

No Note Selected

Select a note from the sidebar or create a new one

); } if (!editor) { return null; } return (
handleTitleChange(e.target.value)} placeholder="Note Title" className="flex-1 text-2xl font-bold border-none outline-none focus:ring-0 bg-transparent text-gray-900 dark:text-gray-100" />
{hasUnsavedChanges && ( Unsaved changes )} {isSaving && ( Saving... )}
{/* Formatting Toolbar */}
); }