diff --git a/.idea/dataSources.local.xml b/.idea/dataSources.local.xml index cdc31de..707e2b3 100644 --- a/.idea/dataSources.local.xml +++ b/.idea/dataSources.local.xml @@ -1,6 +1,6 @@ - + " diff --git a/content/profile.php b/content/profile.php index aefdc10..65ce6c9 100644 --- a/content/profile.php +++ b/content/profile.php @@ -93,7 +93,7 @@ $isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error);
- +

Meine Beiträge

@@ -157,7 +157,9 @@ $isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error);
- +
@@ -180,10 +181,57 @@ $isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error); - - + +
+ + +
+ +

Meine Kommentare

+ + +
+ + 0): ?> + + +
+ + + + + +

+ getContent())); ?> +

+ + + + Zum Beitrag + + +
+ + + + +

Du hast noch keine Kommentare geschrieben.

+ + + +
+ +
+ + + + - \ No newline at end of file diff --git a/content/showArticle.php b/content/showArticle.php index 4457dcc..fc8502e 100644 --- a/content/showArticle.php +++ b/content/showArticle.php @@ -1,22 +1,46 @@ getCommentsByArticle($_GET["id"]); + + foreach ($comments as $comment) { + if ($comment->isReply()) { + $parentId = $comment->getParentCommentId(); + $repliesByParent[$parentId][] = $comment; + } else { + $mainComments[] = $comment; + } + } + } catch (Exception $e) { + $_SESSION["message"] = "internal_error"; + } +} ?> - + Seite: Anzeige für Beiträge + Funktion: Stellt einen übergebenen Beitrag dar. + --> +

Es ist ein interner Fehler aufgetreten. Bitte versuche es erneut.

+ -

- Es ist ein Fehler aufgetreten. Die ID konnte nicht ausgelesen werden. Bitte versuche es erneut. -

+

+ Es ist ein Fehler aufgetreten. Die ID konnte nicht ausgelesen werden. Bitte versuche es erneut. +

@@ -33,52 +57,138 @@ include_once 'php/controller/showArticle-controller.php'; unset($_SESSION["message"]); ?> - -
+ + + +
+

- Von: + + Von: +
-
+
-
+
+ +
- + +
Tags:
+
- - + + +
-
+
+

Kommentare

+
+ + +
+

+ getAuthor()); ?> + getCreated()); ?> +

+ +

getContent())); ?>

+ + + + + +
+ getId()])): ?> + getId()] as $reply): ?> +
+

+ getAuthor()); ?> + getCreated()); ?> +

+ +

getContent())); ?>

+
+ + +
+
+ + +

+ Noch keine Kommentare vorhanden. +

+ +
+ + +
+ "> + + + + + + + + +
+ + + +
+ \ No newline at end of file diff --git a/css/showArticle.css b/css/showArticle.css index ca3b40a..60eef65 100644 --- a/css/showArticle.css +++ b/css/showArticle.css @@ -116,3 +116,96 @@ font-size: 1.85rem; } } + +/* --- KOMMENTARE --- */ + +.article-comments-section { + margin-top: 3rem; + padding-top: 2rem; + border-top: 1px solid #e2e8f0; +} + +.article-comments-section h2 { + font-size: 2rem; + margin-bottom: 1.5rem; +} + +#comments-list { + margin-bottom: 2rem; +} + +.comment-item { + background-color: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 10px; + padding: 1rem 1.25rem; + margin-bottom: 1rem; +} + +#comment-form { + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; +} + +#comment-content { + width: 100%; + min-height: 130px; + padding: 1rem; + border: 1px solid #cbd5e1; + border-radius: 8px; + font-size: 1rem; + font-family: inherit; + resize: vertical; +} + +#comment-form .button { + width: 100%; +} + +.reply-button { + display: inline-block; + margin-top: 0.75rem; + background: none; + border: none; + color: #2563eb; + font-weight: 700; + cursor: pointer; + padding: 0; + font-size: 0.95rem; +} + +.reply-button:hover { + text-decoration: underline; +} + +.comment-replies { + margin-top: 1rem; + margin-left: 2rem; + padding-left: 1rem; + border-left: 3px solid #cbd5e1; +} + +.comment-reply { + background-color: #eef6ff; + margin-top: 1rem; +} + +.reply-info { + margin: 0.5rem 0; + color: #475569; + font-weight: 600; +} +.comment-login-hint { + margin-top: 2rem; + padding: 1.5rem; + background-color: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 10px; + text-align: center; +} + +.comment-login-hint p { + margin-bottom: 1rem; +} \ No newline at end of file diff --git a/index.php b/index.php index 1006322..c61be2c 100644 --- a/index.php +++ b/index.php @@ -53,6 +53,7 @@ if ($pfad === "deleteAccount") { + EduForge diff --git a/js/comments.js b/js/comments.js new file mode 100644 index 0000000..2d3258d --- /dev/null +++ b/js/comments.js @@ -0,0 +1,137 @@ +/** + * Initialisiert die Kommentarfunktion. + * + * Kommentare werden per AJAX gespeichert, + * ohne dass die Seite neu geladen werden muss. + */ +document.addEventListener("DOMContentLoaded", function () { + const form = document.getElementById("comment-form"); + const commentsList = document.getElementById("comments-list"); + const commentContent = document.getElementById("comment-content"); + const parentCommentInput = document.getElementById("parent-comment-id"); + const replyInfo = document.getElementById("reply-info"); + + if (!form || !commentsList || !commentContent || !parentCommentInput || !replyInfo) { + return; + } + + /** + * Aktiviert einen einzelnen Antworten-Button. + * + * @param {HTMLButtonElement} button Antworten-Button + */ + function registerReplyButton(button) { + button.addEventListener("click", function () { + parentCommentInput.value = button.dataset.commentId; + replyInfo.textContent = "Antwort auf " + button.dataset.author; + replyInfo.style.display = "block"; + commentContent.focus(); + }); + } + + /** + * Registriert alle bereits vorhandenen Antwort-Buttons. + */ + document.querySelectorAll(".reply-button").forEach(function (button) { + registerReplyButton(button); + }); + + /** + * Sendet Kommentare per AJAX an den Server. + */ + form.addEventListener("submit", function (event) { + event.preventDefault(); + + const formData = new FormData(form); + const parentCommentId = parentCommentInput.value; + + fetch("php/ajax/add-comment.php", { + method: "POST", + body: formData + }) + .then(response => response.json()) + .then(data => { + if (!data.success) { + alert(data.message); + return; + } + + const emptyMessage = commentsList.querySelector(".no-comments-message"); + + if (emptyMessage) { + emptyMessage.remove(); + } + + const commentElement = document.createElement("div"); + commentElement.classList.add("comment-item"); + + if (parentCommentId) { + commentElement.classList.add("comment-reply"); + + commentElement.innerHTML = ` +

+ ${escapeHtml(data.author)} + ${escapeHtml(data.created)} +

+

${escapeHtml(data.content).replace(/\n/g, "
")}

+ `; + + const parentReplies = document.querySelector( + `.comment-item[data-comment-id="${parentCommentId}"] .comment-replies` + ); + + if (parentReplies) { + parentReplies.appendChild(commentElement); + } + + } else { + commentElement.dataset.commentId = data.commentId; + + commentElement.innerHTML = ` +

+ ${escapeHtml(data.author)} + ${escapeHtml(data.created)} +

+

${escapeHtml(data.content).replace(/\n/g, "
")}

+ + + +
+ `; + + commentsList.prepend(commentElement); + + const newReplyButton = commentElement.querySelector(".reply-button"); + + if (newReplyButton) { + registerReplyButton(newReplyButton); + } + } + + commentContent.value = ""; + parentCommentInput.value = ""; + replyInfo.textContent = ""; + replyInfo.style.display = "none"; + }) + .catch(() => { + alert("Kommentar konnte nicht gesendet werden."); + }); + }); + + /** + * Entfernt HTML-Sonderzeichen aus Nutzereingaben. + * + * @param {string} text Zu bereinigender Text + * @returns {string} Sicherer Text + */ + function escapeHtml(text) { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } +}); \ No newline at end of file diff --git a/php/ajax/add-comment.php b/php/ajax/add-comment.php new file mode 100644 index 0000000..b2c14b9 --- /dev/null +++ b/php/ajax/add-comment.php @@ -0,0 +1,58 @@ + false, + "message" => "Du musst angemeldet sein, um zu kommentieren." + ]); + exit(); +} + +$articleId = $_POST["article_id"] ?? null; +$content = trim($_POST["content"] ?? ""); +$parentCommentId = $_POST["parent_comment_id"] ?? null; + +if ($parentCommentId === "" || $parentCommentId === "0") { + $parentCommentId = null; +} + +if (empty($articleId) || empty($content)) { + echo json_encode([ + "success" => false, + "message" => "Kommentar darf nicht leer sein." + ]); + exit(); +} + +try { + $commentManager = CommentManager::getInstance(); + + $commentId = $commentManager->addComment( + $articleId, + $_SESSION["user_email"], + $content, + $parentCommentId + ); + + echo json_encode([ + "success" => true, + "commentId" => $commentId, + "author" => $_SESSION["user_email"], + "content" => $content, + "created" => date("Y-m-d H:i:s"), + "parentCommentId" => $parentCommentId + ]); + +} catch (Exception $e) { + echo json_encode([ + "success" => false, + "message" => "Kommentar konnte nicht gespeichert werden." + ]); +} \ No newline at end of file diff --git a/php/controller/profile-controller.php b/php/controller/profile-controller.php index e183b20..9990fbc 100644 --- a/php/controller/profile-controller.php +++ b/php/controller/profile-controller.php @@ -3,6 +3,7 @@ require_once "php/model/UserManager.php"; require_once "php/model/Article.php"; require_once "php/model/ArticleManager.php"; +require_once "php/model/CommentManager.php"; require_once "php/validator/user-validator.php"; $error = null; @@ -76,6 +77,9 @@ try { $articleManager = ArticleManager::getInstance(); $userArticles = $articleManager->getArticlesByAuthor($_SESSION["user_email"]); + $commentManager = CommentManager::getInstance(); + $userComments = $commentManager->getCommentsByAuthor($_SESSION["user_email"]); + if (!isset($userArticles)) { $_SESSION["message"] = "user_has_no_articles"; } diff --git a/php/model/Comment.php b/php/model/Comment.php new file mode 100644 index 0000000..b92543b --- /dev/null +++ b/php/model/Comment.php @@ -0,0 +1,115 @@ +id = $id; + $this->articleId = $articleId; + $this->parentCommentId = $parentCommentId; + $this->author = $author; + $this->content = $content; + $this->created = $created; + } + + /** + * Gibt die ID des Kommentars zurück. + * + * @return int Kommentar-ID + */ + public function getId(): int + { + return $this->id; + } + + /** + * Gibt die ID des zugehörigen Beitrags zurück. + * + * @return int Beitrags-ID + */ + public function getArticleId(): int + { + return $this->articleId; + } + + /** + * Gibt die ID des Eltern-Kommentars zurück. + * + * @return int|null ID des Eltern-Kommentars oder null + */ + public function getParentCommentId(): ?int + { + return $this->parentCommentId; + } + + /** + * Gibt zurück, ob der Kommentar eine Antwort ist. + * + * @return bool true wenn der Kommentar eine Antwort ist, sonst false + */ + public function isReply(): bool + { + return $this->parentCommentId !== null && $this->parentCommentId !== 0; + } + + /** + * Gibt den Autor des Kommentars zurück. + * + * @return string Autor + */ + public function getAuthor(): string + { + return $this->author; + } + + /** + * Gibt den Inhalt des Kommentars zurück. + * + * @return string Kommentarinhalt + */ + public function getContent(): string + { + return $this->content; + } + + /** + * Gibt das Erstellungsdatum des Kommentars zurück. + * + * @return string Erstellungsdatum + */ + public function getCreated(): string + { + return $this->created; + } +} \ No newline at end of file diff --git a/php/model/CommentManager.php b/php/model/CommentManager.php new file mode 100644 index 0000000..6ca6cb4 --- /dev/null +++ b/php/model/CommentManager.php @@ -0,0 +1,25 @@ +getConnection(); + + $db->exec(" + CREATE TABLE IF NOT EXISTS comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + article_id INTEGER NOT NULL, + parent_comment_id INTEGER NULL, + author TEXT NOT NULL, + content TEXT NOT NULL, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + "); + + $columns = $db->query("PRAGMA table_info(comments);")->fetchAll(PDO::FETCH_ASSOC); + $hasParentColumn = false; + + foreach ($columns as $column) { + if ($column["name"] === "parent_comment_id") { + $hasParentColumn = true; + break; + } + } + + if (!$hasParentColumn) { + $db->exec("ALTER TABLE comments ADD COLUMN parent_comment_id INTEGER NULL;"); + } + + } catch (PDOException $e) { + throw new RuntimeException("internal_error"); + } + } + + /** + * Baut die Verbindung zur SQLite-Datenbank auf. + * + * @return PDO Datenbankverbindung + */ + private function getConnection() + { + try { + $dsn = 'sqlite:' . __DIR__ . '/../../db/comments.db'; + + $db = new PDO($dsn, null, null); + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + return $db; + + } catch (PDOException $e) { + throw new RuntimeException("internal_error"); + } + } + + /** + * Gibt die Singleton-Instanz zurück. + * + * @return DatabaseCommentManager + */ + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new DatabaseCommentManager(); + } + + return self::$instance; + } + + /** + * Speichert einen neuen Kommentar oder eine Antwort. + * + * @param int $articleId ID des Beitrags + * @param string $author Autor des Kommentars + * @param string $content Inhalt des Kommentars + * @param int|null $parentCommentId ID des Eltern-Kommentars oder null + * + * @return int ID des neu gespeicherten Kommentars + */ + public function addComment( + $articleId, + $author, + $content, + $parentCommentId = null + ) { + try { + $db = $this->getConnection(); + + if ($parentCommentId === "" || $parentCommentId === 0 || $parentCommentId === "0") { + $parentCommentId = null; + } + + $sql = " + INSERT INTO comments ( + article_id, + parent_comment_id, + author, + content + ) + VALUES ( + :articleId, + :parentCommentId, + :author, + :content + ) + "; + + $command = $db->prepare($sql); + + $command->execute([ + ":articleId" => $articleId, + ":parentCommentId" => $parentCommentId, + ":author" => $author, + ":content" => $content + ]); + + return intval($db->lastInsertId()); + + } catch (PDOException $e) { + throw new RuntimeException("internal_error"); + } + } + + /** + * Lädt alle Kommentare eines Beitrags. + * + * @param int $articleId ID des Beitrags + * + * @return Comment[] + */ + public function getCommentsByArticle($articleId) + { + try { + $db = $this->getConnection(); + + $sql = " + SELECT + id, + article_id, + CASE + WHEN parent_comment_id IS NULL THEN NULL + WHEN parent_comment_id = '' THEN NULL + WHEN parent_comment_id = 0 THEN NULL + ELSE parent_comment_id + END AS parent_comment_id, + author, + content, + created + FROM comments + WHERE article_id = :articleId + ORDER BY created ASC + "; + + $command = $db->prepare($sql); + $command->execute([":articleId" => $articleId]); + + return $this->mapRowsToComments($command); + + } catch (PDOException $e) { + throw new RuntimeException("internal_error"); + } + } + + /** + * Lädt alle Kommentare eines Autors. + * + * @param string $author E-Mail-Adresse des Autors + * + * @return Comment[] + */ + public function getCommentsByAuthor($author) + { + try { + $db = $this->getConnection(); + + $sql = " + SELECT + id, + article_id, + CASE + WHEN parent_comment_id IS NULL THEN NULL + WHEN parent_comment_id = '' THEN NULL + WHEN parent_comment_id = 0 THEN NULL + ELSE parent_comment_id + END AS parent_comment_id, + author, + content, + created + FROM comments + WHERE author = :author + ORDER BY created DESC + "; + + $command = $db->prepare($sql); + $command->execute([":author" => $author]); + + return $this->mapRowsToComments($command); + + } catch (PDOException $e) { + throw new RuntimeException("internal_error"); + } + } + + /** + * Wandelt Datenbankzeilen in Comment-Objekte um. + * + * @param PDOStatement $command Ausgeführtes Statement + * + * @return Comment[] + */ + private function mapRowsToComments($command) + { + $comments = []; + + while ($row = $command->fetch(PDO::FETCH_ASSOC)) { + $parentCommentId = null; + + if ( + isset($row["parent_comment_id"]) + && $row["parent_comment_id"] !== null + && $row["parent_comment_id"] !== "" + && intval($row["parent_comment_id"]) !== 0 + ) { + $parentCommentId = intval($row["parent_comment_id"]); + } + + $comments[] = new Comment( + intval($row["id"]), + intval($row["article_id"]), + $parentCommentId, + $row["author"], + $row["content"], + $row["created"] + ); + } + + return $comments; + } +} \ No newline at end of file