Compare commits

..

11 Commits

Author SHA1 Message Date
caroline.slt 325df51812 ReadMe 2026-07-18 19:51:23 +02:00
caroline.slt 97afd1ea06 Kommentare bearbeiten - Fehlerkorrektur 2026-07-18 19:41:39 +02:00
caroline.slt e3903b4f3e Kommentare bearbeiten - Fehlerkorrektur 2026-07-18 19:27:36 +02:00
caroline.slt 031f2afd25 Kommentare bearbeiten 2026-07-18 18:49:48 +02:00
caroline.slt 7189133861 Korrektur 2026-07-18 18:09:11 +02:00
caroline.slt 8c961a56ce Korrektur 2026-07-18 17:54:30 +02:00
caroline.slt 4f95a3d423 JavaScript Korrektur 2026-07-18 17:43:45 +02:00
caroline.slt 5310083e11 JavaScript Korrektur 2026-07-18 17:23:52 +02:00
caroline.slt b564306fb3 JavaScript Korrektur 2026-07-18 17:20:14 +02:00
caroline.slt 7b7208ad6b Kommentare löschen 2026-07-18 16:44:05 +02:00
caroline.slt ada97ec538 Kommentare ohne js verfassen 2026-07-18 15:26:13 +02:00
25 changed files with 1546 additions and 1139 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="dataSourceStorageLocal" created-in="IU-261.25134.95"> <component name="dataSourceStorageLocal" created-in="IU-253.32098.101">
<data-source name="articles" uuid="315cb5c9-2b0f-435b-b602-59823b160908"> <data-source name="articles" uuid="315cb5c9-2b0f-435b-b602-59823b160908">
<database-info product="SQLite" version="3.51.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.51.1.0" dbms="SQLITE" exact-version="3.51.1" exact-driver-version="3.51"> <database-info product="SQLite" version="3.51.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.51.1.0" dbms="SQLITE" exact-version="3.51.1" exact-driver-version="3.51">
<identifier-quote-string>&quot;</identifier-quote-string> <identifier-quote-string>&quot;</identifier-quote-string>
+3
View File
@@ -36,6 +36,9 @@
- JavaScript wird verwendet, um im erweitertem Beitragseditor clientseitig einzelne Content-Boxen erstellen und löschen - JavaScript wird verwendet, um im erweitertem Beitragseditor clientseitig einzelne Content-Boxen erstellen und löschen
zu können. zu können.
- JavaScript wird ebenfalls verwendet, um in die Suchergebnisse clientseitig zu sortieren. - JavaScript wird ebenfalls verwendet, um in die Suchergebnisse clientseitig zu sortieren.
- Die Kommentarfunktion ist zusätzlich auch ohne JavaScript nutzbar. Kommentare und Antworten können auch ohne JavaScript erstellt werden.
- Eigene Kommentare und Antworten können bearbeitet und gelöscht werden. Die Bearbeitung und das Löschen funktionieren ebenfalls ohne JavaScript über normale Formulare und serverseitige Verarbeitung.
## Sonstiges ## Sonstiges
- Das Datenschema befindet sich unter /planung/Datenschema.pdf - Das Datenschema befindet sich unter /planung/Datenschema.pdf
+75 -96
View File
@@ -2,37 +2,20 @@
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
require_once __DIR__ . '/../includes/resultsHelper.php';
$rawResults = $_SESSION["search_results"] ?? []; $all_results = $_SESSION["search_results"] ?? [];
$query = $_SESSION["search_query"] ?? ""; $query = $_SESSION["search_query"] ?? "";
$totalResultsCount = count($all_results);
// Sortierung / Kategorie / Limit: GET-Parameter haben Vorrang, sonst $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10;
// Rückfall auf die zuletzt in der Session gemerkten Werte. So bleibt
// z.B. ein reiner Pagination-Link (?page=2) bei der aktuellen
// Sortierung/Filterung.
$currentSort = $_GET['sort'] ?? ($_SESSION['search_sort'] ?? 'alphabet');
if (!in_array($currentSort, ['alphabet', 'likes', 'newest', 'oldest'])) {
$currentSort = 'alphabet';
}
$_SESSION['search_sort'] = $currentSort;
$currentCategory = strtolower($_GET['category'] ?? ($_SESSION['search_category'] ?? 'all'));
$_SESSION['search_category'] = $currentCategory;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : ($_SESSION['search_limit'] ?? 10);
if (!in_array($limit, [10, 20, 50, 100])) { if (!in_array($limit, [10, 20, 50, 100])) {
$limit = 10; $limit = 10;
} }
$_SESSION['search_limit'] = $limit;
// Serverseitig sortieren + filtern // Gesamtseitenzahl
$filteredResults = sortSearchResults($rawResults, $currentSort); $totalPages = max(1, ceil($totalResultsCount / $limit));
$filteredResults = filterSearchResultsByCategory($filteredResults, $currentCategory);
$totalResultsCount = count($filteredResults);
$totalPages = max(1, (int)ceil($totalResultsCount / $limit));
// Aktuelle Seite auslesen und validieren
$currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1; $currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($currentPage < 1) { if ($currentPage < 1) {
$currentPage = 1; $currentPage = 1;
@@ -40,59 +23,42 @@ if ($currentPage < 1) {
$currentPage = $totalPages; $currentPage = $totalPages;
} }
// Startpunkt im Array berechnen (Offset)
$offset = ($currentPage - 1) * $limit; $offset = ($currentPage - 1) * $limit;
// Dies ist die No-JS-Basisversion: nur die aktuelle Seite wird gerendert. // Nur die Ergebnisse für die aktuelle Seite ausschneiden
// Bei aktiviertem JS lädt js/results-app.js zusätzlich den kompletten //$results = array_slice($all_results, $offset, $limit);
// Datensatz nach (ein einziger Request) und übernimmt Sortierung, Filter $results = $all_results;
// und Pagination danach komplett im Browser, ohne weitere Serveranfragen.
$results = array_slice($filteredResults, $offset, $limit);
$resultCount = count($results); $resultCount = count($results);
// Kategorien für das Auswahlfeld (Wert => Label)
$categories = [
'Deutsch' => 'Deutsch', 'Englisch' => 'Englisch', 'Franzoesisch' => 'Französisch',
'Latein' => 'Latein', 'Literatur' => 'Literatur', 'Mathematik' => 'Mathematik',
'Biologie' => 'Biologie', 'Informatik' => 'Informatik', 'Chemie' => 'Chemie',
'Physik' => 'Physik', 'Astronomie' => 'Astronomie', 'Geschichte' => 'Geschichte',
'Erdkunde' => 'Erdkunde', 'Sozialkunde' => 'Sozialkunde', 'Wirtschaftskunde' => 'Wirtschaftskunde',
'Religion' => 'Religion', 'Ethikunterricht' => 'Ethikunterricht', 'Philosophie' => 'Philosophie',
'Psychologie' => 'Psychologie', 'Kunst' => 'Kunst', 'Musik' => 'Musik', 'Theater' => 'Theater',
'Technik' => 'Technik', 'Werken' => 'Werken', 'Hauswirtschaft' => 'Hauswirtschaft', 'Sport' => 'Sport',
];
?> ?>
<noscript>
Bitte JavaScript aktivieren!
</noscript>
<!-- <!--
Seite: Suchergebnisse Seite: Suchergebnisse
Inhalt: Zeigt die Ergebnisse einer Suche an. Inhalt: Zeigt die Ergebnisse einer Suche an
Funktioniert ohne JavaScript (serverseitige Sortierung/Filter/Pagination
über echte Links + Formulare) und wird bei aktiviertem JS von
js/search-results.js vollständig client-seitig übernommen.
--> -->
<div class="s-res-layout-grid"> <div class="s-res-layout-grid">
<?php include_once "includes/alertMessages.php" ?> <?php include_once "includes/alertMessages.php"?>
<!-- Links: Seitenleiste für Filter und Suche --> <!-- Links: Seitenleiste für Filter und Suche -->
<aside class="s-res-sidebar"> <aside class="s-res-sidebar">
<!-- Formular 1: startet eine NEUE Suche (löst immer einen Server-Request aus) --> <!-- Sortierfuntion Box und Such Box-->
<form action="php/controller/search-results-controller.php" method="GET" id="search-form-id" class="s-res-sidebar-form"> <form action="php/controller/search-results-controller.php" method="GET" id="search-form-id" class="s-res-sidebar-form">
<input type="hidden" id="s-res-page-input" name="page" value="<?php echo $_GET['page'] ?? 1; ?>">
<div class="s-res-sidebar-box"> <div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Suche anpassen</h3> <h3 class="s-res-sidebar-title">Suche anpassen</h3>
<input type="search" id="site-search" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50" required> <input type="search" id="site-search" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50" required>
<button type="submit" class="nav__search-button">Suchen</button> <button type="submit" class="nav__search-button">Suchen</button>
</div> </div>
</form>
<!-- Formular 2: sortiert/filtert die BEREITS gefundenen Ergebnisse.
Ohne JS: echter Reload dieser Seite, aber ohne erneute Datenbank-Suche.
Mit JS: search-results.js übernimmt das live im Browser, der Submit-Button
wird dafür nicht benötigt und über <noscript> versteckt. -->
<form action="index.php" method="GET" id="s-res-filter-form" class="s-res-sidebar-form">
<input type="hidden" name="pfad" value="search-results">
<input type="hidden" name="q" value="<?php echo htmlspecialchars($query); ?>">
<div class="s-res-sidebar-box"> <div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Sortierung</h3> <h3 class="s-res-sidebar-title">Sortierung</h3>
<?php $currentSort = $_SESSION['search_sort'] ?? 'alphabet'; ?>
<div class="s-res-filter-group"> <div class="s-res-filter-group">
<label class="s-res-filter-option"> <label class="s-res-filter-option">
<input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>> <input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>>
@@ -115,17 +81,37 @@ $categories = [
<div class="s-res-sidebar-box"> <div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Kategorie filtern</h3> <h3 class="s-res-sidebar-title">Kategorie filtern</h3>
<select id="category-filter" name="category" class="s-res-limit-select" style="width: 100%; padding: 8px; border-radius: 6px; border: 1px solid #cbd5e1;"> <select id="category-filter" class="s-res-limit-select" style="width: 100%; padding: 8px; border-radius: 6px; border: 1px solid #cbd5e1;">
<option value="all" <?php echo $currentCategory === 'all' ? 'selected' : ''; ?>>Alle Kategorien</option> <option value="all">Alle Kategorien</option>
<?php foreach ($categories as $value => $label): ?> <option value="Deutsch">Deutsch</option>
<option value="<?php echo htmlspecialchars($value); ?>" <?php echo $currentCategory === strtolower($value) ? 'selected' : ''; ?>><?php echo htmlspecialchars($label); ?></option> <option value="Englisch">Englisch</option>
<?php endforeach; ?> <option value="Franzoesisch">Französisch</option>
<option value="Latein">Latein</option>
<option value="Literatur">Literatur</option>
<option value="Mathematik">Mathematik</option>
<option value="Biologie">Biologie</option>
<option value="Informatik">Informatik</option>
<option value="Chemie">Chemie</option>
<option value="Physik">Physik</option>
<option value="Astronomie">Astronomie</option>
<option value="Geschichte">Geschichte</option>
<option value="Erdkunde">Erdkunde</option>
<option value="Sozialkunde">Sozialkunde</option>
<option value="Wirtschaftskunde">Wirtschaftskunde</option>
<option value="Religion">Religion</option>
<option value="Ethikunterricht">Ethikunterricht</option>
<option value="Philosophie">Philosophie</option>
<option value="Psychologie">Psychologie</option>
<option value="Kunst">Kunst</option>
<option value="Musik">Musik</option>
<option value="Theater">Theater</option>
<option value="Technik">Technik</option>
<option value="Werken">Werken</option>
<option value="Hauswirtschaft">Hauswirtschaft</option>
<option value="Sport">Sport</option>
</select> </select>
</div> </div>
<noscript>
<button type="submit" class="nav__search-button">Filter anwenden</button>
</noscript>
</form> </form>
</aside> </aside>
@@ -134,16 +120,18 @@ $categories = [
<div class="s-res-header"> <div class="s-res-header">
<h1 class="s-res-main-title">Suchergebnisse</h1> <h1 class="s-res-main-title">Suchergebnisse</h1>
<p class="s-res-meta"><span id="s-res-result-count"><?php echo $totalResultsCount; ?></span> Treffer für Ihre Suchanfrage "<?php echo htmlspecialchars($query); ?>"</p> <p class="s-res-meta"><?php echo $totalResultsCount; ?> Treffer für Ihre Suchanfrage "<?php echo htmlspecialchars($query); ?>"</p>
</div> </div>
<!-- Ergebnisliste --> <!-- Ergebnisliste -->
<div class="s-res-list"> <div class="s-res-list">
<?php if (!empty($results)): ?> <?php
if (!empty($results)): ?>
<?php foreach ($results as $item):
$likesCount = getLikeCount($item); <?php foreach ($results as $item):
?> // Anzahl der Likes ermitteln (falls es ein Array ist, zählen; falls Zahl, direkt nutzen)
$likesCount = isset($item['likes']) && is_array($item['likes']) ? count($item['likes']) : ($item['likes'] ?? 0);
?>
<div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>"> <div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>">
<div class="s-res-content"> <div class="s-res-content">
<h2 class="s-res-item-title"> <h2 class="s-res-item-title">
@@ -163,50 +151,41 @@ $categories = [
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php elseif (isset($_SESSION["message"]) && $_SESSION["message"] === "invalid_search_query"): ?> <?php
elseif (isset($_SESSION["search_query"]) && $_SESSION["search_query"] !== "" && $resultCount === 0): ?>
<p>Keine Beiträge zu diesem Suchbegriff gefunden.</p>
<?php
elseif (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_search_query"): ?>
<p>Unzulässige Suchanfrage</p> <p>Unzulässige Suchanfrage</p>
<?php elseif ($query !== "" && $totalResultsCount === 0 && count($rawResults) > 0): ?>
<p>Keine Beiträge in dieser Kategorie gefunden.</p>
<?php elseif ($query !== "" && $totalResultsCount === 0): ?>
<p>Keine Beiträge zu diesem Suchbegriff gefunden.</p>
<?php endif; ?> <?php endif; ?>
<?php unset($_SESSION["message"]); ?> <?php
unset($_SESSION["message"]);
?>
</div> </div>
<div class="s-res-pagination-footer"> <div class="s-res-pagination-footer">
<!-- Auswahl der Ergebnisse pro Seite. Gehört per form="..." logisch <!-- Auswahl der Ergebnisse pro Seite -->
zum Filterformular oben -->
<div class="s-res-limit-selector"> <div class="s-res-limit-selector">
<label for="s-res-per-page" class="s-res-limit-label">Ergebnisse pro Seite:</label> <label for="s-res-per-page" class="s-res-limit-label">Ergebnisse pro Seite:</label>
<select id="s-res-per-page" name="limit" form="s-res-filter-form" class="s-res-limit-select"> <select id="s-res-per-page" name="limit" class="s-res-limit-select">
<option value="10" <?php echo $limit === 10 ? 'selected' : ''; ?>>10</option> <option value="10" <?php echo $limit === 10 ? 'selected' : ''; ?>>10</option>
<option value="20" <?php echo $limit === 20 ? 'selected' : ''; ?>>20</option> <option value="20" <?php echo $limit === 20 ? 'selected' : ''; ?>>20</option>
<option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50</option> <option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50</option>
<option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100</option> <option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100</option>
</select> </select>
<noscript><button type="submit" form="s-res-filter-form" class="nav__search-button">Übernehmen</button></noscript>
</div> </div>
<nav class="s-res-page-navigation" aria-label="Seitennavigation"> <div class="s-res-page-navigation">
<!-- No-JS-Fallback: --> <button type="button" class="s-res-page-btn" id="prev-page-btn" data-page="0">«</button>
<noscript>
<?php echo renderNoJsPagination($currentPage, $totalPages, $query, $currentSort, $currentCategory, $limit); ?> <span id="dynamic-page-numbers"></span>
</noscript>
<button type="button" class="s-res-page-btn" id="next-page-btn" data-page="2">»</button>
<!-- JS-Version: wird per search-results.js befüllt/eingeblendet: --> </div>
<div id="js-page-navigation" style="display:none;">
<button type="button" class="s-res-page-btn" id="prev-page-btn">«</button>
<span id="dynamic-page-numbers"></span>
<button type="button" class="s-res-page-btn" id="next-page-btn">»</button>
</div>
</nav>
</div> </div>
</main> </main>
</div> </div>
+310 -26
View File
@@ -5,6 +5,34 @@ $repliesByParent = [];
$articleObj = null; $articleObj = null;
include_once 'php/controller/showArticle-controller.php'; include_once 'php/controller/showArticle-controller.php';
/*
* Ermittelt, ob ohne JavaScript auf einen Kommentar
* geantwortet werden soll.
*/
$replyTo = filter_input(
INPUT_GET,
"reply_to",
FILTER_VALIDATE_INT
);
$replyAuthor = null;
if ($replyTo !== false && $replyTo !== null) {
foreach ($mainComments as $mainComment) {
if ($mainComment->getId() === $replyTo) {
$replyAuthor = $mainComment->getAuthor();
break;
}
}
}
/*
* Eine Antwort darf nur auf einen existierenden
* Hauptkommentar geschrieben werden.
*/
if ($replyAuthor === null) {
$replyTo = null;
}
?> ?>
<!-- <!--
Seite: Anzeige für Beiträge Seite: Anzeige für Beiträge
@@ -17,8 +45,6 @@ include_once 'php/controller/showArticle-controller.php';
<!-- Metadaten & Titel --> <!-- Metadaten & Titel -->
<div class="article-view-top-section"> <div class="article-view-top-section">
<div class="article-view-top-section">
<div class="category-and-likes-row"> <div class="category-and-likes-row">
<?php if (isset($category) && !empty($category)): ?> <?php if (isset($category) && !empty($category)): ?>
<span class="article-view-category"><?php echo htmlspecialchars($category); ?></span> <span class="article-view-category"><?php echo htmlspecialchars($category); ?></span>
@@ -106,39 +132,268 @@ include_once 'php/controller/showArticle-controller.php';
</div> </div>
<?php endif; ?> <?php endif; ?>
<section class="article-comments-section"> <section class="article-comments-section" id="comments">
<h2>Kommentare</h2> <h2>Kommentare</h2>
<?php if (isset($_SESSION["comment_message"])): ?>
<div class="alert-message <?php
echo ($_SESSION["comment_message_type"] ?? "") === "success"
? "is-success"
: "is-error";
?>">
<?php echo htmlspecialchars($_SESSION["comment_message"]); ?>
</div>
<?php
unset($_SESSION["comment_message"]);
unset($_SESSION["comment_message_type"]);
?>
<?php endif; ?>
<div id="comments-list"> <div id="comments-list">
<?php if (!empty($mainComments)): ?> <?php if (!empty($mainComments)): ?>
<?php foreach ($mainComments as $comment): ?> <?php foreach ($mainComments as $comment): ?>
<div class="comment-item" data-comment-id="<?php echo htmlspecialchars($comment->getId()); ?>"> <div class="comment-item"
<p> data-comment-id="<?php echo htmlspecialchars(
<strong><?php echo htmlspecialchars($comment->getAuthor()); ?></strong> (string) $comment->getId()
<span><?php echo htmlspecialchars($comment->getCreated()); ?></span> ); ?>">
</p>
<p><?php echo nl2br(htmlspecialchars($comment->getContent())); ?></p> <?php
$isDeleted = $comment->getContent()
=== "Dieser Kommentar wurde gelöscht.";
?>
<?php if ($isDeleted): ?>
<p class="deleted-comment">
Dieser Kommentar wurde gelöscht.
</p>
<?php else: ?>
<p>
<strong>
<?php echo htmlspecialchars($comment->getAuthor()); ?>
</strong>
<span>
<?php echo htmlspecialchars($comment->getCreated()); ?>
</span>
</p>
<p>
<?php
echo nl2br(
htmlspecialchars($comment->getContent())
);
?>
</p>
<?php if (
isset($_SESSION["user_email"])
&& $_SESSION["user_email"] === $comment->getAuthor()
): ?>
<details class="edit-comment-details">
<summary class="edit-comment-button">
Kommentar bearbeiten
</summary>
<form method="post"
action="index.php?pfad=updateComment"
class="edit-comment-form">
<input type="hidden"
name="comment_id"
value="<?php echo htmlspecialchars(
(string) $comment->getId()
); ?>">
<input type="hidden"
name="article_id"
value="<?php echo htmlspecialchars(
(string) $comment->getArticleId()
); ?>">
<label for="edit-comment-<?php
echo htmlspecialchars((string) $comment->getId());
?>">
Kommentar bearbeiten
</label>
<textarea
id="edit-comment-<?php
echo htmlspecialchars((string) $comment->getId());
?>"
name="content"
required><?php echo htmlspecialchars(
$comment->getContent()
); ?></textarea>
<button type="submit" class="button">
Änderungen speichern
</button>
</form>
</details>
<form method="post"
action="index.php?pfad=deleteComment"
class="delete-comment-form">
<input type="hidden"
name="comment_id"
value="<?php echo htmlspecialchars(
(string) $comment->getId()
); ?>">
<input type="hidden"
name="article_id"
value="<?php echo htmlspecialchars(
(string) $comment->getArticleId()
); ?>">
<button type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');">
Kommentar löschen
</button>
</form>
<?php endif; ?>
<?php if (isset($_SESSION["user_email"])): ?>
<a href="index.php?pfad=<?php
echo urlencode($_GET["pfad"] ?? "showArticle");
?>&id=<?php
echo urlencode((string) $comment->getArticleId());
?>&reply_to=<?php
echo urlencode((string) $comment->getId());
?>#comment-form"
class="reply-button"
data-comment-id="<?php echo htmlspecialchars(
(string) $comment->getId()
); ?>"
data-author="<?php echo htmlspecialchars(
$comment->getAuthor()
); ?>">
Antworten
</a>
<?php endif; ?>
<?php if (isset($_SESSION["user_email"])): ?>
<button type="button"
class="reply-button"
data-comment-id="<?php echo htmlspecialchars($comment->getId()); ?>"
data-author="<?php echo htmlspecialchars($comment->getAuthor()); ?>">
Antworten
</button>
<?php endif; ?> <?php endif; ?>
<div class="comment-replies"> <div class="comment-replies">
<?php if (isset($repliesByParent[$comment->getId()])): ?> <?php if (isset($repliesByParent[$comment->getId()])): ?>
<?php foreach ($repliesByParent[$comment->getId()] as $reply): ?> <?php foreach ($repliesByParent[$comment->getId()] as $reply): ?>
<div class="comment-item comment-reply"> <div class="comment-item comment-reply">
<p>
<strong><?php echo htmlspecialchars($reply->getAuthor()); ?></strong>
<span><?php echo htmlspecialchars($reply->getCreated()); ?></span>
</p>
<p><?php echo nl2br(htmlspecialchars($reply->getContent())); ?></p> <?php
$isReplyDeleted = $reply->getContent()
=== "Dieser Kommentar wurde gelöscht.";
?>
<?php if ($isReplyDeleted): ?>
<p class="deleted-comment">
Dieser Kommentar wurde gelöscht.
</p>
<?php else: ?>
<p>
<strong>
<?php echo htmlspecialchars($reply->getAuthor()); ?>
</strong>
<span>
<?php echo htmlspecialchars($reply->getCreated()); ?>
</span>
</p>
<p>
<?php
echo nl2br(
htmlspecialchars($reply->getContent())
);
?>
</p>
<?php if (
isset($_SESSION["user_email"])
&& $_SESSION["user_email"] === $reply->getAuthor()
): ?>
<details class="edit-comment-details">
<summary class="edit-comment-button">
Antwort bearbeiten
</summary>
<form method="post"
action="index.php?pfad=updateComment"
class="edit-comment-form">
<input type="hidden"
name="comment_id"
value="<?php echo htmlspecialchars(
(string) $reply->getId()
); ?>">
<input type="hidden"
name="article_id"
value="<?php echo htmlspecialchars(
(string) $reply->getArticleId()
); ?>">
<label for="edit-reply-<?php
echo htmlspecialchars((string) $reply->getId());
?>">
Antwort bearbeiten
</label>
<textarea
id="edit-reply-<?php
echo htmlspecialchars((string) $reply->getId());
?>"
name="content"
required><?php echo htmlspecialchars(
$reply->getContent()
); ?></textarea>
<button type="submit" class="button">
Änderungen speichern
</button>
</form>
</details>
<form method="post"
action="index.php?pfad=deleteComment"
class="delete-comment-form">
<input type="hidden"
name="comment_id"
value="<?php echo htmlspecialchars(
(string) $reply->getId()
); ?>">
<input type="hidden"
name="article_id"
value="<?php echo htmlspecialchars(
(string) $reply->getArticleId()
); ?>">
<button type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');">
Kommentar löschen
</button>
</form>
<?php endif; ?>
<?php endif; ?>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php endif; ?> <?php endif; ?>
@@ -153,17 +408,46 @@ include_once 'php/controller/showArticle-controller.php';
</div> </div>
<?php if (isset($_SESSION["user_email"])): ?> <?php if (isset($_SESSION["user_email"])): ?>
<form id="comment-form"> <form id="comment-form"
method="post"
action="php/ajax/add-comment.php">
<input type="hidden" <input type="hidden"
name="article_id" name="article_id"
value="<?php echo htmlspecialchars($_GET["id"] ?? ""); ?>"> value="<?php echo htmlspecialchars(
(string) ($_GET["id"] ?? "")
); ?>">
<input type="hidden" <input type="hidden"
name="parent_comment_id" name="parent_comment_id"
id="parent-comment-id" id="parent-comment-id"
value=""> value="<?php echo $replyTo !== null
? htmlspecialchars((string) $replyTo)
: "";
?>">
<p id="reply-info" class="reply-info" style="display: none;"></p> <p id="reply-info"
class="reply-info"
<?php if ($replyAuthor === null): ?>
style="display: none;"
<?php endif; ?>>
<?php if ($replyAuthor !== null): ?>
Antwort auf <?php echo htmlspecialchars($replyAuthor); ?>
<a href="index.php?pfad=<?php
echo urlencode($_GET["pfad"] ?? "showArticle");
?>&id=<?php
echo urlencode((string) ($_GET["id"] ?? ""));
?>#comment-form">
Abbrechen
</a>
<?php endif; ?>
</p>
<label for="comment-content">
Kommentar
</label>
<textarea name="content" <textarea name="content"
id="comment-content" id="comment-content"
@@ -181,4 +465,4 @@ include_once 'php/controller/showArticle-controller.php';
</div> </div>
<?php endif; ?> <?php endif; ?>
</section> </section>
</main> </main>
+87 -160
View File
@@ -1,184 +1,111 @@
<?php <?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
include_once "php/controller/showCategory-controller.php"; include_once "php/controller/showCategory-controller.php";
require_once __DIR__ . '/../includes/resultsHelper.php';
$rawResults = $_SESSION['category_results'] ?? []; $currentSort = isset($sortStyle) ? $sortStyle : 'alphabet';
$category = $_SESSION['category_name'] ?? ($_GET['category'] ?? ''); $currentPage = isset($page) ? $page : 1;
// Sortierung / Suche / Limit: GET-Parameter haben Vorrang, sonst Rückfall auf
// die zuletzt in der Session gemerkten Werte (analog zu search-results.php).
$currentSort = $_GET['sort'] ?? ($_SESSION['cat_sort'] ?? 'alphabet');
if (!in_array($currentSort, ['alphabet', 'likes', 'newest', 'oldest'])) {
$currentSort = 'alphabet';
}
$_SESSION['cat_sort'] = $currentSort;
$query = trim($_GET['q'] ?? ($_SESSION['cat_query'] ?? ''));
$_SESSION['cat_query'] = $query;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : ($_SESSION['cat_limit'] ?? 10);
if (!in_array($limit, [10, 20, 50, 100])) {
$limit = 10;
}
$_SESSION['cat_limit'] = $limit;
// Serverseitig sortieren + nach Suchbegriff filtern (Kategorie steht bereits fest)
$filteredResults = sortSearchResults($rawResults, $currentSort);
$filteredResults = filterResultsByQuery($filteredResults, $query);
$totalResultsCount = count($filteredResults);
$totalPages = max(1, (int)ceil($totalResultsCount / $limit));
$currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($currentPage < 1) {
$currentPage = 1;
} elseif ($currentPage > $totalPages) {
$currentPage = $totalPages;
}
$offset = ($currentPage - 1) * $limit;
// Dies ist die No-JS-Basisversion: nur die aktuelle Seite wird gerendert.
// Bei aktiviertem JS lädt js/showCategory.js zusätzlich den kompletten
// Kategorie-Datensatz nach (ein einziger Request) und übernimmt Sortierung,
// Suche und Pagination danach komplett im Browser, ohne weitere Serveranfragen.
$results = array_slice($filteredResults, $offset, $limit);
$resultCount = count($results);
?> ?>
<!--
Seite: Kategorie-Ansicht
Inhalt: Zeigt alle Beiträge einer Kategorie an, sortierbar und innerhalb
der Kategorie durchsuchbar.
Funktioniert ohne JavaScript (serverseitige Sortierung/Suche/Pagination
über echte Links + Formulare) und wird bei aktiviertem JS von
js/showCategory.js vollständig client-seitig übernommen.
-->
<div class="s-res-layout-grid">
<?php include_once "includes/alertMessages.php" ?>
<!-- Links: Seitenleiste für Suche (innerhalb der Kategorie) und Sortierung --> <noscript>
<aside class="s-res-sidebar"> Bitte JavaScript aktivieren!
</noscript>
<!-- Sortiert/durchsucht die BEREITS geladenen Kategorie-Beiträge. <!-- Das Formular umschließt jetzt das gesamte Layout -->
Ohne JS: echter Reload dieser Seite, aber ohne erneute Datenbank-Abfrage. <form id="search-form-id" action="index.php" method="GET" style="display: contents;">
Mit JS: js/showCategory.js übernimmt das live im Browser, der Submit-Button <input type="hidden" name="pfad" value="showCategory">
wird dafür nicht benötigt und über <noscript> versteckt. --> <input type="hidden" name="category" value="<?php echo (isset($category) && !empty($category)) ? htmlspecialchars($category) : ''; ?>">
<form action="index.php" method="GET" id="cat-filter-form" class="s-res-sidebar-form"> <input type="hidden" id="s-res-page-input" name="page" value="<?php echo $currentPage; ?>">
<input type="hidden" name="pfad" value="showCategory">
<input type="hidden" name="category" value="<?php echo htmlspecialchars($category); ?>">
<div class="s-res-sidebar-box"> <div class="cat-view-container">
<h3 class="s-res-sidebar-title">In dieser Kategorie suchen</h3>
<input type="search" id="cat-search-input" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50">
</div>
<div class="s-res-sidebar-box"> <!-- LINKE SPALTE: SEITENLEISTE FÜR SORTIERUNG -->
<h3 class="s-res-sidebar-title">Sortierung</h3> <aside class="cat-sidebar">
<div class="s-res-filter-group"> <div class="cat-sidebar-box">
<label class="s-res-filter-option"> <h3 class="cat-sidebar-title">Sortierung</h3>
<input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>> <div class="cat-filter-group">
<span>Alphabetisch</span> <label class="cat-filter-option">
<input type="radio" name="sort" value="alphabet" <?php echo ($currentSort === 'alphabet') ? 'checked' : ''; ?> onchange="this.form.submit();">
Alphabetisch
</label> </label>
<label class="s-res-filter-option"> <label class="cat-filter-option">
<input type="radio" name="sort" value="likes" class="sort-radio" <?php echo $currentSort === 'likes' ? 'checked' : ''; ?>> <input type="radio" name="sort" value="likes" <?php echo ($currentSort === 'likes') ? 'checked' : ''; ?> onchange="this.form.submit();">
<span>Beliebtheit (Likes)</span> Beliebtheit (Likes)
</label> </label>
<label class="s-res-filter-option"> <label class="cat-filter-option">
<input type="radio" name="sort" value="newest" class="sort-radio" <?php echo $currentSort === 'newest' ? 'checked' : ''; ?>> <input type="radio" name="sort" value="newest" <?php echo ($currentSort === 'newest') ? 'checked' : ''; ?> onchange="this.form.submit();">
<span>Neueste Beiträge</span> Neueste Beiträge
</label> </label>
<label class="s-res-filter-option"> <label class="cat-filter-option">
<input type="radio" name="sort" value="oldest" class="sort-radio" <?php echo $currentSort === 'oldest' ? 'checked' : ''; ?>> <input type="radio" name="sort" value="oldest" <?php echo ($currentSort === 'oldest') ? 'checked' : ''; ?> onchange="this.form.submit();">
<span>Älteste Beiträge</span> Älteste Beiträge
</label> </label>
</div> </div>
</div> </div>
</aside>
<noscript> <!-- RECHTE SPALTE: INHALT & PAGINATOR -->
<button type="submit" class="nav__search-button">Anwenden</button> <main class="cat-view-main">
</noscript> <div class="cat-view-header">
</form> <h1 class="cat-view-title">Kategorie: <?php echo (isset($category) && !empty($category)) ? htmlspecialchars($category) : 'Unbekannt'; ?></h1>
<p class="cat-view-meta-text">
<?php echo (isset($totalArticles)) ? $totalArticles : 0; ?> Treffer in dieser Kategorie
</p>
</div>
</aside> <!-- Beiträge -->
<div class="cat-view-list">
<?php if (isset($articles) && !empty($articles)): ?>
<?php foreach ($articles as $article): ?>
<div class="cat-view-item">
<div class="cat-view-content">
<h2 class="cat-view-item-title">
<a href="index.php?pfad=showArticle&id=<?php echo htmlspecialchars($article->getID()); ?>" class="cat-view-link">
<?php echo (isset($article) && !empty($article->getTitle())) ? htmlspecialchars($article->getTitle()) : 'Kein Titel'; ?>
</a>
</h2>
<main class="s-res-main-content"> <div class="cat-view-meta-row">
<p class="cat-view-author">
<div class="s-res-header"> von <span class="cat-view-author-name"><?php echo (isset($article) && !empty($article->getAuthor())) ? htmlspecialchars($article->getAuthor()) : 'Anonym'; ?></span>
<h1 class="s-res-main-title">Kategorie: <?php echo htmlspecialchars($category); ?></h1> </p>
<p class="s-res-meta"> <span class="cat-view-likes">
<span id="s-res-result-count"><?php echo $totalResultsCount; ?></span> Treffer in dieser Kategorie<?php echo $query !== '' ? ' für "' . htmlspecialchars($query) . '"' : ''; ?> ❤️ <?php echo (isset($article) && !empty($article->getLikes())) ? htmlspecialchars($article->getLikes()) : '0'; ?>
</p> </span>
</div> </div>
<!-- Ergebnisliste -->
<div class="s-res-list">
<?php if (!empty($results)): ?>
<?php foreach ($results as $item):
$likesCount = getLikeCount($item);
?>
<div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>">
<div class="s-res-content">
<h2 class="s-res-item-title">
<a href="index.php?pfad=showArticle&id=<?php echo $item['id']; ?>" class="s-res-link">
<?php echo htmlspecialchars($item['title']); ?>
</a>
</h2>
<div class="s-res-meta-row">
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($item['author']); ?></span></p>
<span class="s-res-likes">
❤️ <?php echo $likesCount; ?>
</span>
</div> </div>
<div class="cat-view-arrow">&rarr;</div>
</div> </div>
<div class="s-res-arrow">&rarr;</div> <?php endforeach; ?>
</div> <?php else: ?>
<?php endforeach; ?> <p class="cat-view-meta-text">Keine Artikel in dieser Kategorie gefunden.</p>
<?php endif; ?>
<?php elseif ($query !== ''): ?>
<p>Keine Beiträge in dieser Kategorie zu diesem Suchbegriff gefunden.</p>
<?php else: ?>
<p>Keine Beiträge in dieser Kategorie gefunden.</p>
<?php endif; ?>
<?php unset($_SESSION["message"]); ?>
</div>
<div class="s-res-pagination-footer">
<!-- Auswahl der Ergebnisse pro Seite -->
<div class="s-res-limit-selector">
<label for="s-res-per-page" class="s-res-limit-label">Ergebnisse pro Seite:</label>
<select id="s-res-per-page" name="limit" form="cat-filter-form" class="s-res-limit-select">
<option value="10" <?php echo $limit === 10 ? 'selected' : ''; ?>>10</option>
<option value="20" <?php echo $limit === 20 ? 'selected' : ''; ?>>20</option>
<option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50</option>
<option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100</option>
</select>
<noscript><button type="submit" form="cat-filter-form" class="nav__search-button">Übernehmen</button></noscript>
</div> </div>
<nav class="s-res-page-navigation" aria-label="Seitennavigation"> <!-- Footer Paginator -->
<!-- No-JS-Fallback: --> <?php if (isset($totalPages) && $totalPages > 1): ?>
<noscript> <div class="cat-view-pagination-footer">
<?php echo renderNoJsPaginationCategory($currentPage, $totalPages, $category, $currentSort, $query, $limit); ?> <div></div>
</noscript> <div class="s-res-page-navigation">
<button type="button" class="s-res-page-btn" data-page="<?php echo $currentPage - 1; ?>" <?php echo ($currentPage <= 1) ? 'disabled' : ''; ?>>
&laquo;
</button>
<!-- JS-Version: wird per js/showCategory.js befüllt/eingeblendet: --> <?php for ($i = 1; $i <= $totalPages; $i++): ?>
<div id="js-page-navigation" style="display:none;"> <button type="button"
<button type="button" class="s-res-page-btn" id="prev-page-btn">«</button> class="s-res-page-btn <?php echo ($i === $currentPage) ? 'cat-active-btn' : ''; ?>"
<span id="dynamic-page-numbers"></span> data-page="<?php echo $i; ?>"
<button type="button" class="s-res-page-btn" id="next-page-btn">»</button> <?php echo ($i === $currentPage) ? 'disabled' : ''; ?>>
<?php echo $i; ?>
</button>
<?php endfor; ?>
<button type="button" class="s-res-page-btn" data-page="<?php echo $currentPage + 1; ?>" <?php echo ($currentPage >= $totalPages) ? 'disabled' : ''; ?>>
&raquo;
</button>
</div>
</div> </div>
</nav> <?php endif; ?>
</main>
</div> </div>
</main> </form>
</div>
-171
View File
@@ -1,171 +0,0 @@
<?php
/**
* Hilfsfunktionen rund um die Suchergebnisse.
*
* Werden sowohl von content/search-results.php (No-JS-Rendering)
* als auch von php/controller/search-results-data.php (JSON für
* die JS-Hydration) genutzt, damit Sortier-/Filterlogik nur an
* einer Stelle gepflegt werden muss
*/
function getLikeCount($item): int
{
if (isset($item['likes']) && is_array($item['likes'])) {
return count($item['likes']);
}
return (int)($item['likes'] ?? 0);
}
/**
* Sortiert die Ergebnisse
*/
function sortSearchResults(array $results, string $sortStyle): array
{
switch ($sortStyle) {
case 'likes':
usort($results, function ($a, $b) {
return getLikeCount($b) <=> getLikeCount($a);
});
break;
case 'newest':
usort($results, function ($a, $b) {
return strcmp($b['creationDate'] ?? '', $a['creationDate'] ?? '');
});
break;
case 'oldest':
usort($results, function ($a, $b) {
return strcmp($a['creationDate'] ?? '', $b['creationDate'] ?? '');
});
break;
case 'alphabet':
default:
usort($results, function ($a, $b) {
return strcasecmp($a['title'] ?? '', $b['title'] ?? '');
});
break;
}
return $results;
}
/**
* Filtert die Ergebnisse ('all' = kein Filter).
*/
function filterSearchResultsByCategory(array $results, string $category): array
{
$category = strtolower(trim($category));
if ($category === '' || $category === 'all') {
return $results;
}
return array_values(array_filter($results, function ($item) use ($category) {
return strtolower($item['category'] ?? '') === $category;
}));
}
/**
* Baut eine Such-Ergebnis-URL für einen bestimmten Seitenwechsel (No-JS-Pagination).
*/
function buildSearchResultsUrl(int $page, string $query, string $sort, string $category, int $limit): string
{
return "index.php?pfad=search-results"
. "&q=" . urlencode($query)
. "&sort=" . urlencode($sort)
. "&category=" . urlencode($category)
. "&limit=" . $limit
. "&page=" . $page;
}
/**
* Rendert eine rein serverseitige Pagination für den No-JS-Fall:
*/
function renderNoJsPagination(int $currentPage, int $totalPages, string $query, string $sort, string $category, int $limit): string
{
$html = '';
if ($currentPage > 1) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildSearchResultsUrl($currentPage - 1, $query, $sort, $category, $limit)) . '">&laquo;</a> ';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&laquo;</span> ';
}
for ($i = 1; $i <= $totalPages; $i++) {
if ($i === $currentPage) {
$html .= '<span class="s-res-page-btn s-res-page-btn-active">' . $i . '</span> ';
} else {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildSearchResultsUrl($i, $query, $sort, $category, $limit)) . '">' . $i . '</a> ';
}
}
if ($currentPage < $totalPages) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildSearchResultsUrl($currentPage + 1, $query, $sort, $category, $limit)) . '">&raquo;</a>';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&raquo;</span>';
}
return $html;
}
/**
* Filtert Ergebnisse anhand eines Suchbegriffs
*
* Wird von content/showCategory.php genutzt, um innerhalb einer bereits
* geladenen Kategorie zu suchen.
*/
function filterResultsByQuery(array $results, string $query): array
{
$query = trim($query);
if ($query === '') {
return $results;
}
$needle = mb_strtolower($query);
return array_values(array_filter($results, function ($item) use ($needle) {
$haystack = mb_strtolower(($item['title'] ?? '') . ' ' . ($item['content'] ?? ''));
return mb_strpos($haystack, $needle) !== false;
}));
}
/**
* Baut eine Kategorie-URL für einen bestimmten Seitenwechsel (No-JS-Pagination).
* Analog zu buildSearchResultsUrl, aber für pfad=showCategory (feste Kategorie,
* dafür mit Freitext-Suche innerhalb der Kategorie statt Kategorie-Filter).
*/
function buildCategoryUrl(int $page, string $category, string $sort, string $query, int $limit): string
{
return "index.php?pfad=showCategory"
. "&category=" . urlencode($category)
. "&sort=" . urlencode($sort)
. "&q=" . urlencode($query)
. "&limit=" . $limit
. "&page=" . $page;
}
/**
* Rendert eine rein serverseitige Pagination für showCategory (No-JS-Fall).
* Analog zu renderNoJsPagination.
*/
function renderNoJsPaginationCategory(int $currentPage, int $totalPages, string $category, string $sort, string $query, int $limit): string
{
$html = '';
if ($currentPage > 1) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($currentPage - 1, $category, $sort, $query, $limit)) . '">&laquo;</a> ';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&laquo;</span> ';
}
for ($i = 1; $i <= $totalPages; $i++) {
if ($i === $currentPage) {
$html .= '<span class="s-res-page-btn s-res-page-btn-active">' . $i . '</span> ';
} else {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($i, $category, $sort, $query, $limit)) . '">' . $i . '</a> ';
}
}
if ($currentPage < $totalPages) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($currentPage + 1, $category, $sort, $query, $limit)) . '">&raquo;</a>';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&raquo;</span>';
}
return $html;
}
+13 -8
View File
@@ -15,10 +15,6 @@ if ($pfad === "login") {
include_once "php/controller/login-controller.php"; include_once "php/controller/login-controller.php";
} }
if ($pfad === "search-results-controller") {
include_once "php/controller/search-results-controller.php";
}
if ($pfad === "register") { if ($pfad === "register") {
include_once "php/controller/register-controller.php"; include_once "php/controller/register-controller.php";
} }
@@ -38,11 +34,20 @@ if ($pfad === "logout") {
include_once "php/controller/logout-controller.php"; include_once "php/controller/logout-controller.php";
exit(); exit();
} }
if ($pfad === "deleteAccount") { if ($pfad === "deleteAccount") {
include_once "php/controller/deleteAccount-controller.php"; include_once "php/controller/deleteAccount-controller.php";
exit(); exit();
} }
if ($pfad === "updateComment") {
include_once "php/controller/updateComment-controller.php";
exit();
}
if ($pfad === "deleteComment") {
include_once "php/controller/deleteComment-controller.php";
exit();
}
?> ?>
<!-- <!--
@@ -69,11 +74,11 @@ if ($pfad === "deleteAccount") {
<link rel="stylesheet" href="css/message.css"> <link rel="stylesheet" href="css/message.css">
<link rel="stylesheet" href="css/showCategory.css"> <link rel="stylesheet" href="css/showCategory.css">
<script src="js/paginator.js" async></script>
<script src="js/sorter.js" async></script>
<script src="js/comments.js" defer></script> <script src="js/comments.js" defer></script>
<script src="js/editor.js" async></script> <script src="js/editor.js" async></script>
<script src="js/search-results.js" async></script> <script src="js/filter.js" async></script>
<script src="js/showCategory.js" async></script>
<title>EduForge</title> <title>EduForge</title>
</head> </head>
+192 -43
View File
@@ -11,20 +11,31 @@ document.addEventListener("DOMContentLoaded", function () {
const parentCommentInput = document.getElementById("parent-comment-id"); const parentCommentInput = document.getElementById("parent-comment-id");
const replyInfo = document.getElementById("reply-info"); const replyInfo = document.getElementById("reply-info");
if (!form || !commentsList || !commentContent || !parentCommentInput || !replyInfo) { if (!form || !commentsList || !commentContent || !parentCommentInput) {
return; return;
} }
/** /**
* Aktiviert einen einzelnen Antworten-Button. * Aktiviert einen einzelnen Antworten-Link.
* *
* @param {HTMLButtonElement} button Antworten-Button * @param {HTMLAnchorElement} replyLink Antworten-Link
*/ */
function registerReplyButton(button) { function registerReplyButton(replyLink) {
button.addEventListener("click", function () { replyLink.addEventListener("click", function (event) {
parentCommentInput.value = button.dataset.commentId; /*
replyInfo.textContent = "Antwort auf " + button.dataset.author; * Mit JavaScript wird die Seite nicht neu geladen.
replyInfo.style.display = "block"; * Ohne JavaScript funktioniert der normale Link.
*/
event.preventDefault();
parentCommentInput.value = replyLink.dataset.commentId;
if (replyInfo) {
replyInfo.textContent =
"Antwort auf " + replyLink.dataset.author;
replyInfo.style.display = "block";
}
commentContent.focus(); commentContent.focus();
}); });
} }
@@ -45,18 +56,29 @@ document.addEventListener("DOMContentLoaded", function () {
const formData = new FormData(form); const formData = new FormData(form);
const parentCommentId = parentCommentInput.value; const parentCommentId = parentCommentInput.value;
fetch("php/ajax/add-comment.php", { fetch(form.action, {
method: "POST", method: "POST",
body: formData body: formData,
headers: {
"X-Requested-With": "XMLHttpRequest"
}
}) })
.then(response => response.json()) .then(function (response) {
.then(data => { if (!response.ok) {
throw new Error("Fehlerhafte Serverantwort.");
}
return response.json();
})
.then(function (data) {
if (!data.success) { if (!data.success) {
alert(data.message); alert(data.message);
return; return;
} }
const emptyMessage = commentsList.querySelector(".no-comments-message"); const emptyMessage = commentsList.querySelector(
".no-comments-message"
);
if (emptyMessage) { if (emptyMessage) {
emptyMessage.remove(); emptyMessage.remove();
@@ -64,49 +86,172 @@ document.addEventListener("DOMContentLoaded", function () {
const commentElement = document.createElement("div"); const commentElement = document.createElement("div");
commentElement.classList.add("comment-item"); commentElement.classList.add("comment-item");
commentElement.dataset.commentId = data.commentId;
if (parentCommentId) { if (parentCommentId !== "") {
commentElement.classList.add("comment-reply"); commentElement.classList.add("comment-reply");
commentElement.innerHTML = ` commentElement.innerHTML = `
<p> <p>
<strong>${escapeHtml(data.author)}</strong> <strong>${escapeHtml(data.author)}</strong>
<span>${escapeHtml(data.created)}</span> <span>${escapeHtml(data.created)}</span>
</p> </p>
<p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
`;
const parentReplies = document.querySelector( <p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
<details class="edit-comment-details">
<summary class="edit-comment-button">
Antwort bearbeiten
</summary>
<form
method="post"
action="index.php?pfad=updateComment"
class="edit-comment-form"
>
<input
type="hidden"
name="comment_id"
value="${escapeHtml(data.commentId)}"
>
<input
type="hidden"
name="article_id"
value="${escapeHtml(formData.get("article_id"))}"
>
<textarea
name="content"
required
>${escapeHtml(data.content)}</textarea>
<button type="submit" class="button">
Änderungen speichern
</button>
</form>
</details>
<form
method="post"
action="index.php?pfad=deleteComment"
class="delete-comment-form"
>
<input
type="hidden"
name="comment_id"
value="${escapeHtml(data.commentId)}"
>
<input
type="hidden"
name="article_id"
value="${escapeHtml(formData.get("article_id"))}"
>
<button
type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');"
>
Kommentar löschen
</button>
</form>
`;
const parentReplies = commentsList.querySelector(
`.comment-item[data-comment-id="${parentCommentId}"] .comment-replies` `.comment-item[data-comment-id="${parentCommentId}"] .comment-replies`
); );
if (parentReplies) { if (parentReplies) {
parentReplies.appendChild(commentElement); parentReplies.appendChild(commentElement);
} else {
commentsList.prepend(commentElement);
} }
} else { } else {
commentElement.dataset.commentId = data.commentId;
commentElement.innerHTML = ` commentElement.innerHTML = `
<p> <p>
<strong>${escapeHtml(data.author)}</strong> <strong>${escapeHtml(data.author)}</strong>
<span>${escapeHtml(data.created)}</span> <span>${escapeHtml(data.created)}</span>
</p> </p>
<p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
<button type="button" <p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
class="reply-button"
data-comment-id="${escapeHtml(data.commentId)}"
data-author="${escapeHtml(data.author)}">
Antworten
</button>
<div class="comment-replies"></div> <details class="edit-comment-details">
`; <summary class="edit-comment-button">
Kommentar bearbeiten
</summary>
<form
method="post"
action="index.php?pfad=updateComment"
class="edit-comment-form"
>
<input
type="hidden"
name="comment_id"
value="${escapeHtml(data.commentId)}"
>
<input
type="hidden"
name="article_id"
value="${escapeHtml(formData.get("article_id"))}"
>
<textarea
name="content"
required
>${escapeHtml(data.content)}</textarea>
<button type="submit" class="button">
Änderungen speichern
</button>
</form>
</details>
<form
method="post"
action="index.php?pfad=deleteComment"
class="delete-comment-form"
>
<input
type="hidden"
name="comment_id"
value="${escapeHtml(data.commentId)}"
>
<input
type="hidden"
name="article_id"
value="${escapeHtml(formData.get("article_id"))}"
>
<button
type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');"
>
Kommentar löschen
</button>
</form>
<a
href="#comment-form"
class="reply-button"
data-comment-id="${escapeHtml(data.commentId)}"
data-author="${escapeHtml(data.author)}"
>
Antworten
</a>
<div class="comment-replies"></div>
`;
commentsList.prepend(commentElement); commentsList.prepend(commentElement);
const newReplyButton = commentElement.querySelector(".reply-button"); const newReplyButton =
commentElement.querySelector(".reply-button");
if (newReplyButton) { if (newReplyButton) {
registerReplyButton(newReplyButton); registerReplyButton(newReplyButton);
@@ -115,10 +260,14 @@ document.addEventListener("DOMContentLoaded", function () {
commentContent.value = ""; commentContent.value = "";
parentCommentInput.value = ""; parentCommentInput.value = "";
replyInfo.textContent = "";
replyInfo.style.display = "none"; if (replyInfo) {
replyInfo.textContent = "";
replyInfo.style.display = "none";
}
}) })
.catch(() => { .catch(function (error) {
console.error(error);
alert("Kommentar konnte nicht gesendet werden."); alert("Kommentar konnte nicht gesendet werden.");
}); });
}); });
@@ -126,12 +275,12 @@ document.addEventListener("DOMContentLoaded", function () {
/** /**
* Entfernt HTML-Sonderzeichen aus Nutzereingaben. * Entfernt HTML-Sonderzeichen aus Nutzereingaben.
* *
* @param {string} text Zu bereinigender Text * @param {*} text Zu bereinigender Text
* @returns {string} Sicherer Text * @returns {string} Sicherer Text
*/ */
function escapeHtml(text) { function escapeHtml(text) {
const div = document.createElement("div"); const div = document.createElement("div");
div.textContent = text; div.textContent = String(text ?? "");
return div.innerHTML; return div.innerHTML;
} }
}); });
+116
View File
@@ -0,0 +1,116 @@
let currentClientPage = 1;
let itemsPerPage = 10;
function initFilter() {
const filterSelect = document.getElementById('category-filter');
const listContainer = document.querySelector('.s-res-list');
const limitSelect = document.getElementById('s-res-per-page');
if (!filterSelect || !listContainer) return;
if (limitSelect) {
itemsPerPage = parseInt(limitSelect.value, 10) || 10;
}
updateVisibility();
if (limitSelect) {
limitSelect.addEventListener('change', function() {
//neues limit einlesen
itemsPerPage = parseInt(this.value, 10);
currentClientPage = 1;
updateVisibility();
});
}
filterSelect.addEventListener('change', function() {
currentClientPage = 1;
updateVisibility();
});
const navigationContainer = document.querySelector('.s-res-page-navigation');
if (navigationContainer) {
navigationContainer.addEventListener('click', function(e) {
const button = e.target.closest('.s-res-page-btn');
if (!button || button.disabled) return;
e.preventDefault();
const targetPage = button.getAttribute('data-page');
if (targetPage) {
currentClientPage = parseInt(targetPage, 10);
updateVisibility();
}
});
}
}
function updateVisibility() {
const filterSelect = document.getElementById('category-filter');
const listContainer = document.querySelector('.s-res-list');
const selectedCategory = filterSelect.value.toLowerCase().trim();
const cards = listContainer.querySelectorAll('.s-res-item');
let visibleCards = [];
cards.forEach(card => {
const cardCategory = (card.getAttribute('data-category') || '').toLowerCase().trim();
if (selectedCategory === 'all' || cardCategory.includes(selectedCategory) || selectedCategory.includes(cardCategory)) {
visibleCards.push(card);
} else {
card.style.display = 'none';
}
});
const totalVisible = visibleCards.length;
const totalPages = Math.max(1, Math.ceil(totalVisible / itemsPerPage));
if (currentClientPage < 1) currentClientPage = 1;
if (currentClientPage > totalPages) currentClientPage = totalPages;
const startOffset = (currentClientPage - 1) * itemsPerPage;
const endOffset = startOffset + itemsPerPage;
visibleCards.forEach((card, index) => {
if (index >= startOffset && index < endOffset) {
card.style.display = 'flex';
} else {
card.style.display = 'none';
}
});
updatePaginatorUI(currentClientPage, totalPages);
}
function updatePaginatorUI(currentPage, totalPages) {
const prevBtn = document.getElementById('prev-page-btn');
const nextBtn = document.getElementById('next-page-btn');
const numbersContainer = document.getElementById('dynamic-page-numbers');
if (!prevBtn || !nextBtn || !numbersContainer) return;
prevBtn.setAttribute('data-page', currentPage - 1);
prevBtn.disabled = (currentPage <= 1);
nextBtn.setAttribute('data-page', currentPage + 1);
nextBtn.disabled = (currentPage >= totalPages);
let buttonsHTML = '';
for (let i = 1; i <= totalPages; i++) {
const activeClass = (i === currentPage) ? 's-res-page-btn-active' : '';
buttonsHTML += `<button type="button" class="s-res-page-btn ${activeClass}" data-page="${i}">${i}</button> `;
}
numbersContainer.innerHTML = buttonsHTML;
}
// Hilfsfunktion für Math.ceil in JS
function ceil(val) { return Math.ceil(val); }
// ist das DOM bereits vollständig aufgebaut?
if (document.readyState === 'loading') {
// Falls noch geladen wird, auf das Event warten
document.addEventListener('DOMContentLoaded', initFilter);
} else {
// Falls das HTML bereits komplett da ist, sofort ausführen
initFilter();
}
+27
View File
@@ -0,0 +1,27 @@
function initPaginator() {
const form = document.getElementById('search-form-id');
const pageInput = document.getElementById('s-res-page-input');
const pageButtons = document.querySelectorAll('.s-res-page-navigation .s-res-page-btn');
pageButtons.forEach(button => {
button.addEventListener('click', function() {
if (this.disabled) return;
const targetPage = this.getAttribute('data-page');
if (targetPage && form && pageInput) {
pageInput.value = targetPage;
form.submit();
}
});
});
}
// ist das DOM bereits vollständig aufgebaut?
if (document.readyState === 'loading') {
// Falls noch geladen wird, auf das Event warten
document.addEventListener('DOMContentLoaded', initPaginator);
} else {
// Falls das HTML bereits komplett da ist, sofort ausführen
initPaginator();
}
-238
View File
@@ -1,238 +0,0 @@
/**
* Übernimmt Sortierung, Kategorie-Filter und Pagination der Suchergebnisse
* vollständig clientseitig
*
* Ablauf:
* 1. Kompletten Ergebnis-Datensatz einmalig per fetch() nachladen
* (php/controller/search-results-data.php, liest nur die Session-Daten,
* keine erneute Datenbank-Suche).
* 2. Sortieren + filtern (JS-Array).
* 3. Ergebnisliste + Pagination-UI daraus neu rendern.
*
* Ersetzt sorter.js, filter.js und paginator.js, deren Klick-Handler sich
* gegenseitig ins Gehege kamen.
* Ohne JS bleibt die serverseitig gerenderte Seite (echte Links/Formulare)
* voll funktionsfähig.
*/
(function () {
const state = {
allItems: [],
sort: 'alphabet',
category: 'all',
itemsPerPage: 10,
currentPage: 1,
};
let listContainer, sortRadios, categorySelect, limitSelect;
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
function init() {
listContainer = document.querySelector('.s-res-list');
if (!listContainer) return;
sortRadios = document.querySelectorAll('.sort-radio');
categorySelect = document.getElementById('category-filter');
limitSelect = document.getElementById('s-res-per-page');
prevBtn = document.getElementById('prev-page-btn');
nextBtn = document.getElementById('next-page-btn');
numbersContainer = document.getElementById('dynamic-page-numbers');
jsNav = document.getElementById('js-page-navigation');
resultCountEl = document.getElementById('s-res-result-count');
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;
fetchFullDataset()
.then(function (data) {
state.allItems = data.results || [];
attachEvents();
render();
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);
});
}
function fetchFullDataset() {
return fetch('php/controller/search-results-data.php', { credentials: 'same-origin' })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
});
}
function getFilteredSortedItems() {
let items = state.allItems;
if (state.category !== 'all') {
const wanted = state.category.toLowerCase();
items = items.filter(function (item) {
return (item.category || '').toLowerCase() === wanted;
});
}
items = items.slice().sort(function (a, b) {
if (state.sort === 'likes') {
return (b.likes || 0) - (a.likes || 0);
}
if (state.sort === 'newest' || state.sort === 'oldest') {
return state.sort === 'newest' ? (b.id - a.id) : (a.id - b.id);
}
// alphabet (Standard)
return (a.title || '').toLowerCase().localeCompare((b.title || '').toLowerCase());
});
return items;
}
function render() {
const items = getFilteredSortedItems();
if (resultCountEl) resultCountEl.textContent = items.length;
const totalPages = Math.max(1, Math.ceil(items.length / state.itemsPerPage));
if (state.currentPage > totalPages) state.currentPage = totalPages;
if (state.currentPage < 1) state.currentPage = 1;
const start = (state.currentPage - 1) * state.itemsPerPage;
const pageItems = items.slice(start, start + state.itemsPerPage);
listContainer.innerHTML = '';
if (pageItems.length === 0) {
const empty = document.createElement('p');
empty.textContent = 'Keine Beiträge gefunden.';
listContainer.appendChild(empty);
} else {
pageItems.forEach(function (item) {
listContainer.appendChild(buildCard(item));
});
}
updatePaginatorUI(state.currentPage, totalPages);
}
function buildCard(item) {
const card = document.createElement('div');
card.className = 's-res-item';
card.setAttribute('data-likes', item.likes);
card.setAttribute('data-category', (item.category || '').toLowerCase());
const link = document.createElement('a');
link.className = 's-res-link';
link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id);
link.textContent = item.title;
const title = document.createElement('h2');
title.className = 's-res-item-title';
title.appendChild(link);
const authorName = document.createElement('span');
authorName.className = 's-res-author-name';
authorName.textContent = item.author;
const author = document.createElement('p');
author.className = 's-res-author';
author.append('Von: ', authorName);
const likes = document.createElement('span');
likes.className = 's-res-likes';
likes.textContent = '❤️ ' + item.likes;
const metaRow = document.createElement('div');
metaRow.className = 's-res-meta-row';
metaRow.append(author, likes);
const content = document.createElement('div');
content.className = 's-res-content';
content.append(title, metaRow);
const arrow = document.createElement('div');
arrow.className = 's-res-arrow';
arrow.textContent = '→';
card.append(content, arrow);
return card;
}
function updatePaginatorUI(currentPage, totalPages) {
if (!prevBtn || !nextBtn || !numbersContainer) return;
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= totalPages;
numbersContainer.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 's-res-page-btn' + (i === currentPage ? ' s-res-page-btn-active' : '');
btn.textContent = i;
btn.addEventListener('click', function () {
state.currentPage = i;
render();
});
numbersContainer.appendChild(btn);
}
}
function attachEvents() {
sortRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
state.sort = this.value;
state.currentPage = 1;
render();
});
});
if (categorySelect) {
categorySelect.addEventListener('change', function () {
state.category = this.value;
state.currentPage = 1;
render();
});
}
if (limitSelect) {
limitSelect.addEventListener('change', function () {
state.itemsPerPage = parseInt(this.value, 10) || 10;
state.currentPage = 1;
render();
});
}
if (prevBtn) {
prevBtn.addEventListener('click', function () {
if (state.currentPage > 1) {
state.currentPage -= 1;
render();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
state.currentPage += 1;
render();
});
}
// Mit aktivem JS übernimmt render() Sortierung/Filter live
// ein echtes Absenden des Formulars (No-JS-Fallback) ist dann nicht mehr nötig.
const filterForm = document.getElementById('s-res-filter-form');
if (filterForm) {
filterForm.addEventListener('submit', function (e) {
e.preventDefault();
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
-242
View File
@@ -1,242 +0,0 @@
/**
* Übernimmt Sortierung, Suche (innerhalb der Kategorie) und Pagination der
* Kategorie-Ansicht vollständig clientseitig.
*
* Ablauf:
* 1. Kompletten Kategorie-Datensatz einmalig per fetch() nachladen
* 2. Sortieren + nach Suchbegriff filtern (JS-Array).
* 3. Ergebnisliste + Pagination-UI daraus neu rendern.
*
* Ohne JS bleibt die serverseitig gerenderte Seite (echte Links/Formulare)
* voll funktionsfähig
*/
(function () {
const state = {
allItems: [],
sort: 'alphabet',
query: '',
itemsPerPage: 10,
currentPage: 1,
};
let listContainer, sortRadios, searchInput, limitSelect;
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
let searchDebounceTimer;
function init() {
listContainer = document.querySelector('.s-res-list');
if (!listContainer) return;
sortRadios = document.querySelectorAll('.sort-radio');
searchInput = document.getElementById('cat-search-input');
limitSelect = document.getElementById('s-res-per-page');
prevBtn = document.getElementById('prev-page-btn');
nextBtn = document.getElementById('next-page-btn');
numbersContainer = document.getElementById('dynamic-page-numbers');
jsNav = document.getElementById('js-page-navigation');
resultCountEl = document.getElementById('s-res-result-count');
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;
fetchFullDataset()
.then(function (data) {
state.allItems = data.results || [];
attachEvents();
render();
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);
});
}
function fetchFullDataset() {
return fetch('php/controller/showCategory-data.php', { credentials: 'same-origin' })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
});
}
function getFilteredSortedItems() {
let items = state.allItems;
const needle = state.query.trim().toLowerCase();
if (needle !== '') {
items = items.filter(function (item) {
const haystack = ((item.title || '') + ' ' + (item.content || '')).toLowerCase();
return haystack.indexOf(needle) !== -1;
});
}
items = items.slice().sort(function (a, b) {
if (state.sort === 'likes') {
return (b.likes || 0) - (a.likes || 0);
}
if (state.sort === 'newest' || state.sort === 'oldest') {
return state.sort === 'newest' ? (b.id - a.id) : (a.id - b.id);
}
// alphabet (Standard)
return (a.title || '').toLowerCase().localeCompare((b.title || '').toLowerCase());
});
return items;
}
function render() {
const items = getFilteredSortedItems();
if (resultCountEl) resultCountEl.textContent = items.length;
const totalPages = Math.max(1, Math.ceil(items.length / state.itemsPerPage));
if (state.currentPage > totalPages) state.currentPage = totalPages;
if (state.currentPage < 1) state.currentPage = 1;
const start = (state.currentPage - 1) * state.itemsPerPage;
const pageItems = items.slice(start, start + state.itemsPerPage);
listContainer.innerHTML = '';
if (pageItems.length === 0) {
const empty = document.createElement('p');
empty.textContent = 'Keine Beiträge gefunden.';
listContainer.appendChild(empty);
} else {
pageItems.forEach(function (item) {
listContainer.appendChild(buildCard(item));
});
}
updatePaginatorUI(state.currentPage, totalPages);
}
function buildCard(item) {
const card = document.createElement('div');
card.className = 's-res-item';
card.setAttribute('data-likes', item.likes);
card.setAttribute('data-category', (item.category || '').toLowerCase());
const link = document.createElement('a');
link.className = 's-res-link';
link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id);
link.textContent = item.title;
const title = document.createElement('h2');
title.className = 's-res-item-title';
title.appendChild(link);
const authorName = document.createElement('span');
authorName.className = 's-res-author-name';
authorName.textContent = item.author;
const author = document.createElement('p');
author.className = 's-res-author';
author.append('Von: ', authorName);
const likes = document.createElement('span');
likes.className = 's-res-likes';
likes.textContent = '❤️ ' + item.likes;
const metaRow = document.createElement('div');
metaRow.className = 's-res-meta-row';
metaRow.append(author, likes);
const content = document.createElement('div');
content.className = 's-res-content';
content.append(title, metaRow);
const arrow = document.createElement('div');
arrow.className = 's-res-arrow';
arrow.textContent = '→';
card.append(content, arrow);
return card;
}
function updatePaginatorUI(currentPage, totalPages) {
if (!prevBtn || !nextBtn || !numbersContainer) return;
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= totalPages;
numbersContainer.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 's-res-page-btn' + (i === currentPage ? ' s-res-page-btn-active' : '');
btn.textContent = i;
btn.addEventListener('click', function () {
state.currentPage = i;
render();
});
numbersContainer.appendChild(btn);
}
}
function attachEvents() {
sortRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
state.sort = this.value;
state.currentPage = 1;
render();
});
});
if (searchInput) {
// Live-Filterung während des Tippens (leicht entprellt), da der
// Datensatz bereits vollständig im Browser liegt.
searchInput.addEventListener('input', function () {
clearTimeout(searchDebounceTimer);
const value = this.value;
searchDebounceTimer = setTimeout(function () {
state.query = value;
state.currentPage = 1;
render();
}, 150);
});
}
if (limitSelect) {
limitSelect.addEventListener('change', function () {
state.itemsPerPage = parseInt(this.value, 10) || 10;
state.currentPage = 1;
render();
});
}
if (prevBtn) {
prevBtn.addEventListener('click', function () {
if (state.currentPage > 1) {
state.currentPage -= 1;
render();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
state.currentPage += 1;
render();
});
}
// Mit aktivem JS übernimmt render() Sortierung/Suche live
// ein echtes Absenden des Formulars (No-JS-Fallback) ist dann nicht mehr nötig.
const filterForm = document.getElementById('cat-filter-form');
if (filterForm) {
filterForm.addEventListener('submit', function (e) {
e.preventDefault();
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
+50
View File
@@ -0,0 +1,50 @@
function initSorter() {
const listContainer = document.querySelector('.s-res-list');
const sortRadios = document.querySelectorAll('.sort-radio');
// wenn keine liste vorhanden, abbrechen
if (!listContainer || sortRadios.length === 0) return;
sortRadios.forEach(radio => {
radio.addEventListener('change', function() {
const cards = Array.from(listContainer.querySelectorAll('.s-res-item'));
const sortValue = this.value;
cards.sort((a, b) => {
if (sortValue === 'likes') {
const likesA = parseInt(a.getAttribute('data-likes') || '0', 10);
const likesB = parseInt(b.getAttribute('data-likes') || '0', 10);
return likesB - likesA;
}
else if (sortValue === 'alphabet') {
// alphabetische sortierung
const titleA = a.querySelector('.s-res-link').textContent.trim().toLowerCase();
const titleB = b.querySelector('.s-res-link').textContent.trim().toLowerCase();
return titleA.localeCompare(titleB);
}
else if (sortValue === 'newest' || sortValue === 'oldest') {
// hoehere ID wird als neuer gesehen
const urlA = a.querySelector('.s-res-link').getAttribute('href');
const urlB = b.querySelector('.s-res-link').getAttribute('href');
const idA = parseInt(urlA.match(/id=(\d+)/)[1], 10);
const idB = parseInt(urlB.match(/id=(\d+)/)[1], 10);
return sortValue === 'newest' ? idB - idA : idA - idB;
}
return 0;
});
listContainer.innerHTML = '';
cards.forEach(card => listContainer.appendChild(card));
});
});
}
// ist das DOM bereits vollständig aufgebaut?
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSorter);
} else {
initSorter();
}
+156 -30
View File
@@ -3,32 +3,153 @@ if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
header("Content-Type: application/json");
require_once "../model/CommentManager.php"; require_once "../model/CommentManager.php";
if (!isset($_SESSION["user_email"])) { /**
echo json_encode([ * Prüft, ob die Anfrage durch JavaScript per AJAX gesendet wurde.
"success" => false, */
"message" => "Du musst angemeldet sein, um zu kommentieren." $isAjaxRequest = isset($_SERVER["HTTP_X_REQUESTED_WITH"])
]); && strtolower($_SERVER["HTTP_X_REQUESTED_WITH"]) === "xmlhttprequest";
/**
* Gibt das Ergebnis entweder als JSON zurück oder leitet
* bei einem normalen Formularaufruf wieder zum Beitrag zurück.
*
* @param bool $success War das Speichern erfolgreich?
* @param string $message Rückmeldung für den Benutzer
* @param int|null $articleId ID des Beitrags
* @param array $additionalData Zusätzliche Daten für AJAX
*/
function sendCommentResponse(
$success,
$message,
$articleId,
$additionalData = []
) {
global $isAjaxRequest;
if ($isAjaxRequest) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode(
array_merge(
[
"success" => $success,
"message" => $message
],
$additionalData
)
);
exit();
}
/*
* Bei deaktiviertem JavaScript wird die Rückmeldung
* in der Session gespeichert und die Beitragsseite neu geladen.
*/
$_SESSION["comment_message"] = $message;
$_SESSION["comment_message_type"] = $success ? "success" : "error";
if ($articleId !== null) {
header(
"Location: ../../index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
} else {
header("Location: ../../index.php");
}
exit(); exit();
} }
$articleId = $_POST["article_id"] ?? null; /*
$content = trim($_POST["content"] ?? ""); * Nur POST-Anfragen dürfen Kommentare erstellen.
$parentCommentId = $_POST["parent_comment_id"] ?? null; */
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
sendCommentResponse(
false,
"Ungültige Anfrage.",
null
);
}
if ($parentCommentId === "" || $parentCommentId === "0") { /*
* Die Beitrags-ID wird zuerst eingelesen,
* damit bei Fehlern wieder zum Beitrag zurückgeleitet werden kann.
*/
$articleId = filter_input(
INPUT_POST,
"article_id",
FILTER_VALIDATE_INT
);
/*
* Ein Benutzer muss angemeldet sein.
*/
if (!isset($_SESSION["user_email"])) {
sendCommentResponse(
false,
"Du musst angemeldet sein, um zu kommentieren.",
$articleId !== false ? $articleId : null
);
}
/*
* Weitere Formulardaten einlesen.
*/
$content = trim($_POST["content"] ?? "");
$parentCommentId = filter_input(
INPUT_POST,
"parent_comment_id",
FILTER_VALIDATE_INT
);
/*
* Ein leerer Wert bedeutet, dass es sich um einen
* normalen Hauptkommentar handelt.
*/
if (
!isset($_POST["parent_comment_id"])
|| $_POST["parent_comment_id"] === ""
|| $_POST["parent_comment_id"] === "0"
) {
$parentCommentId = null; $parentCommentId = null;
} }
if (empty($articleId) || empty($content)) { if ($articleId === false || $articleId === null) {
echo json_encode([ sendCommentResponse(
"success" => false, false,
"message" => "Kommentar darf nicht leer sein." "Der zugehörige Beitrag ist ungültig.",
]); null
exit(); );
}
if ($content === "") {
sendCommentResponse(
false,
"Der Kommentar darf nicht leer sein.",
$articleId
);
}
/*
* Eine ungültige Eltern-ID darf nicht gespeichert werden.
*/
if (
isset($_POST["parent_comment_id"])
&& $_POST["parent_comment_id"] !== ""
&& $_POST["parent_comment_id"] !== "0"
&& $parentCommentId === false
) {
sendCommentResponse(
false,
"Der ausgewählte Kommentar ist ungültig.",
$articleId
);
} }
try { try {
@@ -41,18 +162,23 @@ try {
$parentCommentId $parentCommentId
); );
echo json_encode([ sendCommentResponse(
"success" => true, true,
"commentId" => $commentId, "Der Kommentar wurde erfolgreich gespeichert.",
"author" => $_SESSION["user_email"], $articleId,
"content" => $content, [
"created" => date("Y-m-d H:i:s"), "commentId" => $commentId,
"parentCommentId" => $parentCommentId "author" => $_SESSION["user_email"],
]); "content" => $content,
"created" => date("Y-m-d H:i:s"),
"parentCommentId" => $parentCommentId
]
);
} catch (Exception $e) { } catch (Throwable $e) {
echo json_encode([ sendCommentResponse(
"success" => false, false,
"message" => "Kommentar konnte nicht gespeichert werden." "Der Kommentar konnte nicht gespeichert werden.",
]); $articleId
);
} }
+105
View File
@@ -0,0 +1,105 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . "/../model/CommentManager.php";
/*
* Kommentare dürfen nur über ein POST-Formular gelöscht werden.
*/
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("Location: index.php");
exit();
}
$commentId = filter_input(
INPUT_POST,
"comment_id",
FILTER_VALIDATE_INT
);
$articleId = filter_input(
INPUT_POST,
"article_id",
FILTER_VALIDATE_INT
);
/*
* Nur angemeldete Nutzer dürfen Kommentare löschen.
*/
if (!isset($_SESSION["user_email"])) {
$_SESSION["comment_message"] = "Du musst angemeldet sein.";
$_SESSION["comment_message_type"] = "error";
if ($articleId !== false && $articleId !== null) {
header(
"Location: index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
} else {
header("Location: index.php");
}
exit();
}
/*
* Kommentar-ID und Beitrags-ID müssen gültige Zahlen sein.
*/
if (
$commentId === false
|| $commentId === null
|| $articleId === false
|| $articleId === null
) {
$_SESSION["comment_message"] =
"Der Kommentar konnte nicht gelöscht werden.";
$_SESSION["comment_message_type"] = "error";
header("Location: index.php");
exit();
}
try {
$commentManager = CommentManager::getInstance();
/*
* Die E-Mail-Adresse aus der Session wird mitgegeben.
* Dadurch kann der Nutzer nur eigene Kommentare löschen.
*/
$deleted = $commentManager->deleteComment(
$commentId,
$_SESSION["user_email"]
);
if ($deleted) {
$_SESSION["comment_message"] =
"Der Kommentar wurde gelöscht.";
$_SESSION["comment_message_type"] = "success";
} else {
$_SESSION["comment_message"] =
"Der Kommentar wurde nicht gefunden oder gehört nicht dir.";
$_SESSION["comment_message_type"] = "error";
}
} catch (Throwable $e) {
$_SESSION["comment_message"] =
"Der Kommentar konnte nicht gelöscht werden.";
$_SESSION["comment_message_type"] = "error";
}
/*
* Anschließend wird wieder zum Beitrag und zu den Kommentaren geleitet.
*/
header(
"Location: index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
exit();
+35 -15
View File
@@ -16,12 +16,37 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
$_SESSION["message"] = "invalid_search_query"; $_SESSION["message"] = "invalid_search_query";
} else { } else {
try { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
$userManager = UserManager::getInstance(); $userManager = UserManager::getInstance();
$results = $articleManager->search($search); $results = $articleManager->search($search);
$sortStyle = $_GET['sort'] ?? 'alphabet';
$_SESSION['search_sort'] = $sortStyle;
if ($sortStyle === 'alphabet') {
// Titel aufsteigend alphabetiisch sortiert
usort($results, function ($a, $b) {
return strcasecmp($a->getTitle(), $b->getTitle());
});
} elseif ($sortStyle === 'likes') {
usort($results, function($a, $b) {
return $b->getLikeCount() <=> $a->getLikeCount();
});
} elseif ($sortStyle === 'newest') {
// Datum neu zu alt sortiert
usort($results, function($a, $b) {
return strcmp($b->getCreationDate(), $a->getCreationDate());
});
} elseif ($sortStyle === 'oldest') {
// Datum alt zu neu sortiert
usort($results, function($a, $b) {
return strcmp($a->getCreationDate(), $b->getCreationDate());
});
}
// Ergebnisse werden in ein flaches array umgewandelt, da sont incomplete-PHP error im Ergebnis
$safeArrayResults = []; $safeArrayResults = [];
foreach ($results as $obj) { foreach ($results as $obj) {
$safeArrayResults[] = [ $safeArrayResults[] = [
@@ -35,30 +60,25 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
"likes" => $obj->getLikes(), "likes" => $obj->getLikes(),
]; ];
} }
$_SESSION["search_results"] = $safeArrayResults; $_SESSION["search_results"] = $safeArrayResults;
$_SESSION["search_query"] = $search; $_SESSION["search_query"] = $search;
$_SESSION["message"] = "new_search_results"; $_SESSION["message"] = "new_search_results";
} catch (Exception $e) { } catch (Exception $e){
$_SESSION["message"] = "internal_error"; $_SESSION["message"] = "internal_error";
} }
} }
$sort = $_GET['sort'] ?? 'alphabet'; $sort = $_GET['sort'] ?? 'alphabet';
$_SESSION['search_sort'] = $sort;
$category = $_GET['category'] ?? 'all';
$_SESSION['search_category'] = $category;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10; $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10;
if (!searchLimitValidator($limit)) { if (!searchLimitValidator($limit)) {
$limit = 10; $limit = 10;
} }
$_SESSION['search_limit'] = $limit; $page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
header("Location: ../../index.php?pfad=search-results&q=" . urlencode($search) . "&sort=" . urlencode($sort) . "&limit=" . $limit . "&page=" . $page);
// Neue Suche -> immer wieder bei Seite 1 beginnen
header("Location: ../../index.php?pfad=search-results&q=" . urlencode($search) . "&sort=" . urlencode($sort) . "&category=" . urlencode($category) . "&limit=" . $limit . "&page=1");
exit(); exit();
}
} ?>
-32
View File
@@ -1,32 +0,0 @@
<?php
/**
* Liefert den vollständigen, zuletzt gefundenen Ergebnis-Datensatz als JSON.
*
* Wird von js/search-results.js EINMALIG per fetch() geladen, damit Sortierung,
* Filterung und Pagination danach komplett im Browser laufen können, ohne
* weitere Serveranfragen oder eine erneute Datenbank-Suche auszulösen.
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../includes/resultsHelper.php';
$rawResults = $_SESSION['search_results'] ?? [];
$payload = array_map(function ($item) {
return [
'id' => $item['id'],
'title' => $item['title'],
'author' => $item['author'],
'category' => $item['category'] ?? '',
'likes' => getLikeCount($item),
];
}, $rawResults);
echo json_encode([
'query' => $_SESSION['search_query'] ?? '',
'results' => array_values($payload),
]);
+2 -1
View File
@@ -49,8 +49,9 @@ if (isset($_GET["id"]) && !empty($_GET["id"])){
} }
} }
} catch (Exception $e){ } catch (Throwable $e) {
$_SESSION["message"] = "internal_error"; $_SESSION["message"] = "internal_error";
header("Location: index.php");
exit(); exit();
} }
}else{ }else{
+39 -24
View File
@@ -2,51 +2,66 @@
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
require_once 'php/model/Article.php'; require_once 'php/model/Article.php';
require_once 'php/model/ArticleManager.php'; require_once 'php/model/ArticleManager.php';
require_once 'php/model/UserManager.php'; require_once 'php/model/UserManager.php';
require_once 'php/validator/article-validator.php'; require_once 'php/validator/article-validator.php';
require_once __DIR__ . '/../../includes/resultsHelper.php';
if (isset($_GET["category"]) && !empty($_GET["category"]) && articleCategoryValidator($_GET["category"])) {
if (isset($_GET["category"]) && !empty($_GET["category"]) && articleCategoryValidator($_GET["category"])){
$category = $_GET["category"]; $category = $_GET["category"];
// Sortierung auslesen (Standard: alphabet)
$sortStyle = isset($_GET['sort']) ? trim($_GET['sort']) : 'alphabet';
// Aktuelle Seite auslesen
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) { $page = 1; }
try { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
$userManager = UserManager::getInstance(); $userManager = UserManager::getInstance();
$allArticles = $articleManager->getArticlesByCategory($category); $allArticles = $articleManager->getArticlesByCategory($category);
$safeArrayResults = [];
foreach ($allArticles as $article) { foreach ($allArticles as $article) {
$authorName = $userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"]; $article->setAuthor($userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"]);
$safeArrayResults[] = [ }
"id" => $article->getID(), $totalArticles = count($allArticles);
"title" => $article->getTitle(),
"content" => $article->getContent(), // --- SORTIERUNG LOGIK ---
"author" => $authorName, if ($sortStyle === 'alphabet') {
"category" => $article->getCategory(), usort($allArticles, function($a, $b) {
"tags" => $article->getTags(), return strcasecmp($a->getTitle(), $b->getTitle());
"creationDate" => $article->getCreationDate(), });
"likes" => $article->getLikes(), } elseif ($sortStyle === 'likes') {
]; usort($allArticles, function($a, $b) {
return $b->getLikes() <=> $a->getLikes(); // Absteigend nach Likes
});
} elseif ($sortStyle === 'newest') {
usort($allArticles, function($a, $b) {
return strtotime($b->getCreationDate()) <=> strtotime($a->getCreationDate()); // Neueste zuerst
});
} elseif ($sortStyle === 'oldest') {
usort($allArticles, function($a, $b) {
return strtotime($a->getCreationDate()) <=> strtotime($b->getCreationDate()); // Älteste zuerst
});
} }
// Session dient hier (wie bei search-results) als Zwischenspeicher, damit // Paginierung konfigurieren
// js/showCategory.js den kompletten Datensatz per showCategory-data.php $limit = 10;
// nachladen kann, ohne die Kategorie-Abfrage ein zweites Mal auszuführen. $totalPages = ceil($totalArticles / $limit);
$_SESSION['category_results'] = $safeArrayResults; if ($page > $totalPages && $totalPages > 0) { $page = $totalPages; }
$_SESSION['category_name'] = $category;
$offset = ($page - 1) * $limit;
$articles = array_slice($allArticles, $offset, $limit);
} catch (Exception $e) { } catch (Exception $e) {
$_SESSION["message"] = "internal_error"; $_SESSION["message"] = "internal_error";
include_once "content/404.php"; include_once "content/404.php";
exit(); exit();
} }
} else { } else {
$_SESSION["message"] = "invalid_category"; $_SESSION["message"] = "invalid_category";
include_once "content/404.php"; include_once "content/404.php";
exit(); exit();
} }
?>
-35
View File
@@ -1,35 +0,0 @@
<?php
/**
* Liefert den vollständigen, zuletzt geladenen Kategorie-Datensatz als JSON.
*
* Wird von js/showCategory.js EINMALIG per fetch() geladen, damit Sortierung,
* Suche und Pagination danach komplett im Browser laufen können, ohne
* weitere Serveranfragen oder eine erneute Datenbank-Abfrage auszulösen
*
* Analog zu php/controller/search-results-data.php
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../includes/resultsHelper.php';
$rawResults = $_SESSION['category_results'] ?? [];
$payload = array_map(function ($item) {
return [
'id' => $item['id'],
'title' => $item['title'],
'content' => $item['content'] ?? '',
'author' => $item['author'],
'category' => $item['category'] ?? '',
'likes' => getLikeCount($item),
];
}, $rawResults);
echo json_encode([
'category' => $_SESSION['category_name'] ?? '',
'results' => array_values($payload),
]);
@@ -0,0 +1,92 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once "php/model/CommentManager.php";
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("Location: index.php");
exit();
}
if (!isset($_SESSION["user_email"])) {
header("Location: index.php?pfad=login");
exit();
}
$commentId = filter_input(
INPUT_POST,
"comment_id",
FILTER_VALIDATE_INT
);
$articleId = filter_input(
INPUT_POST,
"article_id",
FILTER_VALIDATE_INT
);
$content = trim($_POST["content"] ?? "");
/*
* Ohne gültige Beitrags-ID kann nicht sicher
* zum ursprünglichen Beitrag zurückgeleitet werden.
*/
if (!$articleId) {
header("Location: index.php");
exit();
}
/*
* Kommentar-ID und Inhalt werden geprüft.
*/
if (!$commentId || $content === "") {
$_SESSION["comment_message"] =
"Der Kommentar darf nicht leer sein.";
$_SESSION["comment_message_type"] = "error";
header(
"Location: index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
exit();
}
try {
$commentManager = CommentManager::getInstance();
$updated = $commentManager->updateComment(
$commentId,
$_SESSION["user_email"],
$content
);
if ($updated) {
$_SESSION["comment_message"] =
"Der Kommentar wurde erfolgreich bearbeitet.";
$_SESSION["comment_message_type"] = "success";
} else {
$_SESSION["comment_message"] =
"Der Kommentar konnte nicht bearbeitet werden.";
$_SESSION["comment_message_type"] = "error";
}
} catch (Throwable $e) {
$_SESSION["comment_message"] =
"Beim Bearbeiten des Kommentars ist ein Fehler aufgetreten.";
$_SESSION["comment_message_type"] = "error";
}
header(
"Location: index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
exit();
+5 -5
View File
@@ -13,7 +13,7 @@ class Comment
private int $id; private int $id;
private int $articleId; private int $articleId;
private ?int $parentCommentId; private ?int $parentCommentId;
private string $author; private ?string $author;
private string $content; private string $content;
private string $created; private string $created;
@@ -23,7 +23,7 @@ class Comment
* @param int $id Eindeutige ID des Kommentars * @param int $id Eindeutige ID des Kommentars
* @param int $articleId ID des zugehörigen Beitrags * @param int $articleId ID des zugehörigen Beitrags
* @param int|null $parentCommentId ID des Eltern-Kommentars oder null * @param int|null $parentCommentId ID des Eltern-Kommentars oder null
* @param string $author Autor des Kommentars * @param string|null $author Autor des Kommentars oder null bei gelöschten Kommentaren
* @param string $content Inhalt des Kommentars * @param string $content Inhalt des Kommentars
* @param string $created Erstellungsdatum des Kommentars * @param string $created Erstellungsdatum des Kommentars
*/ */
@@ -31,7 +31,7 @@ class Comment
int $id, int $id,
int $articleId, int $articleId,
?int $parentCommentId, ?int $parentCommentId,
string $author, ?string $author,
string $content, string $content,
string $created string $created
) { ) {
@@ -86,9 +86,9 @@ class Comment
/** /**
* Gibt den Autor des Kommentars zurück. * Gibt den Autor des Kommentars zurück.
* *
* @return string Autor * @return string|null Autor oder null bei gelöschten Kommentaren
*/ */
public function getAuthor(): string public function getAuthor(): ?string
{ {
return $this->author; return $this->author;
} }
+36
View File
@@ -51,4 +51,40 @@ interface CommentManagerDAO
* @return Comment[] Liste der Kommentare * @return Comment[] Liste der Kommentare
*/ */
public function getCommentsByAuthor($author); public function getCommentsByAuthor($author);
/**
* Löscht einen einzelnen Kommentar des angemeldeten Nutzers.
*
* Kommentare ohne Antworten werden vollständig entfernt.
* Kommentare mit Antworten bleiben als anonymer Platzhalter erhalten.
*
* @param int $commentId ID des Kommentars
* @param string $author E-Mail-Adresse des Autors
* @return bool true, wenn der Kommentar gefunden und gelöscht wurde
*/
public function deleteComment(int $commentId, string $author): bool;
/**
* Löscht beziehungsweise anonymisiert alle Kommentare eines Nutzers.
*
* Diese Methode wird bei der Löschung eines Benutzerkontos verwendet.
*
* @param string $author E-Mail-Adresse des Nutzers
* @return void
*/
public function deleteCommentsByAuthor(string $author): void;
/**
* Bearbeitet einen Kommentar des angemeldeten Nutzers.
*
* Nur der Autor des Kommentars darf den Inhalt ändern.
*
* @param int $commentId ID des Kommentars
* @param string $author E-Mail-Adresse des Autors
* @param string $content Neuer Kommentarinhalt
* @return bool true, wenn der Kommentar bearbeitet wurde
*/
public function updateComment(
int $commentId,
string $author,
string $content
): bool;
} }
+191 -1
View File
@@ -17,7 +17,7 @@ class DatabaseCommentManager implements CommentManagerDAO
/** /**
* Erstellt die Kommentartabelle, falls diese noch nicht existiert. * Erstellt die Kommentartabelle, falls diese noch nicht existiert.
*/ */
public function __construct() private function __construct()
{ {
$this->dbPath = __DIR__ . '/../../db/eduforgeDB.db'; $this->dbPath = __DIR__ . '/../../db/eduforgeDB.db';
DatabaseInitializer::initialize($this->dbPath); DatabaseInitializer::initialize($this->dbPath);
@@ -223,4 +223,194 @@ class DatabaseCommentManager implements CommentManagerDAO
return $comments; return $comments;
} }
/**
* Bearbeitet einen eigenen Kommentar.
*
* Der Kommentar wird nur geändert, wenn er dem
* angemeldeten Nutzer gehört.
*
* @param int $commentId ID des Kommentars
* @param string $author E-Mail-Adresse des Autors
* @param string $content Neuer Kommentarinhalt
* @return bool true, wenn der Kommentar bearbeitet wurde
*/
public function updateComment(
int $commentId,
string $author,
string $content
): bool {
try {
$db = $this->getConnection();
$command = $db->prepare("
UPDATE comments
SET content = :content
WHERE id = :commentId
AND author = :author
");
$command->execute([
":content" => $content,
":commentId" => $commentId,
":author" => $author
]);
return $command->rowCount() > 0;
} catch (PDOException $e) {
throw new RuntimeException("internal_error");
}
}
/**
* Löscht einen eigenen Kommentar.
*
* Hat der Kommentar Antworten, wird er anonymisiert.
* Hat er keine Antworten, wird er vollständig gelöscht.
*
* @param int $commentId ID des Kommentars
* @param string $author E-Mail-Adresse des Autors
* @return bool true, wenn der Kommentar gelöscht wurde
*/
public function deleteComment(int $commentId, string $author): bool
{
try {
$db = $this->getConnection();
/*
* Zuerst wird geprüft, ob der Kommentar existiert
* und wirklich dem angemeldeten Nutzer gehört.
*/
$checkCommand = $db->prepare("
SELECT id
FROM comments
WHERE id = :commentId
AND author = :author
");
$checkCommand->execute([
":commentId" => $commentId,
":author" => $author
]);
if ($checkCommand->fetch() === false) {
return false;
}
/*
* Danach wird geprüft, ob Antworten auf den Kommentar existieren.
*/
$replyCommand = $db->prepare("
SELECT COUNT(*)
FROM comments
WHERE parent_comment_id = :commentId
");
$replyCommand->execute([
":commentId" => $commentId
]);
$hasReplies = (int) $replyCommand->fetchColumn() > 0;
if ($hasReplies) {
/*
* Der Kommentar wird für den Kommentarbaum benötigt.
* Deshalb bleibt er als anonymer Platzhalter erhalten.
*/
$deleteCommand = $db->prepare("
UPDATE comments
SET author = NULL,
content = 'Dieser Kommentar wurde gelöscht.'
WHERE id = :commentId
AND author = :author
");
} else {
/*
* Ohne Antworten kann der Kommentar vollständig
* aus der Datenbank entfernt werden.
*/
$deleteCommand = $db->prepare("
DELETE FROM comments
WHERE id = :commentId
AND author = :author
");
}
$deleteCommand->execute([
":commentId" => $commentId,
":author" => $author
]);
return $deleteCommand->rowCount() > 0;
} catch (PDOException $e) {
throw new RuntimeException("internal_error");
}
}
/**
* Löscht beziehungsweise anonymisiert alle Kommentare eines Nutzers.
*
* Kommentare ohne Antworten werden vollständig gelöscht.
* Kommentare mit Antworten bleiben als anonyme Platzhalter erhalten.
*
* @param string $author E-Mail-Adresse des Nutzers
* @return void
*/
public function deleteCommentsByAuthor(string $author): void
{
$db = $this->getConnection();
try {
$db->beginTransaction();
/*
* Zuerst werden alle Kommentare ohne Antworten gelöscht.
*
* Die Schleife ist wichtig, weil durch das Löschen einer Antwort
* eventuell auch der darüberliegende Kommentar keine Antworten
* mehr besitzt und anschließend ebenfalls gelöscht werden kann.
*/
do {
$deleteCommand = $db->prepare("
DELETE FROM comments
WHERE author = :author
AND NOT EXISTS (
SELECT 1
FROM comments AS replies
WHERE replies.parent_comment_id = comments.id
)
");
$deleteCommand->execute([
":author" => $author
]);
$deletedRows = $deleteCommand->rowCount();
} while ($deletedRows > 0);
/*
* Kommentare, auf die noch Antworten anderer Nutzer folgen,
* müssen für den Kommentarbaum erhalten bleiben.
*/
$placeholderCommand = $db->prepare("
UPDATE comments
SET author = NULL,
content = 'Dieser Kommentar wurde gelöscht.'
WHERE author = :author
");
$placeholderCommand->execute([
":author" => $author
]);
$db->commit();
} catch (PDOException $e) {
if ($db->inTransaction()) {
$db->rollBack();
}
throw new RuntimeException("internal_error");
}
}
} }
+11 -11
View File
@@ -46,17 +46,17 @@ class DatabaseInitializer {
$db->exec(" $db->exec("
CREATE TABLE IF NOT EXISTS comments ( CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id INTEGER NOT NULL, article_id INTEGER NOT NULL,
parent_comment_id INTEGER NULL, parent_comment_id INTEGER NULL,
author TEXT NOT NULL, author TEXT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE, FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
FOREIGN KEY (author) REFERENCES users(email) ON DELETE CASCADE, FOREIGN KEY (author) REFERENCES users(email) ON DELETE SET NULL,
FOREIGN KEY (parent_comment_id) REFERENCES comments(id) ON DELETE CASCADE FOREIGN KEY (parent_comment_id) REFERENCES comments(id) ON DELETE CASCADE
); );
"); ");
$initializer = new self(); $initializer = new self();
$availableEmails = $initializer->seedDummyUsers($db); $availableEmails = $initializer->seedDummyUsers($db);