mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
- repertoire-index.php: wrap each filter column in rep-accordion with toggle button, chevron, badge (active filter count); add rep-chip-bar with removable active-filter chips above the columns - repertoire.css: mobile (≤640px) accordion mode — columns collapse to single-open accordion sections with 48px touch targets; chip bar becomes sticky; desktop/tablet layout unchanged via display:none on toggle elements - repertoire.php: JS for single-accordion-open behavior on mobile, HTMX re-init after swap, resize-breakpoint cleanup - docs/repertoire-mobile-propositions.md: analysis + 4 architecture proposals
225 lines
9.0 KiB
PHP
225 lines
9.0 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']
|
||
));
|
||
}
|
||
|
||
// ── Active filter chips ─────────────────────────────────────────────────────
|
||
// Build a flat list of {dim, label, value, url} for every active filter entry.
|
||
$activeChips = [];
|
||
$dimLabels = [
|
||
'years' => 'Année',
|
||
'ap' => 'AP',
|
||
'or' => 'Orientation',
|
||
'fi' => 'Finalité',
|
||
'kw' => 'Mot-clé',
|
||
];
|
||
foreach (['years', 'ap', 'or', 'fi', 'kw'] as $dim) {
|
||
foreach ($activeSets[$dim] as $val) {
|
||
$activeChips[] = [
|
||
'dim' => $dim,
|
||
'label' => $dimLabels[$dim],
|
||
'value' => $val,
|
||
'url' => repToggleUrl($activeSets, $dim, $val),
|
||
];
|
||
}
|
||
}
|
||
?>
|
||
<div id="repertoire-index" class="repertoire-index">
|
||
|
||
<?php if ($anyActive): ?>
|
||
<!-- Active filter chips bar -->
|
||
<div class="rep-chip-bar" role="status" aria-label="Filtres actifs">
|
||
<span class="rep-chip-bar__label">Filtres :</span>
|
||
<?php foreach ($activeChips as $chip): ?>
|
||
<button type="button" class="rep-chip"
|
||
hx-get="<?= htmlspecialchars($chip['url']) ?>"
|
||
hx-target="#repertoire-index"
|
||
hx-swap="outerHTML"
|
||
hx-push-url="true"
|
||
hx-indicator="#rep-indicator"
|
||
aria-label="Retirer le filtre <?= htmlspecialchars($chip['label']) ?> : <?= htmlspecialchars($chip['value']) ?>">
|
||
<span class="rep-chip__dim"><?= htmlspecialchars($chip['label']) ?></span>
|
||
<span class="rep-chip__value"><?= htmlspecialchars($chip['value']) ?></span>
|
||
<span class="rep-chip__remove" aria-hidden="true">×</span>
|
||
</button>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php
|
||
// 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>
|