import React, { useState, useEffect, useRef, useCallback } from 'react' import { useNavigate } from 'react-router-dom' import { StickyNote, Save, Check, AlertTriangle, Bold, Italic, Underline, Strikethrough, Heading1, Heading2, Heading3, List, ListOrdered, Quote, Code, Minus, Highlighter, AlignLeft, AlignCenter, AlignRight, RemoveFormatting, Undo, Redo, Copy, Trash2, Loader2, X, } from 'lucide-react' import { noteService, type UserNote } from '../services/noteService' export const NotepadPage: React.FC = () => { const navigate = useNavigate() const editorRef = useRef(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [initialContent, setInitialContent] = useState('') const [isDirty, setIsDirty] = useState(false) const [lastSavedAt, setLastSavedAt] = useState(null) const [copied, setCopied] = useState(false) const [stats, setStats] = useState({ words: 0, characters: 0 }) // Leave warning modal state const [showExitModal, setShowExitModal] = useState(false) const [pendingNavigationUrl, setPendingNavigationUrl] = useState(null) const [showClearConfirm, setShowClearConfirm] = useState(false) // Ref to always have the latest isDirty & editor content in callbacks const isDirtyRef = useRef(isDirty) isDirtyRef.current = isDirty const initialContentRef = useRef(initialContent) initialContentRef.current = initialContent // Calculate stats from plain text const updateStats = useCallback(() => { if (!editorRef.current) return const text = editorRef.current.innerText || '' const cleanText = text.trim() const words = cleanText ? cleanText.split(/\s+/).filter(Boolean).length : 0 const characters = text.length setStats({ words, characters }) }, []) // Load note from backend useEffect(() => { let mounted = true const fetchNote = async () => { try { setLoading(true) const note: UserNote = await noteService.getNote() if (!mounted) return const content = note?.content || '' setInitialContent(content) initialContentRef.current = content if (note?.updatedAt) { setLastSavedAt(new Date(note.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })) } } catch (err) { console.error('Erro ao carregar bloco de notas:', err) } finally { if (mounted) setLoading(false) } } fetchNote() return () => { mounted = false } }, []) // Sync loaded content into the editor DOM useEffect(() => { if (editorRef.current && !loading) { editorRef.current.innerHTML = initialContent const text = editorRef.current.innerText || '' const isEmpty = text.trim() === '' && !editorRef.current.querySelector('img, hr') editorRef.current.setAttribute('data-empty', isEmpty ? 'true' : 'false') updateStats() } }, [loading, initialContent, updateStats]) // Handle editor input const handleInput = () => { if (!editorRef.current) return const currentHtml = editorRef.current.innerHTML // Check if HTML represents empty (e.g.
,


, etc.) const text = editorRef.current.innerText || '' const isEmpty = text.trim() === '' && !editorRef.current.querySelector('img, hr') editorRef.current.setAttribute('data-empty', isEmpty ? 'true' : 'false') const dirty = currentHtml !== initialContentRef.current setIsDirty(dirty) updateStats() } // Formatting commands via document.execCommand const execCmd = (command: string, value: string | undefined = undefined) => { if (!editorRef.current) return editorRef.current.focus() document.execCommand(command, false, value) handleInput() } // Custom code block insertion const handleFormatCodeBlock = () => { if (!editorRef.current) return editorRef.current.focus() const selection = window.getSelection() if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0) const selectedText = range.toString() || 'Código aqui...' const pre = document.createElement('pre') const code = document.createElement('code') code.textContent = selectedText pre.appendChild(code) range.deleteContents() range.insertNode(pre) // Move cursor after pre const newRange = document.createRange() newRange.setStartAfter(pre) newRange.collapse(true) selection.removeAllRanges() selection.addRange(newRange) handleInput() } } // Save note to database const handleSave = async (showFeedback = true): Promise => { if (!editorRef.current) return false const content = editorRef.current.innerHTML try { setSaving(true) const saved = await noteService.saveNote(content) setInitialContent(content) initialContentRef.current = content setIsDirty(false) const timeStr = saved.updatedAt ? new Date(saved.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) setLastSavedAt(timeStr) return true } catch (err) { console.error('Falha ao salvar o bloco de notas:', err) if (showFeedback) { alert('Ocorreu um erro ao salvar o bloco de notas. Tente novamente.') } return false } finally { setSaving(false) } } // Keyboard shortcut Ctrl+S / Cmd+S useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') { e.preventDefault() handleSave() } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, []) // 1. Browser unload / reload warning useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent) => { if (isDirtyRef.current) { e.preventDefault() e.returnValue = '' } } window.addEventListener('beforeunload', handleBeforeUnload) return () => window.removeEventListener('beforeunload', handleBeforeUnload) }, []) // 2. Browser Back / Forward warning (popstate) useEffect(() => { if (!isDirty) return window.history.pushState(null, '', window.location.pathname) const handlePopState = () => { if (isDirtyRef.current) { const confirmLeave = window.confirm( 'Você possui alterações não salvas no bloco de notas. Deseja realmente sair sem salvar?' ) if (!confirmLeave) { window.history.pushState(null, '', window.location.pathname) } else { setIsDirty(false) window.history.back() } } } window.addEventListener('popstate', handlePopState) return () => window.removeEventListener('popstate', handlePopState) }, [isDirty]) // 3. In-App Navigation Interception useEffect(() => { const handleDocumentClick = (e: MouseEvent) => { if (!isDirtyRef.current) return const target = e.target as HTMLElement | null const anchor = target?.closest('a') if (!anchor) return const href = anchor.getAttribute('href') if (href && !href.startsWith('#') && href !== window.location.pathname) { e.preventDefault() e.stopPropagation() setPendingNavigationUrl(href) setShowExitModal(true) } } // Capture phase intercepts before React Router document.addEventListener('click', handleDocumentClick, true) return () => { document.removeEventListener('click', handleDocumentClick, true) } }, []) // Exit Modal Actions const handleStayOnPage = () => { setShowExitModal(false) setPendingNavigationUrl(null) } const handleDiscardAndExit = () => { setIsDirty(false) setShowExitModal(false) if (pendingNavigationUrl) { navigate(pendingNavigationUrl) } } const handleSaveAndExit = async () => { const success = await handleSave(false) if (success) { setShowExitModal(false) if (pendingNavigationUrl) { navigate(pendingNavigationUrl) } } } // Copy plain text const handleCopyText = async () => { if (!editorRef.current) return const text = editorRef.current.innerText || '' try { await navigator.clipboard.writeText(text) setCopied(true) setTimeout(() => setCopied(false), 2000) } catch { // Fallback } } // Clear note const handleClear = () => { if (editorRef.current) { editorRef.current.innerHTML = '' handleInput() } setShowClearConfirm(false) } return (
{/* Top Header Card */}

Bloco de Notas

{isDirty ? ( Não salvo ) : lastSavedAt ? ( Salvo às {lastSavedAt} ) : null}

Seu espaço livre para rascunhos, tarefas rápidas e anotações formatadas.

{/* Action Buttons */}
{/* Editor Main Card */}
{/* Formatting Toolbar */}
{/* Headings */}
{/* Inline Styles */}
{/* Lists */}
{/* Blocks */}
{/* Alignments */}
{/* Utility Tools */}
{/* Editor Area */}
{loading && (

Carregando suas anotações...

)}
{/* Footer Statistics Bar */}
{stats.words} palavras • {stats.characters} caracteres {isDirty && ( <> • Modificações pendentes de salvamento )}
Pressione Ctrl+S para salvar a qualquer momento
{/* Unsaved Changes Confirmation Modal */} {showExitModal && (

Alterações não salvas

Você alterou o texto do bloco de notas e ainda não salvou. Se sair agora, as novas modificações serão perdidas. Deseja sair sem salvar?

)} {/* Clear Note Confirmation Modal */} {showClearConfirm && (
setShowClearConfirm(false)} />

Limpar Bloco de Notas

Tem certeza de que deseja apagar todo o conteúdo deste bloco de notas? Esta ação não poderá ser desfeita caso você salve em seguida.

)}
) }