diff --git a/frontend/src/components/dashboard/Note.jsx b/frontend/src/components/dashboard/Note.jsx index 885973b..92d95c7 100644 --- a/frontend/src/components/dashboard/Note.jsx +++ b/frontend/src/components/dashboard/Note.jsx @@ -1,6 +1,11 @@ -import { useState, useRef, useEffect, useCallback } from 'react'; +import { useState, useRef, useEffect } from 'react'; import FloatingToolbar from './FloatingToolbar'; +import NoteToolbar from './NoteToolbar'; +import NoteEditor from './NoteEditor'; +import NotePreview from './NotePreview'; +import TableModal from './TableModal'; import { + FiEdit, FiBold, FiItalic, FiUnderline, @@ -10,14 +15,9 @@ import { FiLink, FiTable, FiType, - FiRotateCcw, - FiRotateCw, - FiEye, - FiEdit, FiCode, FiMinus, FiStar, - FiChevronDown, FiCopy, FiClipboard, } from 'react-icons/fi'; @@ -25,9 +25,9 @@ import { FaQuoteRight, FaListOl, FaStrikethrough, FaHighlighter } from 'react-ic import { BiCodeBlock, BiMath } from 'react-icons/bi'; import toast from 'react-hot-toast'; import propTypes from 'prop-types'; -import 'highlight.js/styles/atom-one-dark.css'; -import { renderMarkdown } from '../../utils/markdownRenderer.js'; -import { pagesAPI } from '../../utils/api'; +import { useHistory } from '../../hooks/useHistory'; +import { useImageUpload } from '../../hooks/useImageUpload'; +import { useTableModal } from '../../hooks/useTableModal'; // ============================================================================= // DEVELOPER NOTES @@ -62,174 +62,56 @@ import { pagesAPI } from '../../utils/api'; const Note = ({ activePage, onContentChange, content = '', onSave }) => { const [editorContent, setEditorContent] = useState(content); const [isPreview, setIsPreview] = useState(false); - const [history, setHistory] = useState([]); - const [historyIndex, setHistoryIndex] = useState(-1); - const [isUpdatingFromHistory, setIsUpdatingFromHistory] = useState(false); // Floating toolbar state - const [floatingToolbarEnabled, setFloatingToolbarEnabled] = useState(true); + const [floatingToolbarEnabled] = useState(true); const [toolbarVisible, setToolbarVisible] = useState(false); const [toolbarPos, setToolbarPos] = useState({ x: 0, y: 0 }); const editorRef = useRef(null); - const lineNumbersRef = useRef(null); - const [lineCount, setLineCount] = useState(20); - const [, setIsUploadingImage] = useState(false); - - const lastLoadedPageRef = useRef(null); - // Table modal state - const [showTableModal, setShowTableModal] = useState(false); - const [tableRowsInput, setTableRowsInput] = useState('3'); - const [tableColsInput, setTableColsInput] = useState('3'); - const [includeHeader, setIncludeHeader] = useState(true); - const [includeSerial, setIncludeSerial] = useState(true); - const modalContentRef = useRef(null); - const [showScrollToBottom, setShowScrollToBottom] = useState(false); - const [headerData, setHeaderData] = useState([]); - const [tableData, setTableData] = useState([]); - - useEffect(() => { - if (content !== editorContent && !isUpdatingFromHistory) { - setEditorContent(content); - // Only reset history when switching to a different page - if (lastLoadedPageRef.current !== activePage?.id) { - setHistory([content]); - setHistoryIndex(0); - lastLoadedPageRef.current = activePage?.id; - } - } - }, [content, activePage?.id, editorContent, isUpdatingFromHistory]); - - // useEffect(() => { if (isPreview) hljs.highlightAll(); }, [isPreview, editorContent]); - - // Auto-resize textarea to match content so the outer container remains the single scroller - useEffect(() => { - const ta = editorRef.current; - if (!ta) return; - // Reset height to allow shrink when content is reduced - ta.style.height = 'auto'; - // Set height to the scrollHeight so the textarea grows with content - ta.style.height = `${ta.scrollHeight}px`; - // Also sync the line numbers container height to match textarea for visual alignment - const ln = lineNumbersRef.current; - if (ln) { - ln.style.minHeight = `${ta.scrollHeight}px`; - } - // Compute how many line number rows are needed based on textarea's rendered height - const approxLineHeight = 24; // px - matches the visual line height (h-6 ~ 24px) - const requiredLines = Math.max(1, Math.floor(ta.scrollHeight / approxLineHeight)); - setLineCount(requiredLines); - }, [editorContent, activePage?.id]); - - // Update visibility of the scroll-to-bottom button when modal content changes - useEffect(() => { - if (!showTableModal) return; - const update = () => { - const el = modalContentRef.current; - if (!el) { - setShowScrollToBottom(false); - return; - } - setShowScrollToBottom(el.scrollHeight > el.clientHeight + 8); - }; - const t = setTimeout(update, 50); - window.addEventListener('resize', update); - return () => { - clearTimeout(t); - window.removeEventListener('resize', update); - }; - }, [showTableModal, tableRowsInput, tableColsInput, includeHeader, includeSerial]); - const addToHistory = useCallback( + // Use custom hooks + const { addToHistory, handleUndo, handleRedo, resetHistory, canUndo, canRedo } = useHistory( + content, + onContentChange + ); + const { handleImageUpload } = useImageUpload( + activePage, + editorContent, (newContent) => { - if (isUpdatingFromHistory) return; - - setHistory((prev) => { - // Remove any history after current index (for when user types after undo) - const newHistory = prev.slice(0, historyIndex + 1); - newHistory.push(newContent); - - // Limit history to 50 entries - if (newHistory.length > 50) { - newHistory.shift(); - return newHistory; - } - - return newHistory; - }); - - setHistoryIndex((prev) => { - const newIndex = prev + 1; - // Adjust index if we limited history size - return history.length >= 50 ? 49 : newIndex; - }); + setEditorContent(newContent); + onContentChange?.(newContent); }, - [historyIndex, isUpdatingFromHistory, history.length] + addToHistory ); + const { + showTableModal, + tableRowsInput, + setTableRowsInput, + tableColsInput, + setTableColsInput, + includeHeader, + setIncludeHeader, + includeSerial, + setIncludeSerial, + headerData, + setHeaderData, + tableData, + setTableData, + openTableModal, + closeTableModal, + confirmInsertTable, + } = useTableModal((text, moveCursor) => insertAtCursor(text, moveCursor)); - // Clicking the outer editor container's empty space should move the cursor there. - const handleContainerClick = (e) => { - // Only handle clicks directly on the container (not children like textarea) - if (e.target !== e.currentTarget) return; - - const ta = editorRef.current; - if (!ta) return; - - // Append a couple of newlines to create an empty area and place cursor at end - const appended = '\n\n'; - const newContent = `${editorContent}${appended}`; - setEditorContent(newContent); - addToHistory(newContent); - if (onContentChange) onContentChange(newContent); - - // Focus and move caret to end after DOM updates - setTimeout(() => { - ta.focus(); - ta.setSelectionRange(newContent.length, newContent.length); - }, 0); - }; - - const handleKeyDown = (e) => { - if (e.ctrlKey || e.metaKey) { - switch (e.key.toLowerCase()) { - case 'z': - if (e.shiftKey) { - e.preventDefault(); - handleRedo(); - } else { - e.preventDefault(); - handleUndo(); - } - break; - case 'y': - e.preventDefault(); - handleRedo(); - break; - case 'b': - e.preventDefault(); - wrapSelectedText('**', '**', 'bold text'); - break; - case 'i': - e.preventDefault(); - wrapSelectedText('*', '*', 'italic text'); - break; - case 's': - e.preventDefault(); - if (onSave) { - onSave(); - toast.success('Note saved!'); - } - break; - default: - break; - } + useEffect(() => { + if (content !== editorContent) { + setEditorContent(content); + resetHistory(content, activePage?.id); } - }; + }, [content, activePage?.id, editorContent, resetHistory]); const handleContentChange = (e) => { const newContent = e.target.value; setEditorContent(newContent); - addToHistory(newContent); - if (onContentChange) { onContentChange(newContent); } @@ -312,472 +194,127 @@ const Note = ({ activePage, onContentChange, content = '', onSave }) => { } }; - const handleUndo = () => { - if (historyIndex > 0) { - setIsUpdatingFromHistory(true); - const newIndex = historyIndex - 1; - setHistoryIndex(newIndex); - const previousContent = history[newIndex]; - setEditorContent(previousContent); - - if (onContentChange) { - onContentChange(previousContent); - } - - setTimeout(() => setIsUpdatingFromHistory(false), 0); - } - }; - - const handleRedo = () => { - if (historyIndex < history.length - 1) { - setIsUpdatingFromHistory(true); - const newIndex = historyIndex + 1; - setHistoryIndex(newIndex); - const nextContent = history[newIndex]; - setEditorContent(nextContent); - - if (onContentChange) { - onContentChange(nextContent); - } - - setTimeout(() => setIsUpdatingFromHistory(false), 0); - } - }; - - const scrollModalToBottom = () => { - const el = modalContentRef.current; - if (!el) return; - el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }); - }; - - const openTableModal = () => { - setTableRowsInput('3'); - setTableColsInput('3'); - setIncludeHeader(true); - setIncludeSerial(true); - const r = 3; - const c = 3; - setHeaderData(Array.from({ length: c }, (_, i) => `Header ${i + 1}`)); - setTableData( - Array.from({ length: r }, (_, ri) => - Array.from({ length: c }, (_, ci) => `Cell ${ri * c + ci + 1}`) - ) - ); - setShowTableModal(true); - }; - - // When rows or cols inputs change, clamp to max 20 and resize header/table data accordingly - useEffect(() => { - const rows = Math.max(1, Math.min(20, parseInt(tableRowsInput || '1', 10))); - const cols = Math.max(1, Math.min(20, parseInt(tableColsInput || '1', 10))); - - setHeaderData((prev) => { - const next = prev ? [...prev] : []; - for (let i = 0; i < cols; i++) if (next[i] === undefined) next[i] = `Header ${i + 1}`; - next.length = cols; - return next; - }); - - setTableData((prev) => { - const next = prev ? prev.map((r) => [...r]) : []; - for (let r = 0; r < rows; r++) { - if (!next[r]) next[r] = Array.from({ length: cols }, (_, c) => `Cell ${r * cols + c + 1}`); - for (let c = 0; c < cols; c++) - if (next[r][c] === undefined) next[r][c] = `Cell ${r * cols + c + 1}`; - next[r].length = cols; - } - next.length = rows; - return next; - }); - }, [tableRowsInput, tableColsInput]); - - const closeTableModal = () => setShowTableModal(false); - - const confirmInsertTable = () => { - const rows = parseInt(tableRowsInput, 10); - const cols = parseInt(tableColsInput, 10); - if (Number.isNaN(rows) || Number.isNaN(cols) || rows < 1 || cols < 1) { - toast.error('Invalid table size. Rows and columns must be positive integers.'); - return; - } - const MAX = 20; - const rClamped = Math.max(1, Math.min(MAX, rows)); - const cClamped = Math.max(1, Math.min(MAX, cols)); - const totalCols = (includeSerial ? 1 : 0) + cClamped; + // Clicking the outer editor container's empty space should move the cursor there. + const handleContainerClick = (e) => { + // Only handle clicks directly on the container (not children like textarea) + if (e.target !== e.currentTarget) return; - const headerCells = []; - if (includeSerial) headerCells.push(includeHeader ? '#' : ''); - for (let i = 0; i < cClamped; i++) { - const hv = headerData[i]; - headerCells.push(includeHeader ? (hv ?? `Header ${i + 1}`) : ''); - } - const headerRow = `| ${headerCells.join(' | ')} |`; - const separatorCells = Array.from({ length: totalCols }, () => '---'); - const separatorRowStr = `| ${separatorCells.join(' | ')} |`; + const ta = editorRef.current; + if (!ta) return; - const dataRows = []; - for (let r = 0; r < rClamped; r++) { - const rowCells = []; - if (includeSerial) rowCells.push(`${r + 1}`); - for (let c = 0; c < cClamped; c++) { - const val = - tableData[r] && tableData[r][c] ? tableData[r][c] : `Cell ${r * cClamped + c + 1}`; - const safeVal = String(val).replace(/\|/g, '\\|'); - rowCells.push(safeVal); - } - dataRows.push(`| ${rowCells.join(' | ')} |`); - } + // Append a couple of newlines to create an empty area and place cursor at end + const appended = '\n\n'; + const newContent = `${editorContent}${appended}`; + setEditorContent(newContent); + addToHistory(newContent); + if (onContentChange) onContentChange(newContent); - const tableMarkdown = `\n${headerRow}\n${separatorRowStr}\n${dataRows.join('\n')}\n`; - insertAtCursor(tableMarkdown, 1); - toast.success('Table inserted'); - closeTableModal(); + // Focus and move caret to end after DOM updates + setTimeout(() => { + ta.focus(); + ta.setSelectionRange(newContent.length, newContent.length); + }, 0); }; - // Handle clipboard paste events - useEffect(() => { - const handlePaste = async (e) => { - // Only process if we have an active page and the textarea is focused - if ( - !activePage || - !activePage.id || - !editorRef.current || - document.activeElement !== editorRef.current - ) - return; - - // Check if the clipboard contains image data - const items = e.clipboardData && e.clipboardData.items; - if (!items) return; - - let hasImageItem = false; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - - // Check if the item is an image - if (item.type.indexOf('image') === 0) { - hasImageItem = true; - // Prevent default paste behavior + const handleKeyDown = (e) => { + if (e.ctrlKey || e.metaKey) { + switch (e.key.toLowerCase()) { + case 'z': + if (e.shiftKey) { + e.preventDefault(); + handleRedo(); + } else { + e.preventDefault(); + handleUndo(); + } + break; + case 'y': e.preventDefault(); - - // Get the image blob - const blob = item.getAsFile(); - if (!blob) continue; - - try { - setIsUploadingImage(true); - - // Show a loading toast - const loadingToast = toast.loading('Uploading pasted image...'); - - // Insert temporary placeholder at cursor position - const position = editorRef.current.selectionStart; - const tempPlaceholder = '![Uploading image...]()'; - - const newContent = - editorContent.substring(0, position) + - tempPlaceholder + - editorContent.substring(position); - - setEditorContent(newContent); - if (onContentChange) onContentChange(newContent); - - // Convert blob to base64 - const reader = new FileReader(); - reader.readAsDataURL(blob); - reader.onload = async () => { - try { - // Upload the image - const response = await pagesAPI.uploadImage(reader.result, activePage.id); - - if (response.status === 200 && response.data && response.data.imageUrl) { - // Replace the placeholder with actual image markdown - const imageMarkdown = `![Image](${response.data.imageUrl})`; - const updatedContent = newContent.replace(tempPlaceholder, imageMarkdown); - - setEditorContent(updatedContent); - addToHistory(updatedContent); - - if (onContentChange) onContentChange(updatedContent); - - toast.dismiss(loadingToast); - toast.success('Image uploaded successfully'); - } else { - throw new Error((response.data && response.data.message) || 'Upload failed'); - } - } catch (error) { - // Remove placeholder on error - const updatedContent = newContent.replace(tempPlaceholder, ''); - setEditorContent(updatedContent); - if (onContentChange) onContentChange(updatedContent); - - toast.dismiss(loadingToast); - toast.error(`Failed to upload image: ${error.message || 'Unknown error'}`); - console.error('Image upload error:', error); - } - }; - - reader.onerror = () => { - // Remove placeholder on error - const updatedContent = newContent.replace(tempPlaceholder, ''); - setEditorContent(updatedContent); - if (onContentChange) onContentChange(updatedContent); - - toast.dismiss(loadingToast); - toast.error('Failed to read image data'); - }; - } finally { - setIsUploadingImage(false); + handleRedo(); + break; + case 'b': + e.preventDefault(); + wrapSelectedText('**', '**', 'bold text'); + break; + case 'i': + e.preventDefault(); + wrapSelectedText('*', '*', 'italic text'); + break; + case 's': + e.preventDefault(); + if (onSave) { + onSave(); + toast.success('Note saved!'); } - - // Only process the first image break; - } + default: + break; } - - // If no image was found in clipboard, let the default paste behavior occur - if (!hasImageItem) return; - }; - - // Add paste event listener to the document - document.addEventListener('paste', handlePaste); - - // Clean up the event listener when component unmounts - return () => document.removeEventListener('paste', handlePaste); - }, [activePage, editorContent, onContentChange, addToHistory]); - - // Update the handleImageUpload function to use Cloudinary - const handleImageUpload = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = 'image/*'; - input.onchange = async (e) => { - const file = e.target.files && e.target.files[0]; - if (!file || !activePage || !activePage.id) { - toast.error('Please select an image or make sure a page is active'); - return; + } else if (e.key === 'Enter') { + // Handle list continuation + const textarea = editorRef.current; + if (!textarea) return; + + const cursorPos = textarea.selectionStart; + const text = editorContent; + + // Find the start of the current line + let lineStart = cursorPos; + while (lineStart > 0 && text[lineStart - 1] !== '\n') { + lineStart--; } - try { - setIsUploadingImage(true); - const loadingToast = toast.loading(`Uploading ${file.name}...`); - - // Insert temporary placeholder - const cursorPos = editorRef.current - ? editorRef.current.selectionStart - : editorContent.length; - const tempPlaceholder = `![Uploading ${file.name}...]()`; - - const newContent = - editorContent.substring(0, cursorPos) + - tempPlaceholder + - editorContent.substring(cursorPos); - - setEditorContent(newContent); - if (onContentChange) onContentChange(newContent); - - // Convert file to base64 - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = async () => { - try { - // Upload the image - const response = await pagesAPI.uploadImage(reader.result, activePage.id); - - if (response.status === 200 && response.data && response.data.imageUrl) { - // Replace placeholder with actual image markdown - const imageMarkdown = `![${file.name}](${response.data.imageUrl})`; - const updatedContent = newContent.replace(tempPlaceholder, imageMarkdown); - - setEditorContent(updatedContent); - addToHistory(updatedContent); - - if (onContentChange) onContentChange(updatedContent); - - toast.dismiss(loadingToast); - toast.success('Image uploaded successfully'); - } else { - throw new Error((response.data && response.data.message) || 'Upload failed'); - } - } catch (error) { - // Remove placeholder on error - const updatedContent = newContent.replace(tempPlaceholder, ''); - setEditorContent(updatedContent); - if (onContentChange) onContentChange(updatedContent); - - toast.dismiss(loadingToast); - toast.error(`Failed to upload image: ${error.message || 'Unknown error'}`); - console.error('Image upload error:', error); - } - }; - - reader.onerror = () => { - // Remove placeholder on error - const updatedContent = newContent.replace(tempPlaceholder, ''); - setEditorContent(updatedContent); - if (onContentChange) onContentChange(updatedContent); - - toast.dismiss(loadingToast); - toast.error('Failed to read image file'); - }; - } finally { - setIsUploadingImage(false); + // Get the current line + let lineEnd = cursorPos; + while (lineEnd < text.length && text[lineEnd] !== '\n') { + lineEnd++; + } + const currentLine = text.substring(lineStart, lineEnd); + + // Check for list patterns + const bulletMatch = currentLine.match(/^(\s*)-(\s*.*)?$/); + const numberedMatch = currentLine.match(/^(\s*)(\d+)\.(\s*.*)?$/); + const taskMatch = currentLine.match(/^(\s*)-(\s*)\[([ x])\](\s*.*)?$/); + + if (bulletMatch) { + e.preventDefault(); + const indent = bulletMatch[1]; + const hasContent = bulletMatch[2] && bulletMatch[2].trim(); + if (hasContent) { + // Continue bullet list + insertAtCursor(`\n${indent}- `, 2); + } else { + // Exit list - insert newline with space + insertAtCursor('\n ', 1); + } + } else if (numberedMatch) { + e.preventDefault(); + const indent = numberedMatch[1]; + const number = parseInt(numberedMatch[2], 10); + const hasContent = numberedMatch[3] && numberedMatch[3].trim(); + if (hasContent) { + // Continue numbered list with next number + insertAtCursor(`\n${indent}${number + 1}. `, 3); + } else { + // Exit list + insertAtCursor('\n ', 1); + } + } else if (taskMatch) { + e.preventDefault(); + const indent = taskMatch[1]; + const hasContent = taskMatch[4] && taskMatch[4].trim(); + if (hasContent) { + // Continue task list + insertAtCursor(`\n${indent}- [ ] `, 6); + } else { + // Exit list + insertAtCursor('\n ', 1); + } } - }; - input.click(); + // If not a list, let default behavior happen + } }; - const toolbarGroups = [ - { - name: 'History', - color: 'primary', - buttons: [ - { - icon: FiRotateCcw, - title: 'Undo (Ctrl+Z)', - onClick: handleUndo, - disabled: historyIndex <= 0, - shortcut: 'Ctrl+Z', - }, - { - icon: FiRotateCw, - title: 'Redo (Ctrl+Y)', - onClick: handleRedo, - disabled: historyIndex >= history.length - 1, - shortcut: 'Ctrl+Y', - }, - ], - }, - { - name: 'Format', - color: 'secondary', - buttons: [ - { - icon: FiBold, - title: 'Bold (Ctrl+B)', - onClick: () => wrapSelectedText('**', '**', 'bold text'), - shortcut: 'Ctrl+B', - }, - { - icon: FiItalic, - title: 'Italic (Ctrl+I)', - onClick: () => wrapSelectedText('*', '*', 'italic text'), - shortcut: 'Ctrl+I', - }, - { - icon: FaStrikethrough, - title: 'Strikethrough', - onClick: () => wrapSelectedText('~~', '~~', 'strikethrough text'), - }, - { - icon: FiUnderline, - title: 'Underline', - onClick: () => wrapSelectedText('', '', 'underlined text'), - }, - { - icon: FaHighlighter, - title: 'Highlight', - onClick: () => wrapSelectedText('==', '==', 'highlighted text'), - }, - { - icon: FiCode, - title: 'Inline Code', - onClick: () => wrapSelectedText('`', '`', 'code'), - }, - ], - }, - { - name: 'Structure', - color: 'accent', - buttons: [ - { - icon: FiType, - title: 'Heading 1', - onClick: () => insertAtCursor('\n# ', 2), - }, - { - icon: FiType, - title: 'Heading 2', - onClick: () => insertAtCursor('\n## ', 3), - variant: 'h2', - }, - { - icon: FiType, - title: 'Heading 3', - onClick: () => insertAtCursor('\n### ', 4), - variant: 'h3', - }, - { - icon: FaQuoteRight, - title: 'Blockquote', - onClick: () => insertAtCursor('\n> ', 2), - }, - { - icon: BiCodeBlock, - title: 'Code Block', - onClick: () => insertAtCursor('\n```javascript\n\n```\n', 15), - }, - { - icon: FiMinus, - title: 'Horizontal Rule', - onClick: () => insertAtCursor('\n---\n', 1), - }, - ], - }, - { - name: 'Lists', - color: 'success', - buttons: [ - { - icon: FiList, - title: 'Bullet List', - onClick: () => insertAtCursor('\n- ', 2), - }, - { - icon: FaListOl, - title: 'Numbered List', - onClick: () => insertAtCursor('\n1. ', 3), - }, - { - icon: FiCheck, - title: 'Task List', - onClick: () => insertAtCursor('\n- [ ] ', 6), - }, - { - icon: FiStar, - title: 'Definition List', - onClick: () => insertAtCursor('\nTerm\n: Definition\n', 1), - }, - ], - }, - { - name: 'Media', - color: 'warning', - buttons: [ - { - icon: FiImage, - title: 'Add Image', - onClick: handleImageUpload, - }, - { - icon: FiLink, - title: 'Add Link', - onClick: () => wrapSelectedText('[', '](url)', 'Link text'), - }, - { - icon: FiTable, - title: 'Add Table', - onClick: openTableModal, - }, - { - icon: BiMath, - title: 'Math Formula', - onClick: () => wrapSelectedText('$', '$', 'x^2 + y^2 = z^2'), - }, - ], - }, - ]; - if (!activePage) { return (
@@ -809,7 +346,72 @@ const Note = ({ activePage, onContentChange, content = '', onSave }) => { onClose={() => setToolbarVisible(false)} > {(() => { - const allButtons = toolbarGroups.flatMap((group) => group.buttons); + const allButtons = [ + { + icon: FiBold, + title: 'Bold', + onClick: () => wrapSelectedText('**', '**', 'bold text'), + }, + { + icon: FiItalic, + title: 'Italic', + onClick: () => wrapSelectedText('*', '*', 'italic text'), + }, + { + icon: FaStrikethrough, + title: 'Strikethrough', + onClick: () => wrapSelectedText('~~', '~~', 'strikethrough text'), + }, + { + icon: FiUnderline, + title: 'Underline', + onClick: () => wrapSelectedText('', '', 'underlined text'), + }, + { + icon: FaHighlighter, + title: 'Highlight', + onClick: () => wrapSelectedText('==', '==', 'highlighted text'), + }, + { + icon: FiCode, + title: 'Inline Code', + onClick: () => wrapSelectedText('`', '`', 'code'), + }, + { icon: FiType, title: 'Heading 1', onClick: () => insertAtCursor('\n# ', 2) }, + { icon: FiType, title: 'Heading 2', onClick: () => insertAtCursor('\n## ', 3) }, + { icon: FiType, title: 'Heading 3', onClick: () => insertAtCursor('\n### ', 4) }, + { icon: FaQuoteRight, title: 'Blockquote', onClick: () => insertAtCursor('\n> ', 2) }, + { + icon: BiCodeBlock, + title: 'Code Block', + onClick: () => insertAtCursor('\n```javascript\n\n```\n', 15), + }, + { + icon: FiMinus, + title: 'Horizontal Rule', + onClick: () => insertAtCursor('\n---\n', 1), + }, + { icon: FiList, title: 'Bullet List', onClick: () => insertAtCursor('\n- ', 2) }, + { icon: FaListOl, title: 'Numbered List', onClick: () => insertAtCursor('\n1. ', 3) }, + { icon: FiCheck, title: 'Task List', onClick: () => insertAtCursor('\n- [ ] ', 6) }, + { + icon: FiStar, + title: 'Definition List', + onClick: () => insertAtCursor('\nTerm\n: Definition\n', 1), + }, + { icon: FiImage, title: 'Add Image', onClick: handleImageUpload }, + { + icon: FiLink, + title: 'Add Link', + onClick: () => wrapSelectedText('[', '](url)', 'Link text'), + }, + { icon: FiTable, title: 'Add Table', onClick: openTableModal }, + { + icon: BiMath, + title: 'Math Formula', + onClick: () => wrapSelectedText('$', '$', 'x^2 + y^2 = z^2'), + }, + ]; const rows = [allButtons.slice(0, 8), allButtons.slice(8, 16), allButtons.slice(16, 22)]; // Add Copy and Paste buttons to the last row const copyPasteIcons = [ @@ -910,394 +512,58 @@ const Note = ({ activePage, onContentChange, content = '', onSave }) => { ); })()} - {/* Enhanced Toolbar */} -
-
-
- {/* Enhanced Toolbar Groups */} -
- {toolbarGroups.map((group, groupIndex) => ( -
- {group.buttons - .slice( - 0, - window.innerWidth < 1024 && groupIndex > 2 - ? 1 - : window.innerWidth < 768 && groupIndex > 1 - ? 2 - : group.buttons.length - ) - .map((button, buttonIndex) => { - const Icon = button.icon; - const colorClass = `hover:btn-${group.color}`; - - return ( - - ); - })} -
- ))} -
- {/* Enhanced Preview Toggle */} -
- - -
-
- - {/* Status Bar */} -
-
- -
- {isPreview ? 'Preview Mode' : 'Edit Mode'} -
- {editorContent.length} characters - - {editorContent.split(/\s+/).filter((word) => word.length > 0).length} words - -
-
- Press - Ctrl - + - S - to save -
-
-
-
+ {/* Enhanced Editor/Preview Area */}
{isPreview ? ( -
- {/* Preview Header */} -
-
-
- Preview Mode -
-
- - Live Preview -
-
- - {/* Enhanced Preview Content */} -
-
-
-
+ ) : ( -
- {/* Editor Header */} -
-
-
- Editor Mode -
-
- - - Markdown Enabled - - Auto-save: On - -
-
- - {/* Enhanced Editor */} -
{ - if (!floatingToolbarEnabled) return; - e.preventDefault(); - setToolbarPos({ x: e.clientX, y: e.clientY }); - setToolbarVisible(true); - }} - className="bg-base-100 rounded-2xl border border-base-300 shadow-lg overflow-x-hidden overflow-y-auto max-h-[70vh] min-h-[70vh] relative flex note-container-scrollable" - > - {/* Line Numbers */} -
- {Array.from({ length: lineCount }, (_, i) => ( -
- {i + 1} -
- ))} -
- -