ソースを参照

add metadata endopint and auto update for notes changes

Daniel Bohry 1 ヶ月 前
コミット
758fa9283b

+ 7 - 0
src/main/java/com/lhamacorp/knotes/api/NoteController.java

@@ -1,5 +1,6 @@
 package com.lhamacorp.knotes.api;
 
+import com.lhamacorp.knotes.api.dto.NoteMetadata;
 import com.lhamacorp.knotes.api.dto.NoteRequest;
 import com.lhamacorp.knotes.api.dto.NoteResponse;
 import com.lhamacorp.knotes.api.dto.NoteUpdateRequest;
@@ -25,6 +26,12 @@ public class NoteController {
         return ResponseEntity.ok().body(NoteResponse.from(note));
     }
 
+    @GetMapping("{id}/metadata")
+    public ResponseEntity<NoteMetadata> getMetadata(@PathVariable String id) {
+        Note note = service.findById(id);
+        return ResponseEntity.ok().body(NoteMetadata.from(note));
+    }
+
     @PostMapping
     public ResponseEntity<NoteResponse> save(@RequestBody NoteRequest request) {
         Note savedNote = service.save(request.note());

+ 16 - 0
src/main/java/com/lhamacorp/knotes/api/dto/NoteMetadata.java

@@ -0,0 +1,16 @@
+package com.lhamacorp.knotes.api.dto;
+
+import com.lhamacorp.knotes.domain.Note;
+
+import java.time.Instant;
+
+public record NoteMetadata(String id, Instant createdAt, Instant modifiedAt) {
+
+    public static NoteMetadata from(Note note) {
+        return new NoteMetadata(
+            note.id(),
+            note.createdAt(),
+            note.modifiedAt()
+        );
+    }
+}

+ 4 - 0
src/main/resources/static/index.html

@@ -23,6 +23,10 @@
     </div>
 </div>
 
+<div id="updateToast" class="update-toast hidden">
+    📄 Note was updated
+</div>
+
 <div class="content">
     <textarea class="note-area" id="noteContent" placeholder="Start typing your note..."></textarea>
 </div>

+ 95 - 1
src/main/resources/static/script.js

@@ -2,6 +2,9 @@ const API_BASE = '/api/notes';
 let currentNoteId = null;
 let autoSaveTimeout = null;
 let lastSavedContent = '';
+let currentNoteModifiedAt = null;
+let versionCheckInterval = null;
+const VERSION_CHECK_INTERVAL_MS = 5000;
 
 // Theme management
 function initializeTheme() {
@@ -184,7 +187,7 @@ async function autoSave(content) {
     try {
         if (currentNoteId) {
             // Update existing note
-            await fetch(API_BASE, {
+            const response = await fetch(API_BASE, {
                 method: 'PUT',
                 headers: {
                     'Content-Type': 'application/json',
@@ -194,6 +197,12 @@ async function autoSave(content) {
                     content: content
                 })
             });
+
+            if (response.ok) {
+                const updatedNote = await response.json();
+                // Update version tracking after successful save
+                setCurrentNoteVersion(updatedNote.modifiedAt);
+            }
         } else {
             // Create new note
             const response = await fetch(API_BASE, {
@@ -216,6 +225,9 @@ async function autoSave(content) {
 
                 // Show note ID in header
                 showNoteId(note.id);
+
+                // Set version tracking for new note
+                setCurrentNoteVersion(note.modifiedAt);
             }
         }
 
@@ -226,6 +238,9 @@ async function autoSave(content) {
 }
 
 async function loadNoteById(id) {
+    // Stop any existing version polling
+    stopVersionPolling();
+
     try {
         const response = await fetch(`${API_BASE}/${id}`);
 
@@ -248,6 +263,9 @@ async function loadNoteById(id) {
 
             // Show note ID in header
             showNoteId(note.id);
+
+            // Set current version and start polling for updates
+            setCurrentNoteVersion(note.modifiedAt);
         } else {
             // Note not found (404) or other error - create a new note instead
             console.warn(`Note with ID ${id} not found (${response.status}), creating new note`);
@@ -311,6 +329,9 @@ async function copyNoteLink() {
 }
 
 async function newNote() {
+    // Stop any existing version polling
+    stopVersionPolling();
+
     try {
         const response = await fetch(API_BASE, {
             method: 'POST',
@@ -342,6 +363,9 @@ async function newNote() {
 
             // Show note ID in header
             showNoteId(note.id);
+
+            // Set current version and start polling for updates
+            setCurrentNoteVersion(note.modifiedAt);
         }
     } catch (error) {
         console.error('Failed to create new note:', error);
@@ -393,4 +417,74 @@ function loadNoteFromInput() {
     }
 }
 
+// Version checking and polling
+async function checkForNoteUpdates() {
+    if (!currentNoteId || !currentNoteModifiedAt) {
+        return; // No active note to check
+    }
+
+    try {
+        const response = await fetch(`${API_BASE}/${currentNoteId}/metadata`);
+
+        if (response.ok) {
+            const metadata = await response.json();
+            const serverModifiedAt = new Date(metadata.modifiedAt).getTime();
+            const localModifiedAt = new Date(currentNoteModifiedAt).getTime();
+
+            if (serverModifiedAt > localModifiedAt) {
+                // Note has been updated elsewhere - auto-reload it
+                await autoReloadNote();
+            }
+        }
+    } catch (error) {
+        console.error('Failed to check for note updates:', error);
+    }
+}
+
+function startVersionPolling() {
+    // Clear any existing polling
+    stopVersionPolling();
+
+    if (currentNoteId) {
+        versionCheckInterval = setInterval(checkForNoteUpdates, VERSION_CHECK_INTERVAL_MS);
+    }
+}
+
+function stopVersionPolling() {
+    if (versionCheckInterval) {
+        clearInterval(versionCheckInterval);
+        versionCheckInterval = null;
+    }
+}
+
+function setCurrentNoteVersion(modifiedAt) {
+    currentNoteModifiedAt = modifiedAt;
+    startVersionPolling();
+}
+
+function showUpdateToast() {
+    const toast = document.getElementById('updateToast');
+    if (toast) {
+        toast.classList.remove('hidden');
+
+        // Hide after 2 seconds
+        setTimeout(() => {
+            toast.classList.add('hidden');
+        }, 2000);
+    }
+}
+
+async function autoReloadNote() {
+    if (currentNoteId) {
+        // Temporarily stop polling to avoid conflicts during reload
+        stopVersionPolling();
+
+        // Reload the note
+        await loadNoteById(currentNoteId);
+
+        // Show brief update message
+        showUpdateToast();
+    }
+}
+
 window.addEventListener('load', init);

+ 39 - 0
src/main/resources/static/style.css

@@ -119,6 +119,36 @@ body {
     gap: 10px;
 }
 
+/* Update toast message */
+.update-toast {
+    position: fixed;
+    bottom: 20px;
+    right: 20px;
+    background: var(--accent-primary);
+    color: white;
+    padding: 12px 18px;
+    border-radius: 8px;
+    font-size: 14px;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+    z-index: 1000;
+    animation: slideInUp 0.3s ease-out;
+}
+
+.update-toast.hidden {
+    display: none;
+}
+
+@keyframes slideInUp {
+    from {
+        transform: translateY(100%);
+        opacity: 0;
+    }
+    to {
+        transform: translateY(0);
+        opacity: 1;
+    }
+}
+
 .new-btn {
     background: var(--accent-primary);
     color: white;
@@ -520,6 +550,15 @@ body {
         transform: scale(0.98);
     }
 
+    /* Toast mobile styles */
+    .update-toast {
+        bottom: 15px;
+        right: 15px;
+        padding: 10px 15px;
+        font-size: 13px;
+        border-radius: 6px;
+    }
+
     .dialog-btn:active {
         transform: scale(0.98);
     }