Mercurial
comparison mrjunejune/src/notes/index.html @ 201:6cdee35a7ba9
[MrJuneJune] notes
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 15 Feb 2026 07:07:50 -0800 |
| parents | |
| children | b9b184b3303c |
comparison
equal
deleted
inserted
replaced
| 200:90dfcef375fb | 201:6cdee35a7ba9 |
|---|---|
| 1 <!DOCTYPE html> | |
| 2 <html lang="en"> | |
| 3 <head> | |
| 4 {{/parts/base_head.html}} | |
| 5 <title>Notes</title> | |
| 6 <style> | |
| 7 .notes-page { | |
| 8 max-width: 900px; | |
| 9 margin: 0 auto; | |
| 10 padding: 20px; | |
| 11 } | |
| 12 .notes-header { | |
| 13 display: flex; | |
| 14 justify-content: space-between; | |
| 15 align-items: center; | |
| 16 margin-bottom: 20px; | |
| 17 flex-wrap: wrap; | |
| 18 gap: 12px; | |
| 19 } | |
| 20 .notes-header h1 { | |
| 21 margin: 0; | |
| 22 display: flex; | |
| 23 align-items: center; | |
| 24 gap: 12px; | |
| 25 } | |
| 26 .note-id-display { | |
| 27 font-size: 14px; | |
| 28 color: #666; | |
| 29 background: #f0f0f0; | |
| 30 padding: 4px 12px; | |
| 31 border-radius: 4px; | |
| 32 font-weight: normal; | |
| 33 } | |
| 34 .notes-actions { | |
| 35 display: flex; | |
| 36 gap: 8px; | |
| 37 align-items: center; | |
| 38 } | |
| 39 .notes-actions button, .notes-actions a { | |
| 40 padding: 8px 16px; | |
| 41 border: 1px solid #ccc; | |
| 42 border-radius: 4px; | |
| 43 background: #fff; | |
| 44 cursor: pointer; | |
| 45 text-decoration: none; | |
| 46 color: #333; | |
| 47 font-size: 14px; | |
| 48 } | |
| 49 .notes-actions button:hover, .notes-actions a:hover { | |
| 50 background: #f5f5f5; | |
| 51 } | |
| 52 .notes-actions .btn-primary { | |
| 53 background: #0078ff; | |
| 54 color: white; | |
| 55 border-color: #0078ff; | |
| 56 } | |
| 57 .notes-actions .btn-primary:hover { | |
| 58 background: #0066dd; | |
| 59 } | |
| 60 .notes-actions .btn-danger { | |
| 61 color: #d32f2f; | |
| 62 border-color: #d32f2f; | |
| 63 } | |
| 64 .new-note-dialog { | |
| 65 position: fixed; | |
| 66 top: 0; | |
| 67 left: 0; | |
| 68 right: 0; | |
| 69 bottom: 0; | |
| 70 background: rgba(0,0,0,0.5); | |
| 71 display: none; | |
| 72 align-items: center; | |
| 73 justify-content: center; | |
| 74 z-index: 100; | |
| 75 } | |
| 76 .new-note-dialog.show { | |
| 77 display: flex; | |
| 78 } | |
| 79 .new-note-form { | |
| 80 background: white; | |
| 81 padding: 24px; | |
| 82 border-radius: 8px; | |
| 83 width: 400px; | |
| 84 max-width: 90%; | |
| 85 } | |
| 86 .new-note-form h2 { | |
| 87 margin: 0 0 16px 0; | |
| 88 } | |
| 89 .new-note-form input { | |
| 90 width: 100%; | |
| 91 padding: 10px; | |
| 92 border: 1px solid #ccc; | |
| 93 border-radius: 4px; | |
| 94 font-size: 16px; | |
| 95 box-sizing: border-box; | |
| 96 margin-bottom: 16px; | |
| 97 } | |
| 98 .new-note-form .form-actions { | |
| 99 display: flex; | |
| 100 gap: 8px; | |
| 101 justify-content: flex-end; | |
| 102 } | |
| 103 .new-note-form button { | |
| 104 padding: 8px 16px; | |
| 105 border-radius: 4px; | |
| 106 cursor: pointer; | |
| 107 font-size: 14px; | |
| 108 } | |
| 109 .new-note-form .btn-cancel { | |
| 110 background: #f5f5f5; | |
| 111 border: 1px solid #ccc; | |
| 112 } | |
| 113 .new-note-form .btn-create { | |
| 114 background: #0078ff; | |
| 115 color: white; | |
| 116 border: none; | |
| 117 } | |
| 118 .note-hint { | |
| 119 font-size: 12px; | |
| 120 color: #666; | |
| 121 margin-top: -12px; | |
| 122 margin-bottom: 16px; | |
| 123 } | |
| 124 </style> | |
| 125 </head> | |
| 126 <body> | |
| 127 {{/parts/header.html}} | |
| 128 | |
| 129 <main class="notes-page"> | |
| 130 <div class="notes-header"> | |
| 131 <h1> | |
| 132 Notes | |
| 133 <span class="note-id-display" id="note-id-display">index</span> | |
| 134 </h1> | |
| 135 <div class="notes-actions"> | |
| 136 <button onclick="showNewNoteDialog()" class="btn-primary">+ New Note</button> | |
| 137 <a href="/notes/" id="home-link">Home</a> | |
| 138 <button onclick="logout()" class="btn-danger">Logout</button> | |
| 139 </div> | |
| 140 </div> | |
| 141 | |
| 142 <div id="editor-container"></div> | |
| 143 </main> | |
| 144 | |
| 145 <!-- New Note Dialog --> | |
| 146 <div class="new-note-dialog" id="new-note-dialog"> | |
| 147 <div class="new-note-form"> | |
| 148 <h2>Create New Note</h2> | |
| 149 <input type="text" id="new-note-id" placeholder="note-id (e.g., my-ideas)"> | |
| 150 <p class="note-hint">Use lowercase letters, numbers, and hyphens only</p> | |
| 151 <div class="form-actions"> | |
| 152 <button class="btn-cancel" onclick="hideNewNoteDialog()">Cancel</button> | |
| 153 <button class="btn-create" onclick="createNewNote()">Create & Open</button> | |
| 154 </div> | |
| 155 </div> | |
| 156 </div> | |
| 157 | |
| 158 {{/parts/footer.html}} | |
| 159 | |
| 160 <script src="/public/js/rich_editor.js"></script> | |
| 161 <script> | |
| 162 | |
| 163 let editor = null; | |
| 164 let currentNoteId = 'index'; | |
| 165 | |
| 166 function getAuthToken() { | |
| 167 return localStorage.getItem('notes-auth-token'); | |
| 168 } | |
| 169 | |
| 170 function requireAuth() { | |
| 171 if (!getAuthToken()) { | |
| 172 const returnUrl = encodeURIComponent(window.location.pathname); | |
| 173 window.location.href = '/notes/login?return=' + returnUrl; | |
| 174 return false; | |
| 175 } | |
| 176 return true; | |
| 177 } | |
| 178 | |
| 179 function logout() { | |
| 180 localStorage.removeItem('notes-auth-token'); | |
| 181 window.location.href = '/notes/login'; | |
| 182 } | |
| 183 | |
| 184 function getNoteIdFromPath() { | |
| 185 const path = window.location.pathname; | |
| 186 const match = path.match(/^\/notes\/(.+)$/); | |
| 187 if (match && match[1] && match[1] !== 'login') { | |
| 188 return decodeURIComponent(match[1]); | |
| 189 } | |
| 190 return 'index'; | |
| 191 } | |
| 192 | |
| 193 function showNewNoteDialog() { | |
| 194 document.getElementById('new-note-dialog').classList.add('show'); | |
| 195 document.getElementById('new-note-id').focus(); | |
| 196 } | |
| 197 | |
| 198 function hideNewNoteDialog() { | |
| 199 document.getElementById('new-note-dialog').classList.remove('show'); | |
| 200 document.getElementById('new-note-id').value = ''; | |
| 201 } | |
| 202 | |
| 203 function createNewNote() { | |
| 204 let noteId = document.getElementById('new-note-id').value.trim(); | |
| 205 if (!noteId) return; | |
| 206 | |
| 207 // Sanitize note ID | |
| 208 noteId = noteId.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-'); | |
| 209 | |
| 210 hideNewNoteDialog(); | |
| 211 window.location.href = '/notes/' + encodeURIComponent(noteId); | |
| 212 } | |
| 213 | |
| 214 // Handle Enter key in new note dialog | |
| 215 document.getElementById('new-note-id').addEventListener('keydown', function(e) { | |
| 216 if (e.key === 'Enter') { | |
| 217 e.preventDefault(); | |
| 218 createNewNote(); | |
| 219 } | |
| 220 if (e.key === 'Escape') { | |
| 221 hideNewNoteDialog(); | |
| 222 } | |
| 223 }); | |
| 224 | |
| 225 // Close dialog on backdrop click | |
| 226 document.getElementById('new-note-dialog').addEventListener('click', function(e) { | |
| 227 if (e.target === this) { | |
| 228 hideNewNoteDialog(); | |
| 229 } | |
| 230 }); | |
| 231 | |
| 232 async function uploadFile(file) { | |
| 233 const token = getAuthToken(); | |
| 234 if (!token) { | |
| 235 throw new Error('Not authenticated'); | |
| 236 } | |
| 237 | |
| 238 // Get s3 bucket URL | |
| 239 const response = await fetch('/api/s3/upload-url', { | |
| 240 method: 'POST', | |
| 241 headers: { | |
| 242 'Authorization': 'Bearer ' + token, | |
| 243 'Content-Type': 'application/json' | |
| 244 }, | |
| 245 body: JSON.stringify({ | |
| 246 filename: file.name, | |
| 247 content_type: file.type | |
| 248 }) | |
| 249 }); | |
| 250 | |
| 251 if (!response.ok) { | |
| 252 const error = await response.json(); | |
| 253 throw new Error(error.error || 'Failed to get upload URL'); | |
| 254 } | |
| 255 | |
| 256 const data = await response.json(); | |
| 257 | |
| 258 const uploadResponse = await fetch(data.upload_url, { | |
| 259 method: 'PUT', | |
| 260 headers: { 'Content-Type': file.type }, | |
| 261 body: file | |
| 262 }); | |
| 263 | |
| 264 if (!uploadResponse.ok) { | |
| 265 throw new Error('Failed to upload file to S3'); | |
| 266 } | |
| 267 | |
| 268 return { url: data.public_url, key: data.key }; | |
| 269 } | |
| 270 | |
| 271 async function saveContent(content) { | |
| 272 const token = getAuthToken(); | |
| 273 if (!token) return; | |
| 274 | |
| 275 const response = await fetch('/api/editor/save', { | |
| 276 method: 'POST', | |
| 277 headers: { | |
| 278 'Authorization': 'Bearer ' + token, | |
| 279 'Content-Type': 'application/json' | |
| 280 }, | |
| 281 body: JSON.stringify({ | |
| 282 doc_id: currentNoteId, | |
| 283 content: content | |
| 284 }) | |
| 285 }); | |
| 286 | |
| 287 if (!response.ok) { | |
| 288 throw new Error('Failed to save'); | |
| 289 } | |
| 290 } | |
| 291 | |
| 292 async function loadNote(noteId) { | |
| 293 const token = getAuthToken(); | |
| 294 if (!token) return; | |
| 295 | |
| 296 try { | |
| 297 const response = await fetch('/api/editor/load/' + encodeURIComponent(noteId), { | |
| 298 headers: { 'Authorization': 'Bearer ' + token } | |
| 299 }); | |
| 300 | |
| 301 if (response.ok) { | |
| 302 const data = await response.json(); | |
| 303 editor.setContent(data.content || ''); | |
| 304 } | |
| 305 } catch (error) { | |
| 306 console.error('Failed to load note:', error); | |
| 307 } | |
| 308 } | |
| 309 | |
| 310 // Initialize | |
| 311 document.addEventListener('DOMContentLoaded', function() { | |
| 312 if (!requireAuth()) return; | |
| 313 | |
| 314 currentNoteId = getNoteIdFromPath(); | |
| 315 document.getElementById('note-id-display').textContent = currentNoteId; | |
| 316 | |
| 317 // Update page title | |
| 318 document.title = currentNoteId + ' | Notes'; | |
| 319 | |
| 320 editor = RichEditor.init('editor-container', { | |
| 321 uploadCallback: uploadFile, | |
| 322 saveCallback: saveContent, | |
| 323 debounceMs: 1500, | |
| 324 placeholder: 'Start writing... (paste images, drag files, or use /upload)\n\nTip: Click "+ New Note" to create linked notes.' | |
| 325 }); | |
| 326 | |
| 327 loadNote(currentNoteId); | |
| 328 }); | |
| 329 </script> | |
| 330 </body> | |
| 331 </html> |