瀏覽代碼

add 404 page

Daniel Bohry 1 月之前
父節點
當前提交
1288a99ad4

+ 10 - 6
src/main/java/com/lhamacorp/knotes/service/NoteService.java

@@ -12,27 +12,31 @@ import java.time.Instant;
 @Service
 public class NoteService {
 
-    private final NoteRepository noteRepository;
+    private final NoteRepository repository;
 
-    public NoteService(NoteRepository noteRepository) {
-        this.noteRepository = noteRepository;
+    public NoteService(NoteRepository repository) {
+        this.repository = repository;
+    }
+
+    public boolean exists(String id) {
+        return repository.existsById(id);
     }
 
     public Note findById(String id) {
-        return noteRepository.findById(id)
+        return repository.findById(id)
             .orElseThrow(() -> new NotFoundException("Note with id " + id + " not found!"));
     }
 
     public Note save(String content) {
         Ulid id = UlidCreator.getUlid();
         Instant now = Instant.now();
-        return noteRepository.save(new Note(id.toString(), content, now, now));
+        return repository.save(new Note(id.toString(), content, now, now));
     }
 
     public Note update(String id, String content) {
         Note note = findById(id);
         Instant now = Instant.now();
-        return noteRepository.save(new Note(id, content, note.createdAt(), now));
+        return repository.save(new Note(id, content, note.createdAt(), now));
     }
 
 }

+ 13 - 3
src/main/java/com/lhamacorp/knotes/web/WebController.java

@@ -1,5 +1,6 @@
 package com.lhamacorp.knotes.web;
 
+import com.lhamacorp.knotes.service.NoteService;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -7,12 +8,21 @@ import org.springframework.web.bind.annotation.PathVariable;
 @Controller
 public class WebController {
 
+    private final NoteService service;
+
+    public WebController(NoteService service) {
+        this.service = service;
+    }
+
     /**
      * Handle note ID paths by serving the index.html file
-     * Matches note IDs which are typically 26-character ULIDs
+     * Matches any alphanumeric ID and forwards to 404 if not found
      */
-    @GetMapping("/{noteId:[A-Za-z0-9]{26}}")
+    @GetMapping("/{noteId:[A-Za-z0-9]+}")
     public String serveNoteByPath(@PathVariable String noteId) {
-        return "forward:/index.html";
+        return service.exists(noteId)
+                ? "forward:/index.html"
+                : "forward:/404.html";
     }
+
 }

+ 6 - 0
src/main/resources/application.properties

@@ -0,0 +1,6 @@
+# MongoDB configuration
+spring.data.mongodb.uri=mongodb://localhost:27017/knotes
+
+# Error handling configuration
+server.error.whitelabel.enabled=false
+server.error.path=/error

+ 158 - 0
src/main/resources/static/404.html

@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <meta name="theme-color" content="#007bff">
+    <meta name="apple-mobile-web-app-capable" content="yes">
+    <meta name="apple-mobile-web-app-status-bar-style" content="default">
+    <meta name="apple-mobile-web-app-title" content="kNotes">
+    <meta name="mobile-web-app-capable" content="yes">
+    <meta name="application-name" content="kNotes">
+    <title>Page Not Found - kNotes</title>
+    <link rel="stylesheet" href="style.css">
+</head>
+<body>
+<div class="header">
+    <div class="title"><img src="logo.png" alt="kNotes logo"/> kNotes</div>
+    <div class="header-right">
+        <div class="theme-switch" id="themeSwitch" onclick="toggleTheme()" title="Toggle dark/light theme"></div>
+        <button class="new-btn" onclick="window.location.href='/'">Home</button>
+    </div>
+</div>
+
+<div class="content">
+    <div class="error-content">
+        <div class="error-icon">📄</div>
+        <h1 class="error-title">Page Not Found</h1>
+        <p class="error-message">
+            The page you're looking for doesn't exist. It might have been moved, deleted,
+            or you entered the wrong URL.
+        </p>
+        <div class="error-actions">
+            <button class="dialog-btn primary" onclick="window.location.href='/'">
+                Create New Note
+            </button>
+            <button class="dialog-btn secondary" onclick="showIdInput()">
+                Open Existing Note
+            </button>
+        </div>
+    </div>
+</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>
+    function initializeTheme() {
+        const savedTheme = localStorage.getItem('theme') || 'light';
+        const body = document.body;
+        const themeSwitch = document.getElementById('themeSwitch');
+
+        if (!themeSwitch) return;
+
+        if (savedTheme === 'dark') {
+            body.setAttribute('data-theme', 'dark');
+            themeSwitch.classList.add('dark');
+        } else {
+            body.removeAttribute('data-theme');
+            themeSwitch.classList.remove('dark');
+        }
+
+        updateThemeColor();
+    }
+
+    function toggleTheme() {
+        const body = document.body;
+        const themeSwitch = document.getElementById('themeSwitch');
+
+        if (!themeSwitch) return;
+
+        const currentTheme = body.getAttribute('data-theme');
+
+        if (currentTheme === 'dark') {
+            body.removeAttribute('data-theme');
+            themeSwitch.classList.remove('dark');
+            localStorage.setItem('theme', 'light');
+        } else {
+            body.setAttribute('data-theme', 'dark');
+            themeSwitch.classList.add('dark');
+            localStorage.setItem('theme', 'dark');
+        }
+
+        updateThemeColor();
+    }
+
+    function updateThemeColor() {
+        const themeColorMeta = document.querySelector('meta[name="theme-color"]');
+        const currentTheme = document.body.getAttribute('data-theme');
+
+        if (themeColorMeta) {
+            if (currentTheme === 'dark') {
+                themeColorMeta.setAttribute('content', '#2d2d2d');
+            } else {
+                themeColorMeta.setAttribute('content', '#007bff');
+            }
+        }
+    }
+
+    function showIdInput() {
+        const idInputOverlay = document.getElementById('idInputOverlay');
+        const noteIdInput = document.getElementById('noteIdInput');
+
+        if (idInputOverlay) {
+            idInputOverlay.classList.remove('hidden');
+        }
+
+        if (noteIdInput) {
+            noteIdInput.focus();
+        }
+    }
+
+    function hideIdInput() {
+        const idInputOverlay = document.getElementById('idInputOverlay');
+        const noteIdInput = document.getElementById('noteIdInput');
+
+        if (idInputOverlay) {
+            idInputOverlay.classList.add('hidden');
+        }
+
+        if (noteIdInput) {
+            noteIdInput.value = '';
+        }
+    }
+
+    function loadNoteFromInput() {
+        const noteIdInput = document.getElementById('noteIdInput');
+        if (!noteIdInput) return;
+
+        const id = noteIdInput.value.trim();
+        if (id) {
+            hideIdInput();
+            window.location.href = `/${id}`;
+        }
+    }
+
+    document.addEventListener('DOMContentLoaded', function () {
+        initializeTheme();
+
+        const noteIdInput = document.getElementById('noteIdInput');
+        if (noteIdInput) {
+            noteIdInput.addEventListener('keypress', function (e) {
+                if (e.key === 'Enter') {
+                    loadNoteFromInput();
+                }
+            });
+        }
+    });
+</script>
+</body>
+</html>

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

@@ -615,4 +615,129 @@ body {
         --bg-secondary: #1c2128;
         --bg-tertiary: #2d333b;
     }
+}
+
+/* 404 Error Page Styles */
+.error-content {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    text-align: center;
+    padding: 40px 20px;
+    height: 100%;
+    max-width: 500px;
+    margin: 0 auto;
+}
+
+.error-icon {
+    font-size: 64px;
+    margin-bottom: 24px;
+    opacity: 0.8;
+}
+
+.error-title {
+    font-size: 32px;
+    font-weight: 600;
+    color: var(--text-primary);
+    margin-bottom: 16px;
+    line-height: 1.2;
+}
+
+.error-message {
+    font-size: 16px;
+    color: var(--text-secondary);
+    line-height: 1.6;
+    margin-bottom: 32px;
+    max-width: 400px;
+}
+
+.error-actions {
+    display: flex;
+    gap: 16px;
+    flex-wrap: wrap;
+    justify-content: center;
+}
+
+.error-actions .dialog-btn {
+    min-width: 140px;
+    padding: 12px 24px;
+    font-size: 14px;
+    font-weight: 500;
+    border-radius: 8px;
+    cursor: pointer;
+    border: none;
+    transition: all 0.2s ease;
+    text-decoration: none;
+    display: inline-block;
+}
+
+/* Mobile optimizations for 404 page */
+@media screen and (max-width: 480px) {
+    .error-content {
+        padding: 30px 15px;
+    }
+
+    .error-icon {
+        font-size: 48px;
+        margin-bottom: 20px;
+    }
+
+    .error-title {
+        font-size: 24px;
+        margin-bottom: 12px;
+    }
+
+    .error-message {
+        font-size: 14px;
+        margin-bottom: 24px;
+    }
+
+    .error-actions {
+        flex-direction: column;
+        width: 100%;
+        gap: 12px;
+    }
+
+    .error-actions .dialog-btn {
+        width: 100%;
+        min-width: unset;
+    }
+}
+
+/* Tablet optimizations for 404 page */
+@media screen and (min-width: 481px) and (max-width: 768px) {
+    .error-content {
+        padding: 36px 24px;
+    }
+
+    .error-icon {
+        font-size: 56px;
+    }
+
+    .error-title {
+        font-size: 28px;
+    }
+}
+
+/* Desktop optimizations for 404 page */
+@media screen and (min-width: 1025px) {
+    .error-content {
+        padding: 60px 40px;
+    }
+
+    .error-icon {
+        font-size: 72px;
+        margin-bottom: 28px;
+    }
+
+    .error-title {
+        font-size: 36px;
+        margin-bottom: 20px;
+    }
+
+    .error-message {
+        font-size: 18px;
+        margin-bottom: 40px;
+    }
 }