Mercurial
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mrjunejune/src/public/editor.js Sun Feb 15 09:12:57 2026 -0800 @@ -0,0 +1,215 @@ +let editor = null; +let currentNoteId = 'index'; + +function getAuthToken() { + return localStorage.getItem('notes-auth-token'); +} + +function requireAuth() { + if (!getAuthToken()) { + const returnUrl = encodeURIComponent(window.location.pathname); + window.location.href = '/notes/login?return=' + returnUrl; + return false; + } + return true; +} + +function logout() { + localStorage.removeItem('notes-auth-token'); + window.location.href = '/notes/login'; +} + +function getNoteIdFromPath() { + const path = window.location.pathname; + const match = path.match(/^\/notes\/(.+)$/); + if (match && match[1] && match[1] !== 'login') { + return decodeURIComponent(match[1]); + } + return 'index'; +} + +function showNewNoteDialog() { + document.getElementById('new-note-dialog').classList.add('show'); + document.getElementById('new-note-id').focus(); +} + +function hideNewNoteDialog() { + document.getElementById('new-note-dialog').classList.remove('show'); + document.getElementById('new-note-id').value = ''; +} + +function createNewNote() { + let noteId = document.getElementById('new-note-id').value.trim(); + if (!noteId) return; + + // Sanitize note ID + noteId = noteId.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-'); + + hideNewNoteDialog(); + window.location.href = '/notes/' + encodeURIComponent(noteId); +} + +// Handle Enter key in new note dialog +document.getElementById('new-note-id').addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + createNewNote(); + } + if (e.key === 'Escape') { + hideNewNoteDialog(); + } +}); + +// Close dialog on backdrop click +document.getElementById('new-note-dialog').addEventListener('click', function(e) { + if (e.target === this) { + hideNewNoteDialog(); + } +}); + +async function uploadFile(file) { + const token = getAuthToken(); + if (!token) { + throw new Error('Not authenticated'); + } + + // 1. Create media record + const createResponse = await fetch('/api/media/create', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + filename: file.name, + content_type: file.type + }) + }); + + if (!createResponse.ok) { + const error = await createResponse.json().catch(() => ({})); + throw new Error(error.error || 'Failed to create media record'); + } + + const data = await createResponse.json(); + + // 2. Upload file directly to S3 + const uploadResponse = await fetch(data.upload_url, { + method: 'PUT', + headers: { + 'Content-Type': file.type + }, + body: file + }); + + if (!uploadResponse.ok) { + throw new Error('Failed to upload file to S3'); + } + + // 3. Mark as uploaded (triggers processing for images) + await fetch(`/api/media/${data.media_id}/uploaded`, { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + token + } + }); + + // 4. Poll for images, return immediately for non-images + if (file.type.startsWith('image/')) { + return await pollForProcessedImage(data.media_id, token); + } else { + // For non-images, construct the public URL + const publicUrl = data.upload_url.split('?')[0]; // Remove query params + return { url: publicUrl }; + } +} + +async function pollForProcessedImage(mediaId, token) { + const maxAttempts = 60; // 2 minutes max (60 * 2 seconds) + + for (let i = 0; i < maxAttempts; i++) { + await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second interval + + const statusResponse = await fetch(`/api/media/${mediaId}/status`, { + headers: { + 'Authorization': 'Bearer ' + token + } + }); + + if (!statusResponse.ok) { + console.warn('Status check failed, retrying...'); + continue; + } + + const statusData = await statusResponse.json(); + + if (statusData.status === 'finished') { + return { url: statusData.processed_url }; + } else if (statusData.status === 'error') { + throw new Error(statusData.error_message || 'Processing failed'); + } + // Status is 'uploaded' or 'processing', continue polling + } + + throw new Error('Processing timeout after 2 minutes'); +} + +async function saveContent(content) { + const token = getAuthToken(); + if (!token) return; + + const response = await fetch('/api/editor/save', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + doc_id: currentNoteId, + content: content + }) + }); + + if (!response.ok) { + throw new Error('Failed to save'); + } +} + +async function loadNote(noteId) { + const token = getAuthToken(); + if (!token) return; + + try { + const response = await fetch('/api/editor/load/' + encodeURIComponent(noteId), { + headers: { 'Authorization': 'Bearer ' + token } + }); + + if (response.ok) { + const data = await response.json(); + editor.setContent(data.content || ''); + } + } catch (error) { + console.error('Failed to load note:', error); + } +} + +// Initialize +document.addEventListener('DOMContentLoaded', function() { + if (!requireAuth()) return; + + currentNoteId = getNoteIdFromPath(); + document.getElementById('note-id-display').textContent = currentNoteId; + + // Update page title + document.title = currentNoteId + ' | Notes'; + + editor = RichEditor.init('editor-container', { + uploadCallback: uploadFile, + saveCallback: saveContent, + debounceMs: 1500, + placeholder: 'Start writing... (paste images, drag files, or use /upload)\n\nTip: Click "+ New Note" to create linked notes.' + }); + + loadNote(currentNoteId); +}); +