Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1a32120d6 | |||
| 97fa1de999 | |||
| 7341fe7733 | |||
| 5b0528a914 | |||
| ac64e65dd3 | |||
| 96ffa3288e | |||
| 0c6cf13efa | |||
| d3884d2fc1 | |||
| 1575f4fa24 | |||
| 0aca76d2f4 | |||
| eca6646071 | |||
| 35e5006063 | |||
| fcb9289d80 | |||
| 6595a3d78d | |||
| 41d8ac6721 | |||
| 63c213bae7 | |||
| 61d72436c9 | |||
| 721bc8d833 | |||
| 2666d2d14f | |||
| de1ba88860 | |||
| e0fb04794d | |||
| 6897a29574 | |||
| 8836034670 | |||
| 98b088afc0 | |||
| 51c850d122 | |||
| d590561ade | |||
| ba11c216bd | |||
| 71a1b029cb | |||
| 2126d076d5 | |||
| 2ecd446dd0 | |||
| 97302f1bb3 | |||
| 35d4425086 | |||
| d3435d4cde | |||
| f61c548b9e | |||
| 101a2a247b | |||
| 423941e7ed | |||
| a8e76c782d | |||
| 3f9c048493 | |||
| fee5a701df | |||
| d341312192 | |||
| 4dadfb8863 |
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="dataSourceStorageLocal" created-in="IU-253.32098.101">
|
||||
<component name="dataSourceStorageLocal" created-in="IU-261.24374.151">
|
||||
<data-source name="articles" uuid="315cb5c9-2b0f-435b-b602-59823b160908">
|
||||
<database-info product="SQLite" version="3.51.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.51.1.0" dbms="SQLITE" exact-version="3.51.1" exact-driver-version="3.51">
|
||||
<identifier-quote-string>"</identifier-quote-string>
|
||||
|
||||
+1
-5
@@ -4,11 +4,7 @@
|
||||
-->
|
||||
<main>
|
||||
|
||||
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "internal_error"): ?>
|
||||
<p class="alert-message is-error">
|
||||
Es ist ein interner Fehler beim Speichern aufgetreten. Bitte versuche es erneut.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php include_once "includes/alertMessages.php"?>
|
||||
<?php
|
||||
unset($_SESSION["message"]);
|
||||
?>
|
||||
|
||||
+87
-66
@@ -2,20 +2,37 @@
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
require_once __DIR__ . '/../includes/resultsHelper.php';
|
||||
|
||||
$all_results = $_SESSION["search_results"] ?? [];
|
||||
$rawResults = $_SESSION["search_results"] ?? [];
|
||||
$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])) {
|
||||
$limit = 10;
|
||||
}
|
||||
$_SESSION['search_limit'] = $limit;
|
||||
|
||||
// Gesamtseitenzahl
|
||||
$totalPages = max(1, ceil($totalResultsCount / $limit));
|
||||
// Serverseitig sortieren + filtern
|
||||
$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;
|
||||
if ($currentPage < 1) {
|
||||
$currentPage = 1;
|
||||
@@ -23,21 +40,33 @@ if ($currentPage < 1) {
|
||||
$currentPage = $totalPages;
|
||||
}
|
||||
|
||||
// Startpunkt im Array berechnen (Offset)
|
||||
$offset = ($currentPage - 1) * $limit;
|
||||
|
||||
// Nur die Ergebnisse für die aktuelle Seite ausschneiden
|
||||
//$results = array_slice($all_results, $offset, $limit);
|
||||
$results = $all_results;
|
||||
// Dies ist die No-JS-Basisversion: nur die aktuelle Seite wird gerendert.
|
||||
// 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);
|
||||
|
||||
// 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
|
||||
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">
|
||||
<?php include_once "includes/alertMessages.php" ?>
|
||||
@@ -45,20 +74,25 @@ $resultCount = count($results);
|
||||
<!-- Links: Seitenleiste für Filter und Suche -->
|
||||
<aside class="s-res-sidebar">
|
||||
|
||||
<!-- Sortierfuntion Box und Such Box-->
|
||||
<!-- Formular 1: startet eine NEUE Suche (löst immer einen Server-Request aus) -->
|
||||
<form action="php/controller/search-results-controller.php" method="GET" id="search-form-id" class="s-res-sidebar-form">
|
||||
|
||||
<input type="hidden" id="s-res-page-input" name="page" value="<?php echo $_GET['page'] ?? 1; ?>">
|
||||
|
||||
<div class="s-res-sidebar-box">
|
||||
<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>
|
||||
<button type="submit" class="nav__search-button">Suchen</button>
|
||||
</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">
|
||||
<h3 class="s-res-sidebar-title">Sortierung</h3>
|
||||
<?php $currentSort = $_SESSION['search_sort'] ?? 'alphabet'; ?>
|
||||
<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' : ''; ?>>
|
||||
@@ -81,37 +115,17 @@ $resultCount = count($results);
|
||||
|
||||
<div class="s-res-sidebar-box">
|
||||
<h3 class="s-res-sidebar-title">Kategorie filtern</h3>
|
||||
<select id="category-filter" class="s-res-limit-select" style="width: 100%; padding: 8px; border-radius: 6px; border: 1px solid #cbd5e1;">
|
||||
<option value="all">Alle Kategorien</option>
|
||||
<option value="Deutsch">Deutsch</option>
|
||||
<option value="Englisch">Englisch</option>
|
||||
<option value="Franzoesisch">Französisch</option>
|
||||
<option value="Latein">Latein</option>
|
||||
<option value="Literatur">Literatur</option>
|
||||
<option value="Mathematik">Mathematik</option>
|
||||
<option value="Biologie">Biologie</option>
|
||||
<option value="Informatik">Informatik</option>
|
||||
<option value="Chemie">Chemie</option>
|
||||
<option value="Physik">Physik</option>
|
||||
<option value="Astronomie">Astronomie</option>
|
||||
<option value="Geschichte">Geschichte</option>
|
||||
<option value="Erdkunde">Erdkunde</option>
|
||||
<option value="Sozialkunde">Sozialkunde</option>
|
||||
<option value="Wirtschaftskunde">Wirtschaftskunde</option>
|
||||
<option value="Religion">Religion</option>
|
||||
<option value="Ethikunterricht">Ethikunterricht</option>
|
||||
<option value="Philosophie">Philosophie</option>
|
||||
<option value="Psychologie">Psychologie</option>
|
||||
<option value="Kunst">Kunst</option>
|
||||
<option value="Musik">Musik</option>
|
||||
<option value="Theater">Theater</option>
|
||||
<option value="Technik">Technik</option>
|
||||
<option value="Werken">Werken</option>
|
||||
<option value="Hauswirtschaft">Hauswirtschaft</option>
|
||||
<option value="Sport">Sport</option>
|
||||
<select 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>
|
||||
|
||||
</aside>
|
||||
@@ -120,17 +134,15 @@ $resultCount = count($results);
|
||||
|
||||
<div class="s-res-header">
|
||||
<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>
|
||||
|
||||
<!-- Ergebnisliste -->
|
||||
<div class="s-res-list">
|
||||
<?php
|
||||
if (!empty($results)): ?>
|
||||
<?php if (!empty($results)): ?>
|
||||
|
||||
<?php foreach ($results as $item):
|
||||
// Anzahl der Likes ermitteln (falls es ein Array ist, zählen; falls Zahl, direkt nutzen)
|
||||
$likesCount = isset($item['likes']) && is_array($item['likes']) ? count($item['likes']) : ($item['likes'] ?? 0);
|
||||
$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">
|
||||
@@ -151,39 +163,48 @@ $resultCount = count($results);
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php
|
||||
elseif (isset($_SESSION["search_query"]) && $_SESSION["search_query"] !== "" && $resultCount === 0): ?>
|
||||
<p>Keine Beiträge zu diesem Suchbegriff gefunden.</p>
|
||||
<?php
|
||||
elseif (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_search_query"): ?>
|
||||
<?php elseif (isset($_SESSION["message"]) && $_SESSION["message"] === "invalid_search_query"): ?>
|
||||
<p>Unzulässige Suchanfrage</p>
|
||||
|
||||
<?php elseif ($query !== "" && $totalResultsCount === 0 && count($rawResults) > 0): ?>
|
||||
<p>Keine Beiträge in dieser Kategorie gefunden.</p>
|
||||
|
||||
<?php elseif ($query !== "" && $totalResultsCount === 0): ?>
|
||||
<p>Keine Beiträge zu diesem Suchbegriff gefunden.</p>
|
||||
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
unset($_SESSION["message"]);
|
||||
?>
|
||||
<?php unset($_SESSION["message"]); ?>
|
||||
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<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="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="s-res-filter-form" class="nav__search-button">Übernehmen</button></noscript>
|
||||
</div>
|
||||
|
||||
<div class="s-res-page-navigation">
|
||||
<button type="button" class="s-res-page-btn" id="prev-page-btn" data-page="0">«</button>
|
||||
<nav class="s-res-page-navigation" aria-label="Seitennavigation">
|
||||
<!-- No-JS-Fallback: -->
|
||||
<noscript>
|
||||
<?php echo renderNoJsPagination($currentPage, $totalPages, $query, $currentSort, $currentCategory, $limit); ?>
|
||||
</noscript>
|
||||
|
||||
<!-- JS-Version: wird per search-results.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" data-page="2">»</button>
|
||||
<button type="button" class="s-res-page-btn" id="next-page-btn">»</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
+152
-79
@@ -1,111 +1,184 @@
|
||||
<?php
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
include_once "php/controller/showCategory-controller.php";
|
||||
require_once __DIR__ . '/../includes/resultsHelper.php';
|
||||
|
||||
$currentSort = isset($sortStyle) ? $sortStyle : 'alphabet';
|
||||
$currentPage = isset($page) ? $page : 1;
|
||||
$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" ?>
|
||||
|
||||
<!-- Links: Seitenleiste für Suche (innerhalb der Kategorie) und Sortierung -->
|
||||
<aside class="s-res-sidebar">
|
||||
|
||||
<!-- 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>
|
||||
Bitte JavaScript aktivieren!
|
||||
<button type="submit" class="nav__search-button">Anwenden</button>
|
||||
</noscript>
|
||||
</form>
|
||||
|
||||
<!-- Das Formular umschließt jetzt das gesamte Layout -->
|
||||
<form id="search-form-id" action="index.php" method="GET" style="display: contents;">
|
||||
<input type="hidden" name="pfad" value="showCategory">
|
||||
<input type="hidden" name="category" value="<?php echo (isset($category) && !empty($category)) ? htmlspecialchars($category) : ''; ?>">
|
||||
<input type="hidden" id="s-res-page-input" name="page" value="<?php echo $currentPage; ?>">
|
||||
|
||||
<div class="cat-view-container">
|
||||
|
||||
<!-- LINKE SPALTE: SEITENLEISTE FÜR SORTIERUNG -->
|
||||
<aside class="cat-sidebar">
|
||||
<div class="cat-sidebar-box">
|
||||
<h3 class="cat-sidebar-title">Sortierung</h3>
|
||||
<div class="cat-filter-group">
|
||||
<label class="cat-filter-option">
|
||||
<input type="radio" name="sort" value="alphabet" <?php echo ($currentSort === 'alphabet') ? 'checked' : ''; ?> onchange="this.form.submit();">
|
||||
Alphabetisch
|
||||
</label>
|
||||
<label class="cat-filter-option">
|
||||
<input type="radio" name="sort" value="likes" <?php echo ($currentSort === 'likes') ? 'checked' : ''; ?> onchange="this.form.submit();">
|
||||
Beliebtheit (Likes)
|
||||
</label>
|
||||
<label class="cat-filter-option">
|
||||
<input type="radio" name="sort" value="newest" <?php echo ($currentSort === 'newest') ? 'checked' : ''; ?> onchange="this.form.submit();">
|
||||
Neueste Beiträge
|
||||
</label>
|
||||
<label class="cat-filter-option">
|
||||
<input type="radio" name="sort" value="oldest" <?php echo ($currentSort === 'oldest') ? 'checked' : ''; ?> onchange="this.form.submit();">
|
||||
Älteste Beiträge
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- RECHTE SPALTE: INHALT & PAGINATOR -->
|
||||
<main class="cat-view-main">
|
||||
<div class="cat-view-header">
|
||||
<h1 class="cat-view-title">Kategorie: <?php echo (isset($category) && !empty($category)) ? htmlspecialchars($category) : 'Unbekannt'; ?></h1>
|
||||
<p class="cat-view-meta-text">
|
||||
<?php echo (isset($totalArticles)) ? $totalArticles : 0; ?> Treffer in dieser Kategorie
|
||||
<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>
|
||||
|
||||
<!-- Beiträge -->
|
||||
<div class="cat-view-list">
|
||||
<?php if (isset($articles) && !empty($articles)): ?>
|
||||
<?php foreach ($articles as $article): ?>
|
||||
<div class="cat-view-item">
|
||||
<div class="cat-view-content">
|
||||
<h2 class="cat-view-item-title">
|
||||
<a href="index.php?pfad=showArticle&id=<?php echo htmlspecialchars($article->getID()); ?>" class="cat-view-link">
|
||||
<?php echo (isset($article) && !empty($article->getTitle())) ? htmlspecialchars($article->getTitle()) : 'Kein Titel'; ?>
|
||||
<!-- Ergebnisliste -->
|
||||
<div class="s-res-list">
|
||||
<?php if (!empty($results)): ?>
|
||||
|
||||
<?php foreach ($results as $item):
|
||||
$likesCount = getLikeCount($item);
|
||||
?>
|
||||
<div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>">
|
||||
<div class="s-res-content">
|
||||
<h2 class="s-res-item-title">
|
||||
<a href="index.php?pfad=showArticle&id=<?php echo $item['id']; ?>" class="s-res-link">
|
||||
<?php echo htmlspecialchars($item['title']); ?>
|
||||
</a>
|
||||
</h2>
|
||||
<div class="s-res-meta-row">
|
||||
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($item['author']); ?></span></p>
|
||||
|
||||
<div class="cat-view-meta-row">
|
||||
<p class="cat-view-author">
|
||||
von <span class="cat-view-author-name"><?php echo (isset($article) && !empty($article->getAuthor())) ? htmlspecialchars($article->getAuthor()) : 'Anonym'; ?></span>
|
||||
</p>
|
||||
<span class="cat-view-likes">
|
||||
❤️ <?php echo (isset($article) && !empty($article->getLikes())) ? htmlspecialchars($article->getLikes()) : '0'; ?>
|
||||
<span class="s-res-likes">
|
||||
❤️ <?php echo $likesCount; ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="cat-view-arrow">→</div>
|
||||
<div class="s-res-arrow">→</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php elseif ($query !== ''): ?>
|
||||
<p>Keine Beiträge in dieser Kategorie zu diesem Suchbegriff gefunden.</p>
|
||||
|
||||
<?php else: ?>
|
||||
<p class="cat-view-meta-text">Keine Artikel in dieser Kategorie gefunden.</p>
|
||||
<p>Keine Beiträge in dieser Kategorie gefunden.</p>
|
||||
|
||||
<?php endif; ?>
|
||||
<?php unset($_SESSION["message"]); ?>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Footer Paginator -->
|
||||
<?php if (isset($totalPages) && $totalPages > 1): ?>
|
||||
<div class="cat-view-pagination-footer">
|
||||
<div></div>
|
||||
<div class="s-res-page-navigation">
|
||||
<button type="button" class="s-res-page-btn" data-page="<?php echo $currentPage - 1; ?>" <?php echo ($currentPage <= 1) ? 'disabled' : ''; ?>>
|
||||
«
|
||||
</button>
|
||||
<div class="s-res-pagination-footer">
|
||||
|
||||
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
|
||||
<button type="button"
|
||||
class="s-res-page-btn <?php echo ($i === $currentPage) ? 'cat-active-btn' : ''; ?>"
|
||||
data-page="<?php echo $i; ?>"
|
||||
<?php echo ($i === $currentPage) ? 'disabled' : ''; ?>>
|
||||
<?php echo $i; ?>
|
||||
</button>
|
||||
<?php endfor; ?>
|
||||
<!-- 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>
|
||||
|
||||
<button type="button" class="s-res-page-btn" data-page="<?php echo $currentPage + 1; ?>" <?php echo ($currentPage >= $totalPages) ? 'disabled' : ''; ?>>
|
||||
»
|
||||
</button>
|
||||
<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>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+55
-23
@@ -5,10 +5,25 @@ if (!isset($_SESSION["user"])) {
|
||||
exit();
|
||||
}
|
||||
include_once 'php/controller/showArticle-controller.php';
|
||||
|
||||
// Aktuelle Blockliste ermitteln:
|
||||
$rawContent = '[]';
|
||||
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])) {
|
||||
$rawContent = $_SESSION['old_content'];
|
||||
unset($_SESSION['old_content']);
|
||||
} elseif (isset($content) && !empty($content)) {
|
||||
$rawContent = $content;
|
||||
}
|
||||
|
||||
$blocks = json_decode($rawContent, true);
|
||||
if (!is_array($blocks)) {
|
||||
$blocks = [];
|
||||
}
|
||||
$blocks = array_values($blocks);
|
||||
?>
|
||||
<!--
|
||||
Seite: Beitrag erstellen
|
||||
Inhalt: Formular für die Erstellung eines neuen Beitrags
|
||||
Seite: Beitrag bearbeiten
|
||||
Inhalt: Formular für die Bearbeitung eines Beitrags
|
||||
-->
|
||||
<form method="post" action="php/controller/updateArticle-controller.php?id=<?php if(isset($id) && !empty($id)){echo htmlspecialchars($id);}else{$_SESSION["message"] = "missing_id";} ?>" id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container">
|
||||
|
||||
@@ -27,32 +42,49 @@ include_once 'php/controller/showArticle-controller.php';
|
||||
?>"
|
||||
placeholder="Titel hier eingeben" required>
|
||||
|
||||
<!-- Hier werden die dynamischen divs via JavaScript eingefügt -->
|
||||
<div id="block-container"></div>
|
||||
<!--
|
||||
Content-Blöcke: werden serverseitig als echte, benannte Formularfelder gerendert
|
||||
(blocks[i][type]...). Dadurch funktioniert das Hinzufügen/Entfernen von Blöcken und
|
||||
der Bild-Upload auch ganz ohne JavaScript über einen normalen Formular-Submit.
|
||||
Ist JavaScript aktiv, fängt js/editor.js diese Submits ab und erledigt dieselbe
|
||||
Änderung lokal im DOM, ohne den Server zu belasten.
|
||||
-->
|
||||
<div id="block-container">
|
||||
<?php foreach ($blocks as $i => $block): ?>
|
||||
<?php
|
||||
$blockType = $block['type'] ?? '';
|
||||
if ($blockType !== 'text' && $blockType !== 'image') {
|
||||
continue; // unbekannter/kaputter Block wird übersprungen
|
||||
}
|
||||
?>
|
||||
<div class="editor-block article-editor-scope" data-index="<?php echo (int)$i; ?>">
|
||||
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][type]" value="<?php echo htmlspecialchars($blockType); ?>">
|
||||
|
||||
<?php if ($blockType === 'text'): ?>
|
||||
<textarea name="blocks[<?php echo (int)$i; ?>][text]"
|
||||
placeholder="Schreibe deinen Textblock..."><?php echo htmlspecialchars($block['value'] ?? ''); ?></textarea>
|
||||
<?php else: /* image */ ?>
|
||||
<?php if (!empty($block['value'])): ?>
|
||||
<img src="<?php echo htmlspecialchars($block['value']); ?>"
|
||||
class="block-image-preview"
|
||||
style="max-width:200px;display:block;margin-top:10px;">
|
||||
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][existing_image]" value="<?php echo htmlspecialchars($block['value']); ?>">
|
||||
<?php endif; ?>
|
||||
<input type="file" name="blocks[<?php echo (int)$i; ?>][image]" accept="image/*">
|
||||
<?php endif; ?>
|
||||
|
||||
<button type="submit" name="editor_action" value="delete_block:<?php echo (int)$i; ?>" class="delete-block-btn">✕</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Plus-Button und das Pop-up-Menü -->
|
||||
<div id="add-block-control" class="article-editor-scope add-block-control">
|
||||
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
|
||||
<div id="block-popup" class="article-editor-scope block-popup hidden">
|
||||
<button type="button" data-type="text">Textblock</button>
|
||||
<button type="button" data-type="image">Bild einfügen</button>
|
||||
<div id="block-popup" class="article-editor-scope block-popup">
|
||||
<button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button>
|
||||
<button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unsichtbares Textfeld, das die JSON-Daten hält und an den Controller postet -->
|
||||
<textarea id="content" name="content" style="display:none;"><?php
|
||||
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])){
|
||||
echo htmlspecialchars($_SESSION['old_content']);
|
||||
unset($_SESSION['old_content']);
|
||||
}elseif (isset($content) && !empty($content)){
|
||||
echo htmlspecialchars($content);
|
||||
} else {
|
||||
echo '[]';
|
||||
}
|
||||
?></textarea>
|
||||
|
||||
<!-- unsichtbares Input, um die zu löschenden Bilder zu übergeben-->
|
||||
<input type="hidden" id="deleted-images" name="deleted_images" value="[]">
|
||||
</main>
|
||||
|
||||
<!-- Seitenleiste -->
|
||||
|
||||
@@ -18,11 +18,6 @@
|
||||
Ein Beitrag muss Inhalt besitzen. Text- und Bildelemente dürfen nicht leer sein!
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_category"): ?>
|
||||
<p class="alert-message is-error">
|
||||
Die ausgewählte Kategorie ist ungültig.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_tags"): ?>
|
||||
<p class="alert-message is-error">
|
||||
Ungültige Schlagworte gefunden. Erlaubt sind nur Buchstaben, Zahlen, Leerzeichen und Bindestriche (2-50 Zeichen).
|
||||
|
||||
@@ -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)) . '">«</a> ';
|
||||
} else {
|
||||
$html .= '<span class="s-res-page-btn" aria-disabled="true">«</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)) . '">»</a>';
|
||||
} else {
|
||||
$html .= '<span class="s-res-page-btn" aria-disabled="true">»</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)) . '">«</a> ';
|
||||
} else {
|
||||
$html .= '<span class="s-res-page-btn" aria-disabled="true">«</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)) . '">»</a>';
|
||||
} else {
|
||||
$html .= '<span class="s-res-page-btn" aria-disabled="true">»</span>';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
@@ -2,61 +2,10 @@
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
ob_start();
|
||||
include_once("php/controller/index.php");
|
||||
|
||||
$pfad = $_GET["pfad"] ?? "home";
|
||||
|
||||
/*
|
||||
Controller für Aktionen werden vor der HTML-Ausgabe geladen,
|
||||
damit Weiterleitungen mit header() funktionieren.
|
||||
*/
|
||||
if ($pfad === "login") {
|
||||
include_once "php/controller/login-controller.php";
|
||||
}
|
||||
|
||||
if ($pfad === "register") {
|
||||
include_once "php/controller/register-controller.php";
|
||||
}
|
||||
if ($pfad === "password-forgotten") {
|
||||
include_once "php/controller/password-forgotten-controller.php";
|
||||
}
|
||||
|
||||
if ($pfad === "confirm-register") {
|
||||
include_once "php/controller/confirm-register-controller.php";
|
||||
}
|
||||
|
||||
if ($pfad === "confirm-password") {
|
||||
include_once "php/controller/confirm-password-controller.php";
|
||||
}
|
||||
|
||||
if ($pfad === "logout") {
|
||||
include_once "php/controller/logout-controller.php";
|
||||
exit();
|
||||
}
|
||||
if ($pfad === "deleteAccount") {
|
||||
include_once "php/controller/deleteAccount-controller.php";
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($pfad === "updateComment") {
|
||||
include_once "php/controller/updateComment-controller.php";
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($pfad === "deleteComment") {
|
||||
include_once "php/controller/deleteComment-controller.php";
|
||||
exit();
|
||||
}
|
||||
include_once "php/controller/index-controller.php";
|
||||
?>
|
||||
|
||||
<!--
|
||||
Seite: Index der Lernplattform
|
||||
Funktion: Webseitengerüst, Anzeigen von Content
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="description" content="EduForge">
|
||||
@@ -74,25 +23,21 @@ if ($pfad === "deleteComment") {
|
||||
<link rel="stylesheet" href="css/message.css">
|
||||
<link rel="stylesheet" href="css/showCategory.css">
|
||||
|
||||
<script src="js/paginator.js" async></script>
|
||||
<script src="js/sorter.js" async></script>
|
||||
<script src="js/comments.js" defer></script>
|
||||
<script src="js/editor.js" async></script>
|
||||
<script src="js/filter.js" async></script>
|
||||
<script src="js/search-results.js" async></script>
|
||||
<script src="js/showCategory.js" async></script>
|
||||
|
||||
|
||||
<title>EduForge</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<?php
|
||||
include_once 'includes/navbar.php';
|
||||
|
||||
/*
|
||||
Dynamischer Inhalt:
|
||||
Je nach pfad-Parameter wird die passende Datei aus content geladen.
|
||||
*/
|
||||
if (file_exists('content/' . $pfad . '.php')) {
|
||||
// Dynamischer Inhalt
|
||||
if (isset($pfad) && $pfad !== "404" && file_exists('content/' . $pfad . '.php')) {
|
||||
include_once 'content/' . $pfad . '.php';
|
||||
} else {
|
||||
include_once 'content/404.php';
|
||||
@@ -103,7 +48,3 @@ if ($pfad === "deleteComment") {
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<?php
|
||||
ob_end_flush();
|
||||
?>
|
||||
+85
-124
@@ -1,172 +1,133 @@
|
||||
console.log("Die JavaScript-Datei wurde erfolgreich geladen!");
|
||||
//console.log("editor.js wurde erfolgreich geladen!");
|
||||
|
||||
function initEditor() {
|
||||
const form = document.getElementById("editor-form");
|
||||
|
||||
if (!form) {
|
||||
console.error("Skript abgebrochen: Formular nicht gefunden!");
|
||||
console.error("editor.js abgebrochen: Formular nicht gefunden!");
|
||||
return;
|
||||
} else {
|
||||
console.log("Formular gefunden und Editor initialisiert:", form);
|
||||
console.log("Formular gefunden und editor.js initialisiert:", form);
|
||||
}
|
||||
|
||||
const container = document.getElementById("block-container");
|
||||
const plusButton = document.getElementById("plus-button");
|
||||
const popup = document.getElementById("block-popup");
|
||||
const hiddenContentInput = document.getElementById("content");
|
||||
|
||||
const initialImages = [];
|
||||
// Fortlaufender Zähler für eindeutige Block-Indizes. Wird nie wiederverwendet
|
||||
// (auch nicht nach dem Löschen eines Blocks), damit sich neue und übrig
|
||||
// gebliebene Blöcke beim finalen Submit nie einen Namen teilen.
|
||||
let blockIndex = container.querySelectorAll(".editor-block").length;
|
||||
|
||||
// Ohne JS sind Textblock-/Bild-Button im Pop-up immer sichtbar und ganz normale
|
||||
// Submit-Buttons. Erst mit JS blenden wir das Pop-up standardmäßig aus und
|
||||
// steuern die Sichtbarkeit über den Plus-Button.
|
||||
popup.classList.add("hidden");
|
||||
|
||||
// Pop-up umschalten bei Klick auf das Plus
|
||||
plusButton.addEventListener("click", () => {
|
||||
popup.classList.toggle("hidden");
|
||||
});
|
||||
|
||||
// Klick auf eine Block-Option im Pop-up
|
||||
popup.querySelectorAll("button").forEach(btn => {
|
||||
btn.addEventListener("click", function() {
|
||||
// Klick auf "Textblock" / "Bild einfügen": lokal im DOM anlegen statt zum
|
||||
// Server zu submitten (entlastet den Server, kein Page-Reload nötig).
|
||||
popup.querySelectorAll('[name="editor_action"]').forEach(btn => {
|
||||
btn.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
const type = this.getAttribute("data-type");
|
||||
addBlockElement(type, "");
|
||||
popup.classList.add("hidden");
|
||||
});
|
||||
});
|
||||
|
||||
// Erstellt ein visuelles HTML-Element im Editor
|
||||
function addBlockElement(type, value = "") {
|
||||
const blockDiv = document.createElement("div");
|
||||
blockDiv.classList.add("editor-block");
|
||||
blockDiv.setAttribute("data-type", type);
|
||||
|
||||
// Wenn es ein existierendes Server-Bild beim Laden ist, Pfad im globalen Array sichern
|
||||
if (type === "image" && value && typeof value === 'string' && value.startsWith('uploads/')) {
|
||||
initialImages.push(value);
|
||||
blockDiv.setAttribute("data-value", value);
|
||||
}
|
||||
|
||||
// Löschen-Button
|
||||
const deleteBtn = document.createElement("button");
|
||||
deleteBtn.type = "button";
|
||||
deleteBtn.innerHTML = "✕";
|
||||
deleteBtn.classList.add("delete-block-btn");
|
||||
deleteBtn.addEventListener("click", () => {
|
||||
// ANPASSUNG 2B: Logik hier komplett geleert. Das '✕' entfernt den Block jetzt nur noch sicher aus dem HTML.
|
||||
blockDiv.remove();
|
||||
// Bereits vom Server gerenderte Blöcke (z.B. beim Bearbeiten eines bestehenden
|
||||
// Artikels oder nach einem Validierungsfehler) ebenfalls mit JS-Verhalten ausstatten.
|
||||
container.querySelectorAll(".editor-block").forEach(blockDiv => {
|
||||
bindDeleteButton(blockDiv);
|
||||
bindImageInput(blockDiv);
|
||||
});
|
||||
blockDiv.appendChild(deleteBtn);
|
||||
|
||||
// Erstellt einen neuen Block inkl. echter, benannter Formularfelder
|
||||
// (blocks[i][type], blocks[i][text] bzw. blocks[i][image]). Diese Felder werden
|
||||
// beim finalen Submit ganz normal vom Browser als multipart/form-data verschickt –
|
||||
// es ist kein manuelles Zusammenbauen von JSON beim Absenden mehr nötig.
|
||||
function addBlockElement(type, value = "") {
|
||||
const index = blockIndex++;
|
||||
|
||||
const blockDiv = document.createElement("div");
|
||||
blockDiv.classList.add("editor-block", "article-editor-scope");
|
||||
blockDiv.setAttribute("data-index", String(index));
|
||||
|
||||
const typeInput = document.createElement("input");
|
||||
typeInput.type = "hidden";
|
||||
typeInput.name = `blocks[${index}][type]`;
|
||||
typeInput.value = type;
|
||||
blockDiv.appendChild(typeInput);
|
||||
|
||||
if (type === "text") {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.name = `blocks[${index}][text]`;
|
||||
textarea.placeholder = "Schreibe deinen Textblock...";
|
||||
textarea.value = value;
|
||||
blockDiv.appendChild(textarea);
|
||||
} else if (type === "image") {
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
fileInput.name = `blocks[${index}][image]`;
|
||||
fileInput.accept = "image/*";
|
||||
|
||||
const imgPreview = document.createElement("img");
|
||||
imgPreview.style.maxWidth = "200px";
|
||||
imgPreview.style.display = "block";
|
||||
imgPreview.style.marginTop = "10px";
|
||||
|
||||
if (value && typeof value === 'string') {
|
||||
if (value.startsWith('uploads/') || value.startsWith('data:image/')) {
|
||||
imgPreview.src = value;
|
||||
blockDiv.setAttribute("data-value", value);
|
||||
blockDiv.appendChild(fileInput);
|
||||
}
|
||||
|
||||
const deleteBtn = document.createElement("button");
|
||||
deleteBtn.type = "submit";
|
||||
deleteBtn.name = "editor_action";
|
||||
deleteBtn.value = `delete_block:${index}`;
|
||||
deleteBtn.classList.add("delete-block-btn");
|
||||
deleteBtn.innerHTML = "✕";
|
||||
blockDiv.appendChild(deleteBtn);
|
||||
|
||||
container.appendChild(blockDiv);
|
||||
|
||||
bindDeleteButton(blockDiv);
|
||||
bindImageInput(blockDiv);
|
||||
}
|
||||
|
||||
// Löschen-Button eines Blocks lokal abfangen: entfernt den Block nur aus dem DOM,
|
||||
// statt das Formular zum Server zu senden.
|
||||
function bindDeleteButton(blockDiv) {
|
||||
const btn = blockDiv.querySelector(".delete-block-btn");
|
||||
if (!btn) return;
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
blockDiv.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Zeigt bei Auswahl einer Bilddatei sofort eine Vorschau an. Rein optisch –
|
||||
// der eigentliche Datei-Upload läuft nativ über das <input type="file">.
|
||||
function bindImageInput(blockDiv) {
|
||||
const fileInput = blockDiv.querySelector('input[type="file"]');
|
||||
if (!fileInput) return;
|
||||
|
||||
let imgPreview = blockDiv.querySelector(".block-image-preview");
|
||||
if (!imgPreview) {
|
||||
imgPreview = document.createElement("img");
|
||||
imgPreview.classList.add("block-image-preview");
|
||||
imgPreview.style.maxWidth = "200px";
|
||||
imgPreview.style.display = "none";
|
||||
imgPreview.style.marginTop = "10px";
|
||||
blockDiv.insertBefore(imgPreview, fileInput.nextSibling);
|
||||
}
|
||||
|
||||
fileInput.addEventListener("change", function () {
|
||||
if (this.files && this.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
reader.onload = (e) => {
|
||||
imgPreview.src = e.target.result;
|
||||
blockDiv.setAttribute("data-value", e.target.result);
|
||||
}
|
||||
imgPreview.style.display = "block";
|
||||
};
|
||||
reader.readAsDataURL(this.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
blockDiv.appendChild(fileInput);
|
||||
blockDiv.appendChild(imgPreview);
|
||||
}
|
||||
|
||||
container.appendChild(blockDiv);
|
||||
}
|
||||
|
||||
// beim Abschicken verbleibende Blöcke auslesen UND gelöschte Bilder ermitteln
|
||||
form.addEventListener("submit", function(e) {
|
||||
const blocks = [];
|
||||
const currentImages = [];
|
||||
|
||||
// alle aktuell im Formular verbliebenen Blöcke scannen
|
||||
container.querySelectorAll(".editor-block").forEach(blockDiv => {
|
||||
const type = blockDiv.getAttribute("data-type");
|
||||
let value = "";
|
||||
|
||||
if (type === "text") {
|
||||
value = blockDiv.querySelector("textarea").value;
|
||||
} else if (type === "image") {
|
||||
|
||||
const imgTag = blockDiv.querySelector("img");
|
||||
if (imgTag) {
|
||||
const srcValue = imgTag.getAttribute("src") || "";
|
||||
// Wenn es ein neues Bild ist, nutzen wir das data-value (Base64)
|
||||
if (srcValue.startsWith('data:image/')) {
|
||||
value = blockDiv.getAttribute("data-value") || "";
|
||||
} else {
|
||||
value = srcValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Pfade sammeln, die der Nutzer NICHT gelöscht hat (für den Abgleich)
|
||||
if (value && value.startsWith('uploads/')) {
|
||||
currentImages.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
blocks.push({ type: type, value: value });
|
||||
});
|
||||
|
||||
// das reguläre unsichtbare Content-Feld befüllen
|
||||
hiddenContentInput.value = JSON.stringify(blocks);
|
||||
|
||||
// Differenz berechnen: Welche Bilder aus 'initialImages' fehlen in 'currentImages' ?
|
||||
const deletedImages = initialImages.filter(img => !currentImages.includes(img));
|
||||
|
||||
// das 'deleted_images'-Feld dynamisch befüllen und an den Controller senden
|
||||
let deletedInput = document.getElementById("deleted-images");
|
||||
if (!deletedInput) {
|
||||
deletedInput = document.createElement("input");
|
||||
deletedInput.type = "hidden";
|
||||
deletedInput.id = "deleted-images";
|
||||
deletedInput.name = "deleted_images";
|
||||
form.appendChild(deletedInput);
|
||||
}
|
||||
deletedInput.value = JSON.stringify(deletedImages);
|
||||
});
|
||||
|
||||
// Existierende Blöcke laden (stellt alte Daten aus der Session wieder her)
|
||||
try {
|
||||
const initialBlocks = JSON.parse(hiddenContentInput.value.trim());
|
||||
if (Array.isArray(initialBlocks)) {
|
||||
initialBlocks.forEach(b => {
|
||||
if (b.type === "image" && b.value && typeof b.value === 'string' && !b.value.startsWith('data:image/')) {
|
||||
let cleanPath = b.value.trim().replace(/\\\//g, '/'); // Verwandelt \/ in /
|
||||
|
||||
initialImages.push(cleanPath);
|
||||
addBlockElement(b.type, cleanPath);
|
||||
|
||||
} else {
|
||||
addBlockElement(b.type, b.value);
|
||||
}
|
||||
});
|
||||
console.log("Erfolgreich registrierte Start-Bilder:", initialImages);
|
||||
}
|
||||
} catch(e) {
|
||||
if (hiddenContentInput.value.trim() !== "") {
|
||||
addBlockElement("text", hiddenContentInput.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
let currentClientPage = 1;
|
||||
let itemsPerPage = 10;
|
||||
|
||||
function initFilter() {
|
||||
const filterSelect = document.getElementById('category-filter');
|
||||
const listContainer = document.querySelector('.s-res-list');
|
||||
const limitSelect = document.getElementById('s-res-per-page');
|
||||
|
||||
if (!filterSelect || !listContainer) return;
|
||||
|
||||
if (limitSelect) {
|
||||
itemsPerPage = parseInt(limitSelect.value, 10) || 10;
|
||||
}
|
||||
|
||||
updateVisibility();
|
||||
|
||||
if (limitSelect) {
|
||||
limitSelect.addEventListener('change', function() {
|
||||
|
||||
//neues limit einlesen
|
||||
itemsPerPage = parseInt(this.value, 10);
|
||||
currentClientPage = 1;
|
||||
updateVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
filterSelect.addEventListener('change', function() {
|
||||
currentClientPage = 1;
|
||||
updateVisibility();
|
||||
});
|
||||
|
||||
const navigationContainer = document.querySelector('.s-res-page-navigation');
|
||||
if (navigationContainer) {
|
||||
navigationContainer.addEventListener('click', function(e) {
|
||||
const button = e.target.closest('.s-res-page-btn');
|
||||
if (!button || button.disabled) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const targetPage = button.getAttribute('data-page');
|
||||
if (targetPage) {
|
||||
currentClientPage = parseInt(targetPage, 10);
|
||||
updateVisibility();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateVisibility() {
|
||||
const filterSelect = document.getElementById('category-filter');
|
||||
const listContainer = document.querySelector('.s-res-list');
|
||||
const selectedCategory = filterSelect.value.toLowerCase().trim();
|
||||
const cards = listContainer.querySelectorAll('.s-res-item');
|
||||
|
||||
let visibleCards = [];
|
||||
cards.forEach(card => {
|
||||
const cardCategory = (card.getAttribute('data-category') || '').toLowerCase().trim();
|
||||
if (selectedCategory === 'all' || cardCategory.includes(selectedCategory) || selectedCategory.includes(cardCategory)) {
|
||||
visibleCards.push(card);
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
const totalVisible = visibleCards.length;
|
||||
const totalPages = Math.max(1, Math.ceil(totalVisible / itemsPerPage));
|
||||
|
||||
if (currentClientPage < 1) currentClientPage = 1;
|
||||
if (currentClientPage > totalPages) currentClientPage = totalPages;
|
||||
|
||||
const startOffset = (currentClientPage - 1) * itemsPerPage;
|
||||
const endOffset = startOffset + itemsPerPage;
|
||||
|
||||
visibleCards.forEach((card, index) => {
|
||||
if (index >= startOffset && index < endOffset) {
|
||||
card.style.display = 'flex';
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
updatePaginatorUI(currentClientPage, totalPages);
|
||||
}
|
||||
|
||||
function updatePaginatorUI(currentPage, totalPages) {
|
||||
const prevBtn = document.getElementById('prev-page-btn');
|
||||
const nextBtn = document.getElementById('next-page-btn');
|
||||
const numbersContainer = document.getElementById('dynamic-page-numbers');
|
||||
|
||||
if (!prevBtn || !nextBtn || !numbersContainer) return;
|
||||
|
||||
prevBtn.setAttribute('data-page', currentPage - 1);
|
||||
prevBtn.disabled = (currentPage <= 1);
|
||||
|
||||
nextBtn.setAttribute('data-page', currentPage + 1);
|
||||
nextBtn.disabled = (currentPage >= totalPages);
|
||||
|
||||
let buttonsHTML = '';
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
const activeClass = (i === currentPage) ? 's-res-page-btn-active' : '';
|
||||
buttonsHTML += `<button type="button" class="s-res-page-btn ${activeClass}" data-page="${i}">${i}</button> `;
|
||||
}
|
||||
numbersContainer.innerHTML = buttonsHTML;
|
||||
}
|
||||
|
||||
// Hilfsfunktion für Math.ceil in JS
|
||||
function ceil(val) { return Math.ceil(val); }
|
||||
|
||||
// ist das DOM bereits vollständig aufgebaut?
|
||||
if (document.readyState === 'loading') {
|
||||
// Falls noch geladen wird, auf das Event warten
|
||||
document.addEventListener('DOMContentLoaded', initFilter);
|
||||
} else {
|
||||
// Falls das HTML bereits komplett da ist, sofort ausführen
|
||||
initFilter();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
@@ -1,50 +0,0 @@
|
||||
|
||||
function initSorter() {
|
||||
const listContainer = document.querySelector('.s-res-list');
|
||||
const sortRadios = document.querySelectorAll('.sort-radio');
|
||||
|
||||
// wenn keine liste vorhanden, abbrechen
|
||||
if (!listContainer || sortRadios.length === 0) return;
|
||||
|
||||
sortRadios.forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
const cards = Array.from(listContainer.querySelectorAll('.s-res-item'));
|
||||
const sortValue = this.value;
|
||||
|
||||
cards.sort((a, b) => {
|
||||
if (sortValue === 'likes') {
|
||||
const likesA = parseInt(a.getAttribute('data-likes') || '0', 10);
|
||||
const likesB = parseInt(b.getAttribute('data-likes') || '0', 10);
|
||||
return likesB - likesA;
|
||||
}
|
||||
else if (sortValue === 'alphabet') {
|
||||
// alphabetische sortierung
|
||||
const titleA = a.querySelector('.s-res-link').textContent.trim().toLowerCase();
|
||||
const titleB = b.querySelector('.s-res-link').textContent.trim().toLowerCase();
|
||||
return titleA.localeCompare(titleB);
|
||||
}
|
||||
else if (sortValue === 'newest' || sortValue === 'oldest') {
|
||||
// hoehere ID wird als neuer gesehen
|
||||
const urlA = a.querySelector('.s-res-link').getAttribute('href');
|
||||
const urlB = b.querySelector('.s-res-link').getAttribute('href');
|
||||
|
||||
const idA = parseInt(urlA.match(/id=(\d+)/)[1], 10);
|
||||
const idB = parseInt(urlB.match(/id=(\d+)/)[1], 10);
|
||||
|
||||
return sortValue === 'newest' ? idB - idA : idA - idB;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
listContainer.innerHTML = '';
|
||||
cards.forEach(card => listContainer.appendChild(card));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ist das DOM bereits vollständig aufgebaut?
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initSorter);
|
||||
} else {
|
||||
initSorter();
|
||||
}
|
||||
@@ -1,3 +1,39 @@
|
||||
<?php
|
||||
// Standardpfad
|
||||
$pfad = $_GET["pfad"] ?? "home";
|
||||
|
||||
?>
|
||||
if ($pfad === "logout") {
|
||||
include_once "php/controller/logout-controller.php";
|
||||
exit();
|
||||
} elseif ($pfad === "deleteAccount") {
|
||||
include_once "php/controller/deleteAccount-controller.php";
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($pfad === "login") {
|
||||
include_once "php/controller/login-controller.php";
|
||||
} elseif ($pfad === "register") {
|
||||
include_once "php/controller/register-controller.php";
|
||||
} elseif ($pfad === "password-forgotten") {
|
||||
include_once "php/controller/password-forgotten-controller.php";
|
||||
} elseif ($pfad === "confirm-register") {
|
||||
include_once "php/controller/confirm-register-controller.php";
|
||||
} elseif ($pfad === "confirm-password") {
|
||||
include_once "php/controller/confirm-password-controller.php";
|
||||
} elseif ($pfad === "updateComment") {
|
||||
include_once "php/controller/updateComment-controller.php";
|
||||
} elseif($pfad === "deleteComment") {
|
||||
include_once "php/controller/deleteComment-controller.php";
|
||||
}
|
||||
|
||||
// Whitelist
|
||||
$erlaubte_content_seiten = [
|
||||
"accessibility", "confirm-password", "confirm-register", "createArticle",
|
||||
"datenschutz", "home", "impressum", "login", "nutzungsbedingungen",
|
||||
"password-forgotten", "profile", "register", "search-results",
|
||||
"show-mail", "showArticle", "showCategory", "updateArticle"
|
||||
];
|
||||
|
||||
if (!in_array($pfad, $erlaubte_content_seiten)) {
|
||||
$pfad = "404";
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
require_once '../model/ArticleManager.php';
|
||||
require_once '../model/UserManager.php';
|
||||
require_once '../model/Article.php';
|
||||
@@ -22,31 +23,6 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
|
||||
|
||||
$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 = [];
|
||||
foreach ($results as $obj) {
|
||||
$safeArrayResults[] = [
|
||||
@@ -71,14 +47,19 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
|
||||
}
|
||||
|
||||
$sort = $_GET['sort'] ?? 'alphabet';
|
||||
$_SESSION['search_sort'] = $sort;
|
||||
|
||||
$category = $_GET['category'] ?? 'all';
|
||||
$_SESSION['search_category'] = $category;
|
||||
|
||||
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10;
|
||||
if (!searchLimitValidator($limit)) {
|
||||
$limit = 10;
|
||||
}
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
header("Location: ../../index.php?pfad=search-results&q=" . urlencode($search) . "&sort=" . urlencode($sort) . "&limit=" . $limit . "&page=" . $page);
|
||||
$_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();
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -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),
|
||||
]);
|
||||
@@ -2,66 +2,51 @@
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
require_once 'php/model/Article.php';
|
||||
require_once 'php/model/ArticleManager.php';
|
||||
require_once 'php/model/UserManager.php';
|
||||
require_once 'php/validator/article-validator.php';
|
||||
require_once __DIR__ . '/../../includes/resultsHelper.php';
|
||||
|
||||
if (isset($_GET["category"]) && !empty($_GET["category"]) && articleCategoryValidator($_GET["category"])) {
|
||||
|
||||
$category = $_GET["category"];
|
||||
|
||||
// Sortierung auslesen (Standard: alphabet)
|
||||
$sortStyle = isset($_GET['sort']) ? trim($_GET['sort']) : 'alphabet';
|
||||
|
||||
// Aktuelle Seite auslesen
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
if ($page < 1) { $page = 1; }
|
||||
|
||||
try {
|
||||
$articleManager = ArticleManager::getInstance();
|
||||
$userManager = UserManager::getInstance();
|
||||
|
||||
$allArticles = $articleManager->getArticlesByCategory($category);
|
||||
|
||||
$safeArrayResults = [];
|
||||
foreach ($allArticles as $article) {
|
||||
$article->setAuthor($userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"]);
|
||||
}
|
||||
$totalArticles = count($allArticles);
|
||||
|
||||
// --- SORTIERUNG LOGIK ---
|
||||
if ($sortStyle === 'alphabet') {
|
||||
usort($allArticles, function($a, $b) {
|
||||
return strcasecmp($a->getTitle(), $b->getTitle());
|
||||
});
|
||||
} elseif ($sortStyle === 'likes') {
|
||||
usort($allArticles, function($a, $b) {
|
||||
return $b->getLikes() <=> $a->getLikes(); // Absteigend nach Likes
|
||||
});
|
||||
} elseif ($sortStyle === 'newest') {
|
||||
usort($allArticles, function($a, $b) {
|
||||
return strtotime($b->getCreationDate()) <=> strtotime($a->getCreationDate()); // Neueste zuerst
|
||||
});
|
||||
} elseif ($sortStyle === 'oldest') {
|
||||
usort($allArticles, function($a, $b) {
|
||||
return strtotime($a->getCreationDate()) <=> strtotime($b->getCreationDate()); // Älteste zuerst
|
||||
});
|
||||
$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(),
|
||||
];
|
||||
}
|
||||
|
||||
// Paginierung konfigurieren
|
||||
$limit = 10;
|
||||
$totalPages = ceil($totalArticles / $limit);
|
||||
if ($page > $totalPages && $totalPages > 0) { $page = $totalPages; }
|
||||
|
||||
$offset = ($page - 1) * $limit;
|
||||
$articles = array_slice($allArticles, $offset, $limit);
|
||||
// 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) {
|
||||
$_SESSION["message"] = "internal_error";
|
||||
include_once "content/404.php";
|
||||
exit();
|
||||
}
|
||||
|
||||
} else {
|
||||
$_SESSION["message"] = "invalid_category";
|
||||
include_once "content/404.php";
|
||||
exit();
|
||||
}
|
||||
?>
|
||||
@@ -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),
|
||||
]);
|
||||
@@ -13,11 +13,73 @@ if (!isset($_SESSION["user_email"])) {
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut die Blockliste aus den POST-Daten (blocks[i][type], blocks[i][text],
|
||||
* blocks[i][existing_image]) und ggf. hochgeladenen Dateien (blocks[i][image])
|
||||
* zusammen. Läuft bei JEDEM Submit (Zwischen-Schritt "Block hinzufügen/löschen"
|
||||
* UND finales Speichern), damit neu ausgewählte Bilder in jedem Fall persistiert
|
||||
* werden, bevor PHP die temporäre Upload-Datei nach Request-Ende verwirft.
|
||||
*
|
||||
* @param array $postBlocks $_POST['blocks'] ?? []
|
||||
* @param array $fileBlocks $_FILES['blocks'] ?? []
|
||||
* @param string $uploadDir absoluter Pfad zum uploads-Verzeichnis (mit trailing slash)
|
||||
* @return array Liste von ['type' => 'text'|'image', 'value' => string]
|
||||
*/
|
||||
function rebuildBlocksFromPost(array $postBlocks, array $fileBlocks, string $uploadDir): array {
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
$keys = array_keys($postBlocks);
|
||||
if (isset($fileBlocks['name']) && is_array($fileBlocks['name'])) {
|
||||
$keys = array_unique(array_merge($keys, array_keys($fileBlocks['name'])));
|
||||
}
|
||||
sort($keys, SORT_NUMERIC);
|
||||
|
||||
$blocks = [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$type = $postBlocks[$key]['type'] ?? null;
|
||||
|
||||
if ($type === 'text') {
|
||||
$blocks[] = [
|
||||
'type' => 'text',
|
||||
'value' => $postBlocks[$key]['text'] ?? '',
|
||||
];
|
||||
} elseif ($type === 'image') {
|
||||
// Vorbelegung: bereits vorhandenes Server-Bild (falls Datei nicht ersetzt wird)
|
||||
$value = $postBlocks[$key]['existing_image'] ?? '';
|
||||
|
||||
$hasUpload = isset($fileBlocks['error'][$key]['image'])
|
||||
&& $fileBlocks['error'][$key]['image'] === UPLOAD_ERR_OK;
|
||||
|
||||
if ($hasUpload) {
|
||||
$tmpName = $fileBlocks['tmp_name'][$key]['image'];
|
||||
$originalName = $fileBlocks['name'][$key]['image'];
|
||||
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||||
if (!in_array($extension, $allowedExtensions, true)) {
|
||||
$extension = 'jpg';
|
||||
}
|
||||
|
||||
$fileName = 'img_' . uniqid() . '.' . $extension;
|
||||
$destination = $uploadDir . $fileName;
|
||||
|
||||
if (move_uploaded_file($tmpName, $destination)) {
|
||||
$value = 'uploads/' . $fileName;
|
||||
}
|
||||
// Bei Fehler: alter Wert (falls vorhanden) bleibt erhalten, Block wird nicht verworfen
|
||||
}
|
||||
|
||||
$blocks[] = [
|
||||
'type' => 'image',
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
// unbekannter/fehlender type -> Block wird ignoriert
|
||||
}
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$_SESSION["old_title"] = $_POST["title"] ?? '';
|
||||
$_SESSION["old_content"] = $_POST["content"] ?? '';
|
||||
$_SESSION["old_category"] = $_POST["category"] ?? '';
|
||||
$_SESSION["old_tags"] = $_POST["tags"] ?? '';
|
||||
|
||||
if (isset($_GET["id"]) && !empty($_GET["id"])) {
|
||||
$id = $_GET["id"];
|
||||
@@ -41,13 +103,51 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!isset($_POST["title"]) ||!isset($_POST["content"]) || !isset($_POST["category"])){
|
||||
$uploadDir = __DIR__ . '/../../uploads/';
|
||||
if (!file_exists($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
// Formularzustand (Titel/Tags/Kategorie/Blöcke) immer sichern, damit er nach einem
|
||||
// Redirect (PRG-Pattern oder Validierungsfehler) wieder angezeigt werden kann.
|
||||
$_SESSION["old_title"] = $_POST["title"] ?? '';
|
||||
$_SESSION["old_tags"] = $_POST["tags"] ?? '';
|
||||
$_SESSION["old_category"] = $_POST["category"] ?? '';
|
||||
|
||||
$blocks = rebuildBlocksFromPost($_POST['blocks'] ?? [], $_FILES['blocks'] ?? [], $uploadDir);
|
||||
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Zwischenspeichern
|
||||
// ---------------------------------------------------------------------
|
||||
if (isset($_POST['editor_action']) && $_POST['editor_action'] !== '') {
|
||||
$action = $_POST['editor_action'];
|
||||
|
||||
if ($action === 'add_text') {
|
||||
$blocks[] = ['type' => 'text', 'value' => ''];
|
||||
} elseif ($action === 'add_image') {
|
||||
$blocks[] = ['type' => 'image', 'value' => ''];
|
||||
} elseif (str_starts_with($action, 'delete_block:')) {
|
||||
$deleteIndex = (int) substr($action, strlen('delete_block:'));
|
||||
unset($blocks[$deleteIndex]);
|
||||
$blocks = array_values($blocks);
|
||||
}
|
||||
|
||||
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
|
||||
header("location: ../../content/updateArticle.php?id=$id");
|
||||
exit();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Echtes Speichern
|
||||
// ---------------------------------------------------------------------
|
||||
if (!isset($_POST["title"]) || !isset($_POST["category"])) {
|
||||
$_SESSION["message"] = "missing_parameters";
|
||||
header("location: ../../index.php?pfad=updateArticle&id=$id");
|
||||
exit();
|
||||
} else {
|
||||
$title = $_POST["title"];
|
||||
$content = $_POST["content"];
|
||||
$content = json_encode($blocks, JSON_UNESCAPED_UNICODE);
|
||||
$author = $_SESSION["user_email"];
|
||||
$category = $_POST["category"];
|
||||
$tags = $_POST['tags'] ?? '';
|
||||
@@ -88,84 +188,44 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$cleanedTags = implode(',', $cleanedTags);
|
||||
}
|
||||
|
||||
// --------------------------------------- Base64-Bilder speichern ---------------------------------------------
|
||||
$blocks = json_decode($content, true);
|
||||
$uploadDir = __DIR__ . '/../../uploads/';
|
||||
|
||||
if (!file_exists($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
// ----------------- Gelöschte Bilder über die JS-Löschliste entfernen ----------------- TODO: Gelöschte Bilder über die JS-Löschliste entfernen
|
||||
/*if (isset($_POST['deleted_images'])) {
|
||||
$deletedImages = json_decode($_POST['deleted_images'], true);
|
||||
|
||||
// Wir ermitteln den physisch echten, absoluten Pfad zum uploads-Ordner auf der Festplatte
|
||||
$uploadDir = realpath(__DIR__ . '/../../uploads') . DIRECTORY_SEPARATOR;
|
||||
|
||||
if (is_array($deletedImages)) {
|
||||
foreach ($deletedImages as $imagePath) {
|
||||
// Nur den reinen Dateinamen heraustrennen (z.B. img_65a123.jpg)
|
||||
$filename = basename($imagePath);
|
||||
$fullDeletePath = $uploadDir . $filename;
|
||||
|
||||
// Debugging & Löschen:
|
||||
if (file_exists($fullDeletePath)) {
|
||||
// Versuchen zu löschen. Wenn es fehlschlägt, Fehlermeldung erzwingen
|
||||
if (!@unlink($fullDeletePath)) {
|
||||
$error = error_get_last();
|
||||
die("Datei existiert, aber PHP darf sie nicht löschen! Grund: " . $error['message']);
|
||||
}
|
||||
} else {
|
||||
// Wenn PHP die Datei an diesem Pfad nicht findet, brechen wir zum Debuggen ab
|
||||
// die("PHP findet die Datei nicht unter dem Pfad: " . $fullDeletePath);
|
||||
// ----------------- Verwaiste Bilder aufräumen -----------------
|
||||
// Bilder, die im alten (gespeicherten) Content vorkamen, im neuen aber nicht
|
||||
// mehr referenziert werden, wurden vom Nutzer entfernt oder ersetzt -> löschen.
|
||||
// TODO: Bilder, die innerhalb derselben Bearbeitungs-Sitzung neu hochgeladen und noch vor dem finalen Speichern wieder entfernt wurden, werden hierüber nicht erfasst (sie tauchten nie im alten Content auf) und bleiben als Datei liegen. Für eine vollständige Bereinigung würde sich ein
|
||||
$oldBlocks = json_decode($article->getContent(), true);
|
||||
$oldImagePaths = [];
|
||||
if (is_array($oldBlocks)) {
|
||||
foreach ($oldBlocks as $oldBlock) {
|
||||
if (($oldBlock['type'] ?? '') === 'image'
|
||||
&& !empty($oldBlock['value'])
|
||||
&& is_string($oldBlock['value'])
|
||||
&& str_starts_with($oldBlock['value'], 'uploads/')) {
|
||||
$oldImagePaths[] = $oldBlock['value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
// ----------------------- NEU hinzugefügte Base64-Bilder: --------------------------
|
||||
if (is_array($blocks)) {
|
||||
foreach ($blocks as &$block) {
|
||||
// Prüfen, ob der Block ein Bild ist und ein NEUES Bild (Base64-Format) enthält
|
||||
if (isset($block['type']) && isset($block['value']) && $block['type'] === 'image' && is_string($block['value'])) {
|
||||
|
||||
if (str_starts_with($block['value'], 'data:image/')) {
|
||||
$parts = explode(',', $block['value']);
|
||||
if (count($parts) >= 2) {
|
||||
$metadata = $parts[0];
|
||||
$base64Data = $parts[1];
|
||||
|
||||
preg_match('/data:image\/(?<extension>.*?);/', $metadata, $matches);
|
||||
$extension = $matches['extension'] ?? 'jpg';
|
||||
if ($extension === 'jpeg') { $extension = 'jpg'; }
|
||||
|
||||
$fileName = 'img_' . uniqid() . '.' . $extension;
|
||||
$filePath = $uploadDir . $fileName;
|
||||
|
||||
if (file_put_contents($filePath, base64_decode($base64Data)) !== false) {
|
||||
$block['value'] = 'uploads/' . $fileName;
|
||||
} else {
|
||||
$_SESSION["message"] = "image_upload_error";
|
||||
header("location: ../../index.php?pfad=updateArticle&id=$id");
|
||||
exit();
|
||||
$newImagePaths = [];
|
||||
foreach ($blocks as $block) {
|
||||
if (($block['type'] ?? '') === 'image' && !empty($block['value'])) {
|
||||
$newImagePaths[] = $block['value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($block);
|
||||
}
|
||||
|
||||
// Aktualisiertes Array wieder in JSON konvertieren
|
||||
$finalContent = json_encode($blocks, JSON_UNESCAPED_UNICODE);
|
||||
$orphanedImages = array_diff($oldImagePaths, $newImagePaths);
|
||||
foreach ($orphanedImages as $orphanedImage) {
|
||||
$absolutePath = __DIR__ . '/../../' . $orphanedImage;
|
||||
if (is_file($absolutePath)) {
|
||||
@unlink($absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
|
||||
try {
|
||||
$articleManager = ArticleManager::getInstance();
|
||||
$article = $articleManager->getArticle($id);
|
||||
$article->setTitle($title);
|
||||
$article->setContent($finalContent);
|
||||
$article->setContent($content);
|
||||
$article->setCategory($category);
|
||||
$article->setTags($cleanedTags);
|
||||
$articleManager->updateArticle($id, $article, $author);
|
||||
|
||||
Reference in New Issue
Block a user