mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-07 03:29:19 +02:00
répertoire: rename search.php, 6-column layout, HTMX filter, faded entries disabled, URL-shareable
This commit is contained in:
1
TODO.md
1
TODO.md
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Completed
|
## Completed
|
||||||
|
|
||||||
|
- [x] Répertoire: rename search.php → repertoire.php, 6-column layout (AP + Orientations split, Finalité du Master added), HTMX server-side intersection filter, faded entries disabled, URL-shareable state
|
||||||
- [x] Match Accueil.png mockup
|
- [x] Match Accueil.png mockup
|
||||||
- Nav: brand → "Xamxam", add Répertoire left, Licences/À Propos right
|
- Nav: brand → "Xamxam", add Répertoire left, Licences/À Propos right
|
||||||
- Search bar: full-width below nav (not inline)
|
- Search bar: full-width below nav (not inline)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* ============================================================
|
/* ============================================================
|
||||||
RÉPERTOIRE / SEARCH PAGE (search.php)
|
RÉPERTOIRE / SEARCH PAGE (repertoire.php)
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
@import url("./variables.css");
|
@import url("./variables.css");
|
||||||
@@ -18,16 +18,33 @@
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 4-column index layout ---- */
|
/* ---- 6-column index layout ---- */
|
||||||
.repertoire-index {
|
.repertoire-index {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 2fr 2fr 1.5fr;
|
grid-template-columns: 0.7fr 1.2fr 1.4fr 0.9fr 1.4fr 1fr;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
padding: 0 1.5rem;
|
padding: 0 1.5rem;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 1024px) {
|
||||||
|
.repertoire-index {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
padding: 0 1rem;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repertoire-col {
|
||||||
|
border-right: 1px solid var(--border-secondary);
|
||||||
|
border-bottom: 1px solid var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repertoire-col:nth-child(3n) {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
.repertoire-index {
|
.repertoire-index {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
@@ -72,50 +89,85 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Years column - big bold numbers */
|
/* ---- rep-entry: shared base (button + link variants) ---- */
|
||||||
.repertoire-col:first-child ul a {
|
.rep-entry {
|
||||||
display: block;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.1rem 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.4;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s, opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rep-entry:hover {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Link variant (students col) — no underline by default */
|
||||||
|
.rep-entry--link {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selected: accent color */
|
||||||
|
.rep-entry--selected {
|
||||||
|
color: var(--accent-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Faded/disabled: muted, not interactive */
|
||||||
|
.rep-entry--faded {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Years column — big bold numbers */
|
||||||
|
.repertoire-col[data-col="years"] .rep-entry {
|
||||||
font-size: 2.2rem;
|
font-size: 2.2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
color: var(--text-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 0.1rem 0;
|
|
||||||
transition: color 0.15s;
|
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
|
padding: 0.05rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repertoire-col:first-child ul a:hover,
|
/* Empty state in students column */
|
||||||
.repertoire-col:first-child ul a[aria-current] {
|
.rep-empty {
|
||||||
color: var(--accent-primary);
|
color: var(--text-tertiary);
|
||||||
}
|
font-size: 0.9rem;
|
||||||
|
|
||||||
/* Categories column */
|
|
||||||
.cat-index-label {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-weight: 400;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.15rem;
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Categories, students, keywords columns — shared link style */
|
|
||||||
.repertoire-col:not(:first-child) ul a {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 0.1rem 0;
|
padding: 0.1rem 0;
|
||||||
line-height: 1.4;
|
|
||||||
transition: color 0.15s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.repertoire-col:not(:first-child) ul a:hover,
|
/* ---- HTMX loading indicator ---- */
|
||||||
.repertoire-col:not(:first-child) ul a[aria-current] {
|
.rep-indicator {
|
||||||
color: var(--accent-primary);
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--accent-primary);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rep-indicator.htmx-request {
|
||||||
|
opacity: 1;
|
||||||
|
animation: rep-progress 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rep-progress {
|
||||||
|
0% { transform: scaleX(0); transform-origin: left; }
|
||||||
|
50% { transform: scaleX(0.7); transform-origin: left; }
|
||||||
|
100% { transform: scaleX(1); transform-origin: left; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Search results view (grid) ---- */
|
/* ---- Search results view (grid) ---- */
|
||||||
@@ -165,35 +217,6 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toggle button (index/results) */
|
|
||||||
.view-toggle {
|
|
||||||
display: flex;
|
|
||||||
gap: 0;
|
|
||||||
border: 1px solid var(--border-secondary);
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-toggle__btn {
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-toggle__btn.active,
|
|
||||||
.view-toggle__btn:hover {
|
|
||||||
background: var(--accent-primary);
|
|
||||||
color: var(--accent-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Search controls bar */
|
/* Search controls bar */
|
||||||
.search-controls {
|
.search-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -205,7 +228,6 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* label now wraps the select directly — flex aligns label text + select */
|
|
||||||
.search-filter-label {
|
.search-filter-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -256,7 +278,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search results pagination — reuses same token names as main.css */
|
/* Search results pagination */
|
||||||
.pagination-wrap {
|
.pagination-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
1
public/assets/js/htmx.min.js
vendored
Normal file
1
public/assets/js/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@ require_once APP_ROOT . '/src/SearchController.php';
|
|||||||
// Build controller (performs rate-limit check; exits with HTTP 429 if exceeded)
|
// Build controller (performs rate-limit check; exits with HTTP 429 if exceeded)
|
||||||
$ctrl = SearchController::create();
|
$ctrl = SearchController::create();
|
||||||
|
|
||||||
// Collect all view variables
|
// Collect all view variables (may exit early if HTMX partial request)
|
||||||
extract($ctrl->handle());
|
extract($ctrl->handle());
|
||||||
?>
|
?>
|
||||||
<?php include APP_ROOT . '/templates/head.php'; ?>
|
<?php include APP_ROOT . '/templates/head.php'; ?>
|
||||||
@@ -19,7 +19,7 @@ extract($ctrl->handle());
|
|||||||
<!-- ── RESULTS VIEW ─────────────────────────────────── -->
|
<!-- ── RESULTS VIEW ─────────────────────────────────── -->
|
||||||
|
|
||||||
<!-- Filter controls -->
|
<!-- Filter controls -->
|
||||||
<form class="search-controls" method="GET" action="search.php">
|
<form class="search-controls" method="GET" action="repertoire.php">
|
||||||
<input type="hidden" name="query" value="<?= htmlspecialchars($_GET['query'] ?? '') ?>">
|
<input type="hidden" name="query" value="<?= htmlspecialchars($_GET['query'] ?? '') ?>">
|
||||||
|
|
||||||
<label class="search-filter-label" for="filter-year">Année
|
<label class="search-filter-label" for="filter-year">Année
|
||||||
@@ -58,7 +58,7 @@ extract($ctrl->handle());
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button type="submit" class="search-apply-btn">Filtrer</button>
|
<button type="submit" class="search-apply-btn">Filtrer</button>
|
||||||
<a href="search.php" class="search-reset-link">Réinitialiser</a>
|
<a href="repertoire.php" class="search-reset-link">Réinitialiser</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<main class="search-main" id="main-content">
|
<main class="search-main" id="main-content">
|
||||||
@@ -86,77 +86,10 @@ extract($ctrl->handle());
|
|||||||
<!-- ── RÉPERTOIRE INDEX VIEW ─────────────────────────── -->
|
<!-- ── RÉPERTOIRE INDEX VIEW ─────────────────────────── -->
|
||||||
<main class="search-main" id="main-content">
|
<main class="search-main" id="main-content">
|
||||||
<h1 class="sr-only">Répertoire</h1>
|
<h1 class="sr-only">Répertoire</h1>
|
||||||
<div class="repertoire-index">
|
<span id="rep-indicator" class="rep-indicator htmx-indicator" aria-hidden="true"></span>
|
||||||
|
<?php include APP_ROOT . '/templates/partials/repertoire-index.php'; ?>
|
||||||
<!-- ANNÉES -->
|
|
||||||
<section class="repertoire-col">
|
|
||||||
<h2>Années</h2>
|
|
||||||
<ul>
|
|
||||||
<?php foreach ($years as $y): ?>
|
|
||||||
<li><a href="search.php?year=<?= (int)$y ?>"
|
|
||||||
<?= (isset($_GET['year']) && $_GET['year'] == $y) ? 'aria-current="page"' : '' ?>>
|
|
||||||
<?= (int)$y ?>
|
|
||||||
</a></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- CATÉGORIES -->
|
|
||||||
<section class="repertoire-col">
|
|
||||||
<h2>Catégories</h2>
|
|
||||||
|
|
||||||
<?php if (!empty($orientations)): ?>
|
|
||||||
<span class="cat-index-label">Orientation</span>
|
|
||||||
<ul>
|
|
||||||
<?php foreach ($orientations as $o): ?>
|
|
||||||
<li><a href="search.php?orientation=<?= urlencode($o['name']) ?>"
|
|
||||||
<?= (isset($_GET['orientation']) && $_GET['orientation'] == $o['name']) ? 'aria-current="page"' : '' ?>>
|
|
||||||
<?= htmlspecialchars($o['name']) ?>
|
|
||||||
</a></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (!empty($apPrograms)): ?>
|
|
||||||
<span class="cat-index-label">Ateliers Pluridisciplinaires</span>
|
|
||||||
<ul>
|
|
||||||
<?php foreach ($apPrograms as $ap): ?>
|
|
||||||
<li><a href="search.php?ap_program=<?= urlencode($ap['name']) ?>"
|
|
||||||
<?= (isset($_GET['ap_program']) && $_GET['ap_program'] == $ap['name']) ? 'aria-current="page"' : '' ?>>
|
|
||||||
<?= htmlspecialchars($ap['name']) ?>
|
|
||||||
</a></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
<?php endif; ?>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ÉTUDIANTES -->
|
|
||||||
<section class="repertoire-col">
|
|
||||||
<h2>Étudiantes</h2>
|
|
||||||
<ul>
|
|
||||||
<?php foreach ($authorMap as $name => $id): ?>
|
|
||||||
<li><a href="tfe.php?id=<?= (int)$id ?>">
|
|
||||||
<?= htmlspecialchars($name) ?>
|
|
||||||
</a></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- MOTS-CLÉS -->
|
|
||||||
<section class="repertoire-col">
|
|
||||||
<h2>Mots-clés</h2>
|
|
||||||
<ul>
|
|
||||||
<?php foreach ($keywords as $kw): ?>
|
|
||||||
<li><a href="search.php?keyword=<?= urlencode($kw['name']) ?>"
|
|
||||||
<?= (isset($_GET['keyword']) && $_GET['keyword'] == $kw['name']) ? 'aria-current="page"' : '' ?>>
|
|
||||||
<?= htmlspecialchars($kw['name']) ?>
|
|
||||||
</a></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
|
<script src="/assets/js/htmx.min.js"></script>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php include APP_ROOT . '/templates/footer.php'; ?>
|
<?php include APP_ROOT . '/templates/footer.php'; ?>
|
||||||
137
src/Database.php
137
src/Database.php
@@ -493,6 +493,143 @@ class Database {
|
|||||||
return $stmt->fetchAll();
|
return $stmt->fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute répertoire filter data.
|
||||||
|
*
|
||||||
|
* Given a set of active filters (each an array of values, combined as AND
|
||||||
|
* across filter types, OR within each filter type), returns:
|
||||||
|
* - matched_ids : int[] thesis IDs matching ALL active filters
|
||||||
|
* - years : array all years with matched flag
|
||||||
|
* - ap_programs : array all AP programs with matched flag
|
||||||
|
* - orientations : array all orientations with matched flag
|
||||||
|
* - finality_types: array all finality types with matched flag
|
||||||
|
* - keywords : array all used keywords with matched flag
|
||||||
|
* - students : array [id, authors] rows for matched theses only
|
||||||
|
*
|
||||||
|
* For each column, "matched" means the value appears in at least one thesis
|
||||||
|
* that satisfies all the OTHER active filters (excluding that column's own
|
||||||
|
* filter when computing its own relevance).
|
||||||
|
*
|
||||||
|
* @param array{years:int[], ap:string[], or:string[], fi:string[], kw:string[]} $filters
|
||||||
|
*/
|
||||||
|
public function getRepertoireFilterData(array $filters): array {
|
||||||
|
$baseJoins = "
|
||||||
|
FROM theses t
|
||||||
|
LEFT JOIN orientations o ON t.orientation_id = o.id
|
||||||
|
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
|
||||||
|
LEFT JOIN finality_types ft ON t.finality_id = ft.id
|
||||||
|
";
|
||||||
|
|
||||||
|
// Build WHERE + bindings excluding one dimension (for that column's own relevance)
|
||||||
|
$buildWhere = function(string $exclude) use ($filters): array {
|
||||||
|
$conditions = ['t.is_published = 1'];
|
||||||
|
$bindings = [];
|
||||||
|
|
||||||
|
if ($exclude !== 'years' && !empty($filters['years'])) {
|
||||||
|
$ph = implode(',', array_fill(0, count($filters['years']), '?'));
|
||||||
|
$conditions[] = "t.year IN ($ph)";
|
||||||
|
foreach ($filters['years'] as $v) $bindings[] = (int)$v;
|
||||||
|
}
|
||||||
|
if ($exclude !== 'ap' && !empty($filters['ap'])) {
|
||||||
|
$ph = implode(',', array_fill(0, count($filters['ap']), '?'));
|
||||||
|
$conditions[] = "ap.name IN ($ph)";
|
||||||
|
foreach ($filters['ap'] as $v) $bindings[] = (string)$v;
|
||||||
|
}
|
||||||
|
if ($exclude !== 'or' && !empty($filters['or'])) {
|
||||||
|
$ph = implode(',', array_fill(0, count($filters['or']), '?'));
|
||||||
|
$conditions[] = "o.name IN ($ph)";
|
||||||
|
foreach ($filters['or'] as $v) $bindings[] = (string)$v;
|
||||||
|
}
|
||||||
|
if ($exclude !== 'fi' && !empty($filters['fi'])) {
|
||||||
|
$ph = implode(',', array_fill(0, count($filters['fi']), '?'));
|
||||||
|
$conditions[] = "ft.name IN ($ph)";
|
||||||
|
foreach ($filters['fi'] as $v) $bindings[] = (string)$v;
|
||||||
|
}
|
||||||
|
if ($exclude !== 'kw' && !empty($filters['kw'])) {
|
||||||
|
foreach ($filters['kw'] as $kv) {
|
||||||
|
$conditions[] = 'EXISTS (SELECT 1 FROM thesis_tags tt2 JOIN tags tg2 ON tg2.id=tt2.tag_id WHERE tt2.thesis_id=t.id AND tg2.name=?)';
|
||||||
|
$bindings[] = (string)$kv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [implode(' AND ', $conditions), $bindings];
|
||||||
|
};
|
||||||
|
|
||||||
|
$exec = function(string $sql, array $b): array {
|
||||||
|
$stmt = $this->pdo->prepare($sql);
|
||||||
|
$stmt->execute($b);
|
||||||
|
return $stmt->fetchAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Full intersection — matched thesis IDs
|
||||||
|
[$wAll, $bAll] = $buildWhere('__none__');
|
||||||
|
$matchedIds = array_column($exec("SELECT t.id $baseJoins WHERE $wAll", $bAll), 'id');
|
||||||
|
|
||||||
|
// Years
|
||||||
|
[$w, $b] = $buildWhere('years');
|
||||||
|
$matchedYears = array_column($exec("SELECT DISTINCT t.year $baseJoins WHERE $w ORDER BY t.year DESC", $b), 'year');
|
||||||
|
$allYears = array_column($exec("SELECT DISTINCT year FROM theses WHERE is_published=1 ORDER BY year DESC", []), 'year');
|
||||||
|
$yearsOut = array_map(fn($y) => ['value' => $y, 'matched' => in_array($y, $matchedYears, true)], $allYears);
|
||||||
|
|
||||||
|
// AP programs
|
||||||
|
[$w, $b] = $buildWhere('ap');
|
||||||
|
$matchedAp = array_column($exec("SELECT DISTINCT ap.name $baseJoins WHERE $w AND ap.name IS NOT NULL ORDER BY ap.name", $b), 'name');
|
||||||
|
$allAp = array_column($exec("SELECT name FROM ap_programs ORDER BY name", []), 'name');
|
||||||
|
$apOut = array_map(fn($n) => ['value' => $n, 'matched' => in_array($n, $matchedAp, true)], $allAp);
|
||||||
|
|
||||||
|
// Orientations
|
||||||
|
[$w, $b] = $buildWhere('or');
|
||||||
|
$matchedOr = array_column($exec("SELECT DISTINCT o.name $baseJoins WHERE $w AND o.name IS NOT NULL ORDER BY o.name", $b), 'name');
|
||||||
|
$allOr = array_column($exec("SELECT name FROM orientations ORDER BY name", []), 'name');
|
||||||
|
$orOut = array_map(fn($n) => ['value' => $n, 'matched' => in_array($n, $matchedOr, true)], $allOr);
|
||||||
|
|
||||||
|
// Finality types
|
||||||
|
[$w, $b] = $buildWhere('fi');
|
||||||
|
$matchedFi = array_column($exec("SELECT DISTINCT ft.name $baseJoins WHERE $w AND ft.name IS NOT NULL ORDER BY ft.name", $b), 'name');
|
||||||
|
$allFi = array_column($exec("SELECT name FROM finality_types ORDER BY name", []), 'name');
|
||||||
|
$fiOut = array_map(fn($n) => ['value' => $n, 'matched' => in_array($n, $matchedFi, true)], $allFi);
|
||||||
|
|
||||||
|
// Keywords
|
||||||
|
[$w, $b] = $buildWhere('kw');
|
||||||
|
$matchedKw = array_column($exec(
|
||||||
|
"SELECT DISTINCT tg.name $baseJoins
|
||||||
|
JOIN thesis_tags tt ON tt.thesis_id = t.id
|
||||||
|
JOIN tags tg ON tg.id = tt.tag_id
|
||||||
|
WHERE $w ORDER BY tg.name", $b), 'name');
|
||||||
|
$allKw = array_column($exec(
|
||||||
|
"SELECT DISTINCT tg.name FROM tags tg
|
||||||
|
JOIN thesis_tags tt ON tg.id = tt.tag_id
|
||||||
|
JOIN theses th ON tt.thesis_id = th.id
|
||||||
|
WHERE th.is_published = 1 ORDER BY tg.name", []), 'name');
|
||||||
|
$kwOut = array_map(fn($n) => ['value' => $n, 'matched' => in_array($n, $matchedKw, true)], $allKw);
|
||||||
|
|
||||||
|
// Students (output only — full intersection)
|
||||||
|
$studentsOut = [];
|
||||||
|
if (!empty($matchedIds)) {
|
||||||
|
$ph = implode(',', array_fill(0, count($matchedIds), '?'));
|
||||||
|
$studentsOut = $exec(
|
||||||
|
"SELECT t.id,
|
||||||
|
GROUP_CONCAT(a.name ORDER BY ta.author_order ASC) AS authors
|
||||||
|
FROM theses t
|
||||||
|
JOIN thesis_authors ta ON ta.thesis_id = t.id
|
||||||
|
JOIN authors a ON a.id = ta.author_id
|
||||||
|
WHERE t.id IN ($ph)
|
||||||
|
GROUP BY t.id
|
||||||
|
ORDER BY MIN(a.name) ASC",
|
||||||
|
$matchedIds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'matched_ids' => $matchedIds,
|
||||||
|
'years' => $yearsOut,
|
||||||
|
'ap_programs' => $apOut,
|
||||||
|
'orientations' => $orOut,
|
||||||
|
'finality_types' => $fiOut,
|
||||||
|
'keywords' => $kwOut,
|
||||||
|
'students' => $studentsOut,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all format types
|
* Get all format types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SearchController
|
* SearchController
|
||||||
*
|
*
|
||||||
* Handles all data-fetching logic for the public search / répertoire page.
|
* 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.
|
* a plain array of view variables ready for template inclusion.
|
||||||
*
|
*
|
||||||
* Responsibilities:
|
* Responsibilities:
|
||||||
@@ -11,9 +11,11 @@
|
|||||||
* - GET parameter sanitisation and validation
|
* - GET parameter sanitisation and validation
|
||||||
* - Database queries (search + index listings)
|
* - Database queries (search + index listings)
|
||||||
* - OG / meta tag assembly
|
* - OG / meta tag assembly
|
||||||
|
* - HTMX partial response for repertoire filter swaps
|
||||||
*
|
*
|
||||||
* The class has NO output side-effects; all template rendering stays in
|
* 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
|
class SearchController
|
||||||
{
|
{
|
||||||
@@ -66,9 +68,12 @@ class SearchController
|
|||||||
*/
|
*/
|
||||||
public function handle(): array
|
public function handle(): array
|
||||||
{
|
{
|
||||||
|
$isHtmx = !empty($_SERVER['HTTP_HX_REQUEST']);
|
||||||
$searchParams = $this->collectSearchParams();
|
$searchParams = $this->collectSearchParams();
|
||||||
$hasSearch = !empty($searchParams);
|
$hasSearch = !empty($searchParams);
|
||||||
|
|
||||||
|
$activeFilters = $this->collectFilterParams();
|
||||||
|
|
||||||
$page = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1;
|
$page = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1;
|
||||||
$offset = ($page - 1) * self::ITEMS_PER_PAGE;
|
$offset = ($page - 1) * self::ITEMS_PER_PAGE;
|
||||||
$validationError = null;
|
$validationError = null;
|
||||||
@@ -76,26 +81,25 @@ class SearchController
|
|||||||
$results = [];
|
$results = [];
|
||||||
$totalItems = 0;
|
$totalItems = 0;
|
||||||
$totalPages = 0;
|
$totalPages = 0;
|
||||||
|
$repData = null;
|
||||||
|
|
||||||
|
// For search filter dropdowns (text search mode only)
|
||||||
$years = [];
|
$years = [];
|
||||||
$orientations = [];
|
$orientations = [];
|
||||||
$apPrograms = [];
|
$apPrograms = [];
|
||||||
$keywords = [];
|
|
||||||
$students = [];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($hasSearch) {
|
if ($hasSearch) {
|
||||||
$results = $this->db->searchTheses($searchParams, self::ITEMS_PER_PAGE, $offset);
|
$results = $this->db->searchTheses($searchParams, self::ITEMS_PER_PAGE, $offset);
|
||||||
$totalItems = $this->db->countSearchResults($searchParams);
|
$totalItems = $this->db->countSearchResults($searchParams);
|
||||||
$totalPages = (int) ceil($totalItems / self::ITEMS_PER_PAGE);
|
$totalPages = (int) ceil($totalItems / self::ITEMS_PER_PAGE);
|
||||||
}
|
|
||||||
|
|
||||||
$years = $this->db->getAvailableYears();
|
$years = $this->db->getAvailableYears();
|
||||||
$orientations = $this->db->getAllOrientations();
|
$orientations = $this->db->getAllOrientations();
|
||||||
$apPrograms = $this->db->getAllAPPrograms();
|
$apPrograms = $this->db->getAllAPPrograms();
|
||||||
$keywords = $this->db->getUsedTags();
|
} else {
|
||||||
// Fetch id+authors only — lean query bypassing the fat v_theses_public view
|
// Repertoire index: compute filter data (all columns + matched flags)
|
||||||
$students = $this->db->getPublishedAuthors();
|
$repData = $this->db->getRepertoireFilterData($activeFilters);
|
||||||
|
}
|
||||||
} catch (InvalidArgumentException $e) {
|
} catch (InvalidArgumentException $e) {
|
||||||
$validationError = $e->getMessage();
|
$validationError = $e->getMessage();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
@@ -103,8 +107,10 @@ class SearchController
|
|||||||
$validationError = 'Une erreur est survenue.';
|
$validationError = 'Une erreur est survenue.';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the author index map (répertoire index view)
|
// HTMX partial: render just the index div and exit
|
||||||
$authorMap = $this->buildAuthorMap($students);
|
if ($isHtmx && !$hasSearch && $repData !== null) {
|
||||||
|
$this->renderRepertoirePartial($repData, $activeFilters);
|
||||||
|
}
|
||||||
|
|
||||||
// Preserve all active search/filter params, strip 'page' (pagination partial adds it)
|
// Preserve all active search/filter params, strip 'page' (pagination partial adds it)
|
||||||
$baseParams = array_diff_key($_GET, ['page' => '']);
|
$baseParams = array_diff_key($_GET, ['page' => '']);
|
||||||
@@ -120,22 +126,25 @@ class SearchController
|
|||||||
'validationError' => $validationError,
|
'validationError' => $validationError,
|
||||||
'baseParams' => $baseParams,
|
'baseParams' => $baseParams,
|
||||||
|
|
||||||
// Filter / index data
|
// Repertoire filter state
|
||||||
|
'repData' => $repData,
|
||||||
|
'activeFilters' => $activeFilters,
|
||||||
|
'isHtmx' => $isHtmx,
|
||||||
|
|
||||||
|
// Search filter dropdowns (text search mode only)
|
||||||
'years' => $years,
|
'years' => $years,
|
||||||
'orientations' => $orientations,
|
'orientations' => $orientations,
|
||||||
'apPrograms' => $apPrograms,
|
'apPrograms' => $apPrograms,
|
||||||
'keywords' => $keywords,
|
|
||||||
'authorMap' => $authorMap,
|
|
||||||
|
|
||||||
// Page meta
|
// Page meta
|
||||||
'searchBarValue' => $_GET['query'] ?? '',
|
'searchBarValue' => $_GET['query'] ?? '',
|
||||||
'pageTitle' => 'Répertoire – Posterg',
|
'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' => [
|
'ogTags' => [
|
||||||
'type' => 'website',
|
'type' => 'website',
|
||||||
'title' => 'Répertoire – Posterg',
|
'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.',
|
'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/search.php',
|
'url' => 'https://posterg.erg.be/repertoire.php',
|
||||||
'site_name' => 'Posterg – ERG',
|
'site_name' => 'Posterg – ERG',
|
||||||
],
|
],
|
||||||
'currentNav' => 'repertoire',
|
'currentNav' => 'repertoire',
|
||||||
@@ -147,7 +156,58 @@ class SearchController
|
|||||||
// ── Private helpers ───────────────────────────────────────────────────────
|
// ── 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>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
@@ -174,34 +234,6 @@ class SearchController
|
|||||||
return $params;
|
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 ───────────────────────────────────────────────────
|
// ── Rate-limit response ───────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ try {
|
|||||||
|
|
||||||
echo "\n✅ Test database created successfully!\n";
|
echo "\n✅ Test database created successfully!\n";
|
||||||
echo "Database location: $dbPath\n";
|
echo "Database location: $dbPath\n";
|
||||||
echo "\nYou can now test the search feature at: http://localhost/front-backend/search.php\n";
|
echo "\nYou can now test the search feature at: http://localhost/front-backend/repertoire.php\n";
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
echo "❌ Error: " . $e->getMessage() . "\n";
|
echo "❌ Error: " . $e->getMessage() . "\n";
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ $_thesisId = $_GET['id'] ?? null;
|
|||||||
<a href="/index.php">Xamxam</a>
|
<a href="/index.php">Xamxam</a>
|
||||||
<ul class="nav-left-links">
|
<ul class="nav-left-links">
|
||||||
<li>
|
<li>
|
||||||
<a href="/search.php"
|
<a href="/repertoire.php"
|
||||||
<?= ($_navCurrent === 'repertoire') ? 'aria-current="page"' : '' ?>>Répertoire</a>
|
<?= ($_navCurrent === 'repertoire') ? 'aria-current="page"' : '' ?>>Répertoire</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -61,7 +61,7 @@ $_thesisId = $_GET['id'] ?? null;
|
|||||||
$searchBarValue = $searchBarValue ?? $_GET['query'] ?? '';
|
$searchBarValue = $searchBarValue ?? $_GET['query'] ?? '';
|
||||||
?>
|
?>
|
||||||
<div class="header-search-wrap">
|
<div class="header-search-wrap">
|
||||||
<form method="GET" action="/search.php"
|
<form method="GET" action="/repertoire.php"
|
||||||
role="search" aria-label="Recherche">
|
role="search" aria-label="Recherche">
|
||||||
<label for="site-search-input" class="sr-only">Recherche</label>
|
<label for="site-search-input" class="sr-only">Recherche</label>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||||
|
|||||||
199
templates/partials/repertoire-index.php
Normal file
199
templates/partials/repertoire-index.php
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Partial: répertoire index columns.
|
||||||
|
* Rendered both on full page load and as HTMX partial swap.
|
||||||
|
*
|
||||||
|
* Expected variables:
|
||||||
|
* $repData array output of Database::getRepertoireFilterData()
|
||||||
|
* $activeFilters array{years:int[], ap:string[], or:string[], fi:string[], kw:string[]}
|
||||||
|
*/
|
||||||
|
|
||||||
|
$activeSets = [
|
||||||
|
'years' => array_map('strval', $activeFilters['years'] ?? []),
|
||||||
|
'ap' => $activeFilters['ap'] ?? [],
|
||||||
|
'or' => $activeFilters['or'] ?? [],
|
||||||
|
'fi' => $activeFilters['fi'] ?? [],
|
||||||
|
'kw' => $activeFilters['kw'] ?? [],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Build the student map from matched students only
|
||||||
|
$studentMap = []; // name => id
|
||||||
|
foreach ($repData['students'] as $s) {
|
||||||
|
if (empty($s['authors'])) continue;
|
||||||
|
foreach (explode(',', $s['authors']) as $name) {
|
||||||
|
$name = trim($name);
|
||||||
|
if ($name !== '' && !isset($studentMap[$name])) {
|
||||||
|
$studentMap[$name] = (int)$s['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ksort($studentMap);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the toggle URL for a filter button.
|
||||||
|
* Toggles $value in $dim; keeps all other active filters intact.
|
||||||
|
*/
|
||||||
|
function repToggleUrl(array $sets, string $dim, string $value): string {
|
||||||
|
if (in_array($value, $sets[$dim], true)) {
|
||||||
|
$sets[$dim] = array_values(array_filter($sets[$dim], fn($v) => $v !== $value));
|
||||||
|
} else {
|
||||||
|
$sets[$dim][] = $value;
|
||||||
|
}
|
||||||
|
$params = [];
|
||||||
|
foreach ($sets['years'] as $v) $params[] = 'fy[]=' . urlencode((string)$v);
|
||||||
|
foreach ($sets['ap'] as $v) $params[] = 'ap[]=' . urlencode($v);
|
||||||
|
foreach ($sets['or'] as $v) $params[] = 'or[]=' . urlencode($v);
|
||||||
|
foreach ($sets['fi'] as $v) $params[] = 'fi[]=' . urlencode($v);
|
||||||
|
foreach ($sets['kw'] as $v) $params[] = 'kw[]=' . urlencode($v);
|
||||||
|
$qs = implode('&', $params);
|
||||||
|
return '/repertoire.php' . ($qs ? '?' . $qs : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$anyActive = !empty($activeSets['years']) || !empty($activeSets['ap'])
|
||||||
|
|| !empty($activeSets['or']) || !empty($activeSets['fi'])
|
||||||
|
|| !empty($activeSets['kw']);
|
||||||
|
|
||||||
|
// Common HTMX attributes for all active filter buttons
|
||||||
|
$hx = 'hx-target="#repertoire-index" hx-swap="outerHTML" hx-push-url="true" hx-indicator="#rep-indicator"';
|
||||||
|
?>
|
||||||
|
<div id="repertoire-index" class="repertoire-index">
|
||||||
|
|
||||||
|
<!-- ANNÉES -->
|
||||||
|
<section class="repertoire-col" data-col="years">
|
||||||
|
<h2>Années</h2>
|
||||||
|
<ul>
|
||||||
|
<?php foreach ($repData['years'] as $item):
|
||||||
|
$val = (string)$item['value'];
|
||||||
|
$isActive = in_array($val, $activeSets['years'], true);
|
||||||
|
$isFaded = $anyActive && !$item['matched'] && !$isActive;
|
||||||
|
$cls = 'rep-entry'
|
||||||
|
. ($isActive ? ' rep-entry--selected' : '')
|
||||||
|
. ($isFaded ? ' rep-entry--faded' : '');
|
||||||
|
$url = repToggleUrl($activeSets, 'years', $val);
|
||||||
|
?>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="<?= $cls ?>"
|
||||||
|
aria-pressed="<?= $isActive ? 'true' : 'false' ?>"
|
||||||
|
<?= $isFaded ? 'disabled' : "hx-get=\"" . htmlspecialchars($url) . "\" $hx" ?>>
|
||||||
|
<?= htmlspecialchars($val) ?>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ATELIERS PLURIDISCIPLINAIRES -->
|
||||||
|
<section class="repertoire-col" data-col="ap">
|
||||||
|
<h2>Ateliers Pluridisciplinaires</h2>
|
||||||
|
<ul>
|
||||||
|
<?php foreach ($repData['ap_programs'] as $item):
|
||||||
|
$val = $item['value'];
|
||||||
|
$isActive = in_array($val, $activeSets['ap'], true);
|
||||||
|
$isFaded = $anyActive && !$item['matched'] && !$isActive;
|
||||||
|
$cls = 'rep-entry'
|
||||||
|
. ($isActive ? ' rep-entry--selected' : '')
|
||||||
|
. ($isFaded ? ' rep-entry--faded' : '');
|
||||||
|
$url = repToggleUrl($activeSets, 'ap', $val);
|
||||||
|
?>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="<?= $cls ?>"
|
||||||
|
aria-pressed="<?= $isActive ? 'true' : 'false' ?>"
|
||||||
|
<?= $isFaded ? 'disabled' : "hx-get=\"" . htmlspecialchars($url) . "\" $hx" ?>>
|
||||||
|
<?= htmlspecialchars($val) ?>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ORIENTATIONS -->
|
||||||
|
<section class="repertoire-col" data-col="or">
|
||||||
|
<h2>Orientations</h2>
|
||||||
|
<ul>
|
||||||
|
<?php foreach ($repData['orientations'] as $item):
|
||||||
|
$val = $item['value'];
|
||||||
|
$isActive = in_array($val, $activeSets['or'], true);
|
||||||
|
$isFaded = $anyActive && !$item['matched'] && !$isActive;
|
||||||
|
$cls = 'rep-entry'
|
||||||
|
. ($isActive ? ' rep-entry--selected' : '')
|
||||||
|
. ($isFaded ? ' rep-entry--faded' : '');
|
||||||
|
$url = repToggleUrl($activeSets, 'or', $val);
|
||||||
|
?>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="<?= $cls ?>"
|
||||||
|
aria-pressed="<?= $isActive ? 'true' : 'false' ?>"
|
||||||
|
<?= $isFaded ? 'disabled' : "hx-get=\"" . htmlspecialchars($url) . "\" $hx" ?>>
|
||||||
|
<?= htmlspecialchars($val) ?>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- FINALITÉ DU MASTER -->
|
||||||
|
<section class="repertoire-col" data-col="fi">
|
||||||
|
<h2>Finalité du Master</h2>
|
||||||
|
<ul>
|
||||||
|
<?php foreach ($repData['finality_types'] as $item):
|
||||||
|
$val = $item['value'];
|
||||||
|
$isActive = in_array($val, $activeSets['fi'], true);
|
||||||
|
$isFaded = $anyActive && !$item['matched'] && !$isActive;
|
||||||
|
$cls = 'rep-entry'
|
||||||
|
. ($isActive ? ' rep-entry--selected' : '')
|
||||||
|
. ($isFaded ? ' rep-entry--faded' : '');
|
||||||
|
$url = repToggleUrl($activeSets, 'fi', $val);
|
||||||
|
?>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="<?= $cls ?>"
|
||||||
|
aria-pressed="<?= $isActive ? 'true' : 'false' ?>"
|
||||||
|
<?= $isFaded ? 'disabled' : "hx-get=\"" . htmlspecialchars($url) . "\" $hx" ?>>
|
||||||
|
<?= htmlspecialchars($val) ?>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ÉTUDIANTES -->
|
||||||
|
<section class="repertoire-col" data-col="students">
|
||||||
|
<h2>Étudiantes</h2>
|
||||||
|
<ul>
|
||||||
|
<?php if (empty($studentMap)): ?>
|
||||||
|
<li class="rep-empty">—</li>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($studentMap as $name => $id): ?>
|
||||||
|
<li>
|
||||||
|
<a href="tfe.php?id=<?= (int)$id ?>" class="rep-entry rep-entry--link">
|
||||||
|
<?= htmlspecialchars($name) ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- MOTS-CLÉS -->
|
||||||
|
<section class="repertoire-col" data-col="kw">
|
||||||
|
<h2>Mots-clés</h2>
|
||||||
|
<ul>
|
||||||
|
<?php foreach ($repData['keywords'] as $item):
|
||||||
|
$val = $item['value'];
|
||||||
|
$isActive = in_array($val, $activeSets['kw'], true);
|
||||||
|
$isFaded = $anyActive && !$item['matched'] && !$isActive;
|
||||||
|
$cls = 'rep-entry'
|
||||||
|
. ($isActive ? ' rep-entry--selected' : '')
|
||||||
|
. ($isFaded ? ' rep-entry--faded' : '');
|
||||||
|
$url = repToggleUrl($activeSets, 'kw', $val);
|
||||||
|
?>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="<?= $cls ?>"
|
||||||
|
aria-pressed="<?= $isActive ? 'true' : 'false' ?>"
|
||||||
|
<?= $isFaded ? 'disabled' : "hx-get=\"" . htmlspecialchars($url) . "\" $hx" ?>>
|
||||||
|
<?= htmlspecialchars($val) ?>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
// $searchValue: current search query (optional)
|
// $searchValue: current search query (optional)
|
||||||
$_sbValue = $searchBarValue ?? $_GET['query'] ?? '';
|
$_sbValue = $searchBarValue ?? $_GET['query'] ?? '';
|
||||||
?>
|
?>
|
||||||
<form method="GET" action="/search.php"
|
<form method="GET" action="/repertoire.php"
|
||||||
role="search" aria-label="Recherche">
|
role="search" aria-label="Recherche">
|
||||||
<label for="site-search-input" class="sr-only">Recherche</label>
|
<label for="site-search-input" class="sr-only">Recherche</label>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||||
|
|||||||
Reference in New Issue
Block a user