Compare commits

..

1 Commits

Author SHA1 Message Date
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
8 changed files with 485 additions and 530 deletions
+93 -113
View File
@@ -2,46 +2,33 @@
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"] ?? "";
// --------------------------------------------------------------- // Sortierung / Kategorie / Limit: GET-Parameter haben Vorrang, sonst
// Parameter aus der URL lesen & validieren // Rückfall auf die zuletzt in der Session gemerkten Werte. So bleibt
// --------------------------------------------------------------- // z.B. ein reiner Pagination-Link (?page=2) bei der aktuellen
$allowedCategories = [ // Sortierung/Filterung.
'all', 'Deutsch', 'Englisch', 'Franzoesisch', 'Latein', 'Literatur', $currentSort = $_GET['sort'] ?? ($_SESSION['search_sort'] ?? 'alphabet');
'Mathematik', 'Biologie', 'Informatik', 'Chemie', 'Physik', 'Astronomie', if (!in_array($currentSort, ['alphabet', 'likes', 'newest', 'oldest'])) {
'Geschichte', 'Erdkunde', 'Sozialkunde', 'Wirtschaftskunde', 'Religion', $currentSort = 'alphabet';
'Ethikunterricht', 'Philosophie', 'Psychologie', 'Kunst', 'Musik',
'Theater', 'Technik', 'Werken', 'Hauswirtschaft', 'Sport',
];
$category = $_GET['category'] ?? 'all';
if (!in_array($category, $allowedCategories, true)) {
$category = 'all';
} }
$_SESSION['search_sort'] = $currentSort;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10; $currentCategory = strtolower($_GET['category'] ?? ($_SESSION['search_category'] ?? 'all'));
if (!in_array($limit, [10, 20, 50, 100], true)) { $_SESSION['search_category'] = $currentCategory;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : ($_SESSION['search_limit'] ?? 10);
if (!in_array($limit, [10, 20, 50, 100])) {
$limit = 10; $limit = 10;
} }
$_SESSION['search_limit'] = $limit;
$currentSort = $_SESSION['search_sort'] ?? 'alphabet'; // Serverseitig sortieren + filtern (gleiche Logik wie im JSON-Endpoint für JS-Nutzer)
$filteredResults = sortSearchResults($rawResults, $currentSort);
// --------------------------------------------------------------- $filteredResults = filterSearchResultsByCategory($filteredResults, $currentCategory);
// Diese Berechnung ist der Server-seitige Fallback für Nutzer ohne
// JavaScript. Sobald JS aktiv ist, übernimmt js/search-results.js
// Filterung, Sortierung und Pagination komplett im Browser (auf
// Basis des unten eingebetteten JSON-Datensatzes) und überschreibt
// diese Ausgabe live, ohne dass eine neue Seite geladen wird.
// ---------------------------------------------------------------
$filteredResults = $all_results;
if ($category !== 'all') {
$filteredResults = array_values(array_filter($filteredResults, function ($item) use ($category) {
return ($item['category'] ?? '') === $category;
}));
}
$totalResultsCount = count($filteredResults); $totalResultsCount = count($filteredResults);
$totalPages = max(1, (int)ceil($totalResultsCount / $limit)); $totalPages = max(1, (int)ceil($totalResultsCount / $limit));
@@ -54,64 +41,55 @@ if ($currentPage < 1) {
} }
$offset = ($currentPage - 1) * $limit; $offset = ($currentPage - 1) * $limit;
// Dies ist die No-JS-Basisversion: nur die aktuelle Seite wird gerendert.
// Bei aktiviertem JS lädt js/results-app.js zusätzlich den kompletten
// Datensatz nach (ein einziger Request) und übernimmt Sortierung, Filter
// und Pagination danach komplett im Browser, ohne weitere Serveranfragen.
$results = array_slice($filteredResults, $offset, $limit); $results = array_slice($filteredResults, $offset, $limit);
$resultCount = count($results); $resultCount = count($results);
// Hilfsfunktion, um Pagination-Links zu bauen, die alle aktuellen // Kategorien für das Auswahlfeld (Wert => Label)
// Parameter (q, sort, limit, category) beibehalten. $categories = [
function buildPageUrl(int $page, string $query, string $sort, int $limit, string $category): string 'Deutsch' => 'Deutsch', 'Englisch' => 'Englisch', 'Franzoesisch' => 'Französisch',
{ 'Latein' => 'Latein', 'Literatur' => 'Literatur', 'Mathematik' => 'Mathematik',
$params = [ 'Biologie' => 'Biologie', 'Informatik' => 'Informatik', 'Chemie' => 'Chemie',
'pfad' => 'search-results', 'Physik' => 'Physik', 'Astronomie' => 'Astronomie', 'Geschichte' => 'Geschichte',
'q' => $query, 'Erdkunde' => 'Erdkunde', 'Sozialkunde' => 'Sozialkunde', 'Wirtschaftskunde' => 'Wirtschaftskunde',
'sort' => $sort, 'Religion' => 'Religion', 'Ethikunterricht' => 'Ethikunterricht', 'Philosophie' => 'Philosophie',
'limit' => $limit, 'Psychologie' => 'Psychologie', 'Kunst' => 'Kunst', 'Musik' => 'Musik', 'Theater' => 'Theater',
'category' => $category, 'Technik' => 'Technik', 'Werken' => 'Werken', 'Hauswirtschaft' => 'Hauswirtschaft', 'Sport' => 'Sport',
'page' => $page, ];
];
return 'index.php?' . http_build_query($params);
}
// ---------------------------------------------------------------
// JSON-Datensatz für die clientseitige Aufbereitung (Filter,
// Sortierung, Pagination) durch js/search-results.js. Enthält ALLE
// Treffer der Suche (unabhängig von Kategorie/Sortierung/Seite),
// damit im Browser ohne weitere Serveranfragen gearbeitet werden kann.
// ---------------------------------------------------------------
$jsonItems = array_map(function ($item) {
$likes = isset($item['likes']) && is_array($item['likes']) ? count($item['likes']) : ($item['likes'] ?? 0);
return [
'id' => $item['id'] ?? null,
'title' => $item['title'] ?? '',
'author' => $item['author'] ?? '',
'category' => $item['category'] ?? '',
'likes' => $likes,
'creationDate'=> $item['creationDate'] ?? '',
];
}, $all_results);
?> ?>
<noscript>
Bitte JavaScript aktivieren, um die volle Funktionalität (Live-Filterung ohne Neuladen) zu nutzen. Die Seite funktioniert auch ohne JavaScript Sortierung, Filterung und Seitenwechsel erfolgen dann über die Formular- bzw. Link-Auswahl.
</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/results-app.js vollständig client-seitig übernommen.
--> -->
<div class="s-res-layout-grid"> <div class="s-res-layout-grid">
<?php include_once "includes/alertMessages.php" ?> <?php include_once "includes/alertMessages.php" ?>
<!-- Links: Seitenleiste für Filter und Suche --> <!-- Links: Seitenleiste für Filter und Suche -->
<aside class="s-res-sidebar"> <aside class="s-res-sidebar">
<!-- Sortierfuntion Box und Such Box -->
<form action="php/controller/search-results-controller.php" method="GET" id="searchform-id" class="s-res-sidebar-form">
<input type="hidden" id="s-res-page-input" name="page" value="<?php echo (int)$currentPage; ?>">
<!-- 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">
<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..." <input type="search" id="site-search" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50" required>
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: results-app.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>
@@ -138,36 +116,33 @@ $jsonItems = array_map(function ($item) {
<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" name="category" 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;">
<?php foreach ($allowedCategories as $catValue): <option value="all" <?php echo $currentCategory === 'all' ? 'selected' : ''; ?>>Alle Kategorien</option>
$label = $catValue === 'all' ? 'Alle Kategorien' : ($catValue === 'Franzoesisch' ? 'Französisch' : $catValue); <?php foreach ($categories as $value => $label): ?>
?> <option value="<?php echo htmlspecialchars($value); ?>" <?php echo $currentCategory === strtolower($value) ? 'selected' : ''; ?>><?php echo htmlspecialchars($label); ?></option>
<option value="<?php echo htmlspecialchars($catValue); ?>" <?php echo $category === $catValue ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($label); ?>
</option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
<!-- Für Nutzer ohne JavaScript: expliziter Button, um Sortierung/Kategorie anzuwenden -->
<noscript> <noscript>
<button type="submit" class="nav__search-button">Filter anwenden</button> <button type="submit" class="nav__search-button">Filter anwenden</button>
</noscript> </noscript>
</form> </form>
</aside> </aside>
<main class="s-res-main-content"> <main class="s-res-main-content">
<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" data-query="<?php echo htmlspecialchars($query); ?>"> <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>
<?php echo $totalResultsCount; ?> Treffer für Ihre Suchanfrage "<?php echo htmlspecialchars($query); ?>"
</p>
</div> </div>
<!-- Ergebnisliste: serverseitig gerendert (No-JS-Fallback). Wird bei aktivem JS durch js/search-results.js live ersetzt. --> <!-- Ergebnisliste -->
<div class="s-res-list"> <div class="s-res-list">
<?php if (!empty($results)): ?> <?php if (!empty($results)): ?>
<?php foreach ($results as $item): <?php foreach ($results as $item):
$likesCount = isset($item['likes']) && is_array($item['likes']) ? count($item['likes']) : ($item['likes'] ?? 0); $likesCount = getLikeCount($item);
?> ?>
<div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>"> <div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>">
<div class="s-res-content"> <div class="s-res-content">
@@ -178,55 +153,60 @@ $jsonItems = array_map(function ($item) {
</h2> </h2>
<div class="s-res-meta-row"> <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> <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>
<span class="s-res-likes">
❤️ <?php echo $likesCount; ?>
</span>
</div> </div>
</div> </div>
<div class="s-res-arrow">&rarr;</div> <div class="s-res-arrow">&rarr;</div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php elseif (isset($_SESSION["search_query"]) && $_SESSION["search_query"] !== "" && $resultCount === 0): ?>
<p>Keine Beiträge zu diesem Suchbegriff gefunden.</p> <?php elseif (isset($_SESSION["message"]) && $_SESSION["message"] === "invalid_search_query"): ?>
<?php elseif (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_search_query"): ?>
<p>Unzulässige Suchanfrage</p> <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 unset($_SESSION["message"]); ?> <?php 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, bleibt aber optisch am Fuß der Liste. -->
<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="searchform-id" 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>
<!-- No-JS-Fallback: echte Links, die serverseitig neu paginieren --> <nav class="s-res-page-navigation" aria-label="Seitennavigation">
<div class="s-res-page-navigation"> <!-- No-JS-Fallback: echte, funktionierende Links -->
<?php if ($currentPage > 1): ?> <noscript>
<a class="s-res-page-btn" href="<?php echo htmlspecialchars(buildPageUrl($currentPage - 1, $query, $currentSort, $limit, $category)); ?>">&laquo;</a> <?php echo renderNoJsPagination($currentPage, $totalPages, $query, $currentSort, $currentCategory, $limit); ?>
<?php else: ?> </noscript>
<span class="s-res-page-btn" aria-disabled="true">&laquo;</span>
<?php endif; ?>
<span id="dynamic-page-numbers">Seite <?php echo $currentPage; ?> von <?php echo $totalPages; ?></span> <!-- JS-Version: wird per results-app.js befüllt/eingeblendet -->
<div id="js-page-navigation" style="display:none;">
<?php if ($currentPage < $totalPages): ?> <button type="button" class="s-res-page-btn" id="prev-page-btn">«</button>
<a class="s-res-page-btn" href="<?php echo htmlspecialchars(buildPageUrl($currentPage + 1, $query, $currentSort, $limit, $category)); ?>">&raquo;</a> <span id="dynamic-page-numbers"></span>
<?php else: ?> <button type="button" class="s-res-page-btn" id="next-page-btn">»</button>
<span class="s-res-page-btn" aria-disabled="true">&raquo;</span>
<?php endif; ?>
</div> </div>
</nav>
</div> </div>
</main> </main>
</div>
<!-- Vollständiger, ungefilterter Ergebnis-Datensatz für die clientseitige </div>
Filterung/Sortierung/Pagination (siehe js/search-results.js).
JSON_HEX_* verhindert das Ausbrechen aus dem <script>-Tag bzw. XSS. -->
<script type="application/json" id="s-res-data">
<?php echo json_encode($jsonItems, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>
</script>
+108
View File
@@ -0,0 +1,108 @@
<?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.
*/
/**
* Anzahl der Likes robust ermitteln (Array von Likes ODER bereits eine Zahl).
*/
function getLikeCount($item): int
{
if (isset($item['likes']) && is_array($item['likes'])) {
return count($item['likes']);
}
return (int)($item['likes'] ?? 0);
}
/**
* Sortiert ein Array von Ergebnis-Assoziativarrays (wie in $_SESSION["search_results"] abgelegt).
*/
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 ein Array von Ergebnissen nach Kategorie ('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 (echte <a>-Links) 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;
}
+1 -3
View File
@@ -65,13 +65,11 @@ if ($pfad === "deleteAccount") {
<link rel="stylesheet" href="css/message.css"> <link rel="stylesheet" href="css/message.css">
<link rel="stylesheet" href="css/showCategory.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/search-results.js" async></script>
<title>EduForge</title> <title>EduForge</title>
</head> </head>
+3 -3
View File
@@ -1,13 +1,13 @@
console.log("editor.js wurde erfolgreich geladen!"); console.log("Die JavaScript-Datei wurde erfolgreich geladen!");
function initEditor() { function initEditor() {
const form = document.getElementById("editor-form"); const form = document.getElementById("editor-form");
if (!form) { if (!form) {
console.error("editor.js wurde abgebrochen: Formular nicht gefunden!"); console.error("Skript abgebrochen: Formular nicht gefunden!");
return; return;
} else { } else {
console.log("Formular gefunden und editor.js initialisiert:", form); console.log("Formular gefunden und Editor initialisiert:", form);
} }
const container = document.getElementById("block-container"); const container = document.getElementById("block-container");
+20 -196
View File
@@ -1,203 +1,27 @@
/** function initPaginator() {
* Übernimmt Filterung (Kategorie), Sortierung und Pagination der const form = document.getElementById('search-form-id');
* Suchergebnisse vollständig clientseitig, ohne Neuladen der Seite. const pageInput = document.getElementById('s-res-page-input');
* const pageButtons = document.querySelectorAll('.s-res-page-navigation .s-res-page-btn');
* Voraussetzung: Im HTML liegt ein <script type="application/json" id="s-res-data">
* mit allen Treffern der aktuellen Suche (siehe content/search-results.php).
*
* Ist kein JavaScript aktiv, bleibt die serverseitig gerenderte Ausgabe
* (inkl. echter Pagination-Links und Formular-Submit) unverändert nutzbar.
*/
(function () {
const DEFAULT_PER_PAGE = 10;
const state = { pageButtons.forEach(button => {
all: [], button.addEventListener('click', function() {
filtered: [], if (this.disabled) return;
category: 'all',
sort: 'alphabet',
perPage: DEFAULT_PER_PAGE,
page: 1,
};
const els = {}; const targetPage = this.getAttribute('data-page');
function init() { if (targetPage && form && pageInput) {
const dataScript = document.getElementById('s-res-data'); pageInput.value = targetPage;
const listContainer = document.querySelector('.s-res-list'); form.submit();
if (!dataScript || !listContainer) {
return; // keine Ergebnisliste auf dieser Seite vorhanden
} }
try {
state.all = JSON.parse(dataScript.textContent);
} catch (e) {
console.error('Suchergebnis-Daten konnten nicht gelesen werden.', e);
return;
}
els.list = listContainer;
els.categorySelect = document.getElementById('category-filter');
els.perPageSelect = document.getElementById('s-res-per-page');
els.sortRadios = document.querySelectorAll('.sort-radio');
els.pagerContainer = document.querySelector('.s-res-page-navigation');
els.metaCount = document.querySelector('.s-res-meta');
// Ausgangszustand aus dem serverseitig gerenderten Markup übernehmen,
// damit Browser-zurück / geteilte Links konsistent bleiben.
if (els.categorySelect) state.category = els.categorySelect.value || 'all';
if (els.perPageSelect) state.perPage = parseInt(els.perPageSelect.value, 10) || DEFAULT_PER_PAGE;
const checkedSort = document.querySelector('.sort-radio:checked');
if (checkedSort) state.sort = checkedSort.value;
// Ab hier übernimmt JS: Formular-Steuerelemente lösen keinen
// Seiten-Reload mehr aus, sondern rendern direkt neu.
if (els.categorySelect) {
els.categorySelect.addEventListener('change', function () {
state.category = this.value;
state.page = 1;
render();
});
}
if (els.perPageSelect) {
els.perPageSelect.addEventListener('change', function () {
state.perPage = parseInt(this.value, 10) || DEFAULT_PER_PAGE;
state.page = 1;
render();
});
}
els.sortRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
state.sort = this.value;
state.page = 1;
render();
}); });
}); });
}
render(); // ist das DOM bereits vollständig aufgebaut?
} if (document.readyState === 'loading') {
// Falls noch geladen wird, auf das Event warten
function sortItems(items, sortValue) { document.addEventListener('DOMContentLoaded', initPaginator);
const copy = items.slice(); } else {
switch (sortValue) { // Falls das HTML bereits komplett da ist, sofort ausführen
case 'likes': initPaginator();
copy.sort(function (a, b) { return (b.likes || 0) - (a.likes || 0); }); }
break;
case 'newest':
copy.sort(function (a, b) { return (b.creationDate || '').localeCompare(a.creationDate || ''); });
break;
case 'oldest':
copy.sort(function (a, b) { return (a.creationDate || '').localeCompare(b.creationDate || ''); });
break;
case 'alphabet':
default:
copy.sort(function (a, b) { return a.title.localeCompare(b.title, 'de', { sensitivity: 'base' }); });
break;
}
return copy;
}
function render() {
let items = state.all;
if (state.category && state.category !== 'all') {
items = items.filter(function (it) {
return (it.category || '').toLowerCase() === state.category.toLowerCase();
});
}
state.filtered = sortItems(items, state.sort);
const totalResults = state.filtered.length;
const totalPages = Math.max(1, Math.ceil(totalResults / state.perPage));
if (state.page > totalPages) state.page = totalPages;
if (state.page < 1) state.page = 1;
const start = (state.page - 1) * state.perPage;
const pageItems = state.filtered.slice(start, start + state.perPage);
renderList(pageItems);
renderPager(totalPages);
renderMeta(totalResults);
}
function renderList(items) {
els.list.innerHTML = '';
if (items.length === 0) {
const p = document.createElement('p');
p.textContent = 'Keine Beiträge zu diesem Suchbegriff gefunden.';
els.list.appendChild(p);
return;
}
items.forEach(function (item) {
const div = document.createElement('div');
div.className = 's-res-item';
div.setAttribute('data-likes', item.likes || 0);
div.setAttribute('data-category', (item.category || '').toLowerCase());
div.innerHTML =
'<div class="s-res-content">' +
'<h2 class="s-res-item-title">' +
'<a class="s-res-link"></a>' +
'</h2>' +
'<div class="s-res-meta-row">' +
'<p class="s-res-author">Von: <span class="s-res-author-name"></span></p>' +
'<span class="s-res-likes"></span>' +
'</div>' +
'</div>' +
'<div class="s-res-arrow">&rarr;</div>';
const link = div.querySelector('.s-res-link');
link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id);
link.textContent = item.title;
div.querySelector('.s-res-author-name').textContent = item.author;
div.querySelector('.s-res-likes').textContent = item.likes || 0;
els.list.appendChild(div);
});
}
function renderPager(totalPages) {
if (!els.pagerContainer) return;
els.pagerContainer.innerHTML = '';
const prevBtn = document.createElement('button');
prevBtn.type = 'button';
prevBtn.className = 's-res-page-btn';
prevBtn.textContent = '\u00AB';
prevBtn.disabled = state.page <= 1;
prevBtn.addEventListener('click', function () { goToPage(state.page - 1); });
els.pagerContainer.appendChild(prevBtn);
const span = document.createElement('span');
span.id = 'dynamic-page-numbers';
span.textContent = 'Seite ' + state.page + ' von ' + totalPages;
els.pagerContainer.appendChild(span);
const nextBtn = document.createElement('button');
nextBtn.type = 'button';
nextBtn.className = 's-res-page-btn';
nextBtn.textContent = '\u00BB';
nextBtn.disabled = state.page >= totalPages;
nextBtn.addEventListener('click', function () { goToPage(state.page + 1); });
els.pagerContainer.appendChild(nextBtn);
}
function renderMeta(totalResults) {
if (!els.metaCount) return;
const query = els.metaCount.getAttribute('data-query') || '';
els.metaCount.textContent = totalResults + ' Treffer für Ihre Suchanfrage "' + query + '"';
}
function goToPage(page) {
state.page = page;
render();
els.list.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
+193 -158
View File
@@ -1,198 +1,233 @@
/** /**
* Übernimmt Filterung (Kategorie), Sortierung und Pagination der * Übernimmt Sortierung, Kategorie-Filter und Pagination der Suchergebnisse
* Suchergebnisse vollständig clientseitig, ohne Neuladen der Seite. * vollständig im Browser (kein Reload, keine erneute Suche).
* *
* Voraussetzung: Im HTML liegt ein <script type="application/json" id="s-res-data"> * Ablauf:
* mit allen Treffern der aktuellen Suche (siehe content/search-results.php). * 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 + paginieren rein im Speicher (JS-Array).
* 3. Ergebnisliste + Pagination-UI daraus neu rendern.
* *
* Ist kein JavaScript aktiv, bleibt die serverseitig gerenderte Ausgabe * Ersetzt sorter.js, filter.js und paginator.js, deren Klick-Handler sich
* (inkl. echter Pagination-Links und Formular-Submit) unverändert nutzbar. * gegenseitig ins Gehege kamen (Formular-Submit vs. clientseitiges Hiden).
* Ohne JS bleibt die serverseitig gerenderte Seite (echte Links/Formulare)
* voll funktionsfähig.
*/ */
(function () { (function () {
const DEFAULT_PER_PAGE = 10;
const state = { const state = {
all: [], allItems: [],
filtered: [],
category: 'all',
sort: 'alphabet', sort: 'alphabet',
perPage: DEFAULT_PER_PAGE, category: 'all',
page: 1, itemsPerPage: 10,
currentPage: 1,
}; };
const els = {}; let listContainer, sortRadios, categorySelect, limitSelect;
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
function init() { function init() {
const dataScript = document.getElementById('s-res-data'); listContainer = document.querySelector('.s-res-list');
const listContainer = document.querySelector('.s-res-list'); if (!listContainer) return;
if (!dataScript || !listContainer) {
return; // keine Ergebnisliste auf dieser Seite vorhanden
}
try { sortRadios = document.querySelectorAll('.sort-radio');
state.all = JSON.parse(dataScript.textContent); categorySelect = document.getElementById('category-filter');
} catch (e) { limitSelect = document.getElementById('s-res-per-page');
console.error('Suchergebnis-Daten konnten nicht gelesen werden.', e); prevBtn = document.getElementById('prev-page-btn');
return; 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');
els.list = listContainer; const checkedRadio = document.querySelector('.sort-radio:checked');
els.categorySelect = document.getElementById('category-filter'); state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
els.perPageSelect = document.getElementById('s-res-per-page'); state.category = categorySelect ? categorySelect.value : 'all';
els.sortRadios = document.querySelectorAll('.sort-radio'); state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
els.pagerContainer = document.querySelector('.s-res-page-navigation');
els.metaCount = document.querySelector('.s-res-meta');
// Ausgangszustand aus dem serverseitig gerenderten Markup übernehmen, fetchFullDataset()
// damit Browser-zurück / geteilte Links konsistent bleiben. .then(function (data) {
if (els.categorySelect) state.category = els.categorySelect.value || 'all'; state.allItems = data.results || [];
if (els.perPageSelect) state.perPage = parseInt(els.perPageSelect.value, 10) || DEFAULT_PER_PAGE; attachEvents();
const checkedSort = document.querySelector('.sort-radio:checked');
if (checkedSort) state.sort = checkedSort.value;
// Ab hier übernimmt JS: Formular-Steuerelemente lösen keinen
// Seiten-Reload mehr aus, sondern rendern direkt neu.
if (els.categorySelect) {
els.categorySelect.addEventListener('change', function () {
state.category = this.value;
state.page = 1;
render(); 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);
}); });
} }
if (els.perPageSelect) {
els.perPageSelect.addEventListener('change', function () {
state.perPage = parseInt(this.value, 10) || DEFAULT_PER_PAGE;
state.page = 1;
render();
});
}
els.sortRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
state.sort = this.value;
state.page = 1;
render();
});
});
render(); 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 sortItems(items, sortValue) { function getFilteredSortedItems() {
const copy = items.slice(); let items = state.allItems;
switch (sortValue) {
case 'likes': if (state.category !== 'all') {
copy.sort(function (a, b) { return (b.likes || 0) - (a.likes || 0); }); const wanted = state.category.toLowerCase();
break; items = items.filter(function (item) {
case 'newest': return (item.category || '').toLowerCase() === wanted;
copy.sort(function (a, b) { return (b.creationDate || '').localeCompare(a.creationDate || ''); }); });
break;
case 'oldest':
copy.sort(function (a, b) { return (a.creationDate || '').localeCompare(b.creationDate || ''); });
break;
case 'alphabet':
default:
copy.sort(function (a, b) { return a.title.localeCompare(b.title, 'de', { sensitivity: 'base' }); });
break;
} }
return copy;
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() { function render() {
let items = state.all; const items = getFilteredSortedItems();
if (state.category && state.category !== 'all') {
items = items.filter(function (it) { if (resultCountEl) resultCountEl.textContent = items.length;
return (it.category || '').toLowerCase() === state.category.toLowerCase();
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));
}); });
} }
state.filtered = sortItems(items, state.sort);
const totalResults = state.filtered.length; updatePaginatorUI(state.currentPage, totalPages);
const totalPages = Math.max(1, Math.ceil(totalResults / state.perPage));
if (state.page > totalPages) state.page = totalPages;
if (state.page < 1) state.page = 1;
const start = (state.page - 1) * state.perPage;
const pageItems = state.filtered.slice(start, start + state.perPage);
renderList(pageItems);
renderPager(totalPages);
renderMeta(totalResults);
} }
function renderList(items) { function buildCard(item) {
els.list.innerHTML = ''; const card = document.createElement('div');
card.className = 's-res-item';
card.setAttribute('data-likes', item.likes);
card.setAttribute('data-category', (item.category || '').toLowerCase());
if (items.length === 0) { const link = document.createElement('a');
const p = document.createElement('p'); link.className = 's-res-link';
p.textContent = 'Keine Beiträge zu diesem Suchbegriff gefunden.';
els.list.appendChild(p);
return;
}
items.forEach(function (item) {
const div = document.createElement('div');
div.className = 's-res-item';
div.setAttribute('data-likes', item.likes || 0);
div.setAttribute('data-category', (item.category || '').toLowerCase());
div.innerHTML =
'<div class="s-res-content">' +
'<h2 class="s-res-item-title">' +
'<a class="s-res-link"></a>' +
'</h2>' +
'<div class="s-res-meta-row">' +
'<p class="s-res-author">Von: <span class="s-res-author-name"></span></p>' +
'<span class="s-res-likes"></span>' +
'</div>' +
'</div>' +
'<div class="s-res-arrow">&rarr;</div>';
const link = div.querySelector('.s-res-link');
link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id); link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id);
link.textContent = item.title; link.textContent = item.title;
div.querySelector('.s-res-author-name').textContent = item.author;
div.querySelector('.s-res-likes').textContent = item.likes || 0;
els.list.appendChild(div); 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();
}); });
} }
function renderPager(totalPages) { if (limitSelect) {
if (!els.pagerContainer) return; limitSelect.addEventListener('change', function () {
els.pagerContainer.innerHTML = ''; state.itemsPerPage = parseInt(this.value, 10) || 10;
state.currentPage = 1;
const prevBtn = document.createElement('button');
prevBtn.type = 'button';
prevBtn.className = 's-res-page-btn';
prevBtn.textContent = '\u00AB';
prevBtn.disabled = state.page <= 1;
prevBtn.addEventListener('click', function () { goToPage(state.page - 1); });
els.pagerContainer.appendChild(prevBtn);
const span = document.createElement('span');
span.id = 'dynamic-page-numbers';
span.textContent = 'Seite ' + state.page + ' von ' + totalPages;
els.pagerContainer.appendChild(span);
const nextBtn = document.createElement('button');
nextBtn.type = 'button';
nextBtn.className = 's-res-page-btn';
nextBtn.textContent = '\u00BB';
nextBtn.disabled = state.page >= totalPages;
nextBtn.addEventListener('click', function () { goToPage(state.page + 1); });
els.pagerContainer.appendChild(nextBtn);
}
function renderMeta(totalResults) {
if (!els.metaCount) return;
const query = els.metaCount.getAttribute('data-query') || '';
els.metaCount.textContent = totalResults + ' Treffer für Ihre Suchanfrage "' + query + '"';
}
function goToPage(page) {
state.page = page;
render(); render();
els.list.scrollIntoView({ behavior: 'smooth', block: 'start' }); });
}
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 Filterformulars (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') { if (document.readyState === 'loading') {
+16 -38
View File
@@ -8,43 +8,23 @@ require_once '../model/Article.php';
require_once '../validator/search-validator.php'; require_once '../validator/search-validator.php';
if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) { if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
$search = trim($_GET["q"]);
$search = trim($_GET["q"]);
if (!searchQueryValidator($search)) { if (!searchQueryValidator($search)) {
$_SESSION["search_results"] = []; $_SESSION["search_results"] = [];
$_SESSION["search_query"] = ""; $_SESSION["search_query"] = "";
$_SESSION["message"] = "invalid_search_query"; $_SESSION["message"] = "invalid_search_query";
} else { } else {
try { try {
$articleManager = ArticleManager::getInstance(); $articleManager = ArticleManager::getInstance();
$userManager = UserManager::getInstance(); $userManager = UserManager::getInstance();
$results = $articleManager->search($search); $results = $articleManager->search($search);
$sortStyle = $_GET['sort'] ?? 'alphabet'; // Sortierung/Filterung passiert nicht mehr hier, sondern zentral in
$_SESSION['search_sort'] = $sortStyle; // content/search-results.php (No-JS) bzw. search-results-data.php (JS),
// damit ein Sortier-/Filterwechsel keine erneute Suche mehr braucht.
if ($sortStyle === 'alphabet') {
// Titel aufsteigend alphabetisch 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 sonst incomplete-PHP-Fehler im Ergebnis
$safeArrayResults = []; $safeArrayResults = [];
foreach ($results as $obj) { foreach ($results as $obj) {
$safeArrayResults[] = [ $safeArrayResults[] = [
@@ -62,28 +42,26 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
$_SESSION["search_results"] = $safeArrayResults; $_SESSION["search_results"] = $safeArrayResults;
$_SESSION["search_query"] = $search; $_SESSION["search_query"] = $search;
$_SESSION["message"] = "new_search_results"; $_SESSION["message"] = "new_search_results";
} catch (Exception $e) { } catch (Exception $e) {
$_SESSION["message"] = "internal_error"; $_SESSION["message"] = "internal_error";
} }
} }
$sort = $_GET['sort'] ?? 'alphabet'; $sort = $_GET['sort'] ?? 'alphabet';
$_SESSION['search_sort'] = $sort;
$category = $_GET['category'] ?? 'all';
$_SESSION['search_category'] = $category;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10; $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10;
if (!searchLimitValidator($limit)) { if (!searchLimitValidator($limit)) {
$limit = 10; $limit = 10;
} }
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1; $_SESSION['search_limit'] = $limit;
// Kategorie-Filter durchreichen (Whitelist-Prüfung erfolgt in der View) // Neue Suche -> immer wieder bei Seite 1 beginnen
$category = isset($_GET['category']) ? $_GET['category'] : 'all'; header("Location: ../../index.php?pfad=search-results&q=" . urlencode($search) . "&sort=" . urlencode($sort) . "&category=" . urlencode($category) . "&limit=" . $limit . "&page=1");
header(
"Location: ../../index.php?pfad=search-results&q=" . urlencode($search) .
"&sort=" . urlencode($sort) .
"&limit=" . $limit .
"&category=" . urlencode($category) .
"&page=" . $page
);
exit(); exit();
} }
?>
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* Liefert den vollständigen, zuletzt gefundenen Ergebnis-Datensatz als JSON.
*
* Wird von js/results-app.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),
]);