Compare commits

..

163 Commits

Author SHA1 Message Date
caroline.slt 09f8b11f36 Loader 2026-07-19 21:35:23 +02:00
caroline.slt cc4cebdb45 . 2026-07-19 21:16:54 +02:00
niklas.ortmann 96f38ce72e Merge pull request 'Suche nutzt nun Tags' (#59) from suchergebnisseTags into dev
Reviewed-on: #59
2026-07-19 16:29:40 +02:00
niklas.ortmann b81f0c9e5c Update DatabaseArticleManager.php 2026-07-19 16:28:29 +02:00
niklas.ortmann f8aed4283e Update dataSources.local.xml 2026-07-19 16:28:27 +02:00
niklas.ortmann afd90086d8 Update profile.php 2026-07-19 14:20:43 +02:00
niklas.ortmann a7a54f877c Merge pull request 'Beitragseditor funktioniert mit und ohne JS (+Bilder löschen)' (#57) from BeitragseditorOhneJS into dev
Reviewed-on: #57
2026-07-19 13:45:33 +02:00
NOrtmann1 9b9f71d21c Update DatabaseInitializer.php 2026-07-19 13:44:27 +02:00
NOrtmann1 4b84f0ab09 Update updateArticle-controller.php 2026-07-19 13:30:39 +02:00
NOrtmann1 8c8b148bf4 Update updateArticle-controller.php 2026-07-19 13:15:23 +02:00
NOrtmann1 6bf335ce46 Update createArticle.php 2026-07-19 13:15:20 +02:00
NOrtmann1 144a1015f1 Create article-block-helper.php 2026-07-19 13:15:17 +02:00
NOrtmann1 c0a94b2eee Update createArticle-controller.php 2026-07-19 13:15:14 +02:00
NOrtmann1 f1a32120d6 Update editor.js 2026-07-19 12:48:41 +02:00
NOrtmann1 97fa1de999 Update updateArticle-controller.php 2026-07-19 12:48:38 +02:00
NOrtmann1 7341fe7733 Update updateArticle.php 2026-07-19 12:48:33 +02:00
NOrtmann1 5b0528a914 Update index-controller.php 2026-07-19 10:56:05 +02:00
NOrtmann1 ac64e65dd3 Update index-controller.php 2026-07-19 10:54:33 +02:00
NOrtmann1 96ffa3288e Update dataSources.local.xml 2026-07-19 10:52:21 +02:00
NOrtmann1 0c6cf13efa merge-commit 2026-07-19 10:50:22 +02:00
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
niklas.ortmann d3884d2fc1 Merge branch 'PaginatorOhneJS2' into dev 2026-07-18 18:33:04 +02:00
niklas.ortmann 1575f4fa24 Merge pull request 'Deiteiincludes ($pfad) per whitelist prüfen' (#53) from AllowistDateiincludes into dev
Reviewed-on: #53
2026-07-18 18:31:55 +02:00
niklas.ortmann 0aca76d2f4 Update index.php 2026-07-18 18:26:42 +02:00
niklas.ortmann eca6646071 showCategory.js 2026-07-18 18:23:11 +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
niklas.ortmann 35e5006063 Update navbar.php 2026-07-18 17:39:14 +02:00
niklas.ortmann fcb9289d80 Update navbar.php 2026-07-18 17:38:27 +02:00
niklas.ortmann 6595a3d78d Update index.php 2026-07-18 17:36:52 +02:00
niklas.ortmann 41d8ac6721 Update navbar.php 2026-07-18 17:35:38 +02:00
niklas.ortmann 63c213bae7 Update navbar.php 2026-07-18 17:34:24 +02:00
niklas.ortmann 61d72436c9 Update navbar.php 2026-07-18 17:33:40 +02:00
niklas.ortmann 721bc8d833 Update navbar.php 2026-07-18 17:32:37 +02:00
niklas.ortmann 2666d2d14f Update navbar.php 2026-07-18 17:32:01 +02:00
niklas.ortmann de1ba88860 Update showCategory.php 2026-07-18 17:30:17 +02:00
niklas.ortmann e0fb04794d Update resultsHelper.php 2026-07-18 17:28:53 +02:00
caroline.slt 5310083e11 JavaScript Korrektur 2026-07-18 17:23:52 +02:00
niklas.ortmann 6897a29574 Update search-results-data.php 2026-07-18 17:23:30 +02:00
caroline.slt b564306fb3 JavaScript Korrektur 2026-07-18 17:20:14 +02:00
niklas.ortmann 8836034670 Kategoriepaginator 2026-07-18 17:17:44 +02:00
niklas.ortmann 98b088afc0 Update alertMessages.php 2026-07-18 17:00:31 +02:00
niklas.ortmann 51c850d122 Update 404.php 2026-07-18 16:59:47 +02:00
niklas.ortmann d590561ade refactoring 2026-07-18 16:59:23 +02:00
niklas.ortmann ba11c216bd refactoring 2026-07-18 16:56:41 +02:00
niklas.ortmann 71a1b029cb refactoring 2026-07-18 16:47:09 +02:00
niklas.ortmann 2126d076d5 Entfernung alter js 2026-07-18 16:47:01 +02:00
caroline.slt 7b7208ad6b Kommentare löschen 2026-07-18 16:44:05 +02:00
niklas.ortmann 2ecd446dd0 search-results.js
Ohne JS: search-results.php sortiert/filtert/paginiert komplett serverseitig anhand der GET-Parameter (?sort=&category=&page=) und cached dabei den Session-Datensatz — es wird keine neue Suche ausgelöst, nur der bestehende $_SESSION["search_results"]-Datensatz neu aufbereitet. Echte <a href>-Links für die Pagination, ein <noscript>-Submit-Button für Sortierung/Filter.
Mit JS: results-app.js lädt den kompletten Datensatz einmalig per fetch() von einem neuen schlanken JSON-Endpoint nach und übernimmt Sortierung, Filterung und Pagination danach zu 100% im Browser — keine weiteren Serveranfragen mehr, kein Page-Reload.
sorter.js, filter.js, paginator.js werden durch die eine Datei results-app.js ersetzt (löst den Button-Konflikt).
2026-07-18 16:35:13 +02:00
niklas.ortmann 97302f1bb3 Update search-results-controller.php 2026-07-18 16:22:03 +02:00
niklas.ortmann 35d4425086 Update index-controller.php 2026-07-18 16:19:48 +02:00
niklas.ortmann d3435d4cde Update index.php 2026-07-18 16:14:09 +02:00
niklas.ortmann f61c548b9e Update index.php 2026-07-18 16:11:53 +02:00
niklas.ortmann 101a2a247b Update index-controller.php 2026-07-18 16:08:05 +02:00
niklas.ortmann 423941e7ed Update index-controller.php 2026-07-18 16:06:35 +02:00
niklas.ortmann a8e76c782d Update index-controller.php 2026-07-18 15:53:33 +02:00
niklas.ortmann 3f9c048493 Update index.php 2026-07-18 15:53:31 +02:00
niklas.ortmann fee5a701df Update index-controller.php 2026-07-18 15:50:07 +02:00
niklas.ortmann d341312192 Update index-controller.php 2026-07-18 15:47:25 +02:00
niklas.ortmann 4dadfb8863 Update index.php 2026-07-18 15:47:23 +02:00
caroline.slt ada97ec538 Kommentare ohne js verfassen 2026-07-18 15:26:13 +02:00
niklas.ortmann 0b4c5f988d Merge pull request 'Anzeigenamen' (#51) from AnzeigeNamen into dev
Reviewed-on: #51
2026-07-17 18:09:29 +02:00
niklas.ortmann 59eeb05e4b Update showCategory.php 2026-07-17 18:05:33 +02:00
niklas.ortmann 5d8239822a Update showCategory.php 2026-07-17 18:04:03 +02:00
niklas.ortmann 31585511d0 Update showCategory-controller.php 2026-07-17 18:01:59 +02:00
niklas.ortmann 2ec1b70c91 Update Article.php 2026-07-17 17:59:17 +02:00
niklas.ortmann 8f8b25249c Update showCategory-controller.php 2026-07-17 17:56:31 +02:00
niklas.ortmann 8be6e6d61c Update showCategory-controller.php 2026-07-17 17:55:37 +02:00
niklas.ortmann bad0c08e6f Update search-results-controller.php 2026-07-17 17:51:58 +02:00
niklas.ortmann 55695f3f82 Update showArticle-controller.php 2026-07-17 17:42:18 +02:00
niklas.ortmann 74cbc60cbe Update showArticle.php 2026-07-17 17:41:42 +02:00
niklas.ortmann c09c7ecfa3 Update showArticle-controller.php 2026-07-17 17:41:41 +02:00
niklas.ortmann 944e332b49 Merge pull request 'Kategorieseitenpaginator' (#50) from KategorieseitenPaginator into dev
Reviewed-on: #50
2026-07-17 17:36:10 +02:00
niklas.ortmann 4306785be9 Merge pull request 'Beitrag Bearbeiten Autorenprüfung' (#49) from Beitrag-bearbeiten-Autorenpruefung into dev
Reviewed-on: #49
2026-07-17 17:36:05 +02:00
niklas.ortmann 266b70a094 Update home.php 2026-07-17 17:32:25 +02:00
niklas.ortmann bd8593d212 Update 404.php 2026-07-17 17:29:50 +02:00
niklas.ortmann 0d009262b6 Update showCategory.php 2026-07-17 17:27:16 +02:00
niklas.ortmann bb2c368b1f Seitenleiste 2026-07-17 17:20:54 +02:00
niklas.ortmann a712f5a993 Update showCategory.css 2026-07-17 17:17:38 +02:00
niklas.ortmann 53bee08833 Update showCategory.php 2026-07-17 17:16:42 +02:00
niklas.ortmann 7a2a3ac1e9 Update showCategory.php 2026-07-17 17:13:41 +02:00
niklas.ortmann a5f02c9a6c Update showCategory.php 2026-07-17 17:13:07 +02:00
niklas.ortmann f77f09ec6f Update showCategory.php 2026-07-17 17:11:40 +02:00
niklas.ortmann 535df85cff neue showCategory.css 2026-07-17 17:11:38 +02:00
niklas.ortmann 82e8c8a1a3 Update showCategory.php 2026-07-17 17:08:02 +02:00
niklas.ortmann c0d8fdede6 Update showCategory.php 2026-07-17 17:04:54 +02:00
niklas.ortmann 958ee1c1f5 Update showCategory.php
css
2026-07-17 17:02:11 +02:00
niklas.ortmann 1dd128d480 Update showCategory.php 2026-07-17 16:59:36 +02:00
niklas.ortmann dca716ddc5 Update showCategory-controller.php 2026-07-17 16:59:34 +02:00
niklas.ortmann c1dd4fce2b Update showCategory.php 2026-07-17 16:41:09 +02:00
niklas.ortmann 16cfa92721 Update showCategory.php 2026-07-17 16:29:11 +02:00
niklas.ortmann 3b2ed78c1c Update showCategory.php 2026-07-17 16:16:14 +02:00
niklas.ortmann cebbe8869a Update showArticle-controller.php 2026-07-17 15:59:29 +02:00
niklas.ortmann e07051b356 Update showArticle-controller.php 2026-07-17 15:58:10 +02:00
niklas.ortmann 2d2bbeb39f Update showArticle-controller.php 2026-07-17 15:57:15 +02:00
niklas.ortmann c00443b6fa Update showArticle-controller.php 2026-07-17 15:56:34 +02:00
niklas.ortmann 1d541e285c Update showArticle-controller.php 2026-07-17 15:54:47 +02:00
niklas.ortmann b96c588697 Merge branch 'DummyData' into dev 2026-07-17 15:03:15 +02:00
niklas.ortmann fde9c59f91 Merge branch 'Datenbanken_zusammenfuehren' into dev 2026-07-17 14:57:22 +02:00
niklas.ortmann 72c7363a07 Datenschutz: Cookies 2026-07-10 08:32:07 +02:00
niklas.ortmann 0b36050558 Update dataSources.local.xml 2026-07-10 08:27:22 +02:00
NOrtmann1 00dbf83da4 Merge branch 'Datenbanken_zusammenfuehren' of https://git.gigaworld.ddns.net/niklas.ortmann/webprogrammierung into Datenbanken_zusammenfuehren 2026-07-07 14:13:19 +02:00
NOrtmann1 c3e060b963 Update DatabaseInitializer.php 2026-07-07 14:13:11 +02:00
niklas.ortmann a382d9254e Update README.md 2026-07-01 19:33:56 +02:00
niklas.ortmann 2b7f44b817 Merge remote-tracking branch 'origin/datenschutzkonformeRegistrierung' into dev 2026-07-01 19:30:44 +02:00
niklas.ortmann b5e4f44252 Merge branch 'dev' of https://git.gigaworld.ddns.net/niklas.ortmann/webprogrammierung into dev 2026-07-01 19:19:52 +02:00
niklas.ortmann 8561b02a08 Update README.md 2026-07-01 19:19:43 +02:00
niklas.ortmann 01f4ff2d28 Update dataSources.local.xml 2026-07-01 19:19:41 +02:00
niklas.ortmann b7e4387373 Merge pull request 'Database - und LocalArticleManager angepasst, sodass nun auch nach tags gesucht wird.' (#47) from SearchByTags into dev
Reviewed-on: #47
Reviewed-by: niklas.ortmann <ortmann.niklas@yahoo.de>
2026-07-01 19:15:06 +02:00
niklas.ortmann e9a92eaec7 merge 2026-07-01 19:11:32 +02:00
niklas.ortmann 6aec353d50 Update DatabaseInitializer.php 2026-07-01 19:07:58 +02:00
rirat-0 7a4f2d738f Debugging 2026-07-01 00:00:46 +02:00
rirat-0 a0a469a9df Update der Suchfunktionen um nach tags zu suchen 2026-06-30 23:58:02 +02:00
niklas.ortmann 54209929fe Merge pull request 'Implementierung von Nutzungsbedingungen und Barrierefreiheitserklärung' (#46) from Nutzungsbedingungen&barrierefreiheit into dev
Reviewed-on: #46
Reviewed-by: niklas.ortmann <ortmann.niklas@yahoo.de>
2026-06-30 22:41:56 +02:00
rirat-0 5321534d71 anpassung der filterJS fuer ergebnislimits 2026-06-30 21:13:02 +02:00
caroline.slt 7cf542c270 Richtig Einbinden 2026-06-30 21:09:49 +02:00
caroline.slt c45d23a6b3 Korrektur 2026-06-30 21:07:53 +02:00
caroline.slt ab094429b7 Erste Implementierung 2026-06-30 21:04:24 +02:00
niklas.ortmann 15ea5ddbfb Merge pull request 'datenschutz und impressum wurden erstellt, die readme entsprechend angepasst' (#43) from ImpressumUndDatenschutz into dev
Reviewed-on: #43
Reviewed-by: niklas.ortmann <ortmann.niklas@yahoo.de>
2026-06-30 18:13:51 +02:00
niklas.ortmann b93ca321a2 Update DatabaseInitializer.php 2026-06-30 17:46:42 +02:00
niklas.ortmann 97847c76c4 Update DatabaseInitializer.php 2026-06-30 17:44:36 +02:00
niklas.ortmann 3dae5ee720 Update dataSources.local.xml 2026-06-30 17:42:53 +02:00
rirat-0 cbb662d159 testing 2026-06-30 16:16:14 +02:00
rirat-0 c5e7439541 rechtschreibung 2026-06-30 16:11:08 +02:00
rirat-0 a748d94305 datenschutz und impressum wurden ersten, die readme entsprechend angepasst 2026-06-30 16:08:26 +02:00
NOrtmann1 c2b7462307 Update DatabaseInitializer.php 2026-06-24 17:38:40 +02:00
NOrtmann1 cdbf81878b DummyErstellung in DatabaseInitializer 2026-06-24 17:36:28 +02:00
NOrtmann1 68ec6dedd7 dummy-Anpassungen 2026-06-24 17:29:33 +02:00
NOrtmann1 12782463d7 Dummy-Daten aus den Managern ausgelagert 2026-06-24 17:19:21 +02:00
NOrtmann1 21a935edf3 require_once 'DatabaseInitializer.php'; 2026-06-24 16:41:20 +02:00
NOrtmann1 3d65f79d2c DatabaseInitializer 2026-06-24 16:36:11 +02:00
NOrtmann1 ed31210e3a Fremdschlüssel hinzugefügt 2026-06-24 15:59:41 +02:00
NOrtmann1 5030caa029 DatabaseCommentManager: neue Datenbank 2026-06-24 15:28:02 +02:00
NOrtmann1 4a87152531 DatabaseUserManager: neue Datenbank 2026-06-24 15:26:49 +02:00
NOrtmann1 1401c3afe7 DatabaseArticleManager: neue Datenbank 2026-06-24 15:23:39 +02:00
rirat-0 17034c928b anpassung der search results fuer die filterung nach likes 2026-06-17 22:03:51 +02:00
rirat-0 395041fd44 anpassung, damit die like sortierung client seitig ist 2026-06-17 21:59:12 +02:00
rirat-0 c117c7e641 Merge branch 'dev' into SuchergebnisseJS 2026-06-17 21:48:58 +02:00
rirat-0 12ad9759cf aufraeumen 2 2026-06-17 20:42:31 +02:00
rirat-0 057639be7c anpassung der error message 2026-06-17 20:38:19 +02:00
rirat-0 87e7be1c10 Implementation des validators und korrektes einbinden 2026-06-17 20:30:45 +02:00
rirat-0 1db77346d0 Revert "Implementierung des validators"
This reverts commit 1e6ac0fcd9.
2026-06-17 20:14:59 +02:00
rirat-0 1e6ac0fcd9 Implementierung des validators 2026-06-17 20:14:07 +02:00
rirat-0 b3c74c2172 Debugging 8 2026-06-17 19:54:31 +02:00
rirat-0 0805958033 Anpassung der filter.js
es gab ein problem bei der sortierung, da nur 10 ergebnisse angezeigt werden. mit dieser anpassung koennen ergebnisse 'nachruecken'
2026-06-17 19:47:33 +02:00
rirat-0 5c0e978c93 Debugging 7 2026-06-17 19:38:00 +02:00
rirat-0 520298bae4 Debugging 6 2026-06-17 19:35:02 +02:00
rirat-0 969adc4836 Debugging 5 2026-06-17 19:33:09 +02:00
rirat-0 d9eca06197 Debugging 4 2026-06-17 19:30:11 +02:00
rirat-0 48f2e90e49 Debugging 3 2026-06-17 19:21:58 +02:00
rirat-0 358247a2a1 Filter funktion implementiert 2026-06-17 19:19:31 +02:00
rirat-0 de4d2fe5c0 sortieren nach likes nun moeglich 2026-06-17 17:58:40 +02:00
rirat-0 e1102eb7db Merge branch 'dev' into SuchergebnisseJS 2026-06-17 17:48:08 +02:00
rirat-0 a8df9590fd updater sorter.js fuer client seite 2026-06-17 15:38:59 +02:00
rirat-0 3e453e22ec verschieben der script aufrufe in die index.php 2026-06-17 15:31:05 +02:00
rirat-0 beeab0ec90 aufraeumen 2026-06-17 14:50:05 +02:00
rirat-0 9353a7eaaa Debugging 2 2026-06-17 14:48:33 +02:00
rirat-0 cac8f3046d Debugging 1 2026-06-17 14:45:01 +02:00
rirat-0 66eeac372c Anpassung der results seite und erstellung der script-datei 2026-06-17 14:41:05 +02:00
47 changed files with 3594 additions and 1024 deletions
+8
View File
@@ -13,6 +13,10 @@
- Per Klick auf das Logo gelangt man auf die Home-Seite. - Per Klick auf das Logo gelangt man auf die Home-Seite.
## Ausgelassene Teilaufgaben ## Ausgelassene Teilaufgaben
- Das Anerkennen der Datenschutzerklärung und das Einhalten der
Nutzungsbedingungen (bspw. was Rechte an eingestellten Texten und
hochgeladenen Fotos angeht) muss ein Nutzer bei der Registrierung via einer
Checkbox und dem Opt-In-Verfahren fehlt noch.
## Bekannte Fehler und Mängel ## Bekannte Fehler und Mängel
- Bitte auf die gesetzten TODO's achten. Wenn Inhalte fehlen, sind sie i.d.R. als TODO kommentiert. - Bitte auf die gesetzten TODO's achten. Wenn Inhalte fehlen, sind sie i.d.R. als TODO kommentiert.
@@ -23,6 +27,7 @@
- Bilder im Beitragseditor sollen zukünftig eine Bildunterschrift bekommen und größenverstellbar sein. - Bilder im Beitragseditor sollen zukünftig eine Bildunterschrift bekommen und größenverstellbar sein.
- Die Elemente eines Contents im Beitrag werden momentan stumpf untereinander aufgelistet. Soll später - Die Elemente eines Contents im Beitrag werden momentan stumpf untereinander aufgelistet. Soll später
sich responisve auch nebeneinander orientieren usw. sich responisve auch nebeneinander orientieren usw.
- Die Datenbank ist noch in die verschiedenen Manager unterteilt. Zukünftig werden alle Daten in einer Datenbank gespeichert.
## Besonderheiten des Projektes ## Besonderheiten des Projektes
- Es wurde AJAX verwendet, um asynchrone Erstellung von Kommentaren zu implementieren. Es ermöglicht dem Nutzer, einen - Es wurde AJAX verwendet, um asynchrone Erstellung von Kommentaren zu implementieren. Es ermöglicht dem Nutzer, einen
@@ -31,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
+2 -7
View File
@@ -4,17 +4,12 @@
--> -->
<main> <main>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "internal_error"): ?> <?php include_once "includes/alertMessages.php"?>
<p class="alert-message is-error">
Es ist ein interner Fehler beim Speichern aufgetreten. Bitte versuche es erneut.
</p>
<?php endif; ?>
<?php <?php
unset($_SESSION["message"]); unset($_SESSION["message"]);
?> ?>
<h1>404 - Seite nicht vorhanden</h1> <h1>404 - Seite nicht vorhanden</h1>
<p> <p>
Später im Projekt sollen über index.php?pfad= ... der Inhalt der index.php dynamisch gesetzt werden. Die Seite, nach der du suchst, existiert hier leider nicht. Entweder haben wir sie verlegt, ein hungriger Bug hat sie gefressen, oder du hast dich beim Tippen vertippt. (Wir schieben die Schuld einfach mal ganz unauffällig auf dich.)
Alle Content-Links der Navbar führen zunächst hier her. Exemplarisch wurden für die Fächer Informatik, Physik, Mathematik der Content als Seite erstellt und kann über die Navbar ausgekundschaftet werden.
</p> </p>
</main> </main>
+66
View File
@@ -0,0 +1,66 @@
<!--
Seite: Barrierefreiheitserklärung
Inhalt: Erklärung zur Barrierefreiheit der Plattform
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="../images/logos/logo_icon.ico">
<link rel="stylesheet" href="../css/main.css">
<link rel="stylesheet" href="../css/navbar.css">
<link rel="stylesheet" href="../css/footer.css">
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
?>
<main>
<h1>Barrierefreiheitserklärung</h1>
<p>
Wir bemühen uns, die Plattform EduForge möglichst barrierefrei zugänglich zu gestalten.
Die Website wurde im Rahmen eines Hochschulprojekts erstellt.
</p>
<h2>Stand der Vereinbarkeit</h2>
<p>
Diese Website ist teilweise barrierefrei. Einzelne Funktionen oder Inhalte können
noch nicht vollständig barrierefrei nutzbar sein.
</p>
<h2>Nicht barrierefreie Inhalte</h2>
<p>
Einzelne Bilder verfügen möglicherweise noch nicht über vollständige Alternativtexte.
Außerdem kann die Bedienung mit Tastatur oder Screenreader in einzelnen Bereichen
eingeschränkt sein.
</p>
<h2>Feedback und Kontakt</h2>
<p>
Sollten Ihnen Barrieren auf unserer Website auffallen, kontaktieren Sie uns bitte
über die im Impressum angegebenen Kontaktdaten.
</p>
<h2>Erstellungsdatum</h2>
<p>
Diese Erklärung wurde im Rahmen eines Hochschulprojekts erstellt.
</p>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
+57 -19
View File
@@ -4,12 +4,28 @@ if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login"); header("Location: index.php?pfad=login");
exit(); exit();
} }
// -------------------------------------------------------------------------
// Aktuelle Blockliste ermitteln: Session-Fallback (nach Validierungsfehler
// oder Zwischen-Submit "add_block"/"delete_block") oder leeres Array (Start).
// -------------------------------------------------------------------------
$rawContent = '[]';
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])) {
$rawContent = $_SESSION['old_content'];
unset($_SESSION['old_content']);
}
$blocks = json_decode($rawContent, true);
if (!is_array($blocks)) {
$blocks = [];
}
$blocks = array_values($blocks); // sequentielle Indizes 0..n-1 sicherstellen
?> ?>
<!-- <!--
Seite: Beitrag erstellen Seite: Beitrag erstellen
Inhalt: Formular für die Erstellung eines neuen Beitrags Inhalt: Formular für die Erstellung eines neuen Beitrags
--> -->
<form method="post" action="php/controller/createArticle-controller.php" id="editor-form" class="article-editor-scope.editor-container article-editor-scope editor-container"> <form method="post" action="php/controller/createArticle-controller.php" id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container">
<main class="editor-main"> <main class="editor-main">
<?php include_once "includes/alertMessages.php"?> <?php include_once "includes/alertMessages.php"?>
@@ -18,27 +34,49 @@ if (!isset($_SESSION["user"])) {
value="<?php echo htmlspecialchars($_SESSION['old_title'] ?? ''); unset($_SESSION['old_title']); ?>" value="<?php echo htmlspecialchars($_SESSION['old_title'] ?? ''); unset($_SESSION['old_title']); ?>"
placeholder="Titel hier eingeben" required> placeholder="Titel hier eingeben" required>
<!-- Hier werden die dynamischen divs via JavaScript eingefügt --> <!--
<div id="block-container"></div> Content-Blöcke: werden serverseitig als echte, benannte Formularfelder gerendert
(blocks[i][type], blocks[i][text] bzw. blocks[i][image]). Dadurch funktioniert das
Hinzufügen/Entfernen von Blöcken und der Bild-Upload auch ganz ohne JavaScript über
einen normalen Formular-Submit. Ist JavaScript aktiv, fängt js/editor.js diese
Submits ab und erledigt dieselbe Änderung lokal im DOM, ohne den Server zu belasten.
-->
<div id="block-container">
<?php foreach ($blocks as $i => $block): ?>
<?php
$blockType = $block['type'] ?? '';
if ($blockType !== 'text' && $blockType !== 'image') {
continue; // unbekannter/kaputter Block wird übersprungen
}
?>
<div class="editor-block article-editor-scope" data-index="<?php echo (int)$i; ?>">
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][type]" value="<?php echo htmlspecialchars($blockType); ?>">
<!-- Plus-Button und das Pop-up-Menü --> <?php if ($blockType === 'text'): ?>
<div id="add-block-control" class="article-editor-scope add-block-control"> <textarea name="blocks[<?php echo (int)$i; ?>][text]"
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button> placeholder="Schreibe deinen Textblock..."><?php echo htmlspecialchars($block['value'] ?? ''); ?></textarea>
<div id="block-popup" class="article-editor-scope block-popup hidden"> <?php else: /* image */ ?>
<button type="button" data-type="text">Textblock</button> <?php if (!empty($block['value'])): ?>
<button type="button" data-type="image">Bild einfügen</button> <img src="<?php echo htmlspecialchars($block['value']); ?>"
</div> class="block-image-preview"
style="max-width:200px;display:block;margin-top:10px;">
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][existing_image]" value="<?php echo htmlspecialchars($block['value']); ?>">
<?php endif; ?>
<input type="file" name="blocks[<?php echo (int)$i; ?>][image]" accept="image/*">
<?php endif; ?>
<button type="submit" name="editor_action" value="delete_block:<?php echo (int)$i; ?>" class="delete-block-btn">✕</button>
</div>
<?php endforeach; ?>
</div> </div>
<!-- Unsichtbares Textfeld, das die JSON-Daten hält und an den Controller postet --> <div id="add-block-control" class="article-editor-scope add-block-control">
<textarea id="content" name="content" style="display:none;"><?php <button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])){ <div id="block-popup" class="article-editor-scope block-popup">
echo htmlspecialchars($_SESSION['old_content']); <button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button>
unset($_SESSION['old_content']); <button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button>
} else { </div>
echo '[]'; // Standardmäßig ein leeres JSON-Array </div>
}
?></textarea>
</main> </main>
<!-- Seitenleiste --> <!-- Seitenleiste -->
+29
View File
@@ -155,6 +155,35 @@ Daten &ndash; von ihrer Speicherung abgesehen &ndash; nur mit Ihrer Einwilligung
Aus&uuml;bung oder Verteidigung von Rechtsanspr&uuml;chen oder zum Schutz der Rechte einer anderen Aus&uuml;bung oder Verteidigung von Rechtsanspr&uuml;chen oder zum Schutz der Rechte einer anderen
nat&uuml;rlichen oder juristischen Person oder aus Gr&uuml;nden eines wichtigen &ouml;ffentlichen Interesses der nat&uuml;rlichen oder juristischen Person oder aus Gr&uuml;nden eines wichtigen &ouml;ffentlichen Interesses der
Europ&auml;ischen Union oder eines Mitgliedstaats verarbeitet werden.</p> Europ&auml;ischen Union oder eines Mitgliedstaats verarbeitet werden.</p>
<h4>Cookies</h4>
<p>
Unsere Internetseiten verwenden so genannte „Cookies“. Cookies sind kleine Datenpakete und richten auf
Ihrem Endgerät keinen Schaden an. Sie werden entweder vorübergehend für die Dauer einer Sitzung
(Session-Cookies) oder dauerhaft (permanente Cookies) auf Ihrem Endgerät gespeichert. Session-Cookies
werden nach Ende Ihres Besuchs automatisch gelöscht. Permanente Cookies bleiben auf Ihrem Endgerät
gespeichert, bis Sie diese selbst löschen oder eine automatische Löschung durch Ihren Webbrowser erfolgt.
Cookies haben verschiedene Funktionen. Zahlreiche Cookies sind technisch notwendig, da bestimmte
Webseitenfunktionen ohne diese nicht funktionieren würden (z. B. die Anmeldung). Andere Cookies können zur
Auswertung des Nutzerverhaltens oder zu Werbezwecken verwendet werden.
Cookies, die zur Durchführung des elektronischen Kommunikationsvorgangs, zur Bereitstellung
bestimmter, von Ihnen erwünschter Funktionen oder zur Optimierung der
Website (z. B. Cookies zur Messung des Webpublikums) erforderlich sind (notwendige Cookies), werden auf
Grundlage von Art. 6 Abs. 1 lit. f DSGVO gespeichert, sofern keine andere Rechtsgrundlage angegeben wird.
Der Websitebetreiber hat ein berechtigtes Interesse an der Speicherung von notwendigen Cookies zur
technisch fehlerfreien und optimierten Bereitstellung seiner Dienste. Sofern eine Einwilligung zur
Speicherung von Cookies und vergleichbaren Wiedererkennungstechnologien abgefragt wurde, erfolgt die
Verarbeitung ausschließlich auf Grundlage dieser Einwilligung (Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1
TDDDG); die Einwilligung ist jederzeit widerrufbar.
Sie können Ihren Browser so einstellen, dass Sie über das Setzen von Cookies informiert werden und
Cookies nur im Einzelfall erlauben, die Annahme von Cookies für bestimmte Fälle oder generell ausschließen
sowie das automatische Löschen der Cookies beim Schließen des Browsers aktivieren. Bei der
Deaktivierung von Cookies kann die Funktionalität dieser Website eingeschränkt sein.
Sofern weitere Cookies und Dienste auf dieser Website eingesetzt werden, können Sie dies dieser
Datenschutzerklärung entnehmen.
Auf dieser Webseite werden jedoch nur technisch notwendige Cookies verwendet.
</p>
<p>Quelle: <a href="https://www.e-recht24.de">https://www.e-recht24.de</a></p> <p>Quelle: <a href="https://www.e-recht24.de">https://www.e-recht24.de</a></p>
</main> </main>
+3 -6
View File
@@ -11,12 +11,9 @@ include_once 'php/controller/home-controller.php';
<h1>Home</h1> <h1>Home</h1>
<p> <p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et Schön, dass du da bist. EduForge ist deine digitale Werkstatt für Lernerfolg. Wir unterstützen Schüler und
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet Studierende dabei, die passenden Lernmaterialien zu finden, Wissenslücken zu schließen und Gelerntes gezielt
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, zu vertiefen.
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no
sea takimata sanctus est Lorem ipsum dolor sit amet.
</p> </p>
<!-- Flexbox für aktive Kategorien --> <!-- Flexbox für aktive Kategorien -->
+48 -9
View File
@@ -25,16 +25,55 @@ include_once 'includes/navbar.php';
<main> <main>
<h1>Nutzungsbedingungen</h1> <main>
<p> <h1>Nutzungsbedingungen</h1>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet <p>
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, Diese Nutzungsbedingungen regeln die Nutzung der Plattform EduForge.
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, Mit der Registrierung und Nutzung der Website erkennen Nutzerinnen und Nutzer
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no die nachfolgenden Bedingungen an.
sea takimata sanctus est Lorem ipsum dolor sit amet. </p>
</p>
<h2>1. Nutzung der Plattform</h2>
<p>
EduForge dient dem Austausch von Lerninhalten, Beiträgen und Kommentaren.
Die Nutzung der Plattform darf ausschließlich im Rahmen der geltenden Gesetze erfolgen.
</p>
<h2>2. Verhalten der Nutzerinnen und Nutzer</h2>
<p>
Nutzerinnen und Nutzer verpflichten sich zu einem respektvollen Umgang miteinander.
Das Veröffentlichen von beleidigenden, diskriminierenden, rechtswidrigen oder
anderweitig unangemessenen Inhalten ist untersagt.
</p>
<h2>3. Veröffentlichung von Inhalten</h2>
<p>
Nutzerinnen und Nutzer sind für die von ihnen veröffentlichten Inhalte selbst verantwortlich.
Dies gilt insbesondere für Beiträge, Kommentare, Bilder und sonstige hochgeladene Dateien.
</p>
<h2>4. Urheberrechte</h2>
<p>
Es dürfen ausschließlich Inhalte veröffentlicht werden, an denen die Nutzerinnen und Nutzer
die erforderlichen Rechte besitzen. Durch das Einstellen von Inhalten versichern die
Nutzerinnen und Nutzer, keine Rechte Dritter zu verletzen.
</p>
<h2>5. Entfernung von Inhalten</h2>
<p>
Inhalte, die gegen diese Nutzungsbedingungen oder geltendes Recht verstoßen,
können vom Betreiber der Plattform entfernt werden.
</p>
<h2>6. Änderungen der Nutzungsbedingungen</h2>
<p>
Der Betreiber behält sich vor, diese Nutzungsbedingungen anzupassen, sofern dies
für den Betrieb oder die Weiterentwicklung der Plattform erforderlich ist.
</p>
</main>
</main> </main>
+1 -1
View File
@@ -84,7 +84,7 @@ $isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error);
<form action="php/controller/deleteAccount-controller.php" method="POST"> <form action="php/controller/deleteAccount-controller.php" method="POST">
<button type="submit" <button type="submit"
class="button" class="button"
onclick="return confirm('Möchtest du deinen Account wirklich unwiderruflich löschen?');"> onclick="return confirm('Möchtest du deinen Account und alle deine Beiträge wirklich unwiderruflich löschen?');">
Account löschen Account löschen
</button> </button>
</form> </form>
+117 -59
View File
@@ -2,20 +2,37 @@
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
require_once __DIR__ . '/../includes/resultsHelper.php';
$all_results = $_SESSION["search_results"] ?? []; $rawResults = $_SESSION["search_results"] ?? [];
$query = $_SESSION["search_query"] ?? ""; $query = $_SESSION["search_query"] ?? "";
$totalResultsCount = count($all_results);
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10; // Sortierung / Kategorie / Limit: GET-Parameter haben Vorrang, sonst
// 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;
// Gesamtseitenzahl // Serverseitig sortieren + filtern
$totalPages = max(1, ceil($totalResultsCount / $limit)); $filteredResults = sortSearchResults($rawResults, $currentSort);
$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;
@@ -23,62 +40,92 @@ if ($currentPage < 1) {
$currentPage = $totalPages; $currentPage = $totalPages;
} }
// Startpunkt im Array berechnen (Offset)
$offset = ($currentPage - 1) * $limit; $offset = ($currentPage - 1) * $limit;
// Nur die Ergebnisse für die aktuelle Seite ausschneiden // Dies ist die No-JS-Basisversion: nur die aktuelle Seite wird gerendert.
$results = array_slice($all_results, $offset, $limit); // Bei aktiviertem JS lädt js/results-app.js zusätzlich den kompletten
// Datensatz nach (ein einziger Request) und übernimmt Sortierung, Filter
// 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">
<!-- Sortierfuntion Box und Such Box--> <!-- Formular 1: startet eine NEUE Suche (löst immer einen Server-Request aus) -->
<form id="search-form-id" action="php/controller/search-results-controller.php" method="GET" class="s-res-sidebar-form"> <form action="php/controller/search-results-controller.php" method="GET" id="search-form-id" class="s-res-sidebar-form">
<!-- Dieses Feld hält die aktuelle Seitenzahl für den Submit bereit --> <div class="s-res-sidebar-box">
<input type="hidden" name="page" id="s-res-page-input" value="<?php echo $currentPage; ?>">
<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" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?> onchange="this.form.submit()"> <input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>>
<span>Alphabetisch</span> <span>Alphabetisch</span>
</label> </label>
<!-- Noch disabled, da likes noch nicht implementiert-->
<label class="s-res-filter-option"> <label class="s-res-filter-option">
<input type="radio" name="sort" value="likes" <?php echo $currentSort === 'likes' ? 'checked' : ''; ?> onchange="this.form.submit()"> <input type="radio" name="sort" value="likes" class="sort-radio" <?php echo $currentSort === 'likes' ? 'checked' : ''; ?>>
<span>Beliebtheit (Likes)</span> <span>Beliebtheit (Likes)</span>
</label> </label>
<label class="s-res-filter-option"> <label class="s-res-filter-option">
<input type="radio" name="sort" value="newest" <?php echo $currentSort === 'newest' ? 'checked' : ''; ?> onchange="this.form.submit()"> <input type="radio" name="sort" value="newest" class="sort-radio" <?php echo $currentSort === 'newest' ? 'checked' : ''; ?>>
<span>Neueste Beiträge</span> <span>Neueste Beiträge</span>
</label> </label>
<label class="s-res-filter-option"> <label class="s-res-filter-option">
<input type="radio" name="sort" value="oldest" <?php echo $currentSort === 'oldest' ? 'checked' : ''; ?> onchange="this.form.submit()"> <input type="radio" name="sort" value="oldest" class="sort-radio" <?php echo $currentSort === 'oldest' ? 'checked' : ''; ?>>
<span>Älteste Beiträge</span> <span>Älteste Beiträge</span>
</label> </label>
</div> </div>
</div> </div>
<div class="s-res-sidebar-box">
<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;">
<option value="all" <?php echo $currentCategory === 'all' ? 'selected' : ''; ?>>Alle Kategorien</option>
<?php foreach ($categories as $value => $label): ?>
<option value="<?php echo htmlspecialchars($value); ?>" <?php echo $currentCategory === strtolower($value) ? 'selected' : ''; ?>><?php echo htmlspecialchars($label); ?></option>
<?php endforeach; ?>
</select>
</div>
<noscript>
<button type="submit" class="nav__search-button">Filter anwenden</button>
</noscript>
</form> </form>
</aside> </aside>
@@ -87,16 +134,28 @@ $resultCount = count($results);
<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"><?php echo $totalResultsCount; ?> Treffer für Ihre Suchanfrage "<?php echo htmlspecialchars($query); ?>"</p> <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>
</div> </div>
<p
id="results-loading"
class="results-loading"
role="status"
aria-live="polite"
hidden
>
<span class="results-spinner" aria-hidden="true"></span>
Suchergebnisse werden geladen …
</p>
<!-- Ergebnisliste --> <!-- Ergebnisliste -->
<div class="s-res-list"> <div class="s-res-list">
<?php <?php if (!empty($results)): ?>
if (!empty($results)): ?>
<?php foreach ($results as $item): ?> <?php foreach ($results as $item):
<div class="s-res-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"> <div class="s-res-content">
<h2 class="s-res-item-title"> <h2 class="s-res-item-title">
<a href="index.php?pfad=showArticle&id=<?php echo $item['id']; ?>" class="s-res-link"> <a href="index.php?pfad=showArticle&id=<?php echo $item['id']; ?>" class="s-res-link">
@@ -107,57 +166,56 @@ $resultCount = count($results);
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($item['author']); ?></span></p> <p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($item['author']); ?></span></p>
<span class="s-res-likes"> <span class="s-res-likes">
❤️ <?php echo isset($item['likes']) && is_array($item['likes']) ? count($item['likes']) : 0; ?> ❤️ <?php echo $likesCount; ?>
</span> </span>
</div> </div>
</div> </div>
<div class="s-res-arrow">&rarr;</div> <div class="s-res-arrow">&rarr;</div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php <?php elseif (isset($_SESSION["message"]) && $_SESSION["message"] === "invalid_search_query"): ?>
elseif (isset($_SESSION["search_query"]) && $_SESSION["search_query"] !== "" && $resultCount === 0): ?> <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> <p>Keine Beiträge zu diesem Suchbegriff gefunden.</p>
<?php
elseif (isset($_SESSION["message"]) && $_SESSION["message"] == "missing_parameters"): ?>
<p>Bitte überprüfe deine Sucheingabe und versuche es erneut!</p>
<?php endif; ?> <?php endif; ?>
<?php <?php unset($_SESSION["message"]); ?>
unset($_SESSION["message"]);
?>
</div> </div>
<div class="s-res-pagination-footer"> <div class="s-res-pagination-footer">
<!-- Auswahl der Ergebnisse pro Seite --> <!-- Auswahl der Ergebnisse pro Seite. Gehört per form="..." logisch
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" class="s-res-limit-select" form="search-form-id" onchange="this.form.submit()"> <select id="s-res-per-page" name="limit" form="s-res-filter-form" 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>
<div class="s-res-page-navigation"> <nav class="s-res-page-navigation" aria-label="Seitennavigation">
<button type="button" class="s-res-page-btn" data-page="<?php echo $currentPage - 1; ?>" <?php echo $currentPage <= 1 ? 'disabled' : ''; ?>> <!-- No-JS-Fallback: -->
« <noscript>
</button> <?php echo renderNoJsPagination($currentPage, $totalPages, $query, $currentSort, $currentCategory, $limit); ?>
<!-- Dynamische Seitenzahlen --> </noscript>
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<button type="button" <!-- JS-Version: wird per search-results.js befüllt/eingeblendet: -->
class="s-res-page-btn <?php echo $i === $currentPage ? 's-res-page-btn-active' : ''; ?>" <div id="js-page-navigation" style="display:none;">
data-page="<?php echo $i; ?>"> <button type="button" class="s-res-page-btn" id="prev-page-btn">«</button>
<?php echo $i; ?> <span id="dynamic-page-numbers"></span>
</button> <button type="button" class="s-res-page-btn" id="next-page-btn">»</button>
<?php endfor; ?> </div>
<button type="button" class="s-res-page-btn" data-page="<?php echo $currentPage + 1; ?>" <?php echo $currentPage >= $totalPages ? 'disabled' : ''; ?>> </nav>
»
</button>
</div>
</div> </div>
</main> </main>
+323 -27
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>
@@ -45,8 +71,8 @@ include_once 'php/controller/showArticle-controller.php';
</h1> </h1>
<div class="article-view-meta"> <div class="article-view-meta">
<?php if (isset($author) && !empty($author)): ?> <?php if (isset($name) && !empty($name)): ?>
<span class="article-view-author">Von: <strong><?php echo htmlspecialchars($author); ?></strong></span> <span class="article-view-author">Von: <strong><?php echo htmlspecialchars($name); ?></strong></span>
<?php endif; ?> <?php endif; ?>
</div> </div>
@@ -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"
@@ -173,6 +457,18 @@ include_once 'php/controller/showArticle-controller.php';
<button type="submit" class="button"> <button type="submit" class="button">
Kommentar senden Kommentar senden
</button> </button>
<p
id="comment-loading"
class="comment-loading"
role="status"
aria-live="polite"
hidden
>
<span class="comment-spinner" aria-hidden="true"></span>
Kommentar wird gesendet …
</p>
</form> </form>
<?php else: ?> <?php else: ?>
<div class="comment-login-hint"> <div class="comment-login-hint">
+187 -24
View File
@@ -1,32 +1,195 @@
<?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'] ?? [];
$category = $_SESSION['category_name'] ?? ($_GET['category'] ?? '');
// 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" ?>
<main> <!-- Links: Seitenleiste für Suche (innerhalb der Kategorie) und Sortierung -->
<?php include_once "includes/alertMessages.php"?> <aside class="s-res-sidebar">
<h1><?php if (isset($category) && !empty($category)){ echo htmlspecialchars($category); } ?></h1> <!-- Sortiert/durchsucht die BEREITS geladenen Kategorie-Beiträge.
Ohne JS: echter Reload dieser Seite, aber ohne erneute Datenbank-Abfrage.
Mit JS: js/showCategory.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="cat-filter-form" class="s-res-sidebar-form">
<input type="hidden" name="pfad" value="showCategory">
<input type="hidden" name="category" value="<?php echo htmlspecialchars($category); ?>">
<div class="s-res-list"> <div class="s-res-sidebar-box">
<?php <h3 class="s-res-sidebar-title">In dieser Kategorie suchen</h3>
if (!empty($articles)): ?> <input type="search" id="cat-search-input" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50">
<?php foreach ($articles as $article): ?> </div>
<div class="s-res-item">
<div class="s-res-content"> <div class="s-res-sidebar-box">
<h2 class="s-res-item-title"> <h3 class="s-res-sidebar-title">Sortierung</h3>
<a href="index.php?pfad=showArticle&id=<?php echo $article->getId(); ?>" class="s-res-link"> <div class="s-res-filter-group">
<?php echo htmlspecialchars($article->getTitle()); ?> <label class="s-res-filter-option">
</a> <input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>>
</h2> <span>Alphabetisch</span>
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($article->getAuthor()); ?></span></p> </label>
</div> <label class="s-res-filter-option">
<div class="s-res-arrow">&rarr;</div> <input type="radio" name="sort" value="likes" class="sort-radio" <?php echo $currentSort === 'likes' ? 'checked' : ''; ?>>
<span>Beliebtheit (Likes)</span>
</label>
<label class="s-res-filter-option">
<input type="radio" name="sort" value="newest" class="sort-radio" <?php echo $currentSort === 'newest' ? 'checked' : ''; ?>>
<span>Neueste Beiträge</span>
</label>
<label class="s-res-filter-option">
<input type="radio" name="sort" value="oldest" class="sort-radio" <?php echo $currentSort === 'oldest' ? 'checked' : ''; ?>>
<span>Älteste Beiträge</span>
</label>
</div> </div>
<?php endforeach; ?> </div>
<?php
else: ?>
<p> Es sind noch keine Beiträge in dieser Kategorie enthalten.</p>
<?php endif; ?>
</div>
</main> <noscript>
<button type="submit" class="nav__search-button">Anwenden</button>
</noscript>
</form>
</aside>
<main class="s-res-main-content">
<div class="s-res-header">
<h1 class="s-res-main-title">Kategorie: <?php echo htmlspecialchars($category); ?></h1>
<p class="s-res-meta">
<span id="s-res-result-count"><?php echo $totalResultsCount; ?></span> Treffer in dieser Kategorie<?php echo $query !== '' ? ' für "' . htmlspecialchars($query) . '"' : ''; ?>
</p>
</div>
<p
id="results-loading"
class="results-loading"
role="status"
aria-live="polite"
hidden
>
<span class="results-spinner" aria-hidden="true"></span>
Kategoriebeiträge werden geladen …
</p>
<!-- 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="s-res-arrow">&rarr;</div>
</div>
<?php endforeach; ?>
<?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>
<nav class="s-res-page-navigation" aria-label="Seitennavigation">
<!-- No-JS-Fallback: -->
<noscript>
<?php echo renderNoJsPaginationCategory($currentPage, $totalPages, $category, $currentSort, $query, $limit); ?>
</noscript>
<!-- JS-Version: wird per js/showCategory.js befüllt/eingeblendet: -->
<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>
</main>
</div>
+57 -25
View File
@@ -5,10 +5,25 @@ if (!isset($_SESSION["user"])) {
exit(); exit();
} }
include_once 'php/controller/showArticle-controller.php'; include_once 'php/controller/showArticle-controller.php';
// Aktuelle Blockliste ermitteln:
$rawContent = '[]';
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])) {
$rawContent = $_SESSION['old_content'];
unset($_SESSION['old_content']);
} elseif (isset($content) && !empty($content)) {
$rawContent = $content;
}
$blocks = json_decode($rawContent, true);
if (!is_array($blocks)) {
$blocks = [];
}
$blocks = array_values($blocks);
?> ?>
<!-- <!--
Seite: Beitrag erstellen Seite: Beitrag bearbeiten
Inhalt: Formular für die Erstellung eines neuen Beitrags Inhalt: Formular für die Bearbeitung eines Beitrags
--> -->
<form method="post" action="php/controller/updateArticle-controller.php?id=<?php if(isset($id) && !empty($id)){echo htmlspecialchars($id);}else{$_SESSION["message"] = "missing_id";} ?>" id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container"> <form method="post" action="php/controller/updateArticle-controller.php?id=<?php if(isset($id) && !empty($id)){echo htmlspecialchars($id);}else{$_SESSION["message"] = "missing_id";} ?>" id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container">
@@ -27,32 +42,49 @@ include_once 'php/controller/showArticle-controller.php';
?>" ?>"
placeholder="Titel hier eingeben" required> placeholder="Titel hier eingeben" required>
<!-- Hier werden die dynamischen divs via JavaScript eingefügt --> <!--
<div id="block-container"></div> Content-Blöcke: werden serverseitig als echte, benannte Formularfelder gerendert
(blocks[i][type]...). Dadurch funktioniert das Hinzufügen/Entfernen von Blöcken und
der Bild-Upload auch ganz ohne JavaScript über einen normalen Formular-Submit.
Ist JavaScript aktiv, fängt js/editor.js diese Submits ab und erledigt dieselbe
Änderung lokal im DOM, ohne den Server zu belasten.
-->
<div id="block-container">
<?php foreach ($blocks as $i => $block): ?>
<?php
$blockType = $block['type'] ?? '';
if ($blockType !== 'text' && $blockType !== 'image') {
continue; // unbekannter/kaputter Block wird übersprungen
}
?>
<div class="editor-block article-editor-scope" data-index="<?php echo (int)$i; ?>">
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][type]" value="<?php echo htmlspecialchars($blockType); ?>">
<!-- Plus-Button und das Pop-up-Menü --> <?php if ($blockType === 'text'): ?>
<div id="add-block-control" class="article-editor-scope add-block-control"> <textarea name="blocks[<?php echo (int)$i; ?>][text]"
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button> placeholder="Schreibe deinen Textblock..."><?php echo htmlspecialchars($block['value'] ?? ''); ?></textarea>
<div id="block-popup" class="article-editor-scope block-popup hidden"> <?php else: /* image */ ?>
<button type="button" data-type="text">Textblock</button> <?php if (!empty($block['value'])): ?>
<button type="button" data-type="image">Bild einfügen</button> <img src="<?php echo htmlspecialchars($block['value']); ?>"
</div> class="block-image-preview"
style="max-width:200px;display:block;margin-top:10px;">
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][existing_image]" value="<?php echo htmlspecialchars($block['value']); ?>">
<?php endif; ?>
<input type="file" name="blocks[<?php echo (int)$i; ?>][image]" accept="image/*">
<?php endif; ?>
<button type="submit" name="editor_action" value="delete_block:<?php echo (int)$i; ?>" class="delete-block-btn">✕</button>
</div>
<?php endforeach; ?>
</div> </div>
<!-- Unsichtbares Textfeld, das die JSON-Daten hält und an den Controller postet --> <div id="add-block-control" class="article-editor-scope add-block-control">
<textarea id="content" name="content" style="display:none;"><?php <button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])){ <div id="block-popup" class="article-editor-scope block-popup">
echo htmlspecialchars($_SESSION['old_content']); <button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button>
unset($_SESSION['old_content']); <button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button>
}elseif (isset($content) && !empty($content)){ </div>
echo htmlspecialchars($content); </div>
} else {
echo '[]';
}
?></textarea>
<!-- unsichtbares Input, um die zu löschenden Bilder zu übergeben-->
<input type="hidden" id="deleted-images" name="deleted_images" value="[]">
</main> </main>
<!-- Seitenleiste --> <!-- Seitenleiste -->
+30
View File
@@ -226,3 +226,33 @@ h1 {
text-decoration: none; text-decoration: none;
box-sizing: border-box; box-sizing: border-box;
} }
.comment-loading {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
}
.comment-loading[hidden] {
display: none;
}
.comment-spinner {
width: 16px;
height: 16px;
border: 2px solid #cbd5e1;
border-top-color: #1f2937;
border-radius: 50%;
animation: comment-spinner-rotation 0.8s linear infinite;
}
@keyframes comment-spinner-rotation {
to {
transform: rotate(360deg);
}
}
#comment-form button[type="submit"]:disabled {
cursor: wait;
opacity: 0.65;
}
+29
View File
@@ -271,4 +271,33 @@ CSS für die Suchergebnis-Seite
align-items: flex-start; align-items: flex-start;
} }
} }
/* Ladeanzeige beim Nachladen von Such- und Kategorieergebnissen */
.results-loading {
display: flex;
align-items: center;
gap: 8px;
margin: 15px 0;
color: #4a5568;
font-size: 0.95rem;
}
.results-loading[hidden] {
display: none;
}
.results-spinner {
width: 16px;
height: 16px;
flex-shrink: 0;
border: 2px solid #cbd5e1;
border-top-color: #3182ce;
border-radius: 50%;
animation: results-spinner-rotation 0.8s linear infinite;
}
@keyframes results-spinner-rotation {
to {
transform: rotate(360deg);
}
}
+227
View File
@@ -0,0 +1,227 @@
/* --- ZWEISPALTIIGES GRID-LAYOUT --- */
.cat-view-container {
box-sizing: border-box;
max-width: 95%; /* 95% Monitorbreite wie das Original */
width: 95%;
margin: 2rem auto;
padding: 0 1rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: #212529;
line-height: 1.5;
/* Grid für die Zweispaltigkeit */
display: grid;
grid-template-columns: 320px 1fr; /* 320px Sidebar, Rest für Content */
gap: 2.5rem;
}
.cat-view-container * {
box-sizing: border-box;
}
/* --- SEITENLEISTE (SORTIERUNG) --- */
.cat-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.cat-sidebar-box {
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1.25rem;
}
.cat-sidebar-title {
font-size: 1rem;
font-weight: 700;
color: #1a202c;
margin: 0 0 1rem 0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.cat-filter-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.cat-filter-option {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-size: 0.95rem;
color: #4a5568;
}
.cat-filter-option input[type="radio"] {
margin: 0;
accent-color: #3182ce;
}
/* --- HAUPTINHALT --- */
.cat-view-main {
min-width: 0;
}
.cat-view-header {
margin-bottom: 2rem;
border-bottom: 2px solid #dee2e6;
padding-bottom: 1rem;
}
.cat-view-title {
font-size: 2rem;
color: #1a202c;
margin: 0 0 0.5rem 0;
font-weight: 700;
}
.cat-view-meta-text {
color: #6c757d;
font-size: 0.95rem;
margin: 0;
}
/* --- BEITRÄGE LISTE --- */
.cat-view-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.cat-view-item {
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.cat-view-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border-color: #cbd5e1;
}
.cat-view-content {
flex: 1;
}
.cat-view-item-title {
font-size: 1.25rem;
margin: 0 0 0.25rem 0;
font-weight: 600;
}
.cat-view-link {
color: #3182ce;
text-decoration: none;
}
.cat-view-link:hover {
text-decoration: underline;
color: #2b6cb0;
}
.cat-view-meta-row {
display: flex;
gap: 15px;
align-items: center;
margin-bottom: 0.5rem;
}
.cat-view-author {
font-size: 0.875rem;
color: #4a5568;
margin: 0;
}
.cat-view-author-name {
font-weight: 600;
color: #2d3748;
}
.cat-view-likes {
font-size: 0.9em;
color: #475569;
}
.cat-view-arrow {
font-size: 1.5rem;
color: #a0aec0;
padding-left: 1rem;
transition: color 0.2s ease, transform 0.2s ease;
}
.cat-view-item:hover .cat-view-arrow {
color: #3182ce;
transform: translateX(3px);
}
/* --- FOOTER PAGINATION --- */
.cat-view-pagination-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid #e2e8f0;
}
.cat-view-pagination-footer .s-res-page-navigation {
display: flex;
gap: 0.25rem;
}
.cat-view-pagination-footer .s-res-page-btn {
background-color: #ffffff;
border: 1px solid #cbd5e1;
color: #4a5568;
padding: 0.35rem 0.75rem;
font-size: 0.9rem;
font-weight: 500;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.cat-view-pagination-footer .s-res-page-btn:hover:not(:disabled) {
background-color: #f8f9fa;
border-color: #cbd5e1;
color: #2d3748;
}
.cat-view-pagination-footer .cat-active-btn {
background-color: #3182ce;
border-color: #3182ce;
color: #ffffff;
cursor: default;
}
.cat-view-pagination-footer .s-res-page-btn:disabled:not(.cat-active-btn) {
background-color: #f1f5f9;
border-color: #e2e8f0;
color: #94a3b8;
cursor: not-allowed;
}
/* RESPONSIVE ANPASSUNG (Unter 768px) */
@media (max-width: 768px) {
.cat-view-container {
grid-template-columns: 1fr; /* Stapelt Sidebar und Inhalt untereinander */
gap: 1.5rem;
margin: 1rem auto;
}
.cat-view-pagination-footer {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
}
+5 -5
View File
@@ -18,11 +18,6 @@
Ein Beitrag muss Inhalt besitzen. Text- und Bildelemente dürfen nicht leer sein! Ein Beitrag muss Inhalt besitzen. Text- und Bildelemente dürfen nicht leer sein!
</p> </p>
<?php endif; ?> <?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_category"): ?>
<p class="alert-message is-error">
Die ausgewählte Kategorie ist ungültig.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_tags"): ?> <?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_tags"): ?>
<p class="alert-message is-error"> <p class="alert-message is-error">
Ungültige Schlagworte gefunden. Erlaubt sind nur Buchstaben, Zahlen, Leerzeichen und Bindestriche (2-50 Zeichen). Ungültige Schlagworte gefunden. Erlaubt sind nur Buchstaben, Zahlen, Leerzeichen und Bindestriche (2-50 Zeichen).
@@ -89,6 +84,11 @@
Ihr Passwort wurde erfolgreich geändert. Sie können sich jetzt anmelden. Ihr Passwort wurde erfolgreich geändert. Sie können sich jetzt anmelden.
</p> </p>
<?php endif; ?> <?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "database_error"): ?>
<p class="alert-message is-error">
Es ist ein Datenbankfehler aufgetreten. Bitte versuche es erneut.
</p>
<?php endif; ?>
<?php <?php
unset($_SESSION["message"]); unset($_SESSION["message"]);
?> ?>
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* Baut die Blockliste aus den POST-Daten (blocks[i][type], blocks[i][text],
* blocks[i][existing_image]) und ggf. hochgeladenen Dateien (blocks[i][image])
* zusammen. Läuft bei JEDEM Submit (Zwischen-Schritt "Block hinzufügen/löschen"
* UND finales Speichern/Veröffentlichen), damit neu ausgewählte Bilder in jedem
* Fall persistiert werden, bevor PHP die temporäre Upload-Datei nach
* Request-Ende verwirft.
*
* Wird sowohl vom createArticle- als auch vom updateArticle-Controller genutzt.
*
* @param array $postBlocks $_POST['blocks'] ?? []
* @param array $fileBlocks $_FILES['blocks'] ?? []
* @param string $uploadDir absoluter Pfad zum uploads-Verzeichnis (mit trailing slash)
* @return array Liste von ['type' => 'text'|'image', 'value' => string]
*/
function rebuildBlocksFromPost(array $postBlocks, array $fileBlocks, string $uploadDir): array {
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$keys = array_keys($postBlocks);
if (isset($fileBlocks['name']) && is_array($fileBlocks['name'])) {
$keys = array_unique(array_merge($keys, array_keys($fileBlocks['name'])));
}
sort($keys, SORT_NUMERIC);
$blocks = [];
foreach ($keys as $key) {
$type = $postBlocks[$key]['type'] ?? null;
if ($type === 'text') {
$blocks[] = [
'type' => 'text',
'value' => $postBlocks[$key]['text'] ?? '',
];
} elseif ($type === 'image') {
// Vorbelegung: bereits vorhandenes Server-Bild (falls Datei nicht ersetzt wird)
$value = $postBlocks[$key]['existing_image'] ?? '';
$hasUpload = isset($fileBlocks['error'][$key]['image'])
&& $fileBlocks['error'][$key]['image'] === UPLOAD_ERR_OK;
if ($hasUpload) {
$tmpName = $fileBlocks['tmp_name'][$key]['image'];
$originalName = $fileBlocks['name'][$key]['image'];
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($extension, $allowedExtensions, true)) {
$extension = 'jpg';
}
$fileName = 'img_' . uniqid() . '.' . $extension;
$destination = $uploadDir . $fileName;
if (move_uploaded_file($tmpName, $destination)) {
$value = 'uploads/' . $fileName;
}
// Bei Fehler: alter Wert (falls vorhanden) bleibt erhalten, Block wird nicht verworfen
}
$blocks[] = [
'type' => 'image',
'value' => $value,
];
}
// unbekannter/fehlender type -> Block wird ignoriert
}
return $blocks;
}
+1
View File
@@ -2,4 +2,5 @@
<a href="index.php?pfad=impressum">Impressum</a> <a href="index.php?pfad=impressum">Impressum</a>
<a href="index.php?pfad=datenschutz">Datenschutz</a> <a href="index.php?pfad=datenschutz">Datenschutz</a>
<a href="index.php?pfad=nutzungsbedingungen">Nutzungsbedingungen</a> <a href="index.php?pfad=nutzungsbedingungen">Nutzungsbedingungen</a>
<a href="index.php?pfad=accessibility">Barrierefreiheit</a>
</footer> </footer>
+171
View File
@@ -0,0 +1,171 @@
<?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;
}
+36 -82
View File
@@ -2,95 +2,49 @@
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
ob_start(); include_once "php/controller/index-controller.php";
$pfad = $_GET["pfad"] ?? "home";
/*
Controller für Aktionen werden vor der HTML-Ausgabe geladen,
damit Weiterleitungen mit header() funktionieren.
*/
if ($pfad === "login") {
include_once "php/controller/login-controller.php";
}
if ($pfad === "register") {
include_once "php/controller/register-controller.php";
}
if ($pfad === "password-forgotten") {
include_once "php/controller/password-forgotten-controller.php";
}
if ($pfad === "confirm-register") {
include_once "php/controller/confirm-register-controller.php";
}
if ($pfad === "confirm-password") {
include_once "php/controller/confirm-password-controller.php";
}
if ($pfad === "logout") {
include_once "php/controller/logout-controller.php";
exit();
}
if ($pfad === "deleteAccount") {
include_once "php/controller/deleteAccount-controller.php";
exit();
}
?> ?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<!-- <link rel="stylesheet" href="css/main.css">
Seite: Index der Lernplattform <link rel="stylesheet" href="css/navbar.css">
Funktion: Webseitengerüst, Anzeigen von Content <link rel="stylesheet" href="css/footer.css">
--> <link rel="stylesheet" href="css/search-results.css">
<!DOCTYPE html> <link rel="stylesheet" href="css/createArticle.css">
<html lang="de"> <link rel="stylesheet" href="css/profile.css">
<link rel="stylesheet" href="css/showArticle.css">
<link rel="stylesheet" href="css/message.css">
<link rel="stylesheet" href="css/showCategory.css">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<link rel="stylesheet" href="css/search-results.css">
<link rel="stylesheet" href="css/createArticle.css">
<link rel="stylesheet" href="css/profile.css">
<link rel="stylesheet" href="css/showArticle.css">
<link rel="stylesheet" href="css/message.css">
<script src="js/paginator.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/showCategory.js" async></script>
<title>EduForge</title>
</head>
<body> <title>EduForge</title>
</head>
<?php <body>
include_once 'includes/navbar.php';
/*
Dynamischer Inhalt:
Je nach pfad-Parameter wird die passende Datei aus content geladen.
*/
if (file_exists('content/' . $pfad . '.php')) {
include_once 'content/' . $pfad . '.php';
} else {
include_once 'content/404.php';
}
include_once 'includes/footer.php';
?>
</body>
</html>
<?php <?php
ob_end_flush(); include_once 'includes/navbar.php';
// Dynamischer Inhalt
if (isset($pfad) && $pfad !== "404" && file_exists('content/' . $pfad . '.php')) {
include_once 'content/' . $pfad . '.php';
} else {
include_once 'content/404.php';
}
include_once 'includes/footer.php';
?> ?>
</body>
</html>
+229 -43
View File
@@ -11,20 +11,43 @@ 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) { const submitButton = form
? form.querySelector('button[type="submit"]')
: null;
const loadingMessage = document.getElementById("comment-loading");
if (
!form
|| !commentsList
|| !commentContent
|| !parentCommentInput
|| !submitButton
) {
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();
}); });
} }
@@ -42,21 +65,49 @@ document.addEventListener("DOMContentLoaded", function () {
form.addEventListener("submit", function (event) { form.addEventListener("submit", function (event) {
event.preventDefault(); event.preventDefault();
/*
* Verhindert einen erneuten Submit, während der vorherige
* Kommentar noch gespeichert wird.
*/
if (submitButton.disabled) {
return;
}
const originalButtonText = submitButton.textContent;
submitButton.disabled = true;
submitButton.textContent = "Wird gesendet …";
if (loadingMessage) {
loadingMessage.hidden = false;
}
const formData = new FormData(form); const 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 +115,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,23 +289,35 @@ 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.");
})
.finally(function () {
submitButton.disabled = false;
submitButton.textContent = originalButtonText;
if (loadingMessage) {
loadingMessage.hidden = true;
}
}); });
}); });
/** /**
* 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;
} }
}); });
+82 -121
View File
@@ -1,172 +1,133 @@
console.log("Die JavaScript-Datei wurde erfolgreich geladen!"); //console.log("editor.js wurde erfolgreich geladen!");
function initEditor() { function initEditor() {
const form = document.getElementById("editor-form"); const form = document.getElementById("editor-form");
if (!form) { if (!form) {
console.error("Skript abgebrochen: Formular nicht gefunden!"); console.error("editor.js abgebrochen: Formular nicht gefunden!");
return; return;
} else { } else {
console.log("Formular gefunden und Editor initialisiert:", form); console.log("Formular gefunden und editor.js initialisiert:", form);
} }
const container = document.getElementById("block-container"); const container = document.getElementById("block-container");
const plusButton = document.getElementById("plus-button"); const plusButton = document.getElementById("plus-button");
const popup = document.getElementById("block-popup"); const popup = document.getElementById("block-popup");
const hiddenContentInput = document.getElementById("content");
const initialImages = []; // Fortlaufender Zähler für eindeutige Block-Indizes. Wird nie wiederverwendet
// (auch nicht nach dem Löschen eines Blocks), damit sich neue und übrig
// gebliebene Blöcke beim finalen Submit nie einen Namen teilen.
let blockIndex = container.querySelectorAll(".editor-block").length;
// Ohne JS sind Textblock-/Bild-Button im Pop-up immer sichtbar und ganz normale
// Submit-Buttons. Erst mit JS blenden wir das Pop-up standardmäßig aus und
// steuern die Sichtbarkeit über den Plus-Button.
popup.classList.add("hidden");
// Pop-up umschalten bei Klick auf das Plus
plusButton.addEventListener("click", () => { plusButton.addEventListener("click", () => {
popup.classList.toggle("hidden"); popup.classList.toggle("hidden");
}); });
// Klick auf eine Block-Option im Pop-up // Klick auf "Textblock" / "Bild einfügen": lokal im DOM anlegen statt zum
popup.querySelectorAll("button").forEach(btn => { // Server zu submitten (entlastet den Server, kein Page-Reload nötig).
btn.addEventListener("click", function() { popup.querySelectorAll('[name="editor_action"]').forEach(btn => {
btn.addEventListener("click", function (e) {
e.preventDefault();
const type = this.getAttribute("data-type"); const type = this.getAttribute("data-type");
addBlockElement(type, ""); addBlockElement(type, "");
popup.classList.add("hidden"); popup.classList.add("hidden");
}); });
}); });
// Erstellt ein visuelles HTML-Element im Editor // Bereits vom Server gerenderte Blöcke (z.B. beim Bearbeiten eines bestehenden
// Artikels oder nach einem Validierungsfehler) ebenfalls mit JS-Verhalten ausstatten.
container.querySelectorAll(".editor-block").forEach(blockDiv => {
bindDeleteButton(blockDiv);
bindImageInput(blockDiv);
});
// Erstellt einen neuen Block inkl. echter, benannter Formularfelder
// (blocks[i][type], blocks[i][text] bzw. blocks[i][image]). Diese Felder werden
// beim finalen Submit ganz normal vom Browser als multipart/form-data verschickt
// es ist kein manuelles Zusammenbauen von JSON beim Absenden mehr nötig.
function addBlockElement(type, value = "") { function addBlockElement(type, value = "") {
const index = blockIndex++;
const blockDiv = document.createElement("div"); const blockDiv = document.createElement("div");
blockDiv.classList.add("editor-block"); blockDiv.classList.add("editor-block", "article-editor-scope");
blockDiv.setAttribute("data-type", type); blockDiv.setAttribute("data-index", String(index));
// Wenn es ein existierendes Server-Bild beim Laden ist, Pfad im globalen Array sichern const typeInput = document.createElement("input");
if (type === "image" && value && typeof value === 'string' && value.startsWith('uploads/')) { typeInput.type = "hidden";
initialImages.push(value); typeInput.name = `blocks[${index}][type]`;
blockDiv.setAttribute("data-value", value); typeInput.value = type;
} blockDiv.appendChild(typeInput);
// Löschen-Button
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.innerHTML = "✕";
deleteBtn.classList.add("delete-block-btn");
deleteBtn.addEventListener("click", () => {
// ANPASSUNG 2B: Logik hier komplett geleert. Das '✕' entfernt den Block jetzt nur noch sicher aus dem HTML.
blockDiv.remove();
});
blockDiv.appendChild(deleteBtn);
if (type === "text") { if (type === "text") {
const textarea = document.createElement("textarea"); const textarea = document.createElement("textarea");
textarea.name = `blocks[${index}][text]`;
textarea.placeholder = "Schreibe deinen Textblock..."; textarea.placeholder = "Schreibe deinen Textblock...";
textarea.value = value; textarea.value = value;
blockDiv.appendChild(textarea); blockDiv.appendChild(textarea);
} else if (type === "image") { } else if (type === "image") {
const fileInput = document.createElement("input"); const fileInput = document.createElement("input");
fileInput.type = "file"; fileInput.type = "file";
fileInput.name = `blocks[${index}][image]`;
fileInput.accept = "image/*"; fileInput.accept = "image/*";
const imgPreview = document.createElement("img");
imgPreview.style.maxWidth = "200px";
imgPreview.style.display = "block";
imgPreview.style.marginTop = "10px";
if (value && typeof value === 'string') {
if (value.startsWith('uploads/') || value.startsWith('data:image/')) {
imgPreview.src = value;
blockDiv.setAttribute("data-value", value);
}
}
fileInput.addEventListener("change", function() {
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
imgPreview.src = e.target.result;
blockDiv.setAttribute("data-value", e.target.result);
}
reader.readAsDataURL(this.files[0]);
}
});
blockDiv.appendChild(fileInput); blockDiv.appendChild(fileInput);
blockDiv.appendChild(imgPreview);
} }
const deleteBtn = document.createElement("button");
deleteBtn.type = "submit";
deleteBtn.name = "editor_action";
deleteBtn.value = `delete_block:${index}`;
deleteBtn.classList.add("delete-block-btn");
deleteBtn.innerHTML = "✕";
blockDiv.appendChild(deleteBtn);
container.appendChild(blockDiv); container.appendChild(blockDiv);
bindDeleteButton(blockDiv);
bindImageInput(blockDiv);
} }
// beim Abschicken verbleibende Blöcke auslesen UND gelöschte Bilder ermitteln // Löschen-Button eines Blocks lokal abfangen: entfernt den Block nur aus dem DOM,
form.addEventListener("submit", function(e) { // statt das Formular zum Server zu senden.
const blocks = []; function bindDeleteButton(blockDiv) {
const currentImages = []; const btn = blockDiv.querySelector(".delete-block-btn");
if (!btn) return;
// alle aktuell im Formular verbliebenen Blöcke scannen btn.addEventListener("click", (e) => {
container.querySelectorAll(".editor-block").forEach(blockDiv => { e.preventDefault();
const type = blockDiv.getAttribute("data-type"); blockDiv.remove();
let value = "";
if (type === "text") {
value = blockDiv.querySelector("textarea").value;
} else if (type === "image") {
const imgTag = blockDiv.querySelector("img");
if (imgTag) {
const srcValue = imgTag.getAttribute("src") || "";
// Wenn es ein neues Bild ist, nutzen wir das data-value (Base64)
if (srcValue.startsWith('data:image/')) {
value = blockDiv.getAttribute("data-value") || "";
} else {
value = srcValue;
}
}
// Pfade sammeln, die der Nutzer NICHT gelöscht hat (für den Abgleich)
if (value && value.startsWith('uploads/')) {
currentImages.push(value);
}
}
blocks.push({ type: type, value: value });
}); });
}
// das reguläre unsichtbare Content-Feld befüllen // Zeigt bei Auswahl einer Bilddatei sofort eine Vorschau an. Rein optisch
hiddenContentInput.value = JSON.stringify(blocks); // der eigentliche Datei-Upload läuft nativ über das <input type="file">.
function bindImageInput(blockDiv) {
const fileInput = blockDiv.querySelector('input[type="file"]');
if (!fileInput) return;
// Differenz berechnen: Welche Bilder aus 'initialImages' fehlen in 'currentImages' ? let imgPreview = blockDiv.querySelector(".block-image-preview");
const deletedImages = initialImages.filter(img => !currentImages.includes(img)); if (!imgPreview) {
imgPreview = document.createElement("img");
// das 'deleted_images'-Feld dynamisch befüllen und an den Controller senden imgPreview.classList.add("block-image-preview");
let deletedInput = document.getElementById("deleted-images"); imgPreview.style.maxWidth = "200px";
if (!deletedInput) { imgPreview.style.display = "none";
deletedInput = document.createElement("input"); imgPreview.style.marginTop = "10px";
deletedInput.type = "hidden"; blockDiv.insertBefore(imgPreview, fileInput.nextSibling);
deletedInput.id = "deleted-images";
deletedInput.name = "deleted_images";
form.appendChild(deletedInput);
} }
deletedInput.value = JSON.stringify(deletedImages);
});
// Existierende Blöcke laden (stellt alte Daten aus der Session wieder her) fileInput.addEventListener("change", function () {
try { if (this.files && this.files[0]) {
const initialBlocks = JSON.parse(hiddenContentInput.value.trim()); const reader = new FileReader();
if (Array.isArray(initialBlocks)) { reader.onload = (e) => {
initialBlocks.forEach(b => { imgPreview.src = e.target.result;
if (b.type === "image" && b.value && typeof b.value === 'string' && !b.value.startsWith('data:image/')) { imgPreview.style.display = "block";
let cleanPath = b.value.trim().replace(/\\\//g, '/'); // Verwandelt \/ in / };
reader.readAsDataURL(this.files[0]);
initialImages.push(cleanPath); }
addBlockElement(b.type, cleanPath); });
} else {
addBlockElement(b.type, b.value);
}
});
console.log("Erfolgreich registrierte Start-Bilder:", initialImages);
}
} catch(e) {
if (hiddenContentInput.value.trim() !== "") {
addBlockElement("text", hiddenContentInput.value);
}
} }
} }
-27
View File
@@ -1,27 +0,0 @@
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();
}
+257
View File
@@ -0,0 +1,257 @@
/**
* Ü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;
let loadingElement;
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');
loadingElement = document.getElementById('results-loading');
const checkedRadio = document.querySelector('.sort-radio:checked');
state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
state.category = categorySelect ? categorySelect.value : 'all';
state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
if (loadingElement) {
loadingElement.hidden = false;
}
fetchFullDataset()
.then(function (data) {
state.allItems = data.results || [];
attachEvents();
render();
if (jsNav) {
jsNav.style.display = 'flex';
}
})
.catch(function (err) {
/*
* Falls das Nachladen nicht funktioniert, bleibt die bereits
* serverseitig ausgegebene Ergebnisliste sichtbar.
*/
console.error(
'Suchergebnisse konnten nicht nachgeladen werden:',
err
);
})
.finally(function () {
if (loadingElement) {
loadingElement.hidden = true;
}
});
}
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();
}
})();
+261
View File
@@ -0,0 +1,261 @@
/**
* Ü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 loadingElement;
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');
loadingElement = document.getElementById('results-loading');
const checkedRadio = document.querySelector('.sort-radio:checked');
state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
state.query = searchInput ? searchInput.value : '';
state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
if (loadingElement) {
loadingElement.hidden = false;
}
fetchFullDataset()
.then(function (data) {
state.allItems = data.results || [];
attachEvents();
render();
if (jsNav) {
jsNav.style.display = 'flex';
}
})
.catch(function (err) {
/*
* Falls das Nachladen nicht funktioniert, bleibt die bereits
* serverseitig ausgegebene Kategorieansicht sichtbar.
*/
console.error(
'Kategorie-Beiträge konnten nicht nachgeladen werden:',
err
);
})
.finally(function () {
if (loadingElement) {
loadingElement.hidden = true;
}
});
}
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();
}
})();
+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
);
} }
+100 -114
View File
@@ -5,134 +5,120 @@ if (session_status() === PHP_SESSION_NONE) {
require_once '../model/LocalArticleManager.php'; require_once '../model/LocalArticleManager.php';
require_once '../model/ArticleManager.php'; require_once '../model/ArticleManager.php';
require_once '../validator/article-validator.php'; require_once '../validator/article-validator.php';
require_once '../../includes/article-block-helper.php';
if (!isset($_SESSION["user"])) { if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login"); header("Location: index.php?pfad=login");
exit(); exit();
} }
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$_SESSION["old_title"] = $_POST["title"] ?? '';
$_SESSION["old_content"] = $_POST["content"] ?? '';
$_SESSION["old_category"] = $_POST["category"] ?? '';
$_SESSION["old_tags"] = $_POST["tags"] ?? '';
if(!isset($_POST["title"]) ||!isset($_POST["content"]) || !isset($_POST["category"])){ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$_SESSION["message"] = "missing_parameters";
$uploadDir = __DIR__ . '/../../uploads/';
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// Formularzustand (Titel/Tags/Kategorie/Blöcke) immer sichern, damit er nach einem
// Redirect (PRG-Pattern oder Validierungsfehler) wieder angezeigt werden kann.
$_SESSION["old_title"] = $_POST["title"] ?? '';
$_SESSION["old_category"] = $_POST["category"] ?? '';
$_SESSION["old_tags"] = $_POST["tags"] ?? '';
$blocks = rebuildBlocksFromPost($_POST['blocks'] ?? [], $_FILES['blocks'] ?? [], $uploadDir);
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
// ---------------------------------------------------------------------
// Zwischenspeichern
// ---------------------------------------------------------------------
if (isset($_POST['editor_action']) && $_POST['editor_action'] !== '') {
$action = $_POST['editor_action'];
if ($action === 'add_text') {
$blocks[] = ['type' => 'text', 'value' => ''];
} elseif ($action === 'add_image') {
$blocks[] = ['type' => 'image', 'value' => ''];
} elseif (str_starts_with($action, 'delete_block:')) {
$deleteIndex = (int) substr($action, strlen('delete_block:'));
unset($blocks[$deleteIndex]);
$blocks = array_values($blocks);
}
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
header("location: ../../index.php?pfad=createArticle");
exit();
}
// ---------------------------------------------------------------------
// Echtes Veröffentlichen
// ---------------------------------------------------------------------
if (!isset($_POST["title"]) || !isset($_POST["category"])) {
$_SESSION["message"] = "missing_parameters";
header("location: ../../index.php?pfad=createArticle");
exit();
} else {
$title = $_POST["title"];
$content = json_encode($blocks, JSON_UNESCAPED_UNICODE);
$author = $_SESSION["user_email"];
$category = $_POST["category"];
$tags = $_POST['tags'] ?? '';
// -------------------------------- Validierung der Daten: -------------------------
if (!articleTitleValidator($title)) {
$_SESSION["message"] = "invalid_title";
header("location: ../../index.php?pfad=createArticle");
exit();
}
if (!articleContentValidator($content)) {
$_SESSION["message"] = "invalid_content";
header("location: ../../index.php?pfad=createArticle");
exit();
}
if (!articleCategoryValidator($category)) {
$_SESSION["message"] = "invalid_category";
header("location: ../../index.php?pfad=createArticle");
exit();
}
if (!articleTagValidator($tags)) {
$_SESSION["message"] = "invalid_tags";
header("location: ../../index.php?pfad=createArticle"); header("location: ../../index.php?pfad=createArticle");
exit(); exit();
} else { } else {
$title = $_POST["title"]; $cleanedTags = [];
$content = $_POST["content"]; $rawTags = explode(',', $tags);
$author = $_SESSION["user_email"]; foreach ($rawTags as $rawTag) {
$category = $_POST["category"]; // Leerzeichen am Anfang/Ende des einzelnen Tags entfernen:
$tags = $_POST['tags'] ?? ''; $tag = trim($rawTag);
$cleanedTags[] = $tag;
// -------------------------------- Validierung der Daten: -------------------------
if (!articleTitleValidator($title)) {
$_SESSION["message"] = "invalid_title";
header("location: ../../index.php?pfad=createArticle");
exit();
} }
// Duplikate entfernen:
$cleanedTags = array_unique($cleanedTags);
$cleanedTags = implode(',', $cleanedTags);
}
if (!articleContentValidator($content)) { // ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
$_SESSION["message"] = "invalid_content"; try {
header("location: ../../index.php?pfad=createArticle"); $articleManager = ArticleManager::getInstance();
exit(); // $content enthält bereits die finalen "uploads/..."-Pfade (kein Base64 mehr),
} // da rebuildBlocksFromPost() Datei-Uploads sofort verarbeitet.
$articleManager->addArticle($title, $content, $author, $category, $cleanedTags);
if (!articleCategoryValidator($category)) { // Formulardaten nach erfolgreichem Erstellen aus der Session löschen
$_SESSION["message"] = "invalid_category"; unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
header("location: ../../index.php?pfad=createArticle");
exit();
}
if (!articleTagValidator($tags)) { } catch (\Throwable $e) {
$_SESSION["message"] = "invalid_tags"; $_SESSION["message"] = "internal_error";
header("location: ../../index.php?pfad=createArticle"); header("location: ../../index.php?pfad=createArticle");
exit();
} else {
$cleanedTags = [];
$rawTags = explode(',', $tags);
foreach ($rawTags as $rawTag) {
// Leerzeichen am Anfang/Ende des einzelnen Tags entfernen:
$tag = trim($rawTag);
$cleanedTags[] = $tag;
}
// Duplikate entfernen:
$cleanedTags = array_unique($cleanedTags);
$cleanedTags = implode(',', $cleanedTags);
}
// ----------------- Base64-Bilder verarbeiten und auf Server speichern -----------------
$blocks = json_decode($content, true);
$uploadDir = __DIR__ . '/../../uploads/';
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if (is_array($blocks)) {
foreach ($blocks as &$block) {
// sicherstellen, dass 'type' und 'value' existieren:
if (isset($block['type']) && isset($block['value']) && $block['type'] === 'image' && str_starts_with($block['value'], 'data:image/')) {
// Base64-String zerlegen
$parts = explode(',', $block['value']);
// falls der String korrupt ist und kein Komma hat
if (count($parts) < 2) {
continue;
}
$metadata = $parts[0];
$base64Data = $parts[1];
// Dateiendung ermitteln
preg_match('/data:image\/(?<extension>.*?);/', $metadata, $matches);
$extension = $matches['extension'] ?? 'jpg';
if ($extension === 'jpeg') {
$extension = 'jpg';
}
// Eindeutigen Dateinamen generieren
$fileName = 'img_' . uniqid() . '.' . $extension;
$filePath = $uploadDir . $fileName;
// Datei im /uploads speichern:
if (file_put_contents($filePath, base64_decode($base64Data)) !== false) {
// temporären Base64-String durch den echten Pfad ersetzen
$block['value'] = 'uploads/' . $fileName;
} else {
$_SESSION["message"] = "image_upload_error";
header("location: ../../index.php?pfad=createArticle");
exit();
}
}
}
unset($block);
}
// Aktualisiertes Array wieder in JSON konvertieren
$finalContent = json_encode($blocks, JSON_UNESCAPED_UNICODE);
// ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
try {
$articleManager = ArticleManager::getInstance();
$articleManager->addArticle($title, $content, $author, $category, $cleanedTags);
// Formulardaten nach erfolgreichem Erstellen aus der Session löschen
unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
} catch (\Throwable $e){
$_SESSION["message"] = "internal_error";
header("location: ../../index.php?pfad=createArticle");
exit();
}
$_SESSION["message"] = "new_article";
// Weiterleitung zur Homepage
header("location: ../../index.php");
exit(); exit();
} }
$_SESSION["message"] = "new_article";
// Weiterleitung zur Homepage
header("location: ../../index.php");
exit();
} }
}
?> ?>
+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();
+37 -1
View File
@@ -1,3 +1,39 @@
<?php <?php
// Standardpfad
$pfad = $_GET["pfad"] ?? "home";
?> if ($pfad === "logout") {
include_once "php/controller/logout-controller.php";
exit();
} elseif ($pfad === "deleteAccount") {
include_once "php/controller/deleteAccount-controller.php";
exit();
}
if ($pfad === "login") {
include_once "php/controller/login-controller.php";
} elseif ($pfad === "register") {
include_once "php/controller/register-controller.php";
} elseif ($pfad === "password-forgotten") {
include_once "php/controller/password-forgotten-controller.php";
} elseif ($pfad === "confirm-register") {
include_once "php/controller/confirm-register-controller.php";
} elseif ($pfad === "confirm-password") {
include_once "php/controller/confirm-password-controller.php";
} elseif ($pfad === "updateComment") {
include_once "php/controller/updateComment-controller.php";
} elseif($pfad === "deleteComment") {
include_once "php/controller/deleteComment-controller.php";
}
// Whitelist
$erlaubte_content_seiten = [
"accessibility", "confirm-password", "confirm-register", "createArticle",
"datenschutz", "home", "impressum", "login", "nutzungsbedingungen",
"password-forgotten", "profile", "register", "search-results",
"show-mail", "showArticle", "showCategory", "updateArticle"
];
if (!in_array($pfad, $erlaubte_content_seiten)) {
$pfad = "404";
}
+21 -35
View File
@@ -2,56 +2,34 @@
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
require_once '../model/LocalArticleManager.php';
require_once '../model/ArticleManager.php'; require_once '../model/ArticleManager.php';
require_once '../model/UserManager.php';
require_once '../model/Article.php'; require_once '../model/Article.php';
require_once '../validator/search-validator.php';
if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) { if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
$search = trim($_GET["q"]); $search = trim($_GET["q"]);
if (empty($search)) { if (!searchQueryValidator($search)) {
$_SESSION["search_results"] = []; $_SESSION["search_results"] = [];
$_SESSION["search_query"] = ""; $_SESSION["search_query"] = "";
$_SESSION["message"] = "missing_parameters"; $_SESSION["message"] = "invalid_search_query";
} else { } else {
try { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::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[] = [
"id" => $obj->getId(), "id" => $obj->getId(),
"title" => $obj->getTitle(), "title" => $obj->getTitle(),
"content" => $obj->getContent(), "content" => $obj->getContent(),
"author" => $obj->getAuthor(), "author" => $userManager->findUser($obj->getAuthor())["vorname"] . " " . $userManager->findUser($obj->getAuthor())["nachname"],
"category" => $obj->getCategory(), "category" => $obj->getCategory(),
"tags" => $obj->getTags(), "tags" => $obj->getTags(),
"creationDate" => $obj->getCreationDate(), "creationDate" => $obj->getCreationDate(),
@@ -63,17 +41,25 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
$_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;
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1; if (!searchLimitValidator($limit)) {
header("Location: ../../index.php?pfad=search-results&q=" . urlencode($search) . "&sort=" . urlencode($sort) . "&limit=" . $limit . "&page=" . $page); $limit = 10;
}
$_SESSION['search_limit'] = $limit;
// 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
@@ -0,0 +1,32 @@
<?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),
]);
+14 -1
View File
@@ -5,6 +5,7 @@ if (session_status() === PHP_SESSION_NONE) {
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/CommentManager.php'; require_once 'php/model/CommentManager.php';
if (isset($_GET["id"]) && !empty($_GET["id"])){ if (isset($_GET["id"]) && !empty($_GET["id"])){
@@ -19,12 +20,23 @@ if (isset($_GET["id"]) && !empty($_GET["id"])){
$author = $article->getAuthor(); $author = $article->getAuthor();
$tags = $article->getTags(); $tags = $article->getTags();
$articleObj = $article; // Objekt für die Like-Abfagen sichern $articleObj = $article; // Objekt für die Like-Abfagen sichern
$userManager = UserManager::getInstance();
$name = $userManager->findUser($author)["vorname"]." ".$userManager->findUser($author)["nachname"];
}else{ }else{
//header("location: index.php?pfad=404"); //header("location: index.php?pfad=404");
include_once "content/404.php"; include_once "content/404.php";
exit(); exit();
} }
if($_GET["pfad"] == "updateArticle"){
if($article->getAuthor() != $_SESSION["user_email"]){
$_SESSION["message"] = "unauthorized_access";
header("location: index.php");
exit();
}
}
$commentManager = CommentManager::getInstance(); $commentManager = CommentManager::getInstance();
$comments = $commentManager->getCommentsByArticle($_GET["id"]); $comments = $commentManager->getCommentsByArticle($_GET["id"]);
@@ -37,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{
+35 -4
View File
@@ -1,21 +1,52 @@
<?php <?php
if (session_status() === PHP_SESSION_NONE) {
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/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"];
try { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
$articles = $articleManager->getArticlesByCategory($category); $userManager = UserManager::getInstance();
$allArticles = $articleManager->getArticlesByCategory($category);
$safeArrayResults = [];
foreach ($allArticles as $article) {
$authorName = $userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"];
$safeArrayResults[] = [
"id" => $article->getID(),
"title" => $article->getTitle(),
"content" => $article->getContent(),
"author" => $authorName,
"category" => $article->getCategory(),
"tags" => $article->getTags(),
"creationDate" => $article->getCreationDate(),
"likes" => $article->getLikes(),
];
}
// Session dient hier (wie bei search-results) als Zwischenspeicher, damit
// js/showCategory.js den kompletten Datensatz per showCategory-data.php
// nachladen kann, ohne die Kategorie-Abfrage ein zweites Mal auszuführen.
$_SESSION['category_results'] = $safeArrayResults;
$_SESSION['category_name'] = $category;
} 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
@@ -0,0 +1,35 @@
<?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),
]);
+83 -81
View File
@@ -7,17 +7,14 @@ require_once '../model/LocalArticleManager.php';
require_once '../model/ArticleManager.php'; require_once '../model/ArticleManager.php';
require_once '../model/Article.php'; require_once '../model/Article.php';
require_once '../validator/article-validator.php'; require_once '../validator/article-validator.php';
require_once '../../includes/article-block-helper.php';
if (!isset($_SESSION["user_email"])) { if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login"); header("Location: index.php?pfad=login");
exit(); exit();
} }
if ($_SERVER["REQUEST_METHOD"] === "POST") { if ($_SERVER["REQUEST_METHOD"] === "POST") {
$_SESSION["old_title"] = $_POST["title"] ?? '';
$_SESSION["old_content"] = $_POST["content"] ?? '';
$_SESSION["old_category"] = $_POST["category"] ?? '';
$_SESSION["old_tags"] = $_POST["tags"] ?? '';
if (isset($_GET["id"]) && !empty($_GET["id"])) { if (isset($_GET["id"]) && !empty($_GET["id"])) {
$id = $_GET["id"]; $id = $_GET["id"];
@@ -41,16 +38,56 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
exit(); exit();
} }
if (!isset($_POST["title"]) ||!isset($_POST["content"]) || !isset($_POST["category"])){ $uploadDir = __DIR__ . '/../../uploads/';
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// Formularzustand (Titel/Tags/Kategorie/Blöcke) immer sichern, damit er nach einem
// Redirect (PRG-Pattern oder Validierungsfehler) wieder angezeigt werden kann.
$_SESSION["old_title"] = $_POST["title"] ?? '';
$_SESSION["old_tags"] = $_POST["tags"] ?? '';
$_SESSION["old_category"] = $_POST["category"] ?? '';
$blocks = rebuildBlocksFromPost($_POST['blocks'] ?? [], $_FILES['blocks'] ?? [], $uploadDir);
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
// ---------------------------------------------------------------------
// Zwischen-Schritt: Block hinzufügen oder entfernen (kein echtes Speichern).
// Wird bei aktivem JavaScript per preventDefault() abgefangen und lokal im
// DOM erledigt (js/editor.js) ohne JS läuft dieser Server-Roundtrip.
// ---------------------------------------------------------------------
if (isset($_POST['editor_action']) && $_POST['editor_action'] !== '') {
$action = $_POST['editor_action'];
if ($action === 'add_text') {
$blocks[] = ['type' => 'text', 'value' => ''];
} elseif ($action === 'add_image') {
$blocks[] = ['type' => 'image', 'value' => ''];
} elseif (str_starts_with($action, 'delete_block:')) {
$deleteIndex = (int) substr($action, strlen('delete_block:'));
unset($blocks[$deleteIndex]);
$blocks = array_values($blocks);
}
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
header("location: ../../index.php?pfad=updateArticle&id=$id");
exit();
}
// ---------------------------------------------------------------------
// Echtes Speichern
// ---------------------------------------------------------------------
if (!isset($_POST["title"]) || !isset($_POST["category"])) {
$_SESSION["message"] = "missing_parameters"; $_SESSION["message"] = "missing_parameters";
header("location: ../../index.php?pfad=updateArticle&id=$id"); header("location: ../../index.php?pfad=updateArticle&id=$id");
exit(); exit();
}else{ } else {
$title = $_POST["title"]; $title = $_POST["title"];
$content = $_POST["content"]; $content = json_encode($blocks, JSON_UNESCAPED_UNICODE);
$author = $_SESSION["user_email"]; $author = $_SESSION["user_email"];
$category = $_POST["category"]; $category = $_POST["category"];
$tags = $_POST['tags'] ?? ''; $tags = $_POST['tags'] ?? '';
// -------------------------------- Validierung der Daten: ------------------------- // -------------------------------- Validierung der Daten: -------------------------
if (!articleTitleValidator($title)) { if (!articleTitleValidator($title)) {
@@ -88,91 +125,56 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
$cleanedTags = implode(',', $cleanedTags); $cleanedTags = implode(',', $cleanedTags);
} }
// --------------------------------------- Base64-Bilder speichern --------------------------------------------- // ----------------- Verwaiste Bilder aufräumen -----------------
$blocks = json_decode($content, true); // Bilder, die im alten (gespeicherten) Content vorkamen, im neuen aber nicht
$uploadDir = __DIR__ . '/../../uploads/'; // mehr referenziert werden, wurden vom Nutzer entfernt oder ersetzt -> löschen.
// Hinweis/TODO: Bilder, die innerhalb derselben Bearbeitungs-Sitzung neu
if (!file_exists($uploadDir)) { // hochgeladen und noch vor dem finalen Speichern wieder entfernt wurden,
mkdir($uploadDir, 0755, true); // werden hierüber nicht erfasst (sie tauchten nie im alten Content auf) und
} // bleiben als Datei liegen. Für eine vollständige Bereinigung würde sich ein
// regelmäßiger Cleanup-Job anbieten, der verwaiste Dateien im uploads/-Ordner
// ----------------- Gelöschte Bilder über die JS-Löschliste entfernen ----------------- TODO: Gelöschte Bilder über die JS-Löschliste entfernen // mit den in der Datenbank referenzierten Pfaden abgleicht.
/*if (isset($_POST['deleted_images'])) { $oldBlocks = json_decode($article->getContent(), true);
$deletedImages = json_decode($_POST['deleted_images'], true); $oldImagePaths = [];
if (is_array($oldBlocks)) {
// Wir ermitteln den physisch echten, absoluten Pfad zum uploads-Ordner auf der Festplatte foreach ($oldBlocks as $oldBlock) {
$uploadDir = realpath(__DIR__ . '/../../uploads') . DIRECTORY_SEPARATOR; if (($oldBlock['type'] ?? '') === 'image'
&& !empty($oldBlock['value'])
if (is_array($deletedImages)) { && is_string($oldBlock['value'])
foreach ($deletedImages as $imagePath) { && str_starts_with($oldBlock['value'], 'uploads/')) {
// Nur den reinen Dateinamen heraustrennen (z.B. img_65a123.jpg) $oldImagePaths[] = $oldBlock['value'];
$filename = basename($imagePath);
$fullDeletePath = $uploadDir . $filename;
// Debugging & Löschen:
if (file_exists($fullDeletePath)) {
// Versuchen zu löschen. Wenn es fehlschlägt, Fehlermeldung erzwingen
if (!@unlink($fullDeletePath)) {
$error = error_get_last();
die("Datei existiert, aber PHP darf sie nicht löschen! Grund: " . $error['message']);
}
} else {
// Wenn PHP die Datei an diesem Pfad nicht findet, brechen wir zum Debuggen ab
// die("PHP findet die Datei nicht unter dem Pfad: " . $fullDeletePath);
}
} }
} }
}*/
// ----------------------- NEU hinzugefügte Base64-Bilder: --------------------------
if (is_array($blocks)) {
foreach ($blocks as &$block) {
// Prüfen, ob der Block ein Bild ist und ein NEUES Bild (Base64-Format) enthält
if (isset($block['type']) && isset($block['value']) && $block['type'] === 'image' && is_string($block['value'])) {
if (str_starts_with($block['value'], 'data:image/')) {
$parts = explode(',', $block['value']);
if (count($parts) >= 2) {
$metadata = $parts[0];
$base64Data = $parts[1];
preg_match('/data:image\/(?<extension>.*?);/', $metadata, $matches);
$extension = $matches['extension'] ?? 'jpg';
if ($extension === 'jpeg') { $extension = 'jpg'; }
$fileName = 'img_' . uniqid() . '.' . $extension;
$filePath = $uploadDir . $fileName;
if (file_put_contents($filePath, base64_decode($base64Data)) !== false) {
$block['value'] = 'uploads/' . $fileName;
} else {
$_SESSION["message"] = "image_upload_error";
header("location: ../../index.php?pfad=updateArticle&id=$id");
exit();
}
}
}
}
}
unset($block);
} }
// Aktualisiertes Array wieder in JSON konvertieren $newImagePaths = [];
$finalContent = json_encode($blocks, JSON_UNESCAPED_UNICODE); foreach ($blocks as $block) {
if (($block['type'] ?? '') === 'image' && !empty($block['value'])) {
$newImagePaths[] = $block['value'];
}
}
$orphanedImages = array_diff($oldImagePaths, $newImagePaths);
foreach ($orphanedImages as $orphanedImage) {
$absolutePath = __DIR__ . '/../../' . $orphanedImage;
if (is_file($absolutePath)) {
@unlink($absolutePath);
}
}
// ----------------- Übertragung der validierten Daten in ArticleManager: --------------------------- // ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
try { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
$article = $articleManager->getArticle($id); $article = $articleManager->getArticle($id);
$article->setTitle($title); $article->setTitle($title);
$article->setContent($finalContent); $article->setContent($content);
$article->setCategory($category); $article->setCategory($category);
$article->setTags($cleanedTags); $article->setTags($cleanedTags);
$articleManager->updateArticle($id ,$article, $author); $articleManager->updateArticle($id, $article, $author);
unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]); unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
} catch (\Throwable $e){ } catch (\Throwable $e) {
$_SESSION["message"] = $e->getMessage(); $_SESSION["message"] = $e->getMessage();
header("location: ../../index.php?pfad=updateArticle&id=$id"); header("location: ../../index.php?pfad=updateArticle&id=$id");
exit(); exit();
@@ -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();
+10
View File
@@ -97,6 +97,16 @@ class Article
return $this->author; return $this->author;
} }
/**
* Setzt einen neuen Autor eines Beitrages.
* @param string $author
* @return void
*/
public function setAuthor(string $author)
{
$this->author = $author;
}
/** /**
* Gibt das Veröffentlichungsdatum des Beitrags zurück. * Gibt das Veröffentlichungsdatum des Beitrags zurück.
* @return string * @return string
+1 -144
View File
@@ -18,152 +18,9 @@ class ArticleManager
*/ */
public static function getInstance() public static function getInstance()
{ {
$articleManager = DatabaseArticleManager::getInstance(); // Hier kann zwischen dem lokalen und datenbankbasiertem ArticleManager gewechselt werden.
// 100 fiktionale Fachbeiträge: return DatabaseArticleManager::getInstance(); // Hier kann zwischen dem lokalen und datenbankbasiertem ArticleManager gewechselt werden.
$dummyArticles = [
// --- INFORMATIK & MATHE (1-20) ---
1 => ["Satz des Pythagoras", "Der Satz des Pythagoras beschreibt das Verhältnis der Seitenlängen in einem rechtwinkligen Dreieck.", "mathe", "Dreiecke, Geometrie"],
2 => ["Tunneleffekt", "Der Tunneleffekt beschreibt das quantenmechanische Phänomen, dass Teilchen Barrieren überwinden können.", "physik", "Quantenphysik, Energie"],
3 => ["Datenschutz vs Datensicherheit", "Datenschutz schützt Personen, während Datensicherheit Systeme vor unbefugten Zugriffen schützt.", "informatik", "Daten, DSGVO"],
4 => ["Einführung in Algorithmen", "Ein Algorithmus ist eine präzise Handlungsanweisung zur Lösung eines vordefinierten Problems.", "informatik", "Code, Logik"],
5 => ["Primzahlen im Detail", "Primzahlen sind natürliche Zahlen, die nur durch eins und sich selbst ohne Rest teilbar sind.", "mathe", "Zahlentheorie"],
6 => ["Lineare Algebra", "Vektoren und Matrizen bilden das Fundament für moderne Computergrafik und 3D-Engines.", "mathe", "Matrizen, Vektoren"],
7 => ["Objektorientierte Programmierung", "Die OOP nutzt Klassen und Objekte, um reale Strukturen im Quellcode abzubilden.", "informatik", "OOP, Klassen"],
8 => ["Grundlagen von HTML und CSS", "HTML strukturiert den Inhalt einer Webseite, während CSS für das optische Design zuständig ist.", "informatik", "Web, Frontend"],
9 => ["Die Relativitätstheorie", "Einsteins Theorie revolutionierte unser Verständnis von Raum, Zeit und Gravitation massiv.", "physik", "Einstein, Gravitation"],
10 => ["Datenbanken und SQL", "Strukturierte Abfragesprachen erlauben das effiziente Speichern und Abrufen großer Datenmengen.", "informatik", "SQL, Datenbank"],
11 => ["Wahrscheinlichkeitsrechnung", "Die Stochastik befasst sich mit der mathematischen Analyse von Zufallsexperimenten.", "mathe", "Zufall, Stochastik"],
12 => ["Quantencomputing", "Künftige Quantencomputer nutzen Qubits, um komplexe Berechnungen in Rekordzeit zu lösen.", "informatik", "Hardware, Zukunft"],
13 => ["Die Fibonacci-Folge", "Diese Zahlenreihe beschreibt mathematische Wachstumsmuster, die oft in der Natur vorkommen.", "mathe", "Zahlen, Natur"],
14 => ["Thermodynamik", "Die Hauptsätze der Thermodynamik regeln den Energieaustausch und die Entropie in Systemen.", "physik", "Energie, Wärme"],
15 => ["Cybersecurity Grundlagen", "Die Absicherung von Netzwerken erfordert Firewalls, Verschlüsselung und regelmäßige Audits.", "informatik", "Sicherheit, Hacker"],
16 => ["Kryptographie", "Asymmetrische Verschlüsselungsverfahren sichern heutzutage den gesamten Datenverkehr im Web.", "informatik", "Krypto, Security"],
17 => ["Analysis und Ableitungen", "Die Differentialrechnung untersucht die lokalen Änderungsraten von mathematischen Funktionen.", "mathe", "Analysis, Funktionen"],
18 => ["Schwarze Löcher", "Diese Regionen im Raum besitzen eine so starke Gravitation, dass selbst Licht nicht entkommt.", "physik", "Astronomie, Kosmos"],
19 => ["Git Versionsverwaltung", "Git erlaubt Entwicklern das parallele Arbeiten an Code-Projekten ohne Datenverlust.", "informatik", "Git, DevOps"],
20 => ["Künstliche Intelligenz", "Neuronale Netze versuchen das menschliche Gehirn für maschinelles Lernen nachzubilden.", "informatik", "KI, Software"],
// --- CHEMIE & BIOLOGIE (21-40) ---
21 => ["Das Periodensystem", "Die Elemente sind nach ihrer Ordnungszahl und chemischen Eigenschaften geordnet.", "chemie", "Elemente, Moleküle"],
22 => ["Photosynthese", "Pflanzen wandeln mithilfe von Sonnenlicht Kohlendioxid und Wasser in Glukose und Sauerstoff um.", "biologie", "Pflanzen, Energie"],
23 => ["Aufbau einer Zelle", "Die Zelle ist die kleinste lebende Einheit aller Organismen mit spezialisierten Organellen.", "biologie", "Zellen, Biologie"],
24 => ["Säuren und Basen", "Der pH-Wert misst die Konzentration von Wasserstoff-Ionen in einer wässrigen Lösung.", "chemie", "Labor, pH-Wert"],
25 => ["Die DNA-Struktur", "Die Doppelhelix enthält den genetischen Bauplan für die Entwicklung aller Lebewesen.", "biologie", "Genetik, Erbgut"],
26 => ["Chemische Bindungen", "Kovalente Bindungen entstehen durch das Teilen von Elektronenpaaren zwischen Atomen.", "chemie", "Atome, Bindung"],
27 => ["Das Immunsystem", "Weiße Blutkörperchen und Antikörper schützen den menschlichen Körper vor Krankheitserregern.", "biologie", "Gesundheit, Abwehr"],
28 => ["Katalysatoren", "Katalysatoren beschleunigen chemische Reaktionen, ohne dabei selbst verbraucht zu werden.", "chemie", "Reaktion, Chemie"],
29 => ["Ökosystem Wald", "Das Zusammenspiel von Flora, Fauna und Klima bildet ein hochsensibles ökologisches System.", "biologie", "Natur, Wald"],
30 => ["Die Mendelschen Regeln", "Diese Grundgesetze der Vererbung beschreiben, wie Merkmale an Nachkommen weitergegeben werden.", "biologie", "Genetik, Erbung"],
31 => ["Zustandsformen der Materie", "Fest, flüssig und gasförmig sind die klassischen Aggregatzustände von Stoffen.", "chemie", "Physik, Materie"],
32 => ["Evolutionstheorie", "Charles Darwin begründete die Theorie der natürlichen Auslese und Anpassung von Arten.", "biologie", "Darwin, Evolution"],
33 => ["Organische Chemie", "Die Chemie der Kohlenstoffverbindungen bildet die Basis für alles bekannte Leben.", "chemie", "Kohlenstoff, Chemie"],
34 => ["Das menschliche Gehirn", "Milliarden von Neuronen kommunizieren über Synapsen, um Reize und Gedanken zu verarbeiten.", "biologie", "Neurologie, Nerven"],
35 => ["Der Wasserkreislauf", "Verdunstung, Kondensation und Niederschlag halten das Wasser auf der Erde in Bewegung.", "geographie", "Wasser, Klima"],
36 => ["Aggregatzustände von Wasser", "Wasser zeigt ungewöhnliche Eigenschaften wie die Dichteanomalie beim Gefrieren.", "chemie", "Wasser, Eis"],
37 => ["Blutkreislauf des Menschen", "Das Herz pumpt sauerstoffreiches Blut durch Arterien in alle Organe des Körpers.", "biologie", "Herz, Medizin"],
38 => ["Das Ohmsche Gesetz", "Es beschreibt den direkten Zusammenhang zwischen Spannung, Stromstärke und Widerstand.", "physik", "Strom, Elektronik"],
39 => ["Plattentektonik", "Die Bewegung der Kontinentalplatten führt zu Erdbeben, Vulkanismus und Gebirgsbildung.", "geographie", "Erde, Geologie"],
40 => ["Proteine und Enzyme", "Enzyme wirken als Biokatalysatoren und steuern fast alle Stoffwechselprozesse.", "biologie", "Biochemie, Enzyme"],
// --- GESCHICHTE & WIRTSCHAFT (41-60) ---
41 => ["Das Römische Reich", "Vom Stadtstaat zum Weltreich prägte Rom die Rechts- und Kulturgeschichte Europas.", "geschichte", "Antike, Rom"],
42 => ["Die Französische Revolution", "Freiheit, Gleichheit, Brüderlichkeit beendeten 1789 die absolute Monarchie in Frankreich.", "geschichte", "Europa, Politik"],
43 => ["Inflation erklärt", "Inflation bezeichnet die kontinuierliche Geldentwertung und den Kaufkraftverlust.", "wirtschaft", "Geld, Finanzen"],
44 => ["Die Industrielle Revolution", "Die Erfindung der Dampfmaschine veränderte die Produktion und die Gesellschaft tiefgreifend.", "geschichte", "Industrie, Arbeit"],
45 => ["Angebot und Nachfrage", "Dieses fundamentale Marktgesetz bestimmt den Preis von Gütern in einer freien Wirtschaft.", "wirtschaft", "Markt, Preise"],
46 => ["Der Buchdruck", "Johannes Gutenbergs Erfindung revolutionierte die Verbreitung von Wissen im Mittelalter.", "geschichte", "Medien, Wissen"],
47 => ["Die Entdeckung Amerikas", "Kolumbus' Seereise im Jahr 1492 leitete das Zeitalter der Kolonialisierung ein.", "geschichte", "Entdeckung, Seefahrt"],
48 => ["Kryptowährungen", "Bitcoin nutzt Blockchain-Technologie, um dezentralen digitalen Werttransfer zu erlauben.", "wirtschaft", "Blockchain, Finanzen"],
49 => ["Das antike Griechenland", "Die Wiege der Demokratie und Philosophie brachte Denker wie Sokrates und Platon hervor.", "geschichte", "Antike, Philosophie"],
50 => ["Globalisierung", "Die weltweite Verflechtung in Wirtschaft, Kultur und Politik bringt Chancen und Risiken.", "wirtschaft", "Weltwirtschaft, Handel"],
51 => ["Der Dreißigjährige Krieg", "Ein religiöser und politischer Konflikt verwüstete zwischen 1618 und 1648 Mitteleuropa.", "geschichte", "Krieg, Europa"],
52 => ["Die Weimarer Republik", "Die erste deutsche Demokratie scheiterte an wirtschaftlichen und politischen Krisen.", "geschichte", "Deutschland, Weimar"],
53 => ["Aktien und Börse", "Unternehmen beschaffen sich Kapital durch die Ausgabe von Anteilen an Investoren.", "wirtschaft", "Aktien, Investieren"],
54 => ["Das alte Ägypten", "Pharaonen, Pyramiden und Hieroglyphen zeugen von einer hochentwickelten Hochkultur am Nil.", "geschichte", "Ägypten, Antike"],
55 => ["Der Kalte Krieg", "Das Wettrüsten zwischen USA und UdSSR prägte die globale Politik der Nachkriegszeit.", "geschichte", "Ost-West, Politik"],
56 => ["Zentralbanken und Leitzins", "Durch Zinsänderungen steuern Notenbanken die Geldmenge und bekämpfen Inflation.", "wirtschaft", "Zinsen, Geldpolitik"],
57 => ["Die Seidenstraße", "Das historische Netzwerk von Handelsrouten verband über Jahrhunderte Asien und Europa.", "geschichte", "Handel, Asien"],
58 => ["Das Mittelalter", "Ritter, Burgen und das Feudalsystem prägten diese tausendjährige Epoche Europas.", "geschichte", "Mittelalter, Feudalismus"],
59 => ["Planwirtschaft vs Marktwirtschaft", "Zentrale staatliche Steuerung steht dem freien Spiel der Marktkräfte gegenüber.", "wirtschaft", "Systeme, Wirtschaft"],
60 => ["Die Berliner Mauer", "Ihr Bau 1961 zementierte die Teilung Deutschlands, ihr Fall 1989 beendete sie.", "geschichte", "DDR, Wiedervereinigung"],
// --- ANWENDUNGEN & WEITERE THEMEN (61-100) ---
61 => ["Cloud Computing", "Das Auslagern von Rechenleistung in das Internet spart lokale IT-Infrastruktur ein.", "informatik", "Cloud, Web"],
62 => ["Responsive Webdesign", "Moderne Webseiten passen ihr Layout dynamisch an Smartphones und Desktops an.", "informatik", "Design, CSS"],
63 => ["Der Treibhauseffekt", "Gase in der Atmosphäre verhindern das Entweichen von Wärme ins Weltall.", "physik", "Klima, Umwelt"],
64 => ["Mechanik und Kräfte", "Die Newtonschen Axiome beschreiben, wie Kräfte auf Körper wirken und sie bewegen.", "physik", "Newton, Kraft"],
65 => ["Die Mendelschen Gesetze", "Die Vererbung von Genen folgt klaren statistischen Wahrscheinlichkeiten.", "biologie", "Genetik, Erbsen"],
66 => ["Lichtgeschwindigkeit", "Im Vakuum bewegt sich Licht mit knapp 300.000 Kilometern pro Sekunde.", "physik", "Licht, Relativität"],
67 => ["Die Funktion von APIs", "Programmierschnittstellen erlauben den Datenaustausch zwischen verschiedenen Systemen.", "informatik", "API, Schnittstelle"],
68 => ["Der Goldstandard", "Ein historischen Währungssystem, bei dem Geld durch echtes Gold gedeckt war.", "wirtschaft", "Gold, Währung"],
69 => ["Der Wiener Kongress", "1815 ordneten die europäischen Mächte die Landkarte nach den Napoleonischen Kriegen neu.", "geschichte", "Europa, Diplomatie"],
70 => ["Integrierte Schaltkreise", "Mikrochips enthalten Millionen Transistoren auf kleinstem Raum für Logikschaltungen.", "informatik", "Hardware, Chips"],
71 => ["Die Magellan-Expedition", "Die erste erfolgreiche Weltumsegelung bewies praktisch die Kugelgestalt der Erde.", "geschichte", "Seefahrt, Erde"],
72 => ["Das Internet der Dinge", "Alltagsgegenstände werden vernetzt, um smarte Automatisierungen zu ermöglichen.", "informatik", "IoT, SmartHome"],
73 => ["Halbwertszeit", "Die Zeitspanne, in der sich die Hälfte der instabilen Atome radioaktiv abbaut.", "physik", "Atomphysik, Strahlung"],
74 => ["Elektromagnetismus", "Die Verknüpfung von elektrischen Strömen und magnetischen Feldern treibt Motoren an.", "physik", "Strom, Magnet"],
75 => ["Das Ökosystem Meer", "Ozeane regulieren das Weltklima und bieten Lebensraum für unzählige Arten.", "biologie", "Meer, Ökologie"],
76 => ["Einführung in Docker", "Containerisierung isoliert Anwendungen samt Abhängigkeiten für stabilen Betrieb.", "informatik", "Docker, DevOps"],
77 => ["Das Römische Recht", "Viele moderne europäische Gesetzbücher basieren auf antiken römischen Rechtsprinzipien.", "geschichte", "Recht, Gesetz"],
78 => ["Verhaltensbiologie", "Untersuchung von angeborenen und erlernten Verhaltensweisen bei Mensch und Tier.", "biologie", "Verhalten, Tiere"],
79 => ["Verschlüsselung im Alltag", "HTTPS schützt Passwörter und Zahlungsdaten beim Surfen vor dem Mitlesen.", "informatik", "Web, HTTPS"],
80 => ["Die Magna Carta", "1215 schränkte dieses Dokument die absolute Macht des englischen Königs ein.", "geschichte", "England, Verfassung"],
81 => ["Marktversagen", "Wenn der freie Markt Ressourcen unvollständig verteilt, muss der Staat eingreifen.", "wirtschaft", "Markt, Staat"],
82 => ["Optische Linsen", "Konvexe und konkave Linsen brechen Licht für Brillen, Mikroskope und Kameras.", "physik", "Optik, Licht"],
83 => ["Die Entstehung der Erde", "Vor rund 4,5 Milliarden Jahren ballte sich kosmischer Staub zu unserem Planeten.", "geographie", "Erde, Kosmos"],
84 => ["Grundlagen von JavaScript", "Diese Skriptsprache macht statische Webseiten interaktiv und dynamisch nutzbar.", "informatik", "JS, Webentwicklung"],
85 => ["Die industrielle Landwirtschaft", "Moderne Techniken sichern Welternährung, belasten jedoch oft die Umwelt.", "biologie", "Landwirtschaft, Umwelt"],
86 => ["Das Schwarze Jahr 1929", "Der New Yorker Börsencrash löste die verheerende Weltwirtschaftskrise aus.", "geschichte", "Krise, Finanzen"],
87 => ["Die Evolution des Menschen", "Der Stammbaum des Homo Sapiens entwickelte sich über Millionen Jahre in Afrika.", "biologie", "Mensch, Evolution"],
88 => ["Einführung in Linux", "Das Open-Source-Betriebssystem bildet das Rückgrat moderner Server-Infrastrukturen.", "informatik", "Linux, OS"],
89 => ["Der Urknall", "Die Urknalltheorie beschreibt den Beginn des Universums aus einer Singularität.", "physik", "Astronomie, Urknall"],
90 => ["Das Periodensystem der Elemente", "Dmitri Mendelejew ordnete Elemente logisch nach ihren Atommassen.", "chemie", "Periodensystem, Chemie"],
91 => ["Die Hanse", "Ein mächtiger mittelalterlicher Bund von Kaufleuten dominierte den Nordseehandel.", "geschichte", "Handel, Mittelalter"],
92 => ["Wirtschaftswachstum", "Die Steigerung des Bruttoinlandsprodukts gilt oft als Indikator für Wohlstand.", "wirtschaft", "BIP, Finanzen"],
93 => ["Neuronale Netze", "Diese Strukturen lernen durch mathematische Gewichtung aus riesigen Datenmengen.", "informatik", "KI, Mathematik"],
94 => ["Der Erste Weltkrieg", "Der globale Konflikt von 1914 bis 1918 zerstörte das alte europäische Machtgefüge.", "geschichte", "Europa, Krieg"],
95 => ["Das Gehirn und Hormone", "Botenstoffe steuern Gefühle, Schlafzyklen und Reaktionen des Körpers.", "biologie", "Medizin, Hormone"],
96 => ["SQL Joins erklärt", "Joins verknüpfen Daten aus mehreren Tabellen über gemeinsame Schlüssel.", "informatik", "SQL, Datenbanken"],
97 => ["Wellen-Teilchen-Dualismus", "Quantenobjekte zeigen je nach Messaufbau Eigenschaften von Wellen oder Teilchen.", "physik", "Quanten, Licht"],
98 => ["Der Absolutismus", "Der Sonnenkönig Ludwig XIV. verkörperte die unbeschränkte Herrschaft des Monarchen.", "geschichte", "Frankreich, Monarchie"],
99 => ["Die Funktion von Routern", "Netzwerkgeräte leiten Datenpakete über IP-Adressen an den richtigen Empfänger.", "informatik", "Netzwerk, Internet"],
100 => ["Die Entstehung des Geldes", "Vom Tauschhandel über Naturalgeld bis hin zu modernen digitalen Fiat-Währungen.", "wirtschaft", "Geld, Geschichte"]
];
// 10 Dummy-User:
$authors = [
'max.mustermann@web.de', 'erika.mustermann@web.de', 'john.doe@gmail.com',
'jane.doe@gmail.com', 'anna.schmidt@gmx.de', 'thomas.mueller@gmx.de',
'sabine.fischer@outlook.com', 'michael.weber@outlook.com', 'julia.wagner@t-online.de',
'stefan.becker@t-online.de'
];
foreach ($dummyArticles as $id => $data) {
// Falls der Artikel mit der ID noch nicht existiert, lege ihn an
if ($articleManager->getArticle($id) == null) {
// Verteilt die 10 Autoren gleichmäßig (ID 1 -> Autor 1, ID 10 -> Autor 10, ID 11 -> Autor 1)
$authorEmail = $authors[($id - 1) % 10];
$blockStructure = [
[
'type' => 'text',
'value' => $data[1] // Der originale Text aus dem Dummy-Array
]
];
$jsonContent = json_encode($blockStructure, JSON_UNESCAPED_UNICODE);
$articleManager->addArticle(
$data[0], // Titel
$jsonContent, // Inhalt
$authorEmail, // Rotierende Autoren-E-Mail
$data[2], // Kategorie
$data[3] // Tags
);
}
}
return $articleManager;
} }
} }
+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;
} }
+22 -44
View File
@@ -1,4 +1,5 @@
<?php <?php
require_once 'DatabaseInitializer.php';
require_once 'ArticleManagerDAO.php'; require_once 'ArticleManagerDAO.php';
require_once 'Article.php'; require_once 'Article.php';
/** /**
@@ -9,60 +10,35 @@ require_once 'Article.php';
class DatabaseArticleManager implements ArticleManagerDAO { class DatabaseArticleManager implements ArticleManagerDAO {
private static $instance = null; private static $instance = null;
private $dbPath;
/** /**
* Konstruktor * Konstruktor
* *
* Erstellt die Datenbankverbindung und die Tabelle articles. * Erstellt die Datenbankverbindung und die Tabelle articles.
* @throws InternalServerErrorException * @throws RuntimeException
*/ */
public function __construct() public function __construct()
{ {
if (!file_exists(__DIR__ . '/../../db/articles.db')) { $this->dbPath = __DIR__ . '/../../db/eduforgeDB.db';
try { DatabaseInitializer::initialize($this->dbPath);
$db = $this->getConnection();
// Tabelle für Beiträge
$db->exec("
CREATE TABLE articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
content TEXT,
author TEXT,
category TEXT,
tags TEXT,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);");
// Tabelle für Likes
$db->exec("
CREATE TABLE likes (
article_id INTEGER,
user_id TEXT,
PRIMARY KEY (article_id, user_id),
FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE
);");
unset($db);
} catch (PDOException $e) {
throw new InternalServerErrorException($e->getMessage());
}
}
} }
/** /**
* Baut die Verbindung zur Datenbank auf. * Baut die Verbindung zur Datenbank auf.
* @throws InternalServerErrorException *
* @return PDO Datenbankverbindung
* @throws RuntimeException
*/ */
private function getConnection() private function getConnection()
{ {
try { try {
$user = 'root'; $db = new PDO('sqlite:' . $this->dbPath, null, null);
$pw = null; $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$dsn = 'sqlite:' . __DIR__ . '/../../db/articles.db'; $db->exec('PRAGMA foreign_keys = ON;');
return new PDO($dsn, $user, $pw); return $db;
} catch (PDOException $e) { } catch (PDOException $e) {
throw new InternalServerErrorException($e->getMessage()); throw new RuntimeException("internal_error");
} }
} }
@@ -336,18 +312,18 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$db = $this->getConnection(); $db = $this->getConnection();
$sql = "SELECT id, title, content, author, category, tags, created $sql = "SELECT id, title, content, author, category, tags, created
FROM articles FROM articles
WHERE title LIKE :keyword WHERE title LIKE :keyword
OR content LIKE :keyword;"; OR content LIKE :keyword
OR tags LIKE :keyword;";
$command = $db->prepare($sql); $command = $db->prepare($sql);
if (!$command) { if (!$command) {
throw new InternalServerErrorException("internal_error"); throw new InternalServerErrorException("internal_error");
} }
// Wildcards für die Suche hinzufügen // Wildcards für die SQL-Suche hinzufügen
$searchParam = '%' . $cleankeyword . '%'; $searchParam = '%' . $cleankeyword . '%';
$success = $command->execute([ $success = $command->execute([
":keyword" => $searchParam ":keyword" => $searchParam
]); ]);
@@ -360,10 +336,11 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$filteredArticles = []; $filteredArticles = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$likes = $this->getLikesForArticle(intval($row['id'])); $articleId = intval($row['id']);
$likes = $this->getLikesForArticle($articleId);
$filteredArticles[] = new Article( $filteredArticles[] = new Article(
intval($row['id']), $articleId,
$row['title'] ?? '', $row['title'] ?? '',
$row['content'] ?? '', $row['content'] ?? '',
$row['author'] ?? '', $row['author'] ?? '',
@@ -381,6 +358,7 @@ class DatabaseArticleManager implements ArticleManagerDAO {
} }
} }
/** /**
* Holt alle User-IDs, die einen bestimmten Beitrag geliked haben. * Holt alle User-IDs, die einen bestimmten Beitrag geliked haben.
* *
+197 -38
View File
@@ -1,5 +1,5 @@
<?php <?php
require_once 'DatabaseInitializer.php';
require_once "CommentManagerDAO.php"; require_once "CommentManagerDAO.php";
require_once "Comment.php"; require_once "Comment.php";
@@ -12,43 +12,15 @@ require_once "Comment.php";
class DatabaseCommentManager implements CommentManagerDAO class DatabaseCommentManager implements CommentManagerDAO
{ {
private static $instance = null; private static $instance = null;
private $dbPath;
/** /**
* Erstellt die Kommentartabelle, falls diese noch nicht existiert. * Erstellt die Kommentartabelle, falls diese noch nicht existiert.
*/ */
public function __construct() private function __construct()
{ {
try { $this->dbPath = __DIR__ . '/../../db/eduforgeDB.db';
$db = $this->getConnection(); DatabaseInitializer::initialize($this->dbPath);
$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");
}
} }
/** /**
@@ -59,13 +31,10 @@ class DatabaseCommentManager implements CommentManagerDAO
private function getConnection() private function getConnection()
{ {
try { try {
$dsn = 'sqlite:' . __DIR__ . '/../../db/comments.db'; $db = new PDO('sqlite:' . $this->dbPath, null, null);
$db = new PDO($dsn, null, null);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('PRAGMA foreign_keys = ON;');
return $db; return $db;
} catch (PDOException $e) { } catch (PDOException $e) {
throw new RuntimeException("internal_error"); throw new RuntimeException("internal_error");
} }
@@ -254,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");
}
}
} }
+251
View File
@@ -0,0 +1,251 @@
<?php
/**
* Diese Klasse legt einmalig beim allerersten Start in der exakt korrekten Reihenfolge die Tabellen an
*
* @author Niklas Ortmann
*/
class DatabaseInitializer {
public static function initialize($dbPath) {
if (!file_exists($dbPath)) {
try {
$db = new PDO('sqlite:' . $dbPath);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('PRAGMA foreign_keys = ON;');
$db->exec("
CREATE TABLE IF NOT EXISTS users (
email TEXT PRIMARY KEY,
vorname TEXT NOT NULL,
nachname TEXT NOT NULL,
password TEXT NOT NULL
);
");
$db->exec("
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
content TEXT,
author TEXT,
category TEXT,
tags TEXT,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (author) REFERENCES users(email) ON DELETE SET NULL ON UPDATE CASCADE
);
");
$db->exec("
CREATE TABLE IF NOT EXISTS likes (
article_id INTEGER,
user_id TEXT,
PRIMARY KEY (article_id, user_id),
FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(email) ON DELETE CASCADE
);
");
$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 NULL,
content TEXT NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
FOREIGN KEY (author) REFERENCES users(email) ON DELETE SET NULL,
FOREIGN KEY (parent_comment_id) REFERENCES comments(id) ON DELETE CASCADE
);
");
$initializer = new self();
$availableEmails = $initializer->seedDummyUsers($db);
$initializer->seedDummyArticles($db, $availableEmails);
unset($db);
} catch (PDOException $e) {
throw new RuntimeException("internal-error");
}
}
}
/**
* Erstellt die Dummy-Benutzer in der Datenbank.
*
* @param PDO $db Die aktive Datenbankverbindung
* @return array Liste der erfolgreich angelegten E-Mail-Adressen
*/
private function seedDummyUsers(PDO $db): array {
$dummyUsers = [
['email' => 'max.mustermann@web.de', 'vorname' => 'Max', 'nachname' => 'Mustermann'],
['email' => 'erika.mustermann@web.de', 'vorname' => 'Erika', 'nachname' => 'Mustermann'],
['email' => 'john.doe@gmail.com', 'vorname' => 'John', 'nachname' => 'Doe'],
['email' => 'jane.doe@gmail.com', 'vorname' => 'Jane', 'nachname' => 'Doe'],
['email' => 'anna.schmidt@gmx.de', 'vorname' => 'Anna', 'nachname' => 'Schmidt'],
['email' => 'thomas.mueller@gmx.de', 'vorname' => 'Thomas', 'nachname' => 'Müller'],
['email' => 'sabine.fischer@outlook.com', 'vorname' => 'Sabine', 'nachname' => 'Fischer'],
['email' => 'michael.weber@outlook.com', 'vorname' => 'Michael', 'nachname' => 'Weber'],
['email' => 'julia.wagner@t-online.de', 'vorname' => 'Julia', 'nachname' => 'Wagner'],
['email' => 'stefan.becker@t-online.de', 'vorname' => 'Stefan', 'nachname' => 'Becker']
];
$passwordHash = password_hash("test12345", PASSWORD_DEFAULT);
$userInsertStmt = $db->prepare("INSERT INTO users (email, vorname, nachname, password) VALUES (:email, :vorname, :nachname, :password);");
$availableEmails = [];
foreach ($dummyUsers as $user) {
$userInsertStmt->execute([
':email' => $user['email'],
':vorname' => $user['vorname'],
':nachname' => $user['nachname'],
':password' => $passwordHash
]);
$availableEmails[] = $user['email'];
}
return $availableEmails;
}
/**
* Erstellt die 100 Fachbeiträge in der Datenbank.
*
* @param PDO $db Die aktive Datenbankverbindung
* @param array $availableEmails Pool für die Foreign-Key-Zuweisung des Autors
* @return void
*/
private function seedDummyArticles(PDO $db, array $availableEmails): void
{
$dummyArticles = [
// --- INFORMATIK & MATHE (1-20) ---
1 => ["Satz des Pythagoras", "Der Satz des Pythagoras beschreibt das Verhältnis der Seitenlängen in einem rechtwinkligen Dreieck.", "mathe", "Dreiecke, Geometrie"],
2 => ["Tunneleffekt", "Der Tunneleffekt beschreibt das quantenmechanische Phänomen, dass Teilchen Barrieren überwinden können.", "physik", "Quantenphysik, Energie"],
3 => ["Datenschutz vs Datensicherheit", "Datenschutz schützt Personen, während Datensicherheit Systeme vor unbefugten Zugriffen schützt.", "informatik", "Daten, DSGVO"],
4 => ["Einführung in Algorithmen", "Ein Algorithmus ist eine präzise Handlungsanweisung zur Lösung eines vordefinierten Problems.", "informatik", "Code, Logik"],
5 => ["Primzahlen im Detail", "Primzahlen sind natürliche Zahlen, die nur durch eins und sich selbst ohne Rest teilbar sind.", "mathe", "Zahlentheorie"],
6 => ["Lineare Algebra", "Vektoren und Matrizen bilden das Fundament für moderne Computergrafik und 3D-Engines.", "mathe", "Matrizen, Vektoren"],
7 => ["Objektorientierte Programmierung", "Die OOP nutzt Klassen und Objekte, um reale Strukturen im Quellcode abzubilden.", "informatik", "OOP, Klassen"],
8 => ["Grundlagen von HTML und CSS", "HTML strukturiert den Inhalt einer Webseite, während CSS für das optische Design zuständig ist.", "informatik", "Web, Frontend"],
9 => ["Die Relativitätstheorie", "Einsteins Theorie revolutionierte unser Verständnis von Raum, Zeit und Gravitation massiv.", "physik", "Einstein, Gravitation"],
10 => ["Datenbanken und SQL", "Strukturierte Abfragesprachen erlauben das effiziente Speichern und Abrufen großer Datenmengen.", "informatik", "SQL, Datenbank"],
11 => ["Wahrscheinlichkeitsrechnung", "Die Stochastik befasst sich mit der mathematischen Analyse von Zufallsexperimenten.", "mathe", "Zufall, Stochastik"],
12 => ["Quantencomputing", "Künftige Quantencomputer nutzen Qubits, um komplexe Berechnungen in Rekordzeit zu lösen.", "informatik", "Hardware, Zukunft"],
13 => ["Die Fibonacci-Folge", "Diese Zahlenreihe beschreibt mathematische Wachstumsmuster, die oft in der Natur vorkommen.", "mathe", "Zahlen, Natur"],
14 => ["Thermodynamik", "Die Hauptsätze der Thermodynamik regeln den Energieaustausch und die Entropie in Systemen.", "physik", "Energie, Wärme"],
15 => ["Cybersecurity Grundlagen", "Die Absicherung von Netzwerken erfordert Firewalls, Verschlüsselung und regelmäßige Audits.", "informatik", "Sicherheit, Hacker"],
16 => ["Kryptographie", "Asymmetrische Verschlüsselungsverfahren sichern heutzutage den gesamten Datenverkehr im Web.", "informatik", "Krypto, Security"],
17 => ["Analysis und Ableitungen", "Die Differentialrechnung untersucht die lokalen Änderungsraten von mathematischen Funktionen.", "mathe", "Analysis, Funktionen"],
18 => ["Schwarze Löcher", "Diese Regionen im Raum besitzen eine so starke Gravitation, dass selbst Licht nicht entkommt.", "physik", "Astronomie, Kosmos"],
19 => ["Git Versionsverwaltung", "Git erlaubt Entwicklern das parallele Arbeiten an Code-Projekten ohne Datenverlust.", "informatik", "Git, DevOps"],
20 => ["Künstliche Intelligenz", "Neuronale Netze versuchen das menschliche Gehirn für maschinelles Lernen nachzubilden.", "informatik", "KI, Software"],
// --- CHEMIE & BIOLOGIE (21-40) ---
21 => ["Das Periodensystem", "Die Elemente sind nach ihrer Ordnungszahl und chemischen Eigenschaften geordnet.", "chemie", "Elemente, Moleküle"],
22 => ["Photosynthese", "Pflanzen wandeln mithilfe von Sonnenlicht Kohlendioxid und Wasser in Glukose und Sauerstoff um.", "biologie", "Pflanzen, Energie"],
23 => ["Aufbau einer Zelle", "Die Zelle ist die kleinste lebende Einheit aller Organismen mit spezialisierten Organellen.", "biologie", "Zellen, Biologie"],
24 => ["Säuren und Basen", "Der pH-Wert misst die Konzentration von Wasserstoff-Ionen in einer wässrigen Lösung.", "chemie", "Labor, pH-Wert"],
25 => ["Die DNA-Struktur", "Die Doppelhelix enthält den genetischen Bauplan für die Entwicklung aller Lebewesen.", "biologie", "Genetik, Erbgut"],
26 => ["Chemische Bindungen", "Kovalente Bindungen entstehen durch das Teilen von Elektronenpaaren zwischen Atomen.", "chemie", "Atome, Bindung"],
27 => ["Das Immunsystem", "Weiße Blutkörperchen und Antikörper schützen den menschlichen Körper vor Krankheitserregern.", "biologie", "Gesundheit, Abwehr"],
28 => ["Katalysatoren", "Katalysatoren beschleunigen chemische Reaktionen, ohne dabei selbst verbraucht zu werden.", "chemie", "Reaktion, Chemie"],
29 => ["Ökosystem Wald", "Das Zusammenspiel von Flora, Fauna und Klima bildet ein hochsensibles ökologisches System.", "biologie", "Natur, Wald"],
30 => ["Die Mendelschen Regeln", "Diese Grundgesetze der Vererbung beschreiben, wie Merkmale an Nachkommen weitergegeben werden.", "biologie", "Genetik, Erbung"],
31 => ["Zustandsformen der Materie", "Fest, flüssig und gasförmig sind die klassischen Aggregatzustände von Stoffen.", "chemie", "Physik, Materie"],
32 => ["Evolutionstheorie", "Charles Darwin begründete die Theorie der natürlichen Auslese und Anpassung von Arten.", "biologie", "Darwin, Evolution"],
33 => ["Organische Chemie", "Die Chemie der Kohlenstoffverbindungen bildet die Basis für alles bekannte Leben.", "chemie", "Kohlenstoff, Chemie"],
34 => ["Das menschliche Gehirn", "Milliarden von Neuronen kommunizieren über Synapsen, um Reize und Gedanken zu verarbeiten.", "biologie", "Neurologie, Nerven"],
35 => ["Der Wasserkreislauf", "Verdunstung, Kondensation und Niederschlag halten das Wasser auf der Erde in Bewegung.", "geographie", "Wasser, Klima"],
36 => ["Aggregatzustände von Wasser", "Wasser zeigt ungewöhnliche Eigenschaften wie die Dichteanomalie beim Gefrieren.", "chemie", "Wasser, Eis"],
37 => ["Blutkreislauf des Menschen", "Das Herz pumpt sauerstoffreiches Blut durch Arterien in alle Organe des Körpers.", "biologie", "Herz, Medizin"],
38 => ["Das Ohmsche Gesetz", "Es beschreibt den direkten Zusammenhang zwischen Spannung, Stromstärke und Widerstand.", "physik", "Strom, Elektronik"],
39 => ["Plattentektonik", "Die Bewegung der Kontinentalplatten führt zu Erdbeben, Vulkanismus und Gebirgsbildung.", "geographie", "Erde, Geologie"],
40 => ["Proteine und Enzyme", "Enzyme wirken als Biokatalysatoren und steuern fast alle Stoffwechselprozesse.", "biologie", "Biochemie, Enzyme"],
// --- GESCHICHTE & WIRTSCHAFT (41-60) ---
41 => ["Das Römische Reich", "Vom Stadtstaat zum Weltreich prägte Rom die Rechts- und Kulturgeschichte Europas.", "geschichte", "Antike, Rom"],
42 => ["Die Französische Revolution", "Freiheit, Gleichheit, Brüderlichkeit beendeten 1789 die absolute Monarchie in Frankreich.", "geschichte", "Europa, Politik"],
43 => ["Inflation erklärt", "Inflation bezeichnet die kontinuierliche Geldentwertung und den Kaufkraftverlust.", "wirtschaft", "Geld, Finanzen"],
44 => ["Die Industrielle Revolution", "Die Erfindung der Dampfmaschine veränderte die Produktion und die Gesellschaft tiefgreifend.", "geschichte", "Industrie, Arbeit"],
45 => ["Angebot und Nachfrage", "Dieses fundamentale Marktgesetz bestimmt den Preis von Gütern in einer freien Wirtschaft.", "wirtschaft", "Markt, Preise"],
46 => ["Der Buchdruck", "Johannes Gutenbergs Erfindung revolutionierte die Verbreitung von Wissen im Mittelalter.", "geschichte", "Medien, Wissen"],
47 => ["Die Entdeckung Amerikas", "Kolumbus' Seereise im Jahr 1492 leitete das Zeitalter der Kolonialisierung ein.", "geschichte", "Entdeckung, Seefahrt"],
48 => ["Kryptowährungen", "Bitcoin nutzt Blockchain-Technologie, um dezentralen digitalen Werttransfer zu erlauben.", "wirtschaft", "Blockchain, Finanzen"],
49 => ["Das antike Griechenland", "Die Wiege der Demokratie und Philosophie brachte Denker wie Sokrates und Platon hervor.", "geschichte", "Antike, Philosophie"],
50 => ["Globalisierung", "Die weltweite Verflechtung in Wirtschaft, Kultur und Politik bringt Chancen und Risiken.", "wirtschaft", "Weltwirtschaft, Handel"],
51 => ["Der Dreißigjährige Krieg", "Ein religiöser und politischer Konflikt verwüstete zwischen 1618 und 1648 Mitteleuropa.", "geschichte", "Krieg, Europa"],
52 => ["Die Weimarer Republik", "Die erste deutsche Demokratie scheiterte an wirtschaftlichen und politischen Krisen.", "geschichte", "Deutschland, Weimar"],
53 => ["Aktien und Börse", "Unternehmen beschaffen sich Kapital durch die Ausgabe von Anteilen an Investoren.", "wirtschaft", "Aktien, Investieren"],
54 => ["Das alte Ägypten", "Pharaonen, Pyramiden und Hieroglyphen zeugen von einer hochentwickelten Hochkultur am Nil.", "geschichte", "Ägypten, Antike"],
55 => ["Der Kalte Krieg", "Das Wettrüsten zwischen USA und UdSSR prägte die globale Politik der Nachkriegszeit.", "geschichte", "Ost-West, Politik"],
56 => ["Zentralbanken und Leitzins", "Durch Zinsänderungen steuern Notenbanken die Geldmenge und bekämpfen Inflation.", "wirtschaft", "Zinsen, Geldpolitik"],
57 => ["Die Seidenstraße", "Das historische Netzwerk von Handelsrouten verband über Jahrhunderte Asien und Europa.", "geschichte", "Handel, Asien"],
58 => ["Das Mittelalter", "Ritter, Burgen und das Feudalsystem prägten diese tausendjährige Epoche Europas.", "geschichte", "Mittelalter, Feudalismus"],
59 => ["Planwirtschaft vs Marktwirtschaft", "Zentrale staatliche Steuerung steht dem freien Spiel der Marktkräfte gegenüber.", "wirtschaft", "Systeme, Wirtschaft"],
60 => ["Die Berliner Mauer", "Ihr Bau 1961 zementierte die Teilung Deutschlands, ihr Fall 1989 beendete sie.", "geschichte", "DDR, Wiedervereinigung"],
// --- ANWENDUNGEN & WEITERE THEMEN (61-100) ---
61 => ["Cloud Computing", "Das Auslagern von Rechenleistung in das Internet spart lokale IT-Infrastruktur ein.", "informatik", "Cloud, Web"],
62 => ["Responsive Webdesign", "Moderne Webseiten passen ihr Layout dynamisch an Smartphones und Desktops an.", "informatik", "Design, CSS"],
63 => ["Der Treibhauseffekt", "Gase in der Atmosphäre verhindern das Entweichen von Wärme ins Weltall.", "physik", "Klima, Umwelt"],
64 => ["Mechanik und Kräfte", "Die Newtonschen Axiome beschreiben, wie Kräfte auf Körper wirken und sie bewegen.", "physik", "Newton, Kraft"],
65 => ["Die Mendelschen Gesetze", "Die Vererbung von Genen folgt klaren statistischen Wahrscheinlichkeiten.", "biologie", "Genetik, Erbsen"],
66 => ["Lichtgeschwindigkeit", "Im Vakuum bewegt sich Licht mit knapp 300.000 Kilometern pro Sekunde.", "physik", "Licht, Relativität"],
67 => ["Die Funktion von APIs", "Programmierschnittstellen erlauben den Datenaustausch zwischen verschiedenen Systemen.", "informatik", "API, Schnittstelle"],
68 => ["Der Goldstandard", "Ein historischen Währungssystem, bei dem Geld durch echtes Gold gedeckt war.", "wirtschaft", "Gold, Währung"],
69 => ["Der Wiener Kongress", "1815 ordneten die europäischen Mächte die Landkarte nach den Napoleonischen Kriegen neu.", "geschichte", "Europa, Diplomatie"],
70 => ["Integrierte Schaltkreise", "Mikrochips enthalten Millionen Transistoren auf kleinstem Raum für Logikschaltungen.", "informatik", "Hardware, Chips"],
71 => ["Die Magellan-Expedition", "Die erste erfolgreiche Weltumsegelung bewies praktisch die Kugelgestalt der Erde.", "geschichte", "Seefahrt, Erde"],
72 => ["Das Internet der Dinge", "Alltagsgegenstände werden vernetzt, um smarte Automatisierungen zu ermöglichen.", "informatik", "IoT, SmartHome"],
73 => ["Halbwertszeit", "Die Zeitspanne, in der sich die Hälfte der instabilen Atome radioaktiv abbaut.", "physik", "Atomphysik, Strahlung"],
74 => ["Elektromagnetismus", "Die Verknüpfung von elektrischen Strömen und magnetischen Feldern treibt Motoren an.", "physik", "Strom, Magnet"],
75 => ["Das Ökosystem Meer", "Ozeane regulieren das Weltklima und bieten Lebensraum für unzählige Arten.", "biologie", "Meer, Ökologie"],
76 => ["Einführung in Docker", "Containerisierung isoliert Anwendungen samt Abhängigkeiten für stabilen Betrieb.", "informatik", "Docker, DevOps"],
77 => ["Das Römische Recht", "Viele moderne europäische Gesetzbücher basieren auf antiken römischen Rechtsprinzipien.", "geschichte", "Recht, Gesetz"],
78 => ["Verhaltensbiologie", "Untersuchung von angeborenen und erlernten Verhaltensweisen bei Mensch und Tier.", "biologie", "Verhalten, Tiere"],
79 => ["Verschlüsselung im Alltag", "HTTPS schützt Passwörter und Zahlungsdaten beim Surfen vor dem Mitlesen.", "informatik", "Web, HTTPS"],
80 => ["Die Magna Carta", "1215 schränkte dieses Dokument die absolute Macht des englischen Königs ein.", "geschichte", "England, Verfassung"],
81 => ["Marktversagen", "Wenn der freie Markt Ressourcen unvollständig verteilt, muss der Staat eingreifen.", "wirtschaft", "Markt, Staat"],
82 => ["Optische Linsen", "Konvexe und konkave Linsen brechen Licht für Brillen, Mikroskope und Kameras.", "physik", "Optik, Licht"],
83 => ["Die Entstehung der Erde", "Vor rund 4,5 Milliarden Jahren ballte sich kosmischer Staub zu unserem Planeten.", "geographie", "Erde, Kosmos"],
84 => ["Grundlagen von JavaScript", "Diese Skriptsprache macht statische Webseiten interaktiv und dynamisch nutzbar.", "informatik", "JS, Webentwicklung"],
85 => ["Die industrielle Landwirtschaft", "Moderne Techniken sichern Welternährung, belasten jedoch oft die Umwelt.", "biologie", "Landwirtschaft, Umwelt"],
86 => ["Das Schwarze Jahr 1929", "Der New Yorker Börsencrash löste die verheerende Weltwirtschaftskrise aus.", "geschichte", "Krise, Finanzen"],
87 => ["Die Evolution des Menschen", "Der Stammbaum des Homo Sapiens entwickelte sich über Millionen Jahre in Afrika.", "biologie", "Mensch, Evolution"],
88 => ["Einführung in Linux", "Das Open-Source-Betriebssystem bildet das Rückgrat moderner Server-Infrastrukturen.", "informatik", "Linux, OS"],
89 => ["Der Urknall", "Die Urknalltheorie beschreibt den Beginn des Universums aus einer Singularität.", "physik", "Astronomie, Urknall"],
90 => ["Das Periodensystem der Elemente", "Dmitri Mendelejew ordnete Elemente logisch nach ihren Atommassen.", "chemie", "Periodensystem, Chemie"],
91 => ["Die Hanse", "Ein mächtiger mittelalterlicher Bund von Kaufleuten dominierte den Nordseehandel.", "geschichte", "Handel, Mittelalter"],
92 => ["Wirtschaftswachstum", "Die Steigerung des Bruttoinlandsprodukts gilt oft als Indikator für Wohlstand.", "wirtschaft", "BIP, Finanzen"],
93 => ["Neuronale Netze", "Diese Strukturen lernen durch mathematische Gewichtung aus riesigen Datenmengen.", "informatik", "KI, Mathematik"],
94 => ["Der Erste Weltkrieg", "Der globale Konflikt von 1914 bis 1918 zerstörte das alte europäische Machtgefüge.", "geschichte", "Europa, Krieg"],
95 => ["Das Gehirn und Hormone", "Botenstoffe steuern Gefühle, Schlafzyklen und Reaktionen des Körpers.", "biologie", "Medizin, Hormone"],
96 => ["SQL Joins erklärt", "Joins verknüpfen Daten aus mehreren Tabellen über gemeinsame Schlüssel.", "informatik", "SQL, Datenbanken"],
97 => ["Wellen-Teilchen-Dualismus", "Quantenobjekte zeigen je nach Messaufbau Eigenschaften von Wellen oder Teilchen.", "physik", "Quanten, Licht"],
98 => ["Der Absolutismus", "Der Sonnenkönig Ludwig XIV. verkörperte die unbeschränkte Herrschaft des Monarchen.", "geschichte", "Frankreich, Monarchie"],
99 => ["Die Funktion von Routern", "Netzwerkgeräte leiten Datenpakete über IP-Adressen an den richtigen Empfänger.", "informatik", "Netzwerk, Internet"],
100 => ["Die Entstehung des Geldes", "Vom Tauschhandel über Naturalgeld bis hin zu modernen digitalen Fiat-Währungen.", "wirtschaft", "Geld, Geschichte"]
];
$articleInsertStmt = $db->prepare("
INSERT INTO articles (title, content, author, category, tags)
VALUES (:title, :content, :author, :category, :tags);");
foreach ($dummyArticles as $article) {
// Bestimmt per Zufall einen Autor aus dem Pool der gültigen E-Mails
$randomAuthor = $availableEmails[array_rand($availableEmails)];
// Text in (blocks[i][type]/[text]/[image])-Format umwandeln:
$content = json_encode([
['type' => 'text', 'value' => $article[1]]
], JSON_UNESCAPED_UNICODE);
$articleInsertStmt->execute([
':title' => $article[0],
':content' => $content,
':author' => $randomAuthor,
':category' => $article[2],
':tags' => $article[3]
]);
}
}
}
+7 -24
View File
@@ -1,5 +1,5 @@
<?php <?php
require_once 'DatabaseInitializer.php';
require_once "UserManagerDAO.php"; require_once "UserManagerDAO.php";
/** /**
@@ -11,6 +11,7 @@ require_once "UserManagerDAO.php";
class DatabaseUserManager implements UserManagerDAO { class DatabaseUserManager implements UserManagerDAO {
private static $instance = null; private static $instance = null;
private $dbPath;
/** /**
* Konstruktor. * Konstruktor.
@@ -22,23 +23,8 @@ class DatabaseUserManager implements UserManagerDAO {
*/ */
public function __construct() public function __construct()
{ {
try { $this->dbPath = __DIR__ . '/../../db/eduforgeDB.db';
$db = $this->getConnection(); DatabaseInitializer::initialize($this->dbPath);
$db->exec("
CREATE TABLE IF NOT EXISTS users (
email TEXT PRIMARY KEY,
vorname TEXT NOT NULL,
nachname TEXT NOT NULL,
password TEXT NOT NULL
);
");
unset($db);
} catch (PDOException $e) {
throw new RuntimeException("Benutzerdatenbank konnte nicht erstellt werden.");
}
} }
/** /**
@@ -51,15 +37,12 @@ class DatabaseUserManager implements UserManagerDAO {
private function getConnection() private function getConnection()
{ {
try { try {
$dsn = 'sqlite:' . __DIR__ . '/../../db/users.db'; $db = new PDO('sqlite:' . $this->dbPath, null, null);
$db = new PDO($dsn, null, null);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('PRAGMA foreign_keys = ON;');
return $db; return $db;
} catch (PDOException $e) { } catch (PDOException $e) {
throw new RuntimeException("Verbindung zur Benutzerdatenbank fehlgeschlagen."); throw new RuntimeException("internal_error");
} }
} }
+3 -1
View File
@@ -211,9 +211,11 @@ class LocalArticleManager implements ArticleManagerDAO {
foreach ($articles as $article) { foreach ($articles as $article) {
$title = isset($article['title']) ? strtolower((string)$article['title']) : ''; $title = isset($article['title']) ? strtolower((string)$article['title']) : '';
$content = isset($article['content']) ? strtolower((string)$article['content']) : ''; $content = isset($article['content']) ? strtolower((string)$article['content']) : '';
$tags = isset($article['tags']) ? strtolower((string)$article['tags']) : '';
if (($cleanKeyword !== '' && strpos($title, $cleanKeyword) !== false) || if (($cleanKeyword !== '' && strpos($title, $cleanKeyword) !== false) ||
($cleanKeyword !== '' && strpos($content, $cleanKeyword) !== false)) { ($cleanKeyword !== '' && strpos($content, $cleanKeyword) !== false) ||
($cleanKeyword !== '' && strpos($tags, $cleanKeyword) !== false)) {
$likes = isset($article['likes']) && is_array($article['likes']) ? $article['likes'] : []; $likes = isset($article['likes']) && is_array($article['likes']) ? $article['likes'] : [];
$filteredArticles[] = new Article( $filteredArticles[] = new Article(
+1 -31
View File
@@ -12,38 +12,8 @@ require_once "DatabaseUserManager.php";
class UserManager { class UserManager {
public static function getInstance() { public static function getInstance() {
$userManager = DatabaseUserManager::getInstance();
/* return DatabaseUserManager::getInstance();
* Dummy-User anlegen, falls sie noch nicht existieren.
* Passwort für alle User: test12345
*/
$dummyUsers = [
['email' => 'max.mustermann@web.de', 'vorname' => 'Max', 'nachname' => 'Mustermann'],
['email' => 'erika.mustermann@web.de', 'vorname' => 'Erika', 'nachname' => 'Mustermann'],
['email' => 'john.doe@gmail.com', 'vorname' => 'John', 'nachname' => 'Doe'],
['email' => 'jane.doe@gmail.com', 'vorname' => 'Jane', 'nachname' => 'Doe'],
['email' => 'anna.schmidt@gmx.de', 'vorname' => 'Anna', 'nachname' => 'Schmidt'],
['email' => 'thomas.mueller@gmx.de', 'vorname' => 'Thomas', 'nachname' => 'Müller'],
['email' => 'sabine.fischer@outlook.com', 'vorname' => 'Sabine', 'nachname' => 'Fischer'],
['email' => 'michael.weber@outlook.com', 'vorname' => 'Michael', 'nachname' => 'Weber'],
['email' => 'julia.wagner@t-online.de', 'vorname' => 'Julia', 'nachname' => 'Wagner'],
['email' => 'stefan.becker@t-online.de', 'vorname' => 'Stefan', 'nachname' => 'Becker']
];
$passwordHash = password_hash("test12345", PASSWORD_DEFAULT);
foreach ($dummyUsers as $user) {
if ($userManager->findUser($user['email']) == null) {
$userManager->addUser(
$user['email'],
$user['vorname'],
$user['nachname'],
$passwordHash
);
}
}
return $userManager;
} }
} }
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* Prüft, ob eine Suchanfrage gültig ist.
*
* Erlaubt werden Buchstaben, Zahlen, Umlaute, typische Satzzeichen und Leerzeichen.
* Die Länge muss zwischen 1 und 50 Zeichen liegen.
*
* @param string $query Zu prüfender Suchbegriff
*
* @return bool true wenn die Suche gültig ist, sonst false
*/
function searchQueryValidator($query)
{
$query = trim($query);
// Mindestens 1 Zeichen, maximal 50 Zeichen
$length = mb_strlen($query);
if ($length < 1 || $length > 50) {
return false;
}
// Erlaubt Buchstaben (inkl. Umlaut/ß), Zahlen, Leerzeichen sowie ?, !, ., -, _
$searchPattern = '/^[a-zA-Z0-9äöüÄÖÜß\s?!.,\-_]+$/u';
return preg_match($searchPattern, $query) === 1;
}
/**
* Prüft, ob das gewählte Treffer-Limit erlaubt ist.
*
* @param int|string $limit Das zu prüfende Limit
*
* @return bool true wenn das Limit 10, 20, 50 oder 100 ist, sonst false
*/
function searchLimitValidator($limit)
{
$allowedLimits = [10, 20, 50, 100];
return in_array((int)$limit, $allowedLimits, true);
}