diff --git a/content/showCategory.php b/content/showCategory.php index 1475b8a..2f8a4a9 100644 --- a/content/showCategory.php +++ b/content/showCategory.php @@ -2,31 +2,32 @@ if (session_status() === PHP_SESSION_NONE) { session_start(); } +include_once "php/controller/showCategory-controller.php"; require_once __DIR__ . '/../includes/resultsHelper.php'; -$rawResults = $_SESSION["search_results"] ?? []; +$rawResults = $_SESSION['category_results'] ?? []; +$category = $_SESSION['category_name'] ?? ($_GET['category'] ?? ''); -// 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'); +// 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['search_sort'] = $currentSort; +$_SESSION['cat_sort'] = $currentSort; -$currentCategory = strtolower($_GET['category'] ?? ($_SESSION['search_category'] ?? 'all')); -$_SESSION['search_category'] = $currentCategory; +$query = trim($_GET['q'] ?? ($_SESSION['cat_query'] ?? '')); +$_SESSION['cat_query'] = $query; -$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : ($_SESSION['search_limit'] ?? 10); +$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : ($_SESSION['cat_limit'] ?? 10); if (!in_array($limit, [10, 20, 50, 100])) { $limit = 10; } -$_SESSION['search_limit'] = $limit; +$_SESSION['cat_limit'] = $limit; -// Serverseitig sortieren +// 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)); @@ -41,31 +42,38 @@ if ($currentPage < 1) { $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. +// 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); ?>
Unzulässige Suchanfrage
+ +Keine Beiträge in dieser Kategorie zu diesem Suchbegriff gefunden.
- 0): ?> +Keine Beiträge in dieser Kategorie gefunden.
+ @@ -141,26 +152,25 @@ $resultCount = count($results); + \ No newline at end of file diff --git a/includes/resultsHelper.php b/includes/resultsHelper.php index 8d5db6b..3926cb2 100644 --- a/includes/resultsHelper.php +++ b/includes/resultsHelper.php @@ -101,5 +101,71 @@ function renderNoJsPagination(int $currentPage, int $totalPages, string $query, $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 .= '« '; + } 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/js/showCategory.js b/js/showCategory.js new file mode 100644 index 0000000..08b07ff --- /dev/null +++ b/js/showCategory.js @@ -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(); + } +})(); \ No newline at end of file diff --git a/php/controller/showCategory-controller.php b/php/controller/showCategory-controller.php index 8414723..72f20f4 100644 --- a/php/controller/showCategory-controller.php +++ b/php/controller/showCategory-controller.php @@ -2,40 +2,42 @@ if (session_status() === PHP_SESSION_NONE) { session_start(); } - require_once 'php/model/Article.php'; require_once 'php/model/ArticleManager.php'; require_once 'php/model/UserManager.php'; require_once 'php/validator/article-validator.php'; +require_once __DIR__ . '/../../includes/resultsHelper.php'; -if (isset($_GET["category"]) && !empty($_GET["category"]) && articleCategoryValidator($_GET["category"])){ - $category = $_GET['category'] ?? 'all'; +if (isset($_GET["category"]) && !empty($_GET["category"]) && articleCategoryValidator($_GET["category"])) { - // Sortierung auslesen (Standard: alphabet) - $sortStyle = isset($_GET['sort']) ? trim($_GET['sort']) : 'alphabet'; + $category = $_GET["category"]; - try{ + try { $articleManager = ArticleManager::getInstance(); $userManager = UserManager::getInstance(); - $results = $articleManager->getArticlesByCategory($category); + $allArticles = $articleManager->getArticlesByCategory($category); $safeArrayResults = []; - foreach ($results as $obj) { + foreach ($allArticles as $article) { + $authorName = $userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"]; $safeArrayResults[] = [ - "id" => $obj->getId(), - "title" => $obj->getTitle(), - "content" => $obj->getContent(), - "author" => $userManager->findUser($obj->getAuthor())["vorname"] . " " . $userManager->findUser($obj->getAuthor())["nachname"], - "category" => $obj->getCategory(), - "tags" => $obj->getTags(), - "creationDate" => $obj->getCreationDate(), - "likes" => $obj->getLikes(), + "id" => $article->getID(), + "title" => $article->getTitle(), + "content" => $article->getContent(), + "author" => $authorName, + "category" => $article->getCategory(), + "tags" => $article->getTags(), + "creationDate" => $article->getCreationDate(), + "likes" => $article->getLikes(), ]; } - $_SESSION["search_results"] = $safeArrayResults; - $_SESSION["message"] = "new_search_results"; + // Session dient hier (wie bei search-results) als Zwischenspeicher, damit + // js/showCategory.js den kompletten Datensatz per showCategory-data.php + // nachladen kann, ohne die Kategorie-Abfrage ein zweites Mal auszuführen. + $_SESSION['category_results'] = $safeArrayResults; + $_SESSION['category_name'] = $category; } catch (Exception $e) { $_SESSION["message"] = "internal_error"; @@ -43,24 +45,8 @@ if (isset($_GET["category"]) && !empty($_GET["category"]) && articleCategoryVali exit(); } - $sort = $_GET['sort'] ?? 'alphabet'; - $_SESSION['search_sort'] = $sort; - - $_SESSION['search_category'] = $category; - - $limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10; - if (!searchLimitValidator($limit)) { - $limit = 10; - } - $_SESSION['search_limit'] = $limit; - - // Neue Suche -> immer wieder bei Seite 1 beginnen - header("Location: ../../index.php?pfad=showCategory&category=" . urlencode($category) . "&sort=" . urlencode($sort) . "&limit=" . $limit . "&page=1"); - exit(); } else { $_SESSION["message"] = "invalid_category"; include_once "content/404.php"; exit(); -} - -?> \ No newline at end of file +} \ No newline at end of file diff --git a/php/controller/showCategory-data.php b/php/controller/showCategory-data.php new file mode 100644 index 0000000..bdf66f9 --- /dev/null +++ b/php/controller/showCategory-data.php @@ -0,0 +1,35 @@ + $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), +]); \ No newline at end of file