Bladeren bron

add frontend

Daniel Bohry 1 maand geleden
bovenliggende
commit
a5cffc6e8c

+ 1 - 1
.github/workflows/buildAndRelease.yml

@@ -41,4 +41,4 @@ jobs:
       - name: Build and Push Image
         run: |
           docker buildx create --use
-          docker buildx build --platform linux/amd64,linux/arm64 -t dbohry/knotes-be:latest --push .
+          docker buildx build --platform linux/amd64,linux/arm64 -t dbohry/knotes:latest --push .

+ 1 - 1
gradlew

@@ -6,7 +6,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
 @SpringBootApplication
 public class App {
 
-    public static void main(String[] args) {
+    static void main(String[] args) {
         SpringApplication.run(App.class, args);
     }
 

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

@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.*;
 
 @RestController
 @RequestMapping("api/notes")
+@CrossOrigin(origins = "*")
 public class NoteController {
 
     private final NoteService service;

+ 1 - 1
src/main/resources/application.yml

@@ -4,4 +4,4 @@ server:
 spring:
   mongodb:
     database: ${database:knotes}
-    uri: ${mongo:}
+    uri: ${mongo:mongodb://localhost:27017}

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

@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>kNotes</title>
+    <link rel="stylesheet" href="style.css">
+</head>
+<body>
+    <div class="header">
+        <div class="title">KNotes</div>
+        <div class="header-right">
+            <span class="note-id" id="noteIdDisplay" style="display: none;"></span>
+            <button class="new-btn" onclick="showIdInput()">Open</button>
+            <button class="new-btn" onclick="newNote()">New</button>
+        </div>
+    </div>
+
+    <div class="content">
+        <textarea class="note-area" id="noteContent" placeholder="Start typing your note..."></textarea>
+    </div>
+
+    <div id="idInputOverlay" class="id-input-overlay hidden">
+        <div class="id-input-dialog">
+            <h3>Open Note</h3>
+            <input type="text" id="noteIdInput" class="id-input" placeholder="Enter note ID" />
+            <div class="dialog-buttons">
+                <button class="dialog-btn secondary" onclick="hideIdInput()">Cancel</button>
+                <button class="dialog-btn primary" onclick="loadNoteFromInput()">Open</button>
+            </div>
+        </div>
+    </div>
+
+    <script src="script.js"></script>
+</body>
+</html>

+ 197 - 0
src/main/resources/static/script.js

@@ -0,0 +1,197 @@
+const API_BASE = '/api/notes';
+let currentNoteId = null;
+let autoSaveTimeout = null;
+let lastSavedContent = '';
+
+// Initialize on page load
+function init() {
+    const urlParams = new URLSearchParams(window.location.search);
+    const idFromUrl = urlParams.get('id');
+
+    if (idFromUrl) {
+        loadNoteById(idFromUrl);
+    } else {
+        // Create a new note immediately if no ID in URL
+        newNote();
+    }
+
+    // Set up auto-save on content change
+    const noteContent = document.getElementById('noteContent');
+    noteContent.addEventListener('input', handleContentChange);
+
+    // Handle Enter key in ID input
+    document.getElementById('noteIdInput').addEventListener('keypress', function(e) {
+        if (e.key === 'Enter') {
+            loadNoteFromInput();
+        }
+    });
+}
+
+// Handle content change for auto-save
+function handleContentChange() {
+    const content = document.getElementById('noteContent').value;
+
+    // Clear existing timeout
+    if (autoSaveTimeout) {
+        clearTimeout(autoSaveTimeout);
+    }
+
+    // Use 3-second debounce for all saves
+    autoSaveTimeout = setTimeout(() => {
+        autoSave(content);
+    }, 3000);
+}
+
+// Auto-save function
+async function autoSave(content) {
+    // Don't save if content hasn't changed
+    if (content === lastSavedContent) {
+        return;
+    }
+
+    try {
+        if (currentNoteId) {
+            // Update existing note
+            await fetch(API_BASE, {
+                method: 'PUT',
+                headers: {
+                    'Content-Type': 'application/json',
+                },
+                body: JSON.stringify({
+                    id: currentNoteId,
+                    content: content
+                })
+            });
+        } else {
+            // Create new note
+            const response = await fetch(API_BASE, {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json',
+                },
+                body: JSON.stringify({
+                    note: content
+                })
+            });
+
+            if (response.ok) {
+                const note = await response.json();
+                currentNoteId = note.id;
+
+                // Update URL with new note ID
+                const url = new URL(window.location);
+                url.searchParams.set('id', note.id);
+                window.history.replaceState({}, '', url);
+
+                // Show note ID in header
+                showNoteId(note.id);
+            }
+        }
+
+        lastSavedContent = content;
+    } catch (error) {
+        // Silently fail - no user feedback needed as per requirements
+        console.error('Auto-save failed:', error);
+    }
+}
+
+// Load note by ID
+async function loadNoteById(id) {
+    try {
+        const response = await fetch(`${API_BASE}/${id}`);
+
+        if (response.ok) {
+            const note = await response.json();
+            currentNoteId = note.id;
+
+            // Update content
+            document.getElementById('noteContent').value = note.content;
+            lastSavedContent = note.content;
+
+            // Update URL
+            const url = new URL(window.location);
+            url.searchParams.set('id', note.id);
+            window.history.replaceState({}, '', url);
+
+            // Show note ID in header
+            showNoteId(note.id);
+        }
+    } catch (error) {
+        // Silently fail - no user feedback
+        console.error('Failed to load note:', error);
+    }
+}
+
+// Show note ID in header
+function showNoteId(id) {
+    const noteIdDisplay = document.getElementById('noteIdDisplay');
+    noteIdDisplay.textContent = id;
+    noteIdDisplay.style.display = 'inline-block';
+}
+
+// Hide note ID in header
+function hideNoteId() {
+    document.getElementById('noteIdDisplay').style.display = 'none';
+}
+
+// Create new note
+async function newNote() {
+    // Create empty note immediately to get an ID
+    try {
+        const response = await fetch(API_BASE, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            body: JSON.stringify({
+                note: ''
+            })
+        });
+
+        if (response.ok) {
+            const note = await response.json();
+            currentNoteId = note.id;
+            lastSavedContent = '';
+
+            // Clear content
+            document.getElementById('noteContent').value = '';
+
+            // Update URL with new note ID
+            const url = new URL(window.location);
+            url.searchParams.set('id', note.id);
+            window.history.replaceState({}, '', url);
+
+            // Show note ID in header
+            showNoteId(note.id);
+
+            // Focus on content area
+            document.getElementById('noteContent').focus();
+        }
+    } catch (error) {
+        console.error('Failed to create new note:', error);
+    }
+}
+
+// Show ID input overlay
+function showIdInput() {
+    document.getElementById('idInputOverlay').classList.remove('hidden');
+    document.getElementById('noteIdInput').focus();
+}
+
+// Hide ID input overlay
+function hideIdInput() {
+    document.getElementById('idInputOverlay').classList.add('hidden');
+    document.getElementById('noteIdInput').value = '';
+}
+
+// Load note from input
+function loadNoteFromInput() {
+    const id = document.getElementById('noteIdInput').value.trim();
+    if (id) {
+        hideIdInput();
+        loadNoteById(id);
+    }
+}
+
+// Initialize when page loads
+window.addEventListener('load', init);

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

@@ -0,0 +1,139 @@
+@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap');
+
+* {
+    margin: 0;
+    padding: 0;
+    box-sizing: border-box;
+}
+
+body {
+    font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+    background-color: #fafafa;
+    height: 100vh;
+    display: flex;
+    flex-direction: column;
+}
+
+.header {
+    background: white;
+    border-bottom: 1px solid #e0e0e0;
+    padding: 15px 20px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    min-height: 60px;
+}
+
+.title {
+    font-size: 18px;
+    color: #333;
+    font-weight: 500;
+}
+
+.note-id {
+    font-family: monospace;
+    font-size: 12px;
+    color: #666;
+    background: #f5f5f5;
+    padding: 4px 8px;
+    border-radius: 3px;
+}
+
+.new-btn {
+    background: #007bff;
+    color: white;
+    border: none;
+    padding: 8px 16px;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 14px;
+}
+
+.new-btn:hover {
+    background: #0056b3;
+}
+
+.content {
+    flex: 1;
+    display: flex;
+}
+
+.note-area {
+    flex: 1;
+    border: none;
+    outline: none;
+    padding: 30px;
+    font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+    font-size: 16px;
+    line-height: 1.6;
+    resize: none;
+    background: white;
+}
+
+.note-area::placeholder {
+    color: #999;
+}
+
+.id-input-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0,0,0,0.5);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 1000;
+}
+
+.id-input-dialog {
+    background: white;
+    padding: 30px;
+    border-radius: 8px;
+    box-shadow: 0 4px 20px rgba(0,0,0,0.3);
+    max-width: 400px;
+    width: 90%;
+}
+
+.id-input-dialog h3 {
+    margin-bottom: 15px;
+    color: #333;
+}
+
+.id-input {
+    width: 100%;
+    padding: 12px;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+    font-size: 14px;
+    margin-bottom: 15px;
+}
+
+.dialog-buttons {
+    display: flex;
+    gap: 10px;
+    justify-content: flex-end;
+}
+
+.dialog-btn {
+    padding: 8px 16px;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 14px;
+}
+
+.dialog-btn.primary {
+    background: #007bff;
+    color: white;
+}
+
+.dialog-btn.secondary {
+    background: #6c757d;
+    color: white;
+}
+
+.hidden {
+    display: none;
+}