Skip to content

Commit a3f96f9

Browse files
Merge pull request #147 from Glynis1314/feature/form-enchancement
Enchanced form ui
2 parents 11dafef + 9381570 commit a3f96f9

File tree

3 files changed

+738
-264
lines changed

3 files changed

+738
-264
lines changed

projects/notes/index.html

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,93 @@
1313
<h1>Create Note</h1>
1414
<button class="toggle-btn" id="themeToggle">🌙</button>
1515
</div>
16-
<form id="form">
17-
<input type="text" id="title" placeholder="Title" required>
18-
<input type="text" id="tag" placeholder="Tag (optional)">
19-
<textarea id="content" placeholder="Write your note..." required></textarea>
20-
<button type="submit">Add Note</button>
21-
</form>
16+
17+
<!-- Enhanced Form -->
18+
<div class="notes-form-container">
19+
<form id="form" class="notes-form" aria-labelledby="form-title">
20+
<h2 id="form-title" class="form-title">Add New Note</h2>
21+
22+
<div class="form-group">
23+
<label for="title" class="form-label">
24+
Title
25+
<span class="required-indicator" aria-hidden="true">*</span>
26+
</label>
27+
<input
28+
type="text"
29+
id="title"
30+
class="form-input"
31+
placeholder="Enter note title"
32+
aria-required="true"
33+
required
34+
maxlength="100"
35+
>
36+
<div class="character-count">
37+
<span id="title-count">0</span>/100
38+
</div>
39+
</div>
40+
41+
<div class="form-group">
42+
<label for="tag" class="form-label">Tag</label>
43+
<select id="tag" class="form-select">
44+
<option value="">Select a tag (optional)</option>
45+
<option value="personal">Personal</option>
46+
<option value="work">Work</option>
47+
<option value="ideas">Ideas</option>
48+
<option value="important">Important</option>
49+
<option value="study">Study</option>
50+
</select>
51+
</div>
52+
53+
<div class="form-group">
54+
<label for="content" class="form-label">
55+
Content
56+
<span class="required-indicator" aria-hidden="true">*</span>
57+
</label>
58+
<textarea
59+
id="content"
60+
class="form-textarea"
61+
placeholder="Write your note here..."
62+
aria-required="true"
63+
required
64+
maxlength="1000"
65+
rows="5"
66+
></textarea>
67+
<div class="character-count">
68+
<span id="content-count">0</span>/1000
69+
</div>
70+
</div>
71+
72+
<div class="form-actions">
73+
<button type="submit" class="btn-primary">
74+
Add Note
75+
</button>
76+
<button type="button" class="btn-secondary" id="clear-form">
77+
Clear
78+
</button>
79+
</div>
80+
</form>
81+
</div>
2282
</div>
83+
2384
<div class="right-panel">
2485
<h1>My Notes</h1>
2586
<div class="notes-header">
2687
<input type="text" id="search" placeholder="Search notes...">
2788
</div>
2889
<div class="filter-bar">
2990
<label>Filter by tag:</label>
30-
<select id="tagFilter"></select>
91+
<select id="tagFilter">
92+
<option value="">All tags</option>
93+
<option value="personal">Personal</option>
94+
<option value="work">Work</option>
95+
<option value="ideas">Ideas</option>
96+
<option value="study">Study</option>
97+
</select>
3198
</div>
3299
<div class="grid" id="notes"></div>
33100
</div>
34101
</div>
102+
35103
<script src="main.js"></script>
36104
</body>
37-
</html>
105+
</html>

projects/notes/main.js

Lines changed: 180 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -6,113 +6,212 @@ const grid = document.getElementById('notes');
66
const search = document.getElementById('search');
77
const tagFilter = document.getElementById('tagFilter');
88
const 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

1013
let notes = JSON.parse(localStorage.getItem('notes')) || [];
1114
let currentTheme = localStorage.getItem('theme') || 'dark';
1215

16+
// Initialize app
1317
document.body.classList.toggle('light', currentTheme === 'light');
1418
updateThemeIcon();
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
1630
form.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+
3584
function saveNotes() {
36-
localStorage.setItem('notes', JSON.stringify(notes));
85+
localStorage.setItem('notes', JSON.stringify(notes));
3786
}
3887

3988
function 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, "&amp;")
158+
.replace(/</g, "&lt;")
159+
.replace(/>/g, "&gt;")
160+
.replace(/"/g, "&quot;")
161+
.replace(/'/g, "&#039;");
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

106177
themeToggle.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

113184
function 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
117216
render();
118217
populateTags();

0 commit comments

Comments
 (0)