Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1a32120d6 | |||
| 97fa1de999 | |||
| 7341fe7733 | |||
| 5b0528a914 | |||
| ac64e65dd3 | |||
| 96ffa3288e | |||
| 0c6cf13efa | |||
| d3884d2fc1 | |||
| 1575f4fa24 | |||
| 0aca76d2f4 | |||
| eca6646071 | |||
| 35e5006063 | |||
| fcb9289d80 | |||
| 6595a3d78d | |||
| 41d8ac6721 | |||
| 63c213bae7 | |||
| 61d72436c9 | |||
| 721bc8d833 | |||
| 2666d2d14f | |||
| de1ba88860 | |||
| e0fb04794d | |||
| 6897a29574 | |||
| 8836034670 | |||
| 98b088afc0 | |||
| 51c850d122 | |||
| d590561ade | |||
| ba11c216bd | |||
| 71a1b029cb | |||
| 2126d076d5 | |||
| 2ecd446dd0 | |||
| 97302f1bb3 | |||
| 35d4425086 | |||
| d3435d4cde | |||
| f61c548b9e | |||
| 101a2a247b | |||
| 423941e7ed | |||
| a8e76c782d | |||
| 3f9c048493 | |||
| fee5a701df | |||
| d341312192 | |||
| 4dadfb8863 |
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="dataSourceStorageLocal" created-in="IU-253.32098.101">
|
<component name="dataSourceStorageLocal" created-in="IU-261.24374.151">
|
||||||
<data-source name="articles" uuid="315cb5c9-2b0f-435b-b602-59823b160908">
|
<data-source name="articles" uuid="315cb5c9-2b0f-435b-b602-59823b160908">
|
||||||
<database-info product="SQLite" version="3.51.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.51.1.0" dbms="SQLITE" exact-version="3.51.1" exact-driver-version="3.51">
|
<database-info product="SQLite" version="3.51.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.51.1.0" dbms="SQLITE" exact-version="3.51.1" exact-driver-version="3.51">
|
||||||
<identifier-quote-string>"</identifier-quote-string>
|
<identifier-quote-string>"</identifier-quote-string>
|
||||||
|
|||||||
+1
-5
@@ -4,11 +4,7 @@
|
|||||||
-->
|
-->
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "internal_error"): ?>
|
<?php include_once "includes/alertMessages.php"?>
|
||||||
<p class="alert-message is-error">
|
|
||||||
Es ist ein interner Fehler beim Speichern aufgetreten. Bitte versuche es erneut.
|
|
||||||
</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php
|
<?php
|
||||||
unset($_SESSION["message"]);
|
unset($_SESSION["message"]);
|
||||||
?>
|
?>
|
||||||
|
|||||||
+96
-75
@@ -2,20 +2,37 @@
|
|||||||
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"] ?? "";
|
||||||
$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])) {
|
if (!in_array($limit, [10, 20, 50, 100])) {
|
||||||
$limit = 10;
|
$limit = 10;
|
||||||
}
|
}
|
||||||
|
$_SESSION['search_limit'] = $limit;
|
||||||
|
|
||||||
// Gesamtseitenzahl
|
// Serverseitig sortieren + filtern
|
||||||
$totalPages = max(1, ceil($totalResultsCount / $limit));
|
$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;
|
$currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||||
if ($currentPage < 1) {
|
if ($currentPage < 1) {
|
||||||
$currentPage = 1;
|
$currentPage = 1;
|
||||||
@@ -23,42 +40,59 @@ if ($currentPage < 1) {
|
|||||||
$currentPage = $totalPages;
|
$currentPage = $totalPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Startpunkt im Array berechnen (Offset)
|
|
||||||
$offset = ($currentPage - 1) * $limit;
|
$offset = ($currentPage - 1) * $limit;
|
||||||
|
|
||||||
// Nur die Ergebnisse für die aktuelle Seite ausschneiden
|
// Dies ist die No-JS-Basisversion: nur die aktuelle Seite wird gerendert.
|
||||||
//$results = array_slice($all_results, $offset, $limit);
|
// Bei aktiviertem JS lädt js/results-app.js zusätzlich den kompletten
|
||||||
$results = $all_results;
|
// 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);
|
$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',
|
||||||
|
];
|
||||||
?>
|
?>
|
||||||
<noscript>
|
|
||||||
Bitte JavaScript aktivieren!
|
|
||||||
</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/search-results.js vollständig client-seitig übernommen.
|
||||||
-->
|
-->
|
||||||
<div class="s-res-layout-grid">
|
<div class="s-res-layout-grid">
|
||||||
<?php include_once "includes/alertMessages.php"?>
|
<?php include_once "includes/alertMessages.php" ?>
|
||||||
|
|
||||||
<!-- 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-->
|
<!-- 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">
|
<form action="php/controller/search-results-controller.php" method="GET" id="search-form-id" class="s-res-sidebar-form">
|
||||||
|
|
||||||
<input type="hidden" id="s-res-page-input" name="page" value="<?php echo $_GET['page'] ?? 1; ?>">
|
|
||||||
|
|
||||||
<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..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50" required>
|
<input type="search" id="site-search" name="q" placeholder="Suchen..." 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: search-results.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>
|
||||||
<?php $currentSort = $_SESSION['search_sort'] ?? 'alphabet'; ?>
|
|
||||||
<div class="s-res-filter-group">
|
<div class="s-res-filter-group">
|
||||||
<label class="s-res-filter-option">
|
<label class="s-res-filter-option">
|
||||||
<input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>>
|
<input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>>
|
||||||
@@ -81,37 +115,17 @@ $resultCount = count($results);
|
|||||||
|
|
||||||
<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" 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;">
|
||||||
<option value="all">Alle Kategorien</option>
|
<option value="all" <?php echo $currentCategory === 'all' ? 'selected' : ''; ?>>Alle Kategorien</option>
|
||||||
<option value="Deutsch">Deutsch</option>
|
<?php foreach ($categories as $value => $label): ?>
|
||||||
<option value="Englisch">Englisch</option>
|
<option value="<?php echo htmlspecialchars($value); ?>" <?php echo $currentCategory === strtolower($value) ? 'selected' : ''; ?>><?php echo htmlspecialchars($label); ?></option>
|
||||||
<option value="Franzoesisch">Französisch</option>
|
<?php endforeach; ?>
|
||||||
<option value="Latein">Latein</option>
|
|
||||||
<option value="Literatur">Literatur</option>
|
|
||||||
<option value="Mathematik">Mathematik</option>
|
|
||||||
<option value="Biologie">Biologie</option>
|
|
||||||
<option value="Informatik">Informatik</option>
|
|
||||||
<option value="Chemie">Chemie</option>
|
|
||||||
<option value="Physik">Physik</option>
|
|
||||||
<option value="Astronomie">Astronomie</option>
|
|
||||||
<option value="Geschichte">Geschichte</option>
|
|
||||||
<option value="Erdkunde">Erdkunde</option>
|
|
||||||
<option value="Sozialkunde">Sozialkunde</option>
|
|
||||||
<option value="Wirtschaftskunde">Wirtschaftskunde</option>
|
|
||||||
<option value="Religion">Religion</option>
|
|
||||||
<option value="Ethikunterricht">Ethikunterricht</option>
|
|
||||||
<option value="Philosophie">Philosophie</option>
|
|
||||||
<option value="Psychologie">Psychologie</option>
|
|
||||||
<option value="Kunst">Kunst</option>
|
|
||||||
<option value="Musik">Musik</option>
|
|
||||||
<option value="Theater">Theater</option>
|
|
||||||
<option value="Technik">Technik</option>
|
|
||||||
<option value="Werken">Werken</option>
|
|
||||||
<option value="Hauswirtschaft">Hauswirtschaft</option>
|
|
||||||
<option value="Sport">Sport</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<noscript>
|
||||||
|
<button type="submit" class="nav__search-button">Filter anwenden</button>
|
||||||
|
</noscript>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
@@ -120,18 +134,16 @@ $resultCount = count($results);
|
|||||||
|
|
||||||
<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"><?php echo $totalResultsCount; ?> Treffer für Ihre Suchanfrage "<?php echo htmlspecialchars($query); ?>"</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ergebnisliste -->
|
<!-- Ergebnisliste -->
|
||||||
<div class="s-res-list">
|
<div class="s-res-list">
|
||||||
<?php
|
<?php if (!empty($results)): ?>
|
||||||
if (!empty($results)): ?>
|
|
||||||
|
<?php foreach ($results as $item):
|
||||||
<?php foreach ($results as $item):
|
$likesCount = getLikeCount($item);
|
||||||
// Anzahl der Likes ermitteln (falls es ein Array ist, zählen; falls Zahl, direkt nutzen)
|
?>
|
||||||
$likesCount = isset($item['likes']) && is_array($item['likes']) ? count($item['likes']) : ($item['likes'] ?? 0);
|
|
||||||
?>
|
|
||||||
<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">
|
||||||
<h2 class="s-res-item-title">
|
<h2 class="s-res-item-title">
|
||||||
@@ -151,41 +163,50 @@ $resultCount = count($results);
|
|||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
||||||
<?php
|
<?php elseif (isset($_SESSION["message"]) && $_SESSION["message"] === "invalid_search_query"): ?>
|
||||||
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"): ?>
|
|
||||||
<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
|
<?php unset($_SESSION["message"]); ?>
|
||||||
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 -->
|
||||||
<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" 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>
|
||||||
|
|
||||||
<div class="s-res-page-navigation">
|
<nav class="s-res-page-navigation" aria-label="Seitennavigation">
|
||||||
<button type="button" class="s-res-page-btn" id="prev-page-btn" data-page="0">«</button>
|
<!-- No-JS-Fallback: -->
|
||||||
|
<noscript>
|
||||||
<span id="dynamic-page-numbers"></span>
|
<?php echo renderNoJsPagination($currentPage, $totalPages, $query, $currentSort, $currentCategory, $limit); ?>
|
||||||
|
</noscript>
|
||||||
<button type="button" class="s-res-page-btn" id="next-page-btn" data-page="2">»</button>
|
|
||||||
</div>
|
<!-- JS-Version: wird per search-results.js befüllt/eingeblendet: -->
|
||||||
|
<div id="js-page-navigation" style="display:none;">
|
||||||
|
<button type="button" class="s-res-page-btn" id="prev-page-btn">«</button>
|
||||||
|
<span id="dynamic-page-numbers"></span>
|
||||||
|
<button type="button" class="s-res-page-btn" id="next-page-btn">»</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
+160
-87
@@ -1,111 +1,184 @@
|
|||||||
<?php
|
<?php
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
include_once "php/controller/showCategory-controller.php";
|
include_once "php/controller/showCategory-controller.php";
|
||||||
|
require_once __DIR__ . '/../includes/resultsHelper.php';
|
||||||
|
|
||||||
$currentSort = isset($sortStyle) ? $sortStyle : 'alphabet';
|
$rawResults = $_SESSION['category_results'] ?? [];
|
||||||
$currentPage = isset($page) ? $page : 1;
|
$category = $_SESSION['category_name'] ?? ($_GET['category'] ?? '');
|
||||||
|
|
||||||
|
// Sortierung / Suche / Limit: GET-Parameter haben Vorrang, sonst Rückfall auf
|
||||||
|
// die zuletzt in der Session gemerkten Werte (analog zu search-results.php).
|
||||||
|
$currentSort = $_GET['sort'] ?? ($_SESSION['cat_sort'] ?? 'alphabet');
|
||||||
|
if (!in_array($currentSort, ['alphabet', 'likes', 'newest', 'oldest'])) {
|
||||||
|
$currentSort = 'alphabet';
|
||||||
|
}
|
||||||
|
$_SESSION['cat_sort'] = $currentSort;
|
||||||
|
|
||||||
|
$query = trim($_GET['q'] ?? ($_SESSION['cat_query'] ?? ''));
|
||||||
|
$_SESSION['cat_query'] = $query;
|
||||||
|
|
||||||
|
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : ($_SESSION['cat_limit'] ?? 10);
|
||||||
|
if (!in_array($limit, [10, 20, 50, 100])) {
|
||||||
|
$limit = 10;
|
||||||
|
}
|
||||||
|
$_SESSION['cat_limit'] = $limit;
|
||||||
|
|
||||||
|
// Serverseitig sortieren + nach Suchbegriff filtern (Kategorie steht bereits fest)
|
||||||
|
$filteredResults = sortSearchResults($rawResults, $currentSort);
|
||||||
|
$filteredResults = filterResultsByQuery($filteredResults, $query);
|
||||||
|
|
||||||
|
$totalResultsCount = count($filteredResults);
|
||||||
|
$totalPages = max(1, (int)ceil($totalResultsCount / $limit));
|
||||||
|
|
||||||
|
$currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||||
|
if ($currentPage < 1) {
|
||||||
|
$currentPage = 1;
|
||||||
|
} elseif ($currentPage > $totalPages) {
|
||||||
|
$currentPage = $totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
$offset = ($currentPage - 1) * $limit;
|
||||||
|
|
||||||
|
// Dies ist die No-JS-Basisversion: nur die aktuelle Seite wird gerendert.
|
||||||
|
// Bei aktiviertem JS lädt js/showCategory.js zusätzlich den kompletten
|
||||||
|
// Kategorie-Datensatz nach (ein einziger Request) und übernimmt Sortierung,
|
||||||
|
// Suche und Pagination danach komplett im Browser, ohne weitere Serveranfragen.
|
||||||
|
$results = array_slice($filteredResults, $offset, $limit);
|
||||||
|
$resultCount = count($results);
|
||||||
?>
|
?>
|
||||||
|
<!--
|
||||||
|
Seite: Kategorie-Ansicht
|
||||||
|
Inhalt: Zeigt alle Beiträge einer Kategorie an, sortierbar und innerhalb
|
||||||
|
der Kategorie durchsuchbar.
|
||||||
|
Funktioniert ohne JavaScript (serverseitige Sortierung/Suche/Pagination
|
||||||
|
über echte Links + Formulare) und wird bei aktiviertem JS von
|
||||||
|
js/showCategory.js vollständig client-seitig übernommen.
|
||||||
|
-->
|
||||||
|
<div class="s-res-layout-grid">
|
||||||
|
<?php include_once "includes/alertMessages.php" ?>
|
||||||
|
|
||||||
<noscript>
|
<!-- Links: Seitenleiste für Suche (innerhalb der Kategorie) und Sortierung -->
|
||||||
Bitte JavaScript aktivieren!
|
<aside class="s-res-sidebar">
|
||||||
</noscript>
|
|
||||||
|
|
||||||
<!-- Das Formular umschließt jetzt das gesamte Layout -->
|
<!-- Sortiert/durchsucht die BEREITS geladenen Kategorie-Beiträge.
|
||||||
<form id="search-form-id" action="index.php" method="GET" style="display: contents;">
|
Ohne JS: echter Reload dieser Seite, aber ohne erneute Datenbank-Abfrage.
|
||||||
<input type="hidden" name="pfad" value="showCategory">
|
Mit JS: js/showCategory.js übernimmt das live im Browser, der Submit-Button
|
||||||
<input type="hidden" name="category" value="<?php echo (isset($category) && !empty($category)) ? htmlspecialchars($category) : ''; ?>">
|
wird dafür nicht benötigt und über <noscript> versteckt. -->
|
||||||
<input type="hidden" id="s-res-page-input" name="page" value="<?php echo $currentPage; ?>">
|
<form action="index.php" method="GET" id="cat-filter-form" class="s-res-sidebar-form">
|
||||||
|
<input type="hidden" name="pfad" value="showCategory">
|
||||||
|
<input type="hidden" name="category" value="<?php echo htmlspecialchars($category); ?>">
|
||||||
|
|
||||||
<div class="cat-view-container">
|
<div class="s-res-sidebar-box">
|
||||||
|
<h3 class="s-res-sidebar-title">In dieser Kategorie suchen</h3>
|
||||||
|
<input type="search" id="cat-search-input" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50">
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- LINKE SPALTE: SEITENLEISTE FÜR SORTIERUNG -->
|
<div class="s-res-sidebar-box">
|
||||||
<aside class="cat-sidebar">
|
<h3 class="s-res-sidebar-title">Sortierung</h3>
|
||||||
<div class="cat-sidebar-box">
|
<div class="s-res-filter-group">
|
||||||
<h3 class="cat-sidebar-title">Sortierung</h3>
|
<label class="s-res-filter-option">
|
||||||
<div class="cat-filter-group">
|
<input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>>
|
||||||
<label class="cat-filter-option">
|
<span>Alphabetisch</span>
|
||||||
<input type="radio" name="sort" value="alphabet" <?php echo ($currentSort === 'alphabet') ? 'checked' : ''; ?> onchange="this.form.submit();">
|
|
||||||
Alphabetisch
|
|
||||||
</label>
|
</label>
|
||||||
<label class="cat-filter-option">
|
<label class="s-res-filter-option">
|
||||||
<input type="radio" name="sort" value="likes" <?php echo ($currentSort === 'likes') ? 'checked' : ''; ?> onchange="this.form.submit();">
|
<input type="radio" name="sort" value="likes" class="sort-radio" <?php echo $currentSort === 'likes' ? 'checked' : ''; ?>>
|
||||||
Beliebtheit (Likes)
|
<span>Beliebtheit (Likes)</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="cat-filter-option">
|
<label class="s-res-filter-option">
|
||||||
<input type="radio" name="sort" value="newest" <?php echo ($currentSort === 'newest') ? 'checked' : ''; ?> onchange="this.form.submit();">
|
<input type="radio" name="sort" value="newest" class="sort-radio" <?php echo $currentSort === 'newest' ? 'checked' : ''; ?>>
|
||||||
Neueste Beiträge
|
<span>Neueste Beiträge</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="cat-filter-option">
|
<label class="s-res-filter-option">
|
||||||
<input type="radio" name="sort" value="oldest" <?php echo ($currentSort === 'oldest') ? 'checked' : ''; ?> onchange="this.form.submit();">
|
<input type="radio" name="sort" value="oldest" class="sort-radio" <?php echo $currentSort === 'oldest' ? 'checked' : ''; ?>>
|
||||||
Älteste Beiträge
|
<span>Älteste Beiträge</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- RECHTE SPALTE: INHALT & PAGINATOR -->
|
<noscript>
|
||||||
<main class="cat-view-main">
|
<button type="submit" class="nav__search-button">Anwenden</button>
|
||||||
<div class="cat-view-header">
|
</noscript>
|
||||||
<h1 class="cat-view-title">Kategorie: <?php echo (isset($category) && !empty($category)) ? htmlspecialchars($category) : 'Unbekannt'; ?></h1>
|
</form>
|
||||||
<p class="cat-view-meta-text">
|
|
||||||
<?php echo (isset($totalArticles)) ? $totalArticles : 0; ?> Treffer in dieser Kategorie
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Beiträge -->
|
</aside>
|
||||||
<div class="cat-view-list">
|
|
||||||
<?php if (isset($articles) && !empty($articles)): ?>
|
|
||||||
<?php foreach ($articles as $article): ?>
|
|
||||||
<div class="cat-view-item">
|
|
||||||
<div class="cat-view-content">
|
|
||||||
<h2 class="cat-view-item-title">
|
|
||||||
<a href="index.php?pfad=showArticle&id=<?php echo htmlspecialchars($article->getID()); ?>" class="cat-view-link">
|
|
||||||
<?php echo (isset($article) && !empty($article->getTitle())) ? htmlspecialchars($article->getTitle()) : 'Kein Titel'; ?>
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="cat-view-meta-row">
|
<main class="s-res-main-content">
|
||||||
<p class="cat-view-author">
|
|
||||||
von <span class="cat-view-author-name"><?php echo (isset($article) && !empty($article->getAuthor())) ? htmlspecialchars($article->getAuthor()) : 'Anonym'; ?></span>
|
<div class="s-res-header">
|
||||||
</p>
|
<h1 class="s-res-main-title">Kategorie: <?php echo htmlspecialchars($category); ?></h1>
|
||||||
<span class="cat-view-likes">
|
<p class="s-res-meta">
|
||||||
❤️ <?php echo (isset($article) && !empty($article->getLikes())) ? htmlspecialchars($article->getLikes()) : '0'; ?>
|
<span id="s-res-result-count"><?php echo $totalResultsCount; ?></span> Treffer in dieser Kategorie<?php echo $query !== '' ? ' für "' . htmlspecialchars($query) . '"' : ''; ?>
|
||||||
</span>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Ergebnisliste -->
|
||||||
|
<div class="s-res-list">
|
||||||
|
<?php if (!empty($results)): ?>
|
||||||
|
|
||||||
|
<?php foreach ($results as $item):
|
||||||
|
$likesCount = getLikeCount($item);
|
||||||
|
?>
|
||||||
|
<div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>">
|
||||||
|
<div class="s-res-content">
|
||||||
|
<h2 class="s-res-item-title">
|
||||||
|
<a href="index.php?pfad=showArticle&id=<?php echo $item['id']; ?>" class="s-res-link">
|
||||||
|
<?php echo htmlspecialchars($item['title']); ?>
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
<div class="s-res-meta-row">
|
||||||
|
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($item['author']); ?></span></p>
|
||||||
|
|
||||||
|
<span class="s-res-likes">
|
||||||
|
❤️ <?php echo $likesCount; ?>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cat-view-arrow">→</div>
|
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<div class="s-res-arrow">→</div>
|
||||||
<?php else: ?>
|
</div>
|
||||||
<p class="cat-view-meta-text">Keine Artikel in dieser Kategorie gefunden.</p>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
|
||||||
|
<?php elseif ($query !== ''): ?>
|
||||||
|
<p>Keine Beiträge in dieser Kategorie zu diesem Suchbegriff gefunden.</p>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
<p>Keine Beiträge in dieser Kategorie gefunden.</p>
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php unset($_SESSION["message"]); ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="s-res-pagination-footer">
|
||||||
|
|
||||||
|
<!-- Auswahl der Ergebnisse pro Seite -->
|
||||||
|
<div class="s-res-limit-selector">
|
||||||
|
<label for="s-res-per-page" class="s-res-limit-label">Ergebnisse pro Seite:</label>
|
||||||
|
<select id="s-res-per-page" name="limit" form="cat-filter-form" class="s-res-limit-select">
|
||||||
|
<option value="10" <?php echo $limit === 10 ? 'selected' : ''; ?>>10</option>
|
||||||
|
<option value="20" <?php echo $limit === 20 ? 'selected' : ''; ?>>20</option>
|
||||||
|
<option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50</option>
|
||||||
|
<option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100</option>
|
||||||
|
</select>
|
||||||
|
<noscript><button type="submit" form="cat-filter-form" class="nav__search-button">Übernehmen</button></noscript>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer Paginator -->
|
<nav class="s-res-page-navigation" aria-label="Seitennavigation">
|
||||||
<?php if (isset($totalPages) && $totalPages > 1): ?>
|
<!-- No-JS-Fallback: -->
|
||||||
<div class="cat-view-pagination-footer">
|
<noscript>
|
||||||
<div></div>
|
<?php echo renderNoJsPaginationCategory($currentPage, $totalPages, $category, $currentSort, $query, $limit); ?>
|
||||||
<div class="s-res-page-navigation">
|
</noscript>
|
||||||
<button type="button" class="s-res-page-btn" data-page="<?php echo $currentPage - 1; ?>" <?php echo ($currentPage <= 1) ? 'disabled' : ''; ?>>
|
|
||||||
«
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
|
<!-- JS-Version: wird per js/showCategory.js befüllt/eingeblendet: -->
|
||||||
<button type="button"
|
<div id="js-page-navigation" style="display:none;">
|
||||||
class="s-res-page-btn <?php echo ($i === $currentPage) ? 'cat-active-btn' : ''; ?>"
|
<button type="button" class="s-res-page-btn" id="prev-page-btn">«</button>
|
||||||
data-page="<?php echo $i; ?>"
|
<span id="dynamic-page-numbers"></span>
|
||||||
<?php echo ($i === $currentPage) ? 'disabled' : ''; ?>>
|
<button type="button" class="s-res-page-btn" id="next-page-btn">»</button>
|
||||||
<?php echo $i; ?>
|
|
||||||
</button>
|
|
||||||
<?php endfor; ?>
|
|
||||||
|
|
||||||
<button type="button" class="s-res-page-btn" data-page="<?php echo $currentPage + 1; ?>" <?php echo ($currentPage >= $totalPages) ? 'disabled' : ''; ?>>
|
|
||||||
»
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
</nav>
|
||||||
</main>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
+58
-26
@@ -5,10 +5,25 @@ if (!isset($_SESSION["user"])) {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
include_once 'php/controller/showArticle-controller.php';
|
include_once 'php/controller/showArticle-controller.php';
|
||||||
|
|
||||||
|
// Aktuelle Blockliste ermitteln:
|
||||||
|
$rawContent = '[]';
|
||||||
|
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])) {
|
||||||
|
$rawContent = $_SESSION['old_content'];
|
||||||
|
unset($_SESSION['old_content']);
|
||||||
|
} elseif (isset($content) && !empty($content)) {
|
||||||
|
$rawContent = $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
$blocks = json_decode($rawContent, true);
|
||||||
|
if (!is_array($blocks)) {
|
||||||
|
$blocks = [];
|
||||||
|
}
|
||||||
|
$blocks = array_values($blocks);
|
||||||
?>
|
?>
|
||||||
<!--
|
<!--
|
||||||
Seite: Beitrag erstellen
|
Seite: Beitrag bearbeiten
|
||||||
Inhalt: Formular für die Erstellung eines neuen Beitrags
|
Inhalt: Formular für die Bearbeitung eines Beitrags
|
||||||
-->
|
-->
|
||||||
<form method="post" action="php/controller/updateArticle-controller.php?id=<?php if(isset($id) && !empty($id)){echo htmlspecialchars($id);}else{$_SESSION["message"] = "missing_id";} ?>" id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container">
|
<form method="post" action="php/controller/updateArticle-controller.php?id=<?php if(isset($id) && !empty($id)){echo htmlspecialchars($id);}else{$_SESSION["message"] = "missing_id";} ?>" id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container">
|
||||||
|
|
||||||
@@ -27,32 +42,49 @@ include_once 'php/controller/showArticle-controller.php';
|
|||||||
?>"
|
?>"
|
||||||
placeholder="Titel hier eingeben" required>
|
placeholder="Titel hier eingeben" required>
|
||||||
|
|
||||||
<!-- Hier werden die dynamischen divs via JavaScript eingefügt -->
|
<!--
|
||||||
<div id="block-container"></div>
|
Content-Blöcke: werden serverseitig als echte, benannte Formularfelder gerendert
|
||||||
|
(blocks[i][type]...). Dadurch funktioniert das Hinzufügen/Entfernen von Blöcken und
|
||||||
|
der Bild-Upload auch ganz ohne JavaScript über einen normalen Formular-Submit.
|
||||||
|
Ist JavaScript aktiv, fängt js/editor.js diese Submits ab und erledigt dieselbe
|
||||||
|
Änderung lokal im DOM, ohne den Server zu belasten.
|
||||||
|
-->
|
||||||
|
<div id="block-container">
|
||||||
|
<?php foreach ($blocks as $i => $block): ?>
|
||||||
|
<?php
|
||||||
|
$blockType = $block['type'] ?? '';
|
||||||
|
if ($blockType !== 'text' && $blockType !== 'image') {
|
||||||
|
continue; // unbekannter/kaputter Block wird übersprungen
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="editor-block article-editor-scope" data-index="<?php echo (int)$i; ?>">
|
||||||
|
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][type]" value="<?php echo htmlspecialchars($blockType); ?>">
|
||||||
|
|
||||||
<!-- Plus-Button und das Pop-up-Menü -->
|
<?php if ($blockType === 'text'): ?>
|
||||||
<div id="add-block-control" class="article-editor-scope add-block-control">
|
<textarea name="blocks[<?php echo (int)$i; ?>][text]"
|
||||||
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
|
placeholder="Schreibe deinen Textblock..."><?php echo htmlspecialchars($block['value'] ?? ''); ?></textarea>
|
||||||
<div id="block-popup" class="article-editor-scope block-popup hidden">
|
<?php else: /* image */ ?>
|
||||||
<button type="button" data-type="text">Textblock</button>
|
<?php if (!empty($block['value'])): ?>
|
||||||
<button type="button" data-type="image">Bild einfügen</button>
|
<img src="<?php echo htmlspecialchars($block['value']); ?>"
|
||||||
</div>
|
class="block-image-preview"
|
||||||
|
style="max-width:200px;display:block;margin-top:10px;">
|
||||||
|
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][existing_image]" value="<?php echo htmlspecialchars($block['value']); ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
<input type="file" name="blocks[<?php echo (int)$i; ?>][image]" accept="image/*">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<button type="submit" name="editor_action" value="delete_block:<?php echo (int)$i; ?>" class="delete-block-btn">✕</button>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Unsichtbares Textfeld, das die JSON-Daten hält und an den Controller postet -->
|
<div id="add-block-control" class="article-editor-scope add-block-control">
|
||||||
<textarea id="content" name="content" style="display:none;"><?php
|
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
|
||||||
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])){
|
<div id="block-popup" class="article-editor-scope block-popup">
|
||||||
echo htmlspecialchars($_SESSION['old_content']);
|
<button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button>
|
||||||
unset($_SESSION['old_content']);
|
<button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button>
|
||||||
}elseif (isset($content) && !empty($content)){
|
</div>
|
||||||
echo htmlspecialchars($content);
|
</div>
|
||||||
} else {
|
|
||||||
echo '[]';
|
|
||||||
}
|
|
||||||
?></textarea>
|
|
||||||
|
|
||||||
<!-- unsichtbares Input, um die zu löschenden Bilder zu übergeben-->
|
|
||||||
<input type="hidden" id="deleted-images" name="deleted_images" value="[]">
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Seitenleiste -->
|
<!-- Seitenleiste -->
|
||||||
@@ -289,4 +321,4 @@ include_once 'php/controller/showArticle-controller.php';
|
|||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
@@ -18,11 +18,6 @@
|
|||||||
Ein Beitrag muss Inhalt besitzen. Text- und Bildelemente dürfen nicht leer sein!
|
Ein Beitrag muss Inhalt besitzen. Text- und Bildelemente dürfen nicht leer sein!
|
||||||
</p>
|
</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_category"): ?>
|
|
||||||
<p class="alert-message is-error">
|
|
||||||
Die ausgewählte Kategorie ist ungültig.
|
|
||||||
</p>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_tags"): ?>
|
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_tags"): ?>
|
||||||
<p class="alert-message is-error">
|
<p class="alert-message is-error">
|
||||||
Ungültige Schlagworte gefunden. Erlaubt sind nur Buchstaben, Zahlen, Leerzeichen und Bindestriche (2-50 Zeichen).
|
Ungültige Schlagworte gefunden. Erlaubt sind nur Buchstaben, Zahlen, Leerzeichen und Bindestriche (2-50 Zeichen).
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<?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
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getLikeCount($item): int
|
||||||
|
{
|
||||||
|
if (isset($item['likes']) && is_array($item['likes'])) {
|
||||||
|
return count($item['likes']);
|
||||||
|
}
|
||||||
|
return (int)($item['likes'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sortiert die Ergebnisse
|
||||||
|
*/
|
||||||
|
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 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 .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildSearchResultsUrl($currentPage - 1, $query, $sort, $category, $limit)) . '">«</a> ';
|
||||||
|
} else {
|
||||||
|
$html .= '<span class="s-res-page-btn" aria-disabled="true">«</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)) . '">»</a>';
|
||||||
|
} else {
|
||||||
|
$html .= '<span class="s-res-page-btn" aria-disabled="true">»</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtert Ergebnisse anhand eines Suchbegriffs
|
||||||
|
*
|
||||||
|
* Wird von content/showCategory.php genutzt, um innerhalb einer bereits
|
||||||
|
* geladenen Kategorie zu suchen.
|
||||||
|
*/
|
||||||
|
function filterResultsByQuery(array $results, string $query): array
|
||||||
|
{
|
||||||
|
$query = trim($query);
|
||||||
|
if ($query === '') {
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
$needle = mb_strtolower($query);
|
||||||
|
return array_values(array_filter($results, function ($item) use ($needle) {
|
||||||
|
$haystack = mb_strtolower(($item['title'] ?? '') . ' ' . ($item['content'] ?? ''));
|
||||||
|
return mb_strpos($haystack, $needle) !== false;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut eine Kategorie-URL für einen bestimmten Seitenwechsel (No-JS-Pagination).
|
||||||
|
* Analog zu buildSearchResultsUrl, aber für pfad=showCategory (feste Kategorie,
|
||||||
|
* dafür mit Freitext-Suche innerhalb der Kategorie statt Kategorie-Filter).
|
||||||
|
*/
|
||||||
|
function buildCategoryUrl(int $page, string $category, string $sort, string $query, int $limit): string
|
||||||
|
{
|
||||||
|
return "index.php?pfad=showCategory"
|
||||||
|
. "&category=" . urlencode($category)
|
||||||
|
. "&sort=" . urlencode($sort)
|
||||||
|
. "&q=" . urlencode($query)
|
||||||
|
. "&limit=" . $limit
|
||||||
|
. "&page=" . $page;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendert eine rein serverseitige Pagination für showCategory (No-JS-Fall).
|
||||||
|
* Analog zu renderNoJsPagination.
|
||||||
|
*/
|
||||||
|
function renderNoJsPaginationCategory(int $currentPage, int $totalPages, string $category, string $sort, string $query, int $limit): string
|
||||||
|
{
|
||||||
|
$html = '';
|
||||||
|
|
||||||
|
if ($currentPage > 1) {
|
||||||
|
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($currentPage - 1, $category, $sort, $query, $limit)) . '">«</a> ';
|
||||||
|
} else {
|
||||||
|
$html .= '<span class="s-res-page-btn" aria-disabled="true">«</span> ';
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 1; $i <= $totalPages; $i++) {
|
||||||
|
if ($i === $currentPage) {
|
||||||
|
$html .= '<span class="s-res-page-btn s-res-page-btn-active">' . $i . '</span> ';
|
||||||
|
} else {
|
||||||
|
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($i, $category, $sort, $query, $limit)) . '">' . $i . '</a> ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentPage < $totalPages) {
|
||||||
|
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($currentPage + 1, $category, $sort, $query, $limit)) . '">»</a>';
|
||||||
|
} else {
|
||||||
|
$html .= '<span class="s-res-page-btn" aria-disabled="true">»</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
@@ -2,108 +2,49 @@
|
|||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
ob_start();
|
include_once "php/controller/index-controller.php";
|
||||||
include_once("php/controller/index.php");
|
|
||||||
|
|
||||||
$pfad = $_GET["pfad"] ?? "home";
|
|
||||||
|
|
||||||
/*
|
|
||||||
Controller für Aktionen werden vor der HTML-Ausgabe geladen,
|
|
||||||
damit Weiterleitungen mit header() funktionieren.
|
|
||||||
*/
|
|
||||||
if ($pfad === "login") {
|
|
||||||
include_once "php/controller/login-controller.php";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($pfad === "register") {
|
|
||||||
include_once "php/controller/register-controller.php";
|
|
||||||
}
|
|
||||||
if ($pfad === "password-forgotten") {
|
|
||||||
include_once "php/controller/password-forgotten-controller.php";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($pfad === "confirm-register") {
|
|
||||||
include_once "php/controller/confirm-register-controller.php";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($pfad === "confirm-password") {
|
|
||||||
include_once "php/controller/confirm-password-controller.php";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($pfad === "logout") {
|
|
||||||
include_once "php/controller/logout-controller.php";
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
if ($pfad === "deleteAccount") {
|
|
||||||
include_once "php/controller/deleteAccount-controller.php";
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($pfad === "updateComment") {
|
|
||||||
include_once "php/controller/updateComment-controller.php";
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($pfad === "deleteComment") {
|
|
||||||
include_once "php/controller/deleteComment-controller.php";
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
?>
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="description" content="EduForge">
|
||||||
|
<meta name="author" content="Niklas Ortmann">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
|
||||||
|
|
||||||
<!--
|
<link rel="stylesheet" href="css/main.css">
|
||||||
Seite: Index der Lernplattform
|
<link rel="stylesheet" href="css/navbar.css">
|
||||||
Funktion: Webseitengerüst, Anzeigen von Content
|
<link rel="stylesheet" href="css/footer.css">
|
||||||
-->
|
<link rel="stylesheet" href="css/search-results.css">
|
||||||
<!DOCTYPE html>
|
<link rel="stylesheet" href="css/createArticle.css">
|
||||||
<html lang="de">
|
<link rel="stylesheet" href="css/profile.css">
|
||||||
|
<link rel="stylesheet" href="css/showArticle.css">
|
||||||
|
<link rel="stylesheet" href="css/message.css">
|
||||||
|
<link rel="stylesheet" href="css/showCategory.css">
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="description" content="EduForge">
|
|
||||||
<meta name="author" content="Niklas Ortmann">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="css/main.css">
|
|
||||||
<link rel="stylesheet" href="css/navbar.css">
|
|
||||||
<link rel="stylesheet" href="css/footer.css">
|
|
||||||
<link rel="stylesheet" href="css/search-results.css">
|
|
||||||
<link rel="stylesheet" href="css/createArticle.css">
|
|
||||||
<link rel="stylesheet" href="css/profile.css">
|
|
||||||
<link rel="stylesheet" href="css/showArticle.css">
|
|
||||||
<link rel="stylesheet" href="css/message.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/showCategory.js" async></script>
|
||||||
|
|
||||||
<title>EduForge</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<title>EduForge</title>
|
||||||
|
</head>
|
||||||
<?php
|
<body>
|
||||||
include_once 'includes/navbar.php';
|
|
||||||
|
|
||||||
/*
|
|
||||||
Dynamischer Inhalt:
|
|
||||||
Je nach pfad-Parameter wird die passende Datei aus content geladen.
|
|
||||||
*/
|
|
||||||
if (file_exists('content/' . $pfad . '.php')) {
|
|
||||||
include_once 'content/' . $pfad . '.php';
|
|
||||||
} else {
|
|
||||||
include_once 'content/404.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
include_once 'includes/footer.php';
|
|
||||||
?>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
ob_end_flush();
|
include_once 'includes/navbar.php';
|
||||||
?>
|
|
||||||
|
// Dynamischer Inhalt
|
||||||
|
if (isset($pfad) && $pfad !== "404" && file_exists('content/' . $pfad . '.php')) {
|
||||||
|
include_once 'content/' . $pfad . '.php';
|
||||||
|
} else {
|
||||||
|
include_once 'content/404.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
include_once 'includes/footer.php';
|
||||||
|
?>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
+83
-122
@@ -1,172 +1,133 @@
|
|||||||
console.log("Die JavaScript-Datei wurde erfolgreich geladen!");
|
//console.log("editor.js 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("Skript abgebrochen: Formular nicht gefunden!");
|
console.error("editor.js abgebrochen: Formular nicht gefunden!");
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
console.log("Formular gefunden und Editor initialisiert:", form);
|
console.log("Formular gefunden und editor.js initialisiert:", form);
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = document.getElementById("block-container");
|
const container = document.getElementById("block-container");
|
||||||
const plusButton = document.getElementById("plus-button");
|
const plusButton = document.getElementById("plus-button");
|
||||||
const popup = document.getElementById("block-popup");
|
const popup = document.getElementById("block-popup");
|
||||||
const hiddenContentInput = document.getElementById("content");
|
|
||||||
|
|
||||||
const initialImages = [];
|
// Fortlaufender Zähler für eindeutige Block-Indizes. Wird nie wiederverwendet
|
||||||
|
// (auch nicht nach dem Löschen eines Blocks), damit sich neue und übrig
|
||||||
|
// gebliebene Blöcke beim finalen Submit nie einen Namen teilen.
|
||||||
|
let blockIndex = container.querySelectorAll(".editor-block").length;
|
||||||
|
|
||||||
|
// Ohne JS sind Textblock-/Bild-Button im Pop-up immer sichtbar und ganz normale
|
||||||
|
// Submit-Buttons. Erst mit JS blenden wir das Pop-up standardmäßig aus und
|
||||||
|
// steuern die Sichtbarkeit über den Plus-Button.
|
||||||
|
popup.classList.add("hidden");
|
||||||
|
|
||||||
// Pop-up umschalten bei Klick auf das Plus
|
|
||||||
plusButton.addEventListener("click", () => {
|
plusButton.addEventListener("click", () => {
|
||||||
popup.classList.toggle("hidden");
|
popup.classList.toggle("hidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Klick auf eine Block-Option im Pop-up
|
// Klick auf "Textblock" / "Bild einfügen": lokal im DOM anlegen statt zum
|
||||||
popup.querySelectorAll("button").forEach(btn => {
|
// Server zu submitten (entlastet den Server, kein Page-Reload nötig).
|
||||||
btn.addEventListener("click", function() {
|
popup.querySelectorAll('[name="editor_action"]').forEach(btn => {
|
||||||
|
btn.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
const type = this.getAttribute("data-type");
|
const type = this.getAttribute("data-type");
|
||||||
addBlockElement(type, "");
|
addBlockElement(type, "");
|
||||||
popup.classList.add("hidden");
|
popup.classList.add("hidden");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Erstellt ein visuelles HTML-Element im Editor
|
// Bereits vom Server gerenderte Blöcke (z.B. beim Bearbeiten eines bestehenden
|
||||||
|
// Artikels oder nach einem Validierungsfehler) ebenfalls mit JS-Verhalten ausstatten.
|
||||||
|
container.querySelectorAll(".editor-block").forEach(blockDiv => {
|
||||||
|
bindDeleteButton(blockDiv);
|
||||||
|
bindImageInput(blockDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Erstellt einen neuen Block inkl. echter, benannter Formularfelder
|
||||||
|
// (blocks[i][type], blocks[i][text] bzw. blocks[i][image]). Diese Felder werden
|
||||||
|
// beim finalen Submit ganz normal vom Browser als multipart/form-data verschickt –
|
||||||
|
// es ist kein manuelles Zusammenbauen von JSON beim Absenden mehr nötig.
|
||||||
function addBlockElement(type, value = "") {
|
function addBlockElement(type, value = "") {
|
||||||
|
const index = blockIndex++;
|
||||||
|
|
||||||
const blockDiv = document.createElement("div");
|
const blockDiv = document.createElement("div");
|
||||||
blockDiv.classList.add("editor-block");
|
blockDiv.classList.add("editor-block", "article-editor-scope");
|
||||||
blockDiv.setAttribute("data-type", type);
|
blockDiv.setAttribute("data-index", String(index));
|
||||||
|
|
||||||
// Wenn es ein existierendes Server-Bild beim Laden ist, Pfad im globalen Array sichern
|
const typeInput = document.createElement("input");
|
||||||
if (type === "image" && value && typeof value === 'string' && value.startsWith('uploads/')) {
|
typeInput.type = "hidden";
|
||||||
initialImages.push(value);
|
typeInput.name = `blocks[${index}][type]`;
|
||||||
blockDiv.setAttribute("data-value", value);
|
typeInput.value = type;
|
||||||
}
|
blockDiv.appendChild(typeInput);
|
||||||
|
|
||||||
// Löschen-Button
|
|
||||||
const deleteBtn = document.createElement("button");
|
|
||||||
deleteBtn.type = "button";
|
|
||||||
deleteBtn.innerHTML = "✕";
|
|
||||||
deleteBtn.classList.add("delete-block-btn");
|
|
||||||
deleteBtn.addEventListener("click", () => {
|
|
||||||
// ANPASSUNG 2B: Logik hier komplett geleert. Das '✕' entfernt den Block jetzt nur noch sicher aus dem HTML.
|
|
||||||
blockDiv.remove();
|
|
||||||
});
|
|
||||||
blockDiv.appendChild(deleteBtn);
|
|
||||||
|
|
||||||
if (type === "text") {
|
if (type === "text") {
|
||||||
const textarea = document.createElement("textarea");
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.name = `blocks[${index}][text]`;
|
||||||
textarea.placeholder = "Schreibe deinen Textblock...";
|
textarea.placeholder = "Schreibe deinen Textblock...";
|
||||||
textarea.value = value;
|
textarea.value = value;
|
||||||
blockDiv.appendChild(textarea);
|
blockDiv.appendChild(textarea);
|
||||||
} else if (type === "image") {
|
} else if (type === "image") {
|
||||||
const fileInput = document.createElement("input");
|
const fileInput = document.createElement("input");
|
||||||
fileInput.type = "file";
|
fileInput.type = "file";
|
||||||
|
fileInput.name = `blocks[${index}][image]`;
|
||||||
fileInput.accept = "image/*";
|
fileInput.accept = "image/*";
|
||||||
|
|
||||||
const imgPreview = document.createElement("img");
|
|
||||||
imgPreview.style.maxWidth = "200px";
|
|
||||||
imgPreview.style.display = "block";
|
|
||||||
imgPreview.style.marginTop = "10px";
|
|
||||||
|
|
||||||
if (value && typeof value === 'string') {
|
|
||||||
if (value.startsWith('uploads/') || value.startsWith('data:image/')) {
|
|
||||||
imgPreview.src = value;
|
|
||||||
blockDiv.setAttribute("data-value", value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInput.addEventListener("change", function() {
|
|
||||||
if (this.files && this.files[0]) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = function(e) {
|
|
||||||
imgPreview.src = e.target.result;
|
|
||||||
blockDiv.setAttribute("data-value", e.target.result);
|
|
||||||
}
|
|
||||||
reader.readAsDataURL(this.files[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
blockDiv.appendChild(fileInput);
|
blockDiv.appendChild(fileInput);
|
||||||
blockDiv.appendChild(imgPreview);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteBtn = document.createElement("button");
|
||||||
|
deleteBtn.type = "submit";
|
||||||
|
deleteBtn.name = "editor_action";
|
||||||
|
deleteBtn.value = `delete_block:${index}`;
|
||||||
|
deleteBtn.classList.add("delete-block-btn");
|
||||||
|
deleteBtn.innerHTML = "✕";
|
||||||
|
blockDiv.appendChild(deleteBtn);
|
||||||
|
|
||||||
container.appendChild(blockDiv);
|
container.appendChild(blockDiv);
|
||||||
|
|
||||||
|
bindDeleteButton(blockDiv);
|
||||||
|
bindImageInput(blockDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
// beim Abschicken verbleibende Blöcke auslesen UND gelöschte Bilder ermitteln
|
// Löschen-Button eines Blocks lokal abfangen: entfernt den Block nur aus dem DOM,
|
||||||
form.addEventListener("submit", function(e) {
|
// statt das Formular zum Server zu senden.
|
||||||
const blocks = [];
|
function bindDeleteButton(blockDiv) {
|
||||||
const currentImages = [];
|
const btn = blockDiv.querySelector(".delete-block-btn");
|
||||||
|
if (!btn) return;
|
||||||
// alle aktuell im Formular verbliebenen Blöcke scannen
|
btn.addEventListener("click", (e) => {
|
||||||
container.querySelectorAll(".editor-block").forEach(blockDiv => {
|
e.preventDefault();
|
||||||
const type = blockDiv.getAttribute("data-type");
|
blockDiv.remove();
|
||||||
let value = "";
|
|
||||||
|
|
||||||
if (type === "text") {
|
|
||||||
value = blockDiv.querySelector("textarea").value;
|
|
||||||
} else if (type === "image") {
|
|
||||||
|
|
||||||
const imgTag = blockDiv.querySelector("img");
|
|
||||||
if (imgTag) {
|
|
||||||
const srcValue = imgTag.getAttribute("src") || "";
|
|
||||||
// Wenn es ein neues Bild ist, nutzen wir das data-value (Base64)
|
|
||||||
if (srcValue.startsWith('data:image/')) {
|
|
||||||
value = blockDiv.getAttribute("data-value") || "";
|
|
||||||
} else {
|
|
||||||
value = srcValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pfade sammeln, die der Nutzer NICHT gelöscht hat (für den Abgleich)
|
|
||||||
if (value && value.startsWith('uploads/')) {
|
|
||||||
currentImages.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blocks.push({ type: type, value: value });
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// das reguläre unsichtbare Content-Feld befüllen
|
// Zeigt bei Auswahl einer Bilddatei sofort eine Vorschau an. Rein optisch –
|
||||||
hiddenContentInput.value = JSON.stringify(blocks);
|
// der eigentliche Datei-Upload läuft nativ über das <input type="file">.
|
||||||
|
function bindImageInput(blockDiv) {
|
||||||
|
const fileInput = blockDiv.querySelector('input[type="file"]');
|
||||||
|
if (!fileInput) return;
|
||||||
|
|
||||||
// Differenz berechnen: Welche Bilder aus 'initialImages' fehlen in 'currentImages' ?
|
let imgPreview = blockDiv.querySelector(".block-image-preview");
|
||||||
const deletedImages = initialImages.filter(img => !currentImages.includes(img));
|
if (!imgPreview) {
|
||||||
|
imgPreview = document.createElement("img");
|
||||||
// das 'deleted_images'-Feld dynamisch befüllen und an den Controller senden
|
imgPreview.classList.add("block-image-preview");
|
||||||
let deletedInput = document.getElementById("deleted-images");
|
imgPreview.style.maxWidth = "200px";
|
||||||
if (!deletedInput) {
|
imgPreview.style.display = "none";
|
||||||
deletedInput = document.createElement("input");
|
imgPreview.style.marginTop = "10px";
|
||||||
deletedInput.type = "hidden";
|
blockDiv.insertBefore(imgPreview, fileInput.nextSibling);
|
||||||
deletedInput.id = "deleted-images";
|
|
||||||
deletedInput.name = "deleted_images";
|
|
||||||
form.appendChild(deletedInput);
|
|
||||||
}
|
}
|
||||||
deletedInput.value = JSON.stringify(deletedImages);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Existierende Blöcke laden (stellt alte Daten aus der Session wieder her)
|
fileInput.addEventListener("change", function () {
|
||||||
try {
|
if (this.files && this.files[0]) {
|
||||||
const initialBlocks = JSON.parse(hiddenContentInput.value.trim());
|
const reader = new FileReader();
|
||||||
if (Array.isArray(initialBlocks)) {
|
reader.onload = (e) => {
|
||||||
initialBlocks.forEach(b => {
|
imgPreview.src = e.target.result;
|
||||||
if (b.type === "image" && b.value && typeof b.value === 'string' && !b.value.startsWith('data:image/')) {
|
imgPreview.style.display = "block";
|
||||||
let cleanPath = b.value.trim().replace(/\\\//g, '/'); // Verwandelt \/ in /
|
};
|
||||||
|
reader.readAsDataURL(this.files[0]);
|
||||||
initialImages.push(cleanPath);
|
}
|
||||||
addBlockElement(b.type, cleanPath);
|
});
|
||||||
|
|
||||||
} else {
|
|
||||||
addBlockElement(b.type, b.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log("Erfolgreich registrierte Start-Bilder:", initialImages);
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
if (hiddenContentInput.value.trim() !== "") {
|
|
||||||
addBlockElement("text", hiddenContentInput.value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,4 +137,4 @@ if (document.readyState === "loading") {
|
|||||||
} else {
|
} else {
|
||||||
// Falls das DOM schon fertig geladen ist, führen wir es direkt aus
|
// Falls das DOM schon fertig geladen ist, führen wir es direkt aus
|
||||||
initEditor();
|
initEditor();
|
||||||
}
|
}
|
||||||
-116
@@ -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 += `<button type="button" class="s-res-page-btn ${activeClass}" data-page="${i}">${i}</button> `;
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,39 @@
|
|||||||
<?php
|
<?php
|
||||||
|
// Standardpfad
|
||||||
|
$pfad = $_GET["pfad"] ?? "home";
|
||||||
|
|
||||||
?>
|
if ($pfad === "logout") {
|
||||||
|
include_once "php/controller/logout-controller.php";
|
||||||
|
exit();
|
||||||
|
} elseif ($pfad === "deleteAccount") {
|
||||||
|
include_once "php/controller/deleteAccount-controller.php";
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pfad === "login") {
|
||||||
|
include_once "php/controller/login-controller.php";
|
||||||
|
} elseif ($pfad === "register") {
|
||||||
|
include_once "php/controller/register-controller.php";
|
||||||
|
} elseif ($pfad === "password-forgotten") {
|
||||||
|
include_once "php/controller/password-forgotten-controller.php";
|
||||||
|
} elseif ($pfad === "confirm-register") {
|
||||||
|
include_once "php/controller/confirm-register-controller.php";
|
||||||
|
} elseif ($pfad === "confirm-password") {
|
||||||
|
include_once "php/controller/confirm-password-controller.php";
|
||||||
|
} elseif ($pfad === "updateComment") {
|
||||||
|
include_once "php/controller/updateComment-controller.php";
|
||||||
|
} elseif($pfad === "deleteComment") {
|
||||||
|
include_once "php/controller/deleteComment-controller.php";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whitelist
|
||||||
|
$erlaubte_content_seiten = [
|
||||||
|
"accessibility", "confirm-password", "confirm-register", "createArticle",
|
||||||
|
"datenschutz", "home", "impressum", "login", "nutzungsbedingungen",
|
||||||
|
"password-forgotten", "profile", "register", "search-results",
|
||||||
|
"show-mail", "showArticle", "showCategory", "updateArticle"
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!in_array($pfad, $erlaubte_content_seiten)) {
|
||||||
|
$pfad = "404";
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
require_once '../model/ArticleManager.php';
|
require_once '../model/ArticleManager.php';
|
||||||
require_once '../model/UserManager.php';
|
require_once '../model/UserManager.php';
|
||||||
require_once '../model/Article.php';
|
require_once '../model/Article.php';
|
||||||
@@ -16,37 +17,12 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
|
|||||||
$_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';
|
|
||||||
$_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 = [];
|
$safeArrayResults = [];
|
||||||
foreach ($results as $obj) {
|
foreach ($results as $obj) {
|
||||||
$safeArrayResults[] = [
|
$safeArrayResults[] = [
|
||||||
@@ -60,25 +36,30 @@ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
|
|||||||
"likes" => $obj->getLikes(),
|
"likes" => $obj->getLikes(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$_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;
|
||||||
header("Location: ../../index.php?pfad=search-results&q=" . urlencode($search) . "&sort=" . urlencode($sort) . "&limit=" . $limit . "&page=" . $page);
|
|
||||||
exit();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
// 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();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Liefert den vollständigen, zuletzt gefundenen Ergebnis-Datensatz als JSON.
|
||||||
|
*
|
||||||
|
* Wird von js/search-results.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),
|
||||||
|
]);
|
||||||
@@ -2,66 +2,51 @@
|
|||||||
if (session_status() === PHP_SESSION_NONE) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
require_once 'php/model/Article.php';
|
require_once 'php/model/Article.php';
|
||||||
require_once 'php/model/ArticleManager.php';
|
require_once 'php/model/ArticleManager.php';
|
||||||
require_once 'php/model/UserManager.php';
|
require_once 'php/model/UserManager.php';
|
||||||
require_once 'php/validator/article-validator.php';
|
require_once 'php/validator/article-validator.php';
|
||||||
|
require_once __DIR__ . '/../../includes/resultsHelper.php';
|
||||||
|
|
||||||
|
if (isset($_GET["category"]) && !empty($_GET["category"]) && articleCategoryValidator($_GET["category"])) {
|
||||||
|
|
||||||
if (isset($_GET["category"]) && !empty($_GET["category"]) && articleCategoryValidator($_GET["category"])){
|
|
||||||
$category = $_GET["category"];
|
$category = $_GET["category"];
|
||||||
|
|
||||||
// Sortierung auslesen (Standard: alphabet)
|
|
||||||
$sortStyle = isset($_GET['sort']) ? trim($_GET['sort']) : 'alphabet';
|
|
||||||
|
|
||||||
// Aktuelle Seite auslesen
|
|
||||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
|
||||||
if ($page < 1) { $page = 1; }
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$articleManager = ArticleManager::getInstance();
|
$articleManager = ArticleManager::getInstance();
|
||||||
$userManager = UserManager::getInstance();
|
$userManager = UserManager::getInstance();
|
||||||
|
|
||||||
$allArticles = $articleManager->getArticlesByCategory($category);
|
$allArticles = $articleManager->getArticlesByCategory($category);
|
||||||
|
|
||||||
|
$safeArrayResults = [];
|
||||||
foreach ($allArticles as $article) {
|
foreach ($allArticles as $article) {
|
||||||
$article->setAuthor($userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"]);
|
$authorName = $userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"];
|
||||||
}
|
$safeArrayResults[] = [
|
||||||
$totalArticles = count($allArticles);
|
"id" => $article->getID(),
|
||||||
|
"title" => $article->getTitle(),
|
||||||
// --- SORTIERUNG LOGIK ---
|
"content" => $article->getContent(),
|
||||||
if ($sortStyle === 'alphabet') {
|
"author" => $authorName,
|
||||||
usort($allArticles, function($a, $b) {
|
"category" => $article->getCategory(),
|
||||||
return strcasecmp($a->getTitle(), $b->getTitle());
|
"tags" => $article->getTags(),
|
||||||
});
|
"creationDate" => $article->getCreationDate(),
|
||||||
} elseif ($sortStyle === 'likes') {
|
"likes" => $article->getLikes(),
|
||||||
usort($allArticles, function($a, $b) {
|
];
|
||||||
return $b->getLikes() <=> $a->getLikes(); // Absteigend nach Likes
|
|
||||||
});
|
|
||||||
} elseif ($sortStyle === 'newest') {
|
|
||||||
usort($allArticles, function($a, $b) {
|
|
||||||
return strtotime($b->getCreationDate()) <=> strtotime($a->getCreationDate()); // Neueste zuerst
|
|
||||||
});
|
|
||||||
} elseif ($sortStyle === 'oldest') {
|
|
||||||
usort($allArticles, function($a, $b) {
|
|
||||||
return strtotime($a->getCreationDate()) <=> strtotime($b->getCreationDate()); // Älteste zuerst
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paginierung konfigurieren
|
// Session dient hier (wie bei search-results) als Zwischenspeicher, damit
|
||||||
$limit = 10;
|
// js/showCategory.js den kompletten Datensatz per showCategory-data.php
|
||||||
$totalPages = ceil($totalArticles / $limit);
|
// nachladen kann, ohne die Kategorie-Abfrage ein zweites Mal auszuführen.
|
||||||
if ($page > $totalPages && $totalPages > 0) { $page = $totalPages; }
|
$_SESSION['category_results'] = $safeArrayResults;
|
||||||
|
$_SESSION['category_name'] = $category;
|
||||||
$offset = ($page - 1) * $limit;
|
|
||||||
$articles = array_slice($allArticles, $offset, $limit);
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$_SESSION["message"] = "internal_error";
|
$_SESSION["message"] = "internal_error";
|
||||||
include_once "content/404.php";
|
include_once "content/404.php";
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$_SESSION["message"] = "invalid_category";
|
$_SESSION["message"] = "invalid_category";
|
||||||
include_once "content/404.php";
|
include_once "content/404.php";
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
?>
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Liefert den vollständigen, zuletzt geladenen Kategorie-Datensatz als JSON.
|
||||||
|
*
|
||||||
|
* Wird von js/showCategory.js EINMALIG per fetch() geladen, damit Sortierung,
|
||||||
|
* Suche und Pagination danach komplett im Browser laufen können, ohne
|
||||||
|
* weitere Serveranfragen oder eine erneute Datenbank-Abfrage auszulösen
|
||||||
|
*
|
||||||
|
* Analog zu php/controller/search-results-data.php
|
||||||
|
*/
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../../includes/resultsHelper.php';
|
||||||
|
|
||||||
|
$rawResults = $_SESSION['category_results'] ?? [];
|
||||||
|
|
||||||
|
$payload = array_map(function ($item) {
|
||||||
|
return [
|
||||||
|
'id' => $item['id'],
|
||||||
|
'title' => $item['title'],
|
||||||
|
'content' => $item['content'] ?? '',
|
||||||
|
'author' => $item['author'],
|
||||||
|
'category' => $item['category'] ?? '',
|
||||||
|
'likes' => getLikeCount($item),
|
||||||
|
];
|
||||||
|
}, $rawResults);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'category' => $_SESSION['category_name'] ?? '',
|
||||||
|
'results' => array_values($payload),
|
||||||
|
]);
|
||||||
@@ -13,11 +13,73 @@ if (!isset($_SESSION["user_email"])) {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut die Blockliste aus den POST-Daten (blocks[i][type], blocks[i][text],
|
||||||
|
* blocks[i][existing_image]) und ggf. hochgeladenen Dateien (blocks[i][image])
|
||||||
|
* zusammen. Läuft bei JEDEM Submit (Zwischen-Schritt "Block hinzufügen/löschen"
|
||||||
|
* UND finales Speichern), damit neu ausgewählte Bilder in jedem Fall persistiert
|
||||||
|
* werden, bevor PHP die temporäre Upload-Datei nach Request-Ende verwirft.
|
||||||
|
*
|
||||||
|
* @param array $postBlocks $_POST['blocks'] ?? []
|
||||||
|
* @param array $fileBlocks $_FILES['blocks'] ?? []
|
||||||
|
* @param string $uploadDir absoluter Pfad zum uploads-Verzeichnis (mit trailing slash)
|
||||||
|
* @return array Liste von ['type' => 'text'|'image', 'value' => string]
|
||||||
|
*/
|
||||||
|
function rebuildBlocksFromPost(array $postBlocks, array $fileBlocks, string $uploadDir): array {
|
||||||
|
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||||
|
|
||||||
|
$keys = array_keys($postBlocks);
|
||||||
|
if (isset($fileBlocks['name']) && is_array($fileBlocks['name'])) {
|
||||||
|
$keys = array_unique(array_merge($keys, array_keys($fileBlocks['name'])));
|
||||||
|
}
|
||||||
|
sort($keys, SORT_NUMERIC);
|
||||||
|
|
||||||
|
$blocks = [];
|
||||||
|
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
$type = $postBlocks[$key]['type'] ?? null;
|
||||||
|
|
||||||
|
if ($type === 'text') {
|
||||||
|
$blocks[] = [
|
||||||
|
'type' => 'text',
|
||||||
|
'value' => $postBlocks[$key]['text'] ?? '',
|
||||||
|
];
|
||||||
|
} elseif ($type === 'image') {
|
||||||
|
// Vorbelegung: bereits vorhandenes Server-Bild (falls Datei nicht ersetzt wird)
|
||||||
|
$value = $postBlocks[$key]['existing_image'] ?? '';
|
||||||
|
|
||||||
|
$hasUpload = isset($fileBlocks['error'][$key]['image'])
|
||||||
|
&& $fileBlocks['error'][$key]['image'] === UPLOAD_ERR_OK;
|
||||||
|
|
||||||
|
if ($hasUpload) {
|
||||||
|
$tmpName = $fileBlocks['tmp_name'][$key]['image'];
|
||||||
|
$originalName = $fileBlocks['name'][$key]['image'];
|
||||||
|
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||||||
|
if (!in_array($extension, $allowedExtensions, true)) {
|
||||||
|
$extension = 'jpg';
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = 'img_' . uniqid() . '.' . $extension;
|
||||||
|
$destination = $uploadDir . $fileName;
|
||||||
|
|
||||||
|
if (move_uploaded_file($tmpName, $destination)) {
|
||||||
|
$value = 'uploads/' . $fileName;
|
||||||
|
}
|
||||||
|
// Bei Fehler: alter Wert (falls vorhanden) bleibt erhalten, Block wird nicht verworfen
|
||||||
|
}
|
||||||
|
|
||||||
|
$blocks[] = [
|
||||||
|
'type' => 'image',
|
||||||
|
'value' => $value,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// unbekannter/fehlender type -> Block wird ignoriert
|
||||||
|
}
|
||||||
|
|
||||||
|
return $blocks;
|
||||||
|
}
|
||||||
|
|
||||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||||
$_SESSION["old_title"] = $_POST["title"] ?? '';
|
|
||||||
$_SESSION["old_content"] = $_POST["content"] ?? '';
|
|
||||||
$_SESSION["old_category"] = $_POST["category"] ?? '';
|
|
||||||
$_SESSION["old_tags"] = $_POST["tags"] ?? '';
|
|
||||||
|
|
||||||
if (isset($_GET["id"]) && !empty($_GET["id"])) {
|
if (isset($_GET["id"]) && !empty($_GET["id"])) {
|
||||||
$id = $_GET["id"];
|
$id = $_GET["id"];
|
||||||
@@ -41,16 +103,54 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($_POST["title"]) ||!isset($_POST["content"]) || !isset($_POST["category"])){
|
$uploadDir = __DIR__ . '/../../uploads/';
|
||||||
|
if (!file_exists($uploadDir)) {
|
||||||
|
mkdir($uploadDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formularzustand (Titel/Tags/Kategorie/Blöcke) immer sichern, damit er nach einem
|
||||||
|
// Redirect (PRG-Pattern oder Validierungsfehler) wieder angezeigt werden kann.
|
||||||
|
$_SESSION["old_title"] = $_POST["title"] ?? '';
|
||||||
|
$_SESSION["old_tags"] = $_POST["tags"] ?? '';
|
||||||
|
$_SESSION["old_category"] = $_POST["category"] ?? '';
|
||||||
|
|
||||||
|
$blocks = rebuildBlocksFromPost($_POST['blocks'] ?? [], $_FILES['blocks'] ?? [], $uploadDir);
|
||||||
|
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Zwischenspeichern
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
if (isset($_POST['editor_action']) && $_POST['editor_action'] !== '') {
|
||||||
|
$action = $_POST['editor_action'];
|
||||||
|
|
||||||
|
if ($action === 'add_text') {
|
||||||
|
$blocks[] = ['type' => 'text', 'value' => ''];
|
||||||
|
} elseif ($action === 'add_image') {
|
||||||
|
$blocks[] = ['type' => 'image', 'value' => ''];
|
||||||
|
} elseif (str_starts_with($action, 'delete_block:')) {
|
||||||
|
$deleteIndex = (int) substr($action, strlen('delete_block:'));
|
||||||
|
unset($blocks[$deleteIndex]);
|
||||||
|
$blocks = array_values($blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
|
||||||
|
header("location: ../../content/updateArticle.php?id=$id");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Echtes Speichern
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
if (!isset($_POST["title"]) || !isset($_POST["category"])) {
|
||||||
$_SESSION["message"] = "missing_parameters";
|
$_SESSION["message"] = "missing_parameters";
|
||||||
header("location: ../../index.php?pfad=updateArticle&id=$id");
|
header("location: ../../index.php?pfad=updateArticle&id=$id");
|
||||||
exit();
|
exit();
|
||||||
}else{
|
} else {
|
||||||
$title = $_POST["title"];
|
$title = $_POST["title"];
|
||||||
$content = $_POST["content"];
|
$content = json_encode($blocks, JSON_UNESCAPED_UNICODE);
|
||||||
$author = $_SESSION["user_email"];
|
$author = $_SESSION["user_email"];
|
||||||
$category = $_POST["category"];
|
$category = $_POST["category"];
|
||||||
$tags = $_POST['tags'] ?? '';
|
$tags = $_POST['tags'] ?? '';
|
||||||
|
|
||||||
// -------------------------------- Validierung der Daten: -------------------------
|
// -------------------------------- Validierung der Daten: -------------------------
|
||||||
if (!articleTitleValidator($title)) {
|
if (!articleTitleValidator($title)) {
|
||||||
@@ -88,91 +188,51 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|||||||
$cleanedTags = implode(',', $cleanedTags);
|
$cleanedTags = implode(',', $cleanedTags);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------- Base64-Bilder speichern ---------------------------------------------
|
// ----------------- Verwaiste Bilder aufräumen -----------------
|
||||||
$blocks = json_decode($content, true);
|
// Bilder, die im alten (gespeicherten) Content vorkamen, im neuen aber nicht
|
||||||
$uploadDir = __DIR__ . '/../../uploads/';
|
// mehr referenziert werden, wurden vom Nutzer entfernt oder ersetzt -> löschen.
|
||||||
|
// TODO: Bilder, die innerhalb derselben Bearbeitungs-Sitzung neu hochgeladen und noch vor dem finalen Speichern wieder entfernt wurden, werden hierüber nicht erfasst (sie tauchten nie im alten Content auf) und bleiben als Datei liegen. Für eine vollständige Bereinigung würde sich ein
|
||||||
if (!file_exists($uploadDir)) {
|
$oldBlocks = json_decode($article->getContent(), true);
|
||||||
mkdir($uploadDir, 0755, true);
|
$oldImagePaths = [];
|
||||||
}
|
if (is_array($oldBlocks)) {
|
||||||
|
foreach ($oldBlocks as $oldBlock) {
|
||||||
// ----------------- Gelöschte Bilder über die JS-Löschliste entfernen ----------------- TODO: Gelöschte Bilder über die JS-Löschliste entfernen
|
if (($oldBlock['type'] ?? '') === 'image'
|
||||||
/*if (isset($_POST['deleted_images'])) {
|
&& !empty($oldBlock['value'])
|
||||||
$deletedImages = json_decode($_POST['deleted_images'], true);
|
&& is_string($oldBlock['value'])
|
||||||
|
&& str_starts_with($oldBlock['value'], 'uploads/')) {
|
||||||
// Wir ermitteln den physisch echten, absoluten Pfad zum uploads-Ordner auf der Festplatte
|
$oldImagePaths[] = $oldBlock['value'];
|
||||||
$uploadDir = realpath(__DIR__ . '/../../uploads') . DIRECTORY_SEPARATOR;
|
|
||||||
|
|
||||||
if (is_array($deletedImages)) {
|
|
||||||
foreach ($deletedImages as $imagePath) {
|
|
||||||
// Nur den reinen Dateinamen heraustrennen (z.B. img_65a123.jpg)
|
|
||||||
$filename = basename($imagePath);
|
|
||||||
$fullDeletePath = $uploadDir . $filename;
|
|
||||||
|
|
||||||
// Debugging & Löschen:
|
|
||||||
if (file_exists($fullDeletePath)) {
|
|
||||||
// Versuchen zu löschen. Wenn es fehlschlägt, Fehlermeldung erzwingen
|
|
||||||
if (!@unlink($fullDeletePath)) {
|
|
||||||
$error = error_get_last();
|
|
||||||
die("Datei existiert, aber PHP darf sie nicht löschen! Grund: " . $error['message']);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Wenn PHP die Datei an diesem Pfad nicht findet, brechen wir zum Debuggen ab
|
|
||||||
// die("PHP findet die Datei nicht unter dem Pfad: " . $fullDeletePath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}*/
|
|
||||||
|
|
||||||
// ----------------------- NEU hinzugefügte Base64-Bilder: --------------------------
|
|
||||||
if (is_array($blocks)) {
|
|
||||||
foreach ($blocks as &$block) {
|
|
||||||
// Prüfen, ob der Block ein Bild ist und ein NEUES Bild (Base64-Format) enthält
|
|
||||||
if (isset($block['type']) && isset($block['value']) && $block['type'] === 'image' && is_string($block['value'])) {
|
|
||||||
|
|
||||||
if (str_starts_with($block['value'], 'data:image/')) {
|
|
||||||
$parts = explode(',', $block['value']);
|
|
||||||
if (count($parts) >= 2) {
|
|
||||||
$metadata = $parts[0];
|
|
||||||
$base64Data = $parts[1];
|
|
||||||
|
|
||||||
preg_match('/data:image\/(?<extension>.*?);/', $metadata, $matches);
|
|
||||||
$extension = $matches['extension'] ?? 'jpg';
|
|
||||||
if ($extension === 'jpeg') { $extension = 'jpg'; }
|
|
||||||
|
|
||||||
$fileName = 'img_' . uniqid() . '.' . $extension;
|
|
||||||
$filePath = $uploadDir . $fileName;
|
|
||||||
|
|
||||||
if (file_put_contents($filePath, base64_decode($base64Data)) !== false) {
|
|
||||||
$block['value'] = 'uploads/' . $fileName;
|
|
||||||
} else {
|
|
||||||
$_SESSION["message"] = "image_upload_error";
|
|
||||||
header("location: ../../index.php?pfad=updateArticle&id=$id");
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unset($block);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aktualisiertes Array wieder in JSON konvertieren
|
$newImagePaths = [];
|
||||||
$finalContent = json_encode($blocks, JSON_UNESCAPED_UNICODE);
|
foreach ($blocks as $block) {
|
||||||
|
if (($block['type'] ?? '') === 'image' && !empty($block['value'])) {
|
||||||
|
$newImagePaths[] = $block['value'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$orphanedImages = array_diff($oldImagePaths, $newImagePaths);
|
||||||
|
foreach ($orphanedImages as $orphanedImage) {
|
||||||
|
$absolutePath = __DIR__ . '/../../' . $orphanedImage;
|
||||||
|
if (is_file($absolutePath)) {
|
||||||
|
@unlink($absolutePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
|
// ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
|
||||||
try {
|
try {
|
||||||
$articleManager = ArticleManager::getInstance();
|
$articleManager = ArticleManager::getInstance();
|
||||||
$article = $articleManager->getArticle($id);
|
$article = $articleManager->getArticle($id);
|
||||||
$article->setTitle($title);
|
$article->setTitle($title);
|
||||||
$article->setContent($finalContent);
|
$article->setContent($content);
|
||||||
$article->setCategory($category);
|
$article->setCategory($category);
|
||||||
$article->setTags($cleanedTags);
|
$article->setTags($cleanedTags);
|
||||||
$articleManager->updateArticle($id ,$article, $author);
|
$articleManager->updateArticle($id, $article, $author);
|
||||||
|
|
||||||
unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
|
unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
|
||||||
|
|
||||||
} catch (\Throwable $e){
|
} catch (\Throwable $e) {
|
||||||
$_SESSION["message"] = $e->getMessage();
|
$_SESSION["message"] = $e->getMessage();
|
||||||
header("location: ../../index.php?pfad=updateArticle&id=$id");
|
header("location: ../../index.php?pfad=updateArticle&id=$id");
|
||||||
exit();
|
exit();
|
||||||
|
|||||||
Reference in New Issue
Block a user