Skip to content

Commit 6d6d74d

Browse files
author
User
committed
refactor: modularize components and improve state management
1 parent 7f9c9cc commit 6d6d74d

File tree

11 files changed

+1627
-305
lines changed

11 files changed

+1627
-305
lines changed

src/App.jsx

Lines changed: 296 additions & 305 deletions
Large diffs are not rendered by default.

src/components/Footer/Footer.jsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useNotes } from '../../context/NoteContext';
2+
3+
export default function Footer() {
4+
const { notes } = useNotes();
5+
const currentYear = new Date().getFullYear();
6+
7+
const handleKeyboardShortcuts = (e) => {
8+
e.preventDefault();
9+
// Show keyboard shortcuts modal
10+
alert('Keyboard Shortcuts:\n\n' +
11+
'• Ctrl+N: New Note\n' +
12+
'• Ctrl+S: Save (auto-saves)\n' +
13+
'• Ctrl+D: Toggle Dark Mode\n' +
14+
'• F11: Toggle Fullscreen');
15+
};
16+
17+
return (
18+
<footer className="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 py-2 px-4">
19+
<div className="max-w-7xl mx-auto flex justify-between items-center">
20+
<p className="text-xs text-gray-500 dark:text-gray-400">
21+
{notes.length} note{notes.length !== 1 ? 's' : ''}{currentYear} Markdown Notes
22+
</p>
23+
<div className="flex items-center space-x-4">
24+
<button
25+
onClick={handleKeyboardShortcuts}
26+
className="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors focus:outline-none"
27+
aria-label="View keyboard shortcuts"
28+
>
29+
Keyboard Shortcuts
30+
</button>
31+
<a
32+
href="https://github.com/yourusername/markdown-notes"
33+
target="_blank"
34+
rel="noopener noreferrer"
35+
className="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
36+
aria-label="View on GitHub"
37+
>
38+
GitHub
39+
</a>
40+
</div>
41+
</div>
42+
</footer>
43+
);
44+
}

src/components/Header/Header.jsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { FiPlus } from 'react-icons/fi';
2+
import { useNotes } from '../../context/NoteContext';
3+
import { useTheme } from '../../context/ThemeContext';
4+
import ThemeSwitcher from '../ThemeSwitcher/ThemeSwitcher';
5+
6+
export default function Header() {
7+
const { createNewNote } = useNotes();
8+
const { currentTheme } = useTheme();
9+
10+
return (
11+
<header className="bg-white dark:bg-gray-800 shadow-sm z-10">
12+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
13+
<div className="flex justify-between h-16 items-center">
14+
<div className="flex items-center">
15+
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Markdown Notes</h1>
16+
</div>
17+
<div className="flex items-center space-x-4">
18+
<button
19+
onClick={createNewNote}
20+
className={`inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white ${currentTheme.color} hover:${currentTheme.color.replace('600', '700')} focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-${currentTheme.color.replace('600', '500')}`}
21+
>
22+
<FiPlus className="mr-2 h-4 w-4" />
23+
New Note
24+
</button>
25+
<ThemeSwitcher />
26+
</div>
27+
</div>
28+
</div>
29+
</header>
30+
);
31+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { useMemo } from 'react';
2+
import ReactMarkdown from 'react-markdown';
3+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
4+
import { vscDarkPlus, vs } from 'react-syntax-highlighter/dist/esm/styles/prism';
5+
import { useTheme } from '../../context/ThemeContext';
6+
7+
// Custom components for markdown rendering
8+
const components = {
9+
code({ node, inline, className, children, ...props }) {
10+
const match = /language-(\w+)/.exec(className || '');
11+
return !inline && match ? (
12+
<SyntaxHighlighter
13+
style={vscDarkPlus}
14+
language={match[1]}
15+
PreTag="div"
16+
{...props}
17+
className="rounded-md"
18+
>
19+
{String(children).replace(/\n$/, '')}
20+
</SyntaxHighlighter>
21+
) : (
22+
<code className={className} {...props}>
23+
{children}
24+
</code>
25+
);
26+
},
27+
a({ node, ...props }) {
28+
return (
29+
<a
30+
{...props}
31+
target="_blank"
32+
rel="noopener noreferrer"
33+
className="text-blue-600 dark:text-blue-400 hover:underline"
34+
/>
35+
);
36+
},
37+
h1: ({ node, ...props }) => <h1 className="text-3xl font-bold my-4" {...props} />,
38+
h2: ({ node, ...props }) => <h2 className="text-2xl font-bold my-3 border-b border-gray-200 dark:border-gray-700 pb-1" {...props} />,
39+
h3: ({ node, ...props }) => <h3 className="text-xl font-bold my-2" {...props} />,
40+
h4: ({ node, ...props }) => <h4 className="text-lg font-bold my-2" {...props} />,
41+
p: ({ node, ...props }) => <p className="my-3 leading-relaxed" {...props} />,
42+
ul: ({ node, ...props }) => <ul className="list-disc pl-6 my-2 space-y-1" {...props} />,
43+
ol: ({ node, ...props }) => <ol className="list-decimal pl-6 my-2 space-y-1" {...props} />,
44+
li: ({ node, ...props }) => <li className="my-1" {...props} />,
45+
blockquote: ({ node, ...props }) => (
46+
<blockquote
47+
className="border-l-4 border-gray-300 dark:border-gray-600 pl-4 italic my-4 text-gray-700 dark:text-gray-300"
48+
{...props}
49+
/>
50+
),
51+
table: ({ node, ...props }) => (
52+
<div className="overflow-x-auto my-4">
53+
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700" {...props} />
54+
</div>
55+
),
56+
thead: ({ node, ...props }) => (
57+
<thead className="bg-gray-50 dark:bg-gray-800" {...props} />
58+
),
59+
tbody: ({ node, ...props }) => (
60+
<tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700" {...props} />
61+
),
62+
th: ({ node, ...props }) => (
63+
<th
64+
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
65+
{...props}
66+
/>
67+
),
68+
td: ({ node, ...props }) => (
69+
<td
70+
className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-200"
71+
{...props}
72+
/>
73+
),
74+
hr: ({ node, ...props }) => (
75+
<hr className="my-4 border-t border-gray-200 dark:border-gray-700" {...props} />
76+
),
77+
};
78+
79+
export default function MarkdownPreview() {
80+
const { activeNote } = useNotes();
81+
const { isDarkMode } = useTheme();
82+
83+
// Memoize the markdown content to prevent unnecessary re-renders
84+
const markdownContent = useMemo(() => {
85+
if (!activeNote) return null;
86+
87+
return (
88+
<div className="prose dark:prose-invert max-w-none p-6">
89+
<ReactMarkdown components={components}>
90+
{activeNote.content}
91+
</ReactMarkdown>
92+
</div>
93+
);
94+
}, [activeNote]);
95+
96+
if (!activeNote) {
97+
return (
98+
<div className="flex-1 flex items-center justify-center bg-white dark:bg-gray-900 text-gray-500 dark:text-gray-400">
99+
<p>Select a note to preview</p>
100+
</div>
101+
);
102+
}
103+
104+
return (
105+
<div className="flex-1 flex flex-col overflow-hidden bg-white dark:bg-gray-900">
106+
<div className="px-4 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
107+
<span className="text-xs font-medium text-gray-500 dark:text-gray-400">PREVIEW</span>
108+
</div>
109+
<div className="flex-1 overflow-y-auto">
110+
{markdownContent}
111+
</div>
112+
</div>
113+
);
114+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { useState, useEffect, useRef, useCallback } from 'react';
2+
import { FiEdit2, FiSave, FiX } from 'react-icons/fi';
3+
import { useNotes } from '../../context/NoteContext';
4+
5+
export default function NoteEditor() {
6+
const { activeNote, updateNoteContent, updateNoteTitle } = useNotes();
7+
const [content, setContent] = useState('');
8+
const [isEditingTitle, setIsEditingTitle] = useState(false);
9+
const [title, setTitle] = useState('');
10+
const titleInputRef = useRef(null);
11+
const textareaRef = useRef(null);
12+
13+
// Update local state when active note changes
14+
useEffect(() => {
15+
if (activeNote) {
16+
setContent(activeNote.content || '');
17+
setTitle(activeNote.title || 'Untitled Note');
18+
}
19+
}, [activeNote]);
20+
21+
// Focus the editor when active note changes
22+
useEffect(() => {
23+
if (activeNote && textareaRef.current) {
24+
textareaRef.current.focus();
25+
}
26+
}, [activeNote]);
27+
28+
// Handle content changes with debounce
29+
useEffect(() => {
30+
if (!activeNote) return;
31+
32+
const timer = setTimeout(() => {
33+
if (content !== activeNote.content) {
34+
updateNoteContent(activeNote.id, content);
35+
}
36+
}, 300);
37+
38+
return () => clearTimeout(timer);
39+
}, [content, activeNote, updateNoteContent]);
40+
41+
// Handle title update
42+
const handleTitleUpdate = useCallback(() => {
43+
if (!activeNote) return;
44+
45+
const newTitle = title.trim() || 'Untitled Note';
46+
updateNoteTitle(activeNote.id, newTitle);
47+
setIsEditingTitle(false);
48+
}, [activeNote, title, updateNoteTitle]);
49+
50+
// Handle keyboard shortcuts
51+
const handleKeyDown = useCallback((e) => {
52+
// Save: Ctrl+S or Cmd+S
53+
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
54+
e.preventDefault();
55+
// Show save indicator
56+
const saveBtn = document.querySelector('[aria-label="Save note"]');
57+
if (saveBtn) {
58+
saveBtn.classList.add('animate-ping');
59+
setTimeout(() => saveBtn.classList.remove('animate-ping'), 1000);
60+
}
61+
}
62+
63+
// New line with Shift+Enter
64+
if (e.key === 'Enter' && e.shiftKey) {
65+
e.preventDefault();
66+
const start = e.target.selectionStart;
67+
const end = e.target.selectionEnd;
68+
const newValue = content.substring(0, start) + '\n' + content.substring(end);
69+
setContent(newValue);
70+
71+
// Move cursor to the new line
72+
setTimeout(() => {
73+
textareaRef.current.selectionStart = start + 1;
74+
textareaRef.current.selectionEnd = start + 1;
75+
}, 0);
76+
}
77+
}, [content]);
78+
79+
if (!activeNote) {
80+
return (
81+
<div className="flex-1 flex items-center justify-center bg-white dark:bg-gray-900 text-gray-500 dark:text-gray-400">
82+
<p>Select a note or create a new one</p>
83+
</div>
84+
);
85+
}
86+
87+
return (
88+
<div className="flex-1 flex flex-col border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
89+
{/* Editor header */}
90+
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
91+
<div className="flex items-center justify-between">
92+
<div className="flex-1 min-w-0">
93+
{isEditingTitle ? (
94+
<div className="flex items-center">
95+
<input
96+
ref={titleInputRef}
97+
type="text"
98+
className="text-xl font-semibold bg-transparent border-b border-blue-500 focus:outline-none focus:ring-0 w-full text-gray-900 dark:text-white"
99+
value={title}
100+
onChange={(e) => setTitle(e.target.value)}
101+
onBlur={handleTitleUpdate}
102+
onKeyDown={(e) => {
103+
if (e.key === 'Enter') {
104+
e.preventDefault();
105+
handleTitleUpdate();
106+
} else if (e.key === 'Escape') {
107+
setIsEditingTitle(false);
108+
setTitle(activeNote.title);
109+
}
110+
}}
111+
autoFocus
112+
/>
113+
<button
114+
onClick={() => {
115+
setIsEditingTitle(false);
116+
setTitle(activeNote.title);
117+
}}
118+
className="ml-2 p-1 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 rounded-full hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
119+
aria-label="Cancel editing"
120+
>
121+
<FiX className="h-4 w-4" />
122+
</button>
123+
</div>
124+
) : (
125+
<div className="flex items-center group">
126+
<h2
127+
className="text-xl font-semibold text-gray-900 dark:text-white cursor-text"
128+
onClick={() => setIsEditingTitle(true)}
129+
>
130+
{activeNote.title}
131+
</h2>
132+
<button
133+
onClick={() => {
134+
setIsEditingTitle(true);
135+
setTimeout(() => titleInputRef.current?.focus(), 0);
136+
}}
137+
className="ml-2 p-1 opacity-0 group-hover:opacity-100 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 rounded-full hover:bg-gray-200 dark:hover:bg-gray-600 transition-all"
138+
aria-label="Edit title"
139+
>
140+
<FiEdit2 className="h-4 w-4" />
141+
</button>
142+
</div>
143+
)}
144+
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
145+
Last updated: {new Date(activeNote.updatedAt).toLocaleString()}
146+
</p>
147+
</div>
148+
<div className="ml-4 flex items-center">
149+
<button
150+
onClick={() => {
151+
// Export as Markdown
152+
const blob = new Blob([activeNote.content], { type: 'text/markdown' });
153+
const url = URL.createObjectURL(blob);
154+
const a = document.createElement('a');
155+
a.href = url;
156+
a.download = `${activeNote.title.replace(/\s+/g, '_').toLowerCase()}.md`;
157+
document.body.appendChild(a);
158+
a.click();
159+
document.body.removeChild(a);
160+
URL.revokeObjectURL(url);
161+
}}
162+
className="inline-flex items-center px-3 py-2 border border-gray-300 dark:border-gray-600 text-sm leading-4 font-medium rounded-md text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
163+
title="Export as Markdown"
164+
aria-label="Export note as Markdown file"
165+
>
166+
<FiSave className="mr-2 h-4 w-4" />
167+
Export
168+
</button>
169+
</div>
170+
</div>
171+
</div>
172+
173+
{/* Editor content */}
174+
<div className="flex-1 flex flex-col">
175+
<div className="px-4 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
176+
<span className="text-xs font-medium text-gray-500 dark:text-gray-400">EDITOR</span>
177+
{activeNote && (
178+
<span className="text-xs text-gray-500 dark:text-gray-400">
179+
{content.length} characters • {content.split(/\s+/).filter(Boolean).length} words
180+
</span>
181+
)}
182+
</div>
183+
<div className="flex-1 overflow-auto">
184+
<textarea
185+
ref={textareaRef}
186+
className="w-full h-full p-4 text-gray-900 dark:text-white bg-white dark:bg-gray-900 focus:outline-none resize-none focus:ring-2 focus:ring-blue-500 font-mono text-sm leading-relaxed"
187+
value={content}
188+
onChange={(e) => setContent(e.target.value)}
189+
onKeyDown={handleKeyDown}
190+
placeholder="Start writing your markdown here..."
191+
spellCheck="false"
192+
aria-label="Note content"
193+
/>
194+
</div>
195+
</div>
196+
</div>
197+
);
198+
}

0 commit comments

Comments
 (0)