Split search into search.php; repertoire.php is index-only

This commit is contained in:
Pontoporeia
2026-04-07 15:01:30 +02:00
parent e96ec572be
commit 0c2276d5ad
7 changed files with 181 additions and 136 deletions

View File

@@ -1,6 +1,13 @@
# TODO
## Done
- [x] Split search logic into search.php
- [x] `public/search.php` — new page for text-query search results
- [x] `public/repertoire.php` — stripped to répertoire index only
- [x] `SearchController::handle()` split into `handleSearch()` + `handleRepertoire()`
- [x] Search bar (`header.php`, `search-bar.php`) now POSTs to `/search.php`
- [x] `tfe.php` `?query=` links updated to `/search.php`
- [x] Filter links (`?or[]=`, `?ap[]=`, `?fy[]=`, `?kw[]=`) stay on `repertoire.php`
- [x] TFE metadata values are hyperlinks to repertoire.php with correct filter/search params
- orientation → `or[]`, ap_program → `ap[]`, year → `fy[]`, keywords → `kw[]` (per keyword)
- languages, formats → `query=` (text search); jury members → `query=` (text search)

View File

@@ -5,91 +5,17 @@ require_once APP_ROOT . '/src/SearchController.php';
// Build controller (performs rate-limit check; exits with HTTP 429 if exceeded)
$ctrl = SearchController::create();
// Collect all view variables (may exit early if HTMX partial request)
extract($ctrl->handle());
// Collect all view variables for the répertoire index page
extract($ctrl->handleRepertoire());
?>
<?php include APP_ROOT . '/templates/head.php'; ?>
<?php include APP_ROOT . '/templates/header.php'; ?>
<?php if ($validationError): ?>
<div class="search-error">⚠ <?= htmlspecialchars($validationError) ?></div>
<?php endif; ?>
<?php if ($hasSearch): ?>
<!-- ── RESULTS VIEW ─────────────────────────────────── -->
<!-- Filter controls -->
<form class="search-controls" method="GET" action="repertoire.php">
<input type="hidden" name="query" value="<?= htmlspecialchars($_GET['query'] ?? '') ?>">
<label class="search-filter-label" for="filter-year">Année
<select class="search-filter-select" name="year" id="filter-year">
<option value="">Toutes</option>
<?php foreach ($years as $y): ?>
<option value="<?= (int)$y ?>" <?= (isset($_GET['year']) && $_GET['year'] == $y) ? 'selected' : '' ?>>
<?= (int)$y ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label class="search-filter-label" for="filter-orientation">Orientation
<select class="search-filter-select" name="orientation" id="filter-orientation">
<option value="">Toutes</option>
<?php foreach ($orientations as $o): ?>
<option value="<?= htmlspecialchars($o['name']) ?>"
<?= (isset($_GET['orientation']) && $_GET['orientation'] == $o['name']) ? 'selected' : '' ?>>
<?= htmlspecialchars($o['name']) ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label class="search-filter-label" for="filter-ap">AP
<select class="search-filter-select" name="ap_program" id="filter-ap">
<option value="">Tous</option>
<?php foreach ($apPrograms as $ap): ?>
<option value="<?= htmlspecialchars($ap['name']) ?>"
<?= (isset($_GET['ap_program']) && $_GET['ap_program'] == $ap['name']) ? 'selected' : '' ?>>
<?= htmlspecialchars($ap['name']) ?>
</option>
<?php endforeach; ?>
</select>
</label>
<button type="submit" class="search-apply-btn">Filtrer</button>
<a href="repertoire.php" class="search-reset-link">Réinitialiser</a>
</form>
<main class="search-main" id="main-content">
<output class="search-results-header" role="status"><?= $totalItems ?> résultat<?= $totalItems > 1 ? 's' : '' ?></output>
<?php if (!empty($results)): ?>
<ul class="results-grid">
<?php foreach ($results as $item): ?>
<li><a href="tfe.php?id=<?= (int)$item['id'] ?>" class="result-card">
<span class="result-card__authors"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
<span class="result-card__title"><?= htmlspecialchars($item['title']) ?></span>
<small class="result-card__meta"><?= htmlspecialchars($item['year']) ?><?php if (!empty($item['orientation'])): ?> · <?= htmlspecialchars($item['orientation']) ?><?php endif; ?></small>
</a></li>
<?php endforeach; ?>
</ul>
<?php include APP_ROOT . '/templates/partials/pagination.php'; ?>
<?php else: ?>
<p class="search-empty">Aucun résultat pour cette recherche.</p>
<?php endif; ?>
</main>
<?php else: ?>
<!-- ── RÉPERTOIRE INDEX VIEW ─────────────────────────── -->
<main class="search-main" id="main-content">
<h1 class="sr-only">Répertoire</h1>
<span id="rep-indicator" class="rep-indicator htmx-indicator" aria-hidden="true"></span>
<?php include APP_ROOT . '/templates/partials/repertoire-index.php'; ?>
</main>
<script src="/assets/js/htmx.min.js"></script>
<?php endif; ?>
<?php include APP_ROOT . '/templates/footer.php'; ?>

82
public/search.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
require_once __DIR__ . '/../config/bootstrap.php';
require_once APP_ROOT . '/src/SearchController.php';
// Build controller (performs rate-limit check; exits with HTTP 429 if exceeded)
$ctrl = SearchController::create();
// Collect all view variables for the search results page
extract($ctrl->handleSearch());
?>
<?php include APP_ROOT . '/templates/head.php'; ?>
<?php include APP_ROOT . '/templates/header.php'; ?>
<?php if ($validationError): ?>
<div class="search-error">⚠ <?= htmlspecialchars($validationError) ?></div>
<?php endif; ?>
<!-- Filter controls -->
<form class="search-controls" method="GET" action="search.php">
<input type="hidden" name="query" value="<?= htmlspecialchars($_GET['query'] ?? '') ?>">
<label class="search-filter-label" for="filter-year">Année
<select class="search-filter-select" name="year" id="filter-year">
<option value="">Toutes</option>
<?php foreach ($years as $y): ?>
<option value="<?= (int)$y ?>" <?= (isset($_GET['year']) && $_GET['year'] == $y) ? 'selected' : '' ?>>
<?= (int)$y ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label class="search-filter-label" for="filter-orientation">Orientation
<select class="search-filter-select" name="orientation" id="filter-orientation">
<option value="">Toutes</option>
<?php foreach ($orientations as $o): ?>
<option value="<?= htmlspecialchars($o['name']) ?>"
<?= (isset($_GET['orientation']) && $_GET['orientation'] == $o['name']) ? 'selected' : '' ?>>
<?= htmlspecialchars($o['name']) ?>
</option>
<?php endforeach; ?>
</select>
</label>
<label class="search-filter-label" for="filter-ap">AP
<select class="search-filter-select" name="ap_program" id="filter-ap">
<option value="">Tous</option>
<?php foreach ($apPrograms as $ap): ?>
<option value="<?= htmlspecialchars($ap['name']) ?>"
<?= (isset($_GET['ap_program']) && $_GET['ap_program'] == $ap['name']) ? 'selected' : '' ?>>
<?= htmlspecialchars($ap['name']) ?>
</option>
<?php endforeach; ?>
</select>
</label>
<button type="submit" class="search-apply-btn">Filtrer</button>
<a href="search.php?query=<?= urlencode($_GET['query'] ?? '') ?>" class="search-reset-link">Réinitialiser</a>
</form>
<main class="search-main" id="main-content">
<output class="search-results-header" role="status"><?= $totalItems ?> résultat<?= $totalItems > 1 ? 's' : '' ?></output>
<?php if (!empty($results)): ?>
<ul class="results-grid">
<?php foreach ($results as $item): ?>
<li><a href="tfe.php?id=<?= (int)$item['id'] ?>" class="result-card">
<span class="result-card__authors"><?= htmlspecialchars($item['authors'] ?? '') ?></span>
<span class="result-card__title"><?= htmlspecialchars($item['title']) ?></span>
<small class="result-card__meta"><?= htmlspecialchars($item['year']) ?><?php if (!empty($item['orientation'])): ?> · <?= htmlspecialchars($item['orientation']) ?><?php endif; ?></small>
</a></li>
<?php endforeach; ?>
</ul>
<?php include APP_ROOT . '/templates/partials/pagination.php'; ?>
<?php else: ?>
<p class="search-empty">Aucun résultat pour cette recherche.</p>
<?php endif; ?>
</main>
<?php include APP_ROOT . '/templates/footer.php'; ?>

View File

@@ -53,7 +53,7 @@ extract($ctrl->handle());
<dt>Langue :</dt>
<dd><?php
$langs = array_map('trim', explode(',', $data['languages']));
$langLinks = array_map(fn($l) => '<a href="/repertoire.php?query=' . urlencode($l) . '">' . htmlspecialchars($l) . '</a>', $langs);
$langLinks = array_map(fn($l) => '<a href="/search.php?query=' . urlencode($l) . '">' . htmlspecialchars($l) . '</a>', $langs);
echo implode(', ', $langLinks);
?></dd>
</div>
@@ -64,7 +64,7 @@ extract($ctrl->handle());
<dt>Format :</dt>
<dd><?php
$fmts = array_map('trim', explode(',', $data['formats']));
$fmtLinks = array_map(fn($f) => '<a href="/repertoire.php?query=' . urlencode($f) . '">' . htmlspecialchars($f) . '</a>', $fmts);
$fmtLinks = array_map(fn($f) => '<a href="/search.php?query=' . urlencode($f) . '">' . htmlspecialchars($f) . '</a>', $fmts);
echo implode(', ', $fmtLinks);
?></dd>
</div>
@@ -92,7 +92,7 @@ extract($ctrl->handle());
<div>
<dt>Promoteur·ice interne :</dt>
<dd><?php
$links = array_map(fn($n) => '<a href="/repertoire.php?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $promoteursInternes);
$links = array_map(fn($n) => '<a href="/search.php?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $promoteursInternes);
echo implode(', ', $links);
?></dd>
</div>
@@ -102,7 +102,7 @@ extract($ctrl->handle());
<div>
<dt>Promoteur·ice externe :</dt>
<dd><?php
$links = array_map(fn($n) => '<a href="/repertoire.php?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $promoteursExternes);
$links = array_map(fn($n) => '<a href="/search.php?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $promoteursExternes);
echo implode(', ', $links);
?></dd>
</div>
@@ -112,7 +112,7 @@ extract($ctrl->handle());
<div>
<dt>Président·e du jury :</dt>
<dd><?php
$links = array_map(fn($n) => '<a href="/repertoire.php?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $juryPresidents);
$links = array_map(fn($n) => '<a href="/search.php?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $juryPresidents);
echo implode(', ', $links);
?></dd>
</div>
@@ -122,7 +122,7 @@ extract($ctrl->handle());
<div>
<dt>Lecteur·ices :</dt>
<dd><?php
$links = array_map(fn($n) => '<a href="/repertoire.php?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $juryLecteurs);
$links = array_map(fn($n) => '<a href="/search.php?query=' . urlencode($n) . '">' . htmlspecialchars($n) . '</a>', $juryLecteurs);
echo implode(', ', $links);
?></dd>
</div>

View File

@@ -2,9 +2,11 @@
/**
* SearchController
*
* Handles all data-fetching logic for the public search / répertoire page.
* The entry point (public/repertoire.php) delegates to this class and receives
* a plain array of view variables ready for template inclusion.
* Handles all data-fetching logic for the public search and répertoire pages.
*
* Entry points:
* - public/search.php calls handleSearch() — text-query results
* - public/repertoire.php calls handleRepertoire() — filter index + HTMX swaps
*
* Responsibilities:
* - Rate-limit enforcement (returns early HTTP 429 response when needed)
@@ -13,8 +15,8 @@
* - OG / meta tag assembly
* - HTMX partial response for repertoire filter swaps
*
* The class has NO output side-effects; all template rendering stays in
* public/repertoire.php so the view layer remains easy to inspect and modify.
* The class has NO output side-effects; all template rendering stays in the
* respective public/*.php files so the view layer remains easy to inspect.
* Exception: renderRepertoirePartial() exits early for HTMX requests.
*/
class SearchController
@@ -59,21 +61,17 @@ class SearchController
return new self(Database::getInstance(), $rateLimit);
}
// ── Main entry point ─────────────────────────────────────────────────────
// ── Entry points ──────────────────────────────────────────────────────────
/**
* Process the current request and return all variables needed by the view.
* Handle the search results page (public/search.php).
* Requires a ?query= parameter; always returns search-result view variables.
*
* @return array<string, mixed>
*/
public function handle(): array
public function handleSearch(): array
{
$isHtmx = !empty($_SERVER['HTTP_HX_REQUEST']);
$searchParams = $this->collectSearchParams();
$hasSearch = !empty($searchParams);
$activeFilters = $this->collectFilterParams();
$page = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1;
$offset = ($page - 1) * self::ITEMS_PER_PAGE;
$validationError = null;
@@ -81,25 +79,75 @@ class SearchController
$results = [];
$totalItems = 0;
$totalPages = 0;
$repData = null;
// For search filter dropdowns (text search mode only)
$years = [];
$orientations = [];
$apPrograms = [];
try {
if ($hasSearch) {
$results = $this->db->searchTheses($searchParams, self::ITEMS_PER_PAGE, $offset);
$totalItems = $this->db->countSearchResults($searchParams);
$totalPages = (int) ceil($totalItems / self::ITEMS_PER_PAGE);
$years = $this->db->getAvailableYears();
$orientations = $this->db->getAllOrientations();
$apPrograms = $this->db->getAllAPPrograms();
} else {
// Repertoire index: compute filter data (all columns + matched flags)
$repData = $this->db->getRepertoireFilterData($activeFilters);
} catch (InvalidArgumentException $e) {
$validationError = $e->getMessage();
} catch (Exception $e) {
error_log('SearchController: ' . $e->getMessage());
$validationError = 'Une erreur est survenue.';
}
// Preserve all active params, strip 'page' (pagination partial adds it)
$baseParams = array_diff_key($_GET, ['page' => '']);
$query = $_GET['query'] ?? '';
return [
'searchParams' => $searchParams,
'page' => $page,
'totalItems' => $totalItems,
'totalPages' => $totalPages,
'results' => $results,
'validationError' => $validationError,
'baseParams' => $baseParams,
// Filter dropdowns
'years' => $years,
'orientations' => $orientations,
'apPrograms' => $apPrograms,
// Page meta
'searchBarValue' => $query,
'pageTitle' => $query !== '' ? 'Recherche : ' . $query . ' Posterg' : 'Recherche Posterg',
'metaDescription' => "Résultats de recherche dans le répertoire des TFE de l'erg.",
'ogTags' => [
'type' => 'website',
'title' => 'Recherche Posterg',
'description' => "Résultats de recherche dans le répertoire des TFE de l'erg.",
'url' => 'https://posterg.erg.be/search.php',
'site_name' => 'Posterg ERG',
],
'currentNav' => 'repertoire',
'extraCss' => ['/assets/css/search.css'],
'bodyClass' => 'search-body',
];
}
/**
* Handle the répertoire index page (public/repertoire.php).
* Serves the filter-column index; HTMX partial swaps are handled here too.
*
* @return array<string, mixed>
*/
public function handleRepertoire(): array
{
$isHtmx = !empty($_SERVER['HTTP_HX_REQUEST']);
$activeFilters = $this->collectFilterParams();
$repData = null;
$validationError = null;
try {
$repData = $this->db->getRepertoireFilterData($activeFilters);
} catch (InvalidArgumentException $e) {
$validationError = $e->getMessage();
} catch (Exception $e) {
@@ -108,36 +156,18 @@ class SearchController
}
// HTMX partial: render just the index div and exit
if ($isHtmx && !$hasSearch && $repData !== null) {
if ($isHtmx && $repData !== null) {
$this->renderRepertoirePartial($repData, $activeFilters);
}
// Preserve all active search/filter params, strip 'page' (pagination partial adds it)
$baseParams = array_diff_key($_GET, ['page' => '']);
return [
// Search state
'searchParams' => $searchParams,
'hasSearch' => $hasSearch,
'page' => $page,
'totalItems' => $totalItems,
'totalPages' => $totalPages,
'results' => $results,
'validationError' => $validationError,
'baseParams' => $baseParams,
// Repertoire filter state
'repData' => $repData,
'activeFilters' => $activeFilters,
'isHtmx' => $isHtmx,
// Search filter dropdowns (text search mode only)
'years' => $years,
'orientations' => $orientations,
'apPrograms' => $apPrograms,
'validationError' => $validationError,
// Page meta
'searchBarValue' => $_GET['query'] ?? '',
'searchBarValue' => '',
'pageTitle' => 'Répertoire Posterg',
'metaDescription' => "Parcourez le répertoire des mémoires de fin d'études (TFE) de l'erg École de Recherches Graphiques de Bruxelles.",
'ogTags' => [

View File

@@ -61,7 +61,7 @@ $_thesisId = $_GET['id'] ?? null;
$searchBarValue = $searchBarValue ?? $_GET['query'] ?? '';
?>
<div class="header-search-wrap">
<form method="GET" action="/repertoire.php"
<form method="GET" action="/search.php"
role="search" aria-label="Recherche">
<label for="site-search-input" class="sr-only">Recherche</label>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"

View File

@@ -3,7 +3,7 @@
// $searchValue: current search query (optional)
$_sbValue = $searchBarValue ?? $_GET['query'] ?? '';
?>
<form method="GET" action="/repertoire.php"
<form method="GET" action="/search.php"
role="search" aria-label="Recherche">
<label for="site-search-input" class="sr-only">Recherche</label>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"