diff --git a/content/profile.php b/content/profile.php index 3109587..ef6bdf6 100644 --- a/content/profile.php +++ b/content/profile.php @@ -1,5 +1,4 @@ Treffer für Ihre Suchanfrage ""

+ +
diff --git a/content/showArticle.php b/content/showArticle.php index 71cea81..8f43bd9 100644 --- a/content/showArticle.php +++ b/content/showArticle.php @@ -510,6 +510,18 @@ if ($replyAuthor === null) { + + +
diff --git a/content/showCategory.php b/content/showCategory.php index 2f8a4a9..0ad05eb 100644 --- a/content/showCategory.php +++ b/content/showCategory.php @@ -113,6 +113,17 @@ $resultCount = count($results);

+ +
diff --git a/css/main.css b/css/main.css index a2bc707..a9dc026 100644 --- a/css/main.css +++ b/css/main.css @@ -414,4 +414,34 @@ h1 { /* Geändertes Symbol, wenn der Bereich geöffnet ist */ .edit-comment-details[open] .edit-comment-button::before { content: "▲ "; +} +.comment-loading { + display: flex; + align-items: center; + gap: 8px; + margin-top: 10px; +} + +.comment-loading[hidden] { + display: none; +} + +.comment-spinner { + width: 16px; + height: 16px; + border: 2px solid #cbd5e1; + border-top-color: #1f2937; + border-radius: 50%; + animation: comment-spinner-rotation 0.8s linear infinite; +} + +@keyframes comment-spinner-rotation { + to { + transform: rotate(360deg); + } +} + +#comment-form button[type="submit"]:disabled { + cursor: wait; + opacity: 0.65; } \ No newline at end of file diff --git a/css/search-results.css b/css/search-results.css index a89817c..fbf630a 100644 --- a/css/search-results.css +++ b/css/search-results.css @@ -271,4 +271,33 @@ CSS für die Suchergebnis-Seite align-items: flex-start; } } +/* Ladeanzeige beim Nachladen von Such- und Kategorieergebnissen */ +.results-loading { + display: flex; + align-items: center; + gap: 8px; + margin: 15px 0; + color: #4a5568; + font-size: 0.95rem; +} + +.results-loading[hidden] { + display: none; +} + +.results-spinner { + width: 16px; + height: 16px; + flex-shrink: 0; + border: 2px solid #cbd5e1; + border-top-color: #3182ce; + border-radius: 50%; + animation: results-spinner-rotation 0.8s linear infinite; +} + +@keyframes results-spinner-rotation { + to { + transform: rotate(360deg); + } +} diff --git a/js/comments.js b/js/comments.js index 9ad43a8..5760cf5 100644 --- a/js/comments.js +++ b/js/comments.js @@ -11,7 +11,19 @@ document.addEventListener("DOMContentLoaded", function () { const parentCommentInput = document.getElementById("parent-comment-id"); const replyInfo = document.getElementById("reply-info"); - if (!form || !commentsList || !commentContent || !parentCommentInput) { + const submitButton = form + ? form.querySelector('button[type="submit"]') + : null; + + const loadingMessage = document.getElementById("comment-loading"); + + if ( + !form + || !commentsList + || !commentContent + || !parentCommentInput + || !submitButton + ) { return; } @@ -53,6 +65,23 @@ document.addEventListener("DOMContentLoaded", function () { form.addEventListener("submit", function (event) { event.preventDefault(); + /* + * Verhindert einen erneuten Submit, während der vorherige + * Kommentar noch gespeichert wird. + */ + if (submitButton.disabled) { + return; + } + + const originalButtonText = submitButton.textContent; + + submitButton.disabled = true; + submitButton.textContent = "Wird gesendet …"; + + if (loadingMessage) { + loadingMessage.hidden = false; + } + const formData = new FormData(form); const parentCommentId = parentCommentInput.value; @@ -293,6 +322,14 @@ document.addEventListener("DOMContentLoaded", function () { .catch(function (error) { console.error(error); alert("Kommentar konnte nicht gesendet werden."); + }) + .finally(function () { + submitButton.disabled = false; + submitButton.textContent = originalButtonText; + + if (loadingMessage) { + loadingMessage.hidden = true; + } }); }); diff --git a/js/search-results.js b/js/search-results.js index 9461e39..4d12c06 100644 --- a/js/search-results.js +++ b/js/search-results.js @@ -25,6 +25,7 @@ let listContainer, sortRadios, categorySelect, limitSelect; let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl; + let loadingElement; function init() { listContainer = document.querySelector('.s-res-list'); @@ -38,23 +39,41 @@ numbersContainer = document.getElementById('dynamic-page-numbers'); jsNav = document.getElementById('js-page-navigation'); resultCountEl = document.getElementById('s-res-result-count'); + loadingElement = document.getElementById('results-loading'); const checkedRadio = document.querySelector('.sort-radio:checked'); state.sort = checkedRadio ? checkedRadio.value : 'alphabet'; state.category = categorySelect ? categorySelect.value : 'all'; state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10; + if (loadingElement) { + loadingElement.hidden = false; + } + fetchFullDataset() .then(function (data) { state.allItems = data.results || []; attachEvents(); render(); - if (jsNav) jsNav.style.display = 'flex'; + + if (jsNav) { + jsNav.style.display = 'flex'; + } }) .catch(function (err) { - // JSON-Endpoint nicht erreichbar: die serverseitig gerenderte - // (No-JS-)Ansicht bleibt sichtbar und funktioniert weiter. - console.error('Suchergebnisse konnten nicht nachgeladen werden:', err); + /* + * Falls das Nachladen nicht funktioniert, bleibt die bereits + * serverseitig ausgegebene Ergebnisliste sichtbar. + */ + console.error( + 'Suchergebnisse konnten nicht nachgeladen werden:', + err + ); + }) + .finally(function () { + if (loadingElement) { + loadingElement.hidden = true; + } }); } diff --git a/js/showCategory.js b/js/showCategory.js index 08b07ff..394a721 100644 --- a/js/showCategory.js +++ b/js/showCategory.js @@ -21,6 +21,7 @@ let listContainer, sortRadios, searchInput, limitSelect; let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl; + let loadingElement; let searchDebounceTimer; function init() { @@ -35,23 +36,41 @@ numbersContainer = document.getElementById('dynamic-page-numbers'); jsNav = document.getElementById('js-page-navigation'); resultCountEl = document.getElementById('s-res-result-count'); + loadingElement = document.getElementById('results-loading'); const checkedRadio = document.querySelector('.sort-radio:checked'); state.sort = checkedRadio ? checkedRadio.value : 'alphabet'; state.query = searchInput ? searchInput.value : ''; state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10; + if (loadingElement) { + loadingElement.hidden = false; + } + fetchFullDataset() .then(function (data) { state.allItems = data.results || []; attachEvents(); render(); - if (jsNav) jsNav.style.display = 'flex'; + + if (jsNav) { + jsNav.style.display = 'flex'; + } }) .catch(function (err) { - // JSON-Endpoint nicht erreichbar: die serverseitig gerenderte - // (No-JS-)Ansicht bleibt sichtbar und funktioniert weiter. - console.error('Kategorie-Beiträge konnten nicht nachgeladen werden:', err); + /* + * Falls das Nachladen nicht funktioniert, bleibt die bereits + * serverseitig ausgegebene Kategorieansicht sichtbar. + */ + console.error( + 'Kategorie-Beiträge konnten nicht nachgeladen werden:', + err + ); + }) + .finally(function () { + if (loadingElement) { + loadingElement.hidden = true; + } }); } diff --git a/php/controller/index-controller.php b/php/controller/index-controller.php index e782797..8763069 100644 --- a/php/controller/index-controller.php +++ b/php/controller/index-controller.php @@ -20,6 +20,8 @@ if ($pfad === "login") { include_once "php/controller/confirm-register-controller.php"; } elseif ($pfad === "confirm-password") { include_once "php/controller/confirm-password-controller.php"; +} elseif ($pfad === "profile") { + include_once "php/controller/profile-controller.php"; } elseif ($pfad === "updateComment") { include_once "php/controller/updateComment-controller.php"; } elseif($pfad === "deleteComment") { diff --git a/php/model/DatabaseArticleManager.php b/php/model/DatabaseArticleManager.php index 8e3f57e..ae44b48 100644 --- a/php/model/DatabaseArticleManager.php +++ b/php/model/DatabaseArticleManager.php @@ -63,9 +63,12 @@ class DatabaseArticleManager implements ArticleManagerDAO { VALUES (:title, :content, :author, :category, :tags);"; $command = $db->prepare($sql); + if (!$command) { + throw new InternalServerErrorException("internal_error"); + } // Verknüpft die übergebenen Parameter exakt mit den SQL-Platzhaltern - $command->execute([ + $success = $command->execute([ ":title" => $title, ":content" => $content, ":author" => $author, @@ -73,6 +76,10 @@ class DatabaseArticleManager implements ArticleManagerDAO { ":tags" => $tags ]); + if (!$success) { + throw new InternalServerErrorException("internal_error"); + } + return intval($db->lastInsertId()); } catch (PDOException $e) { @@ -287,7 +294,7 @@ class DatabaseArticleManager implements ArticleManagerDAO { $command = $db->prepare($sql); - // Wildcards für die Suche hinzufügen + // Wildcards für die SQL-Suche hinzufügen $searchParam = '%' . $cleankeyword . '%'; $command->execute([