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>