Compare commits

..

47 Commits

Author SHA1 Message Date
niklas.ortmann 0aca76d2f4 Update index.php 2026-07-18 18:26:42 +02:00
niklas.ortmann eca6646071 showCategory.js 2026-07-18 18:23:11 +02:00
niklas.ortmann 35e5006063 Update navbar.php 2026-07-18 17:39:14 +02:00
niklas.ortmann fcb9289d80 Update navbar.php 2026-07-18 17:38:27 +02:00
niklas.ortmann 6595a3d78d Update index.php 2026-07-18 17:36:52 +02:00
niklas.ortmann 41d8ac6721 Update navbar.php 2026-07-18 17:35:38 +02:00
niklas.ortmann 63c213bae7 Update navbar.php 2026-07-18 17:34:24 +02:00
niklas.ortmann 61d72436c9 Update navbar.php 2026-07-18 17:33:40 +02:00
niklas.ortmann 721bc8d833 Update navbar.php 2026-07-18 17:32:37 +02:00
niklas.ortmann 2666d2d14f Update navbar.php 2026-07-18 17:32:01 +02:00
niklas.ortmann de1ba88860 Update showCategory.php 2026-07-18 17:30:17 +02:00
niklas.ortmann e0fb04794d Update resultsHelper.php 2026-07-18 17:28:53 +02:00
niklas.ortmann 6897a29574 Update search-results-data.php 2026-07-18 17:23:30 +02:00
niklas.ortmann 8836034670 Kategoriepaginator 2026-07-18 17:17:44 +02:00
niklas.ortmann d590561ade refactoring 2026-07-18 16:59:23 +02:00
niklas.ortmann ba11c216bd refactoring 2026-07-18 16:56:41 +02:00
niklas.ortmann 71a1b029cb refactoring 2026-07-18 16:47:09 +02:00
niklas.ortmann 2126d076d5 Entfernung alter js 2026-07-18 16:47:01 +02:00
niklas.ortmann 2ecd446dd0 search-results.js
Ohne JS: search-results.php sortiert/filtert/paginiert komplett serverseitig anhand der GET-Parameter (?sort=&category=&page=) und cached dabei den Session-Datensatz — es wird keine neue Suche ausgelöst, nur der bestehende $_SESSION["search_results"]-Datensatz neu aufbereitet. Echte <a href>-Links für die Pagination, ein <noscript>-Submit-Button für Sortierung/Filter.
Mit JS: results-app.js lädt den kompletten Datensatz einmalig per fetch() von einem neuen schlanken JSON-Endpoint nach und übernimmt Sortierung, Filterung und Pagination danach zu 100% im Browser — keine weiteren Serveranfragen mehr, kein Page-Reload.
sorter.js, filter.js, paginator.js werden durch die eine Datei results-app.js ersetzt (löst den Button-Konflikt).
2026-07-18 16:35:13 +02:00
niklas.ortmann 0b4c5f988d Merge pull request 'Anzeigenamen' (#51) from AnzeigeNamen into dev
Reviewed-on: #51
2026-07-17 18:09:29 +02:00
niklas.ortmann 59eeb05e4b Update showCategory.php 2026-07-17 18:05:33 +02:00
niklas.ortmann 5d8239822a Update showCategory.php 2026-07-17 18:04:03 +02:00
niklas.ortmann 31585511d0 Update showCategory-controller.php 2026-07-17 18:01:59 +02:00
niklas.ortmann 2ec1b70c91 Update Article.php 2026-07-17 17:59:17 +02:00
niklas.ortmann 8f8b25249c Update showCategory-controller.php 2026-07-17 17:56:31 +02:00
niklas.ortmann 8be6e6d61c Update showCategory-controller.php 2026-07-17 17:55:37 +02:00
niklas.ortmann bad0c08e6f Update search-results-controller.php 2026-07-17 17:51:58 +02:00
niklas.ortmann 55695f3f82 Update showArticle-controller.php 2026-07-17 17:42:18 +02:00
niklas.ortmann 74cbc60cbe Update showArticle.php 2026-07-17 17:41:42 +02:00
niklas.ortmann c09c7ecfa3 Update showArticle-controller.php 2026-07-17 17:41:41 +02:00
niklas.ortmann 944e332b49 Merge pull request 'Kategorieseitenpaginator' (#50) from KategorieseitenPaginator into dev
Reviewed-on: #50
2026-07-17 17:36:10 +02:00
niklas.ortmann 0d009262b6 Update showCategory.php 2026-07-17 17:27:16 +02:00
niklas.ortmann bb2c368b1f Seitenleiste 2026-07-17 17:20:54 +02:00
niklas.ortmann a712f5a993 Update showCategory.css 2026-07-17 17:17:38 +02:00
niklas.ortmann 53bee08833 Update showCategory.php 2026-07-17 17:16:42 +02:00
niklas.ortmann 7a2a3ac1e9 Update showCategory.php 2026-07-17 17:13:41 +02:00
niklas.ortmann a5f02c9a6c Update showCategory.php 2026-07-17 17:13:07 +02:00
niklas.ortmann f77f09ec6f Update showCategory.php 2026-07-17 17:11:40 +02:00
niklas.ortmann 535df85cff neue showCategory.css 2026-07-17 17:11:38 +02:00
niklas.ortmann 82e8c8a1a3 Update showCategory.php 2026-07-17 17:08:02 +02:00
niklas.ortmann c0d8fdede6 Update showCategory.php 2026-07-17 17:04:54 +02:00
niklas.ortmann 958ee1c1f5 Update showCategory.php
css
2026-07-17 17:02:11 +02:00
niklas.ortmann 1dd128d480 Update showCategory.php 2026-07-17 16:59:36 +02:00
niklas.ortmann dca716ddc5 Update showCategory-controller.php 2026-07-17 16:59:34 +02:00
niklas.ortmann c1dd4fce2b Update showCategory.php 2026-07-17 16:41:09 +02:00
niklas.ortmann 16cfa92721 Update showCategory.php 2026-07-17 16:29:11 +02:00
niklas.ortmann 3b2ed78c1c Update showCategory.php 2026-07-17 16:16:14 +02:00
17 changed files with 1296 additions and 340 deletions
+87 -66
View File
@@ -2,20 +2,37 @@
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
require_once __DIR__ . '/../includes/resultsHelper.php';
$all_results = $_SESSION["search_results"] ?? []; $rawResults = $_SESSION["search_results"] ?? [];
$query = $_SESSION["search_query"] ?? ""; $query = $_SESSION["search_query"] ?? "";
$totalResultsCount = count($all_results);
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10; // Sortierung / Kategorie / Limit: GET-Parameter haben Vorrang, sonst
// Rückfall auf die zuletzt in der Session gemerkten Werte. So bleibt
// z.B. ein reiner Pagination-Link (?page=2) bei der aktuellen
// Sortierung/Filterung.
$currentSort = $_GET['sort'] ?? ($_SESSION['search_sort'] ?? 'alphabet');
if (!in_array($currentSort, ['alphabet', 'likes', 'newest', 'oldest'])) {
$currentSort = 'alphabet';
}
$_SESSION['search_sort'] = $currentSort;
$currentCategory = strtolower($_GET['category'] ?? ($_SESSION['search_category'] ?? 'all'));
$_SESSION['search_category'] = $currentCategory;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : ($_SESSION['search_limit'] ?? 10);
if (!in_array($limit, [10, 20, 50, 100])) { if (!in_array($limit, [10, 20, 50, 100])) {
$limit = 10; $limit = 10;
} }
$_SESSION['search_limit'] = $limit;
// Gesamtseitenzahl // Serverseitig sortieren + filtern
$totalPages = max(1, ceil($totalResultsCount / $limit)); $filteredResults = sortSearchResults($rawResults, $currentSort);
$filteredResults = filterSearchResultsByCategory($filteredResults, $currentCategory);
$totalResultsCount = count($filteredResults);
$totalPages = max(1, (int)ceil($totalResultsCount / $limit));
// Aktuelle Seite auslesen und validieren
$currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1; $currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($currentPage < 1) { if ($currentPage < 1) {
$currentPage = 1; $currentPage = 1;
@@ -23,21 +40,33 @@ if ($currentPage < 1) {
$currentPage = $totalPages; $currentPage = $totalPages;
} }
// Startpunkt im Array berechnen (Offset)
$offset = ($currentPage - 1) * $limit; $offset = ($currentPage - 1) * $limit;
// Nur die Ergebnisse für die aktuelle Seite ausschneiden // Dies ist die No-JS-Basisversion: nur die aktuelle Seite wird gerendert.
//$results = array_slice($all_results, $offset, $limit); // Bei aktiviertem JS lädt js/results-app.js zusätzlich den kompletten
$results = $all_results; // Datensatz nach (ein einziger Request) und übernimmt Sortierung, Filter
// und Pagination danach komplett im Browser, ohne weitere Serveranfragen.
$results = array_slice($filteredResults, $offset, $limit);
$resultCount = count($results); $resultCount = count($results);
// Kategorien für das Auswahlfeld (Wert => Label)
$categories = [
'Deutsch' => 'Deutsch', 'Englisch' => 'Englisch', 'Franzoesisch' => 'Französisch',
'Latein' => 'Latein', 'Literatur' => 'Literatur', 'Mathematik' => 'Mathematik',
'Biologie' => 'Biologie', 'Informatik' => 'Informatik', 'Chemie' => 'Chemie',
'Physik' => 'Physik', 'Astronomie' => 'Astronomie', 'Geschichte' => 'Geschichte',
'Erdkunde' => 'Erdkunde', 'Sozialkunde' => 'Sozialkunde', 'Wirtschaftskunde' => 'Wirtschaftskunde',
'Religion' => 'Religion', 'Ethikunterricht' => 'Ethikunterricht', 'Philosophie' => 'Philosophie',
'Psychologie' => 'Psychologie', 'Kunst' => 'Kunst', 'Musik' => 'Musik', 'Theater' => 'Theater',
'Technik' => 'Technik', 'Werken' => 'Werken', 'Hauswirtschaft' => 'Hauswirtschaft', 'Sport' => 'Sport',
];
?> ?>
<noscript>
Bitte JavaScript aktivieren!
</noscript>
<!-- <!--
Seite: Suchergebnisse Seite: Suchergebnisse
Inhalt: Zeigt die Ergebnisse einer Suche an Inhalt: Zeigt die Ergebnisse einer Suche an.
Funktioniert ohne JavaScript (serverseitige Sortierung/Filter/Pagination
über echte Links + Formulare) und wird bei aktiviertem JS von
js/search-results.js vollständig client-seitig übernommen.
--> -->
<div class="s-res-layout-grid"> <div class="s-res-layout-grid">
<?php include_once "includes/alertMessages.php" ?> <?php include_once "includes/alertMessages.php" ?>
@@ -45,20 +74,25 @@ $resultCount = count($results);
<!-- Links: Seitenleiste für Filter und Suche --> <!-- Links: Seitenleiste für Filter und Suche -->
<aside class="s-res-sidebar"> <aside class="s-res-sidebar">
<!-- Sortierfuntion Box und Such Box--> <!-- Formular 1: startet eine NEUE Suche (löst immer einen Server-Request aus) -->
<form action="php/controller/search-results-controller.php" method="GET" id="search-form-id" class="s-res-sidebar-form"> <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"> <div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Suche anpassen</h3> <h3 class="s-res-sidebar-title">Suche anpassen</h3>
<input type="search" id="site-search" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50" required> <input type="search" id="site-search" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50" required>
<button type="submit" class="nav__search-button">Suchen</button> <button type="submit" class="nav__search-button">Suchen</button>
</div> </div>
</form>
<!-- Formular 2: sortiert/filtert die BEREITS gefundenen Ergebnisse.
Ohne JS: echter Reload dieser Seite, aber ohne erneute Datenbank-Suche.
Mit JS: search-results.js übernimmt das live im Browser, der Submit-Button
wird dafür nicht benötigt und über <noscript> versteckt. -->
<form action="index.php" method="GET" id="s-res-filter-form" class="s-res-sidebar-form">
<input type="hidden" name="pfad" value="search-results">
<input type="hidden" name="q" value="<?php echo htmlspecialchars($query); ?>">
<div class="s-res-sidebar-box"> <div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Sortierung</h3> <h3 class="s-res-sidebar-title">Sortierung</h3>
<?php $currentSort = $_SESSION['search_sort'] ?? 'alphabet'; ?>
<div class="s-res-filter-group"> <div class="s-res-filter-group">
<label class="s-res-filter-option"> <label class="s-res-filter-option">
<input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>> <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"> <div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Kategorie filtern</h3> <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;"> <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">Alle Kategorien</option> <option value="all" <?php echo $currentCategory === 'all' ? 'selected' : ''; ?>>Alle Kategorien</option>
<option value="Deutsch">Deutsch</option> <?php foreach ($categories as $value => $label): ?>
<option value="Englisch">Englisch</option> <option value="<?php echo htmlspecialchars($value); ?>" <?php echo $currentCategory === strtolower($value) ? 'selected' : ''; ?>><?php echo htmlspecialchars($label); ?></option>
<option value="Franzoesisch">Französisch</option> <?php endforeach; ?>
<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> </select>
</div> </div>
<noscript>
<button type="submit" class="nav__search-button">Filter anwenden</button>
</noscript>
</form> </form>
</aside> </aside>
@@ -120,17 +134,15 @@ $resultCount = count($results);
<div class="s-res-header"> <div class="s-res-header">
<h1 class="s-res-main-title">Suchergebnisse</h1> <h1 class="s-res-main-title">Suchergebnisse</h1>
<p class="s-res-meta"><?php echo $totalResultsCount; ?> Treffer für Ihre Suchanfrage "<?php echo htmlspecialchars($query); ?>"</p> <p class="s-res-meta"><span id="s-res-result-count"><?php echo $totalResultsCount; ?></span> Treffer für Ihre Suchanfrage "<?php echo htmlspecialchars($query); ?>"</p>
</div> </div>
<!-- Ergebnisliste --> <!-- Ergebnisliste -->
<div class="s-res-list"> <div class="s-res-list">
<?php <?php if (!empty($results)): ?>
if (!empty($results)): ?>
<?php foreach ($results as $item): <?php foreach ($results as $item):
// Anzahl der Likes ermitteln (falls es ein Array ist, zählen; falls Zahl, direkt nutzen) $likesCount = getLikeCount($item);
$likesCount = isset($item['likes']) && is_array($item['likes']) ? count($item['likes']) : ($item['likes'] ?? 0);
?> ?>
<div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>"> <div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>">
<div class="s-res-content"> <div class="s-res-content">
@@ -151,39 +163,48 @@ $resultCount = count($results);
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php <?php elseif (isset($_SESSION["message"]) && $_SESSION["message"] === "invalid_search_query"): ?>
elseif (isset($_SESSION["search_query"]) && $_SESSION["search_query"] !== "" && $resultCount === 0): ?>
<p>Keine Beiträge zu diesem Suchbegriff gefunden.</p>
<?php
elseif (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_search_query"): ?>
<p>Unzulässige Suchanfrage</p> <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 endif; ?>
<?php <?php unset($_SESSION["message"]); ?>
unset($_SESSION["message"]);
?>
</div> </div>
<div class="s-res-pagination-footer"> <div class="s-res-pagination-footer">
<!-- Auswahl der Ergebnisse pro Seite --> <!-- Auswahl der Ergebnisse pro Seite. Gehört per form="..." logisch
zum Filterformular oben -->
<div class="s-res-limit-selector"> <div class="s-res-limit-selector">
<label for="s-res-per-page" class="s-res-limit-label">Ergebnisse pro Seite:</label> <label for="s-res-per-page" class="s-res-limit-label">Ergebnisse pro Seite:</label>
<select id="s-res-per-page" name="limit" class="s-res-limit-select"> <select id="s-res-per-page" name="limit" form="s-res-filter-form" class="s-res-limit-select">
<option value="10" <?php echo $limit === 10 ? 'selected' : ''; ?>>10</option> <option value="10" <?php echo $limit === 10 ? 'selected' : ''; ?>>10</option>
<option value="20" <?php echo $limit === 20 ? 'selected' : ''; ?>>20</option> <option value="20" <?php echo $limit === 20 ? 'selected' : ''; ?>>20</option>
<option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50</option> <option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50</option>
<option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100</option> <option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100</option>
</select> </select>
<noscript><button type="submit" form="s-res-filter-form" class="nav__search-button">Übernehmen</button></noscript>
</div> </div>
<div class="s-res-page-navigation"> <nav class="s-res-page-navigation" aria-label="Seitennavigation">
<button type="button" class="s-res-page-btn" id="prev-page-btn" data-page="0">«</button> <!-- 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> <span id="dynamic-page-numbers"></span>
<button type="button" class="s-res-page-btn" id="next-page-btn">»</button>
<button type="button" class="s-res-page-btn" id="next-page-btn" data-page="2">»</button>
</div> </div>
</nav>
</div> </div>
</main> </main>
+2 -2
View File
@@ -45,8 +45,8 @@ include_once 'php/controller/showArticle-controller.php';
</h1> </h1>
<div class="article-view-meta"> <div class="article-view-meta">
<?php if (isset($author) && !empty($author)): ?> <?php if (isset($name) && !empty($name)): ?>
<span class="article-view-author">Von: <strong><?php echo htmlspecialchars($author); ?></strong></span> <span class="article-view-author">Von: <strong><?php echo htmlspecialchars($name); ?></strong></span>
<?php endif; ?> <?php endif; ?>
</div> </div>
+165 -13
View File
@@ -1,32 +1,184 @@
<?php <?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
include_once "php/controller/showCategory-controller.php"; include_once "php/controller/showCategory-controller.php";
?> require_once __DIR__ . '/../includes/resultsHelper.php';
<main> $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" ?> <?php include_once "includes/alertMessages.php" ?>
<h1><?php if (isset($category) && !empty($category)){ echo htmlspecialchars($category); } ?></h1> <!-- 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>
<button type="submit" class="nav__search-button">Anwenden</button>
</noscript>
</form>
</aside>
<main class="s-res-main-content">
<div class="s-res-header">
<h1 class="s-res-main-title">Kategorie: <?php echo htmlspecialchars($category); ?></h1>
<p class="s-res-meta">
<span id="s-res-result-count"><?php echo $totalResultsCount; ?></span> Treffer in dieser Kategorie<?php echo $query !== '' ? ' für "' . htmlspecialchars($query) . '"' : ''; ?>
</p>
</div>
<!-- Ergebnisliste -->
<div class="s-res-list"> <div class="s-res-list">
<?php <?php if (!empty($results)): ?>
if (!empty($articles)): ?>
<?php foreach ($articles as $article): ?> <?php foreach ($results as $item):
<div class="s-res-item"> $likesCount = getLikeCount($item);
?>
<div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>">
<div class="s-res-content"> <div class="s-res-content">
<h2 class="s-res-item-title"> <h2 class="s-res-item-title">
<a href="index.php?pfad=showArticle&id=<?php echo $article->getId(); ?>" class="s-res-link"> <a href="index.php?pfad=showArticle&id=<?php echo $item['id']; ?>" class="s-res-link">
<?php echo htmlspecialchars($article->getTitle()); ?> <?php echo htmlspecialchars($item['title']); ?>
</a> </a>
</h2> </h2>
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($article->getAuthor()); ?></span></p> <div class="s-res-meta-row">
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($item['author']); ?></span></p>
<span class="s-res-likes">
❤️ <?php echo $likesCount; ?>
</span>
</div>
</div> </div>
<div class="s-res-arrow">&rarr;</div> <div class="s-res-arrow">&rarr;</div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php
else: ?> <?php elseif ($query !== ''): ?>
<p> Es sind noch keine Beiträge in dieser Kategorie enthalten.</p> <p>Keine Beiträge in dieser Kategorie zu diesem Suchbegriff gefunden.</p>
<?php else: ?>
<p>Keine Beiträge in dieser Kategorie gefunden.</p>
<?php endif; ?> <?php endif; ?>
<?php unset($_SESSION["message"]); ?>
</div> </div>
<div class="s-res-pagination-footer">
<!-- Auswahl der Ergebnisse pro Seite -->
<div class="s-res-limit-selector">
<label for="s-res-per-page" class="s-res-limit-label">Ergebnisse pro Seite:</label>
<select id="s-res-per-page" name="limit" form="cat-filter-form" class="s-res-limit-select">
<option value="10" <?php echo $limit === 10 ? 'selected' : ''; ?>>10</option>
<option value="20" <?php echo $limit === 20 ? 'selected' : ''; ?>>20</option>
<option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50</option>
<option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100</option>
</select>
<noscript><button type="submit" form="cat-filter-form" class="nav__search-button">Übernehmen</button></noscript>
</div>
<nav class="s-res-page-navigation" aria-label="Seitennavigation">
<!-- No-JS-Fallback: -->
<noscript>
<?php echo renderNoJsPaginationCategory($currentPage, $totalPages, $category, $currentSort, $query, $limit); ?>
</noscript>
<!-- JS-Version: wird per js/showCategory.js befüllt/eingeblendet: -->
<div id="js-page-navigation" style="display:none;">
<button type="button" class="s-res-page-btn" id="prev-page-btn">«</button>
<span id="dynamic-page-numbers"></span>
<button type="button" class="s-res-page-btn" id="next-page-btn">»</button>
</div>
</nav>
</div>
</main> </main>
</div>
+227
View File
@@ -0,0 +1,227 @@
/* --- ZWEISPALTIIGES GRID-LAYOUT --- */
.cat-view-container {
box-sizing: border-box;
max-width: 95%; /* 95% Monitorbreite wie das Original */
width: 95%;
margin: 2rem auto;
padding: 0 1rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: #212529;
line-height: 1.5;
/* Grid für die Zweispaltigkeit */
display: grid;
grid-template-columns: 320px 1fr; /* 320px Sidebar, Rest für Content */
gap: 2.5rem;
}
.cat-view-container * {
box-sizing: border-box;
}
/* --- SEITENLEISTE (SORTIERUNG) --- */
.cat-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.cat-sidebar-box {
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1.25rem;
}
.cat-sidebar-title {
font-size: 1rem;
font-weight: 700;
color: #1a202c;
margin: 0 0 1rem 0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.cat-filter-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.cat-filter-option {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-size: 0.95rem;
color: #4a5568;
}
.cat-filter-option input[type="radio"] {
margin: 0;
accent-color: #3182ce;
}
/* --- HAUPTINHALT --- */
.cat-view-main {
min-width: 0;
}
.cat-view-header {
margin-bottom: 2rem;
border-bottom: 2px solid #dee2e6;
padding-bottom: 1rem;
}
.cat-view-title {
font-size: 2rem;
color: #1a202c;
margin: 0 0 0.5rem 0;
font-weight: 700;
}
.cat-view-meta-text {
color: #6c757d;
font-size: 0.95rem;
margin: 0;
}
/* --- BEITRÄGE LISTE --- */
.cat-view-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.cat-view-item {
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.cat-view-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border-color: #cbd5e1;
}
.cat-view-content {
flex: 1;
}
.cat-view-item-title {
font-size: 1.25rem;
margin: 0 0 0.25rem 0;
font-weight: 600;
}
.cat-view-link {
color: #3182ce;
text-decoration: none;
}
.cat-view-link:hover {
text-decoration: underline;
color: #2b6cb0;
}
.cat-view-meta-row {
display: flex;
gap: 15px;
align-items: center;
margin-bottom: 0.5rem;
}
.cat-view-author {
font-size: 0.875rem;
color: #4a5568;
margin: 0;
}
.cat-view-author-name {
font-weight: 600;
color: #2d3748;
}
.cat-view-likes {
font-size: 0.9em;
color: #475569;
}
.cat-view-arrow {
font-size: 1.5rem;
color: #a0aec0;
padding-left: 1rem;
transition: color 0.2s ease, transform 0.2s ease;
}
.cat-view-item:hover .cat-view-arrow {
color: #3182ce;
transform: translateX(3px);
}
/* --- FOOTER PAGINATION --- */
.cat-view-pagination-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid #e2e8f0;
}
.cat-view-pagination-footer .s-res-page-navigation {
display: flex;
gap: 0.25rem;
}
.cat-view-pagination-footer .s-res-page-btn {
background-color: #ffffff;
border: 1px solid #cbd5e1;
color: #4a5568;
padding: 0.35rem 0.75rem;
font-size: 0.9rem;
font-weight: 500;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.cat-view-pagination-footer .s-res-page-btn:hover:not(:disabled) {
background-color: #f8f9fa;
border-color: #cbd5e1;
color: #2d3748;
}
.cat-view-pagination-footer .cat-active-btn {
background-color: #3182ce;
border-color: #3182ce;
color: #ffffff;
cursor: default;
}
.cat-view-pagination-footer .s-res-page-btn:disabled:not(.cat-active-btn) {
background-color: #f1f5f9;
border-color: #e2e8f0;
color: #94a3b8;
cursor: not-allowed;
}
/* RESPONSIVE ANPASSUNG (Unter 768px) */
@media (max-width: 768px) {
.cat-view-container {
grid-template-columns: 1fr; /* Stapelt Sidebar und Inhalt untereinander */
gap: 1.5rem;
margin: 1rem auto;
}
.cat-view-pagination-footer {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
}
+171
View File
@@ -0,0 +1,171 @@
<?php
/**
* Hilfsfunktionen rund um die Suchergebnisse.
*
* Werden sowohl von content/search-results.php (No-JS-Rendering)
* als auch von php/controller/search-results-data.php (JSON für
* die JS-Hydration) genutzt, damit Sortier-/Filterlogik nur an
* einer Stelle gepflegt werden muss
*/
function getLikeCount($item): int
{
if (isset($item['likes']) && is_array($item['likes'])) {
return count($item['likes']);
}
return (int)($item['likes'] ?? 0);
}
/**
* Sortiert die Ergebnisse
*/
function sortSearchResults(array $results, string $sortStyle): array
{
switch ($sortStyle) {
case 'likes':
usort($results, function ($a, $b) {
return getLikeCount($b) <=> getLikeCount($a);
});
break;
case 'newest':
usort($results, function ($a, $b) {
return strcmp($b['creationDate'] ?? '', $a['creationDate'] ?? '');
});
break;
case 'oldest':
usort($results, function ($a, $b) {
return strcmp($a['creationDate'] ?? '', $b['creationDate'] ?? '');
});
break;
case 'alphabet':
default:
usort($results, function ($a, $b) {
return strcasecmp($a['title'] ?? '', $b['title'] ?? '');
});
break;
}
return $results;
}
/**
* Filtert die Ergebnisse ('all' = kein Filter).
*/
function filterSearchResultsByCategory(array $results, string $category): array
{
$category = strtolower(trim($category));
if ($category === '' || $category === 'all') {
return $results;
}
return array_values(array_filter($results, function ($item) use ($category) {
return strtolower($item['category'] ?? '') === $category;
}));
}
/**
* Baut eine Such-Ergebnis-URL für einen bestimmten Seitenwechsel (No-JS-Pagination).
*/
function buildSearchResultsUrl(int $page, string $query, string $sort, string $category, int $limit): string
{
return "index.php?pfad=search-results"
. "&q=" . urlencode($query)
. "&sort=" . urlencode($sort)
. "&category=" . urlencode($category)
. "&limit=" . $limit
. "&page=" . $page;
}
/**
* Rendert eine rein serverseitige Pagination für den No-JS-Fall:
*/
function renderNoJsPagination(int $currentPage, int $totalPages, string $query, string $sort, string $category, int $limit): string
{
$html = '';
if ($currentPage > 1) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildSearchResultsUrl($currentPage - 1, $query, $sort, $category, $limit)) . '">&laquo;</a> ';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&laquo;</span> ';
}
for ($i = 1; $i <= $totalPages; $i++) {
if ($i === $currentPage) {
$html .= '<span class="s-res-page-btn s-res-page-btn-active">' . $i . '</span> ';
} else {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildSearchResultsUrl($i, $query, $sort, $category, $limit)) . '">' . $i . '</a> ';
}
}
if ($currentPage < $totalPages) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildSearchResultsUrl($currentPage + 1, $query, $sort, $category, $limit)) . '">&raquo;</a>';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&raquo;</span>';
}
return $html;
}
/**
* Filtert Ergebnisse anhand eines Suchbegriffs
*
* Wird von content/showCategory.php genutzt, um innerhalb einer bereits
* geladenen Kategorie zu suchen.
*/
function filterResultsByQuery(array $results, string $query): array
{
$query = trim($query);
if ($query === '') {
return $results;
}
$needle = mb_strtolower($query);
return array_values(array_filter($results, function ($item) use ($needle) {
$haystack = mb_strtolower(($item['title'] ?? '') . ' ' . ($item['content'] ?? ''));
return mb_strpos($haystack, $needle) !== false;
}));
}
/**
* Baut eine Kategorie-URL für einen bestimmten Seitenwechsel (No-JS-Pagination).
* Analog zu buildSearchResultsUrl, aber für pfad=showCategory (feste Kategorie,
* dafür mit Freitext-Suche innerhalb der Kategorie statt Kategorie-Filter).
*/
function buildCategoryUrl(int $page, string $category, string $sort, string $query, int $limit): string
{
return "index.php?pfad=showCategory"
. "&category=" . urlencode($category)
. "&sort=" . urlencode($sort)
. "&q=" . urlencode($query)
. "&limit=" . $limit
. "&page=" . $page;
}
/**
* Rendert eine rein serverseitige Pagination für showCategory (No-JS-Fall).
* Analog zu renderNoJsPagination.
*/
function renderNoJsPaginationCategory(int $currentPage, int $totalPages, string $category, string $sort, string $query, int $limit): string
{
$html = '';
if ($currentPage > 1) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($currentPage - 1, $category, $sort, $query, $limit)) . '">&laquo;</a> ';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&laquo;</span> ';
}
for ($i = 1; $i <= $totalPages; $i++) {
if ($i === $currentPage) {
$html .= '<span class="s-res-page-btn s-res-page-btn-active">' . $i . '</span> ';
} else {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($i, $category, $sort, $query, $limit)) . '">' . $i . '</a> ';
}
}
if ($currentPage < $totalPages) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($currentPage + 1, $category, $sort, $query, $limit)) . '">&raquo;</a>';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&raquo;</span>';
}
return $html;
}
+8 -3
View File
@@ -15,6 +15,10 @@ if ($pfad === "login") {
include_once "php/controller/login-controller.php"; include_once "php/controller/login-controller.php";
} }
if ($pfad === "search-results-controller") {
include_once "php/controller/search-results-controller.php";
}
if ($pfad === "register") { if ($pfad === "register") {
include_once "php/controller/register-controller.php"; include_once "php/controller/register-controller.php";
} }
@@ -63,12 +67,13 @@ if ($pfad === "deleteAccount") {
<link rel="stylesheet" href="css/profile.css"> <link rel="stylesheet" href="css/profile.css">
<link rel="stylesheet" href="css/showArticle.css"> <link rel="stylesheet" href="css/showArticle.css">
<link rel="stylesheet" href="css/message.css"> <link rel="stylesheet" href="css/message.css">
<link rel="stylesheet" href="css/showCategory.css">
<script src="js/paginator.js" async></script>
<script src="js/sorter.js" async></script>
<script src="js/comments.js" defer></script> <script src="js/comments.js" defer></script>
<script src="js/editor.js" async></script> <script src="js/editor.js" async></script>
<script src="js/filter.js" async></script> <script src="js/search-results.js" async></script>
<script src="js/showCategory.js" async></script>
<title>EduForge</title> <title>EduForge</title>
</head> </head>
-116
View File
@@ -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();
}
-27
View File
@@ -1,27 +0,0 @@
function initPaginator() {
const form = document.getElementById('search-form-id');
const pageInput = document.getElementById('s-res-page-input');
const pageButtons = document.querySelectorAll('.s-res-page-navigation .s-res-page-btn');
pageButtons.forEach(button => {
button.addEventListener('click', function() {
if (this.disabled) return;
const targetPage = this.getAttribute('data-page');
if (targetPage && form && pageInput) {
pageInput.value = targetPage;
form.submit();
}
});
});
}
// ist das DOM bereits vollständig aufgebaut?
if (document.readyState === 'loading') {
// Falls noch geladen wird, auf das Event warten
document.addEventListener('DOMContentLoaded', initPaginator);
} else {
// Falls das HTML bereits komplett da ist, sofort ausführen
initPaginator();
}
+238
View File
@@ -0,0 +1,238 @@
/**
* Übernimmt Sortierung, Kategorie-Filter und Pagination der Suchergebnisse
* vollständig clientseitig
*
* Ablauf:
* 1. Kompletten Ergebnis-Datensatz einmalig per fetch() nachladen
* (php/controller/search-results-data.php, liest nur die Session-Daten,
* keine erneute Datenbank-Suche).
* 2. Sortieren + filtern (JS-Array).
* 3. Ergebnisliste + Pagination-UI daraus neu rendern.
*
* Ersetzt sorter.js, filter.js und paginator.js, deren Klick-Handler sich
* gegenseitig ins Gehege kamen.
* Ohne JS bleibt die serverseitig gerenderte Seite (echte Links/Formulare)
* voll funktionsfähig.
*/
(function () {
const state = {
allItems: [],
sort: 'alphabet',
category: 'all',
itemsPerPage: 10,
currentPage: 1,
};
let listContainer, sortRadios, categorySelect, limitSelect;
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
function init() {
listContainer = document.querySelector('.s-res-list');
if (!listContainer) return;
sortRadios = document.querySelectorAll('.sort-radio');
categorySelect = document.getElementById('category-filter');
limitSelect = document.getElementById('s-res-per-page');
prevBtn = document.getElementById('prev-page-btn');
nextBtn = document.getElementById('next-page-btn');
numbersContainer = document.getElementById('dynamic-page-numbers');
jsNav = document.getElementById('js-page-navigation');
resultCountEl = document.getElementById('s-res-result-count');
const checkedRadio = document.querySelector('.sort-radio:checked');
state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
state.category = categorySelect ? categorySelect.value : 'all';
state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
fetchFullDataset()
.then(function (data) {
state.allItems = data.results || [];
attachEvents();
render();
if (jsNav) jsNav.style.display = 'flex';
})
.catch(function (err) {
// JSON-Endpoint nicht erreichbar: die serverseitig gerenderte
// (No-JS-)Ansicht bleibt sichtbar und funktioniert weiter.
console.error('Suchergebnisse konnten nicht nachgeladen werden:', err);
});
}
function fetchFullDataset() {
return fetch('php/controller/search-results-data.php', { credentials: 'same-origin' })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
});
}
function getFilteredSortedItems() {
let items = state.allItems;
if (state.category !== 'all') {
const wanted = state.category.toLowerCase();
items = items.filter(function (item) {
return (item.category || '').toLowerCase() === wanted;
});
}
items = items.slice().sort(function (a, b) {
if (state.sort === 'likes') {
return (b.likes || 0) - (a.likes || 0);
}
if (state.sort === 'newest' || state.sort === 'oldest') {
return state.sort === 'newest' ? (b.id - a.id) : (a.id - b.id);
}
// alphabet (Standard)
return (a.title || '').toLowerCase().localeCompare((b.title || '').toLowerCase());
});
return items;
}
function render() {
const items = getFilteredSortedItems();
if (resultCountEl) resultCountEl.textContent = items.length;
const totalPages = Math.max(1, Math.ceil(items.length / state.itemsPerPage));
if (state.currentPage > totalPages) state.currentPage = totalPages;
if (state.currentPage < 1) state.currentPage = 1;
const start = (state.currentPage - 1) * state.itemsPerPage;
const pageItems = items.slice(start, start + state.itemsPerPage);
listContainer.innerHTML = '';
if (pageItems.length === 0) {
const empty = document.createElement('p');
empty.textContent = 'Keine Beiträge gefunden.';
listContainer.appendChild(empty);
} else {
pageItems.forEach(function (item) {
listContainer.appendChild(buildCard(item));
});
}
updatePaginatorUI(state.currentPage, totalPages);
}
function buildCard(item) {
const card = document.createElement('div');
card.className = 's-res-item';
card.setAttribute('data-likes', item.likes);
card.setAttribute('data-category', (item.category || '').toLowerCase());
const link = document.createElement('a');
link.className = 's-res-link';
link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id);
link.textContent = item.title;
const title = document.createElement('h2');
title.className = 's-res-item-title';
title.appendChild(link);
const authorName = document.createElement('span');
authorName.className = 's-res-author-name';
authorName.textContent = item.author;
const author = document.createElement('p');
author.className = 's-res-author';
author.append('Von: ', authorName);
const likes = document.createElement('span');
likes.className = 's-res-likes';
likes.textContent = '❤️ ' + item.likes;
const metaRow = document.createElement('div');
metaRow.className = 's-res-meta-row';
metaRow.append(author, likes);
const content = document.createElement('div');
content.className = 's-res-content';
content.append(title, metaRow);
const arrow = document.createElement('div');
arrow.className = 's-res-arrow';
arrow.textContent = '→';
card.append(content, arrow);
return card;
}
function updatePaginatorUI(currentPage, totalPages) {
if (!prevBtn || !nextBtn || !numbersContainer) return;
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= totalPages;
numbersContainer.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 's-res-page-btn' + (i === currentPage ? ' s-res-page-btn-active' : '');
btn.textContent = i;
btn.addEventListener('click', function () {
state.currentPage = i;
render();
});
numbersContainer.appendChild(btn);
}
}
function attachEvents() {
sortRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
state.sort = this.value;
state.currentPage = 1;
render();
});
});
if (categorySelect) {
categorySelect.addEventListener('change', function () {
state.category = this.value;
state.currentPage = 1;
render();
});
}
if (limitSelect) {
limitSelect.addEventListener('change', function () {
state.itemsPerPage = parseInt(this.value, 10) || 10;
state.currentPage = 1;
render();
});
}
if (prevBtn) {
prevBtn.addEventListener('click', function () {
if (state.currentPage > 1) {
state.currentPage -= 1;
render();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
state.currentPage += 1;
render();
});
}
// Mit aktivem JS übernimmt render() Sortierung/Filter live
// ein echtes Absenden des Formulars (No-JS-Fallback) ist dann nicht mehr nötig.
const filterForm = document.getElementById('s-res-filter-form');
if (filterForm) {
filterForm.addEventListener('submit', function (e) {
e.preventDefault();
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
+242
View File
@@ -0,0 +1,242 @@
/**
* Übernimmt Sortierung, Suche (innerhalb der Kategorie) und Pagination der
* Kategorie-Ansicht vollständig clientseitig.
*
* Ablauf:
* 1. Kompletten Kategorie-Datensatz einmalig per fetch() nachladen
* 2. Sortieren + nach Suchbegriff filtern (JS-Array).
* 3. Ergebnisliste + Pagination-UI daraus neu rendern.
*
* Ohne JS bleibt die serverseitig gerenderte Seite (echte Links/Formulare)
* voll funktionsfähig
*/
(function () {
const state = {
allItems: [],
sort: 'alphabet',
query: '',
itemsPerPage: 10,
currentPage: 1,
};
let listContainer, sortRadios, searchInput, limitSelect;
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
let searchDebounceTimer;
function init() {
listContainer = document.querySelector('.s-res-list');
if (!listContainer) return;
sortRadios = document.querySelectorAll('.sort-radio');
searchInput = document.getElementById('cat-search-input');
limitSelect = document.getElementById('s-res-per-page');
prevBtn = document.getElementById('prev-page-btn');
nextBtn = document.getElementById('next-page-btn');
numbersContainer = document.getElementById('dynamic-page-numbers');
jsNav = document.getElementById('js-page-navigation');
resultCountEl = document.getElementById('s-res-result-count');
const checkedRadio = document.querySelector('.sort-radio:checked');
state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
state.query = searchInput ? searchInput.value : '';
state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
fetchFullDataset()
.then(function (data) {
state.allItems = data.results || [];
attachEvents();
render();
if (jsNav) jsNav.style.display = 'flex';
})
.catch(function (err) {
// JSON-Endpoint nicht erreichbar: die serverseitig gerenderte
// (No-JS-)Ansicht bleibt sichtbar und funktioniert weiter.
console.error('Kategorie-Beiträge konnten nicht nachgeladen werden:', err);
});
}
function fetchFullDataset() {
return fetch('php/controller/showCategory-data.php', { credentials: 'same-origin' })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
});
}
function getFilteredSortedItems() {
let items = state.allItems;
const needle = state.query.trim().toLowerCase();
if (needle !== '') {
items = items.filter(function (item) {
const haystack = ((item.title || '') + ' ' + (item.content || '')).toLowerCase();
return haystack.indexOf(needle) !== -1;
});
}
items = items.slice().sort(function (a, b) {
if (state.sort === 'likes') {
return (b.likes || 0) - (a.likes || 0);
}
if (state.sort === 'newest' || state.sort === 'oldest') {
return state.sort === 'newest' ? (b.id - a.id) : (a.id - b.id);
}
// alphabet (Standard)
return (a.title || '').toLowerCase().localeCompare((b.title || '').toLowerCase());
});
return items;
}
function render() {
const items = getFilteredSortedItems();
if (resultCountEl) resultCountEl.textContent = items.length;
const totalPages = Math.max(1, Math.ceil(items.length / state.itemsPerPage));
if (state.currentPage > totalPages) state.currentPage = totalPages;
if (state.currentPage < 1) state.currentPage = 1;
const start = (state.currentPage - 1) * state.itemsPerPage;
const pageItems = items.slice(start, start + state.itemsPerPage);
listContainer.innerHTML = '';
if (pageItems.length === 0) {
const empty = document.createElement('p');
empty.textContent = 'Keine Beiträge gefunden.';
listContainer.appendChild(empty);
} else {
pageItems.forEach(function (item) {
listContainer.appendChild(buildCard(item));
});
}
updatePaginatorUI(state.currentPage, totalPages);
}
function buildCard(item) {
const card = document.createElement('div');
card.className = 's-res-item';
card.setAttribute('data-likes', item.likes);
card.setAttribute('data-category', (item.category || '').toLowerCase());
const link = document.createElement('a');
link.className = 's-res-link';
link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id);
link.textContent = item.title;
const title = document.createElement('h2');
title.className = 's-res-item-title';
title.appendChild(link);
const authorName = document.createElement('span');
authorName.className = 's-res-author-name';
authorName.textContent = item.author;
const author = document.createElement('p');
author.className = 's-res-author';
author.append('Von: ', authorName);
const likes = document.createElement('span');
likes.className = 's-res-likes';
likes.textContent = '❤️ ' + item.likes;
const metaRow = document.createElement('div');
metaRow.className = 's-res-meta-row';
metaRow.append(author, likes);
const content = document.createElement('div');
content.className = 's-res-content';
content.append(title, metaRow);
const arrow = document.createElement('div');
arrow.className = 's-res-arrow';
arrow.textContent = '→';
card.append(content, arrow);
return card;
}
function updatePaginatorUI(currentPage, totalPages) {
if (!prevBtn || !nextBtn || !numbersContainer) return;
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= totalPages;
numbersContainer.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 's-res-page-btn' + (i === currentPage ? ' s-res-page-btn-active' : '');
btn.textContent = i;
btn.addEventListener('click', function () {
state.currentPage = i;
render();
});
numbersContainer.appendChild(btn);
}
}
function attachEvents() {
sortRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
state.sort = this.value;
state.currentPage = 1;
render();
});
});
if (searchInput) {
// Live-Filterung während des Tippens (leicht entprellt), da der
// Datensatz bereits vollständig im Browser liegt.
searchInput.addEventListener('input', function () {
clearTimeout(searchDebounceTimer);
const value = this.value;
searchDebounceTimer = setTimeout(function () {
state.query = value;
state.currentPage = 1;
render();
}, 150);
});
}
if (limitSelect) {
limitSelect.addEventListener('change', function () {
state.itemsPerPage = parseInt(this.value, 10) || 10;
state.currentPage = 1;
render();
});
}
if (prevBtn) {
prevBtn.addEventListener('click', function () {
if (state.currentPage > 1) {
state.currentPage -= 1;
render();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
state.currentPage += 1;
render();
});
}
// Mit aktivem JS übernimmt render() Sortierung/Suche live
// ein echtes Absenden des Formulars (No-JS-Fallback) ist dann nicht mehr nötig.
const filterForm = document.getElementById('cat-filter-form');
if (filterForm) {
filterForm.addEventListener('submit', function (e) {
e.preventDefault();
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
-50
View File
@@ -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();
}
+12 -31
View File
@@ -2,8 +2,8 @@
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
require_once '../model/LocalArticleManager.php';
require_once '../model/ArticleManager.php'; require_once '../model/ArticleManager.php';
require_once '../model/UserManager.php';
require_once '../model/Article.php'; require_once '../model/Article.php';
require_once '../validator/search-validator.php'; require_once '../validator/search-validator.php';
@@ -18,41 +18,17 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
try { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
$userManager = UserManager::getInstance();
$results = $articleManager->search($search); $results = $articleManager->search($search);
$sortStyle = $_GET['sort'] ?? 'alphabet';
$_SESSION['search_sort'] = $sortStyle;
if ($sortStyle === 'alphabet') {
// Titel aufsteigend alphabetiisch sortiert
usort($results, function ($a, $b) {
return strcasecmp($a->getTitle(), $b->getTitle());
});
} elseif ($sortStyle === 'likes') {
usort($results, function($a, $b) {
return $b->getLikeCount() <=> $a->getLikeCount();
});
} elseif ($sortStyle === 'newest') {
// Datum neu zu alt sortiert
usort($results, function($a, $b) {
return strcmp($b->getCreationDate(), $a->getCreationDate());
});
} elseif ($sortStyle === 'oldest') {
// Datum alt zu neu sortiert
usort($results, function($a, $b) {
return strcmp($a->getCreationDate(), $b->getCreationDate());
});
}
// Ergebnisse werden in ein flaches array umgewandelt, da sont incomplete-PHP error im Ergebnis
$safeArrayResults = []; $safeArrayResults = [];
foreach ($results as $obj) { foreach ($results as $obj) {
$safeArrayResults[] = [ $safeArrayResults[] = [
"id" => $obj->getId(), "id" => $obj->getId(),
"title" => $obj->getTitle(), "title" => $obj->getTitle(),
"content" => $obj->getContent(), "content" => $obj->getContent(),
"author" => $obj->getAuthor(), "author" => $userManager->findUser($obj->getAuthor())["vorname"] . " " . $userManager->findUser($obj->getAuthor())["nachname"],
"category" => $obj->getCategory(), "category" => $obj->getCategory(),
"tags" => $obj->getTags(), "tags" => $obj->getTags(),
"creationDate" => $obj->getCreationDate(), "creationDate" => $obj->getCreationDate(),
@@ -70,14 +46,19 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
} }
$sort = $_GET['sort'] ?? 'alphabet'; $sort = $_GET['sort'] ?? 'alphabet';
$_SESSION['search_sort'] = $sort;
$category = $_GET['category'] ?? 'all';
$_SESSION['search_category'] = $category;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10; $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10;
if (!searchLimitValidator($limit)) { if (!searchLimitValidator($limit)) {
$limit = 10; $limit = 10;
} }
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1; $_SESSION['search_limit'] = $limit;
header("Location: ../../index.php?pfad=search-results&q=" . urlencode($search) . "&sort=" . urlencode($sort) . "&limit=" . $limit . "&page=" . $page);
// Neue Suche -> immer wieder bei Seite 1 beginnen
header("Location: ../../index.php?pfad=search-results&q=" . urlencode($search) . "&sort=" . urlencode($sort) . "&category=" . urlencode($category) . "&limit=" . $limit . "&page=1");
exit(); exit();
} }
?>
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* Liefert den vollständigen, zuletzt gefundenen Ergebnis-Datensatz als JSON.
*
* Wird von js/search-results.js EINMALIG per fetch() geladen, damit Sortierung,
* Filterung und Pagination danach komplett im Browser laufen können, ohne
* weitere Serveranfragen oder eine erneute Datenbank-Suche auszulösen.
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../includes/resultsHelper.php';
$rawResults = $_SESSION['search_results'] ?? [];
$payload = array_map(function ($item) {
return [
'id' => $item['id'],
'title' => $item['title'],
'author' => $item['author'],
'category' => $item['category'] ?? '',
'likes' => getLikeCount($item),
];
}, $rawResults);
echo json_encode([
'query' => $_SESSION['search_query'] ?? '',
'results' => array_values($payload),
]);
@@ -5,6 +5,7 @@ if (session_status() === PHP_SESSION_NONE) {
require_once 'php/model/Article.php'; require_once 'php/model/Article.php';
require_once 'php/model/ArticleManager.php'; require_once 'php/model/ArticleManager.php';
require_once 'php/model/UserManager.php';
require_once 'php/model/CommentManager.php'; require_once 'php/model/CommentManager.php';
if (isset($_GET["id"]) && !empty($_GET["id"])){ if (isset($_GET["id"]) && !empty($_GET["id"])){
@@ -19,6 +20,9 @@ if (isset($_GET["id"]) && !empty($_GET["id"])){
$author = $article->getAuthor(); $author = $article->getAuthor();
$tags = $article->getTags(); $tags = $article->getTags();
$articleObj = $article; // Objekt für die Like-Abfagen sichern $articleObj = $article; // Objekt für die Like-Abfagen sichern
$userManager = UserManager::getInstance();
$name = $userManager->findUser($author)["vorname"]." ".$userManager->findUser($author)["nachname"];
}else{ }else{
//header("location: index.php?pfad=404"); //header("location: index.php?pfad=404");
include_once "content/404.php"; include_once "content/404.php";
+33 -2
View File
@@ -1,21 +1,52 @@
<?php <?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once 'php/model/Article.php'; require_once 'php/model/Article.php';
require_once 'php/model/ArticleManager.php'; require_once 'php/model/ArticleManager.php';
require_once 'php/model/UserManager.php';
require_once 'php/validator/article-validator.php'; require_once 'php/validator/article-validator.php';
require_once __DIR__ . '/../../includes/resultsHelper.php';
if (isset($_GET["category"]) && !empty($_GET["category"]) && articleCategoryValidator($_GET["category"])) { if (isset($_GET["category"]) && !empty($_GET["category"]) && articleCategoryValidator($_GET["category"])) {
$category = $_GET["category"]; $category = $_GET["category"];
try { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
$articles = $articleManager->getArticlesByCategory($category); $userManager = UserManager::getInstance();
$allArticles = $articleManager->getArticlesByCategory($category);
$safeArrayResults = [];
foreach ($allArticles as $article) {
$authorName = $userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"];
$safeArrayResults[] = [
"id" => $article->getID(),
"title" => $article->getTitle(),
"content" => $article->getContent(),
"author" => $authorName,
"category" => $article->getCategory(),
"tags" => $article->getTags(),
"creationDate" => $article->getCreationDate(),
"likes" => $article->getLikes(),
];
}
// Session dient hier (wie bei search-results) als Zwischenspeicher, damit
// js/showCategory.js den kompletten Datensatz per showCategory-data.php
// nachladen kann, ohne die Kategorie-Abfrage ein zweites Mal auszuführen.
$_SESSION['category_results'] = $safeArrayResults;
$_SESSION['category_name'] = $category;
} catch (Exception $e) { } catch (Exception $e) {
$_SESSION["message"] = "internal_error"; $_SESSION["message"] = "internal_error";
include_once "content/404.php"; include_once "content/404.php";
exit(); exit();
} }
} else { } else {
$_SESSION["message"] = "invalid_category"; $_SESSION["message"] = "invalid_category";
include_once "content/404.php"; include_once "content/404.php";
exit(); exit();
} }
?>
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* Liefert den vollständigen, zuletzt geladenen Kategorie-Datensatz als JSON.
*
* Wird von js/showCategory.js EINMALIG per fetch() geladen, damit Sortierung,
* Suche und Pagination danach komplett im Browser laufen können, ohne
* weitere Serveranfragen oder eine erneute Datenbank-Abfrage auszulösen
*
* Analog zu php/controller/search-results-data.php
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../includes/resultsHelper.php';
$rawResults = $_SESSION['category_results'] ?? [];
$payload = array_map(function ($item) {
return [
'id' => $item['id'],
'title' => $item['title'],
'content' => $item['content'] ?? '',
'author' => $item['author'],
'category' => $item['category'] ?? '',
'likes' => getLikeCount($item),
];
}, $rawResults);
echo json_encode([
'category' => $_SESSION['category_name'] ?? '',
'results' => array_values($payload),
]);
+10
View File
@@ -97,6 +97,16 @@ class Article
return $this->author; return $this->author;
} }
/**
* Setzt einen neuen Autor eines Beitrages.
* @param string $author
* @return void
*/
public function setAuthor(string $author)
{
$this->author = $author;
}
/** /**
* Gibt das Veröffentlichungsdatum des Beitrags zurück. * Gibt das Veröffentlichungsdatum des Beitrags zurück.
* @return string * @return string