@@ -6,113 +6,212 @@ const grid = document.getElementById('notes');
66const search = document . getElementById ( 'search' ) ;
77const tagFilter = document . getElementById ( 'tagFilter' ) ;
88const themeToggle = document . getElementById ( 'themeToggle' ) ;
9+ const clearButton = document . getElementById ( 'clear-form' ) ;
10+ const titleCount = document . getElementById ( 'title-count' ) ;
11+ const contentCount = document . getElementById ( 'content-count' ) ;
912
1013let notes = JSON . parse ( localStorage . getItem ( 'notes' ) ) || [ ] ;
1114let currentTheme = localStorage . getItem ( 'theme' ) || 'dark' ;
1215
16+ // Initialize app
1317document . body . classList . toggle ( 'light' , currentTheme === 'light' ) ;
1418updateThemeIcon ( ) ;
1519
20+ // Character counter functionality
21+ title . addEventListener ( 'input' , ( ) => {
22+ titleCount . textContent = title . value . length ;
23+ } ) ;
24+
25+ content . addEventListener ( 'input' , ( ) => {
26+ contentCount . textContent = content . value . length ;
27+ } ) ;
28+
29+ // Enhanced form submission with loading state
1630form . addEventListener ( 'submit' , e => {
17- e . preventDefault ( ) ;
18- const newNote = {
19- id : crypto . randomUUID ( ) ,
20- title : title . value . trim ( ) ,
21- tag : tag . value . trim ( ) || null ,
22- content : content . value . trim ( ) ,
23- created : Date . now ( ) ,
24- pinned : false
25- } ;
26- notes . unshift ( newNote ) ;
27- saveNotes ( ) ;
28- title . value = '' ;
29- tag . value = '' ;
30- content . value = '' ;
31- render ( ) ;
32- populateTags ( ) ;
31+ e . preventDefault ( ) ;
32+
33+ const submitButton = form . querySelector ( '.btn-primary' ) ;
34+ const originalText = submitButton . textContent ;
35+
36+ // Show loading state
37+ submitButton . classList . add ( 'loading' ) ;
38+ submitButton . textContent = 'Adding...' ;
39+ submitButton . disabled = true ;
40+
41+ // Simulate processing delay for better UX
42+ setTimeout ( ( ) => {
43+ const newNote = {
44+ id : crypto . randomUUID ( ) ,
45+ title : title . value . trim ( ) ,
46+ tag : tag . value || null ,
47+ content : content . value . trim ( ) ,
48+ created : Date . now ( ) ,
49+ pinned : false
50+ } ;
51+
52+ notes . unshift ( newNote ) ;
53+ saveNotes ( ) ;
54+
55+ // Reset form with success animation
56+ form . classList . add ( 'submit-success' ) ;
57+ setTimeout ( ( ) => {
58+ form . classList . remove ( 'submit-success' ) ;
59+ resetForm ( ) ;
60+
61+ // Restore button state
62+ submitButton . classList . remove ( 'loading' ) ;
63+ submitButton . textContent = originalText ;
64+ submitButton . disabled = false ;
65+ } , 300 ) ;
66+
67+ render ( ) ;
68+ populateTags ( ) ;
69+ } , 800 ) ;
70+ } ) ;
71+
72+ // Clear form functionality
73+ clearButton . addEventListener ( 'click' , ( ) => {
74+ resetForm ( ) ;
75+ title . focus ( ) ;
3376} ) ;
3477
78+ function resetForm ( ) {
79+ form . reset ( ) ;
80+ titleCount . textContent = '0' ;
81+ contentCount . textContent = '0' ;
82+ }
83+
3584function saveNotes ( ) {
36- localStorage . setItem ( 'notes' , JSON . stringify ( notes ) ) ;
85+ localStorage . setItem ( 'notes' , JSON . stringify ( notes ) ) ;
3786}
3887
3988function render ( filterText = '' , tagFilterValue = '' ) {
40- grid . innerHTML = '' ;
41- let filteredNotes = notes . filter ( n =>
42- ( n . title . toLowerCase ( ) . includes ( filterText . toLowerCase ( ) ) ||
43- ( n . tag && n . tag . toLowerCase ( ) . includes ( filterText . toLowerCase ( ) ) ) ||
44- n . content . toLowerCase ( ) . includes ( filterText . toLowerCase ( ) ) )
45- ) ;
46-
47- if ( tagFilterValue ) {
48- filteredNotes = filteredNotes . filter ( n => n . tag && n . tag . toLowerCase ( ) === tagFilterValue . toLowerCase ( ) ) ;
49- }
50-
51- if ( filteredNotes . length === 0 ) {
52- grid . innerHTML = `<p style="text-align: center; opacity: 0.6;">No notes found.</p>` ;
53- return ;
54- }
55-
56- filteredNotes . sort ( ( a , b ) => b . pinned - a . pinned || b . created - a . created ) ;
57-
58- for ( const n of filteredNotes ) {
59- const card = document . createElement ( 'article' ) ;
60- card . className = 'card' ;
61- const date = new Date ( n . created ) . toLocaleString ( ) ;
62- card . innerHTML = `
63- <div class="card-header">
64- <h3>${ n . title } </h3>
65- <button class="pin">${ n . pinned ? '📌' : '📍' } </button>
66- </div>
67- ${ n . tag ? `<span class="tag">${ n . tag } </span>` : '' }
68- <p>${ n . content } </p>
69- <div class="note-footer">
70- <small>${ date } </small>
71- <button class="del">Delete</button>
72- </div>
73- ` ;
74-
75- card . querySelector ( '.del' ) . addEventListener ( 'click' , ( ) => {
76- notes = notes . filter ( x => x . id !== n . id ) ;
77- saveNotes ( ) ;
78- render ( search . value , tagFilter . value ) ;
79- populateTags ( ) ;
80- } ) ;
89+ grid . innerHTML = '' ;
90+ let filteredNotes = notes . filter ( n =>
91+ ( n . title . toLowerCase ( ) . includes ( filterText . toLowerCase ( ) ) ||
92+ ( n . tag && n . tag . toLowerCase ( ) . includes ( filterText . toLowerCase ( ) ) ) ||
93+ n . content . toLowerCase ( ) . includes ( filterText . toLowerCase ( ) ) )
94+ ) ;
95+
96+ if ( tagFilterValue ) {
97+ filteredNotes = filteredNotes . filter ( n => n . tag && n . tag . toLowerCase ( ) === tagFilterValue . toLowerCase ( ) ) ;
98+ }
99+
100+ if ( filteredNotes . length === 0 ) {
101+ grid . innerHTML = `<p style="text-align: center; opacity: 0.6; grid-column: 1 / -1; padding: 2rem;">No notes found.</p>` ;
102+ return ;
103+ }
104+
105+ filteredNotes . sort ( ( a , b ) => b . pinned - a . pinned || b . created - a . created ) ;
106+
107+ for ( const n of filteredNotes ) {
108+ const card = document . createElement ( 'article' ) ;
109+ card . className = 'card' ;
110+ const date = new Date ( n . created ) . toLocaleString ( ) ;
111+ card . innerHTML = `
112+ <div class="card-header">
113+ <h3>${ escapeHtml ( n . title ) } </h3>
114+ <button class="pin" aria-label="${ n . pinned ? 'Unpin note' : 'Pin note' } ">${ n . pinned ? '📌' : '📍' } </button>
115+ </div>
116+ ${ n . tag ? `<span class="tag">${ escapeHtml ( n . tag ) } </span>` : '' }
117+ <p>${ escapeHtml ( n . content ) } </p>
118+ <div class="note-footer">
119+ <small>${ date } </small>
120+ <button class="del" aria-label="Delete note">Delete</button>
121+ </div>
122+ ` ;
123+
124+ card . querySelector ( '.del' ) . addEventListener ( 'click' , ( ) => {
125+ if ( confirm ( 'Are you sure you want to delete this note?' ) ) {
126+ notes = notes . filter ( x => x . id !== n . id ) ;
127+ saveNotes ( ) ;
128+ render ( search . value , tagFilter . value ) ;
129+ populateTags ( ) ;
130+ }
131+ } ) ;
132+
133+ card . querySelector ( '.pin' ) . addEventListener ( 'click' , ( ) => {
134+ n . pinned = ! n . pinned ;
135+ saveNotes ( ) ;
136+ render ( search . value , tagFilter . value ) ;
137+ } ) ;
138+
139+ grid . appendChild ( card ) ;
140+ }
141+ }
81142
82- card . querySelector ( '.pin' ) . addEventListener ( 'click' , ( ) => {
83- n . pinned = ! n . pinned ;
84- saveNotes ( ) ;
85- render ( search . value , tagFilter . value ) ;
143+ function populateTags ( ) {
144+ const tags = [ ...new Set ( notes . filter ( n => n . tag ) . map ( n => n . tag ) ) ] ;
145+ tagFilter . innerHTML = `<option value="">All Tags</option>` ;
146+ tags . forEach ( t => {
147+ const opt = document . createElement ( 'option' ) ;
148+ opt . value = t ;
149+ opt . textContent = t ;
150+ tagFilter . appendChild ( opt ) ;
86151 } ) ;
87-
88- grid . appendChild ( card ) ;
89- }
90152}
91153
92- function populateTags ( ) {
93- const tags = [ ...new Set ( notes . filter ( n => n . tag ) . map ( n => n . tag ) ) ] ;
94- tagFilter . innerHTML = `<option value="">All Tags</option>` ;
95- tags . forEach ( t => {
96- const opt = document . createElement ( 'option' ) ;
97- opt . value = t ;
98- opt . textContent = t ;
99- tagFilter . appendChild ( opt ) ;
100- } ) ;
154+ // Utility function to prevent XSS
155+ function escapeHtml ( unsafe ) {
156+ return unsafe
157+ . replace ( / & / g, "&" )
158+ . replace ( / < / g, "<" )
159+ . replace ( / > / g, ">" )
160+ . replace ( / " / g, """ )
161+ . replace ( / ' / g, "'" ) ;
101162}
102163
103- search . addEventListener ( 'input' , ( ) => render ( search . value , tagFilter . value ) ) ;
104- tagFilter . addEventListener ( 'change' , ( ) => render ( search . value , tagFilter . value ) ) ;
164+ // Enhanced search with debouncing
165+ let searchTimeout ;
166+ search . addEventListener ( 'input' , ( ) => {
167+ clearTimeout ( searchTimeout ) ;
168+ searchTimeout = setTimeout ( ( ) => {
169+ render ( search . value , tagFilter . value ) ;
170+ } , 300 ) ;
171+ } ) ;
172+
173+ tagFilter . addEventListener ( 'change' , ( ) => {
174+ render ( search . value , tagFilter . value ) ;
175+ } ) ;
105176
106177themeToggle . addEventListener ( 'click' , ( ) => {
107- document . body . classList . toggle ( 'light' ) ;
108- currentTheme = document . body . classList . contains ( 'light' ) ? 'light' : 'dark' ;
109- localStorage . setItem ( 'theme' , currentTheme ) ;
110- updateThemeIcon ( ) ;
178+ document . body . classList . toggle ( 'light' ) ;
179+ currentTheme = document . body . classList . contains ( 'light' ) ? 'light' : 'dark' ;
180+ localStorage . setItem ( 'theme' , currentTheme ) ;
181+ updateThemeIcon ( ) ;
111182} ) ;
112183
113184function updateThemeIcon ( ) {
114- themeToggle . textContent = document . body . classList . contains ( 'light' ) ? '🌞' : '🌙' ;
185+ themeToggle . textContent = document . body . classList . contains ( 'light' ) ? '🌞' : '🌙' ;
115186}
116187
188+ // Focus management - set focus to title input on page load
189+ window . addEventListener ( 'load' , ( ) => {
190+ title . focus ( ) ;
191+ } ) ;
192+
193+ // Keyboard shortcuts
194+ document . addEventListener ( 'keydown' , ( e ) => {
195+ // Ctrl + / to focus search
196+ if ( e . ctrlKey && e . key === '/' ) {
197+ e . preventDefault ( ) ;
198+ search . focus ( ) ;
199+ }
200+
201+ // Escape to clear search
202+ if ( e . key === 'Escape' && document . activeElement === search ) {
203+ search . value = '' ;
204+ render ( '' , tagFilter . value ) ;
205+ }
206+
207+ // Ctrl + K to clear form (when not in input field)
208+ if ( e . ctrlKey && e . key === 'k' && ! [ 'INPUT' , 'TEXTAREA' , 'SELECT' ] . includes ( document . activeElement . tagName ) ) {
209+ e . preventDefault ( ) ;
210+ resetForm ( ) ;
211+ title . focus ( ) ;
212+ }
213+ } ) ;
214+
215+ // Initialize the app
117216render ( ) ;
118217populateTags ( ) ;
0 commit comments