diff mrjunejune/src/notes/index.html @ 201:6cdee35a7ba9

[MrJuneJune] notes
author MrJuneJune <me@mrjunejune.com>
date Sun, 15 Feb 2026 07:07:50 -0800
parents
children b9b184b3303c
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/notes/index.html	Sun Feb 15 07:07:50 2026 -0800
@@ -0,0 +1,331 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  {{/parts/base_head.html}}
+  <title>Notes</title>
+  <style>
+    .notes-page {
+      max-width: 900px;
+      margin: 0 auto;
+      padding: 20px;
+    }
+    .notes-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 20px;
+      flex-wrap: wrap;
+      gap: 12px;
+    }
+    .notes-header h1 {
+      margin: 0;
+      display: flex;
+      align-items: center;
+      gap: 12px;
+    }
+    .note-id-display {
+      font-size: 14px;
+      color: #666;
+      background: #f0f0f0;
+      padding: 4px 12px;
+      border-radius: 4px;
+      font-weight: normal;
+    }
+    .notes-actions {
+      display: flex;
+      gap: 8px;
+      align-items: center;
+    }
+    .notes-actions button, .notes-actions a {
+      padding: 8px 16px;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+      background: #fff;
+      cursor: pointer;
+      text-decoration: none;
+      color: #333;
+      font-size: 14px;
+    }
+    .notes-actions button:hover, .notes-actions a:hover {
+      background: #f5f5f5;
+    }
+    .notes-actions .btn-primary {
+      background: #0078ff;
+      color: white;
+      border-color: #0078ff;
+    }
+    .notes-actions .btn-primary:hover {
+      background: #0066dd;
+    }
+    .notes-actions .btn-danger {
+      color: #d32f2f;
+      border-color: #d32f2f;
+    }
+    .new-note-dialog {
+      position: fixed;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      background: rgba(0,0,0,0.5);
+      display: none;
+      align-items: center;
+      justify-content: center;
+      z-index: 100;
+    }
+    .new-note-dialog.show {
+      display: flex;
+    }
+    .new-note-form {
+      background: white;
+      padding: 24px;
+      border-radius: 8px;
+      width: 400px;
+      max-width: 90%;
+    }
+    .new-note-form h2 {
+      margin: 0 0 16px 0;
+    }
+    .new-note-form input {
+      width: 100%;
+      padding: 10px;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+      font-size: 16px;
+      box-sizing: border-box;
+      margin-bottom: 16px;
+    }
+    .new-note-form .form-actions {
+      display: flex;
+      gap: 8px;
+      justify-content: flex-end;
+    }
+    .new-note-form button {
+      padding: 8px 16px;
+      border-radius: 4px;
+      cursor: pointer;
+      font-size: 14px;
+    }
+    .new-note-form .btn-cancel {
+      background: #f5f5f5;
+      border: 1px solid #ccc;
+    }
+    .new-note-form .btn-create {
+      background: #0078ff;
+      color: white;
+      border: none;
+    }
+    .note-hint {
+      font-size: 12px;
+      color: #666;
+      margin-top: -12px;
+      margin-bottom: 16px;
+    }
+  </style>
+</head>
+<body>
+  {{/parts/header.html}}
+
+  <main class="notes-page">
+    <div class="notes-header">
+      <h1>
+        Notes
+        <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="logout()" class="btn-danger">Logout</button>
+      </div>
+    </div>
+
+    <div id="editor-container"></div>
+  </main>
+
+  <!-- New Note Dialog -->
+  <div class="new-note-dialog" id="new-note-dialog">
+    <div class="new-note-form">
+      <h2>Create New Note</h2>
+      <input type="text" id="new-note-id" placeholder="note-id (e.g., my-ideas)">
+      <p class="note-hint">Use lowercase letters, numbers, and hyphens only</p>
+      <div class="form-actions">
+        <button class="btn-cancel" onclick="hideNewNoteDialog()">Cancel</button>
+        <button class="btn-create" onclick="createNewNote()">Create & Open</button>
+      </div>
+    </div>
+  </div>
+
+  {{/parts/footer.html}}
+
+  <script src="/public/js/rich_editor.js"></script>
+  <script>
+
+    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');
+      }
+
+      //  Get s3 bucket URL
+      const response = await fetch('/api/s3/upload-url', {
+        method: 'POST',
+        headers: {
+          'Authorization': 'Bearer ' + token,
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          filename: file.name,
+          content_type: file.type
+        })
+      });
+
+      if (!response.ok) {
+        const error = await response.json();
+        throw new Error(error.error || 'Failed to get upload URL');
+      }
+
+      const data = await response.json();
+
+      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');
+      }
+
+      return { url: data.public_url, key: data.key };
+    }
+
+    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);
+    });
+  </script>
+</body>
+</html>