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