From 9edd7846d91edb129231dd0a096ff6b657393bd7 Mon Sep 17 00:00:00 2001 From: Alexandr Kolesov Date: Wed, 28 Jan 2026 23:17:56 +0400 Subject: [PATCH 1/6] Better notes app with folders --- web/src/components/Notes.css | 322 ++++++++++++++++- web/src/components/Notes.tsx | 681 +++++++++++++++++++++++++++++------ 2 files changed, 892 insertions(+), 111 deletions(-) diff --git a/web/src/components/Notes.css b/web/src/components/Notes.css index 6ec0c37..ba50239 100644 --- a/web/src/components/Notes.css +++ b/web/src/components/Notes.css @@ -4,8 +4,9 @@ background: #f8f9fa; } +/* Left Sidebar - Folders */ .notes-sidebar { - width: 280px; + width: 260px; background: #ffffff; border-right: 1px solid #dde1e6; display: flex; @@ -70,13 +71,263 @@ background: #f5f3ff; } +.view-filters { + padding: 12px 16px; + border-bottom: 1px solid #dde1e6; +} + +.filter-checkbox { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.875rem; + color: #464c52; + cursor: pointer; +} + +.filter-checkbox input[type="checkbox"] { + cursor: pointer; +} + +.new-folder-form { + padding: 12px 16px; + border-bottom: 1px solid #dde1e6; + background: #f8f9fa; +} + +.folder-title-input { + width: 100%; + padding: 8px; + border: 1px solid #c1c7cd; + border-radius: 4px; + font-size: 0.875rem; + margin-bottom: 8px; +} + +.form-actions { + display: flex; + gap: 6px; +} + +.btn-save-small, +.btn-cancel-small { + flex: 1; + padding: 6px 12px; + border: none; + border-radius: 4px; + font-size: 0.8125rem; + font-weight: 500; + cursor: pointer; + transition: background 0.15s; +} + +.btn-save-small { + background: #6d4aff; + color: #ffffff; +} + +.btn-save-small:hover:not(:disabled) { + background: #5836e8; +} + +.btn-save-small:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-cancel-small { + background: #e5e7eb; + color: #464c52; +} + +.btn-cancel-small:hover:not(:disabled) { + background: #d1d5db; +} + +.folders-list { + flex: 1; + overflow-y: auto; +} + +.folder-item { + padding: 12px 16px; + border-bottom: 1px solid #f0f1f3; + cursor: pointer; + transition: background 0.15s; +} + +.folder-item:hover { + background: #f5f6f8; +} + +.folder-item.active { + background: #f5f3ff; + border-left: 3px solid #6d4aff; +} + +.folder-item.archived { + opacity: 0.6; +} + +.folder-title { + font-weight: 500; + color: #1c1f23; + margin-bottom: 4px; + font-size: 0.9375rem; + display: flex; + align-items: center; + gap: 4px; +} + +.archived-badge { + font-size: 0.75rem; +} + +.folder-count { + font-size: 0.8125rem; + color: #70757a; +} + +/* Middle Panel - Notes List */ +.notes-middle { + width: 320px; + background: #ffffff; + border-right: 1px solid #dde1e6; + display: flex; + flex-direction: column; +} + +.middle-header { + padding: 16px; + border-bottom: 1px solid #dde1e6; +} + +.folder-header-info { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.middle-header h2 { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + color: #1c1f23; +} + +.folder-archived-label { + font-size: 0.75rem; + padding: 2px 8px; + background: #e5e7eb; + color: #464c52; + border-radius: 10px; + font-weight: 500; +} + +.folder-actions { + display: flex; + gap: 6px; +} + +.btn-icon, +.btn-icon-danger { + padding: 6px 10px; + border: 1px solid #c1c7cd; + border-radius: 4px; + background: #ffffff; + font-size: 0.875rem; + cursor: pointer; + transition: border-color 0.15s, background 0.15s; +} + +.btn-icon:hover { + border-color: #6d4aff; + background: #f5f3ff; +} + +.btn-icon-danger:hover { + border-color: #dc2626; + background: #fef2f2; +} + +.rename-folder-form { + display: flex; + flex-direction: column; + gap: 8px; +} + +.rename-folder-input { + width: 100%; + padding: 8px; + border: 1px solid #c1c7cd; + border-radius: 4px; + font-size: 0.9375rem; +} + +.rename-actions { + display: flex; + gap: 6px; +} + +.btn-save-rename, +.btn-cancel-rename { + flex: 1; + padding: 6px 12px; + border: none; + border-radius: 4px; + font-size: 0.8125rem; + font-weight: 500; + cursor: pointer; +} + +.btn-save-rename { + background: #6d4aff; + color: #ffffff; +} + +.btn-save-rename:hover:not(:disabled) { + background: #5836e8; +} + +.btn-cancel-rename { + background: #e5e7eb; + color: #464c52; +} + +.btn-cancel-rename:hover:not(:disabled) { + background: #d1d5db; +} + +.middle-actions { + padding: 12px 16px; + border-bottom: 1px solid #dde1e6; +} + +.btn-new-note { + width: 100%; + padding: 8px 14px; + border: none; + border-radius: 4px; + background: #6d4aff; + color: #ffffff; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: background 0.15s; +} + +.btn-new-note:hover { + background: #5836e8; +} + .notes-list { flex: 1; overflow-y: auto; } .note-item { - padding: 14px 16px; + padding: 12px 16px; border-bottom: 1px solid #f0f1f3; cursor: pointer; transition: background 0.15s; @@ -91,11 +342,26 @@ border-left: 3px solid #6d4aff; } +.note-item.archived { + opacity: 0.6; +} + +.note-header { + margin-bottom: 6px; +} + .note-title { font-weight: 500; color: #1c1f23; - margin-bottom: 4px; font-size: 0.9375rem; + display: flex; + align-items: center; + gap: 4px; +} + +.pin-icon, +.archived-icon { + font-size: 0.75rem; } .note-preview { @@ -110,6 +376,7 @@ color: #9ca3af; } +/* Right Panel - Note Content */ .notes-content { flex: 1; display: flex; @@ -120,6 +387,13 @@ .editor-header { padding: 16px 20px; border-bottom: 1px solid #dde1e6; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.note-actions-left { display: flex; gap: 8px; } @@ -154,7 +428,7 @@ font-size: 0.875rem; font-weight: 500; cursor: pointer; - transition: border-color 0.15s, color 0.15s; + transition: border-color 0.15s, color 0.15s, background 0.15s; } .btn-secondary:hover:not(:disabled) { @@ -162,6 +436,12 @@ color: #6d4aff; } +.btn-secondary.active { + border-color: #6d4aff; + background: #f5f3ff; + color: #6d4aff; +} + .btn-danger { padding: 9px 18px; border: none; @@ -222,11 +502,38 @@ overflow-y: auto; } +.view-header { + margin-bottom: 20px; +} + .view-title { font-size: 1.75rem; font-weight: 600; color: #1c1f23; - margin: 0 0 20px 0; + margin: 0 0 10px 0; +} + +.view-badges { + display: flex; + gap: 8px; +} + +.badge-pinned, +.badge-archived { + font-size: 0.75rem; + padding: 4px 10px; + border-radius: 12px; + font-weight: 500; +} + +.badge-pinned { + background: #dbeafe; + color: #1e40af; +} + +.badge-archived { + background: #e5e7eb; + color: #464c52; } .view-content { @@ -263,6 +570,7 @@ color: #464c52; font-weight: 500; font-size: 1.125rem; + text-align: center; } .error-message { @@ -285,22 +593,26 @@ } /* Scrollbar styling */ +.folders-list::-webkit-scrollbar, .notes-list::-webkit-scrollbar, .viewer::-webkit-scrollbar { width: 6px; } +.folders-list::-webkit-scrollbar-track, .notes-list::-webkit-scrollbar-track, .viewer::-webkit-scrollbar-track { background: #f8f9fa; } +.folders-list::-webkit-scrollbar-thumb, .notes-list::-webkit-scrollbar-thumb, .viewer::-webkit-scrollbar-thumb { background: #c1c7cd; border-radius: 3px; } +.folders-list::-webkit-scrollbar-thumb:hover, .notes-list::-webkit-scrollbar-thumb:hover, .viewer::-webkit-scrollbar-thumb:hover { background: #9ca3af; diff --git a/web/src/components/Notes.tsx b/web/src/components/Notes.tsx index 4dc673d..8ef9183 100644 --- a/web/src/components/Notes.tsx +++ b/web/src/components/Notes.tsx @@ -1,39 +1,57 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { loadAuthState } from '../lib/auth'; import { encryptBlob, decryptBlob } from '../lib/crypto'; import { upsertBlob, getBlob } from '../lib/api'; import './Notes.css'; -interface Note { +type Note = { id: string; title: string; - content: string; + content: string; // v1: plaintext/markdown createdAt: number; updatedAt: number; -} + pinned?: boolean; + archived?: boolean; +}; -interface NotesData { +type NotesFolder = { + id: string; + title: string; + createdAt: number; + updatedAt: number; + archived?: boolean; notes: Note[]; -} +}; -const BLOB_NAME = 'notes'; +type NotesData = { + folders: NotesFolder[]; +}; + +const BLOB_NAME = 'notes2'; export default function Notes() { - const [notes, setNotes] = useState([]); + const [folders, setFolders] = useState([]); + const [selectedFolder, setSelectedFolder] = useState(null); const [selectedNote, setSelectedNote] = useState(null); - const [title, setTitle] = useState(''); - const [content, setContent] = useState(''); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); + + // Folder management + const [isCreatingFolder, setIsCreatingFolder] = useState(false); + const [newFolderTitle, setNewFolderTitle] = useState(''); + const [isRenamingFolder, setIsRenamingFolder] = useState(false); + const [renameFolderTitle, setRenameFolderTitle] = useState(''); + + // Note editing const [isEditing, setIsEditing] = useState(false); + const [editTitle, setEditTitle] = useState(''); + const [editContent, setEditContent] = useState(''); + + // View filters + const [showArchived, setShowArchived] = useState(false); - // Load notes on mount - useEffect(() => { - loadNotes(); - }, []); - - const loadNotes = async () => { + const loadFolders = useCallback(async () => { setLoading(true); setError(''); @@ -41,7 +59,6 @@ export default function Notes() { const authState = loadAuthState(); if (!authState) throw new Error('Not authenticated'); - // Try to fetch existing notes blob try { const response = await getBlob(authState.token, BLOB_NAME); const decrypted = await decryptBlob( @@ -51,29 +68,33 @@ export default function Notes() { ); const data: NotesData = JSON.parse(decrypted); - setNotes(data.notes || []); + const sorted = (data.folders || []).sort((a, b) => b.updatedAt - a.updatedAt); + setFolders(sorted); } catch (err) { - // Blob doesn't exist yet (404), start with empty notes const error = err as { status?: number }; if (error.status === 404) { - setNotes([]); + setFolders([]); } else { throw err; } } } catch (err) { const error = err as Error; - setError(error.message || 'Failed to load notes'); + setError(error.message || 'Failed to load folders'); } finally { setLoading(false); } - }; + }, []); - const saveNotes = async (updatedNotes: Note[]) => { + useEffect(() => { + loadFolders(); + }, [loadFolders]); + + const saveFolders = async (updatedFolders: NotesFolder[]) => { const authState = loadAuthState(); if (!authState) throw new Error('Not authenticated'); - const data: NotesData = { notes: updatedNotes }; + const data: NotesData = { folders: updatedFolders }; const encrypted = await encryptBlob( data, authState.accountKey, @@ -85,17 +106,165 @@ export default function Notes() { }); }; + // Folder operations + const handleCreateFolder = () => { + setIsCreatingFolder(true); + setNewFolderTitle(''); + }; + + const handleSaveNewFolder = async () => { + if (!newFolderTitle.trim()) { + setError('Folder title is required'); + return; + } + + setSaving(true); + setError(''); + + try { + const now = Date.now(); + const newFolder: NotesFolder = { + id: crypto.randomUUID(), + title: newFolderTitle, + notes: [], + createdAt: now, + updatedAt: now, + }; + + const updatedFolders = [newFolder, ...folders]; + await saveFolders(updatedFolders); + setFolders(updatedFolders); + setSelectedFolder(newFolder); + setIsCreatingFolder(false); + setNewFolderTitle(''); + } catch (err) { + const error = err as Error; + setError(error.message || 'Failed to create folder'); + } finally { + setSaving(false); + } + }; + + const handleCancelNewFolder = () => { + setIsCreatingFolder(false); + setNewFolderTitle(''); + }; + + const handleSelectFolder = (folder: NotesFolder) => { + setSelectedFolder(folder); + setSelectedNote(null); + setIsEditing(false); + }; + + const handleStartRenameFolder = () => { + if (!selectedFolder) return; + setIsRenamingFolder(true); + setRenameFolderTitle(selectedFolder.title); + }; + + const handleSaveRenameFolder = async () => { + if (!selectedFolder) return; + if (!renameFolderTitle.trim()) { + setError('Folder title is required'); + return; + } + + setSaving(true); + setError(''); + + try { + const now = Date.now(); + const updatedFolder = { + ...selectedFolder, + title: renameFolderTitle, + updatedAt: now, + }; + + const updatedFolders = folders.map((f) => + f.id === selectedFolder.id ? updatedFolder : f + ).sort((a, b) => b.updatedAt - a.updatedAt); + + await saveFolders(updatedFolders); + setFolders(updatedFolders); + setSelectedFolder(updatedFolder); + setIsRenamingFolder(false); + setRenameFolderTitle(''); + } catch (err) { + const error = err as Error; + setError(error.message || 'Failed to rename folder'); + } finally { + setSaving(false); + } + }; + + const handleCancelRenameFolder = () => { + setIsRenamingFolder(false); + setRenameFolderTitle(''); + }; + + const handleToggleArchiveFolder = async () => { + if (!selectedFolder) return; + + setSaving(true); + setError(''); + + try { + const now = Date.now(); + const updatedFolder = { + ...selectedFolder, + archived: !selectedFolder.archived, + updatedAt: now, + }; + + const updatedFolders = folders.map((f) => + f.id === selectedFolder.id ? updatedFolder : f + ).sort((a, b) => b.updatedAt - a.updatedAt); + + await saveFolders(updatedFolders); + setFolders(updatedFolders); + setSelectedFolder(updatedFolder); + } catch (err) { + const error = err as Error; + setError(error.message || 'Failed to archive folder'); + } finally { + setSaving(false); + } + }; + + const handleDeleteFolder = async () => { + if (!selectedFolder) return; + if (!confirm(`Delete folder "${selectedFolder.title}"? This will delete all notes inside.`)) return; + + setSaving(true); + setError(''); + + try { + const updatedFolders = folders.filter((f) => f.id !== selectedFolder.id); + await saveFolders(updatedFolders); + setFolders(updatedFolders); + setSelectedFolder(null); + setSelectedNote(null); + } catch (err) { + const error = err as Error; + setError(error.message || 'Failed to delete folder'); + } finally { + setSaving(false); + } + }; + + // Note operations const handleCreateNote = () => { + if (!selectedFolder) return; setIsEditing(true); setSelectedNote(null); - setTitle(''); - setContent(''); + setEditTitle(''); + setEditContent(''); }; const handleSelectNote = (note: Note) => { setSelectedNote(note); - setTitle(note.title); - setContent(note.content); + setEditTitle(note.title); + setEditContent(note.content); setIsEditing(false); }; @@ -103,9 +272,10 @@ export default function Notes() { setIsEditing(true); }; - const handleSave = async () => { - if (!title.trim()) { - setError('Title is required'); + const handleSaveNote = async () => { + if (!selectedFolder) return; + if (!editTitle.trim()) { + setError('Note title is required'); return; } @@ -118,28 +288,39 @@ export default function Notes() { if (selectedNote) { // Update existing note - updatedNotes = notes.map((note) => + updatedNotes = selectedFolder.notes.map((note) => note.id === selectedNote.id - ? { ...note, title, content, updatedAt: now } + ? { ...note, title: editTitle, content: editContent, updatedAt: now } : note ); } else { // Create new note const newNote: Note = { id: crypto.randomUUID(), - title, - content, + title: editTitle, + content: editContent, createdAt: now, updatedAt: now, }; - updatedNotes = [newNote, ...notes]; + updatedNotes = [newNote, ...selectedFolder.notes]; } - await saveNotes(updatedNotes); - setNotes(updatedNotes); + const updatedFolder = { + ...selectedFolder, + notes: updatedNotes, + updatedAt: now, + }; + + const updatedFolders = folders.map((f) => + f.id === selectedFolder.id ? updatedFolder : f + ).sort((a, b) => b.updatedAt - a.updatedAt); + + await saveFolders(updatedFolders); + setFolders(updatedFolders); + setSelectedFolder(updatedFolder); // Select the saved note - const savedNote = updatedNotes.find((n) => n.title === title && n.content === content); + const savedNote = updatedNotes.find((n) => n.title === editTitle && n.content === editContent); if (savedNote) { setSelectedNote(savedNote); } @@ -152,44 +333,138 @@ export default function Notes() { } }; - const handleDelete = async () => { - if (!selectedNote) return; - if (!confirm('Delete this note?')) return; + const handleCancelEdit = () => { + if (selectedNote) { + setEditTitle(selectedNote.title); + setEditContent(selectedNote.content); + setIsEditing(false); + } else { + setEditTitle(''); + setEditContent(''); + setIsEditing(false); + } + }; + + const handleTogglePinNote = async () => { + if (!selectedFolder || !selectedNote) return; setSaving(true); setError(''); try { - const updatedNotes = notes.filter((note) => note.id !== selectedNote.id); - await saveNotes(updatedNotes); - setNotes(updatedNotes); - setSelectedNote(null); - setTitle(''); - setContent(''); - setIsEditing(false); + const now = Date.now(); + const updatedNotes = selectedFolder.notes.map((note) => + note.id === selectedNote.id + ? { ...note, pinned: !note.pinned, updatedAt: now } + : note + ); + + const updatedFolder = { + ...selectedFolder, + notes: updatedNotes, + updatedAt: now, + }; + + const updatedFolders = folders.map((f) => + f.id === selectedFolder.id ? updatedFolder : f + ).sort((a, b) => b.updatedAt - a.updatedAt); + + await saveFolders(updatedFolders); + setFolders(updatedFolders); + setSelectedFolder(updatedFolder); + + const updatedNote = updatedNotes.find((n) => n.id === selectedNote.id); + if (updatedNote) { + setSelectedNote(updatedNote); + } } catch (err) { const error = err as Error; - setError(error.message || 'Failed to delete note'); + setError(error.message || 'Failed to pin note'); } finally { setSaving(false); } }; - const handleCancel = () => { - if (selectedNote) { - setTitle(selectedNote.title); - setContent(selectedNote.content); - setIsEditing(false); - } else { - setTitle(''); - setContent(''); + const handleToggleArchiveNote = async () => { + if (!selectedFolder || !selectedNote) return; + + setSaving(true); + setError(''); + + try { + const now = Date.now(); + const updatedNotes = selectedFolder.notes.map((note) => + note.id === selectedNote.id + ? { ...note, archived: !note.archived, updatedAt: now } + : note + ); + + const updatedFolder = { + ...selectedFolder, + notes: updatedNotes, + updatedAt: now, + }; + + const updatedFolders = folders.map((f) => + f.id === selectedFolder.id ? updatedFolder : f + ).sort((a, b) => b.updatedAt - a.updatedAt); + + await saveFolders(updatedFolders); + setFolders(updatedFolders); + setSelectedFolder(updatedFolder); + + const updatedNote = updatedNotes.find((n) => n.id === selectedNote.id); + if (updatedNote) { + setSelectedNote(updatedNote); + } + } catch (err) { + const error = err as Error; + setError(error.message || 'Failed to archive note'); + } finally { + setSaving(false); + } + }; + + const handleDeleteNote = async () => { + if (!selectedFolder || !selectedNote) return; + if (!confirm('Delete this note?')) return; + + setSaving(true); + setError(''); + + try { + const now = Date.now(); + const updatedNotes = selectedFolder.notes.filter((note) => note.id !== selectedNote.id); + + const updatedFolder = { + ...selectedFolder, + notes: updatedNotes, + updatedAt: now, + }; + + const updatedFolders = folders.map((f) => + f.id === selectedFolder.id ? updatedFolder : f + ).sort((a, b) => b.updatedAt - a.updatedAt); + + await saveFolders(updatedFolders); + setFolders(updatedFolders); + setSelectedFolder(updatedFolder); + setSelectedNote(null); + setEditTitle(''); + setEditContent(''); setIsEditing(false); + } catch (err) { + const error = err as Error; + setError(error.message || 'Failed to delete note'); + } finally { + setSaving(false); } }; + // Export/Import const handleExport = () => { try { - const exportData: NotesData = { notes }; + const exportData: NotesData = { folders }; const dataStr = JSON.stringify(exportData, null, 2); const dataBlob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(dataBlob); @@ -221,34 +496,25 @@ export default function Notes() { const text = await file.text(); const importData: NotesData = JSON.parse(text); - if (!importData.notes || !Array.isArray(importData.notes)) { + if (!importData.folders || !Array.isArray(importData.folders)) { throw new Error('Invalid notes data format'); } - // Validate data structure - for (const note of importData.notes) { - if (!note.id || !note.title || note.content === undefined || - !note.createdAt || !note.updatedAt) { - throw new Error('Invalid note format in import file'); - } - } - - if (!confirm(`Import ${importData.notes.length} notes? This will replace all existing notes.`)) { + if (!confirm(`Import ${importData.folders.length} folders? This will replace all existing data.`)) { return; } setSaving(true); setError(''); - // Save imported notes (overwrites existing data) - await saveNotes(importData.notes); - setNotes(importData.notes); + const sorted = importData.folders.sort((a, b) => b.updatedAt - a.updatedAt); + await saveFolders(sorted); + setFolders(sorted); + setSelectedFolder(null); setSelectedNote(null); - setTitle(''); - setContent(''); setIsEditing(false); - alert(`Successfully imported ${importData.notes.length} notes!`); + alert(`Successfully imported ${sorted.length} folders!`); } catch (err) { const error = err as Error; setError(error.message || 'Failed to import notes'); @@ -260,6 +526,32 @@ export default function Notes() { input.click(); }; + // Helper functions + const getVisibleFolders = () => { + if (showArchived) { + return folders; + } + return folders.filter(f => !f.archived); + }; + + const getVisibleNotes = () => { + if (!selectedFolder) return []; + + let notes = showArchived ? selectedFolder.notes : selectedFolder.notes.filter(n => !n.archived); + + // Sort: pinned first, then by updatedAt + return notes.sort((a, b) => { + if (a.pinned && !b.pinned) return -1; + if (!a.pinned && b.pinned) return 1; + return b.updatedAt - a.updatedAt; + }); + }; + + const getFolderNotesCount = (folder: NotesFolder) => { + const activeNotes = folder.notes.filter(n => !n.archived); + return activeNotes.length; + }; + if (loading) { return (
@@ -268,13 +560,17 @@ export default function Notes() { ); } + const visibleFolders = getVisibleFolders(); + const visibleNotes = getVisibleNotes(); + return (
+ {/* Left Sidebar - Folders */}

Notes

-
@@ -286,50 +582,211 @@ export default function Notes() { 📤 Import
+ +
+ +
+ + {isCreatingFolder && ( +
+ setNewFolderTitle(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter' && newFolderTitle.trim()) { + handleSaveNewFolder(); + } + }} + autoFocus + /> +
+ + +
+
+ )} -
- {notes.length === 0 ? ( -
No notes yet. Create one!
+
+ {visibleFolders.length === 0 ? ( +
+ {showArchived ? 'No folders yet.' : 'No active folders. Create one!'} +
) : ( - notes.map((note) => ( + visibleFolders.map((folder) => (
handleSelectNote(note)} + key={folder.id} + className={`folder-item ${selectedFolder?.id === folder.id ? 'active' : ''} ${folder.archived ? 'archived' : ''}`} + onClick={() => handleSelectFolder(folder)} > -
{note.title}
-
- {note.content.substring(0, 60)} - {note.content.length > 60 ? '...' : ''} -
-
- {new Date(note.updatedAt).toLocaleDateString()} +
+ {folder.archived && 📦} + {folder.title}
+
{getFolderNotesCount(folder)} notes
)) )}
+ {/* Middle Panel - Notes List */} + {selectedFolder ? ( +
+
+ {isRenamingFolder ? ( +
+ setRenameFolderTitle(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter' && renameFolderTitle.trim()) { + handleSaveRenameFolder(); + } + }} + autoFocus + /> +
+ + +
+
+ ) : ( + <> +
+

{selectedFolder.title}

+ {selectedFolder.archived && Archived} +
+
+ + + +
+ + )} +
+ +
+ +
+ +
+ {visibleNotes.length === 0 ? ( +
+ {showArchived ? 'No notes in this folder.' : 'No active notes. Create one!'} +
+ ) : ( + visibleNotes.map((note) => ( +
handleSelectNote(note)} + > +
+
+ {note.pinned && 📌} + {note.archived && 📦} + {note.title} +
+
+
+ {note.content.substring(0, 80)} + {note.content.length > 80 ? '...' : ''} +
+
+ {new Date(note.updatedAt).toLocaleDateString()} +
+
+ )) + )} +
+
+ ) : null} + + {/* Right Panel - Note Content */}
{selectedNote || isEditing ? ( <>
{isEditing ? ( <> - - ) : ( <> - - + + +
+ @@ -344,19 +801,25 @@ export default function Notes() { type="text" className="title-input" placeholder="Note title..." - value={title} - onChange={(e) => setTitle(e.target.value)} + value={editTitle} + onChange={(e) => setEditTitle(e.target.value)} />