|
201
|
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>
|