Mercurial
diff mrjunejune/src/notes/editor.js @ 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/editor.js Sun Feb 15 07:07:50 2026 -0800 @@ -0,0 +1,204 @@ +console.log("june"); + +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 createResp = 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 (!createResp.ok) { + const error = await createResp.json(); + throw new Error(error.error || 'Failed to create media record'); + } + + const { media_id, upload_url } = await createResp.json(); + + // 2. Upload to S3 + const uploadResp = await fetch(upload_url, { + method: 'PUT', + headers: { 'Content-Type': file.type }, + body: file + }); + + if (!uploadResp.ok) { + throw new Error('S3 upload failed'); + } + + // 3. Mark uploaded + await fetch(`/api/media/${media_id}/uploaded`, { + method: 'POST', + headers: { 'Authorization': 'Bearer ' + token } + }); + + // 4. Poll for images, immediate return for non-images + if (file.type.startsWith('image/')) { + return await pollForProcessedImage(media_id); + } else { + // For non-images, return the original S3 URL + const s3_url = upload_url.split('?')[0]; + return { url: s3_url }; + } +} + +async function pollForProcessedImage(mediaId) { + const token = getAuthToken(); + const maxAttempts = 60; // 2 minutes max + + for (let i = 0; i < maxAttempts; i++) { + await new Promise(r => setTimeout(r, 2000)); // 2 sec interval + + const resp = await fetch(`/api/media/${mediaId}/status`, { + headers: { 'Authorization': 'Bearer ' + token } + }); + + if (!resp.ok) continue; + + const { status, processed_url, error_message } = await resp.json(); + + if (status === 'finished') return { url: processed_url }; + if (status === 'error') throw new Error(error_message || 'Processing failed'); + } + + throw new Error('Processing timeout'); +} + +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); +});