diff --git a/content/search-results.php b/content/search-results.php index c4793b5..5cd0a40 100644 --- a/content/search-results.php +++ b/content/search-results.php @@ -2,20 +2,37 @@ if (session_status() === PHP_SESSION_NONE) { session_start(); } +require_once __DIR__ . '/../includes/resultsHelper.php'; -$all_results = $_SESSION["search_results"] ?? []; +$rawResults = $_SESSION["search_results"] ?? []; $query = $_SESSION["search_query"] ?? ""; -$totalResultsCount = count($all_results); -$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10; +// Sortierung / Kategorie / Limit: GET-Parameter haben Vorrang, sonst +// Rückfall auf die zuletzt in der Session gemerkten Werte. So bleibt +// z.B. ein reiner Pagination-Link (?page=2) bei der aktuellen +// Sortierung/Filterung. +$currentSort = $_GET['sort'] ?? ($_SESSION['search_sort'] ?? 'alphabet'); +if (!in_array($currentSort, ['alphabet', 'likes', 'newest', 'oldest'])) { + $currentSort = 'alphabet'; +} +$_SESSION['search_sort'] = $currentSort; + +$currentCategory = strtolower($_GET['category'] ?? ($_SESSION['search_category'] ?? 'all')); +$_SESSION['search_category'] = $currentCategory; + +$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : ($_SESSION['search_limit'] ?? 10); if (!in_array($limit, [10, 20, 50, 100])) { $limit = 10; } +$_SESSION['search_limit'] = $limit; -// Gesamtseitenzahl -$totalPages = max(1, ceil($totalResultsCount / $limit)); +// Serverseitig sortieren + filtern (gleiche Logik wie im JSON-Endpoint für JS-Nutzer) +$filteredResults = sortSearchResults($rawResults, $currentSort); +$filteredResults = filterSearchResultsByCategory($filteredResults, $currentCategory); + +$totalResultsCount = count($filteredResults); +$totalPages = max(1, (int)ceil($totalResultsCount / $limit)); -// Aktuelle Seite auslesen und validieren $currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1; if ($currentPage < 1) { $currentPage = 1; @@ -23,42 +40,59 @@ if ($currentPage < 1) { $currentPage = $totalPages; } -// Startpunkt im Array berechnen (Offset) $offset = ($currentPage - 1) * $limit; -// Nur die Ergebnisse für die aktuelle Seite ausschneiden -//$results = array_slice($all_results, $offset, $limit); -$results = $all_results; +// Dies ist die No-JS-Basisversion: nur die aktuelle Seite wird gerendert. +// Bei aktiviertem JS lädt js/results-app.js zusätzlich den kompletten +// Datensatz nach (ein einziger Request) und übernimmt Sortierung, Filter +// und Pagination danach komplett im Browser, ohne weitere Serveranfragen. +$results = array_slice($filteredResults, $offset, $limit); $resultCount = count($results); +// Kategorien für das Auswahlfeld (Wert => Label) +$categories = [ + 'Deutsch' => 'Deutsch', 'Englisch' => 'Englisch', 'Franzoesisch' => 'Französisch', + 'Latein' => 'Latein', 'Literatur' => 'Literatur', 'Mathematik' => 'Mathematik', + 'Biologie' => 'Biologie', 'Informatik' => 'Informatik', 'Chemie' => 'Chemie', + 'Physik' => 'Physik', 'Astronomie' => 'Astronomie', 'Geschichte' => 'Geschichte', + 'Erdkunde' => 'Erdkunde', 'Sozialkunde' => 'Sozialkunde', 'Wirtschaftskunde' => 'Wirtschaftskunde', + 'Religion' => 'Religion', 'Ethikunterricht' => 'Ethikunterricht', 'Philosophie' => 'Philosophie', + 'Psychologie' => 'Psychologie', 'Kunst' => 'Kunst', 'Musik' => 'Musik', 'Theater' => 'Theater', + 'Technik' => 'Technik', 'Werken' => 'Werken', 'Hauswirtschaft' => 'Hauswirtschaft', 'Sport' => 'Sport', +]; ?> -
- + @@ -120,18 +134,16 @@ $resultCount = count($results);

Suchergebnisse

-

Treffer für Ihre Suchanfrage ""

+

Treffer für Ihre Suchanfrage ""

- - - + + +

@@ -151,41 +163,50 @@ $resultCount = count($results);

- -

Keine Beiträge zu diesem Suchbegriff gefunden.

- +

Unzulässige Suchanfrage

- + + 0): ?> +

Keine Beiträge in dieser Kategorie gefunden.

+ + +

Keine Beiträge zu diesem Suchbegriff gefunden.

+ - +
+ -
+
\ No newline at end of file diff --git a/includes/resultsHelper.php b/includes/resultsHelper.php new file mode 100644 index 0000000..c9dadb1 --- /dev/null +++ b/includes/resultsHelper.php @@ -0,0 +1,108 @@ + 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 -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 .= '« '; + } else { + $html .= '« '; + } + + for ($i = 1; $i <= $totalPages; $i++) { + if ($i === $currentPage) { + $html .= '' . $i . ' '; + } else { + $html .= '' . $i . ' '; + } + } + + if ($currentPage < $totalPages) { + $html .= '»'; + } else { + $html .= '»'; + } + + return $html; +} \ No newline at end of file diff --git a/index.php b/index.php index c9a5cfb..3efa72a 100644 --- a/index.php +++ b/index.php @@ -65,11 +65,10 @@ if ($pfad === "deleteAccount") { - - - + + EduForge diff --git a/js/search-results.js b/js/search-results.js new file mode 100644 index 0000000..e085110 --- /dev/null +++ b/js/search-results.js @@ -0,0 +1,238 @@ +/** + * Übernimmt Sortierung, Kategorie-Filter und Pagination der Suchergebnisse + * vollständig im Browser (kein Reload, keine erneute Suche). + * + * 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 + paginieren rein im Speicher (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 (Formular-Submit vs. clientseitiges Hiden). + * 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 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') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); \ No newline at end of file diff --git a/php/controller/search-results-controller.php b/php/controller/search-results-controller.php index 5e6d125..91237ba 100644 --- a/php/controller/search-results-controller.php +++ b/php/controller/search-results-controller.php @@ -16,37 +16,15 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) { $_SESSION["message"] = "invalid_search_query"; } else { try { - + $articleManager = ArticleManager::getInstance(); $userManager = UserManager::getInstance(); - + $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 + // 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 = []; foreach ($results as $obj) { $safeArrayResults[] = [ @@ -60,25 +38,30 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) { "likes" => $obj->getLikes(), ]; } - + $_SESSION["search_results"] = $safeArrayResults; $_SESSION["search_query"] = $search; $_SESSION["message"] = "new_search_results"; - } catch (Exception $e){ + } catch (Exception $e) { $_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; if (!searchLimitValidator($limit)) { $limit = 10; } - $page = isset($_GET['page']) ? (int)$_GET['page'] : 1; - header("Location: ../../index.php?pfad=search-results&q=" . urlencode($search) . "&sort=" . urlencode($sort) . "&limit=" . $limit . "&page=" . $page); - exit(); - -} + $_SESSION['search_limit'] = $limit; -?> \ No newline at end of file + // 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(); + +} \ No newline at end of file diff --git a/php/controller/search-results-data.php b/php/controller/search-results-data.php new file mode 100644 index 0000000..36b492d --- /dev/null +++ b/php/controller/search-results-data.php @@ -0,0 +1,32 @@ + $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), +]); \ No newline at end of file