diff --git a/content/search-results.php b/content/search-results.php index c4793b5..d9a6399 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 +$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/content/showCategory.php b/content/showCategory.php index 7b83df5..2f8a4a9 100644 --- a/content/showCategory.php +++ b/content/showCategory.php @@ -1,111 +1,184 @@ $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); ?> + +
+ - + + -
-

- von getAuthor())) ? htmlspecialchars($article->getAuthor()) : 'Anonym'; ?> -

- - ❤️ getLikes())) ? htmlspecialchars($article->getLikes()) : '0'; ?> - -
- +
+ +
+

Kategorie:

+

+ Treffer in dieser Kategorie +

+
+ + +
+ + + +
+
+

+ + + +

+
+

Von:

+ + + ❤️ +
-
- - -

Keine Artikel in dieser Kategorie gefunden.

- +
+
+ + + +

Keine Beiträge in dieser Kategorie zu diesem Suchbegriff gefunden.

+ + +

Keine Beiträge in dieser Kategorie gefunden.

+ + + + +
+ +
+ -
- + + + + \ No newline at end of file diff --git a/includes/resultsHelper.php b/includes/resultsHelper.php new file mode 100644 index 0000000..3926cb2 --- /dev/null +++ b/includes/resultsHelper.php @@ -0,0 +1,171 @@ + getLikeCount($a); + }); + break; + case 'newest': + usort($results, function ($a, $b) { + return strcmp($b['creationDate'] ?? '', $a['creationDate'] ?? ''); + }); + break; + case 'oldest': + usort($results, function ($a, $b) { + return strcmp($a['creationDate'] ?? '', $b['creationDate'] ?? ''); + }); + break; + case 'alphabet': + default: + usort($results, function ($a, $b) { + return strcasecmp($a['title'] ?? '', $b['title'] ?? ''); + }); + break; + } + return $results; +} + +/** + * Filtert die Ergebnisse ('all' = kein Filter). + */ +function filterSearchResultsByCategory(array $results, string $category): array +{ + $category = strtolower(trim($category)); + if ($category === '' || $category === 'all') { + return $results; + } + return array_values(array_filter($results, function ($item) use ($category) { + return strtolower($item['category'] ?? '') === $category; + })); +} + +/** + * Baut eine Such-Ergebnis-URL für einen bestimmten Seitenwechsel (No-JS-Pagination). + */ +function buildSearchResultsUrl(int $page, string $query, string $sort, string $category, int $limit): string +{ + return "index.php?pfad=search-results" + . "&q=" . urlencode($query) + . "&sort=" . urlencode($sort) + . "&category=" . urlencode($category) + . "&limit=" . $limit + . "&page=" . $page; +} + +/** + * Rendert eine rein serverseitige Pagination für den No-JS-Fall: + */ +function renderNoJsPagination(int $currentPage, int $totalPages, string $query, string $sort, string $category, int $limit): string +{ + $html = ''; + + if ($currentPage > 1) { + $html .= '« '; + } else { + $html .= '« '; + } + + for ($i = 1; $i <= $totalPages; $i++) { + if ($i === $currentPage) { + $html .= '' . $i . ' '; + } else { + $html .= '' . $i . ' '; + } + } + + if ($currentPage < $totalPages) { + $html .= '»'; + } else { + $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/index.php b/index.php index f2dda6c..42d054c 100644 --- a/index.php +++ b/index.php @@ -23,11 +23,11 @@ include_once "php/controller/index-controller.php"; - - - - - + + + + + EduForge diff --git a/js/filter.js b/js/filter.js deleted file mode 100644 index 72d21fc..0000000 --- a/js/filter.js +++ /dev/null @@ -1,116 +0,0 @@ -let currentClientPage = 1; -let itemsPerPage = 10; - -function initFilter() { - const filterSelect = document.getElementById('category-filter'); - const listContainer = document.querySelector('.s-res-list'); - const limitSelect = document.getElementById('s-res-per-page'); - - if (!filterSelect || !listContainer) return; - - if (limitSelect) { - itemsPerPage = parseInt(limitSelect.value, 10) || 10; - } - - updateVisibility(); - - if (limitSelect) { - limitSelect.addEventListener('change', function() { - - //neues limit einlesen - itemsPerPage = parseInt(this.value, 10); - currentClientPage = 1; - updateVisibility(); - }); - } - - filterSelect.addEventListener('change', function() { - currentClientPage = 1; - updateVisibility(); - }); - - const navigationContainer = document.querySelector('.s-res-page-navigation'); - if (navigationContainer) { - navigationContainer.addEventListener('click', function(e) { - const button = e.target.closest('.s-res-page-btn'); - if (!button || button.disabled) return; - - e.preventDefault(); - - const targetPage = button.getAttribute('data-page'); - if (targetPage) { - currentClientPage = parseInt(targetPage, 10); - updateVisibility(); - } - }); - } -} - -function updateVisibility() { - const filterSelect = document.getElementById('category-filter'); - const listContainer = document.querySelector('.s-res-list'); - const selectedCategory = filterSelect.value.toLowerCase().trim(); - const cards = listContainer.querySelectorAll('.s-res-item'); - - let visibleCards = []; - cards.forEach(card => { - const cardCategory = (card.getAttribute('data-category') || '').toLowerCase().trim(); - if (selectedCategory === 'all' || cardCategory.includes(selectedCategory) || selectedCategory.includes(cardCategory)) { - visibleCards.push(card); - } else { - card.style.display = 'none'; - } - }); - - const totalVisible = visibleCards.length; - const totalPages = Math.max(1, Math.ceil(totalVisible / itemsPerPage)); - - if (currentClientPage < 1) currentClientPage = 1; - if (currentClientPage > totalPages) currentClientPage = totalPages; - - const startOffset = (currentClientPage - 1) * itemsPerPage; - const endOffset = startOffset + itemsPerPage; - - visibleCards.forEach((card, index) => { - if (index >= startOffset && index < endOffset) { - card.style.display = 'flex'; - } else { - card.style.display = 'none'; - } - }); - - updatePaginatorUI(currentClientPage, totalPages); -} - -function updatePaginatorUI(currentPage, totalPages) { - const prevBtn = document.getElementById('prev-page-btn'); - const nextBtn = document.getElementById('next-page-btn'); - const numbersContainer = document.getElementById('dynamic-page-numbers'); - - if (!prevBtn || !nextBtn || !numbersContainer) return; - - prevBtn.setAttribute('data-page', currentPage - 1); - prevBtn.disabled = (currentPage <= 1); - - nextBtn.setAttribute('data-page', currentPage + 1); - nextBtn.disabled = (currentPage >= totalPages); - - let buttonsHTML = ''; - for (let i = 1; i <= totalPages; i++) { - const activeClass = (i === currentPage) ? 's-res-page-btn-active' : ''; - buttonsHTML += ` `; - } - numbersContainer.innerHTML = buttonsHTML; -} - -// Hilfsfunktion für Math.ceil in JS -function ceil(val) { return Math.ceil(val); } - -// ist das DOM bereits vollständig aufgebaut? -if (document.readyState === 'loading') { - // Falls noch geladen wird, auf das Event warten - document.addEventListener('DOMContentLoaded', initFilter); -} else { - // Falls das HTML bereits komplett da ist, sofort ausführen - initFilter(); -} \ No newline at end of file diff --git a/js/paginator.js b/js/paginator.js deleted file mode 100644 index 378174b..0000000 --- a/js/paginator.js +++ /dev/null @@ -1,27 +0,0 @@ -function initPaginator() { - const form = document.getElementById('search-form-id'); - const pageInput = document.getElementById('s-res-page-input'); - const pageButtons = document.querySelectorAll('.s-res-page-navigation .s-res-page-btn'); - - pageButtons.forEach(button => { - button.addEventListener('click', function() { - if (this.disabled) return; - - const targetPage = this.getAttribute('data-page'); - - if (targetPage && form && pageInput) { - pageInput.value = targetPage; - form.submit(); - } - }); - }); -} - -// ist das DOM bereits vollständig aufgebaut? -if (document.readyState === 'loading') { - // Falls noch geladen wird, auf das Event warten - document.addEventListener('DOMContentLoaded', initPaginator); -} else { - // Falls das HTML bereits komplett da ist, sofort ausführen - initPaginator(); -} \ No newline at end of file diff --git a/js/search-results.js b/js/search-results.js new file mode 100644 index 0000000..9461e39 --- /dev/null +++ b/js/search-results.js @@ -0,0 +1,238 @@ +/** + * Übernimmt Sortierung, Kategorie-Filter und Pagination der Suchergebnisse + * vollständig clientseitig + * + * Ablauf: + * 1. Kompletten Ergebnis-Datensatz einmalig per fetch() nachladen + * (php/controller/search-results-data.php, liest nur die Session-Daten, + * keine erneute Datenbank-Suche). + * 2. Sortieren + filtern (JS-Array). + * 3. Ergebnisliste + Pagination-UI daraus neu rendern. + * + * Ersetzt sorter.js, filter.js und paginator.js, deren Klick-Handler sich + * gegenseitig ins Gehege kamen. + * Ohne JS bleibt die serverseitig gerenderte Seite (echte Links/Formulare) + * voll funktionsfähig. + */ +(function () { + const state = { + allItems: [], + sort: 'alphabet', + category: 'all', + itemsPerPage: 10, + currentPage: 1, + }; + + let listContainer, sortRadios, categorySelect, limitSelect; + let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl; + + function init() { + listContainer = document.querySelector('.s-res-list'); + if (!listContainer) return; + + sortRadios = document.querySelectorAll('.sort-radio'); + categorySelect = document.getElementById('category-filter'); + limitSelect = document.getElementById('s-res-per-page'); + prevBtn = document.getElementById('prev-page-btn'); + nextBtn = document.getElementById('next-page-btn'); + numbersContainer = document.getElementById('dynamic-page-numbers'); + jsNav = document.getElementById('js-page-navigation'); + resultCountEl = document.getElementById('s-res-result-count'); + + const checkedRadio = document.querySelector('.sort-radio:checked'); + state.sort = checkedRadio ? checkedRadio.value : 'alphabet'; + state.category = categorySelect ? categorySelect.value : 'all'; + state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10; + + fetchFullDataset() + .then(function (data) { + state.allItems = data.results || []; + attachEvents(); + render(); + if (jsNav) jsNav.style.display = 'flex'; + }) + .catch(function (err) { + // JSON-Endpoint nicht erreichbar: die serverseitig gerenderte + // (No-JS-)Ansicht bleibt sichtbar und funktioniert weiter. + console.error('Suchergebnisse konnten nicht nachgeladen werden:', err); + }); + } + + function fetchFullDataset() { + return fetch('php/controller/search-results-data.php', { credentials: 'same-origin' }) + .then(function (res) { + if (!res.ok) throw new Error('HTTP ' + res.status); + return res.json(); + }); + } + + function getFilteredSortedItems() { + let items = state.allItems; + + if (state.category !== 'all') { + const wanted = state.category.toLowerCase(); + items = items.filter(function (item) { + return (item.category || '').toLowerCase() === wanted; + }); + } + + items = items.slice().sort(function (a, b) { + if (state.sort === 'likes') { + return (b.likes || 0) - (a.likes || 0); + } + if (state.sort === 'newest' || state.sort === 'oldest') { + return state.sort === 'newest' ? (b.id - a.id) : (a.id - b.id); + } + // alphabet (Standard) + return (a.title || '').toLowerCase().localeCompare((b.title || '').toLowerCase()); + }); + + return items; + } + + function render() { + const items = getFilteredSortedItems(); + + if (resultCountEl) resultCountEl.textContent = items.length; + + const totalPages = Math.max(1, Math.ceil(items.length / state.itemsPerPage)); + if (state.currentPage > totalPages) state.currentPage = totalPages; + if (state.currentPage < 1) state.currentPage = 1; + + const start = (state.currentPage - 1) * state.itemsPerPage; + const pageItems = items.slice(start, start + state.itemsPerPage); + + listContainer.innerHTML = ''; + if (pageItems.length === 0) { + const empty = document.createElement('p'); + empty.textContent = 'Keine Beiträge gefunden.'; + listContainer.appendChild(empty); + } else { + pageItems.forEach(function (item) { + listContainer.appendChild(buildCard(item)); + }); + } + + updatePaginatorUI(state.currentPage, totalPages); + } + + function buildCard(item) { + const card = document.createElement('div'); + card.className = 's-res-item'; + card.setAttribute('data-likes', item.likes); + card.setAttribute('data-category', (item.category || '').toLowerCase()); + + const link = document.createElement('a'); + link.className = 's-res-link'; + link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id); + link.textContent = item.title; + + const title = document.createElement('h2'); + title.className = 's-res-item-title'; + title.appendChild(link); + + const authorName = document.createElement('span'); + authorName.className = 's-res-author-name'; + authorName.textContent = item.author; + + const author = document.createElement('p'); + author.className = 's-res-author'; + author.append('Von: ', authorName); + + const likes = document.createElement('span'); + likes.className = 's-res-likes'; + likes.textContent = '❤️ ' + item.likes; + + const metaRow = document.createElement('div'); + metaRow.className = 's-res-meta-row'; + metaRow.append(author, likes); + + const content = document.createElement('div'); + content.className = 's-res-content'; + content.append(title, metaRow); + + const arrow = document.createElement('div'); + arrow.className = 's-res-arrow'; + arrow.textContent = '→'; + + card.append(content, arrow); + return card; + } + + function updatePaginatorUI(currentPage, totalPages) { + if (!prevBtn || !nextBtn || !numbersContainer) return; + + prevBtn.disabled = currentPage <= 1; + nextBtn.disabled = currentPage >= totalPages; + + numbersContainer.innerHTML = ''; + for (let i = 1; i <= totalPages; i++) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 's-res-page-btn' + (i === currentPage ? ' s-res-page-btn-active' : ''); + btn.textContent = i; + btn.addEventListener('click', function () { + state.currentPage = i; + render(); + }); + numbersContainer.appendChild(btn); + } + } + + function attachEvents() { + sortRadios.forEach(function (radio) { + radio.addEventListener('change', function () { + state.sort = this.value; + state.currentPage = 1; + render(); + }); + }); + + if (categorySelect) { + categorySelect.addEventListener('change', function () { + state.category = this.value; + state.currentPage = 1; + render(); + }); + } + + if (limitSelect) { + limitSelect.addEventListener('change', function () { + state.itemsPerPage = parseInt(this.value, 10) || 10; + state.currentPage = 1; + render(); + }); + } + + if (prevBtn) { + prevBtn.addEventListener('click', function () { + if (state.currentPage > 1) { + state.currentPage -= 1; + render(); + } + }); + } + + if (nextBtn) { + nextBtn.addEventListener('click', function () { + state.currentPage += 1; + render(); + }); + } + + // Mit aktivem JS übernimmt render() Sortierung/Filter live + // ein echtes Absenden des Formulars (No-JS-Fallback) ist dann nicht mehr nötig. + const filterForm = document.getElementById('s-res-filter-form'); + if (filterForm) { + filterForm.addEventListener('submit', function (e) { + e.preventDefault(); + }); + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); \ 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/js/sorter.js b/js/sorter.js deleted file mode 100644 index 5a1ee59..0000000 --- a/js/sorter.js +++ /dev/null @@ -1,50 +0,0 @@ - -function initSorter() { - const listContainer = document.querySelector('.s-res-list'); - const sortRadios = document.querySelectorAll('.sort-radio'); - - // wenn keine liste vorhanden, abbrechen - if (!listContainer || sortRadios.length === 0) return; - - sortRadios.forEach(radio => { - radio.addEventListener('change', function() { - const cards = Array.from(listContainer.querySelectorAll('.s-res-item')); - const sortValue = this.value; - - cards.sort((a, b) => { - if (sortValue === 'likes') { - const likesA = parseInt(a.getAttribute('data-likes') || '0', 10); - const likesB = parseInt(b.getAttribute('data-likes') || '0', 10); - return likesB - likesA; - } - else if (sortValue === 'alphabet') { - // alphabetische sortierung - const titleA = a.querySelector('.s-res-link').textContent.trim().toLowerCase(); - const titleB = b.querySelector('.s-res-link').textContent.trim().toLowerCase(); - return titleA.localeCompare(titleB); - } - else if (sortValue === 'newest' || sortValue === 'oldest') { - // hoehere ID wird als neuer gesehen - const urlA = a.querySelector('.s-res-link').getAttribute('href'); - const urlB = b.querySelector('.s-res-link').getAttribute('href'); - - const idA = parseInt(urlA.match(/id=(\d+)/)[1], 10); - const idB = parseInt(urlB.match(/id=(\d+)/)[1], 10); - - return sortValue === 'newest' ? idB - idA : idA - idB; - } - return 0; - }); - - listContainer.innerHTML = ''; - cards.forEach(card => listContainer.appendChild(card)); - }); - }); -} - -// ist das DOM bereits vollständig aufgebaut? -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initSorter); -} else { - initSorter(); -} \ No newline at end of file diff --git a/php/controller/search-results-controller.php b/php/controller/search-results-controller.php index f552e0d..3f067d8 100644 --- a/php/controller/search-results-controller.php +++ b/php/controller/search-results-controller.php @@ -17,37 +17,12 @@ 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 $safeArrayResults = []; foreach ($results as $obj) { $safeArrayResults[] = [ @@ -61,25 +36,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..1c513d5 --- /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 diff --git a/php/controller/showCategory-controller.php b/php/controller/showCategory-controller.php index 938fca0..72f20f4 100644 --- a/php/controller/showCategory-controller.php +++ b/php/controller/showCategory-controller.php @@ -2,66 +2,51 @@ 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"])) { -if (isset($_GET["category"]) && !empty($_GET["category"]) && articleCategoryValidator($_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 { $articleManager = ArticleManager::getInstance(); $userManager = UserManager::getInstance(); + $allArticles = $articleManager->getArticlesByCategory($category); + + $safeArrayResults = []; foreach ($allArticles as $article) { - $article->setAuthor($userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"]); - } - $totalArticles = count($allArticles); - - // --- SORTIERUNG LOGIK --- - if ($sortStyle === 'alphabet') { - usort($allArticles, function($a, $b) { - return strcasecmp($a->getTitle(), $b->getTitle()); - }); - } elseif ($sortStyle === 'likes') { - 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 - }); + $authorName = $userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"]; + $safeArrayResults[] = [ + "id" => $article->getID(), + "title" => $article->getTitle(), + "content" => $article->getContent(), + "author" => $authorName, + "category" => $article->getCategory(), + "tags" => $article->getTags(), + "creationDate" => $article->getCreationDate(), + "likes" => $article->getLikes(), + ]; } - // Paginierung konfigurieren - $limit = 10; - $totalPages = ceil($totalArticles / $limit); - if ($page > $totalPages && $totalPages > 0) { $page = $totalPages; } - - $offset = ($page - 1) * $limit; - $articles = array_slice($allArticles, $offset, $limit); + // 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"; include_once "content/404.php"; 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