Mercurial
changeset 204:e5aed6c36672
[Notes] Added icons and updated styling a bit. Probalby usable now.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 15 Feb 2026 11:02:13 -0800 |
| parents | 92a57bd716c1 |
| children | e07b4b5a66bb |
| files | assets/BUILD assets/Open fully.png assets/cabinet.png assets/icons/Open fully.png assets/icons/binder.png assets/icons/bold.png assets/icons/book.png assets/icons/cabinet.png assets/icons/clipy.png assets/icons/h1.png assets/icons/h2.png assets/icons/h3.png assets/icons/link.png assets/icons/open-book.png mrjunejune/BUILD mrjunejune/main.c mrjunejune/src/notes/editor.js mrjunejune/src/notes/index.html mrjunejune/src/public/editor.js rich_editor/rich_editor.js |
| diffstat | 20 files changed, 106 insertions(+), 235 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/assets/BUILD Sun Feb 15 11:02:13 2026 -0800 @@ -0,0 +1,5 @@ +filegroup( + name = "icons", + srcs = glob(["icons/*.png"]), + visibility = ["//visibility:public"], +)
--- a/mrjunejune/BUILD Sun Feb 15 09:13:09 2026 -0800 +++ b/mrjunejune/BUILD Sun Feb 15 11:02:13 2026 -0800 @@ -36,6 +36,14 @@ dest = "src/public/js", ) +move_files_into_dir( + name = "icons", + srcs = [ + "//assets:icons", + ], + dest = "src/public/icons", +) + filegroup( name = "public_files", srcs = glob(["src/public/*"]), @@ -50,7 +58,7 @@ filegroup( name = "src_files", - srcs = glob(["src/**"]) + [":react_pages", ":shared_js_non_public", ":shared_js_file", ":rich_editor_js"], + srcs = glob(["src/**"]) + [":react_pages", ":shared_js_non_public", ":shared_js_file", ":rich_editor_js", ":icons"], visibility = ["//mrjunejune/test:__pkg__"], )
--- a/mrjunejune/main.c Sun Feb 15 09:13:09 2026 -0800 +++ b/mrjunejune/main.c Sun Feb 15 11:02:13 2026 -0800 @@ -1195,7 +1195,17 @@ char s3_key_original[512]; char s3_key_processed[512]; snprintf(s3_key_original, sizeof(s3_key_original), "uploads/%s/%s", uuid, filename); - snprintf(s3_key_processed, sizeof(s3_key_processed), "uploads/%s/processed.webp", uuid); + + // Only use .webp for images + int is_image = (strncmp(content_type, "image/", 6) == 0); + if (is_image) + { + snprintf(s3_key_processed, sizeof(s3_key_processed), "uploads/%s/processed.webp", uuid); + } + else + { + s3_key_processed[0] = '\0'; // No processed version for non-images + } // Insert into database const char *insert_query = @@ -1238,11 +1248,23 @@ return resp; } + // Build public URL using CloudFront or S3 + char public_url[512]; + if (g_s3_cloudfront_url[0]) + { + snprintf(public_url, sizeof(public_url), "%s/%s", g_s3_cloudfront_url, s3_key_original); + } + else + { + snprintf(public_url, sizeof(public_url), "https://%s.s3.%s.amazonaws.com/%s", + g_s3_bucket, g_s3_region, s3_key_original); + } + // Build response - char *response_body = Dowa_Arena_Allocate(arena, 4096 + strlen(presigned.url)); - snprintf(response_body, 4096 + strlen(presigned.url), - "{\"media_id\":%lld,\"upload_url\":\"%s\",\"expires\":%d}", - (long long)media_id, presigned.url, g_s3_url_expires); + char *response_body = Dowa_Arena_Allocate(arena, 4096 + strlen(presigned.url) + strlen(public_url)); + snprintf(response_body, 4096 + strlen(presigned.url) + strlen(public_url), + "{\"media_id\":%lld,\"upload_url\":\"%s\",\"public_url\":\"%s\",\"expires\":%d}", + (long long)media_id, presigned.url, public_url, g_s3_url_expires); S3_Presigned_URL_Destroy(&presigned);
--- a/mrjunejune/src/notes/editor.js Sun Feb 15 09:13:09 2026 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,215 +0,0 @@ -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 createResponse = 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 (!createResponse.ok) { - const error = await createResponse.json().catch(() => ({})); - throw new Error(error.error || 'Failed to create media record'); - } - - const data = await createResponse.json(); - - // 2. Upload file directly to S3 - 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'); - } - - // 3. Mark as uploaded (triggers processing for images) - await fetch(`/api/media/${data.media_id}/uploaded`, { - method: 'POST', - headers: { - 'Authorization': 'Bearer ' + token - } - }); - - // 4. Poll for images, return immediately for non-images - if (file.type.startsWith('image/')) { - return await pollForProcessedImage(data.media_id, token); - } else { - // For non-images, construct the public URL - const publicUrl = data.upload_url.split('?')[0]; // Remove query params - return { url: publicUrl }; - } -} - -async function pollForProcessedImage(mediaId, token) { - const maxAttempts = 60; // 2 minutes max (60 * 2 seconds) - - for (let i = 0; i < maxAttempts; i++) { - await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second interval - - const statusResponse = await fetch(`/api/media/${mediaId}/status`, { - headers: { - 'Authorization': 'Bearer ' + token - } - }); - - if (!statusResponse.ok) { - console.warn('Status check failed, retrying...'); - continue; - } - - const statusData = await statusResponse.json(); - - if (statusData.status === 'finished') { - return { url: statusData.processed_url }; - } else if (statusData.status === 'error') { - throw new Error(statusData.error_message || 'Processing failed'); - } - // Status is 'uploaded' or 'processing', continue polling - } - - throw new Error('Processing timeout after 2 minutes'); -} - -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); -}); -
--- a/mrjunejune/src/notes/index.html Sun Feb 15 09:13:09 2026 -0800 +++ b/mrjunejune/src/notes/index.html Sun Feb 15 11:02:13 2026 -0800 @@ -133,8 +133,7 @@ <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="goHome()"> Home </button> <button onclick="logout()" class="btn-danger">Logout</button> </div> </div>
--- a/mrjunejune/src/public/editor.js Sun Feb 15 09:13:09 2026 -0800 +++ b/mrjunejune/src/public/editor.js Sun Feb 15 11:02:13 2026 -0800 @@ -14,6 +14,10 @@ return true; } +function goHome() { + window.location.href = '/notes'; +} + function logout() { localStorage.removeItem('notes-auth-token'); window.location.href = '/notes/login'; @@ -118,9 +122,8 @@ if (file.type.startsWith('image/')) { return await pollForProcessedImage(data.media_id, token); } else { - // For non-images, construct the public URL - const publicUrl = data.upload_url.split('?')[0]; // Remove query params - return { url: publicUrl }; + // For non-images, use the public URL from the server + return { url: data.public_url }; } }
--- a/rich_editor/rich_editor.js Sun Feb 15 09:13:09 2026 -0800 +++ b/rich_editor/rich_editor.js Sun Feb 15 11:02:13 2026 -0800 @@ -44,19 +44,19 @@ toolbar.className = 'rich-editor-toolbar'; const buttons = [ - { cmd: 'h1', label: 'H1', title: 'Heading 1' }, - { cmd: 'h2', label: 'H2', title: 'Heading 2' }, - { cmd: 'h3', label: 'H3', title: 'Heading 3' }, + { cmd: 'h1', label: '', title: 'Heading 1', icons: "/public/icons/h1.png" }, + { cmd: 'h2', label: '', title: 'Heading 2', icons: "/public/icons/h2.png" }, + { cmd: 'h3', label: '', title: 'Heading 3', icons: "/public/icons/h3.png" }, { cmd: 'p', label: 'ΒΆ', title: 'Paragraph' }, { cmd: 'ul', label: 'β’', title: 'Bullet List' }, { cmd: 'ol', label: '1.', title: 'Numbered List' }, - { cmd: 'separator' }, - { cmd: 'bold', label: 'B', title: 'Bold' }, - { cmd: 'italic', label: 'I', title: 'Italic' }, + { cmd: 'bold', label: '', title: 'Bold', icons: "/public/icons/bold.png" }, { cmd: 'separator' }, - { cmd: 'link', label: 'π', title: 'Insert Link' }, - { cmd: 'notelink', label: 'π', title: 'Link to Note' }, - { cmd: 'upload', label: 'π', title: 'Upload File' } + { cmd: 'link', label: '', title: 'Insert Link', icons: "/public/icons/link.png" }, + { cmd: 'notelink', label: '', title: 'Link to Note', icons: "/public/icons/binder.png" }, + { cmd: 'upload', label: '', title: 'Upload File', icons: "/public/icons/clipy.png" }, + { cmd: 'separator' }, + { cmd: 'readonly', label: '', title: 'readonly', icons: "/public/icons/book.png" }, ]; buttons.forEach(btn => { @@ -70,6 +70,8 @@ const button = document.createElement('button'); button.type = 'button'; button.className = 'rich-editor-btn'; + if (btn.icons) + button.style.backgroundImage = `url("${btn.icons}")`; button.textContent = btn.label; button.title = btn.title; button.dataset.cmd = btn.cmd; @@ -114,14 +116,24 @@ cursor: pointer; font-size: 14px; min-width: 32px; + background-repeat: no-repeat; + background-position: center; + background-size: 24px 24px; + border: none; } .rich-editor-btn:hover { background: #e9e9e9; + background-repeat: no-repeat; + background-position: center; + background-size: 24px 24px; } .rich-editor-btn:active { background: #ddd; + background-repeat: no-repeat; + background-position: center; + background-size: 24px 24px; } .rich-editor-separator { @@ -218,6 +230,10 @@ .rich-editor-file-input { display: none; } + + ol,ul { + padding: 16px; + } `; document.head.appendChild(style); } @@ -226,6 +242,7 @@ constructor(elementId, options) { this.options = { ...DEFAULT_OPTIONS, ...options }; this.container = document.getElementById(elementId); + this.state = { readOnly: false }; if (!this.container) { throw new Error(`Element with id "${elementId}" not found`); @@ -396,6 +413,35 @@ this.fileInput.click(); } + readOnly() { + this.state.readOnly = !this.state.readOnly; + + if (this.state.readOnly) + { + this.content.contentEditable = false; + this.setStatus('Read-only mode'); + const buttons = this.toolbar.querySelectorAll('.rich-editor-btn'); + buttons.forEach(btn => { + if (btn.dataset.cmd !== 'readonly') { + btn.disabled = true; + } + }); + } + else + { + // Enable editing + this.content.contentEditable = true; + this.content.style.backgroundColor = ''; + this.setStatus('Ready'); + + // Enable toolbar buttons + const buttons = this.toolbar.querySelectorAll('.rich-editor-btn'); + buttons.forEach(btn => { + btn.disabled = false; + }); + } + } + async uploadFile(file) { if (!this.options.uploadCallback) { console.warn('No upload callback configured'); @@ -498,6 +544,9 @@ case 'upload': this.triggerFileUpload(); break; + case 'readonly': + this.readOnly(); + break; } if (this.debouncedSave) this.debouncedSave();