Merge branch 'dev' into 'main'

Potential Produkt Increment

See merge request eb2342s/swe-b1-a!23
This commit was merged in pull request #83.
This commit is contained in:
Matthias Grief
2024-12-11 14:16:24 +01:00
73 changed files with 1517 additions and 189 deletions

View File

@@ -29,7 +29,7 @@ body {
left: 0;
top: 0;
transition: transform 0.3s ease;
z-index: 50;
z-index: 40;
}
.sidebar-header {
@@ -100,7 +100,7 @@ body {
.main-content {
margin-left: 280px;
width: calc(100% - 280px);
padding-top: 5rem;
padding-top: 3rem;
transition: margin-left 0.3s ease;
}
@@ -287,15 +287,6 @@ body {
}
}
.menu-toggle {
padding: 0.75rem;
border-radius: 10px;
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
/* Add floating animation for icons */
@keyframes float {

View File

@@ -28,7 +28,7 @@ body {
left: 0;
top: 0;
transition: transform 0.3s ease;
z-index: 50;
z-index: 40;
}
.sidebar-header {
@@ -187,6 +187,16 @@ body {
width: 100%;
padding: 1rem;
}
.menu-toggle {
display: flex !important;
}
}
@media (min-width: 1025px) {
.menu-toggle {
display: none;
}
}
/* Active nav link style */
@@ -209,12 +219,3 @@ body {
left: 0;
font-weight: bold;
}
.menu-toggle {
padding: 0.75rem;
border-radius: 10px;
cursor: pointer;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}

View File

@@ -1,31 +0,0 @@
// Update search function with fallback animation
function handleSearch() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const topicCards = document.querySelectorAll('.topic-card');
topicCards.forEach(card => {
const title = card.querySelector('.topic-title')?.textContent.toLowerCase() || '';
const description = card.querySelector('.topic-description')?.textContent.toLowerCase() || '';
const relatedTopics = Array.from(card.querySelectorAll('.related-topics li'))
.map(li => li.textContent.toLowerCase())
.join(' ');
const content = `${title} ${description} ${relatedTopics}`;
if (content.includes(searchTerm)) {
card.style.display = 'block';
if (window.gsap) {
gsap.to(card, {
opacity: 1,
y: 0,
duration: 0.3
});
} else {
card.style.opacity = 1;
card.style.transform = 'translateY(0)';
}
} else {
card.style.display = 'none';
}
});
}

View File

@@ -27,7 +27,7 @@ document.addEventListener('DOMContentLoaded', () => {
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 45;
z-index: 30;
transition: opacity 0.3s ease;
`;
document.body.appendChild(overlay);

View File

@@ -0,0 +1,51 @@
<?php
/**
* Konfigurationsdaten
*/
class Config
{
/**
* @return string Verzeichnis für generelle Konfigurationsdaten
*/
public static function getConfigDirectory(): string
{
return "config/";
}
/**
* @return string Verzeichnis für Fächer
*/
public static function getSubjectsDirectory(): string
{
return self::getConfigDirectory() . "subjects/";
}
/**
* @param string $subjectId ID des Faches
* @return string Verzeichnis des Faches
*/
public static function getSubjectDirectory(string $subjectId): string
{
return self::getSubjectsDirectory() . $subjectId . "/";
}
/**
* @param string $subjectId ID des Faches
* @return string Verzeichnis der Themen des Faches
*/
public static function getTopicsDirectory(string $subjectId): string
{
return self::getSubjectDirectory($subjectId) . "topics/";
}
/**
* @param string $subjectId ID des Faches
* @param string $topicId ID des Themas
* @return string Verzeichnis des Themas
*/
public static function getTopicDirectory(string $subjectId, string $topicId): string
{
return self::getTopicsDirectory($subjectId) . $topicId . "/";
}
}

View File

@@ -1,9 +1,10 @@
<?php
require_once("Config.php");
require_once("Util.php");
require_once("TopicData.php");
/**
* Stellt alle relevanten Daten für ein einzeles Fach bereit
* Stellt alle relevanten Daten für ein einzelnes Fach bereit
*
*/
class SubjectData
@@ -24,7 +25,7 @@ class SubjectData
public string $description;
/**
* @var string Themenfarbe des Faches, entweder als hexcode oder CSS-Klasse, noch nicht spezifiziert
* @var string Themenfarbe des Faches als hexcode
*/
public string $color;
@@ -39,6 +40,55 @@ class SubjectData
*/
public array $topics;
/**
* Erstellt ein neues Fach. Es wird noch nichts gespeichert!
* @param string $id Ein eindeutiger Bezeichner für das Fach, darf nur A-Z, a-Z, 0-9 sowie _ und - enthalten
* @param string $displayName Der für User angezeigte Name des Faches, nur reiner Text
* @param string $description Eine kurze Beschreibung des Faches, z.B. für den Text auf der Startseite, kann HTML enthalten
* @param string $color Themenfarbe des Faches als hexcode
* @param string $icon Icon des Faches als Font-Awesome CSS-Klasse
* @param array $topics Alle Themen des Faches als TopicData Object
* @return SubjectData|false Neues Fach oder false, wenn ein Fehler auftritt
*/
public static function createNew(string $id, string $displayName, string $description, string $color, string $icon, array $topics): SubjectData|false
{
$result = new SubjectData();
if(Util::containsIllegalCharacters($id)) {
return false;
}
if(self::exists($id)) {
return false;
}
$result->id = $id;
$result->displayName = $displayName;
$result->description = $description;
$result->color = $color;
$result->icon = $icon;
$result->topics = $topics;
return $result;
}
/**
* Prüft, ob das Thema zu den angegebenen IDs existiert
* @param string $subjectId ID des Faches
* @return bool true, wenn es existiert, sonst false
*/
public static function exists(string $subjectId): bool
{
if(!is_dir(Config::getSubjectDirectory($subjectId))) {
return false;
}
return true;
}
/**
* Gibt alle Fächer als SubjectData Objekt zurück
* @return array Alle Fächer als SubjectData
@@ -47,8 +97,7 @@ class SubjectData
{
$result = array();
$subjectDirectory = "config/subjects";
$subjectNames = scandir($subjectDirectory);
$subjectNames = scandir(Config::getSubjectsDirectory());
usort($subjectNames, function ($a, $b) {
return strcmp($a, $b);
@@ -79,43 +128,171 @@ class SubjectData
{
$result = new SubjectData();
$subjectId = Util::removeIllegalCharacters($subjectId);
if (Util::containsIllegalCharacters($subjectId)) {
return null;
}
$result->id = $subjectId;
$subjectDirectory = "config/subjects/$subjectId";
$filename = "$subjectDirectory/properties.json";
$filename = Config::getSubjectDirectory($subjectId) . "properties.json";
$data = Util::parseJsonFromFile($filename);
if (!isset($data)) {
return null;
}
$result->id = $subjectId;
if (isset($data->displayName)) {
$result->displayName = $data->displayName;
} else {
if (!isset($data->displayName)) {
return null;
}
$result->displayName = $data->displayName;
if (isset($data->description)) {
$result->description = $data->description;
} else {
if (!isset($data->description)) {
return null;
}
$result->description = $data->description;
if (isset($data->color)) {
$result->color = $data->color;
} else {
if (!isset($data->color)) {
return null;
}
$result->color = $data->color;
if (isset($data->icon)) {
$result->icon = $data->icon;
} else {
if (!isset($data->icon)) {
return null;
}
$result->icon = $data->icon;
$result->topics = TopicData::getAll($subjectId);
return $result;
}
/**
* Schreibt alle Daten in Dateien
* @return bool true, wenn erfolgreich, sonst false
*/
public function save(): bool
{
$data = array();
$data["displayName"] = $this->displayName;
$data["description"] = $this->description;
$data["color"] = $this->color;
$data["icon"] = $this->icon;
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
if (!$json) {
return false;
}
$subjectDirectory = Config::getSubjectDirectory($this->getId());
if (!is_dir($subjectDirectory)) {
mkdir($subjectDirectory, 0777, true);
}
if (!Util::writeFileContent($subjectDirectory . "properties.json", $json)) {
return false;
}
return true;
}
/**
* Löscht das Fach inklusive aller zugehörigen Themen
* @return bool true, wenn erfolgreich, sonst false
*/
public function delete(): bool
{
if (!Util::delete(Config::getSubjectDirectory($this->getId()))) {
return false;
}
return true;
}
/**
* Fügt ein neues Thema zum Fach hinzu
* @param TopicData $topic Das neue Thema
* @return bool true, wenn erfolgreich, sonst false
*/
public function addTopic(TopicData $topic): bool
{
if(isset($this->topics[$topic->getId()])) {
return false;
}
$this->topics[] = $topic;
return true;
}
/**
* Entfernt ein Thema vom Fach
* @param TopicData $topic Das zu entfernende Thema
* @return bool true, wenn erfolgreich, sonst false
*/
public function removeTopic(TopicData $topic): bool
{
if(!isset($this->topics[$topic->getId()])) {
return false;
}
$this->topics = array_diff($this->topics, [$topic]);
return true;
}
public function getId(): string
{
return $this->id;
}
public function setId(string $id): void
{
$this->id = $id;
}
public function getDisplayName(): string
{
return $this->displayName;
}
public function setDisplayName(string $displayName): void
{
$this->displayName = $displayName;
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description): void
{
$this->description = $description;
}
public function getColor(): string
{
return $this->color;
}
public function setColor(string $color): void
{
$this->color = $color;
}
public function getIcon(): string
{
return $this->icon;
}
public function setIcon(string $icon): void
{
$this->icon = $icon;
}
public function getTopics(): array
{
return $this->topics;
}
public function setTopics(array $topics): void
{
$this->topics = $topics;
}
}

View File

@@ -1,4 +1,9 @@
<?php
use exception\SubjectDoesNotExistException;
use exception\TopicAlreadyExistsException;
require_once("Config.php");
require_once("Util.php");
class TopicData
@@ -43,6 +48,67 @@ class TopicData
*/
public string $article;
/**
* Erstellt ein neues Thema. Es wird noch nichts gespeichert!
* @param string $id Innerhalb des zugehörigen Faches eindeutige ID, darf nur A-Z, a-z, 0-9 sowie - und _ enthalten
* @param string $subjectId Die eindeutige ID des zugehörigen Faches, das Fach muss schon existieren
* @param string $displayName Der für User angezeigt Name des Themas, darf nur reinen Text enthalten
* @param string $icon Das Icon des Themas als Font-Awesome CSS-Klasse
* @param string $description Eine kurze Beschreibung des Themas, z.B. für die Fachübersichtsseite, darf HTML enthalten
* @param array $relatedTopics Die IDs aller verwandten Themen als String
* @param string $article Der gesamte Erklärungstext zum Thema, enthält fertiges HTML und LATEX Formelsyntax für MathJax https://docs.mathjax.org/en/latest/basic/mathematics.html
* @return TopicData|false Neues Thema oder false, wenn ein Fehler auftritt
*/
public static function createNew(string $id, string $subjectId, string $displayName, string $icon, string $description, array $relatedTopics, string $article): TopicData|false
{
$result = new TopicData();
if (Util::containsIllegalCharacters($subjectId)) {
return false;
}
if (!SubjectData::exists($subjectId)) {
return false;
}
$result->subjectId = $subjectId;
if (Util::containsIllegalCharacters($id)) {
return false;
}
if (self::exists($subjectId, $id)) {
return false;
}
$result->id = $id;
$result->displayName = $displayName;
$result->icon = $icon;
$result->description = $description;
$result->relatedTopics = $relatedTopics;
$result->files = array();
$result->article = $article;
return $result;
}
/**
* Prüft, ob das Thema zu den angegebenen IDs existiert
* @param string $subjectId ID des Faches
* @param string $topicId ID des Themas
* @return bool true, wenn es existiert, sonst false
*/
public static function exists(string $subjectId, string $topicId): bool
{
if (!is_dir(Config::getTopicDirectory($subjectId, $topicId))) {
return false;
}
return true;
}
/**
* Gibt alle Themen zu einem gegebenen Fach zurück
* @param $subjectId string Die ID des Faches
@@ -52,8 +118,7 @@ class TopicData
{
$result = array();
$topicDirectory = "config/subjects/$subjectId/topics";
$topicNames = scandir($topicDirectory);
$topicNames = scandir(Config::getTopicsDirectory($subjectId));
usort($topicNames, function ($a, $b) {
return strcmp($a, $b);
@@ -84,57 +149,319 @@ class TopicData
{
$result = new TopicData();
$subjectId = Util::removeIllegalCharacters($subjectId);
$topicId = Util::removeIllegalCharacters($topicId);
if (Util::containsIllegalCharacters($subjectId)) {
return null;
}
if (Util::containsIllegalCharacters($topicId)) {
return null;
}
$result->id = $topicId;
$result->subjectId = $subjectId;
$topicsDirectory = "config/subjects/$subjectId/topics";
$topicDataDirectory = "$topicsDirectory/$topicId";
$data = Util::parseJsonFromFile("$topicDataDirectory/properties.json");
$data = Util::parseJsonFromFile(Config::getTopicDirectory($subjectId, $topicId) . "properties.json");
if (!isset($data)) {
return null;
}
$result->id = $topicId;
$result->subjectId = $subjectId;
if (isset($data->displayName)) {
$result->displayName = $data->displayName;
} else {
if (!isset($data->displayName)) {
return null;
}
$result->displayName = $data->displayName;
if (isset($data->icon)) {
$result->icon = $data->icon;
} else {
if (!isset($data->icon)) {
return null;
}
$result->icon = $data->icon;
if (isset($data->description)) {
$result->description = $data->description;
} else {
if (!isset($data->description)) {
return null;
}
$result->description = $data->description;
$relatedTopics = array();
if (isset($data->relatedTopics)) {
$result->relatedTopics = $data->relatedTopics;
} else {
$result->relatedTopics = array();
}
if (isset($data->files)) {
$result->files = $data->files;
} else {
$result->files = array();
$relatedTopics = $data->relatedTopics;
}
$result->relatedTopics = $relatedTopics;
$article = Util::readFileContent("$topicDataDirectory/article.html");
$files = Util::getFilesFromDirectory(Config::getTopicDirectory($subjectId, $topicId) . "downloads/");
$result->files = $files;
$article = Util::readFileContent(Config::getTopicDirectory($subjectId, $topicId) . "article.html");
if (!isset($article)) {
$article = "Kein Erklärtext vorhanden";
}
$article = str_replace('$TOPICPATH', $topicDataDirectory, $article);
$result->article = str_replace('$TOPICPATH', Config::getTopicDirectory($subjectId, $topicId) . "images", $article);
$result->article = $article;
$result->cleanupRelatedTopics();
$result->cleanupFiles();
return $result;
}
/**
* Schreibt alle Daten in Dateien
* @return bool true, wenn erfolgreich, sonst false
*/
public function save(): bool
{
$this->cleanupRelatedTopics();
$this->cleanupFiles();
$data = array();
$data["displayName"] = $this->displayName;
$data["icon"] = $this->icon;
$data["description"] = $this->description;
$data["relatedTopics"] = $this->relatedTopics;
$data["files"] = $this->files;
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
if (!$json) {
return false;
}
if (!is_dir(Config::getSubjectDirectory($this->getSubjectId()))) {
return false;
}
$topicDirectory = Config::getTopicDirectory($this->getSubjectId(), $this->getId());
if (!is_dir($topicDirectory)) {
mkdir($topicDirectory, 0777, true);
}
if (!(Util::writeFileContent($topicDirectory . "properties.json", $json)
&& Util::writeFileContent($topicDirectory . "article.html", $json))
) {
return false;
}
return true;
}
/**
* Lädt eine Datei als Download zum Thema hoch
* @param string $name Dateiname von User, z.B. $_FILES['html-input-name']['name'][0]
* @param string $tmp_name Temporärer Pfad zur hochgeladenen Datei, z.B. $_FILES['html-input-name']['tmp_name'][0]
* @return bool true, wenn erfolgreich, sonst false
*/
public function addDownload(string $name, string $tmp_name): bool
{
$downloadDirectory = Config::getTopicDirectory($this->getSubjectId(), $this->getId()) . "downloads/";
if (!is_dir($downloadDirectory)) {
if (!mkdir($downloadDirectory)) {
return false;
}
}
if (!move_uploaded_file($tmp_name, $downloadDirectory . $name)) {
return false;
}
$this->files[] = $name;
return true;
}
/**
* Löscht eine downloadbare Datei des Themas
* @param string $name Dateiname
* @return bool true, wenn erfolgreich, sonst false
*/
public function deleteDownload(string $name): bool
{
if (!isset($this->files[$name])) {
return false;
}
if (!unlink(Config::getTopicDirectory($this->getSubjectId(), $this->getId()) . "downloads/$name")) {
return false;
}
$this->files = array_diff($this->files, [$name]);
return true;
}
/**
* Lädt eine Datei als Bild zum Thema hoch
* @param string $name Dateiname von User, z.B. $_FILES['html-input-name']['name'][0]
* @param string $tmp_name Temporärer Pfad zum hochgeladenen Bild, z.B. $_FILES['html-input-name']['tmp_name'][0]
* @return bool true, wenn erfolgreich, sonst false
*/
public function addImage(string $name, string $tmp_name): bool
{
$imageDirectory = Config::getTopicDirectory($this->getSubjectId(), $this->getId()) . "images/";
if (!is_dir($imageDirectory)) {
if (!mkdir($imageDirectory)) {
return false;
}
}
if (!move_uploaded_file($tmp_name, $imageDirectory . $name)) {
return false;
}
return true;
}
/**
* Prüft für alle verwandten Themen, ob diese auch existieren. Wenn nicht, wird es aus der Liste entfernt
* @return bool true, wenn Elemente entfernt wurden, sonst false
*/
private function cleanupRelatedTopics(): bool
{
$changed = false;
$nonexistentEntries = array();
foreach ($this->relatedTopics as $topic) {
if (!self::exists($this->subjectId, $topic)) {
$nonexistentEntries[] = $topic;
$changed = true;
}
}
$this->relatedTopics = array_diff($this->relatedTopics, $nonexistentEntries);
return $changed;
}
/**
* Prüft für alle Downloads, ob die zugehörige Datei existiert und ob zu jeder Datei ein Eintrag existiert.
* Wenn eine Datei nicht existiert, wird auch der zugehörige Eintrag entfernt.
* Wenn ein Eintrag nicht existiert, wird auch die Datei gelöscht.
* @return bool true, wenn etwas verändert wurde
*/
private function cleanupFiles(): bool
{
$changed = false;
$nonexistentEntries = array();
foreach ($this->files as $file) {
if(!file_exists(Config::getTopicDirectory($this->subjectId, $this->id) . "downloads/$file")) {
$nonexistentEntries[] = $file;
$changed = true;
}
}
$this->files = array_diff($this->files, $nonexistentEntries);
foreach (Util::getFilesFromDirectory(Config::getTopicDirectory($this->subjectId, $this->id) . "downloads/") as $file) {
if(!array_search($file, $this->files)) {
$this->deleteDownload($file);
$changed = true;
}
}
return $changed;
}
/**
* Löscht ein Bild des Themas
* @param string $name Dateiname
* @return bool true, wenn erfolgreich, sonst false
*/
public function deleteImage(string $name): bool
{
if (!unlink(Config::getTopicDirectory($this->getSubjectId(), $this->getId()) . "images/$name")) {
return false;
}
return true;
}
/**
* Löscht das Thema inklusive aller zugehörigen Dateien
* @return bool true, wenn erfolgreich gelöscht, sonst false
*/
public function delete(): bool
{
if (!Util::delete(Config::getTopicDirectory($this->getSubjectId(), $this->getId()))) {
return false;
}
return true;
}
public function getId(): string
{
return $this->id;
}
public function setId(string $id): void
{
$this->id = $id;
}
public function getSubjectId(): string
{
return $this->subjectId;
}
public function setSubjectId(string $subjectId): void
{
$this->subjectId = $subjectId;
}
public function getDisplayName(): string
{
return $this->displayName;
}
public function setDisplayName(string $displayName): void
{
$this->displayName = $displayName;
}
public function getIcon(): string
{
return $this->icon;
}
public function setIcon(string $icon): void
{
$this->icon = $icon;
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description): void
{
$this->description = $description;
}
public function getRelatedTopics(): array
{
return $this->relatedTopics;
}
public function setRelatedTopics(array $relatedTopics): void
{
$this->relatedTopics = $relatedTopics;
}
public function getFiles(): array
{
return $this->files;
}
public function setFiles(array $files): void
{
$this->files = $files;
}
public function getArticle(): string
{
return $this->article;
}
public function setArticle(string $article): void
{
$this->article = $article;
}
}

277
webseite/classes/User.php Normal file
View File

@@ -0,0 +1,277 @@
<?php
require_once("Util.php");
/**
* Repräsentiert einen Nutzer inklusive aller Nutzerdaten
*/
class User
{
/**
* @var string Pfad zum Verzeichnis der Nutzerdaten
*/
private static string $userdataDirectory = "./users/";
/**
* @var string Dateiname der Benutzerdaten
*/
private static string $userdataFile = "users.csv";
/**
* @var string Benutzername, darf A-Z, a-z, 0-9 sowie - und _ enthalten, max. 100 Zeichen
*/
private string $username;
/**
* @var string Passworthash
*/
private string $passwordHash;
/**
* Erstellt einen neuen Benutzer
* @param string $username Benutzername, darf A-Z, a-z, 0-9 sowie - und _ enthalten, max. 100 Zeichen
* @param string $password Passwort, max. 100 Zeichen
* @return User|false Den neu erstellten Benutzer oder false, wenn der Benutzer nicht erstellt werden konnte
*/
public static function createUser(string $username, string $password): User|false
{
if(self::getFromUsername($username) !== false) {
return false;
}
if(strlen($username) > 100) {
return false;
}
if(strlen($password) > 100) {
return false;
}
if (Util::containsIllegalCharacters($username)) {
return false;
}
$passwordHash = password_hash($password, PASSWORD_ARGON2I);
if(!is_dir(self::$userdataDirectory)) {
mkdir(self::$userdataDirectory);
}
$file = fopen(self::$userdataDirectory . self::$userdataFile, "a");
if (!$file) {
return false;
}
fputcsv($file, array($username, $passwordHash),",", '"', "\\");
fclose($file);
return self::getFromUsername($username);
}
/**
* Gibt einen Benutzer anhand eines Benutzernamen zurück
* @param string $username Benutzername nach dem gesucht wird
* @return User|false Der gefundene Besucher oder false, wenn der Benutzer nicht gefunden wurde
*/
public static function getFromUsername(string $username): User|false
{
if (!file_exists(self::$userdataDirectory . self::$userdataFile)) {
return false;
}
$file = fopen(self::$userdataDirectory . self::$userdataFile, "r");
if (!$file) {
return false;
}
while (($data = fgetcsv($file, 300, ',', '"', '\\')) !== false) {
if (count($data) != 2) {
continue;
}
if ($data[0] !== $username) {
continue;
}
$user = new User();
$user->username = $data[0];
$user->passwordHash = $data[1];
return $user;
}
fclose($file);
return false;
}
/**
* Löscht einen Benutzer
* @param string $password Richtiges Passwort zu diesem Nutzer
* @return bool true, wenn erfolgreich gelöscht, sonst false
*/
public function delete(string $password): bool
{
if(!$this->isPasswordCorrect($password)) {
return false;
}
if(!$this->logout()) {
return false;
}
$file = fopen(self::$userdataDirectory . self::$userdataFile, "r");
if(!$file) {
return false;
}
$newCsv = array();
while (($data = fgetcsv($file, 300, ',', '"', '\\')) !== false) {
if (count($data) != 2) {
continue;
}
if ($data[0] !== $this->username) {
$newCsv[] = $data;
}
}
fclose($file);
$file = fopen(self::$userdataDirectory . self::$userdataFile, "w");
if(!$file) {
return false;
}
foreach ($newCsv as $newCsvData) {
fputcsv($file, $newCsvData, ',', '"', '\\');
}
fclose($file);
unset($this->username);
unset($this->passwordHash);
return true;
}
/**
* Ändert das Passwort des Accounts
* @param string $oldPassword altes Passwort
* @param string $newPassword Neues Passwort
* @return bool true, wenn erfolgreich geändert, sonst false
*/
public function changePassword(string $oldPassword, string $newPassword): bool
{
if(!$this->isPasswordCorrect($oldPassword)) {
return false;
}
if(!$this->logout()) {
return false;
}
$file = fopen(self::$userdataDirectory . self::$userdataFile, "c+");
if(!$file) {
return false;
}
$this->passwordHash = password_hash($newPassword, PASSWORD_ARGON2I);
$lastLine = ftell($file);
while (($data = fgetcsv($file, 300, ',', '"', '\\')) !== false) {
if (count($data) != 2) {
} else if ($data[0] !== $this->username) {
} else {
$data[1] = $this->passwordHash;
fseek($file, $lastLine);
fputcsv($file, $data, ',', '"', '\\');
break;
}
$lastLine = ftell($file);
}
fclose($file);
return true;
}
/**
* Prüft, ob ein Passwort für diesen Benutzer korrekt ist
* @param string $password Zu prüfendes Passwort
* @return bool true, wenn korrekt, sonst false
*/
public function isPasswordCorrect(string $password): bool
{
return password_verify($password, $this->passwordHash);
}
/**
* Meldet den Benutzer an, wenn das Passwort richtig ist
* @param string $password Passwort
* @return bool true, wenn erfolgreich, sonst false
*/
public function login(string $password): bool
{
if(!$this->isPasswordCorrect($password)) {
return false;
}
if($this->isLoggedIn()) {
return false;
}
$_SESSION["user"] = $this;
$_SESSION["login_time"] = time();
return true;
}
/**
* Prüft, ob ein Benutzer angemeldet ist
* @return bool true, wenn angemeldet, sonst false
*/
public function isLoggedIn(): bool
{
if(!isset($_SESSION["user"])) {
return false;
}
if($_SESSION["user"] != $this) {
return false;
}
if(time() - $_SESSION["login_time"] > 86400 * 5) {
session_unset();
return false;
}
$_SESSION["login_time"] = time();
return true;
}
/**
* Meldet den Benutzer ab
* @return bool true, wenn erfolgreich abgemeldet, false, wenn vorher schon abgemeldet
*/
public function logout(): bool
{
if(!$this->isLoggedIn()) {
return false;
}
session_unset();
return true;
}
/**
* Gibt den Benutzernamen zurück
* @return string Benutzername
*/
public function getUsername(): string
{
return $this->username;
}
}

View File

@@ -15,6 +15,43 @@ class Util
return preg_replace("/[^a-zA-Z0-9_-]/", "", $string);
}
/**
* Prüft, ob Zeichen außer A-Z, a-z, 0-9 sowie - und _ enthalten sind
* @param string $string Zu prüfender Text
* @return bool true, wenn ungültige Zeichen enthalten sind, sonst false
*/
static function containsIllegalCharacters(string $string): bool
{
if (preg_match("/[^a-zA-Z0-9_-]/", $string)) {
return true;
}
return false;
}
static function getFilesFromDirectory(string $directory): array
{
$files = array();
if (is_dir($directory)) {
$fileNames = scandir($directory);
foreach ($fileNames as $fileName) {
if ($fileName == "." || $fileName == "..") {
continue;
}
$files[] = $fileName;
}
}
return $files;
}
/**
* Liest den gesamten Text aus einer Datei aus
* @param string $filename Dateipfad
* @return string|null Der enthaltene Text oder null bei einem Fehler
*/
static function readFileContent(string $filename): string|null
{
if (!file_exists($filename)) {
@@ -33,6 +70,54 @@ class Util
return $fileContent;
}
/**
* Schreibt Inhalt in eine Datei. Überschreibt vorhandene Inhalte. Erstellt eine neue Datei, falls nötig.
* @param string $filename Dateipfad
* @param string $content Zu schreibender Text
* @return bool true, wenn erfolgreich, false, wenn ein Fehler aufgetreten ist
*/
static function writeFileContent(string $filename, string $content): bool
{
$file = fopen($filename, "w");
if (!$file) {
return false;
}
if (!fwrite($file, $content)) {
return false;
}
fclose($file);
return true;
}
/**
* Löscht eine Datei oder einen Ordner inklusive aller Inhalte
* @param string $path Pfad zur Datei oder Verzeichnis
* @return bool true, wenn gelöscht, sonst false
*/
static function delete(string $path): bool
{
if(is_file($path)) {
if(!unlink($path)) {
return false;
}
} else if(is_dir($path)) {
$entries = scandir($path);
foreach ($entries as $entry) {
if($entry == "." || $entry == "..") {
continue;
}
self::delete($path . "/" . $entry);
}
}
return true;
}
/**
* Öffnet eine Datei und gibt JSON-Inhalte als Array zurück
* @param string $filename Dateipfad

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -4,8 +4,5 @@
"description": "Adverbiale Bestimmungen sind Satzteile, die zusätzliche Informationen über Umstände wie Zeit, Ort, Grund oder Art und Weise geben und dadurch die Handlung des Satzes genauer beschreiben.",
"relatedTopics": [
"wortarten", "vier-faelle"
],
"files": [
"exercise1.pdf"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -4,8 +4,5 @@
"description": "Das Thema \"Geschichten erzählen\" umfasst das kreative Gestalten und Vermitteln von Erlebnissen oder Fantasien durch eine spannende Handlung, interessante Charaktere und lebendige Beschreibungen, um die Zuhörer oder Leser zu fesseln.",
"relatedTopics": [
"satzglieder", "personalpronomen"
],
"files": [
"exercise1.pdf", "exercise2.pdf", "exercise3.pdf"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -4,8 +4,5 @@
"description": "Personalpronomen sind Wörter, die anstelle von Personen oder Dingen verwendet werden, wie zum Beispiel \"ich\", \"du\", \"er\", \"sie\" oder \"es\", um Wiederholungen zu vermeiden und Sätze flüssiger zu gestalten.",
"relatedTopics": [
"wortarten", "geschichten-erzaehlen"
],
"files": [
"exercise1.pdf"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -4,8 +4,5 @@
"description": "Satzglieder sind die Bausteine eines Satzes, die jeweils eine bestimmte Funktion erfüllen, wie Subjekt, Prädikat, Objekt oder adverbiale Bestimmung, und sich gemeinsam verschieben lassen, ohne die grammatische Korrektheit des Satzes zu verändern.",
"relatedTopics": [
"wortarten", "vier-faelle"
],
"files": [
"exercise1.pdf"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -4,8 +4,5 @@
"description": "Die vier Fälle im Deutschen - Nominativ, Genitiv, Dativ und Akkusativ - beschreiben die verschiedenen grammatischen Funktionen eines Nomens oder Pronomens im Satz, wie Subjekt, Besitz, indirektes Objekt oder direktes Objekt.",
"relatedTopics": [
"satzglieder"
],
"files": [
"exercise1.pdf", "exercise2.pdf"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 297 KiB

View File

@@ -4,8 +4,5 @@
"description": "Wortarten sind Kategorien, in die Wörter anhand ihrer grammatischen Funktion und Bedeutung eingeteilt werden, wie zum Beispiel Nomen, Verben, Adjektive und Adverbien.",
"relatedTopics": [
"satzglieder", "adverbiale-bestimmung", "personalpronomen"
],
"files": [
"exercise1.pdf"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 297 KiB

View File

@@ -4,8 +4,5 @@
"description": "Lorem Ipsum",
"relatedTopics": [
],
"files": [
"exercise1.pdf"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -4,8 +4,5 @@
"description": "Die Bruchrechnung ist ein Teil der Mathematik, der das Rechnen mit Brüchen beinhaltet, also das Teilen eines Ganzen in gleich große Teile, und umfasst Operationen wie Addition, Subtraktion, Multiplikation und Division von Brüchen.",
"relatedTopics": [
"schriftliches-multiplizieren", "schriftliches-dividieren", "punkt-vor-strichrechnung", "rechnen-mit-klammern"
],
"files": [
"exercise1.pdf", "exercise2.pdf"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -4,8 +4,5 @@
"description": "Die Regel \"Punkt vor Strichrechnung\" besagt, dass bei mathematischen Berechnungen Multiplikation und Division immer vor Addition und Subtraktion ausgeführt werden müssen, um das richtige Ergebnis zu erhalten.",
"relatedTopics": [
"rechnen-mit-klammern", "bruchrechnung"
],
"files": [
"exercise1.pdf"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -4,8 +4,5 @@
"description": "Rechnen mit Einheiten bedeutet, Größen mit verschiedenen Maßeinheiten wie Meter, Kilogramm oder Liter rechnerisch zu verarbeiten, dabei die Einheiten korrekt umzurechnen und sicherzustellen, dass das Ergebnis in der richtigen Einheit angegeben wird.",
"relatedTopics": [
"schriftliches-dividieren", "bruchrechnung"
],
"files": [
"exercise1.pdf", "exercise2.pdf"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -4,8 +4,5 @@
"description": "Beim Rechnen mit Klammern werden die Rechenoperationen innerhalb der Klammern zuerst ausgeführt, bevor die restlichen Berechnungen im Ausdruck vorgenommen werden, um die korrekte Reihenfolge der Rechenschritte einzuhalten.",
"relatedTopics": [
"punkt-vor-strichrechnung", "bruchrechnung"
],
"files": [
"exercise1.pdf"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -4,8 +4,5 @@
"description": "Schriftliches Dividieren ist eine Methode zur schrittweisen Aufteilung einer Zahl durch eine andere, wobei man die Teilschritte nacheinander schriftlich notiert, um das Ergebnis systematisch zu berechnen.",
"relatedTopics": [
"schriftliches-multiplizieren"
],
"files": [
"exercise1.pdf", "exercise2.pdf", "exercise3.pdf", "exercise4.pdf"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -4,8 +4,5 @@
"description": "Schriftliches Multiplizieren ist eine Rechenmethode, bei der zwei Zahlen schrittweise multipliziert werden, indem man die einzelnen Stellen der Zahlen nacheinander verrechnet, die Teilergebnisse notiert und am Ende addiert, um das Gesamtergebnis zu erhalten.",
"relatedTopics": [
"schriftliches-dividieren"
],
"files": [
"exercise1.pdf"
]
}

392
webseite/header.php Normal file
View File

@@ -0,0 +1,392 @@
<!-- header.php -->
<nav class="fixed top-0 w-full right-0 bg-white/80 backdrop-blur-lg shadow-sm z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<!-- Menu Toggle Button -->
<div class="flex items-center space-x-4">
<button class="menu-toggle hidden text-[var(--primary-color)] border-2 border-[var(--primary-color)] w-10 h-10 flex items-center justify-center rounded-lg hover:bg-[var(--primary-color)] hover:text-white transition duration-300">
<i class="fas fa-bars"></i>
</button>
<a href="index.php" class="flex items-center">
<img src="assets/images/hsgg-logo.png" alt="HSGG Logo" class="h-10 mr-3">
<span class="text-2xl font-bold text-[var(--primary-color)]">HSGG</span>
</a>
</div>
<!-- Login/Logout Button -->
<div class="flex items-center space-x-4">
<!-- Search Button -->
<button id="openSearchDialog"
class="bg-white text-[var(--primary-color)] border-2 border-[var(--primary-color)] w-10 h-10 flex items-center justify-center rounded-lg hover:bg-[var(--primary-color)] hover:text-white transition duration-300">
<i class="fas fa-search"></i>
</button>
<?php
require_once("classes/User.php");
session_start();
if (isset($_SESSION['user']) && $_SESSION['user']->isLoggedIn()) {
?>
<div class="flex items-center space-x-4">
<!-- Dropdown Button Elemente -->
<div class="relative">
<!-- Dropdown Trigger -->
<button id="userDropdownToggle"
class="bg-[var(--primary-color)] text-white px-4 py-2 rounded-lg hover:bg-[var(--accent-color)] transition duration-300 flex items-center">
<span><?php echo htmlspecialchars($_SESSION['user']->getUsername()); ?></span>
<i class="fas fa-chevron-down ml-2"></i>
</button>
<!-- Dropdown Menu -->
<div id="userDropdownMenu"
class="hidden absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border">
<!-- TODO: Accountseite entsprechend verlinken -->
<a href="index.php"
class="block px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-t-lg flex items-center">
<i class="fas fa-user mr-2"></i> Accountseite
</a>
<button id="dropdownChangePasswordButton"
class="block w-full text-left px-4 py-2 text-gray-700 hover:bg-gray-100 flex items-center">
<i class="fas fa-key mr-2"></i> Passwort ändern
</button>
<form id="dropdownLogoutForm" action="logout.php" method="POST" class="block">
<button type="submit"
class="w-full text-left px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-b-lg flex items-center">
<i class="fas fa-sign-out-alt mr-2"></i> Logout
</button>
</form>
</div>
</div>
<!-- Bearbeiten Button --> <!-- TODO: Korrekte/dynamische Verlinkung implementieren -->
<a href="index.php" class="bg-white text-[var(--primary-color)] border-2 border-[var(--primary-color)] w-10 h-10 flex items-center justify-center rounded-lg hover:bg-[var(--primary-color)] hover:text-white transition duration-300">
<i class="fa-solid fa-pen-to-square"></i>
</a>
</div>
<?php
} else {
// Login Button
echo '<button id="loginButton" class="bg-white text-[var(--primary-color)] border-2 border-[var(--primary-color)] w-10 h-10 flex items-center justify-center rounded-lg hover:bg-[var(--primary-color)] hover:text-white transition duration-300">
<i class="fas fa-sign-in-alt"></i>
</button>';
}
?>
</div>
</div>
</div>
</nav>
<!-- Login Popup | erscheint nur, wenn kein Nutzer eingeloggt ist -->
<?php
if (!isset($_SESSION['user']) || !$_SESSION['user']->isLoggedIn()) {
?>
<div id="loginPopup" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50" role="dialog"
aria-labelledby="loginTitle" aria-hidden="true">
<div class="bg-white p-8 rounded-lg shadow-lg w-full max-w-sm relative">
<button id="closePopupButton" class="absolute top-2 right-2 text-gray-500 text-xl"
aria-label="Close Login Popup">&times;
</button>
<h2 id="loginTitle" class="text-2xl font-bold mb-6 text-center">Login</h2>
<form id="loginForm" action="login.php" method="POST">
<div class="mb-4">
<label for="username" class="block text-gray-700 mb-2">Benutzername:</label>
<input type="text" id="username" name="username" class="w-full p-2 border rounded-lg" required
autofocus>
</div>
<div class="mb-4">
<label for="password" class="block text-gray-700 mb-2">Passwort:</label>
<input type="password" id="password" name="password" class="w-full p-2 border rounded-lg" required>
</div>
<button type="submit"
class="w-full bg-[var(--primary-color)] text-white px-4 py-2 rounded-lg hover:bg-[var(--accent-color)] transition duration-300">
Login
</button>
</form>
<div id="errorMessage" class="hidden text-red-500 text-center mt-4">Falscher Benutzername oder Passwort
</div>
</div>
</div>
<?php
}
?>
<!-- Passwort-ändern-Popup -->
<?php
if (isset($_SESSION['user']) && $_SESSION['user']->isLoggedIn()) {
?>
<div id="changePasswordPopup" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50"
role="dialog"
aria-labelledby="changePasswordTitle" aria-hidden="true">
<div class="bg-white p-8 rounded-lg shadow-lg w-full max-w-sm relative">
<button id="closeChangePasswordPopupButton" class="absolute top-2 right-2 text-gray-500 text-xl"
aria-label="Close Change Password Popup">&times;
</button>
<h2 id="changePasswordTitle" class="text-2xl font-bold mb-6 text-center">Passwort ändern</h2>
<form id="changePasswordForm" action="password.php" method="POST">
<div class="mb-4">
<label for="currentPassword" class="block text-gray-700 mb-2">Aktuelles Passwort:</label>
<input type="password" id="currentPassword" name="currentPassword"
class="w-full p-2 border rounded-lg" required>
</div>
<div class="mb-4">
<label for="newPassword" class="block text-gray-700 mb-2">Neues Passwort:</label>
<input type="password" id="newPassword" name="newPassword" class="w-full p-2 border rounded-lg"
required>
</div>
<div class="mb-4">
<label for="confirmNewPassword" class="block text-gray-700 mb-2">Neues Passwort bestätigen:</label>
<input type="password" id="confirmNewPassword" name="confirmNewPassword"
class="w-full p-2 border rounded-lg" required>
</div>
<button type="submit"
class="w-full bg-[var(--primary-color)] text-white px-4 py-2 rounded-lg hover:bg-[var(--accent-color)] transition duration-300">
Passwort ändern
</button>
</form>
<div id="changePasswordErrorMessage" class="hidden text-red-500 text-center mt-4"></div>
</div>
</div>
<?php
}
?>
<!-- Success Message Popup -->
<div id="passwordSuccessPopup" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-white p-6 rounded-lg shadow-lg w-full max-w-sm text-center">
<h2 class="text-xl font-bold text-green-600 mb-4">Erfolg!</h2>
<p>Passwort wurde erfolgreich geändert.</p>
<button id="closeSuccessPopup" class="mt-4 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700">
Schließen
</button>
</div>
</div>
<!-- Search Dialog -->
<div id="searchDialog" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="bg-white p-8 rounded-lg shadow-lg w-full max-w-lg relative">
<button id="closeSearchDialog" class="absolute top-2 right-2 text-gray-500 text-xl">&times;</button>
<h2 class="text-2xl font-bold mb-6 text-center">Suche</h2>
<input type="text" id="searchInput" placeholder="Search..." class="w-full p-2 border rounded-lg mb-4">
<div id="searchResults" class="max-h-64 overflow-y-auto"></div>
</div>
</div>
<script>
// JavaScript to handle opening and closing of the login popup
const loginButton = document.getElementById('loginButton');
const loginPopup = document.getElementById('loginPopup');
const closePopupButton = document.getElementById('closePopupButton');
const usernameInput = document.getElementById('username');
const errorMessage = document.getElementById('errorMessage');
if (loginButton) { // Überprüfen, ob das Element vorhanden ist
loginButton.addEventListener('click', function () {
loginPopup.classList.remove('hidden');
usernameInput.focus(); // Set focus to username field
});
}
if (closePopupButton) { // Überprüfen, ob das Element vorhanden ist
closePopupButton.addEventListener('click', function () {
loginPopup.classList.add('hidden');
});
}
window.addEventListener('click', function (event) {
if (event.target === loginPopup) {
loginPopup.classList.add('hidden');
}
});
// Schließe Popup mit ESC
document.addEventListener('keydown', function (event) {
if (event.key === "Escape" && !loginPopup.classList.contains('hidden')) {
loginPopup.classList.add('hidden');
}
});
// Zeige Fehlermeldung beim Login an
window.addEventListener('DOMContentLoaded', (event) => {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('error') && urlParams.get('error') === '1') {
loginPopup.classList.remove('hidden');
errorMessage.classList.remove('hidden');
}
});
// JavaScript to handle opening and closing of the change password popup
const changePasswordButton = document.getElementById('changePasswordButton');
const changePasswordPopup = document.getElementById('changePasswordPopup');
const closeChangePasswordPopupButton = document.getElementById('closeChangePasswordPopupButton');
if (changePasswordButton) { // Überprüfen, ob das Element vorhanden ist
changePasswordButton.addEventListener('click', function () {
if (changePasswordPopup) {
changePasswordPopup.classList.remove('hidden');
}
});
}
if (closeChangePasswordPopupButton) {
closeChangePasswordPopupButton.addEventListener('click', function () {
if (changePasswordPopup) {
changePasswordPopup.classList.add('hidden');
}
});
}
window.addEventListener('click', function (event) {
if (event.target === changePasswordPopup) {
changePasswordPopup.classList.add('hidden');
}
});
// Close popup with ESC key
document.addEventListener('keydown', function (event) {
if (event.key === "Escape" && !changePasswordPopup.classList.contains('hidden')) {
changePasswordPopup.classList.add('hidden');
}
});
// Zeige Fehlermeldung beim Passwort ändern an
window.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('password_error')) {
const changePasswordPopup = document.getElementById('changePasswordPopup');
const changePasswordErrorMessage = document.getElementById('changePasswordErrorMessage');
const errorType = urlParams.get('password_error');
changePasswordPopup.classList.remove('hidden');
changePasswordErrorMessage.classList.remove('hidden');
switch (errorType) {
case 'wrong_current_password':
changePasswordErrorMessage.textContent = 'Das aktuelle Passwort ist falsch.';
break;
case 'password_mismatch':
changePasswordErrorMessage.textContent = 'Die neuen Passwörter stimmen nicht überein.';
break;
default:
changePasswordErrorMessage.textContent = 'Fehler beim Ändern des Passworts.';
}
}
});
// Zeige Erfolgspopup beim Passwortwechsel an
window.addEventListener('DOMContentLoaded', (event) => {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('password_success')) {
const passwordSuccessPopup = document.getElementById('passwordSuccessPopup');
if (passwordSuccessPopup) {
passwordSuccessPopup.classList.remove('hidden');
}
const closeSuccessPopup = document.getElementById('closeSuccessPopup');
if (closeSuccessPopup) {
closeSuccessPopup.addEventListener('click', () => {
passwordSuccessPopup.classList.add('hidden');
// Optional: Entferne den URL-Parameter ohne Neuladen
const newUrl = window.location.href.split('?')[0];
window.history.replaceState({}, document.title, newUrl);
});
}
}
});
// Dropdown öffnen/schließen
const userDropdownToggle = document.getElementById('userDropdownToggle');
const userDropdownMenu = document.getElementById('userDropdownMenu');
if (userDropdownToggle && userDropdownMenu) {
userDropdownToggle.addEventListener('click', (event) => {
event.stopPropagation(); // Verhindert das Schließen des Menüs bei Klick auf den Button
userDropdownMenu.classList.toggle('hidden');
});
// Schließe Dropdown, wenn außerhalb geklickt wird
window.addEventListener('click', () => {
if (!userDropdownMenu.classList.contains('hidden')) {
userDropdownMenu.classList.add('hidden');
}
});
// Schließe Dropdown mit ESC
document.addEventListener('keydown', (event) => {
if (event.key === "Escape" && !userDropdownMenu.classList.contains('hidden')) {
userDropdownMenu.classList.add('hidden');
}
});
}
// Passwort ändern über Dropdown öffnen
const dropdownChangePasswordButton = document.getElementById('dropdownChangePasswordButton');
if (dropdownChangePasswordButton) {
dropdownChangePasswordButton.addEventListener('click', () => {
const changePasswordPopup = document.getElementById('changePasswordPopup');
if (changePasswordPopup) {
changePasswordPopup.classList.remove('hidden');
}
});
}
document.getElementById('openSearchDialog').addEventListener('click', function () {
document.getElementById('searchDialog').classList.remove('hidden');
});
document.getElementById('closeSearchDialog').addEventListener('click', function () {
document.getElementById('searchDialog').classList.add('hidden');
});
function debounce(func, delay) {
let debounceTimer;
return function () {
const context = this;
const args = arguments;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(context, args), delay);
};
}
const searchInput = document.getElementById('searchInput');
const resultsContainer = document.getElementById('searchResults');
searchInput.addEventListener('input', debounce(function () {
const query = this.value.toLowerCase();
resultsContainer.innerHTML = '';
if (query.length > 0) {
fetch('search.php?query=' + query)
.then(response => response.json())
.then(data => {
data.forEach(item => {
const resultItem = document.createElement('div');
resultItem.classList.add('p-4', 'mb-2', 'rounded-lg', 'bg-white', 'hover:bg-gray-100', 'transition', 'duration-100', 'flex', 'items-center', 'space-x-2', 'cursor-pointer');
const subjectSpan = document.createElement('span');
subjectSpan.classList.add('font-bold');
subjectSpan.textContent = item.displayName.split(' - ')[0];
const breadcrumbSpan = document.createElement('span');
breadcrumbSpan.classList.add('text-gray-500');
breadcrumbSpan.textContent = item.displayName.split(' - ').slice(1).join(' > ');
resultItem.appendChild(subjectSpan);
resultItem.appendChild(breadcrumbSpan);
resultItem.addEventListener('click', function () {
if (item.type === 'subject') {
window.location.href = 'subject.php?subject=' + item.id;
} else {
window.location.href = 'topic.php?subject=' + item.subjectId + '&topic=' + item.id;
}
});
resultsContainer.appendChild(resultItem);
});
});
}
}, 300));
</script>

View File

@@ -16,16 +16,7 @@
</div>
<!-- Navigation -->
<nav class="fixed top-0 w-full bg-white/80 backdrop-blur-lg shadow-sm z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<a href="index.php" class="flex items-center">
<img src="assets/images/hsgg-logo.png" alt="HSGG Logo" class="h-10 mr-3">
<span class="text-2xl font-bold text-[var(--primary-color)]">HSGG</span>
</a>
</div>
</div>
</nav>
<?php include 'header.php'; ?>
<div class="content mt-24 max-w-7xl mx-[16px] xl:mx-auto">
<h1 class="text-3xl font-bold text-gray-900 mb-6">Impressum</h1>

View File

@@ -21,17 +21,7 @@
<div class="blob absolute w-96 h-96 bg-sky-300/30 bottom-0 right-0"></div>
</div>
<!-- Navigation -->
<nav class="fixed top-0 w-full bg-white/80 backdrop-blur-lg shadow-sm z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<a href="index.php" class="flex items-center">
<img src="assets/images/hsgg-logo.png" alt="HSGG Logo" class="h-10 mr-3">
<span class="text-2xl font-bold text-[var(--primary-color)]">HSGG</span>
</a>
</div>
</div>
</nav>
<?php include 'header.php'; ?>
<!-- Hero Section -->
<div class="hidden md:block pt-24 px-4">

24
webseite/login.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
require_once("classes/User.php");
session_start();
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$username = $_POST['username'];
$password = $_POST['password'];
// $testuser = User::createUser("MAX", "password");
// Try to retrieve the user from the username
$user = User::getFromUsername($username);
if ($user && $user->login($password)) {
// Redirect to a protected page if login is successful
header("Location: index.php?login=success");
} else {
// Redirect back to the login page with an error message
header("Location: index.php?error=1");
}
}
?>

22
webseite/logout.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
require_once("classes/User.php");
session_start();
if (isset($_SESSION['user']) && $_SESSION['user'] instanceof User) {
$user = $_SESSION['user'];
if ($user->logout()) {
// Logout successful, redirect to homepage
header("Location: index.php");
exit();
} else {
// Logout failed, handle error
echo "<script>alert('Logout failed. Please try again.'); window.location.href='index.php';</script>";
}
} else {
// No user is logged in, redirect to homepage
header("Location: index.php");
exit();
}
?>

39
webseite/password.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
require_once("classes/User.php");
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_SESSION['user']) || !$_SESSION['user']->isLoggedIn()) {
header("Location: index.php?password_error=not_logged_in");
exit();
}
$currentPassword = $_POST['currentPassword'];
$newPassword = $_POST['newPassword'];
$confirmNewPassword = $_POST['confirmNewPassword'];
$user = $_SESSION['user'];
// Prüfe, ob das aktuelle Passwort korrekt ist
if (!$user->isPasswordCorrect($currentPassword)) {
header("Location: index.php?password_error=wrong_current_password");
exit();
}
// Prüfe, ob die neuen Passwörter übereinstimmen
if ($newPassword !== $confirmNewPassword) {
header("Location: index.php?password_error=password_mismatch");
exit();
}
// Aktualisiere das Passwort
if ($user->changePassword($currentPassword, $newPassword)) {
header("Location: index.php?password_success=1");
exit();
} else {
header("Location: index.php?password_error=update_failed");
exit();
}
}
?>

40
webseite/search.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
require_once("classes/SubjectData.php");
require_once("classes/TopicData.php");
if (!isset($_GET['query'])) {
die(json_encode([]));
}
$query = strtolower(trim($_GET['query']));
$subjects = SubjectData::getAll();
$results = [];
foreach ($subjects as $subject) {
if (strpos(strtolower($subject->displayName), $query) !== false) {
$results[] = [
'type' => 'subject',
'id' => $subject->id,
'displayName' => $subject->displayName
];
}
foreach ($subject->topics as $topic) {
if (
strpos(strtolower($subject->displayName), $query) !== false ||
strpos(strtolower($topic->displayName), $query) !== false ||
strpos(strtolower($topic->description), $query) !== false ||
strpos(strtolower($topic->article), $query) !== false
) {
$results[] = [
'type' => 'topic',
'subjectId' => $topic->subjectId,
'id' => $topic->id,
'displayName' => $subject->displayName . ' - ' . $topic->displayName
];
}
}
}
header('Content-Type: application/json');
echo json_encode($results);
?>

View File

@@ -31,8 +31,9 @@ $topics = $subjectData->topics;
<body class="min-h-screen">
<?php include 'header.php'; ?>
<nav class="sidebar bg-[<?php echo($subjectData->color); ?>]">
<nav class="sidebar bg-[<?php echo($subjectData->color); ?>] pt-24">
<div class="sidebar-header">
<i class="fas fa-graduation-cap"></i>
@@ -47,15 +48,6 @@ $topics = $subjectData->topics;
</a>
</nav>
<div class="search-container bg-white">
<button class="menu-toggle h-12 w-12 border-2 p-1 hover:border-[<?php echo($subjectData->color); ?>]">
<i class="fas fa-bars"></i>
</button>
<input type="text" class="search-box p-3 border-2 w-full focus:border-[<?php echo($subjectData->color); ?>]"
id="searchInput" placeholder="Suche nach Themen, Übungen oder Hilfe..."
oninput="handleSearch()">
</div>
<div class="main-content">
<div class="max-w-7xl mx-auto px-4 py-12 grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 gap-8 mb-8">
@@ -80,13 +72,13 @@ $topics = $subjectData->topics;
<h4>Verwandte Themen:</h4>
<ul>
<?php
foreach ($topicData->relatedTopics as $relatedTopicName) {
$relatedTopic = $subjectData->topics[$relatedTopicName];
if (!isset($relatedTopicName)) {
continue;
}
foreach ($topicData->relatedTopics as $relatedTopicName) {
$relatedTopic = $subjectData->topics[$relatedTopicName];
if (!isset($relatedTopicName)) {
continue;
}
?>
?>
<li onclick="event.stopPropagation();" class="border-[<?php echo($subjectData->color); ?>] border-2">
<a href='<?php echo("topic.php?subject=$subjectData->id&topic=$relatedTopic->id") ?>'>
@@ -95,8 +87,8 @@ $topics = $subjectData->topics;
</li>
<?php
}
?>
}
?>
</ul>
</div>-->
</div>
@@ -108,7 +100,7 @@ $topics = $subjectData->topics;
?>
<a onclick="event.stopPropagation();"
href="<?php echo("config/subjects/$subjectData->id/topics/$topicData->id/$fileName") ?>"
href="<?php echo("config/subjects/$subjectData->id/topics/$topicData->id/downloads/$fileName") ?>"
target="_blank" download
class="download-btn border-2 border-[<?php echo($subjectData->color); ?>]">
<i class="fas fa-file-pdf"></i>

View File

@@ -36,13 +36,10 @@ if (!isset($topicData)) {
<body class="min-h-screen">
<button class="menu-toggle fixed top-4 left-4 z-50 bg-white h-12 w-12 border-2 p-1 hover:border-[<?php echo($subjectData->color); ?>]">
<i class="fas fa-bars"></i>
</button>
<?php include 'header.php'; ?>
<!-- Left Sidebar -->
<nav class="sidebar bg-[<?php echo($subjectData->color); ?>]">
<nav class="sidebar bg-[<?php echo($subjectData->color); ?>] pt-24">
<div class="sidebar-header">
<i class="fas fa-graduation-cap"></i>
<h2><?php echo($subjectData->displayName); ?></h2>
@@ -102,7 +99,7 @@ if (!isset($topicData)) {
foreach ($topicData->files as $fileName) {
?>
<a href='<?php echo("config/subjects/$subjectData->id/topics/$topicData->id/$fileName") ?>'
<a href='<?php echo("config/subjects/$subjectData->id/topics/$topicData->id/downloads/$fileName") ?>'
target="_blank" download
class="download-btn border-[<?php echo($subjectData->color); ?>] border-2">
<i class="fas fa-file-pdf"></i>

2
webseite/users/users.csv Normal file
View File

@@ -0,0 +1,2 @@
admin,"$argon2i$v=19$m=65536,t=4,p=1$UVN1S1loTmxVSEdqTjVFcQ$J2sL51VLx7Deg7xJRnbrKfIVqGduh+nrGOFGFGSZ4vw"
MAX,"$argon2i$v=19$m=65536,t=4,p=1$UVN1S1loTmxVSEdqTjVFcQ$J2sL51VLx7Deg7xJRnbrKfIVqGduh+nrGOFGFGSZ4vw"
1 admin $argon2i$v=19$m=65536,t=4,p=1$UVN1S1loTmxVSEdqTjVFcQ$J2sL51VLx7Deg7xJRnbrKfIVqGduh+nrGOFGFGSZ4vw
2 MAX $argon2i$v=19$m=65536,t=4,p=1$UVN1S1loTmxVSEdqTjVFcQ$J2sL51VLx7Deg7xJRnbrKfIVqGduh+nrGOFGFGSZ4vw