Compare commits

...

16 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
9 changed files with 542 additions and 142 deletions
+5 -5
View File
@@ -66,7 +66,7 @@ $categories = [
Inhalt: Zeigt die Ergebnisse einer Suche an. Inhalt: Zeigt die Ergebnisse einer Suche an.
Funktioniert ohne JavaScript (serverseitige Sortierung/Filter/Pagination Funktioniert ohne JavaScript (serverseitige Sortierung/Filter/Pagination
über echte Links + Formulare) und wird bei aktiviertem JS von über echte Links + Formulare) und wird bei aktiviertem JS von
js/results-app.js vollständig client-seitig übernommen. 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" ?>
@@ -85,7 +85,7 @@ $categories = [
<!-- Formular 2: sortiert/filtert die BEREITS gefundenen Ergebnisse. <!-- Formular 2: sortiert/filtert die BEREITS gefundenen Ergebnisse.
Ohne JS: echter Reload dieser Seite, aber ohne erneute Datenbank-Suche. Ohne JS: echter Reload dieser Seite, aber ohne erneute Datenbank-Suche.
Mit JS: results-app.js übernimmt das live im Browser, der Submit-Button Mit JS: search-results.js übernimmt das live im Browser, der Submit-Button
wird dafür nicht benötigt und über <noscript> versteckt. --> 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"> <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="pfad" value="search-results">
@@ -180,7 +180,7 @@ $categories = [
<div class="s-res-pagination-footer"> <div class="s-res-pagination-footer">
<!-- Auswahl der Ergebnisse pro Seite. Gehört per form="..." logisch <!-- Auswahl der Ergebnisse pro Seite. Gehört per form="..." logisch
zum Filterformular oben, bleibt aber optisch am Fuß der Liste. --> 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" form="s-res-filter-form" class="s-res-limit-select"> <select id="s-res-per-page" name="limit" form="s-res-filter-form" class="s-res-limit-select">
@@ -193,12 +193,12 @@ $categories = [
</div> </div>
<nav class="s-res-page-navigation" aria-label="Seitennavigation"> <nav class="s-res-page-navigation" aria-label="Seitennavigation">
<!-- No-JS-Fallback: echte, funktionierende Links --> <!-- No-JS-Fallback: -->
<noscript> <noscript>
<?php echo renderNoJsPagination($currentPage, $totalPages, $query, $currentSort, $currentCategory, $limit); ?> <?php echo renderNoJsPagination($currentPage, $totalPages, $query, $currentSort, $currentCategory, $limit); ?>
</noscript> </noscript>
<!-- JS-Version: wird per results-app.js befüllt/eingeblendet --> <!-- JS-Version: wird per search-results.js befüllt/eingeblendet: -->
<div id="js-page-navigation" style="display:none;"> <div id="js-page-navigation" style="display:none;">
<button type="button" class="s-res-page-btn" id="prev-page-btn">«</button> <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>
+146 -73
View File
@@ -1,111 +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';
$currentSort = isset($sortStyle) ? $sortStyle : 'alphabet'; $rawResults = $_SESSION['category_results'] ?? [];
$currentPage = isset($page) ? $page : 1; $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" ?>
<noscript> <!-- Links: Seitenleiste für Suche (innerhalb der Kategorie) und Sortierung -->
Bitte JavaScript aktivieren! <aside class="s-res-sidebar">
</noscript>
<!-- Das Formular umschließt jetzt das gesamte Layout --> <!-- Sortiert/durchsucht die BEREITS geladenen Kategorie-Beiträge.
<form id="search-form-id" action="index.php" method="GET" style="display: contents;"> 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="pfad" value="showCategory">
<input type="hidden" name="category" value="<?php echo (isset($category) && !empty($category)) ? htmlspecialchars($category) : ''; ?>"> <input type="hidden" name="category" value="<?php echo htmlspecialchars($category); ?>">
<input type="hidden" id="s-res-page-input" name="page" value="<?php echo $currentPage; ?>">
<div class="cat-view-container"> <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>
<!-- LINKE SPALTE: SEITENLEISTE FÜR SORTIERUNG --> <div class="s-res-sidebar-box">
<aside class="cat-sidebar"> <h3 class="s-res-sidebar-title">Sortierung</h3>
<div class="cat-sidebar-box"> <div class="s-res-filter-group">
<h3 class="cat-sidebar-title">Sortierung</h3> <label class="s-res-filter-option">
<div class="cat-filter-group"> <input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>>
<label class="cat-filter-option"> <span>Alphabetisch</span>
<input type="radio" name="sort" value="alphabet" <?php echo ($currentSort === 'alphabet') ? 'checked' : ''; ?> onchange="this.form.submit();">
Alphabetisch
</label> </label>
<label class="cat-filter-option"> <label class="s-res-filter-option">
<input type="radio" name="sort" value="likes" <?php echo ($currentSort === 'likes') ? 'checked' : ''; ?> onchange="this.form.submit();"> <input type="radio" name="sort" value="likes" class="sort-radio" <?php echo $currentSort === 'likes' ? 'checked' : ''; ?>>
Beliebtheit (Likes) <span>Beliebtheit (Likes)</span>
</label> </label>
<label class="cat-filter-option"> <label class="s-res-filter-option">
<input type="radio" name="sort" value="newest" <?php echo ($currentSort === 'newest') ? 'checked' : ''; ?> onchange="this.form.submit();"> <input type="radio" name="sort" value="newest" class="sort-radio" <?php echo $currentSort === 'newest' ? 'checked' : ''; ?>>
Neueste Beiträge <span>Neueste Beiträge</span>
</label> </label>
<label class="cat-filter-option"> <label class="s-res-filter-option">
<input type="radio" name="sort" value="oldest" <?php echo ($currentSort === 'oldest') ? 'checked' : ''; ?> onchange="this.form.submit();"> <input type="radio" name="sort" value="oldest" class="sort-radio" <?php echo $currentSort === 'oldest' ? 'checked' : ''; ?>>
Älteste Beiträge <span>Älteste Beiträge</span>
</label> </label>
</div> </div>
</div> </div>
<noscript>
<button type="submit" class="nav__search-button">Anwenden</button>
</noscript>
</form>
</aside> </aside>
<!-- RECHTE SPALTE: INHALT & PAGINATOR --> <main class="s-res-main-content">
<main class="cat-view-main">
<div class="cat-view-header"> <div class="s-res-header">
<h1 class="cat-view-title">Kategorie: <?php echo (isset($category) && !empty($category)) ? htmlspecialchars($category) : 'Unbekannt'; ?></h1> <h1 class="s-res-main-title">Kategorie: <?php echo htmlspecialchars($category); ?></h1>
<p class="cat-view-meta-text"> <p class="s-res-meta">
<?php echo (isset($totalArticles)) ? $totalArticles : 0; ?> Treffer in dieser Kategorie <span id="s-res-result-count"><?php echo $totalResultsCount; ?></span> Treffer in dieser Kategorie<?php echo $query !== '' ? ' für "' . htmlspecialchars($query) . '"' : ''; ?>
</p> </p>
</div> </div>
<!-- Beiträge --> <!-- Ergebnisliste -->
<div class="cat-view-list"> <div class="s-res-list">
<?php if (isset($articles) && !empty($articles)): ?> <?php if (!empty($results)): ?>
<?php foreach ($articles as $article): ?>
<div class="cat-view-item"> <?php foreach ($results as $item):
<div class="cat-view-content"> $likesCount = getLikeCount($item);
<h2 class="cat-view-item-title"> ?>
<a href="index.php?pfad=showArticle&id=<?php echo htmlspecialchars($article->getID()); ?>" class="cat-view-link"> <div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>">
<?php echo (isset($article) && !empty($article->getTitle())) ? htmlspecialchars($article->getTitle()) : 'Kein Titel'; ?> <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> </a>
</h2> </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"> <span class="s-res-likes">
<p class="cat-view-author"> ❤️ <?php echo $likesCount; ?>
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> </span>
</div> </div>
</div> </div>
<div class="cat-view-arrow">&rarr;</div> <div class="s-res-arrow">&rarr;</div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php elseif ($query !== ''): ?>
<p>Keine Beiträge in dieser Kategorie zu diesem Suchbegriff gefunden.</p>
<?php else: ?> <?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 endif; ?>
<?php unset($_SESSION["message"]); ?>
</div> </div>
<!-- Footer Paginator --> <div class="s-res-pagination-footer">
<?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' : ''; ?>>
&laquo;
</button>
<?php for ($i = 1; $i <= $totalPages; $i++): ?> <!-- Auswahl der Ergebnisse pro Seite -->
<button type="button" <div class="s-res-limit-selector">
class="s-res-page-btn <?php echo ($i === $currentPage) ? 'cat-active-btn' : ''; ?>" <label for="s-res-per-page" class="s-res-limit-label">Ergebnisse pro Seite:</label>
data-page="<?php echo $i; ?>" <select id="s-res-per-page" name="limit" form="cat-filter-form" class="s-res-limit-select">
<?php echo ($i === $currentPage) ? 'disabled' : ''; ?>> <option value="10" <?php echo $limit === 10 ? 'selected' : ''; ?>>10</option>
<?php echo $i; ?> <option value="20" <?php echo $limit === 20 ? 'selected' : ''; ?>>20</option>
</button> <option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50</option>
<?php endfor; ?> <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' : ''; ?>> <nav class="s-res-page-navigation" aria-label="Seitennavigation">
&raquo; <!-- No-JS-Fallback: -->
</button> <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> </div>
</nav>
</div> </div>
<?php endif; ?>
</main> </main>
</div> </div>
</form>
+70 -7
View File
@@ -5,12 +5,9 @@
* Werden sowohl von content/search-results.php (No-JS-Rendering) * Werden sowohl von content/search-results.php (No-JS-Rendering)
* als auch von php/controller/search-results-data.php (JSON für * als auch von php/controller/search-results-data.php (JSON für
* die JS-Hydration) genutzt, damit Sortier-/Filterlogik nur an * die JS-Hydration) genutzt, damit Sortier-/Filterlogik nur an
* einer Stelle gepflegt werden muss. * einer Stelle gepflegt werden muss
*/ */
/**
* Anzahl der Likes robust ermitteln (Array von Likes ODER bereits eine Zahl).
*/
function getLikeCount($item): int function getLikeCount($item): int
{ {
if (isset($item['likes']) && is_array($item['likes'])) { if (isset($item['likes']) && is_array($item['likes'])) {
@@ -20,7 +17,7 @@ function getLikeCount($item): int
} }
/** /**
* Sortiert ein Array von Ergebnis-Assoziativarrays (wie in $_SESSION["search_results"] abgelegt). * Sortiert die Ergebnisse
*/ */
function sortSearchResults(array $results, string $sortStyle): array function sortSearchResults(array $results, string $sortStyle): array
{ {
@@ -51,7 +48,7 @@ function sortSearchResults(array $results, string $sortStyle): array
} }
/** /**
* Filtert ein Array von Ergebnissen nach Kategorie ('all' = kein Filter). * Filtert die Ergebnisse ('all' = kein Filter).
*/ */
function filterSearchResultsByCategory(array $results, string $category): array function filterSearchResultsByCategory(array $results, string $category): array
{ {
@@ -78,7 +75,7 @@ function buildSearchResultsUrl(int $page, string $query, string $sort, string $c
} }
/** /**
* Rendert eine rein serverseitige Pagination (echte <a>-Links) für den No-JS-Fall. * 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 function renderNoJsPagination(int $currentPage, int $totalPages, string $query, string $sort, string $category, int $limit): string
{ {
@@ -106,3 +103,69 @@ function renderNoJsPagination(int $currentPage, int $totalPages, string $query,
return $html; 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;
}
+5
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";
} }
@@ -68,6 +72,7 @@ if ($pfad === "deleteAccount") {
<script src="js/comments.js" defer></script> <script src="js/comments.js" defer></script>
<script src="js/editor.js" async></script> <script src="js/editor.js" async></script>
<script src="js/search-results.js" async></script> <script src="js/search-results.js" async></script>
<script src="js/showCategory.js" async></script>
<title>EduForge</title> <title>EduForge</title>
+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();
}
})();
@@ -22,9 +22,6 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
$results = $articleManager->search($search); $results = $articleManager->search($search);
// Sortierung/Filterung passiert nicht mehr hier, sondern zentral in
// content/search-results.php (No-JS) bzw. search-results-data.php (JS),
// damit ein Sortier-/Filterwechsel keine erneute Suche mehr braucht.
$safeArrayResults = []; $safeArrayResults = [];
foreach ($results as $obj) { foreach ($results as $obj) {
$safeArrayResults[] = [ $safeArrayResults[] = [
+1 -1
View File
@@ -2,7 +2,7 @@
/** /**
* Liefert den vollständigen, zuletzt gefundenen Ergebnis-Datensatz als JSON. * Liefert den vollständigen, zuletzt gefundenen Ergebnis-Datensatz als JSON.
* *
* Wird von js/results-app.js EINMALIG per fetch() geladen, damit Sortierung, * Wird von js/search-results.js EINMALIG per fetch() geladen, damit Sortierung,
* Filterung und Pagination danach komplett im Browser laufen können, ohne * Filterung und Pagination danach komplett im Browser laufen können, ohne
* weitere Serveranfragen oder eine erneute Datenbank-Suche auszulösen. * weitere Serveranfragen oder eine erneute Datenbank-Suche auszulösen.
*/ */
+23 -38
View File
@@ -2,66 +2,51 @@
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); 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/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"];
// 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 { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
$userManager = UserManager::getInstance(); $userManager = UserManager::getInstance();
$allArticles = $articleManager->getArticlesByCategory($category); $allArticles = $articleManager->getArticlesByCategory($category);
$safeArrayResults = [];
foreach ($allArticles as $article) { foreach ($allArticles as $article) {
$article->setAuthor($userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"]); $authorName = $userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"];
} $safeArrayResults[] = [
$totalArticles = count($allArticles); "id" => $article->getID(),
"title" => $article->getTitle(),
// --- SORTIERUNG LOGIK --- "content" => $article->getContent(),
if ($sortStyle === 'alphabet') { "author" => $authorName,
usort($allArticles, function($a, $b) { "category" => $article->getCategory(),
return strcasecmp($a->getTitle(), $b->getTitle()); "tags" => $article->getTags(),
}); "creationDate" => $article->getCreationDate(),
} elseif ($sortStyle === 'likes') { "likes" => $article->getLikes(),
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
});
} }
// Paginierung konfigurieren // Session dient hier (wie bei search-results) als Zwischenspeicher, damit
$limit = 10; // js/showCategory.js den kompletten Datensatz per showCategory-data.php
$totalPages = ceil($totalArticles / $limit); // nachladen kann, ohne die Kategorie-Abfrage ein zweites Mal auszuführen.
if ($page > $totalPages && $totalPages > 0) { $page = $totalPages; } $_SESSION['category_results'] = $safeArrayResults;
$_SESSION['category_name'] = $category;
$offset = ($page - 1) * $limit;
$articles = array_slice($allArticles, $offset, $limit);
} 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),
]);