répertoire: rename search.php, 6-column layout, HTMX filter, faded entries disabled, URL-shareable

This commit is contained in:
Pontoporeia
2026-04-07 13:57:29 +02:00
parent 088324cb80
commit 572ef75a1e
10 changed files with 522 additions and 197 deletions

View File

@@ -3,7 +3,7 @@
* SearchController
*
* Handles all data-fetching logic for the public search / répertoire page.
* The entry point (public/search.php) delegates to this class and receives
* The entry point (public/repertoire.php) delegates to this class and receives
* a plain array of view variables ready for template inclusion.
*
* Responsibilities:
@@ -11,9 +11,11 @@
* - GET parameter sanitisation and validation
* - Database queries (search + index listings)
* - OG / meta tag assembly
* - HTMX partial response for repertoire filter swaps
*
* The class has NO output side-effects; all template rendering stays in
* public/search.php so the view layer remains easy to inspect and modify.
* public/repertoire.php so the view layer remains easy to inspect and modify.
* Exception: renderRepertoirePartial() exits early for HTMX requests.
*/
class SearchController
{
@@ -66,36 +68,38 @@ class SearchController
*/
public function handle(): array
{
$isHtmx = !empty($_SERVER['HTTP_HX_REQUEST']);
$searchParams = $this->collectSearchParams();
$hasSearch = !empty($searchParams);
$page = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1;
$offset = ($page - 1) * self::ITEMS_PER_PAGE;
$activeFilters = $this->collectFilterParams();
$page = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1;
$offset = ($page - 1) * self::ITEMS_PER_PAGE;
$validationError = null;
$results = [];
$totalItems = 0;
$totalPages = 0;
$repData = null;
// For search filter dropdowns (text search mode only)
$years = [];
$orientations = [];
$apPrograms = [];
$keywords = [];
$students = [];
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);
}
$years = $this->db->getAvailableYears();
$orientations = $this->db->getAllOrientations();
$apPrograms = $this->db->getAllAPPrograms();
$keywords = $this->db->getUsedTags();
// Fetch id+authors only — lean query bypassing the fat v_theses_public view
$students = $this->db->getPublishedAuthors();
} catch (InvalidArgumentException $e) {
$validationError = $e->getMessage();
} catch (Exception $e) {
@@ -103,8 +107,10 @@ class SearchController
$validationError = 'Une erreur est survenue.';
}
// Build the author index map (répertoire index view)
$authorMap = $this->buildAuthorMap($students);
// HTMX partial: render just the index div and exit
if ($isHtmx && !$hasSearch && $repData !== null) {
$this->renderRepertoirePartial($repData, $activeFilters);
}
// Preserve all active search/filter params, strip 'page' (pagination partial adds it)
$baseParams = array_diff_key($_GET, ['page' => '']);
@@ -120,22 +126,25 @@ class SearchController
'validationError' => $validationError,
'baseParams' => $baseParams,
// Filter / index data
// Repertoire filter state
'repData' => $repData,
'activeFilters' => $activeFilters,
'isHtmx' => $isHtmx,
// Search filter dropdowns (text search mode only)
'years' => $years,
'orientations' => $orientations,
'apPrograms' => $apPrograms,
'keywords' => $keywords,
'authorMap' => $authorMap,
// Page meta
'searchBarValue' => $_GET['query'] ?? '',
'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. Recherche par année, orientation, atelier et mots-clés.',
'metaDescription' => "Parcourez le répertoire des mémoires de fin d'études (TFE) de l'erg École de Recherches Graphiques de Bruxelles.",
'ogTags' => [
'type' => 'website',
'title' => 'Répertoire Posterg',
'description' => 'Parcourez le répertoire des mémoires de fin d\'études (TFE) de l\'erg École de Recherches Graphiques de Bruxelles. Recherche par année, orientation, atelier et mots-clés.',
'url' => 'https://posterg.erg.be/search.php',
'description' => "Parcourez le répertoire des mémoires de fin d'études (TFE) de l'erg École de Recherches Graphiques de Bruxelles.",
'url' => 'https://posterg.erg.be/repertoire.php',
'site_name' => 'Posterg ERG',
],
'currentNav' => 'repertoire',
@@ -147,7 +156,58 @@ class SearchController
// ── Private helpers ───────────────────────────────────────────────────────
/**
* Sanitise and collect valid search parameters from $_GET.
* Render the repertoire index partial and exit (for HTMX swaps).
* Never returns.
*/
private function renderRepertoirePartial(array $repData, array $activeFilters): never
{
header('Content-Type: text/html; charset=UTF-8');
$isHtmx = true;
include APP_ROOT . '/templates/partials/repertoire-index.php';
exit;
}
/**
* Collect and sanitise repertoire filter params from $_GET.
* Params: fy[] (years), ap[] (AP programs), or[] (orientations),
* fi[] (finality types), kw[] (keywords)
*
* @return array{years:int[], ap:string[], or:string[], fi:string[], kw:string[]}
*/
private function collectFilterParams(): array
{
$sanitiseStrings = function(mixed $raw, int $maxLen = 100): array {
if (!is_array($raw)) return [];
$out = [];
foreach ($raw as $v) {
$v = trim((string)$v);
if ($v !== '' && mb_strlen($v) <= $maxLen) {
$out[] = $v;
}
}
return array_values(array_unique($out));
};
$years = [];
if (!empty($_GET['fy']) && is_array($_GET['fy'])) {
foreach ($_GET['fy'] as $y) {
$y = (int)$y;
if ($y >= 1900 && $y <= 2100) $years[] = $y;
}
$years = array_values(array_unique($years));
}
return [
'years' => $years,
'ap' => $sanitiseStrings($_GET['ap'] ?? []),
'or' => $sanitiseStrings($_GET['or'] ?? []),
'fi' => $sanitiseStrings($_GET['fi'] ?? []),
'kw' => $sanitiseStrings($_GET['kw'] ?? []),
];
}
/**
* Sanitise and collect valid text search parameters from $_GET.
*
* @return array<string, mixed>
*/
@@ -174,34 +234,6 @@ class SearchController
return $params;
}
/**
* Build an alphabetically-sorted author → thesis-id map from the
* published-authors list. Each author name maps to their first thesis id.
*
* @param array<int, array{id: int, authors: string}> $students
* @return array<string, int>
*/
private function buildAuthorMap(array $students): array
{
$map = [];
foreach ($students as $s) {
if (empty($s['authors'])) {
continue;
}
foreach (explode(',', $s['authors']) as $name) {
$name = trim($name);
if ($name !== '' && !isset($map[$name])) {
$map[$name] = (int) $s['id'];
}
}
}
ksort($map);
return $map;
}
// ── Rate-limit response ───────────────────────────────────────────────────
/**