view 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 source

<!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>