PatchHub

patchhub / notes Public

A tiny notes app that saves in your browser

notes / index.html 11799 bytes · raw
1<!DOCTYPE html>
2<html lang="en">
3<head>
4<meta charset="utf-8">
5<meta name="viewport" content="width=device-width, initial-scale=1">
6<title>Notes</title>
7<style>
8 :root {
9 --bg: #f6f6f4;
10 --panel: #ffffff;
11 --border: #e2e2de;
12 --text: #222;
13 --muted: #888;
14 --accent: #3b6ef5;
15 --selected: #e8eefe;
16 --danger: #d64545;
17 }
18 @media (prefers-color-scheme: dark) {
19 :root {
20 --bg: #1c1c1e;
21 --panel: #242426;
22 --border: #3a3a3c;
23 --text: #eee;
24 --muted: #999;
25 --accent: #6b93ff;
26 --selected: #2f3a55;
27 --danger: #ff6b6b;
28 }
29 }
30 * { box-sizing: border-box; }
31 html, body { height: 100%; margin: 0; }
32 body {
33 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
34 background: var(--bg);
35 color: var(--text);
36 display: flex;
37 }
38 button {
39 font: inherit;
40 cursor: pointer;
41 border: 1px solid var(--border);
42 background: var(--panel);
43 color: var(--text);
44 border-radius: 6px;
45 padding: 6px 12px;
46 }
47 button:hover { border-color: var(--accent); }
48 button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
49 button.danger:hover { border-color: var(--danger); color: var(--danger); }
50
51 /* Sidebar */
52 #sidebar {
53 width: 300px;
54 min-width: 220px;
55 display: flex;
56 flex-direction: column;
57 border-right: 1px solid var(--border);
58 background: var(--panel);
59 }
60 #sidebar-top {
61 padding: 12px;
62 display: flex;
63 gap: 8px;
64 border-bottom: 1px solid var(--border);
65 }
66 #search {
67 flex: 1;
68 min-width: 0;
69 font: inherit;
70 padding: 6px 10px;
71 border: 1px solid var(--border);
72 border-radius: 6px;
73 background: var(--bg);
74 color: var(--text);
75 }
76 #note-list {
77 list-style: none;
78 margin: 0;
79 padding: 0;
80 overflow-y: auto;
81 flex: 1;
82 }
83 #note-list li {
84 padding: 10px 14px;
85 border-bottom: 1px solid var(--border);
86 cursor: pointer;
87 }
88 #note-list li:hover { background: var(--bg); }
89 #note-list li.selected { background: var(--selected); }
90 .note-title {
91 font-weight: 600;
92 white-space: nowrap;
93 overflow: hidden;
94 text-overflow: ellipsis;
95 }
96 .note-snippet, .note-date {
97 font-size: 0.85em;
98 color: var(--muted);
99 white-space: nowrap;
100 overflow: hidden;
101 text-overflow: ellipsis;
102 }
103 .empty-list {
104 padding: 20px 14px;
105 color: var(--muted);
106 text-align: center;
107 }
108
109 /* Editor */
110 #editor {
111 flex: 1;
112 display: flex;
113 flex-direction: column;
114 min-width: 0;
115 }
116 #editor-top {
117 display: flex;
118 align-items: center;
119 gap: 10px;
120 padding: 12px 20px;
121 border-bottom: 1px solid var(--border);
122 }
123 #status { flex: 1; color: var(--muted); font-size: 0.85em; }
124 #title {
125 font: inherit;
126 font-size: 1.5em;
127 font-weight: 600;
128 border: none;
129 outline: none;
130 background: transparent;
131 color: var(--text);
132 padding: 18px 20px 6px;
133 }
134 #body {
135 flex: 1;
136 font: inherit;
137 font-size: 1.05em;
138 line-height: 1.55;
139 border: none;
140 outline: none;
141 resize: none;
142 background: transparent;
143 color: var(--text);
144 padding: 6px 20px 20px;
145 }
146 #placeholder {
147 flex: 1;
148 display: flex;
149 align-items: center;
150 justify-content: center;
151 flex-direction: column;
152 gap: 12px;
153 color: var(--muted);
154 }
155 .hidden { display: none !important; }
156
157 /* Small screens: stack list above editor */
158 @media (max-width: 700px) {
159 body { flex-direction: column; }
160 #sidebar { width: 100%; max-height: 40vh; border-right: none; border-bottom: 1px solid var(--border); }
161 }
162</style>
163</head>
164<body>
165
166<aside id="sidebar">
167 <div id="sidebar-top">
168 <input id="search" type="search" placeholder="Search notes…" aria-label="Search notes">
169 <button id="new-btn" class="primary" title="New note (Ctrl+Alt+N)">+ New</button>
170 </div>
171 <ul id="note-list" aria-label="Notes"></ul>
172</aside>
173
174<main id="editor">
175 <div id="editor-top">
176 <span id="status"></span>
177 <button id="delete-btn" class="danger">Delete</button>
178 </div>
179 <input id="title" type="text" placeholder="Title" aria-label="Note title">
180 <textarea id="body" placeholder="Start writing…" aria-label="Note text"></textarea>
181 <div id="placeholder" class="hidden">
182 <div>No note selected.</div>
183 <button id="placeholder-new" class="primary">Create a note</button>
184 </div>
185</main>
186
187<script>
188(function () {
189 "use strict";
190
191 var STORAGE_KEY = "simple-notes-app.notes";
192 var SELECTED_KEY = "simple-notes-app.selected";
193
194 var notes = [];
195 var selectedId = null;
196 var saveTimer = null;
197 var storageWorks = true;
198
199 var $ = function (id) { return document.getElementById(id); };
200 var listEl = $("note-list");
201 var searchEl = $("search");
202 var titleEl = $("title");
203 var bodyEl = $("body");
204 var statusEl = $("status");
205 var deleteBtn = $("delete-btn");
206 var placeholderEl = $("placeholder");
207
208 // ---------- Storage ----------
209
210 function load() {
211 try {
212 var raw = localStorage.getItem(STORAGE_KEY);
213 var parsed = raw ? JSON.parse(raw) : [];
214 notes = Array.isArray(parsed) ? parsed.filter(function (n) {
215 return n && typeof n.id === "string";
216 }) : [];
217 selectedId = localStorage.getItem(SELECTED_KEY);
218 } catch (e) {
219 storageWorks = false;
220 notes = [];
221 }
222 }
223
224 function save() {
225 clearTimeout(saveTimer);
226 saveTimer = null;
227 try {
228 localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
229 if (selectedId) localStorage.setItem(SELECTED_KEY, selectedId);
230 else localStorage.removeItem(SELECTED_KEY);
231 storageWorks = true;
232 setStatus("Saved");
233 } catch (e) {
234 storageWorks = false;
235 setStatus("⚠ Could not save — your browser is blocking storage or it is full.");
236 }
237 }
238
239 function scheduleSave() {
240 setStatus("Saving…");
241 clearTimeout(saveTimer);
242 saveTimer = setTimeout(save, 300);
243 }
244
245 // ---------- Helpers ----------
246
247 function makeId() {
248 return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
249 }
250
251 function findNote(id) {
252 for (var i = 0; i < notes.length; i++) if (notes[i].id === id) return notes[i];
253 return null;
254 }
255
256 function displayTitle(note) {
257 var t = (note.title || "").trim();
258 if (t) return t;
259 var firstLine = (note.body || "").trim().split("\n")[0];
260 return firstLine ? firstLine.slice(0, 60) : "Untitled";
261 }
262
263 function formatDate(ts) {
264 var d = new Date(ts);
265 var now = new Date();
266 if (d.toDateString() === now.toDateString()) {
267 return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
268 }
269 return d.toLocaleDateString([], { year: "numeric", month: "short", day: "numeric" });
270 }
271
272 function setStatus(text) {
273 if (!storageWorks && text === "Saved") return;
274 statusEl.textContent = text;
275 }
276
277 function sortedNotes() {
278 return notes.slice().sort(function (a, b) { return (b.updated || 0) - (a.updated || 0); });
279 }
280
281 // ---------- Rendering ----------
282
283 function renderList() {
284 var query = searchEl.value.trim().toLowerCase();
285 listEl.textContent = "";
286
287 var visible = sortedNotes().filter(function (n) {
288 if (!query) return true;
289 return ((n.title || "") + "\n" + (n.body || "")).toLowerCase().indexOf(query) !== -1;
290 });
291
292 if (visible.length === 0) {
293 var empty = document.createElement("li");
294 empty.className = "empty-list";
295 empty.textContent = notes.length === 0 ? "No notes yet. Click “+ New” to start." : "No matching notes.";
296 listEl.appendChild(empty);
297 return;
298 }
299
300 visible.forEach(function (note) {
301 var li = document.createElement("li");
302 if (note.id === selectedId) li.className = "selected";
303 li.dataset.id = note.id;
304
305 var title = document.createElement("div");
306 title.className = "note-title";
307 title.textContent = displayTitle(note);
308
309 var snippet = document.createElement("div");
310 snippet.className = "note-snippet";
311 snippet.textContent = (note.body || "").replace(/\s+/g, " ").trim() || "No text";
312
313 var date = document.createElement("div");
314 date.className = "note-date";
315 date.textContent = formatDate(note.updated);
316
317 li.appendChild(title);
318 li.appendChild(snippet);
319 li.appendChild(date);
320 listEl.appendChild(li);
321 });
322 }
323
324 function renderEditor() {
325 var note = findNote(selectedId);
326 var hasNote = !!note;
327 titleEl.classList.toggle("hidden", !hasNote);
328 bodyEl.classList.toggle("hidden", !hasNote);
329 deleteBtn.classList.toggle("hidden", !hasNote);
330 placeholderEl.classList.toggle("hidden", hasNote);
331 if (hasNote) {
332 if (titleEl.value !== note.title) titleEl.value = note.title || "";
333 if (bodyEl.value !== note.body) bodyEl.value = note.body || "";
334 } else {
335 titleEl.value = "";
336 bodyEl.value = "";
337 }
338 if (!storageWorks) {
339 setStatus("⚠ Your browser is blocking storage — notes will not be kept after closing.");
340 }
341 }
342
343 function render() {
344 renderList();
345 renderEditor();
346 }
347
348 // ---------- Actions ----------
349
350 function selectNote(id) {
351 if (saveTimer) save();
352 selectedId = id;
353 render();
354 save();
355 }
356
357 function newNote() {
358 if (saveTimer) save();
359 var now = Date.now();
360 var note = { id: makeId(), title: "", body: "", created: now, updated: now };
361 notes.push(note);
362 selectedId = note.id;
363 searchEl.value = "";
364 render();
365 save();
366 titleEl.focus();
367 }
368
369 function deleteNote() {
370 var note = findNote(selectedId);
371 if (!note) return;
372 if (!confirm("Delete the note “" + displayTitle(note) + "”? This cannot be undone.")) return;
373 notes = notes.filter(function (n) { return n.id !== note.id; });
374 var remaining = sortedNotes();
375 selectedId = remaining.length ? remaining[0].id : null;
376 render();
377 save();
378 }
379
380 function onEdit() {
381 var note = findNote(selectedId);
382 if (!note) return;
383 note.title = titleEl.value;
384 note.body = bodyEl.value;
385 note.updated = Date.now();
386 renderList();
387 scheduleSave();
388 }
389
390 // ---------- Events ----------
391
392 $("new-btn").addEventListener("click", newNote);
393 $("placeholder-new").addEventListener("click", newNote);
394 deleteBtn.addEventListener("click", deleteNote);
395 titleEl.addEventListener("input", onEdit);
396 bodyEl.addEventListener("input", onEdit);
397 searchEl.addEventListener("input", renderList);
398
399 titleEl.addEventListener("keydown", function (e) {
400 if (e.key === "Enter") { e.preventDefault(); bodyEl.focus(); }
401 });
402
403 listEl.addEventListener("click", function (e) {
404 var li = e.target.closest("li[data-id]");
405 if (li) selectNote(li.dataset.id);
406 });
407
408 document.addEventListener("keydown", function (e) {
409 if ((e.ctrlKey || e.metaKey) && e.altKey && (e.key === "n" || e.key === "N")) {
410 e.preventDefault();
411 newNote();
412 }
413 });
414
415 // Make sure nothing is lost if the tab is closed right after typing.
416 window.addEventListener("beforeunload", function () { if (saveTimer) save(); });
417 document.addEventListener("visibilitychange", function () {
418 if (document.visibilityState === "hidden" && saveTimer) save();
419 });
420
421 // Keep multiple open tabs in sync.
422 window.addEventListener("storage", function (e) {
423 if (e.key !== STORAGE_KEY) return;
424 var keep = selectedId;
425 load();
426 selectedId = findNote(keep) ? keep : (sortedNotes()[0] || {}).id || null;
427 render();
428 });
429
430 // ---------- Start ----------
431
432 load();
433 if (!findNote(selectedId)) {
434 var first = sortedNotes()[0];
435 selectedId = first ? first.id : null;
436 }
437 render();
438 if (storageWorks && notes.length) setStatus("All notes saved in this browser");
439})();
440</script>
441</body>
442</html>