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);
+});
+