comparison mrjunejune/src/public/editor.js @ 202:b9b184b3303c

[Notes] Images get processed and it is properly fetched. Thank you.
author MrJuneJune <me@mrjunejune.com>
date Sun, 15 Feb 2026 09:12:57 -0800
parents
children e5aed6c36672
comparison
equal deleted inserted replaced
201:6cdee35a7ba9 202:b9b184b3303c
1 let editor = null;
2 let currentNoteId = 'index';
3
4 function getAuthToken() {
5 return localStorage.getItem('notes-auth-token');
6 }
7
8 function requireAuth() {
9 if (!getAuthToken()) {
10 const returnUrl = encodeURIComponent(window.location.pathname);
11 window.location.href = '/notes/login?return=' + returnUrl;
12 return false;
13 }
14 return true;
15 }
16
17 function logout() {
18 localStorage.removeItem('notes-auth-token');
19 window.location.href = '/notes/login';
20 }
21
22 function getNoteIdFromPath() {
23 const path = window.location.pathname;
24 const match = path.match(/^\/notes\/(.+)$/);
25 if (match && match[1] && match[1] !== 'login') {
26 return decodeURIComponent(match[1]);
27 }
28 return 'index';
29 }
30
31 function showNewNoteDialog() {
32 document.getElementById('new-note-dialog').classList.add('show');
33 document.getElementById('new-note-id').focus();
34 }
35
36 function hideNewNoteDialog() {
37 document.getElementById('new-note-dialog').classList.remove('show');
38 document.getElementById('new-note-id').value = '';
39 }
40
41 function createNewNote() {
42 let noteId = document.getElementById('new-note-id').value.trim();
43 if (!noteId) return;
44
45 // Sanitize note ID
46 noteId = noteId.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
47
48 hideNewNoteDialog();
49 window.location.href = '/notes/' + encodeURIComponent(noteId);
50 }
51
52 // Handle Enter key in new note dialog
53 document.getElementById('new-note-id').addEventListener('keydown', function(e) {
54 if (e.key === 'Enter') {
55 e.preventDefault();
56 createNewNote();
57 }
58 if (e.key === 'Escape') {
59 hideNewNoteDialog();
60 }
61 });
62
63 // Close dialog on backdrop click
64 document.getElementById('new-note-dialog').addEventListener('click', function(e) {
65 if (e.target === this) {
66 hideNewNoteDialog();
67 }
68 });
69
70 async function uploadFile(file) {
71 const token = getAuthToken();
72 if (!token) {
73 throw new Error('Not authenticated');
74 }
75
76 // 1. Create media record
77 const createResponse = await fetch('/api/media/create', {
78 method: 'POST',
79 headers: {
80 'Authorization': 'Bearer ' + token,
81 'Content-Type': 'application/json'
82 },
83 body: JSON.stringify({
84 filename: file.name,
85 content_type: file.type
86 })
87 });
88
89 if (!createResponse.ok) {
90 const error = await createResponse.json().catch(() => ({}));
91 throw new Error(error.error || 'Failed to create media record');
92 }
93
94 const data = await createResponse.json();
95
96 // 2. Upload file directly to S3
97 const uploadResponse = await fetch(data.upload_url, {
98 method: 'PUT',
99 headers: {
100 'Content-Type': file.type
101 },
102 body: file
103 });
104
105 if (!uploadResponse.ok) {
106 throw new Error('Failed to upload file to S3');
107 }
108
109 // 3. Mark as uploaded (triggers processing for images)
110 await fetch(`/api/media/${data.media_id}/uploaded`, {
111 method: 'POST',
112 headers: {
113 'Authorization': 'Bearer ' + token
114 }
115 });
116
117 // 4. Poll for images, return immediately for non-images
118 if (file.type.startsWith('image/')) {
119 return await pollForProcessedImage(data.media_id, token);
120 } else {
121 // For non-images, construct the public URL
122 const publicUrl = data.upload_url.split('?')[0]; // Remove query params
123 return { url: publicUrl };
124 }
125 }
126
127 async function pollForProcessedImage(mediaId, token) {
128 const maxAttempts = 60; // 2 minutes max (60 * 2 seconds)
129
130 for (let i = 0; i < maxAttempts; i++) {
131 await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second interval
132
133 const statusResponse = await fetch(`/api/media/${mediaId}/status`, {
134 headers: {
135 'Authorization': 'Bearer ' + token
136 }
137 });
138
139 if (!statusResponse.ok) {
140 console.warn('Status check failed, retrying...');
141 continue;
142 }
143
144 const statusData = await statusResponse.json();
145
146 if (statusData.status === 'finished') {
147 return { url: statusData.processed_url };
148 } else if (statusData.status === 'error') {
149 throw new Error(statusData.error_message || 'Processing failed');
150 }
151 // Status is 'uploaded' or 'processing', continue polling
152 }
153
154 throw new Error('Processing timeout after 2 minutes');
155 }
156
157 async function saveContent(content) {
158 const token = getAuthToken();
159 if (!token) return;
160
161 const response = await fetch('/api/editor/save', {
162 method: 'POST',
163 headers: {
164 'Authorization': 'Bearer ' + token,
165 'Content-Type': 'application/json'
166 },
167 body: JSON.stringify({
168 doc_id: currentNoteId,
169 content: content
170 })
171 });
172
173 if (!response.ok) {
174 throw new Error('Failed to save');
175 }
176 }
177
178 async function loadNote(noteId) {
179 const token = getAuthToken();
180 if (!token) return;
181
182 try {
183 const response = await fetch('/api/editor/load/' + encodeURIComponent(noteId), {
184 headers: { 'Authorization': 'Bearer ' + token }
185 });
186
187 if (response.ok) {
188 const data = await response.json();
189 editor.setContent(data.content || '');
190 }
191 } catch (error) {
192 console.error('Failed to load note:', error);
193 }
194 }
195
196 // Initialize
197 document.addEventListener('DOMContentLoaded', function() {
198 if (!requireAuth()) return;
199
200 currentNoteId = getNoteIdFromPath();
201 document.getElementById('note-id-display').textContent = currentNoteId;
202
203 // Update page title
204 document.title = currentNoteId + ' | Notes';
205
206 editor = RichEditor.init('editor-container', {
207 uploadCallback: uploadFile,
208 saveCallback: saveContent,
209 debounceMs: 1500,
210 placeholder: 'Start writing... (paste images, drag files, or use /upload)\n\nTip: Click "+ New Note" to create linked notes.'
211 });
212
213 loadNote(currentNoteId);
214 });
215