Compare commits

...

114 Commits

Author SHA1 Message Date
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
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
niklas.ortmann 6897a29574 Update search-results-data.php 2026-07-18 17:23:30 +02:00
niklas.ortmann 8836034670 Kategoriepaginator 2026-07-18 17:17:44 +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
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 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
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
caroline.slt cc5f46c00e Reviewänderungen 2026-06-30 20:25:46 +02:00
rirat-0 1b2ab1cccb rechtschreibung 2026-06-30 20:25:46 +02:00
rirat-0 adbf488e01 datenschutz und impressum wurden ersten, die readme entsprechend angepasst 2026-06-30 20:25:46 +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
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
caroline.slt 6c1a893f80 Passwort vergessen Funktion 2026-06-26 00:05:45 +02:00
caroline.slt 788e803e84 Passwort vergessen Funktion 2026-06-25 23:45:17 +02:00
caroline.slt 4d0f1de01c Passwort vergessen Funktion 2026-06-25 23:40:33 +02:00
caroline.slt e71ee8300e Qualitätsanpassungen 2026-06-25 23:23:55 +02:00
caroline.slt f39885da10 CSS Anpassungen 2026-06-25 23:05:49 +02:00
caroline.slt b7ca5bcfe2 test rückgängig machen 2026-06-25 22:57:35 +02:00
caroline.slt 57b0782577 test 2026-06-25 22:53:45 +02:00
caroline.slt 4f524db221 Implementierung 2026-06-25 22:46:14 +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
39 changed files with 2142 additions and 227 deletions
+6
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
@@ -34,3 +39,4 @@
## Sonstiges ## Sonstiges
- Das Datenschema befindet sich unter /planung/Datenschema.pdf - Das Datenschema befindet sich unter /planung/Datenschema.pdf
- Im Impressum, sowie der Datenschutzerklaerung wurden Dummy-Daten verwendet
+1 -2
View File
@@ -14,7 +14,6 @@
?> ?>
<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>
+30
View File
@@ -0,0 +1,30 @@
<?php
/** @var string $title */
/** @var string $message */
/** @var string $link */
/** @var bool $isSuccess */
$title = $title ?? "Passwort zurücksetzen";
$message = $message ?? "Der Bestätigungslink konnte nicht verarbeitet werden.";
$link = $link ?? "";
$isSuccess = $isSuccess ?? false;
?>
<!--
Ansicht: Passwortbestätigung
Funktion: Zeigt das Ergebnis der Passwortänderung an.
-->
<main class="login-page">
<div class="login-container">
<h1><?php echo htmlspecialchars($title); ?></h1>
<p class="alert-message <?php echo $isSuccess ? 'is-success' : 'is-error'; ?> confirm-message">
<?php echo htmlspecialchars($message); ?>
</p>
<?php echo $link; ?>
</div>
</main>
+29
View File
@@ -0,0 +1,29 @@
<?php
/** @var string $title */
/** @var string $message */
/** @var string $link */
/** @var bool $isSuccess */
$title = $title ?? "Registrierung";
$message = $message ?? "Der Registrierungslink konnte nicht verarbeitet werden.";
$link = $link ?? "";
$isSuccess = $isSuccess ?? false;
?>
<!--
Ansicht: Registrierungsbestätigung
Funktion: Zeigt das Ergebnis der Registrierungsbestätigung an.
-->
<main class="login-page">
<div class="login-container">
<h1><?php echo htmlspecialchars($title); ?></h1>
<p class="alert-message <?php echo $isSuccess ? 'is-success' : 'is-error'; ?> confirm-message">
<?php echo htmlspecialchars($message); ?>
</p>
<?php echo $link; ?>
</div>
</main>
+180 -9
View File
@@ -4,15 +4,186 @@
--> -->
<main> <main>
<h1>Datenschutz</h1> <h1>Datenschutz&shy;erkl&auml;rung</h1>
<h2>1. Datenschutz auf einen Blick</h2>
<h3>Allgemeine Hinweise</h3> <p>Die folgenden Hinweise geben einen einfachen &Uuml;berblick dar&uuml;ber,
was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind
alle Daten, mit denen Sie pers&ouml;nlich identifiziert werden k&ouml;nnen. Ausf&uuml;hrliche Informationen zum
Thema Datenschutz entnehmen Sie unserer unter diesem Text aufgef&uuml;hrten
Datenschutzerkl&auml;rung.</p>
<h3>Datenerfassung auf dieser Website</h3> <h4>Wer ist verantwortlich f&uuml;r die Datenerfassung auf dieser
Website?</h4> <p>Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen
Kontaktdaten k&ouml;nnen Sie dem Abschnitt &bdquo;Hinweis zur verantwortlichen Stelle&ldquo; in dieser
Datenschutzerkl&auml;rung entnehmen.</p> <h4>Wie erfassen wir Ihre Daten?</h4> <p>Ihre Daten werden zum
einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es sich z.&nbsp;B. um Daten handeln, die Sie in ein
Kontaktformular eingeben.</p> <p>Andere Daten werden automatisch oder nach Ihrer Einwilligung beim Besuch
der Website durch unsere IT-Systeme erfasst. Das sind vor allem technische Daten (z.&nbsp;B. Internetbrowser,
Betriebssystem oder Uhrzeit des Seitenaufrufs). Die Erfassung dieser Daten erfolgt automatisch, sobald Sie diese
Website betreten.</p> <h4>Wof&uuml;r nutzen wir Ihre Daten?</h4> <p>Ein Teil der Daten wird erhoben, um eine
fehlerfreie Bereitstellung der Website zu gew&auml;hrleisten. Andere Daten k&ouml;nnen zur Analyse Ihres
Nutzerverhaltens verwendet werden. Sofern &uuml;ber die Website Vertr&auml;ge geschlossen oder angebahnt
werden k&ouml;nnen, werden die &uuml;bermittelten Daten auch f&uuml;r Vertragsangebote, Bestellungen oder
sonstige Auftragsanfragen verarbeitet.</p> <h4>Welche Rechte haben Sie bez&uuml;glich Ihrer Daten?</h4>
<p>Sie haben jederzeit das Recht, unentgeltlich Auskunft &uuml;ber Herkunft, Empf&auml;nger und Zweck Ihrer
gespeicherten personenbezogenen Daten zu erhalten. Sie haben au&szlig;erdem ein Recht, die Berichtigung oder
L&ouml;schung dieser Daten zu verlangen. Wenn Sie eine Einwilligung zur Datenverarbeitung erteilt haben,
k&ouml;nnen Sie diese Einwilligung jederzeit f&uuml;r die Zukunft widerrufen. Au&szlig;erdem haben Sie das Recht,
unter bestimmten Umst&auml;nden die Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen Daten zu
verlangen. Des Weiteren steht Ihnen ein Beschwerderecht bei der zust&auml;ndigen Aufsichtsbeh&ouml;rde
zu.</p> <p>Hierzu sowie zu weiteren Fragen zum Thema Datenschutz k&ouml;nnen Sie sich jederzeit an uns
wenden.</p>
<h2>2. Hosting</h2>
<p>Wir hosten die Inhalte unserer Website bei folgendem Anbieter:</p>
<h3>Externes Hosting</h3> <p>Diese Website wird extern gehostet. Die personenbezogenen Daten, die auf dieser
Website erfasst werden, werden auf den Servern des Hosters / der Hoster gespeichert. Hierbei kann es sich
v.&nbsp;a. um IP-Adressen, Kontaktanfragen, Meta- und Kommunikationsdaten, Vertragsdaten, Kontaktdaten,
Namen, Websitezugriffe und sonstige Daten, die &uuml;ber eine Website generiert werden, handeln.</p> <p>Das
externe Hosting erfolgt zum Zwecke der Vertragserf&uuml;llung gegen&uuml;ber unseren potenziellen und
bestehenden Kunden (Art. 6 Abs. 1 lit. b DSGVO) und im Interesse einer sicheren, schnellen und effizienten
Bereitstellung unseres Online-Angebots durch einen professionellen Anbieter (Art. 6 Abs. 1 lit. f DSGVO). Sofern
eine entsprechende Einwilligung abgefragt wurde, erfolgt die Verarbeitung ausschlie&szlig;lich auf Grundlage von
Art. 6 Abs. 1 lit. a DSGVO und &sect; 25 Abs. 1 TDDDG, soweit die Einwilligung die Speicherung von Cookies oder
den Zugriff auf Informationen im Endger&auml;t des Nutzers (z.&nbsp;B. Device-Fingerprinting) im Sinne des
TDDDG umfasst. Die Einwilligung ist jederzeit widerrufbar.</p> <p>Unser(e) Hoster wird bzw. werden Ihre Daten
nur insoweit verarbeiten, wie dies zur Erf&uuml;llung seiner Leistungspflichten erforderlich ist und unsere
Weisungen in Bezug auf diese Daten befolgen.</p> <p>Wir setzen folgende(n) Hoster ein:</p>
<p>-Platzhalter-</p>
<h4>Auftragsverarbeitung</h4> <p>Wir haben einen Vertrag &uuml;ber Auftragsverarbeitung (AVV) zur Nutzung
des oben genannten Dienstes geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen
Vertrag, der gew&auml;hrleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nach
unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.</p>
<h2>3. Allgemeine Hinweise und Pflicht&shy;informationen</h2>
<h3>Datenschutz</h3> <p>Die Betreiber dieser Seiten nehmen den Schutz Ihrer pers&ouml;nlichen Daten sehr
ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend den gesetzlichen
Datenschutzvorschriften sowie dieser Datenschutzerkl&auml;rung.</p> <p>Wenn Sie diese Website benutzen,
werden verschiedene personenbezogene Daten erhoben. Personenbezogene Daten sind Daten, mit denen Sie
pers&ouml;nlich identifiziert werden k&ouml;nnen. Die vorliegende Datenschutzerkl&auml;rung erl&auml;utert,
welche Daten wir erheben und wof&uuml;r wir sie nutzen. Sie erl&auml;utert auch, wie und zu welchem Zweck das
geschieht.</p> <p>Wir weisen darauf hin, dass die Daten&uuml;bertragung im Internet (z.&nbsp;B. bei der
Kommunikation per E-Mail) Sicherheitsl&uuml;cken aufweisen kann. Ein l&uuml;ckenloser Schutz der Daten vor
dem Zugriff durch Dritte ist nicht m&ouml;glich.</p>
<h3>Hinweis zur verantwortlichen Stelle</h3> <p>Die verantwortliche Stelle f&uuml;r die Datenverarbeitung auf
dieser Website ist:</p> <p>Ammerlaender Heerstrasse, 114-118</p>
<p>Telefon: 0123456789<br />
E-Mail: GruppeC@uol.de</p>
<p>Verantwortliche Stelle ist die nat&uuml;rliche oder juristische Person, die allein oder gemeinsam mit anderen
&uuml;ber die Zwecke und Mittel der Verarbeitung von personenbezogenen Daten (z.&nbsp;B. Namen, E-Mail-
Adressen o. &Auml;.) entscheidet.</p>
<h3>Speicherdauer</h3> <p>Soweit innerhalb dieser Datenschutzerkl&auml;rung keine speziellere Speicherdauer
genannt wurde, verbleiben Ihre personenbezogenen Daten bei uns, bis der Zweck f&uuml;r die Datenverarbeitung
entf&auml;llt. Wenn Sie ein berechtigtes L&ouml;schersuchen geltend machen oder eine Einwilligung zur
Datenverarbeitung widerrufen, werden Ihre Daten gel&ouml;scht, sofern wir keine anderen rechtlich
zul&auml;ssigen Gr&uuml;nde f&uuml;r die Speicherung Ihrer personenbezogenen Daten haben (z.&nbsp;B. steuer-
oder handelsrechtliche Aufbewahrungsfristen); im letztgenannten Fall erfolgt die L&ouml;schung nach Fortfall
dieser Gr&uuml;nde.</p>
<h3>Allgemeine Hinweise zu den Rechtsgrundlagen der Datenverarbeitung auf dieser Website</h3> <p>Sofern Sie
in die Datenverarbeitung eingewilligt haben, verarbeiten wir Ihre personenbezogenen Daten auf Grundlage von Art.
6 Abs. 1 lit. a DSGVO bzw. Art. 9 Abs. 2 lit. a DSGVO, sofern besondere Datenkategorien nach Art. 9 Abs. 1 DSGVO
verarbeitet werden. Im Falle einer ausdr&uuml;cklichen Einwilligung in die &Uuml;bertragung personenbezogener
Daten in Drittstaaten erfolgt die Datenverarbeitung au&szlig;erdem auf Grundlage von Art. 49 Abs. 1 lit. a DSGVO.
Sofern Sie in die Speicherung von Cookies oder in den Zugriff auf Informationen in Ihr Endger&auml;t (z.&nbsp;B. via
Device-Fingerprinting) eingewilligt haben, erfolgt die Datenverarbeitung zus&auml;tzlich auf Grundlage von &sect;
25 Abs. 1 TDDDG. Die Einwilligung ist jederzeit widerrufbar. Sind Ihre Daten zur Vertragserf&uuml;llung oder zur
Durchf&uuml;hrung vorvertraglicher Ma&szlig;nahmen erforderlich, verarbeiten wir Ihre Daten auf Grundlage des
Art. 6 Abs. 1 lit. b DSGVO. Des Weiteren verarbeiten wir Ihre Daten, sofern diese zur Erf&uuml;llung einer
rechtlichen Verpflichtung erforderlich sind, auf Grundlage von Art. 6 Abs. 1 lit. c DSGVO. Die Datenverarbeitung
kann ferner auf Grundlage unseres berechtigten Interesses nach Art. 6 Abs. 1 lit. f DSGVO erfolgen. &Uuml;ber die
jeweils im Einzelfall einschl&auml;gigen Rechtsgrundlagen wird in den folgenden Abs&auml;tzen dieser
Datenschutzerkl&auml;rung informiert.</p>
<h3>Empf&auml;nger von personenbezogenen Daten</h3> <p>Im Rahmen unserer Gesch&auml;ftst&auml;tigkeit
arbeiten wir mit verschiedenen externen Stellen zusammen. Dabei ist teilweise auch eine &Uuml;bermittlung von
personenbezogenen Daten an diese externen Stellen erforderlich. Wir geben personenbezogene Daten nur dann an
externe Stellen weiter, wenn dies im Rahmen einer Vertragserf&uuml;llung erforderlich ist, wenn wir gesetzlich
hierzu verpflichtet sind (z.&nbsp;B. Weitergabe von Daten an Steuerbeh&ouml;rden), wenn wir ein berechtigtes
Interesse nach Art. 6 Abs. 1 lit. f DSGVO an der Weitergabe haben oder wenn eine sonstige Rechtsgrundlage die
Datenweitergabe erlaubt. Beim Einsatz von Auftragsverarbeitern geben wir personenbezogene Daten unserer
Kunden nur auf Grundlage eines g&uuml;ltigen Vertrags &uuml;ber Auftragsverarbeitung weiter. Im Falle einer
gemeinsamen Verarbeitung wird ein Vertrag &uuml;ber gemeinsame Verarbeitung geschlossen.</p>
<h3>Widerruf Ihrer Einwilligung zur Datenverarbeitung</h3> <p>Viele Datenverarbeitungsvorg&auml;nge sind
nur mit Ihrer ausdr&uuml;cklichen Einwilligung m&ouml;glich. Sie k&ouml;nnen eine bereits erteilte Einwilligung
jederzeit widerrufen. Die Rechtm&auml;&szlig;igkeit der bis zum Widerruf erfolgten Datenverarbeitung bleibt vom
Widerruf unber&uuml;hrt.</p>
<h3>Widerspruchsrecht gegen die Datenerhebung in besonderen F&auml;llen sowie gegen Direktwerbung (Art. 21
DSGVO)</h3> <p>WENN DIE DATENVERARBEITUNG AUF GRUNDLAGE VON ART. 6 ABS. 1 LIT. E ODER F
DSGVO ERFOLGT, HABEN SIE JEDERZEIT DAS RECHT, AUS GR&Uuml;NDEN, DIE SICH AUS IHRER
BESONDEREN SITUATION ERGEBEN, GEGEN DIE VERARBEITUNG IHRER PERSONENBEZOGENEN DATEN
WIDERSPRUCH EINZULEGEN; DIES GILT AUCH F&Uuml;R EIN AUF DIESE BESTIMMUNGEN GEST&Uuml;TZTES
PROFILING. DIE JEWEILIGE RECHTSGRUNDLAGE, AUF DENEN EINE VERARBEITUNG BERUHT, ENTNEHMEN
SIE DIESER DATENSCHUTZERKL&Auml;RUNG. WENN SIE WIDERSPRUCH EINLEGEN, WERDEN WIR IHRE
BETROFFENEN PERSONENBEZOGENEN DATEN NICHT MEHR VERARBEITEN, ES SEI DENN, WIR
K&Ouml;NNEN ZWINGENDE SCHUTZW&Uuml;RDIGE GR&Uuml;NDE F&Uuml;R DIE VERARBEITUNG
NACHWEISEN, DIE IHRE INTERESSEN, RECHTE UND FREIHEITEN &Uuml;BERWIEGEN ODER DIE
VERARBEITUNG DIENT DER GELTENDMACHUNG, AUS&Uuml;BUNG ODER VERTEIDIGUNG VON
RECHTSANSPR&Uuml;CHEN (WIDERSPRUCH NACH ART. 21 ABS. 1 DSGVO).</p> <p>WERDEN IHRE
PERSONENBEZOGENEN DATEN VERARBEITET, UM DIREKTWERBUNG ZU BETREIBEN, SO HABEN SIE DAS
RECHT, JEDERZEIT WIDERSPRUCH GEGEN DIE VERARBEITUNG SIE BETREFFENDER PERSONENBEZOGENER
DATEN ZUM ZWECKE DERARTIGER WERBUNG EINZULEGEN; DIES GILT AUCH F&Uuml;R DAS PROFILING,
SOWEIT ES MIT SOLCHER DIREKTWERBUNG IN VERBINDUNG STEHT. WENN SIE WIDERSPRECHEN,
WERDEN IHRE PERSONENBEZOGENEN DATEN ANSCHLIESSEND NICHT MEHR ZUM ZWECKE DER
DIREKTWERBUNG VERWENDET (WIDERSPRUCH NACH ART. 21 ABS. 2 DSGVO).</p>
<h3>Beschwerde&shy;recht bei der zust&auml;ndigen Aufsichts&shy;beh&ouml;rde</h3> <p>Im Falle von
Verst&ouml;&szlig;en gegen die DSGVO steht den Betroffenen ein Beschwerderecht bei einer
Aufsichtsbeh&ouml;rde, insbesondere in dem Mitgliedstaat ihres gew&ouml;hnlichen Aufenthalts, ihres
Arbeitsplatzes oder des Orts des mutma&szlig;lichen Versto&szlig;es zu. Das Beschwerderecht besteht
unbeschadet anderweitiger verwaltungsrechtlicher oder gerichtlicher Rechtsbehelfe.</p>
<h3>Recht auf Daten&shy;&uuml;bertrag&shy;barkeit</h3> <p>Sie haben das Recht, Daten, die wir auf Grundlage
Ihrer Einwilligung oder in Erf&uuml;llung eines Vertrags automatisiert verarbeiten, an sich oder an einen Dritten in
einem g&auml;ngigen, maschinenlesbaren Format aush&auml;ndigen zu lassen. Sofern Sie die direkte
&Uuml;bertragung der Daten an einen anderen Verantwortlichen verlangen, erfolgt dies nur, soweit es technisch
machbar ist.</p>
<h3>Auskunft, Berichtigung und L&ouml;schung</h3> <p>Sie haben im Rahmen der geltenden gesetzlichen
Bestimmungen jederzeit das Recht auf unentgeltliche Auskunft &uuml;ber Ihre gespeicherten personenbezogenen
Daten, deren Herkunft und Empf&auml;nger und den Zweck der Datenverarbeitung und ggf. ein Recht auf
Berichtigung oder L&ouml;schung dieser Daten. Hierzu sowie zu weiteren Fragen zum Thema personenbezogene
Daten k&ouml;nnen Sie sich jederzeit an uns wenden.</p>
<h3>Recht auf Einschr&auml;nkung der Verarbeitung</h3> <p>Sie haben das Recht, die Einschr&auml;nkung der
Verarbeitung Ihrer personenbezogenen Daten zu verlangen. Hierzu k&ouml;nnen Sie sich jederzeit an uns wenden.
Das Recht auf Einschr&auml;nkung der Verarbeitung besteht in folgenden F&auml;llen:</p> <ul> <li>Wenn Sie die
Richtigkeit Ihrer bei uns gespeicherten personenbezogenen Daten bestreiten, ben&ouml;tigen wir in der Regel Zeit,
um dies zu &uuml;berpr&uuml;fen. F&uuml;r die Dauer der Pr&uuml;fung haben Sie das Recht, die
Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</li> <li>Wenn die
Verarbeitung Ihrer personenbezogenen Daten unrechtm&auml;&szlig;ig geschah/geschieht, k&ouml;nnen Sie statt
der L&ouml;schung die Einschr&auml;nkung der Datenverarbeitung verlangen.</li> <li>Wenn wir Ihre
personenbezogenen Daten nicht mehr ben&ouml;tigen, Sie sie jedoch zur Aus&uuml;bung, Verteidigung oder
Geltendmachung von Rechtsanspr&uuml;chen ben&ouml;tigen, haben Sie das Recht, statt der L&ouml;schung die
Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</li> <li>Wenn Sie einen
Widerspruch nach Art. 21 Abs. 1 DSGVO eingelegt haben, muss eine Abw&auml;gung zwischen Ihren und unseren
Interessen vorgenommen werden. Solange noch nicht feststeht, wessen Interessen &uuml;berwiegen, haben Sie das
Recht, die Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</li> </ul>
<p>Wenn Sie die Verarbeitung Ihrer personenbezogenen Daten eingeschr&auml;nkt haben, d&uuml;rfen diese
Daten &ndash; von ihrer Speicherung abgesehen &ndash; nur mit Ihrer Einwilligung oder zur Geltendmachung,
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
Europ&auml;ischen Union oder eines Mitgliedstaats verarbeitet werden.</p>
<p> <h4>Cookies</h4>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et <p>
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet Unsere Internetseiten verwenden so genannte „Cookies“. Cookies sind kleine Datenpakete und richten auf
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, Ihrem Endgerät keinen Schaden an. Sie werden entweder vorübergehend für die Dauer einer Sitzung
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, (Session-Cookies) oder dauerhaft (permanente Cookies) auf Ihrem Endgerät gespeichert. Session-Cookies
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no werden nach Ende Ihres Besuchs automatisch gelöscht. Permanente Cookies bleiben auf Ihrem Endgerät
sea takimata sanctus est Lorem ipsum dolor sit amet. gespeichert, bis Sie diese selbst löschen oder eine automatische Löschung durch Ihren Webbrowser erfolgt.
</p> 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>
</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 -->
+7 -9
View File
@@ -6,14 +6,12 @@
<main> <main>
<h1>Impressum</h1> <h1>Impressum</h1>
<p>Gruppe C<br />
<p> Ammerlaender Heerstrasse, 114-118<br />
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et 26129 Oldenburg</p>
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet <h2>Kontakt</h2>
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, <p>Telefon: 0123456789<br />
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, E-Mail: GruppeC@uni-oldenburg.de</p>
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no <p>Quelle: <a href="https://www.e-recht24.de/impressum-generator.html">https://www.e-recht24.de/impressum-generator.html</a></p>
sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
</main> </main>
+10 -2
View File
@@ -11,8 +11,10 @@ $error = $error ?? null;
<h1>Bitte anmelden</h1> <h1>Bitte anmelden</h1>
<?php if ($error): ?> <?php include_once "includes/alertMessages.php"; ?>
<p style="color:red;">
<?php if (!empty($error)): ?>
<p class="alert-message is-error">
<?php echo htmlspecialchars($error); ?> <?php echo htmlspecialchars($error); ?>
</p> </p>
<?php endif; ?> <?php endif; ?>
@@ -41,6 +43,12 @@ $error = $error ?? null;
anmelden anmelden
</button> </button>
<div class="register-link">
<a href="index.php?pfad=password-forgotten">
Passwort vergessen?
</a>
</div>
<div class="register-link"> <div class="register-link">
<a href="index.php?pfad=register"> <a href="index.php?pfad=register">
Noch keinen Account? Jetzt hier registrieren! Noch keinen Account? Jetzt hier registrieren!
+45 -6
View File
@@ -23,19 +23,58 @@
include_once 'includes/navbar.php'; include_once 'includes/navbar.php';
?> ?>
<main>
<main> <main>
<h1>Nutzungsbedingungen</h1> <h1>Nutzungsbedingungen</h1>
<p> <p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et Diese Nutzungsbedingungen regeln die Nutzung der Plattform EduForge.
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet Mit der Registrierung und Nutzung der Website erkennen Nutzerinnen und Nutzer
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, die nachfolgenden Bedingungen an.
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>
<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>
<?php <?php
+54
View File
@@ -0,0 +1,54 @@
<?php
$error = $error ?? null;
$success = $success ?? null;
?>
<main class="login-page">
<div class="login-container">
<h1>Passwort vergessen</h1>
<?php if (!empty($error)): ?>
<p class="alert-message is-error">
<?php echo htmlspecialchars($error); ?>
</p>
<?php endif; ?>
<?php if (!empty($success)): ?>
<p class="alert-message is-success">
<?php echo $success; ?>
</p>
<?php endif; ?>
<form method="post" action="index.php?pfad=password-forgotten">
<p class="input-label">E-Mail-Adresse:</p>
<input type="email"
name="email"
class="login-input"
placeholder="E-Mail-Adresse"
required>
<p class="input-label">Neues Passwort:</p>
<input type="password"
name="password"
class="login-input"
placeholder="Neues Passwort"
required>
<button type="submit"
name="passwordForgottenSubmit"
class="button">
Passwort zurücksetzen
</button>
<div class="register-link">
<a href="index.php?pfad=login">
Zurück zum Login
</a>
</div>
</form>
</div>
</main>
+8 -1
View File
@@ -1,5 +1,6 @@
<?php <?php
$error = $error ?? null; $error = $error ?? null;
$success = $success ?? null;
?> ?>
<!-- <!--
@@ -12,11 +13,17 @@ $error = $error ?? null;
<h1>Jetzt Registrieren!</h1> <h1>Jetzt Registrieren!</h1>
<?php if (!empty($error)): ?> <?php if (!empty($error)): ?>
<p class="alert-message is-error" style="color:red;"> <p class="alert-message is-error">
<?php echo htmlspecialchars($error); ?> <?php echo htmlspecialchars($error); ?>
</p> </p>
<?php endif; ?> <?php endif; ?>
<?php if (!empty($success)): ?>
<p class="alert-message is-success">
<?php echo $success; ?>
</p>
<?php endif; ?>
<form method="post" action="index.php?pfad=register"> <form method="post" action="index.php?pfad=register">
<p class="input-label">Email:</p> <p class="input-label">Email:</p>
+104 -57
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 -->
<input type="hidden" name="page" id="s-res-page-input" value="<?php echo $currentPage; ?>">
<div class="s-res-sidebar-box"> <div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Suche anpassen</h3> <h3 class="s-res-sidebar-title">Suche anpassen</h3>
<input type="search" id="site-search" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50" required> <input type="search" id="site-search" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50" required>
<button type="submit" class="nav__search-button">Suchen</button> <button type="submit" class="nav__search-button">Suchen</button>
</div> </div>
</form>
<!-- Formular 2: sortiert/filtert die BEREITS gefundenen Ergebnisse.
Ohne JS: echter Reload dieser Seite, aber ohne erneute Datenbank-Suche.
Mit JS: search-results.js übernimmt das live im Browser, der Submit-Button
wird dafür nicht benötigt und über <noscript> versteckt. -->
<form action="index.php" method="GET" id="s-res-filter-form" class="s-res-sidebar-form">
<input type="hidden" name="pfad" value="search-results">
<input type="hidden" name="q" value="<?php echo htmlspecialchars($query); ?>">
<div class="s-res-sidebar-box"> <div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Sortierung</h3> <h3 class="s-res-sidebar-title">Sortierung</h3>
<?php $currentSort = $_SESSION['search_sort'] ?? 'alphabet'; ?>
<div class="s-res-filter-group"> <div class="s-res-filter-group">
<label class="s-res-filter-option"> <label class="s-res-filter-option">
<input type="radio" name="sort" value="alphabet" <?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,17 @@ $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>
<!-- 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 +155,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; ?>
<button type="button" class="s-res-page-btn" data-page="<?php echo $currentPage + 1; ?>" <?php echo $currentPage >= $totalPages ? 'disabled' : ''; ?>>
»
</button>
</div> </div>
</nav>
</div> </div>
</main> </main>
+24
View File
@@ -0,0 +1,24 @@
<?php
/*
* Zeigt den Inhalt einer simulierten E-Mail an.
* Die Datei wird über einen zufällig erzeugten Token geladen.
*/
$token = basename($_GET["token"] ?? "");
$file = __DIR__ . "/../data/mails/" . $token . ".html";
if (!file_exists($file)) {
echo "<p>Datei nicht gefunden.</p>";
exit();
}
?>
<main class="login-page">
<div class="login-container">
<h1>Simulierte E-Mail</h1>
<?php include $file; ?>
</div>
</main>
+2 -2
View File
@@ -45,8 +45,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>
+166 -14
View File
@@ -1,32 +1,184 @@
<?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-sidebar-box">
<h3 class="s-res-sidebar-title">In dieser Kategorie suchen</h3>
<input type="search" id="cat-search-input" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50">
</div>
<div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Sortierung</h3>
<div class="s-res-filter-group">
<label class="s-res-filter-option">
<input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>>
<span>Alphabetisch</span>
</label>
<label class="s-res-filter-option">
<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>
<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>
<!-- Ergebnisliste -->
<div class="s-res-list"> <div class="s-res-list">
<?php <?php if (!empty($results)): ?>
if (!empty($articles)): ?>
<?php foreach ($articles as $article): ?> <?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 $article->getId(); ?>" class="s-res-link"> <a href="index.php?pfad=showArticle&id=<?php echo $item['id']; ?>" class="s-res-link">
<?php echo htmlspecialchars($article->getTitle()); ?> <?php echo htmlspecialchars($item['title']); ?>
</a> </a>
</h2> </h2>
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($article->getAuthor()); ?></span></p> <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>
<div class="s-res-arrow">&rarr;</div> <div class="s-res-arrow">&rarr;</div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php
else: ?> <?php elseif ($query !== ''): ?>
<p> Es sind noch keine Beiträge in dieser Kategorie enthalten.</p> <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 endif; ?>
<?php unset($_SESSION["message"]); ?>
</div> </div>
</main> <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>
+15 -15
View File
@@ -130,13 +130,6 @@ h1 {
color: #1f2937; color: #1f2937;
} }
.input-label {
margin-bottom: 5px;
font-weight: bold;
width: 100%;
color: #1f2937;
}
.login-input { .login-input {
width: 100%; width: 100%;
padding: 12px; padding: 12px;
@@ -210,14 +203,6 @@ h1 {
.form-container { .form-container {
width: 90%; width: 90%;
max-width: 600px; max-width: 600px;
padding: 30px;
background-color: white;
border: 1px solid #dbe3ec;
border-radius: 10px;
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
}
.form-container {
flex: 1 1 450px; flex: 1 1 450px;
padding: 30px; padding: 30px;
background-color: white; background-color: white;
@@ -226,3 +211,18 @@ h1 {
box-shadow: 0 6px 20px rgba(0,0,0,0.1); box-shadow: 0 6px 20px rgba(0,0,0,0.1);
box-sizing: border-box; box-sizing: border-box;
} }
/* Darstellung der Registrierungsbestätigung */
.confirm-message {
text-align: center;
margin: 20px 0;
}
/* Anpassung des Login-Buttons auf der Bestätigungsseite */
.confirm-button {
display: block;
width: 100%;
text-align: center;
text-decoration: none;
box-sizing: border-box;
}
+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;
}
}
+11
View File
@@ -78,6 +78,17 @@
Das Bild konnte nicht hochgeladen werden. Bitte versuche es erneut oder verwende ein anderes Bildformat. Das Bild konnte nicht hochgeladen werden. Bitte versuche es erneut oder verwende ein anderes Bildformat.
</p> </p>
<?php endif; ?> <?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "registration_confirmed"): ?>
<p class="alert-message is-success">
Ihre Registrierung wurde erfolgreich abgeschlossen. Sie können sich jetzt anmelden.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "password_changed"): ?>
<p class="alert-message is-success">
Ihr Passwort wurde erfolgreich geändert. Sie können sich jetzt anmelden.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "database_error"): ?> <?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "database_error"): ?>
<p class="alert-message is-error"> <p class="alert-message is-error">
Es ist ein Datenbankfehler aufgetreten. Bitte versuche es erneut. Es ist ein Datenbankfehler aufgetreten. Bitte versuche es erneut.
+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;
}
+19 -1
View File
@@ -15,9 +15,24 @@ if ($pfad === "login") {
include_once "php/controller/login-controller.php"; include_once "php/controller/login-controller.php";
} }
if ($pfad === "search-results-controller") {
include_once "php/controller/search-results-controller.php";
}
if ($pfad === "register") { if ($pfad === "register") {
include_once "php/controller/register-controller.php"; include_once "php/controller/register-controller.php";
} }
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") { if ($pfad === "logout") {
include_once "php/controller/logout-controller.php"; include_once "php/controller/logout-controller.php";
@@ -52,10 +67,13 @@ if ($pfad === "deleteAccount") {
<link rel="stylesheet" href="css/profile.css"> <link rel="stylesheet" href="css/profile.css">
<link rel="stylesheet" href="css/showArticle.css"> <link rel="stylesheet" href="css/showArticle.css">
<link rel="stylesheet" href="css/message.css"> <link rel="stylesheet" href="css/message.css">
<link rel="stylesheet" href="css/showCategory.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> <title>EduForge</title>
</head> </head>
-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();
}
+238
View File
@@ -0,0 +1,238 @@
/**
* Übernimmt Sortierung, Kategorie-Filter und Pagination der Suchergebnisse
* vollständig clientseitig
*
* Ablauf:
* 1. Kompletten Ergebnis-Datensatz einmalig per fetch() nachladen
* (php/controller/search-results-data.php, liest nur die Session-Daten,
* keine erneute Datenbank-Suche).
* 2. Sortieren + filtern (JS-Array).
* 3. Ergebnisliste + Pagination-UI daraus neu rendern.
*
* Ersetzt sorter.js, filter.js und paginator.js, deren Klick-Handler sich
* gegenseitig ins Gehege kamen.
* Ohne JS bleibt die serverseitig gerenderte Seite (echte Links/Formulare)
* voll funktionsfähig.
*/
(function () {
const state = {
allItems: [],
sort: 'alphabet',
category: 'all',
itemsPerPage: 10,
currentPage: 1,
};
let listContainer, sortRadios, categorySelect, limitSelect;
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
function init() {
listContainer = document.querySelector('.s-res-list');
if (!listContainer) return;
sortRadios = document.querySelectorAll('.sort-radio');
categorySelect = document.getElementById('category-filter');
limitSelect = document.getElementById('s-res-per-page');
prevBtn = document.getElementById('prev-page-btn');
nextBtn = document.getElementById('next-page-btn');
numbersContainer = document.getElementById('dynamic-page-numbers');
jsNav = document.getElementById('js-page-navigation');
resultCountEl = document.getElementById('s-res-result-count');
const checkedRadio = document.querySelector('.sort-radio:checked');
state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
state.category = categorySelect ? categorySelect.value : 'all';
state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
fetchFullDataset()
.then(function (data) {
state.allItems = data.results || [];
attachEvents();
render();
if (jsNav) jsNav.style.display = 'flex';
})
.catch(function (err) {
// JSON-Endpoint nicht erreichbar: die serverseitig gerenderte
// (No-JS-)Ansicht bleibt sichtbar und funktioniert weiter.
console.error('Suchergebnisse konnten nicht nachgeladen werden:', err);
});
}
function fetchFullDataset() {
return fetch('php/controller/search-results-data.php', { credentials: 'same-origin' })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
});
}
function getFilteredSortedItems() {
let items = state.allItems;
if (state.category !== 'all') {
const wanted = state.category.toLowerCase();
items = items.filter(function (item) {
return (item.category || '').toLowerCase() === wanted;
});
}
items = items.slice().sort(function (a, b) {
if (state.sort === 'likes') {
return (b.likes || 0) - (a.likes || 0);
}
if (state.sort === 'newest' || state.sort === 'oldest') {
return state.sort === 'newest' ? (b.id - a.id) : (a.id - b.id);
}
// alphabet (Standard)
return (a.title || '').toLowerCase().localeCompare((b.title || '').toLowerCase());
});
return items;
}
function render() {
const items = getFilteredSortedItems();
if (resultCountEl) resultCountEl.textContent = items.length;
const totalPages = Math.max(1, Math.ceil(items.length / state.itemsPerPage));
if (state.currentPage > totalPages) state.currentPage = totalPages;
if (state.currentPage < 1) state.currentPage = 1;
const start = (state.currentPage - 1) * state.itemsPerPage;
const pageItems = items.slice(start, start + state.itemsPerPage);
listContainer.innerHTML = '';
if (pageItems.length === 0) {
const empty = document.createElement('p');
empty.textContent = 'Keine Beiträge gefunden.';
listContainer.appendChild(empty);
} else {
pageItems.forEach(function (item) {
listContainer.appendChild(buildCard(item));
});
}
updatePaginatorUI(state.currentPage, totalPages);
}
function buildCard(item) {
const card = document.createElement('div');
card.className = 's-res-item';
card.setAttribute('data-likes', item.likes);
card.setAttribute('data-category', (item.category || '').toLowerCase());
const link = document.createElement('a');
link.className = 's-res-link';
link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id);
link.textContent = item.title;
const title = document.createElement('h2');
title.className = 's-res-item-title';
title.appendChild(link);
const authorName = document.createElement('span');
authorName.className = 's-res-author-name';
authorName.textContent = item.author;
const author = document.createElement('p');
author.className = 's-res-author';
author.append('Von: ', authorName);
const likes = document.createElement('span');
likes.className = 's-res-likes';
likes.textContent = '❤️ ' + item.likes;
const metaRow = document.createElement('div');
metaRow.className = 's-res-meta-row';
metaRow.append(author, likes);
const content = document.createElement('div');
content.className = 's-res-content';
content.append(title, metaRow);
const arrow = document.createElement('div');
arrow.className = 's-res-arrow';
arrow.textContent = '→';
card.append(content, arrow);
return card;
}
function updatePaginatorUI(currentPage, totalPages) {
if (!prevBtn || !nextBtn || !numbersContainer) return;
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= totalPages;
numbersContainer.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 's-res-page-btn' + (i === currentPage ? ' s-res-page-btn-active' : '');
btn.textContent = i;
btn.addEventListener('click', function () {
state.currentPage = i;
render();
});
numbersContainer.appendChild(btn);
}
}
function attachEvents() {
sortRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
state.sort = this.value;
state.currentPage = 1;
render();
});
});
if (categorySelect) {
categorySelect.addEventListener('change', function () {
state.category = this.value;
state.currentPage = 1;
render();
});
}
if (limitSelect) {
limitSelect.addEventListener('change', function () {
state.itemsPerPage = parseInt(this.value, 10) || 10;
state.currentPage = 1;
render();
});
}
if (prevBtn) {
prevBtn.addEventListener('click', function () {
if (state.currentPage > 1) {
state.currentPage -= 1;
render();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
state.currentPage += 1;
render();
});
}
// Mit aktivem JS übernimmt render() Sortierung/Filter live
// ein echtes Absenden des Formulars (No-JS-Fallback) ist dann nicht mehr nötig.
const filterForm = document.getElementById('s-res-filter-form');
if (filterForm) {
filterForm.addEventListener('submit', function (e) {
e.preventDefault();
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
+242
View File
@@ -0,0 +1,242 @@
/**
* Übernimmt Sortierung, Suche (innerhalb der Kategorie) und Pagination der
* Kategorie-Ansicht vollständig clientseitig.
*
* Ablauf:
* 1. Kompletten Kategorie-Datensatz einmalig per fetch() nachladen
* 2. Sortieren + nach Suchbegriff filtern (JS-Array).
* 3. Ergebnisliste + Pagination-UI daraus neu rendern.
*
* Ohne JS bleibt die serverseitig gerenderte Seite (echte Links/Formulare)
* voll funktionsfähig
*/
(function () {
const state = {
allItems: [],
sort: 'alphabet',
query: '',
itemsPerPage: 10,
currentPage: 1,
};
let listContainer, sortRadios, searchInput, limitSelect;
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
let searchDebounceTimer;
function init() {
listContainer = document.querySelector('.s-res-list');
if (!listContainer) return;
sortRadios = document.querySelectorAll('.sort-radio');
searchInput = document.getElementById('cat-search-input');
limitSelect = document.getElementById('s-res-per-page');
prevBtn = document.getElementById('prev-page-btn');
nextBtn = document.getElementById('next-page-btn');
numbersContainer = document.getElementById('dynamic-page-numbers');
jsNav = document.getElementById('js-page-navigation');
resultCountEl = document.getElementById('s-res-result-count');
const checkedRadio = document.querySelector('.sort-radio:checked');
state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
state.query = searchInput ? searchInput.value : '';
state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
fetchFullDataset()
.then(function (data) {
state.allItems = data.results || [];
attachEvents();
render();
if (jsNav) jsNav.style.display = 'flex';
})
.catch(function (err) {
// JSON-Endpoint nicht erreichbar: die serverseitig gerenderte
// (No-JS-)Ansicht bleibt sichtbar und funktioniert weiter.
console.error('Kategorie-Beiträge konnten nicht nachgeladen werden:', err);
});
}
function fetchFullDataset() {
return fetch('php/controller/showCategory-data.php', { credentials: 'same-origin' })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
});
}
function getFilteredSortedItems() {
let items = state.allItems;
const needle = state.query.trim().toLowerCase();
if (needle !== '') {
items = items.filter(function (item) {
const haystack = ((item.title || '') + ' ' + (item.content || '')).toLowerCase();
return haystack.indexOf(needle) !== -1;
});
}
items = items.slice().sort(function (a, b) {
if (state.sort === 'likes') {
return (b.likes || 0) - (a.likes || 0);
}
if (state.sort === 'newest' || state.sort === 'oldest') {
return state.sort === 'newest' ? (b.id - a.id) : (a.id - b.id);
}
// alphabet (Standard)
return (a.title || '').toLowerCase().localeCompare((b.title || '').toLowerCase());
});
return items;
}
function render() {
const items = getFilteredSortedItems();
if (resultCountEl) resultCountEl.textContent = items.length;
const totalPages = Math.max(1, Math.ceil(items.length / state.itemsPerPage));
if (state.currentPage > totalPages) state.currentPage = totalPages;
if (state.currentPage < 1) state.currentPage = 1;
const start = (state.currentPage - 1) * state.itemsPerPage;
const pageItems = items.slice(start, start + state.itemsPerPage);
listContainer.innerHTML = '';
if (pageItems.length === 0) {
const empty = document.createElement('p');
empty.textContent = 'Keine Beiträge gefunden.';
listContainer.appendChild(empty);
} else {
pageItems.forEach(function (item) {
listContainer.appendChild(buildCard(item));
});
}
updatePaginatorUI(state.currentPage, totalPages);
}
function buildCard(item) {
const card = document.createElement('div');
card.className = 's-res-item';
card.setAttribute('data-likes', item.likes);
card.setAttribute('data-category', (item.category || '').toLowerCase());
const link = document.createElement('a');
link.className = 's-res-link';
link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id);
link.textContent = item.title;
const title = document.createElement('h2');
title.className = 's-res-item-title';
title.appendChild(link);
const authorName = document.createElement('span');
authorName.className = 's-res-author-name';
authorName.textContent = item.author;
const author = document.createElement('p');
author.className = 's-res-author';
author.append('Von: ', authorName);
const likes = document.createElement('span');
likes.className = 's-res-likes';
likes.textContent = '❤️ ' + item.likes;
const metaRow = document.createElement('div');
metaRow.className = 's-res-meta-row';
metaRow.append(author, likes);
const content = document.createElement('div');
content.className = 's-res-content';
content.append(title, metaRow);
const arrow = document.createElement('div');
arrow.className = 's-res-arrow';
arrow.textContent = '→';
card.append(content, arrow);
return card;
}
function updatePaginatorUI(currentPage, totalPages) {
if (!prevBtn || !nextBtn || !numbersContainer) return;
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= totalPages;
numbersContainer.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 's-res-page-btn' + (i === currentPage ? ' s-res-page-btn-active' : '');
btn.textContent = i;
btn.addEventListener('click', function () {
state.currentPage = i;
render();
});
numbersContainer.appendChild(btn);
}
}
function attachEvents() {
sortRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
state.sort = this.value;
state.currentPage = 1;
render();
});
});
if (searchInput) {
// Live-Filterung während des Tippens (leicht entprellt), da der
// Datensatz bereits vollständig im Browser liegt.
searchInput.addEventListener('input', function () {
clearTimeout(searchDebounceTimer);
const value = this.value;
searchDebounceTimer = setTimeout(function () {
state.query = value;
state.currentPage = 1;
render();
}, 150);
});
}
if (limitSelect) {
limitSelect.addEventListener('change', function () {
state.itemsPerPage = parseInt(this.value, 10) || 10;
state.currentPage = 1;
render();
});
}
if (prevBtn) {
prevBtn.addEventListener('click', function () {
if (state.currentPage > 1) {
state.currentPage -= 1;
render();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
state.currentPage += 1;
render();
});
}
// Mit aktivem JS übernimmt render() Sortierung/Suche live
// ein echtes Absenden des Formulars (No-JS-Fallback) ist dann nicht mehr nötig.
const filterForm = document.getElementById('cat-filter-form');
if (filterForm) {
filterForm.addEventListener('submit', function (e) {
e.preventDefault();
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
@@ -0,0 +1,65 @@
<?php
require_once "php/model/UserManager.php";
/*
Verarbeitung der Passwortbestätigung
Funktion: Liest die zwischengespeicherten Passwortdaten aus,
aktualisiert das Passwort des Benutzers und gibt das Ergebnis
für die View zurück.
*/
$title = "Passwort zurücksetzen";
$message = "";
$link = "";
$isSuccess = false;
$token = basename($_GET["token"] ?? "");
$file = "data/pending-password/" . $token . ".json";
if (empty($token) || !file_exists($file)) {
$message = "Der Bestätigungslink ist ungültig oder bereits abgelaufen.";
} else {
$data = json_decode(file_get_contents($file), true);
if ($data === null || empty($data["email"]) || empty($data["password"])) {
$message = "Die Daten zur Passwortänderung konnten nicht gelesen werden.";
} else {
try {
$dao = UserManager::getInstance();
$user = $dao->findUser($data["email"]);
if ($user !== null) {
$dao->updateUser(
$user["email"],
$user["email"],
$user["vorname"],
$user["nachname"],
$data["password"]
);
$_SESSION["message"] = "password_changed";
header("Location: index.php?pfad=login");
exit();
} else {
$message = "Der Benutzer konnte nicht gefunden werden.";
}
unlink($file);
} catch (Exception $e) {
$message = "Das Passwort konnte nicht geändert werden.";
}
}
}
@@ -0,0 +1,64 @@
<?php
require_once "php/model/UserManager.php";
/*
Verarbeitung der Registrierungsbestätigung
Funktion: Liest die zwischengespeicherten Registrierungsdaten aus,
legt den Benutzer an und gibt das Ergebnis für die View zurück.
*/
$title = "Registrierung";
$message = "";
$link = "";
$isSuccess = false;
$token = basename($_GET["token"] ?? "");
$file = "data/pending/" . $token . ".json";
if (empty($token) || !file_exists($file)) {
$message = "Der Registrierungslink ist ungültig oder bereits abgelaufen.";
} else {
$data = json_decode(file_get_contents($file), true);
if (
$data === null ||
empty($data["email"]) ||
empty($data["vorname"]) ||
empty($data["nachname"]) ||
empty($data["password"])
) {
$message = "Die Registrierungsdaten konnten nicht gelesen werden.";
} else {
try {
$dao = UserManager::getInstance();
if ($dao->findUser($data["email"]) === null) {
$dao->addUser(
$data["email"],
$data["vorname"],
$data["nachname"],
$data["password"]
);
}
unlink($file);
$_SESSION["message"] = "registration_confirmed";
header("Location: index.php?pfad=login");
exit();
} catch (Exception $e) {
$message = "Die Registrierung konnte nicht abgeschlossen werden.";
}
}
}
@@ -0,0 +1,76 @@
<?php
require_once "php/model/UserManager.php";
require_once "php/validator/user-validator.php";
$error = null;
$success = null;
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$email = trim($_POST["email"] ?? "");
$plainPassword = $_POST["password"] ?? "";
if (!userEmailValidator($email)) {
$error = "Bitte gib eine gültige E-Mail-Adresse ein.";
} elseif (!userPasswordValidator($plainPassword)) {
$error = "Das Passwort muss 5 bis 12 Zeichen lang sein.";
} else {
try {
$dao = UserManager::getInstance();
$token = bin2hex(random_bytes(16));
$existingUser = $dao->findUser($email);
if (!is_dir("data/mails") && !mkdir("data/mails", 0777, true)) {
throw new RuntimeException("Ordner data/mails konnte nicht erstellt werden.");
}
if (!is_dir("data/pending-password") && !mkdir("data/pending-password", 0777, true)) {
throw new RuntimeException("Ordner data/pending-password konnte nicht erstellt werden.");
}
if (!is_writable("data/mails") || !is_writable("data/pending-password")) {
throw new RuntimeException("Ordner sind nicht beschreibbar.");
}
if ($existingUser !== null) {
$pendingData = [
"email" => $email,
"password" => $plainPassword
];
file_put_contents(
"data/pending-password/" . $token . ".json",
json_encode($pendingData, JSON_PRETTY_PRINT)
);
}
if ($existingUser !== null) {
$mailContent = "
<h2>Passwort zurücksetzen</h2>
<p>Falls Sie diese Anfrage nicht gestellt haben, können Sie diese Nachricht ignorieren.</p>
<p>
<a href='index.php?pfad=confirm-password&token=$token'>
Passwortänderung bestätigen
</a>
</p>
";
} else {
$mailContent = "
<h2>Passwort zurücksetzen</h2>
<p>Falls Sie diese Anfrage nicht gestellt haben, können Sie diese Nachricht ignorieren.</p>
<p>Für diese E-Mail-Adresse wurde kein Konto gefunden.</p>
";
}
file_put_contents("data/mails/" . $token . ".html", $mailContent);
$success = 'Weitere Infos finden Sie in der Datei
<a href="index.php?pfad=show-mail&token=' . htmlspecialchars($token) . '" target="_blank">xy</a>.';
} catch (Exception $e) {
$error = "Die Passwortänderung konnte nicht verarbeitet werden.";
}
}
}
+60 -8
View File
@@ -4,6 +4,7 @@ require_once "php/model/UserManager.php";
require_once "php/validator/user-validator.php"; require_once "php/validator/user-validator.php";
$error = null; $error = null;
$success = null;
if ($_SERVER["REQUEST_METHOD"] === "POST") { if ($_SERVER["REQUEST_METHOD"] === "POST") {
@@ -24,20 +25,71 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
try { try {
$dao = UserManager::getInstance(); $dao = UserManager::getInstance();
// Token für die simulierte E-Mail und die spätere Bestätigung erzeugen.
$token = bin2hex(random_bytes(16));
$existingUser = $dao->findUser($email);
if (!is_dir("data/mails") && !mkdir("data/mails", 0777, true)) {
throw new RuntimeException("Ordner data/mails konnte nicht erstellt werden.");
}
if (!is_dir("data/pending") && !mkdir("data/pending", 0777, true)) {
throw new RuntimeException("Ordner data/pending konnte nicht erstellt werden.");
}
if (!is_writable("data/mails")) {
throw new RuntimeException("Ordner data/mails ist nicht beschreibbar.");
}
if (!is_writable("data/pending")) {
throw new RuntimeException("Ordner data/pending ist nicht beschreibbar.");
}
if ($existingUser === null) {
$password = password_hash($plainPassword, PASSWORD_DEFAULT); $password = password_hash($plainPassword, PASSWORD_DEFAULT);
$dao->addUser($email, $vorname, $nachname, $password); $pendingData = [
"email" => $email,
"vorname" => $vorname,
"nachname" => $nachname,
"password" => $password
];
$_SESSION["user"] = $vorname . " " . $nachname; file_put_contents(
$_SESSION["user_email"] = $email; "data/pending/" . $token . ".json",
json_encode($pendingData, JSON_PRETTY_PRINT)
);
header("Location: index.php"); $mailContent = "
exit(); <h2>Registrierung bestätigen</h2>
<p>Bitte ignorieren Sie diese Nachricht, wenn Sie sich nicht registrieren wollten.</p>
<p>
<a href='index.php?pfad=confirm-register&token=$token'>
Registrierung bestätigen
</a>
</p>
";
} else {
$mailContent = "
<h2>Registrierung</h2>
<p>Bitte ignorieren Sie diese Nachricht, wenn Sie sich nicht registrieren wollten.</p>
<p>Sie sind bereits registriert.</p>
<p>
<a href='index.php?pfad=password-forgotten'>
Passwort vergessen
</a>
</p>
";
}
file_put_contents("data/mails/" . $token . ".html", $mailContent);
// Neutrale Meldung, damit nicht sichtbar wird, ob die E-Mail bereits registriert ist.
$success = 'Weitere Infos finden Sie in der Datei
<a href="index.php?pfad=show-mail&token=' . htmlspecialchars($token) . '" target="_blank">xy</a>.';
} catch (InvalidArgumentException $e) {
$error = $e->getMessage();
} catch (Exception $e) { } catch (Exception $e) {
$error = "Die Registrierung konnte nicht gespeichert werden."; $error = "Die Registrierung konnte nicht verarbeitet werden.";
} }
} }
} }
+19 -34
View File
@@ -2,56 +2,33 @@
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 +40,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),
]);
+12
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"]);
+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),
]);
+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
+2 -1
View File
@@ -314,7 +314,8 @@ class DatabaseArticleManager implements ArticleManagerDAO {
$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) {
+2
View File
@@ -62,6 +62,8 @@ class DatabaseInitializer {
$availableEmails = $initializer->seedDummyUsers($db); $availableEmails = $initializer->seedDummyUsers($db);
$initializer->seedDummyArticles($db, $availableEmails); $initializer->seedDummyArticles($db, $availableEmails);
unset($db);
} catch (PDOException $e) { } catch (PDOException $e) {
throw new RuntimeException("internal-error"); 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(
+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);
}