mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
When filters are active, viable (matched) keywords now appear at the top of the Mots-clés column so users don't have to scroll past faded entries to find clickable tags. Both groups maintain alphabetical order internally.
191 lines
7.6 KiB
PHP
191 lines
7.6 KiB
PHP
<?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'] ?? [],
|
|
];
|
|
|
|
// ── Students ────────────────────────────────────────────────────────────────
|
|
$studentWorks = [];
|
|
foreach ($repData['students'] as $s) {
|
|
if (empty($s['authors'])) continue;
|
|
foreach (explode(',', $s['authors']) as $name) {
|
|
$name = trim($name);
|
|
if ($name === '') continue;
|
|
$studentWorks[$name][] = (int)$s['id'];
|
|
}
|
|
}
|
|
ksort($studentWorks);
|
|
|
|
// ── Shared helpers ──────────────────────────────────────────────────────────
|
|
|
|
// AP abbreviation mapping (cf. maquette: diminutifs entre crochets)
|
|
const AP_ABBREVIATIONS = [
|
|
'Atelier Pratiques Situées' => '[APS]',
|
|
'Design et Politique du Multiple' => '[DPM]',
|
|
'Lieux, Interdisciplinarités, Écologie, Nécessité, Systèmes' => '[L.I.E.N.S.]',
|
|
];
|
|
|
|
function formatApDisplay(string $name): string {
|
|
$abbr = AP_ABBREVIATIONS[$name] ?? '';
|
|
return $abbr !== '' ? "$name $abbr" : $name;
|
|
}
|
|
|
|
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' . ($qs ? '?' . $qs : '');
|
|
}
|
|
|
|
function repFilterEntry(
|
|
array $item,
|
|
string $dim,
|
|
array $activeSets,
|
|
bool $anyActive,
|
|
bool $colHasMatch,
|
|
string $hx,
|
|
): void {
|
|
$val = (string)$item['value'];
|
|
$isActive = in_array($val, $activeSets[$dim], true);
|
|
$isFaded = $anyActive && $colHasMatch && !$item['matched'] && !$isActive;
|
|
$cls = 'rep-entry'
|
|
. ($isActive ? ' rep-entry--selected' : '')
|
|
. ($isFaded ? ' rep-entry--faded' : '');
|
|
$url = repToggleUrl($activeSets, $dim, $val);
|
|
?>
|
|
<li>
|
|
<button type="button" class="<?= $cls ?>"
|
|
aria-pressed="<?= $isActive ? 'true' : 'false' ?>"
|
|
<?= $isFaded ? 'disabled' : "hx-get=\"" . htmlspecialchars($url) . "\" $hx" ?>>
|
|
<?= htmlspecialchars($dim === 'ap' ? formatApDisplay($val) : $val) ?>
|
|
</button>
|
|
</li>
|
|
<?php
|
|
}
|
|
|
|
// ── Column definitions ──────────────────────────────────────────────────────
|
|
$hx = 'hx-target="#repertoire-index" hx-swap="outerHTML" hx-push-url="true" hx-indicator="#rep-indicator"';
|
|
|
|
$anyActive = !empty($activeSets['years']) || !empty($activeSets['ap'])
|
|
|| !empty($activeSets['or']) || !empty($activeSets['fi'])
|
|
|| !empty($activeSets['kw']);
|
|
|
|
$filterColumns = [
|
|
['dataKey' => 'years', 'dim' => 'years', 'heading' => 'Années'],
|
|
['dataKey' => 'ap_programs', 'dim' => 'ap', 'heading' => 'Ateliers Pluridisciplinaires'],
|
|
['dataKey' => 'orientations', 'dim' => 'or', 'heading' => 'Orientations'],
|
|
['dataKey' => 'finality_types', 'dim' => 'fi', 'heading' => 'Finalité du Master'],
|
|
['dataKey' => 'keywords', 'dim' => 'kw', 'heading' => 'Mots-clés'],
|
|
];
|
|
|
|
$colHasMatches = [];
|
|
foreach ($filterColumns as $col) {
|
|
$colHasMatches[$col['dim']] = !empty(array_filter(
|
|
$repData[$col['dataKey']],
|
|
fn($i) => $i['matched']
|
|
));
|
|
}
|
|
|
|
?>
|
|
<div id="repertoire-index" class="repertoire-index">
|
|
|
|
<?php
|
|
// Sort keywords: matched first (alpha), then unmatched (alpha) — keeps viable
|
|
// tags at the top so users don't have to scroll past faded entries.
|
|
usort($repData['keywords'], fn($a, $b) =>
|
|
($b['matched'] <=> $a['matched']) ?: strcasecmp($a['value'], $b['value'])
|
|
);
|
|
|
|
// Render filter columns in the correct left-to-right order.
|
|
// Students column (non-filter) is inserted between keywords and AP/or/fi/years.
|
|
$renderOrder = ['years', 'ap', 'or', 'fi', 'students', 'kw'];
|
|
|
|
foreach ($renderOrder as $colKey):
|
|
if ($colKey === 'students'): ?>
|
|
<!-- ÉTUDIANTES -->
|
|
<section class="repertoire-col rep-accordion" data-col="students">
|
|
<h2>
|
|
<span class="rep-accordion__heading-text">Étudiant·es</span>
|
|
<button type="button" class="rep-accordion__toggle" aria-expanded="false">
|
|
Étudiant·es
|
|
<span class="rep-accordion__chevron" aria-hidden="true"></span>
|
|
</button>
|
|
</h2>
|
|
<div class="rep-accordion__panel">
|
|
<ul>
|
|
<?php if (empty($studentWorks)): ?>
|
|
<li class="rep-empty">—</li>
|
|
<?php else: ?>
|
|
<?php foreach ($studentWorks as $name => $ids): ?>
|
|
<?php
|
|
$firstId = $ids[0];
|
|
$targetUrl = count($ids) === 1 ? '/tfe?id=' . $firstId : '#';
|
|
$previewUrl = '/repertoire/student-preview?name=' . urlencode($name);
|
|
?>
|
|
<li class="student-entry">
|
|
<a href="<?= htmlspecialchars($targetUrl) ?>"
|
|
class="rep-entry rep-entry--link"
|
|
data-student-name="<?= htmlspecialchars($name) ?>"
|
|
hx-get="<?= htmlspecialchars($previewUrl) ?>"
|
|
hx-target="#student-popover"
|
|
hx-trigger="mouseenter"
|
|
hx-swap="innerHTML">
|
|
<?= htmlspecialchars($name) ?>
|
|
</a>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
<?php else:
|
|
$col = array_values(array_filter($filterColumns, fn($c) => $c['dim'] === $colKey))[0];
|
|
|
|
// Count active filters in this column for the badge
|
|
$activeCount = count($activeSets[$col['dim']]);
|
|
?>
|
|
<section class="repertoire-col rep-accordion" data-col="<?= $col['dim'] ?>">
|
|
<h2>
|
|
<span class="rep-accordion__heading-text"><?= $col['heading'] ?></span>
|
|
<button type="button" class="rep-accordion__toggle" aria-expanded="false">
|
|
<?= $col['heading'] ?>
|
|
<?php if ($activeCount > 0): ?>
|
|
<span class="rep-accordion__badge"><?= $activeCount ?></span>
|
|
<?php endif; ?>
|
|
<span class="rep-accordion__chevron" aria-hidden="true"></span>
|
|
</button>
|
|
</h2>
|
|
<div class="rep-accordion__panel">
|
|
<ul>
|
|
<?php foreach ($repData[$col['dataKey']] as $item):
|
|
repFilterEntry($item, $col['dim'], $activeSets, $anyActive, $colHasMatches[$col['dim']], $hx);
|
|
endforeach; ?>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
<?php endif;
|
|
endforeach; ?>
|
|
|
|
</div>
|