Files
xamxam/public/search.php
Pontoporeia b8529f7abe fix: WCAG 2.1 AA contrast, mobile répertoire layout, and pagination accessibility
Contrast failures (WCAG 1.4.3):
- common.css: remove opacity:0.92 from .site-nav__link (was 4.05:1, now 4.87:1 white-on-purple)
- common.css: placeholder colour #aaa → #767676 (2.32:1 → 4.54:1 on white)
- main.css: filter-info and clear-filter text var(--purple) → var(--purple-dark) (#9557b5 → #7b3fa0, 4.08 → 5.7:1)
- index.php: gradient card lighter stop L=65% → L=40%, darker stop L=45% → L=28%; white text now passes 4.5:1 across all hues

Non-text contrast (WCAG 1.4.11):
- search.css: search-filter <select> border #ddd → #949494 (1.6:1 → 3.0:1 on white)
- admin.css: --admin-border #333#555 (input bottom-border on #1a1a1a: 1.8:1 → 3.1:1)
- admin.css: --admin-text-muted #888#969696 (4.38:1 → 4.54:1 on #242424)

Mobile layout (WCAG 1.4.10 Reflow):
- search.css: add @media (max-width:768px) to collapse répertoire 4-column grid to single column;
  columns switch from right-border to bottom-border separators

Keyboard / screen reader (WCAG 2.1.1, 2.4.4):
- index.php: add aria-label (Première/Précédente/Suivante/Dernière page) and aria-disabled+tabindex=-1
  on disabled pagination links
- templates/search-bar.php: add aria-hidden=true and focusable=false to decorative SVG magnifier

Language (WCAG 3.1.1):
- search.php: add lang=fr to <html> in 429 rate-limit response
2026-03-28 16:52:45 +01:00

244 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/../config/bootstrap.php';
require_once APP_ROOT . '/src/Database.php';
require_once APP_ROOT . '/src/RateLimit.php';
// Rate limiting
$rateLimit = new RateLimit(30, 60);
if (!$rateLimit->check()) {
http_response_code(429);
header('Retry-After: ' . $rateLimit->getResetTime());
echo '<!DOCTYPE html><html lang="fr"><body><h1>Trop de requêtes</h1><p>Réessayez dans ' . $rateLimit->getResetTime() . ' secondes.</p></body></html>';
exit;
}
$rateLimit->sendHeaders();
if (rand(1, 100) === 1) $rateLimit->cleanup();
// Collect search/filter params
$searchParams = [];
if (!empty($_GET['query'])) $searchParams['query'] = trim($_GET['query']);
if (!empty($_GET['year'])) $searchParams['year'] = intval($_GET['year']);
if (!empty($_GET['orientation'])) $searchParams['orientation'] = $_GET['orientation'];
if (!empty($_GET['ap_program'])) $searchParams['ap_program'] = $_GET['ap_program'];
if (!empty($_GET['keyword'])) $searchParams['keyword'] = $_GET['keyword'];
$hasSearch = !empty($searchParams);
$page = isset($_GET['page']) ? intval($_GET['page']) : 1;
$itemsPerPage = 30;
$validationError = null;
try {
$db = Database::getInstance();
$offset = ($page - 1) * $itemsPerPage;
if ($hasSearch) {
$results = $db->searchTheses($searchParams, $itemsPerPage, $offset);
$totalItems = $db->countSearchResults($searchParams);
$totalPages = ceil($totalItems / $itemsPerPage);
} else {
$results = [];
$totalItems = 0;
$totalPages = 0;
}
$years = $db->getAvailableYears();
$orientations = $db->getAllOrientations();
$apPrograms = $db->getAllAPPrograms();
$keywords = $db->getUsedTags();
// Fetch id+authors only — lean query bypassing the fat v_theses_public view
$students = $db->getPublishedAuthors();
} catch (InvalidArgumentException $e) {
$validationError = $e->getMessage();
$results = []; $totalItems = 0; $totalPages = 0;
$years = []; $orientations = []; $apPrograms = []; $keywords = []; $students = [];
} catch (Exception $e) {
error_log("Search error: " . $e->getMessage());
$validationError = "Une erreur est survenue.";
$results = []; $totalItems = 0; $totalPages = 0;
$years = []; $orientations = []; $apPrograms = []; $keywords = []; $students = [];
}
$currentNav = 'repertoire';
$searchBarValue = $_GET['query'] ?? '';
$pageTitle = 'Répertoire Posterg';
$extraCss = ['assets/search.css'];
?>
<?php include APP_ROOT . '/templates/public/head.php'; ?>
<body class="search-body">
<a href="#main-content" class="skip-link">Aller au contenu principal</a>
<?php include APP_ROOT . '/templates/nav.php'; ?>
<?php include APP_ROOT . '/templates/search-bar.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="search.php">
<input type="hidden" name="query" value="<?= htmlspecialchars($_GET['query'] ?? '') ?>">
<div class="search-filter-group">
<span class="search-filter-label">Année</span>
<select class="search-filter-select" name="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>
</div>
<div class="search-filter-group">
<span class="search-filter-label">Orientation</span>
<select class="search-filter-select" name="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>
</div>
<div class="search-filter-group">
<span class="search-filter-label">AP</span>
<select class="search-filter-select" name="ap_program">
<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>
</div>
<button type="submit" class="search-apply-btn">Filtrer</button>
<a href="search.php" class="search-reset-link">Réinitialiser</a>
</form>
<main class="search-main" id="main-content">
<div class="search-results-view">
<p class="search-results-header"><?= $totalItems ?> résultat<?= $totalItems > 1 ? 's' : '' ?></p>
<?php if (!empty($results)): ?>
<div class="results-grid">
<?php foreach ($results as $item): ?>
<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>
<span class="result-card__meta"><?= htmlspecialchars($item['year']) ?><?php if (!empty($item['orientation'])): ?> · <?= htmlspecialchars($item['orientation']) ?><?php endif; ?></span>
</a>
<?php endforeach; ?>
</div>
<?php if ($totalPages > 1): ?>
<div class="pagination-wrap">
<a href="?<?= http_build_query(array_merge($_GET, ['page' => max(1, $page - 1)])) ?>"
class="pagination-btn<?= $page <= 1 ? ' disabled' : '' ?>"
<?= $page <= 1 ? 'aria-disabled="true" tabindex="-1"' : '' ?>
aria-label="Page précédente"></a>
<span class="pagination-info"><?= $page ?> / <?= $totalPages ?></span>
<a href="?<?= http_build_query(array_merge($_GET, ['page' => min($totalPages, $page + 1)])) ?>"
class="pagination-btn<?= $page >= $totalPages ? ' disabled' : '' ?>"
<?= $page >= $totalPages ? 'aria-disabled="true" tabindex="-1"' : '' ?>
aria-label="Page suivante"></a>
</div>
<?php endif; ?>
<?php else: ?>
<p class="search-empty">Aucun résultat pour cette recherche.</p>
<?php endif; ?>
</div>
</main>
<?php else: ?>
<!-- ── RÉPERTOIRE INDEX VIEW ─────────────────────────── -->
<main class="search-main" id="main-content">
<div class="repertoire-index">
<!-- ANNÉES -->
<div class="repertoire-col">
<h2 class="repertoire-col__header">Années</h2>
<?php foreach ($years as $y): ?>
<a href="search.php?year=<?= (int)$y ?>"
class="year-index-item <?= (isset($_GET['year']) && $_GET['year'] == $y) ? 'active' : '' ?>">
<?= (int)$y ?>
</a>
<?php endforeach; ?>
</div>
<!-- CATÉGORIES -->
<div class="repertoire-col">
<h2 class="repertoire-col__header">Catégories</h2>
<?php if (!empty($orientations)): ?>
<span class="cat-index-label">Orientation</span>
<?php foreach ($orientations as $o): ?>
<a href="search.php?orientation=<?= urlencode($o['name']) ?>"
class="cat-index-item <?= (isset($_GET['orientation']) && $_GET['orientation'] == $o['name']) ? 'active' : '' ?>">
<?= htmlspecialchars($o['name']) ?>
</a>
<?php endforeach; ?>
<?php endif; ?>
<?php if (!empty($apPrograms)): ?>
<span class="cat-index-label">Ateliers Pluridisciplinaires</span>
<?php foreach ($apPrograms as $ap): ?>
<a href="search.php?ap_program=<?= urlencode($ap['name']) ?>"
class="cat-index-item <?= (isset($_GET['ap_program']) && $_GET['ap_program'] == $ap['name']) ? 'active' : '' ?>">
<?= htmlspecialchars($ap['name']) ?>
</a>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- ÉTUDIANTES -->
<div class="repertoire-col">
<h2 class="repertoire-col__header">Étudiantes</h2>
<?php
// Build unique author → thesis list
$authorMap = [];
foreach ($students as $s) {
if (empty($s['authors'])) continue;
$names = explode(',', $s['authors']);
foreach ($names as $name) {
$name = trim($name);
if ($name && !isset($authorMap[$name])) {
$authorMap[$name] = $s['id'];
}
}
}
ksort($authorMap);
foreach ($authorMap as $name => $id): ?>
<a href="tfe.php?id=<?= (int)$id ?>" class="student-index-item">
<?= htmlspecialchars($name) ?>
</a>
<?php endforeach; ?>
</div>
<!-- MOTS-CLÉS -->
<div class="repertoire-col">
<h2 class="repertoire-col__header">Mots-clés</h2>
<?php foreach ($keywords as $kw): ?>
<a href="search.php?keyword=<?= urlencode($kw['name']) ?>"
class="keyword-index-item <?= (isset($_GET['keyword']) && $_GET['keyword'] == $kw['name']) ? 'active' : '' ?>">
<?= htmlspecialchars($kw['name']) ?>
</a>
<?php endforeach; ?>
</div>
</div>
</main>
<?php endif; ?>
</body>
</html>