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