changeset 204:e5aed6c36672

[Notes] Added icons and updated styling a bit. Probalby usable now.
author MrJuneJune <me@mrjunejune.com>
date Sun, 15 Feb 2026 11:02:13 -0800
parents 92a57bd716c1
children e07b4b5a66bb
files assets/BUILD assets/Open fully.png assets/cabinet.png assets/icons/Open fully.png assets/icons/binder.png assets/icons/bold.png assets/icons/book.png assets/icons/cabinet.png assets/icons/clipy.png assets/icons/h1.png assets/icons/h2.png assets/icons/h3.png assets/icons/link.png assets/icons/open-book.png mrjunejune/BUILD mrjunejune/main.c mrjunejune/src/notes/editor.js mrjunejune/src/notes/index.html mrjunejune/src/public/editor.js rich_editor/rich_editor.js
diffstat 20 files changed, 106 insertions(+), 235 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/assets/BUILD	Sun Feb 15 11:02:13 2026 -0800
@@ -0,0 +1,5 @@
+filegroup(
+  name = "icons",
+  srcs = glob(["icons/*.png"]),
+  visibility = ["//visibility:public"],
+)
Binary file assets/Open fully.png has changed
Binary file assets/cabinet.png has changed
Binary file assets/icons/Open fully.png has changed
Binary file assets/icons/binder.png has changed
Binary file assets/icons/bold.png has changed
Binary file assets/icons/book.png has changed
Binary file assets/icons/cabinet.png has changed
Binary file assets/icons/clipy.png has changed
Binary file assets/icons/h1.png has changed
Binary file assets/icons/h2.png has changed
Binary file assets/icons/h3.png has changed
Binary file assets/icons/link.png has changed
Binary file assets/icons/open-book.png has changed
--- a/mrjunejune/BUILD	Sun Feb 15 09:13:09 2026 -0800
+++ b/mrjunejune/BUILD	Sun Feb 15 11:02:13 2026 -0800
@@ -36,6 +36,14 @@
   dest = "src/public/js",
 )
 
+move_files_into_dir(
+  name = "icons",
+  srcs = [
+    "//assets:icons",
+  ],
+  dest = "src/public/icons",
+)
+
 filegroup(
   name = "public_files",
   srcs = glob(["src/public/*"]),
@@ -50,7 +58,7 @@
     
 filegroup(
   name = "src_files",
-  srcs = glob(["src/**"]) + [":react_pages", ":shared_js_non_public", ":shared_js_file", ":rich_editor_js"],
+  srcs = glob(["src/**"]) + [":react_pages", ":shared_js_non_public", ":shared_js_file", ":rich_editor_js", ":icons"],
   visibility = ["//mrjunejune/test:__pkg__"],
 )
 
--- a/mrjunejune/main.c	Sun Feb 15 09:13:09 2026 -0800
+++ b/mrjunejune/main.c	Sun Feb 15 11:02:13 2026 -0800
@@ -1195,7 +1195,17 @@
   char s3_key_original[512];
   char s3_key_processed[512];
   snprintf(s3_key_original, sizeof(s3_key_original), "uploads/%s/%s", uuid, filename);
-  snprintf(s3_key_processed, sizeof(s3_key_processed), "uploads/%s/processed.webp", uuid);
+
+  // Only use .webp for images
+  int is_image = (strncmp(content_type, "image/", 6) == 0);
+  if (is_image)
+  {
+    snprintf(s3_key_processed, sizeof(s3_key_processed), "uploads/%s/processed.webp", uuid);
+  }
+  else
+  {
+    s3_key_processed[0] = '\0'; // No processed version for non-images
+  }
 
   // Insert into database
   const char *insert_query =
@@ -1238,11 +1248,23 @@
     return resp;
   }
 
+  // Build public URL using CloudFront or S3
+  char public_url[512];
+  if (g_s3_cloudfront_url[0])
+  {
+    snprintf(public_url, sizeof(public_url), "%s/%s", g_s3_cloudfront_url, s3_key_original);
+  }
+  else
+  {
+    snprintf(public_url, sizeof(public_url), "https://%s.s3.%s.amazonaws.com/%s",
+             g_s3_bucket, g_s3_region, s3_key_original);
+  }
+
   // Build response
-  char *response_body = Dowa_Arena_Allocate(arena, 4096 + strlen(presigned.url));
-  snprintf(response_body, 4096 + strlen(presigned.url),
-           "{\"media_id\":%lld,\"upload_url\":\"%s\",\"expires\":%d}",
-           (long long)media_id, presigned.url, g_s3_url_expires);
+  char *response_body = Dowa_Arena_Allocate(arena, 4096 + strlen(presigned.url) + strlen(public_url));
+  snprintf(response_body, 4096 + strlen(presigned.url) + strlen(public_url),
+           "{\"media_id\":%lld,\"upload_url\":\"%s\",\"public_url\":\"%s\",\"expires\":%d}",
+           (long long)media_id, presigned.url, public_url, g_s3_url_expires);
 
   S3_Presigned_URL_Destroy(&presigned);
 
--- a/mrjunejune/src/notes/editor.js	Sun Feb 15 09:13:09 2026 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,215 +0,0 @@
-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);
-});
-
--- a/mrjunejune/src/notes/index.html	Sun Feb 15 09:13:09 2026 -0800
+++ b/mrjunejune/src/notes/index.html	Sun Feb 15 11:02:13 2026 -0800
@@ -133,8 +133,7 @@
         <span class="note-id-display" id="note-id-display">index</span>
       </h1>
       <div class="notes-actions">
-        <button onclick="showNewNoteDialog()" class="btn-primary">+ New Note</button>
-        <a href="/notes/" id="home-link">Home</a>
+        <button onclick="goHome()"> Home </button>
         <button onclick="logout()" class="btn-danger">Logout</button>
       </div>
     </div>
--- a/mrjunejune/src/public/editor.js	Sun Feb 15 09:13:09 2026 -0800
+++ b/mrjunejune/src/public/editor.js	Sun Feb 15 11:02:13 2026 -0800
@@ -14,6 +14,10 @@
   return true;
 }
 
+function goHome() {
+  window.location.href = '/notes';
+}
+
 function logout() {
   localStorage.removeItem('notes-auth-token');
   window.location.href = '/notes/login';
@@ -118,9 +122,8 @@
   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 };
+    // For non-images, use the public URL from the server
+    return { url: data.public_url };
   }
 }
 
--- a/rich_editor/rich_editor.js	Sun Feb 15 09:13:09 2026 -0800
+++ b/rich_editor/rich_editor.js	Sun Feb 15 11:02:13 2026 -0800
@@ -44,19 +44,19 @@
     toolbar.className = 'rich-editor-toolbar';
 
     const buttons = [
-      { cmd: 'h1', label: 'H1', title: 'Heading 1' },
-      { cmd: 'h2', label: 'H2', title: 'Heading 2' },
-      { cmd: 'h3', label: 'H3', title: 'Heading 3' },
+      { cmd: 'h1', label: '', title: 'Heading 1', icons: "/public/icons/h1.png" },
+      { cmd: 'h2', label: '', title: 'Heading 2', icons: "/public/icons/h2.png" },
+      { cmd: 'h3', label: '', title: 'Heading 3', icons: "/public/icons/h3.png" },
       { cmd: 'p', label: 'ΒΆ', title: 'Paragraph' },
       { cmd: 'ul', label: 'β€’', title: 'Bullet List' },
       { cmd: 'ol', label: '1.', title: 'Numbered List' },
-      { cmd: 'separator' },
-      { cmd: 'bold', label: 'B', title: 'Bold' },
-      { cmd: 'italic', label: 'I', title: 'Italic' },
+      { cmd: 'bold', label: '', title: 'Bold', icons: "/public/icons/bold.png" },
       { cmd: 'separator' },
-      { cmd: 'link', label: 'πŸ”—', title: 'Insert Link' },
-      { cmd: 'notelink', label: 'πŸ“', title: 'Link to Note' },
-      { cmd: 'upload', label: 'πŸ“Ž', title: 'Upload File' }
+      { cmd: 'link', label: '', title: 'Insert Link', icons: "/public/icons/link.png"  },
+      { cmd: 'notelink', label: '', title: 'Link to Note', icons: "/public/icons/binder.png"  },
+      { cmd: 'upload', label: '', title: 'Upload File', icons: "/public/icons/clipy.png"  },
+      { cmd: 'separator' },
+      { cmd: 'readonly', label: '', title: 'readonly', icons: "/public/icons/book.png" },
     ];
 
     buttons.forEach(btn => {
@@ -70,6 +70,8 @@
       const button = document.createElement('button');
       button.type = 'button';
       button.className = 'rich-editor-btn';
+      if (btn.icons)
+        button.style.backgroundImage = `url("${btn.icons}")`;
       button.textContent = btn.label;
       button.title = btn.title;
       button.dataset.cmd = btn.cmd;
@@ -114,14 +116,24 @@
         cursor: pointer;
         font-size: 14px;
         min-width: 32px;
+        background-repeat: no-repeat;
+        background-position: center;
+        background-size: 24px 24px;
+        border: none;
       }
 
       .rich-editor-btn:hover {
         background: #e9e9e9;
+        background-repeat: no-repeat;
+        background-position: center;
+        background-size: 24px 24px;
       }
 
       .rich-editor-btn:active {
         background: #ddd;
+        background-repeat: no-repeat;
+        background-position: center;
+        background-size: 24px 24px;
       }
 
       .rich-editor-separator {
@@ -218,6 +230,10 @@
       .rich-editor-file-input {
         display: none;
       }
+
+      ol,ul {
+        padding: 16px;
+      }
     `;
     document.head.appendChild(style);
   }
@@ -226,6 +242,7 @@
     constructor(elementId, options) {
       this.options = { ...DEFAULT_OPTIONS, ...options };
       this.container = document.getElementById(elementId);
+      this.state = { readOnly: false };
 
       if (!this.container) {
         throw new Error(`Element with id "${elementId}" not found`);
@@ -396,6 +413,35 @@
       this.fileInput.click();
     }
 
+    readOnly() {
+      this.state.readOnly = !this.state.readOnly;
+
+      if (this.state.readOnly)
+      {
+        this.content.contentEditable = false;
+        this.setStatus('Read-only mode');
+        const buttons = this.toolbar.querySelectorAll('.rich-editor-btn');
+        buttons.forEach(btn => {
+          if (btn.dataset.cmd !== 'readonly') {
+            btn.disabled = true;
+          }
+        });
+      }
+      else
+      {
+        // Enable editing
+        this.content.contentEditable = true;
+        this.content.style.backgroundColor = '';
+        this.setStatus('Ready');
+
+        // Enable toolbar buttons
+        const buttons = this.toolbar.querySelectorAll('.rich-editor-btn');
+        buttons.forEach(btn => {
+          btn.disabled = false;
+        });
+      }
+    }
+
     async uploadFile(file) {
       if (!this.options.uploadCallback) {
         console.warn('No upload callback configured');
@@ -498,6 +544,9 @@
         case 'upload':
           this.triggerFileUpload();
           break;
+        case 'readonly':
+          this.readOnly();
+          break;
       }
 
       if (this.debouncedSave) this.debouncedSave();